十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Proteus仿真STM32 PWM呼吸灯:从定时器配置到完整代码实现

Proteus仿真STM32 PWM呼吸灯:从定时器配置到完整代码实现 做STM32实验很多人第一件事就是买开发板。但如果你只是想验证PWM的逻辑或者手头暂时没有板子Proteus仿真完全是够了。我在Proteus里用STM32F103C8T6做了一枚呼吸灯LED从暗到亮、再从亮到暗速度平滑自然整个过程不碰真实硬件只需要Keil编译出一个HEX文件再在Proteus里搭一个最简单的电路就能跑起来。这篇就把从原理到代码再到排查踩坑的完整过程写出来给想用仿真快速验证PWM的读者做参考。1. 为什么要做呼吸灯它是理解PWM的最短路径1.1 一个实验同时打通定时器和GPIO现在回头看呼吸灯这个实验最大的价值不是“灯会呼吸”这个结果而是它逼着你把STM32的定时器、GPIO复用、PWM输出模式、系统时钟这几件事全部串起来。PWM是嵌入式里出镜率最高的外设功能之一电机调速、舵机控制、屏幕背光、LED调色底层全是同一套机制。你在STM32F103C8T6上把呼吸灯跑通后面面临的只是换引脚、换定时器、换占空比计算方式而已。STM32F103C8T6的定时器资源是TIM1到TIM4其中TIM1是高级定时器TIM2、TIM3、TIM4是通用定时器。随便拿一个通道输出PWM都可以。我用的是TIM2的通道1对应PA0引脚。原因很简单PA0在原理图上位置清楚接LED方便也方便用示波器探头去点。1.2 仿真能帮你完成什么不能帮你完成什么先说清楚边界免得你产生错误预期。Proteus仿真是功能仿真它模拟寄存器行为、引脚电平变化、外设逻辑速度上跟真实芯片并非完全一致。它适合验证代码逻辑对不对、PWM波形能不能出来、引脚配置是否正确、占空比变化是否符合预期。它不适合验证真实晶振起振稳定性、GPIO驱动能力极限、PWM信号在长导线上的完整性、MOS管开关损耗。比如有的新手在仿真里直接用PA0点一个大功率LED灯也能亮但换到真实板子上STM32 GPIO的输出电流根本拉不动LED也不够亮。所以我的态度是要学PWM逻辑仿真效率很高要学硬件设计仿真只能帮你完成其中一半。2. 软硬件准备Proteus元件、Keil工程和最小文件清单2.1 Proteus版本和元件搜索Proteus 8 Professional 8.9以上版本对STM32F103系列支持得比较好。打开Proteus后在Pick Devices面板搜索“STM32F103C8T6”直接就能找到这个芯片模型。版本太老的话可能搜不到最省事的办法是升级到新版本别花太多时间折腾元件库。其他会用到的元件也不复杂LED就搜LED-RED电阻搜RES电源和地直接用右侧工具条里的POWER、GROUND端。有些教程会在原理图里画晶振电路、复位电路、104电容如果你用的是Proteus自带的STM32F103C8T6模型这些外部电路不是必须的芯片模型内部已经把时钟和复位逻辑处理得比较理想化了。真正必须处理的反而是电源引脚和BOOT引脚这部分我在第4节会专门说。2.2 Keil MDK工程怎么建最省事软件上我用的是Keil MDK5。新建工程后Device选择“STMicroelectronics - STM32F1 Series - STM32F103C8”接着系统会弹出Manage Run-Time Environment窗口。对于本文这种只用一个main.c的实验不需要把整个标准外设库都加进来但需要保证三样东西齐了启动文件startup_stm32f10x_md.s必须存在Cortex-M3核心文件core_cm3.h和stm32f10x.h要能被include到。最稳妥的办法是直接找一个能编译通过的“STM32F103C8T6标准库空工程模板”把main.c换成我下面这份代码。在Options for Target - Output标签页里一定要勾选Create HEX File。不然Proteus那边双击芯片加载Program File时找不到hex整个项目就卡在这一步。2.3 准备期最容易踩的三个坑第一个坑勾选Create HEX File之后东西被编译到了Debug文件夹或Listings文件夹很多人找不到通常路径是工程目录下的Objects文件夹名字叫工程名.hex。双击芯片后直接去那个目录选文件就行。第二个坑系统时钟假设不一致。Proteus里双击STM32芯片后有一个Crystal Frequency设置项我这里说的是8MHz后面代码里也是按外部8MHz晶振倍频到72MHz的路径来写的。如果你把它设成72MHz很可能会导致SystemInit里的PLL配置对不上仿真表现异常。第三个坑头文件路径没配好。报错找不到stm32f10x.h时八成是Include Paths没加或者空工程模板本身不完整。建议别自己从零建工程直接拿一个验证过的模板改效率高很多。3. 从寄存器到波形PWM输出与呼吸效果的核心原理3.1 定时器怎么产生一个可调的方波PWM的底层逻辑其实很朴素定时器内部有一个自由计数的计数器CNT从0数到自动重装载值ARR然后归零重新开始。同时还有一个比较寄存器CCR。CNT小于CCR时引脚输出一种电平CNT大于等于CCR时引脚输出另一种电平这样引脚上就出现了占空比可调的方波。周期和占空比都有公式可以算。如果系统时钟是72MHz定时器预分频PSC设为71那么定时器计数频率就是72MHz除以72等于1MHz。ARR设为999计数器数完一轮需要1us乘以1000也就是1ms所以PWM频率是1kHz。CCR从0到999变化对应占空比0%到100%。对LED来说1kHz足够高肉眼看不出闪烁呼吸效果靠的是CCR周期性地从0增大到999再从999减小到0。3.2 呼吸感怎么来线性渐变会显得生硬很多人第一版呼吸灯代码是让CCR大步长地线性增、线性减。结果你会发现LED在变化的过程中亮灭两端的过渡很突然中间变化一直很平缓整体观感不像“呼吸”更像一个节拍器。原因在于人眼对暗部亮度的变化更敏感。你盯着一个LED看它从1%亮度变到2%亮度肉眼感知的“变化量”比从50%变到51%大得多。所以想让呼吸曲线舒服占空比不能线性走而应该按余弦规律走。我用的公式是duty (1 - cos(theta)) / 2theta从0均匀扫到2π时duty在0到1之间平滑往返。算成CCR值之后暗的时候变化缓慢中间亮区变化快一些整体就有了一种呼吸般的过渡感。在代码里我会预生成一张100点的正弦表主循环只需要查表更新CCR不需要做实时浮点运算。3.3 定时器通道和IO引脚的对应关系STM32F103C8T6的定时器输出通道对应关系是固定的这部分信息建议收藏备用定时器通道引脚TIM1CH1PA8TIM2CH1PA0TIM2CH2PA1TIM3CH1PA6TIM3CH2PA7TIM4CH1PB6TIM4CH2PB7我在这篇文章里选TIM2_CH1/PA0。如果你手头有其他工程模板改成TIM3_CH1/PA6也可以只要把GPIO配置、TIM选择、OC通道初始化改成对应的就行。4. 在Proteus里搭建STM32呼吸灯电路并加载HEX4.1 元件清单和必然要处理的引脚电路部分其实非常简单元件清单如下元件Proteus搜索名参数数量STM32F103C8T6STM32F103C8T6无1LEDLED-RED红色1电阻RES220欧姆或330欧姆1电源端POWERVCC若干接地端GROUNDGND若干电源引脚是仿真新手最容易出错的地方。Proteus里的STM32F103C8T6模型有很多电源引脚VDD_1和VDD_2要接到VCCVDDA也要接到VCCVREF接VCCVREF-接GNDVSS_1、VSS_2、VSSA全部接GND。这些引脚不接全芯片在仿真里不会正常工作表现就是点运行后一片死寂。NRST引脚建议用一个10k欧姆电阻上拉到VCCBOOT0和BOOT1都接GND这样最接近真实板子的上电状态。虽然Proteus模型对这些引脚要求不算严格但照着接不会错。4.2 连线步骤和网络标号技巧在原理图里先把STM32F103C8T6放在画布中央LED放在右侧。从PA0引出一根线接220欧姆电阻的一端电阻另一端接LED阳极LED阴极接GND。不要试图把VDD、VSS这些线一根根拉得很长那样原理图会乱成一团。直接放POWER和GROUND端然后双击引脚在Wire Label里把它们标成VCC、GND网络名同名网络在原理图上自动相连这是Proteus里最常用的方式。接线完成后在原理图空白处点鼠标可以检查所有连线。这个时候可以顺便看一眼LED的正负极Proteus的LED符号带有箭头标记箭头方向是电流方向别接反了。4.3 加载HEX文件并设置晶振双击原理图上的STM32F103C8T6芯片会弹出一个属性编辑窗口。在Program File那里选中Keil刚才生成的.hex文件然后把Crystal Frequency改成8MHz。这里我要再强调一次Crystal Frequency填8MHz不是72MHz。道理在第2节说过标准库的SystemInit会自己把8MHz倍频到72MHz仿真模型可以模拟这个过程前提是它认为外部晶振是8MHz。点左下角运行按钮如果一切正常LED应该开始缓慢地亮暗变化。第一次跑通的时候你会发现仿真速度可能和真实时间有点差异如果LED呼吸节奏明显偏快或偏慢优先检查代码里的PSC、ARR、SysTick延时参数而不是怀疑Proteus。5. 完整代码用标准外设库配置TIM2输出PWM呼吸效果5.1 可以直接抄的main.c下面这份代码是基于标准外设库写的也就是大家常说的StdPeriph_Lib。新建工程模板后把main.c内容全部替换成下面这段就行。#include stm32f10x.h #include math.h #define PWM_ARR_VALUE 999 #define PWM_PSC_VALUE 71 #define BREATH_POINTS 100 #define BREATH_STEP_MS 15 uint16_t breathTable[BREATH_POINTS]; void GenerateBreathTable(void) { uint16_t i; for (i 0; i BREATH_POINTS; i) { float theta 2.0f * 3.14159265f * i / BREATH_POINTS; float duty (1.0f - cosf(theta)) / 2.0f; breathTable[i] (uint16_t)(duty * PWM_ARR_VALUE); } } void TIM2_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period PWM_ARR_VALUE; TIM_TimeBaseStructure.TIM_Prescaler PWM_PSC_VALUE; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_Cmd(TIM2, ENABLE); } void delayMs(uint32_t ms) { uint32_t i; for (i 0; i ms; i) { SysTick-LOAD 72000 - 1; SysTick-VAL 0; SysTick-CTRL ~SysTick_CTRL_COUNTFLAG_Msk; SysTick-CTRL | SysTick_CTRL_ENABLE_Msk; while ((SysTick-CTRL SysTick_CTRL_COUNTFLAG_Msk) 0) { } SysTick-CTRL ~SysTick_CTRL_ENABLE_Msk; } } int main(void) { uint16_t index 0; GenerateBreathTable(); TIM2_PWM_Init(); while (1) { TIM_SetCompare1(TIM2, breathTable[index]); delayMs(BREATH_STEP_MS); index; if (index BREATH_POINTS) { index 0; } } }5.2 代码里每个关键配置为什么要这么写GPIOA和TIM2的时钟要分别打开因为GPIO挂在APB2总线上TIM2挂在APB1总线上。遗漏任何一个使能后面代码都跑不起来这是新手经常漏的。PA0配置成复用推挽输出也就是GPIO_Mode_AF_PP。定时器PWM输出不是普通拉高拉低电平它是把定时器内部产生的波形信号通过GPIO的复用功能送到引脚上所以模式必须是AF_PP不能用GPIO_Mode_Out_PP。TIM_TimeBaseInit里TIM_Period填999TIM_Prescaler填71得到的PWM频率就是1kHz。如果你想把PWM频率改成500Hz可以保持PSC71把ARR改成1999想保持频率不变但提高占空比分辨率可以降低PSC倍率和加大ARR但两者之间要保证乘积不变。这个参数关系理解了PWM基本就算入门了。PWM模式我选的是PWM1模式。在这个模式下CNT小于CCR时输出高电平CNT大于等于CCR时输出低电平。当你把CCR拉到最大值999LED就常亮CCR设为0LED就完全熄灭。主循环里没有阻塞很久的大延时每次只更新CCR然后等15ms。100个点乘15ms等于1.5秒所以一个呼吸周期大约1.5秒。想让呼吸变慢比如变成3秒一个周期直接把BREATH_STEP_MS改成30或者把点数改成200就行不需要动定时器配置。5.3 寄存器版本的核心遇到精简工程时能救命如果你嫌弃标准库工程文件太多或者手头是一个特别精简的模板用寄存器直接操作也可以完成同样的功能。核心就这几行RCC-APB2ENR | RCC_APB2ENR_IOPAEN; RCC-APB1ENR | RCC_APB1ENR_TIM2EN; GPIOA-CRL ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0); GPIOA-CRL | GPIO_CRL_MODE0 | GPIO_CRL_CNF0_1; TIM2-PSC 71; TIM2-ARR 999; TIM2-CCMR1 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; TIM2-CCER | TIM_CCER_CC1E; TIM2-CR1 | TIM_CR1_CEN; TIM2-CCR1 500;这段代码本质上是把标准库函数展开成了寄存器操作适合拿来对照理解标准库到底在做什么。我建议你至少手写一次寄存器版本之后再回到标准库你会突然明白TIM_OC1Init为什么要初始化那么多结构体成员。5.4 编译报错快速定位编译最常见的报错是找不到stm32f10x.h这种问题基本是工程模板不完整或者头文件路径没加。另一个常见报错是链接时提示SystemInit未定义说明工程里缺少system_stm32f10x.c文件补上就行。还有一种是cosf报错原因是缺math.h头文件。我代码里已经写了include math.h。如果PWM波形出不来先别怀疑PWM配置用排除法把CCR写死成500看LED是不是半亮状态。如果是说明定时器PWM工作正常问题出在呼吸表生成或主循环更新逻辑上。6. 仿真运行与示波器验证把看不见的PWM可视化6.1 用Proteus虚拟示波器观察PA0波形Proteus自带虚拟示波器这是比LED更可靠的调试仪器。在左侧工具箱里选Virtual Instruments再选OSCILLOSCOPE放到原理图空白处把通道A接到PA0上接地端接GND。运行仿真后打开示波器窗口你会看到一条不断变化宽度的方波波形这就是PWM。第一次看到波形时宽度变化会很直观从很窄慢慢变宽再慢慢变窄和LED亮度变化完全对应。如果波形一直是一条高电平直线大概率是配置错了引脚比如PWM输出到了PA1但示波器探到了PA0。如果是一条低电平直线优先检查PA0的IO模式看是不是配成了GPIO_Mode_Out_PP。6.2 常见现象排查表现象可能原因解决办法点击运行后完全没反应没有加载HEX文件或者Crystal Frequency设置不对双击芯片选中HEX晶振设为8MHzLED恒亮不呼吸CCR没在变或者PWM输出被当成普通GPIO输出检查TIM_SetCompare1是否在主循环被调用确认GPIO是复用推挽LED完全不亮LED接反或限流电阻太大换LED极性把电阻换成220欧姆示波器有波形但LED不亮LED串联电阻太大把电阻降到470欧姆以内呼吸速度明显不对系统时钟不是72MHz或SysTick延时单位算错检查工程里SystemInit是否执行PSC和LOAD按当前时钟重算还有一个现象值得单独说在Proteus里仿真这种轻量级电路遇到卡顿导致波形跳动是很正常的。可以在“System - Animation Options”里降低动画帧率仿真会更流畅波形刷新也更稳定。6.3 我建议你先看波形再盯LED说实话LED亮度变化很直观但调试时容易被主观感受误导。波形才是客观证据。我之前在调呼吸曲线那段时间每次改完参数第一件事都是打开示波器看波形有没有按预期展宽和收窄确认之后再看LED感受一下效果。这个顺序能帮你节省很多时间。7. 呼吸灯之后还能做什么PWM扩展方向与实物移植7.1 从单路到多路用PWM控制RGB调色呼吸灯跑通后PWM的扩展试验可以立刻做起来。STM32F103C8T6上TIM2有CH1到CH4共四个通道PA0到PA3四个引脚都能输出PWM。搭一个RGB三色LED三种颜色各用一路PWM每个颜色单独维护一张呼吸表就能调出各种渐变色。代码结构完全一样只是要多初始化几个GPIO和OC通道。7.2 用两个按键控制呼吸速度在Proteus里放两个按键接到PA4和PA5配置成输入模式按下时修改BREATH_STEP_MS或者修改主循环的跳变步长就能实现按键调速。这个改动不需要动PWM初始化只改主循环里的延时和索引递增逻辑。这个实验很适合进一步体会“外设配置一次后续只改数据”这个嵌入式开发思路。7.3 从仿真到实物参数要重新核对的地方Proteus验证通过后你可能会想买一块真实的最小系统板把它跑起来。代码逻辑基本不用改但有几个地方要重新确认。第一是外部晶振STM32F103C8T6最小系统板一般板载8MHz晶振Proteus里我让你填8MHz实物板默认也是8MHz所以SystemInit这段可以通用。第二是LED限流电阻真实STM32GPIO高电平约3.3V红色LED压降约1.8V220欧姆电阻给到的电流大约是(3.3-1.8)/2206.8mA亮度合适这个计算值在Proteus里不会体现但实物必须按真实电流去选电阻。第三是如果用国产兼容MCU比如GD32F103主频上限可能不同PSC和ARR参数建议按实际主频重新算一遍别直接照搬。7.4 关于仿真和实物我一直坚持的看法用Proteus做仿真的最大收益是我能把“改参数”和“看现象”之间的距离缩到最短。改一次PSC立刻看LED亮度变化改一次呼吸表点数立刻看呼吸节奏变化。这种即时反馈对建立直觉特别有帮助。但仿真永远替代不了实物尤其是涉及功率驱动、电机、LED亮度校准这些场景最终还是得在真实电路上验证。建议你把Proteus当成一个低成本实验台先在软件里把逻辑调顺再带着明确的参数去碰硬件这样的学习路径会顺畅很多。我自己的经验是仿真阶段多花十分钟把PWM波形和呼吸表搞清楚后面做实物时基本一次点亮。PWM这个知识看着简单但它贯穿了整个嵌入式入门前半段。呼吸灯只是把门推开的那道缝真正推开之后里面有太多值得折腾的东西。
返回列表