目录

0 书接上文

3 串口空闲中断+DMA半满&全满中断+环形缓冲区实现不定长接收

前提:

流程总览

中断部分(bsp_uart_driver.c)

自定义调用的回调函数

回调函数实现

环形缓冲区head指针动态偏移逻辑:

DMA半满中断回调函数

DMA全满中断回函数

串口空闲中断回调函数

BSP层:串口驱动程序(bsp_uart_driver.c)

APP层:串口解包程序(uart_parse_task.c)

总结

4 Q&A

1. 为什么串口需要环形缓冲区?

2. 串口的单次最大接收数据量是否需要限制?

3. 是否可以根据每次数据量大小来在每次接收时临时malloc内存?

4. 如果想要每次通过malloc来实现,应该怎么判断每次分配多少大小空间的内存?


0 书接上文

【DMA】FreeRTOS下STM32串口接收不定长数据并结合DMA实现环形缓冲区:1 相关函数理论部分与环形缓冲区设计

3 串口空闲中断+DMA半满&全满中断+环形缓冲区实现不定长接收

前提:

配置DMA

再使用 HAL_UARTEx_ReceiveToIdle_DMA() 开启串口空闲中断(串口驱动程序中)。

流程总览

一般来说,RTOS中设计一个串口解包过程:

数据帧格式(一个简单的协议):
帧头0xFE ... 净荷数据 ... 校验和(1字节) 帧尾:0xFF

中断(系统硬件)将数据传输完成的事情告诉串口驱动程序(前端),
串口驱动程序再通知APP解包(后端)
信息传递过程:系统硬件(中断)->前端(BSP层串口驱动程序)->后端(APP层串口解包程序)

中断(系统硬件):bsp_uart_driver.c
1、将数据存入环形缓冲区(中断内强烈不建议数据的拷贝,这里只是学习环形缓冲区演示)
2、通知前端,数据已经就绪(队列)

前端(BSP层串口驱动程序):bsp_uart_driver.c
1、buffer是否已满->停下串口(在下面的代码中没有实现这个功能)
2、将当前数据就绪的事件发送给后端

后端(APP层串口解包程序):uart_parse_task.c
1、对数据进行解包(解包状态机的实现:switch)
2、如果解包数据正确,则打印(对数据进行操作,这里仅打印)

中断部分(bsp_uart_driver.c)

自定义调用的回调函数

根据前面的知识,我们知道首先我们将串口空闲中断、DMA半满中断、DMA全满中断最终都会调用 HAL_UARTEx_RxEventCallback() 回调函数,我们可以去 stm32f4xx_hal_uart.c 文件中的串口中断服务函数、DMA半满中断服务函数、DMA全满中断服务函数那里将 HAL_UARTEx_RxEventCallback() 回调函数换成我们自定义的回调函数。

回调函数实现

环形缓冲区head指针动态偏移逻辑:

按照上面的逻辑,我们可以写出对应的回调函数

DMA半满中断回调函数
void dma_half_irq_callback(UART_HandleTypeDef* huart, uint32_t number_of_data)
{
    if (&huart1 == huart)
    {
        // log_i("-> dma_half_irq_callback(), data_number = [%d]", number_of_data);
        // adjust head pos
        uint32_t head_pos = 0;
        CIR_BUF_RETURN_VALUE ret = CIR_BUF_OK;
        // 获取当前head位置
        ret = get_head_pos(g_cir_buffer_irq_thread, &head_pos);
        // log_i("[h]head_pos = [%d]", head_pos);
        if (CIR_BUF_OK != ret) {
            log_e("error:[h]get_head_pos error!");
        }
        // 获取进入半满中断时,数据已经到达的位置:(CIRCULAR_BUFFER_SIZE / 2) - 1
        // 下一个存放数据的位置:(CIRCULAR_BUFFER_SIZE / 2)
        uint32_t target_data_pos = (CIRCULAR_BUFFER_SIZE / 2);
        // log_i("[h]target_data_pos = [%d]", target_data_pos);
        // 对head取余数,相减得到head与数据下一个位置之间的距离(即head需要偏移的距离)
        // head指向下一个存放数据的位置
        uint32_t pos_in_buffer = head_pos % (CIRCULAR_BUFFER_SIZE / 2);
        // log_i("[h]pos_in_buffer = [%d]", pos_in_buffer);
        uint32_t dis_need_increase = target_data_pos - pos_in_buffer;
        // log_i("[h]dis_need_increase = [%d]", dis_need_increase);
        // 偏移head
        ret = head_pos_increment(g_cir_buffer_irq_thread, \
                                 dis_need_increase);
        if (CIR_BUF_OK != ret) {
            log_e("[h]error:head_pos_increment error!");
        }

        // notify front thread
        // 通知前端,数据已经就绪(消息邮箱(队列))
        BaseType_t ret_1 = 0;
        uint32_t send_data_to_rec_A = IRQ_SEND_TO_THREAD;
        ret_1 = xQueueGenericSendFromISR(queue_uart_irq_thread, \
                                         &send_data_to_rec_A, \
                                         NULL, \
                                         queueOVERWRITE);
        if (pdTRUE != ret_1) {
            log_e("error:dma_half_irq_callback() xQueueSendFromISR() to front queue_uart_irq_thread failed!");
        }
        else {
            log_d("dma_half_irq_callback() xQueueSendFromISR() to front queue_uart_irq_thread success.");
        }
    }
}

DMA全满中断回函数
void dma_cplt_irq_callback(UART_HandleTypeDef* huart, uint32_t number_of_data)
{
    if (&huart1 == huart)
    {
        // log_i("-> dma_cplt_irq_callback(), data_number = [%d]", number_of_data);
        /* adjust head pos */
        uint32_t head_pos = 0;
        CIR_BUF_RETURN_VALUE ret = CIR_BUF_OK;
        // 获取当前head位置
        ret = get_head_pos(g_cir_buffer_irq_thread, &head_pos);
        // log_i("[c]head_pos = [%d]", head_pos);
        if (CIR_BUF_OK != ret) {
            log_e("error:[c]get_head_pos error!");
        }
        // 获取进入全满中断时,数据已经到达的位置:(CIRCULAR_BUFFER_SIZE) - 1
        // 下一个存放数据的位置:(CIRCULAR_BUFFER_SIZE)
        uint32_t target_data_pos = (CIRCULAR_BUFFER_SIZE);
        // log_i("[c]target_data_pos = [%d]", target_data_pos);
        // 对head取余数,相减得到head与下一个存放数据位置之间的距离(即head需要偏移的距离)
        // head指向下一个存放数据的位置
        uint32_t pos_in_buffer = head_pos % (CIRCULAR_BUFFER_SIZE);
        // log_i("[c]pos_in_buffer = [%d]", pos_in_buffer);
        uint32_t dis_need_increase = target_data_pos - pos_in_buffer;
        // log_i("[c]dis_need_increase = [%d]", dis_need_increase);
        // 偏移head
        ret = head_pos_increment(g_cir_buffer_irq_thread, \
            dis_need_increase);
        if (CIR_BUF_OK != ret) {
            log_e("error:[c]head_pos_increment error!");
        }

        // notify front thread
        // 通知前端,数据已经就绪(消息邮箱(队列))
        BaseType_t ret_1 = 0;
        uint32_t send_data_to_rec_A = IRQ_SEND_TO_THREAD;
        ret_1 = xQueueGenericSendFromISR(queue_uart_irq_thread, \
                                         &send_data_to_rec_A, \
                                         NULL, \
                                         queueOVERWRITE);
        if (pdTRUE != ret_1) {
            log_e("error:dma_cplt_irq_callback() xQueueSendFromISR() to front queue_uart_irq_thread failed!");
        }
        else {
            log_d("dma_cplt_irq_callback() xQueueSendFromISR() to front queue_uart_irq_thread success.");
        }
    }
}

串口空闲中断回调函数
void uart_idle_irq_callback(UART_HandleTypeDef* huart, uint32_t number_of_data)
{
    if (&huart1 == huart)
    {
        // log_i("-> uart_idle_irq_callback(), data_number = [%d]", number_of_data);
        /* adjust head pos */
        uint32_t head_pos = 0;
        CIR_BUF_RETURN_VALUE ret = CIR_BUF_OK;
        // 获取当前head位置
        ret = get_head_pos(g_cir_buffer_irq_thread, &head_pos);
        // log_i("[I]head_pos = [%d]", head_pos);
        if (CIR_BUF_OK != ret) {
            log_e("error:[I]get_head_pos error!");
        }
        // 获取进入空闲中断时,数据已经到达的位置:number_of_data - 1
        // 下一个存放数据位置:number_of_data
        uint32_t target_data_pos = number_of_data;
        // log_i("[I]target_data_pos = [%d]", target_data_pos);
        // 对head取余数,相减得到head与下一个存放数据位置之间的距离(即head需要偏移的距离)
        uint32_t pos_in_buffer = head_pos % (CIRCULAR_BUFFER_SIZE);
        // log_i("[I]pos_in_buffer = [%d]", pos_in_buffer);
        uint32_t dis_need_increase = 0;
        if (target_data_pos < pos_in_buffer)
        {
            dis_need_increase = (target_data_pos + CIRCULAR_BUFFER_SIZE) - pos_in_buffer;
        }
        else
        {
            dis_need_increase = target_data_pos - pos_in_buffer;
        }
        // log_i("[I]dis_need_increase = [%d]", dis_need_increase);
        // 偏移head
        ret = head_pos_increment(g_cir_buffer_irq_thread, \
            dis_need_increase);
        if (CIR_BUF_OK != ret) {
            log_e("error:[I]head_pos_increment error!");
        }

        // notify front thread
        // 通知前端,数据已经就绪(消息邮箱(队列))
        BaseType_t ret_1 = 0;
        uint32_t send_data_to_rec_A = IRQ_SEND_TO_THREAD;
        ret_1 = xQueueGenericSendFromISR(queue_uart_irq_thread, \
                                         &send_data_to_rec_A, \
                                         NULL, \
                                         queueOVERWRITE);
        if (pdTRUE != ret_1) {
            log_e("error:uart_idle_irq_callback() xQueueSendFromISR() to front queue_uart_irq_thread failed!");
        }
        else {
            log_d("uart_idle_irq_callback() xQueueSendFromISR() to front queue_uart_irq_thread success.");
        }
    }
}

BSP层:串口驱动程序(bsp_uart_driver.c)

串口驱动程序作为一个靠近中断的任务,需要较高的任务优先级来及时响应中断的通知。

static circular_buffer_t* g_cir_buffer_irq_thread = NULL;   // circular buffer
static QueueHandle_t queue_uart_irq_thread = NULL;          // queue: uart(hardware) send message to front-end thread

extern QueueHandle_t queue_irq_rec_A;       // queue: front-end notify back-end APP

void uart_driver_func(void* argument)
{
    /* Variable */
    uint32_t receive_data = 0;

    // 0.alloc the ring buffer
    circular_buffer_t* p_cir_buffer = creatEmptyCircularBuffer();
    if (NULL == p_cir_buffer){
        log_e("error: create p_cir_buffer failed!");
    }
    else {
        log_i("create p_cir_buffer success.");
        g_cir_buffer_irq_thread = p_cir_buffer;
    }
    
    // 1.create queue for UART(hardware) sending message to me
    queue_uart_irq_thread = xQueueCreate(1, 4);
    if (NULL == queue_uart_irq_thread) {
        log_e("error:queue_uart_irq_thread create failed!");
    }
    else {
        log_i("queue_uart_irq_thread create success.");
    }

    // 2.start uart receive
    #if 1   // receive_DMA
    if (HAL_OK == HAL_UARTEx_ReceiveToIdle_DMA(&huart1, g_cir_buffer_irq_thread->data, 10)){
        log_i("HAL_UART_rec_Idle_DMA init success.");
    }
    else {
        log_e("error:HAL_UART_rec_Idle_DMA init failed!");
    }
#endif  // end of receive_DMA
    
    for (;;)
    {
        // 3.判断是否有中断发来队列通知环形缓冲区已有数据
        xQueueReceive(queue_uart_irq_thread, &receive_data, portMAX_DELAY);
        log_i("irq2front queue_uart_irq_thread receive_data = [%x]", receive_data);
        if (IRQ_SEND_TO_THREAD == receive_data)
        {
            // 4.将当前数据就绪的事件发送给后端
            BaseType_t ret_queue = 0;
            uint32_t send_to_end = FRONT_SEND_TO_END;
            ret_queue = xQueueGenericSend(queue_irq_rec_A, \
                                            & send_to_end, \
                                            0,             \
                                            queueOVERWRITE);
            if (pdTRUE != ret_queue) {
                log_e("error:xQueueSend() front to end queue_irq_rec_A failed!");
            }
            else {
                log_d("xQueueSend() front to end queue_irq_rec_A success.");
            }

        }
        //osDelay(1);
    }
}

        环形缓冲区指针 g_cir_buffer_irq_thread 作为一个定义在 bsp_uart_drivere.c 的静态变量,我们需要在 uart_parse_task.c 文件中拿到环形缓冲区指针对缓冲区内数据进行解包,但是强烈不建议直接在 bsp_uart_drivere.h 头文件中将 g_cir_buffer_irq_thread 变量 extern 出去,我们要遵循最小可见原则,用一个公共接口将 g_cir_buffer_irq_thread 的值传递出去:

/* pass the circular_buffer pointer to external caller */
/* return:
    HAL_OK,
    HAL_ERROR */
HAL_StatusTypeDef get_circular_buffer(circular_buffer_t** circular_buffer)
{
    if (NULL != g_cir_buffer_irq_thread)
    {
        *circular_buffer = g_cir_buffer_irq_thread;
        return HAL_OK;
    }
    else {
        *circular_buffer = NULL;
        return HAL_ERROR;
    }
}

APP层:串口解包程序(uart_parse_task.c)

extern UART_HandleTypeDef huart1;
QueueHandle_t queue_irq_rec_A = NULL;    // queue: front-end notify back-end APP
static circular_buffer_t* g_circular_buffer_from_driver = NULL;    // circular buffer

void uart_rec_A_func(void* argument)
{
    /* Variable */
    uint32_t receive_data = 0;

    // create queue
    queue_irq_rec_A = NULL;
    queue_irq_rec_A = xQueueCreate(1, 4);
    if (NULL == queue_irq_rec_A){
        log_i("queue_irq_rec_A init failed!");
    }
    else{
        log_i("queue_irq_rec_A init success.");
        log_i("p_queue_irq_rec_A = [%x]",queue_irq_rec_A);
    }

    get_front_circular_buffer:
    if (HAL_ERROR == get_circular_buffer(&g_circular_buffer_from_driver)) {
        log_e("error:get_circular_buffer failed!");
        osDelay(1);
        goto get_front_circular_buffer;
    }
    log_i("get_circular_buffer success.");
    
    for (;;)
    {
        xQueueReceive(queue_irq_rec_A, &receive_data, portMAX_DELAY);
        log_i("queue_irq_rec_A receive_data = [%x]", receive_data);
        if (FRONT_SEND_TO_END == receive_data)
        {
            /*
            APP解析固件包过程中,进行校验和检查
            零、进入帧内净荷数据
            一、从包头开始进行叠加每个数据
            二、运行数据解析状态机
            */
            
            // 零、进入环形缓冲区进行帧内净荷数据
            if (NULL == g_circular_buffer_from_driver) {
                log_e("error: NULL pointer");
                return;
            }
            while (BUFFER_IS_EMPTY != buffer_is_empty(g_circular_buffer_from_driver))
            {
                static uint8_t temp_data_array[20] = 0;
                static uint8_t data_counter = 0;
                uint8_t cir_buf_data = 0;
                if (CIR_BUF_OK == get_data(g_circular_buffer_from_driver, &cir_buf_data)) {
                    log_i("read out data success from APP, cir_buf_data = [%d]", cir_buf_data);
                }

                // Parse data
                // 检测到帧尾之前,将数据全部暂存
                // 检测到帧尾之后,开始计算校验和,若相等,则输出
                static FRAME_STATUS status = FRAME_NOT_DETECTED;   // parse data status
                switch (status)
                {
                case FRAME_NOT_DETECTED:
                    // check if FRAME_HEAD is comming
                    if (FRAME_HEAD_FLAG == cir_buf_data)
                    {
                        status = FRAME_HEAD;
                        log_i("start parse uart data");
                    }
                    break;
                case FRAME_HEAD:
                    if (FRAME_END_FLAG == cir_buf_data)
                    {
                        status = FRAME_END;
                        log_i("END");
                        // 计算校验和
                        uint32_t data_rec_sum = temp_data_array[data_counter - 1];
                        uint32_t data_cal_sum = 0;
                        log_i("data_rec_sum = [%d]", data_rec_sum);
                        for (int i = 0;i < data_counter - 1;++i)
                        {
                            data_cal_sum += temp_data_array[i];
                        }
                        log_i("data_cal_sum = [%d]", data_cal_sum);
                        if (data_cal_sum == data_rec_sum)
                        {
                            // 打印净荷数据
                            for (int i = 0;i < data_counter - 1;++i)
                            {
                                log_i("Payload[%d] = [%d]", i, temp_data_array[i]);
                            }
                        }
                        else
                        {
                            log_e("error:data_cal_sum error!");
                        }

                        for (int i = 0;i < data_counter;++i)
                        {
                            temp_data_array[i] = 0;
                        }
                        data_counter = 0;
                        status = FRAME_NOT_DETECTED;
                    }
                    else
                    {
                        temp_data_array[data_counter] = cir_buf_data;
                        ++data_counter;
                        log_i("data_counter = [%d]", data_counter);
                        //log_i("Payload: [%d]", cir_buf_data);
                    }
                    break;
                // case FRAME_END:
                //     break;
                // default:
                //     break;
                }
            }

        }
        osDelay(1);
    }
}

总结

创建一个环形缓冲区,两个指针( head 和 tail )可以在环形缓冲区上一直向一个方向移动,启用串口DMA连续接收,数据会被不断地、连续地接收到缓冲区内,在DMA半满中断、DMA全满中断和串口空闲中断中根据DMA当前搬运数量调整 head 指针的位置,而 APP(串口解包程序) 控制 tail 指针,持续解包数据。

在这个过程中,head 指针的移动需要借助中断回调函数,所以 head 指针的位置是 “一抽一抽” 地动的,而 tail 指针是 APP 解一个字节的数据就递增一次,是 “连续” 地动的。

4 Q&A

1. 为什么串口需要环形缓冲区?

        串口接收数据的速度可能比处理数据的速度更快,使用环形缓冲区可以让处理器有足够的时间来处理已接收的数据,否则可能导致串口数据丢失,并且环形缓冲区是重复使用同一块内存,不需要频繁的内存分配和释放,利用率更高

2. 串口的单次最大接收数据量是否需要限制?

        需要限制,因为串口接收缓冲区大小有限,如果单次接收数据量大于缓冲区大小,也可能会丢失数据,并且处理器处理能力也有限,即使数据量小于缓冲区,但是较大的数据量也会占用处理器较长的时间,如果处理数据任务优先级较高,则会让较低优先级任务得不到运行,影响系统实时性

3. 是否可以根据每次数据量大小来在每次接收时临时malloc内存?

        可以,但是这种方法存在风险,动态内存分配和释放操作的时间是不确定的,较长的时间可能影响系统实时性,并且频繁动态内存分配和释放可能会导致内存碎片问题,严重情况下无法分配到内存,这将会导致导致数据全部丢失

4. 如果想要每次通过malloc来实现,应该怎么判断每次分配多少大小空间的内存?

        通过包头和数据长度来计算需要的空间大小,再malloc。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐