做机器人控制仿真的人,几乎都经历过“模型一发散就想关电脑”的瞬间。我之前在Simulink里搭一个人形机器人的上半身模型,本来只是想让躯干保持直立,顺手加了一条手臂正弦摆动轨迹,结果一运行,躯干角度波形抖到屏幕都快装不下。单独看姿态稳定控制器,PID参数调得还算顺手;单独看手臂摆动控制,轨迹跟踪误差也很小。可两个回路放进同一个模型里,就开始互相拆台。
这其实是人形机器人控制里特别典型的场景:上半身姿态稳定与手臂摆动协同控制。你看人体走路的时候,手臂之所以自然摆动,不只是为了协调好看,更是利用手臂反作用力矩来抵消躯干旋转扰动。姿态稳定和上肢运动之间存在强耦合,不能把它们当成两个独立任务来处理。这篇内容,我就想把这个案例完整拆开讲一遍——从模型怎么选、控制器怎么搭、手臂轨迹怎么规划,到联调时那几个最容易踩的坑,争取让刚接触Simulink做机器人仿真的朋友少走一段弯路。
1. 为什么偏偏拿“上半身姿态+手臂摆动”开刀
1.1 人形机器人控制里最磨人的耦合问题
人形机器人的难点不在于单关节,而在于多刚体之间的力矩耦合。单个关节做PD控制能调得很干净,但一旦把躯干、双臂、双腿连成一条运动链,问题就变了:髋关节用力修正躯干倾斜时,地面支撑腿会受到反作用,躯干质心位置又会变化,连带手臂的惯性力矩也会叠加到躯干上。
具体到这个案例里,姿态稳定回路通过髋关节(或者腰部关节)输出力矩,目的是把躯干角度维持在期望值附近;手臂摆动回路通过肩关节输出力矩,目的是让手臂跟踪某条轨迹。问题在于:手臂加速摆动的时候,根据牛顿第三定律,肩关节同时会对躯干施加一个方向相反的反作用力矩。这个力矩对姿态稳定控制器来说就是一个外部扰动。扰动小的时候,PID还能压下去;手臂摆幅一大、频率一高,姿态环就压不住了,于是你看到躯干来回晃。
反过来也一样:姿态稳定器通过髋关节输出力矩修正躯干姿态时,躯干会带动肩关节基座运动,手臂的实际轨迹也会跟着被拉偏。两套控制器如果各调各的,谁都不知道对方在干什么,系统就很容易变成一个震荡源。
1.2 一个够用又不至于太复杂的仿真模型
完整的人形机器人多体模型包含几十个自由度,仿真模型要是把每个关节都建出来,光是参数标定和初始值设置都能耗掉你一个下午。做“上半身姿态+手臂摆动”协同控制,我建议别一上来就追求个体仿真度极限,而是搭一个能表达核心耦合关系的简化模型。
我当时搭的模型是这样的:把下半身简化为一个固定在髋关节位置的基座,或者更直接一点,让髋关节以下保持静止刚性支撑,只关注躯干和两条手臂。躯干看成一个倒立摆等效刚体,在髋关节处有一个俯仰自由度,代表上半身前后倾斜;两条手臂分别在左右肩关节处各有一个俯仰自由度,用来模拟前后摆动。每个刚体只需要质量、质心位置、转动惯量这几个参数,就可以用欧拉-拉格朗日方程推导出完整的运动方程。
这样的简化虽然丢了腿部反作用力的细节,但保留了核心的物理耦合:手臂摆动会对躯干产生反作用力矩,躯干姿态变化又会影响肩关节基座的位置和姿态。要研究姿态稳定和手臂协同控制,这些信息已经足够;如果一上来就用Simscape Multibody拉全套模型,反而容易被装配约束和碰撞检测等问题绊住。
1.3 这个场景的复现价值在哪
研究这个场景,不只是为了看一个“躯干不倒”的笑话型Demo。它是一个天然的中间练习:既有经典的姿态稳定闭环,又有带相位和幅值规划的轨迹跟踪,还有两个控制任务之间的优先级协调——这三要素几乎贯穿了所有足式机器人上层控制。
把姿态环、手臂轨迹环、协同力矩分配三层都跑通之后,后续再往模型里加腿部步态模块、视觉感知模块,甚至从仿真转到实机,都只是在这个框架里做加法。这也是我为什么拿这个例子当控制仿真入深水区的跳板。
2. 建模仿真前必须想清楚的四件事
2.1 模型选型:倒立摆、浮动基座还是多刚体
倒立摆模型确实适合用来练LQR和PID的基础整定,但它有个硬伤:质心位置相对固定,无法体现手臂摆动对系统质心偏移的影响。你让倒立摆上挂一个摆臂,它就不再是标准倒立摆了。因此,至少要引入多刚体动力学。
在Simulink里有两条路:一是用Simscape Multibody直接在图形环境里拖刚体、加关节、设约束,调起来直观,但仿真内核把很多细节封装了,你能看到结果却不容易看清每个力矩项;二是我更常用的做法——自己推导简化模型的动力学方程,用MATLAB Function模块把方程写进去,或者直接用积分模块搭状态方程。
自己写方程的优势在于:你可以把重力项、科里奥利项、反作用力矩项单独提取出来观察。姿态稳定控制里用得很多的“前馈补偿”,本质上就是把明显可见的干扰项先算出来、再消掉;如果你用的是封装好的多体模块,这部分直觉不容易建立起来。
2.2 执行器模型不能只给一个阶跃
很多人做控制器时把电机当成理想力源,输出多少力矩它就给多少。到联调阶段突然发现实际系统根本跟不上指令。提前在仿真里加入执行器限制,能让你理解什么叫做“控制输出饱和”。
我习惯给每个关节的力执行器加上三样东西:力矩饱和限幅、输出延迟、摩擦力矩。Simulink里可以直接用饱和模块加一个简单的一阶惯性环节来模拟电机响应特性。参数怎么定?要看你想法的物理平台。比如一个舵机级别的肩关节,峰值力矩可能只有十几牛米,响应时间在几十毫秒;如果是带减速器的电机模组,峰值力矩可能能到上百牛米但延迟更大。仿真里先把极限值设得不那么“理想”,后面换真实参数时才不会被性格突变打个措手不及。
2.3 控制目标先写成数学式
建控制器之前花十分钟把控制目标写成表达式,能避免后面“调一点改一点”的毛线球状态。对这个案例,我会这样写:
- 姿态稳定目标:躯干角度 θ_trunk(t) 在存在手臂反作用力矩扰动下,仍然趋向参考 θ_ref(t),差值最终小于容差 δ;
- 手臂跟踪目标:左右肩关节角度 φ_L(t)、φ_R(t) 分别跟踪期望轨迹 φ_des_L(t)、φ_des_R(t);
- 协同目标:由手臂摆动引起的躯干反作用力矩尽可能小,或者被姿态控制器用较小的输出力矩吸收,不以大幅度牺牲手臂轨迹为代价。
把目标写出来之后,你自然会发现:单纯把两个PID的输出力矩加起来根本没法同时满足第二条和第三条。这时候就需要引入任务优先级或协调权重,而不是闷头去调PID参数。这个判断本身就是这个案例最有价值的部分。
2.4 Simulink模型结构从一开始就要分组
我见过太多模型一根线拉到底,所有反馈信号挤在一路总线里,出问题的时候定位通宵。搭Simulink模型时,我会按信号类型分三块:传感器反馈块、控制计算块、执行器与机械模型块。传感器反馈块负责将模型状态转换为带噪声的测量信号;控制计算块内部再细分姿态控制器、手臂轨迹生成器、协同求解器;执行器与机械模型块接收力矩指令,更新系统状态。
三块之间用明确的Signal Bus连接,并且给每条总线定义清晰的名字,比如sensor_bus、cmd_bus。这样后续添加画面和数据记录都方便。注意,控制器如果是离散控制,一定要给控制计算块设置采样时间,并把它包成Atomic Subsystem,否则Simulink在变步长求解时会用随机步长采样你的控制器信号,结果很难复现。
3. 上半身姿态稳定控制器的完整实现路径
3.1 姿态反馈信号从哪来,IMU仿真别太乐观
真实机器人上,上半身姿态通常靠IMU的加速度计和陀螺仪融合得到。仿真里最偷懒、也最危险的做法是直接拉一根线把模型里的理想角度送到控制器。这样调出来的参数基本没法用,因为真实信号有噪声、有漂移、有延迟。
我给模型加测量环节的时候,会根据实际传感器特性构造一个近似IMU输出:给理想姿态角和角速率叠加高斯白噪声,再通过一个低通滤波器模拟惯性环节。一开始噪声强度可以给小一点,比如角度噪声标准差0.01弧度,角速度噪声0.05弧度每秒,先看控制器能不能压得住,再逐渐把噪声加大。你会发现,姿态角上一个看起来不大的一阶低通滤波,就能把相位余量吃掉一大截。
关于滤波方案,实测下来单纯低通滤波会带来明显滞后,角度环容易不稳定。我自己更推荐互补滤波或者更轻量的课程级卡尔曼滤波,姿态估计的带宽更高。不过互补滤波里有一个权重系数需要调,跑仿真时可以试几组对比,感受一下延迟和噪声抑制是如何此消彼长的。
3.2 串级PID:角度环在外面,角速度环在里面
单环PID直接对姿态角输出力矩,也能工作,但抗干扰能力一般。更好用的是航天器姿态控制里常见的串级结构:外环是姿态角度环,输出期望角速度;内环是角速度环,输出力矩。
The control law looks roughly like this:
ω_ref = Kp_a * (θ_ref - θ_trunk) + Ki_a * ∫(θ_ref - θ_trunk) dt
τ_hip = Kp_d * (ω_ref - ω_trunk) + Ki_d * ∫(ω_ref - ω_trunk) dt
外环给的ω_ref是“躯干需要转多快才能回到目标角度”,内环再通过力矩把这个速度跟踪上。这样做的物理意义更清楚:角速度环可以更快抑制扰动,而角度环负责保证最终精度。
整定的顺序我推荐从内环开始:先给Kp_d一个较小的值,在阶跃角速度指令下观察响应,逐步加大Kp_d到系统开始出现微小振荡,然后引入Kd_d(通常对应对角加速度反馈)来增强阻尼。内环稳定后再开角度环,按经典PID方式整定。这个过程虽然听起来简单,但串级结构里如果内环没调稳,外环参数无论怎么组合都会留一个“隐性风险”。
3.3 零力矩点约束怎么在Simulink里参与控制
姿态稳定不只是“躯干角度不偏”。对于一个支撑在地面上的机器人来说,更重要的稳定性判据是地面反作用力合力穿过支撑多边形内部的点,这个点叫零力矩点。ZMP一旦跑出脚掌接触范围,机器人就开始翻转。
在简化仿真模型里,支撑区域可能只是一个虚的支撑点或者小面积面。我们可以基于模型状态在线计算ZMP位置,比如用躯干质心的加速度和重力方向来推测地面反作用力位置。我知道很多人在这个例子里容易忽略ZMP,因为只关注上半身,臀部和腿部都被固定了,ZMP似乎不会变。但一旦加入腿部弹性或基座自由度,ZMP就会表现出来。所以我的建议是:在模型里加一个ZMP计算模块,不一定要立即用它做控制,但至少把它送到示波器里,看你做手臂摆动时ZMP到底移动了多少。
更进阶的玩法是在控制分配层加入ZMP约束:当ZMP接近支撑多边形边界时,主动限制姿态控制力矩的增量,或者让手臂摆动幅度变小变慢,避免进一步恶化稳定性。这是从“姿态稳定”走向“动态稳定”的关键一步,值得多花一点时间。
3.4 我怎么判断PID参数算调好了
仿真里调参最怕进入“只看曲线不思考数值”的状态。我认为一组好的姿态稳定参数,至少要满足三个可量化的标准:在存在手臂正常扰动时,躯干角度稳态偏差不超过0.5°;阶跃扰动的调节时间在0.8秒以内;全程无明显等幅振荡,超调量不超过15%。
我给一组适用于简化模型的参考参数,注意这只是起点,不是万灵药:角度环Kp_a = 12,Ki_a = 0.4,Kd_a = 2.5;角速度环Kp_d = 1.6,Ki_d = 0.1,Kd_d = 0.02。采样时间取1ms。用这组参数跑出来的典型曲线,躯干角能够在扰动后大约0.5秒内回到参考角度,髋关节力矩峰值不会超过执行器限幅。你的模型参数不一样,可能差别很大,关键是看趋势。
4. 手臂摆动协同:反作用力补偿与摆动规划
4.1 手臂摆动为什么能帮上身站稳
很多人以为手臂摆动只是视觉重心在发挥作用,其实它是真实的力矩补偿手段。你可以想象在平衡木上的人张开双臂:手臂朝某个方向摆动,身体会获得一个反向的角动量。手臂反方向摆动时,躯干受到的扰动力矩与手臂加速度相反,在一定程度上抵消了姿态控制器的负担。
在仿真模型里,肩关节对外输出力矩让手臂转动的同时,躯干会收到一个反向力矩。现象就是:手臂往前提得快,躯干就会出现向后仰的趋势。反过来讲,如果我们预先规划手臂轨迹,让这个反向力矩在关键时刻帮助髋关节推动躯干回到期望姿态,那姿态控制力矩就可以小一点,对执行器也友好很多。
所以「协同控制」的核心思路是:把手臂不要只当作要跟踪轨迹的对象,还要当作一个可以被姿态稳定需求调用的小型执行器。轨迹跟踪中可以人为叠加一部分修正信号,让手臂的加速度曲线兼顾姿态补偿需求。
4.2 摆动轨迹生成:别用固定正弦一摆到底
最简单的做法是把期望肩关节角度设成φ_des = A * sin(2πft + phase),A是摆幅,f是摆动频率,phase是左右手臂的相位差。但我在实测中发现,直接把这个正弦角度送给PD跟踪控制器,两个问题很快就会浮出水面:一是手臂快速往返时,关节速度与力矩需求突变,电机限幅会导致轨迹跟踪误差被放大;二是正弦轨迹完全不考虑躯干姿态误差,姿态偏了之后手臂还在按原节奏摆,协同效果为零。
更好的做法是基于“参考加速度”来规划:先规划手臂的期望角加速度轨迹,再经过两次积分得到期望角度和角速度。这样可以把手臂惯性力和反作用力矩都纳入规划框架。比如你期望手臂在某个时刻产生一个向后的补偿力矩,对应的就是一条平滑的加速度脉冲,而不是单纯的正弦位置轨迹。在Simulink里实现这个用积分模块加轨迹生成函数就行。
4.3 协同控制器的分层结构
我用的协同结构是三层:
上层:运动规划器。生成期望躯干参考姿态、左右手臂期望摆动轨迹,以及一个协调变量。协调变量可能是摆动时刻与姿态偏差的映射关系,姿态偏差增大时摆动幅度自动减小;
中层:姿态稳定控制器。输入是躯干姿态误差,输出是髋关节修正力矩以及一个「期望反作用力矩」信号。这个信号告诉上层/下层需要从手臂补偿中得到多大的力矩帮助;
底层:手臂轨迹控制器。输入是期望手臂轨迹和来自中层的补偿力矩需求,输出是肩关节力矩指令。
这三层之间非常重要的概念是优先级:姿态稳定永远是第一优先任务,手臂轨迹跟踪是第二优先任务。换句话说,如果姿态偏差过大,系统可以主动牺牲手臂轨迹的精确性,优先保住躯干稳定。这和人走路被绊一下时手臂会条件反射乱甩其实是同一个道理。
4.4 力矩混叠与优先级处理技巧
直接让姿态控制器给躯干输出力矩,同时肩关节控制器按原计划跟踪轨迹,两者相安无事,那是理想情况。实际联调时你会发现:肩关节不仅要完成手臂轨迹跟踪,还要额外输出补偿力矩给躯干,可同一个电机能输出的力矩是有限的。加在一起很容易就饱和了。
处理方式有两种,对应不同的复杂程度。
第一种是加权减法:τ_shoulder = w_task * τ_tracking + (1 - w_task) * τ_compensation,姿态偏差大时,把w_task调小,让手臂补偿力矩的权重上升;姿态稳定后,再把w_task恢复,让手臂恢复正常轨迹。这个思路实现简单,但要手工调整权重,且没有考虑电机极限。
第二种是零空间投影。把姿态稳定任务对应的关节力矩投影到手臂轨迹控制任务的零空间里:τ = τ_posture + N^T * τ_tracking,其中N是姿态稳定任务雅可比矩阵的零空间投影矩阵。这样可以保证躯干姿态优先,同时在剩余自由度里尽可能跟踪手臂轨迹。零空间方法虽然要一些矩阵运算基础,但它在Simulink里写一个MATLAB Function并不难。我个人建议如果你想往人形机器人控制方向深入,这一块值得花时间啃。
5. 联调时最容易被坑的三个细节
5.1 传感器延迟与滤波相位滞后怎么定
联调第一步,我先不开手臂摆动,只测姿态控制器的鲁棒性。这时候最容易暴露的问题是:加了传感器滤波之后,原本稳定的参数开始振荡。原因很简单,低通滤波会引入相位滞后,相当于给控制器串了一个纯延迟环节。滞后越大,相位裕度越小,原本留的稳定性余量被吃光。
排查方法也很直接:先记录不加滤波器时的阶跃响应,然后加滤波器,观察超调量和振荡频率。如果振荡频率明显下降且波形带有“慢悠悠”的漂移感,大概率是滤波滞后太大。解决办法有三种:降低滤波截止频率是不行的,因为那会让滞后更大;真正有用的思路是改用互补滤波或状态观测器,把角度与角速率信号双通道融合;或者在控制器里对相位滞后做一个“超前补偿”环节,但那样要进一步细调。
5.2 执行器饱和和积分饱和几乎一定会遇到
串级PID里的积分项,我见过太多次因为执行器饱和而积累出一个荒谬的积分值。过程是这样的:姿态偏差很大,控制器输出力矩早就顶到执行器限幅,但积分器还在拼命累积误差;等到姿态开始回摆,比例项已经反向输出了,积分项却还在正向顶着,导致严重的超调。
Simulink里排查这个现象,我通常会在饱和模块后面加一个“饱和标志”信号,在示波器里观察是否长时间处于饱和状态。如果发现饱和经常被触发,那就是电机极限选得太不现实,或者说你的控制目标高到超过了物理能力。抗积分饱和的做法比较基础:给积分器输出加限幅,或者只在执行器未饱和时允许积分更新;在MATLAB里也可以直接把PID模块的“Reset integrator”接上饱和标志信号,这个技巧很实用。
5.3 求解器步长和控制器采样时间的配合是硬杠杆
Simulink里默认可能是变步长求解器,这会带来一个容易忽略的问题:你给控制器设置了离散采样时间,但控制器所在的整个模块可能被变步长仿真反复调用多次,每一次的采样时刻都不均匀。结果就是,同一个PID模块在不同周期内的离散化行为不稳定,仿真结果难以重复。
我的习惯方案是:把机械动力学部分用较细的定步长积分,比如步长1e-4秒,用高精度定步长求解器;把控制计算块设置成1e-3秒采样,也就是1kHz控制频率,并且把控制器放在一个原子子系统里,强制Simulink在同一控制周期内完成所有控制计算。这样仿真时既保留了动力学细节的平滑性,又模拟了真实控制器的离散特性。
也有人喜欢全部用变步长求解器,只在控制器输出端加零阶保持器,但实测下来波形会掺杂步长变化带来的伪振荡,这比定步长多花的那点仿真时间不值得省。
6. 从仿真走向实物前,还能再做些什么
6.1 把控制逻辑快速生成到嵌入式代码
仿真里验证过的控制器,到了实机阶段谁也不想重写一遍代码,除非你想故意引入一套全新的bug。Simulink的优势就在于可以把控制逻辑直接生成为嵌入式C代码。只要模型里所有控制模块都是离散输入输出,采样时间一致,代码生成过程通常很顺利。
我建议在做代码生成前,先把模型里所有不必要的可视化模块,比如示波器、记录模块全部断开,因为代码生成会把它们生成为空转逻辑或直接报错。再把所有输入输出信号通过Inport和Outport显式引出,生成代码后在上位机里做一轮接口测试,确认变量名、字节顺序、时间戳都对应得上,这一步能省下大量联调时间。
6.2 硬件在环测试这件事值得提前搭
硬件在环测试,简单说就是把生成的控制器代码烧到嵌入式硬件里,而机器人本体仍是Simulink里的仿真模型。实物的控制器不会被外界干扰,却能体验到真实代码的运行节奏。我在这个项目里用HIL发现的第一个问题,就是控制任务里有一个耗时操作导致采样周期抖动到了1.3ms,直接引起轻微振荡。纯仿真环境里完全看不到这种现象。
如果你手边有开发板,可以尝试在电脑上跑Simulink模型,控制器代码放在板子上跑,两者通过串口或网口通信。这个流程越早跑通,后续每改一版控制逻辑,验证成本就低很多。
6.3 在这个例子的框架上再扩展什么
这个案例是一个非常好的底子。往上加的最自然的方向是腿部与步态:把固定基座改成浮动基座,加入双腿摆动规划和踝关节/髋关节的平衡策略,再把手臂摆动和步态相位关联。你会发现之前定义的ZMP约束变得更关键了。也可以加入扭矩传感器,让姿态控制器根据实测地面支反力做更精确的前馈。如果对感知有兴趣,还能在Simulink里接入机械臂视觉模型,让手臂摆动随目标物体的位置自适应调整。
我个人的建议是:不要在一次仿真里堆砌太多新模块。选一个方向,把原有框架的接口留好,再一点一点加。仿真工程和写代码一样,局部可验证永远是第一原则。
写到这里,回头再看那个让我一开始怀疑人生的模型,其实困难都不在“算法难”或者“工具不好用”,而在对物理耦合的理解不够。我先调通了姿态稳定回路,又加上手臂摆动轨迹,最后才用零空间优先级把两者捏合在一起。Simulink本身只是一个平台环境,真正决定仿真质量的还是你对模型物理规律、控制器结构、数值步长和信号细节这几个层次的把握。
最后分享一个小技巧:做协同控制仿真时,记得把姿态稳定回路先单独验收,再开手臂轨迹跟踪;开手臂后先用小摆幅、低频正弦跑几十秒,记录所有力矩和角度波形,再去调协同参数。这个小步骤能帮你省掉大量“不知道是哪个环节引起发散”的深夜排查。