1. 为什么需要精准电机控制系统
当你给智能小车装上电机,接上电源就能转,这看起来很简单对吧?但实际做项目时会发现,这种开环控制根本不够用。比如想让小车以固定速度爬坡,或者精确移动到某个位置,仅靠"通电-转动"这种粗暴方式完全无法实现。这就是我们需要构建闭环控制系统的原因。
我做过一个自动送货机器人的项目,最初用开环控制,结果每次走到同一位置能偏差十几厘米。后来加上编码器反馈,精度直接提升到±2mm以内。精准控制的核心在于实时感知+动态调整:通过编码器获取电机实际状态,再用算法计算调整量,最后通过驱动芯片输出修正信号。整个过程就像开车时不断微调方向盘,只不过这里调整的是PWM占空比。
TB6612和编码器的组合,相当于给电机装上了"眼睛"和"手脚"。编码器持续汇报"现在转到哪了",TB6612则根据指令快速响应。这种组合在机器人、3D打印机、CNC机床等需要位置控制的场景中非常常见。接下来我会用最接地气的方式,带你从器件级到系统级完整走通这个流程。
2. TB6612电机驱动实战
2.1 硬件选型与电路设计
第一次用TB6612时,我被它的小体积惊到了——只有指甲盖大小,却能驱动两个直流电机。相比老旧的L298N,它的效率提升不是一点半点。实测驱动12V电机连续工作半小时,芯片温度才40℃左右,而L298N早就烫得能煎鸡蛋了。
接线其实很简单,记住这几个关键点:
- VM引脚:接电机电源(2.5-13.5V),我常用18650锂电池供电
- VCC引脚:接3.3V或5V给逻辑电路供电
- STBY引脚:直接接高电平使能芯片
- PWM引脚:接单片机定时器输出,建议用10kHz频率
这里有个坑要注意:PWM频率不能太低,否则电机会有可闻噪音。但也不能太高,超过100kHz会导致MOS管开关损耗增大。我的经验值是8-15kHz这个甜区。
2.2 软件控制逻辑
控制电机无非就是方向+速度,对应到代码里:
// 初始化GPIO和定时器 void Motor_Init(void) { GPIO_Init(AIN1_PIN, OUTPUT); GPIO_Init(AIN2_PIN, OUTPUT); PWM_Init(PWMA_PIN, 10000); // 10kHz PWM } // 设置电机转向和速度 void Motor_Set(int speed) { if(speed > 0) { GPIO_Write(AIN1_PIN, HIGH); GPIO_Write(AIN2_PIN, LOW); } else { GPIO_Write(AIN1_PIN, LOW); GPIO_Write(AIN2_PIN, HIGH); speed = -speed; } PWM_SetDuty(PWMA_PIN, speed); }实测发现,当PWM占空比低于5%时电机可能无法启动。解决方法是在代码里加个死区补偿:
if(speed < 5 && speed != 0) speed = 5;3. 编码器信号采集与处理
3.1 编码器工作原理揭秘
拆开一个光电编码器,你会看到码盘和光电传感器。电机转动时,码盘上的栅格会交替阻挡光线,产生AB两相脉冲。关键点在于这两路信号的相位差——正转时A相比B相超前90°,反转时则滞后。
霍尔编码器原理类似,只是用磁铁替代了光栅。虽然精度稍低(通常每圈几百个脉冲),但胜在价格便宜且抗干扰强。我的智能小车在户外跑时就用的霍尔编码器,从没出现过信号丢失。
3.2 STM32的编码器模式
STM32的定时器有个超实用功能——硬件编码器接口。配置起来非常简单:
void Encoder_Init(TIM_HandleTypeDef *htim) { TIM_Encoder_InitTypeDef sConfig = {0}; sConfig.EncoderMode = TIM_ENCODERMODE_TI12; sConfig.IC1Polarity = TIM_ICPOLARITY_RISING; sConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI; sConfig.IC1Prescaler = TIM_ICPSC_DIV1; sConfig.IC1Filter = 6; // 适当滤波防抖动 HAL_TIM_Encoder_Init(htim, &sConfig); HAL_TIM_Encoder_Start(htim, TIM_CHANNEL_ALL); }这个配置实现了四倍频计数,意味着每收到一个完整的AB相周期,计数器会加减4。直接读取CNT寄存器就能得到当前位置:
int32_t Get_Position(TIM_HandleTypeDef *htim) { return (int32_t)__HAL_TIM_GET_COUNTER(htim); }4. 闭环控制实现
4.1 速度计算技巧
要计算实时速度,最简单的办法是定时采样位置值:
int32_t last_pos = 0; uint32_t last_time = 0; float Get_Speed(TIM_HandleTypeDef *htim) { int32_t current_pos = Get_Position(htim); uint32_t current_time = HAL_GetTick(); float speed = (current_pos - last_pos) * 1000.0f / (current_time - last_time) / ENCODER_PPR; // 每转脉冲数 last_pos = current_pos; last_time = current_time; return speed; }注意这里有个细节:速度计算间隔不宜过短,否则会受编码器抖动影响。我一般用50ms的采样周期,平衡了响应速度和稳定性。
4.2 PID控制实战
当我把PID算法加到电机控制上时,效果立竿见影。分享一个经过实战检验的PID实现:
typedef struct { float Kp, Ki, Kd; float integral; float last_error; } PID_Controller; float PID_Update(PID_Controller *pid, float error, float dt) { float proportional = pid->Kp * error; pid->integral += error * dt; float integral = pid->Ki * pid->integral; float derivative = pid->Kd * (error - pid->last_error) / dt; pid->last_error = error; return proportional + integral + derivative; }调参时记住这个口诀:"先调P,再调I,最后调D"。具体步骤:
- 把Ki和Kd设为零,逐渐增大Kp直到系统开始振荡
- 取振荡时Kp值的50%作为初始值
- 慢慢增加Ki消除静差,但别加太多否则会超调
- 最后加少量Kd抑制振荡
我的小车电机参数最终定为:Kp=0.8, Ki=0.2, Kd=0.05,供大家参考。
5. 系统集成与调试
把TB6612和编码器组装到小车上后,我遇到了典型的地线干扰问题——电机一转编码器就乱跳。解决方法很简单:
- 电机电源和逻辑电源用0Ω电阻隔离
- 所有信号线加磁环
- 编码器线用双绞线
用示波器看编码器信号时,发现波形边缘有振铃。在AB相线上各加个100Ω电阻串联,问题迎刃而解。这些经验都是踩坑踩出来的,现在你的项目可以直接避开这些雷区。
最后测试闭环效果时,可以先用阶跃响应观察系统行为:给目标速度一个突变,看实际速度是否能快速平稳跟随。我的实测数据显示,加入PID后速度调节时间从原来的1.2秒缩短到了0.3秒,且超调量小于5%。