Введение

Основные особенности I2C:
- Мультимастер. Устройства на шине одновременно могу работать как мастером, так и подчиненным.
- Аппаратный контроль доставки пакета. В конце каждого пакета принимающая сторона обязана подтвердить факт корректного приема входящего пакета.
- Используются всего две линии ввода/вывода.
- Возможность "горячего" подключения новых устройств к шине во время работы.
- Не самая большая скорость обмена, по сравнению с другими интерфейсами.
- Отсутствие встроенного контроля целостности передаваемых данных.
Область применения данной шины лучше всего подходит для взаимодействия устройств в пределах одной печатной платы. Это может быть межконтроллерный обмен, подключение датчиков, микросхем энергонезависимой памяти, драйверов, портов расширения и др. Физическая реализация и принцип действия уже подробно расписан на многих ресурсах в интернете, и повторять ее вновь смысла нет. Как вариант, с этой информацией можно ознакомиться в этой статье.
Данная статья посвящена реализации софтового 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 необходима дополнительная реализация логики работы. Пример по работе с энергонезависимой памятью с помощью этого модуля можно посмотреть здесь.
Не приложен действующий проект по той причине, что данная реализация является промежуточным интерфейсом, и для работы с устройствами I2C необходима дополнительная реализация логики работы. Пример по работе с энергонезависимой памятью с помощью этого модуля можно посмотреть здесь.
>>Здесь псевдоним I2C_DELAY_VALUE
ОтветитьУдалитьи где подвох?
Небольшая ошибка, уже исправил. Константа задает задержку магическим числом, подбирать которую нужно экспериментально.
Удалитьvolatile uint16_t i = I2C_DELAY_VALUE;
А зачем отправлять сигнал NoACK в функции i2cSoft_ReadBuffer?
ОтветитьУдалитьТаким образом мы сообщаем слейву что закончили считывать буфер. Страндарт требует отправлять NoACK.
УдалитьСпасибо! Искал программную реализацию I2c т.к. аппаратная плохо определяла один из датчиков (sht21) - заработала только ваша версия. Единственное вроде должно быть "i2cSoft_PutByte( chipAddress << 1 );" вместо "i2cSoft_PutByte( chipAddress + 1 );"
ОтветитьУдалитьразобрался - все нормально с кодом.
УдалитьСпасибо. Реально полезная статья. Помогла побороть чип STM32L041 (там библиотеки I2C HAL вообще отказывалась работать, LL работала как то криво).
ОтветитьУдалитьклассно что статья помогла, я рад 🙂
Удалить