UART — следующая линия обороны

Резюме

В прошлом выпуске мы рассмотрели STM32CubeIde — где найти, что и как. мы также научились мигать светодиодами и познакомились с низкоуровневым API семейства контроллеров STM32.

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

В этом выпуске я познакомлю вас с одной из самых распространенных и важных последовательных шин — Универсальный асинхронный приемник-передатчик или короче UART.

Теория воспоминаний

Если вы ветеран микроконтроллеров, здесь нечего объяснять относительно шины и протокола связи, стоящего за ней. Но если вы новичок в этой области, я предлагаю вам прочитать эту статью. Он объясняет основные части интерфейса UART.

Подводя итог, UART — это последовательный интерфейс, что означает, что данные передаются байт за байтом последовательно. Другим отличительным признаком UART является количество необходимых ему проводов, которых обычно два: передача (TX) и прием (RX). Таким образом, всякий раз, когда вы видите на печатной плате 2 контакта, которые помечены как TX и RX, вы можете быть на 99,99% уверены, что шина UART находится там (если вы хотите возиться и хотите увидеть, что отправляется на шине UART, вы можете подключить свой Serial-to-USB к этим контактам).

Поскольку для UART обычно требуется 2 провода — TX и RX, но без тактового сигнала (в отличие от его брата SPI и I2C), мы должны убедиться, что устройства, общающиеся друг с другом через UART, должны иметь одинаковую тактовую частоту (или мы называем это скорость передачи).

Как использовать его на STM32

Теперь мы знаем, что для UART требуется 2 провода, следовательно, 2 контакта на микроконтроллере (TX и RX), и оба устройства UART должны согласовать скорость передачи данных. Поэтому следующий вопрос будет заключаться в том, как это будет сделано. Чтобы оценить шину UART на STM32, мы будем использовать шину UART для отправки некоторых данных на ПК. Ниже приведена настройка нашего сценария:

На практике на ПК нет шины UART, поэтому мы не можем подключить STM32 напрямую к ПК, следовательно, нам нужно промежуточное устройство, которым является устройство ST-Link. Согласно руководству пользователя платы, контроллер STM32 подключается к ST-Link, который является отладчиком и программатором, через шину UART. Затем ST-Link действует как Serial-to-USB для преобразования кадра данных UART в кадр USB, который можно отправить на ПК через шину USB. На ПК мы можем прочитать данные с помощью терминальной программы (Tera Term, PuTTY и т. д.).

Написание кода

Конфигурация кода

Давайте начнем писать код, открыв STM32CubeIDE.

Вы уже знали упражнение. Создаем новый проект с чистой распиновкой, как на картинке ниже:

Затем мы открываем вкладку Подключение → выбираем USART2 → Режим: асинхронный, чтобы активировать USART2периферийное устройство.

Когда мы активируем периферийное устройство USART2, одновременно активируются контакты для USART. В нашем случае это контакты PA2 и PA3. Нашим следующим шагом будет определение того, какие выводы STM32 соединяют ST-Link. Как? Мы проверяем руководство пользователя платы.

На странице 25 инструкции упоминается, что:

Интерфейс USART2, доступный на PA2 и PA3 микроконтроллера STM32, может быть подключен к микроконтроллеру ST-LINK, разъему ST morpho или к разъему ARDUINO®…

Итак, мы должны активировать пины PA2 и PA3, что уже сделано STM32CubeIDE.

В разделе часов мы хотим, чтобы STM32 работал на полной скорости, поэтому мы устанавливаем его на 32 МГц.

Прежде чем генерировать наш код, мы хотим убедиться, что используем библиотеку LL. Поэтому в Диспетчере проектов → Дополнительные настройки меняем Драйвер на LL.

Давайте сгенерируем код!

Использование библиотеки LL

Аппаратное обеспечение готово, об инициализации заботится STM32CubeIDE. Поэтому наша задача — сказать STM32 отправить какие-то данные на ПК с помощью LL API. Как это сделать? Мы рассмотрим руководство пользователя STM32L053.

Рисунок 241 на странице 765 очень хорошо описывает процесс передачи байта.

передача начинается с ожидания поднятия флага TXE (пустой регистр данных передачи). Когда флаг поднят, мы можем записать первый байт в регистр данных. Когда первый байт перемещается в сдвиговый регистр для передачи, TXE снова повышается. Мы повторяем процесс, пока не достигнем последнего байта. Наконец, мы ждем флага TC (завершение передачи), пока он не будет поднят.

Имея это в виду, мы можем заняться поиском подходящих API в документации по API.

В документе API мы нашли следующий API (рисунок выше):

  • LL_USART_TransmitData8: передача 8 данных
  • LL_ USART_IsActiveFlag_TC: проверка поднятия флага TC
  • LL_USART_IsActiveFlag_TXE: проверка поднятия флага TXE

Приступаем к кодированию!

Передать данные

В файле main.c между строк:

/* USER CODE BEGIN PV */
/* USER CODE END PV */

добавьте следующий фрагмент

/* USER CODE BEGIN PV */
//create buffer to hold data and a counter
uint8_t text[12] = “hello world\n”;
uint8_t num_of_byte = 0;
/* USER CODE END PV */

И в цикле while мы добавляем следующий фрагмент кода:

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
    while(num_of_byte <= (sizeof(text) - 1))
    {
        //waiting until the Transmit Empty flag is set
        while(!LL_USART_IsActiveFlag_TXE(USART2));
        //send data byte by byte
        LL_USART_TransmitData8(USART2, text[num_of_byte++]);
    }
    //reset the counter
    num_of_byte = 0;
    //Wait until the transmit complete Flag to be raised
    while (!LL_USART_IsActiveFlag_TC(USART2));
    LL_mDelay(2000);
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
}

Фрагмент кода делает именно то, что описано в руководстве. Он ждет, пока не поднимется флаг TXE, а затем отправляет первый байт. Процесс продолжается до последнего байта. Когда последний байт передан, он ожидает, пока не будет установлен флаг TC.

Скомпилируйте и прошейте код!

Запустите терминальную программу (я использую Tera Term) и настройте ее со следующими параметрами:

  • Скорость передачи: 115200
  • Стоповый бит: 1
  • Бит данных: 8
  • Паритет: нет

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

Здорово! Вы можете отправить данные/информацию в терминальную программу, что очень полезно, например. отправка данных датчиков или отладочная информация…

Следующий вопрос будет… Можем ли мы использовать printf() вместо приведенного выше фрагмента, чтобы наш код был более надежным и легким для чтения? Ответ — да, мы можем перенаправить printf() на UART.

Для этого удаляем все между строк:

/* КОД ПОЛЬЗОВАТЕЛЯ НАЧАЛО PV */

/* КОД ПОЛЬЗОВАТЕЛЯ КОНЕЦ PV */

потому что они нам больше не нужны.

Между линиями:

/* НАЧАЛО КОДА ПОЛЬЗОВАТЕЛЯ Включает */

/* КОД ПОЛЬЗОВАТЕЛЯ КОНЕЦ Включает */

добавьте следующий фрагмент:

/* USER CODE BEGIN Includes */
#include “stdio.h”
/* USER CODE END Includes */

Мы должны включить библиотеку stdio.h, потому что мы хотим использовать функцию printf().

Между линиями:

/* USER CODE BEGIN 4 */
/* USER CODE END 4 */

добавьте следующий фрагмент:

/* USER CODE BEGIN 4 */
int _write(int file, uint8_t *buf, int nbytes)
{
    uint8_t num_of_byte = 0;
    while(num_of_byte <= nbytes — 1)
    {
        //waiting until the Transmit Empty flag is set
        while(!LL_USART_IsActiveFlag_TXE(USART2));
        //send data byte by byte
        LL_USART_TransmitData8(USART2, buf[num_of_byte++]);
    }
    //Wait until the transmit complete Flag to be raised
    while (!LL_USART_IsActiveFlag_TC(USART2));
    return nbytes;
}
/* USER CODE END 4 */

Приведенный выше фрагмент повторяет то, что мы сделали в предыдущей части, и остается внутри функции _write(). В глубине библиотеки stdio.h функция _write() будет вызываться функцией printf(). Поэтому всякий раз, когда мы используем функцию printf(), она будет перенаправлена ​​на UART, а затем на программу терминала.

В цикле while мы заменяем код простым вызовом printf():

while (1)
{
   printf(“hello world\n”);
   LL_mDelay(2000);
   /* USER CODE END WHILE */
   /* USER CODE BEGIN 3 */
}

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

Последняя часть головоломки — это получение данных с терминала.

Получить данные

Обычно вы можете использовать метод опроса, чтобы проверить, доступны ли данные с помощью этой функции:

LL_USART_IsActiveFlag_RXNE()

Затем вы можете прочитать байт с помощью функции:

LL_USART_ReceiveData8()

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

Во-первых, мы должны активировать прерывание, вернувшись к интерфейсу генератора кода. В этом интерфейсе мы выполняем следующие шаги: System Core →NVIC →Активировать глобальное прерывание USART2

Сгенерируйте код и вернитесь к файлу main.h.

В файле main.h, между строк,

/* USER CODE BEGIN EFP */
/* USER CODE END EFP */

добавьте следующий фрагмент

/* USER CODE BEGIN EFP */
void ByteReceivedCallback(void);
/* USER CODE END EFP */

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

Меняем на файл stm32l0xx_it.c. В этом файле, в функции USART2_IRQHandler, между строк

/* USER CODE BEGIN USART2_IRQn 0 */
/* USER CODE END USART2_IRQn 0 */

мы добавляем следующий фрагмент

void USART2_IRQHandler(void)
{
    /* USER CODE BEGIN USART2_IRQn 0 */
    ByteReceivedCallback();
    /* USER CODE END USART2_IRQn 0 */
    /* USER CODE BEGIN USART2_IRQn 1 */
    /* USER CODE END USART2_IRQn 1 */
}

Здесь происходит следующее: всякий раз, когда запускается прерывание UART, вызывается функция USART2_IRQHandler для обслуживания прерывания. В свою очередь будет вызвана функция ByteReceivedCallback(). В функции ByteReceivedCallback() мы считываем данные из регистра приема данных, а затем отправляем данные обратно на терминал.

Вернемся к функции main.c между строк.

/* USER CODE BEGIN 4 */
/* USER CODE END 4 */

добавьте следующий фрагмент:

void ByteReceivedCallback(void)
{
    uint8_t ch;
    // read the data from the data reception register
    // by doing this, we also clear the interrupt flag
    ch = LL_USART_ReceiveData8(USART2);
    
    //waiting until the Transmit Empty flag is set
    while(!LL_USART_IsActiveFlag_TXE(USART2));
    
    LL_USART_TransmitData8(USART2, ch);
    
    //Wait until the transmit complete Flag to be raised
    while (!LL_USART_IsActiveFlag_TC(USART2));
}

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

Затем мы отправляем данные обратно в терминал, используя фрагмент, который мы реализовали ранее.

В цикле while нам не нужна строка

printf(“hello world\n”)

поэтому мы можем прокомментировать это.

Скомпилируйте и запишите код в STM32. На терминале попробуйте что-нибудь набрать и увидите результат.

Вывод

До сих пор мы научились использовать коммуникационную шину UART. Мы можем перенаправить printf() на UART, и мы также узнали, как использовать прерывание для получения данных.

Какое приложение для UART?

  • Система ведения журнала с многоуровневым ведением журнала, например. ИНФОРМАЦИЯ, ОТЛАДКА, ОШИБКА
  • Интерфейс командной строки (CLI)
  • Обновление прошивки
  • Интерфейс с некоторым коммуникационным чипом с использованием старой школьной AT-команды (модуль WIFI, сотовый модуль)

Надеюсь, вам понравится блог. Как обычно, код будет доступен на GitHub. Оставайтесь с нами, счастливого кодирования!

Другие эпизоды

Часть 1: Введение

Часть 2: Все дело в GPIO

GitHub-репозиторий