
简介基于STM32的交通灯控制系统是一份典型的嵌入式综合实践项目面向学习STM32开发的电子类学生与嵌入式初学者帮助理解微控制器在实时交通控制场景中的完整应用。压缩包共191个文件约9.28MB以C源文件、头文件、汇编启动文件、工程配置文件uvproj为主另含PPT课件与doc报告文档覆盖从固件工程到方案说明的完整资料链。目前已有7011人浏览学习内容成熟度较高。代码部分包含初始化、定时器中断、传感器处理、控制逻辑与LED显示驱动等模块PPT系统讲解架构设计、硬件搭建与软件流程报告则总结项目目标、实施步骤与问题解决适合用于课程设计、毕业设计或嵌入式入门实战训练。 我印象挺深的一次是帮一个学弟调课程设计。他交上来的STM32交通灯控制系统能跑但一接上数码管整个时序就乱套加个按键想模拟急救车优先通行按下去一点反应都没有。后来翻代码我才发现他全程用的是“纯延时”——主循环里只有延时、翻转电平、再延时按键和显示完全插不进去。这个项目看着简单但真正做扎实的人不多。交通灯控制系统之所以常年被选作课设、毕设题目是因为它麻雀虽小五脏俱全GPIO控制、定时器中断、状态机设计、按键消抖、动态扫描显示全是嵌入式入门必须掌握的基本功。这篇文章我按实际做项目的思路来写不贴一大段让你直接复制的代码而是把硬件怎么搭、程序为什么这样组织、调试会翻哪些车讲清楚适合刚学完STM32基础、正在为课设或者练手发愁的读者。1. 方案推演心里要有数交通灯系统到底考什么1.1 功能需求先拆成模块先别急着接线写代码把需求拆清楚。一个标准的交通灯控制系统通常包含这么几块正常时序控制南北方向绿灯30秒、黄灯3秒然后东西方向绿灯30秒、黄灯3秒四个方向红灯与对方绿灯互锁倒计时显示用数码管显示当前状态剩余秒数按键干预按下可以切换紧急模式或者进入夜间模式声音提示黄灯闪烁或紧急模式时蜂鸣器报警。拆完之后你会发现这个项目其实就四件事IO输出驱动LED、定时器提供时基、数码管显示、按键扫描。这种拆解能力本身就是课设考察的重点。1.2 为什么用STM32而不是51很多人会问交通灯这种控制逻辑51单片机不是也能做确实能做但有两个现实问题。第一课程设计评分通常看扩展性。51做出来的纯功能版本很难往上加传感器、屏幕、通信模块而STM32的资源摆在那里你答辩时可以理直气壮地说“后续可以接车流量检测、无线远程控制”。第二从学习价值看STM32的HAL库和标准库能让你接触到中断优先级、定时器配置、引脚复用这些真正有用的概念。F103C8T6这种芯片蓝丸开发板二十来块钱和51差价不大但学完能处理的项目复杂度完全不是一个量级。1.3 资源评估和IO分配F103C8T6有48个引脚去除电源、复位、晶振、SWD调试占用的部分可用IO还有三十多个。交通灯系统用到的IO如下我给的是一个比较规整的分配方案功能引脚说明南北红/黄/绿PA0 / PA1 / PA2推挽输出东西红/黄/绿PA3 / PA4 / PA5推挽输出数码管段选PC0~PC7共阴数码管段选数码管位选PA6 / PA7 / PA8 / PA9控制第1~4位按键PB1内部上拉输入蜂鸣器PB2推挽输出这里有个底层逻辑要讲清楚LED和蜂鸣器都是数字开关量交给GPIO推挽输出就行数码管段选要频繁切换电平所以选一组连续的引脚写段码表方便。按键必须配上拉输入后面调试部分我会说为什么。2. 硬件连接三个问题不处理好程序写得再好也白搭2.1 避开调试引脚不然LED老大不听使唤不少初学者会把LED随便分配到某个引脚上结果发现某个灯死活不亮或者电平怎么都拉不高。大概率是踩到了JTAG调试引脚的坑。STM32的PB3、PB4、PA15这几个引脚默认是JTAG功能。即使你初始化成普通推挽输出它们也可能不按你预期的逻辑工作因为内核还把它们当调试口用。解决办法有两个一是布线时直接避开这些引脚二是代码里先禁用JTAG、保留SWD__HAL_AFIO_REMAP_SWJ_NOJTAG();这行代码放在GPIO初始化之前把JTAG关掉、只留SWD调试那这三个引脚就能当普通IO用了。实际项目里我更推荐第一种拿洞洞板飞线时避开这几个引脚从根上省事。2.2 LED限流电阻不是随便焊一个就行LED直接接3.3V必烧或者发烫发暗需要串限流电阻。限流电阻的计算方法是R (VCC - VF) / IVCC是3.3VVF是LED正向压降红色约2.0V、绿色约2.2V、蓝色约3.0VI是目标工作电流取5mA到10mA比较合适。以红色LED、10mA为例R (3.3 - 2.0) / 0.01 130Ω实际手边不一定刚好有130Ω选220Ω或者330Ω都行属于“计算作为参考、实际按手头料调整”的典型情况。但330Ω以上就要注意了有些LED电流不到1mA亮度会明显偏暗演示的时候拍出来不好看。2.3 最小系统的坑晶振电容和BOOT0如果你的板子不是现成开发板而是自己做的最小系统板两个细节最容易出问题。一是晶振电容。外接8MHz无源晶振的时候两个负载电容不是随意选的要根据晶振的负载电容CL算。CL一般标注在晶振规格书里常见值是20pF匹配电容的计算经验公式是C 2 × CL - C寄生C寄生大概5pF左右所以CL为20pF时C大约在33pF附近工程上常用15~33pF。电容配不对轻则时钟偏差重则芯片起振失败。二是BOOT0引脚必须拉低到GND。BOOT0悬空或拉高上电后可能进到系统Bootloader现象就是“程序下载成功但跑不起来”。我见过好几个新手在这上面折腾一下午最后拿根杜邦线把BOOT0接到GND就完事了。2.4 ST-Link连不上的排查顺序做最小系统或者自己焊的板子经常遇到STM32 ST-LINK Utility报“no stm32 target found”这个问题。第一次见会慌其实排查顺序是固定的排查项操作SWD线序SWDIO、SWCLK、GND三条必须确认一一对应不要相信杜邦线颜色供电用万用表量芯片VDD有没有3.3V电流不小于100mABOOT0必须接GND不能悬空复位电容复位脚对地电容太大会导致上电无法稳定去掉或换成0.1uF焊接检查LQFP48引脚有没有连锡、虚焊尤其是VDD和VDDA这套顺序我每次调新板子都按着走一遍90%的问题出在供电和线序上。3. 程序架构状态机是交通灯的灵魂3.1 为什么别用“全流程循环延时”学弟当初的做法很典型就是开头说的那种while (1) { // 南北绿灯 LED_NS_GREEN_ON(); delay_ms(30000); // 南北黄灯 LED_NS_YELLOW_ON(); delay_ms(3000); // 东西绿灯 LED_EW_GREEN_ON(); delay_ms(30000); ... }这种写法在纯功能演示下能跑但你只要想加任何东西立刻崩盘。延时期间CPU被占死按键扫描进不去数码管刷新也进不去GPIO状态全靠延时函数硬扛时间长一点还会积累误差。打个比方这就像让一个人按剧本走位但突然路上有人问路他没法即兴反应。交通灯系统本来就是一个天然的状态机你不用状态机去建模等于逆着问题本质写代码。3.2 四状态状态机设计交通灯的正常运行其实只有四个状态在循环状态南北方向东西方向持续时间S1绿灯红灯30秒S2黄灯红灯3秒S3红灯绿灯30秒S4红灯黄灯3秒代码里用枚举把状态定义出来typedef enum { STATE_NS_GREEN, // 南北绿灯 STATE_NS_YELLOW, // 南北黄灯 STATE_EW_GREEN, // 东西绿灯 STATE_EW_YELLOW // 东西黄灯 } TrafficState; TrafficState current_state STATE_NS_GREEN; uint8_t countdown 30;状态切换的条件只有一个倒计时归零。你在状态机里只需要在计数到0的时候执行“当前状态切换到下一个状态 给新状态赋初值”。后面要加紧急模式、夜间模式本质上就是在状态集合里加状态主干逻辑不用动。3.3 定时器中断做时基主循环做切换时基是整个系统的“心跳”我建议用TIM2来做1ms中断主循环里仅靠一个tick计数去推进逻辑。为什么选TIM2因为TIM1是高级定时器配置里牵扯刹车、互补输出这些和本项目无关的概念初学容易把自己绕晕TIM2是通用定时器配置简单适合当普通时基用。中断回调里面只做一件事volatile uint32_t g_ticks 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { g_ticks; } }主循环的逻辑就清爽了static uint32_t last_second 0; while (1) { uint32_t now g_ticks; // 每1000ms处理一次秒级逻辑 if (now - last_second 1000) { last_second now; if (countdown 0) { countdown--; } else { state_transition(); // 切状态并重新赋初值 } } key_process(); // 非阻塞按键扫描 display_refresh(); // 数码管动态刷新 }注意这里用了now - last_second 1000而不是now % 1000 0原因是防止主循环某次被较长的处理卡住后漏掉整秒。这一点在处理多个任务并行时很重要。3.4 数码管倒计时怎么接和怎么刷我的方案是四位数码管显示内容类似“30”“03”这种倒计时。驱动方式用动态扫描所有数码管的段选线并联到PC0~PC7位选线由PA6~PA9控制。动态扫描的原理就是利用视觉暂留快速轮流点亮每一位。主循环里的display_refresh()每次只点亮一位停留1~2ms然后切到下一位。四位轮完一个周期约4~6ms刷新率在200Hz左右人眼看不到闪烁。关键技巧是每位切换前必须先关掉所有位选再置段码最后打开要显示的那一位否则会出现“拖影鬼影”。这个顺序叫“先消隐、再送段码、后选位”是动态扫描的老规矩。4. 调试时最容易翻车的四个细节4.1 按键消抖不能靠死等虽然HAL_GPIO_ReadPin可以直接读按键电平但机械按键按下和松开的过程中有大约10~20ms的抖动不加处理的话一次按压可能被识别成好几次。最笨的办法是检测到按下后delay 20ms再读一次但这和我们的主循环架构冲突延时期间数码管和状态机全卡住了。正确做法是做一个非阻塞消抖if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) GPIO_PIN_RESET) { // 用计数的方式消抖 if (key_debounce 0) { key_debounce 20; // 20ms之后确认 } else { key_debounce--; } } else { key_debounce 0; }实际项目中我会用一个扫描周期为10ms的按键任务累计两到三次采样都有效才判定按下。这样按键扫描不会阻塞其他任务显示也不会闪。4.2 HAL_Delay在中断里用程序会卡死“stm32 延时函数delay卡死”是搜这个项目时经常出现的词很多人都是在定时器中断回调里想调一下HAL_Delay做短延时然后程序就卡死。原因其实不神秘HAL_Delay依赖SysTick中断维护一个tick变量。如果在其他中断回调里调用HAL_Delay而这个中断的优先级高于SysTick那么SysTick永远抢不到CPUtick变量不增长HAL_Delay就一直死在while循环里出不来。所以记住一条铁律中断服务函数里不做耗时操作更不调用延时函数。需要定时的话就用计数变量自己去数。4.3 按键输入必须配上拉按键一端接GPIO、另一端接GND这种接法GPIO必须配置为内部上拉输入。否则按键断开时引脚是浮空状态电平随机跳变读到的结果毫无意义。GPIO_InitStruct.Pin KEY_Pin; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(KEY_GPIO_Port, GPIO_InitStruct);注意如果按键接的是3.3V、GPIO另一端接GND那就要配置为下拉输入。这个方向搞反轻则按键无效重则短路风险。4.4 数码管刷新频率和显示残留动态扫描最常见的问题是“闪”和“拖影”。闪是刷新率不够每位数码管停留时间过长周期超过20ms就会明显闪烁拖影是段码切换瞬间没消隐上一位的亮度残留到了下一位。我的经验值单管停留1ms四管一轮4ms这个参数在绝大多数情况下都不闪。如果还有隐隐的亮度不均可以检查一下位选三极管是不是饱和导通或者段选电阻是不是太大导致驱动电流不够。5. 扩展功能与答辩展示5.1 急救车优先通行加一个状态就能解决的事用状态机会发现扩展功能非常简单。比如模拟急救车请求通过只需要加一个“全红状态”按键触发后不管当前是哪个状态直接把所有方向置为红灯倒计时5秒期间蜂鸣器以1Hz频率鸣响结束后恢复到正常的南北绿灯状态。代码上只是在状态枚举里加STATE_EMERGENCY在state_transition()里加一个分支不影响原来的四状态主流程。这种“增量开发”的体验是流水账代码永远给不了你的。5.2 夜间模式黄灯闪烁夜间模式可以用按键切换也可以用光敏电阻来做——光敏电阻分压输入到ADC光线暗到阈值就自动进入。我这里只说最简单的按键切换。进入夜间模式后状态机只循环两个“状态”黄灯全亮1秒、全灭1秒。东西南北四个黄灯同步闪烁其他灯全部熄灭。实现起来只需要把倒计时从30秒改成1秒状态转移函数里固定切换到闪烁状态。5.3 答辩演示怎么排一个成熟的演示逻辑顺序很重要我建议按这个顺序来上电展示正常交通灯循环数码管倒计时稳定跳动各LED亮度均匀按下紧急按键展示全红蜂鸣器持续时间结束后自动恢复强调“状态机可以随时安全切入切出”切到夜间模式展示黄灯闪烁最后补一句倒计时的时间参数可以在代码开头用宏定义统一修改。这样讲下来评委会看到你从底层GPIO、定时器、状态机到扩展功能都有完整理解和只贴一个“能跑”的Demo是完全不同的印象。项目收尾时我把整套代码重新读了一遍最大的感触是让嵌入式项目稳定运行的关键不是写了多少行代码而是动手之前有没有把功能和结构拆清楚。状态机、时基、非阻塞扫描这些概念都是从这个看似简单的交通灯项目里学到的。如果你手头正好有板子建议不要停留在“能跑”的层面把倒计时、按键、蜂鸣器一起加上调试过程中踩的每一个坑都是收获最大的地方。本文还有配套的精品资源点击获取