Перейти к содержанию

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

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

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

В общем, я достиг комедии дна. Мне понравилось, как работало на 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

programel