RTT_UART(上)

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

266.3.2RTT_UART(上)

// applications/main.c(视频最终状态):保留系统主线程,并关闭周期性日志,避免干扰 MSH 控制台。
#include <rtthread.h>                         // 引入 RT-Thread 内核接口,例如延时函数和 RT_EOK。
#define DBG_TAG "main"                       // 设置日志模块标签;本节虽然关闭了日志输出,但仍保留工程原定义。
#define DBG_LVL DBG_LOG                      // 设置日志等级为允许普通调试日志。
#include <rtdbg.h>                           // 引入 RT-Thread 调试日志接口。
int main(void)                               // RT-Thread 启动后进入的用户主函数。
{                                            // main 函数体开始。
    int count = 1;                           // 初始化循环计数器;本例只用它维持无限循环。
    while (count++)                          // count 从 1 持续递增,在本实验运行期间等价于一直循环。
    {                                        // 主循环开始。
        //LOG_D("Hello RT-Thread!");          // 视频中将周期日志注释掉,防止它不断插入 MSH 交互输出。
        rt_thread_mdelay(1000);              // 让当前线程延时 1000 ms,避免空转占满 CPU。
    }                                        // 主循环结束;正常运行时不会退出。
    return RT_EOK;                           // 保留标准成功返回值;由于前面是无限循环,通常不会执行到这里。
}                                            // main 函数体结束。

// applications/uart_test.c:视频从官方文档复制并实际运行的 UART2 中断接收示例。
#include <rtthread.h>                         // 引入设备、线程、信号量和 MSH 导出接口。
#define SAMPLE_UART_NAME       "uart2"        // 未在命令后指定设备名时,默认操作 RT-Thread 设备 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); // 永久等待接收回调释放信号量,避免无数据时忙轮询。
        }                                     // 读取等待循环结束,此时 ch 中已有 1 字节。
        ch = ch + 1;                          // 将该字节的编码值加 1,例如 ASCII 字符 '6' 变成 '7'。
        rt_device_write(serial, 0, &ch, 1);  // 通过同一串口发回处理后的 1 字节。
    }                                         // 外层无限循环结束;正常运行时不会退出。
}                                             // 工作线程函数体结束。
static int uart_sample(int argc, char *argv[]) // 定义 MSH 命令函数;参数来自命令行。
{                                             // uart_sample 函数体开始。
    rt_err_t ret = RT_EOK;                    // 默认将命令执行结果设为成功。
    char uart_name[RT_NAME_MAX];              // 准备设备名缓冲区,长度使用 RT-Thread 的名称上限。
    char str[] = "hello RT-Thread!\r\n";      // 准备串口打开后首先发送的问候字符串。
    if (argc == 2)                            // 若命令形式为 uart_sample 设备名,则使用用户给出的设备。
    {                                         // 命令带一个额外参数的分支开始。
        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);        // 按名称在 RT-Thread 已注册设备表中查找串口。
    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 命令。

本节工程为 13IO_DEVICE,RT-Thread 版本为 5.0.2。上面的两个文件按视频最终画面和配套工程整理;压缩包中已经出现的 UART3、DMA 等配置属于后续课程,本节不提前引入。

一、IO设备模型框架

1.1 框架理解难点

  • RT-Thread 的 I/O 设备模型包含应用层、通用设备框架、设备驱动框架和硬件驱动,多层对象与回调关系在第一次接触时不容易一次理解完整。
  • 本阶段不要求死记内部实现,先建立一条主线:查找设备 → 打开设备 → 设置回调 → 读写设备 → 必要时控制或关闭设备。
  • 后面的 UART、PIN、ADC、PWM、I2C 五类外设实验会反复使用同一套设备接口;每完成一个实验再回看框架,理解会更具体。

1.2 实验学习方法

  • 采用“以终为始”的方式:先让官方示例跑起来,再根据现象反查 API、驱动注册和硬件连接。
  • 每个实验都按三步学习:先会用,随后解释关键调用,最后复盘设备从注册到被应用访问的完整路径。
  • 本节先处理 UART;PIN、ADC、PWM 和 I2C 将在后续课程中继续验证设备模型。

1.3 官网代码使用

  • 官方示例不是只能阅读的伪代码。本节直接复制“串口设备中断接收并轮询发送”的示例,保存为业务文件 applications/uart_test.c。
  • 复制后仍要结合当前 BSP 修改设备名、引脚和硬件接线;示例中的 uart2 只有在 BSP 已注册同名设备时才能找到。
  • 先保留示例原有执行结构,等现象验证正确后再逐句分析,避免同时改动代码、驱动和硬件而难以定位问题。

二、RT-Thread文档中心

2.1 标准版本选择

  • 从 RT-Thread 文档中心进入“标准版 → 设备与驱动 → UART 设备”,可查看串口原理、配置参数、通用设备 API、接收回调以及完整示例。
  • 本节工程使用 RT-Thread 5.0.2。查阅在线文档时要注意文档版本与工程版本可能不同;若接口或宏有差异,以工程内头文件和源码为准。
  • Studio 中的 RT-Thread 相关入口可以快捷打开文档,但它不是新的协议或工具;原转录中的“rd three”应为 RT-Thread 的语音识别错误。

三、UART设备原理

3.1 串口通信参数

  • UART 是异步串行通信,双方没有共享时钟线,必须预先约定波特率、数据位、校验位和停止位。
  • 一帧通常由以下部分组成:线路空闲时为高电平;起始位为低电平;随后发送数据位(常见配置为 8 位、低位在前);可选奇偶校验位;最后用高电平停止位结束。
  • 本实验使用常见的 115200 8N1:115200 bit/s、8 个数据位、无校验、1 个停止位。
  • 串口是全双工接口:TX 负责发送、RX 负责接收。连接两个设备时要交叉连接 TX 与 RX,并连接公共 GND。

3.2 波特率说明

  • 波特率在本实验中可近似理解为每秒传输的比特数,不等于每秒传输的字节数。
  • 8N1 传输一个有效字节至少要占 1 个起始位、8 个数据位和 1 个停止位,共 10 bit,因此 115200 bit/s 的理论上限约为 11520 byte/s;实际吞吐量还会受软件处理和协议开销影响。
  • 两端参数不一致会产生乱码、丢帧或完全无法通信。排查时要同时核对波特率、数据位、校验位、停止位和电平标准。
  • open、read、write、control 属于设备访问 API,并不是“波特率”的组成部分;原稿将这两类内容混在了一起。

四、串口API函数

4.1 回调函数设置

本节用到的核心 API 如下:

API本例中的作用
rt_device_find()按名字查找已经注册的 uart2 设备
rt_device_open()以 RT_DEVICE_FLAG_INT_RX 打开串口,即中断接收、轮询发送
rt_device_set_rx_indicate()安装接收指示回调 uart_input
rt_device_read()从串口接收缓冲区读取数据
rt_device_write()轮询发送问候语或处理后的字节
rt_device_control()可用于修改串口配置;本示例没有调用
rt_device_close()关闭设备;本示例的常驻线程没有执行关闭流程
  • 数据到达后,设备框架调用 uart_input(dev, size)。本例忽略 dev 和 size,只释放信号量;这种做法让接收通知路径保持短小,把实际读取与处理放在线程上下文中完成。
  • 工作线程先尝试读取 1 字节;没有读到时才永久等待信号量。回调释放信号量后线程被唤醒,再次读取缓冲区。
  • rt_device_read() 的 pos 参数对 UART 没有实际意义,因此示例传入 -1。

五、串口示例代码

5.1 中断接收模式

本例的数据路径是:

  1. MSH 调用 uart_sample,程序查找并打开 uart2。
  2. 串口以中断方式接收字节。数据进入接收缓冲区后,框架调用 uart_input。
  3. 回调释放 rx_sem,让阻塞的 serial 线程恢复运行。
  4. 线程用 rt_device_read() 取出 1 字节,使它的编码值加 1,再用 rt_device_write() 发回。

工程的 rtconfig.h 将 RT_SERIAL_RB_BUFSZ 配为 64,因此本工程默认串口接收环形缓冲区为 64 字节。缓冲区容量不是“单次只能收 64 字节”的简单限制;若应用线程长期来不及读取,持续到达的数据仍可能使缓冲区溢出。

5.2 代码分析流程

  • uart_sample 可使用 uart_sample uart2 显式指定设备;只输入 uart_sample 时,宏 SAMPLE_UART_NAME 让它默认选择 uart2。
  • MSH_CMD_EXPORT 只负责把函数注册成 Shell 命令,真正的串口初始化在用户输入命令后才执行。
  • ch = ch + 1 增加的是一个字节的编码值,不是把十进制数值做加法。例如 ASCII '6'(0x36)变为 '7'(0x37),而 '9'(0x39)会变为 ':'(0x3A),不会变成字符串 "10"。
  • 本示例适合演示,但不是完整产品代码:没有检查信号量初始化、设备打开、回调安装和首次发送的返回值,也没有设计线程退出、设备关闭与信号量脱离流程。
  • 同一上电周期不宜反复运行 uart_sample:它会重复初始化全局信号量并创建新的常驻线程。本实验按“每次上电成功运行一次”理解。

六、项目创建

6.1 工程参数设置

  • 在 RT-Thread Studio 中新建芯片工程,项目名为 13IO_DEVICE。
  • RT-Thread 版本选择 5.0.2,芯片系列/型号选择 STM32F103ZE;实际 CubeMX 器件为 STM32F103ZETx。
  • 控制台选择 UART1,TX 为 PA9、RX 为 PA10;这是 MSH 命令行所在通道。
  • 新建完成后先编译基础工程,确认工具链、BSP 和链接脚本均正常,再加入实验代码。

6.2 调试器配置

  • 调试器选择 ST-LINK,调试接口使用 SWD。ST-LINK 负责下载和调试,不应与 UART 串口通道混为一谈。
  • 视频中新建工程后修改的是 RT-Thread Settings 中的工程配置,并非不存在的“rtv 选项”。
  • 若编译按钮不可用,先在工程或 .c 文件中单击,让 Studio 获得当前工程上下文,再执行构建。

七、时钟配置

7.1 CubeMX设置

  • 通过工程中的 CubeMX 配置打开芯片设置,HSE 选择外部晶振(Crystal/Ceramic Resonator),将系统时钟配置为 72 MHz。
  • CubeMX 的时钟树区域可横向滚动;应核对 SYSCLK、AHB、APB1 和 APB2,而不是只看最终的 72 MHz 数字。
  • 本板所用器件为 STM32F103ZETx。这里的 x 表示仍需由完整订货号确定温度等级等后缀,不能把它识别成“103 z1”。

7.2 芯片封装类型

  • STM32F103ZET6 中,Z 表示 144 引脚,E 表示 512 KB Flash,T 表示 LQFP 封装,6 表示工业温度等级。课程所选器件在 CubeMX 中对应 LQFP144。
  • ST 的订货规则中 H 表示 BGA、T 表示 LQFP。LQFP 的引脚从封装四边伸出;BGA 通过芯片底部的焊球阵列连接 PCB,不能简单说成“有引脚”和“只有焊盘”。
  • “LLQFP”不是该数据手册列出的封装名称,应为 LQFP;同系列还可能有 LFBGA、WLCSP 等封装。
  • 封装会改变外形、焊盘/焊球、引脚编号与 PCB 封装,选型时必须与板上实际芯片一致,不能只凭核心型号相近就替换。

八、串口驱动配置

8.1 控制台串口选择

  • CubeMX 集成流程可能重新生成板级时钟和外设初始化配置。本工程把 USART1 保持为已启用状态,确保 PA9/PA10 上的 UART1 控制台继续可用;不能笼统理解为“CubeMX 一生成代码,RT-Thread 就必然关闭 UART1 驱动”。
  • 其余不需要的外设保持默认,生成代码后关闭 CubeMX,Studio 会刷新工程;生成内容位于 cubemx 目录,不是原转录中的“qmx”。
  • 回到 Studio 后重新编译,确认 CubeMX 生成代码与 RT-Thread BSP 可以共同通过构建。
  • rtconfig.h 中的控制台设备名为 uart1;本节的业务数据通道是 uart2。两个通道职责不同,所以 UART2 收发结果不会自动出现在 UART1 的 MSH 窗口中。

九、代码移植

9.1 测试代码创建

  • 在 applications 下新建 uart_test.c,粘贴官方的“中断接收、轮询发送”示例,完整业务源码见本文标题下方。
  • 文件名是否含下划线不会决定命令名;命令名来自 MSH_CMD_EXPORT(uart_sample, ...) 中导出的函数 uart_sample。
  • UART2 测试代码由 UART1 上的 MSH 命令启动,但数据通过 UART2 的 PA2/PA3 收发。

9.2 编译验证

  • 加入 uart_test.c 后重新编译,确认没有未定义符号、重复定义或语法错误。
  • main.c 原本每秒执行 LOG_D("Hello RT-Thread!")。视频将该行注释掉,只保留 1000 ms 延时,避免周期日志打断 MSH 命令输入和输出。
  • 编译成功只说明生成了固件;还要下载到开发板并复位/重新运行,新的设备配置和命令才会在目标板上生效。

十、硬件连接

10.1 串口接线说明

  • ST-LINK 连接开发板的 SWD 接口,负责固件下载与调试。
  • 白色 USB 串口连接 UART1,作为 RT-Thread MSH 控制台;UART2 另接一个 CH340 USB-TTL 模块,视频中可通过黄/橙线辨认;蓝/紫线连接的另一模块属于 UART3,当前实验不用。
  • UART2 接线遵循交叉原则:开发板 PA2(USART2_TX)接 CH340 的 RX,PA3(USART2_RX)接 CH340 的 TX,双方 GND 相连。
  • 必须使用与开发板兼容的 TTL 电平,通常为 3.3 V 逻辑;不能把传统 RS-232 的正负电压直接接到 MCU 引脚。若转换模块可选 3.3 V/5 V,应先核对模块和板卡规格。
  • Windows 分配的 COM 号会随电脑、USB 口和枚举顺序变化,不能照抄视频中的 COM16 或 COM19;应在设备管理器或终端工具中识别对应转换器。

十一、功能测试

11.1 命令行运行

  1. 编译、下载并复位开发板,让 UART2 的板级宏和 uart_sample 命令注册生效。
  2. 打开 UART1 对应的 MSH 控制台,输入 uart_sample;也可输入 uart_sample uart2。
  3. 初始化成功时,UART2 会先发出 hello RT-Thread!,并启动名为 serial 的工作线程。MSH 没有额外成功提示是正常的。
  4. 再用串口助手打开 UART2 对应的 COM 口,参数设为 115200、8 数据位、无校验、1 停止位。发送 666 应收到 777;发送 234 应收到 345。

注意:问候语是在执行命令时立刻从 UART2 发出的。如果串口助手在命令执行之后才打开,可能看不到这一次问候语,但后续逐字节回传仍可测试。

十二、串口设备调试

12.1 串口设备查找失败

12.1.1 代码功能说明
  • 初次执行 uart_sample 时,控制台实际报错为 find uart2 failed!,不是原稿中的“uart name filed”。
  • 该错误来自 rt_device_find(uart_name) 返回空指针,说明 RT-Thread 的已注册设备表中没有名为 uart2 的设备。
  • 错误发生在打开设备、设置回调和创建线程之前,因此此时不是波特率、串口助手或 TX/RX 接反导致的。
12.1.2 设备查找失败原因
  • 新工程的 drivers/board.h 当时只启用了 UART1;UART2 对应的板级配置没有参与编译,底层驱动也就不会创建并注册 uart2。
  • 应用层写了 SAMPLE_UART_NAME "uart2" 并不会自动生成硬件设备。必须同时具备 BSP 宏、正确引脚、驱动组件和重新运行后的设备注册。
  • 排查 RT-Thread 外设“找不到”时,先用 list_device(若工程提供该命令)检查注册表,再核对 board.h、RT-Thread Settings、驱动初始化返回值和固件是否已经重新下载。

12.2 设备配置文件修改

12.2.1 boardh文件重要性
  • board.h 是该 STM32 BSP 的板级配置入口,声明本板启用哪些 UART 以及它们使用哪些引脚;它不是所有 RT-Thread 工程通用、能够配置一切外设的唯一文件。
  • 驱动源码根据这些宏生成 UART 配置对象,并在系统初始化阶段调用串口设备注册接口。注册完成后,应用层才能用名字找到设备。
  • 因此,业务代码、板级宏和物理接线必须一致:本例三者都指向 UART2 / PA2 / PA3。
12.2.2 串口设备宏定义

本节在 drivers/board.h 中只新增以下三行:

#define BSP_USING_UART2                  // 让 STM32 UART 驱动把 USART2 纳入编译与注册。
#define BSP_UART2_TX_PIN       "PA2"     // 指定 UART2 发送引脚为 PA2。
#define BSP_UART2_RX_PIN       "PA3"     // 指定 UART2 接收引脚为 PA3。
  • BSP_USING_UART2 决定是否实例化 UART2;TX/RX 宏告诉驱动使用哪组管脚。
  • 本节只使用中断接收与轮询发送,不配置 DMA,所以 board.h 注释中的 DMA 步骤暂时跳过。
  • 配套压缩包的最终 13IO_DEVICE 工程还含 UART3 与 DMA 宏,那是后续实验累积的结果,不属于本节视频当时的配置。
12.2.3 引脚配置修改
  • 不能只把 UART1 文本替换成 UART2 就认为引脚必然正确;应查芯片数据手册和开发板原理图。本芯片默认映射中 PA2 为 USART2_TX,PA3 为 USART2_RX。
  • 本例没有启用 USART2 引脚重映射,因此使用 PA2/PA3。若启用了 AFIO 重映射,实际管脚会变化,BSP 宏和接线也必须随之修改。
  • 三个宏加入后要重新编译、下载并复位。只有新固件启动并完成驱动注册,rt_device_find("uart2") 才可能成功。

12.3 代码编译运行

  • 修改 board.h 后重新构建并烧写。再次在 UART1 控制台执行 uart_sample,不再出现 find uart2 failed!,表示设备查找与初始化已经继续向后执行。
  • 控制台没有显示 UART2 的回传字符并不代表失败,因为当前窗口连接的是 uart1;UART2 的发送数据在另一只 CH340 对应的 COM 口上。
  • 若仍失败,依次检查:固件是否重新下载、板级宏是否在当前构建配置中生效、list_device 是否出现 uart2、PA2/PA3 是否被其他功能占用。

12.4 串口终端使用

12.4.1 串口设备切换
  • 多个 USB 串口同时连接时,可通过设备管理器中的硬件标识、逐个插拔观察新增端口,或按线缆标记确认 UART1/UART2,而不是猜测相邻 COM 号。
  • 串口助手选择 UART2 对应的 COM 口,配置为 115200 8N1。切换端口或参数后要先关闭旧连接再重新打开,避免端口被其他程序占用。
  • MobaXterm、Xterm 风格终端或普通串口助手本质上都可以建立串口会话;能否测试成功取决于它们是否打开了正确的 UART2 端口和参数,而不是某个软件天生“不支持”该功能。
12.4.2 测试代码功能
  • 代码每次读出 1 字节,将其编码加 1 后立即发回,所以发送 ASCII 字符串 666 会收到 777,发送 234 会收到 345。
  • 输入字母同样按编码处理,例如 A 会变成 B。输入 9 会得到 :,这能证明程序没有进行十进制字符串解析。
  • 如果串口助手勾选“十六进制发送”,发送的是所填十六进制字节;如果勾选“发送新行”,结尾的 \r、\n 也会分别被加 1 后回传。验证时要明确当前是文本模式还是十六进制模式。
  • 收不到数据时检查 UART2 的 TX/RX 是否交叉、GND 是否共地、COM 口是否正确,以及命令是否已在 UART1 控制台成功执行。

12.5 控制台工作原理

12.5.1 控制台交互演示
  • 串口终端窗口本身只负责收发字节。键盘输入经 UART1 发送给开发板,RT-Thread 的 FinSH/MSH 组件读取命令、执行后再把提示符和结果写回 UART1。
  • 因此输入 list、help 等命令后出现的命令结果来自目标板。键入字符是否立即可见还受目标端回显和终端“本地回显”设置影响,不能仅凭字符颜色断定数据来源。
  • 这与普通聊天软件的本地文本框不同:串口终端如果既没有本地回显,目标板也没有回显,按键后屏幕可以暂时什么都不显示。
12.5.2 控制台实现原理
  • 本工程在 rtconfig.h 中启用了 FinSH/MSH,并把 RT_CONSOLE_DEVICE_NAME 设为 uart1。控制台由 Shell 组件、RT-Thread 控制台接口和 UART1 驱动共同实现,不宜笼统称为“内核里的一堆代码”。
  • UART1 的数据流是“终端键盘 → UART1 RX → MSH 解析/执行 → UART1 TX → 终端显示”;UART2 示例的数据流是“串口助手 → UART2 RX → 加 1 线程 → UART2 TX → 串口助手”。
  • UART2 在当前工程配置中不是控制台,但它并非永远不能当控制台。若修改控制台设备名、正确配置驱动与引脚,UART2 也可以承担控制台功能;本实验只是有意将命令通道和测试数据通道分开。

参考资料