OLED红外温度传感器代码
280.6OLED红外温度传感器代码
/* uart_cfg.c:本节完成 5 秒非阻塞事件抑制后的 UART2 接收线程。 */
void serial2_thread_entry(void *parameter) // 定义 UART2 接收线程入口;本节未使用 parameter 参数。
{ // serial2_thread_entry() 函数体开始。
char ch; // 定义临时变量,每次保存从 UART2 读取的 1 个字节。
static rt_tick_t k210_last_tick=0; // 保存上次录入成功或识别播报时的系统节拍,初值为 0。
while (1) // 永久循环,持续读取并解析 K210 返回帧。
{ // UART2 接收循环开始。
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) // 检查当前候选帧的首字节是否为 0xAF。
{ // 帧头正确的处理分支开始。
uart2_rx_info.rx_cnt++; // 累计已接收字节数,并把写入下标移到下一位置。
}else { // 候选帧没有以 0xAF 开头时执行此分支。
uart2_rx_info.rx_cnt=0; // 丢弃当前候选数据,从缓冲区开头重新找帧头。
} // 帧头判断结束。
if(uart2_rx_info.rx_cnt>3) // 已缓存 4 字节时,开始判断完整的 K210 返回帧。
{ // 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 }; // 定义“人脸录入成功”的语音命令。
rt_device_write(serial3, 0, voice_add_face_succ, 5); // 通过 UART3 向语音模块发送 5 字节命令。
k210_last_tick = rt_tick_get(); // 记录录入成功时刻,随后约 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 }; // 定义“人脸识别成功”的语音命令。
if(rt_tick_get() - k210_last_tick > 5*1000) // 判断距上次记录时刻是否已经超过 5000 个系统节拍。
{ // 超过抑制时间的处理分支开始。
rt_device_write(serial3, 0, voice_check_face_succ, 5); // 允许本次“人脸识别成功”语音播报。
k210_last_tick = rt_tick_get(); // 更新节拍基准,使下一次播报仍需再间隔约 5 秒。
} // 超过抑制时间的处理分支结束。
} // 人脸识别成功分支结束。
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 }; // 定义“人脸删除成功”的语音命令。
rt_device_write(serial3, 0, voice_del_face_succ, 5); // 通过 UART3 触发删除成功语音。
} // 人脸删除成功分支结束。
uart2_rx_info.rx_cnt=0; // 当前 4 字节候选帧处理完毕,清零计数器等待下一帧。
} // 4 字节帧解析区结束。
} // UART2 接收循环结束。
} // serial2_thread_entry() 函数体结束。
/* oled_cfg.c:复制并重命名 OLED 文件后,本节修正的头文件名。 */
#include "oled_cfg.h" // 包含已改名的 OLED 配置头文件,替代旧工程的 oled_config.h。
/* main.c:本节移植 OLED、GY-906 并加入高温蜂鸣器判断后的主函数。 */
#include "key_cfg.h" // 引入按键、蜂鸣器引脚和 io_init() 声明。
#include "oled_cfg.h" // 引入 OLED 初始化与温度显示函数声明。
#include "GY906.h" // 引入 GY-906 初始化和红外温度读取函数声明。
#include "uart_cfg.h" // 引入 UART2、UART3 初始化函数声明。
float temp; // 定义全局浮点变量,保存当前红外目标温度。
int main(void) // 定义 RT-Thread 用户程序入口。
{ // main() 函数体开始。
DisplayCfg_Init(); // 初始化 U8g2、SSD1306 OLED 及 PB6/PB7 软件 I²C 接口。
GY906_Init(); // 初始化 GY-906 使用的 PB6/PB7 软件 SMBus 引脚。
io_init(); // 初始化 PB8 蜂鸣器和两个人脸功能按键。
uart3_init(); // 先初始化向语音模块发送命令的 UART3。
uart2_init(); // 再初始化与 K210 通信的 UART2 及其接收线程。
while (1) // 永久循环,持续完成测温、显示和高温判断。
{ // 主循环开始。
temp = SMBus_ReadTemp(); // 通过自定义 SMBus 驱动读取 MLX90614 的目标温度。
Display_normal(temp); // 在 OLED 上显示本次测得的浮点温度。
if(temp>37) // 判断目标温度是否严格大于 37 ℃。
{ // 高温报警分支开始。
rt_pin_write(BEEP_PIN_NUM, PIN_HIGH); // 将 PB8 置高,使课程使用的蜂鸣器发声。
}else { // 温度不大于 37 ℃时执行此分支。
rt_pin_write(BEEP_PIN_NUM, PIN_LOW); // 将 PB8 置低,关闭蜂鸣器。
} // 温度判断结束。
rt_thread_mdelay(100); // 让 main 线程休眠 100 ms,再进行下一次采样与刷新。
} // 主循环结束。
return RT_EOK; // 保留课程源码的成功返回语句;正常运行时不会执行到这里。
} // main() 函数体结束。
文首代码按本节结束时的进度整理。课程复制进工程但没有逐行讲解的
GY906.c/.h与 OLED 显示函数已在第 272 节精校稿中说明,本节不重复抄入驱动内部实现;归档中的face_check_flag、pwm_cfg.h、pwm_init()、舵机开关门与 7 秒计时属于下一节,未提前加入。代码的有效语句、名称和数值保持课程写法,只移除旧注释并补充逐行说明。
一、结巴问题
这里的“结巴”不是音频文件本身损坏,而是业务事件过于密集:K210 完成人脸录入后马上继续识别摄像头画面,新录入的人脸很快又匹配成功,于是 STM32 先后向 SU-03T 发送“录入成功”和“识别成功”两条语音命令。K210 对同一张持续出现在画面中的脸还会反复返回识别成功帧,因此语音可能连续重复。
把固定延时直接塞进 UART2 接收线程并不理想,但原稿所说“线程卡死”需要准确理解:
- 若调用
rt_thread_mdelay(),只会挂起当前接收线程,其他就绪线程仍可被 RT-Thread 调度; - UART2 接收线程休眠期间不能及时解析新的 K210 返回帧,数据会在驱动缓冲区积压,缓冲区满后还可能丢失;
- 若使用占用 CPU 的忙等待,则还会浪费处理器时间并影响低优先级任务;
- 删除按键仍可能发送命令,但它的返回帧也要等接收线程恢复后才能解析和播报。
本节采用“仍然接收并解析所有帧,只暂时不执行识别成功播报”的时间门控,所以接收线程不会为了等待 5 秒而停在那里。
视频还演示了 GY-906 模块因摆放或带电接线而发生疑似短路。没有测量就不能断言一定是某颗“稳压器”或“变压器”被击穿;VCC 与 GND 或 VCC 与信号脚误碰,可能导致电源限流、稳压器过热、走线损坏或芯片损坏。安全处理应是:
- 插拔、移动和改线前先断电;
- 上电前用万用表电阻/通断挡检查 VCC 与 GND 是否近似短路;
- 核对模块电源范围、极性以及 SDA/SCL 线序,不凭导线颜色判断;
- 首次上电使用有电流限制的电源并观察异常发热;
- 若发生短路,立即断电,先排除接线和焊锡桥连,再更换器件。
准备便宜模块的备用件可以减少开发停工时间,但不能替代断电接线、限流和上电前检查。
二、加延时:人脸录入后的识别抑制
本节口述的“延时”实际是非阻塞时间门控。rt_tick_get() 返回系统启动以来累计的 OS Tick;代码保存一个过去的节拍值,在新事件到达时计算时间差,不主动等待。
当前 15PRJ/rtconfig.h 定义 RT_TICK_PER_SECOND 为 1000,所以 5000 tick 约等于 5 秒。源码使用严格大于 > 5*1000,因此恰好等于 5000 tick 时仍不会进入分支,要到超过该值且下一条识别帧到来时才会播报。
2.1 例题1: 人脸录入后延时处理人脸识别指令
首次时间门控的执行顺序是:
- 收到
AF 05 00 FA,立即播放“人脸录入成功”; - 用
k210_last_tick = rt_tick_get()记录该时刻; - 后续仍然接收并解析
AF 02 编号 FA; - 若当前 tick 与记录值之差不超过 5000,就忽略该帧对应的语音动作;
- 时间差超过 5000 后,下一条识别成功帧才能触发播报。
因此,代码不是“5 秒后才开始接收识别指令”,也没有让 K210 停止识别;它只是在 STM32 侧抑制一段时间内的识别成功语音。
把 5000 解释成 5 秒依赖当前工程的 tick 频率。若以后修改 RT_TICK_PER_SECOND,写死的 5*1000 就不再代表 5 秒;更通用的项目通常使用 5 * RT_TICK_PER_SECOND 或 rt_tick_from_millisecond(5000) 完成换算。本节按要求不改写课程源码。
还存在一个启动边界:k210_last_tick 初值为 0,所以系统启动后的前约 5 秒,即使没有发生过人脸录入,识别成功语音也会被抑制。这是当前代码的实际行为。
2.2 例题2: 人脸识别成功后延时处理重复指令
只在录入成功时记录 tick 还不够。K210 持续识别同一张脸时,会继续发送识别成功帧;第一次被允许播报后,代码必须再次执行:
k210_last_tick = rt_tick_get();
这样同一个变量的意义会随最近一次有效事件更新:
- 刚录入成功时,它表示“录入成功播报时刻”;
- 第一次识别播报后,它表示“最近一次识别成功播报时刻”;
- 未满 5 秒而被忽略的识别帧不会更新它;
- 删除成功分支也不会更新它。
最终效果是识别成功语音的最小间隔约为 5 秒,而不是把每一帧依次延后 5 秒播放。被抑制的事件直接丢弃,不会进入待播队列。
rt_tick_t 在本工程中是无符号节拍类型。采用“当前值减旧值”的写法,在所比较时间远小于计数器完整回绕周期时,可以利用无符号模运算跨过一次 tick 回绕;不要改成简单判断 now > deadline 后就认为两种写法完全等价。
2.3 static在代码中的运用
static rt_tick_t k210_last_tick = 0; 是函数内静态变量:
- 作用域仍然只在
serial2_thread_entry()的代码块内,其他函数不能直接按名称访问它; - 存储期是整个程序运行期,只初始化一次,函数返回后值也会保留;
- 未显式赋初值的静态存储期对象会被初始化为零,本代码仍明确写出
= 0; - 它不是“虽然是局部变量,但就是全局变量”。局部/全局描述作用域位置,静态存储期描述对象存在多久,二者是不同概念。
在这段特定代码中,变量声明在接收线程入口的无限循环之前,而线程入口通常只调用一次且不返回;即便使用普通自动局部变量,它在这一次函数调用期间也不会每轮重新初始化。因此 static 并非当前循环能够记住数值的唯一原因。课程的写法仍有“只初始化一次、静态存储期”的明确语义,文首保持原样。
文件作用域变量前的 static 又是另一种常见用法:它让名称只在当前 .c 文件中具有内部链接。不能把函数内静态变量、文件内静态变量和普通全局变量混成同一个概念。
三、代码测试
烧录后可按以下顺序验证:
- 系统运行超过 5 秒后按录入键,将人脸置于 K210 摄像头视野内;
- 确认首先播放“人脸录入成功”;
- 保持人脸不动,确认随后约 5 秒内没有立刻播放“人脸识别成功”;
- 超过门控时间后,等待下一条识别成功帧,确认播放一次;
- 继续保持人脸,确认两次识别成功播报之间仍相隔约 5 秒;
- 按删除键,确认删除命令和删除成功播报仍能处理。
视频中讲师连续按了两次录入键,因此可能产生两次录入请求;本节代码没有按键消抖、命令忙状态或重复录入保护。5 秒门控只限制“识别成功语音”,不会阻止重复发送录入命令,也不会解决 K210 人脸库中可能出现的重复特征。
本节测试还受光照和摄像头画面影响。若 K210 没有持续检测到人脸,超过 5 秒也不会凭空播放语音;必须在门控期结束后再收到一条新的识别成功帧,STM32 才有事件可处理。
完成语音门控后,本节把第 14 个工程中的以下文件复制到 15PRJ/applications:
| 原文件 | 本工程文件 | 作用 |
|---|---|---|
GY906.c/.h | GY906.c/.h | 用 GPIO 模拟 SMBus,读取 MLX90614 目标温度 |
oled_config.c/.h | oled_cfg.c/.h | 初始化 U8g2/SSD1306 并显示温度 |
OLED 文件改名后,oled_cfg.c 内部的包含语句也必须从 #include "oled_config.h" 改为 #include "oled_cfg.h"。只在文件管理器里改文件名而不改源码引用,会导致编译器找不到旧头文件。
四、主函数编写
本节把温度采集、OLED 显示和蜂鸣器报警接入已有主函数,初始化顺序为:
DisplayCfg_Init() → GY906_Init() → io_init() → uart3_init() → uart2_init()
主循环每约 100 ms 执行一次:
SMBus_ReadTemp()读取 MLX90614 的目标温度;Display_normal(temp)把温度绘制到 OLED;temp > 37时将 PB8 置高,打开课程使用的蜂鸣器;- 其他情况将 PB8 置低;
rt_thread_mdelay(100)只让 main 线程休眠,UART2 接收线程仍可运行。
判断使用的是严格大于,因此边界行为为:
temp 条件 | PB8 输出 | 蜂鸣器状态 |
|---|---|---|
temp > 37 | PIN_HIGH | 响 |
temp <= 37 | PIN_LOW | 不响 |
代码没有滞回和滤波,温度在 37 ℃附近抖动时,蜂鸣器可能频繁切换。驱动也没有向 main() 返回独立的通信错误状态;若读数异常、总线失败或得到非数值,单一的 temp > 37 判断不能构成可靠的安全报警。正式测温产品还要增加错误检测、合理范围检查、连续采样和阈值滞回。
OLED 与 GY-906 在课程工程中共用 PB6/PB7:
| STM32 | OLED | GY-906 | 作用 |
|---|---|---|---|
| PB6 | SCL | SCL | 时钟线 |
| PB7 | SDA | SDA | 双向数据线 |
| GND | GND | GND | 共同电位参考 |
| 合适电源 | VCC | VCC | 按各自模块规格供电 |
两线总线通常依靠开漏输出和上拉电阻实现共享。课程中 OLED 使用 U8g2 的软件 I²C 回调,GY-906 使用自己的 GPIO 软件 SMBus,两套实现并没有通过同一个 RT-Thread I²C 总线对象统一仲裁;当前程序在同一线程中顺序访问,实物可以工作,但以后若多个线程同时操作 PB6/PB7,必须用互斥机制保护完整事务,不能交叉翻转两根线。
4.1 编译错误
复制 OLED 文件后编译失败,直接原因是工程中缺少 u8g2_port.h 及其相关实现。该头文件来自 U8g2 软件包,不是 C 标准库,也不是仅复制 oled_cfg.c/.h 就会自动生成的文件。
排查顺序应为:
- 先确认
oled_cfg.c已包含正确的oled_cfg.h; - 查看第一条编译错误,确认是否提示找不到
u8g2_port.h; - 在 RT-Thread Settings 中添加课程使用的
u8g2-official软件包; - 保存配置并等待软件包下载、工程索引和构建脚本更新完成;
- 清理后重新编译,而不是只靠 IDE 红线是否消失判断。
4.2 添加u8g2组件
课程在软件包中心搜索并添加 u8g2-official,随后按 Ctrl+S 保存。归档的 .config 最终启用了 CONFIG_PKG_USING_U8G2_OFFICIAL,但没有启用 CONFIG_RT_USING_I2C 或 U8g2 硬件 I²C。
这是因为本工程实际选择的是:
- OLED:
u8x8_byte_sw_i2c,由 U8g2 回调直接模拟两线时序; - GY-906:
GY906.c直接调用 PIN 接口模拟 SMBus; - 两者都没有调用 RT-Thread 的
rt_i2c_transfer(),也没有查找i2c1设备。
所以本节不需要为了“用了两根 I²C 线”就额外打开 RT-Thread I²C 设备框架。若以后改用标准 I²C 总线设备,则要同时迁移两个访问方、配置总线与引脚,并处理设备地址和互斥,不能只勾选一个选项就认为迁移完成。
五、RT-Thread文档中心
5.1 RT-Thread简介
课程借文档中心补充说明:此前 UART2 和 UART3 初始化都没有主动设置波特率。15PRJ 中的 RT_SERIAL_CONFIG_DEFAULT 为 115200、8 数据位、1 停止位、无校验、无流控,所以当前两路串口沿用 115200。它不是由 PA2/PA3 或 PB10/PB11 引脚决定的。
默认值恰好符合当前 K210 脚本和课程 SU-03T 固件时,通信才能正常。若把语音模块改为 9600,必须同时把 STM32 的 UART3 改为 9600;只改一端会得到乱码或完全无法识别命令。
5.2 UART设备
RT-Thread 修改串口参数的核心对象与调用关系如下:
| 项目 | 作用 |
|---|---|
struct serial_configure | 保存波特率、数据位、停止位、校验、位序和流控等参数 |
RT_SERIAL_CONFIG_DEFAULT | 生成一份完整的默认配置初值 |
config.baud_rate | 在默认配置基础上只改需要变化的波特率 |
RT_DEVICE_CTRL_CONFIG | 告诉设备控制接口执行“重新配置”命令 |
rt_device_control(serial, ..., &config) | 把配置对象地址传给目标串口驱动 |
课程建议把配置步骤放在 rt_device_find() 成功之后、rt_device_open() 之前。逻辑顺序是:
- 查找并取得目标串口设备句柄;
- 用
RT_SERIAL_CONFIG_DEFAULT初始化整个配置结构体,避免其他字段未初始化; - 修改
baud_rate等确实需要变化的成员; - 调用
rt_device_control(..., RT_DEVICE_CTRL_CONFIG, &config); - 检查返回值,成功后再打开设备并继续注册回调或收发数据。
本节只在官方文档中讲解了这套方法,没有把 9600 配置代码写入 uart2_init() 或 uart3_init()。因此文首源码仍保持两路 UART 的默认 115200,不能把“举例可以改成 9600”误写成项目已经改为 9600。