简介:基于Keil MDK与STM32的模拟交通灯项目完整资料包,面向单片机初学者、嵌入式课程设计学生及电子爱好者,用于理解定时器中断、GPIO控制、状态机设计等核心概念,并能直接迁移到其他外设控制场景。压缩包共199个文件,包含可直接烧录的HEX文件、31个C源码、33个头文件以及o、axf等编译输出,uvprojx工程配置、doc设计文档、pptx演示文稿和txt说明,整体大小仅5.82MB,便于快速下载和离线学习。项目基于STM32标准外设库实现,源码注释较完整,阅读C源码可以掌握定时器初始化、中断处理与状态切换流程;doc文档和PPT则补充了设计思路、硬件连接及实验步骤,适合撰写课程设计报告或答辩展示。已有4197人学习使用,内容经过实践项目检验,可直接打开Keil工程查看、修改并烧录到开发板验证。 很多人把这类“源码+文档+PPT.rar”下载下来,解压、打开工程、点一下编译,然后就关掉了。真正能把里面的东西变成自己能力的,反而没几个。这个基于STM32的模拟交通灯项目,本质上不是让你复刻一个红绿灯,而是通过一个看得见、跑得动、逻辑不算复杂的外设组合,把GPIO控制、时序调度、状态机设计、调试排错这条嵌入式开发的完整链路走一遍。这篇文章我就从硬件电路、软件逻辑、KEIL调试和扩展思路几个角度,把这个项目从头到尾拆开讲透,适合正在做课设、准备毕设,或者刚学完STM32想找一个完整项目练手的读者。
1. 先把这个压缩包打开:项目拆解与知识版图
解压这个.rar之后,你大概率会看到三样东西:源码、文档、PPT。这三样东西对应着学习这个项目的三个层次。
源码部分是一整套KEIL工程,打开之后能看到完整的STM32代码,通常会包含主程序、LED驱动、延时或定时器配置,以及单片机的启动文件和芯片头文件。这里面有两条路线:老一点的项目用的是标准外设库,函数名是GPIO_Init、GPIO_SetBits这种;新一些的用HAL库,函数名是HAL_GPIO_WritePin这种。两条路线原理一样,区别在于HAL库有CubeMX可视化配置,标准库更轻量直接。判断你手上这份源码是哪条路线,就看GPIO的操作代码长什么样。
文档部分一般是实验指导书或者课程设计报告,开头写任务要求,中间是原理图、流程图、代码讲解,最后是运行结果。PPT则是答辩演示用的,常见结构是项目背景、总体设计、硬件电路、软件流程、系统运行效果和总结。这个“源码+文档+PPT”的架构本身就是典型的课设交付物模板,你之后做其他项目也可以直接套这个框架。
那这个交通灯项目为什么值得当练手项目?因为它覆盖了几个很核心的知识点:GPIO输出控制外部器件、系统时钟和延时控制时序、状态机式的逻辑调度,以及如果做了按键扩展,还涉及外部中断。它不涉及通信协议、不涉及复杂传感器数据采集,专注解决最基础的“让单片机按既定时间序列干活”这件事,难度曲线正好适合从点亮LED过渡到完整项目。
从标题里的关键词也能看出来,这个项目的主线就是keil开发环境、stm32主控芯片、模拟交通灯的运行逻辑。把这三点吃透,你就已经迈过嵌入式项目的第一道门槛了。顺带提一句,这类项目网上的源码质量参差不齐,有的注释齐全、结构清晰,有的纯粹是复制粘贴拼出来的,看代码时多留意文件组织方式和注释风格,也能帮你判断一份源码值不值得细看。
2. 硬件连接与驱动设计:从LED限流电阻谈起
交通灯项目在硬件上是非常精简的,核心就是STM32开发板加六个LED(两组红黄绿),再加若干根杜邦线。绝大多数人用的是STM32F103C8T6最小系统板,原因无外乎便宜、资料多、Flash和RAM对这类项目绰绰有余。
先看LED的接法。每个LED的正极接一个GPIO引脚,负极通过限流电阻接地。红黄绿三色LED因为材料不同,正向压降也有差异,红色一般1.8V到2.2V,绿色和黄色通常2.0V到2.4V,所以限流电阻的计算要用到欧姆定律:先定目标电流,比如5mA到10mA,LED压降按2V估算,GPIO输出3.3V,那么电阻就是(3.3 - 2)/ 0.01 = 130欧。实际使用中取220欧或330欧都没问题,亮度略降,但对教学项目完全没有影响。千万别不接电阻直接接LED,轻则过流烧引脚,重则烧芯片IO口,这个锅我见过很多人背过。
六个LED的引脚规划建议按方向分两组,比如PA0接南北绿、PA1接南北黄、PA2接南北红、PA3接东西绿、PA4接东西黄、PA5接东西红。为什么要这么分组?因为代码里操作状态的时候,往往是整个方向一起切换,引脚安排得规律,代码里的宏定义和掩码操作也好写。如果闲得慌,也可以拿一个74HC595串转并芯片做扩展,但不建议在课设里这么干,复杂度上去了,讲不清楚反而扣分。
如果不用杜邦线搭面包板,也可以做一块简单的洞洞板,把LED和电阻焊上去。硬件调试的时候有一点要特别提醒:先检查LED极性和供电电压再上电,STM32核心板用USB线供电或者ST-Link的3.3V输出供电都可以,但要确认共地。很多“灯不亮”问题都是因为核心板和面包板的地没接到一起,或者杜邦线松了,这种基础问题排查起来反而最花时间。
硬件上还有一个容易被忽略的点:LED可以直接用GPIO推挽输出驱动,但单片机的总电流输出能力是有限制的,单个GPIO不要长时间超过20mA,整体电流也要控制。在这个项目里6个LED同时点亮,按5mA算也只有30mA,属于安全范围。如果你想把亮度拉满用10mA甚至更大,就要考虑外部驱动电路了,不过模拟交通灯完全没有必要。
| 器件 | 型号/规格 | 数量 | 说明 |
|---|---|---|---|
| STM32核心板 | STM32F103C8T6最小系统板 | 1 | 主控 |
| 红色LED | 5mm直插 | 2 | 南北/东西红灯 |
| 黄色LED | 5mm直插 | 2 | 南北/东西黄灯 |
| 绿色LED | 5mm直插 | 2 | 南北/东西绿灯 |
| 限流电阻 | 220欧或330欧 | 6 | LED串接限流 |
| ST-Link V2 | 调试下载器 | 1 | 程序下载与调试 |
| 面包板 | 常规830孔 | 1 | 搭建实验电路 |
这张表既是开发清单,也是你之后写课设报告硬件选型章节可以直接参考的内容。实际做的时候,可以把LED按南北和东西分成两排固定在洞洞板或纸板上,模拟路口的样子,演示效果会好很多。
3. 核心逻辑:用状态机把红绿灯调度清楚
交通灯的逻辑单独看任何一个灯,都很简单:红灯结束后变绿灯,绿灯结束后变黄灯,黄灯结束后变红灯,循环往复。难点在于两个方向是联动的:南北方向绿灯的时候,东西方向必须红灯;南北方向黄灯的时候,东西方向还是红灯;南北方向红灯之后,东西方向才开始绿灯。所以正确的思考方式不是“同时控制六个LED”,而是把整个路口看成一个整体,定义出若干相位,每个相位对应一组确定性的LED状态。
以国内常见的交通规则为例,一个完整周期的相位表可以设计成这样:
| 相位 | 南北方向 | 东西方向 | 持续时间 |
|---|---|---|---|
| 相位1 | 绿灯 | 红灯 | 30秒 |
| 相位2 | 黄灯 | 红灯 | 3秒 |
| 相位3 | 红灯 | 绿灯 | 30秒 |
| 相位4 | 红灯 | 黄灯 | 3秒 |
这个表就是状态机的完整定义。代码里对应的做法是定义一个枚举类型,把四个相位作为四个状态,然后用一个时间基准去驱动状态切换。这里我强烈建议用SysTick定时器做一个非阻塞的时间基准,而不是在while循环里用HAL_Delay死等。原因很简单:死等延时在交通灯这种纯顺序任务里勉强能用,但只要你后面想加按键响应、串口打印、倒计时显示,死等就会把系统卡死在那里。
用HAL库实现的时间基准大致是这个思路:在SysTick中断里每毫秒累加一个计数值,然后在主循环里读取这个值判断当前相位的时间是否已经走完。代码框架可以写成这样:
typedef enum { PHASE_SN_GREEN = 0, PHASE_SN_YELLOW, PHASE_EW_GREEN, PHASE_EW_YELLOW } Phase_t; static Phase_t currentPhase = PHASE_SN_GREEN; static uint32_t phaseStartTime = 0; void TrafficLight_Update(void) { uint32_t elapsed = HAL_GetTick() - phaseStartTime; switch (currentPhase) { case PHASE_SN_GREEN: if (elapsed >= 30000) { currentPhase = PHASE_SN_YELLOW; phaseStartTime = HAL_GetTick(); SN_YELLOW_ON(); } break; case PHASE_SN_YELLOW: if (elapsed >= 3000) { currentPhase = PHASE_EW_GREEN; phaseStartTime = HAL_GetTick(); EW_GREEN_ON(); } break; case PHASE_EW_GREEN: if (elapsed >= 30000) { currentPhase = PHASE_EW_YELLOW; phaseStartTime = HAL_GetTick(); EW_YELLOW_ON(); } break; case PHASE_EW_YELLOW: if (elapsed >= 3000) { currentPhase = PHASE_SN_GREEN; phaseStartTime = HAL_GetTick(); SN_GREEN_ON(); } break; default: break; } }这里的SN_YELLOW_ON、EW_GREEN_ON这些函数,在每个相位切换时被调用,做两件事:关闭当前相位的所有LED,打开下一个相位的LED。宏定义或函数里用HAL_GPIO_WritePin把对应的GPIO引脚设成高电平或低电平。注意写清楚,千万别把“关闭当前相位”漏了,不然会出现上一个相位的灯还亮着、下一个相位的灯又亮起来的“幽灵状态”。
GPIO初始化用HAL库写起来是这样:
void LED_GPIO_Init(void) { __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5, GPIO_PIN_RESET); }所有引脚初始化为推挽输出,初始状态全部低电平,也就是所有灯熄灭。这里的GPIO_PIN_0到GPIO_PIN_5对应硬件部分的六个LED,读者可以根据自己实际接线调整引脚号,只要同步修改宏定义即可。
这是用HAL库的实现,如果拿到手的是标准库源码,核心逻辑完全一致,只是函数名换成GPIO_Init、GPIO_SetBits。无论哪套库,状态机的设计思路都是通用的,这也是我反复强调状态机的原因:它把复杂的时序控制变成了一张表、一个状态变量,一次只处理一个状态,排查问题的时候特别直观。其实你回头看市面上那些“一键启停流水灯”“呼吸灯”项目,本质也都是这套思路。
如果想加倒计时显示,那就在相位切换的时候把持续时间加载到倒计时变量里,每秒递减一次,同时刷新数码管或OLED,这是后话,后面扩展部分再细说。
4. KEIL里的编译下载与问题排查
源码到手之后,第一关往往不是看懂代码,而是让代码跑起来。KEIL MDK这个环境本身坑不少,我按从安装到烧录的顺序把常见问题过一遍。
先说芯片包。新建工程或者在旧电脑上打开别人的工程时,KEIL经常会提示找不到芯片。这时你要去Pack Installer里把STM32F1系列的Device Family Pack安装好,或者直接双击一个.pack文件完成安装。很多人卡在“打开工程后芯片型号是空白”或者“编译器报错找不到目标芯片”,多半就是因为缺了对应的支持包。安装完Pack之后,在Device栏里能选到STM32F103C8,这一步过了,后续编译才有基础。
编译阶段常见的报错包括:缺头文件路径、标准库和HAL库混用、找不到core_cm3.h等。缺头文件路径的解决办法是在魔术棒工具条的C/C++选项卡里,把User Include Paths配置好,把工程需要用到的头文件目录都加进去。如果是拿到手的老工程用了标准外设库,而你安装的KEIL版本较新,编译器版本也可能导致错误,可以在编译器选择那里把老的AC5编译器切换回来,很多老工程在AC6下会有兼容性报错,这一点在MDK 5.36之后的版本里尤其明显。
烧录和调试阶段最经典的问题,就是很多人经常遇到的那句"No STM32 Target Found! If your product embeds debug authentication, pl..."。这句话翻译过来就是:ST-Link没找到目标芯片。常见原因按排查顺序排列:检查ST-Link和开发板的连接线是否接对(SWDIO、SWCLK、GND三根线必须牢固)、检查开发板是否单独上电、检查设备管理器里ST-Link驱动是否正常、检查ST-Link接线是否过长松动、检查Flash Download算法是否选对了芯片型号。还有一种情况容易被忽视,就是目标板的复位电容太大导致下载时序失败,这种时候要么换用ST-Link的SWD接口快速连接,要么降低下载时钟频率,在Utilities设置里的SW Speed从4MHz降到1MHz或100kHz试试。
还有一类问题不在KEIL而在工程本身:板子型号不对。比如源码是针对STM32F103ZET6正点原子板写的,而你手上是F103C8T6,那GPIO引脚、启动文件、Flash大小和RAM大小都不一样,直接烧录轻则灯不亮,重则程序直接跑飞。收到任何开源工程的第一件事,是先看它的硬件平台和引脚配置,不要想当然直接烧。
如果你准备在vscode里开发,也可以借助一些插件或者CMake工具把KEIL工程导入进去,但对于课设项目,我建议还是在KEIL里操作,因为老师的电脑、实验室的电脑通常都装着KEIL,演示和答辩更方便。
最后说一个调试阶段的实用技巧:不要只会全速运行看灯闪不闪。在KEIL里给状态切换那几行代码打断点,让程序停在相位切换的瞬间,然后单步看currentPhase这个变量是否按预期变化,远远比肉眼看LED靠谱。如果手头有串口,也可以把相位切换信息通过串口打印出来,这样就算LED接错了,你也能知道程序本身到底跑没跑对。
5. 从课程设计到“作品”:扩展方向与交付物打磨
基本功能跑通之后,这个项目还远没到结束的时候。如果你想让它从“能交差的课设”变成“答辩时能讲出东西来的作品”,可以从功能和文档两个维度往上加料。
功能扩展的优先级我建议这样排。第一优先级是按键控制,用外部中断或者简单的IO轮询加一个按键,模拟“急救车优先通行模式”:按下按键后,当前所有方向立即进入红灯,持续5秒后恢复原有相位。这一个小功能就能引出外部中断、消抖、中断服务程序和状态恢复这几个新知识点,性价比极高。第二优先级是光敏传感器夜间模式,用光敏电阻或者一个简单的光敏模块检测环境亮度,夜间自动把所有方向切换成黄灯闪烁,模拟“夜间交通安全模式”。第三优先级是倒计时显示,用四位或八位数码管显示当前相位的剩余时间,这就涉及动态扫描和定时器,内容量一下就上来了。
文档和PPT的打磨也很重要。很多同学的课设报告写到一半就不知道该写什么,其实核心框架就几条线:需求分析(要解决什么问题、功能指标是什么)、总体设计(系统框图、工作流程)、硬件设计(器件选型、电路原理、引脚分配)、软件设计(状态机、时序图、代码说明)、系统测试(测试用例、运行现象、遇到的问题和解决过程)。你把这个项目从头到尾走一遍,这五章的内容都是现成的,PPT也按这个章节抽出来讲,答辩的时候自然有东西说。报告中放上波形图或者逻辑分析仪抓的时序图,比纯文字有说服力得多。
我自己带过不少学生做类似项目,发现一个共性现象:能拿到源码的人很多,能把状态机解释给老师听的人很少。这个项目真正的价值不在于那几行控制LED的代码,而在于你通过它理解了“一个复杂系统的行为如何被拆成有限个状态”的思路。只要你把相位表、状态切换条件、时间基准这三件事讲清楚,答辩这关也就顺理成章了。
最后再分享一个我惯用的做法:把这个项目跑通之后,故意把某个相位的延时改错,观察故障现象,再对照状态机代码反推原因。这个过程比照着正确代码看十遍都有用,因为排查问题的思路,才是你做下一个项目时真正能迁移的能力。等你能做到“看到灯闪的节奏不对,就知道是哪个状态的时间配错了”,这个交通灯项目才算真正吃透了。
本文还有配套的精品资源,点击获取