1. 为什么选STM32F103C8T6加TB6612这套组合
1.1 一套被反复验证的电机控制入门方案
STM32F103C8T6这颗芯片在嵌入式圈子里几乎是“人手一块”的存在,72MHz主频、64KB Flash、20KB SRAM,加上丰富的高级定时器资源,拿来做直流电机PWM调速属于杀鸡用牛刀。而TB6612FNG作为东芝推出的双通道电机驱动芯片,相比老一代的L298N,效率高出一大截,发热量小,支持1.2A持续电流、3.2A峰值电流,关键是逻辑电平兼容3.3V,可以直接和STM32的IO口对接,不需要额外的电平转换电路。
这套组合解决的核心问题很明确:让单片机用弱电信号去控制大功率直流电机的转速和方向。STM32负责产生PWM波形和方向控制信号,TB6612负责把这些小信号放大成能驱动电机的功率输出。整个链路清晰、成本低、资料多,特别适合刚接触嵌入式电机控制的朋友,也适合需要快速搭建原型验证的工程师。
我见过太多人一上来就想用MOS管搭H桥,结果不是烧管子就是波形不对。TB6612把H桥、死区控制、过流保护都集成好了,你只需要关心PWM占空比和方向引脚,省下来的时间可以花在算法和控制逻辑上。这篇文章我会把硬件接线、CubeMX配置、PWM调速代码、常见问题排查全部讲透,代码可以直接复制到你的工程里用。
1.2 核心器件选型背后的逻辑
先说说为什么是STM32F103C8T6而不是其他型号。这颗芯片有四个定时器,其中TIM1和TIM8是高级定时器,TIM2到TIM5是通用定时器。做直流电机调速,你至少需要一个定时器输出PWM,如果要做双电机控制,就需要两个独立通道。C8T6的TIM1有四个通道,TIM2、TIM3、TIM4各四个通道,资源完全够用。而且它的引脚布局在最小系统板上非常规整,PA8、PA9、PA10、PA11这些常用PWM输出脚都引出来了,接线方便。
TB6612的选型理由更直接:它内部集成了两个H桥,可以同时驱动两个直流电机或者一个步进电机。每个通道有三个控制引脚——AIN1、AIN2控制方向,PWMA控制速度。真值表很简单:AIN1高电平、AIN2低电平,电机正转;AIN1低电平、AIN2高电平,电机反转;两个都是低电平,电机停止(滑行);两个都是高电平,电机刹车(短接制动)。PWM频率建议在10kHz到20kHz之间,太低会有可闻噪声,太高开关损耗增加。
这里有个细节很多人忽略:TB6612的VM引脚是电机电源输入,范围2.5V到13.5V,VCC是逻辑电源,2.7V到5.5V。如果你用3.3V的单片机,VCC接3.3V,VM接电池或电源适配器。千万不能把VM接到3.3V上然后去驱动大电机,电压不够电机转不起来,而且电流会很大。我一般建议VM比电机额定电压高1V到2V,留一点余量给H桥的压降。
2. 硬件连接与电路设计要点
2.1 最小系统板与TB6612的接线表
先上一张我实际项目里用的接线表,你可以直接照着连。STM32F103C8T6最小系统板的引脚标号可能因厂家不同略有差异,但PA、PB口的定义是一样的。
| STM32引脚 | TB6612引脚 | 功能说明 |
|---|---|---|
| PA8 | PWMA | TIM1_CH1,PWM调速信号 |
| PB0 | AIN1 | 方向控制1 |
| PB1 | AIN2 | 方向控制2 |
| PA9 | PWMB | TIM1_CH2,第二路PWM |
| PB2 | BIN1 | 第二路方向控制1 |
| PB3 | BIN2 | 第二路方向控制2 |
| 3.3V | VCC | 逻辑电源 |
| GND | GND | 共地 |
| 12V电源正 | VM | 电机电源 |
| 12V电源负 | GND | 电机电源地 |
| PA0 | STBY | 使能引脚,高电平工作 |
STBY引脚必须拉高,否则TB6612不工作。我习惯把它接到一个GPIO上,这样可以在软件里控制驱动器的使能状态,紧急情况下可以快速切断电机输出。如果你不想占用额外引脚,直接接到3.3V也可以,但就失去了软件使能的能力。
电机输出端AO1、AO2接直流电机的两根线,BO1、BO2接第二个电机。注意电机线不要接反,虽然可以通过软件反转方向,但接线时还是按颜色对应好,方便调试。
2.2 电源设计与滤波电容的取舍
电源部分是最容易出问题的地方。TB6612的VM和VCC必须分开供电,虽然它们可以共地,但电压不同。我见过有人把VM和VCC都接到5V,结果电机一转单片机就复位,原因是电机启动瞬间电流冲击导致电压跌落,单片机供电被拉低。
正确的做法是:单片机用独立的3.3V稳压供电,比如最小系统板上的AMS1117-3.3,输入接5V或者12V都行。TB6612的VCC也接3.3V,VM接电机电源。在TB6612的VM和GND之间并联一个100uF的电解电容和一个0.1uF的陶瓷电容,电解电容吸收低频波动,陶瓷电容滤高频噪声。电机两端再并一个0.1uF的电容,可以减少电机换向时产生的火花干扰。
如果你用电池供电,比如3节18650串联得到11.1V,要注意电池放电到9V时电机可能已经转不动了。我实测下来,12V的直流电机在9V时转速下降约30%,扭矩下降更明显。所以如果对转速稳定性有要求,建议用稳压电源或者锂电池加升压模块。
注意:TB6612的VM绝对最大电压是15V,超过会永久损坏芯片。电机电源不要超过13.5V,留一点安全余量。
2.3 电机选型与TB6612的匹配计算
不是所有直流电机都能直接接TB6612。你需要看两个参数:额定电压和额定电流。TB6612每通道持续电流1.2A,峰值3.2A。如果你用的电机堵转电流超过3.2A,TB6612会触发过流保护或者直接烧掉。
举个例子,常见的130电机额定电压3V到6V,空载电流0.1A左右,堵转电流1A到2A,TB6612完全能带动。但如果是775电机,额定电压12V,空载电流0.5A,堵转电流可能到10A以上,这就必须用更大功率的驱动器,比如BTS7960或者MOS管搭建的H桥。
计算电机功率:P = U × I。12V × 1A = 12W,TB6612的功耗大约是输出电流的平方乘以导通电阻。TB6612的导通电阻典型值0.5Ω,1A电流下功耗0.5W,芯片会有点温热但不需要散热片。如果持续1.2A,功耗0.72W,最好加一个小散热片。
我一般建议留50%的余量,也就是电机持续电流不要超过0.6A,这样TB6612基本不发热,系统稳定性最好。
3. CubeMX配置与PWM参数计算
3.1 定时器配置与频率占空比计算
打开CubeMX,选好STM32F103C8T6芯片,第一步配置时钟。外部晶振8MHz,PLL倍频到72MHz。APB1定时器时钟72MHz,APB2定时器时钟72MHz。TIM1挂在APB2上,时钟就是72MHz。
配置TIM1的Channel1为PWM Generation CH1,模式选PWM Mode 1。Prescaler预分频器设为71,Counter Period自动重装载值设为99。这样PWM频率 = 72MHz / (71+1) / (99+1) = 72,000,000 / 72 / 100 = 10,000Hz = 10kHz。占空比分辨率是100级,也就是1%的精度。
如果你想要更高的调速精度,可以把Counter Period设为999,预分频器设为71,频率变成1kHz,分辨率1000级。但1kHz会有明显的电机啸叫声,人耳能听到。我建议用10kHz,分辨率100级对大多数应用足够了。如果非要高分辨率又不想有噪声,可以把预分频器设为35,Counter Period设为199,频率 = 72MHz / 36 / 200 = 10kHz,分辨率200级。
占空比的计算公式:CCR1 = (Counter Period + 1) × 占空比百分比。比如Counter Period=99,要50%占空比,CCR1 = 100 × 0.5 = 50。要100%占空比,CCR1 = 100,但实际写100会一直输出高电平,因为计数器从0到99,比较值100永远匹配不上,输出保持有效电平。所以最大占空比写99就是99%,写100就是100%。
3.2 GPIO配置与方向控制逻辑
PB0、PB1配置为GPIO_Output,推挽输出,低电平初始。PB2、PB3同样配置。PA0配置为GPIO_Output,初始高电平,控制STBY使能。
方向控制的真值表再强调一遍:
| AIN1 | AIN2 | 电机状态 |
|---|---|---|
| 0 | 0 | 停止(滑行) |
| 0 | 1 | 反转 |
| 1 | 0 | 正转 |
| 1 | 1 | 刹车(短接制动) |
刹车和停止的区别:刹车是电机两端短接,电机快速停转,适合需要急停的场景。停止是电机两端悬空,电机靠惯性慢慢停。我一般在换向前用刹车,减少机械冲击。
3.3 代码生成与工程结构
CubeMX生成代码后,在main.c里添加自己的逻辑。我习惯把电机控制封装成独立的motor.c和motor.h,这样代码结构清晰,也方便移植到其他项目。
// motor.h #ifndef __MOTOR_H #define __MOTOR_H #include "main.h" void Motor_Init(void); void Motor_SetSpeed(uint8_t motor_id, int16_t speed); #endif// motor.c #include "motor.h" extern TIM_HandleTypeDef htim1; void Motor_Init(void) { HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // STBY高电平 } void Motor_SetSpeed(uint8_t motor_id, int16_t speed) { // speed范围-100到100,负数反转 uint16_t pwm_val = 0; if(speed >= 0) { pwm_val = speed; if(motor_id == 1) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); } else { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_RESET); } } else { pwm_val = -speed; if(motor_id == 1) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_SET); } } if(pwm_val > 100) pwm_val = 100; if(motor_id == 1) { __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pwm_val); } else { __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, pwm_val); } }这段代码里,speed参数用有符号16位整数,正数正转,负数反转,绝对值表示占空比百分比。调用Motor_SetSpeed(1, 50)就是电机1以50%占空比正转,Motor_SetSpeed(1, -30)就是30%占空比反转。
4. 实操调试与常见问题排查
4.1 上电前的检查清单
每次焊完板子或者接完线,上电前我一定做这几件事:
- 用万用表蜂鸣档测VM和GND是否短路,VCC和GND是否短路。短路了上电必炸。
- 测STM32的3.3V输出是否正常,一般3.25V到3.35V之间。
- 测TB6612的VCC引脚电压是否3.3V,VM引脚电压是否电机额定电压。
- 检查STBY引脚是否拉高,悬空的话TB6612不工作。
- 检查电机线是否接在AO1、AO2上,不要接到BO1、BO2去了。
我有个朋友曾经把电机接到VM和GND上,一上电电机直接全速转,差点把桌子上的东西打翻。所以接线一定要对照原理图,不要凭感觉。
4.2 电机不转的排查流程
电机不转是最常见的问题,按这个顺序查:
第一步,量TB6612的VM电压。如果VM没有电压,查电源和接线。如果VM有电压但电机不转,继续下一步。
第二步,量STBY引脚电压。如果是低电平,TB6612处于待机模式,所有输出关闭。把STBY拉高。
第三步,量AIN1和AIN2的电压。如果两个都是低电平,电机处于停止状态。检查代码里是否设置了方向。如果两个都是高电平,电机处于刹车状态,也不会转。
第四步,量PWMA引脚的波形。用示波器看有没有PWM输出,频率是不是10kHz,占空比是不是你设置的值。如果没有波形,检查定时器是否启动,HAL_TIM_PWM_Start是否调用。
第五步,量AO1和AO2之间的电压。如果有PWM波形但电机不转,可能是电机坏了或者线断了。用万用表测电机两端电阻,正常应该在几欧姆到几十欧姆之间,无穷大就是断路。
4.3 PWM调速线性度与死区问题
实际调试你会发现,占空比从0%加到20%,电机可能完全不转,这就是电机的启动死区。因为电机有静摩擦力和齿槽转矩,占空比太低时扭矩不够克服阻力。不同电机的死区不一样,130电机大概在15%到25%,370电机在10%到20%,775电机在5%到10%。
解决办法有两个:一是软件补偿,在输出PWM之前加一个死区补偿值。比如死区是20%,你设置10%占空比时,实际输出30%。二是用闭环控制,加编码器测速,用PID调节。开环控制简单够用,闭环控制精度高但复杂。
我一般用软件补偿,代码里加一个dead_zone变量,根据电机实测调整。比如:
#define DEAD_ZONE 20 void Motor_SetSpeed(uint8_t motor_id, int16_t speed) { uint16_t pwm_val = 0; if(speed > 0) { pwm_val = speed + DEAD_ZONE; // 方向设置... } else if(speed < 0) { pwm_val = -speed + DEAD_ZONE; // 方向设置... } if(pwm_val > 100) pwm_val = 100; // 设置CCR... }这样设置10%占空比时,实际输出30%,电机就能转起来。但要注意,补偿后最大占空比会超过100%,需要限幅。
4.4 电机干扰导致单片机复位的处理
电机是很大的干扰源,换向时会产生尖峰电压和电磁辐射。如果发现电机一转单片机就复位或者程序跑飞,说明干扰通过电源或者空间耦合进来了。
电源干扰的解决办法:在TB6612的VM和GND之间并一个1000uF的电解电容,电机两端并0.1uF陶瓷电容,单片机电源入口加LC滤波。空间干扰的解决办法:电机线双绞,远离信号线,TB6612尽量靠近电机放置,STM32和TB6612之间用短线连接。
我遇到过一次电机一转OLED就花屏,后来在电机两端并了0.1uF电容,问题解决。还有一次电机一转串口就收到乱码,把电机线双绞后好了。这些经验都是踩坑踩出来的。
4.5 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 电机完全不转 | STBY低电平 | 拉高STBY |
| 电机完全不转 | VM无电压 | 检查电机电源 |
| 电机只往一个方向转 | 方向引脚接错 | 检查AIN1/AIN2接线 |
| 电机转速不可调 | PWM未启动 | 调用HAL_TIM_PWM_Start |
| 电机转速不可调 | CCR值未更新 | 检查__HAL_TIM_SET_COMPARE |
| 电机有啸叫声 | PWM频率太低 | 提高到10kHz以上 |
| 单片机复位 | 电源干扰 | 加滤波电容 |
| TB6612发热严重 | 电流过大 | 换更大功率驱动器 |
| 电机启动困难 | 死区未补偿 | 加死区补偿值 |
| 电机反转无力 | 电源电压不足 | 提高VM电压 |
5. 进阶玩法与项目扩展
5.1 编码器闭环调速的实现思路
开环调速的精度受电池电压、负载变化影响很大。如果你需要精确控制转速,加一个编码器做闭环是必然选择。常见的增量式编码器输出A、B两相正交信号,STM32的定时器有编码器模式,可以直接读取计数。
配置TIM2为编码器模式,PA0和PA1接编码器的A、B相。定时器会计数,正转加,反转减。每100ms读一次计数值,算出实际转速,和设定值比较,用PID调节PWM占空比。
PID参数整定是个经验活。我一般先用纯比例控制,Kp从0.1开始加,直到转速有轻微振荡,然后加微分Kd抑制振荡,最后加积分Ki消除静差。具体数值和电机、负载有关,没有万能参数。
5.2 多电机协同与差速转向
TB6612有两个通道,可以驱动两个电机做差速转向。比如小车左轮接A通道,右轮接B通道。直行时两个电机同速正转,左转时左轮减速或反转,右轮正转。差速转向的代码逻辑:
void Car_Move(int16_t left_speed, int16_t right_speed) { Motor_SetSpeed(1, left_speed); Motor_SetSpeed(2, right_speed); } void Car_Forward(uint8_t speed) { Car_Move(speed, speed); } void Car_TurnLeft(uint8_t speed) { Car_Move(speed / 2, speed); } void Car_TurnRight(uint8_t speed) { Car_Move(speed, speed / 2); } void Car_SpinLeft(uint8_t speed) { Car_Move(-speed, speed); }这种差速转向在智能小车、机器人底盘上非常常见。注意两个电机的死区可能不一样,需要分别补偿,否则直行时会跑偏。
5.3 用FreeRTOS做多任务电机控制
如果你的项目里电机控制只是其中一个任务,还需要同时处理传感器、通信、显示等,上FreeRTOS会方便很多。STM32F103C8T6的20KB RAM跑FreeRTOS绰绰有余。
我一般创建三个任务:电机控制任务、传感器采集任务、通信任务。电机控制任务优先级最高,每10ms执行一次,读取目标速度和实际速度,计算PID,更新PWM。传感器任务优先级中等,每50ms读一次编码器。通信任务优先级最低,处理串口命令。
任务间通信用队列或者全局变量加互斥锁。注意PWM更新要在任务里做,不要在中断里做,避免中断嵌套问题。
5.4 项目扩展方向
这套基础方案可以扩展出很多玩法。加一个蓝牙模块,手机APP控制转速和方向。加一个OLED屏幕,显示当前转速和占空比。加一个电流传感器,做堵转保护。加一个温度传感器,监测电机温度。加一个WS2812灯带,用PWM加DMA驱动,做状态指示。
我最近做的一个项目是用STM32F103C8T6加TB6612驱动两个直流电机做云台,配合MPU6050做姿态稳定。电机控制部分就是这篇文章的内容,加上PID和姿态解算,效果还不错。所以别小看这个基础方案,它是很多复杂项目的地基。
提示:TB6612的PWM频率不要超过100kHz,数据手册推荐范围是10kHz到20kHz。超过100kHz开关损耗急剧增加,芯片会严重发热。
6. 我个人在实际操作中的几点体会
调试电机控制最忌讳的就是一上来就写复杂代码。我建议先把PWM输出调通,用示波器确认波形正确,再接TB6612,再空载测试电机,最后带负载。每一步都确认无误再进行下一步,这样出问题容易定位。
还有一点,TB6612的GND和STM32的GND必须共地,否则逻辑信号没有参考电平,TB6612会乱动作。我见过有人用两个独立电源,一个给单片机,一个给电机,但忘了共地,结果电机时转时不转,查了半天才发现是地没接。
最后分享一个小技巧:在TB6612的VM和GND之间并一个TVS二极管,比如SMBJ15A,可以吸收电机换向时产生的尖峰电压,保护TB6612不被击穿。这个二极管成本几毛钱,但能大幅提高系统可靠性。我所有用TB6612的项目都会加这个保护,至今没有烧过驱动芯片。