STM32 电机动态 PI 控制:从编码器反馈、增量式 PI 到 Matlab 参数拟合与 PWM 输出

前言

在 STM32 智能小车、移动机器人以及其他直流电机控制系统中,最基础的电机控制方式是直接改变 PWM 占空比。

例如:

复制代码
PWM = 5000;

PWM 增大后,电机得到的驱动力通常会增大,转速也会随之提高。

但是,直接控制 PWM 存在一个明显问题:

PWM 决定的是给电机多大的驱动力,并不能保证电机一定达到某个确定的速度。

同一个 PWM,在不同情况下可能得到不同的实际转速,例如:

复制代码
电池电量充足      → 1.2 m/s
电池电压下降      → 1.0 m/s
负载增加          → 0.8 m/s
地面摩擦增大      → 0.7 m/s

因此,如果希望机器人能够稳定地保持某个目标速度,就需要加入编码器反馈,并利用 PI 或 PID 控制器不断调整 PWM。

本文介绍一种进一步改进的电机控制方法:

动态 PI 速度控制。

它在普通 PI 控制的基础上,让 KPKI 随电机当前速度动态变化,使控制器能够适应不同的速度区间。

完整控制链路如下:

复制代码
目标速度
   ↓
编码器测量实际速度
   ↓
计算速度误差
   ↓
根据当前速度动态计算 KP、KI
   ↓
增量式 PI 计算
   ↓
得到 PWM 控制量
   ↓
PWM 限幅
   ↓
H 桥驱动电机
   ↓
电机速度发生变化
   ↓
编码器再次反馈
   └────────────────→ 下一控制周期

一、动态 PI 控制整体框架

一个典型的四轮移动机器人通常有四个独立电机:

复制代码
Motor A
Motor B
Motor C
Motor D

每个电机都拥有:

复制代码
目标速度 Target
实际速度 Encoder
PI 控制器
PWM 输出

因此可以分别进行速度闭环控制:

复制代码
MOTOR_A.Motor_Pwm =
    Incremental_PI_A(
        MOTOR_A.Encoder,
        MOTOR_A.Target
    );

MOTOR_B.Motor_Pwm =
    Incremental_PI_B(
        MOTOR_B.Encoder,
        MOTOR_B.Target
    );

MOTOR_C.Motor_Pwm =
    Incremental_PI_C(
        MOTOR_C.Encoder,
        MOTOR_C.Target
    );

MOTOR_D.Motor_Pwm =
    Incremental_PI_D(
        MOTOR_D.Encoder,
        MOTOR_D.Target
    );

整体结构可以表示为:

复制代码
             机器人目标运动
                  ↓
             运动学逆解
                  ↓
      ┌───────────┼───────────┐
      ↓           ↓           ↓           ↓
   A目标速度    B目标速度    C目标速度    D目标速度
      ↓           ↓           ↓           ↓
     PI          PI          PI          PI
      ↓           ↓           ↓           ↓
   PWM_A       PWM_B       PWM_C       PWM_D
      ↓           ↓           ↓           ↓
   电机A        电机B        电机C        电机D
      ↑           ↑           ↑           ↑
 编码器A反馈  编码器B反馈  编码器C反馈  编码器D反馈

从控制系统角度来看,每个轮子实际上都是一个独立的速度闭环。


二、动态 PI 核心代码

以下以电机 A 为例:

复制代码
int Incremental_PI_A(float Encoder, float Target)
{
    /* 跨控制周期保存状态 */
    static float Bias;
    static float Pwm;
    static float Last_bias;

    /* 1. 当前速度误差 */
    Bias = Target - Encoder;

    /* 2. 根据当前速度动态计算 PI 参数 */
    Velocity_KP =
        218.0f
        + 179.0f * (Encoder / 3.5f)
        + 21.0f * (Encoder * Encoder)
                  / (3.5f * 3.5f);

    Velocity_KI =
        218.0f
        + 179.0f * (Encoder / 3.5f)
        + 21.0f * (Encoder * Encoder)
                  / (3.5f * 3.5f);

    /* 3. 增量式 PI */
    Pwm += Velocity_KP * (Bias - Last_bias)
         + Velocity_KI * Bias;

    /* 4. PWM 限幅 */
    if(Pwm > 16700)
        Pwm = 16700;

    if(Pwm < -16700)
        Pwm = -16700;

    /* 5. 保存当前误差 */
    Last_bias = Bias;

    return (int)Pwm;
}

这段程序可以拆成五个步骤:

复制代码
目标速度与实际速度比较
        ↓
计算当前误差
        ↓
动态计算 KP、KI
        ↓
增量式 PI 计算 PWM
        ↓
PWM 限幅并输出

下面依次展开。


步骤 1:获取目标速度与实际速度

PI 控制器首先需要知道两件事情:

复制代码
Target

表示:

复制代码
希望电机达到的目标速度

以及:

复制代码
Encoder

表示:

复制代码
编码器测量得到的实际速度

例如:

复制代码
Target  = 1.5 m/s
Encoder = 1.2 m/s

则当前速度误差为:

复制代码
Bias = Target - Encoder;

即:

代入数据:

e(k)=1.5-1.2=0.3

因为误差为正,说明:

复制代码
实际速度 < 目标速度

电机需要继续加速。

反过来,如果:

复制代码
Target  = 1.5 m/s
Encoder = 1.8 m/s

则:

e(k)=1.5-1.8=-0.3

说明:

复制代码
实际速度 > 目标速度

控制器需要降低输出。

因此:

复制代码
Bias > 0
→ 电机偏慢

Bias < 0
→ 电机偏快

Bias ≈ 0
→ 实际速度已经接近目标速度

步骤 2:增量式 PI 控制

1. PI 控制中的 KP 和 KI

PI 控制器包含两个核心参数:

复制代码
KP:比例增益
KI:积分增益

2. KP 的作用

KP 主要决定系统对当前误差变化的响应力度。

可以简单理解为:

复制代码
误差变化大
↓
控制器快速调整

误差变化小
↓
控制器小幅调整

因此 KP 主要影响:

复制代码
响应速度
控制灵敏度
动态跟随能力

KP 太小:

复制代码
响应比较慢

KP 太大:

复制代码
容易出现超调甚至振荡

3. KI 的作用

KI 主要用于消除长期存在的小误差。

例如:

复制代码
目标速度 = 1.0 m/s
实际速度 = 0.95 m/s

虽然只差:

复制代码
0.05 m/s

但如果这种误差持续存在,控制器就应该继续进行修正。

积分作用会持续累积这个误差,使 PWM 逐渐变化,最终尽量实现:

复制代码
实际速度 ≈ 目标速度

所以:

复制代码
KP 主要负责"快速响应"
KI 主要负责"减小稳态误差"

步骤 3:增量式 PI 公式

程序采用:

复制代码
Pwm += Velocity_KP * (Bias - Last_bias)
     + Velocity_KI * Bias;

对应数学表达式:

然后:

这里:

复制代码
e(k)     :当前误差
e(k-1)   :上一次误差
KP       :比例增益
KI       :积分增益
ΔPWM     :本控制周期需要增加或减少的 PWM

1.为什么叫"增量式"

因为控制器并不是每次重新计算一个全新的 PWM。

它计算的是:

在上一次 PWM 基础上,本次需要增加多少或者减少多少。

例如当前:

复制代码
PWM = 5000

控制器根据误差算出:

复制代码
ΔPWM = +300

那么:

复制代码
新 PWM = 5000 + 300
       = 5300

下一周期继续测速。

假设速度已经更接近目标值:

复制代码
ΔPWM = +80

则:

复制代码
PWM = 5380

如此不断迭代。

因此它的过程为:

复制代码
测量
 ↓
计算误差
 ↓
计算 PWM 增量
 ↓
修改 PWM
 ↓
再次测量


步骤 4:根据当前速度动态计算 KP 和 KI

示例程序使用:

程序实现:

复制代码
Velocity_KP =
    218.0f
    + 179.0f * (Encoder / 3.5f)
    + 21.0f * (Encoder * Encoder)
              / (3.5f * 3.5f);

其中:

复制代码
v = Encoder

也就是当前编码器计算出来的实际速度。


三、参数变化示例

假设:

v=1.5

则:

得到约:

如果:

v=3.0

则:

得到约:

因此这个参数模型表现为:

复制代码
速度较低
↓
KP 较小

速度提高
↓
KP 增大

需要注意的是:

这只是当前参数模型的变化趋势,并不意味着所有电机都必须按照"速度越高,KP/KI 越大"的方式调节。

最终关系应该由实验结果决定。



步骤 5:实验数据采集

例如依次测试:

复制代码
0.5 m/s
1.0 m/s
1.5 m/s
2.0 m/s
2.5 m/s
3.0 m/s
3.5 m/s

在每个速度点调节:

复制代码
KP
KI

同时观察:

复制代码
1. 上升时间
2. 超调量
3. 稳态误差
4. 振荡情况
5. 达到目标速度后的稳定程度

最终可以形成类似的数据:

车速 v(m/s) KP KI
0.5 240 250
1.0 270 280
1.5 300 315
2.0 325 350
2.5 355 390
3.0 390 430
3.5 420 450

这里的数据只是用来说明回归流程。

不同电机的:

复制代码
性能
减速比
机械结构
机器人重量
轮胎摩擦
电池状态

都会影响最终 PI 参数,因此不能直接照搬。



四、Matlab 二次回归示例

假设已经获取:

复制代码
v = [0.5 1.0 1.5 2.0 2.5 3.0 3.5];

KP_values = [240 270 300 325 355 390 420];

KI_values = [250 280 315 350 390 430 450];

使用:

复制代码
coeffs_KP = polyfit(v, KP_values, 2);

coeffs_KI = polyfit(v, KI_values, 2);

其中:

复制代码
polyfit(x, y, 2)

最后的:

复制代码
2

表示:

使用二次多项式进行回归。

得到的结果形式:

复制代码
coeffs_KP = [a b c]

对应:

同理:


五、Matlab 绘制拟合结果

除了计算系数,还可以画图观察拟合效果:

复制代码
v = [0.5 1.0 1.5 2.0 2.5 3.0 3.5];

KP_values = [240 270 300 325 355 390 420];

% 二次拟合
p = polyfit(v, KP_values, 2);

% 生成连续速度
v_fit = linspace(0.5, 3.5, 100);

% 根据拟合公式计算KP
KP_fit = polyval(p, v_fit);

% 绘制实验数据
scatter(v, KP_values, 'filled');
hold on;

% 绘制拟合曲线
plot(v_fit, KP_fit);

xlabel('Velocity (m/s)');
ylabel('KP');
title('KP-Velocity Polynomial Fitting');

grid on;

最终可以看到:

复制代码
实验采样点
+
二次拟合曲线

如果拟合曲线能够较好地接近实验数据点,就可以进一步将其写入 STM32。


步骤 6:将回归公式部署到 STM32

Matlab 得到数学模型以后,真正运行机器人时并不需要 Matlab。

只需要把公式写进 STM32:

复制代码
Velocity_KP =
    218.0f
    + 179.0f * (Encoder / 3.5f)
    + 21.0f * (Encoder * Encoder)
              / (3.5f * 3.5f);

运行过程中:

复制代码
编码器测得当前速度
       ↓
代入数学公式
       ↓
得到当前 KP、KI
       ↓
执行 PI

例如:

复制代码
Encoder = 1.5

计算一套 PI 参数。

下一周期:

复制代码
Encoder = 2.3

马上计算另一套参数。

所以这里并不是:

复制代码
每隔一段时间重新人工调一次 PI

而是:

复制代码
MCU 在运行过程中实时自动计算 PI 参数

步骤 7:计算 PWM

获得当前:

复制代码
KP
KI

以后,就可以进入增量式 PI:

复制代码
Pwm += Velocity_KP * (Bias - Last_bias)
     + Velocity_KI * Bias;

完整数据关系:

复制代码
目标速度 Target
        │
        ↓
      计算误差
        ↑
        │
实际速度 Encoder
        │
        └────→计算 KP、KI
                   ↓
               增量式 PI
                   ↓
                  PWM

PI 最终输出的并不是:

复制代码
速度

而是:

电机需要多大的控制量。

在本系统中这个控制量最终就是 PWM。


步骤 8:PWM 限幅

PI 本质上是不断累积调整 PWM。

如果误差很大,理论上可能出现:

复制代码
PWM = 20000
PWM = 30000
...

但实际定时器和电机驱动器都有最大输出范围。

因此需要:

复制代码
if(Pwm > 16700)
    Pwm = 16700;

if(Pwm < -16700)
    Pwm = -16700;

最终控制:

复制代码
-16700 ≤ PWM ≤ 16700

PWM 的:

复制代码
正负号

可以表示:

复制代码
电机方向

而:

复制代码
绝对值

表示:

复制代码
控制力度

例如:

复制代码
+5000

代表一个方向。

复制代码
-5000

代表另外一个方向。

而:

复制代码
10000

相较于:

复制代码
5000

代表更大的驱动输出。


步骤 9:通过 H 桥驱动电机

PI 得到:

复制代码
MOTOR_A.Motor_Pwm

之后,这仍然只是程序中的数字。

需要通过:

复制代码
Set_Pwm();

真正写入定时器比较寄存器。

例如:

复制代码
Set_Pwm(
    MOTOR_A.Motor_Pwm,
    MOTOR_B.Motor_Pwm,
    MOTOR_C.Motor_Pwm,
    MOTOR_D.Motor_Pwm
);

对于双输入 H 桥,一般需要把一个有符号 PWM 转换成两路控制信号。

例如:

复制代码
void Set_Motor_PWM(int motor_pwm)
{
    if(motor_pwm < 0)
    {
        MOTOR_PWM1 = PWM_MAX;
        MOTOR_PWM2 = PWM_MAX + motor_pwm;
    }
    else
    {
        MOTOR_PWM2 = PWM_MAX;
        MOTOR_PWM1 = PWM_MAX - motor_pwm;
    }
}

例如:

复制代码
motor_pwm = +5000

可能得到:

复制代码
PWM1 = 11799
PWM2 = 16799

而:

复制代码
motor_pwm = -5000

得到:

复制代码
PWM1 = 16799
PWM2 = 11799

两路 H 桥控制信号的作用关系交换,因此电机电流方向发生变化,最终实现正反转。


完整程序框架

最终整个电机动态 PI 控制程序可以概括成:

复制代码
void Motor_Control_Task(void)
{
    /* 1. 获取电机目标速度 */
    Drive_Motor(Vx, Vy, Vz);

    /* 2. 编码器测速 */
    Get_Velocity_Form_Encoder();

    /* 3. 动态PI控制 */
    MOTOR_A.Motor_Pwm =
        Incremental_PI_A(
            MOTOR_A.Encoder,
            MOTOR_A.Target
        );

    MOTOR_B.Motor_Pwm =
        Incremental_PI_B(
            MOTOR_B.Encoder,
            MOTOR_B.Target
        );

    MOTOR_C.Motor_Pwm =
        Incremental_PI_C(
            MOTOR_C.Encoder,
            MOTOR_C.Target
        );

    MOTOR_D.Motor_Pwm =
        Incremental_PI_D(
            MOTOR_D.Encoder,
            MOTOR_D.Target
        );

    /* 4. PWM限幅 */
    Limit_Pwm(16700);

    /* 5. 输出到H桥 */
    Set_Pwm(
        MOTOR_A.Motor_Pwm,
        MOTOR_B.Motor_Pwm,
        MOTOR_C.Motor_Pwm,
        MOTOR_D.Motor_Pwm
    );
}

对应流程:

复制代码
Drive_Motor()
运动学逆解
      ↓
获得 Target
      ↓
Get_Velocity_Form_Encoder()
      ↓
获得 Encoder
      ↓
Incremental_PI()
      ↓
动态 KP / KI
      ↓
计算 PWM
      ↓
Limit_Pwm()
      ↓
Set_Pwm()
      ↓
H桥
      ↓
电机
      ↓
编码器反馈

总结

动态 PI 控制的核心并不复杂。

普通 PI 是:

复制代码
固定 KP
固定 KI
    ↓
根据速度误差计算 PWM

动态 PI 则是在 PI 前面增加了一层参数调度:

复制代码
当前速度 v
    ↓
计算 KP(v)
计算 KI(v)
    ↓
增量式 PI
    ↓
PWM

整个电机闭环可以表示为:

复制代码
目标速度
      ↓
编码器反馈实际速度
      ↓
计算速度误差
      ↓
根据当前速度动态计算 KP、KI
      ↓
增量式 PI
      ↓
PWM 控制量
      ↓
PWM 限幅
      ↓
H 桥
      ↓
电机
      ↓
编码器再次反馈

其中 Matlab 主要用于开发阶段:

复制代码
真实实验
↓
不同速度下调试 KP、KI
↓
采集 (v, KP, KI)
↓
Matlab 多项式回归
↓
得到 KP(v)、KI(v)
↓
写入 STM32

机器人真正运行以后,并不需要 Matlab 实时参与。

STM32 只需要:

复制代码
读取当前速度
↓
代入已经得到的函数
↓
实时计算 KP、KI
↓
完成 PI 控制

这种方法本质上是一种计算量较小、实现简单的增益调度策略。

相比单纯固定 PI,它能够让控制器根据当前工作速度选择不同的增益,因此在机器人运行速度范围较大、不同速度区间动态特性差异明显时,具有一定的工程应用价值。

Question1:为什么需要动态 PI

普通 PI 一般采用固定参数:

复制代码
KP = 300;
KI = 250;

无论电机:

复制代码
0.5 m/s
1.5 m/s
3.0 m/s

都使用同样的参数。

但真实电机的控制特性并不是完全不变的。

不同速度区间可能表现出不同特征。

例如:

复制代码
低速:
机械摩擦、死区影响明显

中速:
系统相对稳定

高速:
惯性、反电动势、负载变化影响更加明显

于是固定 PI 可能产生:

复制代码
低速表现很好,高速响应慢

或者

高速表现很好,低速控制过于敏感

动态 PI 的思路就是:

也就是说:

复制代码
速度改变
↓
PI 参数随之改变

这种控制方法实际上可以看作一种简单的:

增益调度 Gain Scheduling。

Question2:这些 KP 和 KI 参数是怎么来的

动态 PI 的关键并不是:

复制代码
KP = a + b*v + c*v*v;

而是如何找到:

复制代码
a
b
c

这些系数。

一个比较直观的工程方法是:

复制代码
真实电机实验
      ↓
设置不同目标速度
      ↓
人工调整 KP、KI
      ↓
寻找效果较好的参数
      ↓
记录:
速度 v
KP
KI
      ↓
将数据输入 Matlab
      ↓
回归分析
      ↓
得到 KP(v)、KI(v)

Question3:为什么使用 Matlab

完成实验以后,得到的是一些离散数据:

复制代码
v = 0.5  → KP = 240
v = 1.0  → KP = 270
v = 1.5  → KP = 300
...

但是机器人实际运行过程中,速度并不一定正好等于这些实验点。

例如:

复制代码
v = 1.37 m/s

此时依然需要知道应该使用多少 KP。

因此需要从:

复制代码
离散实验点

得到:

复制代码
连续数学函数

Matlab 在这里主要承担:

回归拟合与数据分析。

Question4:为什么使用二次多项式回归

复制代码
一次函数
→ 简单,但只能表达线性趋势

二次函数
→ 可以描述一定非线性

高阶函数
→ 表达能力更强,但更容易过拟合

二次多项式属于:

拟合能力与计算复杂度之间比较合适的折中方案。

同时 STM32 计算:

复制代码
K = a + b*v + c*v*v;

非常简单,对 MCU 的实时计算负担也很小。

相关推荐
云泽8081 小时前
深入浅出 STM32(十八):时钟树、总线与寄存器的底层联动机制
stm32·单片机·嵌入式硬件
米饭不加菜2 小时前
C51 语言参考手册
经验分享·笔记·单片机·嵌入式硬件
深圳市宝华视联2 小时前
院感约束下,手术室音视频布线与设备摆放实施注意事项
嵌入式硬件·音视频·实时音视频·视频编解码·嵌入式实时数据库
zq_63893 小时前
利用 MATLAB 调用 STK Object Model COM 显示雷达受干扰前后探测范围的变化
开发语言·matlab
Black_Rock_br3 小时前
ARM服务器固件FVP仿真开发流程深度解析实战指南
arm开发·人工智能·嵌入式硬件·硬件工程
天空'之城4 小时前
单片机基础核心知识点汇总(十四)
单片机·嵌入式硬件
DevHub4 小时前
STM32 USB HID 教程:TinyUSB 实现键盘设备,从描述符到枚举
arm开发·stm32·单片机·嵌入式硬件·mcu·计算机外设
zq_63895 小时前
ObjectModel_diagram.pdf 文档详细总结
开发语言·matlab
恒锐丰小瑞5 小时前
率能SS8844T 40V/2.5A/四通道1/2H桥驱动芯片,IN/EN独立控制接口与低导通电阻(350mΩ),用于纺织机械/工业自动化/机器人
单片机·嵌入式硬件·机器人·自动化