
简介面向51单片机初学者的交通灯控制系统仿真资料包包含完整程序源码与Proteus仿真电路适合电子工程、嵌入式系统课程设计或自学练手。资源基于Proteus搭建红黄绿三色LED灯组模型配合51单片机C语言程序演示利用定时器与中断实现红灯30秒、绿灯20秒、黄灯5秒的常见时序控制逻辑。包内共14个文件核心内容为C源码、HEX烧录文件与DSN仿真工程另有编译生成的OBJ、LST、M51等辅助文件及备份文件便于读者对照分析程序编译和仿真加载过程整体压缩包仅37KB下载和传输都很轻量。已有887人学习下载口碑与实用性经过验证。通过该资料读者可直观理解51单片机I/O口控制、定时器中断编程以及Proteus虚拟仿真流程同时为后续扩展倒计时显示、按键控制等复杂交通灯功能提供基础参考。1. 51单片机Proteus仿真交通灯这个项目到底在解决什么很多人第一次接触51单片机Proteus仿真交通灯时误以为它不过是把几个LED点亮、再让它们轮流亮灭的小儿科。实际上这个看似简单的组合电路里藏着定时器精确定时、状态机设计、动态数码管扫描、IO口驱动能力判断、中断处理这五道关卡任何一个环节想当然都会让仿真直接翻车。它能帮你用最少的时间和硬件成本把单片机从流水灯带进真实控制的场景也就是处理有严格时序约束、有多路输出协同、还有外部突发事件打断控制逻辑的系统。这个项目最适合两类人一类是刚学完定时器和中断、需要一个小而完整的系统练手的学生另一类是准备做课程设计或竞赛基础模块的开发者。它给你的不是眼花缭乱的功能清单而是一条完整闭环电路图长什么样、程序状态怎么切、HEX文件怎么烧进Proteus的芯片里、跑起来不对时从哪里查。把这些摸透后面再做按键矩阵、LCD显示、PWM调速都是同一套思维逻辑的延伸。2. 仿真交通灯的硬件电路设计在Proteus里搭出十字路口的骨架2.1 元件选型与Proteus工程初始化在Proteus里新建工程时我习惯直接用AT89C51作为主控芯片选它的原因不是性能强而是Keil里编译出来的HEX文件可以直接加载而且Proteus对它的外设模型支持得最稳定。如果你手头习惯用STC系列芯片型号里带89C52的也行只要记得在Proteus里把晶振频率改到和Keil工程一致否则定时时间会整体跑偏。需要从元件库调出的清单大致是AT89C51、RESPACK-8排阻两个、LED-RED/LED-GREEN/LED-YELLOW若干、7SEG-MPX4-CA共阳数码管两到三只、BUTTON按键一个、CRYSTAL晶振12MHz、CAP电解电容和CAP陶瓷电容若干。LED的限流电阻不要漏Proteus虽然不会因为过流烧管子但仿真亮度会失真严重时逻辑电平会被拉垮出现信号冲突的诡异现象。电路连接的核心规则是单片机的P1口接LED指示灯P0口加上拉排阻后接数码管的段码P2口接数码管的位选按键接P3.2INT0外部中断引脚。之所以把P0口加上拉电阻是因为P0是开漏输出不加上拉它根本拉不出高电平这是新手最多踩的坑。晶振接在XTAL1和XTAL2之间两个30pF陶瓷电容分别接地复位电路用10uF电解电容和10k电阻组成上电复位。初始化参数这里先给一个基准晶振12MHz机器周期就是1us定时器0用方式1也就是16位定时器初值设置为0x4C00每50ms进入一次中断20次中断后正好1秒。这个时基是整个交通灯计时的命根子后面的状态切换、黄灯闪烁、数码管倒计时全都建立在它上面。Proteus里的虚拟晶振频率如果不改AT89C51默认跟随工程设置务必和代码注释保持一致。2.2 定时器时基让单片机知道一秒有多长交通灯的所有时序都依赖秒这个基本单位偏偏51单片机本身没有秒的概念它只知道机器周期。12MHz晶振下每个机器周期1us定时器每加1要1us16位定时器从0计到65535需要65.535ms直接拿它当1秒用显然不行。所以常见的做法是让定时器每50ms中断一次软件里再维护一个计数器累计20次就是1秒这样既避开了16位溢出上限又让计时误差可以控制在几个微秒内。定时器0方式1的初始化代码只有几行但初值计算容易栽跟头。50ms等于50000us从计数最大值65536里减去50000得到15536换算成十六进制是0x3CB0。每次中断里先重装初值再累加变量这点顺序不能反——如果先累加再重装第一个中断周期的计时是错的。代码里我会把重装值做成宏定义方便不同晶振频率下快速调整。#define CRYSTAL_FREQ 12000000UL // 晶振频率12MHz单位Hz #define TIMER0_RELOAD 65536UL - CRYSTAL_FREQ / 1000 / 20 // 50ms定时重装值 unsigned char timer_count 0; unsigned int second_count 0; void Timer0_Init(void) { TMOD 0xF0; // 清空定时器0的模式位 TMOD | 0x01; // 定时器0工作在方式116位定时器 TH0 (unsigned char)(TIMER0_RELOAD 8); TL0 (unsigned char)(TIMER0_RELOAD); ET0 1; // 开启定时器0中断 EA 1; // 开启总中断 TR0 1; // 启动定时器0 } void Timer0_ISR(void) interrupt 1 { TH0 (unsigned char)(TIMER0_RELOAD 8); TL0 (unsigned char)(TIMER0_RELOAD); timer_count; if (timer_count 20) { // 20个50ms就是1秒 timer_count 0; second_count; } }这段代码里TMOD 0xF0再置0x01是保险写法不会误伤定时器1的配置。重装初值用了宏展开注意宏里减法运算要加括号否则展开后运算优先级会出错。中断服务函数末尾没有清中断标志因为方式1在进入中断时硬件会自动清TF0手动去清反而可能丢掉下一次溢出。把计时逻辑写得再稳一点应该在主循环里用second_count的变化来触发状态机而不是直接在中断里改led输出。中断函数里只做计数主循环里做判断这样可以避免程序正在切换状态时突然被中断打断导致LED显示和内部计时不同步。很多仿真跑起来灯亮得乱七八糟根源就是把这些职责混在了同一个函数里。2.3 数码管动态扫描倒计时显示的实现前提交通灯上方通常要显示倒计时秒数Proteus里最简单的方案是用7SEG-MPX4-CA这种四位共阳数码管它的8个段码引脚全部并在一起接到P0口四个位选引脚分别接P2.0到P2.3。单片机一次只能驱动一位数码管靠快速轮流点亮各位来造成同时显示的错觉扫描频率必须保证在50Hz以上否则人眼会看到明显的闪烁。段码表要按共阳数码管来编码共阳意味着公共端接高电平某一段接地才亮。数字0对应的段码是0xC0表示除g段外其余段全亮具体换算方式是把a到g和dp对应到P0.0到P0.7按位取反。如果你买的是共阴数码管段码表整体取反即可但Proteus里选7SEG-MPX4-CA就是共阳别搞混。动态扫描虽然代码不长但有个隐藏陷阱位选切换的瞬间如果先送段码再选位会看到上一位的残影。正确的顺序是先把所有位选关掉消隐再送新段码最后打开当前位的位选。三个操作缺一不可顺序反了就出现所谓鬼影在Proteus仿真里表现为数字边缘有淡淡的重影。unsigned char code seg_table[] { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E }; void Display_Scan(void) { unsigned char i; for (i 0; i 4; i) { P2 0xF0; // 关闭所有位选消隐 P0 seg_table[digits[i]]; // 送当前位段码 P2 | (1 i); // 打开第i位 } }消隐这一句P2 0xF0是关键它先把四个位选全部拉低再送段码、开位选。如果漏了消隐前一位的段码会残留到当前位显示就会出现重影。扫描函数要放在主循环里持续调用不能放在定时器中断里做全套显示因为中断里做太多IO操作会把时基拉偏秒计时就不准了。3. 交通灯控制程序状态机才是仿真项目真正的灵魂3.1 主干道与支干道的相位切换逻辑交通灯控制的本质是一个有限状态机整个系统只有四种稳定状态南北绿灯加东西红灯、南北黄灯加东西红灯、东西绿灯加南北红灯、东西黄灯加南北红灯。每次切换都伴随定时器秒数的清零和灯的重新赋值。如果把每个状态看成一段代码块你会发现四个状态之间形成了闭环循环这就是十字路口交通灯最常见的两相位控制方式。我一般用结构体加函数指针来写状态机但51单片机内存小初学者直接用switch语句反而更直观。定义一个全局变量current_state保存当前状态主循环里根据状态值执行对应的逻辑每个状态都要判断当前秒数是否达到该状态的持续时间。持续时间的参数用宏定义列在最前面后面想改成30秒还是20秒改宏就行不用动逻辑代码。南北方向和东西方向的灯要分别控制通常P1.0到P1.2控制南北的红黄绿P1.3到P1.5控制东西的红黄绿。写灯的时候不要整个P1口直接赋值因为那样会把无关位也改变。先用位操作或屏蔽方式更新对应位保证每个方向的灯互不干扰。#define NORTH_SOUTH 0 #define EAST_WEST 1 #define GREEN_TIME 30 #define YELLOW_TIME 3 unsigned char current_state 0; unsigned char second_count 0; void Traffic_StateMachine(void) { switch (current_state) { case 0: // 南北绿灯东西红灯 P1 (P1 0x0C) | 0x21; // 高两位清零保留东西灯置位南北绿、东西红 if (second_count GREEN_TIME) { second_count 0; current_state 1; } break; case 1: // 南北黄灯东西仍红 P1 (P1 0x0C) | 0x11; // 南北黄灯亮东西红保持 if (second_count YELLOW_TIME) { second_count 0; current_state 2; } break; case 2: // 东西绿灯南北红灯 P1 (P1 0x0C) | 0x0A; if (second_count GREEN_TIME) { second_count 0; current_state 3; } break; case 3: // 东西黄灯南北仍红 P1 (P1 0x0C) | 0x12; if (second_count YELLOW_TIME) { second_count 0; current_state 0; } break; } }这里P1口的赋值看起来有点绕原因是P1.0到P1.5被六个灯占用而P1.6和P1.7可能接了别的外设不能随便动。P1 0x0C的作用是先把高两位保存下来再把低六位清空然后用按位或的方式设置需要的灯位。这种写法比直接P1 0x21要严谨尤其当你后面把P1口并给其他模块时能少踩很多坑。3.2 紧急模式按键中断与长鸣时的交通灯切换交通灯项目做到这里已经能正常循环跑起来了。但真正让它像一个完整系统的是紧急模式按下按键模拟救护车或消防车通过时所有方向的红灯亮起让社会车辆停止通行。这个需求用查询方式做也可以但查询会占用主循环的时间而且实时性差所以我选择接到P3.2的INT0外部中断上。外部中断0的初始化很简单IT0置1设为下降沿触发EX0置1使能中断EA置1开总中断。下降沿触发比电平触发好在一次按键只触发一次不需要在中断里等按键释放。不过机械按键按下瞬间会有抖动信号理想做法是加10ms去抖但在仿真环境里按键模型本身不会产生真实的机械抖动直接下降沿触发即可这个差异在真实硬件上要重新考虑。void INT0_Init(void) { IT0 1; // 下降沿触发 EX0 1; // 使能外部中断0 EA 1; // 总中断使能 } void INT0_ISR(void) interrupt 0 { emergency_flag 1; // 设置紧急状态标志 } void Emergency_Handler(void) { if (emergency_flag) { emergency_flag 0; P1 (P1 0x0C) | 0x15; // 南北红东西红 current_state 4; // 进入紧急状态 second_count 0; } }紧急模式下最好让整个路口全红持续一段时间后自动恢复到正常轮转。恢复的方式有两种一种是紧急状态持续固定秒数比如10秒后自动回到正常状态另一种是按下一次按键进入紧急模式再按一次退出。我一般倾向于前一种因为它更贴近真实交通控制逻辑程序上也更好处理。注意紧急模式要单独做一个状态值别直接覆盖正常的四个状态否则退出时不知道回到哪个相位。另外中断服务函数里绝对不要写复杂逻辑比如调用数码管扫描或延时函数。中断里只做标志位设置所有实际处理放在主循环里调用Emergency_Handler函数。中断里做太多事情轻则计时跑慢重则嵌套中断导致系统卡死这在Proteus仿真里很难排查因为程序单步执行时一切正常全速跑就出问题。3.3 倒计时显示与黄灯闪烁的细节处理前面提到用数码管显示倒计时最直观的做法是让两边对称显示剩余秒数南北绿灯剩GREEN_TIME东西红灯剩GREEN_TIME数字随着second_count递减。注意这里的倒计时是基于秒数的而second_count是在中断里递增的所以显示剩余值时不能直接改second_count而是用一个临时变量计算剩余秒数再拆成十进制送入显示缓冲区。黄灯闪烁和倒计时有个典型冲突黄灯阶段只有3秒如果这3秒同时也在数码管上倒计时人眼会同时注意灯的变化和数字跳动反而降低辨识度。常见的折中方案是黄灯阶段数码管不做倒计时只显示0或者保持上一个数字不动让驾驶员专注在灯色切换上。void Update_DisplayBuffer(void) { unsigned char remain; if (current_state 0 || current_state 2) { remain GREEN_TIME - second_count; digits[0] remain / 10; digits[1] remain % 10; digits[2] remain / 10; digits[3] remain % 10; } else { // 黄灯阶段显示灭掉或显示00 digits[0] 0; digits[1] 0; digits[2] 0; digits[3] 0; } }黄灯闪烁的实现反过来要借助second_count的奇偶在黄灯状态下读取second_count的最低位为0时点亮黄灯为1时熄灭黄灯这样黄灯以每500ms切换一次的状态闪烁符合真实交通灯的节奏。关键是在闪烁时不要直接改存储当前状态的变量只改输出到P1口的灯位这样即使黄灯暂时熄灭状态机仍然认为它处于黄灯阶段。数码管的显示缓冲数组digits是全局变量扫描函数Display_Scan直接读取它而Update_DisplayBuffer负责写入两个函数职责分离。这样改显示逻辑时不用动扫描函数扫描频率的调节也只影响显示效果不影响状态机。4. 程序与仿真联调让Proteus里的灯按你的代码亮起来4.1 生成HEX文件并加载到Proteus的完整步骤Keil工程写好代码后第一步是确认Target选项里选择的芯片型号和Proteus里一致。接着打开Options for Target切到Output选项卡勾选Create HEX File这样编译后才会生成可以加载到Proteus的.hex文件。很多人编译时报0 Error但Proteus里芯片不跑原因就是这一步没勾Proteus的芯片里根本没有程序。编译成功后在Proteus里双击AT89C51芯片弹开的属性对话框里有一项Program File点击文件夹图标选择刚才生成的hex文件。这里要注意路径里不能有中文和空格否则Proteus可能加载失败或加载了旧文件。加载完别忘了把晶振频率改到和Keil一致12MHz就写12MHzProteus默认的晶振不一定匹配这是定时不准的第一大来源。设置完成后点击Proteus左下角的运行按钮仿真就开始跑了。正常情况下LED按状态机依次亮灭数码管同步倒计时。如果灯没反应先检查左下角是不是显示Running没有则说明仿真根本没跑起来最常见原因是电路连接有悬空引脚或电源没接全。建议在仿真这一步养成一个习惯先跑最小功能验证也就是只让主循环里的状态机跑起来数码管和按键先注释掉。确认LED正常轮转后再逐个加入数码管扫描、按键中断每加一个模块就全速跑一次。这样做的好处是出问题时能明确知道是哪个模块引入的bug而不是面对整个系统一起不工作。4.2 用Proteus的虚拟仪器验证时序是否准确Proteus里有一个说不上冷门但经常被忽略的功能——虚拟示波器和逻辑分析仪。很多人以为Proteus只能看LED亮灭实际上它能把单片机引脚上的电平变化拉成波形让你精确看到每个灯的翻转时刻。用它来验证交通灯的时序比肉眼盯着LED亮灭靠谱得多。把示波器探头接到P1.0引脚上运行仿真你会看到这个引脚每隔30秒左右跳变一次电平。如果两次跳变之间的间隔明显不是30秒说明定时器初值或晶振配置有误。接两个探头分别看南北绿灯和东西绿灯的波形会发现它们之间的相位差正好是半个周期这正是两相位控制的标准特征。还有一个容易被忽视的验证点是黄灯闪烁频率。把示波器接到黄灯对应引脚正常情况应该是500ms高电平、500ms低电平的方波。如果你看到的波形高电平持续1秒、低电平也持续1秒说明你是按照整秒来切换的频率只有真实黄灯闪烁要求的一半。用示波器一测这个玄学问题当场就能定位不用靠肉眼猜。# 示波器接线参考Proteus操作 # 1. 从左侧工具栏选择虚拟仪器模式 # 2. 选择OSCILLOSCOPE放置到原理图空白处 # 3. 示波器A通道接P1.0B通道接P1.3 # 4. 运行仿真观察A/B两通道的电平翻转时间差4.3 仿真参数调节对照表参数调节是联调阶段最频繁的操作。晶振频率、定时器初值、状态切换时间这三个变量互相纠缠改任何一个都要同步考虑另外两个。下面这张表是我平时调整参数时的对照依据按常见问题场景列出应该改哪里。问题现象可能原因调整参数绿灯持续时间过短GREEN_TIME宏太小修改宏定义中的秒数倒计时每秒跳过多个数second_count累加异常检查timer_count是否被清零整体计时变快晶振频率与Keil不一致核对两处晶振设置相同黄灯不闪烁只常亮闪烁判断逻辑丢失检查second_count奇偶判断数码管显示乱码段码表与共阳设置不匹配检查seg_table或数码管类型按键进入紧急模式后无法退出紧急状态未自动恢复在紧急状态代码中增加计时退出这些参数如果没有特别说明我建议直接用前面代码里的默认值绿灯30秒、黄灯3秒、晶振12MHz、定时器50ms中断。先把这套默认参数跑通再按需调整不要上来就随意改宏否则后面排查时你根本不知道基线应该是什么样。5. 避坑与常见问题排查Proteus仿真交通灯最容易翻车的5个环节5.1 LED完全不亮芯片引脚却输出正常电平现象程序编译无误Proteus正常运行但LED就是不肯亮。用电压探针测量P1口引脚高低电平切换都正常可LED两端电压不对。原因这一类问题九成出在限流电阻和LED的接法上。Proteus仿真不是物理硬件它严格按照电路模型的电气参数计算如果限流电阻选得过大比如10kLED上的分压就远低于点亮阈值。反过来如果忘了接地回路电平再正常也是虚电压。解决把限流电阻换成330欧姆到470欧姆之间确认LED阴极确实接地而不是接VCC。AT89C51的P1口内部有上拉驱动LED时低电平点亮也就是LED阳极接VCC、阴极接P1引脚这种方式或者阳极接P1、阴极经电阻接地取决于你用的方向控制逻辑。5.2 数码管出现残影或数字重叠现象倒计时数字显示不清晰数字边缘带淡影尤其在数字快速变化时严重。改变扫描速度症状有缓解但没根除。原因动态扫描的消隐处理没做到位。位选切换的瞬间前一位的段码残留在IO口上新位选打开时把这个残留段码也显示出来了。Proteus对信号的变化模拟得比较理想化但扫描顺序不对时残影依旧可见。解决严格按消隐、送段码、开位选的顺序写扫描函数。每次切换前先把所有位选关闭然后更新P0口的段码最后打开当前位选。如果已经这么写了还残影检查是否在扫描函数里意外调用了延时导致某一位停留时间过长破坏了均匀扫描。5.3 计时不准绿灯持续时间忽长忽短现象绿灯亮的时间不是固定的GREEN_TIME有时多两三秒有时少两三秒看起来没有规律。原因定时器中断服务函数和主循环之间的second_count读写竞争或者中断里做了耗时操作导致后续中断延迟。Proteus仿真是单核执行中断处理时间过长会延迟下一次中断的触发时刻积累下来就是秒数漂移。解决中断服务函数里只做重装初值、timer_count累加、second_count溢出判断三个动作其余任何IO操作全部挪到主循环。这个看起来像玄学的问题本质是中断服务函数写得太长把定时精度拖垮了。把职责理顺计时立刻恢复正常。5.4 仿真运行极慢LED亮灭像慢动作现象点击运行后仿真确实在执行但LED切换间隔明显比设定的秒数长得多甚至十几秒才跳一次。原因Proteus仿真执行速度受电路规模影响动态数码管扫描本身就在以毫秒级频率刷新IO口再加上定时器中断、按键扫描整个电路的事件量很大。电脑性能不足时仿真速度会低于实时速度。解决在Proteus的Debug菜单里关闭不必要的仿真日志输出特别是Pin State日志它会在仿真时记录每个引脚的每一次变化数据量巨大。同时把数码管扫描函数里不必要的延时去掉扫描周期不要卡着极限。如果还是慢优先考虑简化数码管位数或者把显示刷新率从每一个主循环都刷改成每几个循环刷一次。5.5 按下按键没反应或者按键一次系统跳两个状态现象运行仿真时点击按键紧急模式完全没有触发或者只按了一下状态机却像被按了多次。原因按键没有正确接到INT0引脚或者中断触发方式配置不对。下降沿触发要求引脚平时为高电平按下瞬间拉低产生下降沿。如果按键模型接反平时引脚就是低电平按下去反而变成上升沿中断永远不会触发。解决确认按键一端接P3.2另一端接GNDP3.2内部上拉保持高电平。中断初始化里IT0置1、EX0置1、EA置1三者缺一不可。至于按一次跳多个状态是因为主循环里紧急模式处理函数不断被调用每次调用都恢复或切换状态。解决办法是使用一次性标志处理完立即清零并且在紧急状态期间屏蔽重复触发。6. 从仿真到实物让这个交通灯项目真正变成你的入门之作Proteus仿真跑通只是第一步我建议你做完仿真后再做三件小事把项目彻底吃透。第一件事用示波器把每个状态的翻转时间记录下来做成一张表和代码里设定的时间逐项核对。这个习惯能帮你建立时序敏感度以后做任何带时间约束的项目第一反应就是去测波形而不是瞎猜逻辑。第二件事在现有两相位基础上扩展一个左转相位。南北直行绿灯、南北左转绿灯、东西直行绿灯、东西左转绿灯四相位切换比两相位复杂一个量级状态机从4个状态变成8个状态你会在设计过程中理解状态冲突、互锁防护这些问题。第三件事把LED换成红黄绿三个颜色的交通灯模型Proteus元件库里有TRAFFIC_LIGHTS模型可以直接替换单个LED看起来更接近真实的十字路口。我自己做这个项目时栽过最大的跟头是在一个版本里把消隐和位选顺序写反仿真时数字残影严重我以为是Proteus显示刷新问题折腾了很久才发现是代码顺序错误。从那以后我养成了一个习惯每次改完代码先复位仿真并用示波器观察输出再决定要不要继续。这个项目真正的价值不在于灯能不能亮而在于你建立了从电路图、程序、时序到仿真验证这条完整的排查链路。如果你现在正对着一个跑不起来或跑起来不对的仿真先别急着删代码重写按前面排查表的顺序一条条对照九成问题出在晶振配置、定时器初值、IO口接法这三个地方。希望帮到你。本文还有配套的精品资源点击获取