
«Во всех самолётах есть черный ящик. A UART‑логирование это черный ящик вашей прошивки.»
Допустим вы решили делать в своей прошивке printf- отладку. Или даже забабахать UART-CLI (Shell). Многие про нее слышали и некоторые ей пользуются. Или у ваc есть какое -то внешнее устройство конфигурируемое по UART. Например микросхема U-Blox со своим UBX протоколом. Или LTE модуль с AT-командами. Первое с чем Вы столкнетесь - это настроить UART-трансивер. Как же реализовать алгоритм работы с UART периферией?
С приёмом все просто. Настраиваем прерывание по приему байта и принятый байт кладем в ту или иную программную очередь RxFIFO. Для CLI, Для NMEA для ModBus, AT-команд и т. п. Если вы гарантированно знаете сколько должны принять за раз, то можно даже настроить прием по DMA сразу, условно, 512 байт.
Куда интереснее с тем, как быть при отправке массивов. UART это тот редкий случай интерфейса, когда отправка сложнее, чем прием. Сейчас объясню почему. Для начала вспомним немного определений.
Теория
UART - двухпроводной полнодуплексный последовательный способ передачи байт между микроконтроллерами.
FIFO - абстрактная структура данных, которая позволяет добавлять и извлекать элементы по принципу первый пришел первый ушел.
Прерывание (Interrupt Trap) — событие в микропроцессоре, которое провоцирует замену значений регистров процессора и вызов отдельной функции ISR. После исполнения функции ISR управление возвращается обратно в прерванную функцию main со старыми значениями регистров процессора. Как правило, прерывания используются для работы с внешними периферийными устройствами SoCa: Timer, UART, DMA и пр
Суперцикл — тот код, который бесконечно снова и снова исполняется внутри while(1) в функции main()
Критическая секция — участок кода, выполнение которого не должно быть прервано прерываниями процессора.
Вот пожалуй и все определения, которые сегодня потребуются.
В чем проблема?
Неясно куда писать логи, когда UART трансивер еще не проинициализирован. Надо же как-то узнать причину ошибки при инициализации подсистемы тактирования или во время инициализации SysTick, Timer, VectorTable, GPIO. Вообще говоря, до настройки UART в прошивке происходит целая куча всяческих действий про результат отработки которых хорошо бы получить строчку в логе загрузке прошивки. Это настройка Fpu, CORE, ISRTable, Hal, Log, Time, Writer, Int, SysTick, Clk, GPIO, Flash. Хотелось бы про каждый программный компонент получить строчку со статусом выполнения в логе загрузки прошивки. Это же очевидно.
Если вы будете отправлять локальный массив по UART (из стека), то отправляемые данные исказятся при выходе из функции и заходе в следующую. Ибо трансивер работает сам по себе, а код тоже исполняется сам по себе.
Если выделять динамическую память для того, чтобы положить туда данные для отправки, от это тоже не вариант, так как можно забыть освободить эту память.
Если выделить статическую память фиксированного размера (например 64 байт), то непременно появится запрос отправить массив большей длинны (120 байт) и его некуда будет положить чтобы оттуда отправить.
Как быть, если логи появляются быстрее, чем битовая скорость UART?
Как же распетлить весь этот ворох проблем?
Уровень1 (отправка не отходя от кассы)
Самое простое дать команду отправить массив и тут же ждать окончания отправки в бесконечном цикле сразу после функции HAL_UART_Transmit_IT.
bool UART_Send(UartHandle_t* const Node,
const uint8_t* const data,
uint32_t len) {
bool res = false;
// make sure that global ISR enabled
// We send mainly from Stack. We need wait the end of transfer.
if(Node) {
if(Node->init_done && len && data) {
uint32_t start_us = TIME_GetUs();
HAL_StatusTypeDef stat = HAL_UART_Transmit_IT(&Node->uart_h,
data,
len);
if(HAL_OK == stat) {
res = true;
/* We send from Stack. We need wait the end of tx.
Otherwise tx data will not be currupted */
while(false == Node->tx_done) {
uint32_t cur_us = TIME_GetUs();
uint32_t dutation_us = cur_us - start_us;
if(9999999 < dutation_us) {
res = false;
Node->tx_time_out_cnt++;
break;
}
if(HAL_UART_STATE_READY == Node->uart_h.gState) {
res = true;
break;
}
}
}
}
}
return res;
}Плюсы:
+Это очень просто реализовать. Дали отмашку на отправку массива и сидим ждем окончания отправки. По сути это беспонтовый режим polling-а.
+Не нужно дополнительной памяти. Всё работает на стеке.
+Можно отправлять в UART из стековой RAM памяти
+Отправляемые данные не теряются в принципе. Всё что вы хотим отправить контролируемым образом отправляется прямо перед нами. Мы не уйдем из функции uart_send(...) пока всё окончательно не отправится.
+Если зависнем, то в PuTTY хоть останется лог с целеуказанием того места, где мы зависли.
Минусы:
-Процессор работает в холостую пока ждет окончания отправки массива. Чем больше логирования, тем больше холостой работы процессора. Тем ниже производительность. Если скорость 9600 бит в сек, то это совсем не здорово. Если скорость 460800 бит/c то 1 бит отправляется за 2.1 us. Байт отправляется 26 us. 80 байт уйдут за 2ms. CPU занимается обогревом атмосферы.
-Долго происходит печать. Долго отрабатывает функция инициализации.
-Вы не можете пользоваться логированием до того, как проинициализируете UART трансивер.
Если вы сделали такую реализацию отправки, то в большой прошивке вы заметите, что прошивка ощутимо долго стартует. Обычно много логов генерирует именно функция system init. У меня на 460800 суперцикл стартует только после 1.041 cекунд с момента подачи электропитания питания. Это долго. Заметный затуп прошивки, который со временем сильно раздражает разработчика.
Уровень 2 (отправка в супер цикле)
Идея в том, чтобы при отправке в UART не отправлять байты физически, а просто складировать байты в Tx FIFO и идти дальше делать свои дела.
bool UART_Send(const uint8_t num,
const uint8_t* const data,
uint32_t size) {
bool res = false;
if(size) {
if(data) {
UartHandle_t* Node = UART_GetNode(num);
if(Node) {
res = FIFO_PushArray(&Node->TxFifo,
data,
size);
if(false == res) {
Node->tx_error_cnt++;
}
}
}
}
return res;
}А саму отправку делать в суперцикле в отдельной задаче. Выгребать из очереди байт и отправлять их. Можно даже по DMA сразу массив взять и отправить.
Плюсы
+Вы можете пользоваться логированием до инициализации UART трансивера. Данные просто будут лежать в FIFO и ждать своего момента отправки в очереди на отправку.
Минусы
-Если зависнем в init функции до запуска суперцикла с задачей отправки, то программист так и не узнает почему и где зависла прошивка. Придется только включать пошаговый отладчик.
-Очередь может переполниться и вы потеряете ценнейшие логи, которые так никогда и не увидят белый свет.
Уровень 3 (отправка в прерывании)
При отправке по-прежнему складировать в очередь. Отправлять байт в прерывании по окончанию отправки байта. По сути отправка происходит в прерывании.
bool UART_TxNext(const uint8_t num) {
bool res = false;
UartHandle_t* Node = UartGetNode(num);
if(Node) {
if(HAL_UART_STATE_READY == Node->uart_h.gState) {
uint32_t outLen = 0;
res = FIFO_PullArray(&Node->TxFifo,
Node->txBlock,
sizeof(Node->txBlock),
&outLen);
if(res) {
HAL_StatusTypeDef ret = HAL_UART_Transmit_IT(&Node->uart_h,
Node->txBlock,
outLen);
res = HALretToRes(ret);
if(res) {
Node->tx_start_ms = time_get_ms();
Node->tx_done = false;
}
}
}
}
return res;
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef* uart_handle) {
int8_t num = 0;
num = get_uart_index(uart_handle->Instance);
UartHandle_t* Node = UartGetNode(num);
if(Node) {
Node->tx_done = true;
Node->tx_cnt++;
UART_TxNext(num);
}
}
Но это даже не отправка, а отмашка на отправку. Пока байт отправляется процессор возвращается в main и делает какую-то нужную работу.
Плюсы
+Вы можете пользоваться логированием до инициализации UART трансивера. Байты просто будут лежать в очереди и ждать своего момента на отправку. Произойдет это тотчас же как освободится UART трансивер.
+Очередь постоянно и непрерывно освобождается. UART трансимвер пожирает TxFIFO непрерывно.
+Достоинство печати в очередь в том, что вы можете делать логирования даже в прерываниях. Ведь логирование просто сводится к заполнению очереди на отправку.
Минусы
1-Частые прерывания. В пределе UART прерывания происходят после отправки каждого байта. Ситуация осложняется тем, что в микроконтроллере могут быть настроены и включены все 18 UARTов.
2-Иной раз железо подводит. Ты отправляешь байт, а прерывание по окончании отправки не происходит. Приходится вводить time-out по которому надо сбрасывать флаг tx_in_progress=0.
3--При единоразовом вваливании в лог циклопического куска можно добавить ожидание отправки, если в программном буфере TxFIFO не достаточно места, чтобы добавить туда ещё одно сообщение.
4-На время заполнения uart tx fifo следует отключить прерывания глобально (interrupt_control_all()). Создать критическую секцию. Иначе одновременная запись очереди где-то в main и чтение этой же очереди из прерывания по окончанию отправки повредят переменные и данные очереди. На выходе вы увидите испорченный лог.
bool UART_Send(const uint8_t num,
const uint8_t* const data,
uint32_t size) {
bool res = false;
if(size) {
if(data) {
UartHandle_t* Node = UART_GetNode(num);
if(Node) {
uart_wait_fifo_space_ll(Node, size);
res = INTERRUPT_СontrolAll(false);
res = FIFO_PushArray(&Node->TxFifo,
data,
size);
if(false == res) {
Node->tx_error_cnt++;
}
res = INTERRUPT_СontrolAll(true);
}
} else {
res = false;
}
} else {
res = false;
}
return res;
}
Уровень 4 (отправка по DMA в прерывании)
Как снизить частоту прерываний? Классическое решение это активировать DMA. Если за время отправки байта очередь на отправку сильно разрослась и стала чрезмерно большая, то можно перекопировать её в отдельный глобальный массив и отправить по DMA. Это повысить производительность процессора.
UART не может отправлять чаще своей битовой скорости.
Чудес не бывает. Если очередь переполняется и байты вываливаются на пол, значит надо делать одно из двух:
-увеличивать битовую скорость UART трансивера
-увеличивать размер TxFIFO.
Итоги
Итак, подытожим. Чтобы просто нормально и эффективно работать с UART вам надо наворотить в своей прошивке целую кучу программных компонентов.
№ | Программный компонент |
1 | Драйвер тактирования |
2 | Драйвер GPIO |
3 | Драйвер DMA |
4 | Драйвер аппаратного таймера |
5 | Драйвер прерываний NVIC |
6 | Драйвер UART |
7 | Реализация FIFO |
Нормально так да? Как видите сами, чтобы просто по-человечески отправлять текст в UART надо написать мегатонны кода для CLOCK, GPIO, DMA, NVIC и забабахать безотказную FIFO-ху. С нормальным логированием вы будете редко вспоминать про GDB отладку.
Словарь
Сокращение | Расшифровка |
DMA | Direct memory access |
GPIO | general-purpose input/output |
NVIC | Nested Vectored Interrupt Controller |
FIFO | first in, first out |
UART | Universal Asynchronous Receiver/Transmitter |
Источники
Как наш shell похорошел
консоль в микроконтроллере с micro readline
Почему Нам Нужен UART-Shell?
Command line interpreter на микроконтроллере своими руками
Реализация очереди
