简介:面向电子、自动化类专业学生及51单片机初级开发者的流水线打包计数器仿真设计资源,包含程序与Proteus仿真文件,主要解决流水线物品计数、数码管显示、按键设值与超量报警的完整设计问题。资源共15个文件,压缩包仅48KB,涵盖Keil工程源文件(.c/.a51)、编译生成文件(.hex/.obj/.lst)、Proteus仿真原理图(.dsn)及工程配置文件(.uvproj/.uvopt),可同时满足程序阅读与硬件电路仿真需求。已有1295人学习使用,验证了该方案的参考价值。设计核心包括传感器信号触发外部中断、定时/计数器增计数、达到100件启动报警,并通过数码管实时刷新计数值;键盘接口支持修改预设数量,适合作为课程设计或入门综合练习。结合源码与Proteus仿真,可直观理解中断服务程序与主循环的分工,也能在此框架上扩展多路计数、数据存储等进阶功能。
1. 一条包装线从人工数数到单片机计数,第一步该做什么
整条包装线如果靠人工数数,白班数到第500箱基本就开始凭感觉了,漏记和重记到最后对不上账,不是赔一箱两箱的问题。用红外对射开关加51单片机做打包计数器,物体过一箱,光电管输出一个低电平脉冲,单片机计一次数,数码管实时显示,到了设定数量蜂鸣器提醒换箱,这一套逻辑在产线上很常见,也是51单片机课程设计里出现频率最高的题目之一。这套资源包里给的mimasuo.c、Test.hex和仿真图.DSN是能直接跑起来的,下面从硬件配置到Proteus仿真联调,把每个要改的参数和容易踩的坑过一遍。
2. 计数器的硬件骨架:I/O分配、T0计数模式与中断响应设计
2.1 传感器输出与P3.4引脚的连接方式
红外对射光电开关的接收端在光束被物体遮断时,输出端会从高电平拉低,产生一个宽度几十毫秒的低电平脉冲。如果让主循环用查询方式不停读引脚电平,脉冲来的时候主循环恰好正在做数码管刷新,这一箱就会被漏掉。常见做法是把脉冲接到定时器的外部计数输入脚P3.4/T0,把T0配置成计数器模式,脉冲下降沿到来时TH0/TL0自己加1,CPU根本不用管,等到溢出再进中断做一次软件累加即可。
这里有个选型问题:为什么不直接用外部中断INT0?INT0边沿触发同样能感知脉冲,但P3.4的T0计数器对下降沿的捕获不依赖中断响应速度,脉冲密集时不容易丢边沿;而且T1还能留作第二个通道,两条包装线共用一颗单片机时只需把另一路信号接到P3.5。还要注意传感器输出端多数是集电极开路结构,在Proteus仿真里如果用信号源模拟,输出端要加一个10k上拉电阻,否则脉冲边沿因为波形不干净,一次遮挡会触发两次计数。
2.2 TMOD、TH0/TL0的方式字配置与初值计算
T0的工作模式由TMOD寄存器的低四位控制,其中C/T位是定时和计数的总开关,C/T=1时T0不再数机器周期,而是数P3.4引脚的外部脉冲。本工程采用方式1,也就是16位计数模式。
| C/T位 | 方式 | 计数器宽度 | 适用场景 |
|---|---|---|---|
| 0 | 方式1 | 16位定时 | 数码管扫描时基、蜂鸣器延时 |
| 1 | 方式1 | 16位计数 | 包裹脉冲计数,本工程使用 |
| 1 | 方式2 | 8位自动重装 | 高速连续脉冲计数,中断开销更小 |
mimasuo.c里对应的初始化代码是这样写的:
void Timer0_Init(void) { TMOD &= 0xF0; // 只清T0部分,保留T1的配置 TMOD |= 0x05; // 0000 0101,C/T=1,GATE=0,方式1(16位) TH0 = 0xFF; // 初值设为65535 TL0 = 0xFF; // 再接收1个脉冲就溢出进中断 ET0 = 1; // 开T0中断 EA = 1; // 开总中断 TR0 = 1; // 启动T0 }初值取65535的目的是让每个外部脉冲都立刻触发一次中断,中断服务程序里再对pack_num做软件累加。这种方式对中低速包装线完全够用,但要注意中断服务程序里不能做延时,否则下一个脉冲进来时中断还没退出,仍然会丢计数。如果产线速度更高,就把初值改成0xFF,连续收到250个脉冲才进一次中断,中断里把软件累加变量加上250,这样中断次数能减少到原来的四百分之一,代价是实时显示会延迟几十毫秒,肉眼看不出来。
2.3 数码管、按键和蜂鸣器的引脚分配
这个工程的显示部分是四位数码管动态扫描,段选接P0,位选接P2.0到P2.3,按键占用P1.0和P1.1,蜂鸣器由P2.4控制。引脚分配和信号方向整理成下面这张表,后面调Proteus或改实物电路时对着查就行。
| 功能模块 | 引脚 | 说明 |
|---|---|---|
| 段选 a-g-dp | P0 全部 | P0为开漏输出,需上拉电阻或74HC245驱动 |
| 位选 dig0-dig3 | P2.0-P2.3 | 低电平选通共阴数码管 |
| 按键加一 | P1.0 | 接10k上拉,按下为低电平 |
| 按键清零/确认 | P1.1 | 独立按键,短按清零 |
| 蜂鸣器 | P2.4 | 三极管驱动,低电平响 |
P0口在标准51里是开漏输出,直接接共阴数码管段选时驱动能力不足,Proteus仿真里看起来能亮,实际焊板子就会发现数字发暗。这个仿真图里给P0加了一排排阻,是为了接近真实硬件状态,不是随手画的。
3. 从mimasuo.c看C语言实现:计数、显示和按键的并发处理
3.1 主循环只做慢速任务,计数完全交给T0中断
程序结构上把任务分成两类:一类是光靠主循环轮询就能完成的慢速任务,比如数码管刷新、按键扫描、蜂鸣器开关;另一类是对时间敏感的计数任务,必须放在中断里。这样拆的好处是计数的实时性不再依赖主循环的代码长度,哪怕显示刷新写得再慢,只要中断开着,脉冲一个都不会漏。
volatile unsigned int pack_num = 0; bit alarm_flag = 0; #define MAX_COUNT 100 void Timer0_ISR(void) interrupt 1 { // P3.4下降沿触发,T0硬件已捕获,这里只做软件累加 pack_num++; if (pack_num >= MAX_COUNT) { pack_num = 0; // 达到阈值,重新计数下一箱 alarm_flag = 1; // 置位报警标志,通知主循环响蜂鸣器 } } void main(void) { Timer0_Init(); while (1) { DisplayScan(); // 动态扫描刷新数码管 KeyScan(); // 非阻塞按键扫描 if (alarm_flag) // 主循环检查标志位 { BuzzerOn(); delay_ms(200); BuzzerOff(); alarm_flag = 0; } } }代码里pack_num声明成volatile,原因是它在中断里被改写、主循环里被读取,没有volatile修饰时编译器可能把这个变量优化进寄存器,导致主循环读取到旧值。alarm_flag用位变量bit声明,51单片机的位寻址区对这种状态标志非常合适,读写都是单指令,执行快,也不会被主循环的其它操作打断。中断服务函数名后面的interrupt 1是Keil C51的固定写法,数字1对应T0的中断号,不能随便改。
3.2 数码管动态扫描的时序和消隐
四位数码管如果每位都直接接锁存器,需要占用的I/O口数量会多出一倍,所以这里用动态扫描:同一时间只点亮一位,轮流点亮四位,利用人眼视觉暂留形成同时显示的效果。共阴数码管的段码表定义在code区域,这是51单片机访问程序存储器的关键词,数据不会占用宝贵的RAM空间。
code unsigned char seg_code[10] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F }; void DisplayScan(void) { unsigned char bit_sel[4] = {0x01, 0x02, 0x04, 0x08}; unsigned char i; unsigned int temp = pack_num; for (i = 0; i < 4; i++) { P2 = ~bit_sel[i]; // 选中当前位,低电平有效 P0 = seg_code[temp % 10]; // 取个位段码并送到P0 delay_ms(1); // 每位点亮约1ms temp /= 10; // 右移一位十进制数 } }这段代码有个细节:位选和段选的赋值顺序不能反过来。必须先选通位,再送段码,否则切换瞬间会出现前一位的数据残留在下一位上的情况,表现就是数字边缘有淡淡的鬼影。四位一个循环周期约4ms,刷新率250fps,远高于视觉暂留要求,即使放大到六位数码管也只需要把循环次数改成6,刷新率仍然能到160fps以上。延时函数用软件延时实现时,建议用带参形式,延时参数不要超过10ms,不然主循环扫描一次的时间过长,按键响应会发粘。
3.3 按键阈值修改与防抖处理
按键直接连P1.0和P1.1,机械按键按下瞬间会产生几毫秒到十几毫秒的抖动,电平波形在0和1之间反复跳变。如果每个抖动沿都被当作一次按键事件,一次按下可能累加出好几次计数。这里用沿检测加软件延时的方式处理:检测到第一次低电平后先不确认,延时10ms再读一次,仍然为低才判定为有效按下。
void KeyScan(void) { static unsigned char key_up = 0; if (P1_0 == 0 && key_up == 0) // 检测按下沿 { delay_ms(10); // 跳过抖动区间 if (P1_0 == 0) // 二次确认确实是低电平 { pack_num++; key_up = 1; // 标记已处理本次按下 } } if (P1_0 == 1) key_up = 0; // 松手后清除标志 if (P1_1 == 0 && key_up == 0) { delay_ms(10); if (P1_1 == 0) { pack_num = 0; // 短按清零当前计数 key_up = 1; } } if (P1_1 == 1) key_up = 0; }这里用static局部变量保存上次按键状态,比全局变量更内聚。key_up的值在“未按下”和“已处理本次按下”之间切换,本质是判断下降沿而不是电平本身,所以长按不会触发连续加一。如果需求改成“长按1.5秒清零”,可以在确认按下后启动一个计时变量,主循环每轮对它加一,超过阈值再执行清零,不能直接放在这个函数里delay,否则会阻塞数码管显示。
3.4 Keil工程文件与输出文件的作用
拿到RAR解压后,里面除了mimasuo.c还有一堆Startup、OBJ、Hex、lnp、build_log文件。很多人只关心.c和.hex,其它文件当作垃圾直接删,其实它们在调试阶段各有用途。
| 文件 | 作用 |
|---|---|
| STARTUP.A51 / STARTUP.OBJ | Keil自动生成的51启动代码,负责初始化堆栈和清零内部RAM |
| mimasuo.c / mimasuo.OBJ | 用户程序源码和编译产物 |
| Test.uvproj / Test.uvopt | Keil工程文件,.uvopt保存调试器设置和窗口布局 |
| Test.hex | 编译生成的Intel HEX固件,Proteus和烧录器都认这个格式 |
| Test.lnp / Test.build_log.htm | 链接参数文件和编译日志 |
如果Keil工程文件打不开或者编译时提示找不到STARTUP.A51,去Keil安装目录下的C51\LIB里把STARTUP.A51拷贝到工程目录,重新添加进工程即可。build_log.htm里能直接看到最后一次编译有没有错误和警告,报错行号可以精确跳到出问题的源码行,改完再编译时对比日志变化就能确认是否解决。
4. Proteus仿真联调:从DSN电路搭建到HEX固件运行
4.1 打开仿真图并加载HEX固件
用Proteus 8 Professional打开打包计数器.rar里的仿真图.DSN,图里已经放好了AT89C51、四位数码管、按键、蜂鸣器和脉冲输入源。双击AT89C51芯片,在Program File里选择Test.hex,Crystal Frequency手动改成12MHz。这里的频率必须和Keil工程选的晶振值一致,否则抖动的延时函数跑出来的时间整体偏移,数码管扫描会闪烁,蜂鸣器响的时间也不对。
GATE位在这个电路设计中没有用到,TMOD里配置为0即可。控制字里没有动的位保持原来的值,初学容易犯的错是直接写TMOD=0x05,把T1既有的配置一并覆盖掉。这里先与0xF0清空T0部分,再或上0x05,写出这样的代码是工程上更保险的习惯。
4.2 用信号源模拟光电开关的脉冲
Proteus里模拟传感器有两种常用方式:一种是直接用按钮,每按一次产生一个脉冲;另一种是用DCLOCK或DPULSE这类信号源器件,自动以固定频率输出方波,适合长时间跑仿真检查计数逻辑。DPULSE的属性面板里几个关键参数这样设:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| Timestep | 1s | 每1秒来一个脉冲,方便观察 |
| Rise/Fall Time | 1n | 边沿时间设得极短,模拟开关跳变 |
| Pulse Width | 50ms | 低电平持续时间,接近真实光电开关 |
| Initial State | 1 | 初始为高电平,脉冲到来时拉低 |
信号源输出接到P3.4之前务必经过一个10k上拉电阻到VCC。有些人仿真时计数一次跳两次,就是因为信号源输出端在跳变瞬间出现振铃,T0把振铃的下降沿也数进去了。接上上拉电阻后,波形边沿会被整得更干净,计数就稳定了。
4.3 仿真跑不通时的典型报错与对策
Proteus报错信息往往比较抽象,比如弹窗提示“Simulation must be paused”或者“Model not found”。前者是仿真运行中修改了电路参数,需要先暂停再改;后者是元件库版本不一致,仿真图里用到的某个模型在当前版本里不存在,去元件列表里重新放置一个同类器件代替。
| 现象 | 原因 | 对策 |
|---|---|---|
| 数码管不亮或数字乱跳 | 位选/段选接反,共阴共阳选错 | 检查数码管型号,共阳需段码按位取反 |
| 蜂鸣器一直响不停止 | P2.4驱动极性接反 | 确认三极管是PNP还是NPN,低电平响就选PNP |
| 计数一次加2 | 信号未上拉,下降沿抖动 | 输入端并联10k上拉电阻 |
| HEX加载后无反应 | Program File路径为空 | 重新选择Test.hex,确认扩展名正确 |
| 数码管亮度不均 | 位选驱动能力不足 | 位选端加三极管或ULN2003 |
仿真电路里看到数码管四位亮度不均匀,多数不是程序问题,而是位选电流不足。把位选P2.0到P2.3接到ULN2003输入端,输出端再连数码管公共端,每位都能获得足够电流,这个问题就消失了。程序层面能做的调整是延长点亮时间,但牺牲刷新率,只做辅助手段。
5. 排错与进阶:把计数调准,再谈掉电保存
5.1 用Proteus示波器验证脉冲质量与计数一致性
Proteus左侧工具栏里的虚拟示波器可以直接拖到仿真图上,把探针夹在P3.4上,运行仿真后能看到脉冲的波形。确认两点:一是每个脉冲的边沿是否干净,有没有多次上下跳变;二是脉冲宽度是否足够,T0计数用的是下降沿,只要沿存在就能捕获,但信号源上拉电阻选得太大时,脉冲边沿会被拉长成斜坡,极端情况下触发不稳定,上拉电阻用10k比用100k可靠很多。
进阶一点的验证方式是给计数器加一个测试模式,程序里用T1定时器每秒钟翻转一次P1.7电平,外部把P1.7短接到P3.4,跑一分钟后看pack_num是否等于60。等于说明T0计数链路完整,不等于就去查信号路径上的每个接点。
5.2 计数阈值改为可掉电保存的值
本工程把MAX_COUNT写成宏定义,改阈值要重新编译。批量使用时有更省事的方案:把阈值放到EEPROM里,用按键在开机时设置一次,之后掉电也不丢。51单片机里STC系列内置EEPROM,写法和外部存储器一样用MOVC和MOVX指令访问:
void Eeprom_Write(unsigned char addr, unsigned char dat) { IAP_CONTR = 0x80; // 触发一次软件复位前的准备好操作 IAP_CMD = 0x02; // 字节写命令 IAP_ADDRH = addr >> 8; IAP_ADDRL = addr & 0xFF; IAP_DATA = dat; IAP_TRIG = 0x5A; // 触发写操作的固定序列 IAP_TRIG = 0xA5; _nop_(); IAP_CONTR = 0x00; // 关闭IAP功能 }这段代码用的是STC的IAP寄存器组,AT89C51没有这些寄存器,仿真图里要继续用只能改用STC89C52再加载对应HEX。EEPROM写入次数大约十万次,设定阈值这个动作不会频繁发生,寿命完全够用。每次开机时读出来和当前值比较,如果读到0xFF说明从未写过,就用默认值100代替。
5.3 从build_log和lnp文件里找编译线索
Test.build_log.htm里记录着每次编译的详细输出,看到“0 Error(s), 0 Warning(s)”才说明程序编译通过。如果里面有“MULTIPLE PUBLIC DEFINITIONS”或者“UNCALLED SEGMENT”这类提示,分别是重复定义和代码无用段,前者检查有没有两个文件定义了同名函数,后者不影响烧录但说明有函数没被调用,通常是测试代码残留。Test.lnp是链接参数文件,里面按顺序列出了参与链接的所有OBJ和依赖库路径,怀疑工程文件乱时可以打开它确认源头。
把这些文件理解成工程的编译档案而不是垃圾,排查问题时就能多一条路。真正的排错顺序应该是:先看build_log确认程序有没有编译过,再用示波器看P3.4波形确认脉冲有没有到单片机,最后才回到程序里查逻辑。多数仿真跑不动的情况,最后都落在HEX路径没加载或者晶振频率不一致这两处。
本文还有配套的精品资源,点击获取