简介:一份基于STM32F103C6单片机的交通红绿灯系统设计文档,适合单片机课程设计、毕业设计或电子竞赛备赛者参考。文档完整覆盖了系统方案选型、硬件电路设计、软件流程与定时器中断配置等关键环节,并给出四位八段数码管倒计时显示和LED红黄绿指示灯的控制思路,能帮助读者理解交通信号控制逻辑,快速搭建同类模拟系统。资料为单个Word文档,大小约6.32MB,内容包含设计背景、系统总体电路图、主程序流程及关键代码段,结构清晰,便于按章节查阅。目前已有573人学习下载,适合需要撰写设计报告或进行实物制作的STM32初学者参考使用。
1. 基于STM32单片机的交通红绿灯系统:先看它到底能给你什么
基于STM32单片机的交通红绿灯系统是一份很典型的单片机课设/毕设资源,主控选了STM32F103C6,配套红黄绿LED指示灯、四位八段数码管倒计时、Proteus仿真工程和一套完整的Keil软件代码。拿到手你能得到的是一条“最小系统+显示+定时调度”的完整技术链路,还附了一段YOLOv5目标检测的思路说明,用来撑起“智能交通”方向的论文创新点。适合正在做课程设计、需要快速复现一台定时交通灯控制器的学生,也适合想把固定配时改成动态配时的入门嵌入式开发者。这篇就把硬件接法、定时器参数、数码管扫描和排错顺序逐个讲透,这几块通了,整个系统基本就到手了。
2. 硬件设计主线:STM32F103C6资源分配与三块电路的接法
2.1 主控选型:为什么直接绕过51单片机选STM32
资料里对比了PLC、51单片机和STM32三种方案。PLC第一轮就被淘汰,理由很现实:一台小PLC价格两三千起步,成本完全不适合做教学演示系统。51单片机被换掉不是因为点不亮LED,而是做“倒计时+多方向信号灯+后续扩展”的时候,定时器、IO口和中断资源都太紧张。想象一下:东西南北两方向,红黄绿各一组,需要6个IO;四位数码管动态扫描至少再占8个段数据口和4位位选口。用51也能硬挤出来,但刷数码管和跑倒计时中断抢CPU,代码会写得非常别扭。
STM32F103C6是ARM Cortex-M3内核,72MHz主频,片内Flash资料里有的地方写128K,有的写64K,实际以数据手册为准。这里要提醒一句:C6不是C8,别拿C8的64K Flash去套工程配置。不过本设计的程序量很小,几十行HAL配置加状态机代码,几K字节就够,Flash容量不会成为瓶颈。它的GPIO和定时器数量充足,后续加按键、加传感器、加串口通信都有余量,这是选它做核心的根本原因。
选型上另一个关键是开发库。资料里的软件工程用的是STM32CubeMX+HAL库,不是早期标准外设库。HAL库代码结构清晰,初始化函数分类明确,对课设级开发来说查函数和改参数都比寄存器版省时间。缺点是封装层次多,仿真器连接或时钟配置出错时排查要更耐心,这个放到避坑章节具体说。
2.2 红绿灯指示模块:IO引脚分配与低电平点亮逻辑
指示灯部分是整份资料里接线最简单的模块,但资料原文的引脚表存在一个明显错误,需要先纠正。它给出的对应关系是这样的:
| 方向 | 红灯 | 黄灯 | 绿灯 |
|---|---|---|---|
| 东西方向 | PA12 | PA14 | PA13 |
| 南北方向 | PA9 | PA11 | PA12 |
注意南北方向红灯写的也是PA12,和东西方向红灯重复了。同一个IO口不可能同时让东西红灯和南北红灯独立控制,这显然是笔误。按软件代码里的宏定义推断,南北方向红灯应该接PA9、黄灯接PA11、绿灯接PA10。修正后的分配为:东西方向红PA12、黄PA14、绿PA13;南北方向红PA9、黄PA11、绿PA10。接线之前务必把引脚表对照软件宏定义重新核对一遍,这是最容易翻车的地方。
指示灯驱动采用低电平点亮。LED阳极通过限流电阻接到3.3V,阴极接单片机IO口,IO输出低电平时灯亮,输出高电平时灯灭。为什么这么接?STM32 GPIO在输出低电平时吸收电流的能力强,灌电流能力大于拉电流,可以直接驱动小型LED,不需要额外加三极管。限流电阻一般在330Ω到1kΩ之间。按LED压降2V、工作电流5-10mA计算:R=(3.3V-2V)/10mA=130Ω,取220Ω到470Ω比较稳。如果在Proteus仿真里用虚拟LED,电阻阻值不对会出现亮度不足或颜色失真。
本设计把同一方向、同颜色的两个路口LED并联到同一个IO口,比如南北两个方向的红灯都接到PA9。这个做法省IO,逻辑上也成立:同一时刻同一方向两个路口的信号灯状态本就该一致。实际电路里两个LED共用一个限流电阻是可行的,但要注意总电流不能超过GPIO的绝对最大额定值,稳妥做法是每个LED串联独立限流电阻再并联到IO口。
2.3 倒计时显示模块:四位八段数码管的动态扫描
倒计时显示用的是四位八段数码管。硬件上可以做成四个一位数码管,或者两个两位数码管拼成四位显示。因为南北方向和东西方向的倒计时不是同时需要完整显示,所以同一时刻真正有意义的只有一组时间,四个数码管中两位显示数字,另一位可以显示方向标识,比如E或W。如果严格按“显示到下一次改变通行方向所剩的时间”这个需求,两个数码管就够;但资料为了布局对称和信息完整用了四个,答辩时展示“知道每个方向剩余多少秒”比只显示一个数字更有说服力。
动态扫描原理:四位数码管共用一组段数据线(a-g、dp),每一位单独占用一个位选线。任意时刻只点亮一个数码管,把要显示的字形数据送到段线上,然后切换到位选线;轮流点亮四个数码管,每轮间隙控制在2ms到5ms,人眼视觉暂留会认为四个数码管同时亮。刷新频率低于50Hz会出现明显闪烁。资料里的Dsg_Display(data_table[0])就是把字形码查表后送段驱动,SEG_1到SEG_4负责位选。
动态扫描有两个代码里看不出来的细节:段线最好经过74HC245或三极管缓冲,直接挂GPIO上驱动能力有限;数码管如果是共阳,段码表是取反后的值,要和硬件严格对应。实物调的时候先把一个数码管固定点亮,确认段码表方向正确,再扩展到四位的动态扫描。
2.4 最小系统设计:电源、复位、晶振与SWD下载
主控最小系统由四部分组成。电源方面,USB接口提供5V输入,但STM32F103C6工作电压是2V到3.6V,所以5V必须经过LDO稳压到3.3V,常见做法用AMS1117-3.3。建议在电源输入端并联100nF去耦电容,靠近每个电源引脚都放一个,否则仿真没感觉,实物上容易出现复位不稳定。
复位电路是一个按键接到NRST引脚。STM32的复位引脚是低电平有效,NRST内部默认上拉,外部按键接地,按下产生低电平脉冲,持续大于300ns就能触发复位。资料原文写“按键SW1接入高电平到NRST引脚”,这里表述不准确,接法应以芯片数据手册为准。按键抖动可以在NRST对地并联0.1uF电容消抖。
晶振电路采用8MHz外部晶振,经过内部PLL倍频到72MHz。两个负载电容一般取18pF到22pF,具体值跟晶振参数挂钩。Proteus仿真里如果不追求精确计时,可以用内部HSI省去晶振电路,但做串口通信或严格定时还是外置晶振更稳。下载电路选择SWD,只需引出SWDIO、SWCLK、GND和3.3V四个引脚,Keil的Debug选项里选SWD模式就能下载调试,相比JTAG省四个IO,对IO需求紧张的设计是明显优势。
3. 软件设计核心:定时器中断、状态机与倒计时调度
3.1 TIM3定时器初始化:arr与psc怎么配出1秒基准
软件部分的第一块基石是定时器。资料选择通用定时器TIM3,用中断方式产生节拍,而不是在主循环里软延时。原因是交通灯倒计时需要整秒精度,数码管动态扫描也要周期刷新,两者如果用软件延时,主循环会被堵死,按键检测等任务都得排队。
通用定时器3的初始化代码就是资料里那段HAL库写法:
// 通用定时器3中断初始化 // arr:自动重装值,psc:时钟预分频数 // 溢出时间计算公式:Tout = ((arr + 1) * (psc + 1)) / Ft,单位us // Ft = 定时器工作频率,单位MHz,这里TIM3挂在APB1上,时钟为72MHz void TIM3_Init(uint16_t arr, uint16_t psc) { TIM3_Handler.Instance = TIM3; // 选择定时器3 TIM3_Handler.Init.Prescaler = psc; // 预分频系数 TIM3_Handler.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数模式 TIM3_Handler.Init.Period = arr; // 自动重装值 TIM3_Handler.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; // 时钟分频因子 HAL_TIM_Base_Init(&TIM3_Handler); HAL_TIM_Base_Start_IT(&TIM3_Handler); // 使能并开启更新中断 } // 定时器底层驱动,开启时钟,设置中断优先级 // 此函数会被HAL_TIM_Base_Init()内部自动调用 void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM3) { __HAL_RCC_TIM3_CLK_ENABLE(); // 使能TIM3时钟 HAL_NVIC_SetPriority(TIM3_IRQn, 1, 0); // 抢占优先级1,子优先级0 HAL_NVIC_EnableIRQ(TIM3_IRQn); // 使能TIM3中断 } }这段代码有两个要点。第一,TIM3挂在APB1总线上,当APB1分频系数不为1时,定时器时钟会被倍频到72MHz,所以Ft一般取72MHz计算。第二,中断优先级设为抢占1、子优先级0,数值小优先级高。如果你后面加了串口中断,注意不要把所有中断都堆在同一个抢占优先级上,避免互相打断。
要配出1秒中断周期,我的习惯做法是psc取7199、arr取9999。此时分频后的定时器频率为72MHz/(7199+1)=10kHz,计数10000次就是1秒。用公式验证:Tout=(10000×7200)/72000000=1s。如果想把基准改成100ms,arr改999即可。注意HAL库还要实现中断回调函数HAL_TIM_PeriodElapsedCallback,在里面做倒计时减一和数码管扫描计数,否则定时器只是空转,不产生任何业务行为。
3.2 主程序状态机:南北9秒与东西12秒的切换逻辑
资料对应的默认配时是:南北方向(主干道)通行9秒,东西方向(支干道)通行12秒,两个方向切换前有3秒黄灯过渡。正文里还出现过“东西15秒”的描述,和代码里的12秒存在矛盾,实际应以后续逻辑图为准。我的分析结果是:南北方向绿灯9秒,之后黄灯3秒;东西方向绿灯12秒,之后黄灯3秒。改代码时强烈建议把两个方向的绿灯时间定义成宏,黄灯时间单独定义成YELLOW_TIME,不要像原代码那样在状态机里手工硬编码。
核心状态切换逻辑可以整理成下面这段,我在原代码基础上把方向变量名改了,方便对照和修改:
// 倒计时状态机核心逻辑(保留原设计含义,统一变量命名) #define GREEN_NS_SEC 9 // 南北方向绿灯时间,单位秒 #define GREEN_EW_SEC 12 // 东西方向绿灯时间,单位秒 #define YELLOW_TIME 3 // 黄灯过渡时间,单位秒 uint8_t mode; // 0=南北通行,1=东西通行 uint8_t temp_ns; // 南北方向剩余时间 uint8_t temp_ew; // 东西方向剩余时间 void Traffic_Light_Update(void) { if (mode == 0) // 南北通行模式:南北亮绿灯,东西亮红灯 { LED_NS_GREEN_ON(); LED_NS_YELLOW_OFF(); LED_EW_RED_ON(); if (temp_ns == 0) { if (temp_ew == 0) { // 黄灯过渡结束,整周期切换 mode = 1; temp_ew = GREEN_EW_SEC + YELLOW_TIME; temp_ns = temp_ew; LED_NS_RED_ON(); LED_EW_GREEN_ON(); LED_EW_YELLOW_OFF(); } else { // 南北进入黄灯过渡,东西保持红灯 temp_ew--; LED_NS_YELLOW_ON(); LED_NS_GREEN_OFF(); } } else { temp_ns--; temp_ew = temp_ns + YELLOW_TIME; // 同步更新东西方向显示 } } else // 东西通行模式,逻辑与南北对称 { // 东西方向绿灯,南北方向红灯,配时对称 } }这段整理后的代码保留了原始状态机的处理思路,但去掉了上下方向掺杂的混乱注释。核心设计点是temp_ns和temp_ew相差一个YELLOW_TIME,用来表达“黄灯期间也在倒计时”。你想验证不同配时,只需要改三个宏,不用动结构。有些资料版本会用switch-case把“南北绿-南北黄-东西绿-东西黄”拆成四个显式状态,我认为那样更好维护。这个工程用if嵌套也能跑,但“多出来3秒的东西方向”这类注释,就是嵌套太深之后的维护者容易绕晕的信号,我实际重构时默认转成四状态switch写法,答辩讲状态转换也清晰。
3.3 数码管刷新:在倒计时任务里安排动态扫描
数码管显示部分,资料给出了核心的switch-case代码,功能是每调用一次切换一个数码管位选:
switch(led_temp) { case 0: SEG_1(0); SEG_2(1); SEG_3(1); SEG_4(1); // 打开第一位 Dsg_Display(data_table[0]); // 送第一位段码 break; case 1: SEG_1(1); SEG_2(0); SEG_3(1); SEG_4(1); // 打开第二位 Dsg_Display(data_table[1]); break; case 2: SEG_1(1); SEG_2(1); SEG_3(0); SEG_4(1); // 打开第三位 Dsg_Display(data_table[2]); break; case 3: SEG_1(1); SEG_2(1); SEG_3(1); SEG_4(0); // 打开第四位 Dsg_Display(data_table[3]); break; }动态扫描的调度建议放在定时器中断回调里做,而不是放在主循环轮询。比如在TIM3的中断回调里,1秒节拍更新倒计时数据,同时把扫描计数器加一,加到4就回0。这样显示刷新和倒计时更新共用同一个时间基准,不会出现倒计时变量已经变了、数码管还停留在旧数据的情况。如果只在主循环里调用这个函数,刷新率会被状态机其它代码耗时影响,数码管闪烁就从这里来。
data_table[] 里存放的是数字转段码后的字形,比如共阳数码管显示数字0的段码是0xC0。如果发现显示错乱,先确认共阳共阴匹配,再检查段码表位序。资料里没有提供完整段码表,可以从任意八段数码管的手册里补,顺序一般是0-9和A-F,方向字符如E、W自己加到表末。
4. 避坑指南:仿真与实物联调中常见的五类故障
4.1 数码管亮度不均且闪烁
现象:Proteus仿真里四个数码管有的亮有的暗,肉眼能看出明显闪烁。实物上还伴有数字拖影。
原因:动态扫描的刷新率太低,或者每位点亮时间差异太大。原代码用主循环调用数码管scan,状态机里倒计时更新耗时不同,导致每位数码管的导通占空比不一致。
解决:把数码管scan移进固定频率的定时器中断。常见做法是让TIM3以2ms到4ms中断刷新一位,四位数码管完整扫描一轮只要8ms到16ms,刷新率高于60Hz,人眼就感知不到闪烁。检查方法是示波器抓位选脚波形,看每位周期是否均匀、高电平宽度是否一致。我在实物板上踩过坑,数码管扫描被串口中断打断,导致一位偶尔不亮,后来把TIM3的中断优先级提到比串口更高才解决。
4.2 定时器走时偏快或偏慢
现象:倒计时每秒明显快于真实时间,数10秒实际只过8秒;或者相反,10秒要12秒才走完。
原因:STM32定时器频率不是72MHz。如果CubeMX里没选外部晶振,实际用的是内部HSI,HSI误差能达到百分级别。外部8MHz晶振的负载电容不合适也会产生偏移。另一种常见原因是psc、arr配错,比如psc填7200、arr填9999,用公式算一遍就会发现周期不是整数秒。
解决:先确认时钟树配置。CubeMX生成工程时在RCC里明确选HSE外部晶振,PLL输出设为72MHz,启动后用HAL_RCC_GetSysClockFreq()打印核对。然后按psc=7199、arr=9999配置TIM3,溢出周期正好1秒。验证方法是在中断里翻转LED,用秒表掐20次翻转是否等于40秒。如果还偏,调整外部晶振负载电容。
4.3 倒计时最后阶段跳变异常
现象:显示从2秒直接跳到0,或者从绿灯切到另一方向时,黄灯只亮一瞬间,没有完整3秒黄灯过渡。
原因:状态机里temp_ns和temp_ew更新时序不对。原代码在判断temp_ns==0时,会把temp_ew赋值成temp_ns+YELLOW_TIME,如果temp_ns已经减到0,这个算式就少了一秒。另一处是绿灯时间和黄灯时间混在同一个计数器里,显示逻辑把黄灯阶段的值误当绿灯剩余时间。
解决:把绿灯时间、黄灯时间拆成独立逻辑。我重构时用wait_sec存当前方向剩余总时间,green_sec存本方向绿灯时间,状态切换时先置wait_sec=GREEN_NS_SEC+YELLOW_TIME,黄灯阶段单独置状态。每次中断回调里统一减wait_sec,再根据状态去翻灯。这样就算时间差一秒也不至于跳变。验证方法是在状态切换处加串口打印,看每个状态变化的时间戳是否等于预期YELLOW_TIME。
4.4 Proteus仿真运行但灯不亮或数码管无显示
现象:仿真能跑,程序也加载进去了,LED始终不亮或全亮,数码管无任何显示。
原因:Proteus里STM32模型默认频率不是72MHz,或者电路图里没画外部晶振而程序配置选了HSE,芯片启动阶段就卡住了。还有BOOT0引脚悬空,导致芯片进入BootLoader模式,GPIO完全不工作。
解决:在Proteus里给STM32加8MHz晶振和两只负载电容,NRST接好,BOOT0接地,所有VDD和VSS都要连上。程序如果不追求精确频率,可以直接改用HSI内部时钟,在SystemClock_Config里选择HSI并把SystemCoreClock设为16MHz,仿真可以省掉晶振电路。此外蛋疼的是Proteus里LED方向接反不亮,但它不做电流方向校验,所以还要逐根检查LED阴极是否接IO、阳极是否接VCC。
4.5 状态切换瞬间LED出现短暂全亮或全灭
现象:绿灯往黄灯切换时,两只灯同时亮了几十毫秒,或者全部熄灭一下,视觉上很突兀。
原因:多路LED的开关不是原子操作。代码里先执行LED_NS_GREEN_OFF(),再执行LED_NS_YELLOW_ON(),中间隔了好几条指令,如果这时触发定时器中断,就会看到中间态。全灭也一样,先把当前方向所有灯关掉再开另一个方向,中间必然有一个“无灯状态”。
解决:调整切换顺序,先把当前方向切到安全态,再开另一方向。安全态定义为“红灯亮、其余灭”,因为任何时刻都要有红灯表示禁行。代码层面,在状态切换入口暂时关闭定时器中断,等GPIO全部写完再打开,可以完全避免这种瞬间毛刺。硬件上如果用了锁存器或达林顿驱动板,灯的开关可以在同一时刻批量生效,效果更好。
5. YOLOv5扩展模块:这份资源里AI部分的真实价值与取舍
5.1 红绿灯系统里为什么会出现YOLOv5
很多下载这份资料的人第一反应是:一个STM32F103C6的交通灯系统,为什么要写一大段YOLOv5目标检测?这不是错题,而是资源作者为了贴合“智能交通”方向,在软件设计说明里加的一段扩展阐述。它的原始意图是描述一种更高级的场景:在学校或园区门口,摄像头采集人流和车流,YOLOv5检测目标数量和位置,STM32再根据检测结果调整红绿灯时长。
这里要澄清一个技术事实:STM32F103C6的内核是Cortex-M3,72MHz主频、几十KB级内存,不可能跑得起YOLOv5这种卷积神经网络。YOLOv5推理至少需要GPU或NPU,常见部署环境是PC端Python、Jetson系列或带算力的边缘盒子。资料里写的“通过STM32单片机对检测结果进行实时处理”,准确表述应该是两步走:上位机跑YOLOv5检测,把结果通过串口发给STM32,STM32只做策略决策和信号灯控制,不推断图像。
所以这段模块的实际价值不是“嵌入式端跑AI”,而是“AI检测结果如何与信号灯状态机对接”。这个点在答辩里很加分,因为大多数同学的课设只做固定配时,你能讲出“根据车流动态调整绿灯时长”的架构,已经超出基础要求。
5.2 摄像头、上位机与STM32的数据链路
如果想真正把AI思路落地一部分,推荐的分工方案是这样:
USB摄像头 --> 上位机(YOLOv5目标检测, 输出目标计数) | | 串口TX/RX v STM32F103C6(信号灯状态机, 根据计数调整绿灯时长)上位机用Python跑YOLOv5,检测每帧里的车辆和行人数量,然后通过串口发送简单指令给STM32。比如发送字符H表示拥堵,L表示通畅。STM32的串口接收中断里解析这个字符,对应修改GREEN_NS_SEC或GREEN_EW_SEC的值。如果检测到行人较多就延长南北方向绿灯,如果检测到东西方向车流密集就延长东西方向绿灯。
这样做数据链路简单,实时性足够课设演示。STM32端只需增加USART初始化和串口接收中断,再用一个全局变量保存最近收到的策略值,倒计时状态机每次切换前读取即可。上位机检测帧率不需要太高,每秒2-5帧足够,避免占用太多CPU。演示时可以在上位机程序里加一个可视化窗口,实时画出检测框并显示发送的策略字符,画面感很强。
5.3 如果只想做基础版,AI部分怎么取舍
先说结论:不做任何YOLOv5,这套系统依旧是完整可用的交通灯控制器,答辩不存在硬伤。AI部分在原始资料里属于论文扩写性质,没有给出训练数据集、权重文件、推理脚本和STM32通信代码。如果硬跑YOLOv5,大概率被环境配置劝退:要装PyTorch、下载预训练权重、处理模型尺寸和依赖库,光这一步就可能消耗两三天。
如果时间充裕且想把深度学习章节做厚,建议只做一个最小验证:用官方YOLOv5s模型对一段包含车辆行人的视频做检测,统计输出结果,写一段串口通信模拟发送,配合STM32串口接收做红绿灯时长切换。这样创新点和技术栈都成立,又不用真正搭建一套完整摄像头抓拍系统。把总线通信协议和状态机联动讲清楚,已经达到这份资源里“智能控制”的立意。真正的生产级部署要考虑的算力、时延、数据隐私问题,在课设阶段不需要碰。
6. 最后一步:从仿真到实物的调试验证顺序与技巧
6.1 推荐的分步验证顺序
拿到整套代码别急着全部烧进去。按下面这个顺序排查,能省掉大把时间:
第一步,烧一个最简单的GPIO翻转程序,确认主控跑起来了,下载器连接稳定。第二步,单独初始化TIM3,在中断回调里翻转LED,用秒表记录20次翻转时间是否等于40秒,确认定时基准正确。第三步,只点亮南北绿灯,检查IO电平和LED接法。第四步,让倒计时数据跑起来,先不接数码管,用调试器直接观察倒计时变量。第五步,接数码管做动态扫描,确认每一位都显示正确。第六步,整合状态机和数码管显示,跑完整周期。最后再考虑加按键、串口等功能。
6.2 一个很实用的验证技巧:把时间参数临时缩短
写完整个状态机后,每次验证都要等9秒加12秒,效率很低。调试阶段可以把GREEN_NS_SEC和GREEN_EW_SEC临时改成2秒和3秒,YELLOW_TIME改成1秒,整个周期压缩到6秒左右。这样每走一个周期不到10秒,你能在几十秒内观察多轮状态切换,确认LED转换顺序、黄灯过渡时间和数码管倒计时是否完全对应。验证完成后再改回9秒和12秒重新烧录,就能快速回归真参数。
这个技巧从我做第一版交通灯用到现在,每次拿到类似状态机代码都用它做一轮快速验证。从那以后我拿到任何单片机状态机类设计,都强制先跑一遍“压缩参数+串口打印状态值”的流程,确认逻辑没问题再改回真实参数,少走了很多弯路。希望帮到你。
本文还有配套的精品资源,点击获取