Урок 23. Работа с UART через библиотеку HAL с использованием прерываний.

HAL/UART

Научимся управлять интерфейсом UART через HAL-функции с использованием прерываний. Разработаем несколько учебных проектов.

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

В уроке 20 при работе с UART мы использовали аппаратные прерывания. Контроллер в бесконечном цикле выполнял основную программу и “отвлекался” на обслуживание UART только при приходе или окончании передачи данного.

 

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

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

 

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

В уроке 21 мы инициализировали UART для работы в блокирующем режиме. В режиме с использованием прерываний конфигурация UART задается аналогично. Единственное отличие – надо разрешить прерывание UART.

Давайте сделаем это с помощью STM32CubeMX.

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

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

Разрешим работу UART1 в асинхронном режиме (Connectivity -> USART1 -> Mode -> Asynchronous).

В окне Configuration выбираем закладку Parameter Settings и задаем параметры, как в уроке 21.

Установка конфигурации STM32CubeMX

Для работы с использованием прерываний выбираем закладку NVIC Settings и разрешаем прерывания UART1.

Установка конфигурации STM32CubeMX

Завершаем создание проекта.

Откроем проект в Atollic TrueStudio. STM32CubeMX  выполнил для конфигурации UART абсолютно такие же действия, как в уроке 21. Только в файле stm32f1xx_hal_msp.c добавил разрешение прерываний UART1.

/* USART1 interrupt Init */
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);

И в stm32f1xx_it.h появился прототип обработчика прерываний

void USART1_IRQHandler(void);

Теперь можно работать с UART через HAL-функции с использованием прерываний.

 

Передача данных.

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

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

HAL_StatusTypeDef HAL_UART_Transmit_IT (UART_HandleTypeDef * huart, uint8_t * pData, uint16_t Size)

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

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

Функция возвращает значение состояния типа HAL_StatusTypeDef, описанное в уроке 21.

Данные передаются пакетом:

  • через UART, заданным в структуре huart;
  • из массива с указателем pData;
  • длина пакета задана в Size.

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

Вот программа, передающая в цикле 24 байта. Аналог примера из урока 21.

/* USER CODE BEGIN WHILE */

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

while (1) {

  HAL_UART_Transmit_IT(&huart1, str, 24);
  HAL_Delay(500);

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

От соответствующего кода урока 21 программа отличается только именем функции передачи - HAL_UART_Transmit_IT вместо HAL_UART_Transmit. Но теперь программа не останавливается на время передачи данных.

  • В блокирующем режиме программа “зависала” в функции HAL_UART_Transmit примерно на 24 мс, затем отрабатывала задержку 500 мс в HAL_Delay. Таким образом, цикл while выполнялся примерно за 524 мс.
  • Программа этого урока инициирует передачу данных функцией HAL_UART_Transmit_IT и, не ожидая окончания передачи пакета, переходит к отработке задержки HAL_Delay. Передача данных происходит во время выполнения функции HAL_Delay. В этом случае период выполнения цикла while составляет примерно 500 мс.

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

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

CoolTerm

При вызове функции HAL_UART_Transmit_IT в случае, когда передача предыдущего пакета еще не завершилась, функция:

  • игнорирует запуск передачи;
  • возвращает значение HAL_BUSY.

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

while( HAL_UART_Transmit_IT(&huart1, str, 24) == HAL_BUSY );

Состояние процесса передачи UART также можно проверить с помощью функции HAL_UART_GetState. При занятом передачей UART функция возвращает значение HAL_UART_STATE_BUSY_TX.

Аналог предыдущего кода будет выглядеть так.

while( HAL_UART_GetState (&huart1) == HAL_UART_STATE_BUSY_TX ) ;
HAL_UART_Transmit_IT(&huart1, str, 24);

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

HAL_UART_Transmit_IT(&huart1, str, 24);
while( HAL_UART_GetState (&huart1) == HAL_UART_STATE_BUSY_TX ) ;

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

Прервать передачу данных можно вызвав функции HAL_UART_AbortTransmit или HAL_UART_AbortTransmit_IT.

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

Т.е. функции остановки процессов передачи и приема UART (HAL_UART_Abort ) тоже могут работать в блокирующем режиме и в режиме с использованием прерываний.

Следующая программа прерывает передачу пакета из 24 символов через 15 мс после начала передачи.

/* USER CODE BEGIN WHILE */

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

while (1) {

  HAL_UART_Transmit_IT(&huart1, str, 24);
  HAL_Delay(15);
  HAL_UART_AbortTransmit(&huart1);
  HAL_Delay(500);

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

В результате передается только часть данных.

CoolTerm

 

Прием данных с использованием прерываний.

Для этой операции предназначена функция HAL_UART_Receive_IT.

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

HAL_StatusTypeDef HAL_UART_Receive_IT (UART_HandleTypeDef * huart, uint8_t * pData, uint16_t Size)

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

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

Функция возвращает значение состояния типа HAL_StatusTypeDef, описанное в уроке 21.

Принимается пакет данных:

  • через UART, заданным в структуре huart;
  • сохраняются в массиве с указателем pData;
  • длина пакета задана в Size.

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

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

Как следствие, проверка окончания приема данных практически всегда необходима. Выполняется она такими же способами, как и при передаче:

  • проверкой ответа функции HAL_UART_Receive_IT;
  • с использованием функции HAL_UART_GetState.

Вот простейшая программа, реализующая функции эхо-терминала.

uint8_t str[3];

while (1) {

  if( HAL_UART_Receive_IT (&huart1, str, 1) != HAL_BUSY ) {
  while( HAL_UART_Transmit_IT(&huart1, str, 1) == HAL_BUSY );
}

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

В цикле проверяется состояние приема. Если UART освобождается, значит данное принято. Тогда оно передается назад через UART.

При длинных пакетах данные начинают пропускаться.

CoolTerm

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

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

Проблему частично может решить кольцевой буфер, но он только отсрочит время пропуска байта.

Проверять состояние приема можно и с помощью функции HAL_UART_GetState.

uint8_t str[3];

while (1) {

  uint8_t state = HAL_UART_GetState(&huart1);
  if( (state != HAL_UART_STATE_BUSY_RX) && (state != HAL_UART_STATE_BUSY_TX_RX) ) {

    while( HAL_UART_Transmit_IT(&huart1, str, 1) == HAL_BUSY );
    HAL_UART_Receive_IT (&huart1, str, 1);
  }

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

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

if( (state != HAL_UART_STATE_BUSY_RX) && (state != HAL_UART_STATE_BUSY_TX_RX) )

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

Искусственно прервать прием можно функциями HAL_UART_AbortReceive и HAL_UART_AbortReceive_IT . Относительно них справедливо все выше сказанное про аналогичные функции для передачи.

Все примеры, описанные выше, собраны в проекте Lesson23_1.

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

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

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

  • Инициируем функцией HAL_UART_Receive_IT прием одного байта (Size=1);
  • Бесконечное время ждем первый байт.
  • После его приема инициируем прием 9 байтов уже с контролем времени.
  • Если через заданное время оставшиеся 9 байтов не приходят, то завершаем прием функцией HAL_UART_AbortReceive.

Ниже я приведу пример разработки подобного алгоритма обмена.

 

Callback-функции (функции обратного вызова).

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

По-настоящему "красиво" выглядела программа из урока 20. В ней все операции выполнялись в обработчиках прерываний. Основной цикл while был пустым.

Подобные программы, работающие в фоновом режиме, могут быть реализованы с использованием callback-функций.

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

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

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

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

while( HAL_UART_GetState (&huart1) == HAL_UART_STATE_BUSY_TX ) ;

работать не будут.

В HAL-библиотеке callback-функции объявлены с атрибутом weak. Например

__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)

Атрибут weak объявляет функцию как ”слабую” ссылку. Проще говоря, если встретится функция с таким же именем, но без атрибута weak, то она заменит функцию, объявленную с атрибутом weak.

Для нас это означает, что для определения callback-функции достаточно создать свое определение без атрибута weak. И наша функция заменит функцию HAL-библиотеки.

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {

// тело функции

}

Больше никаких действий производить не надо.

Информация по callback-функциям UART есть в справочнике HAL-библиотеки. Сейчас нас интересуют функции:

HAL_UART_TxCpltCallback  - вызывается по завершению передачи данных и

HAL_UART_RxCpltCallback – вызывается по окончанию приема пакета данных.

 

Эхо-терминал с использованием callback-функций.

Создадим эхо-терминал без кольцевого буфера и посмотрим, какой длины пакеты он сможет возвращать без искажений.

Начнем с того, что очистим основной цикл while. Наша цель – оставить его пустым.

Массив str объявим глобальной переменной. Теперь к нему придется обращаться из разных функций.

Добавим 2 переменные-признаки. Первый признак активен при получении нового данного. Второй – при завершении передачи данного, т.е. при свободном для передачи UART.

/* USER CODE BEGIN PV */
uint8_t str[3];
uint8_t dataReceived=0; // признак данное получено
uint8_t dataTransmitted=1; // признак данное передано
/* USER CODE END PV */

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

/* USER CODE BEGIN WHILE */

HAL_UART_Receive_IT (&huart1, str, 1);

while (1) {

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

Определим callback-функции приема и передачи данных.

/* USER CODE BEGIN 4 */

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {

  if(huart == &huart1) {

    dataReceived=1;

    if( dataTransmitted != 0 ) {
      HAL_UART_Transmit_IT(&huart1, str, 1);
      dataReceived=0;
      dataTransmitted=0;
    }

    HAL_UART_Receive_IT (&huart1, str, 1);
  }
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {

  if(huart == &huart1) {

    dataTransmitted=1;

    if( dataReceived != 0 ) {
      HAL_UART_Transmit_IT(&huart1, str, 1);
      dataReceived=0;
      dataTransmitted=0;
    }
  }
}
/* USER CODE END 4 */

При приеме данного мы:

  • Устанавливаем признак dataReceived.
  • Если UART свободен для передачи, то передаем данное.

При завершении передачи данного:

  • Устанавливаем признак dataTransmitted.
  • Если есть принятое данное, то передаем его.

Т.е. передача данного происходит:

  • если данное принято и передатчик свободен;
  • если передача завершена и данное принято.

Полностью проект можно загрузить здесь Lesson23_2.

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

Проверяем.

Окно CoolTerm

Я передал несколько одинаковых строк. Результат примерно одинаковый. Пропускается каждый 13-14й символ.

Для пакетов большей длины надо использовать кольцевой буфер.

 

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

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

Только теперь основной цикл while должен оставаться пустым. Обмен с компьютером будет происходить в фоновом режиме.

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

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

Перед основным циклом запустим прием первого байта команды.

/* USER CODE BEGIN WHILE */

buf[0]=0; buf[1]=0; buf[2]=0;
firstByteWait=1; // признак ожидание первого байта
HAL_UART_Receive_IT (&huart1, buf, 1); // запуск приема

while (1) {

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

Цикл while остается пустым.

Создадим callback-функцию приема данных.

/* USER CODE BEGIN 4 */

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {

  if(huart == &huart1) {

    if( firstByteWait != 0 ) {
      // пришел первый байт
      firstByteWait=0;
      HAL_UART_Receive_IT (&huart1, buf+1, 2); // запуск приема остальных байтов команды
    }
    else {
      // принят весь пакет (3 байта)
      // проверка команды
      if ( (buf[0] == 0x10) && ((buf[0] ^ buf[1] ^ 0xe5) == buf[2]) ) {
        // команда принята правильно

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

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

        // запуск приема
        buf[0]=0; buf[1]=0; buf[2]=0;
        firstByteWait=1;
        HAL_UART_Receive_IT (&huart1, buf, 1);
      }
      else {
        // ошибка
        buf[0]=0; buf[1]=0; buf[2]=0;
        firstByteWait=1;
        HAL_UART_Receive_IT (&huart1, buf, 1); // запуск приема
      }
    }
  }
}
/* USER CODE END 4 */

Эта функция вызывается как при приеме первого данного, так  и при завершении приема всего пакета (еще 2х байтов). Признак firstByteWait позволяет разделить эти события.

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

Запускаем программу верхнего уровня из урока 48 Ардуино, проверяем работу контроллера.

Тестирование обмена

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

 

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

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

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

Такой алгоритм можно реализовать различными способами. Я думаю, самый простой – это контролировать состояние признака firstByteWait. Когда контроллер ожидает прихода 2х оставшихся байтов команды, этот признак находится в состоянии 0. Если это состояние длится более определенного времени (времени тайм-аута), то необходимо  прервать прием данных.

Расточительно тратить на отсчет времени тайм-аута аппаратный таймер. Давайте использовать программный.

Не вдаваясь в подробности, скажу, что в файле stm32f1xx_it.c есть обработчик прерывания тиков системного времени void SysTick_Handler(void). Он вызывается с периодом 1 мс. Давайте в нем и создадим программный таймер контроля времени приема команды.

void SysTick_Handler(void) {

/* USER CODE BEGIN SysTick_IRQn 0 */

  if( firstByteWait != 0 )  timeOut=0; 
  else {
    timeOut++;
    if( timeOut >= 10 ) {
      // ошибка таймаута
      HAL_UART_AbortReceive_IT(&huart1); // остановка приема
      firstByteWait=1; // признак ожидание первого байта
      timeOut=0;
      HAL_UART_Receive_IT (&huart1, (uint8_t *)buf, 1); // запуск приема
    }
  }
/* USER CODE END SysTick_IRQn 0 */

Алгоритм простой.

  • Если признак firstByteWait не равен 0 (ожидается первый байт), то мы сбрасываем счетчик timeout.
  • Если система находится в ожидании оставшихся байтов команды, то мы увеличиваем счетчик на 1 и проверяем, не достиг ли он 10.
  • Если значение счетчика  превышает или равно 10, это означает, что признак firstByteWait был в ненулевом состоянии не менее 10 мс.
  • Тогда мы прерываем прием команды.

 

Полностью проект можно загрузить здесь Lesson23_3.

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

Теперь “лишний” принятый бай не “подвешивает” обмен. Я проверил.

 

По поводу библиотеки LL.

Функции библиотеки LL для управления UART принципиально отличаются от HAL/UART-функций. Они обеспечивают только доступ к регистрам и отдельным битам UART. Работа с ними ничем не отличается от управления UART с использованием регистров CMSIS. Об этом написано в уроке 20.

Поэтому я не буду создавать урок по LL-библиотеке для UART. А только опишу функции LL в справочнике.

 

О работе UART с использованием прямого доступа к памяти я расскажу позже после изучения контроллера DMA.

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

0

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

не в сети 2 недели

Эдуард

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

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

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

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