自适应交通信号灯控制系统:排队长度响应与单片机实现
简介资源聚焦自适应型交通信号灯控制系统的完整设计文档面向嵌入式、单片机及智能交通方向的本科生、毕业设计学生与开发者用于解决传统定时信号灯无法应对实时交通流量变化的问题。方案基于AT89S52单片机集成LED显示模块、驱动电路和车辆检测模拟器件通过车辆检测与信号控制算法实现定时、自适应两种运行模式可依据路口车辆滞留量动态调整放行时间有效提升通行效率。PDF共1个文件压缩包约1.66MB内容覆盖系统总体设计、软硬件设计、主控程序设计、Keil编译调试、系统实现与测试、性能分析与优化等完整章节并附有目录、摘要和参考文献便于按需检索与学习。已有260人学习下载适合作为课程设计参考、毕业设计框架或智能交通控制入门资料具有一定的工程实践参考价值。1. 自适应型交通信号灯控制系统从固定配时到排队长度响应上下班高峰期一个方向堵到路口中央另一个方向却连续放空车这是固定配时信号灯最难受的地方。这套自适应型交通信号灯控制系统用 AT89S52 单片机加 8255 并行接口芯片把红绿灯从“按秒循环”改成“按排队长度响应”东西方向红灯时一旦排队超过预设位置系统直接结束红灯、跳黄灯 5 秒后放行。对做嵌入式毕设、交通控制课设或者想了解单片机传感检测的人来说这个设计的完整度接近一个商品信号机。系统自带定时模式和自适应模式两种工作方式用压力传感器检测队尾位置用 Keil C51 写控制程序8255 扩展 IO、74HC573 锁存、定时器中断、状态机切换这些基本功全被串起来了。适合想复现一个“看得见效果”的智能控制项目的人。下面按硬件选型、车辆检测、双模式软件、调试扩容依次拆开讲每部分都给出可以直接抄的参数和命令。2. 硬件骨架AT89S52、8255、74HC573 怎么分工拿下十字路口2.1 为什么不用 STM32AT89S52 的路口控制到底需要什么单片机选型是最先要定的。这个设计在文档里写的是 AT89S52实际制板时很多人会用 STC89C52两者引脚兼容PDIP-40 封装5V 供电指令集完全一样。红绿灯控制本质上是低速 IO 逻辑读按键、扫数码管、读传感器、切信号灯没有大量运算也没有复杂外设没必要上 STM32。AT89S52 自带 8KB Flash、256B 内部 RAM、两个 16 位定时器、6 个中断源跑一个状态机绰绰有余。STC89C52 的工作电压能做到 3.4V 到 6.0V正常工作电流 4 到 7mA掉电模式小于 0.1 微安这对路口控制器长时间运行是实打实的优点。但 AT89S52 有一个绕不开的短板并口不够用。P0 到 P3 四个 8 位 IO 口听起来不少实际上 P0 口是漏极开路做输出要外接上拉访问外部器件时还要和低 8 位地址分时复用P2 口通常给高位地址P3 口的第二功能占了串口、外部中断和定时器真正能自由使用的只有 P1 口。而这个系统要同时管倒计时数码管、红黄绿信号灯、按键输入和传感器输入端口必然不够。所以扩展 IO 不是可选项是必选项设计里用的是 8255 可编程并行接口芯片。2.2 8255 方式 0 配置三个 8 位口怎么分配8255 有 A、B、C 三个 8 位端口C 口还能拆成两个 4 位口。这个系统不需要握手信号三个端口全部工作在方式 0 基本输入输出就够了。端口分配方案可以直接照下面的表来接这样后面写程序时地址和位定义都不会乱。端口方向接什么作用PA0~PA7输出数码管段码 a~dp倒计时秒数显示PB0~PB3输出数码管位选动态扫描选通哪一位PB4~PB6输出红、黄、绿信号灯控制东西/南北方向灯色PC4~PC7输入东西方向队尾传感器判断东西向排队长度PC0~PC3输入南北方向队尾传感器判断南北向排队长度8255 的初始化就是在控制口写一个方式控制字。下面这段代码把控制字定为 0x90对应二进制 10010000含义是D71 表示这是方式控制字D6D500 表示 A 口方式 0D40 表示 A 口输出D31 表示 C 口高 4 位输入D20 表示 B 口方式 0D10 表示 B 口输出D01 表示 C 口低 4 位输入。#include reg52.h #include absacc.h #define PA XBYTE[0x0000] // A 口数据地址 #define PB XBYTE[0x0001] // B 口数据地址 #define PC XBYTE[0x0002] // C 口数据地址 #define PCTL XBYTE[0x0003] // 控制字地址 void Init8255(void) { PCTL 0x90; // 方式0A口输出B口输出C口高四位输入低四位输入 }这里 XBYTE 是 Keil C51 里访问外部数据存储器的宏编译后会自动生成 MOVX 指令P0 和 P2 口由硬件时序自动切换成地址/数据总线不需要软件去操作 ALE。地址 0x0000 到 0x0003 是假设 8255 的 CS 直接接地、A1A0 由单片机低位地址线控制如果你把 CS 接到 P2.7地址就要按位权偏移。实际项目中我一般会把 A0、A1 接到 P2.0、P2.1这样每个端口地址都能算得清清楚楚。初始化之后要留意 8255 的 RESET 脚如果复位电路没接好端口输出全为 1外设看起来就像完全没工作。2.3 74HC573 锁存器为什么 P0 口必须加这一层74HC573 是八进制三态非反转透明锁存器1D 到 8D 是输入端1Q 到 8Q 是输出端LE 是锁存使能OE 是输出使能。LE 为高电平时输出跟随输入变化LE 由高变低时当前输入被锁存到输出端并保持。OE 为低电平时三态门打开为高电平时输出呈高阻态。这个芯片在这里解决的是 P0 口分时复用问题。8051 访问外部 RAM 或外部器件时先通过 P0 口送出低 8 位地址此时 ALE 引脚产生一个下降沿74HC573 在这个下降沿把地址锁住随后 P0 口切换为数据总线573 的输出端仍然保持刚才的地址外部器件才能正确拿到地址。8255 的数据线 D0 到 D7 挂在 P0 口上如果不加 573数据阶段会把地址覆盖掉读写就会时好时坏。最常见的错误是漏接 ALE或者把 LE 直接接成高电平现象就是写 8255 内容不稳定。即使不扩展外部 RAM74HC573 在数码管动态扫描里也很有用。每次刷新一位数码管时先把段码送到 573再拉低 LE 锁存位选切换后显示内容不会闪烁。很多 51 开发板上都有一颗 573原因就在这里。2.4 LED 驱动和显示电路的设计参数信号灯用红黄绿 LED倒计时用数码管。LED 的优点是高节能、安全性高、寿命长、响应快、运行成本低但单片机 IO 口驱动能力有限不能直接串很多 LED。常见做法是用三极管 S8050 或者 ULN2003 做灌电流驱动单片机输出低电平时 LED 点亮。每个 LED 串一个限流电阻电阻值根据工作电流算红色 LED 压降约 1.8V电源 5V工作电流取 10mA 时限流电阻约 330Ω。项目数值说明LED 工作电流5~20mA亮度和寿命折中限流电阻150~330Ω(5V - Vf) / If 计算驱动管S8050 或 ULN2003灌电流方式基极串 1kΩ数码管刷新率不低于 50Hz约 20ms 刷新一位防闪烁用灌电流方式时要注意逻辑反向单片机引脚输出 0 才亮灯程序里判断灯色时不要搞反。调试时先写一个固定电平点亮单灯确认逻辑方向再上状态机。3. 车辆排队检测压力传感器布点与跳过红灯的判断逻辑3.1 队尾位置传感器测的不是车速是排队长度这套系统最有意思的部分是“根据实时道路车辆滞留量控制放行时间”。滞留量怎么测设计里用的是队尾位置传感器在停车线后的不同位置埋设压力传感器。车辆停在传感器上输出电平就会变化。单片机不关心一辆车开过去时的瞬时信号只关心当前哪几个传感器被压住从而判断排队尾部延伸到了哪里。传感器布点遵循一个简单原则每个方向从停车线开始按一辆车平均占用长度约 5 米布置一个。东西方向放 Sx1、Sx2 直到 SxM南北方向放 Sy1、Sy2 直到 SyM。比如每个方向放 4 个能识别的累计排队长度就是 4 辆车。如果第 4 个传感器被压住说明排队已经延伸到约 20 米外再不放行就可能堵到上游路口。布点越多对排队长度的分级越细但 8255 的 C 口只有 8 位所以实际项目会按关键断面取舍一般一个方向 4 到 6 个足够。PC7PC6PC5PC4东西向排队状态系统处理1111无车正常倒计时11101 辆车不动作11002 辆车不动作0000排队到第 4 个传感器触发提前切换表格里的 0 表示传感器被压住。实际电路里每个传感器输出接一个上拉电阻到 VCC没车时输出高电平有车时输出低电平这个逻辑在程序里要反转过来别把高电平当成有车。3.2 传感器信号读取与软件去抖压力传感器或者机械触点开关在车辆压过瞬间会产生抖动直接读 PC 口很容易误判。最稳妥的软件做法是连续采样三次每两个采样之间间隔 5ms然后按位做三取二表决。下面这段函数会把 C 口的 8 位全部做一次去抖返回去抖后的电平状态。#include reg52.h #include absacc.h #define PC XBYTE[0x0002] void delay_ms(unsigned int n); unsigned char ReadSensorDebounced(void) { unsigned char samples[3]; unsigned char i, mask, cnt; unsigned char out 0; for (i 0; i 3; i) { samples[i] PC; delay_ms(5); } for (mask 0x80; mask; mask 1) { cnt 0; for (i 0; i 3; i) { if (samples[i] mask) cnt; } if (cnt 2) out | mask; // 三次中至少两次为高才最终判为高 } return out; }这个去抖逻辑不是简单的延时等待而是按位表决能滤掉单次采样里的机械颤动。mask 从 0x80 到 0x01 逐位扫描最终 out 里为 1 的位代表该传感器电平为高也就是没车。如果你用的是有效低电平后续判断时要加非运算。去抖的目的不是滤除电气噪声而是防止排队数量在临界位置来回跳变。3.3 自适应切换条件最小红灯保护与连续确认接下来是核心控制逻辑。原设计的描述是东西方向红灯时队尾传感器反馈队伍长度超过设定值系统直接停止红灯倒计时跳到黄灯 5 秒后显示绿灯。南北方向同理。这个逻辑本身没错但工程上直接清零倒计时会有个公平性问题如果红灯刚亮 2 秒就被触发切换另一个方向就永远得不到完整放行时间。我在写程序时会在判断条件里加两个保护最小红灯时间和连续确认。最小红灯时间保证红灯至少亮 10 秒连续确认要求连续两次 1 秒采样都超限才响应防止瞬间排队的误判。下面这段代码是自适应判断的核心先统计东西方向被压住的传感器数量再做连续确认。#define RED_MIN_TIME 10 #define THRESHOLD 4 bit adaptive_trigger; // 提前切换标志主循环消费 unsigned char confirm_count; void AdaptiveCheck(unsigned char state, unsigned char red_remain) { unsigned char sensor, i; unsigned char ew_queue 0; if (red_remain RED_MIN_TIME) return; // 最小红灯时间保护 sensor ReadSensorDebounced(); for (i 0; i 4; i) { if (!(sensor (0x10 i))) // PC4~PC7 低电平表示有车 ew_queue; } if ((state EW_RED_SN_GREEN) (ew_queue THRESHOLD)) { if (confirm_count 1) { adaptive_trigger 1; // 连续两次确认触发提前切换 confirm_count 0; } else { confirm_count; } } else { confirm_count 0; } }这里 threshold 取 4意味着第 4 个传感器被压住才触发。实际标定时取传感器总数的 70% 左右最合适不要取满编否则队尾已经堵到路口中央才切换就晚了。adaptive_trigger 是一个全局标志主循环检测到它之后会在本次状态机运行前把剩余时间清零从而进入黄灯。直接在里面改 red_remain 清零也可以但我会避免原因在第四章主循环顺序里说明。3.4 为什么只检测红灯方向你可能会有疑问为什么只在红灯方向检测排队绿灯方向也在放行排队应该越来越短不需要额外干预。如果绿灯方向排队也很长说明路口已经饱和单纯调整红灯时间没有意义需要联动上游路口或者人工介入。所以自适应逻辑的边界是解决“某一方向绿灯空放、另一方向积压”的非对称流量不是全饱和状态。把这条边界想清楚程序就不会越写越复杂。夜间车少时排队长度达不到阈值系统会自动以定时模式运行这也算一种自然节能策略。4. 双模式软件实现Keil C51 里的定时器分时与状态机4.1 11.0592MHz 晶振下的定时器初值计算软件部分用 Keil C51 开发程序分成三块定时器产生 1 秒基准、状态机推进红绿灯、自适应逻辑修改剩余时间。先算定时器初值。晶振用 11.0592MHz单片机 12 分频后机器周期约 1.085 微秒。T0 工作在方式 116 位定时器最大定时 65.536ms。取 50ms 一个中断周期需要的计数个数是 50000 / 1.085 ≈ 46080转十六进制是 0xB400所以 TH0 0xB4TL0 0x00。50ms 中断 20 次就是 1 秒。void Timer0_Init(void) { TMOD 0x01; // T0 方式116位定时器 TH0 0xB4; TL0 0x00; ET0 1; // 允许 T0 中断 TR0 1; // 启动 T0 EA 1; // 开总中断 } unsigned int ms_count; bit second_flag; void Timer0_ISR(void) interrupt 1 { TH0 0xB4; // 重装初值 TL0 0x00; ms_count 50; if (ms_count 1000) { ms_count 0; second_flag 1; // 置位由主循环消费 } }TMOD 0x01 表示 T0 工作在 16 位定时器模式。中断服务程序里只做两件事重装初值和累加 50ms满 1000ms 置位 second_flag。不要在中断里直接做倒计时和显示因为 51 的中断入口到退出要额外占用时间如果显示函数太长中断还没退出下一次中断就来了时间基准会漂移。所有耗时操作都放到主循环里等 second_flag 再去处理。4.2 状态机定义四个状态覆盖一个完整信号周期交通信号灯不是简单的“红绿红绿”而是一个循环东西绿灯南北红灯、全黄灯、东西红灯南北绿灯、全黄灯再回到东西绿灯。我把黄灯拆成两个状态分别代表从东西绿灯切向南北绿灯和从南北绿灯切向东西绿灯这样黄灯期间两个方向都是黄灯作为路口内车辆清空时间和黄灯固定 5 秒的设定一致。typedef enum { EW_GREEN_SN_RED, // 东西放行南北禁行 EW_YELLOW_SN_YELLOW, // 东西黄南北黄 EW_RED_SN_GREEN, // 南北放行东西禁行 SN_YELLOW_EW_YELLOW // 南北黄东西黄 } LIGHT_STATE; LIGHT_STATE state; unsigned char green_ew, green_sn; unsigned char yellow_time 5; unsigned char remain_sec; void StateMachine_Run(void) { switch (state) { case EW_GREEN_SN_RED: if (remain_sec 0) { state EW_YELLOW_SN_YELLOW; remain_sec yellow_time; OutputLights(state); } break; case EW_YELLOW_SN_YELLOW: if (remain_sec 0) { state EW_RED_SN_GREEN; remain_sec green_sn; OutputLights(state); } break; case EW_RED_SN_GREEN: if (remain_sec 0) { state SN_YELLOW_EW_YELLOW; remain_sec yellow_time; OutputLights(state); } break; case SN_YELLOW_EW_YELLOW: if (remain_sec 0) { state EW_GREEN_SN_RED; remain_sec green_ew; OutputLights(state); } break; } }green_ew 和 green_sn 是启动时通过按键设置的绿灯时长remain_sec 是当前状态剩余秒数每秒减 1减到 0 就换状态。OutputLights 根据 state 去控制 PB4~PB6 的灯色输出。定时模式和自适应模式共用同一套状态机自适应只是在特定状态下提前把 remain_sec 清零其余逻辑完全不变。这样两种模式切换的时候程序结构不会散开。4.3 主循环自适应检测与状态推进的顺序主循环的顺序很关键。如果先推进状态机再做自适应检测当红灯剩余时间已经变成 1 秒时状态机先切到黄灯自适应检测就错过了一次提前切换机会。所以要先检测是否触发再推进状态。到这一步第四章讲的 adaptive_trigger 就派上用场了不能直接在自适应检测函数里把 remain_sec 清零因为主循环里马上会对 remain_sec 做自减unsigned char 从 0 再减会变成 255状态机直接就乱了。void main(void) { unsigned char mode; Init8255(); Timer0_Init(); mode ReadModeKey(); // 读取工作模式 green_ew ReadGreenKey(); // 读取东西绿灯配时 green_sn ReadGreenKey(); // 读取南北绿灯配时 while (1) { if (second_flag) { second_flag 0; if (mode MODE_ADAPTIVE) AdaptiveCheck(state, remain_sec); remain_sec--; if (adaptive_trigger) // 检查自适应触发标志 { remain_sec 0; adaptive_trigger 0; } StateMachine_Run(); DisplayUpdate(); } if (emergency_flag) { EmergencyHandle(); emergency_flag 0; } } }这个顺序保证了两点一是自适应检测看到的是当前状态的真实剩余时间不会和状态推进抢节奏二是 remain_sec 自减之后立即检查 adaptive_trigger把提前切换的意图准确传给状态机。如果你的代码直接在 AdaptiveCheck 里写 remain_sec 0然后在主循环里 remain_sec--程序就会踩到 0 减 1 变 255 的坑。这类问题在 Keil 仿真里很难查因为 255 也是一个合法倒计时但信号灯会卡在某个状态很久才跳。4.4 紧急控制外部中断只置标志不做处理紧急情况控制键用外部中断 INT0 或 INT1 触发。8051 的外部中断适合做紧急事件响应因为它能打断主循环正在执行的任何操作。但中断服务程序里不能做延时和复杂处理正确做法是只置一个全局标志回到主循环后再做放行动作。void ExtInt0_ISR(void) interrupt 0 { emergency_flag 1; // 只置标志不做具体处理 } void EmergencyHandle(void) { state EW_GREEN_SN_RED; // 紧急方向优先放行 remain_sec 30; // 放行30秒后自动恢复 OutputLights(state); }emergency_flag 由外部中断置位主循环检测到之后调用 EmergencyHandle把状态强切到东西放行倒计时设为 30 秒。这里把 30 秒写成固定值是为了避免现场人员忘记复位导致路口长时间锁定。如果你要更保守可以在 EmergencyHandle 里等待人工按键确认但至少要保留一个超时退出机制这是实际路口设备的基本安全要求。5. 调试与扩容把仿真跑通再升级成联网路口控制器这套硬件我建议按三步调。第一步调最小系统只留晶振、复位、P1 口接 LED确认程序能跑第二步调 8255初始化后往 PA 口写 0x55、0xAA用万用表量 PA0~PA7 是否交替变化。如果电平不变先查 CS 地址线和 WR 时序。Proteus 仿真里最容易出现的问题是 8255 的 RESET 没接好导致端口输出全 1看起来像没初始化。第三步再挂传感器和显示分模块验证。现象可能原因检查点数码管乱码74HC573 的 LE 没接 ALE用示波器看 ALE 是否有周期脉冲写 8255 无效地址线 A0/A1 接反对照原理图检查 P2.0/P2.1传感器误触发没有做软件去抖采样间隔 5ms三取二表决红灯被瞬间跳过缺最小红灯保护确认 RED_MIN_TIME 已设置这套设计留了升级接口最简单实用的升级是加串口。AT89S52 的 P3.0 和 P3.1 就是 RXD 和 TXD接 MAX232 或者直接引 TTL 信号就能把信号灯状态、剩余秒数上报。如果想把系统接进互联网串口再接一个 ESP8266 或 DTU 模块数据就能被云端平台读取远端下发配时参数后单片机在串口中断里接收并更新 green_ew 和 green_sn信号机就从单点控制器变成了联网路口控制器。需要注意电平匹配AT89S52 是 5V TTL 电平ESP8266 是 3.3V 逻辑直接连有烧毁风险串电阻或者用双向电平转换模块这是做这类设备时最容易翻车的地方。最后给一个验证自适应逻辑的量化方法用两个电位器模拟传感器输入一个调到阈值以上一个调到阈值以下观察红灯剩余时间是否被清零、黄灯是否固定 5 秒。再做一个计数器统计 60 秒内的切换次数对比定时模式下两个方向的空放率。答辩或汇报时拿出这组对比数据比贴几十行代码更有说服力。本文还有配套的精品资源点击获取