На всякий случай, а то вдруг санкции применят (смаил). Описываемый случай не имеет никакого отношения к реальности и является целиком и полностью выдумкой автора

Раньше было про потоки, семафоры, очереди и HAL

Как-то раз попросили меня посмотреть на одно очень дорогостоящее устройство. Проблема была одна: среди использующих это устройство возникло стойкое убеждение, что 99,99% его цены происходит от того факта, что производитель этого устройства монополист в своей сфере и деваться пользователям этого устройства некуда.



Вооружившись осциллографом, я полез внутрь.

Через некоторое время поиски привели к двум проводкам, которые были в жгуте, соединявшем блоки устройства. Осциллограмма показала, что в проводках почти обычный USART. Почти — потому что «туда» данные бежали на скорости 9600, а обратно на 115200.

Достал два usb-usart переходника, прицепил их входы к RX/TX и начал анализировать протокол. Задача осложнялась разными скоростями, асинхронностью и бинарностью протокола. В общем, через некоторое время я обнаружил, что у меня банально разъезжаются глаза, пытаясь отследить в терминалах, что за чем идет и что на что реагирует.

Что бы сберечь вам зрение и рассудок в аналогичной ситуации, давайте сделаем аппаратный сниффер usart. Причем сделаем его так, что бы ему было все равно на скорости и последовательности передач. Ну а разобравшись с usart, вы потом сможете добавить анализ ножек и всяких SPI одновременно.

Работа сниффера будет простая: у STM32 куча встроенных USART. Пусть 2 их них слушают свои RX ножки и потом через USB выдают их наружу. А мы эти ножки прицепим к RX/TX изучаемого прибора.

Для начала берем уже (надеюсь) привычную плату STM32F3DISCOVERY и с помощью STM32Cube создаем заготовку. В этой заготовке должно быть следующее

5 потоков.

defaultTask — будет мигать светодиодиком «устройство работает и не повисло»
Usart1Rx — будет заниматься приемом данных на 1м порту
Usart2Rx — то же самое, то на 2м
UsbSend — тут будут обрабатываться и посылаться данные наружу
Usart3Tx — а это будет тестовый поток, который будет рассылать цитаты знаменитых роботов. Используем для проверки работоспособности первых двух портов «на вход». Ну или для баловства.

И одну очередь — в которую будут складываться посылки от «приемщиков» данных. Дабы не заморачиваться, я сделал такую структуру

typedef struct {
	uint8_t usart;
	uint8_t byte;
} myMes;


Для начала напишем самую важную функцию — посылку тестовых данных.

for(;;)
{
        // Wall-e: Eeeee... va? 
	uint8_t walle[]="Eeeee... va?\r\n";
	// Short Circuit: Johny Five is Alive!
	uint8_t johny[]="Johny Five is Alive! \r\n";
	// StarWars 
	uint8_t c3po[]="Sir, the possibility of successfully navigating an asteroid field is approximately 3,720 to 1 \r\n";
	HAL_UART_Transmit(&huart3,(uint8_t *)&walle,15,16); //PB10
	HAL_UART_Transmit(&huart2,(uint8_t *)&johny,23,100); //PA2
	HAL_UART_Transmit(&huart1,(uint8_t *)&c3po,97,100);  //PC4
        osDelay(1000);
	HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_15);
}


И проверим ее, просто поочередно зацепившись внешним usart-usb переходником за подходящие ножки. Это по крайней мере даст понимание того, чего ожидать на «приеме», если мы просто соединим ножки RX-TX прямо на плате.



Теперь перейдем к функции, которая принимает и оформляет полученное.

xQueueReceive( RecQHandle, &w, portMAX_DELAY );
if(oldusart!=w.usart || char_count>16)
	{
	oldusart=w.usart;
	newline=1;
	char_count=0;
	}
buf[char_count++]=w.byte;

Из предыдущих статей и знания языка С становится понятно, что просто ждем очередной «посылки» и в зависимости от условий выставляем флаги.

Ну и дальше ждем, пока USB интерфейс будет готов и просто формируем (признаюсь, можно аккуратней) строку и выводим ее пользователю. Расписывать тут я ее не стал, ибо там работа с буфером и строками «в лоб», без каких-либо оптимизаций.

Теперь необходимо написать то, чем мы будем принимать данные.

void Usart1Rx(void const * argument)
{
  /* USER CODE BEGIN Usart1Rx */
  /* Infinite loop */
  for(;;)
  {
		if(HAL_UART_Receive_IT(&huart1, &b1,1)==HAL_OK)
		{
			HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_9);
		}
	osDelay(1);
  }
  /* USER CODE END Usart1Rx */
}

Usart2Rx абсолютно аналогична. Если что, то в реальном коде я оставил комментарии для пытливых пользователей.

И наконец, обработчик прерывания от USART

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *UartHandle)
{ 
	myMes m;
	HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_14);
	switch((uint32_t)UartHandle->Instance)
	{
		case (uint32_t)USART1:
			HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_10);
		  m.usart=1;
		  m.byte=b1;
		  xQueueSend( RecQHandle, &m, portMAX_DELAY  );
		  // Do this need? 
      //HAL_UART_Receive_IT(&huart1, &b1,1);
		  break;
		case (uint32_t)USART2:
			HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_12);
		  m.usart=2;
		  m.byte=b2;
		  xQueueSend( RecQHandle, &m, portMAX_DELAY  );  
		  HAL_UART_Receive_IT(&huart2, &b2,1); 
			break;			
	}
}


Обратите внимание на комментарий посредине и почему для второго порта она не закомментирована

По просьбе одного из внимательных читателей: Так же обратите внимание на то, что я использовал в обработчике прерывания xQueueSend вместо xQueueSendFromISR. Это сделано специально, что бы контроллер в процессе испытаний «а так оно сможет» (или говоря простыми словами, при поднятии скорости на последовательном порту где-то до 57600-115200) начал регулярно зависать. Простое включение отладчика показало бы, что он завис в ожидании на попытке записать в переполненную очередь. Внутри реального кода есть еще пара таких «ловушек».

Компилируем, собираем и глядим в терминал



Кажется, то что надо. Цепляем к 1му порту другой «источник данных» и просто копипастим строку в него.



Кстати, очень хороший вопрос для разбирательства «как работают прерывания» — почему строки от обоих портов не прерывают друг друга?

Но для подтверждения того, что не все так плохо, привожу другой скриншот, где я просто стучал по клавиатуре



Как видите, если «есть шо», то один порт может «перебить» другой.

И наконец, самая интересная фишка получившегося анализатора. Если в коде инициализации физического USART порта добавить следующие строки

huart1.Init.OneBitSampling = UART_ONEBIT_SAMPLING_DISABLED ;
huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_AUTOBAUDRATE_INIT;
huart1.AdvancedInit.AutoBaudRateEnable = UART_ADVFEATURE_AUTOBAUDRATE_ENABLE;
huart1.AdvancedInit.AutoBaudRateMode = UART_ADVFEATURE_AUTOBAUDRATE_ONSTARTBIT;


То микроконтроллер будет сам, на ходу подстраиваться под скорость по стартовому биту. Очень полезно для случаев, когда сначала устройства «обнюхиваются» для идентификации на малой скорости, а потом переходят на большую.

В общем-то и все. Возвращаясь к затравке: полностью аналогичным способом был получен дамп обмена между модулями, довольно быстро методом «тыка» разобран по частям и через некоторое время дорогущий прибор был заменен на стобаксовую коробочку, превосходящую его по ключевому (для заказчика) параметр��. Правда, деньги производителю все равно не перестали платить (там кучка других причин еще была), но зато избавились от боязни «счас сломается, а процесс контролировать надо».

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

PS На фотографии в заголовке макет прибора для лазерной хирургии, для которого мы в studiovsemoe.com писали прошивку. Он не имеет никакого отношения к тому самому прибору. Просто красивая картинка получилась.