📜 ⬆️ ⬇️

Port i2cdevlib on STM32 HAL


I was very surprised when I found out that under STM32 there is no such variety of ready-made drivers for all sorts of i2c sensors, as for Arduino. Those that I managed to find were part of any OS (for example, ChubiOS, FreeRTOS, NuttX) and were more POSIX-like. And I wanted to write under HAL: (

The Arduino community uses the i2cdevlib library for abstraction from hardware when writing sensor drivers. Actually, I am sharing my work - the i2cdevlib port on the STM32 HAL (pull-request has already been sent), and under the cut I will tell you about the pebbles that I collected along the way. Well, the code examples will be.

What we work with


I have a dev board stm32f429i-disco in my hands, a board with gy-87, arduino uno sensors, EmBitz 0.40 development environment (ex Em :: Blocks) and Arduino.
Arduinka was used to compare the results of reading register values. The first porting sensor is BMP085 / BMP180. Selected due to the presence of the sensor and a small amount of code in its driver.

Procedure


  1. Rewrite code from C ++ to C. For the library and for the driver
  2. In i2cdevlib, rewrite the functions of working with i2c on HAL along the way, discarding arduino-related code pieces
  3. Testing results, debugging

')

Rewriting code


For a start, we copy from C ++ to C. No, for a start, I will explain why :)
In the embedded world, pure C is used more often. HAL itself serves as an example. Popular development environments (EmBlocks, Keil) create projects in C. The code that the STM32CubeMX generates is also generic. Yes, and using CBS in C ++ project is easier than transferring the entire project to C ++ for the sake of.

Go. Changing the names of functions, for example, I2Cdev :: readByte was I2Cdev_readByte . Also, do not forget to add such a prefix to all function calls inside the class, where it does not exist ( readByte -> I2Cdev_readByte ). Routine, nothing special.
At the same time, we understand the architecture of the library - only 4 functions that work with iron:

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); 


We perform a similar procedure with the BMP085 driver. We add the missing inclusions (math.h, stdint.h, stdlib.h, string.h) along the way and declare the bool type. This is C, baby) Perhaps it would be worth simply rewriting functions with bool -> uint8_t ...

Also in I2CDev you need to add a link to the structure with initialized i2c, which we will use for communications:
 #include "stm32f4xx_hal.h" I2C_HandleTypeDef * I2Cdev_hi2c; 


Implementing functions on HAL


The first in line will be I2Cdev_readBytes. Here is the original listing, without debug pieces and implementations for different libraries / versions

 /** 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; } 

I don’t quite understand how this crutch with a cycle works, because in the case of length> BUFFER_LENGTH we will specify the initial register in the new one. I assume that the code
 Wire.beginTransmission(devAddr); Wire.write(regAddr); Wire.endTransmission(); Wire.beginTransmission(devAddr); 

must be before the loop. In any case, the meaning is clear, we write under 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; } 

Pay attention to the address shift - devAddr << 1. When I proceeded to testing the library with the driver, the first thing to check the correctness of the module connection was sketched the bus scanner:

 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); } 

You correctly noted, I deliberately took all values ​​from 0-255, and not just 112 addresses allowed by the specification. This made it possible to identify an error - each device on the line responded twice in a row, and not to its address:



Wire.begin () uses a 7-bit address, and HAL uses an 8-bit representation. After a minute of reflection and corrections, we get a working scanner code:
 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); } 

Conclusion - the device address must be shifted to the left to the left before calling the functions HAL_I2C _ ***



Go back to i2cdevlib. Next in line is 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; } 


In the original, MSB and LSB are manually read and written to the buffer.
I'm not lying
 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(); } 


Go to the data recording functions. Here we are waiting for a bit of work with a dynamic array. The fact is that the register address to start recording and the data to be written must be in the same transaction START - STOP bits. And they are transferred to the function separately. For the arduino library, Wire is not a problem, because the programmer himself writes begin / end and sends data between them. We need to put it all in one buffer and transfer. We use malloc and memcpy , which is more efficient than simple copying in a loop.

UPD 07/13/2016 : already redone, instead of dancing with malloc and memcpy , the function HAL_I2C_Mem_Write is used , which takes the device address, the register address and the data that should be written there. Here is a diff commit

 /** 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; } 


Similarly for I2Cdev_writeWords , only memory is allocated for uint16_t + one byte for uint8_t regAddr. HAL'u time that the pointer to uint8_t, but we specify the length of the array correctly :)

 /** 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; } 


Testing results, debugging


For the test, we need to initialize i2c, assign a pointer to the structure in I2Cdev_hi2c, and then work with driver functions to get data from the sensor. Here is the actual listing of the program and the result of its work:
example 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(); } 


It shows temperature in C, pressure in Pascals and height above sea level in meters



Result


The library is ported, two drivers are also ready for work - for BMP085 / BMP180 and MPU6050. I will show the work of the latter in the photo and give an example of the code:
a photo


code example
 #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(); } 


The sensor data was verified with the data obtained through the arduino uno connected to the same sensors.
In the near future I will add drivers for other sensors that I have on hand - ADXL345 and HMC5883L. The rest, perhaps, you will not be difficult to port if necessary. If anything - write, help :)

I hope my work will save someone time and / or facilitate the transition from Arduinok to STM32.
Thank you for your interest!

UPD 07.13.2016 : malloc and memcpy removed, using HAL_I2C_Mem_Write . The original code left to understand the logic of the discussion in the comments. I repeat, here are the changes

Materials read:
I2c specification
Website library i2cdevlib with drivers and other usefulness

Source: https://habr.com/ru/post/259709/


All Articles