
Вот что действительно было тяжело найти, так это адекватный пример реализации 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