STM32 PWM仿真入门:基于Proteus的零成本调试与波形验证指南
简介压缩包内包含一套完整的Proteus STM32 PWM输出仿真工程配套Keil5编写的定时器3 PWM输出代码适合正在学习STM32定时器、PWM原理及Proteus仿真的嵌入式初学者和相关实训课程。整个压缩包共132个文件大小仅2.12MB以C源码、H头文件、编译中间文件、Keil工程文件、hex/axf可执行文件及Proteus仿真工程文件为主结构清晰源文件便于学习编程逻辑工程文件可直接打开运行仿真工程方便复现实验同时包含调试生成的map/lst文件便于分析编译链接结果。资源对应实训八“PWM输出”已吸引2239人学习下载。借助这套工程读者可以深入理解定时器的工作模式、预分频器与自动装载值配置、比较通道选择、占空比与极性调整方法并通过编写PWM输出函数改变LED亮度在Proteus中连接STM32与RGB LED观察颜色变换同时学习利用Proteus的调试工具排查问题是一份兼顾原理、编程与虚拟调试的实用资料。1. 为什么要在Proteus里折腾STM32的PWM仿真很多刚接触STM32的朋友上来就想用开发板跑PWM但现实往往是这样板子还在快递路上Keil装了一半报错手里连个示波器都没有却急着要验证定时器PWM输出的逻辑。这种时候Proteus就是最顺手的工具。它可以说是少数能比较接近真实地仿真STM32外设的软件对PWM这类输出型外设尤其友好。我最早用Proteus做仿真是因为一个课程设计要在STM32F103C8T6上输出可调占空比的PWM来控制电机转速。那时候手头没有电机驱动板也没有逻辑分析仪但我需要先确认代码逻辑和引脚配置对不对。Proteus里放着虚拟示波器和可调占空比的参数改一次代码重新编译点运行就能看波形比反复插拔烧录器要舒服得多。仿真做完硬件一到货烧录后基本一遍过。但我也得先泼一盆冷水Proteus的仿真能力有明确的边界。它能验证寄存器配置、GPIO模式、定时器初始化这些软件逻辑层面是否正确能帮你看到波形形状和占空比变化但模拟信号的边沿质量、真实器件电气特性、外部干扰抗性它在仿真里体现不出来。仿真通过不等于硬件能跑仿真里跑不出来的问题真实电路上可能一上电就冒烟。我的建议是把Proteus当作第一道检查关卡用来快速验证逻辑接着再用开发板做第二道验证。1.1 仿真最大的价值零成本验证代码逻辑PWM输出的核心逻辑其实就三件事定时器时钟分频算好、GPIO引脚复用对好、输出比较寄存器配置正确。这三件事如果任何一个出错烧到板子上表现可能只是没波形或者波形频率不对但你排查起来需要示波器、万用表新手往往无从下手。在Proteus里就不一样了它的虚拟示波器可以挂在任意引脚上波形图直接显示在屏幕上还能自由调整时间轴读取周期和频率。比如你想确认TIM2_CH1输出在PA0上就把示波器探针拖到PA0引脚跑起来立刻看到波形如果有问题绝大多数情况下是代码配置问题改完重新加载HEX文件再跑一遍就行。1.2 谁适合用、谁不适合用适合用Proteus的人还没有开发板的初学者、需要快速做方案验证的工程师、做课程设计需要截图交付的学生、想在教学演示中展示PWM原理的讲师。不适合用的人项目已经进入硬件调试阶段、需要精确测量波形边沿时序、需要验证死区时间或互补PWM驱动H桥的人。如果你处理的是电机驱动的真实项目我劝你还是老老实实买板子、搞个示波器。Proteus里的虚拟示波器虽然是好东西但它的显示刷新率和波形绘制精度跟在实体示波器上看到的完全不是一个量级。2. 环境准备从Proteus安装到Keil联调配置这部分是很多人卡住的地方。本身Stm32在Proteus里的支持就比51单片机晚很多年芯片库、Keil编译生成HEX文件的路径、芯片时钟设置任何一个环节出错后面全白搭。2.1 Proteus版本选择和STM32芯片库先说版本。Proteus 7时代主要支持的是51系列单片机如果你想用STM32F103C8T6老老实实用Proteus 8.6以上的版本。8.9、8.12、8.13这些版本我都试过对STM32的仿真稳定性差别不大挑一个你熟悉的使用即可。新版本里怎么找芯片在元件模式下点击Pick Device搜索框输入STM32F103C8选中STM32F103C8T6即可放置到画布上。这里有一个常见问题如果你的是老版本Proteus可能搜不到STM32F103C8我就遇到过升级前的版本只有STM32F103R6等少数型号。解决办法很简单更新到8.9以上版本或者检查你的自动更新是否有运行库缺失。提示网上搜proteus 许可文件下载能解决授权报错问题但请注意来源可靠性。我个人的经验是安装完成后用管理员身份运行首次启动如果提示缺少VSM文件夹或元件库多半是安装路径有中文或者安装时关闭了杀毒软件重新安装一次就能解决。2.2 在Proteus里新建工程并加载Hex文件新建工程时选择New Project画好原理图后双击STM32F103C8T6芯片弹出的属性对话框里有个Program File项点击后面的文件夹图标选择Keil编译生成的HEX文件。这里有个细节Proteus加载HEX文件后不会立即报错但如果文件路径错了仿真运行时会提示无法找到程序文件。我建议把HEX文件跟工程放在同一个目录下用相对路径比较省心。芯片属性里还有一个非常关键的参数Clock Frequency。STM32F103C8T6外部晶振通常接8MHz芯片经过PLL倍频到72MHz。在Proteus的STM32模型里这个Clock Frequency默认值往往是8MHz。如果你在代码里按72MHz去计算定时器分频但模型时钟还是8MHz出来的PWM频率会整整差9倍。这个坑我至少见过十个人踩过后面单独讲。2.3 Keil工程配置生成HEX文件的开关Keil 5的环境搭建这里不展开说几个跟Proteus联调相关的配置项。新建STM32工程时选设备型号比如STM32F103C8。如果你用标准外设库就是老项目常说的STM32F10x标准固件库在工程配置窗口里要确认三件事C/C选项卡里的Define加上USE_STDPERIPH_DRIVER否则编译报找不到外设库头文件。Output选项卡里勾选Create HEX File这是Proteus加载程序的前提忘了勾选编译后只有AXF文件Proteus不认。在Debug选项卡里如果你想用Proteus的联合调试功能可以选择Proteus VSM Simulator但我个人不太推荐在Proteus里单步调试STM32定时器容易让仿真速度变慢不如先用串口或虚拟示波器看结果。编译通过后工程目录下会出现一个带HEX扩展名的文件把这个文件路径填进Proteus芯片属性里的Program File即可。3. 电路搭建一个能出波形的PWM最小系统有人会觉得仿真里画原理图很随意随便拖几个元件连上就能跑。实际上Proteus的STM32模型对电路连接还是比较讲究的电源、地、复位电路缺了它照样给你跑但波形结果可能失真。你自己搭的时候还是按真实电路的标准来。3.1 元器件清单我提供一个最简的PWM输出验证电路你照着摆放就行元件型号/参数数量作用主控芯片STM32F103C8T61产生PWM发光二极管LED-RED1直观显示占空比变化电阻220Ω~1kΩ1限制LED电流晶振8MHz1提供外部时钟源电容20pF2晶振负载电容电容100nF2电源去耦复位电阻10kΩ1复位电路上拉复位电容100nF1复位延时虚拟示波器OSCILLOSCOPE1观察PWM波形在Proteus左侧工具栏里用元件模式搜索STM32F103C8T6放置芯片搜索LED和RES放置LED与电阻晶振和电容在元件库中直接搜CRYSTAL、CAP。3.2 电源和复位电路别偷懒很多Proteus教程里只画芯片和LED电源地随便放个GROUND端子就不管了。这种图虽然能仿真但不利于你养成好习惯。STM32F103C8T6在Proteus模型里标称3.3V供电你从工具栏的Terminal Mode里拖一个POWER端子双击把电压改成3.3V接到芯片VDD引脚再用一个GROUND端子接VSS。复位电路我建议画上NRST引脚接一个10kΩ电阻到3.3V再接一个100nF电容到地。这个电路在仿真里可能完全无关紧要但复制到真实板子上就是可用的养成习惯很重要。晶振电路同样画上在OSC_IN和OSC_OUT之间接8MHz晶振两个引脚各接一个20pF电容到地。如果你用的是芯片内部RC振荡器也可以不画外部晶振但代码里RCC配置必须改成内部时钟源。我见过有人在仿真里画了外部晶振代码却配置HSI结果定时器跑出来的PWM频率跟理论计算差一大截。3.3 虚拟示波器的接法在Proteus左侧工具栏选择Virtual Instruments Mode面板里有个OSCILLOSCOPE点击放置到画布上。双击它可以打开示波器窗口显示A、B、C、D四个输入通道。把A通道的探针接到你期望输出PWM的引脚上比如PA0把探针的地线接到GND网络。接好之后先跑一次程序然后打开示波器窗口。如果波形不显示检查一下示波器面板上A通道的电压量程默认可能是1V每格如果PWM高电平是3.3V把量程调到5V或2V会更清晰。水平时间轴根据PWM频率调整PWM是500Hz时周期2ms水平量程设到2ms每格左右能看清完整波形。4. 代码配置定时器PWM输出的关键细节代码是整个仿真的灵魂。STM32的PWM输出配置并不难但里面有几个细节决定了你的波形能不能出来、占空比对不对。4.1 选TIM1还是TIM2、TIM3STM32F103C8T6这枚芯片集成的定时器资源包括TIM1高级定时器、TIM2、TIM3、TIM4通用定时器TIM6和TIM7也有但只能做基本定时没有PWM输出通道。不同定时器对应的引脚重新映射表要特别留意TIM1_CH1默认映射在PA8如果你用TIM1输出PWM记得初始化GPIOA。TIM2_CH1默认映射在PA0TIM2_CH2是PA1以此类推。TIM3_CH1映射在PA6TIM3_CH2是PA7等等。实际项目里怎么选如果你只要单路PWMTIM2的PA0输出最顺手因为它是初学者常用的通道。如果你需要互补PWM或带死区控制的信号比如驱动半桥必须用TIM1它支持高级PWM功能。如果你需要同时输出多路PWM可以用TIM2的全部四个通道也可以TIM2和TIM3一起用注意引脚不要冲突。4.2 标准外设库配置PWM的完整代码我用标准外设库Standard Peripheral Library给你一段可以直接复制的代码功能是TIM2_CH1在PA0上输出500Hz、25%占空比的PWM#include stm32f10x.h void TIM2_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, 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 1999; // ARR TIM_TimeBaseStructure.TIM_Prescaler 71; // PSC TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; 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 500; // CCR1 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM2, ENABLE); TIM_Cmd(TIM2, ENABLE); } int main(void) { TIM2_PWM_Init(); while (1) { } }代码逻辑拆开看先把GPIOA的Pin0设置为复用推挽输出这是PWM输出必须的GPIO模式然后配置TIM2的时基分频器PSC设71自动重装值ARR设1999最后配置输出比较模式为PWM1捕获比较寄存器CCR1设500。4.3 频率和占空比的计算方式PWM频率的计算公式是PWM频率 定时器时钟 / (PSC 1) / (ARR 1)STM32F103C8的APB1定时器时钟是72MHz代入公式得到频率72MHz / 72 / 2000 500Hz占空比 CCR / (ARR 1)代入得到占空比500 / 2000 25%这个计算方式在所有STM32系列里通用区别只在定时器时钟来源不同。比如如果你用的是TIM1它是挂在APB2上的时钟同样是72MHz但如果芯片型号不同或时钟树配置不同PSC和ARR算出来的频率就不一样。提示修改Pulse的值可以动态调整占空比。但是务必注意直接给TIM_SetCompare1(TIM2, newCCR)是可以的前提是你在初始化时开启了预装载功能。如果你没开TIM_OC1PreloadConfig寄存器值会立刻生效在高速PWM场景下可能造成波形一个周期内跳变这在真实电机驱动里表现为噪声。仿真里未必看得出来但最好养成开预装载的习惯。4.4 使用TIM1时必须多一行配置如果你改用TIM1输出PWM比如PA8输出PWM一定要记住TIM1是高级定时器它的PWM输出还有一个主输出开关要打开TIM_CtrlPWMOutputs(TIM1, ENABLE);这行代码必须加在TIM_Cmd(TIM1, ENABLE);后面或前面都行但不能省略。很多人用TIM1配置妥妥当当就是没波形就是漏了这一行。TIM_CtrlPWMOutputs对应的是MOE主输出使能位STM32的高级定时器为了安全性复位后这个位是关闭的故意让你显式打开。使用通用定时器TIM2~TIM4时不需要这行配置这也是TIm1跟通用定时器在初始化上最大的区别。5. 仿真运行波形验证与常见坑代码编译通过、HEX加载进Proteus之后点左下角的运行按钮虚拟示波器就应该能出波形了。但实际过程中总会有意外。5.1 运行后一直没波形怎么排查从我用Proteus处理各种报错的经验来看没波形的排查顺序应该是这样的先双击芯片确认Program File路径下确实加载了HEX文件。如果文件没加载芯片不会执行任何代码。检查芯片的Clock Frequency。把8MHz改成72MHz试一下如果波形出来了但频率不对多半是这里的时钟匹配问题。检查示波器接线。探针是否确实连在PA0上地线是否接GND示波器窗口的通道是否选对A通道还是C通道。检查GPIO配置。引脚号对不上是最常见的比如你用TIM3_CH1输出但代码里把GPIO初始化成了Pin0而示波器挂在PA6上自然看不到波形。检查RCC时钟使能。遗漏RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE)时定时器外设时钟没打开整个外设不运转。检查代码里是否有死循环前被某句初始化卡住比如用了while等待某个标志而这个标志在仿真中永远不会置位芯片等于悬空。还有一个容易被忽略的问题Proteus的实时仿真性能问题。如果你画了特别复杂的电路或者同时打开了多个虚拟仪器仿真可能跑得很慢波形看起来纹丝不动。这个时候点一下暂停再运行或者把示波器窗口关掉重新打开往往能恢复正常。如果以上都查了还是没波形建议删掉芯片重新放置一个新的STM32F103C8T6重新加载HEX文件。Proteus偶尔会因为工程文件损坏导致芯片模型工作异常重放一次芯片能解决90%以上的玄学问题。5.2 占空比和理论值对不上波形出来了但测出来的占空比跟代码算的对不上这种情况我从经验里总结出两个高概率原因一是Timer Clock设置和代码预期不一致。比如代码是按72MHz算的但Proteus芯片Clock Frequency还是8MHz实际频率变成8/72倍但占空比受影响不大除非PSC分频后计数器数值溢出。二是示波器自动读数不准。虚拟示波器窗口上显示的频率、Vpp是自动测量值有时候对于边沿不够陡峭的信号读数会偏。你可以把时间轴放大手动读一格时间对应的波形周期。如果你改了CCR值但波形占空比没变检查一下TIM_OC1PreloadConfig是否开启以及库里是否还会在循环里重新初始化TIM2。有人为了调参方便在while循环里重复调用初始化函数结果之前设置的CCR被重置了。5.3 用LED和虚拟电机验证占空比变化占空比通过示波器看最准但为了让效果更直观我建议在PA0引脚上接一个LED串联电阻到地。这样占空比从0%到100%变化时LED亮度会有明显变化。PWM频率不要太低否则LED会闪烁500Hz以上人眼基本察觉不到闪烁100Hz以下会明显看到闪烁这是验证频率是否正确的直观方法。如果你想仿真PWM驱动直流电机Proteus元件库里有个MOTOR-DC也可以搜DC Motor。用PWM输出接到电机驱动芯片的使能端或输入端占空比越大电机转速越快。我之前把PWM接到L298驱动芯片的ENA引脚仿真里能看到电机转速数字变化这个结果虽然跟真实电机有差距但用来讲清楚PWM调速原理完全够了。6. 从PWM仿真延伸出去的进阶玩法一个PWM仿真跑通之后你别急着关掉Proteus顺着这条路径还能做不少延伸实验。很多人在热搜里搜pwm触发adc采样、autosar mcu开发这些话题都离不开PWM这个基础机制。6.1 用PWM控制LED亮度手把手调参在4.2节的代码基础上你可以写一个简单的循环让占空比每隔一段时间递增一次int main(void) { uint16_t ccr 0; TIM2_PWM_Init(); while (1) { for (ccr 0; ccr 1999; ccr 10) { TIM_SetCompare1(TIM2, ccr); for (volatile uint32_t i 0; i 100000; i); } for (ccr 1999; ccr 0; ccr - 10) { TIM_SetCompare1(TIM2, ccr); for (volatile uint32_t i 0; i 100000; i); } } }这段代码的运行效果是LED从灭到亮再到灭用虚拟示波器挂在PA0上可以看到占空比在周期性变化。这个实验特别适合理解CCR寄存器如何影响占空比也方便你在Proteus里截图展示调光效果。6.2 PWM触发ADC采样仿真里的联动玩法“PWM触发ADC采样”在真实项目里很常见比如逆变电源里需要定时采集电流值采样时刻必须与PWM周期同步。在Proteus里可以这样搭用STM32的TIM1某个通道输出PWM同时配置定时器的触发输出TRGO连接到ADC的启动触发源。代码上需要在ADC初始化里设置外部触发模式比如ADC_ExternalTrigConv_T1_CC1表示TIM1捕获比较事件触发ADC转换。这个联动的仿真验证在Proteus里可以实现过程稍微麻烦一点但跑通了你会对外设之间如何通过触发信号协作有更深的理解。仿真里可以放一个模拟电压源接到ADC输入引脚PWM触发ADC采样后将采样结果通过虚拟终端或数组观察。6.3 和CubeMX、固件库模板怎么配合现在的团队开发里用STM32CubeMX生成初始化代码已经是主流标准外设库虽然老但网上教程和例程数量依然庞大。Proteus仿真跟这两条路线都能配合用CubeMX生成工程时选择STM32F103C8芯片在TIM2配置里开启PWM Generation CH1设置Prescaler和Counter Period主频选择72MHz。生成的HAL库代码里核心调用是HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1)。用标准外设库的话就是我前面给的配置方式适合想理解寄存器细节的初学者。我个人的建议是做项目懂HALMX就够用但如果你想彻底搞懂PWM配置背后的原理至少跑一遍标准库代码把ARR、PSC、CCR三个值自己动手算一遍。仿真环境最大的魅力在于试错成本几乎为零你可以随便改参数看波形变化这是看书和看视频完全给不了的体验。回到我自己的仿真习惯每建一个新工程先在Proteus里跑通一个最小PWM波形再往上加LED、电机、ADC这些外部设备。波形出来了外围电路才可能正确波形出不来后面全白搭。这个习惯帮我排掉了很多低级配置错误希望你也能少走弯路。本文还有配套的精品资源点击获取