简介:面向51单片机初学者的完整交通灯设计项目,基于Proteus仿真与Keil工程实现十字路口红绿灯实时控制,适合课程设计、电子竞赛或毕业设计参考。资源包共26个文件,大小83.6MB,包含Proteus仿真文件(pdsprj/pdsbak)、Keil工程源码(.c与.uvproj工程)、hex烧录文件,以及两段mp4讲解视频,目录结构清晰,便于直接运行与二次开发。设计实现了东西向20秒、南北向35秒通行,绿灯转红灯前5秒黄灯过渡,两位数码管倒计时显示,并加入夜间模式。配套原理讲解与功能演示视频,详细说明定时器控制、数码管动态扫描、状态切换逻辑等关键实现,可帮助读者快速掌握51单片机交通灯系统的设计思路与调试方法。当前已有1113人学习下载,值得作为实战练手项目。
1. 基于51单片机的交通灯设计:先用状态机思维看这个题目
基于51单片机的交通灯设计,表面上是课程设计里最常见的一道题,实际上最容易在时序安全上翻车。很多人第一次做都能让红黄绿灯亮起来,但东西向和南北向在某个瞬间同时出绿灯的情况,往往要烧录到实物上才暴露。这个题目的完整技术链路其实包含三块:引脚级的硬件驱动、基于定时器中断的秒级状态机、以及从 Proteus 仿真到实物烧录的排错方法。51 单片机虽然结构不算新,但它的并口输出特性、外部中断和定时器方式寄存器仍然非常适合教学,把交叉路口的控制需求映射到具体 I/O 和寄存器上,理解会非常直接。下文用 STC89C52 作为主控,12MHz 晶振,配合 LED 灯组与共阴数码管,按四相位十字路口模型展开,尽量做到仿真和实物都能照着搭。
2. 硬件电路与引脚分配:51 单片机交通灯的最小系统搭建
交通灯最小系统由电源、晶振复位电路、LED 灯组和倒计时显示四部分组成。先确定每个器件的型号和连接方式,再写代码,否则后面所有引脚定义都要回炉。常见的做法是直接用开发板观察引脚功能,但更可靠的是先从图纸上把 I/O 分清楚,再进 Proteus 画图。
2.1 器件选型:STC89C52 与 AT89C51 怎么选
主控选择上,STC89C52RC 和 AT89C51 是两种最常见的方案。两者的内核都是 8051,指令集完全一样,Proteus 里没有 STC 模型时就用 AT89C51 替代仿真,不影响逻辑验证。差异主要体现在下载方式:STC89C52 支持串口下载,用 STC-ISP 软件就能烧写,适合没有编程器的场景;AT89C51 则需要通用编程器,课程设计里反而多一道工具门槛。存储容量上,STC89C52 有 8KB Flash,交通灯程序编译后通常只有 2-4KB,不存在容量压力。
| 元件 | 型号/规格 | 数量 | 用途 |
|---|---|---|---|
| 单片机 | STC89C52RC 或 AT89C51 | 1 | 逻辑控制 |
| 晶振 | 12MHz | 1 | 提供时基 |
| 瓷片电容 | 22pF | 2 | 晶振负载 |
| 电解电容 | 10uF | 1 | 复位电路 |
| 电阻 | 10kΩ | 1 | 复位下拉 |
| 电阻 | 330Ω | 12 | LED 限流 |
| LED | 红/黄/绿 | 12 | 两方向灯组 |
| 数码管 | 共阴两位 | 1 | 倒计时显示 |
| 按键 | 轻触开关 | 2 | 复位/紧急 |
晶振频率选 12MHz 的用意很直接:51 单片机的机器周期是晶振频率的 12 分频,即 1MHz 机器周期,对应 1us。定时器初值计算时,50000us 的 16 位计数可以直接手算,不需要做小数换算。如果换成 11.0592MHz,串口波特率会更准,但交通灯基本不用串口,优先保证定时器计算方便更合算。
2.1.1 Proteus 仿真的主要坑位:时钟和电源
在 Proteus 里搭建电路时,先双击 AT89C51,确认 Clock Frequency 是 12MHz。很多仿真里数码管跳数奇慢、LED 变化节奏不对,问题不在程序,而在仿真的晶振频率没有同步改成 12MHz。其次要检查电源网络,Proteus 默认 VCC 和 GND 是隐藏电源,LED 阳极接 VCC 时不需要额外放电源符号,但有些初学者手动加了一条 5V 电源线后造成默认电源重复,仿真反而报错。
2.2 引脚分配:用 sbit 把灯位固定下来
下文的电路按低电平点亮设计,LED 阳极通过 330Ω 电阻接 5V,阴极接单片机引脚。P1 口分给东西方向三色灯,P2 口分给南北方向三色灯,P0 口驱动数码管段码。
| 引脚 | 外设 | 引脚 | 外设 |
|---|---|---|---|
| P1.0 | 东西红灯 | P2.0 | 南北红灯 |
| P1.1 | 东西黄灯 | P2.1 | 南北黄灯 |
| P1.2 | 东西绿灯 | P2.2 | 南北绿灯 |
| P0.0-P0.7 | 数码管段码 | P2.3-P2.4 | 数码管位选 |
引脚分配到这一步就可以在 Keil 里定义位变量了:
sbit EW_RED = P1^0; // 东西方向红灯引脚 sbit EW_YELLOW = P1^1; // 东西方向黄灯引脚 sbit EW_GREEN = P1^2; // 东西方向绿灯引脚 sbit NS_RED = P2^0; // 南北方向红灯引脚 sbit NS_YELLOW = P2^1; // 南北方向黄灯引脚 sbit NS_GREEN = P2^2; // 南北方向绿灯引脚位定义把寄存器位和业务语义绑定,后续 switch 状态切换时不需要每次写 P1 = 0x06 这种裸数字,读代码的人能直接看懂控制的是哪个灯。如果后续要换引脚,也只需要改位定义,不用动状态机逻辑。
2.2.1 为什么用低电平点亮而不是高电平
51 单片机标准 I/O 口的拉电流能力弱于灌电流:灌电流最大约 20mA,拉电流只有几百微安到几毫安,视芯片型号而定。如果把 LED 接成引脚输出高电平点亮,灯会偏暗,而且多路同时输出高电平时,内部上拉带来的驱动能力更差。改用低电平点亮后,LED 亮的时候引脚是灌电流状态,亮度更稳定。开发板上看到的“杜邦线接口直接插 LED”往往是因为板子已经内置了驱动电路,自制电路时不能直接照搬。
2.3 数码管连接与动态扫描的必要性
倒计时显示需要两位数码管,一位显示十位、一位显示个位。共阴数码管公共端接地,段码通过 P0 口输出。两位数码管的段码线可以并联在同一组 P0 口上,位选单独接 P2.3 和 P2.4,分时点亮。这样总共只占用 10 个 I/O 口,比独立驱动的 16 个 I/O 少得多。
动态扫描原理是快速切换位选,只让某一时刻只有一位数码管被点亮。人的视觉暂留会让看起来像两位同时显示。扫描周期必须控制在 2-5ms 每位,否则会产生闪烁或亮度不均。这个问题从电路图上看不出来,要等实物显示才会暴露,所以代码里必须把扫描动作放进定时器中断,不能在主循环里用 delay 实现。
3. 定时器中断与状态机:交通灯的秒脉冲与相位切换
交通灯控制最典型的坏实践是:主循环里前 25 秒延时点绿灯,再延时 3 秒点黄灯。这样能跑通最简单的演示,但加上数码管扫描、按键检测以后,相互阻塞会让倒计时卡顿。正确思路是把“时间基准”和“业务逻辑”分离:定时器中断负责产生稳定的秒脉冲,主循环只根据秒脉冲推进状态机。
3.1 状态表先行:四相位十字路口模型
十字路口的基础模型可以压缩成 4 个状态。把东西方向称为 EW,南北方向称为 NS,按通行权依次切换。
| 状态 | EW灯 | NS灯 | 保持时间 |
|---|---|---|---|
| S0 | 绿灯亮 | 红灯亮 | 25s |
| S1 | 黄灯亮 | 红灯亮 | 3s |
| S2 | 红灯亮 | 绿灯亮 | 25s |
| S3 | 红灯亮 | 黄灯亮 | 3s |
状态切换顺序固定为 S0→S1→S2→S3→S0,不允许跳转。这样设计保证了每个时刻只有一个方向持有绿灯:S0 到 S1 的切换只是 EW 绿灯熄灭、黄灯点亮,NS 红灯保持不变;S1 到 S2 是 EW 黄灯灭、红灯亮,同时 NS 红灯灭、绿灯亮。危险瞬间出现在这里,所以灯组输出必须由状态值计算,不能用几个独立的亮灯标志自行组合。
3.1.1 丁字路口的变体
如果题目要求的是丁字路口,状态表从 4 个变成 3 个,分别是:西向东直行、东向西直行、南北方向放行。本质上还是同一套方法,先画车流通行优先级,再映射到状态表。这类变体考察的就是会不会用状态机,时序逻辑没变。
3.2 定时器 T0 初始化:50ms 中断为秒脉冲做基准
采用方式 1 的 16 位定时器,初值计算如下:12MHz 晶振时机器周期 1us,要定时 50000us,初值 = 65536 - 50000 = 15536 = 0x3CB0。于是 TH0=0x3C,TL0=0xB0。
void Timer0_Init(void) { TMOD &= 0xF0; // 仅修改定时器0相关位,不干扰定时器1 TMOD |= 0x01; // 定时器0工作方式1,16位定时 TH0 = 0x3C; // 高8位初值:定时50ms TL0 = 0xB0; // 低8位初值 ET0 = 1; // 开定时器0中断 EA = 1; // 开总中断 TR0 = 1; // 启动定时器0 }TMOD 的位操作写得保守一点:先清低 4 位,再按位或置位,避免覆盖定时器 1 的配置。如果项目里还要用定时器 1 做串口波特率,这个习惯能省下排查时间。EA 是总中断开关,ET0 是定时器 0 的局部开关,两个都要置 1 才会进入中断。
3.3 中断计数与状态机主循环
中断服务函数只做三件事:重装初值、50ms 计数、满 20 次置秒标志。不能让中断函数里执行状态切换,因为状态切换涉及多个灯引脚的赋值,如果和主循环发生竞争,可能出现中间态。
unsigned char state = 0; // 当前状态编号,对应4个灯态 unsigned char sec_cnt = 0; // 50ms计时累计 unsigned char sec_flag = 0; // 秒脉冲就绪标志 unsigned char cur_sec = 25; // 当前状态剩余秒数 void Timer0_ISR(void) interrupt 1 { TH0 = 0x3C; // 重装初值,保持50ms周期 TL0 = 0xB0; sec_cnt++; if (sec_cnt >= 20) // 累计1秒 { sec_cnt = 0; sec_flag = 1; // 通知主循环处理 } } void main(void) { Timer0_Init(); state = 0; cur_sec = 25; while (1) { if (sec_flag) // 每秒处理一次 { sec_flag = 0; if (cur_sec > 0) cur_sec--; if (cur_sec == 0) { state = (state + 1) % 4; // 循环切换相位 switch (state) { case 0: cur_sec = 25; break; case 1: cur_sec = 3; break; case 2: cur_sec = 25; break; case 3: cur_sec = 3; break; } } } Light_Output(state); // 根据状态刷新LED Display(cur_sec); // 动态扫描倒计时 } }重点看几个容易出错的地方。interrupt 1 是 Keil C51 里定时器 0 的向量号,不能换成其他数字。初值重装放在中断函数最前面,避免中断响应延迟导致周期偏移。sec_flag 不是直接用 while 等待,而是置 1 后返回,主循环忙轮询时按键和数码管仍然有机会执行,这在稍后加行人按键时尤其重要。
3.4 状态到灯组的输出映射
Light_Output 函数可以写成 switch-case,但更易维护的是查表法。两个颜色方向共 6 个灯,状态编号 0-3 对应 4 组输出。因为采用低电平点亮,对应位写 0 表示亮,写 1 表示灭。
code unsigned char ew_code[4] = {0x05, 0x0D, 0x0A, 0x09}; // 对应状态: EW绿/NS红(0x05), EW黄/NS红(0x0D), // EW红/NS绿(0x0A), EW红/NS黄(0x09) void Light_Output(unsigned char st) { P1 = (P1 & 0xF8) | (ew_code[st] & 0x07); // 只改P1.0-P1.2 P2 = (P2 & 0xF8) | ((ew_code[st] >> 3) & 0x07); // 高3位映射到P2 }这种写法把表拆成 P1 低 3 位和 P2 低 3 位,实际上更适合用位操作逐灯赋值。对初学者来说更直接的版本是六个 sbit 分别赋 0 或 1,避免位运算产生阅读障碍。查表法的好处是状态扩展时只增加数组元素,比如左转灯加入后,表可以扩成 8 个状态。
4. 参数调整与调试:51 单片机交通灯从仿真到实物的 4 个检查点
交通灯从代码到演示,通常要依次经过 Proteus 仿真验证、Keil 编译烧录、实物接线三个阶段。每个阶段有各自的典型故障,按顺序排查效率最高。
4.1 Proteus 仿真:先查晶振频率再查上拉电阻
Proteus 仿真里倒计时快慢不对,九成是芯片时钟频率没同步。双击 AT89C51,在编辑属性里把 Clock Frequency 改成 12MHz。默认的 1MHz 会导致 50ms 定时实际变成 600ms,倒计时的节奏完全不对。还有一个常见问题是 P0 口驱动共阴数码管时亮度不均,这在硬件上是因为 P0 口没有内部上拉,仿真里表现为段码输出高电平的那一段拉不上去。解决办法是在 P0 口加一个 10kΩ 排阻到 VCC,或在段码设计时改用低电平点亮共阳数码管。
4.2 Keil 编译与 STC-ISP 烧录:hex 容易漏,下载容易失败
Keil 建工程时先确认 Target 选项里勾选了 Create HEX File,否则编译成功也生成不了烧录文件。这个选项默认不是勾选的,很多第一次用 Keil 的人烧录时找不到 .hex 就是这个原因。烧录 STC89C52 时,STC-ISP 软件提示“正在检测目标单片机”,此时要给板子重新上电才能完成握手。USB-TTL 转接模块如果供电不稳,把波特率降到 2400 或 4800 重试,成功率明显提高。
提示:如果多次换 USB 口都下载失败,先测量一下板子上的 VCC 是否稳定在 5V,很多 USB-TTL 在劣质线材上压降达到 0.5V 以上,单片机处于欠压状态。
4.3 核心参数速查表
| 参数 | 推荐值 | 设计依据 |
|---|---|---|
| 晶振频率 | 12MHz | 机器周期 1us,定时器初值整数化 |
| 定时器周期 | 50ms | 16 位定时范围 0-65.5ms,50ms 留有余量 |
| 秒累计次数 | 20 次 | 50ms×20 = 1s,误差可忽略 |
| LED 限流电阻 | 330Ω | 5V 电源,红色 LED 压降约 1.8V |
| 绿灯时长 | 25s | 可改成 20-30s,按路口流量调整 |
| 黄灯时长 | 3s | 常见路口的过渡时间 |
| 数码管每位扫描周期 | 2-5ms | 低于 1ms 亮度下降,高于 10ms 闪烁 |
上表里最容易被人忽略的是数码管扫描周期的来源。如果只在定时器中断里放置秒标志,然后在主循环里轮流显示两位数码管,那么显示切换速率会跟随主循环的空转速度,不稳定。我一般会在定时器中断里再加一个 250us 级别的计数,让中断每 2ms 切换一次位选,保证扫描周期固定。
4.4 实物连线的现象与根因对照
把仿真换成实物后,故障现象往往比仿真更直观。下面几条是出现频率最高的:
- LED 半亮或全不亮:先查限流电阻是否接错方向,再测量单片机引脚电压。低电平点亮时,引脚对地压差接近 0V 才是正常。
- 倒计时忽然跳 2 秒:定时器中断初值没有重装,或者主循环里有长延时阻塞了中断响应,累积误差后秒标志丢失。
- 按键按下后灯乱跳:缺少消抖或者状态机里没有加锁定,一次按键被识别成多次,最直接的办法是引入 10ms 延时消抖加状态锁定变量。
- 数码管显示重影:段码更新和位选切换没有同步,先关位选再送段码,最后打开位选,顺序反了就会出现前一位残留。
这四条现象基本覆盖了课程设计评审时的主要扣分点。每一条都能在 5 分钟内定位,关键是不要同时在多个变量上猜测,一次只改一个参数,重新编译看现象。
5. 扩展与验证:行人按钮、紧急模式和夜间黄闪的实现思路
基础状态机稳定后,可以在不改变主框架的前提下增加三个常见功能。它们共享同一个秒脉冲,加功能时不动定时器,只动状态切换条件和输出映射。
5.1 行人过街按钮:压缩当前相位而不是中断状态机
行人按钮接 P3.2 外部中断 0。按下后,当前绿灯相位剩余时间压缩到 5 秒,然后直接进入行人绿灯状态。这里的关键是行人功能不能打断状态机的正常推进,而是允许它提前结束当前相位。加一个行人锁定变量,防止行人绿灯还没结束就反复触发。
unsigned char ped_lock = 0; void EX0_ISR(void) interrupt 0 { if (ped_lock == 0) // 未锁定才响应 { if (cur_sec > 5) cur_sec = 5; // 当前相位压缩到5秒 ped_lock = 1; // 触发一次后锁定 } }外部中断需要初始化 IT0 = 1 选择下降沿触发,EX0 = 1 开通中断。行人相位结束后要把它作为状态机里的新状态来处理,行人绿灯和时间设置进状态表,而不是在中断服务函数里直接点灯,否则又会出现状态竞争。
5.2 紧急模式与夜间黄闪:用扩展状态代替独立逻辑
夜间模式通过 P3.3 引脚检测开关状态。进入夜间模式后,状态机退化为两个状态的黄灯闪烁,每一种状态维持 1 秒,用现有的秒脉冲翻转输出即可。紧急模式则要单独设计,我的做法是四个方向红灯全亮 8 秒,之后强制复位到状态 S0。完整代码比基础版多的部分就是模式选择函数:
void Mode_Process(void) { if (night_mode && sec_flag) // 夜间黄闪 { yellow_toggle = !yellow_toggle; P1 = yellow_toggle ? 0x02 : 0xFF; // 东西黄灯闪 P2 = yellow_toggle ? 0x02 : 0xFF; // 南北黄灯闪 Display(cur_sec); return; } // 非夜间模式走原来的状态机流程 NS_GREEN = 0; NS_YELLOW = 0; EW_GREEN = 0; EW_YELLOW = 0; }这样设计的好处是模式切换不会破坏原有状态机的 cur_sec 计数。退出夜间模式后,状态机从上次中断的位置继续,不需要重新初始化,但要注意退出瞬间灯组输出可能残留,所以在模式入口先全部熄灭再按当前状态点亮,能有效避免视觉上的跳变。
5.3 验证方法:状态跳转边界检查
最后给出一个实用的验证顺序。第一,把绿灯和黄灯时间临时改短,例如绿灯 5 秒、黄灯 1 秒,加快循环以观察状态跳转是否正确。第二,在 Proteus 里把示波器接到东西绿灯和南北绿灯两个引脚上,观察输出波形是否永远存在低电平重叠,即两个绿灯同时点亮的时间窗口是否为零。第三,实机测试按键功能时,每次修改后断电再上电,避免复位状态残留干扰判断。完成这三步验证,交通灯从仿真到实物的主要质量风险就已经排除。
本文还有配套的精品资源,点击获取