一、前言
国庆前做完了这个项目,其实跟着资料的功能比较简单,所以我决定自己加一些内容,加一些功能之类的吧,然后这篇内容主要记录自己做这个项目遇到的问题、疑惑,还有这么看懂代码,方便自己以后复习这个项目。
二、项目介绍
项目以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_tick=0; 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_cnt=0; } //接收到一个完整的数据包,进入数据包解析 if(uart2_rx_info.rx_cnt>3) { uart2_rx_info.rx_cnt=0; //收到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_tick=rt_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_tick>face_reg_tick) //如果添加人脸的时间已经过去了5s,防止刚添加人脸立即识别导致语音被打断 { const rt_uint8_t voice_check_face_succ[5]={0xAA,0x55,0x2,0x55,0xAA}; K210_last_tick=rt_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_cnt=0; /* 查找系统中的串口设备 */ 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_low=36.0f; static float alarm_high=37.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) { //情况1:K210识别到人脸 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.id=id; ev.param=0; rt_mq_send(app_mq, &ev, sizeof(ev)); } //对业务文件中的消息队列、线程、软件定时器初始化及启动 int app_business_init(void) { app_mq=rt_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(temp>alarm_high) { rt_pin_write(BEEP_PIN_NUM, PIN_HIGH); } else if(temp<alarm_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(argc<3) { rt_kprintf("usage:set temperature alarms low to high\n"); return; } alarm_low=atof(argv[1]);// 需要 #include <stdlib.h> alarm_high=atof(argv[2]); if(alarm_low>alarm_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的视频吧