I2C в STM32 на примере компаса HMC5883L и stm32f4 discovery

Долго, долго я не мог добраться до этого интерфейса. Вот не нравился он мне и все. Встречающиеся на каждом шагу SPI и USART покрывают почти все нужны, а вот датчики экономят провода, вот и понадобился i2c.
Честно сказать, при знакомстве подумалось, что это какой-то 1-wire. Ну очень похож.

Перед собственно программированием, давайте посмотрим, что это вообще такое.
I2C (еще его часто зовут IIC) — один из самых популярных интерфейсов у датчиков. Тут сказывается малое количество проводников! Для передачи данных используются две линии. SDA — линия передачи данных. SCL — линия тактирования. Оба провода должны быть подтянуты к питанию 10К резисторами. На шине может сидеть до 127 устройств. Подключаются они параллельно (если сказать умными словами «по схеме монтажного И»).
Цикл передачи данных включает в себя посылку старт сигнала, данных и стоп сигнала. Тут все очень похоже на USART. Если мастеру нужно послать посылку, он придавливает линию SDA к 0 при активном (единичном) состоянии линии SCL. Это служит для устройств на шине индикатором начала передачи. После этого мастер генерирует тактирование и передает побитно данные. Заканчивается все STOP последовательностью. Когда при высоком уровне на SCL линия SDA переходит с низкого на высокий уровень.
Есть еще куча примочек. Но не будем в них углубляться.

Теперь посмотрим как обобщатся с датчиком HMC5883L. Это цифровой компас.
Нам нужно включить в нем циклический режим измерения и читать данные из трех регистров (для трех осей). Регистры эти разбиты на пары (старшая часть и младшая), поэтому нужно будет еще их «склеивать».
Как водится в мире embedded программирования i2c нужно включить и настроить.

void HMC5883L_I2C_Init(void)
{
    // Включаем тактирование нужных модулей
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
 
    // настройка I2C
    i2c.I2C_ClockSpeed = 100000; 
    i2c.I2C_Mode = I2C_Mode_I2C;
    i2c.I2C_DutyCycle = I2C_DutyCycle_2;
    // адрес отфанарный
    i2c.I2C_OwnAddress1 = 0x15;
    i2c.I2C_Ack = I2C_Ack_Disable;
    i2c.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    I2C_Init(HMC5883L_I2C, &i2c);
 
    // I2C использует две ноги микроконтроллера, их тоже нужно настроить
    gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    gpio.GPIO_Mode = GPIO_Mode_AF;                      
    gpio.GPIO_Speed = GPIO_Speed_50MHz;
    gpio.GPIO_OType = GPIO_OType_OD;
    gpio.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(GPIOB, &gpio);
 
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_I2C1);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_I2C1);
 
    // стартуем модуль I2C
    I2C_Cmd(HMC5883L_I2C, ENABLE);
}

Супер, модуль теперь работает. Дело за малым научится писать и читать.

void HMC5883L_I2C_ByteWrite(u8 slaveAddr, u8* pBuffer, u8 WriteAddr)
{
    while(I2C_GetFlagStatus(HMC5883L_I2C, I2C_FLAG_BUSY));
    /* шлем старт */
    I2C_GenerateSTART(HMC5883L_I2C, ENABLE);
    /* ждем подтверждение */
    while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT));
    /* шлем HMC5883 адрес */
    I2C_Send7bitAddress(HMC5883L_I2C, slaveAddr, I2C_Direction_Transmitter);
    /* ждем подтверждение */
    while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
    /* шлем HMC5883L адрес регистра */
    I2C_SendData(HMC5883L_I2C, WriteAddr);
    /* ждем окончания передачи */
    while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    /* шлем данные */
    I2C_SendData(HMC5883L_I2C, *pBuffer);
    /* ждем */
    while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    /* шлем стоп */
    I2C_GenerateSTOP(HMC5883L_I2C, ENABLE);
}

void HMC5883L_I2C_BufferRead(u8 slaveAddr, u8* pBuffer, u8 ReadAddr, u16 NumByteToRead)
{
    /* ждем пока шина не освободится */
    while (I2C_GetFlagStatus(HMC5883L_I2C, I2C_FLAG_BUSY));
    /* шлем старт */
    I2C_GenerateSTART(HMC5883L_I2C, ENABLE);
    while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT));
    /* шлем адрес */
    I2C_Send7bitAddress(HMC5883L_I2C, slaveAddr, I2C_Direction_Transmitter);
    while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
    /* Для очистки EV6 запускаем модуль снова */
    I2C_Cmd(HMC5883L_I2C, ENABLE);
    /* шлем HMC5883L адрес чтения */
    I2C_SendData(HMC5883L_I2C, ReadAddr);
    while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    /* шлем старт второй раз */
    I2C_GenerateSTART(HMC5883L_I2C, ENABLE);
    while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT));
    /* шлем HMC5883L адрес чтения */
    I2C_Send7bitAddress(HMC5883L_I2C, slaveAddr, I2C_Direction_Receiver);
    while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
    /* Пока есть что читать */
    while (NumByteToRead)
    {
        if (NumByteToRead == 1)
        {
            I2C_AcknowledgeConfig(HMC5883L_I2C, DISABLE);
            I2C_GenerateSTOP(HMC5883L_I2C, ENABLE);
        }
        if (I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_BYTE_RECEIVED))
        {
            *pBuffer = I2C_ReceiveData(HMC5883L_I2C);
            pBuffer++;
            NumByteToRead--;
        }
    }
    I2C_AcknowledgeConfig(HMC5883L_I2C, ENABLE);
}

Учитывая все вышесказанное, настройка датчика будет выглядеть вот так:

void HMC5883L_Init()
{
    HMC5883L_I2C_Init();
    u8 buf = 0;
    // 10k continious mode
    HMC5883L_I2C_ByteWrite(HMC5883L_DEFAULT_ADDRESS, &buf, HMC5883L_MODE_REG);
    HMC5883L_I2C_ByteWrite(HMC5883L_DEFAULT_ADDRESS, &buf, HMC5883L_CONF_REGB);
}

А функции чтения значений из регистров данных вот так:

u16 HMC5883L_ReadX()
{
    u8 msb,lsb;
    HMC5883L_I2C_BufferRead(HMC5883L_DEFAULT_ADDRESS, &msb, HMC5883L_DATA_OUT_X_MSB_REG, 1);
    HMC5883L_I2C_BufferRead(HMC5883L_DEFAULT_ADDRESS, &lsb, HMC5883L_DATA_OUT_X_LSB_REG, 1);
    return (msb << 8) | lsb;
}

u16 HMC5883L_ReadY()
{
    u8 msb,lsb;
    HMC5883L_I2C_BufferRead(HMC5883L_DEFAULT_ADDRESS, &msb, HMC5883L_DATA_OUT_Y_MSB_REG, 1);
    HMC5883L_I2C_BufferRead(HMC5883L_DEFAULT_ADDRESS, &lsb, HMC5883L_DATA_OUT_Y_LSB_REG, 1);
    return (msb << 8) | lsb;
}

u16 HMC5883L_ReadZ()
{
    u8 msb,lsb;
    HMC5883L_I2C_BufferRead(HMC5883L_DEFAULT_ADDRESS, &msb, HMC5883L_DATA_OUT_Z_MSB_REG, 1);
    HMC5883L_I2C_BufferRead(HMC5883L_DEFAULT_ADDRESS, &lsb, HMC5883L_DATA_OUT_Z_LSB_REG, 1);
    return (msb << 8) | lsb;
}

Но это еще не все! Нужно ведь посчитать угол.

dataX = HMC5883L_ReadX();
         dataY = HMC5883L_ReadY();
         dataZ = HMC5883L_ReadZ();
         x = dataX * 0.92;
         y = dataY * 0.92;
         z = dataZ * 0.92;
         
         // считаем угол
         angle = atan2(y, x);
         if (angle  2*PI)
             angle -= 2*PI;
         
         deg = (u16)(angle * (180 / PI)) ; 

Учтите, что значения знаковые. Т.е. переменные dataX,Y,Z должны быть со знаком. Иначе Вы сможете измерить угол только от 0 до 45 градусов. Остальное будет мало сходится с реальностью.

Вот проект для IAR: https://www.dropbox.com/s/tnweie92fkripzv/stm32_HMC5883L.rar
Подписывайтесь, ставьте лайк! ну или как там говорят…

 

Похожий код:

Фото аватара
Алексей Петров

Программист, разработчик с 5 летним опытом работы. Учусь на разработчика игр на Unity и разработчика VR&AR реальности (виртуальной реальности). Основные языки программирования: C#, C++.

Оцените автора
Бла, бла код
Добавить комментарий

  1. sva

    Это все замечательно, вот только портянки while'ов не радуют. Что, если МК помимо работы с датчиками, должен другую работу выполнять? Частота проца 168 МГц, а i2c даже если 400 кГц, то проц тупо простаивать кучу времени будет. Лучше на прерываниях делать. В stm вход в прерывание всего 12 тактов и событий i2c наверняка кучу можно использовать. Точно не знаю, но вряд ли тут ситуация хуже, чем в AVR.

    Ответить
  2. Гость

    Предположим, в системе с десяток датчиков, процу, помимо их опрашивания, необходимо выполнять другую работу. С такой реализацией он будет кучу времени простаивать. Ведь скорость шины i2c очень и очень невысока. Вывод: никаких while'ов, только прерывания. Конечно, надо реализовать логику общения с датчиками с учетом занятой шины, ошибок приема-передачи и др. В представленном виде реализация годится лишь "на попробовать".

    Ответить
  3. lamazavr

    именно на попровобавть и есть)

    посмотреть кто такой i2c

    Ответить
  4. Дмитрий

    Для этого в HAL есть обмен через прерывания или через DMA. Это пример только для проверки работоспособности.

    Ответить
  5. Андрей

    I2C_Direction_Transmitter используется только при передаче, не не при приеме. Просмотрел разные ресурсы на тему I2C везде одна и та же ошибка.

    Ответить