资讯动态

RTThread学习记录14——基于RTThread与K210的人脸识别和红外测温门禁项目的完善与总结

发布时间:2026/10/9 5:37:06 来源:尧图企业网站定制
一、前言国庆前做完了这个项目其实跟着资料的功能比较简单所以我决定自己加一些内容加一些功能之类的吧然后这篇内容主要记录自己做这个项目遇到的问题、疑惑还有这么看懂代码方便自己以后复习这个项目。二、项目介绍项目以STM32ZET6为核心外接的重要模块有语音模块SU-03T和K210模块主要实现的功能设置2个按键按键A负责录入人脸并控制语音模块播报“人脸录入成功”按键B负责删除已录入人脸并控制语音模块播报“人脸删除成功”。当K210已经有录入的人脸时识别到人脸控制语音模块播报“识别人脸成功”并控制舵机模拟开门。同时配备红外温度传感器GY-906-BAA检测温度温度大于设置的阈值时控制语音模块报警。以上是基础的功能我还想额外添加的是加一个人体红外传感器检测人体再控制k210进行识别这样就能防止人脸识别一直进行识别反复播报还可以尝试一下在开门之后人体红外传感器没有检测到人体舵机再自己关门而不是控制舵机以延时的方式关门。三、代码思路stm32zet6作为主控负责集成数据控制外接模块。并使用串口与语音模块和k210模块用数据包的方式进行通信。串口二连接的是k210模块同时需要安装MaixPy-IDE来写K210的代码使用的是yolo2的模型我直接用现成的固件了自己看了一下代码逻辑就行。串口三连接的是语音模块需要使用官方的固件配置教程创建一个自己的固件比如语音模块收到的数据包格式要什么样的使用串口几用到哪些引脚收到指定数据包之后要做什么反应需不需要联动麦克风等等配置。保存通过之后下载SDK包然后使用蜂鸟那个软件使用串口下载到语音模块里。这些官方都有资料智能公元/AI产品零代码平台B站也有。重点是记得配置串口和功能下面是整个项目的数据包指令格式。大体流程就是MCU端按下按键就会触发绑定的回调函数通过串口2给K210发送添加人脸A1 02 00 1A或者删除人脸A1 01 02 1A的操作的指令K210执行完删除、添加、识别人脸的操作后会给MCU发送数据包人脸录入成功AF 05 00 FA、人脸识别成功AF 02 编号 FA、人脸删除成功AF 01 00 FA且人脸识别成功后会控制舵机模拟开门。MCU在收到K210回传的指令之后再通过串口3向语音模块发送对应的指令数据包进行播报人脸录入成功AA 55 01 55 AA、人脸识别成功AA 55 02 55 AA人脸删除成功AA 55 03 55 AA。以及红外温度传感器模块检测到温度过高时MCU会通过串口3向语音模块发送数据包进行播报温度异常AA 55 04 55 AA四、代码详解代码主要记录核心部分比如整个项目的架构怎么实现状态机数据流通的方向各种外设这么交互。对于GY906这个模块我使用的是现成的代码调用函数就能读取温度了IIC则是使用的RTThread下载的u8g2软件包也是调用现成的API函数。1.按键控制代码key_cfg.c#include key_cfg.h #include app_business.h void add_face(void *args) { //按键回调函数不要放日志和阻塞的操作全部移到业务线程app_business.c里这里只做触发 // rt_kprintf(add face successful!\n); // rt_device_write(serial2, 0, add_face_cmd, sizeof(add_face_cmd)); //给k210发送指令添加人脸 app_post_event(EV_BTTON_ADD_FACE); } void delete_face(void *args) { app_post_event(EV_BTTON_DELETE_FACE); } //初始化2个按键引脚并绑定回调中断函数 void IO_init(void) { /* 蜂鸣器引脚为输出模式 */ rt_pin_mode(BEEP_PIN_NUM, PIN_MODE_OUTPUT); /* 默认低电平 */ rt_pin_write(BEEP_PIN_NUM, PIN_LOW); /* 按键0引脚为输入模式 */ rt_pin_mode(KEY0_PIN_NUM, PIN_MODE_INPUT_PULLUP); /* 绑定中断下降沿模式回调函数名为add_face */ rt_pin_attach_irq(KEY0_PIN_NUM, PIN_IRQ_MODE_FALLING, add_face, RT_NULL); /* 使能中断 */ rt_pin_irq_enable(KEY0_PIN_NUM, PIN_IRQ_ENABLE); /* 按键1引脚为输入模式 */ rt_pin_mode(KEY1_PIN_NUM, PIN_MODE_INPUT_PULLUP); /* 绑定中断下降沿模式回调函数名为delete_face */ rt_pin_attach_irq(KEY1_PIN_NUM, PIN_IRQ_MODE_FALLING, delete_face, RT_NULL); /* 使能中断 */ rt_pin_irq_enable(KEY1_PIN_NUM, PIN_IRQ_ENABLE); }key_cfg.h#ifndef APPLICATIONS_KEY_CFG_H_ #define APPLICATIONS_KEY_CFG_H_ #include rtthread.h #include rtdevice.h /* 引脚编号通过查看设备驱动文件drv_gpio.c确定 */ #ifndef BEEP_PIN_NUM #define BEEP_PIN_NUM 24 #endif #ifndef KEY0_PIN_NUM #define KEY0_PIN_NUM 66 #endif #ifndef KEY1_PIN_NUM #define KEY1_PIN_NUM 67 #endif void IO_init(void); void add_face(void *args); void delete_face(void *args); #endif2.串口处理数据的相关代码uart.cfg.c#include uart_cfg.h #include pwm_cfg.h #include app_business.h UART_RX_typedef uart2_rx_info; struct rt_semaphore uart2_rx_sem; rt_device_t serial2; rt_device_t serial3; /* 串口2接收数据回调函数 */ rt_err_t uart2_input(rt_device_t dev, rt_size_t size) { /* 串口接收到数据后产生中断调用此回调函数然后发送接收信号量 */ rt_sem_release(uart2_rx_sem); return RT_EOK; } //核心线程收到k210发送过来的指令控制语音模块进行对应的操作 void serial2_thread_entry(void *parameter) { char ch; static rt_tick_t K210_last_tick0; while (1) { /* 从串口读取一个字节的数据没有读取到则等待接收信号量 */ while (rt_device_read(serial2, -1, ch, 1) ! 1) { /* 阻塞等待接收信号量等到信号量后再次读取数据 */ rt_sem_take(uart2_rx_sem, RT_WAITING_FOREVER); } uart2_rx_info.rx_buf[uart2_rx_info.rx_cnt]ch; //检测第一个数据帧头是否正确不正确的话重新接收数据 if(uart2_rx_info.rx_buf[0]0xAF) { uart2_rx_info.rx_cnt; } else { uart2_rx_info.rx_cnt0; } //接收到一个完整的数据包进入数据包解析 if(uart2_rx_info.rx_cnt3) { uart2_rx_info.rx_cnt0; //收到K210添加人脸的指令 if(uart2_rx_info.rx_buf[1]0x05 uart2_rx_info.rx_buf[2]0x00 uart2_rx_info.rx_buf[3]0xFA) { const rt_uint8_t voice_add_face_succ[5]{0xAA,0x55,0x01,0x55,0xAA}; rt_device_write(serial3, 0, voice_add_face_succ, 5); //给语音模块发送人脸添加成功的指令 K210_last_tickrt_tick_get(); //获取添加人脸时的tick值 } //收到K210人脸识别成功的指令 if(uart2_rx_info.rx_buf[1]0x02 uart2_rx_info.rx_buf[3]0xFA) { if(rt_tick_get()-K210_last_tickface_reg_tick) //如果添加人脸的时间已经过去了5s防止刚添加人脸立即识别导致语音被打断 { const rt_uint8_t voice_check_face_succ[5]{0xAA,0x55,0x2,0x55,0xAA}; K210_last_tickrt_tick_get(); //记得重新刷新K210_last_tick rt_device_write(serial3, 0, voice_check_face_succ, 5); //给语音模块发送人脸识别成功的指令 app_post_event(EV_K210_MATCHED); } } //收到K210删除人脸的指令 if(uart2_rx_info.rx_buf[1]0x01 uart2_rx_info.rx_buf[2]0x00 uart2_rx_info.rx_buf[3]0xFA) { const rt_uint8_t voice_delete_face_succ[5]{0xAA,0x55,0x3,0x55,0xAA}; rt_device_write(serial3, 0, voice_delete_face_succ, sizeof(voice_delete_face_succ)); //给语音模块发送人脸删除成功的指令 } //可以在这里加上收到K210提升温度过高的指令 } } } //串口2、信号量、线程的初始化串口2绑定回调函数 int uart2_init(void) { rt_err_t ret RT_EOK; char str[] uart2 init successful!\r\n; uart2_rx_info.rx_cnt0; /* 查找系统中的串口设备 */ serial2 rt_device_find(SAMPLE_UART2_NAME); if (!serial2) { 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); /* 以中断接收及轮询发送模式打开串口设备 */ rt_device_open(serial2, RT_DEVICE_FLAG_INT_RX); /* 设置接收回调函数 */ rt_device_set_rx_indicate(serial2, uart2_input); /* 发送字符串 */ rt_kprintf(%s\n, str); /* 创建 serial 线程 */ rt_thread_t uart2_thread rt_thread_create(serial2, serial2_thread_entry, RT_NULL, 1024, 25, 10); /* 创建成功则启动线程 */ if (uart2_thread ! RT_NULL) { rt_thread_startup(uart2_thread); } else { ret RT_ERROR; } return ret; } //串口3初始化 int uart3_init(void) { rt_err_t ret RT_EOK; char str[] uart3 init successful!\r\n; /* 查找系统中的串口设备 */ serial3 rt_device_find(SAMPLE_UART3_NAME); if (!serial3) { rt_kprintf(find %s failed!\n, SAMPLE_UART3_NAME); return RT_ERROR; } /* 以中断接收及轮询发送模式打开串口设备 */ rt_device_open(serial3, RT_DEVICE_FLAG_INT_RX); /* 发送字符串 */ rt_kprintf(%s\n, str); return ret; }uart_cfg.h#ifndef APPLICATIONS_UART_CFG_H_ #define APPLICATIONS_UART_CFG_H_ #include rtthread.h //串口用到的数据包结构体 typedef struct { rt_uint8_t rx_buf[20]; rt_uint8_t rx_cnt; }UART_RX_typedef; #define SAMPLE_UART2_NAME uart2 #define SAMPLE_UART3_NAME uart3 #define face_reg_tick 5000 /* 用于接收消息的信号量 */ extern UART_RX_typedef uart2_rx_info; extern struct rt_semaphore uart2_rx_sem; extern rt_device_t serial2; extern rt_device_t serial3; int uart2_init(void); int uart3_init(void); #endif3.业务实现相关代码app_business.c#include rtthread.h #include rtdevice.h #include stdlib.h #include app_business.h #include pwm_cfg.h #include uart_cfg.h #include key_cfg.h //门的状态枚举 typedef enum { ST_IDLE, //门处于空闲状态 ST_DOOR_OPEN, //门处于打开状态 }door_state_t; static door_state_t door_state ST_IDLE; static rt_mq_t app_mq; static rt_timer_t door_close_timer; //加上static表明是内部使用收进本文件 static const rt_uint8_t add_face_cmd[4]{0xA1,0x02,0x00,0x1A}; static const rt_uint8_t delete_face_cmd[4]{0xA1,0x01,0x02,0x1A}; static float alarm_low36.0f; static float alarm_high37.0f; //软件定时器绑定的关门回调函数 static void door_close_timeout_handle(void *parameter) { app_post_event(EV_DOOR_CLOSE_TIMEOUT); } //核心业务处理函数状态机 static void app_state_handle(app_event_t *ev) { switch (ev-id) { //情况1K210识别到人脸 case EV_K210_MATCHED: if(door_state ST_IDLE) { rt_kprintf(door open!\n); rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL, 500000*2*20, 500000*4); door_state ST_DOOR_OPEN; rt_timer_start(door_close_timer); } break; //情况2按下按键添加人脸 case EV_BTTON_ADD_FACE: //添加人脸的操作 rt_kprintf(add face successful!\n); rt_device_write(serial2, 0, add_face_cmd, sizeof(add_face_cmd)); //给k210发送指令添加人脸 break; //情况3按下按键删除人脸 case EV_BTTON_DELETE_FACE: //删除人脸的操作 rt_kprintf(delete face successful!\n); rt_device_write(serial2, 0, delete_face_cmd, sizeof(delete_face_cmd)); //给k210发送指令删除人脸 break; //情况4开门时间到进行关门 case EV_DOOR_CLOSE_TIMEOUT: if (door_state ST_DOOR_OPEN) { // 加个保护只有真开着才关 rt_kprintf(door time out!\n); rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL, 500000*2*20, 500000); door_state ST_IDLE; } break; default: break; } } //业务线程负责接收消息队列里要执行的操作 static void app_thread_entry(void *parameter) { app_event_t ev; while(1) { if(rt_mq_recv(app_mq, ev, sizeof(ev), RT_WAITING_FOREVER) RT_EOK) { app_state_handle(ev); } } } //真正的事件投递函数 void app_post_event(event_id_t id) { app_event_t ev; ev.idid; ev.param0; rt_mq_send(app_mq, ev, sizeof(ev)); } //对业务文件中的消息队列、线程、软件定时器初始化及启动 int app_business_init(void) { app_mqrt_mq_create(app_mq, sizeof(app_event_t), 16, RT_IPC_FLAG_FIFO); if (app_mq RT_NULL) return -RT_ERROR; rt_thread_t tid rt_thread_create(app, app_thread_entry, RT_NULL, 1024, 20, 10); if (tid RT_NULL) return -RT_ERROR; door_close_timer rt_timer_create(door_close_timeout, door_close_timeout_handle, RT_NULL, 7000, RT_TIMER_FLAG_ONE_SHOT); if (door_close_timer RT_NULL) return -RT_ERROR; rt_thread_startup(tid); return RT_EOK; } //留给main函数的判断温度的接口 void app_business_check_tmep(float temp) { if(tempalarm_high) { rt_pin_write(BEEP_PIN_NUM, PIN_HIGH); } else if(tempalarm_low){ rt_pin_write(BEEP_PIN_NUM, PIN_LOW); } } /*————————————————————————————————————导出到MSH方便调试功能————————————————————————————————————*/ void door_open_CMD(int argc,char **argv) { app_post_event(EV_K210_MATCHED); rt_kprintf(posted CMD:door open\n); } MSH_CMD_EXPORT(door_open_CMD,open the door); void door_close_CMD(int argc,char **argv) { app_post_event(EV_DOOR_CLOSE_TIMEOUT); rt_kprintf(posted CMD:door close\n); } MSH_CMD_EXPORT(door_close_CMD,close the door); void door_status_CMD(int argc,char **argv) { rt_kprintf(door status:%s\n,door_state ST_DOOR_OPEN ? OPEN:IDLE); } MSH_CMD_EXPORT(door_status_CMD,show door status); void set_temp_alarm_range_CMD(int argc,char **argv) { if(argc3) { rt_kprintf(usage:set temperature alarms low to high\n); return; } alarm_lowatof(argv[1]);// 需要 #include stdlib.h alarm_highatof(argv[2]); if(alarm_lowalarm_high) { rt_kprintf(alarm_low must the small one!/n); return; } } MSH_CMD_EXPORT(set_temp_alarm_range_CMD, set temp alarm);app_business.h#ifndef APPLICATIONS_APP_BUSINESS_H_ #define APPLICATIONS_APP_BUSINESS_H_ typedef enum { EV_K210_MATCHED, //K210识别到人脸 EV_BTTON_ADD_FACE, EV_BTTON_DELETE_FACE, EV_DOOR_CLOSE_TIMEOUT, } event_id_t; typedef struct { event_id_t id; //触发的事件id rt_uint32_t param;//备用参数 }app_event_t; int app_business_init(void); void app_post_event(event_id_t id); //任务发出函数发给消息队列触发对应函数 void app_business_check_tmep(float temp); #endif4.代码整体架构分析我的旧版 VS ai让我更新的新版我的旧版的代码架构其实很简单甚至没有用到很多的RTThread的知识用了2个线程PIN设备UART设备甚至套的是官方的参考模板修改的虽然也不是不行但是对于RTThread的应用不是很深所以我就让AI给我提出改进方向多一些RTThread的应用也让代码变得更完善更高级一些。旧版框架的问题首先就是上面说的我觉得作为一个基于RTThread的项目但是RTThread的内容较少代码的主要内容都在写裸机就能接触到的东西。旧版的框架其实以uart_cfg.c/.h和key_cfg.c/.h为业务核心的主要问题就是使用大量if嵌套和直接使用外设的API函数显得代码比较混乱不易于一下子理解比如rt_device_write(serial3, 0, voice_check_face_succ, 5); //给语音模块发送人脸识别成功的指令其实再套一个应用层面的函数会更好理解虽然也不影响代码使用但是就是不易理解还有一个问题就是用了很多全局标志位或者说变量来帮助线程间或者文件之间通信、交互虽然也能正常运行但是不利于代码的长期维护和添加修改只要一变动一个地方就会牵扯到整个代码逻辑代码的耦合性太高各种标志位混杂阅读时也不易于理解。新版框架状态机消息队列定时器业务处理线程ai给我的方案就是去掉多余的标志位因为他们的功能都是检测状态、或者是在用同一个串口发送不同种类的数据可以使用状态机来整理收束代码使得代码更简洁更好读懂也更接近企业级的项目。例如app_business.c/.h就是AI让我添加的新的文件专门存放处理业务的代码比如用一个线程专门搜集整个项目会触发的事件识别/删除/添加人脸等等然后使用消息队列存放收集到的事件并根据事件进行不同的操作至于事件有谁发出或者事件从哪来就专门写一个对外的接口app_post_event(event_id_t id)只要k210检测到人脸了就用app_post_event(EV_K210_MATCHED)来发送事件。还有一些细节上的更改比如按键回调函数不要像旧版一样放日志打印和串口发送函数虽然代码也能正常运行但是理论上回调中断函数不能放这种耗时长的操作的所以就换成了app_post_event(event_id_t id)触发还有使用枚举来表示门的不同状态用定时器来控制门的定时关闭而不是用读取tick的方式显得高级和实用还有核心业务处理函数使用状态机的形式我也是第一次手写虽然能理解但是自己想出来还是太困难了让我又学到了一波经验五、其他杂谈1.为什么中断里可以直接调 app_post_event因为app_post_event里面核心是消息队列的发送函数RT-Thread 对 mq 的 send 做了 ISR 兼容如果用的是信号量、或自己用互斥量保护的队列中断里调就是错的。2.状态机的本质状态机表面上是为了让代码更好读其实深层理由是随着状态和情况越来越多if判断只会嵌套越来越多各种标志位逻辑会变得越来越混乱甚至出现自相矛盾的现象状态机就能很好的分开各种情况。3.中断、定时器、UART 线程三个并发源全部把事件投进一个 mq由唯一业务线程串行处理。竞态被架构消除了4.main 只做测温/显示/报警不碰业务逻辑。业务全在独立线程里主循环阻塞 100ms也不影响按键响应和开门实时性体现多线程把慢操作和快响应隔离的体现。六、总结RTThread的学习正式结束了拖沓挺久的了哈哈但是也是圆满完成跟着课程学习感觉教得太浅了自己和AI或者上B站研究很久啊尤其是跟着wds的RTThread底层讲解真的让我对RTThread的底层理解加深很多很多虽然现在也一时忘记了但是再去看应该捡得起来然后就是准备学习ESP32和freertos了希望能快速学完然后早实习然后过六级然后学Linux然后然后工作当然RTThread后续还是会复习的因为底层我感觉还是很重要的虽然日常开发用不上但是对于整个实时操作系统的理解会让我以后学其他知识更快应该就是看B站的wds的视频吧

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑