Метод двойной буферизации UART: удобный для прерывания
UART - отличный протокол передачи как для хобби, так и для профессиональных проектов, но в критически важных системах UART может быть сложным.
UART (Universal Asynchronous Reception Transmission) - популярный протокол для микроконтроллеров для взаимодействия между другими микроконтроллерами и компьютерами. Конструкции с низкой скоростью передачи данных с использованием высокоскоростных микроконтроллеров обычно не имеют проблем с UART. Однако на более высоких скоростях или если микро выполняет множество задач (например, в проекте ZBM), могут возникнуть серьезные проблемы, включая пропущенные байты и порядок этих байтов. Даже в системе, управляемой прерыванием, порядок может быть очень трудно сохранить. В этой статье будет рассмотрен метод, недавно разработанный для этого типа проблем, называемый двойной буферизацией UART.
Примечание: Технический жаргон (Push and Pop)
Для тех, кто не знаком со стеками, нажатие данных означает, что данные помещаются в буфер, а popping - удаление данных из буфера.
Объяснение проблемы
Представьте себе процедуру прерывания, которая выполняет одну задачу: после получения байта над UART он хранит байт в массив буфера и увеличивает счетчик totalBytes.
isr_routine() { if(UART_RECEIVE) { buffer(totalBytes) = UART_GET; totalBytes +; } }
Так как этот массив заполняется данными, наша основная программа берет байты с этого буфера, а затем вычитает из счетчика totalBytes.
program() { do{ if(totalBytes > 0) { cout << buffer(totalBytes); totalBytes --; } }while(1); }
Пока основная программа берет байты с буфера быстрее, чем байты отправляются, порядок байтов сохраняется. Однако, если программа не может быстро вывести байты, а байты добавляются в середине этого цикла (помните, что прерывание имеет приоритет над основным контуром), тогда порядок байтов будет потерян. Но что такое «порядок» байтов «Привет» над UART, и наша основная программа достаточно быстра, вывод cout (при условии, что у нас есть дисплей) также должен быть «Hello», а не «elHlo» или какой-либо другой комбинацией.

Итак, с рассмотренным порядком байтов, давайте теперь посмотрим, как этот «порядок» теряется, если основная программа не может вывести данные из буфера быстрее, чем ISR ставит данные. Для примера предположим, что за время, затрачиваемое нашей программой на получение одного байта из буфера, ISR будет вытолкнуть два байта, переданных по UART. Как будет выглядеть вывод cout? На выходе будет произнесено «elolH». Как это произошло?
- UART быстро отправляет первые два байта, «Он»
- Основная программа занимает один байт, расположенный в конце «e»,
- К этому времени UART отправил еще два байта "ll"
- Основная программа снова берет последний байт, «l»
- UART отправляет последний байт «o»
- Затем основная программа выводит данные из массива, начиная с конца до начала «lH»,
- Результатом является «elolH»
Мало того, что данные потеряли свой порядок, но это даже не наоборот! Чтение байтов назад НЕ РЕШАЕТ проблему. Даже если вы читаете от первого элемента до последнего, вы не можете настроить значение totalBytes, потому что ISR может остановить программу непосредственно перед изменением значения, поместить байт в конец массива и затем, по возвращении, основную программу может сбросить значение totalBytes (тем самым потеряв этот отправленный байт). Если программа не изменяет значение totalBytes из-за потенциальной проблемы с вмешательством в ISR, буфер может переполняться.
Существуют обходные пути, такие как использование циклических буферов, несколько счетчиков и сортировка массивов, но самый простой вариант (и один из лучших) - использование двойного буфера.
Двойной буфер
Двойной буфер можно рассматривать как два полностью отдельных блока, где процедура прерывания работает с одним устройством, а программа работает с другим устройством. Для моделирования процедура прерывания будет называться «Ядром», а функции + программы, которые не составляют процедуру прерывания, будут называться «Пользователь» (они используют данные, ядро обрабатывает аппаратное обеспечение).


Каждый блок имеет две переменные: массив с именем buffer () и счетчик, называемый bufferCount. Буфер хранит данные UART по мере их поступления, а bufferCount содержит количество переданных данных. Этот счетчик можно использовать двумя способами:
- Найдите количество данных в буфере
- Решите, куда вставлять / извлекать данные в буфер из буфера
Примечание. Самый простой способ программирования единиц и мультиплексора - использование многомерного буфера и многомерного счетного буфера.
// Each multi-array element is a unit buffer(2)(32) bufferCounter(2)(1) // Unit A Variables buffer(0)(x) bufferCounter(0)(x) // Unit B Variables buffer(1)(x) bufferCounter(1)(x)
Селектора устройств User и Kernel выполняются с использованием двух переменных: uartKernel и uartUser. Каждое из этих значений всегда противоположное. Ниже приведена таблица истинности для этих значений:
uartUser | uartKernel |
1 | 0 |
0 | 1 |
Мультиплексор решает, какое устройство будет направлено на ядро и пользователь (мультиплексор может находиться только в двух состояниях).
- Состояние A приводит к ядру с использованием блока A и пользователя, использующего блок B
- Состояние B приводит к ядру, использующему блок B, а пользователь использует блок A
Мультиплексор можно переключить, вызывая функцию switchBuffers (). Это не вызывается каждый раз, когда байт считывается из буфера, только после того, как все данные в этом буфере обработаны, и программа готова для получения дополнительной информации от UART.
Когда Пользователь считывает массив, используя следующий код, байты будут в правильном порядке.
for(int i = 0; i <= bufferCounter(uartUser); i+) { cout << buffer(uartUser)(i); }
Ядро использует следующий код для размещения данных в буфере:
isr_routine() { if(UART_RECEIVE) { buffer(uartKernel)(bufferCounter(uartKernel)) = UART_GET; bufferCounter(uartKernel) +; } }
Поскольку значения uartUser и uartKernel всегда разные (1 и 0), это означает, что пользователь и ядро всегда будут обращаться к различным буферам и счетчикам. Итак, как мы получаем информацию от ядра к пользователю? Все, что нам нужно сделать - это переключить значения uartUser и uartKernel, чтобы они указывали на буферы и счетчики друг друга. Поэтому, когда Пользователь читает новые данные, ядро может продолжать запись в неиспользуемый буфер. Чтобы сделать этот переключатель, все, что должен сделать пользователь (прежде чем он обрабатывает новые данные), является вызовом switchBuffers ().
SwitchBuffers() { uartUser = (!uartUser) &0x01; uartKernel = (!uartKernel) &0x01; // Need to reset the counter for the ISR bufferCounter(uartKernel) = 0; }
Итак, давайте посмотрим на эту технологию двойной буферизации в сценарии, когда микроконтроллер находится под большой нагрузкой, и UART передает потоковые данные с удвоенной скоростью, с которой программа может справиться с ней. Как и раньше, UART будет передавать «Hello», а программа User будет печатать символы.
- Потоки UART в «He» в ядро - Места в блок A
- Программные вызовы switchBuffers. Программа печатает «H» из блока A
- Потоки UART в «ll» в ядро - места в блок B
- Программа по-прежнему обрабатывает массив и печатает «e» из блока A
- Потоки UART в «o» в ядро - места в блок B
- Программа обработала блок A и переключает буферы. Печать программы «l»
- Программа по-прежнему обрабатывает массив и печатает «l» из блока B
- Программа по-прежнему обрабатывает массив и выводит «o» из блока B
Метод двойной буферизации полностью блокировал ISR и основную программу, позволил нам сохранить порядок, и создал очень упрощенный код с возможностью для больших буферов. Никакая сортировка массивов не требовалась, ISR не нужно было перемещать элементы для сохранения порядка, и не требовалось сложной циклической буферизации.