1. 从零搭建电机控制环境
第一次用STM32F103C6T6做电机控制时,我对着淘宝买的AB相霍尔编码电机发呆了半小时——这六根线该怎么接?后来发现电机标签其实标得很清楚:红黑是电源线,黄白是霍尔信号线,绿蓝则是AB相编码器输出。这里分享个防呆技巧:用万用表蜂鸣档测阻值,电源线间电阻通常最小(约5-10Ω),编码器线间电阻一般在几百欧姆。
CubeMX配置有个坑我踩过三次:时钟树配置不对会导致所有定时器频率跑偏。对于72MHz主频的STM32F103,建议先配置HSE为8MHz,PLL倍频到72MHz,再给APB1分配36MHz(定时器时钟是它的两倍)。记得勾选"Enabled"选项,否则生成的代码里不会自动启动时钟。
2. 霍尔编码器信号采集实战
AB相编码器的神奇之处在于,STM32的定时器硬件能自动识别转向。在CubeMX里配置编码器模式时,要选"Encoder Mode TI1 and TI2",这样TIMx会自动将计数方向与旋转方向关联。实测发现,正转时计数值递增,反转时递减,连方向判断逻辑都省了。
读取编码器值时有个细节要注意:我最初直接用__HAL_TIM_GET_COUNTER读取原始值,结果电机高速旋转时会出现跳变。后来改成在定时器溢出中断里记录溢出次数,结合计数器值计算真实位置。代码大概长这样:
// 在中断服务函数中 if(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE)){ overflow_count += (TIM2->CR1 & TIM_CR1_DIR) ? -1 : 1; __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE); } // 获取实际位置 int32_t get_encoder_value(){ return overflow_count * 65536 + __HAL_TIM_GET_COUNTER(&htim2); }3. PWM驱动配置的隐藏技巧
电机驱动最怕上下桥臂直通,我的第一个驱动板就是这么烧的。现在会用死区控制,在CubeMX的TIMx配置里,找到"Dead Time"参数,根据MOS管规格设置合适值(通常1-2us)。有个经验公式:死区时间(ns) = (栅极电荷(nC) / 驱动电流(mA)) × 1000。
PWM频率选择也有讲究:1kHz适合大功率电机,小电机建议用10-20kHz。频率太高会导致MOS管开关损耗增大,太低则会有可闻噪音。配置时注意ARR寄存器值不能超过16位上限(65535),我常用公式:
PWM频率 = 定时器时钟 / (PSC + 1) / (ARR + 1)例如72MHz时钟,要得到10kHz PWM,可以设PSC=71,ARR=99。
4. PID算法在HAL库中的实现
调PID参数就像老中医把脉,需要耐心。我的调试步骤一般是:先设Ki=0,Kd=0,逐渐增大Kp直到出现振荡;然后取振荡时Kp值的60%作为初始值,再调Ki消除静差,最后用Kd抑制超调。HAL库的硬件定时器很适合做PID计算周期,例如:
// 在1kHz中断中调用 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){ if(htim == &htim3){ // PID计算定时器 float speed = get_motor_speed(); float output = pid_update(target_speed, speed); set_pwm_duty(output); } }遇到电机抖动时,可以加个低通滤波。我常用的一阶滤波实现如下:
float lpf_filter(float new_value, float old_value, float alpha){ return alpha * old_value + (1 - alpha) * new_value; }alpha取值0.8-0.9效果比较好,太大响应迟钝,太小滤波效果差。
5. 闭环调试中的常见问题
用串口打印实时数据能省去示波器。在HAL库中配置好串口DMA后,可以这样发送数据:
uint8_t buf[64]; int len = sprintf(buf, "%.1f,%.1f\n", target_speed, actual_speed); HAL_UART_Transmit_DMA(&huart1, buf, len);然后在串口助手里绘制曲线观察响应。
最头疼的是编码器噪声问题,我的解决方法是:
- 在编码器线上加磁珠
- PCB布局时信号线远离功率走线
- 软件上做中值滤波
#define FILTER_WINDOW 5 float median_filter(float new_val){ static float buffer[FILTER_WINDOW]; static uint8_t index = 0; buffer[index++] = new_val; if(index >= FILTER_WINDOW) index = 0; // 排序取中值 float temp[FILTER_WINDOW]; memcpy(temp, buffer, sizeof(temp)); bubble_sort(temp); // 实现略 return temp[FILTER_WINDOW/2]; }电机堵转检测也很重要,我通常监测两方面:
- 电流突然增大(通过采样电阻检测)
- 编码器值长时间不变 实现代码类似这样:
if(fabs(current - last_current) > threshold && fabs(speed) < 5){ // 触发保护 set_pwm_duty(0); error_flag |= MOTOR_STALL; }6. 进阶优化技巧
移植FreeRTOS后可以把PID计算放在单独任务中,设置合适的任务优先级。我的经验是:
- 电机控制任务优先级最高
- 通信任务次之
- 状态监测任务最低
任务间通信用队列比全局变量更安全:
// 创建队列 QueueHandle_t speed_queue = xQueueCreate(10, sizeof(float)); // 发送端 float current_speed = get_speed(); xQueueSend(speed_queue, ¤t_speed, 0); // 接收端 float received_speed; if(xQueueReceive(speed_queue, &received_speed, 10)){ // 处理数据 }CAN通信配置时,记得设置合适的过滤器。实验室常用的1Mbps配置:
hcan.Instance = CAN1; hcan.Init.Prescaler = 6; hcan.Init.Mode = CAN_MODE_NORMAL; hcan.Init.SyncJumpWidth = CAN_SJW_1TQ; hcan.Init.TimeSeg1 = CAN_BS1_13TQ; hcan.Init.TimeSeg2 = CAN_BS2_2TQ; hcan.Init.TimeTriggeredMode = DISABLE; // 初始化代码...最后分享一个PID参数整定口诀:"参数整定找最佳,从小到大顺序查。先是比例后积分,微分再最后加。曲线振荡很频繁,比例度盘要放大。曲线漂浮绕大弯,比例度盘往小扳。"