Порт i2cdevlib на STM32 HAL


    Сильно удивился, когда выяснил, что под STM32 нет такого разнообразия готовых драйверов под разного рода i2c сенсоры, как под Arduino. Те, которые мне удалось найти, были частью какой либо ОС (например, ChubiOS, FreeRTOS, NuttX) и были более POSIX-like. А хотелось писать под HAL :(

    Arduino комюнити использует библиотеку i2cdevlib для абстракции от железа при написании драйверов сенсоров. Собственно, делюсь своей работой — порт i2cdevlib на STM32 HAL (pull-request уже отправил), а под катом я расскажу о камушках, которые собрал по пути. Ну и примеры кода будут.

    С чем работаем


    На руках у меня dev board stm32f429i-disco, плата с сенсорами gy-87, arduino uno, среды разработки EmBitz 0.40 (ex Em::Blocks) и Arduino.
    Ардуинка использовалась для сравнения результатов считывания значений регистров. Первый сенсор для портирования — BMP085/BMP180. Выбран ввиду наличия сенсора и небольшого кол-ва кода в его драйвере.

    Порядок действий


    1. Переписать код с С++ на С. Для библиотеки и для драйвера
    2. В i2cdevlib переписать функции работы с i2c на HAL'овские по пути выбросив arduino-related куски кода
    3. Тестирование результатов, отладка


    Переписываем код


    Для начала, переписываем с С++ на С. Нет, для начала — обьясню зачем :)
    В мире embedded намного чаще используется чистый С. Примером тому служит и сам HAL. Популярные среды разработки (EmBlocks, Keil) создают проекты на С. Код, которые генерирует STM32CubeMX также сишный. Да и использовать сишную либу в С++ проекте легче, чем переводить весь проект на С++ ради либы.

    Поехали. Меняем названия функций, например было I2Cdev::readByte стало I2Cdev_readByte. Также не забываем добавлять такой префикс ко всем вызовам функций внутри класса, где его нет (readByte -> I2Cdev_readByte). Рутина, ничего особенного.
    Параллельно понимаем архитектуру библиотеки — всего 4 функции, которые работают с железом:

    uint8_t I2Cdev_readBytes(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint8_t *data, uint16_t timeout);
    uint8_t I2Cdev_readWords(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint16_t *data, uint16_t timeout);
    uint16_t I2Cdev_writeBytes(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint8_t* data);
    uint16_t I2Cdev_writeWords(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint16_t* data);
    


    Аналогичную процедуру проделываем с драйвером BMP085. Дописываем недостающие инклюды (math.h, stdint.h, stdlib.h, string.h) по пути и обьявляем тип bool. Это С, детка) Возможно, стоило бы просто переписать функции с bool -> uint8_t…

    Также в I2CDev надо добавить ссылку на структуру с инициализированным i2c, которую мы будем использовать для коммуникаций:
    #include "stm32f4xx_hal.h"
    
    I2C_HandleTypeDef * I2Cdev_hi2c;
    


    Реализация функций на HAL


    Первой на очереди будет I2Cdev_readBytes. Вот оригинальный листинг, без отладочных кусков и реализаций под разные библиотеки/версии

    /** Read multiple bytes from an 8-bit device register.
     * @param devAddr I2C slave device address
     * @param regAddr First register regAddr to read from
     * @param length Number of bytes to read
     * @param data Buffer to store read data in
     * @param timeout Optional read timeout in milliseconds (0 to disable, leave off to use default class value in I2Cdev::readTimeout)
     * @return Number of bytes read (-1 indicates failure)
     */
    int8_t I2Cdev::readBytes(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint8_t *data, uint16_t timeout) {
        int8_t count = 0;
        uint32_t t1 = millis();
    
        // Arduino v1.0.1+, Wire library
        // Adds official support for repeated start condition, yay!
    
        // I2C/TWI subsystem uses internal buffer that breaks with large data requests
        // so if user requests more than BUFFER_LENGTH bytes, we have to do it in
        // smaller chunks instead of all at once
        for (uint8_t k = 0; k < length; k += min(length, BUFFER_LENGTH)) {
            Wire.beginTransmission(devAddr);
            Wire.write(regAddr);
            Wire.endTransmission();
            Wire.beginTransmission(devAddr);
            Wire.requestFrom(devAddr, (uint8_t)min(length - k, BUFFER_LENGTH));
    
            for (; Wire.available() && (timeout == 0 || millis() - t1 < timeout); count++) {
                data[count] = Wire.read();
            }
        }
    
        // check for timeout
        if (timeout > 0 && millis() - t1 >= timeout && count < length) count = -1; // timeout
    
        return count;
    }
    

    Я не совсем понимаю, как этот костыль с циклом работает, ведь в случае length > BUFFER_LENGTH мы по новой укажем начальный регистр. Предполагаю, что код
    Wire.beginTransmission(devAddr);
    Wire.write(regAddr);
    Wire.endTransmission();
    Wire.beginTransmission(devAddr);
    

    должен быть перед циклом. В любом случае, смысл понятен, пишем под HAL:

    uint8_t I2Cdev_readBytes(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint8_t *data, uint16_t timeout)
    {
        uint16_t tout = timeout > 0 ? timeout : I2CDEV_DEFAULT_READ_TIMEOUT;
    
        HAL_I2C_Master_Transmit(I2Cdev_hi2c, devAddr << 1, ®Addr, 1, tout);
        if (HAL_I2C_Master_Receive(I2Cdev_hi2c, devAddr << 1, data, length, tout) == HAL_OK) 
        	return length;
        else
            return -1;
    }
    

    Обратите внимание на сдвиг адреса — devAddr << 1. Когда я перешел к тестированию библиотеки с драйвером, то первым делом для проверки правильности подключения модуля набросал сканер шины:

    uint8_t i = 0;
    for(i = 0; i<255; i++)
    {
        if(HAL_I2C_IsDeviceReady(&hi2c3, i, 10, 100) == HAL_OK)
    		printf("Ready: 0x%02x", i);
    } 
    

    Вы правильно заметили, я умышленно взял все значения 0-255, а не только 112 разрешенных спецификацией адресов. Это позволило выявить ошибку — каждое устройство на линии отозвалось дважды подряд, при чем, не на свой адрес:



    Wire.begin() использует 7-битный адрес, а HAL — 8-битное представление. Спустя минуту размышлений и исправлений, получаем работающий код сканера:
    uint8_t i = 0;
    for(i = 15; i<127; i++)
    {
        if(HAL_I2C_IsDeviceReady(&hi2c3, i << 1, 10, 100) == HAL_OK)
    		printf("Ready: 0x%02x", i);
    } 
    

    Вывод — адрес устройства нужно самому сдвинуть на бит влево перед вызовом функций HAL_I2C_***



    Возвращаемся дальше к i2cdevlib. Следующая на очереди — I2Cdev_readWords.

    uint8_t I2Cdev_readWords(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint16_t *data, uint16_t timeout)
    {
        uint16_t tout = timeout > 0 ? timeout : I2CDEV_DEFAULT_READ_TIMEOUT;
    
        HAL_I2C_Master_Transmit(I2Cdev_hi2c, devAddr << 1, ®Addr, 1, tout);
        if (HAL_I2C_Master_Receive(I2Cdev_hi2c, devAddr << 1, (uint8_t *)data, length*2, tout) == HAL_OK) 
        	return length;
        else
            return -1;
    }
    


    В оригинале там вручную считывается и по очереди записывается MSB и LSB в буфер.
    не вру
    for (uint8_t k = 0; k < length * 2; k += min(length * 2, BUFFER_LENGTH)) {
        Wire.beginTransmission(devAddr);
        Wire.write(regAddr);
        Wire.endTransmission();
        Wire.beginTransmission(devAddr);
        Wire.requestFrom(devAddr, (uint8_t)(length * 2)); // length=words, this wants bytes
    
        bool msb = true; // starts with MSB, then LSB
        for (; Wire.available() && count < length && (timeout == 0 || millis() - t1 < timeout);) {
            if (msb) {
                // first byte is bits 15-8 (MSb=15)
                data[count] = Wire.read() << 8;
            } else {
                // second byte is bits 7-0 (LSb=0)
                data[count] |= Wire.read();
                #ifdef I2CDEV_SERIAL_DEBUG
                    Serial.print(data[count], HEX);
                    if (count + 1 < length) Serial.print(" ");
                #endif
                count++;
            }
            msb = !msb;
        }
    
        Wire.endTransmission();
    }
    


    Переходим к функциям записи данных. Тут нас ждет немного работы с динамическим массивом. Дело в том, что адрес регистра для начала записи и данные для записи должны быть в одной транзакции START — STOP битов. А в функцию они переданы раздельно. Для arduino библиотеки Wire это не проблема, ведь в ней программист сам пишет begin/end и шлет данные между ними. Нам же надо это все сложить в один буфер и передать. Используем malloc и memcpy, которая эффективнее простого копирования в цикле.

    UPD 13.07.2016: уже переделано, вместо плясок с malloc и memcpy используется функция HAL_I2C_Mem_Write, которая принимает адрес устройства, адрес регистра и данные, которые туда надо записать. Вот коммит diff

    /** Write multiple bytes to an 8-bit device register.
     * @param devAddr I2C slave device address
     * @param regAddr First register address to write to
     * @param length Number of bytes to write
     * @param data Buffer to copy new data from
     * @return Status of operation (true = success)
     */
    uint16_t I2Cdev_writeBytes(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint8_t *data)
    {
        // Creating dynamic array to store regAddr + data in one buffer
        uint8_t * dynBuffer;
        dynBuffer = (uint8_t *) malloc(sizeof(uint8_t) * (length+1));
        dynBuffer[0] = regAddr;
    
        // copy array
        memcpy(dynBuffer+1, data, sizeof(uint8_t) * length);
    
        HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(I2Cdev_hi2c, devAddr << 1, dynBuffer, length+1, 1000);
        free(dynBuffer);
        return status == HAL_OK;
    }
    


    Аналогично и для I2Cdev_writeWords, только память выделяем под uint16_t + один байт на uint8_t regAddr. HAL'у врем, что указатель на uint8_t, но длинну массива указываем правильно :)

    /** Write multiple words to a 16-bit device register.
     * @param devAddr I2C slave device address
     * @param regAddr First register address to write to
     * @param length Number of words to write
     * @param data Buffer to copy new data from
     * @return Status of operation (true = success)
     */
    uint16_t I2Cdev_writeWords(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint16_t* data)
    {
        // Creating dynamic array to store regAddr + data in one buffer
        uint8_t * dynBuffer;
        dynBuffer = (uint8_t *) malloc(sizeof(uint8_t) + sizeof(uint16_t) * length);
        dynBuffer[0] = regAddr;
    
        // copy array
        memcpy(dynBuffer+1, data, sizeof(uint16_t) * length);
        HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(I2Cdev_hi2c, devAddr << 1, dynBuffer, sizeof(uint8_t) + sizeof(uint16_t) * length, 1000);
        free(dynBuffer);
        return status == HAL_OK;
    }
    


    Тестирование результатов, отладка


    Для теста нам необходимо проинициализировать i2c, присвоить указатель на структуру в I2Cdev_hi2c и дальше работать с функциями драйвера для получения данных с сенсора. Вот собственно листинг программы и результат ее работы:
    пример BMP180
    #include "stm32f4xx.h"
    #include "stm32f4xx_hal.h"
    #include <stdint.h>
    #include <stdio.h>
    #include <string.h>
    #include "I2Cdev.h"
    #include "BMP085.h"
    
    I2C_HandleTypeDef hi2c3;
    
    int main(void)
    {
        SystemInit();
        HAL_Init();
    
        GPIO_InitTypeDef GPIO_InitStruct;
    
        /**I2C3 GPIO Configuration
        PC9     ------> I2C3_SDA
        PA8     ------> I2C3_SCL
        */
    
        __GPIOA_CLK_ENABLE();
        __GPIOC_CLK_ENABLE();
    
        GPIO_InitStruct.Pin = GPIO_PIN_9;
        GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
        GPIO_InitStruct.Pull = GPIO_PULLUP;
        GPIO_InitStruct.Speed = GPIO_SPEED_FAST;
        GPIO_InitStruct.Alternate = GPIO_AF4_I2C3;
        HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
    
        GPIO_InitStruct.Pin = GPIO_PIN_8;
        GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
        GPIO_InitStruct.Pull = GPIO_PULLUP;
        GPIO_InitStruct.Speed = GPIO_SPEED_FAST;
        GPIO_InitStruct.Alternate = GPIO_AF4_I2C3;
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
        __I2C3_CLK_ENABLE();
    
        hi2c3.Instance = I2C3;
        hi2c3.Init.ClockSpeed = 400000;
        hi2c3.Init.DutyCycle = I2C_DUTYCYCLE_2;
        hi2c3.Init.OwnAddress1 = 0x10;
        hi2c3.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
        hi2c3.Init.DualAddressMode = I2C_DUALADDRESS_DISABLED;
        hi2c3.Init.OwnAddress2 = 0x11;
        hi2c3.Init.GeneralCallMode = I2C_GENERALCALL_DISABLED;
        hi2c3.Init.NoStretchMode = I2C_NOSTRETCH_DISABLED;
    
        HAL_I2C_Init(&hi2c3);
    
        I2Cdev_hi2c = &hi2c3; // init of i2cdevlib.  
        // You can select other i2c device anytime and 
        // call the same driver functions on other sensors
    
        while(!BMP085_testConnection()) ;
    
        BMP085_initialize();
    
        while (1)
        {
            BMP085_setControl(BMP085_MODE_TEMPERATURE);
            HAL_Delay(BMP085_getMeasureDelayMilliseconds(BMP085_MODE_TEMPERATURE));
            float t = BMP085_getTemperatureC();
    
            BMP085_setControl(BMP085_MODE_PRESSURE_3);
            HAL_Delay(BMP085_getMeasureDelayMilliseconds(BMP085_MODE_PRESSURE_3));
            float p = BMP085_getPressure();
    
            float a = BMP085_getAltitude(p, 101325);
            printf("T: %3.1f  P: %3.0f  A: %3.2f", t, p ,a);
    
            HAL_Delay(1000);
        }
    }
    
    void SysTick_Handler()
    {
        HAL_IncTick();
        HAL_SYSTICK_IRQHandler();
    }
    


    Показывает температуру в С, давление в Паскалях и высоту над уровнем моря в метрах



    Результат


    Библиотека портирована, также готовы к работе два драйвера — для BMP085/BMP180 и MPU6050. Работу последнего покажу на фото и приведу пример кода:
    фото


    пример кода
    #include "stm32f4xx.h"
    #include "stm32f4xx_hal.h"
    #include <stdint.h>
    #include <stdio.h>
    #include <string.h>
    #include "I2Cdev.h"
    #include "BMP085.h"
    #include "MPU6050.h"
    
    I2C_HandleTypeDef hi2c3;
    
    int main(void)
    {
        SystemInit();
        HAL_Init();
    
        GPIO_InitTypeDef GPIO_InitStruct;
    
        /**I2C3 GPIO Configuration
        PC9     ------> I2C3_SDA
        PA8     ------> I2C3_SCL
        */
    
        __GPIOA_CLK_ENABLE();
        __GPIOC_CLK_ENABLE();
    
        GPIO_InitStruct.Pin = GPIO_PIN_9;
        GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
        GPIO_InitStruct.Pull = GPIO_PULLUP;
        GPIO_InitStruct.Speed = GPIO_SPEED_FAST;
        GPIO_InitStruct.Alternate = GPIO_AF4_I2C3;
        HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
    
        GPIO_InitStruct.Pin = GPIO_PIN_8;
        GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
        GPIO_InitStruct.Pull = GPIO_PULLUP;
        GPIO_InitStruct.Speed = GPIO_SPEED_FAST;
        GPIO_InitStruct.Alternate = GPIO_AF4_I2C3;
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
        __I2C3_CLK_ENABLE();
    
        hi2c3.Instance = I2C3;
        hi2c3.Init.ClockSpeed = 400000;
        hi2c3.Init.DutyCycle = I2C_DUTYCYCLE_2;
        hi2c3.Init.OwnAddress1 = 0x10;
        hi2c3.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
        hi2c3.Init.DualAddressMode = I2C_DUALADDRESS_DISABLED;
        hi2c3.Init.OwnAddress2 = 0x11;
        hi2c3.Init.GeneralCallMode = I2C_GENERALCALL_DISABLED;
        hi2c3.Init.NoStretchMode = I2C_NOSTRETCH_DISABLED;
    
        HAL_I2C_Init(&hi2c3);
    
        I2Cdev_hi2c = &hi2c3; // init of i2cdevlib.  
        // You can select other i2c device anytime and 
        // call the same driver functions on other sensors
    
        while(!BMP085_testConnection()) ;
    
        int16_t ax, ay, az;
        int16_t gx, gy, gz;
    
        int16_t c_ax, c_ay, c_az;
        int16_t c_gx, c_gy, c_gz;
    
        MPU6050_initialize();
        BMP085_initialize();
        MPU6050_setFullScaleGyroRange(MPU6050_GYRO_FS_250);
        MPU6050_setFullScaleAccelRange(MPU6050_ACCEL_FS_2);
    
        MPU6050_getMotion6(&c_ax, &c_ay, &c_az, &c_gx, &c_gy, &c_gz);
        while (1)
        {
            BMP085_setControl(BMP085_MODE_TEMPERATURE);
    
            HAL_Delay(BMP085_getMeasureDelayMilliseconds(BMP085_MODE_TEMPERATURE));
            float t = BMP085_getTemperatureC();
    
            BMP085_setControl(BMP085_MODE_PRESSURE_3);
            HAL_Delay(BMP085_getMeasureDelayMilliseconds(BMP085_MODE_PRESSURE_3));
            float p = BMP085_getPressure();
    
            float a = BMP085_getAltitude(p, 101325);
    
            printf(buf, "T: %3.1f  P: %3.0f  A: %3.2f", t, p ,a);
    
            MPU6050_getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
    
            printf("Accel: %d    %d    %d", ax - c_ax, ay - c_ay, az - c_az);
            printf("Gyro: %d    %d    %d", gx - c_gx, gy - c_gy, gz - c_gz);
            
            HAL_Delay(1000);
        }
    }
    
    void SysTick_Handler()
    {
        HAL_IncTick();
        HAL_SYSTICK_IRQHandler();
    }
    


    Данные сенсоров сверялись с данными полученными через arduino uno подключенную к тем же сенсорам.
    В ближайшее время добавлю драйвера для других сенсоров, что у меня есть на руках — ADXL345 и HMC5883L. Остальные, пожалуй, вам не составит труда самостоятельно портировать при необходимости. Если что — пишите, помогу :)

    Надеюсь, моя работа сэкономит кому-то время и/или облегчит переход с Ардуинок на STM32.
    Спасибо за интерес!

    UPD 13.07.2016: malloc и memcpy убрал, используется HAL_I2C_Mem_Write. Первоначальный код оставил, чтобы понятна была логика обсуждения в комментариях. Повторюсь, вот изменения

    Материалы почитать:
    Спецификация i2c
    Сайт библиотеки i2cdevlib с драйверами и другими полезностями

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 13

      0
      Спасибо, пригодится.
        +3
        >> каждое устройство на линии отозвалось дважды подряд, при чем, не на свой адрес

        На свой, на свой. )
        Восьмой бит — бит записи. Соответственно девайс отзывается только на первые семь бит собственно адреса, не зависимо от состояния восьмого бита(игнорируя его).
        Таким образом, циклом for(i = 0; i<255; i++) {...} вы опросили 127 адресов, спрашивая каждый из них дважды.

        P.S. Ну а за либу спасибо. Пригодится в качестве шпаргалки как минимум )
          0
          Абсолютно верно) имелось ввиду, не тот адрес, который в библиотеках указан либо в даташите. Это я больше с учебной целью описал, ведь даже на оф сайте ардуино сканируют без сдвига, просто 0-127.
            0
            А, ну с выводом в HEX без сдвига все понятно. Справедливости ради, в даташитах зачастую все-таки указывают адреса не в виде байта в HEX-представлении, а тупо в виде последовательности битов. Иногда и меньше, чем семь штук, с последующей простыней описания, как конфигурятся остальные биты…

            >> ведь даже на оф сайте ардуино...
            Ну ардуино, с их «lazy coding», это конечно показатель, ага )
          +3
          Использование malloc — это очень не гуд, как будто бы не для микроконтроллера
            0
            Понимаю, но варианты какие? Держать большой буфер локальный и копировать туда, либо вручную слать байты, как в Wire. Второй вариант слабо совместим с идеей держать либу на HAL.
              0
              Вам временный буфер нужен только для того, чтобы туда еще и адрес вставить. Вполне возможно обойтись двумя вызовами HAL_I2C_Transmit. И кстати, не стоит забывать про многопоточность.
                0
                Два вызова transmit не работают, пробовал. BMP180 отказывался давать температуру. Я бы с радостью убрал malloc (
                  0
                  А вот чем можно обойтись — исправлениями в драйверах сенсоров при портировании. Прямо там складывать регистр и данные в один буфер. Как считаете?
              0
              К слову, вчера прочитал статью habrahabr.ru/post/255661 и с желанием потестить фильтр Маджвика — портировал HMC5883L. Сегодня пушну в репозиторий
              0
              Попробую использовать ваш порт :) Разбираюсь в stm32, уже через UART его к компу прикрутил (stm32vldiscovery), теперь подцепил датчик (gy-88), даже кажется пообщался с ним по i2c, но на входе данных с акселерометра чтот то странное идет. Делал все на колене, а тут нашлась ваша либа. Очень удачно на HAL, старые API изучать сначала не хочется, лучше сразу современные использовать, так что попробуем. Спасибо за труд, надеюсь действительно разобраться побыстрее за счет готового инструмента.
                0
                Спасибо, очень приятно узнать, что труд не пропадает! Немного запоздал я с ответом..)
                Мои студенты с нулю подняли гиростабилизированную двухколесную платформу используя этот порт. Удалось даже запустить DMP на чипе MPU6050, правда там нашли ряд костылей и багов мейнстрим ветки.

              Only users with full accounts can post comments. Log in, please.