简介:这份资源围绕自由摆平板控制系统展开,面向参加电子设计竞赛或从事嵌入式控制学习的学生与工程师,帮助解决摆动过程中平板保持水平、硬币不滑落及激光笔定向打靶等动态控制问题。压缩包内共1个docx文档,约36KB,内容以完整设计方案与论证为主,涵盖STM32F103VET6主控、TFT液晶显示、WDD35D4角位移传感器、步进电机及四宏细分驱动器等模块的选型比较与实现思路。文档从摆架框架、检测系统、控制驱动系统三部分展开,详细记录控制器、角度检测、电机与电源模块的方案对比,并给出A/D采样反馈、电机转向与转角调节等关键环节的设计依据。目前已有305人学习,适合需要参考赛题方案、理解开环步进控制与传感器反馈闭环思路的读者,也可作为嵌入式动态控制项目的方案论证与硬件选型参考。
1. 自由摆的控制系统:从「立不起来」到「稳得住」的那条分界线
倒立摆这东西,第一次上手的人几乎都会经历同一个瞬间:明明 PID 参数在仿真里跑得好好的,一上实物,摆杆刚过 5 度就「啪」地拍在限位上了。自由摆的控制系统要解决的,就是这件事——让一根本来不稳定、随时会倒的杆子,靠电机、编码器和一小段控制律,稳稳停在竖直位置,还能抗住你用手推它一下。
它适合谁?做自动控制课程设计的、想入门现代控制理论的、准备把 LQR 或 MPC 落到真实硬件上的工程师。因为倒立摆是极少数「模型清楚、非线性明显、状态可观、成本可控」的验证平台,你能在一台几百块的设备上,把建模、线性化、状态反馈、观测器、实时性这些概念全部走一遍。这篇笔记就按「先建模、再选控制律、再落地调参、最后避坑」的顺序,把自由摆控制系统讲透,参数和代码都能直接抄。
2. 自由摆的动力学建模:为什么不能直接套 PID
2.1 从拉格朗日方程到状态空间
自由摆(这里指一级直线倒立摆:小车在导轨上平动,摆杆绕小车铰点转动)的完整动力学是非线性的,但绝大多数工程实现都从线性化模型入手。设小车质量 M、摆杆质量 m、摆杆半长 l、小车位移 x、摆角 θ(竖直向上为 0)、重力加速度 g、小车所受控制力 F、摩擦系数 b。
用拉格朗日方程推导,忽略摆杆转动惯量的高阶项后,在 θ≈0 附近线性化,得到经典形式:
(M + m)·ẍ + m·l·θ̈ = F - b·ẋ m·l·ẍ + m·l²·θ̈ = m·g·l·θ把它整理成状态空间 ẋ = Ax + Bu,状态取 [x, ẋ, θ, θ̇]:
import numpy as np # 物理参数(按常见小型直线倒立摆取值,单位 SI) M = 0.5 # 小车质量 kg m = 0.2 # 摆杆质量 kg l = 0.3 # 摆杆半长 m g = 9.81 # 重力加速度 b = 0.1 # 小车粘性摩擦系数 den = M + m # 线性化后的 A、B 矩阵 A = np.array([ [0, 1, 0, 0], [0, -b/den, -(m*g)/den, 0], [0, 0, 0, 1], [0, b/(l*den), g*(M+m)/(l*den), 0] ]) B = np.array([[0], [1/den], [0], [-1/(l*den)]]) print("A =\n", A) print("B =\n", B)这段代码的关键在于:A 矩阵第 4 行第 3 列g*(M+m)/(l*den)是正数,意味着 θ 的加速度与 θ 同号——这就是「不稳定」的数学来源,极点有一个落在右半平面。参数说明:l是半长不是全长,很多人第一次建模就栽在这里,导致算出来的极点位置和实测对不上;b如果忽略,仿真会偏乐观,实物上表现为小车「刹不住」。
2.2 能控性与能观性:先确认这题有解
建模完别急着设计控制器,先算能控性矩阵的秩。如果 rank 不满,说明你选的执行器或状态量根本控不住这个系统。
from numpy.linalg import matrix_rank # 能控性矩阵 Ctrb = np.hstack([B, A@B, A@A@B, A@A@A@B]) print("能控性矩阵秩:", matrix_rank(Ctrb)) # 应为 4 # 能观性:假设只测小车位移和摆角 C = np.array([[1, 0, 0, 0], [0, 0, 1, 0]]) Obsv = np.vstack([C, C@A, C@A@A, C@A@A@A]) print("能观性矩阵秩:", matrix_rank(Obsv)) # 应为 4秩为 4 说明系统完全能控能观,可以放心往下走。如果这里算出来小于 4,常见原因是参数单位写错(比如把 cm 当 m)或者状态量漏了一个。
2.3 为什么纯 PID 在自由摆上会翻车
很多人第一反应是「摆角误差进 PID,输出给电机」。仿真里能立住,实物上大概率振荡甚至发散。原因有三:一是 PID 只用了 θ 一个状态,没有利用 ẋ 和 θ̇ 的阻尼信息,相位裕度不够;二是倒立摆是非最小相位系统,小车要先往反方向动一下才能把摆杆拉回来,PID 的即时响应反而帮倒忙;三是积分项在限幅附近会 windup,摆杆一旦偏大,积分饱和,回不来。
所以工程上更稳的路线是:状态反馈(LQR 或极点配置)做内环,再用一个慢速外环做位置跟踪。下面一章就讲怎么把 LQR 落到实物上。
3. 用 LQR 把自由摆立起来:权重怎么调、代码怎么写
3.1 LQR 的代价函数与权重物理含义
LQR 求解的是 u = -Kx,使代价 J = ∫(xᵀQx + uᵀRu)dt 最小。Q 和 R 不是玄学,它们有明确的物理含义:Q 的对角元越大,对应状态被「压得越狠」;R 越大,越舍不得用力,控制更柔和但恢复慢。
对倒立摆,我一般这样起步:
| 状态 | 含义 | 初始 Q 对角值 | 调大后的效果 |
|---|---|---|---|
| x | 小车位置 | 1 | 位置回中更快,但摆角波动变大 |
| ẋ | 小车速度 | 10 | 抑制小车来回窜 |
| θ | 摆角 | 100 | 摆杆更稳,是首要保证量 |
| θ̇ | 摆角速度 | 10 | 减少摆动过冲 |
| R | 控制量 | 0.01 | 越小越激进,电机更容易饱和 |
θ 的权重通常给到 x 的 50~100 倍,因为立住是第一优先级,位置可以慢慢回。
3.2 求解反馈增益 K 的完整代码
from scipy.linalg import solve_continuous_are import numpy as np # 权重矩阵 Q = np.diag([1.0, 10.0, 100.0, 10.0]) R = np.array([[0.01]]) # 求解代数 Riccati 方程 P = solve_continuous_are(A, B, Q, R) # 反馈增益 K = np.linalg.inv(R) @ B.T @ P print("K =", K)solve_continuous_are解的是 AᵀP + PA - PBR⁻¹BᵀP + Q = 0,得到 P 后 K = R⁻¹BᵀP。参数说明:Q 和 R 必须是正定对称矩阵,R 对单输入系统就是标量;如果求解报错,先检查 A、B 维度是否匹配,或者 R 是否写成了 0。
3.3 离散化与实时控制循环
实物控制器跑在 MCU 或实时系统上,必须把连续 K 离散化,或者直接用离散 LQR。采样周期 T 一般取 1~5 ms,太慢会丢相位,太快则噪声放大。
from scipy.signal import cont2discrete T = 0.005 # 5ms 采样 Ad, Bd, _, _, _ = cont2discrete((A, B, np.eye(4), np.zeros((4,1))), T) # 离散 LQR 同样可用 dare 求解,这里直接复用连续 K 做近似 # 更严谨做法:用离散 ARE 重新解 Kd from scipy.linalg import solve_discrete_are Pd = solve_discrete_are(Ad, Bd, Q*T, R*T) Kd = np.linalg.inv(R*T) @ Bd.T @ Pd @ Ad print("Kd =", Kd)注意 Q、R 在离散化时要乘 T 做尺度对齐,否则权重含义会漂。控制循环里读编码器、算 Kx、输出 PWM,顺序不能乱:先读全部状态,再算控制量,最后输出,中间不要插入串口打印,否则 5ms 周期会被打乱。
3.4 状态观测器:编码器不够用时怎么办
如果只有小车位移和摆角两个编码器,没有测速,就需要观测器估计 ẋ 和 θ̇。最省事的是差分加低通,但噪声大;正规做法是 Luenberger 观测器或卡尔曼滤波。
# 观测器极点配置,比控制器极点快 3~5 倍 from scipy.signal import place_poles obs_poles = np.array([-20, -22, -24, -26]) L = place_poles(A.T, C.T, obs_poles).gain_matrix.T print("观测器增益 L =", L)place_poles要求极点不能重复太多,否则数值病态。观测器带宽高于控制器,才能保证估计误差先收敛。实测中如果摆杆高频抖动,多半是观测器把编码器量化噪声放大了,把观测器极点往左推一点或加输出滤波。
4. 自由摆控制系统的避坑与排查:5 条血泪记录
4.1 现象:仿真能立,实物一上电就朝一个方向冲
原因:电机接线极性反了,或者力方向定义与模型 B 矩阵符号不一致。解决:先开环给一个小的正力,看小车往哪边走、摆杆往哪边倒,和模型对照,把 B 的符号改对再闭环。
4.2 现象:摆杆能立但持续小幅高频抖动
原因:控制周期抖动、编码器量化噪声被微分放大、K 的 θ̇ 项过大。解决:把控制循环放进定时器中断,关掉中断里的浮点打印;对 θ̇ 做一阶低通,截止频率取控制带宽的 3~5 倍;适当减小 Q 中 θ̇ 的权重。
4.3 现象:用手推一下摆杆,回中后小车位置偏了很远
原因:LQR 只保证状态收敛,不保证位置无静差,x 权重给太小。解决:增大 Q 中 x 的权重,或者在外环加一个位置积分项,但积分要限幅,否则又会 windup。
4.4 现象:摆角超过 15 度后控制器完全拉不回来
原因:线性化模型只在 θ≈0 附近有效,大角度下非线性项主导,LQR 失效。解决:设置「起摆」逻辑,小角度用 LQR,大角度切换到能量法或直接限位保护;不要指望一个线性控制器覆盖全角度。
4.5 现象:电机发热严重,PWM 长期饱和
原因:R 给太小,K 过大,控制量长期顶到限幅。解决:增大 R,或者对控制量做限幅并加抗饱和(back-calculation);同时检查导轨摩擦是否过大,机械阻力会让控制器一直用力。
5. 从立住到抗扰:自由摆控制的一个进阶技巧
把摆立住只是及格线,真正体现控制系统功力的是「抗扰」和「跟踪」。这里给一个我常用的进阶做法:在 LQR 内环外面套一个基于状态观测的扰动补偿,用一个扩张状态观测器(ESO)估计总扰动,再前馈抵消。
思路是这样:把模型误差、摩擦、外部推力全部归为总扰动 f,扩张成第五个状态,用 ESO 在线估计,控制量变成 u = -Kx - f̂/b0。这样即使模型参数不准,摆杆也能稳住。
# 简化版扩张状态观测器(离散) # z1~z4 估计状态,z5 估计总扰动 beta = np.array([100, 200, 300, 400, 500]) # 观测器带宽参数 b0 = 1/den # 控制增益估计 def eso_update(z, y, u, T): e = y[0] - z[0] # 以小车位移为观测输出 z = z + T * (A@z[:4] + B.flatten()*u + np.array([0,0,0,0,1])*z[4]) z[:4] += beta[:4] * e z[4] += beta[4] * e return z参数说明:beta按观测器带宽 ωo 配置,常用 [5ωo, 10ωo², 10ωo³, 5ωo⁴, ωo⁵] 的量级,ωo 取控制器带宽的 3~5 倍。b0是控制增益的粗略估计,不准也没关系,ESO 会补。实测中这套结构对「导轨不平」「摆杆质量标称不准」这类问题效果明显,代价是调参多了一个维度,新手建议先把纯 LQR 调稳再上。
验证方法很简单:摆杆立稳后,用手指轻推摆杆顶部,看恢复时间和超调;再在小车导轨上贴一小块胶带制造阻力,对比开/关 ESO 的摆角波动幅度。如果 ESO 开启后恢复时间缩短 30% 以上,说明扰动估计起了作用。
我自己踩过最深的坑,是早期迷信「一套参数走天下」,换了摆杆长度还在用旧 K,结果怎么调都抖。后来养成习惯:每次改机械结构,先重新测 l 和 m,重算 A、B,再解 K。这个习惯比任何高级算法都值钱。希望帮到你。
本文还有配套的精品资源,点击获取