Использование UART + DMA при заранее неизветном количестве принимаемых символов (STM32)

У контроллеров STM32, в большинстве своем, отсутствует как таковой буфер приемника UART. Исходя из этой особенности, приходится создавать кольцевой буфер и использовать прерывания. При низкой тактовой частоте ядра и высоких скоростях UART это оказывается очень накладно как по производительности, так и по энергопотреблению. 

Всего этого можно избежать при помощи DMA, но как? Ведь при настройке DMA указывается фиксированный размер буфера, в который копируются принятые данные. К тому же у DMA есть прерывания только при полном и половинном заполнении буфера. Фактически, если вы уверены, что длина принимаемых данных не превысит длину буфера, прерывания от DMA можно не использовать вовсе. А обойтись только прерыванием IDLE от UART.

Флаг прерывания IDLE в интерфейсе UART выставляется в случае, если после стоп-бита последнего переданного символа на линии RX нет данных в течении времени приема одного символа. Флаг IDLE сбрасывается программно.

У DMA в контроллерах STM32 есть два режима работы - нормальный и кольцевой. В нормальном режиме мы указываем область памяти и по заполнению этой области DMA генерирует прерывние и заверщает работу (если мы не отключим его раньше). В кольцевом режиме область памяти, выделенная для DMA работает как обычный кольцевой буфер, но такой кольцевой буфер хранит указатель только на конец. Указатель на начало необходимо создавать программно.

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

У кольцевого режима, к сожалению, есть один минус - мы не можем отследить ситуацию когда буфер заполнен. Точнее мы можем сгенерировать прерывание, но в этот момент буфер будет заполнен от 50 до 100%. Это приводит к неэффективному расходу памяти. В конце статьи я рассскажу почему.

А теперь подробнее о том, как настроить UART и DMA для приема блоков данных произвольной длины.

1. Настраиваем порты ввода вывода для UART
2. Включаем тактирование UART и DMA (это чаще всего забывают)
3. Настраиваем DMA
4. Настраиваем UART
5. Разрешаем необходимые прерывания
6. Включаем DMA
7. Включаем UART

Для примера возьмем самый распространенный контроллер STM32F103C8T6, будем использовать UART1 и пятый канал DMA, к которому UART подсоединен. Я намеренно опущу настройку тактирования, main, а так же первый пункт из списка.


#define UART_RX_BUFFER_SIZE 128
uint8_t uartRxBuffer[UART_RX_BUFFER_SIZE]

void uartInit(void)
{
    /* 2. Enable DMA and UART clocks */
    LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_USART1);
    LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);

  /* 3. Configuring DMA */
    LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_5,
LL_DMA_DIRECTION_PERIPH_TO_MEMORY);
    LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_5, LL_DMA_PRIORITY_LOW);
    LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_5, LL_DMA_MODE_CIRCULAR);
    LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_5, LL_DMA_PERIPH_NOINCREMENT);
    LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_5, LL_DMA_MEMORY_INCREMENT);
    LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_5, LL_DMA_PDATAALIGN_BYTE);
    LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_5, LL_DMA_MDATAALIGN_BYTE);
    LL_DMA_SetPeriphAddress(DMA1, LL_DMA_CHANNEL_5, (uint32_t) &USART1->DR);
    LL_DMA_SetMemoryAddress(DMA1, LL_DMA_CHANNEL_5, (uint32_t) uartRxBuffer);
    LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_5, UART_RX_BUFFER_SIZE );

    /* 4. Configuring UART */
    uartInit.BaudRate = 115200;
    uartInit.DataWidth = LL_USART_DATAWIDTH_8B;
    uartInit.StopBits = LL_USART_STOPBITS_1;
    uartInit.Parity = LL_USART_PARITY_NONE;
    uartInit.TransferDirection = LL_USART_DIRECTION_TX_RX;
    uartInit.HardwareFlowControl = LL_USART_HWCONTROL_NONE;
    LL_USART_Init(USART1, &uartInit);
    LL_USART_ConfigAsyncMode(USART1);
    LL_USART_EnableDMAReq_RX(USART1);
    LL_USART_EnableIT_IDLE(USART1);

    /* 5. Enable IRQ */
    NVIC_EnableIRQ(USART1_IRQn);

    /* 6. Enable DMA */
    LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_5);

    /* 7. Enable UART */
    LL_USART_Enable(USART1);
}

Теперь все готово к приему данных, осталось только написать обработчик прерывания.

void USART1_IRQHandler(void)
{
    if(LL_USART_IsActiveFlag_IDLE(USART1))
    {
        LL_USART_ClearFlag_IDLE(USART1);
        /* make some job with RX buffer */
    }
}

Для определения длины блока данных в буфере относительно его начала можно воспользоваться следующим выражением:

uint32_t dataLength = UART_RX_BUFFER_SIZE - LL_DMA_GetDataLength(DMA1, LL_DMA_CHANNEL_5);

А теперь о том, как работать с кольцевым буфером DMA и избежать его переполнения.
Единственная информация, котрую мы можем получить от DMA в кольцевом режиме - это смещение на следующий записываемый символ относительно начала буфера (то есть коней блока необработанных данных в буфере). В связи с этим вычислять и хранить смещение относительно начала буфера на начала блока необработанных данных необходимо самостоятельно в функции, читающей эти данные из буфера. 

Теперь представим ситуацию, что мы используем кольцевой режим DMA и принимаем большое количество данных. В любой момент буфер может не просто переполниться, а даже перезаписаться. Т.к. DMA хранит только смещение на следующий записываемый символ, узнать, что этот символ еще не был прочитам не представляется возможным. Но зато у DMA есть прерывания по полному и половинному заполнению буфера. Анализируя их, мы можем узнать, заполнен буфер или нет. Тут есть два граничных случая. Первый из них (лучший) - при возникновении обоих прерываний буфер будет заполнен на 100%. Второй (худщий) - при возникновении обоих прерываний буфер будет заполнен лишь на 50% (неэффективное использование памяти.

Подводя итог, стоит отметить, что работа с UART и DMA является наиболее эффективной при использовании RTOS. В устройствах IOT контроллер во время работы DMA может находиться в спящем режиме, увеличивая при этом время автономной работы и не просыпаясь от частых прерываний.

Комментарии

Популярные сообщения из этого блога

Разборка ноутбука Xiaomi Mi notebook pro 15.6 (сушим клавиатуру)

Как запрограммировать STM32 без программатора