SoftSerial: Программный UART


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


Анализ подходов
Основная сложность программного UART заключается в приеме байта. Поэтому разберем возможные подходы к решению этой задачи.
Прием
Чисто математически можно запрограммировать 256 корреляторов и выхватывать байты по свертке с опорным сигналом. Но этот способ потребует очень много вычислений.
С другой стороны можно подойти к синтаксическому разбору UART так же как при синтаксическом разборе сигнала с пульта телевизора. Есть отдельный драйвер внешних прерываний (EXT_INT). Этот драйвер выдает события. Событие это направление перепада напряжения и UpTime этого перепада. В обработчике внешних прерываний события складируются в очередь RxFIFO.
Далее в функции main в супер цикле программного UART надо опрашивать очередь событий на GPIO Rx. Если пауза больше двух бит (рассчитывает по битовой скорости), значит надо ожидать новый кадр. Кадр это 1 байт, спереди которого стартовый бит, сзади бит четности и стоповые биты. Один или два в зависимости от настроек. Временной интервал между изменениями состояний на GPIO должен быть равен длительности бита. Дребезг не обрабатывается. Цель программного UART в том чтобы выделить байт данных из сырого потока событий на линии прерываний. Событие - это пара чисел: направление перепада напряжения и временная отметка этого перепада. Приемник должен сам догадываться какой логический уровень на входе используя арифметику событий. Из потока событий надо сформировать поток битов. Для этого надо вычислять на лету интервал времени между событиями и оценивать сколько целых битов туда может упаковаться. Поток бит передавать на формирователь байта. Формирователь байта и анализирует логическую структуру кадра. На выходе выдаст битовое поле, которое состоит из стартового бита, 8 бит данных, бита четности и стопового бита (одного или два). В случае совпадения бита четности байт данных будет передан на выход приемника программного UART. Здорово? Забудьте про это! При приеме байта 0xFF алгоритм основанный на арифметике событий просто заклинит.
Реализация программного 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. При этом передаем принятый массив из 12*rxOversampling сэмплов на дециматор. При желании дециматор может делать электрическое голосование для борьбы с дребезгом контактов. На выходе дециматора получаем битовое представление UART кадра. По отступу 1 бит и будет лежать принятый байт. При желании можно проверить бит чётности.

Принятый байт помещаем в очередь принятых байтов. Обработку потока достаточно удобно отлаживать отдельно модульными тестами на конкретных массивах входных данных. IDLE переходим в случае продолжительной тишины на пине UART_RX. Формальным критерием тишины может быть состояние RX пина, когда принято более чем 12*rxOversampling сэмплов единиц.
Выделение байтов из потока семплов это процесс реального времени. Каждый кусок массива должен быть обработан до того как прочитаете соседняя часть массива. Иначе произойдет срыв потока и данные будут безвозвратно утрачены.
Все остальные протоколы поверх UART будут просто читать байты из очереди RxFIFO и уже своими отдельными конечными автоматами выделять протокольные пакеты из потока байт.
В общем структура программного UART получается вот такая.

Причечание:
1) Для оперирования с однобитными семплами вам, очевидно, потребуется и однобитная FIFO. Однобитная FIFO - это такая абстрактная структура данных, которая работает по принципу первый пришел первый ушел. При этом её внутренняя память упаковывает семплы максимально плотно. То есть на один семпл один бит. Делается это очевидно из соображений экономии RAM памяти.
2) Для исключения повреждения данных внутри однобитной FIFO при одновременном доступе из main и DMA прерываний надо соблюдать атомарность доступа к очереди. То есть отключать прерывания на операциях push и pull.
3) Внутри функции sw_uart_send() не должно быть вызовов логирования. Иначе возникнет рекурсия, которая переполнит стековую память и прошивка свалится в HardFault.
Отладка.
Я буду отлаживать 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
1) Можно сконфигурировать на любом пине, на любой битовой скорости
2) Можно принимать до 16ти RX пинов, так как GPIO по DMA читает все 16 пинов.
3) Можно регулировать количество прерываний. Вплоть до того что на один переданный байт будет меньше чем 1 DMA прерывание.
Недостатки программного UART
1) Требует RAM память для очередей.
2) Требует процессорное время для ЦОС обработки при декодировании пакета из потока семплов
3) Занимает два потока DMA
4) Занимает от одного до двух аппаратных таймеров
5) Требует жесткого реального времени на обработку массива семплов
Итог
Удалось реализовать программный UART на основе DMA аппаратного таймера и GPIO. Эксперименты показали, что отправка работает корректно.
Как видите, создание программного UART это весьма нетривиальная задача с целым ворохом зависемостей.
1) Двоичный ЦАП (для отправки)
2) Двоичный АЦП (для приема)
3) Двоичная FIFO (для отправки)
4) Драйвер внешних прерываний (для приема)
5) Драйвер аппаратного таймера
6) Драйвер DMA сопроцессора
7) Драйвер GPIO
8) Конечные автоматы синтаксического разбора.
9) Дециматор
10) Программное голосование за значащий бит
11) Функция вычисления четности (не обязательно)
Надеюсь этот текст поможет кому-нибудь решить проблему отсутствия аппаратного UART и тоже запустить программный UART.
Словарь
Сокращение | Расшифровка |
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 |
Декодирование 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. Можете ли вы дать какие-нибудь советы по реализации этого? |
Вопросы
1) Зачем в UART 2 стоповый бит, если 1 стоповый бит даёт большую реальную скорость данных? Для подключения на лету в режиме DMA трансляции. Чтобы приемник мог мгновенно синхронизоваться на первом же байте.
2) Зачем в UART интерфейс заложили 7-битный режим? Ведь в это даже не вмещается 1 байт.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.