ARTICLE DETAIL

资讯详情

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

STM32F103交通灯仿真:GPIO、定时器与状态机实战

STM32F103交通灯仿真:GPIO、定时器与状态机实战 简介基于STM32单片机的交通灯Proteus仿真项目定位于嵌入式系统设计与微控制器编程学习场景适合电子类学生、开发工程师以及智能交通控制方向的研究者。项目完整演示了STM32的GPIO、定时器、中断等外设配置方法并通过Proteus实现红黄绿交通灯时序控制及紧急车辆通行逻辑的仿真验证。压缩包共341个文件整体大小约653.97MB。核心内容包括99个C源码、73个H头文件、Proteus仿真工程pdsprj、Keil工程文件uvprojx/uvoptx以及编译生成的hex/axf固件和中间文件o/crf/d。源码、仿真工程与编译产物分离便于对照阅读和直接加载测试。已有172人学习下载。使用者可借此快速掌握Keil MDK-ARM与Proteus环境搭建理解STM32库函数调用和中断服务编写方式同时获得完整的交通灯控制逻辑参考设计。该案例理论与实践并重是完成单片机仿真开发流程训练的高价值样板。1. 从红灯到绿灯STM32交通灯仿真在学什么东西主干道绿灯30秒、黄灯3秒南北支路绿灯20秒、黄灯3秒四个状态循环一轮恰好56秒。这套时序逻辑放到STM32F103上用定时器中断驱动再通过Proteus仿真核心只有三件事GPIO推挽输出、TIM2分频计算、状态机状态转移任何一处配置错位LED的表现都会和预期差很远。项目自带的DEMO.hex可以直接加载进Proteus的STM32F103R6模型运行DEMO.axf保留调试符号配合Keil MDK-ARM能单步跟踪状态切换。它不依赖任何真实硬件适合刚接触单片机仿真的学生走通全流程也适合在画板前先用软件验证时序逻辑的工程师。下面从电路搭建、定时器配置、状态机实现到联调排查把容易踩坑的细节逐一拆开。2. STM32F103的GPIO与定时器分配仿真电路的第一步2.1 为什么用STM32F103而不是51单片机51单片机的交通灯demo几乎是电子专业入门标配用延时函数配合P1口电平翻转就能完成。但换成STM32F103问题性质就变了72MHz主频下软件延时的精度会受到中断响应的影响而且Proteus对STM32的仿真模型精确模拟了外设寄存器行为用阻塞式延时做长定时仿真里能跑一旦挂上传感器或紧急按钮延时的阻塞问题就会暴露。所以这个项目采用TIM2定时器中断产生1ms基准节拍主循环只做时间累积和状态判断比延时函数可靠比上RTOS又轻量得多。选择STM32F103而不是更高端的F4有一个仿真效率上的实际理由Proteus对Cortex-M4模型的计算量明显大于Cortex-M3F103C6、F103R6这类中小容量型号的模型运行稳定配合Proteus 8 Professional的实时仿真跑56秒周期的交通灯毫无压力。用F407也能跑但SPD速度滑块推到最高后提速不明显操作体验差一截。这套工程面向的是教学和逻辑验证F103的模型成熟度和性价比都是最合适的。2.2 Proteus电路搭建与限流电阻计算Proteus里的LED模型参数和真实器件一致不接限流电阻会让电流钳制在模型允许的最大值LED过亮甚至仿真报警。常规做法是每个LED串一个220Ω电阻5V供电下LED压降按2V估算工作电流约13.6mA亮度足够也不会超出STM32 GPIO的灌电流能力。元件清单如下在Proteus元件库搜索对应关键字即可放置元件Proteus搜索关键字数量说明主控芯片STM32F103R6164脚Flash 32KB红色LEDLED-RED2东西向、南北向红灯黄色LEDLED-YELLOW2东西向、南北向黄灯绿色LEDLED-GREEN2东西向、南北向绿灯220Ω电阻RES6LED限流按钮BUTTON1紧急车辆优先输入8MHz晶振CRYSTAL1HSE外部时钟源22pF电容CAP2晶振负载电容接线规则东西方向绿、黄、红依次接PA0、PA1、PA2南北方向绿、黄、红依次接PB0、PB1、PB2所有LED阴极经220Ω电阻统一接GND。GPIO全部配置为推挽输出复位后默认低电平上电瞬间六盏灯全部熄灭。这里要强调晶振两端的22pF负载电容Proteus仿真时画漏了不报错但换到真实板卡上会导致HSE无法起振。如果工程里用的是STM32F103ZET6这类大容量型号还需要确认Proteus元件库有没有对应模型ZET6和R6引脚不同原理图和固件必须配套。2.2.1 时钟树HSE、PLL与72MHz系统时钟的关系STM32F103有HSI和HSE两条时钟路径HSI是8MHz片上RC振荡器精度一般HSE外接8MHz晶振后经PLL倍频到72MHz系统时钟。Proteus里双击芯片打开属性对话框Clock Frequency这一项填的就是HSE频率必须与工程代码里的HSE_VALUE宏一致。这里有一个高频错误把Clock Frequency误填成72MHz结果PLL倍频后系统时钟变成648MHz仿真器不报错但所有定时器都快了近9倍3秒黄灯肉眼几乎看不到。2.3 TIM2基准时钟的计算与HAL配置TIM2挂在APB1总线上当APB1预分频器设为2时定时器时钟等于APB1的两倍也就是72MHz。要让TIM2每1ms溢出一次需要先预分频再计数static void MX_TIM2_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig {0}; TIM_MasterConfigTypeDef sMasterConfig {0}; htim2.Instance TIM2; htim2.Init.Prescaler 71; // 72MHz / (711) 1MHz1us计数一次 htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 999; // 计数1000次1us * 1000 1ms溢出 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_Base_Init(htim2); sClockSourceConfig.ClockSource TIM_CLOCKSOURCE_INTERNAL; HAL_TIM_ConfigClockSource(htim2, sClockSourceConfig); sMasterConfig.MasterOutputTrigger TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(htim2, sMasterConfig); HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0); HAL_NVIC_EnableIRQ(TIM2_IRQn); }预分频器写71而不是72是因为寄存器值在硬件上自动加1写入71实际是72分频。Period写999代表从0计数到999共1000次。如果把Prescaler误写成72实际分频变成73定时周期偏长约1.4%仿真里肉眼几乎无法察觉但接上虚拟示波器测累计误差就能发现。TIM2中断优先级设为0是抢占优先级最高的一档保证1ms节拍不被其他中断打断时间基准保持确定性。中断服务函数只负责调用HAL库入口实际逻辑全部放进回调函数void TIM2_IRQHandler(void) { HAL_TIM_IRQHandler(htim2); // HAL库内部清除中断标志并回调 } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { g_tick_ms; // 全局毫秒计数需用volatile修饰 } }g_tick_ms声明为volatile uint32_t原因是它在中断上下文写入、主循环上下文读取不加volatile时编译器可能把它优化进寄存器缓存主循环永远读不到新值。Cortex-M3上32位对齐读写在硬件上是原子的主循环读取不需要关中断保护。工程包里同时提供了stm32f1xx_hal_tim.c、stm32f1xx_hal_uart.c、stm32f1xx_hal_spi.c等一批HAL驱动文件本demo实际只用RCC、GPIO、TIM三组其余模块通过Keil工程里的宏定义裁剪不参与编译既减小HEX体积也缩短仿真加载时间。注意修改TIM2预分频或Period后要重新编译并重新加载HEXAutoReloadPreload使能时新参数要等下一次更新事件才生效容易误判为配置无效。3. 状态机驱动的信号灯切换逻辑3.1 四状态循环模型交通灯控制逻辑最适合用有限状态机描述四个状态、固定转移条件、无歧义动作任何时刻读current_state变量就能推出六个LED完整的电平组合。四状态定义如下状态东西方向南北方向持续时间转移条件ST_EW_GREEN绿灯红灯30秒计时耗尽ST_EW_YELLOW黄灯红灯3秒计时耗尽ST_NS_GREEN红灯绿灯20秒计时耗尽ST_NS_YELLOW红灯黄灯3秒计时耗尽因为每个方向同一时刻只有一盏灯点亮GPIO输出组合可以直接枚举状态定义里不存在两个方向同绿的空间。这是状态机方案和独立LED闪烁方案的本质区别后者如果两组灯各由独立定时器管理时序竞争时可能出现四个绿灯同时点亮的事故状态前者从状态空间的构造上就排除了这种可能。状态转移纯时间驱动没有外部条件时沿顺时针循环时长数据抽成常量数组后调整红绿灯配时只改数组元素状态机逻辑一行不用动。3.2 从GPIO初始化到状态输出函数PA0到PA2接东西方向PB0到PB2接南北方向初始化代码把六个引脚统一配置为推挽输出void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); /* GPIO时钟默认关闭必须先使能 */ __HAL_RCC_GPIOB_CLK_ENABLE(); /* 先全部输出低电平再配置模式防止上电瞬间LED误亮 */ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2, GPIO_PIN_RESET); GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; /* 推挽输出 */ GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; /* LED翻转频率低低速档足够 */ HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_GPIO_Init(GPIOB, GPIO_InitStruct); }三个引脚合并初始化是因为模式参数完全相同把写低操作放在配置模式之前是因为复位后的引脚是浮空输入态直接配置成推挽输出的瞬间电平不确定先写低再配模式可确保LED不会闪一下。SPEED_FREQ_LOW对应2MHz翻转速率LED最高每秒翻转几次用高速档除了徒增功耗和仿真计算量没有收益。PA0、PA1、PA2和PB0、PB1、PB2都不是调试端口不需要像使用PA13、PA14那样处理JTAG复用。状态输出函数将枚举值翻译成具体的引脚电平组合void Traffic_OutputState(TrafficState state) { /* 先关闭全部LED再点亮目标灯保证输出与状态定义严格一致 */ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2, GPIO_PIN_RESET); switch (state) { case ST_EW_GREEN: /* 东西绿 南北红 */ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_SET); break; case ST_EW_YELLOW: /* 东西黄 南北红 */ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_SET); break; case ST_NS_GREEN: /* 东西红 南北绿 */ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); break; case ST_NS_YELLOW: /* 东西红 南北黄 */ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); break; } }先全部清零再置位是防止仿真器单步停留在case内部时输出寄存器残留旧值。每个case执行完后PA/PB电平组合与枚举定义严格一致。这套写法给扩展留了余量以后加左转箭头灯只需新增枚举值和对应case全清零策略依然成立不需要重构。3.3 主循环中的时间累积与溢出处理有了1ms的g_tick_ms计数主循环不需要处理中断嵌套状态推进逻辑集中在Traffic_Update函数void Traffic_Update(void) { static uint32_t last_tick 0; uint32_t elapsed g_tick_ms - last_tick; /* 无符号减法正确处理回绕 */ state_counter elapsed; last_tick g_tick_ms; if (state_counter STATE_DURATION[current_state] * 1000UL) { /* 用减法而不是清零保留整数毫秒余量长期运行误差更小 */ state_counter - STATE_DURATION[current_state] * 1000UL; current_state (TrafficState)((current_state 1) % 4); Traffic_OutputState(current_state); } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); HAL_TIM_Base_Start_IT(htim2); current_state ST_EW_GREEN; state_counter 0; Traffic_OutputState(ST_EW_GREEN); while (1) { Traffic_Update(); } }elapsed用无符号减法计算g_tick_ms从0xFFFFFFFF回绕到0时结果依然正确这是嵌入式时间差值计算的标准写法比取模运算少一条除法指令。超时后state_counter用减法而非清零是因为中断回调可能在判断和清零语句之间又累加了几个毫秒清零会丢掉这部分时间让周期偏长减法保留余量长时间运行累计误差更小。整段逻辑没有任何阻塞延时主循环看似忙等实际CPU大部分时间处于WFI低功耗状态Proteus仿真速度稳定。若要移植到真实板卡这套代码可以直接烧录唯一要核对的是SystemClock_Config里的晶振频率和板卡实际值一致。4. Keil MDK-ARM工程搭建与Proteus联调4.1 工程文件结构里的关键文件解压项目后看到的stm32f1xx_hal_*.c系列文件是STM32F1 HAL库的外设驱动源码。stm32f1xx_hal_tim.c是定时器驱动本demo必需stm32f1xx_hal_uart.c和stm32f1xx_hal_usart.c是两套串口驱动前者是独立UART模块后者是USART模块本demo都未启用stm32f1xx_hal_i2c.c、stm32f1xx_hal_spi.c、stm32f1xx_hal_sd.c和stm32f1xx_hal_irda.c分别是I2C、SPI、SDIO和红外接口驱动交通灯场景同样用不到。新建自己的工程时只保留RCC、GPIO、TIM相关文件其余剔除。文件角色对照如下文件作用本demo是否参与编译DEMO.hexIntel HEX固件Proteus加载是DEMO.axfARM调试格式Keil单步调试时使用DEMO.asm汇编源文件含启动代码是stm32f1xx_hal_tim.c定时器驱动是stm32f1xx_hal_uart.c / hal_usart.cUART / USART驱动否stm32f1xx_hal_i2c.c / hal_spi.cI2C / SPI驱动否stm32f1xx_hal_sd.c / hal_irda.cSDIO / IrDA驱动否DEMO_sct.Bak分散加载文件备份一般不动编译前必须安装STM32F1系列Device Pack。新版Keil MDK-ARM通过Pack Installer在线获取STM32F1xx_DFP装完后工程设置里才能选中STM32F103R6。很多人问的Keil5兼容C51和STM32指的是在同一个Keil目录下同时集成C51和ARM两个编译器套件本工程只需ARM编译器不装C51也不影响。离线环境下Device Pack装不上时可以手动下载pack文件双击安装Keil会注册到全局目录。DEMO_sct.Bak这类分散加载备份文件只有改内存布局时才需要研究STM32F103的内部Flash从0x08000000开始的默认方案已经够用。4.2 生成HEX并加载到ProteusKeil工程默认不输出HEX文件必须打开Options for Target的Output选项卡勾选Create HEX File后重新编译。产物在工程目录的Objects或Listings子目录下确认DEMO.hex的时间戳是最新的避免加载旧固件导致仿真结果和源码对不上。习惯于命令行操作的话可以用UV4的批处理模式构建脚本通常长这样UV4.exe -b project.uvprojx -j0 -o build.log # -b 批处理构建-j0 让构建日志实时输出-o 指定日志文件批处理构建在批量验证多个工程时比较省事。编译日志出现0 Error、0 Warning后HEX文件就可以交给Proteus。在Proteus 8 Professional中双击STM32F103R6芯片Program File栏选择DEMO.hexClock Frequency填8MHz。这里填的是外部晶振频率而不是系统主频系统主频由固件里的PLL配置决定。提示Proteus芯片属性里的Clock Frequency改完后要重新加载一次HEX再运行模型不会自动把新时钟同步到已经加载的固件里。若原理图里放置的是STM32F103ZET6而工程编译目标是F103R6需要先在Proteus元件库确认是否添加了对应型号的模型。ZET6和R6的引脚封装、Flash大小都不同原理图和固件必须配套混用最常见的症状就是程序正常编译但仿真毫无反应。4.3 仿真异常的三个排查方向LED完全不亮时先看芯片属性Program File是否真的加载了HEX再看GPIO编号与原理图连线是否一致。PA0、PB0这些引脚在原理图上靠得很近画连线时串位是最高频错误。用Proteus的电压探针或虚拟万用表逐引脚量电平能快速判断是程序没跑还是连线错误。时序整体偏快或偏慢时排查时钟链路三个环节Proteus芯片属性的Clock Frequency、SystemClock_Config里的PLL配置、TIM2的Prescaler三者必须形成自洽的72MHz时钟链路。前面提到的把Clock Frequency误填成72MHz是时序偏快近9倍的典型案例。黄灯绿灯同时点亮时基本可以断定GPIO初始化遗漏了先清零步骤。若闪烁只在状态切换瞬间出现用虚拟示波器抓波形观察状态输出函数是否被连续调用产生了短脉冲这种毛刺在固定周期场景下肉眼发现不了但反映状态机存在重入问题仿真阶段修掉比带到板子上再查成本低得多。5. 紧急车辆优先模式与仿真验证技巧5.1 用外部中断一键接管信号灯真实路口需要为消防车、救护车提供优先通行。常见做法是紧急按钮接PA3配置成下降沿触发的外部中断按键按下时强制把当前状态切换到东西绿灯并重置计时void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin GPIO_PIN_3) { current_state ST_EW_GREEN; /* 强制东西方向放行 */ state_counter 0; Traffic_OutputState(ST_EW_GREEN); } }PA3在F103上默认复用为USART2_RX作为GPIO使用时把Mode设为GPIO_MODE_IT_FALLING即可不需要重映射因为EXTI3中断线对引脚复用不敏感。按钮另一端接地所以Pull字段要配置GPIO_PULLUP保证悬空时引脚电平稳定降低按键抖动误触发概率。回调里不需要手动清中断标志HAL_GPIO_IRQHandler进入回调前已处理。接管是临时抢占优先模式持续30秒后状态机会自然回到正常循环符合优先车辆通过后信号灯自动恢复的预期。5.2 用虚拟示波器验证时序正确性Proteus自带的虚拟示波器比肉眼观察LED可靠得多。把通道A接东西绿灯PA0通道B接南北绿灯PB0运行后读取波形电平宽度。PA0高电平应持续约30秒PA0和PB0上升沿间隔应为东西绿灯加黄灯的33秒。误差在100ms以内说明TIM2分频和状态机累积逻辑正确。想快速验证状态机边界把STATE_DURATION数组临时改成{3, 1, 2, 1}整个周期压缩到7秒示波器上很快能看到四个状态依次切换且无重叠点亮。压缩参数下跑30秒能覆盖十几个完整周期任何时序错乱都会立即暴露。确认无误后恢复数组原值重新编译生成HEX再次加载到Proteus完整跑一轮56秒周期确认恢复后的波形与初始版本行为一致把示波器读数记录存档这份数据就是后续移植到真实板卡的验收基准。本文还有配套的精品资源点击获取
返回列表