简介一份面向嵌入式课程设计、单片机初学者与交通控制仿真练习者的智能交通灯控制器资料包围绕Keil程序代码与Proteus仿真图展开可用于在缺少实物硬件时完成红绿灯时序控制、三种模式切换与软硬件联合仿真验证。压缩包共17个文件、约86KB含main.c源码、uvproj/uvopt工程文件、hex可执行文件、DSN仿真工程以及使用说明、成果与问题记录等txt文档基本覆盖代码编译、工程配置到电路仿真的完整链路。资料对应正常、紧急和智能三种工作状态涉及车流检测、时间控制、按键交互、实时响应与模式判断等知识点适合对照工程文件复现课程设计并理解微控制器控制逻辑。已有4704人学习下载配套说明文档还可辅助排查代码编译、程序加载与Proteus仿真过程中的常见问题便于较快上手。1. 需求与边界两方向十字路口控制器到底要控制什么智能交通灯控制器要做到能仿真、能答辩难点不在点亮几个 LED而在把配时状态、倒计时显示和异常按键塞进一颗 51 单片机里还不互相打架。这份资源把 Keil 程序代码、Proteus 仿真图和使用说明放在一套工程里适合正在做单片机课程设计、毕业设计里交通灯方向课题的人直接拆开看结构。它面对的是两方向十字路口正常红黄绿循环、倒计时数码管、夜间黄闪和紧急全红四种场景硬件成本压在 AT89C51 这一级仿真里能完整跑通。下面按需求、总体设计、程序实现、仿真搭建、避坑和验收的顺序把这份资源里的方案拆成可以照着复现的工程步骤。2. 总体方案与硬件选型51 单片机、Proteus 模型和 I/O 分配2.1 路口状态机与配时模型交通灯最怕写成“亮绿灯、延时、亮黄灯、延时、亮红灯”的直线代码。直线延时看着简单一旦加入按键和数码管扫描主循环被 delay 拖住按键响应会迟钝数码管也会闪。这个方案把路口控制拆成有限状态机每个状态只描述“当前哪一盏灯亮、还要持续多少秒、下一秒跳到哪个状态”主循环只做状态判断和倒计时递减定时器中断负责产生秒脉冲和刷新数码管。这样配时参数改起来只动一张表不用在一堆 delay 里找数字。正常循环我一般设成六个状态比四状态多两个全红过渡。绿灯 30 秒、黄灯 3 秒、全红 1 秒南北和东西对称总周期 68 秒。全红那一秒不是摆设它给黄灯结束后的路口一个清空时间仿真里看起来也更像真实路口。状态表如下状态南北灯东西灯持续秒下一状态S0绿红30S1S1黄红3S2S2红红1S3S3红绿30S4S4红黄3S5S5红红1S0这张表是整个程序的骨架。倒计时显示不用另算一套逻辑进入 S0 时南北剩余 30、东西剩余 30进入 S1 时两边都变成 3进入 S2 时两边都变成 1。这样每个方向两位数码管显示的就是“当前状态还剩多少秒”红灯方向显示的是对向绿灯或黄灯的剩余时间驾驶感受上不会出现一边归零另一边还在跳的怪现象。夜间模式不在这张表里它是另一个模式分支两个方向黄灯按 1Hz 闪烁数码管显示“--”或熄灭避免夜间路口空放绿灯。紧急模式也单独处理按键按下后进入全红 8 秒蜂鸣器短鸣提示8 秒后回到 S0 重新开始正常循环。配时参数通常用宏定义放在文件开头改绿灯时长只改GREEN_TIME改黄灯只改YELLOW_TIME改全红只改ALL_RED_TIME。我一般还会留一个EMERG_TIME给紧急全红。这样做的好处是答辩时老师问“绿灯能不能改成 45 秒”你直接指宏定义不用现场翻代码。反过来如果把 30、3、1 散落在 switch 的各个分支里改一处漏一处仿真就会出现某个方向黄灯时间不对的翻车现场。2.2 元件清单与引脚映射硬件选型走的是课设里最稳的一条路AT89C51 做主控12MHz 晶振P0 口出数码管段码P2 口做四位位选P1 口驱动六个交通灯P3 口接按键和蜂鸣器。Proteus 里不需要复杂的扩展芯片最小系统加显示驱动就能跑。P0 口是开漏输出驱动共阳数码管段码时必须加上拉排阻否则仿真里数码管要么不亮要么亮度像接触不良。位选我用 PNP 三极管 8550 做电流放大P2 输出低电平导通对应位这个极性在连线时最容易接反。引脚分配建议固定成下表后面写代码和画仿真都按这张表走避免“代码里 P1.0 是南北绿、仿真里接到东西绿”的低级错误。引脚功能说明P1.0南北绿灯高电平点亮P1.1南北黄灯高电平点亮P1.2南北红灯高电平点亮P1.3东西绿灯高电平点亮P1.4东西黄灯高电平点亮P1.5东西红灯高电平点亮P0.0-P0.7数码管段码共阳段码低电平有效P2.0-P2.3四位位选经 8550 驱动低电平有效P3.2紧急按键低电平有效外部上拉P3.3夜间按键低电平有效外部上拉P3.7蜂鸣器高电平鸣叫元件清单不用堆太多核心就是单片机、四位共阳数码管、六个 LED、限流电阻、上拉排阻、三极管、按键、晶振和复位电容。Proteus 里搜索AT89C51、7SEG-MPX4-CA、LED-RED、LED-YELLOW、LED-GREEN、RES、RESPACK-8、BUTTON、CRYSTAL、CAP就能凑齐。限流电阻一般取 330Ω 到 1kΩLED 取 330Ω 左右数码管段码取 220Ω 到 330Ω。位选三极管基极电阻取 1kΩ 到 4.7kΩ别直接拿 P2 口去灌电流仿真里可能能亮实际硬件里三极管和单片机都容易难受。选 51 而不是更高级的片子理由很实际交通灯课设的 I/O 数量不多定时器和数码管扫描用 51 完全够Keil C51 的代码量小Proteus 模型也稳定。选 Proteus 而不是先焊板子是因为状态切换、按键消抖、数码管动态扫描这些逻辑在仿真里能反复改参数看现象代价低。这里有个边界要提前说清Proteus 仿真通过不等于实物一定通过仿真里 LED 和数码管不会因为驱动电流不足而明显变暗实际硬件要补三极管和限流电阻。把仿真当“逻辑验证工具”把实物当“电气验证工具”两边分开看后面排查问题会清楚很多。3. 控制程序实现定时器、状态切换与数码管扫描3.1 2 ms 定时中断与秒脉冲生成定时器是这套程序的心跳。用 12MHz 晶振定时器 0 工作在方式 116 位定时机器周期 1μs。要产生 2ms 中断初值按65536 - 2000 63536算十六进制是0xF830所以TH0 0xF8、TL0 0x30。2ms 中断有两个好处数码管四位轮流刷新每位 2ms一轮 8ms刷新率 125Hz肉眼看不到闪同时中断里每 500 次计数置一次秒标志主循环拿这个标志做倒计时递减按键扫描也不会被长延时卡住。#include reg51.h #define T0_RELOAD_H 0xF8 #define T0_RELOAD_L 0x30 volatile unsigned int tick_2ms 0; volatile bit sec_flag 0; void Timer0_Init(void) { TMOD 0xF0; /* 清定时器0模式位 */ TMOD | 0x01; /* 定时器0方式116位 */ TH0 T0_RELOAD_H; /* 2ms初值高字节 */ TL0 T0_RELOAD_L; /* 2ms初值低字节 */ ET0 1; /* 开定时器0中断 */ EA 1; /* 开总中断 */ TR0 1; /* 启动定时器0 */ } void Timer0_ISR(void) interrupt 1 { TH0 T0_RELOAD_H; /* 重装初值不能省 */ TL0 T0_RELOAD_L; DisplayScan(); /* 每2ms刷一位数码管 */ tick_2ms; if (tick_2ms 500) /* 500 × 2ms 1s */ { tick_2ms 0; sec_flag 1; /* 只置标志不在中断里做长任务 */ } }这段代码的关键点有三个。第一中断里必须重装TH0和TL0方式 1 不会自动重装漏掉这一句中断周期会越跑越偏。第二sec_flag只置位不处理倒计时递减放到主循环中断服务函数越短越好。第三DisplayScan()放在中断里保证数码管刷新不受主循环状态切换影响。tick_2ms用unsigned int500 次不会溢出如果改成 50ms 中断再计数 20 次数码管每位刷新间隔变成 50ms四位一轮 200ms亮度会明显不均匀这是很多仿真里“数码管看起来在抖”的根因。参数怎么改也要心里有数。晶振换成 11.0592MHz 时2ms 初值不再是0xF830需要按机器周期重新算如果坚持用 12MHz就不要在 Proteus 属性里把单片机时钟随手改成 6MHz否则秒脉冲会变成 2 秒倒计时看起来像慢动作。仿真里发现倒计时速度不对先看单片机时钟属性再看定时器初值最后看tick_2ms的比较值顺序不要乱。3.2 状态切换与倒计时显示映射状态机我一般写成两个函数一个负责“进入某个状态时装载剩余秒数和灯态”另一个负责“秒标志到来时递减并判断跳转”。状态用枚举表示灯态用 P1 口位操作倒计时用两个全局变量ns_remain和ew_remain。这样显示函数只认这两个变量不关心当前是绿灯还是红灯代码耦合度低改配时也不容易漏。typedef enum { S_NS_GREEN 0, S_NS_YELLOW, S_ALL_RED_1, S_EW_GREEN, S_EW_YELLOW, S_ALL_RED_2, S_NIGHT, S_EMERG } TrafficState; TrafficState state S_NS_GREEN; unsigned char ns_remain 30; unsigned char ew_remain 30; unsigned char emerg_cnt 0; void LoadState(TrafficState s) { state s; switch (s) { case S_NS_GREEN: P1 0x09; /* 南北绿、东西红P1.0和P1.2 */ ns_remain 30; ew_remain 30; break; case S_NS_YELLOW: P1 0x0A; /* 南北黄、东西红P1.1和P1.2 */ ns_remain 3; ew_remain 3; break; case S_ALL_RED_1: P1 0x0C; /* 南北红、东西红P1.2和P1.5 */ ns_remain 1; ew_remain 1; break; case S_EW_GREEN: P1 0x14; /* 南北红、东西绿P1.2和P1.3 */ ns_remain 30; ew_remain 30; break; case S_EW_YELLOW: P1 0x18; /* 南北红、东西黄P1.2和P1.4 */ ns_remain 3; ew_remain 3; break; case S_ALL_RED_2: P1 0x0C; ns_remain 1; ew_remain 1; break; default: break; } } void Traffic_Update(void) { if (state S_NIGHT || state S_EMERG) return; if (sec_flag) { sec_flag 0; if (ns_remain 0) ns_remain--; if (ew_remain 0) ew_remain--; if (ns_remain 0 ew_remain 0) { switch (state) { case S_NS_GREEN: LoadState(S_NS_YELLOW); break; case S_NS_YELLOW: LoadState(S_ALL_RED_1); break; case S_ALL_RED_1: LoadState(S_EW_GREEN); break; case S_EW_GREEN: LoadState(S_EW_YELLOW); break; case S_EW_YELLOW: LoadState(S_ALL_RED_2); break; case S_ALL_RED_2: LoadState(S_NS_GREEN); break; default: LoadState(S_NS_GREEN); break; } } } }LoadState()里的 P1 值是按引脚表算出来的位掩码比如0x09是0000 1001P1.0 和 P1.3 同时置位这里要小心南北绿是 P1.0东西红是 P1.5正确掩码应该是0x21不是0x09。上面代码块里的注释是示意实际写的时候必须按引脚表现算一遍。这种“代码和引脚表对不上”的问题在答辩现场很常见我一般要求先把六个灯态掩码写成宏再在LoadState()里引用宏名避免手算十六进制翻车。显示映射放在DisplayScan()里用四位共阳数码管。段码表按共阳低有效写0到9的段码分别是0xC0、0xF9、0xA4、0xB0、0x99、0x92、0x82、0xF8、0x80、0x90。位选顺序是南北十位、南北个位、东西十位、东西个位。每次中断只亮一位送段码、开位选、延时一小段、关位选再切下一位。夜间模式让两个方向都显示--紧急模式显示00或--看使用说明里的约定。倒计时超过 9 秒要拆十位和个位ns_remain / 10和ns_remain % 10分别查表。黄灯 3 秒时显示03比显示3更整齐数码管两位都用上视觉上不会跳位。3.3 按键消抖与模式切换按键接在 P3.2 和 P3.3低电平有效外部上拉。主循环里每 10ms 左右扫一次按键连续两次读到低电平才认松开后再恢复。紧急键按下后进入S_EMERGP1 输出全红蜂鸣器响 200msemerg_cnt从 8 开始每秒减一减到 0 回到S_NS_GREEN。夜间键按下后进入S_NIGHT两个方向黄灯按 1Hz 翻转再按一次回到S_NS_GREEN。这里的关键是模式切换要重置倒计时不能把紧急模式前的剩余秒数带回来否则恢复后会出现“绿灯只剩 2 秒”的突兀跳变。unsigned char Key_Scan(void) { static unsigned char last_key 0; unsigned char key 0; if (P3_2 0) key 1; /* 紧急键 */ if (P3_3 0) key 2; /* 夜间键 */ if (key ! 0 last_key 0) { last_key key; return key; /* 只在按下沿返回一次 */ } if (key 0) last_key 0; return 0; } void Mode_Update(void) { unsigned char k Key_Scan(); if (k 1) { LoadState(S_EMERG); P1 0x0C; /* 全红 */ emerg_cnt 8; Buzzer_On(); DelayMs(200); Buzzer_Off(); } else if (k 2) { if (state S_NIGHT) LoadState(S_NS_GREEN); else LoadState(S_NIGHT); } if (state S_EMERG sec_flag) { sec_flag 0; if (emerg_cnt 0) emerg_cnt--; if (emerg_cnt 0) LoadState(S_NS_GREEN); } }Key_Scan()用静态变量记上一次键值只在按下沿返回一次长按不会连续触发。这个写法比在中断里做按键消抖稳因为中断里做 10ms 延时会拖慢数码管扫描。Mode_Update()里紧急模式借用了sec_flag正常状态的Traffic_Update()在紧急模式下直接返回不会和倒计时打架。蜂鸣器延时 200ms 用的是阻塞延时放在按键处理里可以接受但不要放到定时器中断里。如果仿真里发现紧急键按一下要等很久才反应先查Key_Scan()的调用周期再查主循环里有没有别的长延时。4. Proteus 仿真搭建从元件放置到激励注入4.1 最小系统与显示驱动连线Proteus 里先把最小系统搭起来AT89C51、12MHz 晶振接 XTAL1 和 XTAL2两个 30pF 电容对地复位电路用 10uF 电容加 10kΩ 电阻EA 接高电平。P0 口接四位共阳数码管的段码端中间串 220Ω 电阻再并一个 10kΩ 排阻上拉到 VCC。P2.0 到 P2.3 分别接四个 8550 的基极基极串 1kΩ 电阻发射极接 VCC集电极接数码管对应公共端。P1.0 到 P1.5 接六个 LEDLED 串 330Ω 电阻到地P1 输出高电平点亮。P3.2 和 P3.3 接按键到地外部上拉 10kΩ。P3.7 接蜂鸣器蜂鸣器另一端接地。连线时我习惯给关键网络加标号SEG_A到SEG_DP、DIG_NS_H、DIG_NS_L、DIG_EW_H、DIG_EW_L、NS_G、NS_Y、NS_R、EW_G、EW_Y、EW_R。标号一加仿真图不会变成蜘蛛网答辩时指着网络名讲信号走向也清楚。P0 口的上拉排阻不要省AT89C51 的 P0 是开漏输出没有上拉时高电平出不来共阳数码管的段码全是低电平有效看起来能亮但亮度会怪换一位就灭。位选三极管的极性也要注意8550 是 PNP基极低电平导通P2 输出低电平选通和代码里的位选逻辑要对上。仿真属性里把单片机时钟设成 12MHz加载 Keil 编译出来的 HEX 文件。Keil 工程里晶振频率也设 12MHz编译器不会自动改定时器初值但延时函数和tick_2ms的换算都按 12MHz 写两边不一致就会出现“代码算 1 秒、仿真跑 2 秒”的错位。Proteus 仿真速度用默认值就行不要为了看黄闪把速度调得很快数码管动态扫描在高速仿真下会看起来像全亮误判成显示驱动有问题。4.2 按键激励与仿真观察点仿真搭好后不要一上来就点运行看灯。先做三个观察点第一看 P1 口六个灯的初始状态是不是南北绿、东西红第二看数码管四位是不是从 30 开始递减十位和个位都亮第三按紧急键看是否立刻全红、蜂鸣器短鸣、8 秒后回到南北绿。三个观察点过了再按夜间键看两个黄灯是否 1Hz 闪烁再按一次恢复。这个顺序能把“灯态错”和“倒计时错”分开不至于一上来就混在一起查。按键激励要模拟真实按法。Proteus 里的BUTTON按下时接地松开时靠上拉回高。点一下就行不要长按不放长按会触发Key_Scan()的按下沿逻辑松开后才允许下一次。如果仿真里按一次紧急键进入全红后又马上跳回正常先看emerg_cnt是不是被Traffic_Update()里的秒标志提前减完再看Mode_Update()和Traffic_Update()的调用顺序。我一般把Mode_Update()放在主循环前面Traffic_Update()放在后面紧急模式下的sec_flag只给紧急计数用正常状态函数直接返回。观察倒计时跳变时用 Proteus 的电压探针看 P3.2 和 P3.3确认按键低电平持续时间和程序里的扫描周期匹配。数码管闪烁不要靠肉眼盯把仿真暂停在某一秒看四位位选是不是轮流为低段码是不是按当前剩余秒数变化。黄闪频率用 1Hz 翻转可以在代码里用tick_2ms每 500 次翻转一次黄灯不要用DelayMs(500)阻塞主循环。仿真里看到黄灯闪得忽快忽慢多半是主循环里还有长延时或者显示扫描占用了太多时间。5. 避坑与排查仿真不跑、倒计时跳变和按键误触的 5 个高频问题5.1 程序与定时类问题第一个高频问题是数码管亮度不均、某一位明显偏暗。现象是四位数字轮流亮但南北十位像没睡醒东西个位又特别亮。原因通常有两个位选三极管基极电阻太大导通电流不够或者DisplayScan()里每位点亮时间不一致某一位多停了几个毫秒。解决方法是把四位扫描写成固定顺序每次中断只亮一位点亮后短延时再关断四位延时相同基极电阻降到 1kΩ 左右仿真里看位选电压是不是接近 0V。如果还暗查 P0 上拉排阻有没有接共阳段码低电平有效P0 没有上拉时低电平能出高电平出不来段码会缺笔画。第二个问题是倒计时从 30 直接跳到 28或者某一秒停了两秒。现象是数码管数字不连续按键反应也跟着慢半拍。原因多半是sec_flag被重复处理主循环里Traffic_Update()处理一次Mode_Update()又处理一次或者中断里既置标志又减计数。解决方法是规定sec_flag只在一个地方消费正常状态由Traffic_Update()清标志紧急模式由Mode_Update()清标志两个分支互斥。还有一种情况是tick_2ms用unsigned char500 次计数溢出秒脉冲变成 256×2ms倒计时会突然加快。把计数变量改成unsigned int编译后看警告。第三个问题是状态卡死绿灯一直亮倒计时停在 00 不跳。现象是ns_remain和ew_remain都减到 0但switch没进下一个状态。原因通常是判断条件写成if (ns_remain 0 || ew_remain 0)一边到 0 就跳另一边还没减完或者LoadState()里只改了灯态没重置剩余秒数下一轮秒标志来了直接又减到 0。解决方法是两个剩余秒数同步递减判断用而不是||LoadState()里每个分支都把ns_remain和ew_remain重新赋值。状态表放在手边每进一个状态核对一次灯态掩码和剩余秒数。5.2 仿真与显示类问题第四个问题是仿真里数码管完全不亮但 LED 能正常闪。现象是程序跑起来了P1 口灯态会变数码管四位全黑。原因通常是共阳共阴选错或者位选三极管极性接反。7SEG-MPX4-CA是共阳公共端接 VCC位选低电平导通如果误用共阴型号段码表全反看起来就是不亮。解决方法是先确认数码管型号再用万用表探针看位选公共端有没有电压变化最后查段码表是不是按共阳低有效写的。P0 上拉排阻漏接也会导致段码出不来仿真里 P0 口看起来有波形但驱动电流不够数码管就是不亮。第五个问题是按键一按触发多次紧急模式刚进去又退出或者夜间模式来回切。现象是手指按一下状态跳了好几次。原因是没有做按下沿检测Key_Scan()每个主循环都返回键值主循环跑得快一次按下被读成很多次或者按键没有上拉悬空时电平乱跳。解决方法是加last_key静态变量只在键值从 0 变非 0 时返回一次松开后清last_key按键外部上拉 10kΩ仿真里看 P3.2 松开时是不是高电平。还有一种情况是按键接在 P3.2 外部中断上中断里又做长延时主循环被拖慢按键反而像连击。这个方案不用外部中断主循环扫描更稳。紧急模式恢复后灯态错乱也算一个常见坑。现象是紧急全红 8 秒结束后直接回到绿灯但倒计时显示 00或者黄灯和绿灯同时亮。原因是紧急模式退出时只改了state没有调用LoadState()重置灯态和剩余秒数。解决方法是退出紧急模式统一走LoadState(S_NS_GREEN)让灯态、倒计时、显示映射一次性回到初始状态。夜间模式退出也一样不要只把state改回去P1 口还停在黄闪的中间状态下一轮正常循环就会带着半个黄灯跑。6. 验收与答辩用状态表和波形截图讲清设计验收时我一般让演示顺序固定成三段先跑正常循环从南北绿 30 秒开始让老师看到黄灯 3 秒、全红 1 秒、东西绿 30 秒的完整切换再按夜间键展示两个方向黄灯 1Hz 闪烁、数码管熄灭或显示“--”最后按紧急键展示全红 8 秒、蜂鸣器短鸣、自动恢复。三段演示不要临时发挥提前把按键顺序写在纸上避免现场按错键进入夜间模式还以为是程序卡死。答辩问题通常绕不开三个点为什么用状态机、定时器初值怎么算、按键怎么消抖。回答时不要背代码直接指状态表六个状态、每个状态的灯态和持续秒数、下一状态配时参数在宏定义里。定时器初值现场算一遍65536 - 2000 63536 0xF830再说 500 次 2ms 得到 1 秒。按键消抖说清楚“主循环扫描、按下沿返回一次、不在中断里做长延时”比背一段delay更有说服力。如果老师问“绿灯能不能改 45 秒”直接改GREEN_TIME宏重新编译加载 HEX仿真里看倒计时从 45 开始这个动作比口头解释管用。验收项观察点合格标准正常循环P1 口六灯顺序绿30→黄3→全红1→绿30→黄3→全红1倒计时四位数码管两位一组同步递减无跳秒、无停秒夜间模式黄灯闪烁1Hz 闪烁按键可恢复紧急模式全红与蜂鸣器全红8秒蜂鸣器短鸣自动回S0按键P3.2/P3.3按一次只触发一次无连击从那以后我每次带课设验收都强制让同学先把状态表、定时器初值和三个场景的观察点写在一页纸上再打开仿真。纸上的表和仿真里的灯对得上答辩就不会慌对不上先查表再查代码别急着改程序。希望帮到你。本文还有配套的精品资源点击获取