UART, RingBuffer и FreeRTOS. Развлекаемся с приёмом и передачей.

Как применять показанный ранее кольцевой буфер для приёма и передачи информации по UART (и не только) в тасках RTOS.

Постановка задачи следующая: нужно принять по uart некоторое количество пакетов, линия может быть зашумлена и в паузах могут появлятся фантомные байты (которые передатчик не передавал), отделить пакеты от мусора и выполнить какие-то действия, скорость соединения не большая, допустим 115200.

Статья будет являться небольшой демонтрацией для работы с FreeRTOS и тем кольцевым буфером, о котором я однажды писал. А uart здесь каким боком? А таким, что данная заметка будет еще и небольшим туториалом для одного хорошего человека.

Погнали...

Сразу говорю - большенство кода будет дано только для понимания принципа работы, а не гарантом полной функциональности, куски взял из одного готового проекта. Примеры для микроконтроллера компании Миландр с использованием их SPL (очень похожа на SPL STLM32F10x), но это видно только в нескольких строках кода, всё остальное универсально.

Пакет информации будет иметь следующую структуру:

  • заголовок - 2 байта;
  • команда - 3 байта;
  • данный - 1 байт;
  • контрольная сумма - 2 байта.
0 1 2 3 4 5 6 7
HEADH HEADL CMD2 CMD1 CMD0 DATA CRCH CRCL

И так, приступим.

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

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

#include "ring_buffer.h"

static TaskHandle_t COMM_SentThread_h = NULL;
static TaskHandle_t COMM_Parse_h = NULL;

uint8_t rx_buff[ SIZE_RX_BUFFER ] = { 0 };
uint8_t tx_buff[ SIZE_TX_BUFFER ] = { 0 };

RING_buffer_t ring_rx;
RING_buffer_t ring_tx;

void COMM_ParseThread(void *argument);
void COMM_SentThread(void *argument);

/*
... тут хренова туча другого кода
*/

void COMM_Init( void )
{
    /*
    Тут мы инициализируем ноги контроллера, uart и разрешаем прерывания на приём.
    */

    // инициализация кольцевых буферов на приём и передачу.
    RING_Init(&ring_rx, rx_buff, SIZE_RX_BUFFER );
    RING_Init(&ring_tx, tx_buff, SIZE_TX_BUFFER );

    // создаём задачи для FreeRTOS.
    xTaskCreate(COMM_ParseThread, "Parse", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1,  &COMM_Parse_h );
    xTaskCreate(COMM_SentThread, "Sent", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1,  &COMM_SentThread_h);
}

Вызов функции выполняется следующим образом. Повторюсь - код является не истиной в последней инстанции, а только лишь ознакомительным материалом.

int main( void )
{
    /* Здесь инициализация  других устройств и модулей */
    COMM_Init();
    /* Здесь инициализация  других устройств и модулей */

    // Запускаем диспетчер задач и понеслась.
    vTaskStartScheduler();

    for(;;){}; // не обязательно
}

Теперь пора познакомиться с нашими обработчиками. За приём информации отвечает прерывание по приёму каждого байта. Сразу оговорюсь, можно сделать оптимальнее, но для первого знакомства или небольшой нагрузки линии связи этого достаточно.

В прерывании происходить очистка флага срабатывания прерывания и загрузка принятого байта в кольцевой буфер.

void UART1_IRQHandler(void)
{
    UART_ClearITPendingBit(MDR_UART1, UART_IT_RX);
    RING_Put(&ring_tx, UART_ReceiveData(MDR_UART1));
}

Передачей занимается задача RTOS, которая отслеживает заполненость кольцевого буфера и при наличии в нем данных отправляет их на линию. Сам буфер заполняется отдельно, это сделано для того, что бы не тормозить функцию которая его заполняет ожиданиями при отправке байтов.

void COMM_SentThread(void *argument)
{
    for ( ;; )
    {
        while(RING_GetCount(&ring_tx))
        {
            UART_SendData(MDR_UART1, RING_Pop( &ring_tx ));
            while (UART_GetFlagStatus(MDR_UART1, UART_FLAG_TXFE) == RESET);
        }

        vTaskDelay(TIME_MSEC * 100);
    }
    #pragma push
    #pragma diag_suppress 111
    vTaskDelete( NULL );
    #pragma pop
}

Кстати, если заметили TIME_MSEC - это количество тактов FreeRTOS в 1 милитекунде. Я себе всегда назначаю несколько макросов, которые помогают писать более понятный и не зависящий от настроек FreeRTOS код.

#define TIME_MSEC                   portTICK_PERIOD_MS  ///< тактов FreeRTOS в 1 милитекунде
#define TIME_SEC                    configTICK_RATE_HZ  ///< тактов FreeRTOS в 1 секунде
#define TIME_MINUT                  (TIME_SEC * 60)     ///< тактов FreeRTOS в 1 минуте

А теперь самое интересное - разбор пакета.

void COMM_ParseThread(void *argument)
{
    char cmd[3] = {0};
    uint8_t datax;
    for ( ;; )
    {
        // тут будем парсить ответ
        while(RING_GetCount(&ring_rx) >= SIZE_RX_COMMAND)
        {
            if((RING_ShowSymbol(&ring_rx, 0) == MARKER_H)          // Проверка заголовка
            && (RING_ShowSymbol(&ring_rx, 1) == MARKER_L)          // Проверка заголовка
            && RING_CRC16ccitt(&ring_rx, SIZE_RX_COMMAND - 4, 2))  // Проверка контрольной суммы пакета
            {
                //убираем маркер из кольцевого буфера
                RING_Pop(&ring_rx);
                RING_Pop(&ring_rx);

                for(uint8_t j = 0; j < 3; j++) cmd[j] = RING_Pop( &ring_rx );

                datax = RING_Pop( &ring_rx );

                // удаляем CRC из буфера
                RING_Pop( &ring_rx );
                RING_Pop( &ring_rx );

                if     (COMM_Compare( cmd, "CC1", 3 )) Run_CMD1(datax); // Выполнение действий команды 1
                else if(COMM_Compare( cmd, "CC2", 3 )) Run_CMD2(datax); // Выполнение действий команды 2
                else if(COMM_Compare( cmd, "CC3", 3 )) Run_CMD3(datax); // Выполнение действий команды 3
                else if(COMM_Compare( cmd, "CC4", 3 )) Run_CMD4(datax); // Выполнение действий команды 4
            }
            else
            {
                // удаляем первый символ из рингбуфера, и парсим заново
                RING_Pop( &ring_rx );
            }
        }

        vTaskDelay(TIME_MSEC * 100);
    }
    #pragma push
    #pragma diag_suppress 111
    vTaskDelete( NULL );
    #pragma pop
}

А теперь разберёмся в алгоритме работы данной задачи.

  1. Задача ждёт пока в буфере не наберётся достатояное количество байт - больше или равное размеру пакета (SIZE_RX_COMMAND).
  2. Затем проверяет заголовок и контрольную сумму в пакете, если всё совпадает, то переходит на следующий шаг, если нет, то удаляет первый байт из буфера и начинает парсинг заново.
  3. Если распознавание пакета прошло успешно, то удаляем маркеры (заголовок) из буфера и считываем оттуда команду и данные, а затем удаляем байты контрольной суммы. Переходим к распознаванию команд и их выполненению.

Собственно всё. На этот раз файлов для загрузки не будет, извините.

Хорошего кодинга и до новых встреч.