资讯动态

STM32F103C8T6与TB6612电机驱动实战:PWM调速与调试指南

发布时间:2026/9/29 2:11:18 来源:尧图企业网站定制
1. 从一堆吃灰的电机说起为什么选TB6612而不是L298N手头攒了好几个拆机的小型直流电机有从旧打印机里拆的有从玩具车上扒的还有几个带编码器的减速电机。之前一直用L298N驱动模块便宜、接线简单但用久了问题就出来了发热严重压降大电机转速明显比直接接电源时慢一截。后来换了TB6612模块同样的电机、同样的电源转速上去了模块摸上去也只是温温的。这个对比太明显了所以这次干脆把STM32F103C8T6和TB6612的组合从头到尾整理一遍。STM32F103C8T6这颗芯片不用多介绍Cortex-M3内核72MHz主频64KB Flash20KB SRAMLQFP48封装市面上最小系统板十几块钱就能买到。它的定时器资源很够用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和AIN2PB10和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-13599。这样PWM分辨率是3600级足够精细了。如果取预分频器为71则定时器时钟为1MHz自动重装载值为50-149分辨率只有50级调速时会有明显的台阶感。所以预分频器越小分辨率越高但自动重装载值不能超过65535。3. PWM调速的底层逻辑从定时器计数到占空比3.1 STM32定时器输出PWM的原理STM32的通用定时器TIM3本质上是一个向上计数的计数器从0数到自动重装载值ARR然后重新从0开始。在计数过程中计数器值CNT会和捕获比较寄存器CCR比较当CNT小于CCR时输出高电平当CNT大于等于CCR时输出低电平。这样输出的方波频率由ARR决定占空比由CCR决定。占空比的计算公式是占空比 CCR / (ARR1)。比如ARR设为3599CCR设为1800占空比就是1800/360050%。电机两端的平均电压就是电源电压乘以占空比所以改变CCR就能改变电机转速。这里要注意CCR的值不能超过ARR1否则输出一直是高电平电机全速运转。我在调试时曾经把CCR设成4000而ARR是3599结果电机直接满速差点把桌上的东西撞飞。所以代码里最好加一个限幅保护。3.2 电机方向控制的真值表TB6612的方向控制逻辑很清晰AIN1和AIN2的组合决定电机A的转向AIN1AIN2电机状态00停止滑行01正转10反转11刹车短接制动停止和刹车的区别在于停止是让电机两端悬空电机靠惯性滑行到停刹车是让电机两端短接产生反向电动势阻碍转动停得更快。实际项目中如果要求快速停车用刹车模式如果要求平稳停车用停止模式。PWM信号加在PWMA引脚上方向引脚决定转向两者配合就能实现正反转调速。比如正转时AIN10、AIN21PWMA输出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设为3599TIM_Prescaler设为0PWM频率就是72MHz/360020kHz。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有6VVCC有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的VMSTM32还是用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引脚都是血泪教训。现在这套代码已经稳定运行在我的小车上每天跑半个小时没再出过问题。如果你也在做类似的项目希望这些经验能帮你少走点弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑