简介:压缩包内是一套面向嵌入式爱好者和物联网开发者的完整工程,实现儿童误锁车内的远程报警系统。系统划分为车辆状态检测、车内情况采集与报警执行三大模块,通过卫星定位、惯性测量单元和接近开关判断车辆是否行驶及车门状态,利用温度传感器和气体传感器监测车内环境,并采用人体微波雷达与振动传感器联动识别后座人员活动,降低误报率。摄像头可抓拍并实时上传图像,手机应用能远程控制车窗升降,蜂鸣器现场示警。资源共包含三百余个文件,涉及C语言源码、工程配置、编译生成的可执行文件、原理图与印刷电路板设计、安卓客户端安装包以及详细设计文档,压缩包整体约一百一十六兆,目录结构清晰,便于源码阅读和二次开发。已有六百四十三人学习,适合需要完整项目参考的嵌入式学习者快速掌握多传感器协同、无线通信及移动端联动的实现思路。
1. 从“锁车”到“报警”的 10 分钟,STM32 能做多少事
夏天中午的封闭车厢,10 分钟内就能升到 50℃以上。儿童被困车内的悲剧往往不是“车门打不开”,而是监护人在车外根本不知道车里还有人。市面上带远程监控的车型很多,但存量车、低配车、网约车并不都有这套能力。基于 STM32 的儿童误锁车内远程报警系统,本质上是给普通车辆补一套独立的“离车后舱内生命存在检测 + 远程报警”硬件,不依赖原车总线,逻辑完全可控。它的典型落地方式是:STM32 单片机接入人体感应、门锁状态、振动传感器,检测到“车门已锁 + 舱内疑似有人”两个条件后,延时确认再通过 4G/GPRS 模块发短信或上云告警。这套方案特别适合 STM32 学习和毕设场景,因为从 GPIO 采样、定时器延时到串口 AT 指令,正好覆盖了嵌入式开发的主干技能。本文不写商业量产方案,就按一个能跑通、能演示、能扩展的工程来拆,源码结构也会顺着这个思路展开。
2. 硬件选型与系统架构:STM32 儿童误锁报警要先解决“哪个信号可信”
2.1 主控选型:STM32F103C8T6 为什么是这套系统的主力
先看一个反直觉的结论:儿童误锁报警系统里,选 MCU 的第一要素不是算力,而是外设数量和低功耗待机能力。STM32F103C8T6 是 48 引脚封装,自带 3 个 USART、2 个 I2C、2 个 SPI、4 个 16 位定时器、2 个 12 位 ADC,还支持 STOP 和 STANDBY 模式。做远程报警需要同时挂接探测传感器、通信模块、蜂鸣器/LED 指示,这些接口刚好够用,不用额外扩展芯片,板上体积也能压下来。
再者,F103 系列的大部分代码与 F1 全系列兼容,后期如果从 C8T6 换成引脚更多的 RCT6,迁移成本低。对毕设和工程预研来说,选它的另外两层理由是资料密度和调试工具链成熟。Keil5、STM32CubeMX、江科大的外设教程、各类 STM32 开发环境配置文章都围绕 F1 展开,遇到怪异问题能搜到现成答案,这对项目推进很关键。
2.2 传感器与信号接入:不能只靠一个 PIR 红外模块
人体存在检测最常见的方案是 HC-SR501 热释电红外模块,成本几块钱,输出就是 GPIO 电平。它放在车内的明显缺点是误报与漏报并存:夏天阳光暴晒下车内温度升高,PIR 容易误触发;儿童在后排睡着不移动,PIR 又可能因为红外特征被座椅遮挡检测不到。
所以这套系统里我一般会做信号冗余,至少接入三类信号:
| 信号 | 传感器/接口 | 判断作用 | 接线建议 |
|---|---|---|---|
| 人体存在 | HC-SR501 PIR | 舱内是否有生命活动 | 接到 STM32 的 PA0,按键输入模式 |
| 门锁/车门状态 | 门碰开关或锁车电平检测 | 是否处于锁止离车状态 | 读取 PB1 电平,按键输入模式 |
| 车体振动 | SW-420 振动模块或加速度计 | 辅助判断是否有人在车内活动 | 接 PA1,外部中断触发 |
三类信号的组合逻辑比单独用 PIR 可靠:振动信号用于防止“PIR 短暂低电平”造成的漏报,门锁状态用于确认监护人已经离车。如果是学习板没有真实门锁线,可以用一个拨码开关模拟锁车信号,调试逻辑完全一致。
2.3 远程通信选型:短信、4G 网络还是手机蓝牙
远程报警的最终出口决定了这个系统是“演示玩具”还是“能用的原型”。方案选择上有一个迁移路径比较实用的原则:报警类设备需要双通道,主通道发送详细数据,次级通道做短信兜底,防止网络异常时完全失联。
| 通信方案 | 优点 | 缺点 | 适用阶段 |
|---|---|---|---|
| 蓝牙 BLE | 成本低、开发快 | 有效距离短,离开车就断 | 调通主逻辑 |
| GPRS/2G 短信 | 覆盖广,不依赖 App | 2G 退网地区不可用 | 演示远程报警 |
| 4G Cat.1 模块 | 网络稳定、可上云 | 资费、代码量稍大 | 接近量产原型 |
| 车载以太网 | 带宽大,但非存量车方案 | 需要原车支持 | 新车型集成 |
代码包里最常见的实现是 GSM 模块发短信,比如 SIM800 系列,配合 AT 指令。但要注意,如果项目发生在部分已退 2G 网络的地区,演示时会出现“本地实验室正常,一上车就发不出短信”的情况。稳妥的做法是主控预留两个通信接口,一个接 4G Cat.1 模组用 MQTT 上报,一个接 GSM 用短信兜底,两路都跑不通时启动蜂鸣器/双闪事件提示。
3. 状态机设计与关键代码:STM32 怎么从 GPIO 数据中判定“误锁”
3.1 先定义状态,再写中断和定时器
新手常见的错误是一上来就轮询 PIR 和门锁电平,读到高电平就报警。实际车辆场景里,PIR 模块上电后有 30 秒左右的稳定期,人体移动产生的脉冲也有长有短,直接读电平会把瞬间噪声当成有效信号。
我一般把系统拆成四个状态:待机、锁车确认、舱内检测、报警确认。代码层面先定义状态枚举,再通过事件迁移:
typedef enum { STANDBY, // 初始待机,未锁车 LOCK_CONFIRM, // 检测到锁车信号,等待稳定 CABIN_CHECK, // 锁车成立,检测舱内人体活动 ALARM_CONFIRM // 人体存在持续超时,进入报警 } SysState; volatile SysState sys_state = STANDBY; volatile uint8_t lock_pin_level; // 门锁状态引脚采样值 volatile uint8_t pir_pin_level; // PIR 输出采样值用一个枚举管理状态而不是用散落的全局标志位,收益在后面排错时非常明显:串口打印里可以直接输出状态枚举值,一眼看出程序卡在哪个环节。
3.2 门锁状态读取与 PIR 采样去抖的核心代码
门锁输入这种开关量必须做去抖。车的门锁开关来自机械触点,车门锁止瞬间会有持续几十毫秒的抖动,不加滤波就会出现一次锁车触发四五次中断。常见做法是“连续多次采样一致才认为电平有效”。GPIO 配置为输入上拉,循环读取并统计:
#define SAMPLE_TIMES 8 // 连续采样次数 #define SAMPLE_DELAY 10 // 每次采样间隔 10ms uint8_t read_debounced_gpio(GPIO_TypeDef *port, uint16_t pin) { uint8_t stable_level = 0; uint8_t cnt_high = 0; for (uint8_t i = 0; i < SAMPLE_TIMES; i++) { if (HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_SET) { cnt_high++; } HAL_Delay(SAMPLE_DELAY); } // 8 次里至少 7 次为高,才认定高电平 stable_level = (cnt_high >= 7) ? 1 : 0; return stable_level; }参数说明:SAMPLE_TIMES和SAMPLE_DELAY共同决定了去抖时间是 80ms,对车锁抖动足够;cnt_high >= 7这个阈值给了一格容错,防止偶发毛刺;HAL_Delay在简单轮询场景里没问题,但要注意这个函数会阻塞主循环,后续加了通信模块后建议改成定时器插桩。
PIR 信号的读取更讲究。HC-SR501 带可调延时输出,模块上的电位器如果调到最大,输出高电平可能持续 5 分钟,这会导致只要人动过一次,后续就一直被判为“有人”。必须在主控制里加“信号跌落检测”:只有在 PIR 输出从高变低之后,才认为一次人体活动结束,再启动下一轮判断。
void cabin_detect_task(void) { static uint8_t last_pir_level = 0; uint8_t cur_pir_level = HAL_GPIO_ReadPin(PIR_PORT, PIR_PIN); // 检测到 PIR 上升沿,说明有人体活动 if (cur_pir_level == GPIO_PIN_SET && last_pir_level == GPIO_PIN_RESET) { human_activity_count++; last_pir_level = 1; } // PIR 变为低电平,活动事件结束 if (cur_pir_level == GPIO_PIN_RESET && last_pir_level == 1) { last_pir_level = 0; } }逻辑说明:human_activity_count是活动次数计数器,而不是电平状态。判断报警时看的是“在确认时间内发生了至少 N 次活动”,这样坐车内玩手机的小孩能触发,静止不动但存在呼吸的婴儿则要靠更敏感的红外阵列或毫米波雷达来补,PIR 单传感器本身存在物理局限。
3.3 用定时器和系统 Tick 实现报警延时确认
报警必须有时延,防止“监护人下车拿东西,30 秒后又回来”导致误报。我习惯用系统 Tick 计数器实现时延,而不是HAL_Delay阻塞等待,因为阻塞期间无法响应振动传感器的紧急触发。
uint32_t tick_lock_confirm_start; // 记录锁车确认起始时间 uint32_t tick_detect_start; // 记录舱内检测起始时间 #define LOCK_STABLE_MS 3000 // 锁车信号持续 3 秒才算真正锁车 #define DETECT_WINDOW_MS 30000 // 锁车后检测窗口 30 秒 #define ACTIVITY_THRESHOLD 2 // 窗口内至少 2 次活动 void state_machine_tick(void) { switch (sys_state) { case LOCK_CONFIRM: if (HAL_GetTick() - tick_lock_confirm_start >= LOCK_STABLE_MS) { sys_state = CABIN_CHECK; tick_detect_start = HAL_GetTick(); } break; case CABIN_CHECK: if (human_activity_count >= ACTIVITY_THRESHOLD) { sys_state = ALARM_CONFIRM; } if (HAL_GetTick() - tick_detect_start >= DETECT_WINDOW_MS) { // 窗口内没有足够活动,自动回到待机 sys_state = STANDBY; } break; default: break; } }代码里的时间参数是最需要按车型调的部分:车辆机械结构不同,锁车振动导致 PIR 晃动的时长也不同。3 秒锁车确认、30 秒检测窗口、2 次活动阈值是起始推荐,不等于适合所有车。后文第 5 章会讲具体怎么根据实车环境调这三个参数。
4. 远程报警链路:STM32 怎么通过 AT 指令把消息发到手机上
4.1 短信报警的 AT 指令序列与串口初始化
通信模块在源码包里通常以“串口 + AT 指令”的方式封装。初始化流程固定:先发测试指令确认模块在线,再检查 SIM 卡,最后设置短信为文本模式。我习惯把这些指令统一放到一个send_at_command函数里,超时未返回 OK 就重试 3 次,避免硬件上电时序不稳定导致的偶发无响应。
uint8_t gsm_send_sms(const char *phone, const char *message) { char cmd[128]; snprintf(cmd, sizeof(cmd), "ATSMS=C\r\n>%s\x1a", phone); // 设置短信格式为文本模式,1 表示文本格式 // AT+CMGS 后面先以 \r\n> 提示符结束,再发送正文,最后以 0x1A(CTRL+Z) 结束 gsm_uart_send("AT\r\n", 100); gsm_uart_send("AT+CMGF=1\r\n", 100); gsm_uart_send(cmd, 5000); return gsm_wait_response("OK", 5000); }参数说明:AT+CMGF=1设置文本模式短信,这样中文字符内容可以直接通过 Unicode 编码发送;AT+CMGS=手机号\r\n进入短信输入状态,模块返回>后接收正文;正文结束必须发送0x1A,也就是 Ctrl+Z,模块才会把短信发出去。gsm_wait_response里的 5000ms 超时针对发送执行过程,不要缩到 1 秒,因为弱信号下模块重传协议栈会占时间。
4.2 从短信到 MQTT 上云:报警记录的 JSON 信息体设计
短信报警能解决“有人知道”,但解决不了“有记录、可统计”。工程化时会在 STM32 和 4G 模组之间走 MQTT 协议,把报警事件上报到物联网平台。协议格式不复杂,核心是把设备 ID、报警类型和采集到的传感器计数组合成一个 JSON 字符串。在单片机里不拼复杂 JSON,直接塞固定结构:
typedef struct { char device_id[12]; uint8_t alarm_type; uint16_t activity_count; uint32_t timestamp; } AlarmPacket; AlarmPacket alarm = { .device_id = "CARALARM001", .alarm_type = 1, // 1=误锁报警 .activity_count = human_activity_count, .timestamp = HAL_GetTick() };上报时的实际组包我建议直接用snprintf拼字符串,然后通过串口发给模组:
char payload[128]; snprintf(payload, sizeof(payload), "{\"dev\":\"%s\",\"type\":%d,\"cnt\":%d,\"ts\":%lu}", alarm.device_id, alarm.alarm_type, alarm.activity_count, (unsigned long)alarm.timestamp); // 将 payload 通过 AT+QMTCONN / AT+QMTPUB 等指令交给 4G 模组发送timestamp用的是HAL_GetTick(),这个可读性不强,正式项目里应改成从 RTC 读取的 UNIX 时间戳。device_id建议与 SIM 卡卡号或盒子序列号绑定,这样才能在平台端区分多辆车。
4.3 去重与事件合并:避免同一事件轰炸监护人
远程报警最烦人的就是重复短信。锁车后儿童来回挪动,PIR 连续触发,报警系统如果每触发一次就发一条短信,监护人手机上会瞬间出现十几个未接告警。要对报警事件做合并:一次锁车周期内,只发送第一条告警,后续追加状态则通过平台历史记录查询。
uint8_t alarm_sent_flag = 0; if (sys_state == ALARM_CONFIRM && alarm_sent_flag == 0) { gsm_send_sms(guardian_phone, "儿童误锁报警: 车门已锁, 舱内检测到人体活动"); alarm_sent_flag = 1; // 本次锁车周期内不再重复发送 } // 车门解锁后,清除标志位,准备下一次锁车检测 if (read_debounced_gpio(LOCK_PORT, LOCK_PIN) == 0) { alarm_sent_flag = 0; human_activity_count = 0; sys_state = STANDBY; }这段逻辑的关键在于alarm_sent_flag的生命周期:它是被锁车状态复位的,而不是被时间复位的。如果有人一直不开锁,系统会保持报警但不会刷屏;一旦开锁,状态机立刻回到待机,为下一次锁车做准备。这条规则要写进代码注释里,否则后续维护容易改成“半小时后可以重新报警”的定时逻辑,那就会引入新的误报窗口。
5. 实车测试、低功耗与固件升级:把 STM32 误锁报警调到不矫情、不失联
5.1 三组参数的实际调法与验证方法
项目进入联调阶段,最不能偷懒的是用真实车辆做误锁模拟。把车开到一个相对安静的停车场,车上留人,车外用手机秒表计时,验证三个问题:关车门锁车后多久能收到报警短信;窗户留缝和全部关闭时 PIR 是否有不同表现;后排有人睡觉不动时是否能检测到。
| 参数 | 推荐起始值 | 出现误报时 | 出现漏报时 |
|---|---|---|---|
| LOCK_STABLE_MS | 3000ms | 增加到 5000ms | 保持 |
| DETECT_WINDOW_MS | 30000ms | 缩短到 15000ms | 延长到 60000ms |
| ACTIVITY_THRESHOLD | 2 次 | 提高到 4 次 | 降到 1 次 |
提醒一句:误报和漏报本质是同一对矛盾。活动阈值调高会减少误报但增加漏报,要在“减少打扰”和“保证报警”之间平衡,推荐锁定在一个规则上:宁可多报一次,不可漏掉一次。另外还有个容易被忽略的点,PIR 模块的灵敏度电位器出厂默认在中间,装车后要根据后排座椅距离重调,否则会出现“人坐在后排正中检测不到,手挥到副驾才报”的错位现象。
5.2 低功耗处理:锁车状态下的 STM32 自动进入 STOP 模式
车在锁车后可能持续数小时,通信模块、传感器全部高功耗运转是不现实的。我的做法是把系统分成两级:锁车后 30 秒内保持全速检测,确认车内没人后进入低功耗轮询;车内有人的报警场景下,STM32 保持运行但降低蓝牙和屏幕功耗。进入 STOP 模式的入口很简单,关键是唤醒源。
void enter_low_power_if_safe(void) { if (sys_state == STANDBY && alarm_sent_flag == 0) { // 关闭 PIR 供电, 保留门锁引脚的外部中断作为唤醒源 HAL_GPIO_WritePin(P_EN_PORT, P_EN_PIN, GPIO_PIN_RESET); HAL_SuspendTick(); HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // 唤醒后恢复 HAL_ResumeTick(); HAL_GPIO_WritePin(P_EN_PORT, P_EN_PIN, GPIO_PIN_SET); } }代码里HAL_SuspendTick()和HAL_ResumeTick()是必须的一对操作。STOP 模式下 SysTick 停摆,如果不挂起 Tick,唤醒后会因为 HAL 库的 Tick 计数错乱导致所有延时函数行为异常。门锁引脚要用外部中断 EXTI 模式而不是轮询模式,否则无法唤醒。低功耗调试常见问题是“电流没降下来”,原因多半是板上 LED 指示灯、稳压芯片静态电流没有一起关,低压差线性稳压器在微安级待机场景反而成了耗电大户。
5.3 用 Bootloader 做现场升级,不用每次拆车刷 Keil
最后是一个实用技巧:报警参数和电话号码往往要改。单纯依赖 Keil 烧录,意味着每次改监护人手机号都要拆设备。更合理的做法是在 STM32 里分两个区:Bootloader 区和 App 区。Bootloader 跑在 0x08000000,通过串口接收固件包后写入 App 区,App 区起始地址按实际 Flash 大小偏移。
这样现场需要改电话号时,只需要让 4G 模组走远程通道把参数下发到 App 区里的专门存储页,复位后生效。Bootloader 不参与业务功能,负责校验固件 CRC 和保护升级失败回滚。设备第一次烧录时在 Keil 里把程序的起始地址设置成 0x08008000,并生成对应 Hex,后续所有功能迭代都能远程完成,不用把已经装进车里的设备再拆下来,这是从“原型”走向“真正可维护”的关键一步。
整个系统做到这一步,已经不只是“一个检测到人就发短信”的传感器,而是一个带状态管理、告警去重、低功耗策略和远程维护能力的完整嵌入式终端。想在此基础上继续扩展,可以把振动传感器升级为毫米波雷达以解决 PIR 对静止人体的漏检,也可以叠加 CAN 收发器读取原车车门状态,让锁车信号真正来自汽车总线而不是额外的接触开关。
本文还有配套的精品资源,点击获取