ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32三闭环直流电机控制:位置式PID与HAL库实现

STM32三闭环直流电机控制:位置式PID与HAL库实现 简介这是一份面向STM32开发者与电机控制工程师的直流有刷电机三闭环控制源代码工程基于STM32-F4系列和HAL库采用位置式PID实现位置环、速度环、电流环的串级调节可用于机器人、云台、传送带等需要精准定位与稳速的场景。压缩包共555个文件约20.51MB以158个h头文件、103个c源文件为主体并包含编译产生的o、d、s等中间文件以及icf链接脚本、uvprojx工程文件源码覆盖初始化配置、PID算法、三闭环控制逻辑和编码器中断处理等完整模块。已有401人学习下载结构清晰便于直接移植或二次开发。通过工程源码读者可以快速理解位置环通过编码器反馈计算目标位置、速度环维持转速恒定、电流环调节绕组电流提供扭矩的嵌套关系掌握PID参数整定方法和用C语言与HAL库搭建电机控制框架的具体思路是进阶嵌入式运动控制不可多得的参考资料。1. 三闭环不是三个PID叠加先把控制架构看懂直流有刷电机做位置控制很多新手直接写一个位置环PID输出接PWM结果要么抖得厉害要么带载就偏。三闭环电流环到速度环再到位置环的价值在于把“力”“速度”“位置”三个物理量分层控制每一层只干一件事带宽从内到外递减。这套基于STM32-F4的HAL库工程使用位置式PID和C语言实现编码器提供位置和速度反馈采样电阻配合ADC做电流反馈适合机器人关节、云台、小型数控滑台这类伺服级位置控制场景。如果你之前只在F103上跑过单环这套工程里的三环级联结构、限幅位置和中断调度逻辑是值得完整拆一遍的。2. 位置式PID与HAL库外设映射初始化怎么写才对2.1 位置式PID的离散化公式与C语言结构体位置式PID的输出直接对应执行器的绝对控制量比如PWM占空比。标准的离散化公式为u(k) Kp * e(k) Ki * sum(e) Kd * [e(k) - e(k-1)]其中 e(k) 是当前误差sum(e) 是误差累积项e(k) - e(k-1) 是误差变化率。它与增量式PID的本质区别在于位置式输出的是“当前应该给多少”增量式输出的是“在上一次基础上加多少”。位置式一旦积分项失控输出就会饱和增量式天然带有积分截断效果但执行器需要具备保持能力比如步进电机直流电机配PWM则两者都可行。下面是一个可跨平台复用的PID结构体直接嵌入电机控制工程typedef struct { float Kp; // 比例增益 float Ki; // 积分增益 float Kd; // 微分增益 float target; // 目标值 float actual; // 实际反馈值 float error; // 当前误差 float error_last; // 上次误差 float integral; // 积分累加 float out; // PID输出 float out_max; // 输出限幅 float integral_max; // 积分限幅 } PID_TypeDef; float PID_Calc(PID_TypeDef *pid, float target, float actual) { pid-target target; pid-actual actual; pid-error target - actual; pid-integral pid-error; // 积分限幅防止深度饱和 if (pid-integral pid-integral_max) pid-integral pid-integral_max; if (pid-integral -pid-integral_max) pid-integral -pid-integral_max; pid-out pid-Kp * pid-error pid-Ki * pid-integral pid-Kd * (pid-error - pid-error_last); pid-error_last pid-error; // 输出限幅 if (pid-out pid-out_max) pid-out pid-out_max; if (pid-out -pid-out_max) pid-out -pid-out_max; return pid-out; }这段代码把位置式PID和限幅逻辑封装在一起。integral_max与out_max需要分别设置积分限幅应小于输出限幅否则积分项会先于比例项把输出顶到饱和导致回程慢。我一般把积分限幅设为输出限幅的 70%80%让比例项保留足够的调节余量。2.2 HAL库外设配置Timer PWM、编码器接口、ADC采样三闭环的物理量来源决定了外设选型电流环需要 ADC 快速采样速度环和位置环需要编码器计数PWM 输出则交给定时器。在 STM32-F4 系列上常见做法是用 TIM1 或 TIM8 高级定时器产生 PWM因为它们带互补输出和刹车输入配合 H 桥驱动时可以直接接故障保护信号用一个通用定时器如 TIM2、TIM3、TIM4配置为编码器模式硬件负责对 A/B 相计数CPU 只需读取htim-Instance-CNT电流采样使用 ADC1 的注入通道或者普通规则通道配合 DMA。电流环若要做 10kHz 控制每次进入中断都触发一次 ADC 转换。HAL 库的初始化代码通常由 STM32CubeMX 生成但要注意几个容易出错的点编码器模式的定时器要开启SlaveMode为TIM_SLAVEMODE_RESET或者TIM_SLAVEMODE_GATED触发源选择TIM_TS_CI1F_ED或TIM_TS_CI1FE2不同 F4 型号的索引定义略有差异。PWM 初始化时Pulse初始值应设为占空比中点如 1000避免上电瞬间电机猛转。2.3 初始化代码与参数说明以下是一段针对 F4 的 PWM 和编码器初始化核心配置省略了 Clock 和 GPIO 部分// TIM1 PWM 初始化PWM频率 20kHz占空比初始 50% TIM_HandleTypeDef htim1; htim1.Instance TIM1; htim1.Init.Period 1000 - 1; // 20MHz / (9991) 20kHz htim1.Init.Prescaler 42 - 1; // 84MHz / 42 2MHz 计数时钟 htim1.Init.CounterMode TIM_COUNTERMODE_UP; htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim1); TIM_OC_InitTypeDef oc {0}; oc.OCMode TIM_OCMODE_PWM1; oc.Pulse 500; // 50% 占空比 oc.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim1, oc, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); // TIM2 编码器模式4000线编码器经过4倍频 TIM_Encoder_InitTypeDef enc {0}; enc.EncoderMode TIM_ENCODERMODE_TI1FP1; htim2.Instance TIM2; htim2.Init.Period 0xFFFF; HAL_TIM_Encoder_Init(htim2, enc); HAL_TIM_Encoder_Start(htim2, TIM_CHANNEL_ALL);参数说明Prescaler把定时器时钟分频到合适的计数频率Period决定 PWM 频率或编码器计数溢出范围。编码器模式下Period不参与 PWM 生成但会影响溢出判断我用0xFFFF并让上位机处理正反转换向如果只控制正转且行程固定可以缩小Period以提高位置阈值触发的精度。PWM 频率选 20kHz 是为了避开音频噪声同时保证 H 桥 MOS 管开关损耗不过大。3. 三环联动位置环、速度环、电流环的执行顺序与限幅3.1 级联结构与控制周期三闭环不是三个 PID 同时各自计算而是按照“外环输出作为内环目标”的方式串联。电流环在最内层直接控制功率输出速度环接收位置环输出的目标速度位置环最终决定运动轨迹。带宽要求是电流环 速度环 位置环否则内环无法快速响应外环的变化系统会震荡。控制环典型控制周期反馈源输出目标执行位置电流环10kHz100usADC采样电阻电压PWM占空比定时器更新中断速度环1kHz1ms编码器差分计数电流目标值定时器更新中断位置环100Hz10ms编码器绝对计数值速度目标值低级中断或主循环电流环最快因为它直接面对反电动势和电感延迟。速度环计算量适中放在定时器中断里。位置环不需要太高频率因为机械惯性已经过滤了高频分量我用 100Hz省出的 CPU 时间可以做通信和监控。3.2 电流环ADC采样与PWM占空比映射电流环的反馈来自电机回路中的采样电阻。将运放放大后的电压接入 ADC 引脚ADC 转换结果通过公式换算成实际电流#define ADC_REF_V 3.3f #define ADC_RES 4096.0f #define AMP_GAIN 10.0f #define SENSE_RES 0.05f float adc_to_current(uint32_t adc_value) { float voltage (adc_value / ADC_RES) * ADC_REF_V; // 典型运放电路是差分放大输出 电流 * 采样电阻 * 增益 float current voltage / (SENSE_RES * AMP_GAIN); return current; }注意这里没有考虑运放的偏置电压。如果运放是单电源供电0A 电流时的输出往往不是 0V而是在 1.65V 附近。此时需要先做零点校准在电机停止时读一次 ADC 值作为偏移量再换算电流。电流环 PID 的输出是 PWM 占空比因此out_max要跟占空比范围匹配。比如 PWM 周期是 1000那电流环输出限幅就设为 800 左右留出死区时间余量。3.3 速度环编码器测速与一阶低通滤波速度环的反馈值来自编码器计数差分。常见的 M 法测速公式如下float speed_get_rpm(TIM_HandleTypeDef *htim, int32_t *last_cnt, float dt) { int32_t now (int32_t)htim-Instance-CNT; int32_t delta now - *last_cnt; *last_cnt now; // 编码器线数 * 4 是每转脉冲数dt 是测速周期秒 float rpm (float)delta / (PPR * 4.0f) / dt * 60.0f; return rpm; }delta要考虑溢出补偿尤其当编码器计数越过0xFFFF时直接相减会出现较大跳变。常见做法是把 int16 强制转换为 int32 后相减利用符号扩展恢复真实差值。测速周期越短速度值越毛躁所以我会在速度环入口加一阶低通滤波speed_filtered 0.85f * speed_filtered 0.15f * speed_raw;滤波系数 0.85 和 0.15 意味着滤波时间常数大约是测速周期的 6 倍左右。如果想更平滑可以把系数调成 0.9/0.1但代价是速度环带宽变低位置环会更容易超调。这个系数需要跟位置环的 PID 一起调不能单独追求某一个环的响应。3.4 位置环目标位置与梯形速度规划位置环输入是目标位置和当前编码器位置输出是目标速度。如果起点和终点距离较远直接给满速度会导致机械冲击所以我在位置环外增加一个简单的梯形速度规划根据剩余距离把目标速度分成加速区、匀速区、减速区。#define MAX_SPEED 300.0f // RPM #define ACCEL_DIST 2000 // 编码器脉冲数预设的加减速距离 float position_loop(int32_t target_pos, int32_t current_pos, uint32_t period_ms) { float dist (float)(target_pos - current_pos); float speed_cmd; if (dist 2.0f * ACCEL_DIST) { speed_cmd MAX_SPEED; } else if (dist 0) { speed_cmd MAX_SPEED * dist / (2.0f * ACCEL_DIST); } else if (dist -2.0f * ACCEL_DIST) { speed_cmd -MAX_SPEED; } else { speed_cmd MAX_SPEED * dist / (-2.0f * ACCEL_DIST); } return speed_cmd; }这段逻辑相当于一个 P 控制器比例系数是MAX_SPEED / (2 * ACCEL_DIST)。位置环本身还可以再加位置式 PID但那样需要小心积分饱和如果目标位置突然改变积分项会累积巨大误差导致实际位置过冲。工程上常见的做法是位置环只做比例控制或 PD 控制速度环用 PI 控制器来消除稳态误差。本工程里三个环都用位置式 PID那就必须给位置环配置极小的 Ki甚至直接设为 0。3.5 三环控制主循环代码三环集中在两个地方执行电流环在定时器更新中断里高频执行速度环和位置环分别在各自周期任务里执行。下面是一段常见的调度骨架// 1ms 定时器中断回调也是速度环周期 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM_UPDATE_INT) { // 电流环在100us中断里已更新过pwm_duty speed_raw speed_get_rpm(htim2, last_cnt, 0.001f); speed_filt 0.85f * speed_filt 0.15f * speed_raw; // 速度环输出作为电流环目标如果电流环使能则转发 current_target PID_Calc(pid_speed, speed_target, speed_filt); // 每10ms执行一次位置环 position_tick; if (position_tick 10) { position_tick 0; speed_target PID_Calc(pid_position, pos_target, position_get()); } } }执行顺序要注意先采样电流再算电流环最后再读编码器算速度环避免同一时刻读写同一个变量造成竞争。电流环中断里必须实时更新pwm_duty速度环中断里计算出的current_target只放入全局变量由电流环中断在下一个周期取用。这种做法让三个环形成“级联同步”而不是“全在同一个中断里串联计算”保证内环的确定性。4. 参数整定与常见坑从震荡到收敛4.1 先电流环再速度环最后位置环三环参数不能从头到尾一起调否则震荡源都不知道来自哪层。我会先把内环的反馈断开单独测试。电流环整定时只需看 PWM 占空比和 ADC 采样电流的跟随关系。给一个阶跃电流目标观察电流爬升时间是否在 1ms 以内。如果电流上升过慢调大 Kp如果出现高频震荡减小 Ki。速度环整定时把电流环闭环然后给速度阶跃先调 Kp 让速度快速逼近目标再加 Ki 消除稳态误差。最后调位置环通常在速度环已经稳定的前提下位置环 Kp 从较小值逐渐增大直到听不到电机明显的冲击声。我一般用一组保守参数作为起点电流环 Kp0.5Ki0.05Kd0速度环 Kp50Ki5Kd0.5位置环 Kp2Ki0Kd0.1。不同电机电气时间常数差异很大这个起点只保证安全之后按“先比例后积分最后加一点微分”的顺序逐步逼近极限。4.2 积分饱和、微分噪声和限幅冲突位置式PID 三个环节各有各的坑。积分饱和在三闭环中最常见速度环目标是 1000RPM电机堵转后实际速度是 0积分项会一直累加。等负载消失电机突然冲向高速就是因为积分项已经顶到最大值。解决办法是积分分离或抗饱和当输出接近限幅时停止积分累加。我常用下面这个条件判断if ((pid-out pid-out_max pid-error 0) || (pid-out -pid-out_max pid-error 0)) { // 不累加积分 } else { pid-integral pid-error; }微分噪声来自编码器和 ADC。电流环的 ADC 采样值如果带有开关噪声微分项会放大这些毛刺导致 PWM 抖动。我给电流环的微分项加了一个一阶低通或者干脆 Kd0。速度环的微分项输入已经经过测速滤波通常不需要额外处理位置环的微分项如果直接用编码器原始差值效果等价于“预测速度”但会被扰动脉冲干扰所以位置环 Kd 通常很小。4.3 常见问题排查表现象可能原因检查点电机嗡嗡响不转PWM频率太低或死区不足将PWM提高到16kHz以上检查H桥死区设置有电流但电机无力电流环限幅太小查看电流环out_max是否远小于占空比上限定位过冲且来回震荡位置环Kp过大或速度环带宽不足先减小速度环滤波系数再降位置环Kp静止时电机发热速度环Ki偏大或编码器零漂查看速度环输出是否非零做零速钳位高速丢步编码器采样周期太长提高速度环频率或改用M/T测速法排查时要在代码里增加一个“环旁路”开关直接给电流环一个固定占空比看电机能否平稳转动。如果这一步都不行问题在硬件没必要调 PID。这个方法在调云台和滑台时救过我好几次能快速把问题拆成硬件回路和控制算法两类。5. 验证方法与一个实用调试技巧用串口示波器看三环波形三环参数调试光靠看电机转动手感是不够的必须把内部变量导出来。常见的验证方法是给位置环一个阶跃目标记录位置、速度、电流响应观察超调量、振荡次数和稳态误差。也可以在速度环单独给方波目标看速度跟随曲线是否平滑。我常用的调试技巧是写一个简单的二进制串口示波器协议把三个环的target和actual用 float 拆字节发出上位机用 SerialPlot 或 VOFA 绘图union FloatByte { float value; uint8_t bytes[4]; }; void send_debug(float pos_act, float spd_act, float cur_act) { union FloatByte fb; uint8_t buf[13]; buf[0] 0xAA; // 帧头 fb.value pos_act; memcpy(buf[1], fb.bytes, 4); fb.value spd_act; memcpy(buf[5], fb.bytes, 4); fb.value cur_act; memcpy(buf[9], fb.bytes, 4); buf[13] 0x55; // 帧尾 HAL_UART_Transmit(huart1, buf, 13, 10); }收发两边约定好帧头、帧尾和字节序上位机就能把位置、速度、电流三条曲线解析出来。这个函数放到速度环中断里每 1ms 发一帧波特率用 921600不会阻塞主流程。最终验证时我给位置环输入一个 1000 脉冲的阶跃观测波形如果位置曲线第一次超过目标后回调超过 10%说明速度环带宽不够或者位置环 Kp 偏大如果速度曲线在加速段出现震荡应该先降低速度环 Kp而不是去动位置环参数。调整完记下一组参数再在不同负载下测一遍堵转和跟随确认没有积分饱和触发。调试现场保留这组串口波形数据后续改版换电机时对照曲线就能快速判断新电机的时间常数差异。本文还有配套的精品资源点击获取
返回列表