Вот что действительно было тяжело найти, так это адекватный пример реализации i2c подчиненного устройства на STM32 с использованием прерываний. Нашлись несколько примеров блокирующих реализаций и пара вопросов на st-community.
А вот прям более-менее рабочий проект, где есть реализация нескольких регистров ведомого устройства, да еще и с разным уровнем доступа – такого очень не хватало. Буквально, чуть:
- http://amitesh-singh.github.io/stm32/2018/01/07/making-i2c-slave-using-stm32f103.html Тут дружок сделал хороший пример, но на libopencm3 и обработку засунул в прерывания.
- https://community.st.com/s/article/how-to-create-an-i2c-slave-device-using-the-stm32cube-library Пример “эхо” с одним без внутренних регистров. Этот пример у меня постоянно отваливался.
- https://youtu.be/8SisMjqqgmc Самое базовое взаимодействие Raspberry и STM32 c FreeRTOS. Видос без слов
- https://blog.radiotech.kz/stm32/i2c-chast-2-perevod-iz-knigi-mastering-stm32/ Глава из книги Mastering STM32. Пример только с чтением регистров заранее известной длины. Не завелось.
- https://os.mbed.com/docs/mbed-os/v6.15/apis/i2cslave.html (заблочено) Отлично взлетевшая реализация i2c с прерываниями на mbed OS. Прицепом идет целая ОСь. Ну, и заблоченная онлайн ide.
- Пет-проект рандомного разработчика wandrade/GISMo на гитхабе. На всякий случай создал форк проекта (https://github.com/tunerok/GISMo)
В общем, я достиг комедии дна. Мне понравилось, как работало на mbed OS, поэтому, я полез в исходники (https://github.com/ARMmbed/mbed-os и https://github.com/ARMmbed/mbed-hal). И начал грустно вычитывать код и размашисто резать лишние зависимости.
Логика работы подопытного следующая – есть обработчики прерываний, которые заполняют/опустошают буфферы, выставляют флажки готовности, пытаются отработать ошибки. В “основном” цикле эти флажки читаются и программа выполняет определенные действия в зависимости от установленных флажков – записывает или выдает данные.
При генерации проекта/инициализации руками – не забываем назначить адрес устройства и включить прерывания от i2c.
В общем-то и оставим эту логику. Вполне работает.
Что осталось-то? В итоге в мэйне у нас есть пара функций инициализации:
reg_factory() – сбрасывает значения регистров в дефолтные
i2c_slave_init(&hi2c1) – инициализирует логику слейв-устройства на выбранном интерфейсе.
/* USER CODE BEGIN 2 */ //Restoring regs factory settings reg_factory(); //Init i2c-slave i2c_slave_init(&hi2c1); //Start i2c irq HAL_I2C_EnableListen_IT(&hi2c1); /* USER CODE END 2 */
В главном цикле мы опрашиваем состояние буферов на входящие события – i2c_slave_receive().
И обрабатываем их в парсере protocol_i2c_parse(i2c_event).
Также проверяем, не зависла ли из-за нас линия в i2c_slave_check_timeout();
/* USER CODE BEGIN WHILE */ while (1) { //Check i2c events i2c_event = i2c_slave_receive(); //Serving events over the i2c bus protocol_i2c_parse(i2c_event); //i2c bus hang check i2c_slave_check_timeout(); /* USER CODE END WHILE */
Адреса регистров у нас определены в файле registers.h.
#define REG_VERSION_ADDR 0x00 #define REG_UINT16_RW_ADDR 0x01 #define REG_INT16_RW_ADDR 0x02 #define REG_BOOL_RW_ADDR 0x03 #define REG_CHAR_RW_ADDR 0x04 #define REG_UINT16_RO_ADDR 0x11 #define REG_INT16_RO_ADDR 0x12 #define REG_BOOL_RO_ADDR 0x13 #define REG_CHAR_RO_ADDR 0x14
Их дефолтные значения и уровни доступа определяем в registers.c.
volatile reg_t g_i2c_reg_data[] = { [VERSION] = { READ_ONLY, REG_VERSION_ADDR, CHAR, {.char_val = 0x01}, {0} }, [UINT16_RW] = { FULL_ACCESS, REG_UINT16_RW_ADDR, BOOL, {.uint16_val = 0x00}, {0} }, [INT16_RW] = { FULL_ACCESS, REG_INT16_RW_ADDR, UINT16, {.int16_val = 0x00}, {0} }, [BOOL_RW] = { FULL_ACCESS, REG_BOOL_RW_ADDR, UINT16, {.bool_val = 0x00}, {0} }, [CHAR_RW] = { FULL_ACCESS, REG_CHAR_RW_ADDR, UINT16, {.char_val = 0x00}, {0} }, [UINT16_RO] = { READ_ONLY, REG_UINT16_RO_ADDR, UINT16, {.uint16_val = 0x3344}, {0} }, [INT16_RO] = { READ_ONLY, REG_INT16_RO_ADDR, UINT16, {.int16_val = 0x2233}, {0} }, [BOOL_RO] = { READ_ONLY, REG_BOOL_RO_ADDR, BOOL, {.bool_val = 0x01}, {0} }, [CHAR_RO] = { READ_ONLY, REG_CHAR_RO_ADDR, UINT16, {.char_val = 0x15}, {0} }, };
Структура такая:
{ДОСТУП, АДРЕСС, ТИП_ДАННЫХ, {ЗНАЧЕНИЕ ПО-УМОЛЧАНИЮ}, {ЗНАЧЕНИЕ ТЕКУЩЕЕ}}.
Более подробно описано в хедер-файле registers.h
Еще небольшой интерес может представлять функция парсинга состояния слейв-устройства. За пример также была взята реализация в mbesOS.
int protocol_i2c_parse(int i2c_event){ reg_idx_t idx; static uint8_t buff[5] = {0}; switch (i2c_event) { //Мы собираемся читать данные case ReadAddressed: { //Находим индекс регистра(регистр - это ж массив) из адреса idx = reg_get_idx(buff[0]); //Проверка доступа if ((idx != NONE) && (idx != ECHO) && (g_i2c_reg_data[idx].access != WRITE_ONLY)){ //Отправляем данные значения регистра i2c_slave_write((uint8_t *)&g_i2c_reg_data[idx].value, reg_get_len(idx)); } else{ //Ошибка доступа или адрес вне диапазона buff[0] = 0xAA; buff[1] = 0xAA; i2c_slave_write(buff, 2); } break; } case WriteGeneral: { //НЕ ИСПОЛЬЗУЕТСЯ } //Мы хотим писать в регистр. (на самом деле мы всегда попадаем сюда по любой просьбе от мастера) //Поэтому нам нужно проверить длину пакета данных. //Первыми полученными данными - будет адрес регистра. //Если длина сообщения больше 1, то будем писать пришедшие данные в регистр. case WriteAddressed: { int data_cnt = 0; data_cnt = i2c_slave_read(buff, 3); if (data_cnt > 1){ //Если данных будет больше, чем 1, то мастер собрался что-то писать в регистры //Находим индекс регистра из адреса idx = reg_get_idx(buff[0]); //Проверка доступа if ((idx != NONE) && (idx != ECHO) && (g_i2c_reg_data[idx].access != READ_ONLY)){ //И меняем данные на пришедшие switch (g_i2c_reg_data[idx].value_type) { case UINT16: g_i2c_reg_data[idx].value.uint16_val = (uint16_t)(buff[1] | buff[2] << 8); break; case BOOL: g_i2c_reg_data[idx].value.char_val = buff[1] & 0x01; break; case CHAR: g_i2c_reg_data[idx].value.char_val = buff[1]; break; default: return 0; } //После изменения данных, можно вызвать какую-то функцию. Например, чтобы обработать новые данные или еще что-то. На ваше усмотрение protocol_reg_ctrl(idx); } } //Данных нет, поэтому мы попадем в ReadAddressed при следующем заходе в этот свич с адресом, который будет лежать в buf[0] break; } default: break; } return 1; }
В моем примере я сделал инкремент Read Only регистра при изменении данных в доступном для записи регистре.
Запуск
Для тестов взял старую Orange Pi One. Подключил к TWI1 (в системе определяется как 2-я шина I2C), объединил землю. Подтяжка линий есть уже на OrangePi.
Фотографий не будет, т.к. тут всего 3 проводка
Нужно установить i2ctools и проверяем:
root@orangepione:~# i2cdetect -y 2 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- 21 -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- -- -- root@orangepione:~# i2cget -y 2 0x21 0x00 0x01 root@orangepione:~# i2cget -y 2 0x21 0x01 w 0x0000 root@orangepione:~# i2cget -y 2 0x21 0x11 w 0x3344 root@orangepione:~# i2cset -y 2 0x21 0x01 0x0055 w root@orangepione:~# i2cget -y 2 0x21 0x01 w 0x0055 root@orangepione:~# i2cget -y 2 0x21 0x11 w 0x3345
Работает
Ткнувшись логическим анализатором, на линии увидел ожидаемую каринку.
В общем, получился небольшой пример, более-менее универсальный для STM32-устройств. Данный проект сделан для самой популярной на Алиэкспесс платы-отладки Nucleo-F303RE
Проверено на STM32L433, STM32F302R8
Функция для контроля таймаута БИТА(в i2c_slave.c) пока очень плоха, адаптируйте под себя или ждите обновление
Ссылка на GitHub:
Обновление от 14.08.2023
Оказалось, что при работе подобной системы в составе достаточно жирного проекта могут возникать коллизии, зависания, недоступность шины в самые случайные моменты времени. Причем, стандартные средства для перезагрузки/реинициализации не особо помогают, т.к. общее поседение системы зависит не только от слейв-устройства, но и от мастера. Поэтому, если планируется в основном цикле программы иметь еще и продолжительные, ресурсоемкие операции – стоит переписать способ общения слейва на использование только прерываний. То есть – все операции с i2c шиной будут выполняться только в прерывании.
В итоге, мы убираем задержки и отработку ответов в основном цикле и переносим все в прерывания. Работу с данными в регистрах можно осуществлять в основном цикле программы, но чтение и запись – лучше оставить в прерывании, если нет больших объемов данных. Если данных очень много – то стоит подумать о ПДП.
При ОЧЕНЬ активной загрузке интерфейс может не справиться и прерывания не будут включены. Для того, чтобы поймать этот момент, стоит проверять регистр CR1 на включенные прерывание на совпадение адреса, прерывание на ошибки и включенную периферию
Конечно – это, всего лишь, пример. Использовать стоит аккуратно
void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode){ UNUSED(AddrMatchCode); // Если мастер пишет, слушаем необходимое количество байт if(TransferDirection == I2C_DIRECTION_TRANSMIT){ // Первый запрос на запись всегда составляет 1 байт запрошенного регистрового адреса. // Сохранить его в I2C_slave_obj.reg_address if(!I2C_slave_obj.reg_addr_rcvd) HAL_I2C_Slave_Sequential_Receive_IT(hi2c, &I2C_slave_obj.reg_address, 1, I2C_FIRST_FRAME); } else { // Если мастер отправляет запрос на чтение, вернуть значение данных в запрошенном регистре. I2C_slave_obj.curr_idx = reg_get_idx(I2C_slave_obj.reg_address); if ((I2C_slave_obj.curr_idx != NONE)&& (I2C_slave_obj.curr_idx != ECHO)&& (g_i2c_reg_data[I2C_slave_obj.curr_idx].access != WRITE_ONLY)){ HAL_I2C_Slave_Sequential_Transmit_IT(hi2c, (uint8_t*)&g_i2c_reg_data[I2C_slave_obj.curr_idx].value.uint16_val, reg_get_len(I2C_slave_obj.curr_idx), I2C_LAST_FRAME); } } } void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c){ // Это вызывается после основного запроса на запись. в первый раз это будет адрес регистра. // Второй раз, если это запрос на запись, это будет полезная нагрузка if(!I2C_slave_obj.reg_addr_rcvd){ // Если reg_addr_rcvd имеет значение false, это означает, что мастер ждет данные I2C_slave_obj.reg_addr_rcvd = 1; I2C_slave_obj.curr_idx = reg_get_idx(I2C_slave_obj.reg_address); if ((I2C_slave_obj.curr_idx != NONE)&& (I2C_slave_obj.curr_idx != ECHO)&& (g_i2c_reg_data[I2C_slave_obj.curr_idx].access != READ_ONLY)){ HAL_I2C_Slave_Sequential_Receive_IT(hi2c, (uint8_t*)&g_i2c_reg_data[I2C_slave_obj.curr_idx].value.uint16_val, reg_get_len(I2C_slave_obj.curr_idx), I2C_NEXT_FRAME); } } else { // Если reg_addr_rcvd установлен, это означает, что этот обратный вызов был возвращен после получения данных регистра I2C_slave_obj.reg_addr_rcvd = 0; //добавим быструю обработку protocol_reg_ctrl(I2C_slave_obj.curr_idx); I2C_slave_obj.curr_idx = NONE; } HAL_I2C_EnableListen_IT(hi2c); }
Обновленная версия программы на гите:
Comment