Фундаментальный недостаток кода последовательной связи, заключается в том, что, если пользовательский код не постоянно опрашивает USART и не готов принимать символы сразу после их поступления, существует высокая вероятность переполнения (одного) буфера приема и данные будут потеряны. В идеале нам нужно решение, которое не требует такой высокой степени внимания со стороны пользовательского кода и которое также может гарантировать, что символы не будут потеряны. Точно так же, когда пользовательский код хочет передать строку, он должен ждать, пока буфер передачи не опустеет после каждого отправленного символа.
Частичным решением является создание в программном обеспечении более крупных буферов передачи и приема и использование обработчика прерываний для управления деталями приема и передачи символов. Обработчик прерываний представляет собой пользовательскую подпрограмму, которая выполняется асинхронно с кодом пользователя, когда буфер передачи USART становится пустым или буфер приема USART заполняется. Это освобождает пользовательскую программу от бремени мониторинга буферов USART и, предоставляя дополнительное буферное пространство, помогает отделить пользовательскую программу от процесса связи. Это только частичное решение, так как после того, как больший программный буфер приема заполнится, все полученные дополнительные символы будут потеряны. Полное решение использует дополнительные сигналы управления потоком USART для блокировки связи, когда в будущем не будет места.
Реализации getchar и putchar на основе опроса испытывают значительные проблемы с производительностью. В случае putchar код приложения замедляется до скорости передачи в бодах USART всякий раз, когда он пытается передавать последовательные символы. Ситуация с getchar еще более тяжелая — если код приложения не постоянно отслеживает регистр приема данных USART, существует значительный риск потери данных. В этом разделе мы покажем, как прерывания в сочетании с программными буферами могут использоваться для смягчения, но не устранения проблемы. Полное решение требует использования аппаратного управления потоком в сочетании с прерываниями.
В управляемом прерываниями USART ответственность за прием и передачу данных делится между кодом приложения и кодом прерывания. Код прерывания вызывается всякий раз, когда происходят сконфигурированные события; например, когда регистр передаваемых данных пуст или регистр принимаемых данных заполнен. Код прерывания отвечает за удаление условия прерывания. В случае полного регистра принимаемых данных обработчик прерываний удаляет условие прерывания путем считывания полученных данных. Код приложения и обработчик прерываний взаимодействуют через пару программных буферов, реализованных в виде очередей.
Основная идея проста. Всякий раз, когда код приложения выполняет putchar, символ добавляется в очередь TX, и всякий раз, когда код приложения выполняет getchar, символ удаляется из очереди RX. Аналогичным образом, всякий раз, когда вызывается обработчик прерываний USART1_IRQHandler, обработчик удаляет условие прерывания, перемещая символ из очереди TX в регистр передаваемых данных или перемещая символ из регистра принимаемых данных в очередь RX. Все трудности реализации возникают из-за обработки крайних случаев, когда две очереди либо пусты, либо заполнены.
Проектные решения для крайних случаев различаются для приложения и кода прерывания. Наиболее важным требованием для кода прерывания является то, что он может никогда не блокироваться. Например, если регистр принимаемых данных заполнен и очередь RX также заполнена, единственный способ удалить условие прерывания — это прочитать регистр принимаемых данных и отбросить данные. Таким образом, управляемый прерыванием USART не может полностью устранить проблему потерянных данных решения на основе опроса, которое будет сопровождаться добавлением управления потоком данных. Напротив, код приложения может блокироваться. Например, если приложение выполняет putchar и очередь TX заполнена, то она может «опросить», чтобы дождаться полного удаления условия (обработчиком прерываний). В этом случае код приложения снова замедляется до скорости передачи, но только после заполнения очереди TX. Важным решением для реализации является размер очередей, чтобы предотвратить зависание приложения.
Допустим, структура данных очереди с двумя операциями:
struct Queue; struct Queue UART1_TXq , UART1_RXq; int Enqueue(struct Queue *q, uint8_t data); int Dequeue(struct Queue *q, uint8_t *data);
Операция Enqueue принимает в качестве параметров очередь и байт данных; его функция заключается в добавлении данных в очередь. Если очередь заполнена, то Enqueue должен вернуть ошибку (0). Таким образом, надежное использование операции постановки в очередь может потребовать повторных вызовов. Операция удаления аналогична, но операция удаляет байт данных из очереди. Как и в случае с Enqueue, Dequeue возвращает индикатор успеха, и для надежной работы может потребоваться несколько вызовов.
Далее мы предполагаем, что необходимое аппаратное обеспечение прерывания включено для двух условий: регистр принимаемых данных не пуст, а регистр передаваемых данных пуст. Код обработчика прерываний должен затем справиться с этими двумя возможными условиями:
static int TxPrimed = 0;
int RxOverflow = 0;
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1 , USART_IT_RXNE) != RESET)
{
uint8_t data;
// buffer the data (or toss it if there's no room
// Flow control will prevent this
data = USART_ReceiveData(USART1) & 0xff;
if (!Enqueue(&UART1_RXq , data))
RxOverflow = 1;
}
if(USART_GetITStatus(USART1 , USART_IT_TXE) != RESET)
{
uint8_t data;
/* Write one byte to the transmit data register */
if (Dequeue(&UART1_TXq , &data)){
USART_SendData(USART1 , data);
} else {
// if we have nothing to send, disable the interrupt
// and wait for a kick
USART_ITConfig(USART1 , USART_IT_TXE , DISABLE);
TxPrimed = 0;
}
}
}
Обратите внимание, что код приема и передачи содержат основные элементы реализаций опроса putchar и getchar, они по-разному обрабатывают угловые случаи. Если в очереди приема нет места, обработчик прерываний получает, но отбрасывает любые данные в регистре приема. Всякий раз, когда обработчик прерываний отбрасывает данные, он устанавливает глобальную переменную RxOverflow равной 1; это зависит от кода приложения, чтобы отслеживать эту переменную и решать, как обрабатывать потерянные данные. Если очередь TX пуста, обработчик прерываний не может разрешить условие прерывания (поскольку ему нечего записать в регистр передаваемых данных), поэтому он отключает условие USART_IT_TXE. Переменная TxPrimed используется, чтобы сообщить приложению (в частности, putchar), что прерывание необходимо повторно включить, когда данные добавляются в очередь TX.
Новая реализация getchar довольно проста (ее можно обобщить, чтобы заменить реализацию uart_getc)
int getchar(void)
{
uint8_t data;
while (!Dequeue(&UART1_RXq , &data));
return data;
}
Новая реализация для putchar требует дополнительной проверки для определения необходимости повторного включения условия прерывания передачи:
int putchar(int c)
{
while (!Enqueue(&UART1_TXq , c));
if (!TxPrimed) {
TxPrimed = 1;
USART_ITConfig(USART1 , USART_IT_TXE , ENABLE);
}
}
Асинхронное взаимодействие между кодом обработчика прерываний и кодом приложения может быть довольно тонким. Обратите внимание, что мы устанавливаем TxPrimed перед повторным включением прерывания. Если бы порядок был обратным, то вновь включенный обработчик прерываний мог бы очистить очередь передачи и очистить TxPrimed до того, как приложение установит TxPrimed, тем самым потеряв сигнал от обработчика к коду приложения. По общему признанию, USART достаточно медленный и маловероятно, что этот сценарий маловероятен, но с помощью кода прерывания он платит за программную защиту.
Осталось две задачи — реализовать требуемую структуру данных очереди и разрешить прерывания.
Безопасные прерывания очереди
Последний фрагмент нашего кода USART, управляемого прерываниями, — это реализация очереди. Наиболее распространенный подход заключается в предоставлении кольцевого буфера фиксированного размера с отдельными указателями чтения и записи.
За круговым буфером стоит несколько ключевых идей. Указатель чтения «преследует» указатель записи вокруг буфера; таким образом, функции приращения должны обернуться вокруг. Кроме того, указатель записи всегда ссылается на «пустое» место. Таким образом, буфер с N + 1 местоположениями может содержать не более N элементов данных. Таким образом, мы определяем базовую структуру данных следующим образом:
struct Queue {
uint16_t pRD, pWR;
uint8_t q[QUEUE_SIZE];
};
static int QueueFull(struct Queue *q)
{
return (((q->pWR + 1) % QUEUE_SIZE) == q->pRD);
}
static int QueueEmpty(struct Queue *q)
{
return (q->pWR == q->pRD);
}
static int Enqueue(struct Queue *q, uint8_t data)
{
if (QueueFull(q))
return 0;
else {
q->q[q->pWR] = data;
q->pWR = ((q->pWR + 1) == QUEUE_SIZE) ? 0 : q->pWR + 1;
}
return 1;
}
static int Dequeue(struct Queue *q, uint8_t *data)
{
if (QueueEmpty(q))
return 0;
else {
*data = q->q[q->pRD];
q->pRD = ((q->pRD + 1) == QUEUE_SIZE) ? 0 : q->pRD + 1;
}
return 1;
}
Несмотря на свою простоту, такая общая структура данных предоставляет широкие возможности для условий гонки данных. Обратите внимание, что операция Enqueue (Dequeue) записывает (читает) местоположение очереди, на которое ссылаются, перед обновлением указателя qWR (qRD). Порядок операций важен! Как представлено, реализация предполагает одного читателя и одного писателя. В ситуации с несколькими устройствами для чтения или записи (например, с несколькими потоками) требуется переменная блокировки, чтобы предотвратить скачки данных. В такой ситуации обработчик прерываний может не использовать блокировку и, следовательно, должен иметь монопольный доступ к одному концу очереди.
Аппаратное управление потоком
Хотя добавление управляемых прерыванием буферов повышает производительность и надежность передачи данных, этого недостаточно для предотвращения переполнения буфера и, следовательно, потери данных. В этом разделе рассмотрим использование двух дополнительных сигналов RTS (запрос на отправку) и CTS (очистить для отправки), которые могут использоваться для включения управления «аппаратным потоком». Эти сигналы присутствуют как на мосте usb-uart так и на STM32. На практике nRTS (nCTS) от STM32 подключается к nCTS (nRTS) посредством usb-usrt моста. Когда STM32 готов к приему данных, он активируется
(обнуляет) nRTS и когда он хочет прекратить получать данные, он вызывает nRTS. Точно так же мост usb-uart будет использовать свой сигнал nRTS (подключенный к nCTS STM32), чтобы указать свою готовность к приему данных. Эти два контакта описаны в документации STM32 следующим образом:
Если управление потоком RTS включено .., тогда nRTS утверждается (привязан к низкому уровню), пока приемник USART готов к приему новых данных. Когда приемный регистр заполнен, nRTS отменяется, указывая, что передача, как ожидается, остановится в конце текущего кадра …
Если управление потоком CTS включено .., то передатчик проверяет вход nCTS перед передачей следующего кадра. Если nCTS заявлен (привязан к низкому уровню), то передаются следующие данные (при условии, что данные должны быть переданы …), иначе передача не происходит. Когда nCTS отменяется во время передачи, текущая передача завершается до остановки передатчика.
Этот подход называется аппаратным управлением потоком, потому что сигнализация выполняется аппаратно, а не программными механизмами, которые полагаются на вставку специальных управляющих символов в поток данных. Представленное нами решение не является полностью аппаратно-управляемым — мы проталкиваем вывод nRTS через программное обеспечение (например, обработчик прерываний), чтобы указать удаленному устройству, что оно должно прекратить передачу, и позволить оборудованию USART остановить передатчик всякий раз, когда удаленное устройство отменяет nCTS ,
К сожалению, полностью автоматизированный подход обречен на провал. Чтобы понять это, рассмотрим это утверждение из часто задаваемых вопросов по FTDI (FTDI является известным производителем мостовых чипов usb-uart:
Если [nCTS] — логическая единица, это означает, что внешнее устройство не может принять больше данных. FTxxx прекратит передачу в пределах 0-3 символов, в зависимости от того, что находится в буфере.
Это потенциальное переполнение 3 символов иногда создает проблемы. Клиенты должны знать, что FTxxx — это устройство USB, а не «нормальное» устройство RS232, как на ПК. Как таковое устройство работает на пакетной основе, а не на байтовой.
Таким образом, поведение реальных устройств не всегда соответствует ожиданиям STM32. Единственное жизнеспособное решение для программного обеспечения — деактивировать nRTS, в то время как оно все еще способно получать дополнительные входные данные! Эта проблема в основном неразрешима, так как нет четкой спецификации, определяющей количество допустимых переполнений символов.

