简介:基于STM32F103与L298N的直流电机PWM调速Proteus仿真项目资源,面向嵌入式初学者、课程设计与工程实践人员,解决从PWM调速原理到实际驱动电路仿真验证的完整学习链路。资源共138个文件,压缩包仅2.62MB,包含34个头文件与33个C源码构成的Keil工程、HEX固件、Proteus仿真工程(pdsprj/pdsbak)及工程配置和编译辅助文件,可对照源码与电路同步学习。目前已有12336人浏览学习这一项目,适用于STM32入门、直流电机控制或Proteus仿真练习。读者可依据资源中的源码与仿真工程,理解PWM占空比与电机转速的关系,掌握STM32F103的GPIO和定时器PWM输出配置,学会L298N双H桥驱动器的EN/IN引脚控制方法,并能加载HEX到Proteus电机模型中运行仿真,快速验证硬件逻辑,节省实物联调时间。 用STM32F103做直流电机的PWM调速,再放到Proteus里仿真验证,驱动芯片选L298N——这套方案,我猜很多人都不陌生。课程设计、毕业设计、电子竞赛热身,只要是跟直流电机控制沾边的项目,十有八九会碰到。它不像直接用开发板点灯那么单调,也不像纯理论分析占空比那样摸不着头脑,刚好卡在“能看懂原理”和“能动手验证”之间。
这篇文章就从一个实际能跑通的项目出发,把电路连接、定时器配置、占空比计算、代码实现和排查经验完整过一遍。不管你是刚学STM32的在校生,还是想快速验证调速方案的开发者,只要照着这条流程走,基本就能在Proteus里把电机调速的效果跑出来。我会把每一步为什么这么做的原因也讲清楚,这样换一颗芯片、换一个电机,你也能照样推出来。
1. 项目定位与整体设计思路
1.1 为什么用PWM而不是直接调电压
直流电机的转速,本质上是靠电枢两端的平均电压决定的。最简单粗暴的办法是串电阻调压或者用线性稳压器调压,但这两个方案的问题都很明显:串电阻会浪费大量功率,电阻发热严重;线性稳压器同样是把多余的能量以热的形式消耗掉。而且电机在不同负载下电流变化很大,电阻分压的结果让转速根本不是线性的,控制起来很别扭。
PWM调速就不一样了。它用固定频率的高电平脉冲去驱动电机,通过改变脉冲的宽度,也就是占空比,来改变平均电压。占空比50%,平均电压就是电源电压的一半;占空比80%,平均电压就是电源电压的80%。实际加到电机两端的是方波,但电机绕组有电感,电流会被电感“抹平”,所以电机感受到的等效电压是平滑的。这个方案效率极高,开关管要么全导通、要么全关断,损耗很小,而且占空比和速度之间近似线性,控制起来直观。
STM32F103做PWM的优势在于,它的定时器本身就是硬件PWM发生器。CPU只需要往比较寄存器里写一个数,引脚就会自动输出对应占空比的波形,不需要软件去翻转IO口,不占用CPU时间,精度也高。这正是电机调速、呼吸灯、舵机控制这些场景最需要的特性。
1.2 STM32F103、L298N、Proteus三者如何配合
STM32F103在这个项目里承担的任务是“大脑”,负责生成PWM信号和控制方向信号。它本身输出的电流很小,只有几毫安,直接驱动直流电机根本带不动,所以中间必须加驱动芯片。
L298N就是最常见的选择之一。它内部是两个完整的H桥,可以控制两个直流电机的正反转和速度。每个H桥的4个开关管由逻辑输入电平控制,而电机的供电来自外部电源,不经过单片机。这就把“弱电控制”和“强电驱动”隔离开了,逻辑上非常清晰。
Proteus在这里扮演的是“虚拟试验台”。在没有实物板子的情况下,它可以把STM32、L298N、直流电机、示波器全部放到一张图纸上跑仿真。尤其是Proteus 8以上的版本,对STM32的支持已经比较完整,可以加载编译生成的hex文件,直接观察电机转速变化,还能用虚拟示波器看PWM引脚的实际波形。这对于验证控制逻辑、排查接线错误,效率比反复烧写实物高不少。
这套组合还有一个好处:从仿真到实物迁移的代价很小。Proteus里验证过的引脚分配和逻辑,拿到真实STM32F103最小系统板上,只要接线一致,代码基本不用大改。这也是我推荐入门者先跑仿真、再上手实物的原因。
2. Proteus仿真电路搭建:从最小系统到电机驱动
2.1 仿真环境准备与芯片放置
Proteus仿真STM32F103,用的是它自带的STM32模型库。打开Proteus后,在元件搜索框里输入“STM32F103C8”或者“STM32F103C8T6”,就能找到对应的芯片模型。这里我用的芯片是STM32F103C8T6,48引脚封装,资源足够,也是目前最常见的型号之一。如果你用的版本里搜不到,注意检查软件版本,Proteus 8.9以后的STM32器件库都比较完整,老版本可能需要手动导入器件库文件,这一点网上资料很多,但本质上就是换新版本的事。
放置芯片后,建议先把电源引脚处理清楚。STM32F103C8T6在Proteus里默认接好VDD和VSS,但为了仿真稳定,还是要确认一下有没有额外的VDDA、VSSA引脚需要接电源。放置L298N芯片时,元件搜索框输入“L298”就能找到。Proteus的库里面就写着L298,它和实物L298N模块的引脚定义是兼容的,放心用。
此外还需要放置一个直流电机模型,搜索“MOTOR-DC”或者“MOTOR”就能找到。为了能看到转速变化,我建议再放一个虚拟示波器,方便观察PA6引脚上的PWM波形。
2.2 L298N引脚功能与电路连接
L298N的引脚看起来多,但其实每个功能都很清晰。以控制电机A这一路为例,涉及的核心引脚是VSS、VS、GND、ENA、IN1、IN2、OUT1、OUT2。
VSS是逻辑电源引脚,接5V,给L298N内部的逻辑电路供电,这个必须接,不接的话芯片内部逻辑不工作;VS是电机电源引脚,接电机供电的正极,仿真里我用12V做演示;GND必须把逻辑电源和电机电源的地连在一起,同时和STM32的地也要连在一起,这一点最容易出错,地线不共地,PWM信号的电平基准就不一致,电机会出现完全不转或者乱转的情况。
IN1和IN2是方向控制输入,接STM32的两个普通GPIO,比如PB0和PB1。ENA是使能引脚,接STM32的PWM输出脚,比如PA6。L298N内部逻辑是:ENA为高电平时,OUT1和OUT2才跟随IN1、IN2的电平变化;ENA为低电平时,电机两端被切断,处于自由停车状态。把ENA接PWM信号,就等于用PWM占空比控制有效导通时间,达到调速目的。
OUT1和OUT2接电机两端。这里需要注意,OUT1和OUT2的电压是随IN1、IN2的状态变化的,电机正转时OUT1为高OUT2为低,反转时反过来。所以电机两端不要接反,但即使接反了,也只是电机翻转方向反了,不会烧东西,调试时灵活处理就行。
2.3 电机、电源、地线的布置细节
在Proteus里画电路时,最容易踩的坑是电源和地的画法。STM32仿真模型和L298N模型都需要明确的电位参考,不能只靠默认的VCC端子。我的做法是:用Proteus侧边栏的“终端模式”放置电源端子,+5V端接L298N的VSS,+12V端接L298N的VS,GND端子接到芯片对应的GND引脚上。
电机两端我习惯接一个反向并联的二极管,仿真里这个二极管的作用是模拟真实电路中的续流保护。虽然Proteus仿真不一定会因为没接二极管而出错,但从养成好习惯的角度讲,接了更接近实物电路。真实电机是感性负载,关断瞬间会产生反向电动势,如果没有续流回路,容易击穿驱动芯片。Proteus里看不太明显,但实物调试时这个问题很致命。
还有一个细节:STM32的PA6作为PWM输出,在Proteus里不需要额外配置复用功能,但真实芯片上必须设置为复用推挽输出模式。这也是后面代码里要做的重要初始化。仿真模型对GPIO模式的约束没那么严格,但代码不能因此省略。
3. 定时器PWM配置与参数计算
3.1 PWM波形产生的原理
STM32的通用定时器TIM3产生PWM,靠的是内部计数器和比较寄存器之间的碰撞。定时器不断从0向上计数到自动重装载值ARR,计数过程中,当计数值到达比较寄存器CCR时,输出引脚的电平会翻转一次;计数到ARR后回到0,电平再翻转一次。这样在引脚上就形成了一个周期性的方波,高电平宽度由CCR决定,周期由ARR决定。
占空比的计算公式是:占空比 = CCR / (ARR + 1) × 100%。只要CCR往大了写,高电平时间就长,平均电压就高,电机转速就快。往小了写,转速会降下来。整个过程中,定时器的硬件自动完成计数和比较,CPU不需要频繁干预,这是硬件PWM和软件延时翻转IO口的根本区别。
3.2 定时器时钟来源与分频计算
TIM3挂在APB1总线上,STM32F103的APB1最高时钟是36MHz,但定时器时钟有个倍频机制:当APB1分频系数不为1时,定时器时钟自动翻倍到72MHz。系统上电后默认使用的是内部8MHz HSI,但实际工程里标准做法是使用外部8MHz晶振配合PLL倍频到72MHz系统主频。Proteus仿真里,如果烧写的是标准库工程生成的hex文件,时钟初始化时会自动完成这个配置。
TIM3最终工作的时钟频率是72MHz。为了得到想要的PWM频率,需要设置预分频器PSC和自动重装载值ARR。推导公式是:
PWM频率 = 72MHz / (PSC + 1) / (ARR + 1)
直流电机调速常用的PWM频率是10kHz到20kHz。低于10kHz电机会有可闻噪声,转速也会不稳;高于20kHz进入超声波频段,噪声消失,但对开关管的开关损耗要求更高。这里我选20kHz作为目标频率,这样既安静,参数也比较好算。
取PSC = 35,那么 (PSC+1) = 36,72MHz / 36 = 2MHz。再取ARR = 99,那么 (ARR+1) = 100,2MHz / 100 = 20kHz。这组参数的好处在于,PWM的分辨率是1%,也就是CCR从0到99变化时,占空比能精确对应0%到99%,非常直观。
3.3 TIM3_CH1输出PWM的实际配置代码
标准库V3.5的代码风格虽然看着啰嗦,但结构清晰,很适合用来理解原理。整个输出PWM的初始化可以分为三步:开启TIM3和GPIO的时钟、配置GPIO为复用推挽输出、配置定时器时基和PWM输出通道。
void TIM3_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); 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); TIM_TimeBaseInitStructure.TIM_Period = arr; TIM_TimeBaseInitStructure.TIM_Prescaler = psc; TIM_TimeBaseInitStructure.TIM_ClockDivision = 0; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure); 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(TIM3, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }这段代码有几个点值得注意。PA6默认就是TIM3的通道1输出引脚,不需要重映射。如果哪天PA6被其他功能占用,需要把TIM3_CH1重映射到PB4,那么还要开启AFIO时钟并调用GPIO_PinRemapConfig(GPIO_Remap_TIM3, ENABLE)。TIM_OCMode_InitStructure.TIM_OCMode先选PWM1模式还是PWM2模式,差别只是有效电平的极性,PWM1配合TIM_OCPolarity_High是最常用的组合。TIM_Pulse初始化为0,意味着上电瞬间占空比是0%,电机不会突然全速转动,这个细节在实物调试时能避免很多麻烦。
4. 调速控制逻辑与完整实现
4.1 方向控制信号逻辑
L298N的方向控制逻辑很直白。以电机A这一路为例,当ENA为高电平时,如果IN1=1、IN2=0,OUT1输出高电平、OUT2输出低电平,电机正转;反过来IN1=0、IN2=1,电机反转;如果IN1=IN2,电机不转。实际工程里,为了保证安全,最好在切换方向前先把占空比降到0,避免电机带着惯性突然反向,产生很大的冲击电流。
方向控制的GPIO初始化也很简单,选PB0和PB1作为两个方向引脚,配置成通用推挽输出:
void Motor_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); } void Motor_SetDirection(u8 dir) { if(dir == FORWARD) { 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); } }这里我特意把方向引脚放在PB0和PB1,和PWM引脚PA6分开,避免在调试时混淆。选引脚时要注意避开JTAG下载占用的一些引脚,比如PA13、PA14、PB3、PB4。虽然Proteus仿真时无所谓,但实物调试时这几个默认被调试接口占用,容易出奇怪问题。
4.2 占空比调节的几种实现方式
占空比调节核心就一句话:调用TIM_SetCompare1(TIM3, 值),往通道1的比较寄存器里写目标值。这个值范围是0到99,因为是100级分辨率。
最简单的演示方式是让占空比自动循环变化,从0加到99再减回来,模拟呼吸灯一样的效果。实现如下:
int main(void) { u8 duty = 0; TIM3_PWM_Init(99, 35); Motor_GPIO_Init(); Motor_SetDirection(FORWARD); while(1) { for(duty = 0; duty <= 99; duty = duty + 5) { TIM_SetCompare1(TIM3, duty); delay_ms(200); } for(duty = 99; duty > 0; duty = duty - 5) { TIM_SetCompare1(TIM3, duty); delay_ms(200); } } }每次增加5%的占空比,电机转速会看到明显阶梯式变化。如果你想让调速更细腻,可以把循环里的步进改成1%,但为了让Proteus仿真里肉眼看到变化过程,5%的步进更直观。delay_ms可以用简单的软件延时函数实现,也可以用SysTick定时器来做精确延时。仿真时软件延时就够了,不用太纠结精度。
更接近实际应用的做法是用按键调速:按一下按键占空比加10%,再按另一个按键减10%。这个方法适合做交互演示,代码无非是在while循环里加按键扫描逻辑。再进一步,还可以接一个电位器,通过ADC读取模拟量,映射到占空比上,实现连续调压调速。这样整个系统就从单纯的PWM扩展到ADC采集和数据处理,适合作为扩展学习方向。
4.3 保护逻辑与故障处理思路
电机驱动里,“PWM故障保护”是一个容易被新手忽略但在实物中极其重要的点。你想想,如果电机堵转,电流会飙升,这时候如果不能及时切断PWM输出,驱动芯片很可能直接烧掉。L298N本身有过热保护,但标准型号没有集成电流检测,需要自己在外部电路上加采样电阻。
STM32F103的高级定时器TIM1和TIM8带有刹车功能,当刹车输入引脚接收到有效电平,PWM输出会被硬件强制关闭,响应速度是纳秒级,根本不需要软件介入。但TIM3是通用定时器,没有刹车功能。所以如果你的项目对安全性要求高,建议把PWM通道从TIM3换到TIM1或TIM8的某个通道上。
仿真阶段虽然不需要考虑过流烧板子,但代码里可以预留软件保护思路:比如定义了一个motor_error标志位,检测到异常时把占空比清零并让电机进入自由停车状态。或者在切换方向时,先调用TIM_SetCompare1(TIM3, 0)延迟一小段时间再改IN1、IN2的电平。这些细节写进代码里,以后迁移到实物项目时能省下不少调试时间。
5. 常见问题与排查技巧
5.1 电机不转的检查清单
Proteus仿真里电机纹丝不动,是出现频率最高的问题。我总结下来,排在前三的原因基本都是接线和使能问题。
第一,L298N的VSS没接5V。很多新手只接了VS(电机电源),觉得芯片有电了就行,但L298N的逻辑部分需要单独的5V供电,VSS不接,内部逻辑根本不工作,IN1、IN2给再多电平也没反应。第二,GND没有共地。这是Proteus仿真里最容易忽略的,STM32的GND、L298N的VSS地、VS地必须连到同一个网络,否则PWM信号没有统一的电平参考。第三,ENA没有拉高或者没有接PWM。如果ENA接到固定高电平,电机只会全速转或者不转,没有调速效果;如果ENA悬空,它内部是下拉的,默认使能不了,电机当然不转。
排查顺序我建议是:先看L298N的VSS和GND,再看ENA是不是接到了PA6,最后用虚拟示波器看PA6有没有波形输出。从“电源→逻辑→波形”这个顺序走,基本很快能定位问题。
5.2 转速调节不平滑或者没有明显变化
如果在Proteus里观察电机,觉得转速好像一直很快,减不下来,先检查ARR和CCR的数值关系。占空比 = CCR / (ARR + 1),如果ARR设成了9999,而CCR只在0到99之间变,那占空比最大也才1%,电机当然看不出明显变化。
另一种情况是占空比寄存器更新了,但电机转速一直不变。这种问题在真实芯片上往往是预装载没开导致,TIM_OC1PreloadConfig这个函数的调用很重要。预装载的意思是,寄存器里的新值要等当前PWM周期结束才生效,防止在更新瞬间产生毛刺波形。这个问题在Proteus仿真里不太容易暴露,但实物调试时很典型。
还有一点:Proteus里的直流电机模型和真实电机一样,占空比很低时转矩很小,电机可能停在原地不转。当你看到占空比低于10%电机没动静,不一定是程序错了,先把占空比加到30%以上再观察,这才是正常的工作区。想要更平滑的调速效果,可以把PWM分辨率从100级提升到1000级甚至更高,也就是把ARR调大,同时同步调大PSC,保证频率不变。
5.3 虚拟示波器显示异常的处理
Proteus的虚拟示波器是最直观的调试工具,我建议每次调PWM都把示波器挂到PA6引脚上,观察频率和占空比是否符合预期。如果示波器上看到的不是方波而是奇怪的毛刺或低电平,先别急着怀疑代码,检查一下定时器时钟有没有正常使能,RCC_APB1PeriphClockCmd那行注释掉的话,TIM3整个不工作,输出自然异常。
另外,示波器探头所接的网络名要和PA6引脚匹配。Proteus里如果从PA6引出一条线,线上标了别的网络标号,示波器就直接显示那条网络名的信号了。这种低级错误最容易让人一脸懵,排查方法就是双击示波器输入端的连线,确认网络标签真的是PA6。
还有一个经验是:Proteus仿真时,把PWM频率设得太高,比如超过100kHz,虚拟示波器的采样率可能跟不上,波形显示会失真或者出现锯齿。20kHz这个参数在仿真和实物里都很合适,属于怎么调都不会出问题的安全区间。
5.4 代码编译时的常见坑
这个项目用标准库V3.5写代码,最容易出的编译错误有两类。第一类是只添加了stm32f10x_gpio.o和stm32f10x_tim.o源文件,但忘了在stm32f10x_conf.h里注释掉对应的头文件包含或者使能对应的模块宏。标准库的文件组织比较死板,缺一个依赖就编译报错,而且报错信息还不直观。
第二类是启动文件选错。STM32F103C8T6是中等容量(Medium Density)芯片,必须使用startup_stm32f10x_md.s启动文件;如果选了hd(高容量)的启动文件,程序下载到Proteus里可能能跑,但实际用在大容量芯片上时机正确,仿真时可能出现中断不进、延时不准的情况。这种问题卡住人一整天都很正常。
编译生成的hex文件,要在Proteus里“双击芯片”打开属性窗口,在Program File那一栏指定hex路径。同时把Crystal Frequency改成8MHz,和代码里的外部晶振频率保持一致。这一步不做,仿真时时钟不对,PWM频率自然也不对。
写在最后:个人实践体会
这套STM32F103 + L298N + PWM调速的仿真项目,每一次重新做都能复习一遍好几门课的知识:STM32定时器的时基、PWM模式、GPIO复用功能、H桥驱动逻辑,还有Proteus仿真的信号分析技巧。我个人的建议是,先把代码里TIM_SetCompare1的值改成固定数,比如50,用示波器确认PA6确实输出了50%占空比的20kHz方波,然后再写循环调速的逻辑。这样从“稳定输出”到“动态调节”一步一个脚印,排查起来最省时间。
如果你在Proteus里把这一整套流程跑通了,我强烈建议找一块真实的STM32F103最小系统板和L298N模块,按照同样的接线方式焊一套实物出来。你会发现仿真里忽略的很多细节,比如L298N的发热、电机启动瞬间的电流冲击、电源纹波对转速的影响,在实物里都会被放大。到那时候你再回头看PWM调速的原理,理解会完全上另一个台阶。这也是仿真和实物搭配学习最有意思的地方。
本文还有配套的精品资源,点击获取