ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

倒立摆Simulink仿真:从状态空间建模到LQR与PID控制

倒立摆Simulink仿真:从状态空间建模到LQR与PID控制

简介:倒立摆是经典的控制系统研究对象,其非线性特性和稳定性问题具有很高的学习价值。这套仿真资源面向自动化、控制工程及相关专业的学生与工程师,提供在MATLAB/Simulink环境下搭建倒立摆模型的完整方案,适合课程设计、科研入门或自学实践。压缩包共包含9个文件,以MATLAB脚本(.m)、Simulink模型(.slx和.mdl)为主,另附PPT与Word文档,分别用于说明建模思路、控制原理和仿真结果分析,整体仅372KB。资源内容覆盖倒立摆动力学模型搭建、PID/LQR等控制策略设计、Simulink子系统构建以及仿真参数配置,多个不同版本的模型和脚本便于对照不同实现方式,从数学模型到控制器实现均有可运行示例。目前已有2356人学习下载,适合需要快速上手倒立摆仿真或进行课程设计的读者参考,能够帮助深入理解非线性系统建模与控制理论。

1. 倒立摆 Simulink 仿真:看着是搭模型,实际是搭控制律

倒立摆在 Simulink 里的仿真,表面上是拖几个积分器再加个 Scope 的事,真正让新手翻车的全在后半程:模型方程怎么解耦、状态顺序怎么约定、LQR 算出来的反馈增益为什么接进模块就发散。这些坑我在调平衡车和某跨平台系统的模拟项目X里都踩过。这篇文章按我习惯的落地路径走:先把被控对象的数学模型线性化,再在 Simulink 里搭可复现的仿真模型,接着用 LQR 作为主控制方案、PID 作为兜底方案,最后给出参数整定顺序和排错手段。适合正在做课程设计或者准备把倒立摆算法迁移到实物上的工程师,读完能照着建一个自己的仿真工程,而不是只交一份截图报告。

2. 从动力学方程到状态空间:线性化这一步决定仿真成败

2.1 为什么先建模再打开 Simulink

很多初学者打开 Simulink 就开拖,拖完发现仿真结果完全不可信。原因很简单:控制器的设计,尤其是 LQR 这类状态反馈控制,依赖的是被控对象的状态空间模型,而不是 Scope 里的波形。必须先在小角度平衡点附近把非线性方程线性化成标准的 A、B 矩阵,Simulink 里的仿真模型才有意义。

我一般用集中质量模型来做第一次仿真验证:小车质量 M、摆杆末端集中质量 m、摆杆长度 L,驱动力 F 作用在小车水平方向。这个模型忽略了摆杆自身转动惯量和摩擦,但足够用于验证控制算法;等控制律调稳了,再换成更精细的刚体动力学模型不迟。

这个模型的非线性方程可以写成两组:

水平方向力平衡方程:

(M + m) ẍ + m L (θ̈ cosθ - θ̇² sinθ) = F

绕摆轴转动方程:

L θ̈ + ẍ cosθ = g sinθ

其中 θ 是摆杆偏离竖直方向的角度,顺时针为正,θ=0 是平衡点。这两个方程是耦合的,Simulink 里不能直接一个积分器画到底,需要先解耦出 ẍ 和 θ̈ 的显式表达式,后面建模块时才不会出现代数环。

2.2 小角度线性化:手算 A、B 矩阵

在 θ≈0、θ̇≈0 附近做线性化,sinθ≈θ、cosθ≈1、θ̇²≈0。把上面的方程组化简后解出:

θ̈ ≈ [(M + m) g θ - F] / (M L)

ẍ ≈ (F - m g θ) / M

选定状态向量为 [x, ẋ, θ, θ̇],控制输入为 F,就可以直接写出状态空间表达式 ẋ = A x + B F:

A = [0 1 0 0; 0 0 -mg/M 0; 0 0 0 1; 0 0 (M+m)g/(ML) 0] B = [0; 1/M; 0; -1/(ML)]

这套矩阵和我后面 Simulink 模型里用的状态顺序完全一致。记住这个约定,后面接 LQR 反馈增益时就不会出现状态顺序错位的问题。实际工程里最常见的翻车点,就是这里用 [θ, θ̇, x, ẋ] 的顺序算 K,Simulink 里却按 [x, ẋ, θ, θ̇] 接,结果特征值怎么看都是稳定的,一仿真就飞。

2.3 参数基准表与单位约定

仿真要可复现,参数必须固定。我常用的基准参数如下表,单位全部采用国际单位制:

参数符号数值单位
小车质量M0.5kg
摆杆末端质量m0.2kg
摆杆长度L0.3m
重力加速度g9.81m/s²
初始角度θ(0)0.1rad

角度一律用弧度,严禁在 Simulink 模块里混用角度制。我见过有人把初值填成 10,Scope 里直接出来一条冲到天上去的线,最后发现是把 10° 当成 10 rad 填进去了。单位统一这件事,写进脚本第一行注释都不为过。

3. 在 Simulink 里把模型搭起来:模块选择与参数表

3.1 搭非线性模型:MATLAB Function 块加双积分链

先建一个空模型,命名为 invpend.slx。我习惯把整个被控对象封装成一个子系统,里面放一个 MATLAB Function 块,输入是状态向量和驱动力 F,输出是 ẍ 和 θ̈,再用两条积分链还原出速度与位置。这里的核心是避免代数环,所以 MATLAB Function 块内部必须直接给出 ẍ、θ̈ 的显式解。

双击 MATLAB Function 块,写入如下代码:

function [xdd, thdd] = pendulum_states(x, xdot, th, thdot, F, M, m, L, g) % 倒立摆非线性模型解耦表达式 % 输入: 小车位置x、速度xdot、摆角th、角速度thdot、驱动力F % 输出: 小车加速度xdd、摆角角加速度thdd sinTh = sin(th); cosTh = cos(th); % 从转动方程解出角加速度 % theta_dd = ((M+m)*g*sin(th) - F*cos(th) - m*L*thdot^2*sin(th)*cos(th)) % / (L*(M + m*sin(th)^2)) numTh = (M + m) * g * sinTh - F * cosTh - m * L * thdot^2 * sinTh * cosTh; denTh = L * (M + m * sinTh^2); thdd = numTh / denTh; % 再代入水平力平衡方程解出小车加速度 % x_dd = (F - m*L*theta_dd*cos(th) + m*L*thdot^2*sin(th)) / (M+m) xdd = (F - m * L * thdd * cosTh + m * L * thdot^2 * sinTh) / (M + m); end

这里的解耦表达式就是用第一节的联立方程手推出来的,注意 thdd 先算出来再代入 xdd 的表达式,这样 MATLAB Function 块内部是一条纯前向计算链,不会产生代数环。如果反过来在函数里用 xdd 和 thdd 互相引用,Simulink 在求解时就会报代数环错误,仿真步长稍微一改结果就变,这是模块搭建阶段最值得注意的一件事。

函数参数的传递方式,建议把 M、m、L、g 做成 MATLAB Function 块的参数,而不是在函数体里写死。双击 MATLAB Function 块,在编辑器的「编辑数据」里把 M、m、L、g 定义为参数,然后在外部工作空间赋值。这样做的好处是后面做批量调参时不需要改函数代码,只需要改工作空间的变量。

3.2 积分链与控制反馈环怎么接

模型内部的信号连接顺序如下:MATLAB Function 块输出 thdd,接一个积分器得到 thdot,再接一个积分器得到 th;输出 xdd,接积分器得到 xdot,再接积分器得到 x。四个状态通过 Mux 合成一个四维向量,作为状态反馈控制器的输入。

控制器部分我用一个 Gain 块实现状态反馈,增益矩阵就是 LQR 计算出来的 K。这里有一个非常隐蔽的坑:Simulink 的 Gain 块默认是元素级乘法,K 是 1×4 的矩阵,状态向量是 4×1,直接连上来会把 K 的四个元素分别乘到四个状态上,完全不是矩阵乘法。必须双击 Gain 块,把乘法模式从「Element-wise(K.u)」改成「Matrix(Ku)」,否则仿真出来的结果和理论完全对不上,还很难排查。

子系统内部完整信号链如下:

状态向量 [x; xdot; th; thdot] -- 进入控制器子系统 控制器输出 F 回到 MATLAB Function 块的 F 输入 MATLAB Function 输出 xdd/thdd 接积分链 积分链产生的四个状态再接回控制器输入端

为了观察控制器未生效时被控对象的开环特性,建议在 MATLAB Function 块的 F 输入端前面加一个「手动开关」模块,调试时先打到零输入,看看摆杆是不是自然倒下,确认模型方向正确后再切到反馈控制。这一步能省掉大量无效排错。

3.3 线性化快速验证模型:State-Space 模块

对于只想验证 LQR 算法的场景,可以不搭非线性模型,直接用 Simulink 的 State-Space 模块。在脚本或工作空间里定义好 A、B、C、D 矩阵,然后在模块参数里填变量名即可。这样搭建速度快,也方便用线性系统理论来校核仿真结果。

M = 0.5; m = 0.2; L = 0.3; g = 9.81; A = [0 1 0 0; 0 0 -m*g/M 0; 0 0 0 1; 0 0 (M+m)*g/(M*L) 0]; B = [0; 1/M; 0; -1/(M*L)]; C = eye(4); D = zeros(4, 1);

State-Space 模块的参数设置里,把 A、B、C、D 分别填成变量名,初始状态 x0 填 [0; 0; 0.1; 0],表示摆杆初始偏角 0.1 rad。这里的 C 矩阵用单位阵,是为了让 Scope 能同时看到 x、ẋ、θ、θ̇ 四个状态的变化。

我建议两个模型都建:非线性模型用于最终验证,线性模型用于控制律设计和对拍。调好一组 K 之后,先在 State-Space 模型上确认闭环特征值合理,再到非线性模型上跑一次。如果两者结果在小角度范围内差异小于 5%,说明模型搭建正确,控制律也基本没问题。

4. 控制器设计与参数整定:LQR 为主,PID 兜底

4.1 用 LQR 算状态反馈增益:Q 权重的物理含义

倒立摆平衡控制最标准的做法是 LQR,原因不是它精度有多高,而是它显式地考虑了每个状态的代价,调参时脑子里有物理图景。设计代码如下:

% 被控对象参数 M = 0.5; m = 0.2; L = 0.3; g = 9.81; % 状态空间模型,状态顺序: x, xdot, th, thdot A = [0 1 0 0; 0 0 -m*g/M 0; 0 0 0 1; 0 0 (M+m)*g/(M*L) 0]; B = [0; 1/M; 0; -1/(M*L)]; % LQR 权重矩阵 % Q 对角元素对应: x, xdot, th, thdot Q = diag([1000, 10, 100, 1]); R = 1; % 求解反馈增益 K = lqr(A, B, Q, R); % 校核闭环极点,确认都在左半平面 eig_cl = eig(A - B * K); disp(K); disp(eig_cl);

Q 矩阵的每一个对角元素都有明确的物理含义:Q(1,1) 对应小车位置 x 的代价,权重越大,小车越不愿意偏离原点;Q(3,3) 对应摆杆角度 θ 的代价,权重越大,摆杆越不愿意偏离竖直方向;Q(2,2) 和 Q(4,4) 分别对应速度和角速度的代价,主要起阻尼作用,抑制振荡。R 对应控制力 F 的代价,R 越小控制越激进,R 越大控制越保守。

我给新手推荐的调参顺序是:先把 Q 设成 diag([1, 1, 1, 1]) 跑通,确认能站稳;然后逐步加大 Q(3,3) 的权重,让角度恢复更快更硬;再加大 Q(1,1) 的权重,让小车回到原点;最后加一点 Q(2,2) 和 Q(4,4) 的权重抑制超调。不要一上来就填一组大数字,否则控制量可能超出物理可行的范围,仿真里看起来稳了,实际机构根本执行不了。

4.2 仿真里加限幅:没有饱和的 LQR 是骗人的

LQR 设计出来的控制力是线性的,仿真中如果不加限幅,角度扰动稍大时 F 可能算出几百牛顿,这在小车上根本不可能。我在 Simulink 的控制器输出端一定会加一个饱和模块 Saturation,限幅范围按实际电机推力设定,一般取 ±20 N 到 ±50 N 之间。

要注意的是,加饱和之后系统变成非线性,LQR 的稳定性保证就只在小角度范围内成立了。所以在仿真中要测试一下:初始角 0.1 rad 时能稳定、0.3 rad 时是否还能拉回来、0.5 rad 时是不是就倒下了。这个临界角度就是这套线性控制器的适用范围,写报告时也是重要的技术指标。

4.3 PID 兜底:什么时候用串级 PID

LQR 虽然好,但有人因为没接触过状态空间或者想少写矩阵,仍然会用 PID。单环 PID 只能控制角度,控制结果是摆杆稳住但小车一路漂移,跑出轨道边界。所以我一般建议用串级 PID 结构:内环角度 PD 控制摆杆竖直,外环位置 P 控制小车归位。

典型起点参数如下表:

控制环控制器比例系数范围微分/积分系数
内环角度PD80~150微分 5~15
外环位置P 或 PI1~5视位置超调情况加积分

内环的微分项是关键,相当于给系统加阻尼,没有它摆杆会在平衡点附近来回抖。外环输出的是内环角度目标值,也就是通过主动让摆杆倾斜一个微小角度来带动小车归位,这是倒立摆位置控制的经典思路。

LQR 和 PID 的对比,我建议做成一张表放到报告里:LQR 调参次数少、抗扰能力强、天然多变量耦合,缺点是依赖精确模型;PID 实现简单、物理意义直观,但串级结构调起来更繁琐,抗扰上不如 LQR。对做实物的人来说,我的经验是先 LQR 把平衡点站稳,再用 PID 对付小幅外部扰动,两种算法没有绝对优劣之分。

5. 倒立摆仿真的四个高发坑:排查路径与复现手法

5.1 初始角度稍大就发散,控制器好像失效了

现象:初始角 0.1 rad 时仿真稳定,改成 0.5 rad 后摆杆直接倒下,或者出现持续振荡不收敛。

原因:LQR 是基于平衡点附近线性化模型设计的,当初始角超过约 0.35 rad(20°)时,sinθ≈θ 和 cosθ≈1 的近似误差已经很大,线性控制器不能保证非线性系统的稳定性。另一个可能原因是加了饱和限幅,控制量饱和导致等效增益下降,系统进入非线性失稳区。

解决:先确认仿真模型是非线性模型而不是线性 State-Space 模型;如果是线性模型,大角度发散说明控制律本身没问题,只是适用边界小。要覆盖大角度场景,要么改用能量控制把摆杆摆起后再切 LQR 平衡段,要么在 Q 矩阵中显著提高角度权重,同时允许更大的控制力上限。我一般会在仿真里做一个扫角测试:0.05、0.1、0.2、0.3、0.5 rad 逐个跑一遍,把稳定边界记录成一条曲线,这样后续做实物时心里有底。

5.2 仿真报代数环错误,或者求解器一换结果就变

现象:Simulink 提示「代数环」警告;或者同一个模型用 ode45 和 ode15s 跑出来波形差别很大;严重时 Scope 里直接出现 NaN。

原因:MATLAB Function 块里如果 xdd 和 thdd 互相引用,或者反馈信号不经过任何延迟直接参与自身计算,就会形成代数环。求解器会尝试迭代解这个环,解不出来就发散。另外倒立摆系统本身在模型存在刚度差异时,对求解器也比较敏感。

解决:在 MATLAB Function 块内部严格按「先算 thdd、再算 xdd」的前向顺序计算,不互相引用;如果确实存在难以消除的代数环,就在反馈回路里加一个 Memory 模块切断直接信号路径,代价是引入一拍延迟,通常不影响小角度平衡控制的稳定性。求解器方面,固定步长推荐 1e-3 秒到 1e-4 秒,变步长推荐 ode23t 或 ode15s 这类对中等刚性系统更稳的求解器。血泪经验是:每次改模型结构之后,顺手看一下求解器配置页,确认没被改回默认值。

5.3 闭环特征值明明稳定,仿真就是飞了

现象:在脚本里用 eig(A-B*K) 看,特征值实部全为负,理论上系统稳定;但同样的 K 接进 Simulink,Scope 里波形直接发散。

原因:八成是状态顺序不一致。脚本里算 K 时状态顺序是 [x; xdot; th; thdot],Simulink 里 Mux 合流的顺序变成了 [th; thdot; x; xdot],反馈增益张冠李戴。另一个常见原因是 Gain 块没有切换成矩阵乘法模式,把矩阵乘变成了逐元素乘。

解决:在脚本开头和 Simulink 的 Mux 处都写上状态顺序注释,并把 K 作为工作空间变量传参,不要在 Gain 块里手工填一串数字。Gain 块的乘法模式必须改为「Matrix(K*u)」。验证方法很简单:把 Mux 的顺序和脚本里定义的状态顺序逐行比对一次,通常 30 秒就能发现错位。

5.4 摆杆站稳了,但小车一直往一个方向漂移

现象:θ 稳定在 0 附近,波形很漂亮,但 x 坐标单调增大,仿真后期小车已经跑到边界外。

原因:如果用的是单环角度 PID,状态里根本没有位置项,控制器不会关心小车在哪;如果用的 LQR,说明 Q(1,1) 和 Q(2,2) 相对角度权重太小,位置偏差产生的代价不足以让控制器纠正。

解决:LQR 方案中加大 Q(1,1) 和 Q(2,2),比如从 [1000, 10, 100, 1] 调到 [5000, 50, 500, 5],再观察 x 是否回归原点。PID 方案中必须加外环位置控制,输出作为内环角度目标,形成串级结构。调完后加一步验证:给定初始位置偏移 x0=0.5 m,看小车能否回到原点附近,这个测试能直接暴露位置环欠缺的问题。

6. 把仿真变成可信工具:工作点验证与批量调参脚本

仿真做到能站稳只是第一步,能批量调参、量化评估性能、并验证模型可信度,才算把仿真变成可迁移到实物的工具。我每次调完一组参数都要跑一遍评估脚本,而不是肉眼看 Scope 波形。

下面是一个可复用的批量调参脚本骨架:

% 批量 LQR 调参脚本 % 先运行模型参数定义部分,再运行本脚本 clear; clc; M = 0.5; m = 0.2; L = 0.3; g = 9.81; A = [0 1 0 0; 0 0 -m*g/M 0; 0 0 0 1; 0 0 (M+m)*g/(M*L) 0]; B = [0; 1/M; 0; -1/(M*L)]; % 候选 Q 矩阵 Q_candidates = { diag([1000, 10, 100, 1]); diag([2000, 20, 300, 2]); diag([5000, 50, 500, 5]); }; R = 1; resultTable = table(); for i = 1:length(Q_candidates) Q = Q_candidates{i}; K = lqr(A, B, Q, R); % 把 K 传到 Simulink Gain 块 assignin('base', 'K_gain', K); % 运行仿真 sim('invpend'); % 从 To Workspace 提取数据 % 假设 To Workspace 输出变量名为 simout,格式为 Array t = simout(:, 1); x = simout(:, 2); theta = simout(:, 4); % 计算稳定时间: 角度进入正负0.02 rad 且保持 idx = abs(theta) < 0.02; if any(idx) settleTime = t(find(idx, 1, 'first')); else settleTime = NaN; end % 计算小车最大位移和控制量峰值 maxAbsX = max(abs(x)); maxAbsU = max(abs(simout(:, 5))); % 记录结果 resultTable = [resultTable; table(Q(1,1), Q(3,3), settleTime, maxAbsX, maxAbsU)]; end disp(resultTable);

脚本里关键的一步是用 assignin 把 K 推送到基础工作空间,Simulink 的 Gain 块参数直接写 K_gain 即可。这样跑多个候选 Q 矩阵时不需要手动改模块参数。To Workspace 模块的输出格式建议设成 Array,一列时间、一列 x、一列 xdot、一列 θ、一列 θdot、一列控制量。

模型可信度验证方面,我常用的做法是拿非线性模型和线性 State-Space 模型对拍。初始角 0.1 rad、控制器不接入时,两条 θ 曲线应当高度重合;接入同一条 LQR 控制律后,稳定时间和超调量差异应小于 10%。差异过大说明非线性模型实现有误,优先检查 MATLAB Function 块里的解耦公式和单位换算。

我最后留一个习惯,也当作给看这篇文章的人一个提醒:每次跑新参数前,先确认工作空间里的 A、B 矩阵和 Simulink 模型里的状态顺序一致,这个检查只需要一分钟,但能省掉半天排查时间。那次我把 Q 权重调了两天,最后发现只是 MATLAB Function 块里状态排列顺序和 K 的排列差了一位,从那以后我再也没凭感觉跳过这步检查。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进