Драйвер интерфейса LIN для микроконтроллеров на базе UART.

Встречайте третью часть из серии статей, посвященных интерфейсу LIN! Предыдущие части доступны по ссылкам:

И в этой статье мы займемся ровно тем, чем и планировали, а именно напишем свой драйвер для обмена данными по LIN. Базой послужит обычный модуль UART микроконтроллера STM32. Но при желании можно будет легко портировать драйвер на любой другой микроконтроллер, внеся незначительные изменения. Никаких аппаратных возможностей именно STM32 для интерфейса LIN мы использовать не будем, все по-честному 🙂

Итак физически драйвер по классике будет состоять из двух файлов – заголовочного и файла с исходным кодом:

  • lin.c
  • lin.h

Цель поставим себе такую – сделать использование LIN максимально простым, то есть вызвали одну функцию отправки/приема – получили результат. Вся остальная работа будет скрыта внутри драйвера.

Как и во второй статье цикла отправной точкой будут служить возможные состояния устройства при работе в качестве Master’а или Slave’а. Только здесь список состояний будет расширенным относительно нашего первого примера:

typedef enum
{
	LIN_IDLE,
	LIN_RECEIVING_BREAK,
	LIN_RECEIVING_SYNC,
	LIN_RECEIVING_ID,
	LIN_RECEIVING_DATA,
	LIN_RECEIVING_CHECKSUM,
	LIN_SENDING_BREAK,
	LIN_SENDING_SYNC,
	LIN_SENDING_ID,
	LIN_SENDING_DATA,
	LIN_SENDING_CHECKSUM,
} LIN_State;

Без лишних слов переходим сразу к делу! И первое на очереди – это генерация (в случае ведущего устройства) и детектирование (в случае подчиненного) поля Break. Для этой задачи мы задействуем один из таймеров – выбрать можно абсолютно любой. Кроме того, само собой, необходимо выделить один из модулей USART для работы с LIN. Настраиваем все вышеперечисленное в STM32CubeMx:

Настройка USART в STM32CubeMx.
Настройка таймера в STM32CubeMx.

Таймер у меня будет генерировать прерывание по переполнению каждые 25 мкс.

В программе объявим указатели для хранения данных обо всех выбранных периферийных модулях, то есть об USART’е, таймере и портах, которые будут работать в качестве сигналов Rx и Tx этого USART’а:

static UART_HandleTypeDef *uartHandle;
static TIM_HandleTypeDef *timerHandle;

static GPIO_TypeDef *rxPort;
static GPIO_TypeDef *txPort;

static uint32_t rxPin;
static uint32_t txPin;

Да, кстати, конечно же, по устоявшейся традиции полный проект я прикреплю в конце статьи 🙂 При необходимости использования LIN в своих разработках можно не вникать в работу драйвера, а просто добавить к себе и использовать!

Продолжаем… Все эти указатели нужны для того, чтобы при изменении конкретной периферии, то есть перехода, например, с таймера TIM4 на TIM3 правки необходимо было произвести только в одном месте. А именно в функции инициализации LIN, вот, собственно, ее код:

void LIN_Init(UART_HandleTypeDef *uHandle, TIM_HandleTypeDef *tHandle, LIN_Mode mode,
              GPIO_TypeDef *rPort, uint32_t rPin, GPIO_TypeDef *tPort, uint32_t tPin)
{  
	uartHandle = uHandle;
	timerHandle = tHandle;
	rxPort = rPort;
	rxPin = rPin;
	txPort = tPort;
	txPin = tPin;
  
	curMode = mode;
	curState = LIN_IDLE;
	curBaudrate = uartHandle->Init.BaudRate;

	HAL_TIM_Base_Start_IT(timerHandle);

	breakCntLimit = (1000000 * LIN_BREAK_SIZE_BITS / curBaudrate) / LIN_TIMER_PERIOD_US;
	breakCntUpperLimit = breakCntLimit + breakCntLimit * LIN_BREAK_DEVIATION_PERCENT / 100;
	breakCntLowerLimit = breakCntLimit - breakCntLimit * LIN_BREAK_DEVIATION_PERCENT / 100;

	isInit = 1;
}

В демо-примере для проверки работы драйвера у меня выбраны USART2, TIM4 и устройство работает в режиме Slave:

LIN_Init(&huart2, &htim4, LIN_SLAVE, GPIOA, GPIO_PIN_3, GPIOA, GPIO_PIN_2);

Кроме прочего, здесь еще выбираются ножки контроллера, относящиеся к USART’у. Концепция точно такая же – при изменении выводов нужно будет менять программу только в одном месте, а не искать обращения к периферии везде по коду драйвера.

Как видите, при инициализации мы также рассчитываем значения нескольких переменных:

  • breakCntLimit – это длительность поля Break, приведенная к периоду нашего таймера. В прерывании по переполнению таймера при генерации Break мы будем увеличивать счетчик. Соответственно, переменная breakCntLimit показывает, сколько раз должен переполниться таймер за время брейка.
  • breakCntUpperLimit и breakCntLowerLimit – это верхний и нижний порог для обнаружения Break. Если счетчик попадает в этот интервал, то мы считаем, что брейк обнаружен.

В дефайны вынесены некоторые конфигурационные параметры:

#define LIN_TIMER_PERIOD_US                                             25

#define LIN_BREAK_SIZE_BITS                                             13
#define LIN_BREAK_DEVIATION_PERCENT                                     10
  • LIN_TIMER_PERIOD_US – здесь мы указываем выбранный нами ранее период переполнения таймера.
  • LIN_BREAK_SIZE_BITS – размер поля Break в битах.
  • LIN_BREAK_DEVIATION_PERCENT – задает окно для обнаружения брейка относительно идеальной длительности, равной breakCntLimit. В нашем случае получаем ±10%.

Итак, давайте разберемся, как мы будем генерировать и отлавливать Break… С обнаружением все, в целом, несложно – если у нас Slave находится в режиме LIN_RECEIVING_BREAK, то в прерывании по таймеру проверяем уровень сигнала на ножке Rx:

uint8_t LIN_ReadRxPortState()
{
	uint8_t res = HAL_GPIO_ReadPin(rxPort, rxPin);
	return res;
}

Если точнее, то это происходит в функции LIN_TimerProcess(), которую мы вызываем из callback’а по переполнению. Если на ножке 0, то начинаем увеличивать счетчик breakCnt. Когда на Rx появляется единица – проверяем, что значение счетчика попадает в нужный нам интервал:

case LIN_RECEIVING_BREAK:
	rxPortState = LIN_ReadRxPortState();
    
	if (rxPortState == 0)
	{
		breakCnt++;
	}
	else
	{
		if (breakCnt != 0)
		{
			if ((breakCnt <= breakCntUpperLimit) && (breakCnt >= breakCntLowerLimit))
			{
				curState = LIN_RECEIVING_SYNC;
				uint16_t temp = uartHandle->Instance->DR;
				HAL_UART_Receive_IT(uartHandle, &rxByte, 1);
			}
          
			breakCnt = 0;
		}
	}
	break;

При отправке брейка задача усложняется. Нам нужно переконфигурировать Tx USART’а на работу в режиме обычного порта ввода-вывода, а затем, после отправки Break, вернуть все на круги своя:

void LIN_WriteTxPortState(uint8_t state)
{  
	GPIO_InitTypeDef GPIO_InitStruct = {0};
  
	if (state == 0)
	{
		HAL_GPIO_DeInit(txPort, txPin);
    
		GPIO_InitStruct.Pin = txPin;
		GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
		GPIO_InitStruct.Pull = GPIO_NOPULL;
		GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
		HAL_GPIO_Init(txPort, &GPIO_InitStruct);

		HAL_GPIO_WritePin(txPort, txPin, GPIO_PIN_RESET);
	}
	else
	{
		HAL_GPIO_WritePin(txPort, txPin, GPIO_PIN_SET);

		HAL_GPIO_DeInit(txPort, txPin);

		GPIO_InitStruct.Pin = txPin;
		GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
		GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
		HAL_GPIO_Init(txPort, &GPIO_InitStruct);
	}
}

И снова возвращаемся в обработчик прерывания таймера:

case LIN_SENDING_BREAK:
	if (breakCnt == 0)
	{
		LIN_WriteTxPortState(0);
		breakCnt++;
	}
	else
	{          
		if (breakCnt == breakCntLimit)
		{
			breakCnt = 0;
			LIN_WriteTxPortState(1);
			curState = LIN_SENDING_SYNC;
			HAL_UART_Transmit_IT(uartHandle, &syncByte, 1);
		}
		else
		{          
			breakCnt++;
		}
	}
	break;

Обратите внимание, что здесь нам нет необходимости использовать диапазон допустимых значений длительности брейка. Просто сравниваем с идеальным рассчитанным значением breakCntLimit. Это вытекает из того, что при приеме длительность может плавать в зависимости от разных факторов, а при генерации брейка мы четко инкрементируем счетчик и выдаем нужную длительность.

В прерывании таймера у нас решается еще одна задача, а именно проверка на потерю части LIN пакета:

case LIN_RECEIVING_ID:
case LIN_RECEIVING_DATA:
case LIN_RECEIVING_SYNC:
	rxTimeoutCnt++;

	if (rxTimeoutCnt >= rxTimeoutCntLimit)
	{
		HAL_UART_AbortReceive(uartHandle);
		curState = LIN_IDLE;
	}

	break;

Когда устройство находится в ожидании принятых данных (идентификатора, байта данных или поля синхронизации), начинаем увеличивать счетчик rxTimeoutCnt. Обнуляется этот счетчик в callback’е по окончанию приема данных по USART. Соответственно, если данные так и не пришли, то rxTimeoutCnt становится равен rxTimeoutCntLimit и мы обрываем прием и переходим в состояние ожидания. Значения порога в микросекундах задается так:

#define LIN_RX_TIMEOUT_US                                               100000

Перемещаемся в функцию LIN_UartProcess(), вызывается она из callback-ов по окончанию приема и передачи данных по USART. И в ней мы, соответственно, обрабатываем поочередно все состояния LIN-устройства. Многое кстати похоже на то, что мы делали в предыдущей статье по интерфейсу LIN 🙂

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

И для этого у нас есть ряд функций. Во первых, функция инициализации, с которой мы уже сталкивались:

void LIN_Init(UART_HandleTypeDef *uHandle, TIM_HandleTypeDef *tHandle, LIN_Mode mode, GPIO_TypeDef *rPort, uint32_t rPin, GPIO_TypeDef *tPort, uint32_t tPin);

Далее идет функция для организации процесса передачи данных:

LIN_Error LIN_Transmit(uint32_t id, uint8_t *ptr, uint8_t len);

Возможные возвращаемые значения выглядят следующим образом:

typedef enum {
	LIN_OK,
	LIN_BUSY,
	LIN_NOT_INIT,
} LIN_Error;

В качестве аргументов мы передаем PID, указатель на данные и количество байт для передачи. Контрольную сумму здесь не учитываем отдельным байтом и не добавляем в отправляемые данные, все произойдет автоматически. Чтобы отследить окончание передачи, можно использовать функцию:

LIN_State LIN_GetState();

Возвращаемое значение соответствует состоянию устройства. По окончанию передачи произойдет переход в режим ожидания – LIN_IDLE, что и можно отследить.

При использовании этой функции для Master’а в линию будут отправлены последовательно:

Передача данных по LIN.

Поскольку Slave в LIN не может сам инициировать обмен данными, то для подчиненного эту функцию имеет смысл вызывать только из callback’а по приему PID (в том случае, если из значения идентификатора Slave понимает, что необходимо выслать ведущему данные).

Если мы, будучи ведущим устройством, хотим получить данные от Slave, то для этого припасена функция:

LIN_Error LIN_Receive(uint32_t id, uint8_t *ptr, uint8_t len);

Возвращаемое значение и аргументы имеют точно такой же смысл как и для передачи. Аналогично, для Slave вызов этой функции должен происходить после приема идентификатора. Master же при вызове этой функции выдает в шину поля Break, Sync и PID и встает на прием данных:

Интерфейс LIN. Прием данных.

Для Slave’а все процессы работают несколько иначе – никаких функций вызывать не нужно. Ведомый изначально переходит в состояние приема LIN-фрейма, а точнее в режим ожидания Break. И далее вся работа уже протекает по факту приема данных от Master’а. Для работы со Slave’ом я добавил два callback’а:

  • LIN_SlaveIdRxCallback(uint8_t id) – по приему PID – для анализа полученного значения и принятия решения о дальнейших действиях. Ведь именно из значения PID подчиненный понимает, нужно передавать данные или принимать, а также количество байт данных.
  • LIN_RxCpltCallback() – по окончанию приема данных. Этот callback нужен уже непосредственно для обработки принятых данных, при этом контрольную сумму проверять не нужно, это происходит внутри драйвера. И callback будет вызван только в том случае, если контрольная сумма верна.

Работа с этими функциями протекает точно также, как и с другими callback-ами. Просто переопределяем эти функции в своем коде и добавляем в них любые необходимые действия.

Ну вот и разобрались 🙂 Давайте организуем демо-проект для проверки возможностей и работоспособности драйвера. И для тестирования добавим модуль USART1, настроенный на работу в режиме LIN, как в этом примере.

В итоге на USART1 с аппаратной поддержкой LIN у нас будет Master. А на USART2 у нас будет Slave, который будет работать исключительно на голом USART’е при помощи нашего драйвера. Ну и, соответственно, соединяем эти модули USART между собой.

Первый тест – Master передает данные, а Slave принимает. Код ведущего у нас в цикле while(1):

// Master transmitter section

HAL_LIN_SendBreak(&huart1);

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

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

uint8_t checkSum = LIN_CalcCheckSum(linMasterTxData, LIN_DATA_BYTES_NUM);
HAL_UART_Transmit(&huart1, &checkSum, 1, 10);

linMasterTxCnt++;

HAL_Delay(1000);

// End of master transmitter section

Для передачи данных Master’ом у нас используется PID 0x3A, а для запроса и приема данных от Slave – 0x3B.

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

Добавляем наш пользовательский код в callback по приему PID:

void LIN_SlaveIdRxCallback(uint8_t id)
{  
	if (id == LIN_RX_ID)
	{
		uint8_t txBytes = LIN_GetDataLength(id);    
		LIN_Transmit(0, linSlaveTxData, txBytes);
	}
	else
	{
		if (id == LIN_TX_ID)
		{
			uint8_t rxBytes = LIN_GetDataLength(id);
			LIN_Receive(0, linSlaveRxData, rxBytes);
		}
		else
		{
			LIN_Reset();
		}
	}
}

Анализируем принятый PID и решаем, принимаем или передаем. Кстати, опять же из идентификатора мы получаем количество байт для приема/передачи при помощи функции LIN_GetDataLength(id). Кстати, заметьте, для Slave’а не важно, какое значение идентификатора мы передадим в функции приема и передачи, поскольку за выдачу в линию PID отвечает Master. Так что можем спокойно в качестве первого аргумента использовать 0.

И также переопределяем callback по окончанию передачи:

void LIN_RxCpltCallback()
{
	linSlaveRxCnt++;
}

Здесь просто инкрементируем счетчик принятых фреймов. Запускаем программу и под отладчиком можем видеть, что счетчик переданных Master’ом пакетов (linMasterTxCnt) в точности соответствует счетчику пакетов, принятых Slave’ом:

Программа для передачи ведущим.

А теперь, второй тестовый режим! Master отправляет заголовок пакета и начинает прием данных от Slave’а:

// Master receiver section

HAL_LIN_SendBreak(&huart1);

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

linMasterRequestCnt++;

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

uint8_t rxCheckSum = 0;
HAL_UART_Receive(&huart1, &rxCheckSum, 1, 10);

uint8_t calcCheckSum = LIN_CalcCheckSum(linMasterRxData, LIN_DATA_BYTES_NUM);

if (rxCheckSum == calcCheckSum)
{
  linMasterRxCnt++;
}

HAL_Delay(1000);

// End of master receiver section

Slave же про приему этого идентификатора начинает отправку своих данных. Для проверки у нас используются счетчик запросов ведущего linMasterRequestCnt и счетчик принятых, опять же ведущим, пакетов данных – linMasterRxCnt:

Программа для передачи подчиненным.

Все работает по плану! Таким образом, можно просто добавить в свой проект драйвер LIN’а и использовать те функции, которые мы обсудили, для того, чтобы быстро и просто организовать работу по интерфейсу LIN.

А теперь под занавес статьи, реализуем Master’а на нашем драйвере и поставим его на отправку данных раз в секунду. Кода минимум, раз:

LIN_Init(&huart2, &htim4, LIN_MASTER, GPIOA, GPIO_PIN_3, GPIOA, GPIO_PIN_2);

И два:

LIN_Transmit(linTxId, linMasterTxData, 8);

Вот и все! А теперь посмотрим на сигнал на экране осциллографа:

Осциллограмма LIN.

Все в точности соответствует теоретическим аспектам работы интерфейса LIN.

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

Проект можно скачать по ссылке – MT_LIN_SW_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