Урок 22. Работа с UART через библиотеку HAL. Прием данных в блокирующем режиме.

Работа с UART через HAL

Продолжим изучение работы с UART через библиотеку HAL. Научимся принимать данные в блокирующем режиме.

Предыдущий урок     Список уроков     Следующий урок

Прием данных в блокирующем режиме менее востребован в реальных приложениях, чем передача. При передаче программа “зависает” на определенное, предсказуемое время – на длительность передачи пакета данных. Это время не бесконечное, оно может быть подсчитано заранее. И, как правило, оно небольшое, особенно при высоких скоростях передачи.

 

Например, передача 1 байта при скорости 9600 бод занимает примерно 1 мс. Во многих даже рабочих приложениях вполне допустимо потратить на передачу данных несколько миллисекунд. А если увеличить скорость обмена до максимальной стандартной скорости 115200, то передача 1 байта потребует всего 87 мкс. А время передачи пакета, например,  из 10 байтов сократится до 0,87 мс.

С приемом данных в блокирующем режиме несколько сложнее. Данные поступают от другого аппаратного устройства. Как правило, неизвестно, когда они придут. Если обмен инициирует другое (ведущее) устройство, то приемное (ведомое) устройство должно постоянно ожидать поступления данных. Ожидать бесконечно. Получается, что программа должна бесконечно ”висеть” в HAL-функции приема.

Несмотря на все вышесказанное, существует немало вариантов оправданного использования HAL-библиотеки для приема данных UART в блокирующем режиме.

Прежде всего, для реализации устройства, работающего в режиме ведущего в локальных сетях. К примеру, вспомните протокол ModBus из уроков Ардуино. Ведущий контроллер посылает ведомому пакет данных и определенное время ждет ответа. Если ответ не приходит в течение заданного времени, то определяется ошибка транзакции.

Ниже я покажу, как можно решить проблему бесконечного ожидания данных ведомым контроллером, хотя и с некоторыми ограничениями.

Наверное, можно найти выход и для других реальных ситуаций. Оптимальное решение инженерной задачи предполагает не только оптимизацию в использовании ресурсов микроконтроллера или языка программирования, но и касается уровня знаний разработчика. В каких-то случаях может лучше “выкрутится” с помощью HAL-функций, чем тратить время на изучение CMSIS регистров и схемы UART.

Только я не призываю все делать с помощью HAL-библиотеки. У нее много недостатков, какие-то задачи невозможно решить с ее помощью. Тем не менее, в своих реальных приложениях я ограниченно, но использую HAL-библиотеку.

 

Прием данных в блокирующем режиме.

Производится с помощь функции HAL_UART_Receive.

HAL_StatusTypeDef HAL_UART_Receive (UART_HandleTypeDef * huart, uint8_t * pData, uint16_t Size, uint32_t Timeout)

Параметры:

huart – указатель на структуру параметров конфигурации типа UART_HandleTypeDef.

pData – указатель на буфер принятых данных.

Size – количество данных, которые надо принять.

Timeout – тайм-аут операции в мс.

Функция возвращает значение типа HAL_StatusTypeDef, общепринятое для UART HAL-функций. Описано в предыдущем уроке ссылка.

Все очевидно. Единственное, на чем я хочу заострить внимание это то, что функция считывает данные, из буферного регистра DR при активном флаге RXNE (буферный регистр не пуст). Т.е. она не сбрасывает, не удаляет последнее данное, если оно не было считано.

Например, UART настроен и готов к приему данных. На его аппаратный вход RX было передано несколько данных. Данные не считывались. Поэтому все они, кроме последнего, были ”забыты”, а последний байт остался в буферном регистре. Прошел час. Мы вызвали функцию HAL_UART_Receive(&huart1, str, 1, 5). Она считает тот самый последний байт из переданного пакета.

Сбросить последнее данное UART можно функцией HAL_UART_AbortReceive.

Разработаем эхо-терминал подобный тому, какой мы сделали в  уроке 20, но с использованием HAL-библиотеки. Напомню, что он должен просто отсылать принятые данные назад компьютеру.

Продолжим модифицировать проект из предыдущего урока, в котором уже настроена конфигурация UART1. Я скопировал его и переименовал в Lesson22_1.

Алгоритм программы предельно прост:

  • В цикле пытаемся считать байт из UART.
  • Если байт успешно принят, то передаем его назад.

/* USER CODE BEGIN WHILE */

uint8_t str[100];

while (1)
{
  if( HAL_UART_Receive(&huart1, str, 1, 3) == HAL_OK ) {

  // получен байт из UART
  HAL_UART_Transmit(&huart1, str, 1, 3); //передача байта
}

/* USER CODE END WHILE */

 

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!
 

 

Насколько получилось проще и быстрее по сравнению с программой из урока 20, в котором мы использовали CMSIS регистры.

Проверим программу.

  • Запустим CoolTerm.
  • Установим соединение (кнопка Connect).
  • Откроем окно для передачи данных (Connection -> Send String).
  • Теперь все данные отосланные из окна SendString появляются в основном окне программы.

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

Программа CoolTerm

В слове ”терминала” исчезает то буква ”т”, то буква ”е”. Что происходит.

В этом случае данные поступают в UART одним пакетом, без пауз между байтами. Я попробовал изобразить, последовательность операций с UART.

Последовательность операций эхо-терминала

  • После передачи 7го бита, перед стоп-битом функция HAL_UART_Receive получает первый байт.
  • Сразу же функция HAL_UART_Transmit отсылает его в UART и ждет окончания передачи.
  • После стопового бита программа выходит из HAL_UART_Transmit и вызывает HAL_UART_Receive.
  • Второй байт уже принят и находится в буферном регистре UART, поэтому функция HAL_UART_Receive тут же получает байт и передает управление основной программе.
  • Принятый байт 2 передается функцией HAL_UART_Transmit.

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

Проблема в том, что передача в блокирующем режиме “съедает” время на прием целого байта.  Если бы мы задали в функции HAL_UART_Receive прием нескольких байтов или принимали бы их по одному, но без передачи, то все бы работало нормально.

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

 

Пример реализации обмена данными STM32 и компьютера.

В качестве примера давайте разработаем контроллер, который будет обмениваться данными с компьютером по протоколу из урока 48 Ардуино. В этом уроке представлена программа верхнего уровня, которая кроме вывода данных на экран компьютера ведет статистику ошибок. Сможем увидеть, насколько надежно работает обмен , не проскакивают ли изредка ошибки.

Напомню, что в этом уроке к плате Ардуино мы подключили термодатчик DS18B20, резисторный делитель к аналоговому входу, кнопку и светодиод.

  • На компьютер передаются данные: значение температуры, напряжение на аналоговом входе и состояние кнопки.
  • Компьютер передает контроллеру состояние светодиода.

У нас все эти элементы не подключены к STM32. Будем просто передавать какие-то, заранее заданные  значения.

Используется простой числовой протокол обмена.

От компьютера поступает команда в следующем формате.

Номер байта Формат числа Назначение
0 byte Код операции + адрес контроллера (0x10)
1 byte Состояние светодиода (младший бит)
2 byte Контрольный код ( байт 0 ^ байт 1 ^ 0xe5)

В ответ на компьютер от контроллера передается следующий пакет.

Номер байта Формат числа Назначение
0 … 3 float Температура
4 … 7 float Напряжение
8 byte Состояние кнопки (младший бит)
9 byte Резерв
10, 11 int Контрольная сумма (сумма байтов 0 … 9  ^ 0xa1e3)

В уроке 48 Ардуино протокол описан подробно.

Вот, как выглядит программа, реализующая обмен данными контроллера STM32 с компьютером.

Загрузить проект можно по ссылке Lesson22_2

 

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!
 

uint8_t str[100];

struct {
  float t; // температура
  float u; // напряжение
  uint8_t b; // состояние кнопки
  uint8_t r; // резерв
  uint16_t s; // контрольная сумма
} par = {36.6, 12.5, 1, 0};

while (1)
{
  // ожидание паузы в передаче данных
  if( HAL_UART_Receive(&huart1, str, 1, 20) == HAL_OK ) continue;

  // ожидание 1го байта команды
  while ( HAL_UART_Receive(&huart1, str, 1, 10) != HAL_OK ) ;

  // прием оставшихся 2х байтов команды
  if( HAL_UART_Receive(&huart1, str+1, 2, 20) != HAL_OK ) continue;

  // проверка команды
  if ( (str[0] == 0x10) && ((str[0] ^ str[1] ^ 0xe5) != str[2]) ) continue;

  // подсчет контрольного кода ответа
  uint16_t sum= 0;
  for (uint16_t i=0; i<10; i++) sum += * ((uint8_t *)(& par) + i);
  par.s = sum ^ 0xa1e3;

  // ответ на компьютер
  HAL_UART_Transmit(&huart1, (uint8_t *)(& par), 12, 30);

  /* USER CODE END WHILE */
  /* USER CODE BEGIN 3 */
}

  • Создаем и инициализируем структуру с параметрами, которые будут передаваться на компьютер.

struct {
  float t; // температура
  float u; // напряжение
  uint8_t b; // состояние кнопки
  uint8_t r; // резерв
  uint16_t s; // контрольная сумма
} par = {36.6, 12.5, 1, 0};

Можно использовать массив, но у нас параметры разных типов. Удобнее работать со структурой.

В бесконечном цикле:

  • Ждем паузу в передаче данных. Транзакции обмена разделяются паузами. Пауза означает, что сейчас будет передана новая команда. Если мы не будем выделять начало команды, то возможны неприятности. Например, мы включили контроллер в момент, когда передача уже началась или был аппаратный сбой при передаче. Тогда  наш контроллер может начать принимать данные с середины команды. Будет полная ерунда. Даже может зависнуть обмен. В нашей программе управление возвращается к началу цикла при каждом поступлении данного. И только, если в течение 20 мс не было ни одного данного, программа перейдет к следующему шагу.

// ожидание паузы в передаче данных
if( HAL_UART_Receive(&huart1, str, 1, 20) == HAL_OK ) continue;

  • Ждем прихода 1го байта команды от компьютера. Как только он приходит, выходим из цикла на следующий шаг.

// ожидание 1го байта команды
while ( HAL_UART_Receive(&huart1, str, 1, 10) != HAL_OK ) ;

  • Принимаем оставшиеся 2 байта команды и проверяем контрольный код команды. Если байты не пришли или код неверен, то возвращаемся к началу основного цикла.

// прием оставшихся 2х байтов команды
if( HAL_UART_Receive(&huart1, str+1, 2, 20) != HAL_OK ) continue;

// проверка команды
if ( (str[0] == 0x10) && ((str[0] ^ str[1] ^ 0xe5) != str[2]) ) continue;

  • Подсчитываем контрольный код ответа и посылаем ответный пакет данных на компьютер.

// подсчет контрольного кода ответа
uint16_t sum= 0;
for (uint16_t i=0; i<10; i++) sum += * ((uint8_t *)(& par) + i);
par.s = sum ^ 0xa1e3;

// ответ на компьютер
HAL_UART_Transmit(&huart1, (uint8_t *)(& par), 12, 30);

  • Возвращаемся на начало цикла, опять проверяем, что передача закончилась.

 

 

Проверяем, что получилось. Загружаем программу в STM32. На компьютере запускаем программу UART_Arduino_PC. Загрузить ее можно из урока 48 Ардуино.

Проверка обмена данными STM32 и компьютера

У меня ни единой ошибки за полчаса непрерывной работы.

Реализация протокола обмена между STM32 и компьютером с помощью функций HAL-библиотеки в блокирующем режиме получилась очень простой и понятной.

  • Мы не разбирались с CMSIS-регистрами UART.
  • Не использовали прерывания.
  • Вся программа поместилась в одном программном блоке в виде последовательности операций.
  • Не пришлось отдельно отрабатывать тайм-ауты операций обмена. Мы использовали тайм-ауты HAL-функций. Кстати, при работе с UART через прерывания, тайм-ауты придется отслеживать дополнительными средствами.

 

В следующем уроке будем разбираться, как использовать HAL-библиотеку для управления UART в режиме прерываний.

 

Предыдущий урок     Список уроков     Следующий урок

1

Автор публикации

не в сети 26 минут

Эдуард

280
Комментарии: 1936Публикации: 197Регистрация: 13-12-2015

5 комментариев на «Урок 22. Работа с UART через библиотеку HAL. Прием данных в блокирующем режиме.»

  1. Что имеется ввиду «Ждать данные»? Как только они прилетят по UART, так увеличиться rx_counter. Можно ставить проверку этой переменной до вызова get_char(). Кстати, сам недавно переносил на stm, заметил что там немного другая SPL. Постараюсь на днях поправить файл, за одно положить вариант под stm.

    0
  2. для связки stm32l151cb с модемом bg95n странное дело — HAL_UART_Transmit(…, «ATI\r\n», stlen(«ATI\r\n»), 300) выполняется отлично, а вот HAL_UART_Receive(&huart1, &rx_data, 1000); выдает HAL_TIMEOUT, время менял от 1 до 0xffff — результат тот же — HAL_TIMEOUT
    ответ модема крайне важен-без него нельзя двигать дальше.
    поэтому и блокирующий режим используется.
    вариант HAL_UART_Receive_DMA еще какой-то выход, но….

    0
    • Добрый день!
      Возникла подобная ситуация при коннекте stm32f031k6 с esp 8622.
      Удалось ли вам найти решение?

      0
  3. Эдуард, добрый день. Сделал все в соответствии с уроком, однако при обмене периодически появляются ошибки. Загрузил ваш проект, не помогло, ошибки периодически появляются. На 421 цикл приходится 21 ошибка (STM32CubeIDE 1.10.1, отладочная плата BluePill).

    0
    • Здравствуйте!
      У меня работает без ошибок. Может у вас проблемы с аппаратной частью. Программу верхнего уровня можно проверить подключив к плате Ардуино и загрузив в нее программу из урока 48.

      0

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Нажимая кнопку "Отправить" Вы даёте свое согласие на обработку введенной персональной информации в соответствии с Федеральным Законом №152-ФЗ от 27.07.2006 "О персональных данных".