资讯动态

基于STM32的智能交通灯管理系统:状态机与定时器中断实践

发布时间:2026/10/3 7:20:58 来源:尧图企业网站定制
我先把话说在前头无论你是做毕业设计、课程设计还是单纯想拿一块STM32开发板练手做点带传感器、带按键、带显示的完整小系统“基于STM32的智能交通灯管理系统”这个题目都是性价比极高的选择。它不像纯点灯那样无聊又比做四轴飞控、织网机器人那种动不动就翻车的项目友好得多——麻雀虽小五脏俱全做完之后你对GPIO控制、定时器中断、外部中断、数字逻辑状态机、传感器信号处理这些嵌入式基本功都会有一个非常扎实的掌握。平时开车等红灯的时候你会发现路口的红绿灯有两种一种是傻乎乎固定配时高峰期照样让你空等另一种是“聪明”灯车多方向绿灯时间自动拉长没有车的方向能快速放行。咱们这次要做的就是把后面这套逻辑在STM32上完整落地。文章会从需求拆解开始把硬件选型、电路设计、软件状态机、常见坑点全部过一遍即使你手里的板子是几十块钱的STM32F103C8T6最小系统板也能照着重现。1. 系统需求拆解与总体设计思路1.1 智能交通灯到底“智能”在哪里最简单的红绿灯控制逻辑一个51单片机、三组LED、写死几个delay就搞定了为什么要专门做一套带“STM32”、“管理”、“系统”字眼的设计因为真实路口的交通灯控制系统至少要处理下面几类需求基础倒计时显示南北、东西两个方向的红绿灯按设定配时交替切换同时用数码管或OLED显示剩余秒数。紧急车辆优先当救护车、消防车需要通过时按下紧急按钮系统强制把当前放行方向切到绿灯让特种车辆快速通过疏散后恢复原有配时。车流量感应在支路安置红外对射或地磁传感器检测支路是否有车等待。主路有车、支路没车时主路保持绿灯支路来车达到一定数量或等待时间后支路切换为绿灯。夜间模式深夜车流稀少不需要完整的红绿灯周期可以进入黄灯闪烁状态或缩短各相位时间。参数可调不同路口车流量差异很大红绿灯时长不应该写死在代码里而是可以通过按键调整并存储。把这些需求写成一个功能清单就非常清晰了功能模块实现方式优先级红绿灯状态切换STM32 GPIO控制LED必做倒计时显示2位/4位数码管或OLED必做紧急优先控制按键外部中断高车流量检测红外对射传感器高夜间模式 / 参数调整按键扫描EEPROM存储中也就是说“智能”的核心在于系统必须有多输入传感器、按键、多输出红绿灯、显示和可切换运行策略而不是一条路走到死的固定时序。1.2 为什么选择STM32F103C8T6作为主控很多人在选题时会纠结用51单片机不行吗用Arduino不行吗我认真说一句51能跑但跑得很吃力Arduino能跑但做完之后你对底层的理解非常浅而STM32正好卡在“性能充裕”和“资料丰富”的黄金交叉点上。性能充裕STM32F103C8T6是Cortex-M3内核72MHz主频、64KB Flash、20KB RAM。跑一个交通灯状态机加上数码管刷新CPU占用率不到5%剩下的资源足够你折腾RTOS、加OLED动画、做串口上位机监控。外设匹配这个题目需要定时器做精准计时TIM定时器、外部中断处理紧急按键EXTI、PWM模拟或GPIO直控灯组、I2C/SPI驱动OLED、ADC读取传感器或电位器。STM32F103这些外设全都标配每个都能踩到点子上。生态成熟学习资料铺天盖地无论是标准外设库、HAL库还是LL库网上都有完整例程。真遇到问题搜个报错信息都比冷门单片机容易十倍。这里再补充一句选型细节F103C8T6是LQFP48封装RAM/Flash在同系列里属于“低配”但对交通灯这种应用绰绰有余。如果你后续想加GUI比如用LVGL跑一块屏幕建议直接上F103RCT6256KB Flash或者F407系列。不过作为课程设计/毕设C8T6能吃得很饱。1.3 系统整体架构与信息流做嵌入式项目写代码之前先在脑子里把“数据从哪来、往哪去、怎么处理”这一条链路理清楚比直接打开Keil堆代码重要得多。这个系统的信息流其实很直接输入部分4个按键模式切换、紧急优先、参数增加、参数减少红外传感器可扩展2~3路检测特定方向是否来车调试串口接收上位机指令或向上位机发送状态信息核心处理状态机调度维护当前路口放行相位、剩余时间、下一相位切换运行模式切换普通模式、紧急模式、夜间模式、参数设置模式计时与延时所有状态切换必须由定时器中断驱动不允许用阻塞式delay输出部分南北方向3组LED红黄绿 东西方向3组LED红黄绿数码管倒计时显示或OLED蜂鸣器提示紧急模式切换时发声串口日志输出我把这部门放到后面软件部分细说这里先让你有个概念这个项目的复杂度不在某一个点上而在于各个模块之间怎么协同工作。比如你在主循环里用delay延时紧急模式按键来了根本响应不了这就是典型的协同失败。正确姿势是把时间基准交给定时器中断主循环只负责“决策”不负责“数数”。2. 硬件电路设计与关键元器件计算2.1 GPIO口分配原则与LED驱动电路STM32F103C8T6虽然有37个GPIO但不是每个都能随便用。有些引脚默认功能会干扰调试下载比如PA13/PA14/PA15、PB3/PB4这5个脚是JTAG/SWD复用引脚所以分配引脚时第一时间把这些排除掉。以下是常见的可用引脚分配表供参考功能引脚备注南北红灯PA0推挽输出南北黄灯PA1推挽输出南北绿灯PA2推挽输出东西红灯PA3推挽输出东西黄灯PA4推挽输出东西绿灯PA5推挽输出数码管段选/位选PB0~PB15动态扫描紧急按键PA6外部中断上拉输入模式/参数按键PA7/PA8/PA9上拉输入红外传感器输入PA10/PA11上拉输入或ADCI2C OLEDPB6/PB7复用开漏串口调试PA9/PA10复用推挽注意与按键避开如果你没有用数码管而是用OLED显示倒计时那么PB口只占用两个I2C引脚就够了富余的GPIO可以接蜂鸣器、传感器电源控制等。LED驱动的电路设计值得多说两句。STM32 GPIO在推挽输出模式下单脚最大输出电流大约±25mA但芯片整体功耗有限直接从引脚灌电流点亮LED虽然能亮却不专业——尤其是灯组变为“控制外部灯板”时GPIO直接驱动继电器或大功率LED完全带不动。推荐的做法是GPIO接三极管S8050或MOS管做开关驱动示例如下// LED驱动管脚初始化标准外设库写法 GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_2MHz; // LED属于低频器件2MHz足够 GPIO_Init(GPIOA, GPIO_InitStructure);电机驱动方面如果只是演示用5V小灯珠一根导线串个220Ω限流电阻接到GPIO也行。但做管理系统的实物演示时建议还是用ULN2003达林顿管阵列来驱动220V交流信号灯——GPIO输出高电平给ULN2003输入端输出端接继电器线圈或双向可控硅实现弱电控制强电的隔离效果。安全第一220V部分一定要隔离实验时优先用LED灯板做模拟。2.2 电源电路与复位/启动配置STM32F103C8T6最小系统板一般自带USB转串口和AMS1117-3.3V稳压供电用USB 5V就行。但完整的管理系统设计中如果加入了多个传感器、数码管、蜂鸣器、继电器5V外设和3.3V系统必须分开供电并在电源入口做好滤波。这里给出一个稳妥的电源方案输入DC 5V / 2A适配器或USB供电3.3VAMS1117-3.3线性稳压输入输出各接10μF100nF电容滤波5V外设直接从输入5V取电LED灯板、蜂鸣器、红外传感器电源统一从这里拉关键点STM32的VDD每个引脚旁边加100nF去耦电容VCAP引脚接2.2μF钽电容如果你用的是带板载稳压的最小系统板这步可以跳读但外设供电不能偷懒否则数码管刷新时电压一跌RGB灯就开始乱跳、红外误触发排查起来比改代码还痛苦。2.3 传感器与按键输入电路红外对射传感器模块是交通灯车流量检测中最常见的选型。它的原理很简单发射管持续发射红外光接收管一直导通输出低电平当车辆挡住光线时接收管截止输出高电平。模块自带LM393比较器调阈值直接输出TTL电平给STM32的GPIO读取即可。接线就三根VCC、GND、OUT。按键部分有两种接法外部中断触发和GPIO轮询扫描。紧急优先按键必须用外部中断EXTI因为它在理论上随时可能被按下如果主循环刚好在数码管刷新、串口打印轮询就会错过毫秒级的按键时间普通模式切换和参数加减按键放在主循环里扫描就够了没必要浪费中断资源。按键硬件上一定要做消抖可别指望软件delay(10)能包治百病。推荐的做法是RC硬件消抖10kΩ上拉电阻100nF电容到地再配合软件10ms稳定判断。如果只用软件消抖在按键触点氧化后还是会偶发抖动漏判。3. 系统软件核心逻辑与实现步骤3.1 状态机建模交通灯逻辑的灵魂智能交通灯控制逻辑本质就是一个有限状态机FSM。这一点我会不厌其烦地强调因为这是整套软件最值得学习的地方。先定义相位状态typedef enum { STATE_NORTH_GREEN, // 南北绿灯东西红灯 STATE_NORTH_YELLOW, // 南北黄灯东西红灯 STATE_EAST_GREEN, // 东西绿灯南北红灯 STATE_EAST_YELLOW, // 东西黄灯南北红灯 STATE_ALL_YELLOW_BLINK, // 夜间黄闪模式 STATE_EMERGENCY_CLEAR // 紧急模式专用状态 } TrafficState;普通白天模式下状态流转是南北绿灯(30s) → 南北黄灯(3s) → 东西绿灯(30s) → 东西黄灯(3s) → 回到南北绿灯看起来非常简单但这里藏着第一个大坑绿灯和黄灯之间必须存在一个所有方向都为红灯的间隔时间。真实路口在绿灯切换红绿之前会有“全红时间”确保交叉路口的车辆和行人清空这个时间通常1~3秒。很多课程设计的交通灯都没有做这个从美观上没问题从合理性上一眼就会被答辩老师问倒。为此状态机里需要加入过渡状态我建议这样设计南北绿灯 → 南北绿灯闪3秒提示即将变灯 → 南北黄灯(3s) → 全红(2s) → 东西绿灯 → ...如果觉得状态太多不好管理也可以在状态结构体里面通过事件驱动完成。核心思路是每个状态有进入时要执行的GPIO输出动作、有持续的时间、到了时间就跳到下一个状态。典型的实现方式#define T_GREEN_N_S 30 // 南北绿灯时间可按键调整 #define T_YELLOW 3 // 黄灯时间 #define T_ALL_RED 2 // 全红时间 typedef struct { TrafficState state; uint16_t counter; // 剩余时间计数 } TrafficFSM;在定时器中断中每秒减1减到0就触发状态切换在主循环中检测切换标志并执行输出更新。3.2 精准计时用定时器中断替代delay很多新手习惯这样写GPIO拉高→delay(30000)→GPIO拉低→delay(20000)最后做出来的系统一旦有按键或传感器介入就全线崩溃。智能交通灯管理系统里我不会让任何核心逻辑跑在阻塞式delay上。用SysTick或者TIM2作为时间基准每1ms进入一次中断软件计数器累加volatile uint32_t tick_ms 0; void SysTick_Handler(void) { tick_ms; }然后代码里需要一个非阻塞延时判断工具函数uint8_t WaitMs(uint32_t *last_tick, uint32_t interval) { if (tick_ms - *last_tick interval) { *last_tick tick_ms; return 1; // 时间到了返回1 } return 0; }这里千万不要用if (tick_ms target)这种写法因为中断和主循环并不同步极易漏掉跳变。用“差值目标”的方式只要tick_ms是单调递增的就算偶尔被其他中断卡住也不会丢时间。这种思路在RTOS的vTaskDelay底层也是这么实现的。另外提示一下STM32F103的SysTick在标准外设库中是SysTick_Config(SystemCoreClock / 1000)在HAL库里是HAL_SYSTICK_Config()它本身就是为操作系统时钟节拍设计的拿来做毫秒基准再合适不过。3.3 车流量感应与自适应配时逻辑传感器检测部分其实不复杂复杂的是“检测到信号之后系统要怎么决策”。这里我分享一套比较实用的方案不用上什么复杂AI算法纯逻辑就能在答辩时讲清楚。在支路上放两路红外传感器一路靠近路口停止线前一路再往后几米停车排队区。当主路绿灯放行时如果支路停止线前有车第一路传感器持续被挡住超过2秒说明这位车主等了一个周期系统应该在下一轮把支路放行时间延长。如果两路都被占满说明车流量很大支路绿灯时间直接拉满到最大值。实现逻辑可以这样写typedef struct { uint8_t sensor_stopline; // 停止线前传感器状态 uint8_t sensor_queue; // 排队区传感器状态 uint16_t waiting_seconds; // 支路等待时间计数 } BranchDetector;主循环里每秒更新一次等待计数如果支路检测到车而本方向是红灯waiting_seconds。当主路绿灯周期结束时检查这个值waiting_seconds 15或者sensor_queue为高 → 支路绿灯时间设为基础时间的1.5倍否则 → 支路绿灯按基础时间执行这玩意儿叫“基于车辆到达检测的感应控制”专业上属于自适应交通控制里最朴素的实现。但你别小看它运到现场它的效率就已经能比固定配时提升不少了。3.4 紧急模式与夜间模式切换细节紧急车辆优先功能在嵌入式实现上最需要照顾的是“模式恢复”逻辑。按下紧急按键后系统要尽快让当前路口变成放行状态但不能直接一步跳绿——如果当前方向正好是绿灯没问题如果当前方向是红灯直接切绿就会造成交叉方向刚起步的车和紧急车辆相撞。我的实现办法是设置一个紧急模式标志然后让状态机在到达安全切换点时再强制跳转void Emergency_Trigger(void) { emergency_flag 1; } void Traffic_FSM_Tick(void) { if (emergency_flag) { // 等当前周期到达全红间隔状态后直接进入南北绿灯放行 if (fsm.state STATE_ALL_RED) { fsm.state STATE_NORTH_GREEN; fsm.counter T_EMERGENCY_GREEN; // 比如30秒 emergency_flag 0; } } }夜间模式的切换就更直白了按键或者定时判断比如晚上22点到早上6点把状态机切到黄闪状态。黄闪的准确含义是“该方向允许通过但需观察”——通常主干道保持黄闪或者支路红闪具体规则各路口不同代码里只需要让两组黄灯以1Hz频率交替闪烁即可。3.5 OLED倒计时显示驱动用OLED来显示倒计时图形比数码管灵活得多而且接线比数码管省脚位。SSD1306驱动的0.96寸OLEDI2C接口只要两个引脚。初始化部分用标准库操作是这样的I2C_InitTypeDef I2C_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_OD; // I2C必须开漏 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure);显示刷新时注意不要把整个屏幕全部重绘。倒计时数字每秒变化只需将上一秒数字所在的区域清掉用DrawRectangle填充背景色再在新位置画当前数字。否则每秒钟全屏刷新会占用大量CPU时间导致其他逻辑调度被卡顿。4. 调试记录与常见问题排查调试过程是整个项目里最“养人”的阶段。我把自己在实际做这套系统时踩过的几个典型坑拿出来分享这些问题你在做的时候很可能也会遇到。4.1 定时器中断里别做“重活”刚写完系统时我把数码管刷新函数直接放在了定时器中断服务函数里结果主循环里的按键扫描经常失灵。查了很久用示波器测量发现中断执行时间已经超过了1ms的周期中断没退出主循环根本没机会跑。解决办法很简单中断里只做标记和计数把耗时操作比如OLED刷屏、状态机切换全部放到主循环中执行volatile uint8_t fsm_tick_flag 0; void SysTick_Handler(void) { tick_ms; if (tick_ms % 1000 0) { fsm_tick_flag 1; // 每秒置位一次主循环处理 } } int main(void) { while (1) { if (fsm_tick_flag) { fsm_tick_flag 0; Traffic_FSM_Tick(); // 状态机切换 OLED_DisplayTime(remain_sec); } Key_Scan(); Sensor_Update(); } }4.2 红外传感器误触发与供电噪声第一次接入红外模块时发现它在数码管切换的瞬间频繁误触发。用万用表一量5V电源在数码管段选跳变的瞬间有接近0.5V的跌落噪声红外模块供电被污染了。解决办法做了三层一是红外模块的电源和数码管电源分开走线物理隔离二是在模块VCC-GND之间并一个100μF电解电容104陶瓷电容三是软件上增加连续有效性判断传感器信号需要连续保持20ms以上才确认有效有效后还需要“去抖锁定”100ms防止同一辆车被重复计数。4.3 GPIO引脚复用冲突这是一个典型的入门级翻车现场。我用PA9和PA10做串口同时又把按键接在PA10上结果焊好板子发现按键按下时串口会乱码——因为PA10默认是USART1_RX你灌信号进去串口接收缓冲区全是垃圾。所以分配引脚时如果你要用串口调试就优先避开USART引脚如果必须复用需要在代码初始化时先GPIO_PinRemapConfig或明确切换复用功能。做硬件前先画一张引脚分配表把每个GPIO的功能、默认复用、占用情况列清楚能省很多事。4.4 Keil工程配置与下载问题接着说说开发环境。无论你用标准外设库还是CubeMXHAL库Keil MDK里都容易踩几个坑芯片型号选错新建工程时如果没选对STM32F103C8编译链接时会出现Flash Download Failed或烧录成功后不运行。C8T6的RAM是20KBFlash是64KB在Target选项卡里要对应设置否则编译器按大容量芯片分配内存越界了也不知道。下载器识别不到用ST-LINK下载时要确保驱动安装正确如果用的是“ST-Link V2”山寨版连接时降低速度到1MHz稳定很多。头文件路径缺失标准外设库移植到新工程时最容易报一堆error: #5: cannot open source input file stm32f10x.h就是没把Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x和Libraries/STM32F10x_StdPeriph_Driver/inc加入Include Paths。这个错误我见过无数人问包括我自己当年也卡过。4.5 参数掉电保存的坑系统里的绿灯时长参数修改后如果直接断电重启又要重新设置。要把参数保存到EEPROM里STM32F103C8T6内部没有真正的EEPROM但在Flash最后几页预留了模拟EEPROM空间。也可以外挂AT24C02但这样硬件多一块芯片。我这里推荐一个更轻量的办法使用内部Flash最后一个扇区存储参数注意擦除时应先备份当前扇区未修改的数据防止整片擦掉后参数丢失。精简版的保存代码如下// 假设把参数存在Flash最后一页, 先擦除再写入 uint32_t addr 0x0800FC00; // 根据Flash大小调整 FLASH_Unlock(); FLASH_ErasePage(addr); FLASH_ProgramWord(addr, (uint32_t)config_data); FLASH_Lock();如果不想碰Flash操作至少要加一段参数RAM保护开机时检测按键只有按下“设置键”时允许修改防止误触导致配置漂移。4.6 常见问题速查表现象可能原因排查与解决下载程序后LED不亮GPIO时钟未使能检查RCC_APB2PeriphClockCmdGPIOA是APB2总线数码管亮度闪烁动态扫描频率偏低扫描周期大于20ms建议每位分配1~2ms轮询按键偶尔不响应没有消抖 / 中断优先级不对增加硬件RC或软件10ms去抖必要时抢占优先级设高红外误报供电噪声 / 环境光干扰电源隔离滤波、连续电平判定、安装遮光罩串口打印乱码波特率不匹配 / 引脚占用冲突检查USART初始化参数确认引脚无其他复用外设按键设置掉电丢失没有EEPROM存储写Flash模拟EEPROM或外挂AT24C02紧急模式恢复不成状态机无恢复路径在状态机上增加emergency_flag先切换至全红再放行5. 扩展思考这套系统还能怎么升级到这里基于STM32的智能交通灯管理系统主体部分就算做完了。不过它只是起点。做完这个项目之后如果把底层逻辑吃透了往上叠加的东西其实非常多通信组网用ESP8266或SIM800模块把路口各方向的车流量及当前灯态上报到MQTT服务器就能做成“城市路口云监控系统”——在Web端画一个大屏每一路口的红绿灯状态实时显示。这个扩展方向很受毕业设计、物联网竞赛欢迎。多路口协调主干道上连续三四个路口如果每个路口独立运行车速稍微快点就会连续吃红灯。如果让相邻路口通过LoRa或RS485通信共享配时信息实现“绿波带”协调控制那技术含量直接上了一个台阶。AI流量预测把过去的车流量数据存下来用简单的时间序列预测算法比如移动平均、最小二乘拟合法预测下一个小时的车流量动态调节配时。这个不是嘴上说说用STM32跑个简单线性回归模型完全可行RAM和Flash都够用。可视化上位机用Qt或Python写一个串口上位机实时展示路口状态。这样调试时不需要一直盯着OLED或开发板看电脑屏幕上直接数红灯秒数效率会高出不少。不过要提醒一句扩展的每一步都会引入新的复杂度如果时间紧、又是第一次做STM32项目先老老实实把红绿灯状态机跑顺把掉电存储、紧急模式这些基础功能做扎实再谈扩展。项目做出来的高度不在于功能数量多而在于每一个功能都能说得清原理、禁得住追问。我做这个项目的核心体会是所谓“智能”并不玄乎它就是“感知 → 决策 → 执行 → 反馈”这个闭环在嵌入式系统上的落地。交通灯管理是这样其他所有智能硬件项目也都是这样。你把这个环的每一环都亲自实现了一遍以后再面对任何单片机项目心里就有底了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑