Урок 21. Работа с UART через библиотеку HAL. Инициализация интерфейса и передача данных в блокирующем режиме. Отладка программ с помощью UART. Функция sprintf.

HAL_UART_Transmit

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

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

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

 

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

В справочнике HAL-библиотеки появилась таблица с функциями управления UART и страница с описанием этих функций. Просмотрите ее перед дальнейшим чтением урока, чтобы хотя бы приблизительно понимать, какие возможности предоставляет библиотека. Большинство функций понятны без подробного описания.

 

Инициализация, установка конфигурации UART.

Наверное, самая простая и очевидная задача. Давайте сделаем это с помощью STM32CubeMX, затем посмотрим, как конфигуратор инициализировал UART, какие HAL-функции использовал. На мой взгляд, применение STM32CubeMX для этой задачи вполне оправдано и главное удобно.

Создадим проект Lesson21_1.

Настроим систему тактирования на частоту 72 мГц. На всякий случай конфигурируем вывод светодиода общего назначения (PC13) как выход.

В нашей системе к компьютеру подключен UART1. Будем использовать его.

Выбираем вкладку в левом меню Connectivity, в нем же UART1. В окне Mode разрешаем работу UART в асинхронном режиме.

Конфигурирование

На изображении микроконтроллера выводы UART PA9 (TX) и PA10(RX) стали зелеными, т.е. задействованными. Конфигуратор сам установил их режимы, разрешил тактирование UART от шины APB.  И нам не пришлось разбираться, к какой шине он подключен, вспоминать, как устанавливается конфигурация портов ввода/вывода.

На установку конфигурации UART через регистры CMSIS в предыдущем уроке мы потратили много времени. Копались в документации, разбирались с устройством контроллера UART, назначением каждого бита, вычисляли константы, определяющие скорость обмена и т.п. Сейчас сделаем это за полминуты.

 

В окне Configuration выбираем закладку Parameter Settings и задаем нужные параметры. Скорость обмена в числовом виде (Baud Rate) и из двух вариантов выбираем длину слова (Word Lenth).

Конфигурирование

Следующее поле (Parity) позволяет:

  • отключить контроль четности (None);
  • включить контроль четности (Even);
  • использовать контроль по нечетности (Odd).

Конфигурирование

Отключаем.

Можно выбрать формат с одним или двумя стоповыми битами (поле StopBits).

Конфигурирование

Выбираем один стоп-бит.

В расширенных параметрах (Advanced Parameters) можно разрешить только передачу или только прием (поле Data Direction).

Конфигурирование

В последнем поле (Over Sampling) задано число выборок сигнала RX.

Пока можно на этом остановиться, но мы изучим еще вкладку  GPIO Setting, с помощью которой можно изменить режимы выводов UART.

Для выходного сигнала TX можно задать частоту тактирования.

Конфигурирование

Для входного сигнала RX конфигуратор позволяет подключить подтягивающий резистор к питанию или земле.

Конфигурирование

Все, можно работать с UART. Завершаем создание проекта. Вот, что получилось у меня Lesson21_1.

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

Давайте откроем проект в Atollic TrueStudio, посмотрим и осознаем, что сделал конфигуратор. Возможно, нам в будущем потребуется изменять режим UART динамически или захочется задать его конфигурацию без использования STM32CubeMX.

В файле stm32f1xx_hal_msp.c :

Разрешается тактирование UART1.

__HAL_RCC_USART1_CLK_ENABLE();

Разрешается тактирование порта GPIOA. Выводы этого порта используются для UART1 (PA9 и PA10).

__HAL_RCC_GPIOA_CLK_ENABLE();

Задается режим для выводов PA9 и PA10.

GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

Как это все делать, мы уже знаем из предыдущих уроков. Новое – это конфигурирование UART.

Режим UART задается с помощью функции HAL_UART_Init . В справочнике самые минимальные сведения об этой функции занимают 3 страницы. Но ничего сложного в ней нет.

Функция HAL_UART_Init имеет один аргумент – структуру параметров конфигурации UART_HandleTypeDef. А  одним из элементов этой структуры является другая структура UART_InitTypeDef. Она определяет аппаратные параметры UART. Для конфигурации UART необходимо задать значения полей этих структур и вызвать функцию инициализации HAL_UART_Init.

Разберемся на примере нашего проекта. Все происходит в файле main.c.

Создается экземпляр структуры типа UART_HandleTypeDef с именем huart1.

UART_HandleTypeDef huart1;

Поле Instance обязательно должно быть задано. Оно определяет базовый адрес UART, т.е. какой именно UART будет использоваться.

huart1.Instance = USART1;

Дальше заданы поля структуры Init. Это аппаратные параметры UART, которые мы устанавливали в STM32CubeMX.

huart1.Init.BaudRate = 9600;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;

И в завершение вызывается функция инициализации.

if (HAL_UART_Init(&huart1) != HAL_OK)
{
  Error_Handler();
}

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

 

Работа с UART через HAL-библиотеку в блокирующем режиме.

Собственно работа с UART подразумевает выполнение двух операций передачи и приема данных.

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

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

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

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

 

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

Для этого существует функция HAL_UART_Transmit.

Формат функции:

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

У нее 4 параметра:

  • huart – указатель на структуру конфигурации типа UART_HandleTypeDef. Мы будем использовать уже заданный при инициализации экземпляр huart1.
  • pData – указатель на буфер передаваемых данных.
  • Size – количество данных, которые надо передать.
  • Timeout – тайм-аут операции в мс, т.е. время на выполнение операции.

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

  • HAL_OK возвращается при успешном окончании операции UART.
  • HAL_ERROR означает, что возникла ошибка. Как правило, это неправильно заданные аргументы. Например, был задан UART с номером 50.
  • HAL_TIMEOUT – ошибка тайм-аута, т.е. за заданное время не удалось выполнить операцию.
  • HAL_BUSY – UART занят, в данный момент он используется другой функцией.

Применительно к передаче в блокирующем режиме:

  • При использовании UART только в блокирующем режиме ошибка HAL_BUSY появиться не может. Как UART может быть занят, если выход из функции передачи происходит только после ее завершения. А использование UART одновременно и в блокирующем и неблокирующем режимах хотя и возможно, но бессмысленно.
  • Ошибка тайм-аута возможна только при неправильной конфигурации UART или неверном значении параметра Timeout. Т.е. если мы пытаемся передать данные через UART, а он не может этого сделать или делает медленнее, чем мы рассчитывали.

Т.е. при использовании в блокирующем режиме возвращаемое значение функции HAL_UART_Transmit можно не проверять.

Собственно, из описания параметров HAL_UART_Transmit  понятно, как передавать данные.

Попробуем в цикле отсылать на компьютер текстовое сообщение. Режим UART уже  сконфигурирован. Остается в файл main.c вставить вызов функции передачи и задержку.

/* USER CODE BEGIN WHILE */

uint8_t str[] = "Проверка передачи UART\r\n\0";

while (1)
{
  HAL_UART_Transmit(&huart1, str, 24, 30);
  HAL_Delay(500);

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

  • Мы перед бесконечным циклом создали и инициализировали текстовую строку str.
  • В цикле вызываем функцию HAL_UART_Transmit с параметрами:
    • Указатель на структуру, определяющую конкретный UART.
    • Имя текстовой строки.
    • Подсчитали количество символов в строке и задали его в третьем параметре.
    • Вычислили время передачи строки. При скорости 9600 бод время передачи байта составляет 1042 мкс (таблица из урока 20). Общее время передачи строки из 24 символов – 25 мс. Добавим на запас 5 мс и зададим последний параметр равным 30.

Для проверки загружаем программу в отладочную плату, запускаем  CoolTerm.

TermCool

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

Можно задать текстовую строку непосредственно в параметре функции.

/* USER CODE BEGIN WHILE */

while (1)
{
  HAL_UART_Transmit(&huart1, "Проверка передачи UART\r\n\0", 24, 30);
  HAL_Delay(500);

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

Если лень считать количество символов в строке, то можно использовать встроенную функцию strlen.

#include < string.h >
size_t *strlen (const char *str);

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

Strlen определяет конец строки по символу 0. Надо не забыть его добавить.

/* USER CODE BEGIN WHILE */

uint8_t str[] = "Проверка передачи UART\r\n\0";

while (1)
{
  HAL_UART_Transmit(&huart1, str, strlen(str), 30);
  HAL_Delay(500);

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

Если кого-то смущают предупреждения компилятора, то надо явно подключить файл библиотеки:

/* USER CODE BEGIN Includes */
#include <string.h>
/* USER CODE END Includes */

И более строго подойти к типу данных параметра функции strlen.

HAL_UART_Transmit(&huart1, str, strlen((char *)str), 30);

Я писал выше, что при передаче в блокирующем режиме вряд ли необходима проверка ответа функции HAL_UART_Transmit, но при желании это можно сделать так.

if( HAL_UART_Transmit(&huart1, str, strlen((char *)str), 30) != HAL_OK ) {
  // ошибка передачи
}

Последнее, что я считаю необходимым добавить относительно функции HAL_UART_Transmit, это то, что она возвращает управление основной программе при реальном окончании передачи. Т.е. не тогда, когда данное загружено в буферный регистр USART_DR, а когда пуст сдвиговый регистр. При выходе функция ожидает активного состояния флага TC регистра USART_SR.

 

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

У микроконтроллеров STM32 есть встроенный интерфейс SWD (Serial Wire Debug), через который можно не только загружать программу, но и производить ее отладку. Для подключения компьютера к SWD-интерфейсу необходимы дополнительные аппаратные средства, например, программатор ST-Link.

Будем считать, что на текущем этапе у нас нет такого программатора. Дальше речь идет об отладке программ с помощью интерфейса UART, через который плата подключена к компьютеру.

Основная функция средств отладки – это увидеть состояние программы, узнать значение переменных и т.п. Сделать это можно, передав отладочную информацию на компьютер через интерфейс UART. В нашем отладочном модуле он уже подключен к компьютеру. Данные отладки можно наблюдать с помощью любого терминала COM-порта, например, CoolTerm.

Для передачи отладочных данных будем использовать функцию HAL_UART_Transmit. Выводить текстовую информацию мы уже умеем. Вот пример, в котором при проходе программы через начало цикла выводится информация об этом.

while (1)
{
  uint8_t str[] = "Начало цикла\r\n\0";
  HAL_UART_Transmit(&huart1, str, strlen((char *)str), 30);

  HAL_Delay(2000);
}

Остается разобраться, как передавать значения переменных. По сути это задача преобразования данных в текстовые строки. На сайте есть урок 30 Ардуино, посвященный этой теме. Каждый может выбрать удобный для себя способ или разработать свой собственный.

 

Функция sprintf.

В этом уроке для формирования отладочных данных будем использовать функцию форматированного ввода вывода sprintf. Это самый удобный, универсальный способ вывода текстовой информации вместе со значениями переменных.

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

Функция сложная, многофункциональная. Я приведу только необходимый нам минимум информации для работы с ней.

Описание функции выглядит так:

int sprintf(char *buf, const char *format, ...);

Обязательных параметров 2:

buf – указатель на текстовую строку, в которую будет записан результат;

format – строка форматирования.

Это функция с переменным числом аргументов. От строки форматирования зависит количество остальных параметров.

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

uint8_t str[50];
sprintf(str, "Проверка\n");
HAL_UART_Transmit(&huart1, str, strlen((char *)str), 50);

Чтобы компилятор не выдавал предупреждения, надо строже подойти к типам параметров. Мы создали массив типа uint8_t, а функция требует указатель на char.

sprintf((char *)str, "Проверка\n");

Казалось бы, такое использование функции sprintf бессмысленно, но она позволяет добавлять в текстовую строку значения параметров.  Для этого используются управляющие последовательности. Для каждого типа данных, своя последовательность. Они позволяют вставлять в текстовую строку данные различных форматов.

Управляющая последовательность имеет вид:

%[флаги][ширина][.точность][размер]тип

Обязательными элементами являются символ начала управляющей последовательности (%) и тип. С них и начнем. Я привел только самые необходимые из спецификаторов.

Спецификатор типа Назначение
%d Вывод целого числа в десятичной системе исчисления.
%u Вывод целого беззнакового числа в десятичной системе исчисления.
%x Вывод целого числа в шестнадцатеричной системе исчисления.
%f Вывод числа с плавающей запятой.
%c Вывод символа с заданным кодом.
%s Вывод строки с заданным указателем.

Каждая управляющая последовательность добавляет свой параметр к функции sprintf. Вот примеры.

Операторы

uint32_t x= 12345;
sprintf(str, "Значение переменной x= %d", x);

поместят в строку str следующую текстовую информацию:

Значение переменной x= 12345.

12345 – это значение переменной x в десятичном виде.

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

uint8_t str[100];
uint32_t x= 12345;
float y= 1.2345678;
uint8_t z= 0x61;
char str1[]= "Строка\0";

sprintf(str, "Проверка спецификаторов типа\n x=%d\n x=%x\n y=%f\n z=%c\n str1=%s\n ------------", x, x, y, z, str1);

HAL_UART_Transmit(&huart1, str, strlen((char *)str), 100);

Вот, что отобразил терминал.

TermCool

К текстовой строке добавились:

  • 12345 - значение переменной x в десятичном виде;
  • 3039 - значение этой же переменной в шестнадцатеричном виде;
  • переменная с плавающей запятой никак не отобразилась,
  • a – символ, соответствующий коду в переменной z (0x61);
  • Строка – текстовая строка, указатель на которую содержится в Str1.

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

sprintf(str, "x= %5d\n", 12); // выведет "x=    12"

Можно лишние пробелы заполнить нулями.

sprintf(str, "x= %05d\n", 12); // выведет "x= 00012"

Если ширина будет меньше числа, то оно будет напечатано полностью.

sprintf(str, "x= %3d\n", 123456); // выведет "x= 123456"

Повторю, это самый минимум сведений о функции sprintf. В интернете много информации о ней. При желании можете ознакомиться.

 

Вывод чисел с плавающей запятой с помощью sprintf в средах программирования микроконтроллеров.

Во всех известных мне программных средствах для программирования микроконтроллеров на языке C функция sprintf не поддерживает вывод чисел с плавающей запятой.

Существует следующий способ решения этой проблемы.

Допустим, нам необходимо вывести переменную типа float. Стандартное решение не работает.

float y= 18.2345678;
sprintf(str, "y= %f", y);

Мы уже проверяли.  Результат ”y=”.

Тогда выводим число float, используя только целочисленные переменные.

float y= 18.2345678;
sprintf(str, "y= %d.%03d", (uint32_t)y, (uint16_t)((y - (uint32_t)y)*1000.) );

Поясню последовательность преобразования.

Выводим целую часть, печатаем децимальную точку.

sprintf(str, "y= %d.", (uint32_t)y);

Вычисляем и выводим дробную часть. Для этого:

  • Вычисляем дробную часть в формате float.

y - (uint32_t)y

  • Умножаем ее на число 10 в степени равной количеству знаков после запятой. Например, для трех знаков после запятой умножаем на 1000.

(y - (uint32_t)y)*1000.

  • Выделяем целую часть из дробной части исходного числа.

(uint16_t)((y - (uint32_t)y)*1000.)

  • Выводим целую часть числа, децимальную точку и дробную часть. Не забываем указать при выводе дробной части число разрядов с нулями в незначащих полях (%03d).

sprintf(str, "y= %d.%03d", (uint32_t)y, (uint16_t)((y - (uint32_t)y)*1000.) );

Получаем следующую текстовую строку.

“y= 18.234”

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

Последний обобщающий пример урока.

uint8_t str[100];
uint16_t x=0;
float y= 5.0;

while (1)
{
  x++;
  y += 0.01;

  sprintf(str, "Цикл номер %d y= %d.%02d\n", x, (uint32_t)y, (uint16_t)((y - (uint32_t)y)*100.) );
  HAL_UART_Transmit(&huart1, str, strlen((char *)str), 100);

  HAL_Delay(1000);

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

В цикле увеличиваются две переменные целочисленного и плавающего типа. Первая увеличивается на 1, вторая на 0.01. Значения переменных выводятся через UART.

TermCool

 

Прием данных UART в блокирующем режиме в этот урок не поместился. Этой задаче будет посвящен следующий урок.

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

3

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

не в сети 10 часов

Эдуард

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

9 комментариев на «Урок 21. Работа с UART через библиотеку HAL. Инициализация интерфейса и передача данных в блокирующем режиме. Отладка программ с помощью UART. Функция sprintf.»

  1. Нет, ну это же несерьезно! Есть же DMA! Да, библиотека SD (та, которая идет в комплекте с Ардуино) корявая и нужно менять, но ведь проблема то глобальнее. Та же самая картина наблюдается в библиотеке работы с экраном, и даже слушание UART’а у меня сделано через опрос. В общем, я начал думать, что переписывание всех компонентов на HAL это не такая уж и глупая идея. Начал, конечно, с чего попроще — драйвера UART, который слушает поток данных от GPS. Интерфейс ардуино не позволяет прицепиться к прерыванию UART и выхватывать приходящие символы на лету. В итоге единственный способ получать данные — это постоянный опрос. Я, конечно, добавил vTaskDelay(10) в обработчик GPS, чтобы хоть немного снизить загрузку, но на самом деле это костыль. Первая мысль, конечно, была прикрутить DMA. Это даже сработало бы, если бы не протокол NMEA. Проблема в том, что в этом протоколе информация просто идет потоком, а отдельные пакеты (строки) разделяются символом переноса строки. При этом каждая строка может быть различной длины. Из-за этого заранее неизвестно сколько данных нужно принять. DMA так не работает — там количество байт нужно задавать заранее при инициализации пересылки. Короче говоря, DMA отпадает, ищем другое решение. Если посмотреть внимательно на дизайн библиотеки NeoGPS, то видно, что входные данные библиотека принимает побайтно, но значения обновляются только тогда, когда пришла вся строка (если быть точнее, то пакет из нескольких строк). Т.о. без разницы, кормить библиотеке байты по одному по мере приема, или потом все сразу. Так, что можно сэкономить процессорное время – сохранять принятую строку в буфер, при этом делать это можно прямо в прерывании. Когда строка принята целиком – можно начинать обработку. Вырисовывается следующий дизайн Хотя инициализация слизана из STM32GENERIC она полностью соответствует той, которую предлагает CubeMX

    0
    • Сделайте кольцевой буфер и прием по DMA.
      Рассчитать исходя из ваших настроек бодрейта за какое время заполнится 90% этого буфера. Организовать прерывание таймером при заполнении 90%, и в прерывании извлекать принятые данные из этого буфера в рабочий массив для последующей обработки. 10% оставить на время обработки прерывания — чтобы не переполнился буфер и не потерять ни 1 байта принятых данных.
      Таким образом можно минимально использовать CPU на задачу приема.

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

    1
  3. Здравствуйте.
    Подскажите пожалуйста, как на физическом уровне соединить STM32f407VET c портом USB в компьютере для передачи отладочной информации в терминал? Достаточно ли для этого ST-Link V2 или нужно что то еще?

    0

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

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

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