尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于51单片机的交通灯状态机设计与Proteus仿真实现

基于51单片机的交通灯状态机设计与Proteus仿真实现 简介基于MSP430F249单片机的红绿黄交通灯控制系统设计包面向单片机原理、计算机组成原理及嵌入式方向的初学者和课程设计学生可用于理解红绿灯控制逻辑、数显倒计时实现及东西/南北方向状态切换等典型实验环节。资源共21个文件压缩包仅164KB涵盖C语言主程序、Proteus仿真DSN原理图、IAR工程的EWP/EWW配置文件、可直接烧录的HEX文件以及用于调试的批处理脚本和状态记录文件完整覆盖从源码编写、仿真验证到下载运行的开发闭环目录结构按工程调试与仿真分类便于快速定位。已有2475人学习下载。方案按红灯30秒、绿灯25秒、黄灯5秒自动循环使用两位共阴极数码管实时显示剩余秒数所有时间参数均可灵活修改。通过学习可掌握MSP430定时器中断、数码管动态扫描及74HC573锁存器驱动方法并能根据需求调整时序适合作为单片机课程设计或实验延伸的参考模板。1. 基于单片机的红绿黄灯设计的核心是把“时间”切成状态城市路口红绿灯最常跑的动作其实是两句话南北放行时东西必须全禁东西放行时南北必须全禁。翻译成单片机逻辑这就是一个三色灯状态机——红灯、绿灯、黄灯不是三个独立程序而是同一个循环在不同时间点切换端口电平。51单片机上这块电路非常简单难的是状态切换时倒计时怎么同步、黄灯只亮 5 秒怎么实现、按键插入紧急模式后状态怎么收尾。这篇文章沿着 Proteus 仿真 51 单片机的流程把从状态表设计到 C 代码实现的完整路径写清楚适合正在准备单片机课程设计、蓝桥杯单片机备赛或者想把手边基于 STM32 交通灯的方案简化到 51 平台的工程师。实际动手后你会发现这个项目九成工作量不在点亮 LED而在时间基准。用 for 嵌套死等 delay灯能亮但按键和倒计时显示会互相拖拽状态一多改起来很难受。能稳定上板子的做法是定时器做毫秒时基主循环只干状态切换和显示扫描。下文代码统一以 AT89C51、12MHz 晶振、Keil C51 为例换到 STC 单片机时只需改头文件并确认引脚定义。2. 交通灯相位拆解与 51 单片机定时器时基设计2.1 相位真值表四个阶段、两种时长先定控制逻辑。以最简单的十字路口为例南北为主干道东西为支路完整周期 60 秒分四个阶段。放行时间 25 秒、黄灯 5 秒是课程设计最常见的参数。黄灯选 5 秒的物理依据是城市道路 60km/h 车速下驾驶员看到黄灯到车辆完全停止约需 30 米加上反应时间5 秒刚刚覆盖交叉口清空距离。时间再短尾车容易滞留在路口内。这里为什么不用“亮灯时间堆叠”而是用相位思维因为任何时刻四个方向总共只有一组灯在动作其余三个方向必须陪跑。用相位表可以避免出现“南北绿灯期间东西也是绿灯”这类致命逻辑错误。以东西方向为基准四个相位的灯态如下阶段时长南北红南北黄南北绿东西红东西黄东西绿相位A25秒关关开开关关相位B5秒关开关开关关相位C25秒开关关关关开相位D5秒开关关关开关从相位 A 进入相位 C中间隔着相位 B南北方向已经给了黄灯缓冲东西方向同理相位 D 承担清空任务。四个相位就是一套完整的安全切换序列。对应到单片机枚举typedef enum { PHASE_A 0, // 南北绿灯25s东西红灯25s PHASE_B 1, // 南北黄灯5s 东西红灯5s PHASE_C 2, // 南北红灯25s东西绿灯25s PHASE_D 3 // 南北红灯5s 东西黄灯5s } TrafficPhase;枚举值同时充当主循环 switch 的 case 编号每个 case 内部只做两件事按 phase 点亮对应方向端口判断倒计时是否归零。灯态与状态的映射必须严格一一对应多写一个 else 分支后期都要反复查看真值表直接在初始化时把相位表固化成宏或函数最能减少低级错误。2.2 T0 装载值计算与定时器初始化AT89C51 的机器周期是晶体频率的十二分频12MHz 晶振下机器周期正好 1 微秒。定时器工作在方式 1 时是 16 位加法计数器从装载值加到 65536 溢出一次。这里需要一个 5 毫秒的稳定时基配合软件计数器累计到 1 秒所以装载值等于 65536 减 5000 等于 60536换算成十六进制是 0xEC78。初始化代码#include reg52.h void Timer0_Init(void) { TMOD 0x01; // 只配置T0为方式116位定时器 TH0 0xEC; // 装载值高字节0xEC TL0 0x78; // 装载值低字节0x78合计0xEC78 ET0 1; // 打开T0中断允许位 EA 1; // 打开全局中断允许位 TR0 1; // 置位后T0开始计数 }TMOD 0x01 只把低四位设为 0001高四位仍为 0即 T1 不受影响。如果板子上 T1 已被串口占用这里要写成TMOD 0xF0; TMOD | 0x01;两条语句避免覆盖 T1 的方式字。TH0、TL0 写入顺序没有要求但这两个寄存器合起来才是完整的计数值初始化封装成函数后后面所有重装都依赖同一对常量别在中断里另算一遍。中断服务函数里最容易被忽略的是重装初值volatile unsigned int tick5ms 0; volatile unsigned int secCnt 0; void Timer0_ISR(void) interrupt 1 using 1 { TH0 0xEC; // 方式1不是自动重装载必须在中断里重新赋值 TL0 0x78; if (tick5ms 200) // 200次 x 5ms 1s { tick5ms 0; secCnt; // secCnt每秒加1主循环读取后决定是否切相位 } }using 1 给中断服务函数分配独立寄存器组可以减少任务切换时的入栈出栈量但寄存器组深度有限项目里中断嵌套变多时不如依赖 Keil 的自动管理。secCnt 声明为 volatile 是这里的关键主循环读它之前必须临时保存快照因为中断可能发生在任何一条指令之间直接拿 secCnt 做多次比较会造成前后值不一致。2.3 为什么不用纯软件延时时基统一放在中断里的收益单片机 C 语言程序设计实训里流水灯程序用 delay 没问题但交通灯不同主循环要同时响应按键、刷新倒计时、切换灯态任何一个阻塞式延时都会让另外两件事卡住。把时基放进定时器中断后主循环只需要关心“secCnt 是否到了 25 或 5”显示和按键即使偶尔被拉长 2 毫秒时间基准也不会漂移。这个设计直接决定后面状态机能不能写薄。状态机本身不产生时间它只消费时间所有延时都沉淀在中断里主循环每个 case 都是纯逻辑。STC 单片机换过来时唯一要改的是晶振频率常数12MHz 换成 11.0592MHz 时装载值要按 65536 减 4608 计算因为 11.0592MHz 的机器周期约 1.085 微秒。3. 数码管倒计时显示段码表、动态扫描与组帧顺序3.1 共阴数码管段码表怎么来的倒计时显示用四位一体的共阴数码管段选接 P0位选接 P2。七段数码管从 a 到 g 再加小数点 dp 共 8 段分别对应 P0.0 到 P0.7共阴接法下某段为高电平时点亮。0 到 9 的段码表是固定的直接写成查表常量unsigned char code SegTable[] { 0x3f, 0x06, 0x5b, 0x4f, 0x66, // 0 1 2 3 4 0x6d, 0x7d, 0x07, 0x7f, 0x6f // 5 6 7 8 9 };0x3f 的二进制是 0011 1111低六位全为 1正好点亮 a、b、c、d、e、f 六段显示数字 0。0x06 是 0000 0110只点亮 b、c 两段显示数字 1。手工推导时从低位 a 段开始对号入座比死记硬背可靠。如果换成共阳数码管记得把整个表按位取反同时位选逻辑换成高电平选中。3.2 四位动态扫描与消隐四位共用同一组段选线不可能同时显示不同数字只能循环点亮先驱动第一位送它的段码延时 2 毫秒然后驱动第二位送段码再延时 2 毫秒四轮下来约 8 毫秒人眼看到的是四个数字同时稳定显示。这个刷新频率约 125Hz远超 50Hz 的闪烁阈值。扫描代码里最重要的语句是切位之前先关段码sbit SEG_POWER P0; // 段选口 void Display(unsigned char ns, unsigned char es) { unsigned char buf[4]; unsigned char i; buf[0] ns / 10; // 南北倒计时十位 buf[1] ns % 10; // 南北倒计时个位 buf[2] es / 10; // 东西倒计时十位 buf[3] es % 10; // 东西倒计时个位 for (i 0; i 4; i) { P0 0x00; // 消隐先把段码清零 P2 ~(0x01 i); // 位选低电平选中第i位 P0 SegTable[buf[i]]; // 再送当前位段码 _nop_(); // 极短延时等待电平稳定 } }消隐不做的后果是“拖影”某一位换数字时新位选已经打开但 P0 上还是上一位的段码视觉上会看到几毫秒的乱码闪烁。P2 这里用低电平选中因为共阴数码管的公共端接低电位如果实物是共阳位选逻辑翻转段码表取反二者必须配套改。位选切换之间插入_nop_()是给 TTL 电平一个稳定时间仿真中看不出差别实物板上能明显减少首位微微发亮的毛病。3.3 倒计时组帧与显示刷新节奏每次调用 Display 时传入的是南北和东西各自剩下的秒数。两位十进制数用除法和取余拆成十位与个位整个过程是纯 CPU 计算不占时间基准。真正要注意的是调用频率主循环每轮都调 Display 会导致扫描周期不稳定按键扫描逻辑被卡在显示循环里出不来。常见做法是把显示刷新放在主循环末尾状态切换放在开头整个循环周期控制在 2 到 3 毫秒内。定时器中断已经维护了秒计数主循环哪怕偶尔晚 1 毫秒进入下一轮也只有这一个毫秒级抖动不会累积成秒级误差。这也是把黄色闪烁做成 0.5 秒交替的基础——主循环不需要精确延时读 secCnt 奇偶就能切换黄灯亮灭。4. 状态机主循环相位切换、秒计数清零与按键夜间模式4.1 switch-case 状态机架构状态机的入口是主循环里的一个普通函数每次进入只做三件事按当前相位设置灯态计算剩余秒数判断是否满足切相位条件。相位用静态变量保存跨循环调用依然持有上一个值。sbit NS_RED P1^0; // 南北红灯 sbit NS_YEL P1^1; // 南北黄灯 sbit NS_GRN P1^2; // 南北绿灯 sbit EW_RED P1^3; // 东西红灯 sbit EW_YEL P1^4; // 东西黄灯 sbit EW_GRN P1^5; // 东西绿灯 void Lights(bit nR, bit nY, bit nG, bit eR, bit eY, bit eG) { NS_RED nR; NS_YEL nY; NS_GRN nG; EW_RED eR; EW_YEL eY; EW_GRN eG; }Lights 函数把一次灯态切换收敛成一个函数调用switch 里不再出现散落的 P1 位赋值。这样做的直接好处是紧急模式下可以一口气把六只灯全部拉黑恢复时再按相位重新点亮不会漏一个方向。主循环状态函数void NormalRun(void) { static unsigned char phase PHASE_A; unsigned char remainNS, remainEW; switch (phase) { case PHASE_A: Lights(0, 0, 1, 1, 0, 0); // 南北绿东西红 remainNS 25 - secCnt; remainEW 25 - secCnt; if (secCnt 25) PhaseSwitch(phase); break; case PHASE_B: Lights(0, 1, 0, 1, 0, 0); // 南北黄东西红 remainNS 5 - secCnt; remainEW 5 - secCnt; if (secCnt 5) PhaseSwitch(phase); break; case PHASE_C: Lights(1, 0, 0, 0, 0, 1); // 南北红东西绿 remainNS 25 - secCnt; remainEW 25 - secCnt; if (secCnt 25) PhaseSwitch(phase); break; case PHASE_D: Lights(1, 0, 0, 0, 1, 0); // 南北红东西黄 remainNS 5 - secCnt; remainEW 5 - secCnt; if (secCnt 5) PhaseSwitch(phase); break; default: phase PHASE_A; break; } Display(remainNS, remainEW); }两个方向在阶段 A 和 B 的剩余秒数相同因为南北和东西总是同时进入新阶段。实际路口如果两个方向放行时间不对称可以各自维护独立的倒计时变量switch 里分别计算即可状态机框架不用动。4.2 相位切换时秒计数清零的原子操作PhaseSwitch 函数里最关键的细节是关闭 T0 中断后再清 secCnt。因为 secCnt 是在中断里被加一的主循环执行if (secCnt 25)判断后紧接着做secCnt 0这两条语句中间如果插进一次中断新的秒计数会被马上改成 1导致新相位第一秒凭空消失。void PhaseSwitch(unsigned char *ph) { ET0 0; // 关闭T0中断禁止secCnt在切换期间被改写 secCnt 0; // 秒计数归零新相位从0开始计时 ET0 1; // 恢复T0中断 (*ph); // 指向下一个相位 if (*ph PHASE_D) *ph PHASE_A; }关闭中断的时间极短只有两条赋值指令不会造成时基丢失。如果想更严谨可以在 ET0 0 之前先读一次 secCnt 保存现场但交通灯这个场景用不上直接清零就是正确的相位起点。相位变量用指针传入让函数同时承担“切换”和“清零”两个职责调用处不用重复这段关中断逻辑。4.3 按键触发夜间黄闪模式与现场恢复夜间黄闪是交通灯的常见附加需求四个方向黄灯同时 0.5 秒亮、0.5 秒灭提示车辆减速观察通过。按键接在 P3.2 上用查询方式扫描配合 20 毫秒消抖防止一次按下触发多次模式翻转。sbit KEY_MODE P3^2; bit NightMode 0; // 0为正常模式1为夜间黄闪 void KeyScan(void) { static unsigned char last 1; // 按键上一状态高电平为空闲 if (KEY_MODE 0 last 1) // 下降沿触发 { Delay20ms(); // 软件消抖 if (KEY_MODE 0) { NightMode ~NightMode; while (KEY_MODE 0); // 等待按键释放防止连按 } } last KEY_MODE; }用下降沿而不是电平触发是因为长按一张按键时 secCnt 仍然在走主循环每轮都会进入这个 if模式会被反复翻转。加上last 1的前一状态判断后只有按下瞬间才触发一次。while 等待释放虽然阻塞但一个手指按下去最多几百毫秒对交通灯的秒级计数没有影响。夜间黄闪的运行函数void YellowFlashRun(void) { unsigned char f secCnt 0x01; // 0和1交替变化 Lights(0, f, 0, 0, f, 0); // 四方向黄灯同亮同灭 Display(0x88, 0x88); // 倒计时显示熄灭显示8用于巡检 }显示0x88只是一个可选状态目的是夜间模式时维护人员能确认数码管还在工作。secCnt 的奇偶变化周期是 1 秒黄灯亮 0.5 秒灭 0.5 秒正好满足国标对警示灯闪烁频率的一般要求。如果想调整闪烁速度可以在中断里单独维护一个 500 毫秒计数器把奇偶判断改成该计数器的最高位。5. 基于 Proteus 仿真验证晶振配置、延时校准与常见坑5.1 芯片晶振频率与 HEX 加载Proteus 里双击 AT89C51 芯片默认晶振频率通常是 12MHz但如果模板里残留了其他设置程序运行时序会整体漂移。打开 MCU 属性对话框确认 Crystal Frequency 填 12MHzProgram File 指向 Keil 生成的 .hex 文件。加载后点击运行先看六只灯能否按相位顺序点亮再看数码管数字是否同步从 25 倒计时到 0。灯会亮但时间不对优先查这一项而不是去改代码里的装载值。5.2 用虚拟示波器校准 5ms 时基软件延时和晶振频率都可以通过示波器验证。临时加一段测试代码把 P3.7 在每个中断里翻转一次然后用 Proteus 的虚拟示波器夹在 P3.7 上观察方波周期。代码如下void Timer0_ISR(void) interrupt 1 using 1 { TH0 0xEC; TL0 0x78; P3_7 !P3_7; // 每5ms翻转一次示波器上看到10ms方波 }P3.7 每进一次中断翻转一次电平翻转周期是 10 毫秒因为一个完整方波需要两次翻转。示波器上如果看到 10.0ms 左右的方波说明机器周期和装载值都正确如果看到明显偏大或偏小优先检查晶振频率设置其次检查 Keil 工程里 Target 页面的 Xtal(MHz) 是否也填了 12这两个频率必须一致。5.3 数码管限流电阻与消隐顺序Proteus 仿真中数码管不接限流电阻也能跑但段电流过大时仿真步长会被拖慢倒计时的秒跳变得一顿一顿。在每段位选线上串 220 欧姆到 330 欧姆的排阻段码电压降到约 2V视觉亮度和仿真速度都会正常。实物板上不加限流电阻的后果更直接数码管长期超电流工作段码亮度快速衰减。倒数第二个值得记住的坑是消隐顺序。上面 Display 函数里先P0 0x00再切 P2这个顺序不能反。先切位选再清段码的瞬间新位选的数码管会亮一下上一位置的残影在仿真里表现为轻微拖影实物板上则是明显的“鬼影”。把消隐放在位选切换之前是动态扫描代码里性价比最高的一个改动。最后说蓝桥杯备赛里常见的失步问题倒计时还在走但灯态和数字对不上。多数情况不是状态机逻辑错而是编译优化把 secCnt 读进了寄存器循环内多次比较拿到不同瞬间的值。解决办法是保持 secCnt 的 volatile 声明不要在中断之外对它做复合赋值或自增需要修改时一律走 PhaseSwitch 或临时变量中转。这套验证顺序跑通后把 LedCtrl 和 Display 的引脚定义抽到一个头文件里换到 STC 板子时只改这一处整个交通灯工程就完成了移植。本文还有配套的精品资源点击获取
返回列表