简介:本资源是一套基于51单片机的交通灯控制系统完整实训方案,面向嵌入式初学者、单片机课程设计学生及硬件实践爱好者,解决交通信号逻辑控制、定时倒计时显示与人机交互扩展等典型教学难点。压缩包共18个文件,包含C语言源码(main.c)、Keil工程文件(uvproj/uvopt)、编译输出(hex/lst/obj)、电路原理图(docx)、设计文档与总结报告(docx/doc)、使用说明(txt)及实物图等,覆盖从原理分析、代码实现到硬件验证的全流程。资源包仅1.28MB,轻量易下载,结构清晰,便于快速导入开发环境并实操调试。已有2694人学习下载,配套资料详实:不仅实现基础红绿黄灯时序(含黄灯闪烁5次)、双方向同步倒计时显示,还提供按键设置主/次干道通行时间及模拟紧急车辆通行的扩展功能,附带多版本程序对比与注意事项详解,显著降低单片机实训入门门槛。
1. 为什么用51单片机做交通灯,反而比STM32更适合作为嵌入式硬件入门项目?
很多刚接触嵌入式硬件的同学一上来就想用STM32——性能强、外设多、生态好。但真要动手搭一个能跑通、能调通、能讲清信号时序的交通灯系统,51单片机反而是更扎实的起点。它资源有限(4KB ROM、128B RAM)、寄存器直观(TMOD、TH0、TL0、IE、IP一目了然)、中断结构简单(两级优先级、5个中断源),逼你直面定时器溢出率计算、数码管动态扫描刷新率、LED驱动电流匹配这些底层硬约束。本项目就是典型:东西/南北两组灯共用一套倒计时逻辑,黄灯必须在绿转红前精确闪烁5次(每次亮灭各200ms),而所有时间显示仅靠2位共阴数码管动态扫描实现——这意味着你得在16ms内完成一次段码+位选切换,否则就会出现“鬼影”或亮度不均。这不是理论题,是实打实的时序博弈。适合电子类、自动化、机电专业做单片机实训的学生,也适合想补足硬件时序思维的嵌入式转岗工程师。项目包里带Proteus可仿真电路、Keil C51工程模板、实物接线图和两版源码(基础版+按键扩展版),从原理图到烧录验证全链路闭环。
2. 定时器T0与T1协同调度:如何用51单片机实现毫秒级精准倒计时与黄灯节拍控制
交通灯的核心不是“亮灯”,而是“按时序亮灯”。本项目要求绿灯60秒→黄灯闪烁5次(每次200ms亮+200ms灭)→红灯60秒,且东西与南北方向状态严格互斥。若用软件延时(如for(i=0;i<10000;i++)),CPU被锁死无法响应按键或刷新数码管;若只用一个定时器,又难以兼顾倒计时秒级精度与黄灯毫秒级节拍。解决方案是T0与T1双定时器分工:T0工作于方式1(16位定时),每50ms溢出一次,作为系统心跳;T1工作于方式2(8位自动重装),每1ms溢出一次,专用于黄灯亮灭计时与数码管扫描基准。
2.1 T0定时器配置:构建50ms系统节拍
// main.c 中定时器初始化部分 void Timer0_Init() { TMOD |= 0x01; // T0方式1,16位定时 TH0 = (65536 - 50000) / 256; // 11.0592MHz晶振下,50ms对应50000个机器周期 TL0 = (65536 - 50000) % 256; ET0 = 1; // 开T0中断 EA = 1; // 开总中断 }提示:51单片机机器周期 = 晶振频率 / 12。11.0592MHz晶振下,1个机器周期为1.085μs,50ms需约46080个机器周期。此处取50000是为简化计算并留出中断响应余量,实际误差由主循环中的计数补偿(见后文)。
T0中断服务程序(ISR)负责三件事:
- 每20次中断(即1秒)递减倒计时变量
sec_count; - 每次中断更新数码管显示缓冲区
disp_buf[2]; - 检查黄灯状态机,触发T1启停。
// T0中断服务函数 void Timer0_ISR() interrupt 1 { static unsigned char t0_cnt = 0; TH0 = (65536 - 50000) / 256; // 重装初值 TL0 = (65536 - 50000) % 256; t0_cnt++; if(t0_cnt >= 20) { // 累计20×50ms=1s t0_cnt = 0; if(sec_count > 0) sec_count--; // 倒计时减1 } // 数码管动态扫描:每50ms刷新一位 static bit digit_sel = 0; if(digit_sel == 0) { P0 = disp_buf[0]; // 送十位段码 P2 = 0xFE; // 位选:点亮十位(P2.0低电平有效) digit_sel = 1; } else { P0 = disp_buf[1]; // 送个位段码 P2 = 0xFD; // 位选:点亮个位(P2.1低电平有效) digit_sel = 0; } }2.2 T1定时器配置:黄灯闪烁与数码管消隐的毫秒级控制器
黄灯要求“闪烁5次”,每次亮200ms、灭200ms,总耗时2s。若用T0的50ms节拍去拆分,需40次中断,状态机复杂且易受主循环干扰。改用T1方式2(8位自动重装)生成1ms基准,通过计数器精确控制:
// 黄灯状态机变量 unsigned char yel_cnt = 0; // 1ms计数器 bit yel_on = 0; // 黄灯当前状态(1=亮,0=灭) unsigned char yel_flash = 0; // 已闪烁次数(0~5) // T1初始化:1ms定时 void Timer1_Init() { TMOD |= 0x20; // T1方式2,8位自动重装 TH1 = 0xFC; // 11.0592MHz下,1ms对应252(0xFC) TL1 = 0xFC; ET1 = 1; // 开T1中断 } // T1中断服务函数 void Timer1_ISR() interrupt 3 { yel_cnt++; if(yel_cnt >= 200) { // 每200ms翻转一次黄灯 yel_cnt = 0; yel_on = ~yel_on; if(yel_on == 0) { // 黄灯灭时计数一次 yel_flash++; if(yel_flash >= 5) { // 闪烁满5次,进入红灯状态 yel_flash = 0; state = RED; // 切换主状态机 } } } }注意:T1的
TH1=0xFC是关键参数。计算公式为:重装值 = 256 - (晶振频率 / 12 / 1000)。11.0592MHz代入得256-922.4= -666.4 → 取模256得252(0xFC)。若用12MHz晶振,则应为TH1=0xEC(236),否则定时不准。
2.3 主状态机设计:红绿灯相位切换与冲突规避
交通灯本质是有限状态机(FSM)。本项目定义4个状态:GREEN_EW(东西绿,南北红)、YELLOW_EW(东西黄,南北红)、GREEN_NS(南北绿,东西红)、YELLOW_NS(南北黄,东西红)。状态切换必须满足:
- 绿灯结束→黄灯启动→黄灯结束→红灯启动;
- 东西与南北状态永不同时为绿;
- 黄灯期间另一方向保持红灯(无冲突)。
// 状态机主循环(在main()中while(1)内执行) switch(state) { case GREEN_EW: P1 = 0x0F; // P1.0~P1.3:东西绿(0),南北红(1); 共阴LED低电平亮 sec_count = 60; // 设定60秒倒计时 if(sec_count == 0) { state = YELLOW_EW; TR1 = 1; // 启动T1控制黄灯 } break; case YELLOW_EW: P1 = 0x07; // 东西黄(P1.2),南北红(P1.0~P1.1,P1.3) if(yel_flash >= 5) { state = GREEN_NS; TR1 = 0; // 关闭T1 } break; case GREEN_NS: P1 = 0xF0; // 南北绿(P1.4~P1.7),东西红(P1.0~P1.3) sec_count = 60; if(sec_count == 0) { state = YELLOW_NS; TR1 = 1; } break; case YELLOW_NS: P1 = 0xE0; // 南北黄(P1.6),东西红(P1.0~P1.3) if(yel_flash >= 5) { state = GREEN_EW; TR1 = 0; } break; }关键点:
P1 = 0x0F等赋值直接操控IO口电平,这是51单片机裸机编程的典型特征。0x0F二进制为00001111,低4位为1(红灯灭)、高4位为0(绿灯亮)——但实际电路采用共阴接法,低电平点亮,故P1.0=0使东西方向红灯亮。务必对照参考电路.docx中LED阳极接VCC、阴极经限流电阻接P1口的拓扑确认逻辑电平极性。
3. 数码管动态扫描与段码映射:2位共阴数码管的高效驱动策略
本项目仅用2位数码管显示倒计时(如“60”、“59”…“00”),却要求无闪烁、无残影、亮度均匀。这依赖两个核心:一是动态扫描频率≥50Hz(即每位刷新间隔≤20ms),二是段码与位选严格时序配合。项目采用T0中断每50ms刷新一位,2位轮询周期100ms(10Hz),看似低于50Hz阈值,但因人眼视觉暂留及LED余辉效应,在实验室环境仍可接受;若需更高品质,可将T0中断周期改为10ms(需重新计算初值)。
3.1 段码表设计:共阴数码管标准编码
数码管段码对应a~g及dp(小数点)共8段。共阴型需输出高电平点亮对应段。标准0~9段码如下(code[10]):
| 数字 | 段码(十六进制) | 二进制(a~g,dp) | 说明 |
|---|---|---|---|
| 0 | 0xC0 | 11000000 | a,b,c,d,e,f亮,g,dp灭 |
| 1 | 0xF9 | 11111001 | b,c亮,其余灭 |
| 2 | 0xA4 | 10100100 | a,b,d,e,g亮 |
| 3 | 0xB0 | 10110000 | a,b,c,d,g亮 |
| 4 | 0x99 | 10011001 | b,c,f,g亮 |
| 5 | 0x92 | 10010010 | a,c,d,f,g亮 |
| 6 | 0x82 | 10000010 | a,c,d,e,f,g亮 |
| 7 | 0xF8 | 11111000 | a,b,c亮 |
| 8 | 0x80 | 10000000 | 全亮 |
| 9 | 0x90 | 10010000 | a,b,c,d,f,g亮 |
// 段码表(共阴,高电平点亮) unsigned char code_table[10] = {0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90}; // 将倒计时数值分解为十位、个位,并查表获取段码 void update_display(unsigned char sec) { unsigned char tens = sec / 10; unsigned char ones = sec % 10; disp_buf[0] = code_table[tens]; // 十位段码 disp_buf[1] = code_table[ones]; // 个位段码 }3.2 位选信号与IO口复用:P2口驱动2位数码管
项目电路使用P2口低2位(P2.0、P2.1)作为位选信号,低电平有效(即P2=0xFE时P2.0=0,十位点亮;P2=0xFD时P2.1=0,个位点亮)。此处存在IO复用风险:若P2口其他引脚连接按键或传感器,位选操作可能干扰其功能。解决方案是在位选前关闭相关外设,或改用专用IO(如P3口)。
// 动态扫描关键代码(位于T0 ISR中) static bit digit_sel = 0; if(digit_sel == 0) { P0 = disp_buf[0]; // P0口输出十位段码 P2 = 0xFE; // P2.0=0,十位选通;P2.1=1,个位关闭 digit_sel = 1; } else { P0 = disp_buf[1]; // P0口输出个位段码 P2 = 0xFD; // P2.0=1,十位关闭;P2.1=0,个位选通 digit_sel = 0; }注意:P0口作为段码输出时,需外接上拉电阻(通常10kΩ)。51单片机P0口是开漏输出,不接上拉则无法输出高电平,导致段码失效。
参考电路.docx中应明确标注此电阻位置,若实物图中未体现,需自行焊接。
3.3 消隐处理:避免位选切换时的“拖影”现象
当从十位切换到个位时,若先关十位再开个位,存在短暂全暗期;若先开个位再关十位,则两位置同时点亮造成数字重叠。正确做法是:在切换位选前,先将P0置为全0(熄灭所有段),再切换位选,最后输出新段码。
// 改进后的动态扫描(防拖影) if(digit_sel == 0) { P0 = 0x00; // 消隐:先关所有段 P2 = 0xFE; // 选通十位 delay_us(10); // 等待10μs稳定 P0 = disp_buf[0]; // 输出十位段码 digit_sel = 1; } else { P0 = 0x00; // 消隐 P2 = 0xFD; // 选通个位 delay_us(10); P0 = disp_buf[1]; digit_sel = 0; }delay_us(10)是微秒级延时,可用空指令实现:
void delay_us(unsigned char us) { while(us--); }该延时确保位选信号建立后再输出段码,彻底消除拖影。
4. 按键扩展功能实现:三键循环设置与紧急通行模式的硬件消抖策略
基础版交通灯已满足课程设计要求,但程序2(有附加功能).txt实现了按键扩展:K1为设置键,K2为确定键,K3为紧急键。按K1一次进入“主干道绿灯时间设置”,二次进入“主干道红灯时间设置”,三次触发“紧急通行”(东西南北全绿灯10秒)。此功能引入两大挑战:按键消抖与长按识别。
4.1 硬件消抖电路与软件滤波双重保障
机械按键存在10~20ms抖动,直接读取会导致多次触发。项目采用RC硬件消抖(参考电路.docx中K1~K3按键并联0.1μF电容接地)+软件延时滤波。软件部分在主循环中检测下降沿后延时20ms再确认:
// 按键扫描函数(放在main() while(1)中) void key_scan() { static unsigned char key_state = 0; unsigned char key_read; key_read = P3 & 0x07; // P3.0~P3.2接K1~K3,低电平有效 if(key_read != 0x07) { // 有键按下(P3.0~P3.2非全1) delay_ms(20); // 硬件消抖后软件延时 key_read = P3 & 0x07; if(key_read != 0x07) { switch(key_read) { case 0x06: // K1按下(P3.0=0) if(key_state == 0) { key_state = 1; set_mode++; // 循环:0→1→2→0 if(set_mode > 2) set_mode = 0; } break; case 0x05: // K2按下(P3.1=0) if(set_mode == 0) green_ew = set_value; // 设置东西绿灯时间 if(set_mode == 1) red_ew = set_value; // 设置东西红灯时间 break; case 0x03: // K3按下(P3.2=0) emergency_flag = 1; break; } } } else { key_state = 0; // 释放 } }提示:
P3 & 0x07屏蔽P3高5位,仅读取低3位按键状态。0x06二进制为00000110,表示P3.0=0(K1按下),P3.1=1、P3.2=1(K2、K3未按)。这种位掩码读取是51单片机按键检测的标准写法。
4.2 长按识别与紧急模式实现
紧急通行要求“K3长按2秒触发”,而非短按。需在按键检测中加入计时:
// 紧急模式长按检测 static unsigned int k3_press_time = 0; if((P3 & 0x04) == 0) { // K3按下(P3.2=0) k3_press_time++; if(k3_press_time >= 200) { // 200×10ms=2s emergency_flag = 1; k3_press_time = 0; } } else { k3_press_time = 0; // 松开重置 }紧急模式下,所有方向绿灯亮,倒计时强制为10秒,且禁止其他状态切换:
// 主循环中紧急模式处理 if(emergency_flag) { P1 = 0x00; // 所有灯绿(P1全0,共阴低电平亮) sec_count = 10; if(sec_count == 0) { emergency_flag = 0; state = GREEN_EW; // 恢复正常流程 } }4.3 时间设置流程与用户交互反馈
设置模式下,需通过数码管显示当前设定值,并允许用户用K1循环增减(每按一次+1,满99归0)。为提升体验,增加闪烁提示:
// 设置模式显示(在T0 ISR中) if(set_mode != 0) { static unsigned char flash_cnt = 0; flash_cnt++; if(flash_cnt < 10) { // 亮500ms if(set_mode == 1) update_display(green_ew); // 显示东西绿灯时间 if(set_mode == 2) update_display(red_ew); // 显示东西红灯时间 } else if(flash_cnt < 20) { // 灭500ms disp_buf[0] = 0x00; disp_buf[1] = 0x00; } else flash_cnt = 0; }5. Keil C51工程配置与Proteus仿真验证:从源码到实物的全流程调试技巧
拿到template.uvproj后,需正确配置Keil才能编译通过。常见问题集中在晶振频率、存储模式、启动文件三处。本项目使用11.0592MHz晶振,ROM大小为4KB(Small模式),必须启用STARTUP.A51启动代码。
5.1 Keil C51关键配置参数表
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| Device | AT89C51 或 STC89C52 | 必须与实物芯片一致,STC型号需额外加载ISP烧录工具 |
| Clock Frequency | 11.0592 MHz | 影响定时器初值计算,必须与电路板晶振匹配 |
| Output → Create HEX File | ✓ 勾选 | 生成.hex文件供烧录器使用 |
| Target → Memory Model | Small | 变量默认存于内部RAM,符合51资源限制 |
| Target → Code Rom Size | 4K | 匹配AT89C51的4KB Flash容量 |
| Output → Browse Information | ✓ 勾选 | 生成调试符号,支持断点调试 |
注意:若使用STC单片机(如STC89C52RC),需在
Project → Options for Target → Target中取消勾选Use On-chip ROM,因其内部Flash地址映射与标准51不同。
5.2 Proteus仿真快速验证步骤
打开
Proteus仿真图(项目未提供,但可依参考电路.docx自行搭建):- 放置AT89C51,晶振11.0592MHz,12pF负载电容;
- P1口接8个LED(东西南北各2红1绿1黄,共8路);
- P0口接2位共阴数码管(a~g段接P0.0~P0.6,dp接P0.7);
- P2.0、P2.1接数码管位选;
- P3.0~P3.2接三个独立按键(上拉至VCC,按键接地)。
加载HEX文件:右键单片机 →
Edit Properties→Program File→ 选择Keil生成的.hex。运行仿真:点击
Play按钮,观察LED状态切换与数码管倒计时是否符合预期。若黄灯不闪,检查T1中断是否开启(ET1=1);若数码管乱码,检查段码表是否为共阴编码。
5.3 实物调试高频故障排查清单
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 数码管全不亮 | P0口未接上拉电阻;位选信号P2.0/P2.1始终为高电平 | 用万用表测P2.0电压,确认按键或程序是否意外拉高;焊接10kΩ上拉电阻至P0口 |
| LED亮度不均 | 限流电阻阻值过大(>330Ω);共阴/共阳接法与段码逻辑相反 | 测LED阳极电压,若为VCC则为共阴,段码需高电平点亮;更换220Ω电阻 |
| 倒计时跳变(如60→58) | T0初值计算错误;中断服务程序中未重装TH0/TL0 | 用示波器测P1.0方波周期,反推定时器误差;检查ISR内TH0/TL0重装语句是否遗漏 |
| 按键无响应 | P3口未配置为输入(51单片机P3默认准双向口,读取前需先写1);硬件消抖电容失效 | 在main()开头添加P3 = 0xFF;;更换0.1μF电容 |
提示:烧录时若提示“校验失败”,大概率是HEX文件与芯片型号不匹配。STC单片机需用STC-ISP工具烧录,AT89C51需用专用编程器(如USBasp)。切勿用STC-ISP烧录AT89C51,会导致芯片锁死。
最后一步验证:用手机慢动作录像拍摄数码管刷新过程,确认每位点亮时间是否严格交替,无重影。这才是51单片机交通灯设计落地的终极标尺。
本文还有配套的精品资源,点击获取