ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于STM32的交通灯控制系统设计与Proteus仿真实现

基于STM32的交通灯控制系统设计与Proteus仿真实现 简介基于 STM32/ARM Cortex-M 内核的交通灯控制系统完整项目包含 Proteus 仿真、Keil 工程源码与配套参考报告面向电子类专业学生、单片机爱好者及嵌入式课程设计场景。资源包共 90 个文件压缩后仅 1.74MB以 C 源码和 H 头文件为主同时包含 Keil 工程文件.uvprojx/.uvoptx、Proteus 仿真工程.pdsprj/.pdsbak、HEX 固件、PDF 参考报告和说明文档结构清晰便于按模块阅读。已有 11513 人学习浏览是学习 STM32 外设驱动和状态机设计的实用资料。读者可获得完整可运行的交通灯控制代码覆盖 GPIO、定时器、OLED 显示等模块配合 Proteus 工程可直观验证运行逻辑参考报告则给出设计思路、实现步骤和常见问题排查方法对课程设计或项目复现很有帮助。1. 项目需求拆解与整体方案设计1.1 这个项目到底要做什么拿到基于STM32的交通灯控制系统仿真keil代码参考报告这个标题很多刚入坑单片机的朋友第一反应是这不就是流水灯换个花样吗。说实话我当年也是这么想的但真正上手做完才发现一个完整的交通灯控制系统其实把一个入门工程师需要掌握的核心技能都串起来了定时器中断、GPIO控制、按键输入、数码管动态扫描、状态机设计以及Proteus仿真联调和Keil工程管理。这个项目要解决的痛点是很多新手一上来就想做复杂项目但连最基本的时间片轮询思想都没建立起来导致代码东拼西凑今天能跑明天就挂。而交通灯系统恰好是一个功能边界清晰、状态逻辑明确、硬件外设丰富的训练载体麻雀虽小五脏俱全。做完这个项目你对STM32的中断系统、时钟树、GPIO复用模式的理解会有一个质的提升。适合的人群很明确学过C语言、知道GPIO点灯但还没做过完整小项目的初学者以及正在准备毕业设计、课程设计需要一套可仿真、可下载、可写报告的完整方案的在校学生。1.2 方案选型为什么是STM32F103C8T6选型这件事我建议直接上STM32F103C8T6也就是常说的蓝丸核心板。理由有三。第一性价比极高。一块核心板十几块钱坏了不心疼而且淘宝、立创商城到处都有货。第二资料密度大。这颗芯片是ST出货量最大的型号之一无论你是用标准外设库还是HAL库遇到问题基本都能搜到答案。第三资源够用且不浪费。交通灯系统用到定时器TIM、GPIO、EXTI外部中断、可能还有串口打印调试信息C8T6的64KB Flash和20KB RAM完全够用不至于出现杀鸡用牛刀的资源浪费也不会像51单片机那样捉襟见肘。有朋友可能会问51单片机也能做交通灯为什么非要用STM32我的回答是51做这个项目确实更简单但正是这种简单让你学不到中断优先级管理、时钟树配置、复用功能映射这些真正值钱的东西。STM32版本虽然调试时麻烦一点但你把这块硬骨头啃下来后面再做任何嵌入式项目底层思维都是通的。2. 核心模块原理与代码实现2.1 定时器中断交通灯时序的底层引擎交通灯系统最核心的需求是什么是按时切换。让主干道绿灯亮30秒、黄灯亮3秒支干道绿灯亮20秒、黄灯亮3秒这条时序链必须精确不能靠delay()死等——因为死等期间CPU啥也干不了按键扫描、数码管刷新全都会卡死。正确做法是用定时器中断产生一个心跳节拍我习惯把基础中断周期设为100ms。以STM32F103C8T6默认的72MHz系统主频为例TIM2挂载在APB1总线上需要先计算预分频系数和自动重装载值。void TIM2_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseStructure.TIM_Period 999; // ARR自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler 7199; // PSC预分频系数 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); TIM_Cmd(TIM2, ENABLE); }这里的计算逻辑是72MHz ÷ 7200 10kHz也就是说经过预分频后计数器每0.1ms加1再计数1000次就是100ms。如果你想要1s中断一次把TIM_Period改成9999即可。这个参数换算关系务必要自己会算面试或者答辩时被问到定时器怎么产生1ms时基几乎是必考题。中断服务函数里做的事情很简单一个软件计数器累加加到10就是1秒然后修改变量sec主循环根据sec的变化去推进状态机。volatile uint16_t tick_100ms 0; volatile uint8_t sec_flag 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); tick_100ms; if (tick_100ms 10) { tick_100ms 0; sec_flag 1; // 每秒置位一次主循环消费这个标志 } } }这里有一个新手很容易踩的坑中断服务函数里尽量少做事不要在里面写LED翻转、数码管扫描这类耗时操作。你只需要把时间到了这个信号通过标志位传出去让主循环去处理具体业务。这样做的原因是保证中断函数短小精悍避免中断嵌套太深导致系统时钟漂移。2.2 按键消抖与模式切换的状态机设计交通灯不只有自动运行一种模式我设计的系统还支持两个按键按键1用于切换自动模式和紧急模式按键2用于在紧急模式下让四个方向全红模拟消防车/救护车优先通行。这个功能看起来简单但涉及一个经典问题——按键消抖。机械按键在按下和释放的瞬间触点会出现约5~20ms的抖动如果不做处理一次按下会被识别成多次。消抖方案有硬件RC滤波和软件延时两种考虑到成本99%的项目都用软件消抖。我的做法是检测到电平变化后延时20ms再读一次确认电平确实变了才算有效按下。uint8_t Key_Scan(void) { static uint8_t key_up 1; if (key_up (KEY1_GPIO 0)) { Delay_Ms(20); // 跳过抖动区 if (KEY1_GPIO 0) { key_up 0; return KEY1_PRESSED; } } else if (KEY1_GPIO 1) { key_up 1; } return KEY_NONE; }状态机是整个系统代码的灵魂。我用一个current_state变量表示当前处于哪个状态用state_time记录当前状态已经持续了几秒。每个100ms中断里更新state_time到了设定值就跳转到下一个状态。状态0主干道绿灯亮支干道红灯亮持续30秒状态1主干道黄灯亮支干道红灯亮持续3秒状态2主干道红灯亮支干道绿灯亮持续20秒状态3主干道红灯亮支干道黄灯亮持续3秒很多教程会直接给一张完整的状态表但没说清楚为什么状态0要持续30秒而状态2只持续20秒。这里的逻辑是主干道车流量大绿灯时间长支干道车流量小绿灯时间短。两个方向的周期错开主干道一个完整周期是33秒303支干道是23秒203彼此并不对称但这才是真实路口的情况。在紧急模式下状态机不再自动跳转所有方向的红灯常亮黄灯以1Hz频率闪烁。这个功能我强烈建议做进去因为它是展示你中断嵌套处理能力的最佳场景——紧急按键可以设计成EXTI外部中断抢占定时器中断的优先级。2.3 数码管倒计时显示与动态扫描交通灯需要实时显示每个方向的剩余秒数我用的是两位共阴数码管主干道和支干道各一个。这里涉及两个知识点段码表和动态扫描。段码表说白了就是数字到引脚电平的映射。共阴数码管中要点亮某个段对应引脚输出高电平。数字0~9对应的段码如下const uint8_t seg_code[] {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};这个数组背不下来也没关系写代码时对照数码管引脚图一个一个算就行。注意0x3F是数字0二进制是0011 1111对应a~f六个段全亮、g段不亮正好是0的形状。动态扫描的原理比较有意思人的眼睛有视觉暂留效应如果一个数码管位选信号轮流导通每次只点亮一个数码管刷新频率高于50Hz人眼看起来就像所有数码管同时在亮。这种时分复用的思路在嵌入式里非常常见LED点阵屏、LCD显示屏底层都是这么干的。void Display_Scan(void) { static uint8_t pos 0; // 关闭所有位选防止残影 GPIO_ResetBits(GPIOB, GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15); // 根据当前位选输出段码 if (pos 0) { GPIO_SetBits(GPIOB, GPIO_Pin_12); // 打开主干道十位 GPIO_Write(GPIOA, seg_code[main_sec / 10]); } else if (pos 1) { GPIO_SetBits(GPIOB, GPIO_Pin_13); // 打开主干道个位 GPIO_Write(GPIOA, seg_code[main_sec % 10]); } // 支干道两个位类似 pos; if (pos 4) pos 0; }这个扫描函数必须放在定时器中断里或者主循环高频调用。放在主循环的问题在于如果主循环里恰好在处理按键延时或者状态切换数码管刷新就会卡顿肉眼能看到明显的闪烁。所以我建议把扫描函数放在TIM中断里注意保持中断函数精简实测下来刷新周期2~5ms就能做到无闪烁显示。3. 仿真搭建与联调要点3.1 Proteus中搭建STM32最小系统与硬件连线仿真阶段的工具链是Keil MDK Proteus 8。很多新手第一次用Proteus画STM32电路时会卡住元件库里找不到STM32F103C8T6。这是因为新版Proteus把STM32模型放在了STM32分类下需要输入完整型号搜索。选好芯片后需要配置两个关键参数晶振频率设为8MHz对应代码里的HSE_VALUE、勾选Program File加载编译生成的hex文件。电路连接的核心是这个思路PA0~PA7接主干道6个LED红黄绿×2和支干道对应引脚PB12~PB15接数码管位选PA8~PA15经限流电阻接数码管段选。Proteus仿真里LED如果不加电阻也能亮但建议还是加上一方面更接近真实电路另一方面防止电流过大导致仿真异常。我自己习惯用220Ω的限流电阻实际做板子时也用这个值。有一个Proteus特有的坑必须提醒仿真时右下角有播放/暂停/步进按钮很多人把仿真速度调到最大结果LED切换快到看不清误以为代码有bug。正确的做法是点击左下角的帧率设置把动画速度调到20帧/秒左右这样才能明显看到状态切换过程。另外Proteus 8的仿真时间不是实时的偶尔会出现定时器比真实时间快的情况这时候对比数码管倒计时和手机秒表能差出10%左右属于正常现象不要因此怀疑自己的定时器参数算错了。3.2 Keil工程配置与仿真联调的几个关键设置Keil MDK方面我建议使用标准外设库StdPeriph_Lib而不是HAL库原因很实在网上90%的交通灯参考代码都是标准库写的资料好找而且标准库的寄存器操作思路更能帮你理解芯片底层。工程配置里几个必须注意的点第一魔术棒Options for Target里的Debug选项卡要选择Use Simulator还是Use ST-Link。如果只做Proteus仿真选Simulator即可如果要下载到实物板子选ST-Link并配置Flash Download选项勾选Reset and Run让程序下载后自动运行。第二C/C选项卡里Define填STM32F10X_MD, USE_STDPERIPH_DRIVER。这个宏定义告诉标准库你要用的是中等容量器件这个不填编译会报一堆#error。很多新手第一次编译报错全是这个原因。第三生成hex文件才能加载到Proteus里。Output选项卡勾选Create HEX File编译后到工程目录的Objects文件夹找.hex文件。联调时最经典的问题场景是Proteus里点了运行LED一个都不亮。排查顺序应该是先确认程序有没有下载进去双击芯片看Program File路径对不对再检查GPIO时钟有没有使能——STM32所有外设的时钟默认是关闭的你用GPIOA之前必须调用RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE)。这一步漏了代码里操作寄存器全白搭这是新手最容易忘、又最基础的一步。3.3 从仿真到实物的移植差异仿真跑通之后往实物板子迁移基本是水到渠成的事但有三个差异要提前心里有数。仿真里LED直接接GPIO就能亮实物必须加限流电阻否则LED会烧毁或者把GPIO口搞坏。STM32的GPIO最大输出电流约25mALED正常工作电流取5~10mA就够了串联220Ω~1kΩ电阻都行。仿真里按键不抖、不接触不良实物按键按下去之后必须消抖否则状态机可能啪地跳两下。仿真里你是看不到这个问题的但实物调试时消抖逻辑就是刚需。还有一点容易忽略实物板上如果用的晶振不是8MHz代码里SystemInit()里的时钟配置就不对整颗芯片的运行速度会异常。比如你淘宝买的板载晶振是8MHz还好万一买到12MHz的板子代码里的RCC_PLLConfig换算出来的主频就不对定时器时间全乱。买板子时一定确认晶振频率或者直接用芯片内部HSI时钟默认8MHz也能跑只是没有外部晶振精确。4. 常见问题与调试经验记录4.1 Keil编译与下载的报错速查我整理了一下这个项目里最容易碰到的几个报错和处理方案放在一张表里你们直接对号入座。报错内容原因分析解决方案error: #20: identifier GPIO_Pin_0 is undefined没包含stm32f10x_gpio.h头文件检查#include stm32f10x_gpio.h或者stm32f10x_conf.h里是否注释掉了对应外设头文件error: L6218E: Undefined symbol TIM_Cmd标准库源文件没添加进工程在Keil工程里添加stm32f10x_tim.c并重新编译warning: #223-D: function delay_ms declared implicitly函数没有在头文件中声明添加void delay_ms(uint16_t ms);声明或检查头文件是否被包含J-Link提示Clone或盗版J-Link固件被识别为盗版换用ST-Link或更新J-Link驱动到较新版本程序下载后不运行Flash Download里没勾选Reset and Run在Options for Target - Utilities - Settings里勾选Reset and Run这些报错表面看着吓人实际上只要工程模板搭建正确基本都能避开。我建议新手不要从零开始建工程直接找一个成熟的标准库工程模板在上面改代码重点把精力放在业务逻辑上。4.2 数码管显示异常和仿真速度的坑数码管显示这一块最常见的两个问题是乱码和闪烁。乱码通常是段选和位选的GPIO配置出了问题。比如你把段选接到了PA0~PA7但是代码里操作的是GPIOA的低8位而GPIO_Write(GPIOA, seg_code[main_sec / 10])会一次性写入16个引脚高8位引脚如果被复用成了别的功能可能就会干扰段码。建议段选引脚固定在同一个端口的低8位并确保没有和调试下载引脚冲突。闪烁则基本是动态扫描频率太低导致的。人眼的临界闪烁频率大约在50Hz也就是一个数码管每秒至少刷新50次。四位数码管每个都要刷50次那扫描函数每秒至少要执行200次换算下来扫描周期不能超过5ms。如果你的主循环里有一段耗时的延时比如按键消抖的20ms数码管刷新就会被卡住肉眼看起来就是一亮一灭。解决办法就是我只在中断里刷新显示别在主循环里做这件事。关于仿真速度我前面提到过一点这里再补充一个细节。Proteus左下角的帧率控制其实有三个选项Minimum、Maximum和Real Time。做交通灯这种有时序逻辑的仿真选Real Time模式才能让倒计时和真实时间基本同步。如果你发现倒计时比实际快多半是选了Maximum模式。另外一个技巧是如果某次LED状态切换你还没看清就跳过去了可以暂停仿真用左下角的单步按钮逐步推进或者临时把定时器的TIM_Period改大一点让整个时序变慢方便观察逻辑。4.3 GPIO复用与引脚冲突的排查思路做这个项目时如果用的核心板有板载LED或者板载USB转串口芯片你就得留意引脚冲突的问题。STM32F103C8T6的PA13~PA15、PB3、PB4默认是JTAG调试引脚如果你把数码管段选接到了这些引脚上在代码里必须先把JTAG关闭才能当普通GPIO用。GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);这行代码的作用是将PA13、PA14保留给SWD调试接口PA15、PB3、PB4释放为普通GPIO。注意关掉JTAG之后只保留SWD串行调试也能用ST-Link下载程序所以放心关。我在这个项目里就是用PA15、PB3、PB4接了数码管的位选一开始没做重映射数码管怎么都不亮排查了半天才反应过来是JTAG占用的问题。这个坑比较隐蔽你们如果遇到引脚电平明明配置了但输出去没反应一定要第一时间检查是不是复用冲突。另外两个容易混淆的引脚是PB2和PB10~PB11。PB2默认是BOOT1引脚上拉状态很多核心板把它引出来但没标注清楚你用之前最好拿万用表量一下。PB10、PB11是USART3的引脚如果你的核心板没有引出USART3这两个引脚拿来当普通GPIO用没问题但如果和板载器件共享了就要留意。4.4 这个项目还可以怎么扩展交通灯项目做完之后千万不要觉得结束了。这个项目的架构完全可以往多个方向延伸我列几个比较有实战价值的扩展方向。加行人过街按钮。平时机动车道绿灯通行行人按下按钮后系统在当前状态结束后给行人方向插入一段绿灯通行时间。这个功能本质是给状态机增加一个中断状态是学习状态机嵌套的绝佳练习。做多路口联动控制。用两个STM32通过串口通信实现主干道和支路交叉口的协调配时A路口绿灯亮起后延迟若干秒B路口才变绿模拟绿波带效果。这个项目做下来你会把串口中断、数据帧协议设计这些知识点全部串起来。加倒计时语音播报。通过语音模块在黄灯亮起时播报黄灯请注意在红灯剩余3秒时播报即将变灯。这算是给系统加了一个外设控制的维度难度不大但拿来展示加分很多。我个人在实际操作中的体会是这个项目最值钱的部分不是那几行LED控制代码而是你从头到尾完成了一次需求分析→方案设计→代码实现→仿真验证→实物调试的完整闭环。很多人在学习嵌入式时习惯看教程、抄代码但只有亲手把状态机调通、亲眼看到数码管从9变到0、亲耳听到实物板子按键咔哒一声切换模式你才算真正迈进了嵌入式开发的大门。做完这个交通灯我建议你试着不参考任何资料从零开始把代码重新写一遍——能写出来你就真的会了。本文还有配套的精品资源点击获取
返回列表