带过不少做嵌入式课设的同学之后,我发现一个特别普遍的现象:点灯、按键、串口这些实验都能顺利跑通,一旦进入"用STM32控制直流电机"这个项目,很快就卡住了。有人调了一下午,最后发现是L293D的使能引脚悬空;有人示波器上波形完全正确,电机却纹丝不动,一查是电机模型的额定电压和驱动电源不匹配。这类问题在Proteus仿真里其实很好排查,因为所有网络都是可视的,比实物排查直观得多。
这篇博文,我把自己在Proteus里跑通的一整套直流电机PWM控制方案完整复盘一遍:从PWM调速原理、仿真电路搭建、定时器配置、代码编写到烧录调试和排错,按实际操作顺序来。重点给出一份可以直接编译加载的STM32F103C8T6标准库代码,并把每个关键参数是怎么算出来的讲清楚。不管你是刚接触定时器输出PWM的新手,还是正在准备课程设计想找个参考工程,都可以照着这套流程在自己电脑上复现。
1. 先把原理讲透:PWM调速为什么能控制直流电机
1.1 电机控制前必须想明白的三个问题
动手接线之前,先回答三个问题,这三个问题直接决定了整个电路方案长什么样。
第一,STM32的GPIO引脚能不能直接驱动电机?不能。STM32F103C8T6的GPIO输出电流能力大概在20mA级别,而一个普通的直流电机正常工作电流轻松到几百毫安,启动瞬间甚至能到额定电流的好几倍。直接用引脚带电机,轻则转不动,重则把引脚烧掉。
第二,PWM到底是怎么实现调速的?电机两端加的不是可变的直流电压,而是一串固定频率、脉宽可变的高电平脉冲。电机依靠自身的电感和机械惯性,对脉冲进行"平均",得到等效的直流电压。占空比越大,平均电压越高,转速越快。
第三,方向怎么控制?单片机的GPIO只能输出高或者低,想让电机反转,需要让电机两端的电压极性反过来。这需要H桥电路,或者集成H桥的驱动芯片。
这三个问题想清楚了,后面的电路和代码就顺理成章。
1.2 占空比与平均电压:PWM调速的本质
PWM的英文是Pulse Width Modulation,脉冲宽度调制。一个周期内高电平持续的时间占整个周期的比例叫占空比。用公式表达就是:
duty = T_on / T_period
假设高电平电压是U_h,那么一个周期内的平均电压大概是:
U_avg = duty x U_h
举个例子,12V电源通过PWM驱动电机,占空比50%时,电机感受到的等效电压约6V。占空比25%,等效电压约3V。这个"等效"不是瞬时成立的,而是因为电机是感性负载,线圈中的电流不会突变,硬件上天然具备低通滤波的特性,会把高频脉冲平滑成近似直流的电流。
PWM调速相比串联电阻调速、线性稳压电源调速,最大的优势是效率高。功率管只工作在完全导通和完全关断两种状态,导通时损耗小,关断时没有电流,几乎不浪费能量。这也是为什么电机调速、LED调光、舵机控制这类场景全都在用PWM。
PWM频率的选择也值得多说一句。频率太低,比如低于几百赫兹,电机会出现明显的顿挫感甚至抖动,因为电流断续太严重。频率太高,开关损耗变大,驱动芯片发热明显。直流电机调速工程上常用10kHz到20kHz,大家熟知的"无刷电机油门信号"其实也是PWM类信号。Proteus仿真中取5kHz也足够,既能保证转速平滑,又方便示波器观察。
1.3 为什么3.3V的引脚不能直接驱动12V电机
电压和电流两个维度都不匹配。GPIO高电平是3.3V,而电机额定电压可能是6V、12V甚至更高,GPIO输出一个3.3V根本达不到电机的最低启动电压。再退一步说,就算电机额定电压正好是3.3V,驱动电流也完全不够,前面已经算过,引脚输出能力只有几十毫安。
所以整个系统需要分两部分:STM32输出的是控制信号,负责"决策";驱动芯片或者驱动电路负责"出力",把控制信号放大成功率信号。驱动芯片的输入侧接3.3V逻辑信号,输出侧接电机电源和电机绕组,输入输出之间通过内部逻辑完成信号搬移。
这也是为什么市面上直流电机驱动器几乎都有一个"逻辑电源"和一个"电机电源"引脚,两个电源分别供给逻辑部分和功率部分。
2. 仿真电路搭建:元件、选型与完整连线
2.1 元件清单和Proteus搜索方法
Proteus中每个元件都有对应的库内关键词,直接搜索最快。本项目的元件清单如下:
| 元件 | Proteus搜索关键词 | 数量 | 作用 |
|---|---|---|---|
| STM32F103C8T6 | STM32F103C8T6 | 1 | 主控 |
| L293D | L293D | 1 | 电机驱动 |
| DC电机 | DC MOTOR | 1 | 被控对象 |
| 按键 | BUTTON | 4 | 调速、换向、启停 |
| 电解电容 | CAP-ELEC | 3 | 电源滤波 |
| 电阻 | RES | 若干 | 上拉/复位 |
Proteus中放置元件后,双击元件可以修改参数。DC Motor元件需要根据实际电源设置额定电压,这个细节后面会专门讲,也是很多人电机不转的隐藏原因。
2.2 驱动器选型:L293D比ULN2003强在哪
很多教材上喜欢用ULN2003驱动电机,但ULN2003是达林顿管阵列,内部是集电极开路结构,只能把负载一端接到电源、另一端通过管子导通到地。也就是说它只能做单向开关,没法让电机反转。用它做直流电机调速只能调速度,不能换方向。
L293D是双H桥驱动芯片,一路就是一个完整的H桥。所谓H桥,就是四个开关管接成H形,负载电机横跨中间。通过控制两组对角线上的开关管分别导通,可以让电机两端电压极性互换,从而实现正反转。H桥还有一个好处:当上下桥臂同时导通时,电机两端短接,实现刹车;当所有管子关断时,电机进入惰性滑行状态。
L293D每个通道的输出电流是600mA,峰值1.2A,驱动小型直流电机绰绰有余。最常用的控制关系如下:
| 1,2EN | IN1 | IN2 | OUT1 | OUT2 | 电机状态 |
|---|---|---|---|---|---|
| PWM | H | L | 跟随PWM | L | 正转调速 |
| PWM | L | H | L | 跟随PWM | 反转调速 |
| L | X | X | L | L | 停止 |
| H | H | H | H | H | 刹车 |
注意看,EN使能脚接PWM信号,IN1和IN2接方向控制信号。这个接法非常重要,后面排错章节会专门展开。只要掌握了这张表,L293D的用法就吃透了。
2.3 关键连线:电源、共地、复位与时钟
电路接线的第一个原则是电源分三路,共地必须贯通。
STM32F103C8T6的VDD引脚全部接3.3V,VDDA也接3.3V,BOOT0接GND,NRST通过10k电阻上拉至3.3V,并加一个0.1uF电容到GND。L293D的VSS接5V,作为逻辑电源;VS接电机电源,我用12V。两个电源的GND全部连接在一起,同时连接到STM32的GND。如果逻辑电源和电机电源不共地,L293D内部的信号参考电位就没有建立,输出根本没法工作。
第二个原则是晶振频率要和代码匹配。Proteus中放置STM32F103C8T6后,双击芯片,把Crystal Frequency设置为8MHz,与标准库中HSE_VALUE的默认值一致。这个设置非常容易被忽略,如果你代码里按8MHz算好定时器频率,但仿真芯片实际用的是12MHz晶振配置,所有时序全部偏移,PWM频率也会对不上。
去耦电容在仿真中看着不起眼,但加上更接近真实习惯。VS和VSS引脚各加一个0.1uF和10uF电容到GND,可以明显减小电源纹波,避免仿真中出现莫名其妙的电平抖动。
2.4 引脚分配方案
我的引脚分配是这样的:
| 引脚 | 功能 | 连接目标 |
|---|---|---|
| PA6 | TIM3_CH1,PWM输出 | L293D的1,2EN |
| PA7 | TIM3_CH2,可扩展第二路PWM | 空闲或备用 |
| PB0 | 方向控制A | L293D的1A |
| PB1 | 方向控制B | L293D的2A |
| PA0 | 加速按键 | 按键另一端接GND |
| PA1 | 减速按键 | 按键另一端接GND |
| PA2 | 正反转切换 | 按键另一端接GND |
| PA3 | 启停控制 | 按键另一端接GND |
L293D的OUT1、OUT2接电机两端。L293D的3,4EN和3A、4A这组暂不使用,把3,4EN接地,防止干扰。整个项目只用一片L293D的其中一路,另一路还能留着扩展第二台电机。
2.5 电路检查清单
连线完成后,建议按下面这个清单过一遍再进调试阶段:
- BOOT0是否接GND,NRST是否上拉
- L293D的VS、VSS是否都供电,GND是否所有模块共地
- 电机是否接在OUT1和OUT2之间
- PA6到1,2EN之间有没有断线或错误网络
- 晶振频率是否在芯片属性中设置成8MHz
- 4个按键是否一端接PA口、另一端接GND
这个清单能在仿真运行之前过滤掉超过一半的低级错误。
3. 定时器PWM配置:寄存器逻辑与代码实现
3.1 为什么是TIM3_CH1/CH2
STM32F103C8T6有TIM1、TIM2、TIM3、TIM4,外加两个基本定时器TIM6和TIM7。TIM1是高级定时器,带互补输出和刹车功能,适合电机控制里的三相无刷驱动器,但对单台直流电机来说资源过剩。通用定时器TIM2、TIM3、TIM4足够。
我选了TIM3,原因是它的通道1和通道2默认映射在PA6和PA7,与按键使用的PA0到PA3不冲突,以后扩展双路电机控制很方便。TIM2的CH1和CH2也支持PWM,但PA0和PA1在后续还要做按键输入,如果把它们用作PWM,按键就得挪位置,没必要给自己添麻烦。
另外要提醒一个STM32F103的老坑:APB1预分频为2时,APB1总线时钟是36MHz,但挂在APB1上的通用定时器时钟不是36MHz,而是72MHz。原因是STM32F1系列在APB1预分频系数大于1时,定时器时钟会自动倍频到APB1的2倍。很多教程里只给了72MHz这个结果,没解释原因,导致新手一算PWM频率怎么算都对不上,实际上就是在这里被绕晕了。
3.2 PWM频率与占空比的计算方法
定时器产生PWM的核心寄存器有三个:TIM_Prescaler、TIM_Period和TIM_Pulse。
- TIM_Prescaler分频计数器,内部计数时钟先经过它分频
- TIM_Period是自动重装载值,计数器从0数到它然后重新开始
- TIM_Pulse是捕获比较值,计数器数到这个值触发电平翻转
PWM频率的计算公式:
F_PWM = 定时器时钟 / ((PSC + 1) x (ARR + 1))
占空比的计算公式:
duty = CCR / (ARR + 1)
我用的是PSC=14、ARR=999,定时器时钟72MHz,代入后:
F_PWM = 72MHz / (15 x 1000) = 4800Hz
占空比就直接由CCR决定,例如CCR=500时占空比约50%,CCR=0时占空比0,CCR=999时接近100%。
为什么选这个组合?4800Hz对直流电机来说不会一顿一顿,同时示波器能清楚看到完整的PWM周期,仿真体验好。如果想贴近实物习惯,可以把PSC改成9、ARR设为719,得到10kHz,公式一样,读者可以自己验算一遍。
3.3 完整代码:初始化、调速、正反转与控制逻辑
代码基于标准外设库3.5,工程创建好后只需要把下面的main.c替换进去,并在Options for Target里面勾选Create HEX File。需要确保工程中加入了stm32f10x_gpio.c、stm32f10x_tim.c、stm32f10x_rcc.c这三个外设库文件。
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_tim.h" #include "stm32f10x_rcc.h" #define PWM_ARR 999 #define PWM_PSC 14 void Delay_Ms(uint16_t ms) { volatile uint16_t i, j; for (i = 0; i < ms; i++) for (j = 0; j < 7200; j++) ; } void RCC_Config(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); } void GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; /* PA6/PA7:TIM3_CH1/CH2,复用推挽输出 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); /* PB0/PB1:方向控制,普通推挽输出 */ 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); /* PA0~PA3:按键输入,上拉输入,按下读到低 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOA, &GPIO_InitStructure); } void TIM3_PWM_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_TimeBaseStructure.TIM_Period = PWM_ARR; TIM_TimeBaseStructure.TIM_Prescaler = PWM_PSC; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &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(TIM3, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); } void Motor_Forward(void) { GPIO_SetBits(GPIOB, GPIO_Pin_0); GPIO_ResetBits(GPIOB, GPIO_Pin_1); } void Motor_Reverse(void) { GPIO_ResetBits(GPIOB, GPIO_Pin_0); GPIO_SetBits(GPIOB, GPIO_Pin_1); } void Motor_Brake(void) { GPIO_SetBits(GPIOB, GPIO_Pin_0); GPIO_SetBits(GPIOB, GPIO_Pin_1); } void Motor_Stop(void) { GPIO_ResetBits(GPIOB, GPIO_Pin_0); GPIO_ResetBits(GPIOB, GPIO_Pin_1); } int main(void) { uint16_t speed = 0; uint8_t dir = 1; uint8_t run = 0; RCC_Config(); GPIO_Config(); TIM3_PWM_Config(); Motor_Stop(); while (1) { if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == RESET) /* 加速 */ { if (speed < PWM_ARR) speed += 50; if (speed > PWM_ARR) speed = PWM_ARR; TIM_SetCompare1(TIM3, speed); while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == RESET); } if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) == RESET) /* 减速 */ { if (speed >= 50) speed -= 50; else speed = 0; TIM_SetCompare1(TIM3, speed); while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) == RESET); } if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2) == RESET) /* 换向 */ { speed = 0; TIM_SetCompare1(TIM3, speed); Delay_Ms(20); /* 先让电机停下来再换向 */ if (dir == 1) { dir = 0; Motor_Reverse(); } else { dir = 1; Motor_Forward(); } while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2) == RESET); } if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_3) == RESET) /* 启停 */ { run = !run; if (run) { if (dir == 1) Motor_Forward(); else Motor_Reverse(); TIM_SetCompare1(TIM3, speed); } else { speed = 0; TIM_SetCompare1(TIM3, speed); Motor_Stop(); } while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_3) == RESET); } } }代码逻辑拆开说几段。GPIO_Config里PA6和PA7配置成复用推挽输出,这样才能把定时器内部产生的波形送到引脚上,普通推挽输出是引不出来PWM的。PB0和PB1配置成普通推挽输出,用来输出方向电平。PA0到PA3配置成上拉输入,按键按下时引脚读到低电平,和我的按键接线方式呼应。
TIM3_PWM_Config里的关键是PWM模式选择。我用的PWM1模式,在这个模式下,计数器CNT小于CCR时输出有效电平,大于CCR时输出无效电平,配合TIM_OCPolarity_High就得到正占空比波形。理解这个对应关系,后面示波器上看到波形就能立刻判断配置是否正确。
主循环里的逻辑很直白,但有一个细节值得注意:换向操作时,先把speed清零,再延时,最后才切换方向。这是模拟实物操作时的规范流程,因为电机带着转速突然反接电源,冲击电流很大,对驱动芯片和机械结构都不友好。仿真中体现不出来,但习惯应该从仿真就开始养成。
4. 烧录仿真:Hex加载、波形验证与运行判断
4.1 Keil侧生成Hex的准备工作
代码写好后,打开Options for Target,在Output标签页勾选Create HEX File。这一步不勾,Proteus就没有程序文件可以加载。
同时确认一下Device标签页选的芯片是STM32F103C8Tx,或者至少是同系列的C8T6。选错芯片型号会导致链接脚本和Flash容量不对,虽然PWM功能不太受影响,但可能编译出奇怪的问题。
编译时如果报错缺某个外设库文件,回到工程组列表,把stm32f10x_gpio.c、stm32f10x_tim.c、stm32f10x_rcc.c添加进去。如果是在原工程上替换的代码,记得把标准库头文件路径配置对。
编译通过后,会在工程目录下的Output文件夹里生成同名.hex文件。记住一个容易踩的坑:这个hex文件的路径最好全英文。路径里只要出现中文,很多情况下Proteus加载时会静默失败,现象就是点击运行后芯片纹丝不动。
4.2 Proteus侧加载Hex和时钟设置
回到Proteus,双击原理图中的STM32F103C8T6芯片,弹出属性对话框。在Program File一栏选择刚才生成的hex文件。这一栏如果为空或者路径不对,仿真时芯片就是空的,什么都不会执行。
同时把Crystal Frequency设置为8MHz。这一步和第2.3节强调的一样,必须和代码中的HSE_VALUE匹配。标准库system_stm32f10x.c默认HSE_VALUE是8000000,也就是8MHz。如果这里设置成12MHz,PLL倍频后主频会变成108MHz,超出STM32F103的72MHz额定范围,仿真模型可能会给出警告,而且所有外设时序全部乱套。
加载完成后,点击左下角运行按钮。如果一切正常,能看到DEMO程序在控制台显示运行状态,电机模型会开始转动。如果没反应,进入下一节按顺序排查。
4.3 用示波器看PWM波形
Proteus左侧工具栏找到Virtual Instruments,添加Virtual Oscilloscope,把通道1的探头接到PA6引脚,地接GND。点击运行,再按一下加速按键,示波器上应该能看到幅值3.3V左右的方波。调节右下角的Time/Div到200us左右,可以清晰看到完整脉冲周期。
判断波形是否正确的三个点:
- 频率对不对。4.8kHz的方波,一个周期约208us,在200us一格的时间轴上应该看到差不多一格一个周期
- 幅值够不够。STM32引脚输出高电平是3.3V,波形顶部应该是平坦的3.3V,不会出现圆角或严重跌落
- 占空比是否随按键变化。按加速键,高电平宽度变宽;按减速键,高电平宽度变窄。如果这两点做不到,说明PWM配置有问题,或者按键根本没被扫描到
示波器在这个项目里是最核心的调试工具。我调试这类仿真时,第一件事永远是先看信号,再看动画。很多排错只要一上示波器,问题原因就自己浮出来了。
4.4 电机转没转怎么判断
电机转没转,不能只看动画。Proteus里的DC Motor模型在低速或者占空比极低时,动画看上去像没转,容易误判。我通常用三个信号综合判断:
第一,旋转动画。电机轴上如果有转动箭头或者标记,说明在转。第二,输出波形。把示波器通道接到L293D的OUT1脚,能看到跟随PWM输入的方波,幅值接近VS电压。第三,电流表。在VS电源线上串一个电流表,能看到电流值随占空比变化而变化。三个都正确,系统才算真正通了。
电流这一点特别容易被忽略。电机堵转、负载变化、占空比变化,电流都会有明显反应。Proteus仿真里加一个电流表非常方便,能帮你建立对"电机电流"的直觉,这对以后做实物大有帮助。
5. 电机不转?按这条链路逐个排查
5.1 第一步:确认芯片本身在运行
每次排障都从最简单的地方开始。先别管PWM,写个最朴素的GPIO点灯程序,把一个引脚输出高电平接个LED,如果LED亮了,说明芯片、时钟、hex加载链路全部正常。如果LED不亮,问题基本出在Proteus配置层面:hex路径有中文、Crystal Frequency不对、芯片供电没接全、NRST一直处于复位状态。
这个"先证明芯片能跑"的习惯,能把从源头上就错误的情况和后面复杂问题快速区分开。很多人一上来就抱着一大段PWM代码查寄存器配置,查了半天,其实芯片根本没运行。
5.2 第二步:确认PWM波形真的到达驱动器
芯片没问题,接着用示波器接PA6引脚。这里要特别提醒一个细节:程序启动后speed初始值是0,占空比是0%,PA6输出一直是低电平,示波器上看起来就是一条0V直线。这不一定是故障。你需要按一下加速按键,再观察波形。
如果按了加速键还是没有波形,常见的原因有:
- TIM3没有使能。检查有没有调用TIM_Cmd(TIM3, ENABLE)
- GPIO没有配置成复用功能。PA6还是普通推挽输出时,定时器波形出不来
- TIM_OC1Init没调用。通道1没有做比较输出初始化
- 代码里改了CCR但定时器没配置预装载,导致影子寄存器没有更新
从波形入手,一查一个准。
5.3 第三步:L293D周边常见接线错误
波形到了1,2EN引脚,电机还是不转,问题大概率在L293D周边。我见过最多的错误是:把PWM信号接到了IN1或者IN2,而EN引脚直接接VCC。这个接法下,电机要么全速转,要么不转,按键调速完全无效。原因就是本章前面那张真值表:EN决定输出是否跟随,如果EN固定为高,那输出就是恒定的高电平或者低电平,PWM根本没起作用。
另一个高频错误是1,2EN悬空。L293D的使能脚悬空时输入状态不确定,输出处于高阻或者不定状态,电机当然不会正常转。仿真里悬空尤其隐蔽,因为Proteus不会报错,只会表现为"莫名其妙不工作"。
排查时用两个电压探针,分别测OUT1和OUT2对GND的电压。正常正转调速时,OUT1应该是跟随PWM的高电平变化,OUT2应该是低电平,两者之间才有电压差,电机两端才有电流。如果OUT1和OUT2都是0V,检查VS有没有供12V;如果OUT1和OUT2都是高电平或者都是低电平,检查IN1和IN2的组合是否符合预期。
5.4 第四步:电机模型与电源匹配问题
排除前面所有问题后,再双击DC Motor元件检查属性。电机模型有一个额定电压参数,如果额定电压和目标供电电压差距太大,Proteus模型的转速计算公式会给出一个很不合理的值。比如供电12V但电机模型额定电压设成3V,仿真中电机可能会直接转飞或者表现异常。
我的做法是,DC Motor的额定电压设成12V,和VS保持一致。这样占空比50%时电机两端等效电压6V,模型计算出来的转速更符合物理直觉。
另外检查一下L293D的VS和VSS是不是都接了。VS是电机电源,VSS是逻辑电源。很多初学者只接VS,觉得"电机有电了"就行,但L293D内部逻辑部分没有VSS供电,输入信号全部无效,输出自然没有反应。
6. 从仿真走向实物:驱动方案与工程扩展
6.1 仿真和实物的真实差距
Proteus仿真能帮你验证逻辑正确性,但它呈现的是一个偏理想化的世界。仿真中不会有接触电阻,不会有电源压降,不会有大电流时地线上的干扰电压,也不会有电机换向时产生的反电动势尖峰。
实物中最需要注意的差距有三点:
第一,电机是感性负载,PWM关闭的瞬间,绕组电流不能突变,会产生反向电动势。这个尖峰可能达到电源电压的数倍。用L293D这类的集成芯片,内部已经集成续流二极管,问题不大;如果用分离MOS管搭H桥,必须在每个开关管两端并联续流二极管,否则MOS管很容易被打穿。
第二,电源的瞬时响应能力。电机启动电流远大于额定电流,如果电源余量不足,一上电电压就被拖垮,STM32可能复位,直接导致"一开电机单片机就重启"的诡异现象。
第三,逻辑电平和驱动芯片的匹配。STM32的GPIO高电平是3.3V,L293D的逻辑部分用5V供电,3.3V高电平可以满足TTL高电平阈值,可以直接驱动。但有些驱动芯片要求5V逻辑电平,直接接3.3V会识别不到高电平,这时需要电平转换电路。选驱动方案之前一定要查数据手册中的逻辑电平规格。
6.2 不同功率下的驱动芯片选型建议
L293D适合学习和小功率应用,驱动能力600mA,不能做大功率。实际项目里根据功率选择驱动方案,我的建议参考这张表:
| 方案 | 驱动能力 | 能否正反转 | 适合场景 |
|---|---|---|---|
| L293D | 600mA,峰值1.2A | 能 | 学习、小功率课设 |
| TB6612FNG | 1.2A,峰值3.2A | 能 | 3.3V逻辑直驱,机器人小车 |
| ULN2003 | 500mA/通道 | 不能 | 单向调速、步进电机 |
| 分离MOS管H桥 | 取决于MOS管 | 能 | 10A以上大功率电机 |
TB6612FNG我实际用过不少,体积小,逻辑高电平兼容3.3V,性能比L293D好,做小型智能车项目非常合适。大功率场景下,IR2104驱动半桥加MOS管,或者直接买集成驱动模块,效率更高,但调试复杂度也上升。作为学习者,先用L293D把PWM控制、正反转、H桥原理搞明白,后面换更高性能的芯片就是看数据手册、改改代码的事。
6.3 基于这个工程的三个扩展方向
这个工程本身是个很完整的基础框架,往上扩展很容易。
第一个方向是双路电机控制。TIM3的CH2也就是PA7引脚还有一路PWM输出,接L293D的3,4EN和3A、4A,就可以驱动两台电机,配合方向控制逻辑就能做一个简易差速小车平台。代码上只需要把TIM_OC1Init复制一份改成TIM_OC2Init,加两个方向引脚就行。
第二个方向是测速闭环。把普通电机换成带编码器输出的直流电机,Proteus库里有MOTOR-ENCODER模型,可以输出频率与转速成正比的脉冲。用STM32的TIM2编码器模式测量脉冲频率和方向,再和当前CCR值做PID运算,就能实现恒速控制。这是机器人项目里最常用的算法之一,直接从PWM开环跳进PID闭环,能理解一堆自动控制的概念。
第三个方向是把调试数据通过串口发出来。在代码中加个UART发送,每隔几百毫秒把当前速度、方向、占空比通过串口打印,Proteus的Virtual Terminal可以直接连接串口观察输出。这能帮你建立"程序里到底在发生什么"的可视化,也顺便把串口调试的基本功练了一遍。
这个项目再往后就是完整的电机驱动库,把PWM配置、方向控制、速度设置封装成几个函数,不管是做小车还是做机械臂,都能直接复用。
最后说一下我自己的习惯。每次搭这类仿真,我都是把"芯片能否运行、PWM波形是否正确、驱动输出是否有电压差"这三步当作固定流程,任何一步不明确,绝不往下走。这种办法看起来笨,实际最省时间。特别是Proteus里所有信号都能直接看,如果连示波器都不肯用,把波形确认清楚,等到了实物阶段排查成本会高出好几倍。这套流程跑通之后,你手里的不只是一个仿真工程,还有一套完整的电机控制调试方法论,做实物的时候会轻松很多。