基于51单片机与Proteus的红外人数计数器设计
2026/9/16 9:59:24 网站建设 项目流程

简介:基于51单片机与Proteus仿真实现的楼道人数计数器设计方案,面向电子、自动化、计算机等专业学生及单片机初学者。方案以AT89C51为主控制器,用外部中断响应进入/离开信号,实现0~999人数统计,并通过数码管实时显示,覆盖硬件连接、C语言编程、仿真调试与论文撰写等完整环节。资源包共11个文件,大小约153KB,包含Proteus仿真图、KEILC51工程、C语言源码、hex烧录文件、设计说明文档及效果预览图,便于直接打开仿真、阅读程序、烧录验证或用于报告编写。目前已有94人浏览/学习,可作为同类计数项目的参考。资料提供可编译的完整源码和可运行仿真图,可帮助理解外部中断响应、红外/光电传感器信号处理、数码管动态刷新以及0~999溢出处理等关键设计;配套论文清晰梳理了从需求分析到软硬件实现的思路,适合作为课程设计、毕业设计或技能实训的借鉴模板。

1. 楼道人数计数器看着是“数数”,难点其实是“进出方向”

楼道人数计数器这个题目,表面上是统计“过了几个人”,但只装一个传感器数出来只是脉冲个数,不知道这人是上楼还是下楼。对楼道、宿舍、小型办公楼来说,真正有价值的是“当前楼层还剩多少人”这个净数,它决定了要不要开照明、要不要触发人员超限报警。所以项目会用两路红外对管并排安装在门框两侧,用先遮挡A再遮挡B判定“进”,反过来判定“出”。这套逻辑用51单片机加Proteus仿真来做非常合适:仿真图里不需要真实红外光路,用两个按键模拟遮挡,代码里用外部中断加状态机就能把出入判别、人数显示、上限报警全部跑通;最后把仿真图、源代码、论文三个交付物对应起来,是课程设计和毕业设计最常见的完整度。接下来从硬件电路一步步说到联调排错。

2. 在Proteus里搭楼道人数计数器硬件:AT89C51、双红外对管与LCD1602

2.1 为什么用双红外对管而不是单个传感器

先明确一个问题:单路红外对管输出的是“有没有人经过”的电平脉冲,它没有顺序信息。双红外对管的意义是形成A、B两个通道,安装时A靠近门口外侧、B靠近里侧。人进门,先挡外侧再挡里侧,于是INT0先产生下降沿;人出门则相反。两路信号到达的先后次序就被编码成了方向,这是整个项目的基础。

真实硬件中,红外发射管和一体化接收头之间存在光路,遮挡时接收头输出电平变化。由于环境光会引入干扰,接收头输出通常先送LM393比较器整形,再进单片机引脚。Proteus仿真没有光路模型,所以我一般直接用两个按键接在P3.2和P3.3上,按下代表遮挡、松开代表恢复。这样省去传感器建模,又保留了“边沿事件”这个核心逻辑。

2.2 仿真图元件清单与Proteus搜索关键字

搭建仿真图之前先确认元件。下面是常用的Proteus元件清单,名称和仿真库里的搜索关键字一致:

元件Proteus库关键字参数与接法作用
单片机AT89C51晶振12MHz,EA接VCC主控,处理中断与计数
液晶LM016LD0-D7接P0,RS/RW/E接P2.0-P2.2显示当前人数与进出累计
按键BUTTONA接P3.2,B接P3.3,另一端接地模拟红外被人体遮挡
排阻RESPACK-88个10k电阻,公共端接VCC给P0口数据线提供上拉
电阻RES10k,接按键到VCC外部上拉,防引脚浮动
晶振电容CAP22pF,接X1、X2到地配合12MHz晶振起振
复位电容CAP-ELEC10uF,RST接电源正极上电复位
蜂鸣器SOUNDERP2.7经NPN三极管驱动人数达到上限报警

提示:Proteus里直接搜LCD1602往往搜不到,常见模型名是LM016L,功能与LCD1602完全一致。

P0口是51单片机里唯一没有内部上拉的口,驱动LCD数据总线时必须加RESPACK-8。漏掉排阻的典型现象是LCD有显示但字符乱码、对比度异常。EA引脚必须接VCC,否则单片机默认从外部程序存储器取指令,Proteus里仿真会直接停在原地。

2.3 仿真图接线顺序与“总线画法”的取舍

Proteus仿真图搭建步骤我一般这样走:先放AT89C51和晶振复位电路,再放LCD和排阻,最后接两个按键。晶振用X1、X2接12MHz,各带一个22pF电容到地;RST引脚接10uF电容到VCC、10k电阻到地,这是标准的RC上电复位电路,仿真和实物都靠它让程序从头执行。

P0到LCD这8根数据线是最容易把原理图画乱的部分。Proteus里有总线工具,但很多情况下用网络标号比总线更清楚。我通常的做法是给P0.0-P0.7打上D0-D7标号,LCD一侧同名引用,仿真图看上去就是整齐的9根短接线加8个标签。注意“给单片机添加引脚”这个说法在Proteus里不准确,放置器件时引脚已经带好,你要做的是用网络标号连接两个引脚,而不是在原理图库编辑器里造一个新的元器件封装。

两个按键分别接到P3.2和P3.3,另一端接地。P3口内部有上拉,仿真里直接这样接就能触发下降沿;如果速度调慢后仍然偶发误触发,给按键加一个10k外部上拉电阻会更稳。B通道与A通道的间距在仿真图里体现为两个按键的位置,真实项目里这个间距取40到60cm,太近会导致两人并排时两路同时被遮挡,状态机认不出方向。

2.4 按键信号如何映射成“人体遮挡”

仿真中一个完整的进门动作是:先按下A按键并保持,再按下B按键并保持,然后依次松开。重点在于下降沿顺序是先A后B。代码里只关心按下瞬间,所以松开顺序不影响计数,这个细节和真实红外对管的行为一致:人体通过时先挡A、后挡B,A先恢复、B后恢复,但判断在B挡上的时刻已经完成。

这一节写进论文就是“传感器信号分析与选型”,元件清单和参数表可以直接作为硬件设计的依据。

3. 中断、防抖与方向状态机:人数计数器核心源代码

3.1 为什么外部中断,以及为什么必须是下降沿触发

A、B通道分别接P3.2和P3.3,这两个引脚恰好是51单片机的INT0和INT1外部中断入口。选外部中断而不是主循环轮询的原因很直接:LCD刷新、蜂鸣器检测、按键扫描都会让主循环变慢,人在门前正常走过,遮挡时间大约100到200毫秒,轮询很可能漏掉几十毫秒的窄脉冲;外部中断能在引脚出现下降沿时立刻置一个事件标志,处理节奏完全由传感器驱动。

触发方式要设为下降沿触发,写寄存器是IT0 = 1; IT1 = 1;,对应TCON位设置为1表示下降沿触发,置0表示低电平触发。低电平触发在这里不能用的原因是:遮挡期间电平持续为低,单片机会反复进入中断,造成一次通过被记成好几次。下降沿只在电平从高变低的那一瞬间产生,天然匹配“遮挡”事件。

3.2 方向状态机:先A后B进,先B后A出

状态机是整个计数的关键。只有A被遮挡或只有B被遮挡时,不急着计数,而是先记成“先见到哪个通道”。当第二路下降沿到来时,比对顺序然后一次性完成计数。为了避免人走到一半停在门口导致状态残留,还需要一个超时复位。

当前状态发生事件新状态与动作
0(空闲)A下降沿置状态1,记“先见A”
0(空闲)B下降沿置状态2,记“先见B”
1(先见A)B下降沿cur加1,inTotal加1,状态归0
2(先见B)A下降沿cur减1,outTotal加1,状态归0
1或2超过200ms无新事件状态归0,丢弃未完成的序列

为什么要200ms超时?因为它既滤掉了按键抖动,又能处理“只挡了一个传感器就缩回去”的情况。楼道里经常有人探一下头又缩回去,没有超时机制,这一次不完整遮挡就会留在状态机里,下一次真正通过时序列判断就会错。200ms对正常通行足够长,对“探头”又足够短。

3.3 Keil C51下可直接使用的计数核心代码

核心代码在Keil C51下编译,晶振选12MHz,Timer0作为20ms时基。先看传感器事件处理:

#include <reg51.h> #include <stdio.h> #define uint unsigned int #define uchar unsigned char sbit IR_A = P3^2; /* 通道A,对应INT0 */ sbit IR_B = P3^3; /* 通道B,对应INT1 */ sbit BEEP = P2^7; /* 满员报警输出 */ volatile uchar evA = 0, evB = 0; /* 中断置位的事件标志 */ uchar state = 0; /* 方向状态机状态 */ uint cur = 0; /* 当前人数 */ uint inTotal = 0, outTotal = 0; /* 进入、离开累计人次 */ void delay_ms(uint n) { uint i, j; for (i = 0; i < n; i++) for (j = 0; j < 120; j++); } void int0_isr(void) interrupt 0 { evA = 1; } void int1_isr(void) interrupt 2 { evB = 1; }

中断服务子程序里不处理具体逻辑,只置事件标志。这样做是因为中断里不宜做延时操作,一旦在ISR里调用delay_ms,另一路中断就会被长时间屏蔽;而把处理放到主循环后,状态机的每一步都能被逐步观察,Proteus仿真时更容易发现问题。

接着是状态机的处理函数:

void check_sensor(void) { if (evA) { evA = 0; delay_ms(2); /* 去抖,确认电平稳定为低 */ if (IR_A == 0) { if (state == 2) { /* 先见B后见A,判定为离开 */ if (cur > 0) cur--; outTotal++; state = 0; } else if (state == 0) { state = 1; /* 第一次遇到A,等待B */ } /* state为1时说明同一边重复触发,忽略 */ } } if (evB) { evB = 0; delay_ms(2); if (IR_B == 0) { if (state == 1) { /* 先见A后见B,判定为进入 */ cur++; inTotal++; state = 0; } else if (state == 0) { state = 2; /* 第一次遇到B,等待A */ } } } }

这里有两层防护。第一层是2毫秒的二次电平确认,能滤掉按键触点抖动和Proteus仿真中信号毛刺;第二层是状态机的200毫秒超时复位,保证不完整遮挡不会污染下一次判断。2毫秒参数在仿真时可以适当调大,但如果调大到几十毫秒,反而会拖慢响应,实际工程里的典型区间是2到5毫秒。

timer0中断提供时基和超时复位:

void timer0_isr(void) interrupt 1 { static uchar idle = 0; TH0 = 0xB1; TL0 = 0xE0; /* 12MHz下20ms重装值 */ if (state != 0) { if (++idle >= 10) { idle = 0; state = 0; /* 200ms无新事件,复位状态机 */ } } else { idle = 0; } } void main(void) { IT0 = 1; IT1 = 1; /* 下降沿触发 */ EX0 = 1; EX1 = 1; TMOD = 0x01; TH0 = 0xB1; TL0 = 0xE0; ET0 = 1; TR0 = 1; EA = 1; lcd_init(); while (1) { check_sensor(); /* 显示刷新等由lcd驱动补齐 */ } }

这段代码的可靠性在于三个解耦:中断只做标记,状态机在主循环里跑,超时复位由独立时钟驱动。遇到Proteus仿真的时序问题,先把这三件事拆开看是哪一环出了问题,比直接改计数变量有效得多。lcd_init、lcd_str这类函数按手头1602驱动补齐即可,接口名保持不变就能直接合入。

3.4 LCD显示与满员报警的串法

显示部分我一般放到主循环的20ms时基里,刷新足够。核心是两条:

sprintf(disbuf, "CUR:%4d IN:%4d", cur, inTotal); lcd_str(0x80, disbuf); sprintf(disbuf, "OUT:%4d", outTotal); lcd_str(0xC0, disbuf);

Keil C51的sprintf能跑但代码量偏大,做实物时建议改成查表或逐位取余数转ASCII。满员报警只需要一句:if (cur >= 100) BEEP = 0; else BEEP = 1;,阈值100可以在程序开头用宏定义,方便仿真时改成5、10这种小数字看效果。

4. 仿真联调:Keil生成HEX、Proteus加载与常见坑

4.1 Keil工程里的两个必检项

Keil工程建立后,先打开Project → Options for Target。Target标签页把晶振频率从默认值改成12MHz,这个值要与Proteus里单片机属性一致,否则Timer0的20ms初值就与实际时间对不上。Output标签页勾选Create HEX File,然后重新编译。编译输出窗口出现类似下面的内容,说明HEX文件已经生成:

Program Size: data=129.0 xdata=0 code=3788 creating hex file...

HEX文件默认生成在整个工程的Objects目录下,也可以在Output标签页里手动指定路径。没看到“creating hex file”这一行,后面的Proteus加载就是空操作,很多仿真“没反应”其实卡在这一步。

4.2 Proteus里加载HEX和更换元件的顺序

双击Proteus仿真图中的AT89C51芯片,在Edit Component对话框里做三件事:Program File选择上一节Keil生成的HEX文件;Crystal Frequency设置成12MHz;确认EA引脚已经接高电平。这三项全部正确后,点击左下角运行按钮。

LCD第一行初始化后会显示一行空格,按下A按键一次再按B按键一次,当前人数从0变成1。如果LCD没有反应,先检查Proteus左下角的运行状态是否是“Running”,仿真图处于暂停状态时按键当然无效。

4.3 仿真中常见问题与排查对照表

下面是我在Proteus里调试这种红外计数电路时最常遇到的几类问题,直接按现象对答案:

故障现象可能原因处理办法
LCD有背光但无字符P0口排阻没接加RESPACK-8,公共端接VCC
LCD显示乱码数据线标号错位逐根核对P0.0-P0.7与LCD引脚对应
按下按键计数无变化未加载HEX或EA悬空双击单片机重新加载HEX,EA接VCC
一次遮挡被记成多次触发方式设成低电平IT0/IT1必须置1,用下降沿
进出方向判断错乱两键连续太快慢速操作,间隔大于200ms
运行一会状态不准动画速度过快降低Animation Speed后重测

4.4 用示波器验证两路信号的先后顺序

Proteus虚拟仪器栏里有OSCILLOSCOPE,我习惯把CH1接P3.2、CH2接P3.3。按下A按键时,CH1会有一条下沿;紧接着按B,CH2下沿出现。如果CH1和CH2的下沿几乎同时到达,说明两个按键按得太快或实际安装间距过近,这种情况状态机就无法分辨方向,真实项目靠两路间距和传感器响应时间去拉开时间差。

提示:跑仿真遇到方向判断异常,先不要改代码,先在示波器上确认A、B下沿顺序符合预期。信号顺序错了,后面状态机怎么调都没意义。

5. 验证计数准确率:测试用例、双向同时遮挡与AT24C02掉电保存

5.1 十次进出的标准测试用例

楼道人数计数器给论文的测试数据,最通用的是“连续通行准确率”。仿真实操是按顺序执行下表,每次间隔保持0.5秒以上:

用例操作期望cur期望inTotal期望outTotal
1先A后B110
2先B后A011
3连续三次先A后B341
4只遮挡A然后松开341
5连续五次先B后A046

用例4对应“探头后缩回”场景,预期人数不变,验证的是200毫秒超时复位是否生效。如果用例4实际人数变了,说明state在下次触发前没有被清掉,重点检查timer0中断里的idle计时。

5.2 双向同时遮挡的兜底策略与论文里的误差分析

真实楼道里必然存在两人并排进出,两路下降沿几乎同时出现。代码层面的兜底策略是:在check_sensor里加一个同时标志判断,evA和evB同时为1时,默认按进入一次处理,并把两个标志和state都清零。这是因为并排场景相对于进向、出向没有明确先后,做误判处理总比漏计数好,但要在论文误差分析里明确写“并排通过按单人处理”。

更有效的工程手段是拉大两个红外对管的安装间距,让遮挡时间差大于传感器的响应时间。间距从30cm提高到60cm后,两路下降沿的时间差可以稳定到几十毫秒以上,这对状态机来说已经非常安全。

5.3 把当前人数存进AT24C02

课程设计通常要求断电后计数不丢失,扩展一颗AT24C02是最常见的做法。接线是SCL接P2.3、SDA接P2.4,A0、A1、A2和WP接地,两根线各接4.7k上拉。写一个字节的代码结构如下:

void eeprom_write(uchar addr, uchar dat) { i2c_start(); i2c_write(0xA0); /* 器件地址,写操作 */ i2c_write(addr); i2c_write(dat); i2c_stop(); } uchar eeprom_read(uchar addr) { uchar dat; i2c_start(); i2c_write(0xA0); i2c_write(addr); i2c_start(); i2c_write(0xA1); /* 器件地址,读操作 */ dat = i2c_read(1); i2c_stop(); return dat; }

每次cur变化后调用eeprom_write(0x00, (uchar)cur);,上电初始化时执行cur = eeprom_read(0x00);。i2c_start、i2c_write、i2c_read三个基础函数是标准I2C时序,网上和Keil官方示例都能找到,这里不展开。注意AT24C02单字节写完后要等约5毫秒内部写周期,掉电瞬间持续写入会存在写失败,功能验证时不要用“断电瞬间还在写”的场景。

把SDA和SCL的端口定义改成sbit SDA = P2^3; sbit SCL = P2^4;,再在main函数里补上cur = eeprom_read(0x00);,掉电保存部分就完整接入了楼道人数计数器的仿真工程。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询