RTT_UART(下)

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

267.3.3RTT_UART(下)

#include <rtthread.h>                         // 引入 RT-Thread 的设备、线程、信号量和 MSH 命令接口。
#define SAMPLE_UART_NAME       "uart2"        // 未指定命令行参数时,默认使用名为 uart2 的设备。
static struct rt_semaphore rx_sem;            // 定义静态信号量,用于通知工作线程“串口已有数据”。
static rt_device_t serial;                    // 保存当前选中的串口设备句柄。
static rt_err_t uart_input(rt_device_t dev, rt_size_t size) // 定义接收指示回调;本例未使用 dev 和 size。
{                                             // 接收回调函数体开始。
    rt_sem_release(&rx_sem);                  // 释放一次信号量,唤醒等待接收数据的工作线程。
    return RT_EOK;                            // 向设备框架返回处理成功。
}                                             // 接收回调函数体结束。
static void serial_thread_entry(void *parameter) // 定义串口工作线程入口;本例未使用 parameter。
{                                             // 工作线程函数体开始。
    char ch;                                  // 保存每次读取的 1 字节数据。
    while (1)                                 // 线程持续处理串口数据。
    {                                         // 外层无限循环开始。
        while (rt_device_read(serial, -1, &ch, 1) != 1) // 尝试读 1 字节;UART 不使用位置参数,所以这里传 -1。
        {                                     // 没有成功读到 1 字节时进入等待。
            rt_sem_take(&rx_sem, RT_WAITING_FOREVER); // 永久等待接收回调释放信号量,避免无数据时忙轮询。
        }                                     // 读到 1 字节后退出内层循环。
        ch = ch + 1;                          // 将该字节的编码值加 1,例如 ASCII 字符 '1' 变为 '2'。
        rt_device_write(serial, 0, &ch, 1);  // 把处理后的 1 字节通过当前串口发回电脑。
    }                                         // 外层无限循环结束;正常运行时不会退出。
}                                             // 工作线程函数体结束。
static int uart_sample(int argc, char *argv[]) // 定义 MSH 命令函数;argc 是参数个数,argv 保存各参数字符串。
{                                             // uart_sample 函数体开始。
    rt_err_t ret = RT_EOK;                    // 默认把命令执行结果设为成功。
    char uart_name[RT_NAME_MAX];              // 准备保存串口设备名的缓冲区。
    char str[] = "hello RT-Thread!\r\n";      // 准备设备打开后立即发送的问候字符串。
    if (argc == 2)                            // 命令名加一个设备名时,参数总数恰好为 2。
    {                                         // 指定设备名的分支开始。
        rt_strncpy(uart_name, argv[1], RT_NAME_MAX); // 把第二个参数复制为本次要查找的设备名。
    }                                         // 指定设备名的分支结束。
    else                                      // 未恰好提供一个设备名参数时,使用默认设备。
    {                                         // 默认设备分支开始。
        rt_strncpy(uart_name, SAMPLE_UART_NAME, RT_NAME_MAX); // 把默认的 uart2 复制到本地缓冲区。
    }                                         // 默认设备分支结束。
    serial = rt_device_find(uart_name);        // 按名称查找已经由 BSP 注册的串口设备。
    if (!serial)                              // 返回空句柄表示设备不存在。
    {                                         // 设备查找失败处理开始。
        rt_kprintf("find %s failed!\n", uart_name); // 在控制台打印查找失败的设备名。
        return RT_ERROR;                      // 返回错误,不再继续初始化。
    }                                         // 设备查找失败处理结束。
    rt_sem_init(&rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO); // 初始化计数为 0、按 FIFO 顺序唤醒的信号量。
    rt_device_open(serial, RT_DEVICE_FLAG_INT_RX); // 以中断接收方式打开设备;发送未指定中断或 DMA,采用轮询方式。
    rt_device_set_rx_indicate(serial, uart_input); // 把 uart_input 注册为接收指示回调。
    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); // 创建栈 1024 字节、优先级 25、时间片 10 tick 的线程。
    if (thread != RT_NULL)                    // 检查线程对象是否创建成功。
    {                                         // 线程创建成功分支开始。
        rt_thread_startup(thread);            // 启动工作线程,使其参与系统调度。
    }                                         // 线程创建成功分支结束。
    else                                      // 线程对象创建失败时进入此分支。
    {                                         // 线程创建失败分支开始。
        ret = RT_ERROR;                       // 把命令返回值改为错误。
    }                                         // 线程创建失败分支结束。
    return ret;                               // 向 MSH 返回初始化结果。
}                                             // uart_sample 函数体结束。
MSH_CMD_EXPORT(uart_sample, uart device sample); // 将函数导出为可在 MSH 中输入的 uart_sample 命令。

本节继续分析上一节创建的 applications/uart_test.c,代码未改成 DMA 版本。视频中曾把 rt_device_write() 的 pos 临时改为 2 和 -3 做实验,确认 UART 不使用该参数后恢复为 0;上方收录的是本节最终源码。

一、RT-Thread

1.1 单片机复位

  • 阅读这类示例时,可以先从文件末尾的 MSH_CMD_EXPORT 找到命令入口,再回到 uart_sample(),最后跟进它调用的回调函数和线程入口。这是顺着调用关系阅读,并不是所有 C 代码都必须机械地“从下往上看”。
  • uart_sample() 可以理解为这条 MSH 命令的入口函数,但它不是新的 main():系统启动入口没有改变,只有用户在 Shell 中输入命令后,MSH 才调用它。
  • hello RT-Thread! 来自 uart_sample() 中的 rt_device_write(),不是单片机复位时自动打印的启动信息。复位会清除本例创建的线程和信号量;复位完成后还要重新输入 uart_sample,才会再次初始化并发送问候语。
  • 如果串口助手在执行命令之后才打开,问候语可能已经发送完毕;这不影响后续逐字节回传测试。

1.2 RT-Thread Studio文件

  • 本节分析的是业务文件 applications/uart_test.c,上方已按视频与配套工程原样收录并逐行补充注释。
  • 初次阅读时可先把 argc、argv 看成 MSH 传给命令函数的参数,等理解设备查找、打开、回调和线程流程后,再回头分析命令行传参。
  • argc 的完整英文是 argument count,表示参数个数;argv 是 argument vector,保存每个参数字符串的地址。原稿中的“AR jc”是 argc 的语音识别错误。

1.3 查找设备

  • rt_device_find(uart_name) 按名字查询 RT-Thread 已注册设备对象,成功时返回设备句柄,失败时返回 RT_NULL。它不是扫描物理引脚,也不会仅凭字符串 "uart3" 自动创建 UART3。
  • 若 board.h 没有启用相应串口,底层驱动就不会注册该设备,程序会打印 find uart3 failed!。补齐板级宏、重新编译、下载并复位后,设备才可能被找到。
  • serial 是全局设备句柄。后续的 open、设置回调、read 和 write 都通过这个句柄操作同一设备。
  • rt_sem_init(&rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO) 把信号量初值设为 0,因此没有接收事件时,工作线程会阻塞;RT_IPC_FLAG_FIFO 表示有多个等待线程时按先等待、先唤醒的顺序处理。
  • rt_device_open(serial, RT_DEVICE_FLAG_INT_RX) 选择中断接收。由于没有同时指定 RT_DEVICE_FLAG_INT_TX 或 RT_DEVICE_FLAG_DMA_TX,本例发送采用轮询方式;INT_RX 不是原稿中的“int 2叉”。

1.4 设置回调函数

  • rt_device_set_rx_indicate(serial, uart_input) 把 uart_input 注册为接收指示回调。串口数据由中断接收并放入接收缓冲区后,设备框架调用该函数通知上层。
  • 回调参数 dev 是产生通知的设备句柄,size 表示当前接收缓冲区中的数据量;本例没有利用这两个参数,只释放一次 rx_sem。
  • 回调与硬件中断服务程序不是同一个函数。更准确的关系是:底层 ISR 处理硬件并通知串口框架,框架再调用应用注册的接收回调;本例的回调通常运行在中断上下文中,所以只做可在中断中使用的信号量释放,不进行耗时的数据处理。
  • rt_sem_release() 只负责发出“有数据到达”的同步通知,字符本身仍保存在串口接收缓冲区中,随后由线程调用 rt_device_read() 取走。

1.5 发送字符串

函数原型可概括为:

rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size); // 向设备写入 buffer 指向的 size 字节数据。
  • dev 是设备句柄,buffer 是待发送数据的首地址,size 是期望写入的字节数,返回值是实际写入的字节数。
  • pos 在通用设备接口中表示写位置或偏移,但 UART 是字符流设备,不使用这个参数。因此视频把它从 0 改为 2、-3,发送结果仍然是完整的 hello RT-Thread!。
  • 改变 pos 不会让 buffer 自动偏移。如果确实想从字符串第三个字符开始发送,应改变缓冲区指针和长度,例如传入 &str[2] 与相应长度,而不是修改 UART 的 pos。
  • sizeof(str) - 1 排除了 C 字符串末尾自动附加的 \0,但会发送正文以及 \r\n。
  • 本示例没有检查 rt_device_write() 的返回值。产品代码应比较实际写入长度,不能默认所有字节都已发送成功。

1.6 创建线程

rt_thread_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10) 的参数含义为:

参数本例值含义
线程名"serial"用于对象管理和调试显示
入口函数serial_thread_entry线程启动后执行的函数
入口参数RT_NULL本例不向线程传递私有参数
栈大小1024动态分配 1024 字节线程栈
优先级25RT-Thread 数值越小优先级越高;本工程共有 32 个优先级
时间片1010 个 OS tick;本工程 RT_TICK_PER_SECOND=1000,所以恰好是 10 ms
  • 原稿中的“优先级可能为 3”不正确,源码明确是 25。
  • rt_thread_create() 只创建线程对象;返回非空后还要调用 rt_thread_startup(),线程才进入就绪队列并参与调度。
  • 若线程创建失败,代码只把 ret 改为 RT_ERROR。已打开的设备和已初始化的信号量没有在失败路径中清理,这是教学示例的简化之处。

1.7 接收信号量分析

工作流程应按下面的时序理解:

  1. serial 线程进入外层无限循环,先调用 rt_device_read(serial, -1, &ch, 1) 尝试读取 1 字节。
  2. 如果返回值不是 1,说明本次没有成功取得完整的 1 字节,线程进入内层循环并用 rt_sem_take(..., RT_WAITING_FOREVER) 阻塞。
  3. UART 收到数据后,底层中断将数据放入接收缓冲区,随后调用 uart_input();回调释放信号量。
  4. 等待中的线程被唤醒,回到内层 while 的条件处,再次调用 rt_device_read()。
  5. 成功读取 1 字节时返回 1,表达式 1 != 1 为假,线程退出内层循环。
  6. 线程执行 ch = ch + 1,再把结果写回当前串口;之后进入下一轮,若没有新数据就再次阻塞。

补充说明:如果数据和信号量通知在线程调用 read 前已经到达,第一次 read 可能直接成功,不必先阻塞。这里的信号量是唤醒机制,真正判断是否读到数据的依据仍是 rt_device_read() 的返回值。

1.8 传输参数分析

  • 只输入 uart_sample 时,argc == 1,代码进入 else,把默认宏 SAMPLE_UART_NAME 的值 "uart2" 复制到 uart_name。
  • 输入 uart_sample uart3 时,argc == 2,其中 argv[0] 是 "uart_sample",argv[1] 是 "uart3";代码查找并操作 uart3。
  • 设备名是注册名,应使用工程中的准确拼写与大小写。当前 STM32 BSP 注册的是小写 uart1、uart2、uart3,不是显示文字“串口三”或大写 UART3。
  • 该示例对 argc 的处理较宽松:输入零个附加参数或两个以上附加参数都会退回默认 uart2,并不会提示“参数过多”。
  • RT_NAME_MAX 在本工程中为 8,uart1~uart3 都能安全放入缓冲区。若把示例推广到更长的设备名,还应注意 rt_strncpy() 在源字符串过长时的结尾 \0 问题。

1.9 串口DMA配置

原标题写作“串口 DMA 配置”,但视频本节实际只新增了 UART3 的普通中断接收配置;讲师在结尾明确把 DMA 示例留到下一课。本节不能提前加入 DMA 宏或 DMA 业务代码。

在 drivers/board.h 中新增:

#define BSP_USING_UART3                  // 让 STM32 串口驱动实例化并注册 uart3。
#define BSP_UART3_TX_PIN       "PB10"    // 指定 USART3 默认发送引脚为 PB10。
#define BSP_UART3_RX_PIN       "PB11"    // 指定 USART3 默认接收引脚为 PB11。
  • STM32F103ZE 数据手册给出的默认复用功能是 PB10/USART3_TX、PB11/USART3_RX;原转录把 PB11 识别成 TX,正确方向是 RX。
  • PB10、PB11 还复用 I2C2 和定时器功能,工程中不能在没有协调复用配置的情况下让这些外设同时占用同一引脚。
  • 物理连接仍要交叉:开发板 PB10/TX 接 USB-TTL 模块 RX,PB11/RX 接模块 TX,并连接公共 GND;使用与 MCU 兼容的 3.3 V TTL 电平。
  • 三行宏必须全部使用 UART3。从 UART2 配置复制时如果漏改其中一处,可能导致宏不匹配或 UART3 没有被注册。
  • 修改后必须重新编译、下载并复位,再执行 uart_sample uart3。仅修改编辑器中的文件不会改变开发板当前运行的固件。

1.10 应用案例

1.10.1 例题: UART通信测试
  • 视频电脑当时的映射是 COM19→UART1、COM16→UART2、COM17→UART3,这只是那台电脑当次枚举的结果,不能作为固定规律。
  • UART1 用作 MSH 控制台;UART2、UART3 各接一只 CH340 USB-TTL 模块。测试 UART3 时,在 UART1 控制台输入 uart_sample uart3,再从 UART3 对应串口助手观察 hello RT-Thread! 并测试字符加一回传。
  • Windows 上同一个 COM 口通常只能被一个串口程序独占打开。如果 MobaXterm 已打开 UART1,另一个串口助手会提示端口被占用;先关闭前一个会话,后一个程序才能打开该 COM 口。
  • 发送文本 1 会收到文本 2,发送 hello 会按每个 ASCII 字节分别加 1 后返回;这不是字符串整体加法。若使用十六进制发送模式,显示和输入规则也随终端设置改变。
1.10.2 例题: 窗口一UART通信测试
  • 复位后可在 UART1 的 MSH 中输入 uart_sample uart1。设备查找可能成功,问候语也能从 UART1 发出;所以“UART1 被控制台使用就绝对不能再打开”并不准确。
  • 但此时 MSH 和新建的 serial 线程都从同一个 uart1 接收缓冲区读数据。某个输入字节会被其中一个读取者先取走,Shell 命令可能残缺,示例线程也可能收不到预期数据,行为不再可靠。
  • 电脑端还存在独立的端口占用问题:若 MSH 终端软件仍占有 UART1 的 COM 口,串口助手无法同时打开它。关闭 MSH 会话只能解除电脑端占用,不能消除板端两个读取者竞争同一设备的问题。
  • 因此本实验推荐 UART1 专用于控制台,UART2 或 UART3 专用于业务数据;复位可以终止本例创建的工作线程并恢复干净的演示环境。

1.11 传参原理分析

命令行与参数数组的对应关系如下:

MSH 输入argcargv[0]argv[1]最终设备名
uart_sample1uart_sample不存在默认 uart2
uart_sample uart22uart_sampleuart2uart2
uart_sample uart32uart_sampleuart3uart3
uart_sample uart12uart_sampleuart1uart1,但会与控制台竞争
uart_sample uart3 extra3uart_sampleuart3因 argc != 2,反而退回默认 uart2
  • Shell 会把命令名本身计入 argc,所以“传入一个设备名”时参数总数是 2;再多传一个字符串时总数是 3。
  • argv 不是把设备句柄直接传给函数。代码先把 argv[1] 的字符串复制到 uart_name,再由 rt_device_find() 根据名字取得真正的设备句柄,并写入全局变量 serial。
  • 选择 uart3 后,后续 open/read/write 使用的是 UART3 句柄;选择 uart2 时则使用 UART2 句柄。业务流程没有因为换串口而复制一份,这是通用设备接口带来的复用能力。
  • 教学实验应在每次复位后只运行一次 uart_sample。不复位就重复执行会重新初始化全局信号量、覆盖全局 serial 句柄并新建另一个永久循环线程,多个线程将共享同一组全局对象,可能产生竞争和不可预期结果。

参考资料