Pull to refresh

Порт i2cdevlib на STM32 HAL

Developing for Arduino *

Сильно удивился, когда выяснил, что под 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, &regAddr, 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, &regAddr, 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 с драйверами и другими полезностями
Tags:
Hubs:
Total votes 23: ↑23 and ↓0 +23
Views 21K
Comments Comments 13