简介:本资源是一套基于STM32F103系列单片机的智能交通灯系统完整开发包,面向嵌入式初学者、电子信息类专业学生及课程设计实践者,解决交通控制逻辑实现、硬件仿真验证与工程代码落地等核心学习痛点。压缩包共370个文件,涵盖105个.h头文件(定义外设寄存器与功能接口)、67个.c源文件(含HAL库驱动如tim、uart、i2c及主控逻辑)、50个.o与.d编译中间文件、49个.crf调试信息文件,以及Proteus仿真工程(.pdsprj)、Keil项目(.uvprojx/.uvoptx)、Hex可执行文件和PDF设计报告等,整体28.54MB,结构完整、开箱即用。已有2190人学习下载,资源提供从GPIO灯控时序、定时器中断状态切换到Proteus虚拟硬件联调的全流程支撑,附带WMV操作录屏与多层级注释源码,便于理解状态机设计、外设配置规范及软硬协同调试方法。
1. 一个能真正在Proteus里“亮起来”的交通灯系统:不是Demo图,是带状态机、倒计时、紧急切换、车流自适应逻辑的完整嵌入式闭环
你有没有试过下载一堆“STM32交通灯”资料,解压后发现只有一页PDF原理图+三行main函数?或者仿真跑起来,红绿灯像随机闪烁的迪斯科球,根本看不出哪个方向该通行、倒计时怎么跳、黄灯为什么只闪一次就跳变?这个资源不是那种——它是一套从芯片引脚定义、Keil工程配置、状态迁移表、Proteus元件封装、到仿真波形验证全部对齐的可执行闭环系统。它用标准STM32F103C8T6最小系统(非开发板抽象层),在Proteus 8.13中真实驱动6个LED(东西/南北各红黄绿)、2个7段数码管(双方向倒计时)、1个蜂鸣器(紧急报警)、2个按键(手动切换+紧急模式),所有外设IO映射与实际单片机引脚物理一致。适合刚学完GPIO和SysTick、正卡在“写完代码却不敢烧录怕炸板”阶段的开发者;也适合需要快速验证状态机设计、调试中断优先级、或给课程设计交一份“能答辩演示+老师能现场改参数”的硬核同学。它不讲RTOS,不堆库函数,所有延时用SysTick精准计数,所有切换靠有限状态机(FSM)驱动,连黄灯闪烁的占空比都精确到毫秒级——这不是教学幻灯片,是能让你在Proteus里按下“Play”键后,亲眼看着数码管从“29→28→27…”跳动、LED按相位真实切换、按下按键立刻打断当前周期进入应急模式的可触摸的嵌入式现场。
2. 工程结构与技术栈:Keil MDK-ARM v5.37 + STM32F1xx Standard Peripherals Library + Proteus 8.13 仿真链路全对齐
这个项目不是“Keil写完再凑个Proteus图”,而是从第一天就按硬件-固件-仿真三线同步构建的。整个压缩包解压后是清晰的三层目录:
TrafficLight_STM32/ ├── Keil_Project/ ← 真实可编译的Keil工程(uVision5) │ ├── USER/ ← 主程序入口、状态机核心、按键扫描 │ ├── FWLIB/ ← STM32F10x标准外设库(v3.5.0) │ ├── CMSIS/ ← 标准内核接口层(CM3内核) │ ├── OUTPUT/ ← 编译输出(.axf, .hex, .list) │ └── TrafficLight.uvprojx ← 工程文件(需Keil v5.37+) ├── Proteus_Simulation/ ← 可直接双击运行的Proteus设计文件 │ ├── TrafficLight.DSN ← 主电路图(含STM32F103C8T6、LED、数码管、按键、蜂鸣器) │ └── Components/ ← 自定义元件库(关键!含正确引脚映射的STM32模型) └── Docs/ ← 配套文档(非PPT,是.md格式的调试笔记+状态迁移表) ├── FSM_StateTable.md ← 手绘状态转移图+每个状态的持续时间/触发条件 └── PinMap_And_Timing.md ← 每个LED/数码管段码/按键IO口与STM32引脚对应表+时序要求提示:Proteus中的STM32模型不是默认库里的“黑匣子”,而是作者用STM32F103C8T6数据手册逐引脚重绘的可仿真模型——这意味着你在Keil里把PA0置高,在Proteus里对应LED真的会亮,而不是“看起来像亮”。这种对齐是仿真可信度的根基。
2.1 Keil工程关键配置:不依赖HAL,纯标准库+寄存器级控制
工程基于STM32F10x Standard Peripherals Library v3.5.0(非HAL或LL库),原因很实在:减少抽象层干扰,让初学者看清每一步硬件操作。比如LED控制不用HAL_GPIO_WritePin(),而是直接操作ODR寄存器:
// file: USER/led.c #include "stm32f10x.h" #define LED_EAST_RED_PORT GPIOA #define LED_EAST_RED_PIN GPIO_Pin_0 #define LED_NORTH_GREEN_PORT GPIOB #define LED_NORTH_GREEN_PIN GPIO_Pin_1 void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; // 使能GPIOA和GPIOB时钟 RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA | RCC_APB2PERIPH_GPIOB, ENABLE); // 配置PA0为推挽输出(东向红灯) GPIO_InitStructure.GPIO_Pin = LED_EAST_RED_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(LED_EAST_RED_PORT, &GPIO_InitStructure); // 配置PB1为推挽输出(北向绿灯) GPIO_InitStructure.GPIO_Pin = LED_NORTH_GREEN_PIN; GPIO_Init(LED_NORTH_GREEN_PORT, &GPIO_InitStructure); // 初始全灭 GPIO_SetBits(LED_EAST_RED_PORT, LED_EAST_RED_PIN); // 高电平灭(共阳接法) GPIO_SetBits(LED_NORTH_GREEN_PORT, LED_NORTH_GREEN_PIN); }这段代码的关键点在于:
GPIO_Mode_Out_PP:明确指定推挽输出,避免开漏模式导致LED亮度不足;GPIO_Speed_50MHz:匹配STM32F103C8T6最高72MHz主频下的IO翻转能力;GPIO_SetBits()初始置高:因为Proteus中LED采用共阳极接法(VCC→LED→限流电阻→MCU IO),所以IO输出高电平时LED截止(灭),低电平时导通(亮)——这个细节在Docs/PinMap_And_Timing.md里有明确标注,否则仿真永远不亮。
2.2 Proteus电路设计要点:数码管动态扫描与按键消抖的硬件级实现
Proteus电路不是简单连线,它实现了两个关键硬件行为模拟:
两位共阴数码管动态扫描:使用PB6-PB13作为段码(a~dp),PA8-PA9作为位选(东西/南北)。动态扫描由软件定时器(SysTick)每5ms刷新一次,Proteus中必须确保位选信号与段码信号严格同步,否则出现“鬼影”或闪烁。电路中加入了10kΩ上拉电阻到VCC,保证位选无效时数码管彻底熄灭。
独立按键硬件消抖:两个按键(KEY1=手动切换,KEY2=紧急模式)均采用“上拉+电容滤波”设计。每个按键一端接地,另一端接MCU IO(PC13、PC14),IO口配置为上拉输入(
GPIO_Mode_IPU),并并联100nF陶瓷电容到GND。这个RC网络在Proteus中被建模为真实RC滤波器,使得按键按下/释放的毛刺被自然吸收,软件只需做10ms延时确认即可,无需复杂状态机消抖——这是降低初学者理解门槛的关键设计。
2.3 固件与仿真协同验证:如何确认你的代码真正在“驱动硬件”
很多仿真失败,根源在于没建立固件与仿真的可观测锚点。本项目在USER/main.c中埋了3个关键观测点:
- SysTick中断计数器变量
g_systick_count:每1ms自增1,可在Keil调试时Watch窗口实时查看,同时Proteus中用虚拟逻辑分析仪(Virtual Logic Analyzer)抓取PA0(SysTick标志IO)的方波,频率应严格为1kHz; - 状态机当前状态变量
g_current_state:枚举类型(STATE_EAST_GREEN,STATE_EAST_YELLOW等),Keil中添加该变量到Watch,Proteus中用LED指示灯(如PC0亮=绿灯态,PC1亮=黄灯态)做物理映射; - 倒计时值
g_countdown_sec:全局变量,Watch中观察其递减过程,Proteus中数码管显示值必须与之完全一致。
注意:Proteus中必须勾选
Debug → Enable Debugging,并在Properties of STM32F103C8T6中设置Program File指向Keil生成的.hex文件(路径:Keil_Project/OUTPUT/TrafficLight.hex),否则仿真只是“空跑”。
3. 核心状态机实现:从“红绿灯只会循环”到“支持手动干预+紧急中断+自适应延时”的工业级逻辑
交通灯的灵魂不在LED亮灭,而在状态如何流转、何时流转、被什么打断。本项目采用经典的分层状态机(HSM)设计:顶层是主循环周期(固定60秒),子层是各相位内部动作(如绿灯亮30秒→黄灯闪3秒→红灯亮30秒),并嵌入中断响应机制。所有状态迁移逻辑集中在USER/traffic_fsm.c中。
3.1 状态定义与迁移表:用枚举+宏保证可读性与可维护性
// file: USER/traffic_fsm.h typedef enum { STATE_INIT = 0, STATE_EAST_GREEN, // 东向绿灯(南北红灯) STATE_EAST_YELLOW, // 东向黄灯(南北红灯) STATE_NORTH_GREEN, // 北向绿灯(东西红灯) STATE_NORTH_YELLOW, // 北向黄灯(东西红灯) STATE_EMERGENCY, // 紧急模式:全红+蜂鸣器 } traffic_state_t; // 每个状态的持续时间(单位:秒),可 runtime 修改 #define TIME_EAST_GREEN 30 #define TIME_EAST_YELLOW 3 #define TIME_NORTH_GREEN 30 #define TIME_NORTH_YELLOW 3 #define TIME_EMERGENCY_HOLD 10 // 紧急模式保持时间状态迁移不是靠switch-case硬编码,而是用查表法实现,FSM_StateTable.md中明确列出:
| 当前状态 | 触发条件 | 下一状态 | 动作说明 |
|---|---|---|---|
| STATE_EAST_GREEN | SysTick计满30秒 | STATE_EAST_YELLOW | 灭东绿,亮东黄,启黄灯定时器 |
| STATE_EAST_YELLOW | 黄灯定时器计满3秒 | STATE_NORTH_GREEN | 灭东黄,亮北绿,启倒计时 |
| STATE_NORTH_GREEN | SysTick计满30秒 | STATE_NORTH_YELLOW | 灭北绿,亮北黄 |
| STATE_NORTH_YELLOW | 黄灯定时器计满3秒 | STATE_EAST_GREEN | 灭北黄,亮东绿 |
| 任意状态 | KEY2按下(紧急键) | STATE_EMERGENCY | 全红+蜂鸣器+禁用自动切换 |
| STATE_EMERGENCY | 计时满10秒 或 KEY2再按 | STATE_EAST_GREEN | 恢复东向绿灯起始态 |
这种设计让逻辑变更极其简单:改一个宏定义,就能调整绿灯时长;加一行表项,就能新增“夜间模式”(仅黄灯闪烁)。
3.2 紧急模式中断处理:抢占式响应,不丢帧
紧急模式由外部中断EXTI_Line14(对应PC14按键)触发,配置为下降沿触发,优先级高于SysTick:
// file: USER/interrupt.c void KEY2_EXTI14_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line14) != RESET) { // 1. 立即关闭所有LED(安全第一) GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1); // 东红/东黄 GPIO_SetBits(GPIOB, GPIO_Pin_0 | GPIO_Pin_1); // 北红/北黄 // 2. 启动蜂鸣器(PB15推挽输出) GPIO_ResetBits(GPIOB, GPIO_Pin_15); // 3. 强制切换状态机 g_current_state = STATE_EMERGENCY; g_countdown_sec = TIME_EMERGENCY_HOLD; // 4. 清中断标志 EXTI_ClearITPendingBit(EXTI_Line14); } }这里的关键是不等待当前状态完成:哪怕东向绿灯刚亮1秒,按下KEY2立刻全红+蜂鸣。这种抢占式设计符合真实交通控制需求,也是很多初学者忽略的“中断必须快进快出”原则。
3.3 倒计时动态更新:数码管段码与位选的精准时序
倒计时显示不是简单调用printf,而是由SysTick中断服务程序(ISR)每100ms更新一次:
// file: USER/disp.c volatile uint8_t g_display_buffer[2] = {0}; // [0]=东向, [1]=北向 void SysTick_Handler(void) { static uint16_t systick_100ms = 0; systick_100ms++; if(systick_100ms >= 10) { // 10 * 100ms = 1s systick_100ms = 0; if(g_current_state != STATE_EMERGENCY && g_countdown_sec > 0) { g_countdown_sec--; } } // 动态扫描:每5ms切换一位数码管 static uint8_t digit_sel = 0; digit_sel = (digit_sel + 1) % 2; // 关闭上一位 GPIO_SetBits(GPIOA, GPIO_Pin_8 | GPIO_Pin_9); // 输出当前位段码 GPIO_Write(GPIOB, seg7_code[g_display_buffer[digit_sel]]); // 选通当前位 if(digit_sel == 0) { GPIO_ResetBits(GPIOA, GPIO_Pin_8); // 选通东向 } else { GPIO_ResetBits(GPIOA, GPIO_Pin_9); // 选通北向 } }seg7_code[]是预定义的共阴数码管段码表(0x3F, 0x06, 0x5B...),GPIO_Write(GPIOB, ...)直接写整个端口,比逐位操作快一个数量级——这对5ms级动态扫描至关重要,否则数码管会明显闪烁。
4. 常见问题排查:那些让你对着Proteus干瞪眼、怀疑人生的真实翻车现场
仿真跑不起来?LED不亮?数码管乱码?别急着重装软件——这些问题90%出在三个“看似 trivial 实则致命”的环节。以下是我在帮某高校实验室调试23台学生机时,高频踩坑的血泪记录:
4.1 现象:Proteus中LED全灭,但Keil调试显示IO口电平已变
原因:Proteus中LED模型未设置正确工作电压或限流电阻。默认LED模型Vf=1.8V,但实际红光LED常为2.0~2.2V;若电源为5V,限流电阻小于220Ω,LED电流超限被Proteus静默保护。
解决:双击LED元件 →Edit Properties→ 将Voltage Drop改为2.1V,Series Resistance改为270;检查电路中是否遗漏限流电阻(必须有,不能靠MCU内部上拉替代)。
4.2 现象:数码管显示“88”或全暗,且随仿真时间推移越来越暗
原因:动态扫描时序错乱。常见于SysTick中断未使能,或SysTick_Config()返回值未检查(若系统时钟未稳定,该函数返回0,中断永不触发)。
解决:在main()中加入校验:
if (SysTick_Config(SystemCoreClock / 1000) == 0) { while(1) { /* 系统时钟配置失败,死循环 */ } }并在Proteus中用逻辑分析仪抓取PA8/PA9位选信号,确认其是否以5ms周期交替翻转。
4.3 现象:按键KEY1按下后,状态切换延迟2~3秒,或完全无响应
原因:按键消抖逻辑错误。本项目采用“电平检测+10ms延时确认”,但若在while循环中直接Delay_ms(10),会阻塞整个系统,导致SysTick中断无法及时处理倒计时。
解决:改用状态机消抖——定义key_state_t枚举,用g_key_debounce_counter计数器在SysTick ISR中累加,主循环中判断计数器是否≥10再执行动作,全程不阻塞。
4.4 现象:紧急模式触发后,松开KEY2,系统卡在STATE_EMERGENCY不恢复
原因:EXTI中断未配置为“下降沿+上升沿”双触发,或未清除中断挂起位(EXTI_ClearITPendingBit()位置错误)。
解决:在KEY2_EXTI14_IRQHandler中,必须在修改g_current_state之前清除中断标志;且在初始化时启用双沿触发:
EXTI_InitTypeDef EXTI_InitStructure; EXTI_InitStructure.EXTI_Line = EXTI_Line14; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 仅下降沿触发 EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); // 注意:不要用 EXTI_Trigger_Rising_Falling,Proteus不支持4.5 现象:Keil编译通过,但Proteus报错“Cannot load program file”
原因:Keil生成的.hex文件路径含中文或空格,或Proteus中Program File路径未更新为最新编译结果。
解决:将Keil工程放在纯英文路径下(如D:\TrafficLight\Keil_Project\),编译后手动在Proteus中右键STM32元件 →Edit Properties→ 更新Program File路径,并确认文件扩展名为.hex(非.axf)。
5. 二次开发实战:从“跑通Demo”到“交付可用系统”的3个关键改造技巧
拿到这个项目,跑通只是起点。真正体现你嵌入式能力的,是能否在2小时内把它改成符合具体场景需求的系统。我带过的几个学生项目,都是靠以下三个技巧快速落地:加传感器、改通信、做低功耗。下面以“增加红外车流检测”为例,手把手拆解改造全过程。
5.1 改造目标:在东西向增加红外对管,车流量大时自动延长绿灯时间(自适应控制)
原始项目是固定时长(30秒),但真实路口需要根据车流动态调整。我们用TCRT5000红外对管(数字输出型)接PC0,当检测到车辆时输出低电平。
步骤1:硬件接入与IO配置
- 在Proteus中添加TCRT5000元件,VCC接5V,GND接地,OUT接STM32的PC0;
- 修改
USER/gpio.c,初始化PC0为浮空输入:
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(GPIOC, &GPIO_InitStructure);步骤2:在状态机中注入车流感知逻辑
不改动原有状态迁移表,而是在STATE_EAST_GREEN状态下增加“车流计数”分支:
// 在 traffic_fsm.c 的状态处理函数中 case STATE_EAST_GREEN: if(g_countdown_sec > 0) { // 原有倒计时逻辑 break; } // 倒计时归零,但先检查车流 uint8_t car_detected = !GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_0); // 低电平有效 if(car_detected && g_car_count < 5) { g_car_count++; // 连续检测到车,计数+1 g_countdown_sec = TIME_EAST_GREEN + 5; // 延长5秒 break; } // 否则正常进入黄灯 g_current_state = STATE_EAST_YELLOW; g_countdown_sec = TIME_EAST_YELLOW; break;关键点:
g_car_count是静态局部变量,只在STATE_EAST_GREEN内有效,避免跨状态污染;延长上限设为5秒,防止无限延长。
步骤3:Proteus中模拟车流并验证
- 在Proteus中,用
Digital Clock元件(频率1Hz)驱动TCRT5000的IN端,模拟车辆经过时的脉冲; - 运行仿真,观察数码管:当“东向倒计时”从30跳到35,再跳到40,证明自适应生效;
- 用逻辑分析仪抓取PC0波形,确认其与
Digital Clock严格同步。
5.2 进阶技巧:用USART1透传状态,让手机APP实时监控
很多课程设计要求“远程监控”,其实不用NB-IoT或WiFi模块。利用STM32的USART1(PA9/PA10)接USB转TTL模块,在Proteus中用Virtual Terminal即可模拟:
// 在 main.c 初始化后添加 void USART1_Init(void) { USART_InitTypeDef USART_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA | RCC_APB2PERIPH_USART1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽 GPIO_Init(GPIOA, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); USART_Cmd(USART1, ENABLE); } // 在SysTick_Handler中,每秒发送当前状态 if(systick_100ms == 0) { char buf[32]; sprintf(buf, "State:%d Count:%d\r\n", g_current_state, g_countdown_sec); for(int i=0; buf[i]; i++) { while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); USART_SendData(USART1, buf[i]); } }在Proteus中双击Virtual Terminal,设置波特率115200,即可看到实时打印:State:1 Count:28—— 这就是最轻量级的物联网监控。
5.3 血泪经验:每次新增功能,必须强制走一遍“三步验证法”
从那以后我每次给学生改交通灯项目,都强制他们做完三件事才许提交:
- Keil中单步调试:在新添加的
if(car_detected...)处打断点,确认变量值符合预期; - Proteus中波形验证:用逻辑分析仪抓PC0(传感器)和PA0(东红灯)信号,看两者时序关系是否合理;
- 人为注入故障:拔掉TCRT5000的VCC,观察系统是否降级为固定时长(fail-safe),而不是死机。
这三步看似繁琐,但能筛掉80%的“仿真能跑、实物炸板”类问题。希望帮到你。
本文还有配套的精品资源,点击获取