08 апреля 2014

Пример реализации софтового I2C на STM32

Введение

I2C - последовательная шина данных для связи интегральных схем, разработанная фирмой Philips в начале 1980-х как простая шина внутренней связи для создания управляющей электроники. Используется для соединения низкоскоростных периферийных компонентов с материнской платой, встраиваемыми системами и мобильными телефонами. (с) wikipedia.

Основные особенности I2C:

  1. Мультимастер. Устройства на шине одновременно могу работать как мастером, так и подчиненным.
  2. Аппаратный контроль доставки пакета. В конце каждого пакета принимающая сторона обязана подтвердить факт корректного приема входящего пакета.
  3. Используются всего две линии ввода/вывода.
  4. Возможность "горячего" подключения новых устройств к шине во время работы.
  5. Не самая большая скорость обмена, по сравнению с другими интерфейсами.
  6. Отсутствие встроенного контроля целостности передаваемых данных.

Область применения данной шины лучше всего подходит для взаимодействия устройств в пределах одной печатной платы. Это может быть межконтроллерный обмен, подключение датчиков, микросхем энергонезависимой памяти, драйверов, портов расширения и др. Физическая реализация и принцип действия уже подробно расписан на многих ресурсах в интернете, и повторять ее вновь смысла нет. Как вариант, с этой информацией можно ознакомиться в этой статье.
Данная статья посвящена реализации софтового I2C. В качестве подопытного камня выбран STM32F100RBT6, установленный на STM32VLDISCOVERY. Но, в силу софтовости метода, код легко переносится на другие платформы. За основу была взята готовая библиотека, ссылка на которую утеряна за давностью времен.

Важно отметить что для реализации интерфейса I2C порты ввода/вывода должны поддерживать режим "открытый коллектор", либо имитировать его. Такая имитация необходима, например, на контроллерах AVR. В STM32 все реализуется намного проще, за счет аппаратной поддержки этого режима.

Реализация

Для начала объявим макросы, с помощью которых будем манипулировать портами ввода/вывода микроконтроллера:
#define I2C_GPIO                    GPIOB
#define I2C_RCC_APB2Periph_GPIO     RCC_APB2Periph_GPIOB
#define GPIO_Pin_SDA                GPIO_Pin_0
#define GPIO_Pin_SCL                GPIO_Pin_1

#define SCLH                        I2C_GPIO->BSRR = GPIO_Pin_SCL
#define SCLL                        I2C_GPIO->BRR = GPIO_Pin_SCL

#define SDAH                        I2C_GPIO->BSRR = GPIO_Pin_SDA
#define SDAL                        I2C_GPIO->BRR = GPIO_Pin_SDA
#define SCLread                     I2C_GPIO->IDR & GPIO_Pin_SCL
#define SDAread                     I2C_GPIO->IDR & GPIO_Pin_SDA
Эти макросы позволяют открыть/закрыть выходной транзистор порта ввода/вывода, а также прочитать его текущее состояние.
Также объявим псевдонимы для результата, который будет возвращать функция в случае успеха или ошибки:
#define I2C_RESULT_SUCCESS          0
#define I2C_RESULT_ERROR            (-1)
Инициализация нашего модуля I2C сводится к инициализации портов ввода/вывода микроконтроллера:
/**
 *  @brief  Инициализация модуля i2c, а именно портов ввода/вывода
 *  @param  void
 *  @return void
 */
void i2cSoft_Init ()
{
    GPIO_InitTypeDef GPIO_InitStructure;
    RCC_APB2PeriphClockCmd( I2C_RCC_APB2Periph_GPIO, ENABLE );
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_SCL | GPIO_Pin_SDA;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
    GPIO_Init( I2C_GPIO, &GPIO_InitStructure );
}
Создадим вспомогательную функцию задержки, которую будем использовать в библиотеке:
/**
 *  @brief  Реализация простой задержки
 *  @param  void
 *  @return void
 */
static void i2cSoft_Delay ()
{
    volatile uint16_t i = I2C_DELAY_VALUE;
    while ( i ) {
        i--;
    }
}
Здесь псевдоним I2C_DELAY_VALUE, который объявлен в i2c_Soft.h, определяет длину задержки. Она может варьироваться для разных контроллеров, работающих на разных скоростях.
Далее создадим функции работы с шиной: формирование Старта, Стопа, Подтверждения и др.
/**
 *  @brief  Отправка последовательности СТАРТ в шину
 *  @param  void
 *  @return bool - результат выполнения функции:
 *          true в случае успеха
 *          false в случае ошибки
 */
static bool i2cSoft_Start ()
{
    SDAH;                       // отпустить обе линии, на случай
    SCLH;                       // на случай, если они были прижаты
    i2cSoft_Delay();
    if ( !(SDAread) )           // если линия SDA прижата слейвом,
        return false;           // то сформировать старт невозможно, выход с ошибкой
    SDAL;                       // прижимаем SDA к земле
    i2cSoft_Delay();
    if ( SDAread )              // если не прижалась, то шина неисправна
        return false;           // выход с ошибкой
    i2cSoft_Delay();
    return true;                // старт успешно сформирован
}

/**
 *  @brief  Отправка последовательности СТОП в шину
 *  @param  void
 *  @return bool - результат выполнения функции:
 *          true в случае успеха
 *          false в случае ошибки
 */
static void i2cSoft_Stop ()
{
    SCLL;                       // последовательность для формирования Стопа
    i2cSoft_Delay();
    SDAL;
    i2cSoft_Delay();
    SCLH;
    i2cSoft_Delay();
    SDAH;
    i2cSoft_Delay();
}

/**
 *  @brief  Отправка последовательности ACK в шину
 *  @param  void
 *  @return void
 */
static void i2cSoft_Ack ()
{
    SCLL;
    i2cSoft_Delay();
    SDAL;                       // прижимаем линию SDA к земле
    i2cSoft_Delay();
    SCLH;                       // и делаем один клик линием SCL
    i2cSoft_Delay();
    SCLL;
    i2cSoft_Delay();
}

/**
 *  @brief  Отправка последовательности NO ACK в шину
 *  @param  void
 *  @return void
 */
static void i2cSoft_NoAck ()
{
    SCLL;
    i2cSoft_Delay();
    SDAH;                       // отпускаем линию SDA
    i2cSoft_Delay();
    SCLH;                       // и делаем один клик линием SCL
    i2cSoft_Delay();
    SCLL;
    i2cSoft_Delay();
}

/**
 *  @brief  Проверка шины на наличие ACK от слейва
 *  @param  void
 *  @return bool - результат выполнения функции:
 *          true  - если ACK получен
 *          false - если ACK НЕ получен
 */
static bool i2cSoft_WaitAck ()
{
    SCLL;
    i2cSoft_Delay();
    SDAH;
    i2cSoft_Delay();
    SCLH;                       // делаем половину клика линией SCL
    i2cSoft_Delay();
    if ( SDAread ) {            // и проверяем, прижал ли слейв линию SDA
        SCLL;
        return false;
    }
    SCLL;                       // завершаем клик линией SCL
    return true;
}
С помощью этих базовых функций уже можно отправлять и получать байты в/из шины. Создадим на их основе две функции для данных целей:
/**
 *  @brief  Отправка одного байта data в шину
 *  @param  uint8_t data - байт данных для отправки
 *  @return void
 */
static void i2cSoft_PutByte ( uint8_t data )
{
    uint8_t i = 8;              // нужно отправить 8 бит данных
    while ( i-- ) {             // пока не отправили все биты
        SCLL;                   // прижимаем линию SCL к земле
        i2cSoft_Delay();
        if ( data & 0x80 )      // и выставляем на линии SDA нужный уровень
            SDAH;
        else
            SDAL;
        data <<= 1;
        i2cSoft_Delay();
        SCLH;                   // отпускаем линию SCL
        i2cSoft_Delay();        // после этого слейв сразу же прочитает значение на линии SDA
    }
    SCLL;
}

/**
 *  @brief  Чтение одного байта data из шины
 *  @param  void
 *  @return uint8_t - прочитанный байт
 */
static uint8_t i2cSoft_GetByte ()
{
    volatile uint8_t i = 8;     // нужно отправить 8 бит данных
    uint8_t data = 0;

    SDAH;                       // отпускаем линию SDA. управлять ей будет слейв
    while ( i-- ) {             // пока не получили все биты
        data <<= 1;
        SCLL;                   // делаем клик линией SCL
        i2cSoft_Delay();
        SCLH;
        i2cSoft_Delay();
        if ( SDAread ) {        // читаем значение на линии SDA
            data |= 0x01;
        }
    }
    SCLL;
    return data;                // возвращаем прочитанное значение
}
Минимальный слой для работы с шиной I2C готов. Теперь мы можем работать с шиной, записывая и читая по одному байту в режиме мастера. Но интерфейс I2C специфицирует логический уровень передачи пакетов. В этих пакетах указывается адрес слейва, с которым работает мастер и намерения мастера (чтение или запись). Для работы на этом уровне создадим еще две функции. Одна из них будет читать указанное количество байт в указанный буфер из слейва с указанным адресом. Другая соответственно будет записывать данные.
/**
 *  @brief  Чтение данных из шины в буфер buffer, размером sizeOfBuffer
 *          у слейва с адресом chipAddress.
 *  @param  uint8_t chipAddress     - адрес подчиненного
 *          uint8_t *buffer         - указатель на буфер, куда класть
 *                                    прочитанные данные
 *          uint32_t sizeOfBuffer   - количество байт для чтения
 *  @return int - результат выполнения фунции:
 *          I2C_RESULT_SUCCESS  - в случае успеха
 *          I2C_RESULT_ERROR    - в случае ошибки
 */
int i2cSoft_ReadBuffer ( uint8_t chipAddress, uint8_t *buffer, uint32_t sizeOfBuffer )
{
    if ( !i2cSoft_Start() )
        return I2C_RESULT_ERROR;

    i2cSoft_PutByte( chipAddress + 1 );
    if ( !i2cSoft_WaitAck() ) {
        i2cSoft_Stop();
        return I2C_RESULT_ERROR;
    }

    while ( sizeOfBuffer != 0 ) {
        *buffer = i2cSoft_GetByte();

        buffer++;
        sizeOfBuffer--;
        if ( sizeOfBuffer == 0 ) {
            i2cSoft_NoAck();
            break;
        }
        else
            i2cSoft_Ack();
    }
    i2cSoft_Stop();
    return I2C_RESULT_SUCCESS;
}

/**
 *  @brief  Запись данных в шину из буфера buffer, размером sizeOfBuffer
 *          в слейва с адресом chipAddress.
 *  @param  uint8_t chipAddress     - адрес подчиненного
 *          uint8_t *buffer         - указатель на буфер, откуда читать
 *                                    записываемые данные
 *          uint32_t sizeOfBuffer   - количество байт для записи
 *  @return int - результат выполнения фунции:
 *          I2C_RESULT_SUCCESS  - в случае успеха
 *          I2C_RESULT_ERROR    - в случае ошибки
 */
int i2cSoft_WriteBuffer ( uint8_t chipAddress, uint8_t *buffer, uint32_t sizeOfBuffer )
{
    if ( !i2cSoft_Start() )
        return I2C_RESULT_ERROR;

    i2cSoft_PutByte( chipAddress );
    if ( !i2cSoft_WaitAck() ) {
        i2cSoft_Stop();
        return I2C_RESULT_ERROR;
    }

    while ( sizeOfBuffer != 0 ) {
        i2cSoft_PutByte( *buffer );
        if ( !i2cSoft_WaitAck() ) {
            i2cSoft_Stop();
            return I2C_RESULT_ERROR;
        }

        buffer++;
        sizeOfBuffer--;
    }
    i2cSoft_Stop();
    return I2C_RESULT_SUCCESS;
}
В итоге мы имеем софтовую реализацию интерфейса I2C в режиме мастера. Данная библиотека умеет отправлять и принимать произвольное количество байт в/из шины по любому адресу слейва. Данные функции достаточно удобны для последующего использования в слоях более высокого уровня, например, при работе с энергонезависимой памятью.

Исходные файлы можно скачать отсюда.

Не приложен действующий проект по той причине, что данная реализация является промежуточным интерфейсом, и для работы с устройствами I2C необходима дополнительная реализация логики работы. Пример по работе с энергонезависимой памятью с помощью этого модуля можно посмотреть здесь.

8 комментариев:

  1. >>Здесь псевдоним I2C_DELAY_VALUE
    и где подвох?

    ОтветитьУдалить
    Ответы
    1. Небольшая ошибка, уже исправил. Константа задает задержку магическим числом, подбирать которую нужно экспериментально.
      volatile uint16_t i = I2C_DELAY_VALUE;

      Удалить
  2. А зачем отправлять сигнал NoACK в функции i2cSoft_ReadBuffer?

    ОтветитьУдалить
    Ответы
    1. Таким образом мы сообщаем слейву что закончили считывать буфер. Страндарт требует отправлять NoACK.

      Удалить
  3. Спасибо! Искал программную реализацию I2c т.к. аппаратная плохо определяла один из датчиков (sht21) - заработала только ваша версия. Единственное вроде должно быть "i2cSoft_PutByte( chipAddress << 1 );" вместо "i2cSoft_PutByte( chipAddress + 1 );"

    ОтветитьУдалить
  4. Спасибо. Реально полезная статья. Помогла побороть чип STM32L041 (там библиотеки I2C HAL вообще отказывалась работать, LL работала как то криво).

    ОтветитьУдалить