STM32 и интерфейс LIN. Настройка и обмен данными. STM32CubeMx.

Приветствую всех на нашем сайте! Выходит в свет вторая часть из обещанных трех, посвященных работе с интерфейсом LIN. Сегодня вооружимся микроконтроллером STM32 и реализуем связь по LIN в разных режимах работы с использованием аппаратных средств. Напоминаю, что в третьей статье будем писать свой драйвер LIN с нуля на голом UART’е!

Что ж, подготовительный этап будет протекать как обычно – открываем STM32CubeMx и настраиваем нужную периферию. Для этого проекта мы будем использовать два модуля USART, настроенных на работу в режиме LIN, соединенных между собой. Вообще, строго говоря, подключение необходимо выполнять через микросхемы LIN-трансиверов:

Шина LIN.

Но у меня сейчас, к сожалению, нет под рукой TJA1021, так что соединяем модули USART просто напрямую. На процесс написания программы это никак не повлияет и на работоспособности не отразится 🙂

Схема для теста LIN.

USART1 и USART2 настраиваем абсолютно одинаково, не забываем включить прерывания:

Настройка LIN в STM32CubeMx.
USART прерывания в STM32CubeMx.

Кроме того, как обычно, конфигурируем тактирование микроконтроллера:

Настройки тактирования STM32.

На этом взаимодействие с Cube закончено, генерируем проект и открываем его для дальнейшей работы!

Итак, в чем же заключается аппаратная поддержка LIN в STM32. На самом деле, функционал фактически минимальный. Для Master’а есть возможность аппаратно сгенерировать поле Break, используя функцию:

HAL_LIN_SendBreak(UART_HandleTypeDef *huart)

Для Slave есть возможность отловить все тот же брейк в прерывании. В общем-то вот и все 🙂

Давайте чуть подробнее разберем механизм обнаружения поля Break подчиненным устройством. Стоит отметить, что в HAL Driver по какой-то причине решили не делать никаких функций для этого, поэтому будем работать “руками”. В общем, то нам нужно будет всего лишь включить прерывание по обнаружению брейка:

__HAL_UART_ENABLE_IT(&huart2, UART_IT_LBD);

В данном случае у меня использован USART2.

И далее в прерывании, после стандартной обработки прерывания HAL’ом отлавливаем событие обнаружения брейка:

uint32_t isrflags = huart2.Instance->SR;

if ((isrflags & USART_SR_LBD) != RESET)
{
	CLEAR_BIT(huart2.Instance->SR, USART_SR_LBD);
	
	// .....
}

Все, на этом аппаратные возможности заканчиваются, все остальное мы сейчас сделаем сами!

На USART1 у нас будет Master, соответственно, на USART2 – Slave. И начнем с более сложного, а именно с кода подчиненного устройства. Но прежде всего прочего нужно обсудить, что мы вообще собираемся делать 🙂

Итак, реализуем два режима работы:

  • Master отправляет заголовок (header) пакета с определенным PID, после чего Slave переходит в режим приема данных.
  • Master отправляет заголовок с идентификатором, который сигнализирует Slave’у о том, что надо ответить ведущему порцией данных. Соответственно, Master после отправки заголовка должен встать на прием.

Пусть значения PID для примера будут такими:

  • 0x3A – соответствует первому случаю, то есть Master отправляет заголовок и данные.
  • 0x3B – это уже второй случай из перечисленных.

Важное дополнение – как вы помните, байт PID помимо 6-ти битов идентификатора включает в себя еще два бита четности. Для примера же будем просто использовать вышеупомянутые значения, без учета четности.

Добавляем в наш проект эти значения и пару переменных (ссылку на полный проект я обязательно добавлю в конце статьи):

#define LIN_TX_ID                                                         0x3A
#define LIN_RX_ID                                                         0x3B
uint8_t linTxId = LIN_TX_ID;
uint8_t linRxId = LIN_RX_ID;

Кроме того, объявляем массивы для хранения данных ведущего и подчиненного:

#define LIN_DATA_BYTES_NUM                                                9
uint8_t linMasterData[LIN_DATA_BYTES_NUM];
uint8_t linSlaveData[LIN_DATA_BYTES_NUM];

В проекте все находится в разных файлах, здесь же для наглядности я буду комбинировать код чуть иначе. Принимать и отправлять мы будем по 8 байт, то есть максимально возможное количество байт в одном фрейме. Но обратите внимание, что в массивах по 9 байт – еще 1 байт мы выделили под прием и отправку контрольной суммы.

И раз уж об этом зашла речь, то вот функция для расчета контрольной суммы (будем использовать классический алгоритм):

uint8_t LIN_CalcCheckSum(uint8_t *data, uint8_t len)
{
	uint16_t sum = 0;
	
	for (uint8_t i = 0; i < len; i++)
	{
		sum += data[i];
	}
  
	while(sum > 0xFF)
	{
		sum -= 0xFF;
	}
  
	sum = 0xFF - sum;
  
	return (uint8_t)sum;
}

И для систематизации работы программы добавим возможные режимы, в которых может находиться Slave:

typedef enum
{
	LIN_RECEIVING_BREAK     = 0x01,
	LIN_RECEIVING_SYNC      = 0x02,
	LIN_RECEIVING_ID        = 0x03,
	LIN_RECEIVING_DATA      = 0x04,
	LIN_SENDING_DATA        = 0x05,
} LIN_State;

Суть тут ясна из названий, так что даже не будем останавливаться на этом отдельно 🙂

Итак, изначально Slave у нас готов к приему данных и находится в состоянии ожидания Break’а:

LIN_State slaveState = LIN_RECEIVING_BREAK;

Код приема брейка, как мы уже обсудили, поместим в обработчик прерывания:

/**
	* @brief This function handles USART2 global interrupt.
	*/
void USART2_IRQHandler(void)
{
	/* USER CODE BEGIN USART2_IRQn 0 */

	/* USER CODE END USART2_IRQn 0 */
	HAL_UART_IRQHandler(&huart2);
	/* USER CODE BEGIN USART2_IRQn 1 */

	if (slaveState == LIN_RECEIVING_BREAK)
	{
		uint32_t isrflags = huart2.Instance->SR;
    
		if ((isrflags & USART_SR_LBD) != RESET)
		{
			CLEAR_BIT(huart2.Instance->SR, USART_SR_LBD);
			uint16_t temp = huart2.Instance->DR;
			slaveState = LIN_RECEIVING_SYNC;
			HAL_UART_Receive_IT(&huart2, &rxByte, 1);
		}
	}
        
  /* USER CODE END USART2_IRQn 1 */
}

Первым делом, проверяем, что Slave находится в ожидании поля Break, затем проверяем, не вызвано ли прерывание как раз-таки приемом этого поля, и, если да, то переводим Slave в режим ожидания поля Sync. Тут есть небольшой нюанс в виде чтения регистра данных USART’а, это нужно для корректного приема последующих байт:

uint16_t temp = huart2.Instance->DR;

Вся остальная логика у нас будет в callback-функциях по окончанию приема и передачи. Начинаем с приема и двигаемся по всем возможным состояниям Slave устройства:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if (huart == &huart2)
	{
		uint8_t checkSum = 0;

		switch(slaveState)
		{
			case LIN_RECEIVING_SYNC:
				if (rxByte == LIN_SYNC_BYTE)
				{
					slaveState = LIN_RECEIVING_ID;
					HAL_UART_Receive_IT(&huart2, &rxByte, 1);
				}
				else
				{
					slaveState = LIN_RECEIVING_BREAK;
				}
				break;
        
			default:
				break;
		}
	}
}

Проверяем принятый байт, если значение верное – 0x55 – встаем на прием PID, иначе возвращаемся в исходную точку. Добавляем ветку для приема идентификатора:

case LIN_RECEIVING_ID:
	if (rxByte == LIN_RX_ID)
	{
		slaveState = LIN_SENDING_DATA;

		for (uint8_t i = 0; i < LIN_DATA_BYTES_NUM - 1; i++)
		{
			linSlaveData[i] = 0x30 + i;
		}

		linSlaveData[LIN_DATA_BYTES_NUM - 1] = LIN_CalcCheckSum(linSlaveData, LIN_DATA_BYTES_NUM - 1);

		HAL_UART_Transmit_IT(&huart2, linSlaveData, LIN_DATA_BYTES_NUM);
	}
	else
	{
		if (rxByte == LIN_TX_ID)
		{
			slaveState = LIN_RECEIVING_DATA;
			HAL_UART_Receive_IT(&huart2, linSlaveData, LIN_DATA_BYTES_NUM);
		}
		else
		{
			slaveState = LIN_RECEIVING_BREAK;
		}
	}
	break;

Здесь все чуть сложнее, но, в принципе, тоже довольно прозрачно 🙂 Проверяем PID и по его значению определяем, что нам следует делать в дальнейшем. И тут, как вы помните, два варианта:

  • Ожидать приема байт данных от Master’а
  • Или отправить свою порцию данных

Для случая передачи данных Slave’ом заполняем массив тестовыми значениями и дополняем его 9-м байтом – контрольной суммой. А если PID не соответствует тем 2-м значениям, которые мы определили для этого примера, то возвращаем устройство в начальное положение, а именно в состояние ожидания брейка.

И, наконец, последняя часть callback-а по приему:

case LIN_RECEIVING_DATA:
	checkSum = LIN_CalcCheckSum(linSlaveData, LIN_DATA_BYTES_NUM - 1);
        
	if (linSlaveData[LIN_DATA_BYTES_NUM - 1] == checkSum)
	{
		linSlaveRxCnt++;
	}
        
	slaveState = LIN_RECEIVING_BREAK;          
	break;

Как мы обсудили ранее, принимаем 9 байт, то есть 8 байт данных и байт контрольной суммы. И когда все данные приняты – рассчитываем контрольную сумму по первым 8-ми байтам и сравниваем ее значение с 9-ым байтом массива данных. В случае успеха инкрементируем переменную linSlaveRxCnt. Этот счетчик будет для нас сигналом успешно принятых данных.

Но в зависимости от принятого идентификатора Slave мог также перейти в режим передачи данных, поэтому этот случай нам также нужно обработать. И делаем мы это в коде callback-функции по окончанию передачи данных:

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
	if (huart == &huart2)
	{
		switch(slaveState)
		{        
			case LIN_SENDING_DATA:
				linSlaveTxCnt++;
				slaveState = LIN_RECEIVING_BREAK;
				break;
        
			default:
				break;
		}
	}
}

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

При работе Master’а в режиме отправки данных Slave’у:

for (uint8_t i = 0; i < LIN_DATA_BYTES_NUM - 1; i++)
{
	linMasterData[i] = 0x40 + i;
}
      
HAL_LIN_SendBreak(&huart1);

HAL_UART_Transmit(&huart1, &linSync, 1, 10);
HAL_UART_Transmit(&huart1, &linTxId, 1, 10);

linMasterData[LIN_DATA_BYTES_NUM - 1] = LIN_CalcCheckSum(linMasterData, LIN_DATA_BYTES_NUM - 1);

HAL_UART_Transmit(&huart1, linMasterData, LIN_DATA_BYTES_NUM, 10);

linMasterTxCnt++;
HAL_Delay(1000);

Заполняем данные тестовыми значениями и последовательно отправляем:

  • Break
  • Sync
  • PID
  • Данные
  • Контрольную сумму

Все четко по формату пакета интерфейса LIN. Для примера я сделал отправку раз в секунду.

В режиме, когда Master отправляет в шину заголовок и ожидает данные от подчиненного все практически идентично:

for (uint8_t i = 0; i < LIN_DATA_BYTES_NUM; i++)
{
	linMasterData[i] = 0x00;
}
        
HAL_LIN_SendBreak(&huart1);

HAL_UART_Transmit(&huart1, &linSync, 1, 10);
HAL_UART_Transmit(&huart1, &linRxId, 1, 10);

HAL_UART_Receive(&huart1, linMasterData, LIN_DATA_BYTES_NUM, 10);

uint8_t checkSum = LIN_CalcCheckSum(linMasterData, LIN_DATA_BYTES_NUM - 1);

if (linMasterData[LIN_DATA_BYTES_NUM - 1] == checkSum)
{
  linMasterRxCnt++;
}

HAL_Delay(1000);

Разница только в том, что после отправки PID устройство встает на прием данных, а не начинает передачу, плюс по окончанию приема проверяем контрольную сумму. Для индикации работоспособности здесь также будем пользоваться обычными счетчиками пакетов linMasterRxCnt и linMasterTxCnt.

Переключение между режимами работы в проекте осуществляется переменной linMasterTask. Master отправляет:

uint8_t linMasterTask = LIN_MASTER_TX;

Master принимает:

uint8_t linMasterTask = LIN_MASTER_RX;

Собираем, прошиваем, запускаем, проверяем!

Для случая, когда ведущий отправляет данные подчиненному смотрим на счетчики linMasterTxCnt и linSlaveRxCnt. А также на значения в массивах linMasterData и linSlaveData. Под отладчиком можно наблюдать как счетчики параллельно инкрементируются, то есть отправляемые пакеты успешно принимаются:

Программа для передачи по интерфейсу LIN.

Аналогично и в режиме, когда Master получает данные от Slave:

Программа для приема по интерфейсу LIN.

Здесь уже вступают в игру счетчики linMasterRxCnt и linSlaveTxCnt. Ну и кроме того, значения в массивах принимаемых и отправляемых данных в точности совпадают!

И на этой позитивной ноте заканчиваем сегодняшнюю статью, спасибо за внимание! При возникновении любых вопросов, смело пишите в комментарии, во всем разберемся!

Ссылка на проект – MT_LIN_HW_Example.

Поделиться!

Подписаться
Уведомление о
guest
0 Комментарий
Inline Feedbacks
View all comments

Присоединяйтесь!

Profile Profile Profile Profile Profile
Vkontakte
Twitter

Язык сайта

Август 2020
Пн Вт Ср Чт Пт Сб Вс
 12
3456789
10111213141516
17181920212223
24252627282930
31  

© 2013-2020 MicroTechnics.ru