倒立摆这玩意儿,在控制界算是个经典玩具了。做控制的人如果没跟它打过照面,那基本等于练武术没扎过马步——但问题也出在这:市面上的倒立摆教程,绝大多数走到“线性化”就刹车了。取平衡点、泰勒展开、丢掉高阶项,最后整出一个漂亮的LQR或者状态反馈,仿真图一贴,任务完成。你要是也这么干过,会不会觉得有点不过瘾?反正我是觉得,线性化处理就像在温室里养花,搬出去风一吹雨一打就露馅。真到了摆杆要从自然下垂状态被“踢”起来的时候,线性模型连半个周期都撑不下去,什么仿真优雅全给你打回原形。
所以这次咱们换个玩法:不做线性化处理,直接跟非线性模型硬碰硬。标题里我说适合“会拧螺丝但想玩电焊的老司机”,意思是这篇文章的读者,不是那种刚学完PID还一头雾水的纯新手,而是已经掌握了基础控制方法、想往非线性控制里蹚一蹚的实干派。你可以是正在做机器人、平衡小车项目的研究生,也可以是搞运动控制的工程师。咱们从动力学方程推导开始,一步步把能量控制加滑模控制这套“非线性原教旨”方案跑通,最后给出能跑的Python仿真代码和调参经验。文章里所有公式和代码,我都在仿真和实物项目上蹚过,不少坑是真踩过才知道怎么回事。
1. 线性化在倒立摆上的三个死穴
1.1 第一个死穴:局部模型的工作域小得可怜
先聊聊线性化到底是怎么做的。以倒立摆平衡点(摆杆竖直向上,角度θ=0)附近为例,把sinθ和cosθ做泰勒展开,只留一阶项:
sinθ ≈ θ,cosθ ≈ 1
这套近似在θ很小的时候确实漂亮,但“很小”到底有多小?我算过一笔账:θ=0.1 rad(约5.7°)时,sinθ=0.09983,相对误差只有0.17%,基本可以忽略;θ=0.3 rad(约17°)时,sinθ=0.2955,误差已经涨到1.5%;等θ到了0.5 rad(约28.6°),sinθ=0.479,误差超过4%。而倒立摆从自然下垂到竖直倒立,摆角要跨过整整π rad——这误差已经不是“尺子量赤道”那种级别了,根本就是用看显微镜的尺子去量高速公里。
更麻烦的是,线性化不只是把sinθ改成θ那么简单。它会顺手把很多与角度强相关的耦合项也给“焊死”在常数上。比如摆杆转动对小车运动的反作用项里含有cosθ,线性化之后直接当成常数1处理。在平衡点附近这是聪明的简化,在大角度摆动时就是自欺欺人。我用一句话总结:线性化不是不能用,是它的有效工作域约等于“摆已经快要立住的那一小会儿”。但倒立摆最难的部分,恰恰是如何把摆从“垂着”弄到“立着”。
1.2 第二个死穴:起摆阶段缺少全局结构信息
倒立摆的起摆过程本质是什么?是通过小车的来回移动,把能量“泵”进摆杆系统里,让摆杆从最低点附近积累足够的动能和势能,最后冲过竖直倒立点。这个过程依赖一个关键物理事实:重力力矩在大角度下是非线性的,而且符号会随角度变化。当摆杆在倒立点一侧时,重力会把它往倒立点方向拉;当摆杆在另一侧时,重力方向又会反转。这种“方向翻转”的信息,在线性化模型里根本不存在。
我见过不少同学试图用线性化的状态空间模型设计一个“起摆控制器”,折腾半天发现出来的控制律只会把摆杆当成一个有劲儿的弹簧来回推,能量却泵不进去。为什么?因为线性模型只在平衡点邻域内描述了系统,它“看不见”下垂平衡点那一侧的物理规则。就好比你让一个只见过北京地图的人去规划全国自驾路线,他能画出个大概,但真跑起来必然迷路。非线性模型则把这个叫做“全局构型”的信息完整保留下来,能量法才能正常工作。
1.3 第三个死穴:鲁棒性被高估,一碰扰动就现原形
线性控制理论经常提到稳定裕度,LQR设计出来,增益裕度好像还蛮宽,但那个数字是基于线性化模型的。模型在平衡点附近可靠,稳定性结论才可靠。倒立摆里的非线性项——尤其是θ̇²这种速度耦合项——在大摆角、大角速度下产生的“离心效果”非常明显,线性控制器对它完全没有感知。你设计时没感知,它就专挑你没感知的时候捣乱。实物上常见的场景是:摆好不容易立住了,某次扰动让角度偏出去20°,控制器按线性模型算出来的力方向还是“小角度修正”的样子,结果就是给扰动再踩一脚油门,杆子瞬间摔下去。
以前我在一台小型倒立摆上试过用线性化LQR做全状态反馈,再指望它在起摆成功后直接接管。折腾了一个多星期,仿真怎么跑都稳,一到实物上,起摆过程里稍微有一点点摩擦不均带来的速度波动,杆子就能在切换瞬间摔得哐哐响。后来我才想明白:不是LQR差,是我拿一个局部模型当全局模型用。要根治这个问题,就得在设计阶段把非线性模型的“真实构型”用上。这也是我这篇文章要一路走到底的原因。
2. 从物理到方程:手把手建立倒立摆非线性模型
2.1 建模前的参数约定和坐标系选择
在推公式之前,得先把“物理账”立清楚。我这儿说的倒立摆,是经典的小车-摆杆系统:一辆小车可以在水平轨道上运动,摆杆通过转轴连接在小车上,驱动方式是水平力F推小车,摆杆转轴上没有直接施加的驱动力矩。这个模型是很多平衡小车、机器臂项目的简化抽象,参数也基本可以直接套用。
下面的表是我常用的参数设定,单位我写得很清楚,方便你后续在仿真里直接抄作业:
| 符号 | 含义 | 单位 | 典型值 |
|---|---|---|---|
| M | 小车质量 | kg | 1.0 |
| m | 摆杆质量 | kg | 0.3 |
| l | 摆杆质心到转轴的距离 | m | 0.3 |
| I | 摆杆绕转轴的转动惯量 | kg·m² | 0.03 |
| b | 小车与轨道间的粘滞摩擦系数 | N·s/m | 0.1 |
| g | 重力加速度 | m/s² | 9.81 |
| F | 施加在小车上的水平控制力 | N | -10~10 |
这里得特别提醒一句:建模时用到的l,是“摆杆质心到转轴的距离”,不是整个摆杆的物理长度。很多人一开始没注意,把摆杆总长当l用,模型参数直接差一倍,后面的控制增益全都跟着错。对于匀质摆杆,如果总长是0.6 m,那质心到转轴就是0.3 m;转动惯量按绕端点转轴的匀质杆公式算,大约是I=(1/3)m×总长²=0.3×0.36/3=0.036 kg·m²,取0.03只是在模型里留了一点简化余量。
坐标系我是这么定义的:小车位移x向右为正,摆杆角度θ以竖直向上为0,以顺时针为正。也就是说θ=0是目标倒立平衡点,θ=π是摆杆自然垂吊的位置。但很多国内教材习惯把下垂点定义为θ=0,你要是不小心把两种约定混用,公式里那些正负号能折磨你一整晚。我的建议是:无论用什么约定,每一步都把坐标图在纸上画两遍,再往公式里代。
2.2 用拉格朗日方程推导耦合非线性方程
拉格朗日法对这类带刚性连接的多体系统非常合适,因为它不用去解约束力,只要把系统的动能和势能写出来就行。小车动能是½Mẋ²。摆杆质心速度要小心:质心位置的水平分量是x+l·sinθ,竖直分量是l·cosθ,对时间求导后得到:
v_x = ẋ + lθ̇cosθ v_y = -lθ̇sinθ
摆杆的动能分两部分,一部分是质心平动动能,一部分是绕质心转动动能。把速度代入并展开,总动能为:
T = ½(M+m)ẋ² + m·l·ẋ·θ̇·cosθ + ½(I + m·l²)·θ̇²
势能取倒立点为参考零势能:
V = m·g·l·(1 - cosθ)
这里为什么要用(1-cosθ)而不是cosθ?因为我希望势能在倒立平衡点处是0,这样目标状态的总能量就是0,等会儿做能量控制的时候基准特别清爽。总能量E的表达式后面也会经常用到。
把拉格朗日量L=T-V代入拉格朗日方程,并加上小车轨道摩擦项b·ẋ的耗散力,整理后得到两个强耦合的二阶非线性微分方程:
(M+m)·ẍ + b·ẋ + m·l·θ̈·cosθ - m·l·θ̇²·sinθ = F m·l·ẍ·cosθ + (I + m·l²)·θ̈ + m·g·l·sinθ = 0
第一眼看到这俩方程是不是很头大?我帮你拆开看。第一个方程描述的是小车方向的力平衡:除了整车的质量惯性项(M+m)ẍ,还有摆杆旋转对小车产生的反作用力m·l·θ̈·cosθ,以及一个非常容易被忽略的离心式耦合项m·l·θ̇²·sinθ。第二个方程描述的是摆杆绕转轴的力矩平衡,重力力矩m·g·l·sinθ和大角度下小车加速度通过cosθ传递的影响都在里面。这四个项,每一个都带着sinθ或cosθ,这正是非线性模型和线性模型的本质区别。
为了数值解方便,我把两个方程写成矩阵形式:
[[M+m , m·l·cosθ], [m·l·cosθ , I+m·l²]] · [ẍ ] = [ -b·ẋ + m·l·θ̇²·sinθ + F ] [θ̈ ] [ -m·g·l·sinθ ]
左边这个矩阵叫惯性矩阵,一般是正定的,所以只要给定当前状态和控制力F,就能通过矩阵求逆解出ẍ和θ̈。这个形式是后续所有控制器设计和仿真落地的地基。
2.3 为什么写成矩阵运算而不是背展开式
非线性控制设计里,我们经常把系统写成仿射形式ẋ=f(x)+g(x)·u,但这套形式对倒立摆这种两个人一起求解的状态,手推展开式特别难看。有次我为了跟别人讨论,硬是把θ̈的表达式展开了,写了小半页纸,满屏的sin²θ和cos²θ纠缠在一起,到最后都分不清哪些项可以约掉,浪费了一晚上。
后来我学乖了:直接在代码里用numpy的矩阵求逆,每一个控制周期算一遍D⁻¹,把加速度当场解出来。这么做有几个好处。第一是不容易手滑,矩阵求逆是数值计算,不会出现“丢了个cosθ”这种低级错误。第二是控制律推导时只需要关心α和β这种数值增益,大大简化了表达。第三是换参数的时候不用重新推导公式,if我用展开式,改一下转动惯量就得回归一次代数推导,但在矩阵形式下只改一个数字就行。
这个习惯我一直保留到现在,但凡遇到多体动力学系统,我第一反应都是把动力学写成D(q)·q̈=C(q,q̇,u)的标准形式,然后交给线性代数去处理。这不只是工程上的偷懒,也是模型可维护性的关键。
3. 控制策略设计:先“甩”起来,再“锁”稳住
3.1 起摆阶段:把能量当成唯一的控制标的
倒立摆最刺激的部分就是从下垂到倒立的起摆过程。传统做法是设计一条角度轨迹,然后用非线性跟踪控制去追,但问题是角度轨迹本身该怎么规划?从π到0怎么过渡才不容易过冲?在没有详细动力学参数时,这个问题比想象中麻烦。我采用的是一种更“粗暴”但非常优雅的做法:能量控制。
系统的总机械能就是之前推导时得到的那个表达式:
E = ½(M+m)·ẋ² + ½(I + m·l²)·θ̇² + m·g·l·(1 - cosθ)
目标状态是摆杆在倒立点静止,此时所有速度为0,θ=0,势能取0,所以目标能量E* = 0。起摆问题的核心就变成了:怎样通过外力F,把系统能量从初始位置(最低点)一步步抬到目标能量附近。
怎么做?定义一个李雅普诺夫候选函数V = ½(E - E*)²。对时间求导,会出现能量变化率。忽略摩擦的时候,系统能量变化率恰好等于外力功率F·ẋ(内力不改变总机械能,重力是保守力),所以:
V̇ = (E - E*)·F·ẋ
要让V̇小于0,就得让(E - E*)和F·ẋ异号。最容易想到的是用符号函数强制异号,但那种控制律在实机上会有明显抖振——控制力一会儿正一会儿负,不仅执行器受不了,能量也会在目标附近反复跳动,就是起不来。我的处理是把符号函数换成双曲正切,设计的控制律是:
F_swingup = -k_E · tanh(γ_E · (E - E*) · ẋ)
为什么用tanh?因为tanh的输入和输出同号,当(E-E*)·ẋ为正时tanh为正,前面再加个负号,能量变化率必定为负;同时tanh有平滑饱和特性,输出范围在(-1,1)之间,天然限幅,执行器不会被高频拉扯。k_E和γ_E是调节起摆行为的参数:k_E控制整体力量大小,γ_E控制tanh在零点附近的斜率。
这招在实物上好使到什么程度?我用同样的控制律在电机驱动的平衡小车上做过实验,只要小车行程够,能量控制都能稳定地把摆从下垂“泵”到接近倒立。唯一要注意的是摩擦,如果小车导轨摩擦特别大,理想能量推导里的Ė=F·ẋ就不太成立,控制效果会变肉,解决办法是适当加大k_E,让注入能量跑赢摩擦损耗。
3.2 平衡阶段:用滑模控制器直接对付非线性模型
起摆把摆送到倒立点附近之后,就要切换到平衡控制器。很多人的第一反应是“用LQR呗,平衡点附近效果好得很”。确实,如果只要在±5°范围内维稳,LQR是够用的。但我既然坚持不上线性化,那就直接把非线性模型的滑模控制器设计出来。滑模控制的好处是:它可以把非线性项全部保留并利用起来,同时对抗模型误差和外部扰动的能力也很强。
滑模设计的第一步是选择滑模面。这里我只控制在角度环,先让摆角收敛:
s = θ̇ + λ·θ
λ是正参数,决定摆角误差收敛速度。当系统状态落在s=0的流形上时,θ̇=-λθ,摆角按一阶惯性方式收敛到0,简洁又直观。对s求导得到:
ṡ = θ̈ + λθ̇
要让系统趋近滑模面,我给ṡ设定一个理想的趋近律:
ṡ = -ε·tanh(s/φ) - k·s
其中ε和k是非负增益,φ是边界层厚度。之所以不用sign(s)而用tanh(s/φ),是为了在接近滑模面时平滑过渡,减小高频抖振。这个趋近律意味着,我们希望驱动θ̈达到一个特定的目标值:
θ̈_desired = -λθ̇ - ε·tanh(s/φ) - k·s
但θ̈并不是控制输入,真正的输入是水平力F。怎么把F求出来?还记得之前写的矩阵方程吗?在任意状态(q,q̇)下,解矩阵方程得到:
[ẍ; θ̈] = D⁻¹·(C + [F; 0])
我定义两个关键标量: α = D⁻¹C的第二个分量(无控制力时,摆角的“自然加速度”) β = D⁻¹·[1; 0]的第二个分量(单位水平力对摆角加速度的增益)
那么很自然地就有θ̈ = α + β·F。把期望的θ̈带回去,解出控制律:
F_balance = (θ̈_desired - α) / β
这个公式虽然写了一堆字母,但真正的含义非常清楚:先拿α做非线性前馈,抵消当前角度下重力、科里奥利、摩擦这些自然项;再拿滑模项把系统“钉”在滑模面上。这不比把sinθ改成θ然后祈祷大角度下不出事有说服力多了?
这里还有一个必须提醒的坑:当摆杆接近水平(cosθ≈0附近)时,β会变得接近0。这意味着水平推力对摆角加速度几乎没有直接控制权,物理学上很好解释——力作用线刚好通过转轴,力臂为零,你再怎么用力也转不动摆杆。控制律里除数趋于0,随便一点数值误差都会被放大成爆炸性控制量。我在判断切换时机的时候,专门加了β的下限门槛,做不到的事不硬做。
3.3 起摆控制器和滑模控制器的“接力”逻辑
两套控制器各自负责一个阶段,中间必须有一个可靠的切换逻辑。我用的是“能量+状态”双重判据:当摆角绝对值小于某个阈值、角速度绝对值也小于阈值时,认为起摆控制器已经把摆“喂”到滑模控制器能接管的状态,于是切换;切换后如果状态又跑远了,再切回起摆控制器。为了防止在边界上来回弹跳,我做了滞回:进入平衡模式的阈值比退出平衡模式的阈值更严格,这样系统的两个模式切换带是重叠的,不会出现微小区间内的反复横跳。
我实测过的一组切换参数是:进入滑模控制的θ门槛设为0.3 rad(约17°),角速度门槛1.0 rad/s;退出门槛则放宽到0.5 rad和1.5 rad/s。另外还要在代码里判断当前状态下的|β|,如果|β|<0.01,无论能量控制表现得怎么好,都先别切——因为这时候滑模控制器本身处于“力不从心”的构型。
这个切换逻辑听上去简单,但我最开始做的时候吃了不少亏。有一版我在切换时没加滞回,结果摆杆在阈值附近疯狂切换,一会儿起摆一会儿平衡,整个系统的控制力像抽风一样,把能量控制辛辛苦苦泵进去的能量又抖没了。所以这东西你别嫌麻烦,该加的滞回、该加的判断条件,一个都不能少。
4. Python仿真:从方程到能跑的代码
4.1 为什么用Python而不是Simulink做非线性仿真
倒立摆仿真这个事,工具其实挺多。Simulink图形化建模是一把好手,但我个人更偏爱用Python,原因很朴素:后期要做参数扫描、机器学习和跟其他算法联调都方便。尤其是非线性模型下,控制律里要频繁更新矩阵D和C,用numpy的两个函数搞定,比在Simulink里拖模块连信号线高效得多。
仿真模式上我强烈建议用固定步长的数值积分,最好是自己写一个RK4循环。为什么不用scipy自带的solve_ivp那种自适应步长?因为控制器和真实物理系统是分离的,物理上每一步都在推进,控制量只在采样周期更新一次。自适应步长器为了精度可能会在某一步里反复细化积分步,这时候控制器如果也跟着变,仿真就失真了。固定步长虽然效率低一点,但每个时间步都对应一段真实时间,控制器的“每20ms算一次力”这个节奏就模拟得特别真。
4.2 一个可以直接复制的Python骨架
下面这段代码是把核心的三个部分拆出来:动力学方程、能量起摆控制律、滑模平衡控制律。我加了不少注释,你可以直接拿去跑。
import numpy as np # 系统参数(单位均为国际单位制) M = 1.0 # 小车质量 kg m = 0.3 # 摆杆质量 kg l = 0.3 # 摆杆质心到转轴距离 m I = 0.03 # 摆杆绕转轴转动惯量 kg·m^2 b = 0.1 # 小车粘滞摩擦系数 N·s/m g = 9.81 # 重力加速度 F_MAX = 10.0 # 状态向量 y = [x, theta, x_dot, theta_dot] # theta = 0 为竖直向上,theta = pi 为自然下垂 def dynamics(y, F): x, theta, x_dot, theta_dot = y D = np.array([ [M + m, m * l * np.cos(theta)], [m * l * np.cos(theta), I + m * l**2] ]) C = np.array([ -b * x_dot + m * l * theta_dot**2 * np.sin(theta) + F, -m * g * l * np.sin(theta) ]) acc = np.linalg.solve(D, C) return np.array([x_dot, theta_dot, acc[0], acc[1]]) def system_energy(y): x, theta, x_dot, theta_dot = y return 0.5*(M+m)*x_dot**2 \ + 0.5*(I+m*l**2)*theta_dot**2 \ + m*g*l*(1 - np.cos(theta)) def swingup_law(y, kE=3.0, gamma=1.0): E = system_energy(y) return -kE * np.tanh(gamma * E * y[2]) # y[2] 是 x_dot def balance_law(y, lam=8.0, eps=2.0, k=2.0, phi=0.05): x, theta, x_dot, theta_dot = y D = np.array([ [M + m, m * l * np.cos(theta)], [m * l * np.cos(theta), I + m * l**2] ]) C = np.array([ -b * x_dot + m * l * theta_dot**2 * np.sin(theta), -m * g * l * np.sin(theta) ]) DinvC = np.linalg.solve(D, C) alpha = DinvC[1] beta = np.linalg.solve(D, np.array([1.0, 0.0]))[1] s = theta_dot + lam * theta desired_thetaddot = -lam * theta_dot - eps * np.tanh(s / phi) - k * s if abs(beta) < 1e-6: # 力臂为零的构型,不做无效控制 return 0.0 return (desired_thetaddot - alpha) / beta然后再写一个固定步长RK4主循环。初始状态我这儿直接取小车在原点、摆杆自然下垂、速度全为零:
def simulate(dt=0.001, T=6.0): y = np.array([0.0, np.pi, 0.0, 0.0]) theta_hist = [] t_hist = [] t = 0.0 # 切换参数(带滞回) enter_theta = 0.3 enter_dtheta = 1.0 exit_theta = 0.5 exit_dtheta = 1.5 balancing = False while t < T: # 控制器模式选择 theta = y[1]; theta_dot = y[3] if balancing: if abs(theta) > exit_theta or abs(theta_dot) > exit_dtheta: balancing = False else: if abs(theta) < enter_theta and abs(theta_dot) < enter_dtheta: balancing = True F = balance_law(y) if balancing else swingup_law(y) F = np.clip(F, -F_MAX, F_MAX) # RK4积分 k1 = dynamics(y, F) k2 = dynamics(y + 0.5*dt*k1, F) k3 = dynamics(y + 0.5*dt*k2, F) k4 = dynamics(y + dt*k3, F) y = y + (dt/6.0)*(k1 + 2*k2 + 2*k3 + k4) t_hist.append(t) theta_hist.append(theta) t += dt return np.array(t_hist), np.array(theta_hist)这段代码跑完之后,你把theta_hist画出来,会看到摆角从π附近一路震荡衰减,最后稳定到0附近。这里有个细节我刚才差点漏掉:swingup_law里我写的是np.tanh(gamma * E * y[2]),为什么没有写成(E - E*)?因为E取0,所以(E-E)=E,偷懒省了一行。你要是把目标能量改成其他值,或者系统里有额外储能,就得老老实实把E_ref写清楚。
4.3 参数调节的“手感”从哪来
代码能跑通只是入门,参数能不能调出好效果,才是仿真和实物之间那条真正的分界线。我先挑几个最常调参的参数说。
k_E,能量控制增益,控制起摆阶段的总体“蛮力”。它太小,摆杆会在下垂点附近磨蹭半天,能量曲线跟心电图一样上上下下就是不抬头;它太大,摆杆会像喝多了酒一样冲过头,能量控制器在目标值附近来回补偿,起摆过程变得非常颠簸。我的经验是先从小到大试,k_E从1开始,看6秒内能不能完成起摆,不行就加倍,一般在2到4之间能找到合适的平衡点。
γ_E,这是tanh输入端的斜率系数。γ_E太小,tanh在零点附近几乎是线性的,起摆过程温柔但慢;γ_E太大,tanh趋于符号函数,起摆快但更容易抖。我一般设γ_E=1.0,如果发现起摆后半段能量噪声大,会试着降到0.5。
平衡阶段的几组参数稍有不同。λ取5到10之间比较合适,它决定角度误差的收敛速度,但也影响噪声放大程度,λ太大会让控制器变得“神经质”,一点点角速度噪声都变成大动作。ε和k同时影响趋近滑模面的速度,k是线性收敛项,ε是常数项,后者对抵抗模型误差和外界扰动特别关键,但它也是抖振的主要来源。φ的边界层厚度一般取0.05到0.1,太薄了tanh退化为符号函数,太厚了滑模性质又会被染得一干二净。
我把这些参数的作用和调参方向整理成了表格,方便你快速定位问题出在哪:
| 参数 | 作用 | 过小表现 | 过大表现 |
|---|---|---|---|
| k_E | 起摆能量注入强度 | 摆不起来,能量缓慢 | 摆角震荡,过冲严重 |
| γ_E | 能量误差的“放大器” | 起摆太肉 | 抖振加剧 |
| λ | 滑模面收敛速度 | 收敛慢,低频摆动 | 噪声放大,执行器疲于奔命 |
| ε | 滑模常值趋近项 | 抗扰动能力差 | 抖振明显 |
| k | 滑模线性趋近项 | 收敛慢 | 稳定但过度敏感 |
| φ | 边界层厚度 | 接近sign函数,抖振 | 滑模特性消失,稳态精度差 |
5. 实战踩坑:起摆抖振、切换失稳与参数调校
5.1 症状一:能量控制“泵”不动,摆杆一直左右磨蹭
仿真里最容易遇到的现象是:摆角曲线一直在π附近震荡,幅度没有明显增长,能量误差倒是起起伏伏,但就是攒不出足够多的能量把摆推上去。我排查这个问题的顺序是这样的:先看控制力F是否真的在变化,还是被某个限制卡成常数。如果力已经饱和到底却又没效果,那就不是增益问题,而是系统里摩擦力太狠了。我有一回把摩擦系数b当成0.1,实际模型里却暗中写了0.5,结果能量控制怎么调k_E都起不了摆,浪费了两个小时。
还有一个容易忽略的点:能量控制依赖小车位移x的幅度。如果小车的行程被限幅得非常小(比如物理轨道只有30厘米),能量控制就泵不进去那么多能量,因为每次来回推动空间不够。仿真里没有行程限制,又要加进这个坑。解决办法是加大F_MAX的同时也要考虑滑轨长度,实在不行就用更大摆角的初始状态或者修改轨道约束,把这个物理限制反映到仿真里去。
5.2 症状二:起摆成功后一切换就摔杆
这个是我当年被实物教育得最惨的一个坑。仿真里能量控制把摆杆甩过竖直位置,角度已经小于0.3 rad,角速度也不大,一切换到滑模,按理说应该顺利接管。可实际跑起来,切换后的第一拍往往就会输出一个特别大的控制力,然后“咔”一下把杆子拍飞。
后来我分析了原因,主要有三层。第一层是切换瞬间的构型问题,如果摆杆接近水平或者β很小,滑模控制器算出来的控制力增益是失真的,哪怕公式上没错,实际效果也会放大小误差。第二层是切换时控制量跳变的问题,因为起摆控制器和滑模控制器在切换前后的输出幅值、方向很可能完全不一致,控制力瞬间突变给机械系统一个很大的冲击。第三层是切换阈值定得太宽,进入条件看似满足,但角速度还是太大,滑模控制器要立刻把角速度“掰”回来,已经超出执行器的瞬时能力。
我的解法在3.3里已经提到了:加滞回、判断β、设置更严格的进入条件。但实际操作里还有一个更土但很有用的技巧——在切换后的前0.1秒内给控制力做一个斜坡渐变,不让它跳变,而是从当前值平滑过渡到滑模控制器计算出的目标值。这个“软切换”极大减少了机械冲击,实测摆杆的摔杆概率下降了九成以上。
5.3 症状三:平衡控制抖振得像拖拉机
滑模控制器理论上的优势是鲁棒性强,但直接用sign(s)做趋近律,控制力会处在一种理论上的“无限频”切换中。现实里执行器跟不上这种切换频率,结果就是高频抖振:控制力曲线像锯齿一样噼里啪啦跳,电机发热严重,测到的摆角噪声也特别大。
解决抖振的经典方案就是我在趋近律里用的tanh边界层。φ越大,趋近律在滑模面附近越平滑,控制力越柔和,但代价是稳态精度会下降,因为系统允许在滑模面附近一定的“泄漏”。我做了一组对比:φ从0.01取到0.1,控制力抖振幅度下降了大概70%,而稳态摆角误差只从0.005 rad恶化到0.02 rad左右。对于大多数倒立摆应用来说,这个换算是划算的。如果还嫌抖,我还会给控制量再加一个截止频率约为采样频率1/5的一阶低通滤波,疗效更明显。
5.4 症状四:模型参数不准,非线性控制器也开始“打摆子”
有人觉得非线性控制器天生对参数不敏感,这是误解。滑模控制对模型误差有鲁棒性,但鲁棒性是有代价的——如果你的模型参数误差太大,α这个非线性前馈项就算得不准,控制器要借用更大的ε和k去压制误差,最后你就得在抖振和响应速度之间做痛苦权衡。
我做过一个实验:把模型里的m从0.3改成0.4,也就是误差33%,其他参数不变,看看滑模控制还能不能稳住。结果是可以,但控制力明显变得“急躁”,抖振量增大了大概一倍,稳态精度也降了一个数量级。老老实实做一次模型辨识,把质量和转动惯量校到10%以内,比后期调大ε去“硬扛”要划算得多。所以我建议仿真阶段就加一点参数摄动测试,看看控制器增益余量够不够,心里有个底。
5.5 症状五:仿真步长和采样周期不匹配导致的“假失控”
最后一个坑跟仿真实现有关。有些同学把仿真步长设得很大,比如0.05秒一积分,控制器却是每0.01秒更新一次,结果系统看起来“失控”了。这个失控不是控制器设计的锅,而是数值积分精度不够,尤其是能量起摆阶段,θ̇项非线性和重力力矩都很“凶”,大步长RK4积分会引入明显的相位滞后,相当于给原系统额外加了一堆人为延迟。
我建议仿真步长设到0.001秒,控制器采样周期可以放宽到0.01秒,模拟数字控制器里“每10ms更新一次控制量,但积分步还是1ms”的真实场景。这样系统的数值误差可以有效控制,控制器的离散化效应也能如实反映。要是时间约束比较紧,可以先用欧拉法粗跑一遍,再上RK4验证关键参数点,别一上来就在大步长上跟算法死磕。
最后补两句动手心得
这套倒立摆非线性控制的完整流程,我从头到尾跑下来的最大体会是:别迷信参数调优能救一切,真正决定成败的,是建模阶段对物理结构的理解。能量起摆能推多久、滑模切换会不会在某个构型上失灵、β在什么地方接近零,这些问题只有在不看线性化掉的那堆项之后,才会浮出水面,逼着你去思考系统的真实约束。
写这篇文章的时候,我特意把很多失败路径都写了进去。你如果在仿真或者实物上踩到类似的坑,别急着怀疑是自己笨。这个领域就是这样,理论公式看着干净,一旦把执行器饱和、摩擦、噪声这些“脏东西”加进来,每个环节都得重新过一遍。我对自己的要求是:任何控制器仿真,先把执行器饱和和传感器噪声这两样加进去再调。前期麻烦归麻烦,之后上实物的心里那叫一个踏实。
倒立摆这个“玩具”,够你折腾很久,也希望你能从“拧螺丝”顺利晋级到“玩电焊”的阶段。真调通了,顺手把同一套思路搬到平衡车、机械臂甚至无人机姿态控制上,你会发现,原来非线性并没有想象中那么难啃。