Сильно удивился, когда выяснил, что под 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. Выбран ввиду наличия сенсора и небольшого кол-ва кода в его драйвере.
Порядок действий
- Переписать код с С++ на С. Для библиотеки и для драйвера
- В i2cdevlib переписать функции работы с i2c на HAL'овские по пути выбросив arduino-related куски кода
- Тестирование результатов, отладка
Переписываем код
Для начала, переписываем с С++ на С. Нет, для начала — обьясню зачем :)
В мире 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, которая эффективнее простого копирования в цикле.
/** 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 и дальше работать с функциями драйвера для получения данных с сенсора. Вот собственно листинг программы и результат ее работы:
#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.
Спасибо за интерес!
Материалы почитать:
Спецификация i2c
Сайт библиотеки i2cdevlib с драйверами и другими полезностями
Комментарии (11)
dsd_corp
07.06.2015 23:58+3>> каждое устройство на линии отозвалось дважды подряд, при чем, не на свой адрес
На свой, на свой. )
Восьмой бит — бит записи. Соответственно девайс отзывается только на первые семь бит собственно адреса, не зависимо от состояния восьмого бита(игнорируя его).
Таким образом, циклом for(i = 0; i<255; i++) {...} вы опросили 127 адресов, спрашивая каждый из них дважды.
P.S. Ну а за либу спасибо. Пригодится в качестве шпаргалки как минимум )Anvol Автор
08.06.2015 00:05Абсолютно верно) имелось ввиду, не тот адрес, который в библиотеках указан либо в даташите. Это я больше с учебной целью описал, ведь даже на оф сайте ардуино сканируют без сдвига, просто 0-127.
dsd_corp
08.06.2015 01:31А, ну с выводом в HEX без сдвига все понятно. Справедливости ради, в даташитах зачастую все-таки указывают адреса не в виде байта в HEX-представлении, а тупо в виде последовательности битов. Иногда и меньше, чем семь штук, с последующей простыней описания, как конфигурятся остальные биты…
>> ведь даже на оф сайте ардуино...
Ну ардуино, с их «lazy coding», это конечно показатель, ага )
true_man
08.06.2015 05:54+3Использование malloc — это очень не гуд, как будто бы не для микроконтроллера
Anvol Автор
08.06.2015 10:48Понимаю, но варианты какие? Держать большой буфер локальный и копировать туда, либо вручную слать байты, как в Wire. Второй вариант слабо совместим с идеей держать либу на HAL.
true_man
08.06.2015 11:02Вам временный буфер нужен только для того, чтобы туда еще и адрес вставить. Вполне возможно обойтись двумя вызовами HAL_I2C_Transmit. И кстати, не стоит забывать про многопоточность.
Anvol Автор
08.06.2015 11:05Два вызова transmit не работают, пробовал. BMP180 отказывался давать температуру. Я бы с радостью убрал malloc (
Anvol Автор
08.06.2015 11:15А вот чем можно обойтись — исправлениями в драйверах сенсоров при портировании. Прямо там складывать регистр и данные в один буфер. Как считаете?
Anvol Автор
08.06.2015 10:59К слову, вчера прочитал статью habrahabr.ru/post/255661 и с желанием потестить фильтр Маджвика — портировал HMC5883L. Сегодня пушну в репозиторий
Anvol Автор
08.06.2015 15:38+1Сказано — сделано )
github.com/anvol/i2cdevlib/tree/master/STM32/HMC5883L
10s
Спасибо, пригодится.