Training / Docs

状态机与 PID 进阶训练

面向机器人控制的专项训练:状态机设计、异常保护、PID 工程处理与综合联调。

电控组进阶训练:状态机与 PID

本文作为《26_机器人创新团队电控暑期学习》的进阶训练补充。

适合已经完成 GPIO、串口、定时器、PWM、编码器、电机基础控制,并具备基本 C 语言模块化能力的同学。

本阶段不再以“能让电机转起来”为目标,而是开始训练控制逻辑设计、闭环控制、调参、异常处理和工程化思维


一、训练目标

完成本阶段后,应能够独立完成以下任务:

最终目标是做到:

状态机负责“现在应该做什么”,PID 负责“这个动作应该怎么稳定地完成”。


第一部分:状态机


二、为什么机器人程序需要状态机

初学者很容易写出下面这种程序:

while (1)
{
    if (key == 1)
    {
        motor_run();
    }

    if (sensor == 1)
    {
        motor_stop();
    }

    if (flag == 2)
    {
        servo_run();
    }

    if (error == 1)
    {
        motor_stop();
    }
}

功能少时还能运行,但随着项目增加,会逐渐出现:

例如一个搬运机器人可能需要执行:

上电

初始化

等待启动

寻找目标

接近目标

抓取

搬运

放置

返回

任务完成

这类“按阶段执行”的系统非常适合用状态机描述。


三、状态机的基本概念

有限状态机英文为:

Finite State Machine
FSM

一个状态机通常包含四个核心元素:

  1. 状态 State
  2. 事件 Event
  3. 状态转移 Transition
  4. 状态动作 Action

例如:

WAIT
  │ 按下启动键

RUN
  │ 检测到终点

STOP

其中:


四、最基础的状态机写法

首先使用枚举定义状态:

typedef enum
{
    STATE_IDLE = 0,
    STATE_RUN,
    STATE_STOP,
    STATE_ERROR
} SystemState_t;

然后定义当前状态:

SystemState_t system_state = STATE_IDLE;

主循环中使用:

switch (system_state)
{
    case STATE_IDLE:
        Motor_Stop();

        if (start_flag)
        {
            system_state = STATE_RUN;
        }
        break;

    case STATE_RUN:
        Motor_SetSpeed(500);

        if (finish_flag)
        {
            system_state = STATE_STOP;
        }
        break;

    case STATE_STOP:
        Motor_Stop();
        break;

    case STATE_ERROR:
        Motor_Stop();
        break;

    default:
        system_state = STATE_ERROR;
        break;
}

这已经比大量零散 if 更容易阅读。


五、状态机中最重要的三类动作

一个完整状态通常可以拆分成:

进入状态
持续执行
退出状态

即:

Enter
Update
Exit

1. Enter:进入状态时只执行一次

例如进入运行状态时:

2. Update:处于该状态时周期执行

例如:

3. Exit:离开状态时执行一次

例如:

这种思想非常重要。

如果不区分 Enter 和 Update,可能出现:

case STATE_RUN:
    PID_Reset();
    PID_Calculate();
    break;

那么 PID 每次循环都会被清零,积分项永远无法工作。


六、推荐的状态切换函数

建议不要在工程各处直接写:

system_state = STATE_RUN;

而是统一通过函数切换:

void State_Change(SystemState_t new_state)
{
    if (system_state == new_state)
    {
        return;
    }

    State_Exit(system_state);

    system_state = new_state;

    State_Enter(system_state);
}

这样以后可以统一加入:

例如:

void State_Change(SystemState_t new_state)
{
    printf("State: %d -> %d\r\n", system_state, new_state);

    State_Exit(system_state);

    system_state = new_state;
    state_enter_time = HAL_GetTick();

    State_Enter(system_state);
}

七、完整状态机代码框架

typedef enum
{
    STATE_IDLE = 0,
    STATE_INIT,
    STATE_RUN,
    STATE_FINISH,
    STATE_ERROR,
    STATE_ESTOP
} SystemState_t;

static SystemState_t current_state = STATE_INIT;
static uint32_t state_enter_time = 0;

void State_Enter(SystemState_t state)
{
    switch (state)
    {
        case STATE_IDLE:
            Motor_Stop();
            break;

        case STATE_INIT:
            break;

        case STATE_RUN:
            PID_Reset(&motor_pid);
            Encoder_Reset();
            break;

        case STATE_FINISH:
            Motor_Stop();
            break;

        case STATE_ERROR:
            Motor_Stop();
            break;

        case STATE_ESTOP:
            Motor_Stop();
            break;
    }
}

void State_Update(void)
{
    switch (current_state)
    {
        case STATE_IDLE:
            if (Start_Key_Pressed())
            {
                State_Change(STATE_RUN);
            }
            break;

        case STATE_INIT:
            if (System_Init_OK())
            {
                State_Change(STATE_IDLE);
            }
            else if (HAL_GetTick() - state_enter_time > 3000)
            {
                State_Change(STATE_ERROR);
            }
            break;

        case STATE_RUN:
            Motor_Control_Update();

            if (Task_Finished())
            {
                State_Change(STATE_FINISH);
            }

            if (Motor_Stall_Detected())
            {
                State_Change(STATE_ERROR);
            }
            break;

        case STATE_FINISH:
            break;

        case STATE_ERROR:
            break;

        case STATE_ESTOP:
            break;
    }
}

void State_Exit(SystemState_t state)
{
    switch (state)
    {
        case STATE_RUN:
            Motor_Stop();
            break;

        default:
            break;
    }
}

void State_Change(SystemState_t new_state)
{
    if (new_state == current_state)
    {
        return;
    }

    State_Exit(current_state);

    current_state = new_state;
    state_enter_time = HAL_GetTick();

    State_Enter(current_state);
}

八、状态超时机制

机器人程序中不能假设每一步永远都能成功。

例如:

进入“等待机械臂到位”状态

如果舵机掉线,程序可能永久卡住。

因此每个可能失败的状态最好加入超时:

if (Arm_IsReady())
{
    State_Change(STATE_NEXT);
}
else if (HAL_GetTick() - state_enter_time > 2000)
{
    error_code = ERROR_ARM_TIMEOUT;
    State_Change(STATE_ERROR);
}

推荐所有关键动作都思考:

正常完成条件是什么?
最长允许多久?
失败后跳到哪里?
是否允许重试?

九、避免使用阻塞式 delay

不推荐:

Motor_SetSpeed(500);
HAL_Delay(3000);
Motor_Stop();

因为这 3 秒中:

推荐改成状态机:

case STATE_FORWARD:
    Motor_SetSpeed(500);

    if (HAL_GetTick() - state_enter_time >= 3000)
    {
        State_Change(STATE_STOP);
    }
    break;

这就是非阻塞式程序设计


十、子状态机

大型机器人不要把所有流程塞进一个状态机。

例如系统可以分成:

System State Machine

├── Chassis State Machine
├── Arm State Machine
├── Vision State Machine
└── Communication State Machine

主状态机负责:

IDLE
AUTO_RUN
MANUAL
ERROR
ESTOP

底盘内部再负责:

STOP
FORWARD
TURN_LEFT
TURN_RIGHT
ALIGN

机械臂内部再负责:

HOME
MOVE_TO_TARGET
GRAB
LIFT
PLACE

这样代码结构会明显清晰。


十一、状态机训练任务

任务 1:LED 状态机

设计:

OFF
SLOW_BLINK
FAST_BLINK
ON

要求:

任务 2:电机运行状态机

设计:

IDLE
ACCELERATE
RUN
DECELERATE
STOP
ERROR

要求:

任务 3:简易自动流程

设计:

WAIT_START
FORWARD
TURN
FORWARD_2
FINISH
ERROR

要求:


第二部分:PID 控制


十二、为什么需要闭环控制

最简单的电机控制是:

Motor_SetPWM(500);

这种方式属于开环控制。

问题是相同 PWM 并不意味着相同速度。

电机速度还会受到:

影响。

因此更可靠的方法是:

目标速度

计算误差

PID

PWM

电机

编码器测量实际速度
   └────────反馈────────┘

这就是闭环控制。


十三、PID 基本公式

误差:

e(t) = target - feedback

连续 PID:

u(t) = Kp × e(t)
     + Ki × ∫e(t)dt
     + Kd × de(t)/dt

其中:

在单片机中使用离散计算。


十四、P:比例项

比例项:

P = Kp × error

例如:

目标速度 = 1000 rpm
实际速度 = 700 rpm
误差 = 300 rpm
Kp = 0.5
P = 150

误差越大,控制输出越大。

Kp 太小

表现:

Kp 太大

表现:


十五、I:积分项

积分项会累积历史误差:

integral += error × dt
I = Ki × integral

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

例如:

目标 = 1000
实际长期稳定在 950

单纯使用 P 时可能始终存在 50 的误差。

加入积分以后,积分会持续累积,直到输出进一步增加,使实际值接近目标。

Ki 太小

Ki 太大

这就是积分饱和问题。


十六、D:微分项

微分关注误差变化速度:

derivative = (error - last_error) / dt
D = Kd × derivative

可以理解为:

如果误差正在快速变小,说明系统即将接近目标,D 项提前“踩刹车”。

因此 D 可以:

但 D 对噪声非常敏感。

编码器速度波动或 IMU 抖动都可能导致 D 项剧烈变化。

因此实际工程中经常:


十七、位置式 PID

位置式 PID 每次直接计算完整输出:

output = kp * error
       + ki * integral
       + kd * derivative;

推荐结构体:

typedef struct
{
    float kp;
    float ki;
    float kd;

    float target;
    float feedback;

    float error;
    float last_error;

    float integral;
    float derivative;

    float output;

    float output_max;
    float integral_max;

    float dt;
} PID_t;

计算函数:

float PID_Calculate(PID_t *pid, float target, float feedback)
{
    pid->target = target;
    pid->feedback = feedback;

    pid->error = target - feedback;

    pid->integral += pid->error * pid->dt;

    if (pid->integral > pid->integral_max)
        pid->integral = pid->integral_max;

    if (pid->integral < -pid->integral_max)
        pid->integral = -pid->integral_max;

    pid->derivative = (pid->error - pid->last_error) / pid->dt;

    pid->output = pid->kp * pid->error
                + pid->ki * pid->integral
                + pid->kd * pid->derivative;

    if (pid->output > pid->output_max)
        pid->output = pid->output_max;

    if (pid->output < -pid->output_max)
        pid->output = -pid->output_max;

    pid->last_error = pid->error;

    return pid->output;
}

十八、PID_Reset 为什么重要

每次重新开始控制前,应考虑清除:

例如:

void PID_Reset(PID_t *pid)
{
    pid->error = 0;
    pid->last_error = 0;
    pid->integral = 0;
    pid->derivative = 0;
    pid->output = 0;
}

特别是在状态机中:

IDLE → RUN

进入 RUN 时通常应该执行:

PID_Reset(&speed_pid);

否则上一次运行留下的积分可能导致电机突然输出较大的 PWM。


十九、采样周期 dt

PID 不能脱离时间讨论。

例如:

pid->dt = 0.01f;

表示 PID 以:

10 ms
100 Hz

运行。

如果 PID 原本 10 ms 运行一次,后来改成 1 ms,但是 Ki、Kd 不做适配,系统响应可能完全不同。

推荐使用固定周期执行 PID:

1 ms
5 ms
10 ms
20 ms

速度环常见:

5~10 ms

位置环可以更慢一些。

不要在一个执行周期完全不确定的 while(1) 中随意调用 PID。


二十、输出限幅

电机 PWM 不能无限大。

例如最大 PWM:

#define MOTOR_PWM_MAX 1000

则 PID 输出必须限制:

-1000 ~ +1000

否则 PID 算出:

3500

最终硬件仍然只能输出 1000,积分却可能继续增加,引起严重积分饱和。


二十一、积分限幅

积分一定要限制。

例如:

if (pid->integral > 500)
{
    pid->integral = 500;
}

否则电机堵转 3 秒时:

目标速度 = 1000
实际速度 = 0
误差持续很大

积分会不断增长。

当障碍物突然解除时,电机可能直接满功率冲出去。


二十二、积分分离

当误差非常大时,不一定需要积分。

例如:

if (fabsf(pid->error) < 200)
{
    pid->integral += pid->error * pid->dt;
}

含义:

误差很大 → 主要用 P 快速接近
误差较小 → 开启 I 消除静差

这叫积分分离。


二十三、积分清零条件

常见工程做法:

if (target == 0)
{
    pid->integral = 0;
}

或者:

if (error * last_error < 0)
{
    pid->integral *= 0.5f;
}

表示误差过零后减弱积分。

具体策略根据系统调整。


二十四、死区

传感器和机构经常存在小范围抖动。

例如位置误差:

±1 mm

不需要一直驱动电机来回修正。

可以设置:

if (fabsf(error) < dead_zone)
{
    error = 0;
}

例如:

dead_zone = 2.0f;

二十五、微分先行与微分滤波

简单微分:

derivative = (error - last_error) / dt;

当目标值突然改变时,误差也会瞬间变化,导致 D 项突然很大。

实际工程中可以对反馈做微分:

derivative = -(feedback - last_feedback) / dt;

或者给微分加入一阶低通:

d_filtered = alpha * d_filtered
           + (1.0f - alpha) * derivative;

这样可以减弱噪声。


二十六、增量式 PID

增量式 PID 不直接计算完整输出,而是计算输出变化量:

Δu(k)

常见公式:

Δu(k) = Kp[e(k)-e(k-1)]
      + Ki e(k)
      + Kd[e(k)-2e(k-1)+e(k-2)]

然后:

u(k) = u(k-1) + Δu(k)

示例:

float PID_Incremental(PID_t *pid, float target, float feedback)
{
    float delta_output;

    pid->error = target - feedback;

    delta_output = pid->kp * (pid->error - pid->last_error)
                 + pid->ki * pid->error
                 + pid->kd * (pid->error
                            - 2.0f * pid->last_error
                            + pid->prev_error);

    pid->output += delta_output;

    if (pid->output > pid->output_max)
        pid->output = pid->output_max;

    if (pid->output < -pid->output_max)
        pid->output = -pid->output_max;

    pid->prev_error = pid->last_error;
    pid->last_error = pid->error;

    return pid->output;
}

位置式和增量式的区别

项目 位置式 PID 增量式 PID
输出 当前完整控制量 控制量变化值
理解难度 较低 稍高
调试 直观 需要理解累计输出
常见应用 位置、速度、角度 电机速度等

入门阶段建议先熟练位置式 PID,再理解增量式 PID。


第三部分:PID 调参


二十七、调 PID 前必须先确认硬件正确

很多“PID 调不出来”其实不是 PID 问题。

调参前必须确认:

最关键的一项:

PID 输出增加时,反馈值必须朝目标方向变化。

如果:

目标速度 = +500
PID 输出 = +PWM
但编码器反馈变成负数

那么闭环方向错误,PID 会直接发散。


二十八、VOFA+ 应该显示什么

建议至少发送四条曲线:

target
feedback
error
output

例如:

printf("%.2f,%.2f,%.2f,%.2f\r\n",
       pid.target,
       pid.feedback,
       pid.error,
       pid.output);

只观察电机“看起来转得差不多”是不够的。

需要看:


二十九、速度环调参流程

推荐先:

Ki = 0
Kd = 0

只调 Kp。

第一步:从很小的 Kp 开始

例如:

Kp = 0.1

逐渐增加:

0.1
0.2
0.5
1.0
...

观察反馈是否更快跟随目标。

第二步:找到明显振荡附近

当 Kp 较大时可能出现:

目标 ───────────
反馈 _/\/\/\/\/\_

说明比例已经过强。

将 Kp 适当降低。

第三步:加入 Ki

如果系统稳定但始终存在静差:

目标 = 500
实际 = 470

逐渐增加 Ki。

第四步:必要时加入 Kd

速度环通常不一定需要 D。

如果噪声较大,D 反而可能让输出抖动。

因此常见方案是:

PI 控制

三十、位置环调参流程

位置环目标例如:

目标位置 = 10000 encoder_count

最简单可以先用 P:

speed_target = position_kp × position_error

然后位置环输出作为速度环目标:

位置误差

位置 PID

目标速度

速度 PID

PWM

这就是串级 PID。


三十一、串级 PID

串级结构:

目标位置

位置环

目标速度

速度环

PWM

电机

代码示例:

float target_speed;
float pwm_output;

target_speed = PID_Calculate(&position_pid,
                             target_position,
                             actual_position);

pwm_output = PID_Calculate(&speed_pid,
                           target_speed,
                           actual_speed);

Motor_SetPWM(pwm_output);

串级 PID 调参原则

先调内环,再调外环。

即:

先把速度环调稳定
再调位置环

因为外环依赖内环。

如果速度环本身都不稳定,位置环无法正常工作。

通常:

速度环执行频率 > 位置环执行频率

例如:

速度环:100 Hz
位置环:20 Hz

第四部分:状态机 + PID 综合设计


三十二、状态机和 PID 如何配合

一个完整机器人控制系统可以这样分工:

状态机负责

何时启动
当前任务
目标是什么
何时结束
失败后怎么办

PID 负责

怎样快速、稳定、准确地达到目标

例如自动小车:

STATE_FORWARD

状态机设置:

左轮目标速度 = 500 rpm
右轮目标速度 = 500 rpm

PID 负责让实际轮速追踪目标。

当编码器位置达到指定值:

STATE_FORWARD → STATE_TURN

此时状态机重新设置目标。


三十三、综合示例:定长行驶

需求:

按下启动键
小车前进 1 米
到达后停止

状态:

IDLE
RUN
BRAKE
FINISH
ERROR

RUN 状态:

case STATE_RUN:
{
    float left_pwm;
    float right_pwm;

    left_pwm = PID_Calculate(&left_speed_pid,
                             500,
                             left_speed);

    right_pwm = PID_Calculate(&right_speed_pid,
                              500,
                              right_speed);

    Motor_SetLeftPWM(left_pwm);
    Motor_SetRightPWM(right_pwm);

    if (distance >= 1.0f)
    {
        State_Change(STATE_BRAKE);
    }

    if (Motor_Stall_Detected())
    {
        error_code = ERROR_MOTOR_STALL;
        State_Change(STATE_ERROR);
    }
}
break;

状态机控制流程,PID 控制速度。


三十四、综合示例:转角控制

目标:

小车原地转 90°

可以使用 IMU 角度反馈:

目标角度

角度 PID

目标角速度 / 差速速度

左右轮速度 PID

PWM

外层:

yaw_output = PID_Calculate(&yaw_pid,
                           target_yaw,
                           actual_yaw);

转换成左右轮目标:

left_target_speed  = -yaw_output;
right_target_speed =  yaw_output;

然后分别进入速度环。


第五部分:异常检测和保护


三十五、堵转检测

典型堵转条件:

PWM 很大
但编码器速度接近 0
并持续一段时间

示例:

if (fabsf(pid_output) > 800 && fabsf(actual_speed) < 20)
{
    if (HAL_GetTick() - stall_start_time > 1000)
    {
        stall_flag = 1;
    }
}
else
{
    stall_start_time = HAL_GetTick();
}

不要写成:

if (speed == 0)
{
    error = 1;
}

因为电机刚启动时速度也可能暂时为 0。


三十六、通信超时

例如视觉模块正常应该每 100 ms 发送一次数据。

记录最后收到数据的时间:

vision_last_rx_time = HAL_GetTick();

检查:

if (HAL_GetTick() - vision_last_rx_time > 500)
{
    vision_online = 0;
}

状态机可以决定:

继续运行
降级运行
进入 ERROR

三十七、传感器异常

需要检查:

例如 IMU 角速度异常:

if (fabsf(gyro_z) > GYRO_MAX_VALID)
{
    imu_error = 1;
}

三十八、急停设计

急停优先级应该高于普通流程。

推荐主循环:

if (Emergency_Stop_Active())
{
    State_Change(STATE_ESTOP);
}
else
{
    State_Update();
}

进入急停后:


第六部分:代码工程化


三十九、推荐文件结构

Core/

├── App/
│   ├── app_main.c
│   ├── state_machine.c
│   ├── state_machine.h
│   ├── robot_control.c
│   └── robot_control.h

├── Control/
│   ├── pid.c
│   ├── pid.h
│   ├── filter.c
│   └── filter.h

├── Driver/
│   ├── motor.c
│   ├── motor.h
│   ├── encoder.c
│   ├── encoder.h
│   ├── imu.c
│   └── imu.h

└── BSP/
    ├── bsp_uart.c
    ├── bsp_can.c
    └── bsp_timer.c

不要写成:

main.c
3000 行

四十、推荐 PID 接口

pid.h

#ifndef __PID_H
#define __PID_H

typedef struct
{
    float kp;
    float ki;
    float kd;

    float error;
    float last_error;
    float prev_error;

    float integral;
    float derivative;

    float output;

    float integral_max;
    float output_max;

    float dt;
} PID_t;

void PID_Init(PID_t *pid,
              float kp,
              float ki,
              float kd,
              float dt,
              float output_max,
              float integral_max);

void PID_Reset(PID_t *pid);

float PID_PositionCalculate(PID_t *pid,
                            float target,
                            float feedback);

float PID_IncrementalCalculate(PID_t *pid,
                               float target,
                               float feedback);

#endif

四十一、PID 参数不要散落在工程中

不推荐:

output = 1.3f * error + 0.05f * integral;

推荐集中管理:

#define LEFT_SPEED_KP  1.3f
#define LEFT_SPEED_KI  0.05f
#define LEFT_SPEED_KD  0.00f

或者保存在参数结构体中。

后续还可以通过串口在线修改 PID 参数。


第七部分:在线调参


四十二、串口在线修改 PID

建议训练一个简单协议:

KP=1.5
KI=0.02
KD=0.1
TARGET=500

单片机解析后:

speed_pid.kp = value;

这样不需要每次:

修改代码
→ 编译
→ 烧录
→ 测试

可以直接实时调参。


四十三、建议的调参数据帧

VOFA+ 输出:

目标值
实际值
误差
P 项
I 项
D 项
总输出

例如:

printf("%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\r\n",
       target,
       feedback,
       error,
       p_out,
       i_out,
       d_out,
       output);

这样出现异常时能快速判断:

是 P 太大?
是积分累积?
还是 D 被噪声放大?

第八部分:综合进阶训练


四十四、训练项目一:单电机速度闭环

目标

让编码器电机稳定运行在指定速度。

必须实现

验收

目标速度依次设置:

200
500
800
500
0
-300

需要观察:


四十五、训练项目二:单电机位置闭环

目标

让电机转到指定编码器位置。

目标:

0 → 5000 → 10000 → 3000 → 0

要求:

进阶:

位置环 + 速度环串级 PID

四十六、训练项目三:小车定距离运动

状态机:

IDLE
START
FORWARD
BRAKE
FINISH
ERROR
ESTOP

要求:


四十七、训练项目四:小车转角

要求:

测试:

+90°
-90°
180°
45°

四十八、训练项目五:完整任务状态机

设计:

BOOT

SELF_CHECK

IDLE

START

MOVE_FORWARD

TURN

ALIGN

ACTION

RETURN

FINISH

任何状态都可能进入:

ERROR
ESTOP

要求:


第九部分:常见错误


四十九、状态机常见错误

1. 到处直接修改状态变量

不推荐:

state = RUN;

工程中几十处都这样写后很难追踪状态来源。

建议统一:

State_Change(RUN);

2. 一个状态做太多事情

如果一个 case 超过几百行,需要拆分。

3. 状态没有超时

导致传感器异常后永久卡死。

4. 状态切换条件互相冲突

例如:

if (finish)
    State_Change(FINISH);

if (error)
    State_Change(ERROR);

需要明确错误和完成谁优先。

5. 在状态中大量使用 delay

会破坏实时性和异常响应。


五十、PID 常见错误

1. 编码器方向反了

表现:

输出越大
误差越大
最终满 PWM

2. 控制周期不固定

导致参数效果随主循环负载变化。

3. 没有输出限幅

PID 输出远超硬件范围。

4. 没有积分限幅

发生积分饱和。

5. 一开始 P、I、D 全部一起调

无法判断哪个参数导致变化。

6. 只看电机,不看曲线

无法准确判断超调和振荡。

7. D 项直接使用高噪声数据

导致输出疯狂抖动。

8. 串级 PID 内外环一起调

正确顺序:

先内环
再外环

第十部分:验收标准


五十一、基础验收

需要能够现场解释:


五十二、代码验收

必须独立完成:

pid.c
pid.h
state_machine.c
state_machine.h
motor.c
motor.h
encoder.c
encoder.h

要求:


五十三、波形验收

必须能够用 VOFA+ 展示:

target
feedback
error
output

并能根据波形解释:


五十四、综合验收项目

建议最终完成一个小车自动流程:

等待启动

直行 1 m

转弯 90°

继续直行 0.5 m

停止

任务完成

必须包含:


十一、推荐学习顺序

建议按照以下顺序训练:

1. 枚举 + switch 状态机

2. 非阻塞定时

3. Enter / Update / Exit

4. 超时和异常状态

5. 编码器速度计算

6. P 控制

7. PI 速度环

8. PID 工程限幅

9. VOFA+ 调参

10. 位置环

11. 串级 PID

12. 状态机 + PID 综合项目

不要一开始直接写复杂串级 PID。

真正重要的是:

每增加一个功能,都必须能够解释它为什么存在、输入是什么、输出是什么、异常时会发生什么。


十二、进一步进阶方向

完成本训练后,可以继续学习:

控制方向

机器人方向

工程方向


十三、最终要求

进阶训练结束后,不应只做到:

“我会套 PID 公式。”

而应该能够做到:

我知道为什么要闭环
我知道反馈是否可信
我知道控制周期是多少
我知道参数为什么这样调
我知道发生振荡时从哪里排查
我知道程序现在处于什么状态
我知道动作失败后系统应该怎么办
我知道如何把控制逻辑拆成可维护的模块

对于电控成员来说,真正重要的不是把某个 PID 参数背下来,而是逐渐形成:

传感器反馈
→ 状态判断
→ 控制目标
→ 闭环控制
→ 执行器输出
→ 异常监测
→ 状态转移

这样一整套完整控制思维。