
Порой возникает ситуация, что тебе дают электронную плату, а там на отладочный разъём не выведен UART. У меня такая история была уже трижды. При этом предстоит сложная отладка и надо помощь CLI, которую некуда вывести. Или другой случай. Все аппаратные UART‑ы заняты под трансиверы и модули. При этом нужен еще один UART для отладки. Еще есть микроконтроллеры совсем без аппаратного UART. Либо использовать что‑то типа Segger RTT. Одкано из-за отсутствия прав администратора на рабочем PC блокируется сама возможность подключаться к локальным сокетам. Поэтому Segger J-link RTT отпадает. В этом случае одним из решений может быть пуск программного UART.
Теория
UART — это синхронный последовательный полнодуплексный интерфейс передачи байтов по проводам. Каждый микроконтроллер обладает аппаратным UART трансивером.
UART кадр — это битовый пакет который содержит 1 стартовый бит, 8 бит данных, бит четности и стоповые биты.


Анализ подходов
Основная сложность программного UART заключается в приеме байта. Поэтому разберем возможные подходы к решению этой задачи.
" Поймать летящий мяч сложнее, чем его кинуть. "
Прием
Чисто математически можно запрограммировать 256 корреляторов (на каждый байт в отдельности) и выхватывать байты по свертке с опорным кодовым сигналом. Но этот способ потребует очень много вычислений FIR фильтром.
С другой стороны можно подойти к синтаксическому разбору UART так же как при синтаксическом разборе сигнала с пульта телевизора. Есть отдельный драйвер внешних прерываний (EXT_INT). Этот драйвер выдает события. Событие это направление перепада напряжения и UpTime этого перепада. В обработчике внешних прерываний события складируются в очередь RxFIFO.
Далее в функции main в супер цикле программного UART надо опрашивать очередь событий на GPIO Rx. Если пауза больше двух бит (рассчитывается по битовой скорости), значит надо ожидать новый кадр. Кадр это 1 байт, спереди которого стартовый бит, сзади бит четности и стоповые биты. Один или два в зависимости от настроек. Временной интервал между изменениями состояний на GPIO должен быть равен длительности бита. Дребезг не обрабатывается. Цель программного UART в том чтобы выделить байт данных из сырого потока событий на линии прерываний. Событие — это пара чисел: направление перепада напряжения и временная отметка этого перепада. Приемник должен сам догадываться какой логический уровень на входе используя арифметику событий. Из потока событий надо сформировать поток битов. Для этого надо вычислять на лету интервал времени между событиями и оценивать сколько целых битов туда может упаковаться. Поток бит передавать на формирователь байта. Формирователь байта и анализирует логическую структуру кадра. На выходе выдаст битовое поле, которое состоит из стартового бита, 8 бит данных, бита четности и стопового бита (одного или два). В случае совпадения бита четности байт данных будет передан на выход приемника программного UART. Здорово? Забудьте про это! При приеме байта 0xFF алгоритм основанный на арифметике событий просто заклинит. В случае приема 0xFF у вас будет только два события и такой алгоритм не поймет, где заканчивается UART кадр.
Реализация программного UART
UART это интерфейс без самосинхронизации. К этому приходится приспосабливаться в реализации программного UART. Ядром программного UART являются два программных компонента: двоичный ЦАП и двоичный АЦП. Именно на этих столпах и построен мой вариант программного UART.
Отправка байт через программный UART (двоичный ЦАП).
С отправкой всё просто. Каждый раз, когда мы вызываем функцию отправки байта через программный UART формируется UART кадр в виде битового потока (bitstream). Этот пучок битов помещается в бинарную очередь на отправку для BIN DAC. Тут же инициируется отправка массива семплов. Как только BIN DAC закончит отправку вызывается обработчик окончания отправки по DMA2. Прямо в этом обработчике следует прочитать очередь на отправку семплов и в случае нахождения там новых битов тут же инициировать новую отправку по BIN DAC. И так до тех пор пока все биты не будут отправлены. Самую первую отправку в двоичный ЦАП инициирует непосредственно функция sw_uart_send(..).
Важен один момент. Чтобы не разорвать UART кадр минимальная отправка должна быть не менее размера одного кадра в семплах. Размер отправки должен быть кратным размеру одного кадра. Этот минимальный размер семплов зависит от значения передискретизации UART бита. При этом размеры каждой отдельной отправки должны быть кратными размеру одного кадра на данной частоте дискретизации. То есть нельзя разрывать байты в проводе UART_TX.
Для отправки частота дискретизации BIN DAC может совпадать с битовой скоростью данного UART интерфейса. То есть передискретизация равна единице.
Приём байтов через программный UART
С отправкой разобрались. Однако как быть с приёмом? Обработка делится на два этапа: захват и обработка.
Захват потока:
Захват семплов это процесс жесткого реального времени. Тут можно использовать тот же алгоритм, который используется в диктофонах. Слушать пин UART_RX при помощи GPIO циклическим DMA. Двумя массивами. Пока записывается часть 1 обрабатывается часть 2.
Пока записывается часть 2 обрабатывается часть 1. Обработка происходит в суперцикле внутри функции main.

Выделение байтов из голого потока семплов это процесс реального времени. Каждый кусок массива должен быть обработан до того как прочитаете соседняя часть массива. Иначе произойдет срыв потока и данные будут безвозвратно утрачены.
Обработка потока
На данном этапе у нас есть Си‑функция, которая получает нули и единицы. Так каждому принятому сэмплу будет оказано внимание. Ее задача выхватывать UART пакеты.
Захват пакета
Это классическая задача для конечного автомата (KA). Входы КА — это ноль или единица. Выходы это массив семплов в начале которого UART кадр. Состояний тут может быть всего два: IDLE и REC. При инициализации мы находимся в Idle. В случае появления семпла 0 переходим в состояние rec и остаемся в rec пока не примет (1+8+1+2=12 бит). Учитывая частоту дискретизации это 12*rxOversampling сэмплов. При переполнении счетчика rxCnt сваливаемся обратно в IDLE. Обработку потока достаточно удобно отлаживать отдельно модульными тестами на контрольных массивах входных данных.

В состояние IDLE переходим в случае продолжительной тишины на пине UART_RX. Формальным критерием тишины в UART может быть такое состояние RX пина, когда принято более чем 12*rxOversampling сэмплов из одних сплошных единиц.
Снятие с несущей
Вот мы получили последовательность в которой лежит UART пакет. Передаем принятый массив из 12*rxOversampling сэмплов на дециматор. При желании дециматор может делать электрическое программное голосование для борьбы с дребезгом контактов.
Обработка кадра
На выходе дециматора получаем голое битовое представление UART кадра. По сути получаем готовое битовое поле.
typedef union { uint32_t dword; uint16_t word[2]; struct { uint32_t start_bit:1; uint32_t byte:8; uint32_t parity:1; uint32_t stop1:1; uint32_t stop2:1; uint32_t res:20; }; }SwUartFrameRx_t;
По отступу 1 бит и будет лежать наш принятый байт. При желании можно проверить бит чётности.
/* Возвращает 1, если количество единичных бит нечётное (odd [2 4 6 8]), иначе 0 (1 3 5 7 9) 1 = odd number of ones, 0 = even */ uint8_t u8_calc_parity_bit(const uint8_t in_data) { uint8_t data = in_data; uint8_t parity = 0; while (data) { parity ^= (data & 1); data >>= 1; } return parity; } /* Even parity: бит дополняет число единиц до чётного */ uint8_t uart_even_parity(const uint8_t data) { return u8_calc_parity_bit(data); } /* Odd parity: бит дополняет число единиц до нечётного */ uint8_t uart_odd_parity(const uint8_t data){ return u8_calc_parity_bit(data) ^ 1; }
Также можно проверить, что старт бит в самом деле ноль, а стоповые биты в самом деле единицы.
Принятый байт помещаем в очередь принятых байтов. Все остальные протоколы поверх UART будут просто читать байты из очереди RxFIFO и уже своими отдельными конечными автоматами выделять протокольные пакеты из потока байт.
В общем структура программного UART получается вот такая.

Причечание:
1) Для оперирования с однобитными семплами вам, очевидно, потребуется и однобитная же FIFO. Однобитная FIFO — это такая абстрактная структура данных, которая работает по принципу первый пришел первый ушел. При этом её внутренняя память упаковывает семплы максимально плотно. То есть на один семпл предусмотрен один бит. Делается это, очевидно, из соображений экономии RAM памяти.
2) Для исключения повреждения данных внутри однобитной FIFO при одновременном доступе из main и DMA прерываний надо соблюдать атомарность доступа к очереди. То есть отключать прерывания на операциях push и pull. В программировании это называется критической секцией.
3) Внутри функции sw_uart_send() не должно быть вызовов логирования. Иначе возникнет рекурсия, которая переполнит стековую память и ваша прошивка свалится в HardFault.
4) Для отладки прошивок не достаточно только одной отправки в UART. Представьте себе типовую прошивку размером 320 kByte Flash памяти, которая собрана из 80...120-ти программных компонентов. Если каждый программный компонент будет лить в UART свои отладочные логи, то вы банально не успеете ничего прочитать и понять. У Вас на экране будет Ниагарский водопад из белых логов. При этом всякая долгая печать в UART ещё и занимает системную шину, DMA каналы, отвлекает ядро своими прерываниями и бизнес логика (функционал) на микроконтроллере поэтому исполняется медленнее. Эта проблема полностью решается, когда у вас есть возможность отправить строку в прошивку. Эдакую команду. То есть присутствует полудуплексная (или полнодуплексная) CLI. С CLI вы можете включать или откл. логи для каждого конкретного программного компонента в отдельности. Более того, с CLI Вы можете для каждого отдельного программного компонента в прошивке включать и отключать уровни логирования: INFO, NOTICE, WARNING, DEBUG, PARANIOD, TRACE в зависимости от глубины которую вы готовы сейчас анализировать.

У меня про это есть отдельная методичка: Отладка программ уровнями логирования (или медицинская карта вашей программы)
Отладка.
Я буду отлаживать UART на широко распространенной учебно‑тренировочной электронной плате JL_32F4XX с микроконтроллером STM32F407ZGT6 на борту. Для проведения теста надо соединить аппаратный UART2 c пинами программного UART‑a. По сути надо установить только два джампера.
GPIO | PinMux | Dir | EndGPIO | Пояснение |
PA2 | USART2_TX | Out | PA1 | ToTest |
PA3 | USART2_RX | in | PA4 | ToTest |
PA4 | SW_USART_TX | out | PA3 | Tested |
PA1 | SW_USART_RX | in | PA2 | Tested |
PA9 | UART1_TX | out | на TeraTerm | CLI |
PA10 | UART1_RX | in | на TeraTerm | CLI |
схематически это можно показать так

Далее отправлять байты в оба направления и убеждаться, что происходит обоюдный приём.
Достоинства программного UART на основе сцепки GPIO+DMA2+TIMER
1) Можно сконфигурировать UART абсолютно на любом GPIO пине.
2) Можно принимать до 16ти RX пинов, так как GPIO по DMA читает все 16 пинов.
3) Можно регулировать количество прерываний просто циферкой в конфиге. В частности, что на один переданный UART байт будет меньше чем одно DMA прерывание. Например, мы можем принять 1000 байт и у нас будет только два DMA2 прерывания: половина приема и полный прием. Мы можем отправить 1000 UART-овых байт в виде массива семплов и у нас будет только одно DMA2 прерывание на конец перемещения.
4) Можно запрограммировать несколько экземпляров программного UART. Ограничение это только каналы DMA.
5) Частые прерывания по аппаратным таймерам вообще не вызываются.
7) SPI не используется.
8) Внешние прерывая EXT INT совершенно не используются.
9) Можно задать совершенно любую битовую скорость. Это особенно полезно, учитывая температурный дрейф тактирования UART на другой стороне. То есть можно не только задать любую частоту, дак можно ещё и задать частоту близкую к предыдущей частоте, как в + так и в -.
10) Простота реализации. Тут просто даже нечему ломаться.
Недостатки программного UART
1) Требует RAM память для очередей отправки и приема.
2) Требует процессорное время для ЦОС обработки при декодировании пакета из потока семплов
3) Полноценный программный UART занимает два потока DMA.
4) Занимает от одного до двух аппаратных таймеров.
5) Требует жесткого реального времени на обработку массива семплов
Итог
Удалось реализовать программный UART на основе сцепки DMA2, аппаратного таймера и GPIO. Эксперименты показали, 100%-ную надежность отправки и приема программным UART на битовых скоростях: 110, 300, 600, 1200, 2400, 4800, 9600 и 14400 бит/c. Собрана отдельная demo прошивка с CLI терминалом, работающим полностью на этом программном UART.

Как видите, создание программного UART это весьма нетривиальная задача с целым ворохом всяческих зависимостей:
1) Двоичный ЦАП (для отправки)
2) Двоичный АЦП (для приема)
3) Двоичная FIFO (для отправки)
4) Функция вычисления четности (не обязательно)
5) Драйвер аппаратного таймера
6) Драйвер DMA сопроцессора (только DMA2)
7) Драйвер GPIO
8) Конечный автомат захвата пакета из потока семплов.
9) Дециматор
10) Программное голосование за значащий бит
Вот, пожалуй, и всё.

Надеюсь этот текст поможет кому‑нибудь тоже решить проблему отсутствия аппаратного UART, запустить полноценный программный UART и пользоваться shell отладкой.
Словарь
Сокращение | Расшифровка |
UART | Universal Asynchronous Receiver/Transmitter |
ЦАП | Цифро‑аналоговый преобразователь |
FIFO | First In, First Out |
АЦП | Аналого‑цифровой преобразователь |
DMA | Direct Memory Access |
GPIO | General Purpose Input/Output |
Ссылки
Название | ссылка |
Исходные коды прошивки программного UART | https://github.com/aabzel/trunk/tree/main/source/projects/jl\_32f4xx\_sw\_uart\_gcc\_m |
SoftSerial: Двоичный ЦАП на STM32 | |
SoftSerial: Двоичный АЦП на STM32 (или самодельный логический анализатор) | |
Однобитная FIFO | https://github.com/aabzel/trunk/tree/main/source/adt/bit\_fifo |
CLI через Segger J‑Link RTT на ARM Cortex‑M | https://habr.com/ru/articles/1018168/?ysclid=muiuiyf68e761679509 |
Отладка программ уровнями логирования (или медицинская карта вашей программы) | |
Декодирование IR сигнала с TV (или исследование пультовых лучей) | |
Почему нам нужен интерфейс командной строки? | |
UART (Universal Asynchronous Receiver/Transmitter) | |
Demo прошивка программного UART | https://github.com/aabzel/Artifacts/tree/main/jl_32f4xx_sw_uart_gcc_m |
STM32F4XX | |
Простейшая программная реализация UART для микроконтроллера | |
Как наш shell похорошел @Katbert | |
Программный UART на Attiny13 | |
Как Работать с UART на Микроконтроллерах (UART + FIFO = LOG) | |
ПРОГРАММНЫЙ UART В MICROCHIP STUDIO | https://madmentat.ru/index.php/2-statejki/198-programmnyj‑uart‑v-microchip‑studio |
Я пытаюсь реализовать программный UART для взаимодействия с устройством, так как у меня больше нет UART (hw) на моем MCU. Можете ли вы дать какие‑нибудь советы по реализации этого? |
Вопросы
Зачем в UART 2 стоповый бит, если 1 стоповый бит даёт большую реальную скорость данных? Для подключения на лету в режиме DMA трансляции. Чтобы приемник мог мгновенно синхронизоваться на первом же байте.
Зачем в UART интерфейс заложили 7-битный режим? Ведь в этот кадр даже не вмещается 1 байт.
По какому алгоритму в интерфейсе UART вычисляется бит чётности? Как выглядит Си функция вычисления четности для UART байта?

