电控组进阶训练:状态机与 PID
本文作为《26_机器人创新团队电控暑期学习》的进阶训练补充。
适合已经完成 GPIO、串口、定时器、PWM、编码器、电机基础控制,并具备基本 C 语言模块化能力的同学。
本阶段不再以“能让电机转起来”为目标,而是开始训练控制逻辑设计、闭环控制、调参、异常处理和工程化思维。
一、训练目标
完成本阶段后,应能够独立完成以下任务:
- 使用枚举、结构体和
switch-case编写清晰的有限状态机 - 设计状态进入条件、退出条件、超时条件和异常转移
- 避免在
while(1)中堆叠大量相互耦合的if - 编写非阻塞式流程控制代码
- 理解位置式 PID 和增量式 PID
- 能解释 P、I、D 三项对系统动态响应的影响
- 正确处理采样周期、积分饱和、输出限幅和微分噪声
- 使用 VOFA+ 同时观察目标值、反馈值、误差和 PID 输出
- 能独立完成速度环 PID 调参
- 能独立完成位置环 PID 调参
- 能建立位置环 + 速度环串级 PID
- 能将状态机和 PID 组合成完整控制流程
- 能设计超时、堵转、传感器异常和急停逻辑
- 能将状态机、PID、电机、编码器等拆分成独立模块
最终目标是做到:
状态机负责“现在应该做什么”,PID 负责“这个动作应该怎么稳定地完成”。
第一部分:状态机
二、为什么机器人程序需要状态机
初学者很容易写出下面这种程序:
while (1)
{
if (key == 1)
{
motor_run();
}
if (sensor == 1)
{
motor_stop();
}
if (flag == 2)
{
servo_run();
}
if (error == 1)
{
motor_stop();
}
}
功能少时还能运行,但随着项目增加,会逐渐出现:
- 条件互相覆盖
- 一个变量被多个位置修改
- 无法判断当前执行到哪一步
- 某个动作重复触发
delay()阻塞其他功能- 出现异常时不知道应该跳到什么流程
- 修改一个功能后影响另外多个功能
例如一个搬运机器人可能需要执行:
上电
↓
初始化
↓
等待启动
↓
寻找目标
↓
接近目标
↓
抓取
↓
搬运
↓
放置
↓
返回
↓
任务完成
这类“按阶段执行”的系统非常适合用状态机描述。
三、状态机的基本概念
有限状态机英文为:
Finite State Machine
FSM
一个状态机通常包含四个核心元素:
- 状态 State
- 事件 Event
- 状态转移 Transition
- 状态动作 Action
例如:
WAIT
│ 按下启动键
↓
RUN
│ 检测到终点
↓
STOP
其中:
WAIT、RUN、STOP是状态- “按下启动键”是事件
WAIT → RUN是状态转移RUN状态中执行电机控制就是状态动作
四、最基础的状态机写法
首先使用枚举定义状态:
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:进入状态时只执行一次
例如进入运行状态时:
- 清零编码器
- 清零 PID 积分
- 记录开始时间
- 打开电机
- 设置目标值
2. Update:处于该状态时周期执行
例如:
- PID 计算
- 读取传感器
- 判断结束条件
- 判断异常条件
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
要求:
- 按键切换状态
- 禁止使用
HAL_Delay() - LED 闪烁必须使用系统时间实现
任务 2:电机运行状态机
设计:
IDLE
ACCELERATE
RUN
DECELERATE
STOP
ERROR
要求:
- 加速阶段 PWM 缓慢增加
- 运行 3 秒
- 减速阶段 PWM 缓慢下降
- 电机堵转时立即进入 ERROR
任务 3:简易自动流程
设计:
WAIT_START
FORWARD
TURN
FORWARD_2
FINISH
ERROR
要求:
- 每个状态有独立超时
- 串口实时输出当前状态
- 急停可以从任何状态进入
ESTOP
第二部分: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
其中:
Kp:比例系数Ki:积分系数Kd:微分系数u(t):控制输出
在单片机中使用离散计算。
十四、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 太大
- 超调明显
- 振荡严重
- 积分累积过多
- 目标变为 0 后仍然持续输出
这就是积分饱和问题。
十六、D:微分项
微分关注误差变化速度:
derivative = (error - last_error) / dt
D = Kd × derivative
可以理解为:
如果误差正在快速变小,说明系统即将接近目标,D 项提前“踩刹车”。
因此 D 可以:
- 降低超调
- 抑制快速振荡
但 D 对噪声非常敏感。
编码器速度波动或 IMU 抖动都可能导致 D 项剧烈变化。
因此实际工程中经常:
- 速度环先使用 PI
- 位置环使用 P 或 PD
- 必要时对 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 问题。
调参前必须确认:
- 电机正反转方向正确
- 编码器方向正确
- 编码器数值稳定
- 速度计算公式正确
- 编码器线数正确
- 减速比正确
- PWM 输出范围正确
- 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();
}
进入急停后:
- 电机立即关闭
- 舵机停止动作或进入安全位
- PID 清零
- 禁止自动恢复
- 等待人工确认
第六部分:代码工程化
三十九、推荐文件结构
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 被噪声放大?
第八部分:综合进阶训练
四十四、训练项目一:单电机速度闭环
目标
让编码器电机稳定运行在指定速度。
必须实现
- PWM 驱动
- 编码器读取
- 速度计算
- 固定周期控制
- PI 控制
- 输出限幅
- 积分限幅
- 串口修改目标值
- VOFA+ 曲线
验收
目标速度依次设置:
200
500
800
500
0
-300
需要观察:
- 是否稳定跟踪
- 是否明显振荡
- 是否存在大幅超调
- 正反转是否正确
四十五、训练项目二:单电机位置闭环
目标
让电机转到指定编码器位置。
目标:
0 → 5000 → 10000 → 3000 → 0
要求:
- 使用位置环
- 设置死区
- 设置最大速度
- 到位后不能持续明显抖动
进阶:
位置环 + 速度环串级 PID
四十六、训练项目三:小车定距离运动
状态机:
IDLE
START
FORWARD
BRAKE
FINISH
ERROR
ESTOP
要求:
- 双电机速度 PID
- 编码器计算距离
- 运行到指定距离后减速停止
- 堵转检测
- 超时检测
- 串口输出状态
四十七、训练项目四:小车转角
要求:
- IMU 读取 yaw
- 原地旋转 90°
- 角度 PID
- 左右轮速度环
- 转角完成后自动停止
- 误差进入死区后保持稳定
测试:
+90°
-90°
180°
45°
四十八、训练项目五:完整任务状态机
设计:
BOOT
↓
SELF_CHECK
↓
IDLE
↓
START
↓
MOVE_FORWARD
↓
TURN
↓
ALIGN
↓
ACTION
↓
RETURN
↓
FINISH
任何状态都可能进入:
ERROR
ESTOP
要求:
- 每个状态都有 Enter / Update / Exit
- 每个运动状态都有超时
- 每个执行器都有反馈判断
- 电机闭环控制
- 串口输出状态变化
- 能打印错误码
- 不允许使用长时间阻塞式
delay
第九部分:常见错误
四十九、状态机常见错误
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 内外环一起调
正确顺序:
先内环
再外环
第十部分:验收标准
五十一、基础验收
需要能够现场解释:
- 什么是状态机
- 为什么状态机比大量
if更适合复杂流程 - 什么是 Enter / Update / Exit
- 为什么状态需要超时
- 什么是非阻塞程序
- P、I、D 各自作用
- 什么是积分饱和
- 为什么 D 对噪声敏感
- 什么是位置式 PID
- 什么是增量式 PID
- 什么是串级 PID
- 为什么串级 PID 先调内环
五十二、代码验收
必须独立完成:
pid.c
pid.h
state_machine.c
state_machine.h
motor.c
motor.h
encoder.c
encoder.h
要求:
- 能正常编译
- 无大量警告
- 命名清晰
- 不依赖大量全局变量
- PID 参数集中管理
- 状态切换统一管理
- 有异常保护
五十三、波形验收
必须能够用 VOFA+ 展示:
target
feedback
error
output
并能根据波形解释:
- 上升时间
- 超调
- 振荡
- 静差
- 输出饱和
五十四、综合验收项目
建议最终完成一个小车自动流程:
等待启动
↓
直行 1 m
↓
转弯 90°
↓
继续直行 0.5 m
↓
停止
↓
任务完成
必须包含:
- 状态机
- 双电机速度 PID
- 编码器
- IMU
- 串级控制或角度控制
- 非阻塞时序
- 超时保护
- 堵转保护
- 急停
- VOFA+ 调试
- Git 版本记录
十一、推荐学习顺序
建议按照以下顺序训练:
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
- ADRC
- LQR
- MPC 基础
机器人方向
- 麦克纳姆轮运动学
- 底盘速度解算
- 里程计
- IMU + 编码器融合
- 云台串级 PID
- 平衡车控制
- 机械臂位置控制
工程方向
- 分层状态机 HSM
- 事件驱动状态机
- FreeRTOS 状态管理
- 软件看门狗
- 故障树
- 参数在线调节
- Flash 保存 PID 参数
- 上位机参数管理
- 自动化调参和日志分析
十三、最终要求
进阶训练结束后,不应只做到:
“我会套 PID 公式。”
而应该能够做到:
我知道为什么要闭环
我知道反馈是否可信
我知道控制周期是多少
我知道参数为什么这样调
我知道发生振荡时从哪里排查
我知道程序现在处于什么状态
我知道动作失败后系统应该怎么办
我知道如何把控制逻辑拆成可维护的模块
对于电控成员来说,真正重要的不是把某个 PID 参数背下来,而是逐渐形成:
传感器反馈
→ 状态判断
→ 控制目标
→ 闭环控制
→ 执行器输出
→ 异常监测
→ 状态转移
这样一整套完整控制思维。