Обновить

Как Работать с UART на Микроконтроллерах (UART + FIFO = LOG)

Уровень сложностиПростой
Время на прочтение14 мин
Охват и читатели13K
Всего голосов 8: ↑8 и ↓0+9
Комментарии29

Комментарии 29

Есть много других вариантов организации отправки логов. Можно ждать отправки не всегда, а только в случае, если в программном буфере TxFIFO не достаточно места, чтобы добавить туда ещё одно сообщение, если позволяет логика программы, и с таймаутом. Можно вообще хранить сообщения об ошибках не в конечном виде (в данном случае текстовом, хотя возможны и много других вариантов), а в виде FIFO из специальных объектов с кодом ошибки, параметрами, и таймштампом, и преобразовывать их в выходной формат в одной из задач в суперцикле - это позволяет сильно экономить память на мелких микроконтроллерах. При этом число различных вариантов ошибок обычно ограничено, а если они повторяются быстрее отправки - сообщения о них можно объединять для экономии памяти. Ну а отправка по DMA вообще мало связана со способом добавления сообщений в TxFIFO - её можно использовать во множестве случаев, это больше зависит от используемого микроконтрроллера и скорости передачи данных. Разумеется, буфер TxFIFO должен быть выделен статически.

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

Гениально!

Можно ждать отправки не всегда, а только в случае, если в программном буфере TxFIFO не достаточно места, чтобы добавить туда ещё одно сообщение, если позволяет логика программы, и с таймаутом. 

Да. У меня это выглядит так

/*Wait until a free space appears in the Tx Queue*/
bool UART_WaitFifoSpace_LL(UartHandle_t* node, uint32_t size) {
    bool res = false;
    if(node->init_done) {
        uint32_t cnt = 0;
        uint32_t up_time_start = TIME_GetMs();
        while(1) {
            cnt++;
            uint32_t spare = FIFO_GetSpare(&node->TxFifo);
            if(size <= spare) {
                res = true;
                break;
            }
            uint32_t up_time = TIME_GetMs();
            uint32_t diff = up_time - up_time_start;
            if(200 < diff) {
                res = false;
                break;
            }
        }
    } else {
        res = false;
    }
    return res;
}

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

зачем вообще это делать на микроконтроллерах? Логи наверно на большой ПК приходят, наверно там попроще будет

преобразовывать их в выходной формат /!

А из

много других вариантов

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

зачем вообще это делать на микроконтроллерах? Логи наверно на большой ПК приходят, наверно там попроще будет

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

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

Да. Бинарные протоколы компактнее текстовых. Это факт.

Рискну посоветовать две вещи:
Первая (очевидная, да, кэп!) - не использовать один и тот же UART для логов и для связи с чем-то другим. Все-таки stdin, stdout и stderr это разные файлы, даром, что они почти всегда прицеплены к консоли.

Вторая: попробуйте пользоваться вызовами RTOS. По крайней мере FreeRTOS на серии ESPxx сама расставит мьютексы при инициализации драйвера и две задачи не попытаются писать в один UART каждая свое. Другими словами, многое из ПО системного уровня уже написано до нас ))

Мне на работе запрещено использовать FreeRTOS так как это "не доверенный код".

Все конечно же ради безопасности и надежности. В прочем, ничего нового.

А Eclipse ThreadX (ThreadX) относится к доверенному коду?

Если что - Eclipse ThreadX (ThreadX) сертифицирована для критически важных приложений безопасности. 

The following standards have been used for certification:

  • IEC 61508-3:2010; clause 7.4.2.12, route 3S

  • IEC 62304:2015

  • ISO 26262-8:2018; clause 12

  • EN 50128:2011; clause 7.3.4.7

Спасибо.

А HAL, значит, "доверенный код?

сама расставит мьютексы при инициализации драйвера и две задачи не попытаются писать в один UART каждая свое. 

У меня NoRTOS прошивка.

Первая (очевидная, да, кэп!) - не использовать один и тот же UART для логов и для связи с чем-то другим.

Как же тогда один кабель Ethernet используется для сотен протоколов внутри трафика?

Обычно это делается при помощи драйверов разного уровня. L2, канальный уровень, выше него L3 и т.д., все эти stdout и stderr - они значительно выше. Я бы самостоятельно писать TCP/IP стэк на уровне "отправь блок в контроллер eth - получи блок из контроллера eth" не стал, долго это и трудоемко, если качественно делать, а самое главное, что оно уже готовое есть, написано. Разобраться как написано и адаптировать при необходимости - надо, а вот с нуля писать ...

Уже сделано это не про эмбеддед, кмк. Вообще культура переиспользования не развита почти никак, ввиду очень сильной зарегулированности и традиционной ригидности.

Единственное, где оно развито в А-word,. Его не то, что бы любят, но, что я видел в либах, оно там часто реально лучше написано, чем стандартный код стандартного встраиваемого деда.

Спасибо за развернутый ответ, я "в теме", я сам эбмеддед. Но культура - не мы ли сами ее такой делаем?

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

Из собственного опыта: когда я был еще только кодером я сталкивался с подобными ограничениями. Ключевым оказался не спор, а бизнес-аргумент. Я приходил к руководству с двумя вариантами: "Мы можем получить надежный продукт через N месяцев, используя проверенные открытые решения, или потратить почти в два раза больше времени, создавая и отлаживая свой аналог с теми же рисками". Когда риски и сроки становятся измеримы, здравый смысл часто побеждает. И это неважно, руководство гражданское или военное, любому руководству важен результат вовремя.

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

Классный разбор! 👍

Спасибо. Надеюсь заметка поможет Вам в программировании микроконтроллеров.

А что если буфер приемника забился? Мы туда пихаем, а он не может. XON/XOFF?

Надо ждать в for(;;) пока UART трансивер под освободит TxFIFO и вложить туда новые данные.
Или просто увеличивать счетчик об ошибках.

Никогда не ждите событий в бесконечных циклах - в них и поляжете. В baremetal прошивке по-хорошему должен быть только 1 бесконечный мастер цикл, иные должны иметь тайм-аут с возможностью его обработать (хотя бы светодиодом поморгать, чтобы дать понять, что словили ошибку при ожидании события), либо берете решение на RTOS. Если на этапе отладки падать - это нормально и даже полезно, то в проде лучше так не делать.

Никогда не ждите событий в бесконечных циклах - в них и поляжете.

У меня как раз есть условие выхода

            uint32_t up_time = TIME_GetMs();
            uint32_t diff = up_time - up_time_start;
            if(200 < diff) {
                res = false;
                break;
            }

С давнего времени использую кольцевые буфера. Функция write, если uart свободен, пишет в него, остальное в кольцевой буфер, если занят то все в кольцевой буфер, в функции запрещается прерывание только от этого uart. И забываем, дальше в прерывании буфер постепенно выгребается в железо. Размер буфера прикидывается из интенсивности обмена, если критично, то перед записью проверяем свободное место в буфере (при подобранном размере буфера переполнение практически невозможно, если только не превысить пропускную способность на данной скорости). Логи из других прерываний пишу только во время низкоуровневой отладки, да и то редко когда они дерутся между собой. А так - в прерывании ставим флаг - пишем только в фоне, в критической секции запрещаем не глобально, а конкретное прерывание. И никаких блокирующих функций!

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации