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_rb和uart1_rb_pool是软件层的 ringbuffer,用来缓存“已经收到、但主循环还没处理”的数据 -
uart_dma_buffer是uart_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 半传输中断,避免中途多进一次回调

回调函数
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);
}
}
也就是说:
-
当前这版:调试版模板
-
后续那版:协议版模板