
简介基于STM32单片机智能交通灯设计全套工程包面向嵌入式初学者、电子竞赛备赛者及相关课程设计人群解决交通灯时序控制、Proteus仿真验证与实际电路搭建等典型问题。资源共370个文件压缩包约28.54MB包含大量C语言与头文件源码.c/.h、Keil工程.uvprojx、编译生成的hex/axf、Proteus仿真工程.pdsprj以及PDF/DOC说明文档与原理图等覆盖“编写—编译—仿真—调试”完整流程。设计基于STM32F10x系列重点关注GPIO驱动LED电路、定时器中断切换红绿灯状态、C语言状态机逻辑并提供Proteus虚拟环境下信号波形观察与排错思路。资料附设计报告、电路图、实验截图及操作视频.wmv可无实物快速复现并理解交通灯控制算法。已有2179人学习/下载适合系统入门STM32与智能交通控制设计。 我去年帮人调试过好几个基于STM32的智能交通灯设计说实话这个题目在毕业设计和课程实训里出现频率极高但很多人拿到的资料要么程序风格混乱没法改要么Proteus仿真文件元件库对不上直接打不开。借着这个项目标题我把整套方案从头到尾拆开讲一遍包括设计思路、关键代码逻辑、Proteus搭建步骤以及我踩过的几个典型坑希望能帮你少走弯路。1. 整体设计方案与思路拆解1.1 为什么选择STM32系列来做交通灯控制很多人的第一反应是做个交通灯用51单片机不就行了确实51能完成基本时序控制但STM32在这个题目里的优势在于定时器资源更丰富。交通灯核心是精确计时STM32的通用定时器可以做到微秒级中断配合中断服务程序做状态切换比51的定时器更稳定。GPIO口充足且可以承受5V逻辑输入部分引脚方便直接接数码管、LED灯组。学习价值更高。STM32是目前工业控制和嵌入式开发的主流平台做完这个项目后续扩展倒车雷达、智能小车、环境监测等毕业设计底层配置逻辑是通用的。从实用角度讲Proteus仿真中通常选用STM32F103系列最常见的型号比如STM32F103C6或STM32F103R6其100脚以下的封装在仿真里资源完全够用。注意STM32项目不像51那样需要设置EA1、TMOD等寄存器它的外设都是挂在总线上的使用前必须先使能对应外设时钟RCC这是很多新手从51转过来后最容易漏掉的地方。1.2 智能交通灯的功能需求与工作状态拆解一套标准的智能交通灯系统通常包含以下功能点东西方向主干道和南北方向支干道的红绿灯自动切换。每个方向配套倒计时数码管显示方便观察剩余时间。支持紧急模式特勤车辆通行可通过按键强制切换。支持夜间黄灯闪烁模式通过拨码开关或按键开启。系统上电后自动进入正常运行时序。这里我按照主干道优先、支干道轮换的方式来设计。状态机是整个程序的核心逻辑。常规的时序可以设定为状态主干道东西支干道南北持续时间S0绿灯亮放行红灯亮禁行30秒S1黄灯亮过渡红灯亮禁行3秒S2红灯亮禁行绿灯亮放行20秒S3红灯亮禁行黄灯亮过渡3秒整个系统在S0到S3之间循环。相比直接用delay函数死等我更建议用定时器中断配合标志位来做这样CPU可以在计时过程中同时处理按键输入和数码管刷新不会出现按下紧急模式按钮后卡死半天的尴尬。2. 核心原理与工程细节解析2.1 交通灯状态切换逻辑为什么用状态机状态机是单片机项目里最常见的程序架构。做交通灯如果不引入状态机概念直接用if-else嵌套会很痛苦——因为四个状态之间的切换条件不是简单的顺序执行中间需要判断当前计数是否结束、是否有紧急按键触发、是否处于夜间模式。实际代码中我定义一个枚举类型来标识当前状态typedef enum { STATE_MAIN_GREEN 0, // 主干道绿灯 STATE_MAIN_YELLOW, // 主干道黄灯 STATE_BRANCH_GREEN, // 支干道绿灯 STATE_BRANCH_YELLOW, // 支干道黄灯 STATE_EMERGENCY, // 紧急通行 STATE_NIGHT_FLASH // 夜间闪烁 } TrafficState;在主循环或中断里只需要根据当前状态决定灯组输出同时递减对应的计数变量。当计数值减到0时切换到下一个状态。这种写法逻辑清晰后期想调整时间参数只需要改宏定义不需要大改程序结构。2.2 精准计时的实现思路定时器中断 变量累加交通灯对时间精度要求不算苛刻但也不能用粗暴的delay延时否则系统响应不了按键中断。我的做法是配置TIM2或TIM3作为基础时基让它每1ms触发一次中断。在中断服务函数里对一个全局变量tick_ms做自增。主循环中判断tick_ms是否达到目标值比如5000表示5秒。关键的定时器配置代码结构如下以TIM3为例使用库函数风格void TIM3_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_TimeBaseStructure.TIM_Prescaler 7199; // 72MHz / 7200 10kHz TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 9; // 10kHz / 10 1kHz 1ms TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM3, ENABLE); } void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); tick_ms; } }这里最核心的坑在于如果你在Proteus仿真里使用内部RC时钟或者设置错了系统时钟频率整个延时时间会全部漂移。Proteus默认仿真STM32时如果代码里没有显式配置RCC经常出现定时器频率不对的问题。所以我在实际工程里优先选择8MHz外部晶振然后通过PLL倍频到72MHz这样仿真和实物行为一致。2.3 数码管动态扫描刷新的细节倒计时显示一般用两位或四位一体数码管。Proteus里最常用的元件是7SEG-MPX4-CC共阴或7SEG-MPX4-CA共阳。用共阴还是共阳直接决定段码表怎么写也决定驱动管脚的电平逻辑。如果用的是共阴数码管公共端COM要由单片机输出低电平选中位段选引脚输出高电平点亮对应段。如果接反数码管怎么刷都不亮。动态扫描的原理就是利用人眼视觉暂留快速轮流点亮每一位。要注意刷新频率我一般把刷新周期控制在2~4ms左右也就是每位间隔1ms左右。刷新太快单片机忙着刷数码管影响其他逻辑刷新太慢能明显看到闪烁。我在中断里直接刷新const uint8_t seg_code[10] { 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F // 9 };需要特别提醒Proteus仿真里数码管如果直接接STM32的GPIO建议给每个段选引脚串一个220Ω~330Ω的限流电阻否则仿真上看不出问题实际焊板子时数码管容易过流发烫亮度也不均匀。3. 实操过程与Proteus仿真搭建详解3.1 Proteus元件选型与电路连接打开Proteus我用的是8.9版本8.x以上都兼容新建工程后从元件库里查找以下元件元件名称关键字用途STM32F103C6STM32F103C6主控芯片数码管7SEG-MPX4-CC倒计时显示共阴四位一体红色LEDLED-RED红灯黄色LEDLED-YELLOW黄灯绿色LEDLED-GREEN绿灯电阻RES限流/上拉按键BUTTON紧急模式、夜间模式切换电路连接需要注意几个地方LED灯组接法LED负极接STM32 GPIO引脚正极通过330Ω电阻接5V电源这样是灌电流方式STM32 GPIO输出低电平点亮。这种方式比推挽直接输出高电平点灯更稳因为STM32的GPIO灌电流能力通常大于拉电流能力。数码管数据线A~G和DP依次接GPIO我用的是PA0~PA7四个公共端接PB0~PB3。注意公共端和三极管不需要在仿真里连接实物板子才需要加三极管驱动因为STM32引脚拉不动四位同时点亮的大电流。晶振电路在仿真里可以不接外部晶振程序里用内部RC也能跑起来但为了仿真和实物一致还是建议按照标准电路放上8MHz晶振和两个20pF电容。提示Proteus仿真时默认STM32不需要接复位电路和BOOT配置也不影响运行。但如果你把同样的工程烧录到实际开发板上务必检查BOOT0引脚要接地否则芯片上电后进不了用户程序。3.2 源程序设计思路与关键代码整个程序我拆成几个功能模块LED控制、数码管驱动、定时器、按键检测、状态机主循环。这样写的好处是调试方便比如单独测试数码管时不用关心交通灯状态逻辑。主循环的简化框架可以这样理解int main(void) { SystemInit(); GPIO_Config(); TIM3_Init(); Key_GPIO_Init(); current_state STATE_MAIN_GREEN; state_counter MAIN_GREEN_TIME; while (1) { if (tick_ms 1) { tick_ms 0; Traffic_State_Machine(); // 状态机处理 Display_Scan(); // 数码管动态扫描 Key_Scan(); // 按键检测 } } }这种架构下主循环不断检测tick_ms标志保证时间基准是稳定的1ms。状态机函数里根据当前状态点亮对应灯组并且每秒递减计数器计数器减到0时按顺序切换状态。紧急模式的处理逻辑是按键按下后无论当前东西还是南北方向通行立即让当前绿灯方向转入黄灯3秒再切换到另一方向绿灯并且让紧急方向保持30秒绿灯。这里用了一个紧急标志位状态机在切换方向前会先检查这个标志。void Traffic_State_Machine(void) { static uint16_t count_1s 0; count_1s; if (count_1s 1000) // 1秒到达 { count_1s 0; if (state_counter 0) { state_counter--; } else { // 状态切换 switch (current_state) { case STATE_MAIN_GREEN: current_state STATE_MAIN_YELLOW; state_counter YELLOW_TIME; break; case STATE_MAIN_YELLOW: current_state STATE_BRANCH_GREEN; state_counter BRANCH_GREEN_TIME; break; case STATE_BRANCH_GREEN: current_state STATE_BRANCH_YELLOW; state_counter YELLOW_TIME; break; case STATE_BRANCH_YELLOW: current_state STATE_MAIN_GREEN; state_counter MAIN_GREEN_TIME; break; default: break; } } } // 根据状态点亮对应灯组 switch (current_state) { case STATE_MAIN_GREEN: Main_Green_On(); Branch_Red_On(); break; // 其余case类似... } }一个容易忽略的细节黄灯时间到了以后应该让该方向红灯亮起再让另一方向绿灯亮中间不要留下“全灭”的状态。调试时最容易出现的问题是状态切换的瞬间所有灯灭了一下就是因为在代码里先关了所有灯再点亮目标灯。正确做法是一起设置或者先点亮目标灯再关闭旧灯。3.3 仿真编译与烧录步骤Proteus本身不能编译C语言需要先用Keil MDK或者STM32CubeIDE生成HEX文件然后加载到Proteus的单片机里。具体操作路径如下在Keil中编写完毕程序勾选Output选项卡里的Create HEX File编译生成.hex文件。回到Proteus双击电路里的STM32F103C6芯片。在Program File一栏点击文件夹图标选择刚才生成的HEX文件。晶振频率根据代码里的时钟初始化填写如果代码里用内部RC就填8MHz如果用外部PLL则可以不填或填外部晶振频率Proteus主要用它做时间仿真步长计算。点击Proteus左下角的运行按钮观察仿真效果。注意如果点击运行后STM32芯片上没有任何反应先检查是否成功加载了HEX文件。这是一个非常基础的错误但几乎每天都有新手问。加载完后芯片图标的颜色会有变化通常是绿色没加载时是灰的。4. 常见问题与排查技巧实录4.1 仿真运行时数码管不亮或亮度暗这个现象在Proteus里很常见原因是动态扫描刷新频率不够或者GPIO配置成了开漏模式。STM32的GPIO默认是浮空输入状态你需要把它配置为推挽输出GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出 GPIO_Init(GPIOA, GPIO_InitStructure);另外Proteus仿真速度如果调得太低也会让人感觉数码管在闪。运行仿真时右下角有仿真步长控制如果频率显示低于20%绿色条说明仿真速度不够电脑配置不行或者电路中仿真元件太多。我的经验是把没必要的虚拟仪器全删掉仿真速度能提升不少。4.2 Keil编译报错STM32型号选择不对Keil中如果没有预先安装对应的器件包编译会报错Device not found。这个不属于程序问题纯粹是开发环境问题。解决办法是Keil5需要先安装STMicroelectronics系列的支持包在Pack Installer中找STM32F1xx_DFP。在Options for Target - Device里选择STMicroelectronics - STM32F1系列 - 具体的芯片型号对应Proteus里的型号比如STM32F103C6。使用了库函数但没添加对应的stm32f10x_conf.h头文件也会报一堆找不到定义检查C/C选项卡里的Include Paths是否包含了标准库路径。如果用的是标准外设库还需要定义STM32F10X_HD或者STM32F10X_MD根据Flash大小选择。我见过有人用STM32F103C8的工程却在配置里选了STM32F10X_HD结果程序能编译但运行行为异常。另外一个常见错误提示是error: no stm32 target found! if your product embeds debug authentication, please...这个提示通常出现在连接调试器时而不是编译阶段Proteus仿真里一般不会遇到。如果你在真实板子上调试看到这段英文先检查ST-Link驱动、芯片供电、SWDIO和SWCLK接线。不过在Proteus里直接运行仿真即可不需要烧录调试器。4.3 按键切换紧急模式无效按键没有反应90%的可能是GPIO模式配置问题。按键检测的经典电路是按键一端接GPIO引脚另一端接地GPIO内部配置为上拉输入。这样按键按下时引脚读到低电平。GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入如果配成了推挽输出按键按下根本不会改变引脚电平程序自然检测不到。还有一个细节是按键消抖我习惯用10ms左右的软件消抖不然按下一次可能触发多次状态切换紧急模式进去就出不来了。消抖代码很简单void Key_Scan(void) { static uint8_t last_key_state 1; static uint16_t key_debounce 0; uint8_t current_key_state GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_8); if (current_key_state 0 last_key_state 1 key_debounce 0) { key_debounce 10; // 10ms后确认 } if (key_debounce 0) { key_debounce--; if (key_debounce 0 current_key_state 0) { // 确认按键按下切换状态 Emergency_Mode_Trigger(); } } last_key_state current_key_state; }这里使用了一个简单的递减延时逻辑不会阻塞其他代码运行非常适合在主循环中调用。4.4 仿真运行速度极慢或中途崩溃Proteus仿真STM32本身比仿真51要消耗更多资源因为ARM内核的复杂度高很多。如果仿真跑到一半卡死建议做这几件事把虚拟示波器、逻辑分析仪等不必要设备全部删除只保留基本的LED、数码管。将仿真步长调节为单片机的实时比例可以通过运行控制面板调整但尽量让程序逻辑以真实的定时器中断运行不要用无限加速。Protesu版本过低时对STM32的支持不完善建议至少用8.6以上版本我实测8.9和8.17都比较稳定。尽量不要在仿真里使用PA8/PA9这种映射到USART1的引脚做普通IO控制。某些情况下Proteus默认加载串口外设状态导致引脚电平异常。实操中的补充经验最后再分享一个我用的比较多的调试验证技巧在状态机的每个分支里临时加一个调试变量实时发送到虚拟串口Proteus里直接接一个VIRTUAL TERMINAL这样可以在电脑上看到当前状态编号和计数值不需要通过眼睛去数LED闪烁。等程序稳定后再把这些调试代码删掉。这个做法非常适合排查那种“看起来灯没反应实际代码在跑”的情况。另外这次项目里的倒计时时间参数我只用了简单宏定义后期如果要扩展成根据车流量动态调整绿灯时长只需要把宏定义替换成变量再准备一个红外对管模块或光电传感器接口就能升级。整个项目从仿真迁移到实物也非常方便只需要把STM32F103最小系统板、LED灯组和数码管按仿真电路焊好即可程序不用改。希望这份拆解对正在做这个题目的你有帮助。做单片机项目最大的阻力往往不是代码本身而是对整体架构的不清晰。把状态机理清楚、把定时器中断配好、把Proteus的元件连接确认完后面的一切都会顺起来。本文还有配套的精品资源点击获取