简介面向电子工程与嵌入式学习初学者的51单片机Proteus交通灯仿真资源适合用于单片机课程设计、毕业设计或入门实践重点解决定时器、中断、I/O端口控制以及Proteus仿真验证等问题帮助读者掌握从电路搭建、程序编写到在线调试的完整流程。压缩包共14个文件包含C语言源程序、可烧录的HEX文件、Proteus仿真工程DSN和Keil工程文件UV2等其中HEX可直接烧录DSN用于打开仿真电路UV2便于修改重编译压缩包整体仅37KB下载和使用都很便捷。目前已有887人学习下载。通过这套资料学习者可以熟悉交通灯红、绿、黄不同时长例如红灯30秒、绿灯20秒、黄灯5秒的定时控制逻辑掌握定时器配置、中断服务程序写法以及Keil与Proteus联调方法无需真实硬件即可直观观察灯光切换节省硬件成本同时提高程序调试与排错能力并理解实际项目中常见的计数误差与中断处理问题。1. 51单片机Proteus仿真交通灯程序与仿真各占一半缺谁都跑不通把 51 单片机 Proteus 仿真交通灯当成课设来做的人一半卡在程序另一半卡在仿真连不上。这个仿真工程的核心价值在两点一是用一颗最常用的 51 内核芯片把“定时器 状态机”这套基本功跑通二是在 Proteus 里把原理图、元件选型、hex 文件加载整条链路走一遍。它不要求你懂多高深的理论但需要同时驾驭代码和电路两条线。适合正在做单片机课设、备赛基础训练或者只想在电脑上先验证逻辑再碰板子的初学者。另外提醒一句标题里的 Protues 是被写错很多年的拼法正确拼写是 Proteus搜资料时别用错。2. 交通灯逻辑建模先画状态表再写定时器2.1 交通灯的本质是状态机不是 if-else 堆出来的交通灯程序拆开看灯与灯之间是互相约束的东西向亮绿灯时南北向必须是红灯从绿灯切到红灯必须经过黄灯部分路口还需要绿灯先闪烁几次再转黄灯。这些约束如果不用状态机而是直接在 while(1) 里写 if(秒数25) 开灯、if(秒数27) 关灯代码会随着相位增多迅速变成一团乱麻改一个时间参数就要翻半天。我习惯先把一个周期的相位画成状态表表写清楚了程序只是翻译。下面这个表是“东西绿闪 南北绿闪”的六相位版本总周期 60 秒在课设里很常见相位编号阶段名称东西方向南北方向持续秒数0东西直行绿灯红灯251东西绿闪绿灯闪烁红灯32东西黄灯黄灯红灯23南北直行红灯绿灯254南北绿闪红灯绿灯闪烁35南北黄灯红灯黄灯2可以看到任意时刻两个方向不可能同时出现绿灯这就是安全约束。状态 0 到状态 2 是东西方向拥有通行权的完整过程状态 3 到状态 5 轮换给南北方向。表的最后一列可以直接变成程序里的持续时间数组中间一列变成灯控函数里的赋值依据。用状态机的第二个好处是扩展方便。想在状态 0 之后插入一个左转相位只需要在表里加一行、在数组里加一个元素不需要大改主循环逻辑。很多初学者觉得状态机抽象其实交通灯就是状态机最贴近生活的例子把这个仿真做透后面写复杂项目时思路会顺很多。2.2 用定时器中断搭秒基准delay 是交通灯里最大的坑做这个仿真时最容易被忽视的就是时间基准。用 delay 倒计时是新手常见做法但它在交通灯里是翻车的根源delay 期间 CPU 被占死数码管没法动态扫描按键没法响应多个 delay 嵌套还会让时间失真。所以负责任的方案是让硬件定时器产生一个固定间隔的中断在中断里累计出 500ms 和 1s 两个时间标志主循环看到标志置位才去处理业务逻辑。这里选择定时器 0 的方式 1也就是 16 位定时器。以 12MHz 晶振为例机器周期是 1us定时 50ms 需要 50000 个机器周期。16 位计数器从初值数到 65536 溢出所以初值等于 65536 - 50000 15536换算成十六进制就是 0x3CB0。初始化代码如下void Timer0_Init(void) { TMOD 0x01; // 定时器0方式116位 TH0 0x3C; // 初值高8位(65536-50000)/256 TL0 0xB0; // 初值低8位(65536-50000)%256 ET0 1; // 打开定时器0中断 EA 1; // 打开总中断 TR0 1; // 启动定时器 }TMOD 的 0x01 表示定时器 0 工作在方式 1也就是 16 位不自动重装模式。正因为不自动重装中断服务函数里必须手动把初值重新写回去否则定时器会从 0 开始计数溢出周期变成 65.536ms秒数就会越走越快。这个坑我踩过一次后面在避坑章节会专门展开。中断服务函数负责两件事一是重装初值二是累计时间标志。50ms 进一次中断累计 10 次正好 500ms累计 20 次正好 1svolatile unsigned char cnt_50ms 0; volatile unsigned char flag_1s 0; // 1秒标志 volatile unsigned char flag_half_s 0; // 0.5秒标志用于绿灯闪烁 void Timer0_ISR(void) interrupt 1 { TH0 0x3C; // 软件重装初值 TL0 0xB0; cnt_50ms; if (cnt_50ms % 10 0) flag_half_s 1; // 500ms到了 if (cnt_50ms 20) { cnt_50ms 0; flag_1s 1; // 1s到了 } }注意 cnt_50ms、flag_1s 这些变量被中断和主循环同时访问必须加 volatile 修饰否则编译器可能优化成寄存器副本导致主循环读不到最新值表现就是秒数跳变、状态切换失灵。中断函数里只做累加和置位不做复杂逻辑这也是嵌入式开发的铁律。到这里时间基准就搭好了1s 用于相位切换和倒计时500ms 用于绿闪。这套结构在后面加按键、加夜间模式时都能直接复用不会因为加了功能导致时间乱掉。3. 可抄作业的完整程序状态机代码与数码管动态扫描3.1 引脚分配先锁端口再写逻辑写代码之前先定引脚分配。我常用的分配方案是把六个灯全部放在 P1 口这样一次赋值就能控制所有灯逻辑清晰P0 口驱动数码管段码P2 口低四位做位选。具体如下功能端口有效电平说明东西向绿灯P1.0高电平点亮阳极接 P1.0阴极经限流电阻接地东西向黄灯P1.1高电平点亮同上东西向红灯P1.2高电平点亮同上南北向绿灯P1.3高电平点亮同上南北向黄灯P1.4高电平点亮同上南北向红灯P1.5高电平点亮同上数码管段码P0.0-P0.6高电平点亮段共阴数码管数码管位选P2.0-P2.3低电平选中P2 口内部有上拉灯用高电平点亮还是低电平点亮都行。我这里选高电平点亮代码里 1 代表灯亮初学者理解起来更直观。P2 口用于位选是因为它内部自带弱上拉输出低电平时能可靠拉低公共端而 P0 口是开漏结构用来驱动共阴数码管的段码在仿真中能跑通但实物要加外部上拉这一点在第 5 章专门讲。3.2 全局变量与状态数据表引脚分配完就可以把第 2 章的状态表翻译成程序常量了。51 单片机内部 RAM 只有 128 字节把所有常量放在 RAM 里很浪费用 code 关键字把段码表和相位时间表放到 ROM 中#include REG51.H typedef unsigned char u8; typedef unsigned int u16; code u8 SEG_CODE[10] { 0x3F, 0x06, 0x5B, 0x4F, 0x66, // 0 1 2 3 4 0x6D, 0x7D, 0x07, 0x7F, 0x6F // 5 6 7 8 9 }; code u8 PHASE_TIME[6] {25, 3, 2, 25, 3, 2}; u8 current_phase 0; // 当前相位编号 u8 second_left; // 当前相位剩余秒数 u8 ew_sec, ns_sec; // 两个方向数码管显示值 volatile unsigned char cnt_50ms 0; volatile unsigned char flag_1s 0; volatile unsigned char flag_half_s 0;SEG_CODE 是共阴数码管的段码表0x3F 对应数字 00x06 对应数字 1依次到 0x6F 对应数字 9。PHASE_TIME 就是把状态表的持续时间列直接搬过来索引 0 到 5 分别对应东西直行、东西绿闪、东西黄灯、南北直行、南北绿闪、南北黄灯。把状态表变成数组有一个好处改时间只需要改这一行数组。比如课设要求东西直行 30 秒把 25 改成 30 就行不需要动任何逻辑代码。这也是状态机设计带来的可维护性。3.3 灯控函数与相位切换灯控函数的思路是先让 P1 口全部输出 0 灭掉所有灯再根据当前相位点亮对应的灯。这里给每个相位赋一个整数值直接把该亮的灯位写成 1void Set_Lights(u8 phase) { P1 0x00; // 先灭全部灯 switch (phase) { case 0: P1 0x21; break; // 东西绿(bit0) 南北红(bit5) case 1: P1 0x20; break; // 南北红保持东西绿闪烁由主循环翻转 case 2: P1 0x22; break; // 东西黄(bit1) 南北红 case 3: P1 0x0C; break; // 东西红(bit2) 南北绿(bit3) case 4: P1 0x04; break; // 东西红保持南北绿闪烁由主循环翻转 case 5: P1 0x14; break; // 东西红 南北黄(bit4) } }0x21 等于二进制 00100001表示 bit0 和 bit5 为 1也就是东西绿灯和南北红灯同时亮。0x0C 等于 00001100表示 bit2 和 bit3 为 1即东西红灯和南北绿灯亮。这样一次赋值就把两个方向的灯都设置好了。这里有一个细节绿闪状态 1 和状态 4 在 Set_Lights 里没有点亮绿灯绿灯的亮灭完全交给主循环翻转对应 bit。这么做的原因是闪烁需要周期性翻转而翻转时不能影响其他灯的状态用异或操作只翻转一个 bit 最安全。相位切换逻辑放在主循环中每次 1s 标志到来时让 second_left 减一减到 0 就切到下一个相位void main(void) { Timer0_Init(); current_phase 0; second_left PHASE_TIME[0]; Set_Lights(current_phase); while (1) { if (flag_1s) // 1秒时间到 { flag_1s 0; if (second_left 0) second_left--; ed_sec second_left; // 数码管显示当前相位剩余秒数 ns_sec second_left; if (second_left 0) // 当前相位结束 { current_phase; if (current_phase 5) current_phase 0; second_left PHASE_TIME[current_phase]; Set_Lights(current_phase); } } if (flag_half_s) // 0.5秒时间到处理绿闪 { flag_half_s 0; if (current_phase 1) P1 ^ 0x01; // 翻转东西绿灯 if (current_phase 4) P1 ^ 0x08; // 翻转南北绿灯 } Display_Scan(); // 数码管动态扫描 } }主循环的逻辑很短处理秒计数、处理闪烁、扫描数码管。两个方向数码管这里都显示当前相位的剩余秒数这是课设里最常见、也最简单直观的显示方式。如果你想让两个方向分别显示各自红灯剩余时间可以维护两个独立计数器在状态切换时重新赋值即可。3.4 数码管动态扫描消隐与位选共阴数码管动态扫描的核心是“分时点亮”同一时刻只有一位数码管通电靠人眼视觉暂留形成多位数码管同时显示的效果。扫描函数如下void Display_Scan(void) { static u8 pos 0; // 当前扫描位0~3 P2 0xFF; // 消隐先关闭所有位选 P0 0x00; // 段码清零防止拖影 switch (pos) { case 0: // 第一块数码管个位东西向 P0 SEG_CODE[ew_sec % 10]; P2 0xFE; // P2.00选中第一个公共端 break; case 1: // 第一块数码管十位东西向 P0 SEG_CODE[ew_sec / 10]; P2 0xFD; // P2.10 break; case 2: // 第二块数码管个位南北向 P0 SEG_CODE[ns_sec % 10]; P2 0xFB; // P2.20 break; case 3: // 第二块数码管十位南北向 P0 SEG_CODE[ns_sec / 10]; P2 0xF7; // P2.30 break; } pos; if (pos 3) pos 0; }先消隐再送段码是为了防止“拖影”。如果先选通位选、再更新段码上一位的数字会在新段码写入前短暂显示在当前位上肉眼看起来就是数字边缘发虚。P2 口低四位分别控制两块数码管的两个公共端低电平选中和共阴数码管的极性正好匹配。这套扫描函数放在主循环里调用每次只刷一位四位数码管转一圈只需要几个微秒刷新频率远高于人眼闪烁阈值。也正因为主循环里没有长时间阻塞的 delay这个刷新频率才稳定。如果你用的是一体化四位共阴数码管位选逻辑完全一样只是公共端编号不同改一下 P2 的位选值即可。4. Proteus 建仿真工程从元件布局到加载 hex 文件4.1 元件清单在 Proteus 里搜什么程序写完下一步是在 Proteus 里搭电路。很多初学者卡在这一步是因为元件名搜不准。Proteus 的库是英文命名必须知道确切的搜索关键词。我把这个仿真需要的元件整理成一张表元件Proteus 搜索关键词数量设置参数51 单片机AT89C511Clock Frequency 12MHz红色发光二极管LED-RED2无绿色发光二极管LED-GREEN2无黄色发光二极管LED-YELLOW2无电阻RES6330 欧姆两位共阴数码管7SEG-MPX2-CC2共阴按钮BUTTON1预留扩展使用放置元件的方法是点击左侧工具栏的 Pick from Libraries输入关键词后双击元件再在原理图上点击放置。注意 7SEG-MPX2-CC 是两位共阴一体数码管库里有同名的共阳版本 7SEG-MPX2-CA区别只在最后一个字母。选错了段码表和高低电平逻辑全部要对调这是最容易翻车的地方。LED 的限流电阻我建议直接给 330 欧姆。Proteus 里电阻默认值是 10K如果忘记改LED 的亮度会很暗看起来就像接触不良。改阻值的方式是双击放置好的电阻在 Resistance 属性里输入 330。4.2 连线要点先画电源地再连灯和数码管布线顺序直接影响排查效率我一般按下面这个顺序来。先把电源和地的网络铺好再连灯和数码管最后连控制信号。第一步从左侧工具栏选 Power Terminal Mode拖一个 GROUND 端子放到原理图空白处。这个地是所有 LED 阴极、数码管公共端的公共回路没有它整个电路不工作。第二步放置 AT89C51双击芯片把 Clock Frequency 设为 12MHz。Proteus 默认的 AT89C51 电源引脚是隐藏的默认已经接好 VCC 和 GND不需要额外画电源引脚。第三步放 6 个 LED阳极接 P1.0 到 P1.5阴极串一个 330 欧姆电阻后接到 GROUND。方向很重要接反了 LED 永远不亮。Proteus 里 LED 符号的三角形一端是阳极横杠一端是阴极。第四步放两块 7SEG-MPX2-CC。每块数码管有 7 个段码引脚分别接 P0.0 到 P0.6DP 引脚悬空不接。第一块的公共端 C1 接 P2.0、C2 接 P2.1第二块的 C1 接 P2.2、C2 接 P2.3。两个公共端对应数码管的十位和个位接反了显示的数字就是颠倒的。需要注意P0 口段码与单片机引脚之间不需要串电阻仿真里直连即可实物制作时需要在 P0 口加上拉电阻和限流电阻这个差别在第 5 章详细说。4.3 生成 hex 文件并加载到单片机Proteus 本身不编译 C 语言它只认 hex 文件。所以需要先用 C51 编译器把程序编译成 hex再加载到 Proteus 的 AT89C51 中。这里以 Keil C51 工程为例。在 Keil 里新建工程时芯片型号选择 Atmel 的 AT89C51把上一章的 main.c 加入工程。然后打开 Project 菜单里的 Options for Target切到 Output 选项卡勾选 Create HEX File 复选框。这个勾选默认是关闭的不打开的话编译结果里只有 .axf 文件Proteus 加载不了。编译成功后hex 文件会生成在工程对应的输出目录中。回到 Proteus双击原理图中的 AT89C51在 Program File 一栏点击文件夹图标选择刚才生成的 hex 文件。这一步做完单片机内部就有了可执行程序。在 Proteus 里点左下角的运行按钮仿真就开始跑。如果此时灯和数码管都没反应不要急着怀疑接线先回到 AT89C51 的属性界面确认 Program File 路径是否有效。很多情况是 hex 文件路径含中文Proteus 解析不了把工程路径改成纯英文再重新生成一次。4.4 仿真验证用秒表对时长把时间临时调短仿真跑起来只是第一步还要验证程序逻辑是否正确。两个方向的车行灯必须严格互斥也就是说任意时刻 P1 口不能同时出现东西绿灯和南北绿灯。你可以暂停仿真观察 P1 口六个引脚的电平也可以直接肉眼看 LED。我建议做两件事。第一拿手机秒表对一个完整周期 60 秒检查每个相位的持续时间是否符合状态表。第二把 PHASE_TIME 数组临时改成 {2, 1, 1, 2, 1, 1}重新编译加载。这样 8 秒就能跑完一个周期不用盯着屏幕干等一分钟就能验证所有相位切换和绿闪节奏。验证完再把时间数组改回去。绿闪节奏也要单独看状态 1 和状态 4 的绿灯应该以 0.5 秒为间隔闪烁也就是亮 0.5 秒、灭 0.5 秒和真实路口绿闪的频率一致。如果发现闪烁过快或过慢检查 flag_half_s 的置位逻辑和 cnt_50ms 的累加方式。5. 交通灯仿真避坑指南5 个最常见的翻车现场做这个仿真的人十个里有七八个会撞上下面这几个问题。每条都是先描述现象再给原因最后给解决方法。照着排查能省下大半天时间。5.1 数码管不亮或显示乱码现象仿真运行后数码管要么全灭要么显示的内容和代码对不上比如应该显示 5 却显示成 2。原因最常见的是元件选错把共阳数码管当成共阴放了。共阳和共阴的段码表刚好反逻辑公共端的有效电平也相反代码不可能兼容两种情况。其次是位选信号极性写反共阴公共端需要低电平选中代码里却写成了高电平选中。解决回到元件选择列表确认放进去的是 7SEG-MPX2-CC 而不是 7SEG-MPX2-CA。再看 Display_Scan 里 P2 的输出值共阴必须用 0xFE、0xFD 这套低电平有效值。最后做个对照测试写一个测试代码让 P0 输出 0x3FP2 输出 0xFE如果第一位数码管显示 0说明硬件接法和代码逻辑匹配否则就是段码或位选接反。5.2 秒数跳变不走 25、24、23 的正常递减现象数码管倒计时从某个值瞬间变成 0或者直接跳到一个随机数字完全没有递减过程。原因99% 是共享变量没有加 volatile。flag_1s 在中断里被置位主循环里读取如果编译器把它优化到寄存器里主循环永远读不到新值状态切换就会错乱。另外中断服务函数里如果忘了重装初值定时器溢出周期完全不可控秒数自然不对。解决把 cnt_50ms、flag_1s、flag_half_s 全部用 volatile 修饰这是硬性要求。中断里加 TH0 0x3C、TL0 0xB0 两行重装代码不要漏。这两处检查完秒数跳变问题基本消失。如果还跳检查 interrupts 是否被意外关掉。5.3 LED 灯不亮或亮度不均匀现象六个灯里有几个能亮有几个明显暗或者全部亮但光线很弱和教程里的效果差很多。原因电阻阻值不对。Proteus 里新放上去的电阻默认 10K10K 限流会让 LED 电流只有零点几毫安亮度必然不够。不同颜色的 LED 正向压降不同同样的阻值下绿光和红光亮度也会有差异。解决双击每个电阻把 Resistance 改为 330。如果需要更亮的视觉效果可以减小到 220。不建议小于 100因为单片机 IO 口灌电流能力有限阻值太小可能反向拉低引脚电平导致逻辑判断异常。仿真里调到一个肉眼清晰、不刺眼的亮度即可。5.4 仿真正常真机焊板后数码管昏暗现象Proteus 里一切正常秒数、状态切换、绿闪都对但按同样的程序焊到开发板或洞洞板上数码管明显偏暗甚至段码缺笔。原因Proteus 仿真模型不会精确模拟 51 单片机 P0 口的开漏特性。真实芯片的 P0 口内部没有上拉晶体管输出高电平时驱动能力极弱直接驱动共阴数码管段码必然亮度不足。这属于仿真和实物的电气差异不是程序问题。解决实物制作时在 P0 口外接一个 10K 的电阻排上拉到 VCC给段码提供高电平驱动源。同理LED 的限流阻值也要按实物的 LED 工作电流重新计算不能直接抄仿真的参数。这个坑提醒我们仿真验证的是逻辑正确性不是电气可行性。5.5 加载 hex 后单片机完全没反应现象程序编译成功hex 文件也加载到 AT89C51 了但运行后一个灯都不亮数码管也全灭。原因第一Program File 路径无效常见于工程路径包含中文或空格。第二晶振频率被改成 0 或者一个不合理的值导致单片机内部时钟异常。第三复位引脚状态不对Proteus 的 AT89C51 虽然默认隐藏电源引脚但外部复位电路未连接时某些版本模型会默认处于复位状态。解决先双击 AT89C51在 Program File 里重新选择 hex 文件并把路径改成纯英文。确认 Clock Frequency 为 12MHz。把 RST 引脚通过一个 10K 电阻接地再通过一个按键接 VCC组成标准复位电路。这样处理后绝大多数“毫无反应”的问题都能解决。提示如果以上五条都排查完仍然异常用 Proteus 的调试菜单查看单片机引脚电平。把某个灯直接接一个固定高电平确认灯本身没问题再逐步缩小范围到程序或连线。6. 最后一公里给交通灯加上夜间模式与按键优先课设做到仿真跑通只是及格线想要拿高分还得让系统看起来“聪明”。这里提供一个投入产出比最高的扩展按键切换夜间黄闪模式。夜间车流量小真实路口的信号灯会切换为黄灯闪烁模式提醒车辆减速观察通过。用 51 单片机实现这个逻辑很简单——在原有状态机之外加一个运行模式位。按下 P3.2 口接的按键进入夜间模式主循环不再执行相位切换而是让两个方向的黄灯同时以 1Hz 的频率闪烁。sbit KEY_NIGHT P3^2; bit night_mode 0; // 0正常运行1夜间黄闪 // 放在 main 的 while(1) 中 if (KEY_NIGHT 0) { while (KEY_NIGHT 0); // 等待按键释放简单消抖 night_mode !night_mode; P1 0x00; // 清空所有灯 } if (night_mode) { // 夜间模式两个黄灯同时闪烁 if (flag_half_s) { flag_half_s 0; P1 (P1 0x12) ? 0x00 : 0x12; // 0x12bit1bit4两个黄灯 } } else { // 正常运行时的秒计数、相位切换、数码管扫描逻辑 }这里的 0x12 是 P1.1 和 P1.4 同时为 1也就是东西黄灯和南北黄灯都点亮。每 0.5 秒翻转一次视觉上就是两个黄灯同步闪烁。按键检测放在主循环里由于循环一直快速执行按键响应几乎没有延迟。如果你想更进一步可以在另一个按键上实现应急车辆优先通行按下后强制切换到全红 2 秒再让某个方向绿灯常亮 15 秒结束后恢复原相位。核心思路是“模式优先级高于相位切换”在进入正常相位切换前先检查应急标志。这个扩展能体现你对状态机的理解程度答辩时比单纯跑通仿真更有说服力。我自己的习惯是每加一个功能先在纸上把模式流转画出来再动手改代码。第一次做这个仿真时我就是因为跳过了状态表直接写逻辑结果加夜间模式时把相位切换改得一团糟。后来养成一个习惯任何仿真工程的程序改动都先问自己“我在改状态机的哪个环节”这个习惯让我后来的项目少踩了很多坑。希望帮到你。本文还有配套的精品资源点击获取