如果你也在折腾Proteus仿真STM32那我猜你大概率体会过那种“代码好像没错、电路好像也接对了但仿真就是不动”的煎熬。这篇文章就用一个非常经典的例子——通过STM32F103C8T6的定时器输出PWM控制直流电机的转速把从仿真电路搭建、PWM参数计算到Keil代码编写和Proteus联调的整套流程完整捋一遍。适合刚接触STM32定时器PWM、想用Proteus做电机控制仿真或者正在做小车、风机、水泵等课程设计的朋友参考。仿真和实物最大的区别在于Proteus里允许你反复犯错而实物电路一次接错可能就冒烟了。所以我建议所有电机控制类项目都先把这个仿真流程跑通再考虑买器件动手焊板子。下面直接进入正题先把整个项目的设计和选型思路讲清楚。1. 项目整体设计与方案选型为什么用Proteus仿真直流电机调速1.1 这个项目到底在解决什么问题直流电机调速是电机控制里最基础也最常用的场景。机器人小车、电风扇、水泵、传送带、电动窗帘本质上都是同一个逻辑给电机一个合适的电压让它按你想要的速度转起来。控制电压最优雅的方式不是调一个可调电阻而是用PWM也就是脉冲宽度调制通过改变高电平占空比来改变电机两端的平均电压从而控制转速。这个项目选定了一个很典型的控制需求两个按键调速一个按键换向。按键1增加占空比电机加速按键2减小占空比电机减速按键3切换电机正反转。整套逻辑在真实小车上非常常见放到Proteus里做仿真核心目的是验证三件事第一STM32定时器能不能稳定输出频率和占空比都可调的PWM波形第二PWM信号送到电机驱动芯片后电机的转速是否真的跟随占空比变化第三方向控制引脚的逻辑组合是否正常。用仿真先跑通这套流程后面换成真实硬件时你只需要关注驱动电路的电流、散热和电磁干扰逻辑层面的问题已经被提前过滤掉了。1.2 为什么选STM32F103C8T6而不是51或者Arduino很多人第一次做PWM调速会用51单片机。51当然能做但它的定时器PWM通常要靠定时中断里翻转IO来实现占空比调节、频率切换、多通道输出都很别扭精度也一般。Arduino用analogWrite虽然方便但很多学校课程、企业预研项目对底层寄存器的掌握有要求而且Arduino的生态和STM32的工程化程度还是有差距。STM32F103C8T6是Cortex-M3内核主频72MHz64KB Flash20KB SRAM。它最大的优势是定时器外设丰富通用定时器TIM2、TIM3、TIM4都自带硬件PWM输出功能。你要做的事情只是配置好几个寄存器定时器硬件就会自动把PWM波形输出到指定引脚全程不占用CPU主循环还可以同时处理按键扫描、通信、显示等任务。而且这颗芯片的国产替代方案非常多市面上几块钱就能买到最小系统板资料和例程铺天盖地Proteus 8.x也直接支持它的仿真模型。对学习者来说它是平衡复杂度、成本和资料丰富度的最佳选择。1.3 仿真和实物之间的“信任边界”用Proteus做电机仿真有一个很重要的认知要提前建立仿真不能百分百还原实物但能把绝大多数逻辑错误拦截住。可以相信仿真结果的部分包括定时器配置是否正确、PWM波形频率和占空比是否符合预期、GPIO引脚映射和复用关系是否正确、按键扫描和方向控制逻辑是否合理、占空比变化后电机转速的变化趋势是否正确。这些都是数字逻辑层面的东西仿真和实物基本一致。不能相信的部分包括电机的绝对转速值、启动电流和堵转电流、反电动势对电路的影响、驱动芯片的真实发热量、PWM高频开关带来的电磁干扰。Proteus里的直流电机模型是理想化的如果你在仿真里看到转速和占空比严格线性那是模型简化后的结果真实电机有摩擦、死区、非线性转速不会那么听话。明白这条边界之后你就知道仿真的定位是什么了它帮你验证“逻辑和配置”剩下的“硬件调校”还得回到实物上。所以我做项目的习惯是仿真必跑但不迷信仿真结果。2. Proteus电路搭建与器件选型先让硬件“跑”起来2.1 在Proteus里搭建STM32F103C8T6最小系统打开Proteus 8.x在元件搜索框输入“STM32F103C8”就能找到这颗芯片的仿真模型。有些版本显示为“STM32F103C8T6”直接放置即可。先说明一点Proteus里的STM32模型和实物一样引脚分布、复用功能、GPIO配置都是按数据手册来的所以电路图不能乱画PA6就是PA6不能当普通IO到处飞线。把芯片放到画布后第一步是配置电源。Proteus的STM32模型内部有隐藏电源引脚通常VDD和VSS已经隐含连接但部分版本还是需要你在原理图上显式放置3.3V电源到VDDA、VSSA等引脚。稳妥的做法是点击芯片在属性里确认Power Pin Visibility确保芯片供电正常。第二步是配置晶振。STM32F103C8T6内部虽然有HSI内部时钟但标准库的SystemInit默认使用外部8MHz晶振然后倍频到72MHz。所以必须在OSC_IN和OSC_OUT两个引脚之间放一个8MHz晶振或者直接在芯片属性里的“Crystal Frequency”填8MHz。这一步经常有人漏掉最后导致延时函数、PWM频率全部对不上。第三步双击STM32芯片在弹出的属性对话框里找到Program File这里要填入Keil编译生成的hex文件路径。很多人到了这一步不知道怎么填其实只要在Keil工程输出目录下找到.hex文件选中它就行。别忘了同时检查Crystal Frequency选项默认可能是4MHz或者0必须改成8MHz。2.2 直流电机模型与驱动芯片接线Proteus元件库里搜索“MOTOR-DC”可以找到直流电机模型注意它的引脚符号通常标注为SA、SB、SC。SA和SB就是电机两端SC是测速输出脚部分版本里也叫Tacho。这个SC脚可以输出一个频率随转速变化的脉冲信号后续如果你想做测速闭环它可以作为速度反馈源。电机不能直接挂在STM32的GPIO上。STM32的GPIO输出电流只有几毫安到二十毫安级别而直流电机启动电流动辄几百毫安甚至安培级而且电机是感性负载断电瞬间会产生反向电动势直接接GPIO轻则电机不转重则烧芯片。所以中间必须加驱动电路。本文选用L298作为H桥驱动芯片。L298是双H桥单路最大驱动电流2A内部自带续流二极管非常适合用来驱动直流电机。在Proteus里搜索L298放置后接线如下表所示L298引脚连接到作用IN1 / IN2STM32的PB0 / PB1方向控制信号ENASTM32的PA6PWM使能与调速OUT1 / OUT2电机SA / SB输出驱动电机VS电机电源正极仿真用12V电机供电VSS5V电源逻辑电路供电GNDSTM32的GND共地必须连请注意L298的VS接的是电机电源VSS接的是逻辑电源两者电压范围不同不能混接。仿真里VS用12VVSS用5VSTM32这边用3.3V供电GND全部连在一起。如果你不想用L298也可以用L293D或者用两个NPN三极管搭简易驱动但L298的H桥特性更接近实物小车常用的TB6612或DRV8833方案行为参考价值更高所以本文按L298来写。2.3 调试工具示波器是仿真的眼睛仿真和实物调试有一个共同点你得先看到信号才能判断问题出在哪儿。Proteus左侧工具栏里有Virtual Instruments点开后能找到OSCILLOSCOPE示波器。把示波器的A通道接在PA6上就能直接看到PWM波形把B通道接在L298的OUT1上可以看到经过H桥后的电机驱动波形。示波器一定要在联调之前就接好。否则你只看到电机转但不知道PWM频率对不对、占空比有没有变化一旦电机不动你连是软件问题还是硬件问题都分不清。按键电路就比较简单了三个轻触按钮分别接PA0、PA1、PA2另一端接GND同时PA0到PA2在代码里配置为内部上拉输入。按键按下时引脚读到低电平松开时读回高电平。这里要注意Proteus的按键模型不需要外接上拉电阻因为代码里开了内部上拉如果你用的是外部按键模块就需要在原理图上额外放上拉电阻。3. PWM调速核心原理与定时器配置从波形到代码3.1 PWM调速的本质斩波与平均电压PWM调速的原理说白了就是“快速开关电源”。你以固定的频率把电机电源接通再断开接通时间占整个周期的比例叫占空比D。电机是感性负载加机械惯性它不会因为电源断了几微秒就立刻停下来而是会平滑地“记住”平均电压。平均电压就是 U_avg U × DU是供电电压D是0到1之间的占空比。用生活里的例子类比你在房间里快速开关灯人眼不会看到闪烁只会觉得房间光线的亮度变了。开灯时间比例越高房间越亮电机也是同理占空比越高平均电压越高转速越快。那PWM频率选多少合适频率太低比如几十Hz电机会发出明显的“嗡嗡”声转速也会一顿一顿的这就是在听觉上感知到了开关过程频率太高比如几十kHz开关损耗变大驱动芯片发热严重。直流电机常见的PWM频率范围是1kHz到20kHz本文选1kHz方便示波器观察也符合Proteus仿真模型的参数。实物调试时如果你嫌吵可以提到10kHz以上。3.2 STM32定时器为什么能输出PWMCNT、ARR、CCR的关系STM32芯片的输出PWM功能核心在三个寄存器上CNT计数器、ARR自动重装值、CCR捕获比较寄存器。理解这三个寄存器的关系整个PWM原理就通了一半。先说CNT。它就像一个秒表在时钟脉冲驱动下从0开始向上计数每来一个时钟脉冲就加1。ARR是闹钟的目标值当CNT数到ARR时计数器清零重新开始下一轮计数。每“从0数到ARR”的这段周期就是PWM的一个周期。CCR则像一个比较器。在每个计数周期里硬件会不停地比较CNT和CCR的大小。在PWM模式1下当CNT小于CCR时输出引脚为高电平当CNT大于等于CCR时输出引脚为低电平。到了下个周期CNT清0又重新输出高电平。整个过程完全由定时器硬件完成CPU不需要干预。所以你只需要设置三个东西计数时钟频率决定每计数一次花多少时间ARR值决定PWM周期CCR值决定高电平持续多长时间也就是占空比。CCR改变时PWM波形的高电平宽度会立刻变化电机转速就跟着变了。3.3 关键参数计算72MHz下配出1kHz PWMSTM32F103C8T6系统主频72MHzTIM3是挂在APB1总线上的通用定时器。这里有个很关键的细节APB1预分频如果设为2那么APB1外设时钟是36MHz但定时器的时钟不是36MHz而是36MHz的2倍也就是72MHz。这是新手最容易算错的地方代码里配置的系统时钟经过倍频后APB1分频为2TIM3的输入时钟就是72MHz。我们目标是PWM频率1kHz占空比分辨率要精细。公式是PWM频率 TIM时钟 / ((PSC 1) × (ARR 1))先设预分频PSC71这样计数时钟变成 72MHz / (71 1) 1MHz也就是1微秒计数一次。这个选择很干净方便心算。再设ARR999则PWM周期 (999 1) × 1us 1000us 1ms频率正好1kHz。占空比 CCR / (ARR 1)因为ARR999所以CCR的范围是0到999占空比从0%到99.9%调节步进细腻大约0.1%一级。参数数值说明TIM时钟72MHzAPB1预分频2时定时器时钟为72MHzPSC71预分频后计数频率1MHzARR999计数周期1000usCCR0~999占空比0%~99.9%PWM频率1kHz满足L298和直流电机要求如果你想把频率改成10kHz只要把ARR改成99即可但占空比分辨率会降到约1%这是一个典型的“频率越高调节精度越粗”的取舍实际项目中要根据电机和驱动芯片的指标来权衡。4. Keil MDK代码编写与Proteus联合仿真跑通完整流程4.1 工程创建与标准库基本配置代码部分我基于STM32标准外设库V3.5来写。用寄存器版本也可以但标准库可读性和上手难度更平衡适合大多数人直接抄作业。Keil 5里新建工程时芯片选择STM32F103C8。在Manage Run-Time Environment里不用勾选太多组件我们只用到GPIO和TIM外设。标准的工程文件结构建议包含stm32f10x.h、stm32f10x_gpio.c、stm32f10x_tim.c、stm32f10x_rcc.c以及内核相关的core_cm3.c。如果你用的是别人的工程模板记得在C/C选项里的Define一栏确认写上“USE_STDPERIPH_DRIVER, STM32F10X_MD”否则标准库的驱动定义不会生效。还有一个容易忽略的坑Output选项卡里必须勾选Create HEX File否则Proteus那边会找不到可加载的固件文件。这个选项默认是关闭的很多人Keil编译成功却导不出hex就是因为没勾选这里。4.2 完整代码PWM初始化、按键调速、方向切换下面是最核心的部分。我把完整的main.c贴出来里面包含了PWM初始化、方向GPIO初始化、按键检测和主循环逻辑。代码里注释尽量写清楚方便你直接移植。#include stm32f10x.h #include stm32f10x_gpio.h #include stm32f10x_tim.h #include stm32f10x_rcc.h // 全局变量当前占空比对应的比较值范围0~999默认50% uint16_t pulse 500; // 方向标志1为正转0为反转 uint8_t dir 1; // 简单延时函数仅供按键消抖和仿真使用 void delay_ms(uint32_t ms) { uint32_t i; while (ms--) { for (i 0; i 7200; i) { // 空循环约1ms 72MHz注意仿真和实物频率需一致 } } } // 定时器3通道1输出PWM引脚PA6 void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 使能GPIOA、AFIO和TIM3时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // PA6配置为复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 定时器时基72MHz / (711) 1MHz即1us计数一次 TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 999; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // PWM模式1初始占空比50% TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse pulse; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); // 使能自动重装预装载启动定时器 TIM_ARRPreloadConfig(TIM3, ENABLE); TIM_Cmd(TIM3, ENABLE); } // 方向控制引脚初始化PB0接IN1PB1接IN2 void Dir_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 默认正转IN1高IN2低 if (dir) { GPIO_SetBits(GPIOB, GPIO_Pin_0); GPIO_ResetBits(GPIOB, GPIO_Pin_1); } else { GPIO_ResetBits(GPIOB, GPIO_Pin_0); GPIO_SetBits(GPIOB, GPIO_Pin_1); } } // 按键初始化PA0加速PA1减速PA2换向内部上拉输入 void KEY_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); } int main(void) { PWM_Init(); Dir_GPIO_Init(); KEY_Init(); while (1) { // 加速按下PA0占空比增加 if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) 0) { delay_ms(10); // 简单消抖 while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) 0); // 等待松开 if (pulse 999) pulse 50; TIM_SetCompare1(TIM3, pulse); } // 减速按下PA1占空比减小 if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) 0) { delay_ms(10); while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) 0); if (pulse 50) pulse - 50; TIM_SetCompare1(TIM3, pulse); } // 换向按下PA2切换PB0/PB1输出状态 if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2) 0) { delay_ms(10); while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2) 0); dir !dir; if (dir) { GPIO_SetBits(GPIOB, GPIO_Pin_0); GPIO_ResetBits(GPIOB, GPIO_Pin_1); } else { GPIO_ResetBits(GPIOB, GPIO_Pin_0); GPIO_SetBits(GPIOB, GPIO_Pin_1); } } } }这个代码里main函数中没有串口和OLED逻辑很纯粹一个循环里检测三个按键改变CCR比较值或者方向引脚状态。PA6输出的PWM信号进L298的ENA引脚PB0、PB1控制L298的IN1、IN2从而控制电机转速和方向。4.3 编译、生成hex、加载到Proteus的完整步骤代码写好之后按F7编译。如果工程配置正确编译输出窗口会显示“0 Error(s), 0 Warning(s)”然后在工程目录下的Objects或Listings文件夹里找到生成的.hex文件。接下来回到Proteus双击STM32F103C8芯片在Edit Component对话框里找到Program File点文件夹图标选中刚才生成的hex。同时确认Crystal Frequency设置为8MHz。这一步完成后点击Proteus左下角的运行按钮。正常情况下你应该能在示波器上看到PA6引脚输出频率1kHz、占空比约50%的方波。按下加速按键你会发现波形的高电平宽度明显变宽电机转速随之变快按下减速按键高电平变窄电机转速变慢按下换向按键L298的OUT1和OUT2输出极性反转电机反转。如果你的Proteus运行后电机纹丝不动先别急着改代码按下面顺序排查先看示波器有没有PWM波形如果没有找到芯片属性重新确认hex是否加载成功如果有波形但电机不转大概率是L298的VS/VSS接反或者GND没有共地如果电机转但按键无效检查按键是否配置了内部上拉以及引脚是否接对。5. 常见问题与排查技巧实录为什么我的电机不转5.1 典型问题速查表我把仿真和实物调试中最常见的问题整理成了一张表你可以直接按图索骥现象可能原因解决方法PA6无波形芯片没加载hex检查Program File是否正确选择.hex文件PA6无波形未配置复用推挽检查GPIO_Mode是否为GPIO_Mode_AF_PPPA6无波形定时器时钟没使能RCC_APB1PeriphClockCmd使能TIM3有波形但电机不转L298的VSS/V S供电接错VSS接5VVS接电机电源GND共地有波形但电机不转方向引脚逻辑不对检查IN1和IN2是否有有效电平组合电机转速不随按键变化按键没消抖或引脚接错用示波器观察CCR变化检查按键电平频率和预期相差很大芯片属性晶振频率没改Crystal Frequency必须设为8MHz按键按下无反应内部上拉没开启GPIO_Mode配置为GPIO_Mode_IPU占空比越大转速反而越慢PWM模式或极性配反检查PWM模式1/2和OCPolarity设置5.2 我踩过的一个典型坑PWM输出电平反了有一回我仿真一个类似项目代码逻辑看起来完全没有问题PWM波形也正常但电机转速和占空比的关系反了占空比越大电机越慢。当时我盯着示波器看了好久后来才意识到是PWM模式配置的问题。STM32的PWM有模式1和模式2之分。PWM模式1是CNT小于CCR时输出有效电平默认高电平PWM模式2正好相反CNT大于CCR时输出有效电平。如果代码里用的是PWM2同时把极性设置成低电平有效那波形看起来仍然是“对”的但实际占空比含义已经反了。L298的ENA引脚期望的是高电平使能低电平关断一个取反逻辑就让转速曲线完全反过来。排查方法很简单把CCR从小往大调一圈盯住示波器的波形看高电平宽度是变宽还是变窄。变宽说明配置正确变窄说明模式或极性配反了。这种问题在实物上更隐蔽因为电机转起来你很难用肉眼分辨“占空比增加了但转速降低”是驱动问题还是负载问题必须靠示波器判断。5.3 另一个隐藏很深的坑Proteus晶振频率没设对这句话我在前面提过但值得单独拿出来说因为它太容易被忽略。Keil工程里SystemInit默认用HSE外部晶振按照8MHz石英晶振来计算72MHz主频。但Proteus的STM32模型默认Crystal Frequency可能不是8MHz有的版本默认是4MHz甚至为0。如果你在Proteus里加载hex后发现PWM频率偏差巨大比如本该是1kHz却变成了2kHz或者按键延时明显过快、电机转速异常偏高多半就是这个原因。修改方法很直接双击芯片把Crystal Frequency改成8MHz重新运行仿真。这个设置和代码里的时钟配置必须保持一致。如果以后你自己画了板子用了12MHz晶振那这里就要改成12MHz同时代码里的RCC配置也要相应调整。仿真只是工具它模拟的是你设定的硬件条件不是你想象中的条件。5.4 仿真通过之后转实物电路必须注意的事仿真跑通后很多人会急着下单买模块这时候有几件事不说清楚实物大概率要吃一次亏。第一共地。L298的GND、STM32的GND、电机电源的GND必须全部连在一起。很多新手只接信号线忘了共地导致控制信号完全乱掉。共地是数字电路和驱动电路协作的基本前提。第二续流二极管。L298内部已经集成了续流二极管实物如果用分立元件搭H桥必须在电机两端反向并联快恢复二极管否则关断瞬间的反电动势会打坏驱动管。Proteus里虽然看不到这个细节但实物绝对不能省。第三独立供电。STM32用3.3V电源L298的逻辑侧用5V电机电源单独供电不要把所有电源都堆在一个7805上面。电机启动电流会拉低母线电压分分钟让单片机复位。第四3.3V和5V逻辑电平匹配。STM32的GPIO高电平是3.3VL298的输入阈值一般可以兼容但如果你用的是某些需要5V高电平的驱动芯片就要加电平转换。Proteus里不会出现这种问题实物上却非常常见。第五PWM频率的调整。仿真里用1kHz只是为了方便观察实物为了减噪和减小电流纹波建议把频率提到10kHz以上。改频率只需要把ARR从999改成99同时把pulse的步进从50改成5逻辑不用动。最后聊聊我个人对这套流程的体会做单片机项目这几年Proteus仿真帮我省下的时间不是一星半点。尤其是电机控制这类容易烧器件的方向一次真实的反接、一次驱动芯片短路都可能让你几十块钱打水漂。先仿真再实物本质上是把试错成本从硬件挪到了软件里。真正到了实物的阶段你手里已经有了一套经过验证的逻辑剩下的只是把电压、电流、电平这些“物理参数”调到匹配状态。这篇文章里的例子虽然简单但它的价值在于跑通了一条完整的链路STM32的定时器如何产生PWM、PWM如何经过H桥变成电机驱动能力、按键如何实时改变占空比和方向。你完全可以在这个基础上扩展出更多东西用编码器测速做PID闭环、用串口发指令调速、加OLED显示当前转速、改成双电机差速小车控制。每加一块功能都会逼着你去吃透一层新的知识这就是做技术最踏实的成长路径。如果仿真过程中遇到问题回头看看速查表再不行就把示波器接上按信号链路一层一层排除。代码能抄经验和思路得自己攒。