按键、串口2代码编写

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

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 显示和蜂鸣器。本节只完成两条基础链路:

  1. 按键下降沿中断 → STM32 通过 USART2 向 K210 发送录入或删除命令;
  2. 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 BitSTM32F103ZE方向作用
IO9 / UART2_TXPA3 / USART2_RXK210 → STM32返回录入、识别和删除结果
IO10 / UART2_RXPA2 / USART2_TXSTM32 → K210发送录入、删除命令
GNDGND公共参考两块板即使分别供电也必须连接

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计算用途
24PB81 × 16 + 8蜂鸣器
66PE24 × 16 + 2录入按键
67PE34 × 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 示例,并做了以下业务改造:

  1. 把原来的 beep_on()、beep_off() 改为 add_face()、del_face();
  2. 保留 PB8 蜂鸣器初始化,供后续高温报警使用,但按键回调不再直接控制蜂鸣器;
  3. PE2 按键发送 A1 02 00 1A,通知 K210 开始录入人脸;
  4. PE3 按键发送 A1 01 02 1A,通知 K210 删除已录入人脸;
  5. 删除 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() 的关键步骤如下:

  1. rt_device_find("uart2") 在 RT-Thread 设备对象中查找 UART2;
  2. rt_sem_init() 创建初始计数为 0 的接收信号量;
  3. rt_device_open(..., RT_DEVICE_FLAG_INT_RX) 以中断接收模式打开设备;
  4. rt_device_set_rx_indicate() 登记接收通知回调;
  5. rt_thread_create() 创建 UART2 接收线程,成功后调用 rt_thread_startup();
  6. 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) 的参数含义为:

参数本节传值含义
devserial2已查找到并打开的 UART2 设备句柄
pos0设备位置参数;普通串口设备通常不使用文件偏移
buffer命令数组指向待发送的原始字节
size4请求发送 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人脸删除成功

如何缓存、找帧头、判断帧尾并解析这三类返回值属于下一节内容。本节不能把最终工程中的解析器提前当作已经写完。

参考资料