串口23代码调试

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

279.5串口23代码调试

/* drivers/board.h:本节补齐的 UART2、UART3 板级配置。 */
#define BSP_USING_UART2                                  // 使能 STM32 BSP 的 USART2 驱动,使系统注册 uart2 设备。
#define BSP_UART2_TX_PIN       "PA2"                     // 把 USART2 的发送引脚配置为 PA2。
#define BSP_UART2_RX_PIN       "PA3"                     // 把 USART2 的接收引脚配置为 PA3。
#define BSP_USING_UART3                                  // 使能 STM32 BSP 的 USART3 驱动,使系统注册 uart3 设备。
#define BSP_UART3_TX_PIN       "PB10"                    // 把 USART3 的发送引脚配置为 PB10。
#define BSP_UART3_RX_PIN       "PB11"                    // 把 USART3 的接收引脚配置为 PB11;本节业务暂未使用接收功能。

/* uart_cfg.h:本节新增的接收状态与 UART3 声明。 */
typedef struct                                           // 定义用于管理 UART2 接收状态的结构体类型。
{                                                        // UART_RX_typedef 结构体成员开始。
    rt_uint8_t rx_buf[20];                               // 分配 20 字节接收缓冲区;本节每次实际解析 4 字节帧。
    rt_uint8_t rx_cnt;                                   // 记录当前已缓存的字节数,同时也是下一个字节的写入下标。
}UART_RX_typedef;                                        // 将该结构体类型命名为 UART_RX_typedef。
UART_RX_typedef uart2_rx_info;                           // 定义 UART2 的接收缓冲区和计数状态对象。
#define SAMPLE_UART3_NAME       "uart3"                  // 定义要从 RT-Thread 设备管理器中查找的 UART3 设备名。
rt_device_t serial3;                                     // 定义 UART3 设备句柄,供语音指令发送使用。
int uart2_init(void);                                    // 声明 UART2 初始化函数。
int uart3_init(void);                                    // 声明 UART3 初始化函数。

/* uart_cfg.c:在 uart2_init() 开头新增的计数器初始化语句。 */
uart2_rx_info.rx_cnt = 0;                                // 首次启动 UART2 时,从接收缓冲区第 0 个位置开始写入。

/* uart_cfg.c:本节用帧解析逻辑替换上一节的“加一并回发”测试代码。 */
void serial2_thread_entry(void *parameter)               // 定义 UART2 接收线程入口;本节没有使用 parameter 参数。
{                                                        // serial2_thread_entry() 函数体开始。
    char ch;                                             // 定义临时变量,每次从 UART2 读取 1 个字节。
    while (1)                                            // 永久循环,使线程持续接收 K210 的返回数据。
    {                                                    // 外层接收循环开始。
        while (rt_device_read(serial2, -1, &ch, 1) != 1) // 尝试读取 1 字节;没有读到时进入等待循环。
        {                                                // 等待接收通知的循环开始。
            rt_sem_take(&uart2_rx_sem, RT_WAITING_FOREVER); // 阻塞等待 UART2 接收回调释放信号量。
        }                                                // 等待接收通知的循环结束。
        uart2_rx_info.rx_buf[uart2_rx_info.rx_cnt] = ch; // 把刚收到的字节写入当前计数值对应的缓冲区位置。
        if(uart2_rx_info.rx_buf[0] == 0xAF)              // 检查当前候选帧的第 1 字节是否为 K210 返回帧头 0xAF。
        {                                                // 帧头正确的处理分支开始。
            uart2_rx_info.rx_cnt++;                      // 已缓存字节数加 1,并把写入下标移到下一位置。
        }else {                                          // 当前候选帧不是以 0xAF 开头时执行此分支。
            uart2_rx_info.rx_cnt=0;                      // 丢弃当前字节,回到缓冲区开头等待下一帧。
        }                                                // 帧头判断结束。

        if(uart2_rx_info.rx_cnt>3)                       // 已收到 4 字节后,开始判断完整的固定长度返回帧。
        {                                                // 4 字节帧解析区开始。
            if(uart2_rx_info.rx_buf[1] == 0x05 && uart2_rx_info.rx_buf[2] == 0x0 && uart2_rx_info.rx_buf[3] == 0xFA) // 判断 AF 05 00 FA:人脸录入成功。
            {                                            // 人脸录入成功分支开始。
                const uint8_t voice_add_face_succ[5] = {0xAA ,0x55 ,0x01 ,0x55 ,0xAA }; // 定义“人脸录入成功”的 5 字节语音帧。
                rt_device_write(serial3, 0, voice_add_face_succ, 5); // 通过 UART3 向语音模块发送完整的 5 字节语音帧。
            }                                            // 人脸录入成功分支结束。
            if(uart2_rx_info.rx_buf[1] == 0x02 && uart2_rx_info.rx_buf[3] == 0xFA) // 判断 AF 02 编号 FA:人脸识别成功,编号可变。
            {                                            // 人脸识别成功分支开始。
                const uint8_t voice_check_face_succ[5] = {0xAA ,0x55 ,0x02 ,0x55 ,0xAA }; // 定义“人脸识别成功”的 5 字节语音帧。
                rt_device_write(serial3, 0, voice_check_face_succ, 5); // 通过 UART3 触发“人脸识别成功”语音。
            }                                            // 人脸识别成功分支结束。
            if(uart2_rx_info.rx_buf[1] == 0x01 && uart2_rx_info.rx_buf[2] == 0x0 && uart2_rx_info.rx_buf[3] == 0xFA) // 判断 AF 01 00 FA:人脸删除成功。
            {                                            // 人脸删除成功分支开始。
                const uint8_t voice_del_face_succ[5] = {0xAA ,0x55 ,0x03 ,0x55 ,0xAA }; // 定义“人脸删除成功”的 5 字节语音帧。
                rt_device_write(serial3, 0, voice_del_face_succ, 5); // 通过 UART3 触发“人脸删除成功”语音。
            }                                            // 人脸删除成功分支结束。
            uart2_rx_info.rx_cnt=0;                      // 无论 4 字节内容是否匹配,都清零计数并等待下一候选帧。
        }                                                // 4 字节帧解析区结束。
    }                                                    // 外层接收循环结束。
}                                                        // serial2_thread_entry() 函数体结束。

/* uart_cfg.c:本节新增的 UART3 初始化函数。 */
int uart3_init(void)                                     // 定义 UART3 初始化函数,供 main() 调用。
{                                                        // uart3_init() 函数体开始。
    rt_err_t ret = RT_EOK;                               // 先把返回结果设为成功。
    char str[] = "uart3 init successful!\r\n";          // 定义 UART3 初始化完成后的控制台提示文本。
    serial3 = rt_device_find(SAMPLE_UART3_NAME);          // 按名称查找 BSP 已注册的 uart3 设备。
    if (!serial3)                                        // 判断是否没有找到 uart3 设备。
    {                                                    // UART3 查找失败分支开始。
        rt_kprintf("find %s failed!\n", SAMPLE_UART3_NAME); // 在默认控制台打印查找失败的设备名。
        return RT_ERROR;                                 // 返回错误,停止后续 UART3 初始化。
    }                                                    // UART3 查找失败分支结束。
    rt_device_open(serial3, RT_DEVICE_FLAG_INT_RX);      // 按课程源码以中断接收标志打开 UART3;本节实际只调用其发送接口。
    rt_kprintf(" %s\n", str);                           // 在默认控制台打印 UART3 初始化成功提示。
    return ret;                                          // 返回 UART3 初始化结果。
}                                                        // uart3_init() 函数体结束。

/* main.c:本节新增并强调的初始化顺序。 */
uart3_init();                                            // 先初始化语音串口,保证 serial3 句柄可供接收线程使用。
uart2_init();                                            // 再初始化 K210 串口并启动可能立即使用 serial3 的接收线程。

文首代码只收录本节实际新增或修改的业务片段。课程归档中的 k210_last_tick、face_check_flag、5 秒抑制和开门逻辑属于下一节,不能提前算入本节;原注释已移除,代码本身、名称与数值保持课程写法,仅补充逐行说明。

一、人脸识别项目

本节把上一节搭好的 UART2 接收线程改造成一个简易的 4 字节帧解析器,并增加 UART3,把 K210 的识别结果转成 SU-03T 语音模块能够识别的 5 字节命令。完整数据路径是:

按键 → STM32 UART2 → K210 → STM32 UART2 → 结果解析 → STM32 UART3 → SU-03T → 语音播报

1.1 上节回顾

上一节已经完成两部分代码:

  • PE2、PE3 按键分别通过 USART2 发送人脸录入与删除命令;
  • UART2 使用“接收回调释放信号量、线程读取字节”的方式接收数据,但线程中仍是旧示例的“收到字节后加 1 再回发”,尚未解析 K210 返回帧。

本节开头先用 CH340 观察 STM32 是否真的从 PA2 发出了数据。临时监听时的正确接线如下:

STM32F103ZECH340作用
PA2 / USART2_TXRXDCH340 接收并显示 STM32 发出的命令
GNDGND为 UART 电平提供明确的共同参考

视频只接了一根黄色信号线,并认为两块设备通过同一个 USB Hub 已经共地。这可能在当时恰好可用,却不是可靠接法。调试时应始终用导线直接连接 GND;不能把 USB Hub 内部地线、屏蔽层或不同电源适配器当成确定的信号地。

PA2 可以同时接 K210 的 RX 与 CH340 的 RX 来被动观察发送数据,因为两者都是接收端;但不能让 CH340 TX 和 K210 TX 同时驱动 PA2。任何正式接线都应先核对逻辑电平,UART 信号脚不是 5 V 电源脚。

1.2 现象分析

复位后,UART1 控制台出现 find uart2 failed!,按录入或删除按键时 CH340 也收不到预期数据。这说明首要问题不在 K210 的业务协议,而在 STM32 运行时根本没有找到名为 uart2 的 RT-Thread 设备。

原稿中的“报道点(节点)h”是语音识别错误,课程实际打开的是 drivers/board.h。应用层调用 rt_device_find("uart2") 之前,BSP 必须先根据该文件中的宏启用 USART2 并注册设备:

  • BSP_USING_UART2:启用 USART2;
  • BSP_UART2_TX_PIN "PA2":指定 TX 为 PA2;
  • BSP_UART2_RX_PIN "PA3":指定 RX 为 PA3。

“代码已经编译通过”不代表运行时一定能找到设备。应用文件可以正常引用通用设备 API,但如果 BSP 没有注册 uart2,rt_device_find() 仍会返回空指针。

1.3 问题解决

补齐 board.h 的 UART2 配置并重新编译、下载后,控制台打印 uart2 init successful!,按键也能从 PA2 发出以下 4 字节命令:

操作STM32 → K210 的 HEX 帧按键日志
录入人脸A1 02 00 1Aadd_face cmd send successful!
删除人脸A1 01 02 1Adel_face cmd send successful!

串口调试助手必须使用 HEX 显示,不能把字节误读成字符串 a10102。这里的 0x00 是帧中的真实数据字节,不是要发给串口的 C 字符串终止符。

还要区分三种“成功”:

  1. uart2 init successful! 只说明代码找到了设备并执行到初始化函数末尾;
  2. add_face cmd send successful! 等日志在课程代码中打印于实际发送之前,而且没有检查返回值,只说明按键回调被执行;
  3. 只有实际抓到 4 个发送字节,并收到 K210 对应的返回帧,才算端到端通信成立。

1.4 串口接收函数

本节删除上一节的 ch = ch + 1 和单字节回发,改为“逐字节缓存、满 4 字节后解析”。RT-Thread 的接收回调只负责通知;接收线程被信号量唤醒后才调用 rt_device_read() 取走数据并运行解析逻辑,这样不会把完整业务处理塞进中断回调。

1.4.1 结构体定义

UART_RX_typedef 把缓冲区和接收计数组合到同一个对象中:

  • rx_buf[20]:保存收到的原始字节;虽然数组预留 20 字节,本节协议固定只解析前 4 字节;
  • rx_cnt:既表示当前已经缓存多少字节,也表示下一个字节应写入哪个下标;
  • uart2_rx_info:UART2 对应的一份实际接收状态。

初始化 UART2 时必须先执行 uart2_rx_info.rx_cnt = 0,否则计数器若不是确定的零值,第一次写入就可能落在错误位置。这里的全局对象按 C 规则本来也会被清零,但显式初始化能表达意图,也便于以后改变对象的存储位置。

课程仍把 uart2_rx_info、serial2、serial3 和信号量的实体定义放在头文件中。多个 .c 文件包含该头时可能产生重复定义;更规范的工程通常在头文件写 extern 声明,在 uart_cfg.c 中保留唯一实体。本节未做这项重构,文首按课程源码保留。

1.4.2 接收数据处理

每次循环只读取 1 字节,处理顺序如下:

  1. rt_device_read() 读到 1 字节时,把它放入 rx_buf[rx_cnt];
  2. 如果 rx_buf[0] 是帧头 0xAF,就将 rx_cnt 加 1;
  3. 如果候选帧不是以 0xAF 开头,就将 rx_cnt 清零,丢弃当前字节;
  4. rx_cnt > 3 表示已经累计 4 字节,进入协议判断;
  5. 判断结束后无条件清零,开始接收下一组 4 字节。

代码把 char ch 赋给 rt_uint8_t 数组元素,最终保存的是 0~255 的原始字节值,因此可以稳定比较 0xAF、0xFA。协议处理时应看十六进制字节,不应把它们当成可打印字符。

1.4.3 接收数据判断

K210 在本项目中返回 3 类固定长度帧:

K210 → STM32 返回帧含义必须检查的字段
AF 05 00 FA人脸录入成功命令 05、固定值 00、帧尾 FA
AF 02 编号 FA人脸识别成功命令 02、帧尾 FA;第 3 字节是动态人脸编号
AF 01 00 FA人脸删除成功命令 01、固定值 00、帧尾 FA

第一个字节已在缓存阶段检查,所以各分支从 rx_buf[1] 开始判断。识别成功帧不能强制要求 rx_buf[2] == 0,因为该字节携带识别到的人脸编号;录入和删除成功帧的第三字节才是固定的 0x00。

这是一份适合课堂验证的最小解析器,但还不是健壮的流式协议解析器:

  • 代码只持续检查 rx_buf[0],不会验证中途是否意外出现新帧头;
  • 从 0xAF 开始收满 4 字节后,无论内容是否有效都会清零;
  • 没有字节间超时、校验和、长度字段或滑动重同步;
  • 丢字节、插入杂波或帧错位后,当前候选帧可能作废,要等下一次从 0xAF 重新开始;
  • 三个判断使用独立的 if,但三类命令码互斥,因此一帧正常数据只会命中一个分支。

在当前流程中,计数到 4 就立即归零,所以 20 字节数组不会被正常接收路径写越界;但这不能代替协议边界检查。以后若修改帧长或去掉归零语句,必须同时增加 rx_cnt < sizeof(rx_buf) 的保护。

1.4.4 应用案例

解析成功后,STM32 不直接播放声音,而是通过 UART3 给 SU-03T 发送一条 5 字节二进制指令。也就是说,K210 返回协议和语音模块输入协议是两套不同的帧格式,STM32 在中间完成“事件到语音编号”的映射。

1.4.4.1 例题: 人脸录入成功指令处理

人脸录入成功的完整链路是:

K210: AF 05 00 FA → STM32 判断录入成功 → UART3: AA 55 01 55 AA → SU-03T 播报“人脸录入成功”

原稿将 AF 02 写成人脸录入成功是不正确的:0x05 才是录入成功事件,0x02 是识别成功事件。舵机开门也没有在本节实现,不能把下一节的门禁逻辑提前并入本例。

1.4.4.2 例题: 人脸识别成功/删除成功指令处理

三类事件和语音命令的映射如下:

K210 返回STM32 解析结果STM32 → SU-03T播报内容
AF 05 00 FA录入成功AA 55 01 55 AA人脸录入成功
AF 02 编号 FA识别成功AA 55 02 55 AA人脸识别成功
AF 01 00 FA删除成功AA 55 03 55 AA人脸删除成功

计数器不是“识别成功后才归零”,而是在收到任意一组 4 字节候选帧并完成三项判断后统一归零。即使 4 字节没有匹配任何指令,也会被丢弃并重新找帧头。

1.4.5 串口三配置

本节在 drivers/board.h 中把 USART3 配置为 PB10/PB11:

STM32F103ZEUSART3 功能SU-03T本节是否使用
PB10TXB6 / UART1_RXD使用,STM32 向语音模块发命令
PB11RXB7 / UART1_TXD未使用,课程业务不接收语音模块数据
GND公共参考地GND必须连接

发送方 TX 必须接收方 RX,因此 PB10 应接 SU-03T 的 B6,而不是 B7。SU-03T 可使用独立的稳定 5 V 电源,但它与 STM32 必须直接共地;模块 VCC 供电电压和 UART 信号逻辑电平是两件事,不能把 5 V 电源接到 B6/B7。

原稿称“波特率由 B10、B11 决定”是错误的。PB10/PB11 只决定信号连接到哪些物理引脚,波特率由两端串口配置决定。本工程没有调用 rt_device_control() 改写 UART3 参数,因而沿用 RT-Thread 工程中的默认配置 115200、8N1、无流控;这与课程烧入 SU-03T 的固件配置必须一致。

UART3 只负责发送,所以课程删除了接收信号量、接收回调和接收线程。归档仍用 RT_DEVICE_FLAG_INT_RX 打开 serial3,这是从 UART2 初始化复制后留下的写法;本节应认识到“以中断接收标志打开”和“业务实际上只发送”不是一回事,但文首不能擅自改写课程源码。

1.4.6 串口三初始化

main() 必须先调用 uart3_init(),再调用 uart2_init()。原因是 UART2 初始化会创建并启动 serial2 接收线程;如果 K210 很快返回数据,该线程会立刻调用 rt_device_write(serial3, ...)。先初始化 UART3,才能保证此时 serial3 已保存有效设备句柄。

UART3 初始化步骤为:

  1. 用 rt_device_find("uart3") 查找设备;
  2. 找不到时打印 find uart3 failed! 并返回错误;
  3. 打开设备;
  4. 在 UART1 默认控制台打印 uart3 init successful!。

uart3 init successful! 同样不是外部通信成功的证明。代码没有检查 rt_device_open() 和后续 rt_device_write() 的返回值;最终仍需靠语音模块是否收到正确的 5 字节帧并播报来验证链路。

原稿列出的“温度异常”对应 AA 55 04 55 AA,该帧已配置在语音模块固件中,但本节没有在 STM32 业务源码里发送它,因此不应放入本节文首代码。

1.4.7 发送函数定义

课程没有再封装一个独立的“语音发送函数”,而是在三个解析分支里分别定义局部只读数组,再直接调用:

rt_device_write(serial3, 0, voice_xxx, 5)

参数含义如下:

参数含义
serial3已查找并打开的 UART3 设备句柄
0串口字符设备不使用的偏移位置参数
voice_xxx指向 5 字节二进制语音帧的首地址
5请求发送 5 字节;课程协议不是 6 字节

语音帧固定为 AA 55 功能码 55 AA。const 只防止函数内通过该数组名修改内容,不负责校验帧格式,也不代表 5 字节一定全部发出。严谨代码应检查 rt_device_write() 的返回值是否为 5,但本节视频没有加入这项处理,所以文首保持原样。

复制三个分支时,容易遗留错误变量名。课程调试中逐一改成 voice_add_face_succ、voice_check_face_succ、voice_del_face_succ,并补齐缺失的分号。编译器报错位置有时在真正漏写符号的下一行,排查时应连同前一条语句一起检查。

1.4.8 测试日志

本节整机测试流程为:

  1. 确认 K210、STM32、SU-03T 均正确供电且直接共地;
  2. 核对 K210 与 USART2 的 TX/RX 交叉接线,以及 PB10 到 SU-03T B6 的单向发送接线;
  3. 重新编译、下载 STM32 程序,打开 UART1 日志;
  4. 复位后确认先看到 UART3 初始化信息,再看到 UART2 初始化信息;
  5. 按录入按键并将人脸对准摄像头;
  6. 核对是否先收到录入成功事件并播放对应语音。

测试中,“人脸录入成功”之后很快又播放“人脸识别成功”,并不是串口把两条指令覆盖或顺序打乱。K210 完成录入后将人脸加入数据库,随后的摄像头帧仍在持续识别同一张脸,于是立即再次返回识别成功帧;当前 STM32 代码收到一次就播放一次,所以两条语音会紧接着出现,持续识别时还可能重复播报。

课程在本节末尾只提出下一步要增加判断或延时,尚未实现。用于抑制重复识别的 5 秒时间判断、识别成功标志以及开门控制,都属于下一节内容。

若完全无语音,可按链路从前到后排查:

现象优先检查
find uart2 failed! / find uart3 failed!board.h 使能宏、引脚宏和工程是否重新编译下载
按键日志存在,但 K210 无动作PA2→K210 RX、波特率、共地及实际发送的 4 字节帧
K210 已返回,但 STM32 不处理K210 TX→PA3、返回帧是否以 AF 开头并恰好为 4 字节
STM32 已命中分支,但 SU-03T 不播报PB10→B6、共同 GND、SU-03T 供电、115200 8N1 和 5 字节 HEX 帧
录入后立即重复播识别成功当前程序缺少事件抑制,属于本节已确认、下节处理的问题

参考资料