Найти тему
ProgramEl

I2C Slave устройство на STM32. Сегодня с регистрами

Вот что действительно было тяжело найти, так это адекватный пример реализации i2c подчиненного устройства на STM32 с использованием прерываний. Нашлись несколько примеров блокирующих реализаций и пара вопросов на st-community.

А вот прям более-менее рабочий проект, где есть реализация нескольких регистров ведомого устройства, да еще и с разным уровнем доступа - такого очень не хватало. Буквально, чуть:

В общем, я достиг комедии дна. Мне понравилось, как работало на mbed OS, поэтому, я полез в исходники (https://github.com/ARMmbed/mbed-os и https://github.com/ARMmbed/mbed-hal). И начал грустно вычитывать код и размашисто резать лишние зависимости.

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

В общем-то и оставим эту логику. Вполне работает.

Что осталось-то? В итоге в мэйне у нас есть пара функций инициализации:

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

Работает

Ткнувшись логическим анализатором, на линии увидел ожидаемую каринку.

-2

В общем, получился небольшой пример, более-менее универсальный для STM32-устройств. Данный проект сделан для самой популярной на Алиэкспресс платы-отладки Nucleo-F303RE

Проверено на STM32L433, STM32F302R8

Функция для контроля таймаута БИТА(в i2c_slave.c) пока очень плоха, адаптируйте под себя или ждите обновление

Ссылка на GitHub:

GitHub - tunerok/stm32_i2c_slave_examlpe: An example of the implementation of an I2C slave device on STM32 with work through interruptions