☰
STM32F103C8T6与TB6612电机驱动实战:PWM调速与调试指南
2026/9/29 2:11:05 网站建设 项目流程

1. 从一堆吃灰的电机说起:为什么选TB6612而不是L298N

手头攒了好几个拆机的小型直流电机,有从旧打印机里拆的,有从玩具车上扒的,还有几个带编码器的减速电机。之前一直用L298N驱动,模块便宜、接线简单,但用久了问题就出来了:发热严重,压降大,电机转速明显比直接接电源时慢一截。后来换了TB6612模块,同样的电机、同样的电源,转速上去了,模块摸上去也只是温温的。这个对比太明显了,所以这次干脆把STM32F103C8T6和TB6612的组合从头到尾整理一遍。

STM32F103C8T6这颗芯片不用多介绍,Cortex-M3内核,72MHz主频,64KB Flash,20KB SRAM,LQFP48封装,市面上最小系统板十几块钱就能买到。它的定时器资源很够用,TIM1、TIM2、TIM3、TIM4都能输出PWM,其中TIM1是高级定时器,带互补输出和死区控制,驱动电机时用起来很顺手。TB6612FNG是东芝的电机驱动芯片,双通道输出,每通道持续电流1.2A,峰值3.2A,支持PWM频率最高100kHz,内置低压检测和热关断保护。和L298N相比,TB6612是MOSFET输出,导通电阻只有0.5Ω左右,效率高得多,而且不需要外接续流二极管。

这套组合适合谁呢?如果你在做智能小车、云台、自动门锁、传送带模型这类需要精确控制电机转速和方向的项目,或者单纯想学PWM调速的原理,这篇文章里的内容可以直接参考。我会从硬件接线讲到PWM配置,再到代码实现和实际调试中遇到的问题,尽量把每个环节说透。

注意:TB6612的VM引脚是电机电源输入,VCC是逻辑电源输入,两者必须都接,而且VM和VCC的电压范围不同,接反或漏接都会导致模块不工作。

2. 硬件连线:引脚分配与电源处理的几个关键决策

2.1 STM32F103C8T6的引脚选择逻辑

先确定用哪些引脚。STM32F103C8T6的PA0到PA7、PB0到PB1对应TIM3的通道1到4和部分重映射,PA8到PA11对应TIM1的通道1到4。我习惯把TIM1留给需要互补输出的场合,这次驱动两个直流电机,每个电机需要一路PWM加两路方向控制,所以选TIM3的通道1和通道2分别输出两路PWM。

具体分配如下:PA6接TIM3_CH1,对应电机A的PWM输入;PA7接TIM3_CH2,对应电机B的PWM输入。方向控制引脚用普通GPIO就行,PB0和PB1控制电机A的AIN1和AIN2,PB10和PB11控制电机B的BIN1和BIN2。STBY引脚接PB12,用来使能或关闭TB6612输出。这样分配的好处是TIM3的四个通道都在PA6、PA7、PB0、PB1上,但PB0和PB1我用来做方向控制了,所以PWM只用PA6和PA7两路,互不冲突。

这里有个细节:STM32F103C8T6的PB3和PB4默认是JTAG引脚,如果要用它们做普通GPIO,需要先关闭JTAG功能。我这次没用这两个引脚,但如果你需要更多IO,记得在代码里加上复用重映射的配置。

2.2 TB6612模块的接线要点

TB6612模块上通常有这些引脚:VM、VCC、GND、AO1、AO2、BO1、BO2、AIN1、AIN2、BIN1、BIN2、PWMA、PWMB、STBY。接线时注意以下几点:

  • VM接电机电源,范围2.5V到13.5V,根据你的电机额定电压来选。我用的电机是6V的,所以VM接6V电源。
  • VCC接3.3V或5V逻辑电源,和STM32的供电保持一致。如果STM32用3.3V供电,VCC也接3.3V,这样逻辑电平匹配,不需要电平转换。
  • GND必须和STM32的GND共地,否则PWM信号没有参考电平,模块不会正常工作。
  • AO1和AO2接电机A的两根线,BO1和BO2接电机B的两根线。如果发现电机转向和预期相反,把这两根线对调即可,不需要改代码。
  • STBY接高电平使能,接低电平待机。我把它接到PB12,方便用代码控制。

电源处理上有个坑:电机启动瞬间电流很大,如果和STM32共用一路电源,可能导致STM32复位。我的做法是电机电源和逻辑电源分开,用两路独立的稳压电源,或者至少在电机电源端并一个大电容(比如470μF)来吸收启动电流冲击。

2.3 一个容易被忽略的细节:PWM频率的选择

TB6612支持最高100kHz的PWM频率,但并不是越高越好。频率太低,电机会有可闻的啸叫声;频率太高,开关损耗增加,模块发热。经过实测,10kHz到20kHz是比较合适的范围,既超出人耳听觉范围(20kHz以上听不到),又不会让模块明显发热。

STM32F103C8T6的TIM3挂在APB1总线上,时钟频率是72MHz。要得到20kHz的PWM频率,预分频器和自动重装载值的计算如下:72MHz / (预分频器+1) / (自动重装载值+1) = 20kHz。取预分频器为0,则自动重装载值为3600-1=3599。这样PWM分辨率是3600级,足够精细了。如果取预分频器为71,则定时器时钟为1MHz,自动重装载值为50-1=49,分辨率只有50级,调速时会有明显的台阶感。所以预分频器越小,分辨率越高,但自动重装载值不能超过65535。

3. PWM调速的底层逻辑:从定时器计数到占空比

3.1 STM32定时器输出PWM的原理

STM32的通用定时器TIM3本质上是一个向上计数的计数器,从0数到自动重装载值(ARR),然后重新从0开始。在计数过程中,计数器值(CNT)会和捕获比较寄存器(CCR)比较,当CNT小于CCR时,输出高电平;当CNT大于等于CCR时,输出低电平。这样输出的方波频率由ARR决定,占空比由CCR决定。

占空比的计算公式是:占空比 = CCR / (ARR+1)。比如ARR设为3599,CCR设为1800,占空比就是1800/3600=50%。电机两端的平均电压就是电源电压乘以占空比,所以改变CCR就能改变电机转速。

这里要注意:CCR的值不能超过ARR+1,否则输出一直是高电平,电机全速运转。我在调试时曾经把CCR设成4000,而ARR是3599,结果电机直接满速,差点把桌上的东西撞飞。所以代码里最好加一个限幅保护。

3.2 电机方向控制的真值表

TB6612的方向控制逻辑很清晰,AIN1和AIN2的组合决定电机A的转向:

AIN1AIN2电机状态
00停止(滑行)
01正转
10反转
11刹车(短接制动)

停止和刹车的区别在于:停止是让电机两端悬空,电机靠惯性滑行到停;刹车是让电机两端短接,产生反向电动势阻碍转动,停得更快。实际项目中,如果要求快速停车,用刹车模式;如果要求平稳停车,用停止模式。

PWM信号加在PWMA引脚上,方向引脚决定转向,两者配合就能实现正反转调速。比如正转时AIN1=0、AIN2=1,PWMA输出50%占空比,电机就以半速正转。

3.3 死区时间:一个不需要但值得了解的概念

TIM1是高级定时器,支持互补输出和死区插入。死区时间是指互补的两个PWM信号在切换时,先关断一个再开通另一个,中间插入一段两者都关断的时间,防止上下桥臂直通短路。TB6612内部已经集成了死区控制,所以用TIM3这种通用定时器输出单路PWM就足够了,不需要额外配置死区。但如果你用分立MOS管搭H桥,死区时间就是必须考虑的问题,通常设置在几百纳秒到几微秒之间,具体取决于MOS管的开关速度。

4. 代码实现:从寄存器配置到可复用的电机驱动库

4.1 定时器PWM初始化的完整代码

先看TIM3的PWM初始化。我用的是标准外设库,如果你用HAL库,逻辑类似,只是函数名不同。

#include "stm32f10x.h" void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 配置PA6和PA7为复用推挽输出 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、PB10、PB11、PB12为普通推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOB, &GPIO_InitStructure); // 定时器基础配置 TIM_TimeBaseStructure.TIM_Period = 3599; // ARR值,决定PWM频率 TIM_TimeBaseStructure.TIM_Prescaler = 0; // 预分频器 TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // 配置通道1和通道2为PWM模式 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比为0 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &TIM_OCInitStructure); TIM_OC2Init(TIM3, &TIM_OCInitStructure); // 使能预装载寄存器 TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); // 使能定时器 TIM_Cmd(TIM3, ENABLE); }

这段代码里,TIM_Period设为3599,TIM_Prescaler设为0,PWM频率就是72MHz/3600=20kHz。TIM_Pulse初始为0,电机不转。TIM_OCMode_PWM1表示CNT小于CCR时输出高电平,这是最常用的模式。

4.2 电机驱动函数的封装

把电机控制封装成几个函数,用起来更方便。我定义了电机A和电机B的枚举,以及正转、反转、停止、刹车四种状态。

#define MOTOR_A 0 #define MOTOR_B 1 #define FORWARD 0 #define BACKWARD 1 #define STOP 2 #define BRAKE 3 void Motor_SetDirection(uint8_t motor, uint8_t dir) { if (motor == MOTOR_A) { switch (dir) { case FORWARD: GPIO_ResetBits(GPIOB, GPIO_Pin_0); GPIO_SetBits(GPIOB, GPIO_Pin_1); break; case BACKWARD: GPIO_SetBits(GPIOB, GPIO_Pin_0); GPIO_ResetBits(GPIOB, GPIO_Pin_1); break; case STOP: GPIO_ResetBits(GPIOB, GPIO_Pin_0 | GPIO_Pin_1); break; case BRAKE: GPIO_SetBits(GPIOB, GPIO_Pin_0 | GPIO_Pin_1); break; } } else if (motor == MOTOR_B) { switch (dir) { case FORWARD: GPIO_ResetBits(GPIOB, GPIO_Pin_10); GPIO_SetBits(GPIOB, GPIO_Pin_11); break; case BACKWARD: GPIO_SetBits(GPIOB, GPIO_Pin_10); GPIO_ResetBits(GPIOB, GPIO_Pin_11); break; case STOP: GPIO_ResetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11); break; case BRAKE: GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11); break; } } } void Motor_SetSpeed(uint8_t motor, uint16_t speed) { if (speed > 3599) speed = 3599; // 限幅保护 if (motor == MOTOR_A) { TIM_SetCompare1(TIM3, speed); } else if (motor == MOTOR_B) { TIM_SetCompare2(TIM3, speed); } } void Motor_Enable(void) { GPIO_SetBits(GPIOB, GPIO_Pin_12); // STBY拉高 } void Motor_Disable(void) { GPIO_ResetBits(GPIOB, GPIO_Pin_12); // STBY拉低 }

这套函数用起来很直观:先调用Motor_Enable()使能模块,然后Motor_SetDirection()设置方向,最后Motor_SetSpeed()设置速度。速度值从0到3599对应0%到100%占空比。

4.3 一个简单的调速测试程序

写个主程序测试一下,让电机先正转加速,再反转加速,循环往复。

int main(void) { uint16_t speed; PWM_Init(); Motor_Enable(); while (1) { // 正转加速 Motor_SetDirection(MOTOR_A, FORWARD); for (speed = 0; speed < 3600; speed += 100) { Motor_SetSpeed(MOTOR_A, speed); Delay_ms(50); } // 正转减速 for (speed = 3600; speed > 0; speed -= 100) { Motor_SetSpeed(MOTOR_A, speed); Delay_ms(50); } // 反转加速 Motor_SetDirection(MOTOR_A, BACKWARD); for (speed = 0; speed < 3600; speed += 100) { Motor_SetSpeed(MOTOR_A, speed); Delay_ms(50); } // 反转减速 for (speed = 3600; speed > 0; speed -= 100) { Motor_SetSpeed(MOTOR_A, speed); Delay_ms(50); } // 刹车停止 Motor_SetDirection(MOTOR_A, BRAKE); Delay_ms(500); } }

Delay_ms函数可以用SysTick实现,这里不展开。实际跑起来,电机会从静止逐渐加速到全速,然后减速到停,再反向加速,效果很平滑。如果发现加速过程中有顿挫感,可能是速度步进太大,把100改成50或更小会更细腻。

5. 调试中遇到的真实问题与排查过程

5.1 电机不转:从电源查到信号

第一次接线完成后,上电,电机纹丝不动。排查过程如下:

先测VM和VCC电压,VM有6V,VCC有3.3V,正常。再测STBY引脚,发现是低电平,原来PB12初始化后默认输出低电平,而我在主程序里忘了调用Motor_Enable()。加上之后,电机还是不动。

接着用示波器看PA6引脚,发现没有PWM波形。检查代码,发现PWM_Init()里使能了TIM3时钟,但忘了使能GPIOA的时钟。补上RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE)之后,波形出来了。

最后电机转了,但转速很慢。测PWMA引脚波形,占空比只有10%左右。检查代码,发现TIM_SetCompare1的参数传错了,把速度值传成了方向值。修正后一切正常。

这个排查过程说明:电机不转的原因可能有多层,从电源到使能信号到PWM输出,逐级排查才能定位问题。我的习惯是先用万用表测静态电压,再用示波器看动态波形,最后查代码逻辑。

5.2 电机启动时STM32复位:电源干扰的解决

电机能转之后,发现每次电机启动瞬间,STM32会复位,串口打印的信息从头开始。这是典型的电源干扰问题。电机启动电流大,拉低了电源电压,导致STM32的欠压复位。

解决方法有三个:一是电机电源和逻辑电源分开供电,我用了一个独立的6V电源给TB6612的VM,STM32还是用USB供电;二是在电机电源端并一个大电容,我并了一个470μF的电解电容和一个0.1μF的陶瓷电容,大电容吸收低频波动,小电容滤高频噪声;三是在TB6612的VM和GND之间加一个TVS二极管,钳位电压尖峰。三个措施一起上,复位问题彻底解决。

提示:如果电机和STM32必须共用电源,至少要在电源入口处加LC滤波电路,并且PCB布局时电机电源走线要远离MCU。

5.3 PWM调速时的非线性:为什么低速时电机不转

把占空比从0慢慢调大,发现占空比低于15%时电机不转,只有嗡嗡声。这是因为电机有静摩擦力,启动需要一定的扭矩,而低速时PWM的平均电压太低,产生的电流不足以克服静摩擦。

解决办法有两个:一是提高PWM频率到20kHz以上,减少电流纹波,但效果有限;二是加入启动补偿,在低速时给一个短暂的满占空比脉冲,让电机先转起来,再降到目标速度。我在代码里加了一个简单的启动函数:

void Motor_StartWithBoost(uint8_t motor, uint16_t targetSpeed) { Motor_SetSpeed(motor, 3599); // 先全速 Delay_ms(50); // 持续50ms Motor_SetSpeed(motor, targetSpeed); // 再降到目标速度 }

这样电机就能在低速下平稳启动了。当然,如果你的项目对低速性能要求高,建议用带编码器的电机做闭环控制,通过PID调节来保证低速扭矩。

5.4 TB6612发热问题:从PWM频率和散热入手

连续运行半小时后,TB6612模块有点烫手。测电流,电机额定电流0.3A,远低于模块的1.2A持续电流,不应该这么热。分析原因,可能是PWM频率太低导致开关损耗大。我把频率从1kHz提高到20kHz,发热明显减轻。另外,模块背面的散热焊盘要焊接到PCB的铜箔上,如果悬空,散热效果差很多。我在模块下面垫了一块铝片,用导热胶粘住,温度降了十几度。

6. 进阶玩法:从单电机到多电机协同

6.1 用TIM1实现四路PWM输出

如果需要驱动四个电机,TIM3只有四个通道,但PB0和PB1被方向控制占用了,所以PWM不够用。这时候可以用TIM1的四个通道,PA8、PA9、PA10、PA11分别输出四路PWM,方向控制用其他GPIO。TIM1是高级定时器,配置方法和TIM3类似,但需要额外使能TIM_CtrlPWMOutputs(TIM1, ENABLE),否则没有输出。

6.2 加入编码器实现闭环调速

开环调速的精度受电源电压和负载影响很大,加上编码器就能做闭环。STM32F103C8T6的TIM2和TIM4支持编码器模式,可以直接读取编码器脉冲。把编码器A相和B相接到TIM2的CH1和CH2,配置为编码器接口模式,就能得到电机的实际转速。然后用PID算法调节PWM占空比,让实际转速跟随目标转速。这部分内容比较多,以后可以单独写一篇。

6.3 用FreeRTOS做多任务调度

如果项目里还要处理按键、显示、通信等任务,裸机的前后台架构会力不从心。STM32F103C8T6的20KB SRAM跑FreeRTOS绰绰有余。可以把电机控制、按键扫描、OLED显示分别放到不同的任务里,用队列传递速度设定值。FreeRTOS的移植在STM32F103C8T6上很成熟,网上资料也多,这里不展开。

7. 几个让代码更可靠的小技巧

第一个技巧是给PWM输出加软启动。每次改变速度设定值时,不要直接跳到目标值,而是用斜坡函数逐步逼近。这样电机电流变化平缓,对电源的冲击小,机械结构也不会因为突然加速而抖动。实现方法很简单,在定时器中断里每10ms调整一次CCR值,每次增加或减少固定步长,直到达到目标值。

第二个技巧是方向切换时先刹车再换向。如果电机正在正转,直接切到反转,会有很大的反向电流,可能触发TB6612的过流保护。正确的做法是先刹车100ms,等电机停稳后再切换方向。代码里可以在Motor_SetDirection函数里加一个判断,如果当前方向和目标方向相反,先执行刹车延时再设置新方向。

第三个技巧是定期检查STBY引脚状态。如果代码里有多处控制STBY,可能会因为逻辑冲突导致模块意外关闭。我的做法是把STBY的控制集中在一个函数里,并且用一个全局变量记录当前状态,避免重复操作。

第四个技巧是给电机电源加保险丝。虽然TB6612有保护,但万一电机堵转时间过长,电流持续超标,模块还是会烧。加一个自恢复保险丝,成本几毛钱,能省下换模块的麻烦。

这套STM32F103C8T6加TB6612的方案,我从最初的点灯测试到现在的多电机协同,前后折腾了大概两周。中间烧过一个TB6612模块(电源接反了),烧过一个STM32最小系统板(电机电源串入了5V到3.3V引脚),都是血泪教训。现在这套代码已经稳定运行在我的小车上,每天跑半个小时,没再出过问题。如果你也在做类似的项目,希望这些经验能帮你少走点弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询