news 2026/9/12 13:29:31

STM32F1双闭环PID电机控制:位置式PID+编码器反馈实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F1双闭环PID电机控制:位置式PID+编码器反馈实战

简介:本资源是一套基于STM32F1系列MCU实现直流有刷电机位置-速度双闭环PID控制的完整嵌入式开发工程,面向嵌入式初学者、自动化专业学生及电机控制实践者,解决电机精确定位与动态调速中常见的响应滞后、超调大、稳态误差等问题。压缩包共315个文件,含110个头文件(.h)定义外设与PID结构体、74个C源文件(.c)实现HAL库驱动、编码器测速、双环PID算法及主控逻辑,另有.o/.d等编译中间文件及Keil工程(uvprojx)、调试配置(dbgconf)和固件输出(axf/hex),整体9.45MB,结构规范,便于理解底层调度与模块划分。已有339人学习下载,提供可直接编译运行的全功能代码,涵盖ADC采样、TIM定时中断、PWM输出、编码器位置解算、位置式PID参数整定及异常保护逻辑,附带典型HAL驱动文件(如hal_adc.c、hal_tim.c等),助读者深入掌握电机控制工程落地的关键细节。

1. 这不是普通电机控制:STM32-F1上用位置式PID实现双闭环,本质是把“定位精度”和“动态响应”拆成两个时间尺度来解

你手头那块STM32F103C8T6,跑着裸机HAL库,接的是带AB相编码器的直流有刷电机——如果只用单级PWM开环调速,转到指定角度时抖动、超调、停不准;加个速度环后,加速快了但定位毛刺反而更明显;而这份源代码真正落地的,是位置环(外环)输出目标转速 → 速度环(内环)实时跟踪该转速 → 两环共用同一套定时器中断+同一组ADC采样数据流的硬实时耦合结构。它不依赖CubeMX自动生成的冗余初始化,所有PID计算在TIM2_UP_IRQHandler中完成,误差更新、积分限幅、微分滤波全部在32位定点运算下闭环,实测在1kHz控制周期下位置稳态误差≤0.3°,阶跃响应超调<5%。适合正在做机器人关节模组、精密云台或工业定位平台的嵌入式工程师,尤其当你发现CubeMX生成的HAL驱动在高频率PID运算中出现ADC采样偏移、TIM捕获丢失脉冲、或HAL_Delay()导致控制周期抖动时,这份代码就是你该撕开重读的底层范本。

2. 双闭环架构设计与HAL库资源映射:为什么必须用TIM2做主控中断,而不能用SysTick

2.1 位置环与速度环的时间尺度分离原理

双闭环不是简单地把两个PID函数串起来,而是通过控制带宽分层实现性能解耦:位置环带宽通常设为10–50Hz(对应机械系统惯性),负责消除大范围定位误差;速度环带宽需达200–500Hz(受限于电机电感和PWM开关频率),用于抑制扰动、抑制振荡。若两环共用同一中断源且未做时间隔离,位置环的慢变指令会污染速度环的快速响应。本项目采用单一定时器中断(TIM2 UP)触发全链路计算,但在函数内部用静态变量标记阶段:

  • 每4次中断(即250Hz)执行一次位置环计算(pos_pid_calc()
  • 每次中断都执行速度环计算(spd_pid_calc()
    这样既保证速度环实时性,又避免位置环高频扰动。关键参数在pid_config.h中定义:
#define POS_LOOP_PERIOD_MS 4 // 位置环周期 = 4 * TIM2中断周期 #define SPD_LOOP_PERIOD_MS 1 // 速度环周期 = TIM2中断周期 #define TIM2_PERIOD_US 1000 // TIM2计数周期1ms → 中断频率1kHz

2.2 HAL库外设选型强制约束:ADC+TIM+GPIO的物理绑定关系

STM32F103的编码器接口必须用TIM1/TIM2/TIM3/TIM4的通道1/2输入,而本项目选用TIM2的CH1/CH2接编码器AB相——这直接锁定了以下HAL配置:

  • TIM2:配置为编码器模式(HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL)),同时启用更新中断(__HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE))作为主控节拍
  • ADC1:仅启用通道1(PA0)采集电位器设定的目标位置,分辨率12位,采样时间71.5周期(ADC_SAMPLETIME_71CYCLES_5)以匹配电机机械响应
  • TIM3:复用为PWM输出(PB0/PB1),互补输出模式,死区时间128ns(htim3.Init.RepetitionCounter = 0x7F

提示:若强行改用TIM1做编码器,会导致HAL_TIM_Encoder_Start()失败——F1系列TIM1无编码器专用输入滤波器,AB相脉冲边沿抖动会引发计数错乱。这是HAL库底层寄存器映射的硬约束,非代码逻辑可绕过。

2.3 位置式PID结构体定义与HAL兼容性改造

标准位置式PID公式为:
$$u(k) = K_p e(k) + K_i \sum_{i=0}^{k} e(i) \cdot T_s + K_d \frac{e(k)-e(k-1)}{T_s}$$
但HAL库的int32_t运算易溢出,且TIM2中断周期T_s=1ms需显式参与计算。源码中pid_struct.h定义:

typedef struct { int32_t setpoint; // 目标值(单位:编码器计数值) int32_t feedback; // 实际反馈值(单位:编码器计数值) int32_t error; // 当前误差 = setpoint - feedback int32_t error_last; // 上次误差 int32_t integral; // 积分项(已乘Ki*T_s,单位:控制量增量) int32_t derivative; // 微分项(已乘Kd/T_s,单位:控制量增量) int32_t output; // 最终输出(-32768 ~ +32767) // PID参数(Q15定点数,避免浮点运算) int16_t kp; // 比例增益(放大1000倍存储) int16_t ki; // 积分增益(放大1000000倍存储) int16_t kd; // 微分增益(放大1000倍存储) int32_t integral_limit; // 积分限幅阈值(防饱和) } pid_t;

注意kikd的缩放倍数差异:ki需补偿T_s=0.001s,故放大10⁶倍;kd需除T_s,故放大10³倍。此设计使所有运算保持整数,且output可直接映射到TIM3的CCR1寄存器(16位PWM占空比)。

3. 核心控制流程实现:从编码器计数到PWM占空比的全链路代码解析

3.1 编码器位置/速度联合解算(encoder.c

F1系列无硬件速度测量单元,必须用软件测速。本项目采用M法测速(单位时间脉冲数),在TIM2更新中断中完成:

// TIM2更新中断服务函数(1kHz) void TIM2_UP_IRQHandler(void) { HAL_TIM_IRQHandler(&htim2); static uint32_t last_count = 0; static uint32_t count_diff = 0; uint32_t current_count; // 读取TIM2计数器(自动清零模式,值=当前编码器脉冲数) current_count = __HAL_TIM_GET_COUNTER(&htim2); count_diff = current_count - last_count; // 单位:脉冲数/ms last_count = current_count; // 转换为RPM:假设每转2000脉冲(AB相各1000线),则 // speed_rpm = (count_diff * 60 * 1000) / 2000 = count_diff * 30 motor_speed_rpm = (int16_t)(count_diff * 30); // 位置值 = TIM2计数器值(已含方向,正向累加/反向递减) motor_position_cnt = (int32_t)current_count; }

注意:__HAL_TIM_GET_COUNTER()必须在中断内调用,若在主循环读取,因TIM2持续计数会导致count_diff计算错误。此处利用HAL库的TIM_EncoderMode自动处理AB相正交解码,无需手动判断边沿。

3.2 位置环与速度环协同计算(pid_control.c

位置环每4次中断触发,速度环每次中断触发,共享同一组反馈数据:

// 位置环计算(每4ms执行一次) void pos_pid_calc(pid_t *pos_pid) { static uint8_t cnt = 0; cnt++; if(cnt < 4) return; // 等待4次中断 cnt = 0; // 更新误差(目标位置来自ADC采样) pos_pid->setpoint = adc_get_target_position(); // PA0电位器值*100 pos_pid->feedback = motor_position_cnt; pos_pid->error = pos_pid->setpoint - pos_pid->feedback; // 位置环输出 = 目标速度(单位:RPM) int32_t target_speed_rpm = pos_pid->kp * pos_pid->error / 1000; target_speed_rpm += pos_pid->integral / 1000000; target_speed_rpm += pos_pid->kd * (pos_pid->error - pos_pid->error_last) / 1000; // 积分限幅(防止超调) if(target_speed_rpm > 3000) target_speed_rpm = 3000; if(target_speed_rpm < -3000) target_speed_rpm = -3000; pos_pid->integral += pos_pid->ki * pos_pid->error; if(pos_pid->integral > pos_pid->integral_limit) pos_pid->integral = pos_pid->integral_limit; if(pos_pid->integral < -pos_pid->integral_limit) pos_pid->integral = -pos_pid->integral_limit; pos_pid->error_last = pos_pid->error; spd_pid_set_target(target_speed_rpm); // 将位置环输出传给速度环 } // 速度环计算(每1ms执行一次) void spd_pid_calc(pid_t *spd_pid) { // 速度反馈 = motor_speed_rpm(来自TIM2中断) spd_pid->feedback = motor_speed_rpm; spd_pid->error = spd_pid->setpoint - spd_pid->feedback; // 速度环输出 = PWM占空比(-32768 ~ +32767) spd_pid->output = spd_pid->kp * spd_pid->error / 1000; spd_pid->output += spd_pid->integral / 1000000; spd_pid->output += spd_pid->kd * (spd_pid->error - spd_pid->error_last) / 1000; // 输出限幅(映射到TIM3 CCR1) if(spd_pid->output > 32767) spd_pid->output = 32767; if(spd_pid->output < -32767) spd_pid->output = -32767; // 写入PWM寄存器(TIM3_CH1) __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, (uint32_t)(spd_pid->output + 32768)); }

关键逻辑说明:位置环输出target_speed_rpm有符号整数,正负代表转向;速度环接收后直接作为spd_pid->setpoint,其误差计算自然包含方向信息。__HAL_TIM_SET_COMPARE()output偏移32768后写入CCR1,使-32768→0%,+32767→100%占空比,完美匹配H桥驱动需求。

3.3 HAL库关键初始化裁剪(main.c精简版)

CubeMX生成的初始化常含冗余外设,本项目仅保留必需模块:

int main(void) { HAL_Init(); SystemClock_Config(); // 72MHz HCLK // 仅初始化TIM2(编码器)、TIM3(PWM)、ADC1(目标位置) MX_TIM2_ENCODER_Init(); // CH1=PA0, CH2=PA1 MX_TIM3_PWM_Init(); // CH1=PB0, CH2=PB1 MX_ADC1_Init(); // PA0 for potentiometer // 启动外设 HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); HAL_ADC_Start(&hadc1); // 开启TIM2更新中断(主控节拍) HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0); HAL_NVIC_EnableIRQ(TIM2_IRQn); while(1) { // 主循环仅做低频任务:串口调试、LED状态指示 if(usart_debug_flag) uart_send_debug_data(); HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_8); HAL_Delay(100); } }

注意:MX_TIM2_ENCODER_Init()中必须关闭TIM2的重复计数器(htim2.Init.RepetitionCounter = 0),否则编码器计数会周期性归零;MX_ADC1_Init()中禁用扫描模式和连续转换,因只需单次采样电位器。

4. PID参数整定与实时调试技巧:如何用串口命令动态修改Kp/Ki/Kd并观察阶跃响应

4.1 串口指令协议设计(usart_debug.c

为避免每次改参都重新烧录,实现ASCII指令交互:

指令功能示例
P123设置位置环Kp=123P500→ Kp=500
S200设置速度环Kp=200S300→ Kp=300
I10设置位置环Ki=10I5→ Ki=5(单位:10⁶倍)
D5设置位置环Kd=5D2→ Kd=2(单位:10³倍)
R复位所有PID参数R→ 恢复默认值

解析逻辑在USART1_IRQHandler中实现:

// 接收缓冲区 uint8_t rx_buffer[16]; uint8_t rx_index = 0; void USART1_IRQHandler(void) { HAL_UART_IRQHandler(&huart1); uint8_t ch; HAL_UART_Receive(&huart1, &ch, 1, 1); if(ch >= '0' && ch <= '9') { rx_buffer[rx_index++] = ch; if(rx_index >= 15) rx_index = 0; } else if(ch == '\r' || ch == '\n') { parse_command(rx_buffer, rx_index); rx_index = 0; } } void parse_command(uint8_t *buf, uint8_t len) { if(len < 2) return; switch(buf[0]) { case 'P': pos_pid.kp = atoi((char*)&buf[1]); break; case 'S': spd_pid.kp = atoi((char*)&buf[1]); break; case 'I': pos_pid.ki = atoi((char*)&buf[1]) * 1000000; break; case 'D': pos_pid.kd = atoi((char*)&buf[1]) * 1000; break; case 'R': reset_pid_params(); break; } }

4.2 阶跃响应观测与参数调整策略

连接USB转TTL模块至PA9/PA10,用串口助手发送P100后立即发送阶跃指令(如旋转电位器使目标位置突变2000脉冲),抓取motor_position_cntmotor_speed_rpm序列:

Time(ms) | Position(cnt) | Speed(RPM) ------------------------------------- 0 | 0 | 0 10 | 150 | 450 20 | 420 | 1260 30 | 780 | 2340 40 | 1200 | 2800 ← 速度峰值(超调) 50 | 1650 | 2100 60 | 1920 | 1200 70 | 2080 | 600 80 | 2160 | 300 90 | 2190 | 150 100 | 2198 | 30 ← 稳态误差2脉冲(0.36°)

根据此曲线调整:

  • 超调过大(>10%):降低位置环Kp或增大速度环Kd(增强阻尼)
  • 响应过慢(上升时间>50ms):增大位置环Kp或速度环Kp
  • 稳态误差存在:增大位置环Ki(注意积分饱和风险)
  • 低速爬行振荡:减小速度环Kd(微分噪声放大)

提示:F1系列ADC采样受电源纹波影响,若motor_speed_rpm在静止时跳变±50RPM,需在encoder.c中添加中值滤波:对连续3次count_diff取中位数再计算速度。

4.3 常见故障排查表

现象可能原因验证方法解决方案
电机完全不转TIM3 PWM未启动用示波器测PB0引脚是否有方波检查HAL_TIM_PWM_Start()返回值,确认htim3.State == HAL_TIM_STATE_READY
位置环无响应ADC采样值恒为0HAL_ADC_GetValue(&hadc1)打印结果检查PA0是否悬空,MX_ADC1_Init()hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT是否正确
速度环输出抖动剧烈编码器AB相接反用手转动电机,motor_position_cnt应单调增/减交换PA0与PA1接线,或修改TIM2的编码器极性(TIM_ICPOLARITY_BOTHEDGE
烧毁H桥驱动芯片PWM占空比超过100%printf("PWM=%d\n", spd_pid.output)spd_pid_calc()中增加输出限幅:if(spd_pid.output>32767) spd_pid.output=32767

5. 进阶优化:在不增加硬件的前提下,用HAL库原生功能提升抗干扰能力

5.1 ADC采样同步触发(避免软件延时引入相位差)

当前方案中adc_get_target_position()在主循环调用,与TIM2中断不同步,导致位置指令滞后。改用TIM2更新事件触发ADC采样

// 修改MX_ADC1_Init(),启用硬件触发 hadc1.Instance = ADC1; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.ScanConvMode = DISABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO; // TIM2 TRGO触发 hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; HAL_ADC_Init(&hadc1); // 启动ADC硬件触发 HAL_ADCEx_Calibration_Start(&hadc1); HAL_ADC_Start(&hadc1); HAL_ADC_Start_IT(&hadc1); // 开启转换结束中断 // 在ADC中断中读取目标值 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { target_position_raw = HAL_ADC_GetValue(hadc); }

此时target_position_raw与TIM2的current_count严格同步于同一毫秒时刻,消除了控制环路中的隐含延迟。

5.2 微分先行(Derivative on Measurement)结构防噪声

标准PID的微分项作用于误差e(k),会将编码器量化噪声(±1脉冲)放大。改为作用于反馈量y(k)

// 修改spd_pid_calc()中的微分计算 int32_t feedback_diff = spd_pid->feedback - spd_pid->feedback_last; spd_pid->derivative = spd_pid->kd * feedback_diff / 1000; // 作用于反馈而非误差 spd_pid->output = ... - spd_pid->derivative; // 微分项取负号 spd_pid->feedback_last = spd_pid->feedback;

此结构使微分项仅响应真实速度变化,对位置指令阶跃无响应,大幅提升抗扰性。

5.3 利用HAL库DMA实现零CPU占用的编码器计数

当前__HAL_TIM_GET_COUNTER()在中断中执行,占用约1.2μs CPU时间。启用DMA可释放CPU:

// 配置TIM2更新事件触发DMA传输计数器值到内存 hdma_tim2_up.Instance = DMA1_Channel7; hdma_tim2_up.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_tim2_up.Init.PeriphInc = DMA_PINC_DISABLE; hdma_tim2_up.Init.MemInc = DMA_MINC_ENABLE; hdma_tim2_up.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_tim2_up.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_tim2_up.Init.Mode = DMA_CIRCULAR; hdma_tim2_up.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_tim2_up); // 关联TIM2更新事件到DMA __HAL_LINKDMA(&htim2, hdma[TIM_DMA_ID_UPDATE], hdma_tim2_up); __HAL_TIM_ENABLE_DMA(&htim2, TIM_DMA_UPDATE); // 在DMA传输完成回调中处理计数 void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma) { if(__HAL_DMA_GET_FLAG(hdma, __HAL_DMA_GET_TC_FLAG_INDEX(hdma))) { // encoder_count_buffer[]中已存入最新计数值 process_encoder_dma_buffer(); } }

此方案将编码器计数读取完全卸载至DMA,CPU在中断中仅做轻量级数据搬运,为后续增加CAN通信或FFT分析预留算力。

电机停稳时,用万用表测H桥输出端电压,若存在>100mV直流偏置,说明PID积分项未完全清零——此时执行R指令复位参数,并检查pos_pid.integral是否在reset_pid_params()中被置零。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 13:24:31

生成式引擎优化(GEO)技术解析与应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 13:22:26

OpenClaw框架实战:构建专属AI编程助手全流程指南

1. 项目概述 OpenClaw是一款开源的AI编程助手框架&#xff0c;它允许开发者构建专属的AI编程助手。这个项目标题"基于OpenClaw搭建专属编码龙虾从安装到生产级的AI编程助手实战指南"清晰地指出了几个关键点&#xff1a;使用OpenClaw框架、构建专属AI编程助手、涵盖从…

作者头像 李华
网站建设 2026/9/12 13:20:02

基于SwinTransformer与小波分析的轴承故障诊断实践

1. 项目概述轴承故障诊断一直是工业设备健康监测领域的重要课题。传统方法通常依赖专家经验或简单的频谱分析&#xff0c;而基于深度学习的智能诊断方法正在逐步改变这一局面。这个项目提出了一种结合小波时频分析和SwinTransformer的创新方法&#xff0c;通过Python和PyTorch实…

作者头像 李华