RTT_UART_DMA

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

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 接收需要同时打开两个层级:

  1. 全局串口 DMA 框架:在 RT-Thread Settings 中启用 Serial DMA Mode,使 rtconfig.h 生成 RT_SERIAL_USING_DMA。
  2. 具体串口与方向:在 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:

  1. 进入 Components → Device Drivers → Serial Device Drivers,也可搜索 serial 快速定位。
  2. 保持 UART 设备驱动已启用,勾选 Enable Serial DMA Mode。
  3. RX buffer size 保持本实验默认值 64。
  4. 按 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 代码分析: 线程实体与数据处理

  1. 线程用 rt_memset() 清零本轮的 msg。
  2. rt_mq_recv() 在队列为空时永久阻塞;回调成功发送消息后,线程被唤醒。
  3. RT-Thread 5.0.2 的 rt_mq_recv() 成功时返回实际收到的消息字节数,因此课程源码使用 result > 0 判断成功。
  4. 线程按照 msg.dev 和 msg.size 调用 rt_device_read(),把 DMA 缓冲区中的数据复制到应用缓冲区。
  5. 线程原样回写数据,并将其作为文本打印到控制台,然后回到队列继续等待。

版本注意:部分旧版 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_RXRT_DEVICE_FLAG_DMA_RX
接收粒度示例每次处理 1 字节一次通知可对应一批数据
回调到线程信号量,只表示“有数据”消息队列,携带设备句柄和长度
数据处理字节编码加 1 后回传原样回传,并打印到 UART1
发送方式轮询仍是轮询,并非 TX DMA
  • 本节正确逻辑是“DMA/驱动缓冲数据 → 回调发送消息 → 线程收到消息 → rt_device_read() 取数据”,不是“通过信号量实现 DMA 交互”。
  • DMA 减少的是 CPU 对每个字节搬运的参与度,适合连续或成批数据;回调、队列、线程和协议处理仍由 CPU 完成。
  • 教学示例应在每次复位后运行一次。反复执行会重新初始化全局消息队列、覆盖全局设备句柄并创建多个永久线程,可能产生竞争。
  • 完整产品还应检查全部返回值,并设计设备关闭、线程退出、消息队列脱离、缓冲区溢出与二进制数据处理策略。

参考资料

Prev: RTT_UART(下) Next: RTT_PIN