简介:基于STM32单片机开发的车内环境检测与儿童滞留报警系统,面向嵌入式单片机学习者和汽车安全类项目开发者,覆盖从硬件设计到软件实现的全流程。资源共6个文件,包含2个mp4(实物运行演示与APP端操作录屏)、2个docx(开题报告与整体方案)、1个pdf电路原理图以及1个sql数据库脚本,压缩包总大小41.68MB,结构清晰便于按模块参考。已有71人访问学习,适合需要借鉴完整项目框架、电路连接和报警处理逻辑的读者。通过实物演示视频可快速了解系统工作状态,配合原理图和方案文档能深入理解传感器采集、STM32数据处理及短信电话报警机制,sql文件则展示了数据管理方式,整体对毕业设计、课程设计或个人技能提升均有实用价值。
1. 这套系统到底解决什么问题:儿童滞留报警不是“多个传感器凑一起”那么简单
夏天午后,露天车位的座舱升温比大多数人以为的快得多:外界35℃,关窗暴晒20分钟,车内温度就能逼近50℃,方向盘烫手、座椅发烫,这已经是热伤害阈值。比高温更隐蔽的是“儿童滞留”——后座儿童睡着了没被家长发现,车门锁上,等有人注意到时往往已经过了40分钟。“基于STM32的车内环境检测和儿童滞留报警系统”这类项目,核心不是把DHT22、MQ-7、人体感应模块堆到一块最小系统板上,而是要在“锁车后人还在车内”这件事上给出可靠且不误报的判定,并在确认后执行蜂鸣器、双闪、短信这类能叫来人的动作。它在毕设、智能座舱预研和小成本安全产品原型里都很常见,zip工程包能让你拿到Keil工程骨架,但真正的功夫在传感器组合逻辑和状态机设计上,这也是本篇要拆开讲清楚的地方。
2. 先把方案拆开:传感器选型、主控选型与工程骨架
2.1 环境检测传感器:DHT22、MQ-7 和 CO2 传感器的取舍
环境检测通常覆盖温度、湿度、一氧化碳和CO2四项,但每一项都有传感器型号和成本的现实约束。
| 检测项 | 常用器件 | 接口 | 关键参数 | 主要坑 | 成本量级 |
|---|---|---|---|---|---|
| 温湿度 | DHT22 | 单总线 | -40~80℃,±0.5℃ | 单总线时序严格,读取容易卡死 | 低 |
| 温湿度 | SHT30 | I2C | -40~125℃,±0.3℃ | 驱动简单但比DHT22贵十几元 | 中低 |
| 一氧化碳 | MQ-7 | ADC模拟输出 | 20~2000ppm量级 | 上电需预热,基线漂移严重 | 低 |
| VOC/CO2等效 | SGP30 / CCS811 | I2C | 400~60000ppm CO2等效 | 标定逻辑依赖环境,单机意义有限 | 中 |
我一般优先选DHT22 + MQ-7的组合,理由很实在:DHT22便宜、量程覆盖车内全年温度场景,SHT30固然稳定,但在毕设成本敏感的场景里,差价和驱动简单程度不足以让人放弃DHT22的量程优势。MQ-7是半导体加热型传感器,输出的是模拟电压,必须走ADC读,它不像电化学传感器那样出厂就有绝对精度,但用来检测“车内一氧化碳浓度较正常水平明显上升”这个相对变化比绝对浓度更有意义。对于CO2,SGP30这类MEMS传感器给的是“等效CO2”,不是真数值,在量产车里可以作为辅助证据,单独拿它做报警依据容易翻车。环境检测部分真正的任务是回答两个问题:车内热害风险有多高、有没有“人存在导致CO2/湿度变化”的旁证。
2.2 儿童滞留检测传感器:压力、PIR、微波与最终组合
儿童滞留检测比环境检测更难选型,因为“人还在车里”这件事在传感器层面没有完美解。座椅压力传感器放在坐垫下或安全座椅卡扣处,能可靠覆盖“静态人体存在”,但它不区分坐的是人还是放了一个装满衣服的背包。热释电红外PIR检测的是红外辐射变化,成年人下车后车内红外场景依然会因为烈日透过玻璃加热座椅而持续变化,空车暴晒时频繁误触发。微波雷达RCWL-0516基于多普勒效应,对微动敏感,但儿童深睡眠时呼吸引起的胸腹起伏幅度极小,信号可能落入噪声区间。视觉方案加上人脸/人体检测是最准的,但STM32F103跑不动模型,隐私问题也让量产阻力很大。
常见的可靠组合是“座椅压力作为主判据 + PIR或微波作为辅助活动信号”,判定逻辑做成:锁车后座椅压力持续存在、且没有检测到开门动作、同时环境温度或CO2出现人存在倾向的变化,才进入预警。压力传感器解决“有没有载重”,PIR或微波解决“这个载重像不像活物”,环境传感器解决“当前车内环境是否正在恶化”。这三类信号没有一个单独可信,组合起来才是一个像样的滞留检测方案。
2.3 主控与工程骨架:为什么是 STM32F103C8T6 + 标准库
项目标题直接点名STM32,最合理的主控选择是STM32F103C8T6最小系统板。理由不复杂:你搜索“stm32项目”“基于stm32的毕业设计”时,九成参考资料围绕这块芯片展开;标准库把寄存器封装得刚好能看懂底层,答辩时能讲清时钟树、ADC和TIM的配置方式;板子十几块钱,做坏了不心疼。相比HAL库,标准库的工程模板更小,程序在MDK里编译快,而且“keil5 stm32 标准工程模板”这类关键词下有大量能直接对照的配置截图,对新手友好得多。HAL库本身没有问题,但在“资源少、工期短、要能自己再改”的场景里,标准库的可阅读性优势更明显。
拿到一个现成的STM32 zip工程包,我通常会先检查三处:工程里是否配置好了时钟树(F103的外部晶振通常是8MHz,倍频到72MHz);串口1是否已经可以作为调试日志口;调试器设置是否选了ST-Link并保留SWD模式。工程文件我会这样组织:User放main和中断回调,BSP放各传感器驱动,APP放状态机和报警逻辑,SYSTEM放延时、串口、定时器基础函数。这样划分以后,传感器换了、报警方式换了,都不需要动状态机骨架。
3. 环境检测通道:从 ADC 采样到温湿度读取的落地步骤
3.1 ADC 采集 MQ-7:采样时间、软件触发与滑动平均
MQ-7输出的是模拟电压,接在STM32的ADC引脚上。F103的ADC是12位,参考电压直接取3.3V,分辨率约0.8mV/LSB,对半导体传感器来说够用。ADC的采样时间这个参数经常被忽略,它决定内部采样电容的充电时间。对MQ-7这种毫秒级慢变信号,直接选最长的239.5周期,稳定性比短采样明显好。下面这段代码按软件触发方式做10次采样取平均,足以把传感器自身噪声抹平。
/* BSP_ADC.c:PA1 复用为 ADC1_IN1,软件触发单通道 */ #include "bsp_adc.h" #define ADC_SAMPLE_NUM 10 /* 每轮采样次数 */ #define ADC_CHANNEL ADC_Channel_1 uint16_t ReadMQ7Average(void) { uint32_t sum = 0; uint16_t raw = 0; ADC_SoftwareStartConvCmd(ADC1, ENABLE); for (int i = 0; i < ADC_SAMPLE_NUM; i++) { while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); /* 等转换完成 */ raw = ADC_GetConversionValue(ADC1); sum += raw; DelayMs(2); /* 两次采样间隔,避免采到电源纹波尖峰 */ } ADC_SoftwareStartConvCmd(ADC1, DISABLE); return (uint16_t)(sum / ADC_SAMPLE_NUM); }这里有几个参数需要说明:一是ADC采样时间,初始化结构体里用ADC_SampleTime_239Cycles5,而不是默认的1.5周期,慢信号的采样更稳;二是每轮间隔用2ms,MQ-7电压变化频率远低于50Hz,没必要连续采样;三是平均窗口10次,响应延迟只有20ms,不耽误报警判定。读取结果不代表浓度,它是后续基线计算和阈值判断的原始输入。
3.2 DHT22 单总线读取:把容易卡死的时序写成有限重试
DHT22的单总线时序是这套系统里最容易卡死的地方。主机先拉低总线20ms左右唤醒传感器,然后释放,从机响应后先拉低80us再拉高80us,随后按40bit数据逐位输出。位0的高电平约26-28us,位1的高电平约50us,依靠这个脉宽差来区分。最典型的翻车是主机等待电平翻转时没有超时保护,传感器没响应,程序就永远停在while里,整块系统“假死”。下面这个读字节函数把超时限制写进了每个等待循环。
/* 读取单总线一个字节,所有等待均带超时上限 */ static uint8_t DHT22_ReadByte(void) { uint8_t data = 0; for (int i = 7; i >= 0; i--) { uint16_t t = 0; while (GPIO_ReadInputDataBit(DHT_GPIO, DHT_PIN) == 0) { if (++t > 120) return 0xFF; /* 低电平超时,返回错误 */ } t = 0; while (GPIO_ReadInputDataBit(DHT_GPIO, DHT_PIN) == 1) { if (++t > 120) return 0xFF; /* 高电平超时,返回错误 */ } data <<= 1; if (t > 45) data |= 1; /* 高电平时长超45次循环判定为位1 */ } return data; }需要说明的是这个45是一个经验临界值:位1高电平约50us,位0约28us,在72MHz主频下,普通循环一次约几微秒,取45作为分界线能在两种位宽之间留出余量。超时上限120次约等于数百微秒,足够覆盖最慢的位时序,又不会在传感器短路或未接时的死循环里无法脱身。读取整个DHT22数据帧的流程还要注意:先拉低18ms再释放,等待80us低、80us高,然后连续读5个字节。读完后校验校验位,校验失败时不要清零温度,要继续用上一次有效值,否则一个毛刺会让后续判定逻辑误以为温度骤变。
提示:换用DHT11时,位时序和温湿度换算公式都不一样,DHT11温度是整数、湿度是整数,DHT22的小数位是直接除以10。工程里如果二选一,别把两个型号的驱动混在一起改。
3.3 阈值不是拍脑袋:环境数据与报警分级的联动判据
环境检测的最终输出不是一堆原始数值,而是一个危险等级。我常用的做法是把环境数据分成三档,与滞留状态机联动,而不是独立触发报警。温度超过50℃且持续30秒以上,判定为热害风险;MQ-7的读数超过上电基线值的1.5倍并持续10秒,判定为一氧化碳异常;湿度在密闭车内持续上升且与座舱空调开启状态不符,作为“人体存在”的辅助旁证。阈值建议设成相对值而不是绝对值,MQ-7尤其如此——新车、旧车的本底电压差异很大,固定阈值几乎必误报。
报警分级可以这样约定:I级是环境预警,温度或CO单项越限,只做串口和本地LED提示;II级是滞留预警,锁车后状态机检测到座椅有人且进入确认窗口;III级是危险报警,滞留确认且环境达到I级以上,此时蜂鸣器长鸣、双闪开启并发送短信。把环境检测和滞留判定分开,是避免“车刚熄火、人还在搬东西”这种短时场景直接触发短信报警的关键。
4. 滞留判定核心:从“座椅有人”到“分级报警”的状态机
4.1 为什么单一传感器必然误报:三个真实的干扰场景
如果只靠座椅压力判断儿童滞留,副驾驶放一个装满书的书包就能触发整个报警链,小区里半夜能响一整排车。如果只靠PIR判断,烈日下车窗热辐射会不定期地让红外传感器误报。如果只靠环境温度,夏天任何一辆熄火的车在阳光下都能超过报警阈值。
最常见的是这三个真实场景:一是家长下车拿快递,手机钱包留在副驾位,座椅压力持续存在,PIR可能因为人已经离开而不触发——压力信号是“有东西”,不是“有人”;二是儿童在后座睡觉,呼吸浅、活动少,PIR检测不到微动信号,如果逻辑设计成“PIR无信号就退出报警”,就等于漏报;三是对向车道汽车发动机的余热和地面辐射在午后会让PIR透镜接收到的红外波动极不规则。所有这些都指向同一个结论:儿童滞留报警系统必须是一个多输入的状态机,而不是任何一路传感器的电平判断。
4.2 状态机与定时器节拍:1 秒中断驱动的判定流程
状态机设计成五个状态:IDLE(行驶或车门未锁)、ARMING(刚停车熄火,进入观察窗口)、CHECK(确认窗口,5分钟内持续检测)、ALERT(已确认滞留,执行报警)、CLEAR(报警已解除)。驱动节拍用TIM3产生1秒中断,每次中断给系统tick加1,状态机每秒钟跑一次。相比在while里做延时判断,定时器节拍不会阻塞串口日志和按键扫描,排查问题也方便——改一秒钟中断里的tick值,整个时间尺度就能缩放。
/* APP/state_machine.c:基于1秒定时器中断的状态机 */ typedef enum { ST_IDLE = 0, ST_ARMING, ST_CHECK, ST_ALERT, ST_CLEAR } SysState; static SysState g_state = ST_IDLE; static uint32_t g_tick = 0; /* TIM3 每秒中断 +1 */ void StateMachine_Run(void) { switch (g_state) { case ST_IDLE: /* 熄火且座椅有载重,进入驻车观察 */ if (CarPowerOff() && SeatPressed()) { g_state = ST_ARMING; g_tick = 0; } break; case ST_ARMING: if (CarPowerOn()) { g_state = ST_IDLE; break; } /* 重新上电,取消观察 */ if (g_tick >= 60) { g_state = ST_CHECK; g_tick = 0; } /* 60秒后进入确认窗口 */ break; case ST_CHECK: if (!SeatPressed()) { g_state = ST_IDLE; break; } /* 载重消失,人已离开 */ if (g_tick >= 300) { g_state = ST_ALERT; StartAlarm(); /* 5分钟仍有人 */ } break; case ST_ALERT: if (DoorOpened() || SeatReleased()) { StopAlarm(); g_state = ST_CLEAR; /* 有人开门或起身 */ } break; case ST_CLEAR: if (CarPowerOn()) g_state = ST_IDLE; /* 下次通电前复位 */ break; default: g_state = ST_IDLE; break; } }两个时间参数值得细说。ARMING窗口设60秒,用来过滤“车主熄火后还在车里玩手机”这种短时停留;CHECK窗口设300秒,给儿童翻身、换姿势等动作留出余量,同时也避免短时停车拿东西触发短信。SeatPressed是压力传感器经过5次连续读值去抖动后的结果,DoorOpened读取门碰开关或检测到12V电源从ON切到OFF的信号。PIR或微波在这个状态机里不是必选条件,而是作为附加特征记录到日志里:在确认窗口内如果检测到微动,说明载重是活体,可以缩短进入ALERT的等待时间;反过来,PIR没有任何输出也不能排除孩子正在深度睡眠,所以不能拿它当“非有人”的证据。
4.3 报警执行链:蜂鸣器、双闪与短信推送的并行动作
报警动作要分级并行。蜂鸣器用TIM定时器的PWM通道输出,频率调到2.5kHz,人耳在车内噪音背景下最容易留意到。双闪直接通过两个GPIO控制继电器通断,频率1Hz,占空比50%。短信通知是救援链路里最关键的远程通道,用SIM800C或4G Cat-1模块挂在串口2上,通过AT指令发送。常见做法是文本模式下直接发送:
/* BSP_GSM.c:通过 USART2 与 SIM800C 通信,发送报警短信 */ void SendAlertSMS(void) { UART2_SendString("AT+CMGF=1\r\n"); /* 设置为文本模式 */ DelayMs(300); UART2_SendString("AT+CMGS=\"13800000000\"\r\n"); DelayMs(300); UART2_SendString("ALERT:child in car,temp=42C\r\n"); UART2_SendString("\x1A"); /* 0x1A 是短信结束符 */ }这段代码有两点要说明。第一,每个AT指令之间必须延时,SIM800C处理一条指令通常需要几百毫秒,连续发指令会把模块内部的命令队列塞爆,收不到“OK”却继续发下一条。第二,短信内容里尽量不要带中文,模块字库兼容性问题在SIM800C老固件上很常见,英文报警文本最能保证送达。真正量产时,报警动作还要写进Flash保存一个标志位,车载电源断电后重新上电,系统读到这个标志会再次执行30秒报警,防止“报警过程中整车断电、第二天重新上电时漏报”的极端情况。
5. 联调避坑:5个让这套系统“翻车”的高频问题
5.1 DHT22读取卡死:主循环被单总线等待拖停
现象:上电后串口日志停在“DHT init”这一行之后再无输出,用ST-Link调试发现程序跳进了某个while循环。
原因:主机发完20ms低电平触发信号后,DHT22从机没有按要求响应,代码在等待电平翻转的while里死等。传感器未接、接错引脚、供电不足都会导致从机无响应,而早期版本的驱动通常没有超时保护。
解决:所有等待电平翻转的循环都加上超时计数,读字节函数返回0xFF表示失败,在读取流程里连续失败3次就放弃本轮,温度湿度沿用上一次有效值。同时把DHT22的供电从3.3V换到5V,长线场景下3.3V供电的上升沿过缓会让时序判断错位。
5.2 MQ-7 读数乱跳:开机半小时内一氧化碳“爆表”
现象:刚上电时ADC读到的电压忽高忽低,从几十mV一路爬到接近满量程,或者某个瞬间跳变几千个LSB。
原因:MQ-7的加热电阻需要预热,冷态电阻与稳定工作电阻差很多,绝对电压在上电半小时内根本没有参考意义。另外,车内的12V降压模块纹波会耦合进模拟地,ADC采样值会叠加电源噪声。
解决:上电后先连续读60秒平均值作为基线,后续所有判断都基于“当前值/基线值”的相对偏移;ADC采样时间设置为239.5周期,采样前加一个小的软件延时避开电源开关瞬间;传感器供电用单独的LDO+钽电容滤波,别直接从最小系统板的3.3V引脚取电。
5.3 暴晒下的 PIR 乱报:热辐射穿透镜片
现象:空无一人的车里,正午暴晒时PIR每隔几分钟输出一次运动信号,状态机日志里活动标志频繁翻转。
原因:PIR检测的是目标区域红外辐射的变化,暴晒下车窗和座椅表面的热辐射分布不均匀,气流和玻璃温度波动会让透镜视野内的辐射不断变化,传感器把它解读为“有人活动”。
解决:把PIR透镜朝下倾斜安装,加遮光罩避免直射光进入;灵敏度跳线调到最低挡;更关键的是在判定逻辑里把PIR定位成“辅助活动信号”,只用来缩短确认窗口,不参与“是否有人”的否决判断。
5.4 报警进入死循环:触发后没有任何路径退出
现象:坐在后座测试,系统正常进入ALERT状态,人下车关上车门后,报警仍然持续响,直到断电复位。
原因:状态机的ALERT分支里只写了进入报警的动作,没有写退出条件,或者退出条件依赖的压力传感器正好被安全带卡扣压住,释放不彻底。
解决:在设计阶段就要求ALERT状态必须支持至少两条退出路径:检测到车门打开、座椅压力消失、收到短信回复指定数字、按下手套箱内的解除按键,四条任选其二。实测时优先用“座椅压力释放”验证,这条路径能顺带检验压力传感器的安装位置是否正确。
5.5 Keil 下载失败:JTAG 被禁用后 SWD 也被关死
现象:第一次烧录程序后,MDK再点下载报Error: Flash Download failed - Target DLL has been cancelled,ST-Link在Utility里也连不上芯片。
原因:代码中用了GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE)关闭了所有调试口,程序烧进Flash后SWD引脚被释放给普通GPIO,调试器第二次就找不到目标了。
解决:只禁用JTAG、保留SWD,使用GPIO_Remap_SWJ_JTAGDisable。如果已经关死,用ST-Link Utility在按住复位键的同时点击connect,趁芯片运行被暂停的窗口擦除整个Flash;Windows下的驱动问题优先卸掉第三方串口驱动,装回ST-Link官方驱动。这个坑在“stm32禁用jtag”相关提问里出现频率极高,每次都有人不信邪硬试。
6. 验证方法:用 OFF→ON 驻车模拟测一下误报率
6.1 场景清单与“误报/漏报”两个指标
验证这套系统不能只做功能测试,要按场景分类记录两种结果:误报(车内没人却报警)和漏报(车内有人却没报警)。我常用的最小场景集有五个:空车暴晒60分钟,应当不报警;副驾放背包锁车30分钟,应当不报警;后座真人在睡眠状态锁车6分钟,应当触发ALERT并发出短信;停车熄火后司机在驾驶座停留4分钟,应当只停留在ARMING状态;报警触发后开门离开,应当30秒内解除警报。
每次测试都要保留传感器事件日志,不然遇到间歇性误报就只能靠“复现”这个玄学手段排查。串口日志格式定成一行一个事件帧:
timestamp|state|temp|hum|co_ratio|seat|motion|event 1720000000|CHECK|42.1|58.2|1.32|1|0|enter_check6.2 用一段小脚本统计误报率和漏报率
测试完成后把上位机保存的CSV日志丢给小脚本,做一个简单的按状态过滤统计。用Python最直接:
import sys total_exposure = 0 false_alarm = 0 miss_alarm = 0 for line in open(sys.argv[1]): parts = line.strip().split("|") if len(parts) < 8: continue state, event = parts[1], parts[7] if event == "enter_check": total_exposure += 1 elif event == "alarm_trigger": if state == "ALERT" and total_exposure: false_alarm += 1 print(f"误报: {false_alarm} 次, 正确进入ALERT: {total_exposure} 次")这个脚本里,enter_check事件对应状态机进入确认窗口,代表系统认为“可能有人滞留”;alarm_trigger事件对应实际触发报警。真实场景回放时,把所有进入CHECK但最终没有触发ALERT的记录人工核对一遍,就能定位是压力阈值过高还是PIR干扰拖慢了确认流程。我现在的习惯是每轮测试都保存日志,没有日志辅助回放的改动不算是验证过——毕竟这系统关涉安全,宁可多跑两轮,也别拿侥幸当结论。希望帮到你。
本文还有配套的精品资源,点击获取