第一次拿到TB6612FNG这个小黑盒子时,我盯着上面密密麻麻的引脚有点发懵。这玩意儿比传统的L298N体积小了近一半,但性能却强了不少——最大1.2A持续电流输出,3.2A峰值电流,效率高达95%,关键是几乎不发热。这让我想起去年用L298N做小车时,散热片烫到能煎鸡蛋的尴尬场景。
模块正面印着的引脚定义特别实用:
最让我惊喜的是STBY(Standby)引脚的设计。有次调试时电机突然疯转,差点把测试台掀翻,后来发现是忘记初始化STBY引脚。现在我的代码里一定会先写HAL_GPIO_WritePin(STBY_GPIO_Port, STBY_Pin, GPIO_PIN_RESET),等所有初始化完成再启用,安全系数直接拉满。
去年给学弟演示时,我们连着烧了三块STM32F103,最后发现是PWMA接错了定时器通道。现在我的工作台上永远贴着张接线速查表:
| 模块引脚 | STM32F103C8T6连接点 | 注意事项 |
|---|---|---|
| PWMA | PA6 (TIM3_CH1) | 必须带~的PWM引脚 |
| AIN1 | PB12 | 普通GPIO即可 |
| AIN2 | PB13 | 推挽输出模式 |
| STBY | PB14 | 初始化务必拉高 |
实测中发现个玄学问题:如果电机电源和MCU共地时出现抖动,在VM和GND之间加个470μF电容立马稳如老狗。有次用航模电池供电,电机启动瞬间导致单片机复位,后来在电源输入端并联了0.1μF陶瓷电容和100μF电解电容组合,问题迎刃而解。
定时器配置绝对是新手最容易翻车的地方。记得第一次调PWM时,电机发出刺耳的啸叫声,原来是把TIM_TimeBaseInitTypeDef中的Prescaler设成了72-1(系统时钟72MHz),导致PWM频率高达100kHz。后来改成下面这个配置,电机运行瞬间安静:
c复制TIM_HandleTypeDef htim3;
htim3.Instance = TIM3;
htim3.Init.Prescaler = 72 - 1; // 1MHz计数频率
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 1000 - 1; // 1kHz PWM频率
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&htim3);
TIM_OC_InitTypeDef sConfigOC;
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500; // 初始占空比50%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);
调试时发现个隐藏技巧:在CubeMX里配置定时器时,把"AutoReloadPreload"设为Enable,这样修改ARR值时就不会产生毛刺。有次做速度平滑调节时没开这个选项,电机转速变化时会出现明显顿挫。
经过五个版本的迭代,我的电机驱动库终于稳定了。核心是这个速度控制函数,支持-100~+100的速度范围:
c复制typedef struct {
GPIO_TypeDef* IN1_Port;
uint16_t IN1_Pin;
GPIO_TypeDef* IN2_Port;
uint16_t IN2_Pin;
TIM_HandleTypeDef* PWM_TIM;
uint32_t PWM_Channel;
} Motor_TypeDef;
void Motor_SetSpeed(Motor_TypeDef* motor, int8_t speed)
{
// 限制速度范围
speed = (speed > 100) ? 100 : (speed < -100) ? -100 : speed;
// 设置方向
if(speed >= 0) {
HAL_GPIO_WritePin(motor->IN1_Port, motor->IN1_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(motor->IN2_Port, motor->IN2_Pin, GPIO_PIN_RESET);
} else {
HAL_GPIO_WritePin(motor->IN1_Port, motor->IN1_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(motor->IN2_Port, motor->IN2_Pin, GPIO_PIN_SET);
speed = -speed;
}
// 设置PWM占空比
__HAL_TIM_SET_COMPARE(motor->PWM_TIM, motor->PWM_Channel, speed);
}
最近给这个库加了加速度限制功能,防止启停时电流冲击太大。实测发现当加速度控制在每秒30%速度变化时,电机寿命能延长三倍以上。具体实现就是在主循环里做渐变:
c复制void Motor_RampToSpeed(Motor_TypeDef* motor, int8_t target, uint8_t ramp_rate)
{
static int8_t current_speed = 0;
if(current_speed < target) {
current_speed += ramp_rate;
if(current_speed > target) current_speed = target;
}
else if(current_speed > target) {
current_speed -= ramp_rate;
if(current_speed < target) current_speed = target;
}
Motor_SetSpeed(motor, current_speed);
}
上周实验室又有个学弟的电机只会转不会停,帮他排查时整理了这份常见问题清单:
__HAL_RCC_TIM3_CLK_ENABLE()有个血泪教训:千万别用杜邦线接大电流电机!有次比赛前夜,杜邦线接触不良导致电阻增大,TB6612FNG的MOS管直接击穿,现场冒烟的场面实在太美。现在我都用XT30接头+硅胶线,电流再大也不虚。
给编码器电机加上PID控制后,速度稳定性提升惊人。分享我的PID核心代码:
c复制typedef struct {
float Kp, Ki, Kd;
float integral;
float prev_error;
} PID_Controller;
float PID_Update(PID_Controller* pid, float setpoint, float measurement)
{
float error = setpoint - measurement;
pid->integral += error;
if(pid->integral > 1000) pid->integral = 1000;
if(pid->integral < -1000) pid->integral = -1000;
float derivative = error - pid->prev_error;
pid->prev_error = error;
return pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative;
}
void Motor_PID_Update(Motor_TypeDef* motor, PID_Controller* pid, float target_rpm)
{
float current_rpm = Encoder_GetSpeed(); // 获取编码器速度
float output = PID_Update(pid, target_rpm, current_rpm);
Motor_SetSpeed(motor, (int8_t)output);
}
调参时建议先用Ziegler-Nichols法:先把Ki和Kd设为零,逐渐增大Kp直到系统开始震荡,然后取这个Kp值的60%作为最终Kp,震荡周期T作为参考来设置Ki和Kd。我的小车参数是Kp=0.8, Ki=0.05, Kd=0.3,不同电机可能需要微调。