STM32交通灯项目源码详解:基于Keil的状态机实现

STM32交通灯项目源码详解:基于Keil的状态机实现 简介这是一份基于Keil MDK与STM32的模拟交通灯课程设计资料包面向单片机初学者、电子相关专业学生及嵌入式爱好者帮助理解定时器中断、GPIO控制、状态机等典型应用。资源包含完整工程源码、设计文档与答辩PPT源码涉及stm32f10x系列驱动、RCC时钟配置、TIM定时器等模块文档与PPT则覆盖硬件连接、软件流程和演示要点。压缩包共199个文件主要类型包括C源文件、头文件、编译生成的o/axf/hex以及doc、pptx说明材料整体大小约5.82MB目录结构清晰便于对照学习。已有4197人学习下载适合用于课程设计、实验报告或项目入门参考。 每到期末或者课程设计季总能在各个技术群里看到有人求“STM32交通灯项目”的压缩包。你手上这个“keil基于stm32的模拟交通灯项目源码文档ppt.rar”从命名就能看出来是干嘛的Keil MDK开发环境、STM32主控、模拟交通灯控制附带源码、设计文档和答辩PPT。这种项目在嵌入式学习里属于非常经典的“练手课设”二合一选题它覆盖了GPIO输出控制、定时器、状态机、按键输入、数码管显示这些核心知识点既能交差课程设计也能作为入门STM32的第一块跳板。我见过太多人下载完压缩包之后直接懵住不知道从哪里开始看也不知道源码里那些函数到底在干什么。这篇文章我就把这个项目的硬件方案、代码逻辑、状态机设计、常见报错全部拆开讲一遍让你真正把这套资源用起来。1. 先想明白这个项目到底在练什么1.1 为什么交通灯是STM32入门的经典选题交通灯项目之所以在单片机教学里“长盛不衰”是因为它控制的本质就是“按时序翻转输出引脚电平”——这看起来很简单但往深了做它会逼着你去解决几个非常现实的问题多个信号灯之间如何协调切换每个状态的持续时间怎么精确控制系统在跑延时的时候怎么还能响应按键这些问题的背后恰好对应了STM32的几大核心外设GPIO用来点亮LED定时器用来产生时基外部中断或者轮询用来检测按键如果要做倒计时显示还要驱动数码管或者LCD。换句话说你把这个项目吃透再去接触串口、PWM、ADC、I2C这些外设时会顺手很多。因为代码架构的思想是通用的。很多网上的源码包包括江科大原江协科技那套STM32入门视频里配套的例程也是从“点灯”一路讲到状态机控制的底层思路完全一致。所以这个项目不只是“做一个交通灯”而是在帮你不自觉地建立起“用状态机管理外设”的嵌入式编程习惯。另外这个项目还有一个很现实的用途答辩现场展示效果直观。红绿灯交替闪烁、倒计时跳变老师一眼就能看到功能是否正常配合文档和PPT能讲出内容比做一个纯逻辑性的小工具在演示上有优势得多。这也是为什么历届课设选题里交通灯永远有一席之地。1.2 拿到压缩包后先看清硬件方案我建议你解压之后先别急着编译烧录而是先看文档里或者原理图里的引脚分配。交通灯项目最常见的硬件方案是用STM32F103C8T6最小系统板配合面包板、LED灯珠、1kΩ限流电阻、按键、自锁开关或者独立按键做输入。基础的“十字路口双向交通灯”需要6个LED东西方向红黄绿各一个南北方向红黄绿各一个。如果要做带左转的进阶版那就变成10个LED再加倒计时数码管则还需要两个两位一体共阴数码管、驱动三极管或者直接用74HC595扩展。常见引脚分配可以是这样的不同源码可能不一样以你拿到的工程里定义的宏为准功能引脚说明东西方向红灯PA0推挽输出东西方向黄灯PA1推挽输出东西方向绿灯PA2推挽输出南北方向红灯PA3推挽输出南北方向黄灯PA4推挽输出南北方向绿灯PA5推挽输出行人按钮PB0输入上拉按下为低电平模式切换按键PB1输入上拉数码管段码PC0-PC7共阴数码管段选数码管位选PD0-PD1控制两个数码管位这里有个新手特别容易踩的坑LED引脚和数码管段码混在同一个GPIO口时如果代码里同时操作两组引脚会出现显示干扰。大多数初版源码里不会同时开数码管和LED分时复用但如果你的压缩包版本做了8位数码管倒计时就要留意代码里是不是用了定时器中断做动态扫描主循环里只在状态切换时才翻转LED电平避免显示闪烁。拿到代码后第一件事应该是把引脚宏定义从头到尾读一遍如果你自己重连了线改起来也方便。2. Keil工程搭建环境、工程结构与初始化代码2.1 建工程时最容易出问题的几个点不少人在打开源码后第一反应是“怎么编译一堆报错”先别急着怀疑源码大概率是你的Keil环境没配置对。Keil MDK现在主流的版本是5.x官网能下到5.36或者5.37安装完之后要记得通过Pack Installer安装对应芯片的器件支持包。如果你打开工程后魔术棒Options for Target里Device一栏是空的或者编译提示“Cannot open source file xxx.h”基本可以确定是器件包没装好。STM32F103系列用的是Keil.STM32F1xx_DFP这个包直接在Pack Installer里搜索安装即可。还有一类问题是老工程和新版本Keil之间的兼容性。很多网上下载的源码是几年前用Keil 4建的工程用Keil 5打开时会提示“Device not found”或者加载不了启动文件这时候最稳妥的办法是新建一个空工程把源码里的.c和.h文件全部添加进去而不是强行去改老工程文件。另外STM32标准外设库版本也有讲究F1系列比较常见的是V3.5标准外设库里面包含了inc和src两个文件夹工程里要正确包含头文件路径否则一堆“undefined identifier”就会冒出来。2.2 GPIO初始化灯能不能亮全看这一步交通灯程序再复杂本质也是从GPIO初始化开始的。标准外设库写法的初始化代码通常长这样GPIO_InitTypeDef GPIO_InitStructure {0}; /* 使能GPIOA时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* 配置PA0-PA5为推挽输出 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure);这里有两个关键点。第一个是GPIO_ModeLED控制用的是推挽输出GPIO_Mode_Out_PP可以输出高电平和低电平点亮LED的方式通常有两种一种是引脚输出高电平点亮此时LED正极接引脚、负极通过限流电阻接GND另一种是引脚输出低电平点亮LED正极接3.3V、负极接引脚。两种接法在代码里对应的就是GPIO_SetBits和GPIO_ResetBits的调用关系看代码之前一定要先确认硬件接法否则会出现“灯一上电就亮怎么都控制不了”的怪现象。第二个关键点是GPIO_Speed。对于交通灯这种不追求高速翻转的场景2MHz都够用但你直接给50MHz也完全没问题。真正要注意的是有些源码会把PB3、PB4、PA13、PA14、PA15这类引脚拿来做LED这几个引脚默认复用为JTAG调试口初始化时如果不先用GPIO_Remap_SWJ_Disable关闭JTAG功能就会出现“下载完程序板子跑不起来但用调试器又能看到程序在运行”的诡异问题。所以看到源码里如果有类似GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE)的代码不是多余的那是在“释放”被调试口占用的引脚。3. 交通灯的核心逻辑状态机、定时和按键3.1 状态表与状态机的设计思路大部分交通灯源码的核心都不是“点灯”而是“什么时候点哪一盏灯”。正规的做法是设计一张状态表把整个工作周期拆成若干个互斥的时序状态主循环一遍遍扫描当前状态时间到了就跳转。拿最典型的双向六灯模型举例可以拆成四个基础状态状态编号东西方向南北方向持续时间S1绿灯亮红灯亮30秒S2黄灯闪烁红灯亮5秒S3红灯亮绿灯亮30秒S4红灯亮黄灯闪烁5秒S1到S4反复循环就是最简单的固定配时交通灯。用状态机的写法主循环里只需要一个switch-case就能管理全部逻辑switch (current_state) { case STATE_EW_GREEN: EW_GREEN_ON(); /* 东西绿灯南北红灯 */ NS_RED_ON(); if (get_tick() - state_start_tick 30000) { current_state STATE_EW_YELLOW; state_start_tick get_tick(); } break; case STATE_EW_YELLOW: EW_YELLOW_ON(); if (get_tick() - state_start_tick 5000) { current_state STATE_NS_GREEN; state_start_tick get_tick(); } break; /* 其他状态类似 */ }用状态机而不是单纯地“延时到→切换灯”写法的好处在于它把“灯的显示状态”和“时间的推进方式”解耦了。系统在任意一个时刻只需要知道自己处于哪个状态切换条件也相对清晰不像是用几个Delay函数硬串起来一旦要插入一个行人按键、紧急车辆优先之类的逻辑整个流程全乱套。这一点是这份源码里最值得学习的地方——它没有为“功能少”牺牲架构的清晰性。另外注意状态切换的时间判断不要直接写if (current_tick 30000)这种因为时间是从上电那一刻开始累计的状态已经循环了好几轮这个条件就永远不再成立。规范的写法是记录下进入当前状态时的基准时刻state_start_tick用当前时刻减去基准时刻得到“本状态已经持续了多久”。这也是很多自己改源码的人容易翻车的地方。3.2 定时方案阻塞延时不够用如果你只是点亮几盏灯做演示用HAL_Delay或者标准库的Delay函数可以跑通。但一旦加入按键检测、数码管动态扫描阻塞延时会让系统“卡死”——延时期间CPU无法响应任何输入。所以稍微完整一点的源码都会采用“定时器产生固定时基主循环轮询状态”的时间管理方式。最常见的是用SysTick或者TIM2产生一个1ms的中断中断服务函数里累加一个全局变量volatile uint32_t system_tick 0; void SysTick_Handler(void) { system_tick; } uint32_t get_tick(void) { return system_tick; }主循环里所有的时间判断都基于get_tick()函数的返回值。这样做的好处是系统始终保留了对按键事件的响应能力如果再接一个蜂鸣器做倒计时提示音也只需要在对应的时间点翻转一下引脚就行不用专门写一个“鸣响函数”去死等。对新手来说理解这个“基于节拍驱动状态切换”的模式比背下一堆“延时3秒”的API重要得多。3.3 按键与行人按钮有按键功能的交通灯通常会在基础状态机之上增加一个“行人请求”通道。按键用PB0做外部中断输入或者主循环轮询都行但一定要处理抖动。硬件上可以加一个104电容并联在按键两端软件上利用前面的时基做20ms左右的消抖if (KEY_PRESSED 0) { /* 低电平有效 */ delay_ms(20); if (KEY_PRESSED 0) { pedestrian_request 1; /* 记录请求而不是立刻切换 */ } }注意在中断里或者轮询里检测到按键后最优的做法是先给一个标志位置1让主循环在某个安全的时间点去响应。比如如果当前刚好是机动车道绿灯刚起步的状态立即切到行人绿灯可能会让司机措手不及所以可靠的实现是当前绿灯剩余时间大于某个阈值时按完直接切如果剩余时间很短就把请求挂起等下一个周期。这块逻辑虽然小但体现了“多任务”系统的雏形也是答辩时老师最喜欢追问的地方——“如果你的按键在红灯快结束的时候按了系统怎么处理”能答出“延迟到合理时机再切换”分数就上去了。4. 调试与烧录那些让人头大的报错4.1 连接不上芯片的排查路线不管代码写得多漂亮烧录不进去就一切都白搭。网上交通灯压缩包的评论区最常见的求助就是这个报错error: no stm32 target found! if your product embeds debug authentication, please check if debug authentication is disabled and retry.这句话看着长翻译过来就一句调试器连不上芯片。排查路线我建议固定下来从硬件到软件依次检查。第一查接线。ST-Link V2或者DAP-Link和板子之间至少要有四根线SWDIO、SWCLK、GND、3.3V。很多人只用杜邦线接了两根信号线忘记共地就会出现间歇性连不上。第二查电源。如果板子是从ST-Link的3.3V引脚取电要给调试器足够的供电能力劣质杜邦线压降大的时候芯片供电不足也会直接导致连接失败。第三查Keil设置。打开魔术棒Debug一栏必须把调试器选成“ST-Link Debugger”然后点旁边的Settings正常情况下能在SW Device框里看到芯片的IDCODE如果有内容说明物理连接没问题剩下的就是程序或者配置问题。如果这一步看到的是“No device found”或者“Target not found”那就要用ST-Link Utility这类工具去做底层擦除了。很多时候是因为之前烧进去的程序把SWD引脚复用成普通GPIO了——交通灯项目的代码如果偷懒把PA13或者PB3引出来点灯就有可能出现“烧了一次程序之后第二次下载失败”的灵异现象。解决办法是按住板子上的复位键在Keil里点下载等进度条出现的那一瞬松开复位键或者反过来先点下载再瞬间按复位趁启动代码还没跑起来把新固件烧进去然后立即在程序里禁用SWJ或者不再占用调试引脚。另外Keil烧录时如果提示“No Algorithm found”或者“Erase Failed”通常是Flash下载算法没配好在Utilities一栏的Settings里勾选正确的编程算法STM32F10x系列是512KB Flash选ST-Link自带的算法即可。这块网上资料很多但很少有人告诉你换了一个下载器之后这两个选项经常会被偷偷重置成默认值这说明拿到新工程后第一步检查调试器配置永远是值得的。4.2 项目还能怎么扩展基础版模拟交通灯跑通以后扩展方向其实非常多。最常见的进阶是把固定配时改成多时段配时比如早晚高峰配时不同这个只需要在状态管理器里加一个时间判断再进阶一点把倒计时数码管加上用TM1650这类I2C接口的驱动芯片可以节省IO口还有加超声波传感器模拟车流量检测的车多的时候自动延长绿灯时间这就是一个带“感应控制”的智能交通灯雏形了。答辩PPT里如果加上这个改进点内容厚度会直接提升一个档次。工具链方面如果嫌Keil又老又慢可以试试在VSCode里配置嵌入式开发环境配合EIDE插件或者CMake去编译工程甚至可以导入Keil工程调试体验会舒服很多。但我不建议新手一开始就用这玩意儿因为Keil MDK的调试器集成和Flash算法配置开箱即用出问题概率低等你能把这套交通灯玩明白了再去折腾VSCode也不迟。此外Keil里也可以设置编译后自动生成bin文件的User命令比如用fromelf.exe工具把axf转换成bin这对接产线烧录或者用其他工具做OTA升级时很有用属于“可以不会但不能不知道”的操作。实话说这个交通灯项目我前前后后给不同朋友做过好几个版本。带左转灯的、带倒计时的、带光敏传感器夜间切换黄灯模式的都有。做得越多越觉得真正值钱的不是那几盏灯怎么闪而是代码里那个“状态机”的骨架——它往后能迁移到按键控制、智能家居、传感器采集几乎任何带时序逻辑的项目里。所以我特别建议你把压缩包里的源码重写一遍不是抄而是对着最终效果图自己从驱动层开始搭。最开始可能憋两个小时都写不出一个完整的switch-case但当你亲手把第一个完整周期跑通看到那两排LED按顺序亮起来的时候你对STM32的整个学习节奏都会不一样。本文还有配套的精品资源点击获取