ringbuffer

ringbuffer课后作业

这一页先把老师课上的 DMA + 空闲中断 + ringbuffer 思路,收成我自己能复用的 UART 接收模板。

当前这版先定位成“调试版模板”:

  • uart_app_init() 负责把接收链拉起来

  • HAL_UARTEx_RxEventCallback() 负责把 DMA 收到的数据搬进 ringbuffer

  • uart_task() 负责把 ringbuffer 里的数据取出来打印,先确认整条通路是通的

后面如果要做控制/通信协议,就只改 uart_task() 后面的解析逻辑,前面两层尽量不动。

变量

 static struct rt_ringbuffer uart1_rb;
 static uint8_t uart1_rb_pool[256] = {0};
 uint8_t uart_rx_dma_buffer[128] = {0};
 uint8_t uart_dma_buffer[128] = {0};

这 4 个变量可以分成两层来看:

  • uart_rx_dma_buffer 是 DMA 直接写入的第一层缓冲区

  • uart1_rbuart1_rb_pool 是软件层的 ringbuffer,用来缓存“已经收到、但主循环还没处理”的数据

  • uart_dma_bufferuart_task() 里临时取数据用的缓冲区

可以理解成:

  • 第一层:DMA 先接一批

  • 第二层:回调把这一批搬进 ringbuffer

  • 第三层:uart_task() 再从 ringbuffer 取出来处理

初始化

下面这两句没有直接写在 MX_USART1_UART_Init() 里面:

  • HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_rx_dma_buffer, sizeof(uart_rx_dma_buffer))

  • __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT)

而是单独放进 uart_app_init()

这样做的原因是:

  • MX_USART1_UART_Init() 更偏底层硬件初始化

  • uart_app_init() 更偏应用层接收模板初始化

也就是说,串口硬件先初始化好,再由 uart_app_init() 把“DMA + 空闲中断 + ringbuffer”这条接收链拉起来。

 void uart_app_init(void)
 {
     rt_ringbuffer_init(&uart1_rb, uart1_rb_pool, sizeof(uart1_rb_pool));
 ​
     if (HAL_UARTEx_ReceiveToIdle_DMA(&huart1,
                                      uart_rx_dma_buffer,
                                      sizeof(uart_rx_dma_buffer)) != HAL_OK)
     {
         Error_Handler();
     }
 ​
     __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);
 }

这 3 句各自负责的事:

  • rt_ringbuffer_init(...):初始化软件环形缓冲区

  • HAL_UARTEx_ReceiveToIdle_DMA(...):启动串口 DMA 空闲接收

  • __HAL_DMA_DISABLE_IT(..., DMA_IT_HT):关闭 DMA 半传输中断,避免中途多进一次回调

image-20260506204425334

回调函数

 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
 {
     if (huart->Instance == USART1)
     {
         HAL_UART_DMAStop(huart);
         rt_ringbuffer_put(&uart1_rb, uart_rx_dma_buffer, Size);
         memset(uart_rx_dma_buffer, 0, sizeof(uart_rx_dma_buffer));
         HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_rx_dma_buffer, sizeof(uart_rx_dma_buffer));
         __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);
     }
 }

这个回调在当前模板里只做 4 件事:

  • 判断是不是 USART1

  • 停掉这一轮 DMA

  • 把本次收到的 Size 个字节搬进 ringbuffer

  • 重新启动下一轮 ReceiveToIdle_DMA

这里要特别记住:

  • Size 表示这次事件发生前,DMA 已经成功收到多少字节

  • ringbuffer 负责缓存字节流,不负责协议解析

  • 真正的“这一帧怎么拆”应该放在后面的逻辑层里处理

这里先用 rt_ringbuffer_put(),而不是 rt_ringbuffer_put_force()

原因是:

  • put() 满了会丢掉新来的部分数据,但不会覆盖旧数据

  • put_force() 满了会覆盖旧数据

如果后面主要做控制/通信协议,默认用 put() 会更保守一些,也更方便排查问题。

uart_task 调试版模板

 void uart_task(void)
 {
     uint16_t length;
 ​
     length = rt_ringbuffer_data_len(&uart1_rb);
 ​
     if (length == 0) return;
 ​
     if (length > sizeof(uart_dma_buffer) - 1)
     {
         length = sizeof(uart_dma_buffer) - 1;
     }
 ​
     rt_ringbuffer_get(&uart1_rb, uart_dma_buffer, length);
     uart_dma_buffer[length] = '\0';
 ​
     my_printf(&huart1, "uart data: %s\n", uart_dma_buffer);
 ​
     memset(uart_dma_buffer, 0, sizeof(uart_dma_buffer));
 }

这一版 uart_task() 先定位成“调试版模板”,主要目的是先验证:

  • DMA 能不能收上来

  • 回调能不能把数据放进 ringbuffer

  • 主循环能不能把 ringbuffer 里的数据正常取出来

它现在做的事情很直接:

  • 先看 ringbuffer 里有没有数据

  • 有数据就取出来

  • 补一个字符串结束符 \0

  • my_printf() 打印出来

所以这版更适合:

  • 串口回显调试

  • 先看收数链路通不通

  • 先确认空闲中断和 ringbuffer 配合有没有问题

后面怎么升级成协议版

如果后面要做控制/通信协议,不建议一直停留在 %s 打印这一步。

更合适的升级方向是:

  • uart_app_init() 不动

  • HAL_UARTEx_RxEventCallback() 不动

  • 只把 uart_task() 改成“从 ringbuffer 取字节,再喂给状态机”

也就是:

  • 当前这版:调试版模板

  • 后续那版:协议版模板

协议版的 uart_task() 可以先记住这个方向:

 void uart_task(void)
 {
     uint8_t ch;
 ​
     while (rt_ringbuffer_getchar(&uart1_rb, &ch))
     {
         uart_parser_feed(ch);
     }
 }

这时候:

  • ringbuffer 还是只负责存字节

  • uart_task() 负责把字节送给解析器

  • uart_parser_feed() 再去判断帧头、长度、校验这些内容

完整代码

 #include "usart_app.h"
 #include "ringbuffer.h"
 ​
 static struct rt_ringbuffer uart1_rb;
 static uint8_t uart1_rb_pool[256] = {0};
 uint8_t uart_rx_dma_buffer[128] = {0};
 uint8_t uart_dma_buffer[128] = {0};
 ​
 void uart_app_init(void)
 {
     rt_ringbuffer_init(&uart1_rb, uart1_rb_pool, sizeof(uart1_rb_pool));
 ​
     if (HAL_UARTEx_ReceiveToIdle_DMA(&huart1,
                                      uart_rx_dma_buffer,
                                      sizeof(uart_rx_dma_buffer)) != HAL_OK)
     {
         Error_Handler();
     }
 ​
     __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);
 }
 ​
 int my_printf(UART_HandleTypeDef *huart, const char *format, ...)
 {
     char buffer[512];
     va_list arg;
     int len;
 ​
     va_start(arg, format);
     len = vsnprintf(buffer, sizeof(buffer), format, arg);
     va_end(arg);
 ​
     HAL_UART_Transmit(huart, (uint8_t *)buffer, (uint16_t)len, 0xFF);
     return len;
 }
 ​
 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
 {
     if (huart->Instance == USART1)
     {
         HAL_UART_DMAStop(huart);
         rt_ringbuffer_put(&uart1_rb, uart_rx_dma_buffer, Size);
         memset(uart_rx_dma_buffer, 0, sizeof(uart_rx_dma_buffer));
         HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_rx_dma_buffer, sizeof(uart_rx_dma_buffer));
         __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);
     }
 }
 ​
 void uart_task(void)
 {
     uint16_t length;
 ​
     length = rt_ringbuffer_data_len(&uart1_rb);
 ​
     if (length == 0) return;
 ​
     if (length > sizeof(uart_dma_buffer) - 1)
     {
         length = sizeof(uart_dma_buffer) - 1;
     }
 ​
     rt_ringbuffer_get(&uart1_rb, uart_dma_buffer, length);
     uart_dma_buffer[length] = '\0';
 ​
     my_printf(&huart1, "uart data: %s\n", uart_dma_buffer);
 ​
     memset(uart_dma_buffer, 0, sizeof(uart_dma_buffer));
 }

本次提问整理

这一段把刚才围绕 DMA + 空闲中断 + ringbuffer 的问题重新收一下,后面复习时可以直接看这里。

1. 整体数据流到底是什么

当前这套代码可以分成三层:

  • uart_rx_dma_buffer[128]
    DMA 直接接收串口数据,先放到这里

  • uart1_rb_pool[256] + uart1_rb
    回调函数触发后,把这一批数据搬进 ringbuffer

  • uart_dma_buffer[128]
    uart_task() 需要处理数据时,再从 ringbuffer 取出来放到这里

也就是:

  • 第一层:DMA 收

  • 第二层:ringbuffer 缓存

  • 第三层:任务函数处理

可以把它记成:

  • uart_rx_dma_buffer 是收货区

  • uart1_rb_pool 是仓库

  • uart_dma_buffer 是工作台

2. 为什么不直接用 DMA 缓冲区做最终处理

因为 DMA 缓冲区更适合“先收上来”,不适合直接拿来做复杂业务。

如果直接在 DMA 缓冲区上处理,容易遇到这些问题:

  • 新数据和旧数据边界不清楚

  • 回调里做太多事会变重

  • 主循环处理速度慢时,不好缓存连续来的多批数据

所以这里多加了一层 ringbuffer,目的就是:

  • 回调里只搬运

  • 主循环里再处理

  • 中断和业务层解耦

3. 你这版和老师那版的差别

老师那版更像教学版,你这版更适合往个人模板收。

老师那版同时混了两条思路:

  • 单字节中断 + 超时拼包

  • DMA + ringbuffer

你后面收敛出来的主线是:

  • DMA + idle + ringbuffer

这个区别很重要,因为模板最好只保留一条正式接收链,不然后面自己也容易绕进去。

4. 三个模板函数分别负责什么

uart_app_init()

职责:

  • 初始化软件 ringbuffer

  • 启动 DMA 空闲接收

  • 关闭 DMA 半传输中断

它的重点不是处理数据,而是把整条接收链拉起来。

HAL_UARTEx_RxEventCallback()

职责:

  • 判断是不是 USART1

  • 先停这一轮 DMA

  • 把本次收到的 Size 个字节搬进 ringbuffer

  • 重新启动下一轮接收

它只负责“搬运字节”,不负责协议解析。

uart_task()

职责:

  • 从 ringbuffer 里取数据

  • 当前调试版先打印

  • 后面协议版再接状态机

所以当前这版 uart_task() 只是调试版入口,不是最终协议版。

5. 为什么回调里最后选 rt_ringbuffer_put()

这里最后改成偏老师那种更保守的方式:

 HAL_UART_DMAStop(huart);
 rt_ringbuffer_put(&uart1_rb, uart_rx_dma_buffer, Size);

原因是:

  • rt_ringbuffer_put() 满了只会丢掉新来的部分数据

  • rt_ringbuffer_put_force() 满了会覆盖旧数据

如果后面主要做控制/通信协议,默认用 put() 更保守,也更方便查问题。

6. HAL_OK 是什么

HAL_OK 是 HAL 库定义的返回状态值,表示:

  • 这次 HAL 函数执行成功了

它一般和 HAL_StatusTypeDef 一起出现。

例如:

 if (HAL_UARTEx_ReceiveToIdle_DMA(&huart1,
                                  uart_rx_dma_buffer,
                                  sizeof(uart_rx_dma_buffer)) != HAL_OK)
 {
     Error_Handler();
 }

意思就是:

  • 如果启动 DMA 空闲接收失败

  • 就进入错误处理

7. 当前模板是什么定位

当前版本先定位成“调试版模板”:

  • uart_app_init() 负责拉起接收链

  • HAL_UARTEx_RxEventCallback() 负责搬运 DMA 数据

  • uart_task() 负责把收到的数据取出来打印

这一步的目的不是立刻做协议,而是先确认:

  • DMA 通了没有

  • idle 回调进了没有

  • ringbuffer 存和取都正常没有

8. 后面怎么升级成协议版

如果后面要做控制/通信协议,建议保留前两层不动:

  • uart_app_init() 不动

  • HAL_UARTEx_RxEventCallback() 不动

只把 uart_task() 改成“从 ringbuffer 取字节,再喂给状态机”。

方向可以先记成:

 void uart_task(void)
 {
     uint8_t ch;
 ​
     while (rt_ringbuffer_getchar(&uart1_rb, &ch))
     {
         uart_parser_feed(ch);
     }
 }

也就是说:

  • 当前这版:调试版模板

  • 后续那版:协议版模板