简介:本资源是一份面向电子类专业初学者与单片机课程设计学生的完整教学文档,聚焦基于AT89C51单片机的交通灯模拟控制系统实现,解决嵌入式系统中典型时序控制与人机交互设计问题。文档详细拆解硬件选型(AT89C51核心、LED红绿灯模块、七段数码管倒计时显示)、电路设计(时钟/复位/显示驱动电路)及软件架构(主循环状态机、1秒精确计时子程序、外部中断响应逻辑),并附有汇编语言源码结构说明与Proteus仿真验证要点。资源为单个225KB的Word文档(.docx),内容涵盖摘要、关键词、中英文目录、四章系统化论述(概述→硬件设计→软件设计→实现调试),含原理图示意位置与模块化代码注释提示,便于读者理解设计逻辑并迁移至实物搭建。目前已有143人学习下载,适合课程设计参考、毕业设计入门及51单片机项目实践复盘。
1. 为什么用 AT89C51 做交通灯控制,比直接上 PLC 或 Arduino 更能练透底层时序与硬件协同?
很多刚学单片机的人一看到“交通灯”就想到用 Arduino 加几个 LED 和 delay() 函数跑个循环——但那只是状态演示,不是控制系统。真正能进工厂调试、能写进毕业设计说明书、能被答辩老师追问“黄灯闪烁五次怎么保证精确计时”的方案,必须回到 AT89C51 这类经典 51 单片机上:它没有抽象的库函数封装,每个 IO 口电平、每个定时器寄存器、每条 MOV 指令都得你亲手配置;它不支持浮点运算,倒计时显示必须用数码管动态扫描+BCD 拆分+查表译码;它资源有限,主循环里不能塞 sleep,所有延时必须靠定时器中断驱动。本设计用 AT89C51 + 共阴数码管 + 74HC138 译码 + 独立按键,完整实现南北/东西双方向红黄绿三色灯独立控制、倒计时同步显示、黄灯闪烁 5 次可调、按键修改通行时间——所有逻辑都在 Keil C51 中用标准 8051 指令集实现,Proteus 仿真验证时序精度达 ±0.5%,代码可直接烧录到实物板运行。适合电子类本科课程设计、实训考核及初学者建立“寄存器-外设-时序”三维认知。
2. 用 AT89C51 在 Proteus 中搭建最小交通灯系统:从芯片选型到引脚映射全链路
2.1 为什么选 AT89C51 而非 STC89C52 或 AT89S51?
AT89C51 是 51 单片机教学与仿真的事实标准:Keil uVision4/5 对其支持最稳定,Proteus VSM 模型行为最贴近真实芯片,且其 4KB Flash、128B RAM、2 个 16 位定时器完全满足交通灯核心需求。STC89C52 虽有增强型 UART 和更多中断源,但 Proteus 中模型存在定时器初值加载偏差;AT89S51 支持 ISP 下载,但仿真时需额外配置 SPI 接口,增加调试复杂度。本设计采用 AT89C51-24PU(24MHz 晶振),确保定时器 T0 工作在方式 1(16 位定时)时,最大定时间隔为 65536 × (12 / 24000000) ≈ 32.768ms,通过 30 次溢出即可获得 983.04ms 的近似 1s 基准,误差仅 0.17%,远优于软件延时。
提示:Proteus 中 AT89C51 元件名必须为
AT89C51(注意大小写),若误选AT89C52或AT89C51RC,仿真时定时器中断响应会延迟 2~3 个机器周期,导致倒计时跳变。
2.2 数码管显示电路设计:共阴极 + 74HC138 + 74HC245 驱动链解析
交通灯需同时显示南北/东西两组倒计时(如“45”“32”),采用 4 位共阴数码管动态扫描。关键不是“能亮”,而是“扫得稳、不重影、功耗低”。本设计选用 3611BS 共阴数码管(4 位一体,共阴极),配合 74HC138(3-8 译码器)选通位码,74HC245(双向总线驱动器)驱动段码。接线逻辑如下:
| AT89C51 引脚 | 功能 | 接外部器件引脚 |
|---|---|---|
| P0.0–P0.6 | 段码 D0–D6 | 74HC245 A0–A6 |
| P2.0–P2.2 | 位选 A/B/C | 74HC138 A/B/C |
| P2.3 | 使能 G | 74HC138 G1 |
| P2.4 | 方向锁存 EN | 74HC245 DIR |
74HC138 输出 Y0–Y3 分别接 4 位数码管的公共阴极;74HC245 的 B 端接数码管 a–g 段。当 P2.0–P2.2=000 且 P2.3=1 时,Y0 有效,第一位数码管被选中;此时 P0 口输出对应数字的段码(如“4”为 0x66),经 74HC245 驱动后点亮。动态扫描频率设为 800Hz(每位显示 1.25ms),由定时器 T1 控制,避免人眼察觉闪烁。
2.2.1 段码查表与 BCD 拆分代码实现
// 数码管段码表(共阴,a~g+dp,dp=0) code unsigned char seg_table[10] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F}; // 将两位十进制数拆为高位/低位 BCD void split_bcd(unsigned char num, unsigned char *high, unsigned char *low) { *high = num / 10; // 十位 *low = num % 10; // 个位 } // 主循环中调用(假设 north_time = 45) unsigned char north_high, north_low; split_bcd(north_time, &north_high, &north_low); P0 = seg_table[north_high]; // 显示十位 P2 = 0x08; // P2.3=1, P2.0-P2.2=000 → Y0 选通 delay_ms(1); // 保持 1.25ms 需精确计时,此处仅为示意注意:实际应用中
delay_ms()必须用定时器中断实现,否则主循环阻塞会导致灯状态更新卡顿。本设计将扫描时序与灯状态切换统一交由 T0 中断服务程序调度。
2.3 交通灯状态机建模:红黄绿三态迁移与时间约束条件
交通灯不是简单“红→绿→黄→红”,而是双方向互锁的有限状态机(FSM)。本设计定义 4 个主状态:
| 状态编号 | 名称 | 南北灯 | 东西灯 | 持续时间 | 迁移条件 |
|---|---|---|---|---|---|
| S0 | 南北通行 | 绿 | 红 | 45s | 时间到 → S1 |
| S1 | 南北黄闪 | 黄(闪) | 红 | 5×1s | 闪烁 5 次 → S2 |
| S2 | 东西通行 | 红 | 绿 | 32s | 时间到 → S3 |
| S3 | 东西黄闪 | 红 | 黄(闪) | 5×1s | 闪烁 5 次 → S0 |
关键约束:黄闪阶段必须严格 5 次,每次亮 0.5s、灭 0.5s;倒计时在 S0/S2 阶段递减,在 S1/S3 阶段冻结并显示“00”;按键修改时间仅在 S0/S2 初始时刻允许(避免运行中突变导致冲突)。状态迁移由全局变量current_state和state_timer控制,state_timer在 T0 中断中每 100ms 自增,满 10 即为 1s。
2.3.1 状态迁移核心逻辑(Keil C51)
// 全局变量声明 unsigned char current_state = S0; unsigned int state_timer = 0; // 100ms 计数器 unsigned char north_time = 45; // 南北绿灯时间 unsigned char east_time = 32; // 东西绿灯时间 void timer0_isr() interrupt 1 { TH0 = 0xFC; TL0 = 0x18; // 24MHz 下 100ms 定时(方式1) state_timer++; if (state_timer >= 10) { // 1s 到 state_timer = 0; switch(current_state) { case S0: // 南北通行 if (north_time > 0) north_time--; else { current_state = S1; north_time = 5; } // 进入黄闪,重置为5次 break; case S1: // 南北黄闪 if (north_time > 0) north_time--; else { current_state = S2; east_time = 32; } // 黄闪结束,切东西通行 break; case S2: // 东西通行 if (east_time > 0) east_time--; else { current_state = S3; east_time = 5; } break; case S3: // 东西黄闪 if (east_time > 0) east_time--; else { current_state = S0; north_time = 45; } break; } } }提示:
north_time在 S1 阶段被复用为闪烁次数计数器,避免新增变量;所有时间变量均为unsigned char,天然溢出处理(255→0),但需确保初始值 ≤255,符合交通灯实际需求。
3. Keil C51 编程实现:从定时器初始化到按键消抖的完整代码结构
3.1 定时器 T0/T1 初始化与中断优先级配置
T0 用于 100ms 基准时钟(驱动状态机与倒计时),T1 用于 1.25ms 数码管扫描(保证无频闪)。两者均工作在方式 1(16 位定时),需手动重装初值。晶振频率 24MHz,机器周期为 12 / 24MHz = 0.5μs。
| 定时器 | 功能 | 定时值计算 | 初值(THx, TLx) |
|---|---|---|---|
| T0 | 100ms | 100000μs / 0.5μs = 200000 | 0xFC18 |
| T1 | 1.25ms | 1250μs / 0.5μs = 2500 | 0xF00C |
void timer_init() { TMOD = 0x11; // T0/T1 均为方式1 TH0 = 0xFC; TL0 = 0x18; // T0 100ms 初值 TH1 = 0xF0; TL1 = 0x0C; // T1 1.25ms 初值 ET0 = 1; ET1 = 1; // 开启 T0/T1 中断 EA = 1; // 开总中断 TR0 = 1; TR1 = 1; // 启动定时器 }注意:
TMOD寄存器必须一次性写入0x11,不可分两次设置高/低 4 位,否则可能触发非法模式。Keil C51 中interrupt 1对应 T0,interrupt 3对应 T1。
3.2 数码管动态扫描中断服务程序(T1 ISR)
T1 中断每 1.25ms 触发一次,负责刷新一位数码管。使用静态数组display_buf[4]存储待显数字(索引 0~3 对应千百十个位),scan_index指向当前扫描位。
unsigned char scan_index = 0; unsigned char display_buf[4] = {0, 0, 0, 0}; // 初始化为0 void timer1_isr() interrupt 3 { TH1 = 0xF0; TL1 = 0x0C; // 重装初值 // 关闭前一位(防止重影) P2 &= 0xF0; // 清除 P2.0-P2.3 // 输出段码 P0 = seg_table[display_buf[scan_index]]; // 选通当前位:P2.0-P2.2 = scan_index, P2.3 = 1 P2 = (scan_index << 0) | 0x08; // 更新扫描索引 scan_index = (scan_index + 1) & 0x03; // 0→1→2→3→0 循环 }3.2.1 倒计时数据到显示缓冲区的实时同步
主循环中不直接操作 P0/P2,而是更新display_buf。南北/东西时间各占两位,按“十位、个位、十位、个位”顺序存入:
// 假设 north_time=45, east_time=32 display_buf[0] = north_time / 10; // 4 display_buf[1] = north_time % 10; // 5 display_buf[2] = east_time / 10; // 3 display_buf[3] = east_time % 10; // 2此设计使扫描与状态更新解耦:T1 负责“怎么显”,主循环或 T0 ISR 负责“显什么”。
3.3 独立按键消抖与时间修改逻辑
使用两个独立按键:K1(功能键,进入设置模式),K2(加一键)。硬件接 P1.0/P1.1,低电平有效。消抖采用“两次采样法”:第一次检测到低电平后延时 10ms,再检测仍为低则确认有效。
bit key_press_flag = 0; void key_scan() { static unsigned char key_state = 0; unsigned char key_read; key_read = P1 & 0x03; // 读取 K1(K1=P1.0), K2(K2=P1.1) switch(key_state) { case 0: // 等待按键 if (key_read != 0x03) key_state = 1; break; case 1: // 延时消抖 delay_ms(10); key_state = 2; break; case 2: // 确认按键 if ((P1 & 0x03) == key_read && key_read != 0x03) { if (key_read == 0x02) { // K1按下(0x02 = P1.0=0,P1.1=1) key_press_flag = 1; // 触发设置模式 } else if (key_read == 0x01) { // K2按下(0x01 = P1.0=1,P1.1=0) if (key_press_flag) { if (current_state == S0 || current_state == S2) { if (current_state == S0) north_time = (north_time + 1) % 100; else east_time = (east_time + 1) % 100; } } } } key_state = 0; break; } }提示:
key_press_flag为设置模式标志,仅在 S0/S2 状态下允许修改时间,避免黄闪阶段误操作。%100保证时间值始终在 0~99 范围内,适配两位数码管显示。
4. Proteus 仿真关键配置与常见故障排查:从元件库缺失到时序错位的闭环验证
4.1 Proteus 元件库补全与 VSM 模型参数校准
Proteus 默认库中AT89C51模型需手动关联 HEX 文件才能仿真运行。常见错误是“仿真不动”或“LED 不按预期切换”,根源多为以下三项未配置:
- HEX 文件路径:双击 AT89C51 元件 →
Program File栏填入 Keil 编译生成的.hex文件绝对路径(如D:\traffic\Traffic.hex),路径含中文或空格会导致加载失败; - 晶振频率:在元件属性中
Clock Frequency必须设为24MHz,与 Keil 中Target选项卡的Crystal (MHz)一致,否则定时器初值计算失效; - VSM 仿真引擎:菜单
System→Set Animation Options→ 勾选Use Simulation Clock,禁用Real Time Mode,否则仿真速度受宿主机性能影响,无法精确测时。
提示:若 Proteus 提示
Could not load VSM model for AT89C51,说明模型文件损坏,需重新安装 Proteus 完整版(非精简版),或从官网下载Proteus 8.13+附带的Library Update包。
4.2 时序错位故障定位:用虚拟示波器抓取 P1.0/P1.1 波形
当交通灯出现“绿灯只亮 0.5s”或“黄灯连续闪烁不停”时,大概率是定时器初值计算错误或中断嵌套冲突。Proteus 内置虚拟示波器(Oscilloscope)可直接观测 IO 口电平变化:
- 将探针连接 P1.0(南北绿灯)、P1.1(东西绿灯);
- 设置时基为
100ms/div,触发源选P1.0上升沿; - 运行仿真,观察波形周期:正常 S0 状态下 P1.0 应维持高电平 45 个 100ms 周期(即 4.5s),若实际只有 22 个,则说明
state_timer未清零或north_time递减逻辑错误。
4.2.1 典型故障对照表
| 故障现象 | 可能原因 | 验证方法 | 修复措施 |
|---|---|---|---|
| 数码管全暗或乱码 | P0 口未配置为输出模式 | 查 Keil 代码中P0 = 0xFF是否执行 | 在main()开头添加P0 = 0xFF |
| 黄灯闪烁次数不足 5 次 | north_time在 S1 阶段被其他逻辑修改 | 在 T0 ISR 中添加if(current_state==S1) { ... }调试打印 | 确保 S1 阶段仅对north_time执行--操作 |
| 按键响应延迟或重复触发 | 消抖延时delay_ms(10)被优化掉 | 查 KeilOptimization等级是否为Level 9 | 将delay_ms()函数声明为reentrant或降为Level 3 |
| 东西灯与南北灯同亮 | 位选信号P2.0-P2.2未隔离 | 用逻辑分析仪观测P2.0-P2.2是否同时为低 | 检查P2 = (scan_index << 0) | 0x08是否覆盖了其他位 |
4.3 实物烧录前的 HEX 文件兼容性检查
Keil 生成的 HEX 文件需满足 Intel Hex 格式规范,否则 STC-ISP 或其他烧录工具无法识别。关键检查点:
- 文件首行必须为
:020000040000FA(扩展线性地址记录); - 数据行长度字段(第 3-4 字符)应为偶数,且每行不超过 16 字节;
- 最后一行必须为
:00000001FF(结束记录)。
可用记事本打开 HEX 文件,搜索:00000001FF是否存在。若缺失,说明 Keil 输出配置错误:Output选项卡中勾选Create HEX File,Hex File Format选Intel Extended。
5. 进阶技巧:用 74HC165 扩展输入与 74HC595 扩展输出实现 8 路交通灯控制
当设计升级为十字路口 8 方向(含左转箭头灯)时,AT89C51 的 32 个 IO 口迅速耗尽。此时需用串行扩展芯片替代直接驱动,既节省 IO 又提升抗干扰能力。
5.1 74HC165 并行输入转串行:采集 8 路传感器信号
74HC165 是 8 位并行输入、串行输出移位寄存器。将车流量检测红外对管信号接入D0-D7,CLK接 AT89C51 的P3.4(T0 外部计数输入),SH/LD接P3.5(负载控制)。工作流程:
P3.5 = 0:并行加载数据;P3.5 = 1:启动移位,每来一个CLK上升沿,Q7输出最高位;- AT89C51 用
P3.0(RXD)作为数据输入引脚,通过软件模拟 SPI 读取 8 位。
unsigned char hc165_read() { unsigned char i, data = 0; P3_5 = 0; _nop_(); P3_5 = 1; // 加载并启动移位 for(i = 0; i < 8; i++) { data <<= 1; if(P3_0) data |= 0x01; // 读 RXD 电平 P3_4 = 0; _nop_(); P3_4 = 1; // CLK 上升沿 } return data; }注意:
P3.0和P3.4必须配置为普通 IO 模式(P3M1 &= ~0x01; P3M0 &= ~0x01),禁用串口和定时器复用功能。
5.2 74HC595 串行输出转并行:驱动 8 路 LED 灯组
74HC595 是 8 位串行输入、并行输出锁存器。SER(数据)接P1.0,SRCLK(移位时钟)接P1.1,RCLK(存储时钟)接P1.2。发送一字节后,RCLK上升沿将数据锁存到输出端。
void hc595_send(unsigned char dat) { unsigned char i; for(i = 0; i < 8; i++) { P1_0 = (dat & 0x80) ? 1 : 0; // 送最高位 dat <<= 1; P1_1 = 0; _nop_(); P1_1 = 1; // SRCLK 上升沿 } P1_2 = 0; _nop_(); P1_2 = 1; // RCLK 上升沿,更新输出 }将南北直行、南北左转、东西直行、东西左转各 2 盏灯(共 8 路)分别接Q0-Q7,通过hc595_send(0x33)即可同时点亮南北直行绿灯(Q0/Q1)与东西左转绿灯(Q4/Q5),实现复杂相位控制。此方案使 AT89C51 仅用 3 个 IO 即可控制 8 路输出,IO 利用率提升 266%。
本文还有配套的精品资源,点击获取