Порт i2cdevlib на STM32 HAL

Автор: admin от 7-06-2015, 16:08, посмотрело: 697

Порт 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. Выбран ввиду наличия сенсора и небольшого кол-ва кода в его драйвере.

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



  • Переписать код с С++ на С. Для библиотеки и для драйвера

  • В 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, &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 разрешенных спецификацией адресов. Это позволило выявить ошибку — каждое устройство на линии отозвалось дважды подряд, при чем, не на свой адрес:

    Порт i2cdevlib на STM32 HAL

    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 на STM32 HAL

    Возвращаемся дальше к 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 в буфер.

    Переходим к функциям записи данных. Тут нас ждет немного работы с динамическим массивом. Дело в том, что адрес регистра для начала записи и данные для записи должны быть в одной транзакции 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 и дальше работать с функциями драйвера для получения данных с сенсора. Вот собственно листинг программы и результат ее работы:

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

    Порт i2cdevlib на STM32 HAL

    Результат


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


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

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

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

    Источник: Хабрахабр

    Категория: Программирование

    Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
    Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

    Добавление комментария

    Имя:*
    E-Mail:
    Комментарий:
    Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
    Введите два слова, показанных на изображении: *