简介:这份资源是面向嵌入式初学者与高校课程设计学生的STM32模拟交通灯项目完整资料包,基于Keil μVision开发环境,用C语言实现红绿黄灯定时切换,涉及GPIO控制、定时器中断与状态机设计等知识点,适合作为单片机编程入门或电子工程课程设计的实践参考。压缩包共199个文件,约5.82MB,包含31个c源文件与33个h头文件构成的核心代码,以及crf、o、d等编译中间文件,另有doc设计文档、pptx演示文稿和txt说明,便于理解设计思路与硬件连接。目前已有4202人学习下载,热度较高。读者可从中获得可直接编译的Keil工程、完整的源码注释、设计报告与答辩PPT,既能对照文档梳理交通灯控制逻辑,也能动手烧录验证,加深对STM32外设配置与嵌入式开发流程的理解。
1. 从一份 keil 工程说起:stm32 模拟交通灯到底能跑出什么
很多人第一次接触单片机课程设计,都是从交通灯开始的。它不像温湿度采集那样依赖外部传感器,也不像电机控制那样需要额外驱动模块,一块最小系统板加几个 LED 就能把整个状态机跑通。但真正拿到一份 keil 基于 stm32 的模拟交通灯项目源码加文档加 ppt 的压缩包时,问题往往不是"能不能跑",而是"我该怎么把它变成自己的东西"。这份资源的核心价值在于:它提供了一套完整的、可编译的 keil 工程,配套的文档说明了设计思路和模块划分,ppt 则可以直接用于答辩或汇报场景。适合谁?正在做单片机课程设计的学生、需要快速搭出交通灯演示原型的嵌入式新手、以及想找一个标准状态机练手模板的开发者。你不需要从零画原理图,也不需要自己推演倒计时逻辑,但你需要知道怎么把工程导入、怎么改引脚、怎么调时序,否则它永远只是别人硬盘里的一个压缩包。
2. 拆开压缩包先看什么:工程结构、时钟树与 GPIO 配置
拿到一个 keil 工程,最忌讳的就是双击打开直接点编译。血泪经验告诉我,先看目录结构,再看时钟配置,最后才动代码。这份资源的工程结构大概率是标准库或 HAL 库的组织方式,下面按常见情况拆解。
2.1 工程目录与关键文件识别
解压后你会看到类似这样的结构:
TrafficLight_STM32/ ├── CORE/ ├── HARDWARE/ │ ├── LED/ │ ├── TIMER/ │ └── KEY/ ├── STM32F10x_FWLib/ ├── USER/ │ ├── main.c │ ├── stm32f10x_it.c │ └── system_stm32f10x.c ├── OBJ/ └── TrafficLight.uvprojxUSER/main.c是主逻辑入口,交通灯的状态切换、倒计时更新都在这里。HARDWARE下按外设分文件夹,LED 驱动、定时器配置、按键扫描各自独立。STM32F10x_FWLib是标准外设库,说明这个工程大概率基于 STM32F103 系列。TrafficLight.uvprojx是 keil 的工程文件,双击它就能打开整个工程。
提示:如果解压后没有
STM32F10x_FWLib文件夹,而是看到Drivers/STM32F1xx_HAL_Driver,那说明用的是 HAL 库版本,后续配置方式会不同,但逻辑层代码基本一致。
2.2 时钟树配置:为什么你的延时总是不准
交通灯的核心是精确的倒计时,而倒计时的基础是系统时钟。标准库工程里,时钟配置通常在system_stm32f10x.c的SystemInit函数中完成。默认情况下,外部晶振 8MHz 经过 PLL 9 倍频后得到 72MHz 系统时钟。如果你用的板子晶振不是 8MHz,或者用的是内部时钟,这里不改,后面所有延时都会偏。
// system_stm32f10x.c 中的关键配置 #define SYSCLK_FREQ_72MHz 72000000 static void SetSysClockTo72(void) { // 使能外部高速晶振 RCC->CR |= ((uint32_t)RCC_CR_HSEON); // 等待 HSE 就绪 while(!(RCC->CR & RCC_CR_HSERDY)); // 配置 PLL:HSE 作为输入,9 倍频 RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMULL)); RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9); // 使能 PLL 并等待锁定 RCC->CR |= RCC_CR_PLLON; while(!(RCC->CR & RCC_CR_PLLRDY)); // 切换系统时钟到 PLL RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW)); RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL; while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08); }这段代码的逻辑是:先启动外部晶振,稳定后配置 PLL 倍频系数,最后把系统时钟源切换到 PLL 输出。参数说明:RCC_CFGR_PLLMULL9表示 9 倍频,如果你改成RCC_CFGR_PLLMULL6,系统时钟就变成 48MHz,所有基于 SysTick 的延时都会相应变化。常见坑是板子上没有焊接外部晶振,或者晶振频率不是 8MHz,这时候程序会卡在while(!(RCC->CR & RCC_CR_HSERDY))死循环里,现象就是 LED 完全不亮。
2.3 GPIO 初始化:交通灯引脚怎么映射
交通灯的 LED 通常接在 GPIO 的推挽输出模式上。以常见的 PA0 到 PA5 接六个 LED(东西红黄绿、南北红黄绿)为例,初始化代码如下:
// led.c 中的 GPIO 初始化 void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; // 使能 GPIOA 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 配置 PA0-PA5 为推挽输出,50MHz 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_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5); }逻辑说明:先开时钟,再配引脚模式和速度,最后设置初始电平。参数说明:GPIO_Mode_Out_PP是推挽输出,驱动 LED 足够;GPIO_Speed_50MHz影响翻转速率,交通灯场景下用 2MHz 也完全够,但改成 50MHz 不会有副作用。注意GPIO_SetBits和GPIO_ResetBits取决于你的 LED 是高电平点亮还是低电平点亮,如果接的是共阳极 LED,低电平才亮,初始状态应该用GPIO_ResetBits全部点亮再熄灭,或者直接保持高电平。
3. 状态机与定时器:交通灯逻辑怎么写才不翻车
交通灯的本质是一个有限状态机,四个状态循环切换:东西绿南北红、东西黄南北红、东西红南北绿、东西红南北黄。每个状态持续固定时间,倒计时用定时器中断更新。这部分是整份源码的核心,也是文档和 ppt 里最值得参考的部分。
3.1 状态机设计:用 switch-case 还是状态表
常见做法是用switch-case配合一个全局状态变量。代码大概长这样:
// main.c 中的状态机片段 typedef enum { EW_GREEN_NS_RED = 0, EW_YELLOW_NS_RED, EW_RED_NS_GREEN, EW_RED_NS_YELLOW } TrafficState; volatile TrafficState currentState = EW_GREEN_NS_RED; volatile uint8_t countDown = 30; // 当前状态剩余秒数 void TrafficLight_Update(void) { switch(currentState) { case EW_GREEN_NS_RED: // 东西绿,南北红 GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 东西绿亮 GPIO_SetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_2); // 东西黄红灭 GPIO_ResetBits(GPIOA, GPIO_Pin_3); // 南北红亮 GPIO_SetBits(GPIOA, GPIO_Pin_4 | GPIO_Pin_5); // 南北黄绿灭 if(countDown == 0) { currentState = EW_YELLOW_NS_RED; countDown = 3; // 黄灯 3 秒 } break; case EW_YELLOW_NS_RED: // 东西黄,南北红 GPIO_ResetBits(GPIOA, GPIO_Pin_1); GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_2); GPIO_ResetBits(GPIOA, GPIO_Pin_3); GPIO_SetBits(GPIOA, GPIO_Pin_4 | GPIO_Pin_5); if(countDown == 0) { currentState = EW_RED_NS_GREEN; countDown = 30; } break; // 后两个状态类似,省略 } }逻辑说明:每个状态里先设置对应的 LED 电平,然后判断倒计时是否归零,归零就切换到下一个状态并重置倒计时。参数说明:countDown是 volatile 类型,因为它会在中断里被修改。绿灯 30 秒、黄灯 3 秒是常见配置,你可以根据实际路口需求调整,但黄灯不建议低于 2 秒,否则视觉上会感觉闪烁。
3.2 定时器中断:1 秒基准怎么产生
倒计时的精度取决于定时器中断。用 SysTick 或者通用定时器 TIM2 都可以。以 TIM2 为例,配置成 1 秒中断一次:
// timer.c 中的定时器初始化 void TIM2_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 定时器时钟 72MHz,预分频 7199,周期 9999 // 中断频率 = 72MHz / (7199+1) / (9999+1) = 1Hz TIM_TimeBaseStructure.TIM_Period = 9999; TIM_TimeBaseStructure.TIM_Prescaler = 7199; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE); } // 中断服务函数 void TIM2_IRQHandler(void) { if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); if(countDown > 0) { countDown--; } } }逻辑说明:TIM2 挂在 APB1 总线上,时钟为 72MHz。预分频 7199 后得到 10kHz 计数频率,再经过 9999 个周期溢出,正好 1 秒。参数说明:TIM_Prescaler和TIM_Period的计算公式是中断频率 = 时钟频率 / (Prescaler+1) / (Period+1)。如果你改了系统时钟,这两个值必须同步改,否则倒计时速度会不对。常见坑是忘记在中断里清除标志位,导致程序反复进中断,主循环完全跑不动。
3.3 数码管倒计时显示:段选与位选怎么配合
如果项目里带了数码管显示倒计时,那还会涉及动态扫描。常见做法是用两个数码管分别显示东西和南北方向的剩余秒数,通过位选快速切换,视觉上同时亮起。这部分代码通常在HARDWARE/SEG或类似目录下,核心是段码表和扫描函数。段码表把 0-9 的数字转换成 GPIO 输出电平,扫描函数每 5ms 切换一次位选。参数说明:扫描频率不能太低,低于 50Hz 会看到明显闪烁;也不能太高,否则数码管亮度不够。常见坑是段码表搞反了共阴共阳,现象是显示的数字缺笔画或者全亮。
4. 避坑与排查:编译通过但灯不亮,先查这五处
这份资源虽然结构完整,但实际跑起来翻车的地方不少。下面五条是我在类似工程里反复遇到的。
4.1 现象:keil 编译报错 "cannot open source input file"
原因:工程里引用的库文件路径和你解压后的实际路径不一致。常见于从别人电脑打包过来的工程,绝对路径被写死在.uvprojx里。
解决:在 keil 中右键工程 → Options for Target → C/C++ → Include Paths,把里面所有路径删掉,重新添加你本地的STM32F10x_FWLib/inc、CORE、USER、HARDWARE下的各子目录。改完后重新编译。
4.2 现象:程序下载后 LED 全亮或全灭,没有任何变化
原因:GPIO 初始电平设置反了,或者 LED 的共阴共阳接法和代码假设不一致。
解决:先确认硬件是低电平点亮还是高电平点亮。用万用表测一下 LED 正极接的是 VCC 还是 GPIO。如果是低电平点亮,初始化时应该用GPIO_SetBits让所有 LED 先熄灭,状态机里用GPIO_ResetBits点亮对应 LED。反过来就全部对调。
4.3 现象:倒计时数字跳得飞快或慢得离谱
原因:系统时钟配置和定时器参数不匹配。比如板子实际是 8MHz 晶振,但代码按 72MHz 算分频,中断频率就变成 8/72 倍,倒计时慢得让人以为死机。
解决:用示波器或者逻辑分析仪测一个 GPIO 翻转频率,反推实际系统时钟。如果没有仪器,就在主循环里翻转一个 LED,用手机秒表粗略计时。确认时钟后,按公式重新算TIM_Prescaler和TIM_Period。
4.4 现象:数码管显示重影或闪烁
原因:动态扫描的位选切换太快或太慢,或者段选和位选没有做消隐。
解决:在切换位选之前先把段选全部关闭,再打开下一位的段选。扫描周期控制在 5ms 左右,八位数码管总刷新率约 25Hz,人眼基本看不到闪烁。如果还是闪,检查是不是在中断里做了太多事情,导致扫描函数被阻塞。
4.5 现象:按键调整倒计时时间没反应
原因:按键扫描没有做消抖,或者按键引脚和 LED 引脚冲突。
解决:按键扫描加 10ms 延时消抖,或者用定时器中断做状态机扫描。检查原理图,确认按键用的 GPIO 没有被 LED 占用。如果资源里没有按键功能,那这条可以忽略,但文档里通常会提到扩展方向。
5. 从能跑到好用:把这份工程改成你自己的答辩项目
把工程跑通只是第一步,真正让这份资源发挥价值的是把它变成你自己的东西。下面几个技巧是我在类似项目里反复用的。
5.1 用宏定义把时间参数抽出来
原始代码里 30 秒、3 秒这些数字散落在各处,改起来容易漏。在main.c顶部加一组宏:
#define GREEN_TIME 30 #define YELLOW_TIME 3 #define RED_TIME 30 #define ALL_RED_TIME 2 // 全红过渡时间然后把状态机里的countDown = 30全部替换成countDown = GREEN_TIME。这样答辩时老师问"能不能改成 45 秒",你只需要改一个数字,重新编译下载,十秒钟搞定。这个习惯我从第一次做课设就养成了,后来做任何带时间参数的嵌入式项目都先抽宏。
5.2 加一个全红过渡状态
真实路口在红绿切换之间会有 1-2 秒全红,防止两个方向同时放行。在状态机里插入一个ALL_RED状态,持续 2 秒,所有方向红灯亮。这个改动在 ppt 里可以作为"设计优化"单独讲一页,比单纯复述代码有说服力。
5.3 用串口打印状态切换日志
如果板子上有串口,初始化 USART1 后在每次状态切换时打印一行日志:
printf("State: %d, CountDown: %d\r\n", currentState, countDown);这样调试时不用盯着 LED 猜,串口助手里一目了然。参数说明:波特率 115200,8 位数据位,1 位停止位,无校验。注意printf需要重定向fputc函数,标准库工程里通常已经写好了,如果没有就自己加一个。
5.4 文档和 ppt 怎么用才不浪费
配套文档一般包含设计目标、硬件框图、软件流程图、模块说明。不要直接照抄,把流程图里的状态机部分用自己的话重新画一遍,把模块说明里的引脚表对照你的实际板子改掉。ppt 通常有背景、方案、实现、演示、总结几个部分,你可以保留框架,但把截图换成你自己跑起来的实物照片,把代码片段换成你改过的版本。这样答辩时老师一眼就能看出是你自己做的,而不是从压缩包里原样搬出来的。
从那以后我每次拿到别人的工程,都强制走一遍"改宏定义、加日志、换截图"的流程,确保自己真的理解了每一行代码在干什么。希望这份拆解能帮你把这份 stm32 交通灯资源真正用起来,而不是让它继续躺在硬盘里。
本文还有配套的精品资源,点击获取