基于MCS-51与8255A的交通灯控制器设计:定时器、状态机与IO扩展实战

发布时间:2026/11/2 4:11:23
基于MCS-51与8255A的交通灯控制器设计:定时器、状态机与IO扩展实战

简介:这份毕业设计论文面向电子信息、自动化等专业的学生与单片机初学者,围绕「如何用单片机实现一套可动态调节的交通灯控制系统」展开,从交通灯发展历史讲到系统方案落地,适合作为课程设计、毕业设计选题参考或自学单片机控制项目的实践范本。资源包共1个doc文档,约257KB,内容涵盖MSC-51系列ATSC51单片机内部结构剖析、8255A可编程并行I/O接口的配合使用、硬件原理图与接口电路说明,以及初始化、时间设定、状态切换等完整源程序代码。系统实现了按车流量编程调整红绿灯时长、倒计时剩5秒黄灯闪烁警示、车辆闯红灯报警、绿灯时段车流量检测与数码管显示等功能,并附有测试结果分析。目前已有1011人学习,可帮助读者理清软硬件协同设计思路,掌握从理论分析、电路设计到编程调试的完整流程。

1. 交通灯控制器:为什么一个 8 位单片机就能撑起路口秩序

路口那套红黄绿循环,看起来是"定时亮灯"这么简单,但真正让它稳定跑起来的,是一套被压缩到极致的时序系统。很多人第一次看这类设计会觉得"不就是延时加 IO 输出",可一旦动手就会发现:秒从哪来、倒计时怎么不飘、黄灯闪烁怎么和主循环不打架、车流量检测怎么插进绿灯窗口——每一个都是坑。这份毕业设计论文用 MCS-51 系列单片机加 8255A 并行接口芯片搭了一套完整控制器,把红绿灯循环、倒计时显示、闯红灯报警、车流量检测全塞进一颗 8 位核心里。搞懂它,等于搞懂"资源极度受限时,控制逻辑如何被拆成定时器、中断、状态机三件套",这套思路放到今天任何嵌入式控制场景都不过时。

2. 定时基准:1 秒到底是怎么被"数"出来的

2.1 为什么不能直接定时 1 秒

MCS-51 的定时器是加法计数器,从初值往上数,数到溢出就产生中断。论文里给了通式 TC = M − C,M 是计数器模值,方式 1 下 M = 2^16 = 65536。关键在时间换算:机器周期是时钟周期的 12 倍,若主频 12MHz,一个机器周期正好 1 微秒,方式 1 最大定时 65536 × 1μs ≈ 65.5ms。也就是说,硬件单次最多只能"看住"65 毫秒,1 秒根本装不下。

这就是第一个反直觉点:秒不是定时器给的,是软件数出来的。论文的做法是让 T0 每 50ms 溢出一次,主程序里放一个初值 20 的软件计数器,中断里减 1,减到 0 就认为 1 秒到了。50ms × 20 = 1000ms,误差被摊到 20 次中断里,单次中断响应延迟对总时长的影响被稀释,这是资源受限系统里非常典型的"硬件粗定时 + 软件细分频"结构。

初值计算:TC = 65536 − 50000 = 15536 = 0x3CB0,所以 TH0 装 0x3C,TL0 装 0xB0。论文代码里写的是MOV TH0,#3CH/MOV TL0,#B0H,和这个推导完全对得上。

2.2 中断服务程序里的分频逻辑

; T0 中断服务程序:50ms 一次,20 次凑 1 秒 ORG 000BH LJMP T0_INT T0_INT: MOV TL0, #9AH ; 重装初值低字节(论文另一处用 10ms 定时) MOV TH0, #0F1H ; 重装初值高字节 INC R4 INC R5 CJNE R5, #0FAH, T01 ; R5 数到 250(0xFA)才继续,否则直接去显示 MOV R5, #00H ; R5 清零,准备下一轮 DEC R3 ; 倒计时秒数减一 DEC R2 ; 报警剩余秒数减一 T01: ACALL DISP ; 每次中断都刷新一次显示 RETI

这段代码有两个值得注意的设计。第一,重装初值放在中断入口最前面,保证下一次定时周期从固定点开始,避免中断响应时间抖动累积成秒误差。第二,CJNE R5,#0FAH,T01这个比较用的是 250 而不是 20,说明论文里实际采用的是 10ms 定时、250 次凑 2.5 秒的变体,和前面 50ms×20 的推导是两套写法混在同一份文档里——这在学生设计里很常见,读的时候要盯住"定时周期 × 分频次数 = 目标时间"这个不变量,别被具体数字绕晕。

提示:判断一份定时逻辑对不对,别看它写 20 还是 250,先算(M − TC) × 机器周期 × 分频次数,结果等于目标时间就成立。

2.3 软件延时为什么只能当备胎

论文还给了纯软件延时方案:DELAY1双重循环 256×256 = 65536 次,每次约 2μs,约 125ms;外层DELAY循环 8 次凑 1 秒。这种写法能跑,但有两个硬伤:一是循环次数依赖主频,换晶振就得重算;二是延时期间 CPU 被完全占死,键盘扫描、闯红灯检测全部停摆。所以论文最终主程序走的是定时器中断路线,软件延时只留给显示刷新这种"短、可丢"的场景。这个取舍很关键——凡是影响状态推进的时间基准,必须走硬件定时器;凡是只影响观感的短等待,才允许软件空转。

3. 状态机:红黄绿循环为什么不能写成顺序延时

3.1 四个状态与公共黄灯窗口

论文表 2 给出的时序是:东西红灯 60 秒(南北绿灯)→ 黄灯 5 秒 → 东西绿灯 80 秒(南北红灯)→ 黄灯 5 秒 → 循环。注意黄灯是"东西南北两干道的公共停车时间",也就是说黄灯期间两个方向都不放行,这 5 秒是清空路口的安全窗口。

如果按"亮红灯→延时 60 秒→亮黄灯→延时 5 秒→亮绿灯"这种顺序写法,程序会卡在延时里,倒计时显示没法刷新,闯红灯检测也没法响应。论文的做法是把每个状态拆成"设置输出 + 倒计时递减"两件事,倒计时递减挂在 1 秒中断里,主循环只负责状态切换判断。这就是一个最朴素的时间驱动状态机:

状态8255 PA 口输出含义持续
N000xDD东西红、南北绿60s
N10xD4 / 0xDD 交替双黄闪烁5s
N20xEB东西绿、南北红80s
N30xE2 / 0xEB 交替双黄闪烁5s

PA 口输出的是灯的组合码,0xDD = 1101 1101,对应位为 0 的引脚驱动共阳极 LED 点亮。这里有个容易翻车的细节:论文说"发光二极管为共阳极接法,输出低电平对应二极管发光",所以置位方法点亮灯,写 1 是灭、写 0 是亮。第一次调这个电路的人十有八九会把极性搞反,灯全亮或全灭。

3.2 黄灯闪烁怎么不阻塞主流程

黄灯闪烁的实现是论文里比较巧的一段:

N1: MOV R3, #05H ; 闪烁总时长 5 秒 MOV DPTR, #0000H MOV A, #0D4H ; 双黄亮 MOVX @DPTR, A N11: MOV R4, #00H N12: CJNE R4, #7DH, $ ; 空循环约 0.5 秒 N13: MOV DPTR, #0000H MOV A, #0DDH ; 黄灯灭(回到红灯组合) MOVX @DPTR, A N14: MOV R4, #00H CJNE R4, #7DH, $ ; 再空循环约 0.5 秒 CJNE R3, #00H, N1 ; 5 秒没到就继续闪

CJNE R4,#7DH,$里的$表示"跳回本条指令",配合 R4 自增形成忙等循环,0x7D = 125 次,按机器周期估算约 0.5 秒。亮 0.5 秒、灭 0.5 秒,循环 5 次正好 5 秒。这段是阻塞式的,闪烁期间主循环停在这里,但因为它只持续 5 秒且期间不需要响应其他状态切换,所以可以接受。判断一段阻塞代码能不能留,看它阻塞期间有没有必须响应的事件——黄灯窗口里没有倒计时递减以外的任务,所以忙等是安全的;但如果你在这 5 秒里还想扫键盘,就必须改成中断驱动。

3.3 倒计时递减与状态切换的耦合

倒计时变量 R3 在中断里每秒减一,主循环里用CJNE R3,#00H,N01判断是否减到零,到零就跳出当前状态进入下一个。这种"中断改值、主循环查值"的模式避免了主循环和中断同时写同一个变量,但有个隐患:R3 是 8 位寄存器,中断里DEC R3和主循环里CJNE R3,#00H之间没有保护,如果主循环正好在判断时被中断打断,理论上可能读到中间值。在 1 秒量级的时间尺度上这个竞争窗口极小,实际不会出问题,但严格做法应该是关中断读、开中断用,或者用标志位传递"时间到"事件。论文没做这层保护,属于"能跑但不严谨"的典型学生代码,读的时候要心里有数。

4. 8255A 扩展:51 的 IO 口为什么不够用

4.1 端口预算算给你看

8051 名义上有 P0、P1、P2、P3 四组 8 位口,但真正能自由用的没几个:P0 分时复用做地址/数据总线,P2 出高 8 位地址,P3 的第二功能被中断、串口、定时器占着。论文的系统里要接的东西包括:红黄绿灯 6 路输出、双位数码管段选 8 路加位选 2 路、键盘输入、车流量检测输入、闯红灯检测输入。光数码管就吃掉 10 根线,P1 那 8 位根本不够。

所以论文用 8255A 做并行扩展,PA 口出信号灯组合码,PB/PC 口驱动数码管,控制字写 0x80 表示三个端口全部方式 0 输出。方式 0 是基本 IO,输出带锁存、输入不锁存,正好匹配"灯和数码管都是纯输出"的需求。这里的设计动机很直接:51 的 IO 是准双向口,驱动能力弱且没有锁存语义,外扩一片 8255 既补了端口数量,又补了输出锁存,灯的状态写进去就稳住,不会因为 CPU 去忙别的事而丢。

4.2 片选与地址译码

论文用 P0.7 接 8255 的 CS,全译码方式下 P0.7 = 0 时片选有效,P0.1、P0.0 选择端口:

; 8255 端口地址映射(P0.7=0 有效) ; 00H -> PA 口(信号灯) ; 01H -> PB 口(数码管段选/位选) ; 02H -> PC 口(数码管另一组) ; 03H -> 控制口(写方式字) MOV DPTR, #0003H MOV A, #80H ; 方式控制字:D7=1 标志位,三端口方式 0 输出 MOVX @DPTR, A

MOVX是访问外部数据存储器空间的指令,8255 被映射到外部 RAM 地址上,所以用MOVX @DPTR写。这里有个经典坑:P0 口同时承担地址低 8 位和数据总线,必须外接 74LS373 锁存地址,论文硬件清单里正好有 74LS373,就是干这个的。ALE 引脚在访问外部存储器时输出锁存脉冲,把 P0 上的地址打进 373,之后 P0 才释放出来传数据。没有 373,地址和数据会在 P0 上打架,8255 收到的端口号就是错的,这是硬件调试时最先要查的点。

4.3 数码管动态扫描与段码表

论文用共阴极数码管,段码表 0x3F、0x06、0x5B、0x4F、0x66、0x6D、0x7D、0x07、0x7F 对应 0 到 8。显示子程序DISP先把倒计时值做二转十(DIV AB,B 装 10,商进 79H 存十位、余数进 7AH 存个位),再查表输出段码,位选用 0xF7 和 0xFB 分别选十位和个位。动态扫描靠人眼视觉暂留,两位轮流亮,只要刷新频率够高就看不出闪烁。刷新挂在 1 秒中断里其实偏慢,但论文的显示子程序在每次中断都调一次,加上主循环里的调用,实际刷新率够用。如果发现数码管有轻微抖动,把DISP挪到更高频的定时中断里就行。

5. 避坑与排查:这类设计最容易翻车的五个地方

5.1 灯全亮或全灭,极性搞反

现象:上电后六个灯要么一起亮,要么一个不亮,改代码也没用。原因:共阳极 LED 低电平点亮,代码里按"写 1 亮"的直觉写,输出全反。解决:先拿万用表量 PA 口某一位,确认写 0 时该位确实拉低;再对照 0xDD、0xEB 这些组合码逐位核对,0 的位置就是亮灯位。

5.2 倒计时跳秒或走快走慢

现象:数码管数字不是每秒减一,有时跳两下,有时停一下。原因:定时器初值重装位置不对,或者中断里做了耗时操作(比如整屏刷新)导致下一次定时起点漂移。解决:重装 TH0/TL0 放在中断入口第一条,显示刷新用查表加短延时,别在中断里做除法以外的重活;用示波器量 P3.0 或某个空闲 IO 翻转,确认中断周期稳定在 50ms。

5.3 8255 写进去没反应

现象:MOVX @DPTR,A执行了,灯不变。原因:片选译码错、地址锁存没接、或者方式字没先写。解决:先写控制口 0x80 再写数据口,顺序不能反;用逻辑分析仪抓 P0.7 和 WR 引脚,确认片选和写脉冲同时有效;检查 74LS373 的 LE 是否接 ALE、OE 是否接地。

5.4 闯红灯报警误触发

现象:没车闯红灯蜂鸣器也响。原因:检测输入 P2.0 没有上拉或消抖,悬空时读到随机电平。解决:输入端加上拉电阻,软件里连续采样几次一致才认定有效;报警逻辑里加"倒计时未到零才检测"的前置条件,论文代码里MOV A,R3 / JZ N1就是干这个的,别把这句删了。

5.5 车流量计数溢出或不清零

现象:数码管显示的车流量数字乱跳,或者一直停在某个值。原因:计数变量 R7 到 0x64(100)后回绕处理不当,或者下一次绿灯没有重新装载。解决:CJNE R7,#64H,E1之后要显式MOV R7,#00H,并在绿灯状态入口处重置计数窗口,保证每个绿灯周期独立统计。

6. 设计取舍:8 位核跑交通控制,边界在哪里

这套设计的价值不在于"能亮灯",而在于它把一颗 8 位单片机的能力边界摸得很清楚。定时器只有两个、IO 只有四组、RAM 只有 128 字节可用,所以它必须做三件事:把秒拆成"硬件 50ms + 软件分频"、把 IO 缺口用 8255 补上、把状态推进交给中断而不是主循环空转。这三条合起来就是资源受限系统的通用套路——凡是时间基准,交给硬件;凡是端口不够,用锁存扩展;凡是并发事件,用中断解耦。

它的局限也同样清晰。阻塞式黄灯闪烁占了 5 秒 CPU,这期间如果要做车流量统计就得另想办法;倒计时变量没有临界区保护,靠时间尺度大来兜底;车流量检测只是简单计数,没有做平滑滤波,一辆车压两次传感器就多算一次。这些在毕业设计场景里可以接受,但放到真实路口就是血泪经验级别的隐患。我一般会这么理解:这类设计的正确读法不是照抄代码,而是看它在每个资源冲突点上选了哪条路、为什么那条路在当时约束下是合理的。

验证一套交通灯控制逻辑是否真的可靠,最直接的办法是拿逻辑分析仪同时抓 PA 口输出和定时中断引脚,把 60 秒、5 秒、80 秒、5 秒四个窗口的波形拉出来,量每个窗口的实际时长和理论值的偏差。偏差在 1% 以内说明定时链没问题;如果黄灯窗口明显偏长,多半是忙等循环的机器周期估算错了,得按实际晶振重算CJNE的循环次数。从那以后我每次看这类定时逻辑,都强制先把"机器周期 × 循环次数 = 目标时间"这条等式在纸上算一遍再读代码,省得被文档里混用的两套参数带偏。希望帮到你。

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