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

资讯详情

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

51单片机交通灯状态机设计:定时器中断与Proteus仿真实现

51单片机交通灯状态机设计:定时器中断与Proteus仿真实现 简介基于51单片机的交通灯设计资料包面向电子/嵌入式初学者与课程设计学生完整实现了十字路口东西、南北方向红黄绿指示灯的实时控制与Proteus仿真覆盖从需求分析到仿真验证的完整流程适合需要完整体验状态机与中断控制的读者。设计包含20s与35s分向通行时序、绿灯转红灯前5s黄灯过渡、两位七段码倒计时显示以及夜间模式等典型功能可对照Keil工程与main.c代码快速理解状态切换逻辑。压缩包共26个文件约83.6MB主要包含Proteus仿真工程、Keil工程文件、两份MP4讲解视频以及模板备份和工作区文件目录结构便于按需取用已有1113人学习/浏览。资源附带原理讲解视频和代码讲解视频可直观复盘设计思路定时器与中断、数码管动态扫描、夜间模式切换等实现细节可对照代码与视频逐段理解仿真备份与编译中间文件也能帮助读者修改参数、排查故障并完成二次扩展适合课程设计或毕业设计参考。1. 交通灯不是点灯51单片机状态机设计的真正难点把红黄绿灯接上IO口、写几个delay谁都能让LED闪起来。但交通灯的要求是“东西向20s、南北向35s、黄灯过渡5s、七段码倒计时、夜间模式”这就不是点灯了而是一个带严格时序的状态机调度问题。很多人在Proteus仿真51单片机时发现数码管乱跳、时间误差大、切换瞬间灯全亮根本原因不是不会写LED而是没有把“当前状态”“剩余秒数”“显示刷新”拆开处理。这篇以基于51单片机的交通灯设计为例讲清楚状态迁移、定时器分频和仿真联调的关键做法适合正在做单片机课程设计或想用Proteus完整跑一遍项目的人。2. 时序拆解从20s/35s到状态机与定时器分频2.1 需求里的隐藏时序东西向和南北向并不是简单“你绿我红”先看原始功能要求东西向通行时间20s南北向通行时间35s绿灯变红灯前5s内绿灯灭黄灯亮2位七段码倒计时夜间模式。这里的“通行时间”明确指绿灯亮的时间黄灯过渡不算在通行时间内。所以一个完整周期不能只是2035还要把黄灯5s算进去。以东西向绿灯20s为例它结束前5s必须切黄灯所以实际的“东西向放行窗口”由绿灯20s和黄灯5s构成。南北向同理绿灯35s加黄灯5s。为了保证两个方向不在同一时刻放行必须把四个灯组放进一个离散状态序列里。我一般用下面四个状态来做这也是本设计在Keil工程里main.c的实际思路状态东西向南北向持续时间S1绿红20sS2黄红5sS3红绿35sS4红黄5sS1时东西向绿灯亮南北向红灯。20s后切S2东西向黄灯亮南北向仍然红灯。5s后切S3南北向绿灯亮东西向红灯。35s后切S4南北向黄灯东西向红灯。最后回到S1。这样任意时刻都不会出现两个方向同时放行绿灯变黄灯也严格发生在“由绿灯变红灯的5s内”。注意S2和S4里东西向或南北向的红灯是一直点亮的但数码管要显示的倒计时却是各自方向剩余的通行时间。很多初版代码在这里出错以为状态切换后红灯要灭掉结果把红绿灯当成普通LED随意置位。正确做法是用一个独立变量控制六个LED再用另一个变量记录当前状态剩余秒数二者在同一个状态标签里同步更新。2.2 为什么选择定时器而不是delay课程设计里最常见的错误是用void delay(unsigned int ms)做秒延时然后在主循环里切换状态。没有倒计时时能跑一旦加上2位七段码倒计时delay期间CPU被占住数码管扫描和按键检测全部卡顿累计误差还特别大。Proteus仿真51单片机时20s延时的实际结果可能只有19.2s。正确做法是用定时器产生固定时基。AT89C51这类51单片机常用T0方式1也就是16位定时器。配合12MHz晶振选50ms中断一次。50ms是折中值16位定时器在12MHz下最大定时约65.5ms选50ms不会溢出中断频率20Hz做数码管动态扫描也没有肉眼可见的闪烁。定时器初值计算12MHz晶振下机器周期为1us定时50ms需要50000个周期。16位计数初值N 65536 - 50000 15536 0x3CB0所以TH0 0x3C; TL0 0xB0;。每50ms中断一次软件计数器加1累计20次就是1s。主循环只检查这个“秒标志位”不需要任何阻塞等待。2.3 硬件资源分配IO口怎么够用一套标准方案是P1口控制红黄绿LEDP0口做七段码段选P2口做位选。具体分配如下模块引脚分配说明东西向红/黄/绿灯P1.0 / P1.1 / P1.2高电平点亮南北向红/黄/绿灯P1.3 / P1.4 / P1.5高电平点亮数码管段码 a-gP0.0-P0.6共阳极低电平点亮段数码管十位/个位位选P2.0 / P2.1动态扫描输出夜间模式开关P3.2外部中断或普通按键这里有个常见分歧直接用P0口驱动段码但P0口内部没有上拉电阻Proteus里可能会出现“仿真正常、实物不亮”的情况。如果是实物制作要在P0口外接10kΩ排阻。如果只在Proteus仿真51单片机不接也能跑我建议先把仿真调通做实物时再补排阻。共阳极还是共阴极代码里只要改一个SEG_CODE[]数组。多数开发板上的数码管是共阳极所以段码表写成0xC0, 0xF9, 0xA4, ...如果仿真图里用的是共阴极段码表整体反相即可。这一点直接决定倒计时显示是不是“乱码”。我习惯在Keil工程里加一个宏定义编译时切换段码表避免维护两套代码。3. Keil 代码实现定时器扫描、按键消抖与七段码倒计时3.1 状态机的C语言表达有了状态定义代码就可以拆成两部分状态切换和灯输出。我用一个state变量表示当前状态0、1、2、3分别对应S1到S4。秒倒计时用sec_left表示每秒减1减到0就执行状态迁移。#define S1 0 // 东西绿南北红20s #define S2 1 // 东西黄南北红5s #define S3 2 // 东西红南北绿35s #define S4 3 // 东西红南北黄5s unsigned char state S1; unsigned char sec_left 20; unsigned int t50ms 0; bit one_sec_flag 0;状态迁移函数里sec_left减到0时切换状态并重新赋值同时调用light_set()更新LEDvoid state_update(void) { if (sec_left 0) { switch (state) { case S1: state S2; sec_left 5; break; case S2: state S3; sec_left 35; break; case S3: state S4; sec_left 5; break; case S4: state S1; sec_left 20; break; } light_set(); } }注意sec_left是unsigned char35没有溢出问题。但如果你以后要调整到更长的时间比如120s就要改成unsigned int否则超过255会归零。3.2 定时器中断时基不是delay定时器初始化和中断服务程序是整套代码的骨架。下面是标准写法void timer0_init(void) { TMOD 0xF0; TMOD | 0x01; // T0方式116位定时器 TH0 0x3C; TL0 0xB0; // 50ms ET0 1; EA 1; TR0 1; } void timer0_isr(void) interrupt 1 { TH0 0x3C; TL0 0xB0; // 重装初值 t50ms; if (t50ms 20) { t50ms 0; one_sec_flag 1; } }参数说明TMOD 0xF0; TMOD | 0x01;是保护定时器1的配置位只修改T0的低4位重装初值必须在中断里做否则下一次定时长度错误one_sec_flag是给主循环的“秒脉冲”信号。主循环里不要用delay等待只检查标志位while (1) { if (one_sec_flag) { one_sec_flag 0; sec_left--; state_update(); } seg_scan(); key_scan(); }这里特别注意不要在state_update()里再写一次sec_left--否则一秒会减两次。我调试时遇到过这个bug现象是倒计时比实际快一倍Proteus里观察特别明显。3.3 七段码倒计时动态扫描和显示缓存2位七段码需要动态扫描。设一个显示缓存display_num每秒更新一次seg_scan()每2ms切换一位。这样人眼不会看到闪烁扫描也不会阻塞主循环。unsigned char display_num 20; unsigned char code seg_duan[] { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90 }; // 共阳数码管段码 0-9 void seg_scan(void) { unsigned char ten display_num / 10; unsigned char one display_num % 10; P2 0xFE; // 位选十位 P0 seg_duan[ten]; delay_short(2); P2 0xFD; // 位选个位 P0 seg_duan[one]; delay_short(2); }参数说明P2 0xFE和0xFD是低电平选中数码管位具体极性取决于Proteus里位选三极管是PNP还是NPN。如果仿真图直接用共阴数码管接P2可能是高电平选通需要换成0x01和0x02。delay_short(2)是2ms短延时只用于让每一位稳定显示不能替代定时器。另一个容易踩的坑display_num减到0后状态切换下一次要立刻更新。我是在state_update()中同步赋值的switch (state) { case S1: display_num sec_left; break; case S2: display_num sec_left; break; case S3: display_num sec_left; break; case S4: display_num sec_left; break; }更简洁的做法是直接在state_update()最后统一执行display_num sec_left;因为所有状态都会走到这里。3.4 状态切换时LED为什么闪一下一个典型现象状态切换瞬间两个方向红灯同时亮一帧Proteus里看起来像“全红闪烁”。原因是对P1口的赋值分散在主循环和中断里没有集中输出。我建议所有对P1口赋值的代码集中在light_set()里根据状态查一张灯态表void light_set(void) { switch (state) { case S1: P1 0x13; break; // 东绿P1.2、南红P1.3 case S2: P1 0x0B; break; // 东黄P1.1、南红P1.3 case S3: P1 0x2C; break; // 东红P1.0、南绿P1.5 case S4: P1 0x14; break; // 东红P1.0、南黄P1.4 } }上面的十六进制值假设LED是高电平点亮。只要保证light_set()在状态切换时第一件事被调用再修改倒计时变量就不会出现全红闪帧。这个函数在夜间模式里也要复用但逻辑要单独处理。4. Proteus 仿真联调晶振、共阴数码管与常见bug排查4.1 从Keil到Proteus的完整通路Keil工程编译成功后生成template.hex。打开Proteus工程新工程.pdsprj双击51单片机芯片在Program File里加载hexClock Frequency改为12MHz。然后点运行观察灯和数码管。必须强调Proteus的仿真速度受晶振频率影响Keil里的定时器初值按12MHz计算Proteus也必须是12MHz否则倒计时速度不对。很多人发现20s实际只要2s基本都是晶振频率没改。建议先在Keil里设置断点在light_set()内运行到断点后检查P1口数值。如果和预期一致再去Proteus看灯。这样能快速区分是代码问题还是仿真连线问题。4.2 数码管不亮或乱码的排查步骤Proteus仿真51单片机最常见的四类问题我按出现频率排序段码表和数码管类型不匹配。共阳极段码表0xC0表示0如果仿真图用的是共阴极0xC0显示出来的会是镜像乱码。检查元器件名称是7SEG-COM-ANODE还是7SEG-COM-CATHODE然后修改段码表。位选方向反了。使用两位7SEG-MUX数码管时位选是低有效还是高有效要看内部连接。现象是两个数字重叠闪烁解决办法是把P2 0xFE/0xFD换成P2 0x01/0x02。P0口没有上拉电阻。Proteus里P0口是开漏输出直接驱动段码可能很暗或全亮。我习惯在仿真图里加一个RESPACK-8排阻上拉到VCC问题立刻消失。刷新速度太慢。seg_scan()里的短延时超过5ms肉眼就能看到数码管闪烁。保持在2ms左右即可。4.3 时间误差的量化验证验证倒计时是否准确我用了两个方法。第一把20s临时改成2s来测试用手机秒表连续测3个周期误差应小于0.2s。第二在Keil里用虚拟逻辑分析仪观察P1.0引脚波形测量高电平宽度是否符合状态时长。如果误差明显按三个方向排查定时器重装初值是否在中断里执行如果放在主循环里每次中断间隔会漂移。Keil优化等级是否过高。默认8级优化可能把t50ms变量优化成寄存器副本但中断里的赋值和主循环里的判断不同步。建议把优化等级调到0级或1级。主循环里是否有阻塞延时。delay_short(2)单次很短但长期运行会累积到当前状态总时间导致一个周期后误差扩大。4.4 工程文件里的构建产物你解压后会看到template.build_log.htm、template.plg、main.LST这些是Keil的编译日志和列表文件不是源码。它们的作用是确认工程有没有编译通过。打开template.build_log.htm看到0 Error(s)就能确定hex文件是新的。.pdsprj是Proteus工程.pdsbak是自动备份直接打开新工程.pdsprj就能看到完整电路不需要从零搭建。5. 夜间模式的边界情况从“灭灯”到黄灯闪烁的实现5.1 夜间模式该灭哪些灯夜间车流量少常见做法是所有红绿灯关闭只让四个黄灯以1Hz闪烁提示路口有信号灯。这里要注意不能在原状态切换函数里改因为夜间模式不按20s/35s时序走。我加了一个night_mode标志按下P3.2后进入。void night_loop(void) { if (night_mode) { if (one_sec_flag) { one_sec_flag 0; P1 ^ 0x12; // 翻转P1.1和P1.4两个黄灯位 } return; } // 正常状态处理 if (one_sec_flag) { one_sec_flag 0; sec_left--; state_update(); } }P1 ^ 0x12是异或操作每1s翻转一次黄灯位其他灯的位置不受影响。这里的0x12对应P1.1和P1.4和正常状态里的黄灯引脚一致。5.2 退出夜间模式后如何恢复当前时刻这是容易被忽略的边界情况。如果夜间模式退出的时刻正好是某一状态的中途不能粗暴地重新从20s开始否则通行时间就不对了。我在进入夜间模式前保存state和sec_left退出时恢复void enter_night_mode(void) { saved_state state; saved_sec sec_left; night_mode 1; P1 0x00; // 关闭所有红绿灯 } void exit_night_mode(void) { night_mode 0; state saved_state; sec_left saved_sec; light_set(); }这样退出后交通流不会突变。按键消抖可以用简单的20ms延时或者在定时器中断里做5ms级轮询。我推荐后者避免阻塞数码管扫描。5.3 验证方法与一个实用技巧建议做一个10分钟连续运行测试记录四个状态切换的时刻。用Keil的虚拟示波器观察P1.0到P1.5的波形验证红灯、绿灯时长是否满足20s和35s。夜间模式测试时确认只有黄灯引脚有方波其他引脚保持低电平。这里有个进阶技巧把交通灯状态信息通过串口发送。在Keil里用printf或UART窗口每秒输出一次state和sec_left不用一直盯着Proteus也能快速定位状态迁移有没有跳步。我给夜里调试用的串口重定向代码如下void uart_send_state(void) { SBUF S; while (!TI); TI 0; SBUF state 0; while (!TI); TI 0; }在state_update()后调用这个函数Keil的UART窗口里就能看到S0、S1、S2、S3的变化节奏。如果状态跳过了S2或S3说明秒数赋值或状态迁移条件写错了比盯着数码管更容易定位。本文还有配套的精品资源点击获取
返回列表