RTT_UART_DMA
268.3.4RTT_UART_DMA
#include <rtthread.h> // 引入 RT-Thread 的设备、线程和消息队列接口。
#define SAMPLE_UART_NAME "uart3" // 未指定命令参数时,默认使用 uart3。
struct rx_msg // 定义接收通知在消息队列中的消息格式。
{ // 接收消息结构体开始。
rt_device_t dev; // 保存产生接收通知的串口设备句柄,不是设备名字符串。
rt_size_t size; // 保存当前驱动接收缓冲区中可读取的数据长度。
}; // 接收消息结构体结束。
static rt_device_t serial; // 保存本次命令选中的串口设备句柄。
static struct rt_messagequeue rx_mq; // 定义静态消息队列控制块,用于连接回调和工作线程。
static rt_err_t uart_input(rt_device_t dev, rt_size_t size) // 定义 DMA 接收指示回调。
{ // 接收回调函数体开始。
struct rx_msg msg; // 创建一条即将发送到消息队列的局部消息。
rt_err_t result; // 保存消息发送结果。
msg.dev = dev; // 记录本次通知来自哪个串口设备。
msg.size = size; // 记录该设备当前可读取的数据长度。
result = rt_mq_send(&rx_mq, &msg, sizeof(msg)); // 非阻塞地把消息元数据复制到消息队列。
if ( result == -RT_EFULL) // 判断队列是否已满、无法再放入新消息。
{ // 队列已满的处理分支开始。
rt_kprintf("message queue full!\n"); // 在控制台提示消息队列已满;保留课程源码中的全角感叹号。
} // 队列已满的处理分支结束。
return result; // 把消息发送结果返回给设备框架。
} // 接收回调函数体结束。
static void serial_thread_entry(void *parameter) // 定义串口数据处理线程入口;本例未使用 parameter。
{ // 工作线程函数体开始。
struct rx_msg msg; // 保存从消息队列取出的设备句柄与数据长度。
rt_err_t result; // 保存消息队列接收结果;在 5.0.2 中成功值是实际消息长度。
rt_uint32_t rx_length; // 保存 rt_device_read() 实际读出的字节数。
static char rx_buffer[RT_SERIAL_RB_BUFSZ + 1]; // 分配接收缓冲区,并多留 1 字节存放字符串结束符。
while (1) // 线程持续等待并处理 DMA 接收通知。
{ // 无限循环开始。
rt_memset(&msg, 0, sizeof(msg)); // 每轮先清零消息结构体,避免残留字段干扰判断。
result = rt_mq_recv(&rx_mq, &msg, sizeof(msg), RT_WAITING_FOREVER); // 永久等待一条接收通知消息。
if (result > 0) // RT-Thread 5.0.2 返回实际消息长度,大于 0 表示接收成功。
{ // 成功取得消息后的数据处理分支开始。
rx_length = rt_device_read(msg.dev, 0, rx_buffer, msg.size); // 从通知对应的串口实际读取数据。
rx_buffer[rx_length] = '\0'; // 在有效数据后补结束符,便于按 C 字符串打印。
rt_device_write(serial, 0, rx_buffer, rx_length); // 将原始数据从当前串口原样发回,不发送结束符。
rt_kprintf("%s\n",rx_buffer); // 同时把文本打印到默认控制台 UART1。
} // 数据处理分支结束。
} // 无限循环结束;正常运行时不会退出。
} // 工作线程函数体结束。
static int uart_dma_sample(int argc, char *argv[]) // 定义可由 MSH 调用的 UART DMA 示例函数。
{ // uart_dma_sample 函数体开始。
rt_err_t ret = RT_EOK; // 默认把命令执行结果设为成功。
char uart_name[RT_NAME_MAX]; // 准备保存目标串口注册名的缓冲区。
static char msg_pool[256]; // 为静态消息队列提供 256 字节内存池。
char str[] = "hello RT-Thread!\r\n"; // 准备串口成功打开后首先发送的问候字符串。
if (argc == 2) // 命令名后恰好带一个设备名时,参数总数为 2。
{ // 指定设备名的分支开始。
rt_strncpy(uart_name, argv[1], RT_NAME_MAX); // 把 argv[1] 复制为本次使用的设备名。
} // 指定设备名的分支结束。
else // 未恰好带一个设备名时,采用默认设备。
{ // 默认设备分支开始。
rt_strncpy(uart_name, SAMPLE_UART_NAME, RT_NAME_MAX); // 把默认的 uart3 复制到缓冲区。
} // 默认设备分支结束。
serial = rt_device_find(uart_name); // 按注册名查找串口设备并保存句柄。
if (!serial) // 空句柄表示系统中没有该设备。
{ // 设备查找失败分支开始。
rt_kprintf("find %s failed!\n", uart_name); // 在 UART1 控制台打印失败的设备名。
return RT_ERROR; // 返回错误,不继续初始化。
} // 设备查找失败分支结束。
rt_mq_init(&rx_mq, "rx_mq", // 初始化名为 rx_mq 的静态消息队列。
msg_pool, // 指定消息队列使用的 256 字节内存池。
sizeof(struct rx_msg), // 指定每条消息最多容纳一个 rx_msg 结构体。
sizeof(msg_pool), // 告诉内核消息池的总字节数。
RT_IPC_FLAG_FIFO); // 多线程等待时按先到先得的 FIFO 顺序唤醒。
rt_device_open(serial, RT_DEVICE_FLAG_DMA_RX); // 以 DMA 接收、默认轮询发送方式打开串口。
rt_device_set_rx_indicate(serial, uart_input); // 注册 DMA 接收完成后的指示回调。
rt_device_write(serial, 0, str, (sizeof(str) - 1)); // 发送问候语,不发送末尾的 '\0'。
rt_thread_t thread = rt_thread_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10); // 创建串口处理线程。
if (thread != RT_NULL) // 检查线程对象是否创建成功。
{ // 线程创建成功分支开始。
rt_thread_startup(thread); // 启动线程,使其参与系统调度。
} // 线程创建成功分支结束。
else // 线程对象创建失败时进入此分支。
{ // 线程创建失败分支开始。
ret = RT_ERROR; // 把命令返回值改为错误。
} // 线程创建失败分支结束。
return ret; // 向 MSH 返回本次初始化结果。
} // uart_dma_sample 函数体结束。
MSH_CMD_EXPORT(uart_dma_sample, uart device dma sample); // 导出 uart_dma_sample MSH 命令。
本节在
13IO_DEVICE工程的applications目录中新建业务文件uart_dma.c,源码来自 RT-Thread 官方“DMA 接收、轮询发送”示例。上方保持课程配套源码原样,只移除旧注释并增加逐行说明。
一、RT-Thread的串口使用
1.1 RT-Thread的串口使用知识点1
上一节的普通串口实验包含三个可复用的知识点:在 board.h 中启用具体外设、用信号量连接接收回调与线程、通过 argc/argv 让同一命令选择不同设备。本节保留“回调只通知、线程再处理”的设计,但把中断逐字节接收改为 DMA 批量接收,并用消息队列同时传递设备句柄与可读长度。
1.1.1 信号量的使用
上一节中,回调调用 rt_sem_release(),线程调用 rt_sem_take();线程无数据时阻塞,而不是连续调用 rt_device_read() 占用 CPU。本节回顾这一思想是为了对比,DMA 示例本身不再使用信号量,而是使用消息队列。
1.1.1.1 函数的传参
uart_dma_sample(int argc, char *argv[])是 MSH 命令函数,不是系统真正的main(),但参数组织方式与命令行程序相同。- 命令名也计入
argc:uart_dma_sample的argc为 1,uart_dma_sample uart3的argc为 2,再追加一个参数则为 3。 argv是字符串指针数组:argv[0]指向命令名,argv[1]指向用户给出的设备名。星号表示指针,不能把它笼统理解为“字符串地址的信号”。- 本例默认设备是小写
uart3;也可执行uart_dma_sample uart2,但前提是 UART2 的 DMA 接收能力也已在 BSP 中配置。
1.1.1.2 应用案例
-
1.1.1.2.1 例题: 串口设备DMA接收使用示例
- 不需要新建工程,继续使用
13IO_DEVICE。 - 在
applications下新建 C 源文件uart_dma.c,粘贴官方 DMA 接收示例;原稿所说的“新建 UART 文件夹”不正确。 - 示例导出的命令是
uart_dma_sample,不是上一节的uart_sample。
- 不需要新建工程,继续使用
-
1.1.1.2.2 例题: 串口DMA接收样例
- 不带设备参数时默认使用
uart3。编译、下载、复位后,在 UART1 的 MSH 控制台输入uart_dma_sample。 - 视频一开始同时打开 COM16、COM17 是因为尚未确认端口;随后确认当时 COM17 对应 UART3、COM16 对应 UART2。COM 号由 Windows 动态分配,不能照抄。
- 初次运行时 UART3 串口助手看不到
hello RT-Thread!,不是因为设备名默认值错误,而是 DMA 框架和 UART3 RX DMA 尚未启用,rt_device_open(...DMA_RX)失败。 - 教学源码没有检查
rt_device_open()的返回值,因此打开失败时没有直接打印错误;设备引用计数仍为 0,后续rt_device_write()也写不出去。这是“代码能编译但无输出”的真正原因。程序还会继续创建线程,而该线程因收不到 DMA 回调消息一直阻塞。
- 不带设备参数时默认使用
二、RT-Thread的串口使用详解
2.1 配置步骤
让 UART3 使用 DMA 接收需要同时打开两个层级:
- 全局串口 DMA 框架:在 RT-Thread Settings 中启用 Serial DMA Mode,使
rtconfig.h生成RT_SERIAL_USING_DMA。 - 具体串口与方向:在
board.h定义BSP_UART3_RX_USING_DMA,让 BSP 为 UART3 的 RX 配置 DMA 通道。
只完成其中一层都不够。BSP_USING_UART3 只表示启用普通 UART3,不能代替 RX DMA 宏;反过来,只打开全局 DMA 支持也不会自动决定哪个串口、哪个方向使用 DMA。
本例只配置 RX DMA。rt_device_open(serial, RT_DEVICE_FLAG_DMA_RX) 没有指定 DMA_TX 或 INT_TX,所以发送仍是轮询方式,不能称为“串口收发全部使用 DMA”。
2.2 kconfig配置
在 RT-Thread Studio 中打开 RT-Thread Settings:
- 进入
Components → Device Drivers → Serial Device Drivers,也可搜索serial快速定位。 - 保持 UART 设备驱动已启用,勾选
Enable Serial DMA Mode。 - RX buffer size 保持本实验默认值 64。
- 按
Ctrl+S保存,等待 Studio 更新工程配置。这里是启用条件编译选项,不是修改或下载一份新的“内核代码”。
保存后,rtconfig.h 中应出现:
#define RT_SERIAL_USING_DMA // 编译串口框架的 DMA 支持代码。
#define RT_SERIAL_RB_BUFSZ 64 // 把串口接收缓冲区设为 64 字节。
然后在 drivers/board.h 的 UART3 配置后加入:
#define BSP_UART3_RX_USING_DMA // 为 UART3 的接收方向启用 DMA。
- 正确宏名是
BSP_UART3_RX_USING_DMA,不是原稿中的UART3_RX_DMA。 - 本 BSP 将 STM32F103 的 USART3_RX 映射到 DMA1 Channel 3,并使用循环 DMA 缓冲区;UART 空闲中断以及 DMA 半传输/完成事件用于报告新数据长度。
- 对初学者可把硬件路径理解为:
PB11 → USART3 接收器/数据寄存器 → DMA1 Channel 3 → RAM 中的 64 字节接收缓冲区。DMA 的外设地址保持不变、内存地址随字节递增,写到缓冲区末尾后以循环模式继续;它不会改变波特率,也不会改变 PB11 仍是 UART3 RX 这一物理接线。 - 本例 TX 仍从 PB10 发出并由 CPU 轮询写串口,因此物理上仍要把 USB-TTL 的 TX 接 PB11、RX 接 PB10,并让两端 GND 共地。
- 配置完成后重新编译、下载并复位。打开 UART3 对应串口助手,再在 UART1 控制台运行
uart_dma_sample,UART3 应收到hello RT-Thread!。 - 若改用 UART2,除了命令传
uart2,还要按 BSP 支持情况定义BSP_UART2_RX_USING_DMA,不能复制 UART3 宏后只改业务命令。
三、结束
3.1 代码分析: 串口回环与初始化
- 从
MSH_CMD_EXPORT找到uart_dma_sample(),再跟进uart_input()与serial_thread_entry(),可以快速建立调用关系;这比机械地从文件末尾逐行倒读更准确。 - 初始化流程为:解析设备名 → 查找设备 → 初始化消息队列 → 以 DMA_RX 打开设备 → 注册回调 → 发送问候语 → 创建并启动工作线程。
- UART3 接收到的数据会从 UART3 原样回传,同时通过
rt_kprintf()打印到 UART1 控制台。因此两个窗口显示相同文本来自两次不同的输出调用,不是硬件把 UART3 自动“回环到 UART1”。 - 命令允许选择 UART2,但对应端口必须具备 RX DMA 配置。默认使用 UART3 是业务代码选择,不代表 DMA 只能用于 UART3。
3.2 代码分析: 消息队列与串口设备操作
msg_pool[256]是消息队列的静态内存池;每条业务消息是一个struct rx_msg,内核还会为每条消息保留管理头和对齐空间,所以队列容量不能简单按256 / sizeof(rx_msg)计算。rt_mq_init()初始化控制块、消息格式、内存池和 FIFO 等待策略。队列传递的只是{dev, size}元数据,不是接收到的 UART 字节本身。rt_device_open(serial, RT_DEVICE_FLAG_DMA_RX)与上一节的RT_DEVICE_FLAG_INT_RX不同:DMA 控制器把 USART 数据寄存器中的字节搬运到 RAM 接收缓冲区,CPU 不必为每个字节执行一次搬运代码。- DMA 并不等于“完全没有中断或 CPU 工作”。本 BSP 仍通过 UART 空闲中断、DMA 半传输/完成中断确定新数据长度,再由 CPU 执行回调、消息队列和应用处理。
rt_device_set_rx_indicate()安装uart_input;首次rt_device_write()仍采用轮询发送并输出问候语。
3.3 代码分析: 线程创建与数据接收
rt_thread_create("serial", ..., 1024, 25, 10)创建动态线程:栈 1024 字节、优先级 25、时间片 10 tick;本工程 tick 频率为 1000 Hz,因此这里的 10 tick 等于 10 ms。- 创建成功后必须调用
rt_thread_startup(),线程才进入调度;真正读取 DMA 接收数据的是该工作线程。 - 线程长期阻塞在
rt_mq_recv(..., RT_WAITING_FOREVER),有通知才运行,不会持续轮询串口。 - 示例没有检查消息队列初始化、设备打开、回调注册和首次写入的返回值。调试无输出时,应逐项检查这些返回值,而不能只看编译是否成功。
3.4 代码分析: 线程实体与回调函数关系
struct rx_msg 只有两个成员:
| 成员 | 实际含义 | 不包含什么 |
|---|---|---|
dev | 产生通知的 rt_device_t 设备句柄 | 不是 "uart3" 这样的设备名字符串 |
size | 当前可从该设备读取的字节数 | 不包含实际接收字符 |
- 驱动完成一批 DMA 接收后调用回调;回调把这两个字段复制进消息队列,工作线程从队列取出后再调用
rt_device_read()。 - 回调不是“线程处理消息时调用的函数”,而是设备框架在接收事件到达时调用的通知函数。线程与回调互不直接调用,二者通过消息队列解耦。
- 与只传“有数据”这一事件的信号量相比,消息队列还能携带设备来源和数据长度,更适合批量接收通知。
3.5 代码分析: 接收数据回调函数
uart_input(dev, size)收到的是设备句柄和可读长度。回调先填充局部msg,再用rt_mq_send()非阻塞发送到rx_mq。rt_mq_send()会把结构体内容复制进队列,因此回调返回后,局部变量msg失效也不会让已入队消息丢失。- 队列满时返回
-RT_EFULL,这意味着本次通知没有入队,工作线程可能无法按预期及时处理对应数据。该错误不能视为“次要细节”。 - 课程源码在队列满时调用
rt_kprintf()。由于回调通常运行在中断上下文中,产品代码应避免在这里做耗时或可能阻塞的输出,更合适的做法是递增错误计数并在线程中报告。
3.6 代码分析: 线程实体与数据处理
- 线程用
rt_memset()清零本轮的msg。 rt_mq_recv()在队列为空时永久阻塞;回调成功发送消息后,线程被唤醒。- RT-Thread 5.0.2 的
rt_mq_recv()成功时返回实际收到的消息字节数,因此课程源码使用result > 0判断成功。 - 线程按照
msg.dev和msg.size调用rt_device_read(),把 DMA 缓冲区中的数据复制到应用缓冲区。 - 线程原样回写数据,并将其作为文本打印到控制台,然后回到队列继续等待。
版本注意:部分旧版 RT-Thread 文档和示例把 rt_mq_recv() 的成功条件写成 result == RT_EOK。本工程使用 5.0.2,其内核已经在 2023 年改为返回实际消息长度,所以不能把当前源码的 > 0 擅自改成 == 0。
3.7 代码分析: 数据读取与输出
rt_device_read(msg.dev, 0, rx_buffer, msg.size)才是真正把串口数据读入应用数组;回调和消息队列只传通知元数据。rx_length是实际读到的字节数,可能小于请求的msg.size。后续回写必须使用rx_length,不能盲目使用通知长度。rx_buffer长度为RT_SERIAL_RB_BUFSZ + 1,本工程即 65 字节。最后多出的 1 字节供rx_buffer[rx_length] = '\0'使用;若以后增大串口 RX 缓冲区,应用缓冲区也要同步调整。rt_device_write(serial, 0, rx_buffer, rx_length)将收到的字节原样发回 UART3,与上一节ch + 1的错位回传不同。rt_kprintf("%s\n", rx_buffer)经默认控制台uart1输出。补上的\0只服务于%s,没有通过 UART3 发出。%s适合文本演示,不适合任意二进制数据:数据中若含0x00,控制台显示会提前结束;UART3 的write仍会按rx_length回写全部字节。
3.8 总结: DMA使用逻辑与交互原理
| 对比项 | 上一节中断接收示例 | 本节 DMA 接收示例 |
|---|---|---|
| 打开标志 | RT_DEVICE_FLAG_INT_RX | RT_DEVICE_FLAG_DMA_RX |
| 接收粒度 | 示例每次处理 1 字节 | 一次通知可对应一批数据 |
| 回调到线程 | 信号量,只表示“有数据” | 消息队列,携带设备句柄和长度 |
| 数据处理 | 字节编码加 1 后回传 | 原样回传,并打印到 UART1 |
| 发送方式 | 轮询 | 仍是轮询,并非 TX DMA |
- 本节正确逻辑是“DMA/驱动缓冲数据 → 回调发送消息 → 线程收到消息 →
rt_device_read()取数据”,不是“通过信号量实现 DMA 交互”。 - DMA 减少的是 CPU 对每个字节搬运的参与度,适合连续或成批数据;回调、队列、线程和协议处理仍由 CPU 完成。
- 教学示例应在每次复位后运行一次。反复执行会重新初始化全局消息队列、覆盖全局设备句柄并创建多个永久线程,可能产生竞争。
- 完整产品还应检查全部返回值,并设计设备关闭、线程退出、消息队列脱离、缓冲区溢出与二进制数据处理策略。