按键、串口2代码编写
278.4按键、串口2代码编写
/* key_cfg.h:本节创建的按键配置头文件。 */
#ifndef APPLICATIONS_KEY_CFG_H_ // 若尚未包含本头文件,则保留下方声明,防止重复包含。
#define APPLICATIONS_KEY_CFG_H_ // 定义头文件保护宏,和上面的条件判断配套使用。
#include <rtthread.h> // 引入 RT-Thread 内核类型、日志和 IPC 等接口。
#include <rtdevice.h> // 引入 PIN、串口等 RT-Thread 设备接口。
#ifndef BEEP_PIN_NUM // 仅在工程未从其他位置定义蜂鸣器引脚时使用默认值。
#define BEEP_PIN_NUM 24 // 把蜂鸣器控制端设为 RT-Thread PIN 24,即 PB8。
#endif // 结束蜂鸣器引脚的条件定义。
#ifndef KEY0_PIN_NUM // 仅在工程未从其他位置定义第一个按键时使用默认值。
#define KEY0_PIN_NUM 66 // 把第一个按键设为 RT-Thread PIN 66,即 PE2。
#endif // 结束第一个按键引脚的条件定义。
#ifndef KEY1_PIN_NUM // 仅在工程未从其他位置定义第二个按键时使用默认值。
#define KEY1_PIN_NUM 67 // 把第二个按键设为 RT-Thread PIN 67,即 PE3。
#endif // 结束第二个按键引脚的条件定义。
void io_init(void); // 声明按键和蜂鸣器初始化函数,供 main.c 调用。
void add_face(void *args); // 声明“录入人脸”按键的中断回调函数。
void del_face(void *args); // 声明“删除人脸”按键的中断回调函数。
#endif // 结束 APPLICATIONS_KEY_CFG_H_ 头文件保护范围。
/* key_cfg.c:本节创建的按键业务源文件。 */
#include "key_cfg.h" // 引入本模块的引脚宏和函数声明。
#include "uart_cfg.h" // 引入 UART2 设备句柄 serial2,供按键回调发送命令。
void add_face(void *args) // 定义第一个按键的回调;本课程没有使用 args 参数。
{ // add_face() 函数体开始。
rt_kprintf("add_face cmd send successful!\n"); // 向默认控制台打印“录入命令已发送”的调试信息。
const rt_uint8_t add_face_cmd[4] = {0xA1, 0x02, 0x00, 0x1A}; // 定义不可在本函数中改写的 4 字节人脸录入命令。
rt_device_write(serial2, 0, add_face_cmd, 4); // 通过 UART2 发送数组中的 4 个原始字节给 K210。
} // add_face() 函数体结束。
void del_face(void *args) // 定义第二个按键的回调;本课程没有使用 args 参数。
{ // del_face() 函数体开始。
rt_kprintf("del_face cmd send successful!\n"); // 向默认控制台打印“删除命令已发送”的调试信息。
const rt_uint8_t add_del_cmd[4] = {0xA1, 0x01, 0x02, 0x1A}; // 定义不可在本函数中改写的 4 字节人脸删除命令。
rt_device_write(serial2, 0, add_del_cmd, 4); // 通过 UART2 发送数组中的 4 个原始字节给 K210。
} // del_face() 函数体结束。
void io_init(void) // 定义按键和蜂鸣器的初始化函数,改为由 main() 直接调用。
{ // io_init() 函数体开始。
rt_pin_mode(BEEP_PIN_NUM, PIN_MODE_OUTPUT); // 把 PB8 配置为输出,用于后续项目中的蜂鸣器控制。
rt_pin_write(BEEP_PIN_NUM, PIN_LOW); // 先输出低电平,使蜂鸣器保持关闭。
rt_pin_mode(KEY0_PIN_NUM, PIN_MODE_INPUT_PULLUP); // 把 PE2 配为内部上拉输入,松开按键时保持高电平。
rt_pin_attach_irq(KEY0_PIN_NUM, PIN_IRQ_MODE_FALLING, add_face, RT_NULL); // 给 PE2 绑定下降沿中断,按下时调用 add_face()。
rt_pin_irq_enable(KEY0_PIN_NUM, PIN_IRQ_ENABLE); // 使能 PE2 的 PIN 中断。
rt_pin_mode(KEY1_PIN_NUM, PIN_MODE_INPUT_PULLUP); // 把 PE3 配为内部上拉输入,松开按键时保持高电平。
rt_pin_attach_irq(KEY1_PIN_NUM, PIN_IRQ_MODE_FALLING, del_face, RT_NULL); // 给 PE3 绑定下降沿中断,按下时调用 del_face()。
rt_pin_irq_enable(KEY1_PIN_NUM, PIN_IRQ_ENABLE); // 使能 PE3 的 PIN 中断。
} // io_init() 函数体结束。
/* uart_cfg.h:本节创建的 UART2 配置头文件。 */
#ifndef APPLICATIONS_UART_CFG_H_ // 若尚未包含本头文件,则保留下方声明。
#define APPLICATIONS_UART_CFG_H_ // 定义 UART 配置头文件的保护宏。
#include <rtthread.h> // 引入 RT-Thread 类型、线程、信号量和设备对象声明。
#define SAMPLE_UART2_NAME "uart2" // 定义要在设备管理器中查找的 UART2 设备名。
struct rt_semaphore uart2_rx_sem; // 定义用于通知接收线程的 UART2 信号量对象。
rt_device_t serial2; // 定义 UART2 设备句柄,保存 rt_device_find() 的返回值。
int uart2_init(void); // 声明 UART2 初始化函数,供 main.c 调用。
#endif // 结束 APPLICATIONS_UART_CFG_H_ 头文件保护范围。
/* uart_cfg.c:本节完成的 UART2 初始化与接收骨架。 */
#include "uart_cfg.h" // 引入 UART2 设备名、句柄、信号量和函数声明。
rt_err_t uart2_input(rt_device_t dev, rt_size_t size) // 定义 UART2 接收通知回调;本节未使用 dev 和 size 的值。
{ // uart2_input() 函数体开始。
rt_sem_release(&uart2_rx_sem); // 串口收到数据后释放信号量,唤醒等待中的接收线程。
return RT_EOK; // 返回 RT_EOK,表示回调处理完成。
} // uart2_input() 函数体结束。
void serial2_thread_entry(void *parameter) // 定义 UART2 接收线程入口;本节未使用 parameter 参数。
{ // serial2_thread_entry() 函数体开始。
char ch; // 定义变量,每次保存从串口读出的 1 个字节。
while (1) // 永久循环,使线程持续处理 UART2 接收数据。
{ // 外层循环开始。
while (rt_device_read(serial2, -1, &ch, 1) != 1) // 尝试读取 1 字节;未读到时进入内层等待循环。
{ // 内层等待循环开始。
rt_sem_take(&uart2_rx_sem, RT_WAITING_FOREVER); // 阻塞等待接收回调释放信号量,然后再次尝试读取。
} // 内层等待循环结束。
ch = ch + 1; // 沿用旧示例:把收到的字节值加 1,本节尚未改为 K210 帧解析。
rt_device_write(serial2, 0, &ch, 1); // 沿用旧示例:把加 1 后的字节从 UART2 发回去。
} // 外层循环结束。
} // serial2_thread_entry() 函数体结束。
int uart2_init(void) // 定义 UART2 初始化函数,不再接收 MSH 命令行参数。
{ // uart2_init() 函数体开始。
rt_err_t ret = RT_EOK; // 先把函数结果设为成功,线程创建失败时再改为错误。
char str[] = "uart2 init successful!\r\n"; // 定义 UART2 初始化完成后输出到默认控制台的提示字符串。
serial2 = rt_device_find(SAMPLE_UART2_NAME); // 按名称查找 BSP 已注册的 uart2 设备。
if (!serial2) // 判断是否没有找到 uart2 设备。
{ // 设备查找失败分支开始。
rt_kprintf("find %s failed!\n", SAMPLE_UART2_NAME); // 在默认控制台打印查找失败的设备名。
return RT_ERROR; // 返回错误,后续初始化不再执行。
} // 设备查找失败分支结束。
rt_sem_init(&uart2_rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO); // 初始化静态信号量对象,初始计数为 0,按 FIFO 顺序唤醒等待线程。
rt_device_open(serial2, RT_DEVICE_FLAG_INT_RX); // 以中断接收模式打开 UART2;发送仍使用普通设备写接口。
rt_device_set_rx_indicate(serial2, uart2_input); // 注册 UART2 接收通知回调函数。
// rt_device_write(serial2, 0, str, (sizeof(str) - 1)); // 课程已注释旧示例的字符串发送,避免把日志文本发给 K210。
rt_thread_t uart2_thread = rt_thread_create("serial2", serial2_thread_entry, RT_NULL, 1024, 25, 10); // 创建接收线程:栈 1024、优先级 25、时间片 10。
if (uart2_thread != RT_NULL) // 判断接收线程是否创建成功。
{ // 线程创建成功分支开始。
rt_thread_startup(uart2_thread); // 启动 UART2 接收线程,使其进入调度。
} // 线程创建成功分支结束。
else // 接收线程创建失败时执行此分支。
{ // 线程创建失败分支开始。
ret = RT_ERROR; // 把初始化结果改为错误。
} // 线程创建失败分支结束。
rt_kprintf(" %s\n", str); // 通过默认控制台打印 UART2 初始化提示,而不是发给 K210。
return ret; // 返回 UART2 初始化和线程创建结果。
} // uart2_init() 函数体结束。
/* main.c:本节在主文件中新增的声明和初始化调用。 */
#include "key_cfg.h" // 让 main.c 能看到 io_init() 的声明。
#include "uart_cfg.h" // 让 main.c 能看到 uart2_init() 的声明。
io_init(); // 初始化蜂鸣器、两个按键及其下降沿中断。
uart2_init(); // 查找并打开 UART2,再创建串口接收线程。
上方按视频进度整理本节实际写到的业务代码:按键模块已经完成,UART2 仍保留第 13 个工程中“收到 1 字节、数值加 1、再发回”的测试处理。本节结尾只提出下一步要解析 K210 的 3 类返回帧,尚未编写解析逻辑,因此没有提前抄入课程归档最终版中的帧解析、UART3 语音控制和门禁代码。代码有效语句、函数名和数值保持课程写法,只移除旧注释并补充逐行说明。
一、代码编写
项目最终要组合按键、K210 串口通信、语音模块串口、舵机 PWM、红外测温、OLED 显示和蜂鸣器。本节只完成两条基础链路:
- 按键下降沿中断 → STM32 通过 USART2 向 K210 发送录入或删除命令;
- USART2 接收回调 → 释放信号量 → 接收线程读取字节,为下一节解析 K210 返回帧搭好框架。
供电、共地与串口接线
课程中的 Maix Bit 可以由自己的 USB Type-C 口单独供电,也可以在确认电源能力和接线无误后由系统 5 V 电源供电。Sipeed 给出的 Maix Bit 规格为 5 V 供电、供电能力应大于 300 mA;摄像头、LCD 和 KPU 同时运行时,使用独立且稳定的 USB 电源更容易避免主控板 5 V 轨压降。
“K210 和 STM32 必须供电”不是这段口述的关键,正确要求是:两块板可以分别供电,但 GND 必须直接相连。UART 接收端判断的是信号线相对本机地的电压;没有共同参考地时,即使 TX/RX 连接正确,电平也可能漂移而产生乱码或完全无法通信。
| K210 Maix Bit | STM32F103ZE | 方向 | 作用 |
|---|---|---|---|
| IO9 / UART2_TX | PA3 / USART2_RX | K210 → STM32 | 返回录入、识别和删除结果 |
| IO10 / UART2_RX | PA2 / USART2_TX | STM32 → K210 | 发送录入、删除命令 |
| GND | GND | 公共参考 | 两块板即使分别供电也必须连接 |
TX 要接对方 RX,RX 要接对方 TX。Maix Bit 规格书标明 IO9/IO10 属于 3.3 V IO,STM32F103 的这组串口也按 3.3 V 逻辑工作;5 V 只能接电源输入,不能直接送入 UART 信号脚。
1.1 问题与编译
视频新建的是第 15 个项目工程 15PRJ。开始增加业务模块前先完整编译一次,其意义是建立基线:若空工程已经报错,应先解决工程生成、芯片型号、BSP 配置或文件大小写问题;否则后续无法判断错误来自新代码还是工程本身。
课程在 drivers/board.h 中已经启用:
BSP_USING_UART1:PA9/PA10,作为 RT-Thread 默认控制台,rt_kprintf()日志从这里输出;BSP_USING_UART2:PA2/PA3,用于和 K210 通信;BSP_USING_UART3:PB10/PB11,后续用于语音模块。
本节调用 rt_device_find("uart2") 的前提,就是 UART2 已被 BSP 驱动注册为名为 uart2 的设备。若没有打开 BSP_USING_UART2,应用文件即使编译成功,运行时也会查找失败。
1.1.1 按键
1.1.1.1 新建头文件
课程实际创建的文件是 key_cfg.h,不是原稿识别出的“k config.h”。它承担三类内容:
- 引入 RT-Thread 内核与设备接口;
- 定义蜂鸣器、按键对应的 RT-Thread PIN 编号;
- 声明需要从其他
.c文件调用的io_init(),以及课程放入头文件的两个按键回调。
PIN 编号不是 STM32 封装脚号。此 BSP 每个 GPIO 端口占 16 个连续编号,编号计算为“端口序号 × 16 + 位号”:
| 数字 | 正确 GPIO | 计算 | 用途 |
|---|---|---|---|
| 24 | PB8 | 1 × 16 + 8 | 蜂鸣器 |
| 66 | PE2 | 4 × 16 + 2 | 录入按键 |
| 67 | PE3 | 4 × 16 + 3 | 删除按键 |
课程归档旧注释把 66、67 写成 PD8、PD9,这是错误的;按代码和 BSP 映射,它们是 PE2、PE3。接线或看原理图时应以 GPIO 名和实际板卡网络为准,不能照抄错误注释。
1.1.1.2 新建源文件
对应源文件为 key_cfg.c,它包含 key_cfg.h 和 uart_cfg.h:前者提供按键配置,后者提供 UART2 句柄 serial2,使按键回调能调用 rt_device_write()。
文件分工应理解为:
.h提供跨文件需要看到的宏、类型和声明;.c保存函数实现和本模块内部状态;- 使用某个函数或对象的
.c文件包含相应头文件,而不是包含另一个.c文件。
原稿所说“需包含 uart_config.c”不正确。C 工程应让每个 .c 文件分别参与编译,通过头文件共享声明;直接 #include 源文件容易造成重复定义。
1.1.1.3 复制之前的按键代码
本节复用第 13 个工程的 PIN 示例,并做了以下业务改造:
- 把原来的
beep_on()、beep_off()改为add_face()、del_face(); - 保留 PB8 蜂鸣器初始化,供后续高温报警使用,但按键回调不再直接控制蜂鸣器;
- PE2 按键发送
A1 02 00 1A,通知 K210 开始录入人脸; - PE3 按键发送
A1 01 02 1A,通知 K210 删除已录入人脸; - 删除 MSH 命令导出,改由
main()直接调用io_init()。
命令数组使用 rt_uint8_t,保证每个元素是 1 字节;0x 表示十六进制。const 表示本次回调执行期间不能通过该数组名改写内容,若代码尝试赋值,编译器应报错。它不能校验通信协议,也不能保证 K210 已收到数据。
两条日志中的 send successful 实际打印在 rt_device_write() 之前,而且代码没有检查写入函数的返回值,所以它只表示“程序走到了发送位置”,不表示驱动一定写出了 4 字节,更不表示 K210 已正确接收和执行。严谨验证至少要检查 rt_device_write() 返回的字节数,并等待 K210 返回对应结果帧。
两个按键使用内部上拉和下降沿中断:松开时 PE2/PE3 为高电平,按下后被拉低,高→低的跳变触发回调。课程代码没有消抖,机械触点可能在一次按下中产生多个下降沿,从而连续发送多帧命令。
还要注意,中断回调通常运行在中断上下文。课程为了快速搭建功能,直接在回调中调用 rt_kprintf() 和 rt_device_write();这两个操作可能耗时,设备实现不同还可能涉及锁或等待。工程化做法应让回调只记录事件或释放信号量,由线程完成日志和串口发送。本节按要求保留课程源码,不在文首擅自改写。
课程在 main() 中先调用 io_init()、后调用 uart2_init()。前者已经使能按键中断,后者才给 serial2 赋予有效设备句柄;如果恰好在两次调用之间触发按键,回调可能拿尚未初始化的句柄调用 rt_device_write()。稳健设计应先准备好 UART2,再开放会使用它的按键事件,或在发送前验证句柄;本节仍保留视频中的原调用顺序。
1.1.2 串口二
本项目 USART2 的角色是 K210 数据链路:STM32 从 PA2 发送命令,从 PA3 接收 K210 返回帧。rt_kprintf() 仍走默认控制台 UART1,不能把“日志串口”和“业务串口”混在一起,否则 ASCII 日志会污染 K210 的二进制协议流。
1.1.2.1 新建文件夹和头文件
课程实际创建 uart_cfg.h 和 uart_cfg.c。讲师不建议命名为 uart_config.h,原因不是所有带 config 的名称都会报错,而是 BSP 已经存在一个同名 drivers/include/config/uart_config.h。若两个同名头文件同时位于编译器包含路径中,#include "uart_config.h" 可能命中错误文件;使用独特的 uart_cfg.h 可以避免歧义。
课程把 uart2_rx_sem 和 serial2 的定义直接写在头文件中。这样只要多个 .c 文件包含该头,就可能产生多个同名全局定义;旧工具链可能以 common 符号形式合并,较新的 -fno-common 配置则可能在链接时报“multiple definition”。更规范的组织方式是头文件只写 extern 声明,并在 uart_cfg.c 中保留唯一实体定义。但这一改法没有在本节视频中实施,所以文首仍原样保留课程写法。
1.1.2.2 复制之前的串口二代码
本节从第 13 个工程的 uart_test.c 复制串口设备示例,再做这些修改:
- 固定设备名为
uart2,删除 MSH 命令行参数和设备名复制逻辑; - 把设备句柄、信号量、回调、线程入口和线程对象统一加上
2,避免后续加入 UART3 时混淆; - 初始化函数改名为
uart2_init(void),由main()直接调用; - 注释掉原来通过 UART2 发送
hello RT-Thread!的代码,避免文本日志进入 K210 协议链路; - 改用
rt_kprintf()在默认 UART1 控制台打印uart2 init successful!。
课程口述“把所有 static 都删除”过于宽泛。static 修饰文件作用域函数或变量时,表示符号仅在当前 .c 文件可见:
io_init()和uart2_init()要被main.c调用,因此确实不能继续保持文件内static,还必须在头文件声明;uart2_input()和serial2_thread_entry()只在uart_cfg.c内部使用,保留static反而更能避免命名冲突;- 是否出现“未使用”警告取决于符号有没有在当前编译单元中被引用,并不是只要写了
static就会报警。
本节为保持课堂结果而沿用非 static 写法,但不能把“跨文件调用”和“全部删除 static”画等号。
1.1.2.3 成功查询
uart2_init() 的关键步骤如下:
rt_device_find("uart2")在 RT-Thread 设备对象中查找 UART2;rt_sem_init()创建初始计数为 0 的接收信号量;rt_device_open(..., RT_DEVICE_FLAG_INT_RX)以中断接收模式打开设备;rt_device_set_rx_indicate()登记接收通知回调;rt_thread_create()创建 UART2 接收线程,成功后调用rt_thread_startup();rt_kprintf()在控制台输出初始化提示。
打印 uart2 init successful! 最多说明设备已找到且程序执行到函数末尾。课程代码没有检查 rt_sem_init()、rt_device_open()、rt_device_set_rx_indicate() 和 rt_thread_startup() 的返回值,因此这条日志不能证明所有步骤都成功,更不能证明外部 TX/RX 接线和 K210 通信正常。
本工程没有调用 rt_device_control() 显式设置 UART2 参数,而是沿用 STM32 BSP 的 RT_SERIAL_CONFIG_DEFAULT。课程 K210 脚本使用 115200、8N1,所以必须确认当前 BSP 默认值与之相同;若以后修改全局默认串口配置,UART2 两端也必须同步。
1.1.2.4 传输参数
rt_device_write(serial2, 0, cmd, 4) 的参数含义为:
| 参数 | 本节传值 | 含义 |
|---|---|---|
dev | serial2 | 已查找到并打开的 UART2 设备句柄 |
pos | 0 | 设备位置参数;普通串口设备通常不使用文件偏移 |
buffer | 命令数组 | 指向待发送的原始字节 |
size | 4 | 请求发送 4 字节,不是字符串长度 |
串口发送不会自动添加帧头、帧尾或结束符。数组中的 A1 ... 1A 就是线上全部 4 字节,不能用 strlen() 处理二进制帧,因为帧内的 0x00 会被 C 字符串函数当成结尾。
接收侧采用“中断通知 + 信号量 + 线程读取”的分工:驱动发现数据到达时调用 uart2_input(),回调只释放信号量;serial2 线程被唤醒后,用 rt_device_read() 每次取 1 字节。这样避免在线程中无休止轮询,也把实际数据处理放在线程上下文中。
本节末尾仍保留旧示例的 ch = ch + 1 和单字节回发,只是定位了下一步应该修改的位置。K210 会返回以下 3 类 4 字节帧:
| K210 返回帧 | 含义 |
|---|---|
AF 05 00 FA | 人脸录入成功 |
AF 02 编号 FA | 人脸识别成功,第三字节为人脸编号 |
AF 01 00 FA | 人脸删除成功 |
如何缓存、找帧头、判断帧尾并解析这三类返回值属于下一节内容。本节不能把最终工程中的解析器提前当作已经写完。