尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于STM32的智能交通灯设计:状态机与Proteus仿真全解析

基于STM32的智能交通灯设计:状态机与Proteus仿真全解析 简介基于STM32单片机的智能交通灯设计Proteus仿真完整方案面向嵌入式初学者、电子设计竞赛学生及交通灯课程设计人员解决在缺少实物硬件时难以验证控制逻辑的问题可系统学习 GPIO 驱动、定时器中断、红绿黄灯时序切换的完整开发流程。资源包共370个文件压缩体积28.54MB以C语言源文件.c/.h、Proteus仿真工程.pdsprj、Keil工程配置、编译链接文件、PDF设计报告与hex烧录文件为主附带演示视频和调试辅助文件便于对照实物效果进行排错与二次开发。方案覆盖硬件接口设计、定时器配置、C语言程序逻辑、Proteus虚拟仿真与调试优化等关键环节包含三色灯控制、各方向车辆与行人通行时序等核心逻辑配套电路图、设计报告和仿真模型有助于将课堂知识转化为可运行的项目。已有2179人学习浏览资料完整度较高适合需要完整源码、仿真环境与设计文档配合学习的实践型开发者。 每年课设季总会收到同款问题“智能交通灯这种题目网上资料一抓一大把为什么我照着下载的仿真图做要么烧录报错要么灯永远只亮一个”细聊下来就会发现多数人拿到的所谓“全套资料”要么仿真图和源程序版本对不上要么代码没有注释一改就废。我这套基于STM32的智能交通灯设计源程序、Proteus仿真、说明文档是完整配套的在Proteus 8.x上反复跑过也帮不少同学顺利交了课设。这篇文章不搞虚的把从需求拆分到代码实现、从画仿真电路到跑通验证的过程完整讲一遍适合正在做STM32课设、或者想入门Proteus仿真调试的同学直接参考。1. 交通灯项目到底难在哪不是点灯是状态管理1.1 先做需求拆解别急着开Keil很多初学者拿到“智能交通灯”题目第一反应就是写个主循环红灯延时30秒绿灯延时20秒循环下去不就行了听起来很直白但实际写到一半就会发现问题——倒计时数码管需要独立刷新按键要插队切换模式紧急车辆来了要立刻抢通这些东西全部塞进一个delay延时循环里代码很快就会乱成一锅粥。所以拿到题目的第一步不是写代码而是把需求拆清楚。一个标准的ST32智能交通灯设计至少包含以下几块主干道和支干道两个方向各有一组红、黄、绿三色LED灯。两个方向的倒计时数码管实时显示当前状态的剩余秒数。一个按键用于进入紧急模式比如让主干道强制绿灯、支干道强制红灯。一个开关用于夜间模式所有方向黄灯以固定频率闪烁。状态切换时蜂鸣器短鸣一声提醒行人。这套需求看起来不多但它天然是一个有限状态机系统在当前状态下运行等待计时结束再迁移到下一个状态。用状态机思维去设计代码结构会清晰很多后面加紧急模式、夜间模式都只是往状态迁移表里补分支的事。1.2 状态迁移表把逻辑写死在纸面上我习惯先画一张状态表把所有状态列出来再确定它们的持续时间和迁移条件。这个项目核心就是四个常规状态加上两类特殊模式状态主干道支干道持续时间下一状态S0 主干道通行绿灯红灯30秒S1S1 主干道黄灯黄灯红灯3秒S2S2 支干道通行红灯绿灯20秒S3S3 支干道黄灯红灯黄灯3秒S0紧急模式和夜间模式不改变这张主表的顺序只是在紧急模式触发时强制把状态跳到一个特殊分支退出后再根据剩余时间回到原状态。这样设计的好处是任何时候你都能用一行注释说清楚系统现在在干什么答辩时被问到底层逻辑也能顺手把状态迁移表画给老师看。1.3 用定时器产生“秒”的基准而不是用delay状态机的核心是“时间”。最忌讳的做法是HAL_Delay(30000)这种死等式延时因为在延时期间数码管扫描、按键检测、蜂鸣器全部处于阻塞状态系统对外界毫无响应。正确的做法是用一个硬件定时器产生1ms中断在中断里对计数器累加累加到1000就是1秒同时这个计数器还可以作为数码管动态扫描和按键消抖的时间基准。我习惯这样设计时间变量volatile unsigned int ms_ticks 0; // 系统运行毫秒计数 volatile unsigned int second_cnt 0; // 当前状态已运行秒数在定时器中断服务函数里简单累加主循环里只负责读取判断。这样做的好处是系统时间基准和业务逻辑完全解耦后面想扩展一个“倒计时最后3秒蜂鸣器滴滴响”的功能只需要在主循环里判断second_cnt的值即可不用动定时器配置。2. 选型复盘为什么是STM32F103C8T6加Proteus 82.1 主控选型STM32F103C8T6是最稳的方案做交通灯仿真主控芯片的选择其实不多。我见过有人用STC89C52做51单片机教程多、上手门槛低但它的IO口驱动能力弱配置定时器要自己算初值Proteus里的老模型也容易出怪问题。用Arduino做更简单但在不少课程设计要求里Arduino会被认为是“封装过度”答辩时容易被追问内部原理。STM32F103C8T6这颗芯片在这类课设里几乎是完美答案它价格便宜Proteus元件库里模型成熟标准外设库和HAL库的资料都极其丰富。更重要的是它的IO口足够多主控、4位数码管、12个LED、按键蜂鸣器加起来引脚依然有富余不用为引脚不够去搞矩阵扫描。如果你之前跟过江科大或者正点原子的STM32教程那用标准外设库会非常顺手如果新生代同学习惯用STM32CubeMX配置HAL库Proteus加载Hex文件后同样能跑实测没有问题。2.2 Proteus仿真的价值可调试、可演示、可反复改这年头做一个完整的实物交通灯需要焊板子、买器件、烧录器出了问题还得万用表量。Proteus仿真的最大价值是“把改代码的成本降到几乎为零”。我在仿真里验证状态机的完整时序确认数码管每一位显示正确再决定要不要做实物。仿真还有一个实物没有的好处可以随时暂停单步观察某个引脚当前是高电平还是低电平这对于排查“为什么这个灯不亮”的定位效率极高。需要提醒的是仿真跑通不代表实物一定没问题比如Proteus里LED不接限流电阻也能亮但实物里早晚烧灯。不过对于课设验收仿真已经能解决大部分演示和评分需求这也是这套资料坚持“源程序加仿真”配套的原因。2.3 资料包结构拿到文件后第一步看什么我整理的这套资料内部文件夹是这样划分的├── 01-源程序 │ ├── 智能交通灯.uvprojx // Keil工程文件 │ ├── Core/Src/main.c // 主逻辑 │ ├── Core/Src/tim.c // 定时器配置 │ └── 智能交通灯.hex // 已编译好的烧录文件 ├── 02-Proteus仿真 │ └── 智能交通灯.pdsprj // Proteus工程文件 ├── 03-原理图与PCB │ ├── 原理图.pdf │ └── PCB工程文件 ├── 04-参考资料 │ ├── 芯片数据手册 │ └── Proteus安装与加载说明 └── 05-使用说明 └── 环境配置与运行步骤.md拿到资料包后我的建议是按这个顺序看先看05-使用说明把环境跑通再打开02-Proteus仿真看电路连接关系最后回头读01-源程序的main.c。千万别上来就改代码因为你还不清楚仿真里按钮接的是哪个引脚、数码管是共阴还是共阳改了也是瞎改。3. 核心代码拆解定时器、状态机、扫描显示三件事3.1 GPIO初始化先确定每一组灯挂在哪个端口交通灯的物理连接直接决定代码怎么写。以我这份设计为例主干道三色灯接PC0、PC1、PC2支干道三色灯接PC3、PC4、PC5数码管段选线接PB0到PB7位选线接PA0到PA3。这样分组的好处是逻辑清晰不管是点亮整组灯还是刷新数码管都能用一次端口操作完成。用HAL库初始化很简单__HAL_RCC_GPIOC_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOC, GPIO_InitStruct);这里有个容易被人忽略的细节LED驱动引脚最好选择推挽输出如果是开漏输出Proteus里的LED亮度会明显偏暗甚至完全不亮。在实物上这个问题更突出所以代码里GPIO_MODE_OUTPUT_PP这个模式要记住。3.2 状态机主循环switch-case比一堆if else好维护状态机的实现方式有很多种我推荐用switch-case配合枚举类型可读性最高。核心代码如下typedef enum { STATE_GREEN_MAIN 0, STATE_YELLOW_MAIN, STATE_GREEN_SLAVE, STATE_YELLOW_SLAVE } TrafficState; TrafficState state STATE_GREEN_MAIN; void TrafficLight_Task(void) { switch (state) { case STATE_GREEN_MAIN: MainRoad_Green(); SlaveRoad_Red(); if (second_cnt 30) { state STATE_YELLOW_MAIN; second_cnt 0; } break; case STATE_YELLOW_MAIN: MainRoad_Yellow(); SlaveRoad_Red(); if (second_cnt 3) { state STATE_GREEN_SLAVE; second_cnt 0; } break; case STATE_GREEN_SLAVE: MainRoad_Red(); SlaveRoad_Green(); if (second_cnt 20) { state STATE_YELLOW_SLAVE; second_cnt 0; } break; case STATE_YELLOW_SLAVE: MainRoad_Red(); SlaveRoad_Yellow(); if (second_cnt 3) { state STATE_GREEN_MAIN; second_cnt 0; } break; } }这段代码把每个状态下该点亮哪些灯、持续多久、跳到哪个状态写得明明白白。second_cnt的递增由定时器中断完成主循环里只负责比较和跳转。这样整个状态机没有任何阻塞点按键响应、数码管扫描都能在主循环的其他函数里实时进行。还有一个小技巧紧急模式的实现不需要单独写一个分支只需要在状态机函数开头加一个判断如果紧急模式标志位被置位就直接强制让主干道绿灯、支干道红灯同时把second_cnt清零退出紧急模式后系统自然会在1秒后重新进入正常状态迁移。3.3 数码管动态扫描别让闪烁毁了整个演示4位数码管的倒计时显示是很多同学容易翻车的地方。它采用动态扫描原理同一时间只点亮一位高速轮流点亮4位利用人眼视觉暂留形成“4位同时显示”的效果。扫描频率必须在50Hz以上也就是每一位的刷新时间大约5ms我用1ms定时器做扫描基准正好合适。数码管段码表需要提前准备好以共阴数码管为例const unsigned char seg_code[] { 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F, // 9 0x00 // 全灭 };扫描函数像这样写void Display_Scan(void) { static unsigned char pos 0; // 需要显示4位数主干道十位、个位支干道十位、个位 switch (pos) { case 0: PICK_PA0; SEG_Write(seg_code[main_sec / 10]); break; case 1: PICK_PA1; SEG_Write(seg_code[main_sec % 10]); break; case 2: PICK_PA2; SEG_Write(seg_code[slave_sec / 10]); break; case 3: PICK_PA3; SEG_Write(seg_code[slave_sec % 10]); break; } pos; if (pos 4) pos 0; }SEG_Write函数把段码发送到PB端口发送完立刻切换位选。这里要注意必须先送段码再切换位选否则会出现一位短暂显示上一位的残影。这个问题在实物上不仔细看很难发现但在Proteus里暂停仿真时看得很清楚属于必须提前处理掉的细节。3.4 按键消抖10毫秒延时确认按键在按下和松开的瞬间电平会出现不稳定的抖动如果不做消抖处理一次按键可能被误判成多次触发。实际工程中常用“延时去抖”法检测到按键电平变化后延时10到20毫秒再次读取如果电平状态不变才确认本次按键有效。void Key_Scan(void) { static unsigned char last_state 1; if (HAL_GPIO_ReadPin(GPIOD, GPIO_PIN_0) 0 last_state 1) { HAL_Delay(10); // 跳过抖动区 if (HAL_GPIO_ReadPin(GPIOD, GPIO_PIN_0) 0) { is_emergency !is_emergency; } } last_state HAL_GPIO_ReadPin(GPIOD, GPIO_PIN_0); }在Proteus仿真里用交互型按钮点击时不会像真实按键那样有明显抖动但代码依然要保留消抖逻辑。因为你的代码最终是要能移植到实物板子上的仿真里砍掉消抖实物就等着被吐槽按键不灵吧。4. Proteus电路搭建与程序烧录全流程4.1 元件清单与接线要点Proteus新建工程时选择“Create a schematic from a selected template”芯片从元件库搜索STM32F103C8即可。整套电路需要的元件如下元件型号/参数数量备注主控芯片STM32F103C8T61Proteus库名通常为STM32F103C8发光二极管LED-RED / LED-YELLOW / LED-GREEN12两个方向各6个数码管7SEG-MPX2-CA 或 7SEG-MPX2-CC2两方向各一个两位数码管电阻RES 220Ω12LED限流用电阻RES 1kΩ8数码管段选限流可简化按键BUTTON2手动控制和复位开关SWITCH1夜间模式切换蜂鸣器BUZZER1状态切换提示接线时的几个关键点LED的正极接STM32的GPIO引脚负极通过220欧姆电阻接地这样的低电平点亮逻辑在代码里对应GPIO_PIN_RESET点亮。但如果你更习惯高电平点亮把LED正极接3.3V、负极通过电阻接GPIO也行前提是代码里GPIO初始化保持一致。数码管部分要注意共阴还是共阳段码表完全不同。我上面给的段码表是共阴数码管的如果你在Proteus里选了7SEG-MPX2-CC需要把段码取反这一步经常有人搞反导致显示出来的数字像乱码。4.2 加载Hex文件的正确方式代码在Keil里编译通过后会在工程目录下的Objects或MDK-ARM文件夹里生成.hex文件。回到Proteus双击STM32F103C8芯片在“Program File”一栏选择这个hex文件。很多人漏掉这一步就直接点运行仿真器自然报错。还有一个重要参数是“Crystal Frequency”默认可能是4M或8M需要和你代码里的时钟配置一致。我这份代码默认按8M外部晶振配置所以这里也填8M。如果这里填错倒计时走秒速度会明显不对这是Proteus仿真的一个经典坑——程序里用HAL_Delay这类相对延时还好但秒计数依赖绝对时间配置不一致就会整个乱套。4.3 运行仿真的正确姿势加载完hex点击左下角的运行按钮交通灯就开始跑了。如果一开始没反应先检查两个地方一是芯片有没有成功加载程序二是仿真脚本是否处于暂停状态。Proteus运行后用鼠标点击交互型按钮会模拟一次按下动作适合用来测试紧急模式入口。切换夜间模式用的是开关元件点击一次保持闭合再点击一次断开这个状态是持续性的适合做模式切换。这个区别很多新手不知道总拿按钮当开关用点了松开后发现模式没保持还以为代码写错了。5. 实测踩坑记录从“No target found”到LED不亮5.1 加载程序阶段的“No target found”到底是什么问题很多人在Proteus里点运行弹出类似No target found的报错第一反应是去查烧录器配置折腾半天没结果。实际上在仿真环境里这个报错绝大多数情况是hex文件没有正确加载到芯片模型里Proteus找不到可执行程序不是真的“找不到目标器件”。正确的排查顺序是双击芯片确认“Program File”路径下有内容再确认该路径下的hex文件是最近一次编译生成的最后检查芯片型号是否选错。我有一次在仿真图里放了两个STM32芯片只给其中一个加载了hex结果运行后一个正常一个完全没反应也容易被误判成程序问题。5.2 LED不亮或者暗得离谱优先查限流电阻和GPIO模式仿真中LED完全不亮最可能的原因是GPIO初始化成了开漏输出。开漏模式下IO口只能拉低不能主动拉高LED两端没有电位差自然不亮。解决办法是把GPIO模式改成推挽输出这在前面已经提到。如果LED能亮但亮度很暗则是限流电阻太大。220欧姆在3.3V供电下已经足够如果用了10k欧姆电流只有0.3mA左右LED亮度会肉眼可见地低。Proteus对LED亮度模拟是真实器件模型不是单纯的亮或不亮所以这个细节能直接看出来。排查时双击LED看电流参数就能确认限流电阻是否合适。5.3 仿真时间与现实时间严重不符Proteus仿真STM32的实时性受电脑CPU性能影响很大跑复杂图形界面时会比真实时间慢这是正常现象不代表程序有问题。如果倒计时明显过快或过慢除了检查芯片的“Crystal Frequency”是否和代码一致外还可以在菜单System里选择Model Options把仿真精度调到合适的档位。另外如果你在代码里开了编译优化等级某些延时行为也会发生变化。调试阶段我建议用默认优化等级等所有功能跑通后再尝试提高优化等级这样既保证稳定性也让仿真速度快一点。5.4 数码管显示乱码或残影数码管乱码通常出在共阴共阳选反段码表直接不对应。残影则多是动态扫描时序问题段码和数据位切换的顺序不对。我的经验是先送段码再开位选最后延时1ms循环扫描。不要反过来先开位选再送段码那样上一位的数字会瞬态显示在当前位上视觉上就是一层淡淡的影子。如果残影在Proteus里观察不到但在实物上很明显可以适当缩短每位点亮时间到2ms同时保证整个扫描周期不超过8ms人眼就完全分辨不出来了。5.5 按键无响应先分清交互按钮和开关Proteus里BUTTON和SWITCH是两种完全不同的元件。BUTTON按下时导通松开后自动断开适合做点动按钮比如紧急模式触发SWITCH点击一次锁定闭合并保持适合做模式开关比如夜间模式。代码里的按键扫描逻辑要跟元件类型对应BUTTON可以读边沿SWITCH则需要读电平状态或者加一级状态锁存。如果仿真里按键完全没反应先检查按键是否连到了代码所读的引脚再确认GND连接是否正确。Proteus里的BUTTON一端接GPIO另一端接GND是最常见的接法内部上拉电阻在软件里配置成GPIO_PULLUP这样按键未按下时读高电平按下时读低电平代码逻辑上才判断得准。这套项目做完之后我个人最大的体会是交通灯虽然逻辑简单但它把定时器、中断、状态机、动态扫描、输入消抖这些嵌入式基础知识点全部串了起来是性价比极高的一次练手。如果你正在做这个题目建议先改状态机里的三个时间参数比如把主干道绿灯改成10秒感受一下整体联动变化再动手加紧急模式和夜间模式。等这些功能都跑通了再回头看我前面说的引脚配置和显示细节你会发现很多问题都迎刃而解。本文还有配套的精品资源点击获取
返回列表