每年一到课程设计或毕业设计季,总有人抽到“51单片机公交车人数统计及防火系统”这个题目。这题看似常规,实际上手就知道,它是个典型的“麻雀虽小、五脏俱全”项目:既要处理红外对射传感器在车门处的双向人员计数,又要兼顾烟雾、火焰、温度三路防火信号的采集与联动报警,还得让所有逻辑在一颗51内核芯片上稳定跑起来。我当时做这个题目时没少折腾,从传感器选型到状态机逻辑再到供电纹波问题,来回改了好几版。这篇文章就把整个系统的硬件配置、计数状态机设计、防火联动逻辑、以及我实测中踩过的坑完整梳理一遍,给准备动手的朋友一份可以直接参考的路线。
1. 硬件方案先想清楚:一套用51做车载监测的合理配置
很多同学拿到题第一件事就是开Keil写代码,这是最容易走弯路的地方。硬件方案没定清楚,后面所有联调都是在给自己挖坑。我先讲清楚我最终确定的配置,再解释每一处选型背后的理由。
1.1 51平台在这个题目里为什么仍然能打
先别急着质疑“都什么年代了还用51”。这个题目的传感器交互逻辑其实很简洁:人数统计只需要两路数字电平输入(四个门就是四路),防火部分需要一路烟雾浓度模拟量、一路火焰数字量、一路温度单总线,输出侧是LCD显示、蜂鸣器、继电器。拢共就这些外设,传统STC89C52的IO资源完全够用,主频12MHz处理这几个传感器的扫描绰绰有余。
当然51有个先天短板——内部没有ADC模块。如果你坚持用最传统的STC89C52,烟雾传感器的模拟量输出就得外挂一颗ADC0832或ADC0809。我的建议是直接换用STC12C5A60S2,它的管脚兼容传统51,内部自带10位ADC,程序上改动不大,却能省掉一颗外部ADC芯片和一堆跳线。严格来说它还属于51内核,题目认这个平台,你也少遭罪。
还有一点容易被忽略:51的IO驱动能力有限,所有外部模块必须保证信号电平匹配,不能指望单片机的IO直接带动继电器这类电磁器件。后面第3章会专门讲继电器的驱动方式。
1.2 人数统计传感器选型:红外对射、踏垫、视觉方案对比
人数统计这个功能,市场上能用的方案大致有三种,我列个表对比一下:
| 方案类型 | 代表器件 | 单门成本 | 51平台适配度 | 主要问题 |
|---|---|---|---|---|
| 红外对射/漫反射 | E18-D80NK光电开关 | 约10-20元/个 | 高,直接输出TTL电平 | 强光下可能误触发 |
| 踏垫压力阵列 | 压敏薄膜传感器 | 较高 | 低,需多路ADC采集 | 踩踏寿命短、信号重叠 |
| 视觉识别 | OpenMV、摄像头 | 高 | 低,51算力严重不足 | 需额外主控,开发量大 |
我最终选了红外方案,具体用的是E18-D80NK漫反射式光电开关。原因很直接:它自带三线接口(棕色正极、蓝色负极、黑色信号输出),输出就是数字电平,遮挡时电平跳变,51的IO口直接读就行,不需要任何信号调理电路。E18-D80NK本身是漫反射型,检测距离可以通过尾部电位器调到5-80厘米,装车门框两侧做“伪对射”效果也行。
如果你那边的环境光干扰特别严重,可以考虑换成对射式光电开关,发射和接收分体安装在门框两侧。对射式抗光干扰能力更强,但安装时要两侧走线,工作量大一点。课程设计场景下漫反射式基本够用。
1.3 防火传感器的组合与整车供电规划
防火部分我用的是三件套:MQ-2烟雾传感器模块、火焰传感器模块、DS18B20温度传感器。MQ-2负责检测可燃气体和烟雾浓度,火焰传感器负责识别明火红外光谱,DS18B20负责测量环境温度。这三路信号融合判断,远比单一传感器可靠。
供电是整个项目里最容易被新手低估的环节。MQ-2内部有一个加热电阻,工作时电流能到150-200mA,这个电流绝对不能从单片机IO口取,必须直接从5V电源轨供电。公交车实车环境是24V铅酸电池组,所以系统前端需要一块LM2596降压模块把24V降到5V。我当时的供电拓扑是:24V进LM2596,输出5V母线,5V母线再分三路——单片机系统一路、传感器一路、继电器单独一路。三路在电源入口处汇合,但走线分开,避免继电器动作时的大电流波动传导到单片机。
显示屏我推荐LCD1602的I2C版本,也就是板载PCF8574芯片的那种,只占用两根IO线。用传统并口LCD1602也不是不行,但要占8根数据线加3根控制线,且对比度电位器没调好就直接花屏,调试成本高不少。
2. 人数统计模块:双向判定状态机的设计与抗干扰处理
人数统计的核心难点不是“检测到有人”,而是“判断这个人是上车还是下车”。用单个传感器只能知道有人经过,方向必须靠两个传感器组合判定,这就要设计一套状态机逻辑。
2.1 车门传感器安装布局的关键尺寸
两个传感器装在哪里、装多高、间隔多远,直接决定计数准确率。我反复试验后确定了一套在实验室模拟环境中表现稳定的布局:以内门为例,传感器A装在门框内侧(车厢内部方向),传感器B装在门框外侧(车门台阶方向),两者水平间距约30-40厘米。这个间距要保证一个人正常通过时两个传感器会依次触发,但又不能大到让两个人同时处于检测区。
安装高度我取在离地1米左右,这个位置对应成年人腰部。装低了容易被小孩或蹲下的乘客挡住检测路径导致漏计,装高了又可能漏掉身材较矮的乘客。传感器探测方向倾斜向下约15度安装,主要是为了减少来自车窗方向的环境光干扰,实测这个角度对阳光直射有一定抑制作用。
2.2 双传感器方向判定状态机的实现
状态机逻辑其实不复杂,核心思想是“先触发外侧再触发内侧,判为上车;先触发内侧再触发外侧,判为下车”。我画了一张状态转移表方便理解:
| 当前状态 | 触发事件 | 动作 |
|---|---|---|
| 空闲 | 外侧传感器被遮挡 | 进入“外侧先触发”状态 |
| 空闲 | 内侧传感器被遮挡 | 进入“内侧先触发”状态 |
| 外侧先触发 | 内侧传感器被遮挡 | 人数加1,回到空闲 |
| 外侧先触发 | 外侧传感器恢复 | 视为干扰,回到空闲 |
| 内侧先触发 | 外侧传感器被遮挡 | 人数减1,回到空闲 |
| 内侧先触发 | 内侧传感器恢复 | 视为干扰,回到空闲 |
这里有个细节:内外传感器的信号有效电平取决于你手上的模块型号。E18-D80NK的黑色信号线在无遮挡时输出高电平,遮挡时拉低,所以代码里判断的是“低电平等于遮挡”。如果你的模块是常开型,逻辑反过来写就行。我在代码里刻意用!outer和!inner表示遮挡,就是为了让后续的时序逻辑不依赖具体电平极性。
2.3 上下车混行与瞬时遮挡的处理方案
实验室环境下状态机很好用,一到模拟人流场景就暴露问题。最常见的是两种情况。
第一种是乘客伸手调整背包带或刷卡的动作触发了外侧传感器,但没有继续进入车厢,此时如果只按状态机判断,会一直停在“外侧先触发”状态。我的处理是加一个时间窗口:外侧触发后如果在500毫秒内内侧没有触发,状态就自动复位。用定时器实现,每次进入“外侧先触发”状态时清零计时,主循环里检查超时标志。
第二种是前门上车、后门下车同时进行。这种情况我建议两个门独立跑同一套状态机,各计各的。由于上下车的人流在物理上是分开的,两个门的传感器信号不会串扰,人数增量等于前门上车数减去后门下车数。注意需要在程序里定义“前门上客数、后门下客数、当前车上人数”三个变量,显示时取当前车上人数。
乘客“先下后上”的顺序也很重要。如果设计逻辑本身不支持双向,只靠单个传感器累计,那车门一开全是下车的人,计数直接就乱了。双向状态机是我认为这个题目里最值得花时间打磨的部分,你把这套逻辑吃透,面试时被问“这个系统怎么防误判”也能答得有条理。
3. 防火模块:烟雾阈值、火焰识别与联动动作的关系设计
防火部分看着简单,实际上坑全在“怎么不误报”上。如果只是传感器数值超了阈值就疯狂报警,那这系统在真实车厢里根本没法用——乘客抽烟、夏天暴晒、发动机尾气串入,都足以触发误报。
3.1 MQ-2的两种输出方式与基线校准
MQ-2模块上通常有两个输出引脚:数字量输出DO和模拟量输出AO,对应板载LM393比较器的两路处理结果。DO引脚通过电位器设定阈值,超过就输出电平跳变,可以直接接51的IO口,适合只做“报警/不报警”二元判断。AO引脚输出0-5V模拟电压,需要接ADC才能读。
我建议在你演示答辩时同时用好两路:用AO做浓度趋势显示,用DO做阈值硬报警。STC12C5A60S2内部自带ADC,AO接ADC输入引脚即可读到0-1023的数值。如果你用的是传统89C52,就外接ADC0832,串行通信只占用三根IO线,比并行的ADC0809实用得多。
MQ-2最让人头疼的问题是上电初期的读数漂移。传感器内部加热丝冷态电阻和正常工作时差异巨大,刚上电的输出电压会明显偏高,过几分钟才稳定。如果系统一上电就立即比较阈值,必然出现“开机即报警”的尴尬场面。我用的方案是启动时延时约5分钟预热,然后连续读取10秒的ADC值取平均值,作为环境基线。后续报警阈值用“基线+固定增量”表示,而不是写死的绝对值。这样不同教室、不同天气下,阈值都能自动适应环境本底浓度。
3.2 火焰传感器在车厢场景中的误报源分析
火焰传感器模块本质上是一颗对红外光谱敏感的光电二极管加上比较器电路。它确实能识别打火机火焰、蜡烛火焰,但它同样能识别太阳光和白炽灯里的红外分量。这在公交车场景里是致命的:夏天阳光穿过车窗玻璃直接照在传感器上,红外分量非常强,必然误报。
我的处理思路有两个方向。硬件上,给火焰传感器加了一个遮光套筒——用黑色热缩管或者3D打印一个长3-4厘米的圆筒,套在传感器感光元件前方,把接收视角限制在约30度以内,只让它看到正前方的火源,屏蔽侧面和上方的杂散光。软件上,火焰信号不作为独立报警条件,而是作为融合判断的一路。
至于灵敏度电位器,我调试时反复对比了不同位置的响应:取一个打火机火焰,放在传感器正前方30厘米处能稳定触发;把白炽灯放在同样位置,不能触发。按这个标准调电位器,基本可以避免大部分常规光源误报。
3.3 三级报警策略与继电器的正确驱动
报警功能我是按三级来设计的,每一级对应不同的联动动作:
- 预警级:DS18B20读数超过60摄氏度,或烟雾浓度超过“基线+阈值增量的50%”,点亮警示LED灯,LCD显示“SMOKE RISK”。
- 报警级:烟雾浓度超过完整阈值,且火焰传感器有效,此时蜂鸣器以1Hz频率断续鸣叫,LCD切换到报警界面并显示当前人数。
- 联动级:连续确认报警状态超过2秒钟,继电器吸合,模拟切断车厢危险电路或启动灭火装置(演示时我接了一个小风扇代表自动灭火设备)。
三级策略的好处是可以避免单个传感器的瞬时扰动直接触发最重量级的联动。尤其要强调“连续确认超过2秒”这个设计——继电器一旦动作就要涉及后续状态复位,必须给它留足判断空间。
继电器驱动必须单独拿出来说。51单片机的IO口灌电流极限约20mA,而常见5V继电器线圈需要50-80mA,直接接上去轻则驱动不动,重则烧IO口。我的接法是:51的IO口输出接一个S8050三极管的基极,基极串联1k电阻限流,发射极接地,继电器线圈接在5V电源和集电极之间。脉宽调制或普通高电平输出都可以驱动。最关键的一步是:继电器线圈两端必须反向并联一个1N4007续流二极管。没有这个二极管,继电器断电瞬间线圈产生的反向感应电动势能轻松击穿三极管和单片机IO口。我第一次焊板子时漏了这个二极管,调试时连续烧掉两个三极管,后来查资料才意识到是反电动势的问题。
4. 联调实测中遇到的五个典型问题以及处理办法
系统分模块单独调试时都看似正常,一旦组合起来跑,各种稀奇古怪的故障就冒出来了。这里挑五个我实际遇到过、且能复现的问题,把排查过程和最终处理办法交代清楚,这些内容比功能代码本身更值钱。
4.1 前门后门交替触发导致人数反向跳动
现象是前门计数正常,后门只要有人上下,前门显示的人数就会跳变。我一开始以为是状态机逻辑错乱,反复检查代码没发现问题,后来用万用表同时量两个门的传感器信号线,才发现是地线问题。前后门传感器虽然都接开发板,但后门传感器的地线路径过长,且和前门传感器共用了同一条细地线,后门触发时地线上的电位波动通过共用地线串到了前门信号线上。
处理办法是重新整理线路,所有传感器采用星型接地,每个传感器单独铺地线到电源地汇合点,而不是串联式共地。同时信号线加10k上拉电阻到VCC,增强抗干扰能力。这个问题的本质是电源回流路径设计不当,电路板布局时就要避免“一根地线走到底”。
4.2 MQ-2预热期的开机空报
实机测试刚通电,LCD还没初始化完就触发烟雾报警,这问题在第3章提过。我当时的临时处理是在初始化函数里做了一个大约3分钟的延时,等待MQ-2加热丝稳定后再进入主循环。后来觉得3分钟干等着太蠢,改成了动态基线校准:系统启动先延时5分钟,再采样取基线。这个方案验证下来比较稳,但缺点是不能一上电就立即响应真实火情。
如果做产品化,建议改成开机快速预热状态,用较小电流对MQ-2加热丝进行快速升温,30秒内稳定工作;课程设计阶段就不必折腾这个了,5分钟预热完全可以接受。
4.3 继电器吸合瞬间单片机整机复位
这是最让人崩溃的故障:程序每次执行联动动作,LCD就“嘀”地重启。排查时发现,继电器吸合瞬间线圈电流从0跳到几十毫安,导致5V电源轨电压瞬间跌落几百毫伏,单片机供电不足直接复位。
处理办法从两个层面解决:硬件上,继电器独立供电回路,并在继电器电源端并联一个470uF电解电容吸收瞬态电流冲击;软件上,继电器动作前延时几十毫秒,避开LCD刷新和传感器采样的敏感时刻。联动逻辑里我也会先关闭LCD背光再驱动继电器,动作完成后再恢复,实测能显著降低复位概率。
4.4 LCD1602满屏乱码块
LCD显示出来全是方块“豆腐块”,调对比度电位器也没效果。这个问题十有八九是初始化时序不对。1602液晶要求上电后等待一段时间再发初始化命令,而且命令之间的延时不能太短。我的代码在LCD初始化之前先delay_ms(100),每个初始化命令之间再插delay_ms(5),之后就再也没出现过满屏乱码。
另外一个容易忽视的点:I2C版的LCD1602如果接线过长或未接上拉电阻,也可能出现随机乱码。PCF8574模块上一般自带上拉,但最好在SDA和SCL线上各接一个4.7k上拉电阻到VCC,确保信号质量。
4.5 仿真器调试正常、实机跑飞的问题
仿真环境供电来自USB的5V,稳定得很;实机供电是LM2596降压,纹波明显偏大。我的代码在仿真器上怎么跑都没事,一接到实机就不定时跳飞或卡死。排查发现LM2596输出端只接了一个220uF电容,纹波还是超标,我在输出端并了一个1000uF电解电容和一个0.1uF陶瓷电容,同时把反馈采样走线尽量靠近输出端而不是靠近电感,问题就消失了。
| 故障现象 | 根因 | 处理办法 |
|---|---|---|
| 前后门计数互相串扰 | 共用地线回流路径不良 | 星型接地,信号线加上拉 |
| 开机即报烟雾警 | MQ-2预热漂移 | 延时5分钟+动态基线校准 |
| 继电器动作时单片机复位 | 大电流导致电源跌落 | 继电器独立供电,并联470uF电容 |
| LCD满屏乱码 | 初始化时序不符 | 上电延时100ms,命令间隔5ms |
| 实机运行跑飞 | 降压模块纹波过大 | 输出端加大小电容组合 |
5. 核心代码逻辑参考:计数状态机、浓度处理与主循环骨架
核心代码我单独列一节,方便你直接对照着改。以下代码基于STC89C52/STC12C5A60S2编写,传感器引脚按P3口定义,实际使用时按你的接线调整即可。
5.1 双向计数状态机代码
#include <reg51.h> sbit OuterSensor = P3^2; // 外侧传感器,遮挡为0 sbit InnerSensor = P3^3; // 内侧传感器,遮挡为0 #define IDLE 0 #define OUTER_FIRST 1 #define INNER_FIRST 2 unsigned char door_state = IDLE; unsigned int passenger_count = 0; unsigned int timeout_cnt = 0; void door_scan(void) { bit outer = OuterSensor; bit inner = InnerSensor; switch (door_state) { case IDLE: if (outer == 0) { door_state = OUTER_FIRST; timeout_cnt = 0; } else if (inner == 0) { door_state = INNER_FIRST; timeout_cnt = 0; } break; case OUTER_FIRST: if (inner == 0) { // 内侧接着触发:从外到内,上车 passenger_count++; door_state = IDLE; } else if (outer == 1) { // 外侧恢复且内侧未触发:视为干扰 door_state = IDLE; } else { if (++timeout_cnt > 50) door_state = IDLE; // 约500ms复位 } break; case INNER_FIRST: if (outer == 0) { // 外侧接着触发:从内到外,下车 if (passenger_count > 0) passenger_count--; door_state = IDLE; } else if (inner == 1) { door_state = IDLE; } else { if (++timeout_cnt > 50) door_state = IDLE; } break; } }代码逻辑和状态转移表完全一致。需要注意timeout_cnt是主循环累加的计数,主循环执行周期约10ms时,50次对应约500ms。实际调节这个值即可改变“等待确认”的时间窗口。
5.2 烟雾浓度处理与报警阈值判断
这里给出采用STC12C5A60S2内部ADC的简化逻辑。如果你用传统89C52外接ADC0832,只需把read_smoke()函数替换为对应的SPI读取函数即可。
unsigned int smoke_baseline = 0; unsigned int read_smoke(void) { // 读取ADC模块0通道,返回0-1023数值,不同芯片寄存器名不同 // 以STC12C5A60S2为例,配置ADC_CONTR后读取ADC_RES和ADC_RESL return ADC_value; } void calibrate_smoke_baseline(void) { unsigned long sum = 0; unsigned char i; for (i = 0; i < 10; i++) { sum += read_smoke(); delay_ms(100); } smoke_baseline = sum / 10; } unsigned char check_smoke_alarm(void) { unsigned int smoke_now = read_smoke(); if (smoke_now > smoke_baseline + 200) return 1; // 阈值增量按实测调整 return 0; }我调试时用的阈值增量是200,对应ADC 0-1023的约20%。不同环境的烟雾本底不一样,这个值需要现场标定。你可以做一个简单实验:正常环境下读一个基线值,然后用打火机在传感器旁边保持2秒不点燃(只出丁烷气体),观察读数变化,把报警阈值设在这个实验读数的80%左右比较稳妥。
5.3 主循环调度与显示刷新骨架
主循环结构我用了一组软件定时器标志位来分时调度。所有传感器扫描在主循环里轮询,LCD刷新通过计数标志位控制在500毫秒一次,避免高频刷新导致LCD闪烁和干扰传感器采样。
void main(void) { unsigned char tick_500ms = 0; system_init(); lcd1602_init(); delay_ms(300); calibrate_smoke_baseline(); // 预热后校准,或者开机延时5分钟后再调用 while (1) { door_scan(); check_fire_condition(); // 500ms刷新一次显示 if (++tick_500ms >= 50) { tick_500ms = 0; lcd_display_count(passenger_count); lcd_display_smoke(read_smoke()); } delay_ms(10); } } void check_fire_condition(void) { unsigned char smoke_alarm = check_smoke_alarm(); unsigned char flame_alarm = read_flame_sensor(); unsigned char temp_alarm = read_temperature() > 60; if (smoke_alarm && flame_alarm && temp_alarm) { fire_action_alarm(); // 三级报警:继电器、蜂鸣器联动 } else if (smoke_alarm && !flame_alarm) { led_warn_on(); // 预警级:亮灯不联动 } else { all_reset(); // 复位报警输出 } }我把防火判断写成了“烟雾+火焰+温度”三者都满足才触发联动级。严格来说,这种方法在真实火情中会稍微拉长响应时间,但对课程设计场景足够了——你要展示的是“防误报的工程化思维”,而不是一个动不动就炸锅的玩具。
至于蜂鸣器的断续鸣叫,我用定时器中断翻转来实现:定时器每100ms翻转一次蜂鸣器IO,听到的就是1Hz“嘀-嘀-嘀”的节奏。中断里不要做任何耗时操作,只做标志位置位。
这套代码跑通后,我再额外做了一件事:把“当前人数”和“报警状态”通过串口打印到上位机,方便演示时在电脑上也看到数据。这个功能对答辩特别有用,评委能直观看到系统在实时工作。串口初始化代码我就不贴了,网上大把参考,主体逻辑还是以上这些。
最后说点实在的。这个项目真正考察的不是某个高深算法,而是你能不能把一堆传感器可靠地组织在一个系统里。状态机设计、时间窗口防误判、MQ-2的基线校准、继电器续流二极管、电源纹波滤波,这些点每一个单独拎出来都很基础,但组合在一起就是工程能力的体现。我建议你动手前先把硬件供电和信号接线画清楚,再写代码;联调时务必一步步来,每个传感器先单独验证,最后再整合主循环。等你把这套系统从头到尾跑通,再回头看,会觉得整个流程里最有价值的不是代码本身,而是你排查问题的思路和处理细节的能力。如果调试中遇到问题,欢迎在评论区留言,看到都会尽量回复。