I2cdevlib port on STM32 HAL


    I was very surprised when I found out that under STM32 there is no such variety of ready-made drivers for various kinds of i2c sensors as under Arduino. The ones I managed to find were part of some OS (e.g. ChubiOS, FreeRTOS, NuttX) and were more POSIX-like. I wanted to write under HAL :( the

    Arduino community uses the i2cdevlib library to abstract from iron when writing sensor drivers. Actually, I’m sharing my work - i2cdevlib port on STM32 HAL (I already sent the pull-request), and under the cat I’ll talk about the pebbles that I collected along the way. Well, there will be code examples.

    What are we working with


    On my hands I have a dev board stm32f429i-disco, a board with gy-87 sensors, arduino uno, the development environment EmBitz 0.40 (ex Em :: Blocks) and Arduino.
    Arduinka was used to compare the results of reading register values. The first port sensor is the 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 from i2c to HAL'ovskie along the way throwing away arduino-related pieces of code
    3. Testing Results, Debugging


    Rewrite the code


    For starters, we rewrite from C ++ to C. No, for starters - I’ll explain why :)
    In the embedded world, pure C is used much more often. HAL itself is an example of this. Popular development environments (EmBlocks, Keil) create C projects. The code that STM32CubeMX generates is also large. And it’s easier to use sybnaya lib in a C ++ project than to translate the whole project into C ++ for the sake of it.

    Go. We change the names of functions, for example, I2Cdev :: readByte became 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.
    In parallel, we understand the architecture of the library - there are only 4 functions that work with hardware:

    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 path and declare the type bool. This is C, baby) Perhaps it would be worth just rewriting the 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 the HAL


    The first in line will be I2Cdev_readBytes. Here is the original listing, without debugging 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 works with a loop, because in the case of length> BUFFER_LENGTH we will specify the initial register in a new way. I guess the code
    Wire.beginTransmission(devAddr);
    Wire.write(regAddr);
    Wire.endTransmission();
    Wire.beginTransmission(devAddr);
    

    should 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 switched to testing the library with the driver, the first thing I did was to check the connection of the module with a 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 noticed, I deliberately took all the values ​​0-255, and not just 112 addresses allowed by the specification. This allowed us to identify an error - each device on the line responded twice in a row, and,



    moreover , to its own 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 itself needs to be shifted a bit to the left before calling the HAL_I2C _ *** functions. We



    return further 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, it reads manually and writes MSB and LSB to the buffer in turn.
    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();
    }
    


    We turn to the functions of data recording. Here we are waiting for a bit of work with a dynamic array. The fact is that the register address for starting recording and the data for writing should be in one START transaction - STOP bits. And they are transferred to the function separately. For the Wire library arduino, this is not a problem, because in it the programmer writes begin / end himself and sends the data between them. We need to put all this into one buffer and pass it. 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 HAL_I2C_Mem_Write function is used , which accepts the device address, register address and data that must be written there.Here is the 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 the memory is allocated for uint16_t + one byte per uint8_t regAddr. HAL'u time that the pointer to uint8_t, but the length of the array is specified 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 the driver functions to receive data from the sensor. Here is the actual listing of the program and the result of its work:
    BMP180 example
    #include "stm32f4xx.h"
    #include "stm32f4xx_hal.h"
    #include 
    #include 
    #include 
    #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();
    }
    


    Shows temperature in C, pressure in Pascals and altitude 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 code:
    a photo


    code example
    #include "stm32f4xx.h"
    #include "stm32f4xx_hal.h"
    #include 
    #include 
    #include 
    #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 checked with the data received through 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, it will not be difficult for you to independently port if necessary. If anything - write, I will help :)

    I hope my work saves someone time and / or facilitates 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 was left to make the discussion logic in the comments understandable. I repeat, here are the changes.

    Materials to read:
    i2c specification
    I2cdevlib library site with drivers and other utilities

    Also popular now: