基于STM32F103C6T6与CubeMx-HAL库的AB相霍尔编码电机PID闭环控制实战

张开发
2026/4/18 23:14:00 15 分钟阅读

分享文章

基于STM32F103C6T6与CubeMx-HAL库的AB相霍尔编码电机PID闭环控制实战
1. 从零搭建电机控制环境第一次用STM32F103C6T6做电机控制时我对着淘宝买的AB相霍尔编码电机发呆了半小时——这六根线该怎么接后来发现电机标签其实标得很清楚红黑是电源线黄白是霍尔信号线绿蓝则是AB相编码器输出。这里分享个防呆技巧用万用表蜂鸣档测阻值电源线间电阻通常最小约5-10Ω编码器线间电阻一般在几百欧姆。CubeMX配置有个坑我踩过三次时钟树配置不对会导致所有定时器频率跑偏。对于72MHz主频的STM32F103建议先配置HSE为8MHzPLL倍频到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可以设PSC71ARR99。4. PID算法在HAL库中的实现调PID参数就像老中医把脉需要耐心。我的调试步骤一般是先设Ki0,Kd0逐渐增大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, current_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参数整定口诀参数整定找最佳从小到大顺序查。先是比例后积分微分再最后加。曲线振荡很频繁比例度盘要放大。曲线漂浮绕大弯比例度盘往小扳。

更多文章