简介:这是一套面向嵌入式初学者与电子类专业学生的STM32交通灯控制系统完整实践资料,围绕ARM Cortex-M内核微控制器展开,解决交叉路口信号灯智能化控制这一典型实时应用问题。压缩包共191个文件,约9.28MB,以c源文件、h头文件、o目标文件、crf编译中间文件及s启动文件为主,另含uvproj工程、sct分散加载、hex烧录文件、map映射表,以及1份doc报告和1份ppt演示文稿,覆盖从底层驱动到工程配置的完整链路。代码部分包含时钟与GPIO初始化、定时器中断切换灯态、车流量传感器处理、控制逻辑与LED显示驱动等模块;PPT讲解系统框图、硬件连接、软件流程与控制算法;报告则总结项目目标、实施步骤、挑战与解决方案。目前已有7027人学习下载,适合通过代码与文档对照,理解微控制器在交通控制中的实际工程应用,提升硬件设计与软件开发能力。
1. 从一份 STM32 交通灯资源说起:为什么它值得你花时间拆一遍
如果你正在搜「STM32 交通灯」,大概率不是想听一遍红黄绿怎么循环——那个逻辑用 51 单片机也能写。真正让人卡住的,往往是另一类问题:答辩 PPT 里状态机图怎么画才不被导师追问、报告里定时器参数怎么算才自洽、代码烧进去之后灯不亮到底是硬件还是逻辑的锅。这份资源就是冲着这些场景来的,它把 STM32 交通灯控制系统拆成了可复现的工程包,包含源码、PPT 和报告三部分,适合课程设计、嵌入式入门练手,以及需要快速搭出一套能讲清楚、能跑起来的演示系统的人。下面我按自己拆包的习惯,把这份东西从原理到落地讲透,重点放在参数怎么定、坑在哪。
2. 交通灯状态机与 STM32 定时器:先搞懂为什么这么设计
2.1 状态机不是画着好看,它决定了代码结构
交通灯的本质是一个有限状态机,四个状态循环:东西绿、东西黄、南北绿、南北黄。很多新手一上来就写四个delay堆在一起,结果按键调时间、紧急模式全加不进去。这份资源用的是状态机写法,每个状态对应一个结构体,里面存当前灯色、持续时间、下一个状态。这样做的直接好处是:改配时只改结构体数组,不用动逻辑;加一个夜间黄闪模式,只需要多一个状态分支。
我一般会把状态定义成枚举,配一个状态表:
typedef enum { EW_GREEN, EW_YELLOW, NS_GREEN, NS_YELLOW, NIGHT_FLASH } TrafficState; typedef struct { TrafficState state; uint16_t duration_ms; // 该状态持续毫秒 TrafficState next; } StateNode; // 配时表:东西绿 8s,黄 2s,南北绿 8s,黄 2s StateNode stateTable[] = { {EW_GREEN, 8000, EW_YELLOW}, {EW_YELLOW, 2000, NS_GREEN}, {NS_GREEN, 8000, NS_YELLOW}, {NS_YELLOW, 2000, EW_GREEN}, };逻辑说明:duration_ms决定这个状态待多久,next指向下一个状态。主循环里只做一件事——查表、计时、切换。参数怎么改?把 8000 改成 12000 就是东西向绿灯 12 秒,不需要动任何判断语句。注意duration_ms用uint16_t最大只能到 65535,也就是 65 秒左右,如果要做 90 秒长配时,得换成uint32_t,这是很多人第一次改参数时会踩的溢出坑。
2.2 定时器选型:SysTick 还是 TIM,差别比你想的大
资源里用的是 TIM2 做 1ms 基准中断,而不是 SysTick。原因很实际:SysTick 通常被 HAL 库拿去做HAL_Delay的时基,如果你在中断里再塞状态机计时,容易和库函数打架。TIM2 独立出来,中断里只做一件事——给每个状态倒计时减 1,减到 0 就切状态。
配置 TIM2 的关键参数就三个:预分频器、自动重装载值、时钟分频。假设系统时钟 72MHz,要 1ms 中断一次:
// 72MHz / 72 = 1MHz,即 1us 计数一次 htim2.Init.Prescaler = 72 - 1; // 计数 1000 次 = 1ms htim2.Init.Period = 1000 - 1; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;逻辑说明:预分频器把 72MHz 降到 1MHz,自动重装载值决定数到多少溢出。两个参数一乘就是中断周期。这里有个常见翻车点:有人把 Prescaler 写成 72,结果实际是 72+1=73 分频,周期变成 1.0139ms,跑一天下来累积误差好几秒。STM32 的预分频器和重装载值都是「写进去的值加一才生效」,这个加一必须记住。
2.3 GPIO 驱动:灌电流还是拉电流,决定你烧不烧灯
交通灯用 6 个 LED 代表两个方向的红黄绿。资源里 LED 接的是推挽输出,低电平点亮。为什么低电平点亮?因为 STM32 的 GPIO 灌电流能力比拉电流强,一般单个引脚灌电流可以到 20mA 以上,拉电流则弱一些。如果你接的是共阳 LED,代码里就要写HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET)来点亮。
我见过有人直接把 LED 接在引脚和地之间,然后写高电平点亮,灯是亮了但亮度很暗,还以为是代码问题。其实是拉电流不够。稳妥做法是 LED 串联限流电阻后接 3.3V,引脚输出低电平灌电流点亮。限流电阻怎么算?红色 LED 压降约 1.8V,目标电流 8mA,电阻 = (3.3 - 1.8) / 0.008 ≈ 187Ω,取 200Ω 或 220Ω 都行。这个计算写进报告里,导师一看就知道你是真搭过电路。
3. 从零跑通:工程搭建、代码烧录与配时调试
3.1 工程目录怎么组织,决定了你后期改起来痛不痛
拿到资源包后别急着打开 main.c 就编译。先看目录结构,常见做法是分成 Core、Drivers、User 三层。Core 放 CubeMX 生成的初始化代码,Drivers 放 HAL 库,User 放你自己的状态机、GPIO 封装、按键处理。这样分的好处是:CubeMX 重新生成代码时不会覆盖你的业务逻辑。
如果你自己从 CubeMX 起工程,按这个顺序配:
- RCC 里把 HSE 设为 Crystal/Ceramic Resonator,外部晶振比内部 RC 准得多,串口通信和定时都靠它。
- SYS 里 Debug 选 Serial Wire,不选的话下次可能连不上下载器。
- GPIO 里把 6 个 LED 引脚设为 Output Push Pull,初始电平 High(灭)。
- TIM2 设为 Internal Clock,分频和重装载按上面算的填。
- 中断里把 TIM2 global interrupt 勾上,优先级设 1 或 2,别设 0,0 留给系统关键中断。
生成代码后,把状态机逻辑写进while(1)之前,中断回调里写倒计时。注意 HAL 库的中断回调函数名是HAL_TIM_PeriodElapsedCallback,写错一个字母就不会进中断,这是血泪经验。
3.2 烧录与调试:灯不亮时先查这三处
代码编译通过不代表灯会亮。我一般按这个顺序排查:
第一,查时钟。在SystemClock_Config里确认HAL_RCC_GetHCLKFreq()返回的是不是 72MHz。如果外部晶振没起振,HAL 库会自动切到内部 HSI,频率变成 16MHz,定时器周期全乱,灯闪得飞快或者根本不闪。
第二,查引脚。用万用表量 LED 两端电压,如果一直是 3.3V 不变,说明引脚没翻转。这时候看代码里HAL_GPIO_TogglePin的端口和引脚号对不对,GPIOA和GPIO_PIN_5这种参数写反了不会报错,但灯就是不亮。
第三,查中断。在HAL_TIM_PeriodElapsedCallback里翻转一个测试引脚,用示波器看有没有 1ms 方波。没有的话,检查HAL_TIM_Base_Start_IT(&htim2)有没有调用。很多人初始化完定时器忘了启动,代码看着全对,就是不动。
// 主循环里启动定时器中断,只调一次 HAL_TIM_Base_Start_IT(&htim2); // 中断回调里做倒计时 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { if (currentDuration > 0) { currentDuration--; } else { // 切换到下一个状态 currentState = stateTable[currentState].next; currentDuration = stateTable[currentState].duration_ms; updateLEDs(currentState); } } }逻辑说明:currentDuration是全局变量,每 1ms 减一。减到 0 就查表切状态并刷新 LED。参数duration_ms在状态表里改,updateLEDs里根据状态写 GPIO。注意中断里不要放HAL_Delay或串口打印,会阻塞其他中断,导致计时不准。
3.3 配时参数怎么定:别拍脑袋,按路口流量算
报告里如果只写「绿灯 8 秒」会被问为什么是 8 秒。常见做法是按路口车流量估算:假设东西向车流量是南北向的 1.5 倍,总周期 20 秒,那东西绿可以给 8 秒,南北绿给 5 秒,黄灯各 2 秒,剩下 3 秒做全红清空路口。这个计算过程写进报告,比单纯贴代码有说服力。
资源里的配时表可以直接改,改完重新编译烧录就行。如果你要加按键调时间,在外部中断里改stateTable数组的值,注意改的时候先关定时器中断,改完再开,否则可能改到一半状态切换了,出现两个方向同时绿灯的玄学现象。
4. 避坑与排查:这五个坑我替你踩过了
4.1 现象:灯全亮或全灭,代码逻辑看着没问题
原因:GPIO 初始化时GPIO_PIN用了GPIO_PIN_All,或者HAL_GPIO_Init的Pin字段没按位或正确。另一个可能是 LED 共阳共阴搞反了,代码写低电平点亮但硬件是共阴接法。
解决:用万用表逐个量引脚电平,对照代码里HAL_GPIO_WritePin的参数。共阳接法时,点亮写GPIO_PIN_RESET,灭写GPIO_PIN_SET,和共阴完全相反。不确定就写个测试循环,让每个引脚轮流翻转,看哪个灯对应哪个引脚。
4.2 现象:定时器中断进不去,或者进去一次就死机
原因:中断优先级配置冲突,或者回调函数里调用了阻塞函数。还有一种情况是HAL_TIM_Base_Start_IT在MX_TIM2_Init之前调用了,定时器还没初始化就启动,直接进 HardFault。
解决:确认MX_TIM2_Init()在main里先执行,再调HAL_TIM_Base_Start_IT。中断回调里只做标志位翻转和计数,不要放printf。如果用了 FreeRTOS,注意中断优先级不能高于configMAX_SYSCALL_INTERRUPT_PRIORITY,否则会触发断言。
4.3 现象:仿真器能连上但烧录失败,报 Flash 写保护
原因:STM32 的读保护或写保护被意外开启,常见于之前烧过带保护的程序,或者 Option Bytes 被改过。
解决:用 ST-Link Utility 或 CubeProgrammer 连接后,在 Option Bytes 里把 Read Out Protection 设为 Level 0,写保护全部解除,然后全片擦除再烧。注意解除保护会清空 Flash,代码要重新烧。这个操作不可逆,但一般开发板无所谓。
4.4 现象:按键调时间时状态机乱跳,偶尔两个方向同时绿
原因:按键中断和定时器中断同时访问currentState和currentDuration,没有做临界区保护。改配时的时候定时器还在跑,改到一半状态切换了。
解决:在按键中断里先HAL_TIM_Base_Stop_IT(&htim2),改完配时表再HAL_TIM_Base_Start_IT(&htim2)。或者用__disable_irq()和__enable_irq()包住修改代码。更稳妥的做法是按键只改一个标志位,主循环里检测到标志位再改配时,避免在中断里做复杂操作。
4.5 现象:报告里写的定时器参数和实际代码对不上
原因:CubeMX 里改了分频和重装载值,但报告里抄的是旧参数,或者忘了把「写进去的值要加一」算进去。
解决:报告里的参数表直接截图 CubeMX 的配置界面,或者从生成的tim.c里复制Prescaler和Period的值,然后在旁边注明实际分频 = 值 + 1。我一般会在报告里加一列「实际值」,避免答辩时被追问。
5. 进阶玩法:把交通灯做成能讲故事的演示系统
5.1 加一个夜间黄闪模式,PPT 里多一页状态图
基础功能跑通后,加夜间模式是性价比最高的进阶。实现方式很简单:在状态表里加一个NIGHT_FLASH状态,持续 500ms,下一个状态指向自己,LED 输出改成黄灯翻转。按键切换白天/夜间模式,PPT 里画一张状态迁移图,标注两个模式之间的切换条件,答辩时能讲出「根据时段自动切换」的设计思路。
// 夜间模式:黄灯 500ms 翻转一次 void nightFlash(void) { HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // 东西黄 HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_3); // 南北黄 }逻辑说明:这个函数放在 500ms 定时中断里调用,或者单独用一个定时器。注意夜间模式下其他灯要灭掉,否则红绿黄一起亮,看起来像故障。
5.2 用串口打印状态日志,报告里多一张时序图
把每次状态切换的时间戳和状态名通过串口打印出来,波特率 115200,格式化成 CSV。跑十分钟,把日志导入 Excel 画一张时序图,报告里放上去,比单纯贴代码直观得多。串口打印注意不要在中断里做,放主循环里轮询标志位。
// 主循环里检测状态切换标志,打印日志 if (stateChanged) { stateChanged = 0; printf("%lu,%d\r\n", HAL_GetTick(), currentState); }逻辑说明:HAL_GetTick()返回系统运行毫秒数,currentState是状态枚举值。打印出来的日志可以验证配时是否准确,比如东西绿实际持续了 8000ms 还是 8003ms,误差在可接受范围内就说明定时器配置没问题。
5.3 验证方法:用逻辑分析仪抓波形,比肉眼靠谱
最后说一个我每次做完嵌入式项目都会走的验证步骤:用逻辑分析仪抓 6 个 LED 引脚的波形,看状态切换的时序对不对。逻辑分析仪便宜的一百多块,采样率 24MHz 足够看毫秒级信号。抓到的波形和状态表对照,如果东西绿持续 8 秒、黄 2 秒、南北绿 8 秒、黄 2 秒,周期 20 秒,那就说明代码和硬件都对了。这个步骤写进报告的「测试与验证」章节,比只写「烧录后观察灯亮」专业得多。
从那以后我每次做嵌入式演示系统,都会强制走一遍「逻辑分析仪抓波形 + 串口日志对照」的流程,肉眼观察太容易漏掉偶发的时序错误。希望这份拆解帮到你,资源包里的源码、PPT 和报告可以直接拿去改,配时参数和状态表按你的路口场景调整就行。
本文还有配套的精品资源,点击获取