机器狗入门3 - 混合运动解析及足端逆运动学

代码结构概览

整个项目采用**有限状态机(FSM)**架构,主要模块包括:

plainText

main.cpp                    # 主入口,创建控制框架
├── ControlFrame.cpp        # 控制框架,调用FSM
├── FSM/
│   ├── FSM.cpp             # 状态机核心,管理状态切换
│   ├── State_Trotting.cpp  # 小跑步态状态(主要运动模式)
│   ├── State_FreeStand.cpp # 自由站立状态
│   └── State_FixedStand.cpp# 固定站立状态
├── Gait/
│   ├── GaitGenerator.cpp   # 步态轨迹生成器
│   └── FeetEndCal.cpp      # 足端位置计算
├── control/
│   └── BalanceCtrl.cpp     # 平衡控制
└── interface/
    └── WirelessHandle.cpp  # 无线手柄输入

左右转动代码位置分析

1. Trotting 小跑状态(核心转动控制)

文件: /home/wd/micu_unitreedog_base/src/unitree_guide/unitree_guide/src/FSM/State_Trotting.cpp

关键代码:

读取用户转动指令 (第136-146行):

cpp

void State_Trotting::getUserCmd(){
    /* Movement */
    _vCmdBody(0) = invNormalize(_userValue.ly, _vxLim(0), _vxLim(1));  // 前进/后退
    _vCmdBody(1) = -invNormalize(_userValue.lx, _vyLim(0), _vyLim(1)); // 左右平移
    
    /* Turning */
    _dYawCmd = -invNormalize(_userValue.rx, _wyawLim(0), _wyawLim(1));  // 左右转动!
    _dYawCmd = 0.9*_dYawCmdPast + (1-0.9) * _dYawCmd;  // 低通滤波
    _dYawCmdPast = _dYawCmd;
}

计算转动命令 (第148-165行):

cpp

void State_Trotting::calcCmd(){
    /* Movement */
    _vCmdGlobal = _B2G_RotMat * _vCmdBody;
    
    /* Turning */
    _yawCmd = _yawCmd + _dYawCmd * _ctrlComp->dt;  // 累积计算目标偏航角
    _Rd = rotz(_yawCmd);                            // 生成旋转矩阵
    _wCmdGlobal(2) = _dYawCmd;                      // 设置全局角速度(绕Z轴)
}

设置命令到步态生成器 (第89行):

cpp

_gait->setGait(_vCmdGlobal.segment(0,2), _wCmdGlobal(2), _gaitHeight);

2. 自由站立状态的转动控制

文件: /home/wd/micu_unitreedog_base/src/unitree_guide/unitree_guide/src/FSM/State_FreeStand.cpp

关键代码 (第44-47行):

cpp

void State_FreeStand::run(){
    Vec34 vecOP;
    _userValue = _lowState->userValue;

    vecOP = _calcOP( invNormalize(_userValue.lx, _rowMin, _rowMax),        // 横滚
                     invNormalize(_userValue.ly, _pitchMin, _pitchMax),    // 俯仰
                    -invNormalize(_userValue.rx, _yawMin, _yawMax),        // 偏航(左右转)
                     invNormalize(_userValue.ry, _heightMin, _heightMax) ); // 高度
    _calcCmd(vecOP);
}

3. 足端位置计算(转动时的脚位调整)

文件: /home/wd/micu_unitreedog_base/src/unitree_guide/unitree_guide/src/Gait/FeetEndCal.cpp

关键代码 (第27-45行):

cpp

Vec3 FeetEndCal::calFootPos(int legID, Vec2 vxyGoalGlobal, float dYawGoal, float phase){
    // ... 计算下一步位置 ...
    
    _nextYaw = _dYaw*(1-phase)*_Tswing + _dYaw*_Tstance/2 + _kyaw*(dYawGoal - _dYaw);
    
    // 根据转动角度调整脚的位置
    _nextStep(0) += _feetRadius(legID) * cos(_yaw + _feetInitAngle(legID) + _nextYaw);
    _nextStep(1) += _feetRadius(legID) * sin(_yaw + _feetInitAngle(legID) + _nextYaw);
    
    _footPos = _est->getPosition() + _nextStep;
    _footPos(2) = 0.0;
    return _footPos;
}

4. 无线手柄输入(转动指令来源)

文件: /home/wd/micu_unitreedog_base/src/unitree_guide/unitree_guide/src/interface/WirelessHandle.cpp

关键代码 (第57-61行):

cpp

userValue.L2 = killZeroOffset(_keyData.L2, 0.08);
userValue.lx = killZeroOffset(_keyData.lx, 0.08);   // 左摇杆X轴
userValue.ly = killZeroOffset(_keyData.ly, 0.08);   // 左摇杆Y轴
userValue.rx = killZeroOffset(_keyData.rx, 0.08);   // 右摇杆X轴 -> 控制转动
userValue.ry = killZeroOffset(_keyData.ry, 0.08);   // 右摇杆Y轴

转动控制流程总结

plainText

无线手柄右摇杆X轴 (rx)
        │
        ▼
WirelessHandle::receiveHandle()
        │
        ▼
lowState.userValue.rx
        │
        ▼
┌───────────────────────────────────────┐
│ 状态判断                               │
└───────────────────────────────────────┘
        │
        ├── FREESTAND ──► State_FreeStand::run()
        │                      │
        │                      ▼
        │              调整身体姿态(偏航角)
        │
        └── TROTTING ──► State_Trotting::getUserCmd()
                               │
                               ▼
                      State_Trotting::calcCmd()
                               │
                               ▼
                      GaitGenerator::setGait()
                               │
                               ▼
                      FeetEndCal::calFootPos()
                               │
                               ▼
                      计算各脚的目标位置

关键变量说明

变量名 含义 范围
_userValue.rx 手柄右摇杆X轴输入 [-1, 1]
_dYawCmd 目标偏航角速度 _wyawLim 限制
_yawCmd 累积的目标偏航角 弧度
_wCmdGlobal(2) 全局坐标系下的Z轴角速度 弧度/秒

总结

机器狗的左右转动控制主要在 State_Trotting.cpp 中实现

  1. 输入层:手柄右摇杆X轴 (rx) 通过 WirelessHandle 读取
  2. 命令层getUserCmd() 将手柄输入转换为角速度指令 _dYawCmd
  3. 计算层calcCmd() 累积计算目标偏航角,并传递给步态生成器
  4. 执行层GaitGeneratorFeetEndCal 根据转动指令调整各脚的轨迹

站立状态下的转动控制在 State_FreeStand.cpp 中实现,通过调整身体姿态实现原地转动。

WaveGenerator 的工作机制详解

一、Vec4(bias) 的核心作用

1. 参数定义

cpp

WaveGenerator(0.2, 0.8, Vec4(0, 0, 0, 0))
//                          ↑
//                    bias = [腿1, 腿2, 腿3, 腿4] 的相位偏置

2. 相位计算原理(WaveGenerator.cpp:107)

cpp

_normalT(i) = fmod(_passT + _period - _period * _bias(i), _period) / _period;

公式解读

  • _passT:从步态开始到现在的时间
  • _period * _bias(i):第i条腿的相位偏移量
  • _normalT(i):归一化到 [0,1) 的相位值

3. 支撑/摆动判定(WaveGenerator.cpp:108-117)

cpp

if (_normalT(i) < _stRatio){        // 0.8 以内 → 支撑相
    contact(i) = 1;                  // 腿着地
    phase(i) = _normalT(i) / _stRatio;      // 相位归一化到 [0,1]
} else {                             // 0.8 以外 → 摆动相
    contact(i) = 0;                  // 腿抬起
    phase(i) = (_normalT(i) - _stRatio) / (1 - _stRatio);  // 相位归一化到 [0,1]
}

二、不同步态模式的对比

1. Pronk(四条腿同步)

cpp

Vec4(0, 0, 0, 0)

plainText

时间轴 →
腿1:  ████████░░░░  (█=支撑 0.8, ░=摆动 0.2)
腿2:  ████████░░░░  完全同步
腿3:  ████████░░░░
腿4:  ████████░░░░

相位关系:0 ─ 0 ─ 0 ─ 0

2. Trot(对角小跑)

cpp

Vec4(0, 0.5, 0.5, 0)

plainText

时间轴 →
腿1:  ████████░░░░
腿2:  ░░░░████████  延迟0.5个周期
腿3:  ░░░░████████  延迟0.5个周期
腿4:  ████████░░░░

相位关系:0 ─ 0.5 ─ 0.5 ─ 0
         ↖───────┘↖───────┘
          对角腿同步

3. Crawl(爬行)

cpp

Vec4(0, 0.25, 0.5, 0.75)

plainText

时间轴 →
腿1:  ████████░░░░
腿2:  ░░████████░░  延迟0.25
腿3:  ░░░░████████  延迟0.5
腿4:  ░░░░░░░░████  延迟0.75

相位关系:0 ─ 0.25 ─ 0.5 ─ 0.75
         依次延迟,轮流支撑

三、WaveGenerator 的职责边界

WaveGenerator 只负责「时间节律控制」

plainText

WaveGenerator 输出:
├── contact[4]  → 每条腿是否着地(1=着地,0=抬起)
└── phase[4]    → 每条腿当前阶段(0~1,表示在支撑/摆动中的位置)

WaveGenerator 不负责:
├── 腿要抬多高
├── 腿要迈多远
├── 关节角度是多少

后续模块的职责

plainText

WaveGenerator
        │
        ├─► contact → GaitGenerator → 判断腿是支撑还是摆动
        │
        └─► phase   → GaitGenerator → 插值计算腿的位置(摆线轨迹)
                            │
                            ▼
                    FeetEndCal → 计算足端落点(考虑速度、转动)
                            │
                            ▼
                    unitreeRobot::getQ() → 逆运动学求解关节角度

四、左右转动时的协作流程

完整数据流

plainText

手柄输入 (rx → dYawCmd)
        │
        ▼
State_Trotting::calcCmd()
        │
        ├─► _yawCmd      → 目标偏航角累积
        ├─► _Rd = rotz(_yawCmd) → 旋转矩阵
        └─► _wCmdGlobal(2) = _dYawCmd → 角速度指令
                    │
                    ▼
        GaitGenerator::setGait(vxy, wz, height)
                    │
                    ▼
        GaitGenerator::run()
                    │
                    ├─► contact[i]==1 → 支撑腿:保持当前位置
                    │
                    └─► contact[i]==0 → 摆动腿
                              │
                              ▼
                    FeetEndCal::calFootPos()
                              │
                              ├─► 计算落点(考虑转动)
                              └─► 返回目标位置
                                        │
                                        ▼
                              摆线插值(使用 phase[i])
                                        │
                                        ▼
                              unitreeRobot::getQ() → 关节角度

五、关键区别:时间节律 vs 运动学

模块 职责 输出
WaveGenerator 时间节律控制 contact, phase
GaitGenerator 轨迹生成 足端位置、速度
FeetEndCal 落点计算 考虑速度/转动的目标点
unitreeRobot 逆运动学 关节角度

总结

  1. Vec4(bias) 确实约定了四条腿的运动顺序和时间节点
  • 通过相位偏置决定哪条腿先动、哪条腿后动
  • 不同的 bias 向量对应不同的步态模式
  1. WaveGenerator 不直接涉及逆运动学
  • 它只输出「时间信号」(contact 和 phase)
  • 逆运动学是在 unitreeRobot::getQ() 中完成的
  • 转动时的落点调整是在 FeetEndCal 中根据速度指令计算的
  1. 协作关系

plainText

步态模式(bias) → 时间节律(contact/phase) → 轨迹生成(落点+摆线) → 逆运动学(关节角度)

WaveGenerator 负责「时间节律控制」,而左右转动/混合移动的调整发生在「轨迹生成」和「逆运动学」阶段

速度指令到逆运动学的完整转换流程

一、整体转换架构

plainText

用户手柄输入
       │
       ▼
速度指令 (vx, vy, wz)
       │
       ▼
┌──────────────────────────────────────────┐
│ 1. 坐标变换:身体坐标系 → 全局坐标系       │
└──────────────────────────────────────────┘
       │
       ▼
┌──────────────────────────────────────────┐
│ 2. 落点计算:根据速度计算足端目标位置      │
└──────────────────────────────────────────┘
       │
       ▼
┌──────────────────────────────────────────┐
│ 3. 轨迹生成:摆线插值生成连续轨迹          │
└──────────────────────────────────────────┘
       │
       ▼
┌──────────────────────────────────────────┐
│ 4. 逆运动学:足端位置 → 关节角度          │
└──────────────────────────────────────────┘
       │
       ▼
关节角度指令 (q1, q2, q3) × 4条腿

二、详细转换流程

1. 用户输入到速度指令

文件: State_Trotting.cpp (第136-146行)

cpp

void State_Trotting::getUserCmd(){
    /* 前进/后退 */
    _vCmdBody(0) = invNormalize(_userValue.ly, _vxLim(0), _vxLim(1));
    
    /* 左右平移 */
    _vCmdBody(1) = -invNormalize(_userValue.lx, _vyLim(0), _vyLim(1));
    
    /* 左右转动 */
    _dYawCmd = -invNormalize(_userValue.rx, _wyawLim(0), _wyawLim(1));
}

输入输出

手柄输入 输出指令 物理含义
ly (左摇杆Y) _vCmdBody(0) 前进/后退速度
lx (左摇杆X) _vCmdBody(1) 左右平移速度
rx (右摇杆X) _dYawCmd 偏航角速度

2. 坐标变换与目标计算

文件: State_Trotting.cpp (第148-165行)

cpp

void State_Trotting::calcCmd(){
    /* 身体坐标系 → 全局坐标系 */
    _vCmdGlobal = _B2G_RotMat * _vCmdBody;
    
    /* 累积计算目标偏航角 */
    _yawCmd = _yawCmd + _dYawCmd * _ctrlComp->dt;
    _Rd = rotz(_yawCmd);
    _wCmdGlobal(2) = _dYawCmd;
}

关键变量

  • _B2G_RotMat:身体坐标系到全局坐标系的旋转矩阵
  • _vCmdGlobal:全局坐标系下的速度指令
  • _Rd:期望的身体姿态旋转矩阵

3. 步态生成与落点计算

文件: FeetEndCal.cpp (第27-45行)

cpp

Vec3 FeetEndCal::calFootPos(int legID, Vec2 vxyGoalGlobal, float dYawGoal, float phase){
    // 当前身体状态
    _bodyVelGlobal = _est->getVelocity();
    _bodyWGlobal = _lowState->getGyroGlobal();

    // 计算下一步位移(前馈+反馈)
    _nextStep(0) = _bodyVelGlobal(0)*(1-phase)*_Tswing 
                 + _bodyVelGlobal(0)*_Tstance/2 
                 + _kx*(_bodyVelGlobal(0) - vxyGoalGlobal(0));
    
    _nextStep(1) = _bodyVelGlobal(1)*(1-phase)*_Tswing 
                 + _bodyVelGlobal(1)*_Tstance/2 
                 + _ky*(_bodyVelGlobal(1) - vxyGoalGlobal(1));
    
    // 转动补偿
    _nextYaw = _dYaw*(1-phase)*_Tswing + _dYaw*_Tstance/2 + _kyaw*(dYawGoal - _dYaw);
    _nextStep(0) += _feetRadius(legID) * cos(_yaw + _feetInitAngle(legID) + _nextYaw);
    _nextStep(1) += _feetRadius(legID) * sin(_yaw + _feetInitAngle(legID) + _nextYaw);

    // 最终落点 = 身体位置 + 相对位移
    _footPos = _est->getPosition() + _nextStep;
    _footPos(2) = 0.0;
    return _footPos;
}

核心公式

plainText

落点位置 = 身体当前位置 + 速度×时间 + 转动补偿

4. 轨迹插值

文件: GaitGenerator.cpp (第53-61行)

cpp

Vec3 GaitGenerator::getFootPos(int i){
    Vec3 footPos;
    // 摆线插值
    footPos(0) = cycloidXYPosition(_startP.col(i)(0), _endP.col(i)(0), (*_phase)(i));
    footPos(1) = cycloidXYPosition(_startP.col(i)(1), _endP.col(i)(1), (*_phase)(i));
    footPos(2) = cycloidZPosition(_startP.col(i)(2), _gaitHeight, (*_phase)(i));
    return footPos;
}

摆线插值公式

cpp

// XY平面插值
float cycloidXYPosition(float start, float end, float phase){
    float phasePI = 2 * M_PI * phase;
    return (end - start)*(phasePI - sin(phasePI))/(2*M_PI) + start;
}

// Z轴插值(抬腿)
float cycloidZPosition(float start, float h, float phase){
    float phasePI = 2 * M_PI * phase;
    return h*(1 - cos(phasePI))/2 + start;
}

5. 逆运动学求解(核心)

文件: unitreeLeg.cpp (第64-100行)

腿部结构模型

plainText

        髋关节(abad) q1
            │
            ▼
        大腿(hip) q2
            │
            ▼
        小腿(knee) q3
            │
            ▼
           足底

逆运动学算法

cpp

Vec3 QuadrupedLeg::calcQ(Vec3 pEe, FrameType frame){
    // 1. 坐标系转换:BODY → HIP
    Vec3 pEe2H = pEe - _pHip2B;
    
    float px = pEe2H(0);  // X方向(前进)
    float py = pEe2H(1);  // Y方向(侧移)
    float pz = pEe2H(2);  // Z方向(高度)

    // 2. 求解髋关节角度 q1
    float L = sqrt(pow(py,2) + pow(pz,2) - pow(l1,2));
    q1 = atan2(pz*l1 + py*L, py*l1 - pz*L);

    // 3. 求解膝关节角度 q3
    float b = sqrt(pow(c, 2) - pow(a, 2));  // 肩到脚的距离
    float temp = (pow(b3z, 2) + pow(b4z, 2) - pow(b, 2)) / (2*fabs(b3z*b4z));
    q3 = acos(temp);
    q3 = -(M_PI - q3);

    // 4. 求解大腿角度 q2
    a1 = py*sin(q1) - pz*cos(q1);
    a2 = px;
    m1 = b4z*sin(q3);
    m2 = b3z + b4z*cos(q3);
    q2 = atan2(m1*a1 + m2*a2, m1*a2 - m2*a1);

    return Vec3(q1, q2, q3);
}

三条腿的几何关系

plainText

              /|
             / | l3 (小腿)
            /  |
           /   |
          /    |
    l2   /     |
   (大腿)/      |
        └──────┘
           b

几何求解步骤

  1. q1(髋关节外展角):通过投影到YZ平面,用反正切求解
  2. q3(膝关节角):利用余弦定理求解
  3. q2(大腿角):根据已求角度,通过正切求解

6. 速度级逆运动学(可选)

文件: unitreeLeg.cpp (第103-111行)

cpp

Vec3 QuadrupedLeg::calcQd(Vec3 q, Vec3 vEe){
    // 雅可比矩阵逆解
    return calcJaco(q).inverse() * vEe;
}

雅可比矩阵unitreeLeg.cpp 第119-147行)描述关节角速度与末端速度的关系:

plainText

v_ee = J(q) * q_dot
q_dot = J(q)^(-1) * v_ee

三、完整数据流总结

plainText

手柄输入 (ly, lx, rx)
       │
       ▼
State_Trotting::getUserCmd()
       │
       ▼
速度指令 (_vCmdBody, _dYawCmd)
       │
       ▼
State_Trotting::calcCmd()
       │
       ▼
全局速度 (_vCmdGlobal, _wCmdGlobal)
       │
       ▼
GaitGenerator::setGait()
       │
       ▼
FeetEndCal::calFootPos()
       │
       ▼
足端目标位置 (_posFeetGlobalGoal)
       │
       ▼
GaitGenerator::run() → 摆线插值
       │
       ▼
平滑轨迹 (feetPos, feetVel)
       │
       ▼
unitreeLeg::calcQ()
       │
       ▼
关节角度 (q1, q2, q3) × 4条腿

四、关键模块职责

模块 职责 输入 输出
State_Trotting 速度指令生成与坐标变换 手柄输入 全局速度指令
FeetEndCal 足端落点计算 速度、角速度 落点位置
GaitGenerator 轨迹插值 起点、终点、相位 平滑轨迹
QuadrupedLeg 逆运动学求解 足端位置 关节角度

五、数学本质

逆运动学的核心是几何求解

  1. 问题定义:已知足端在空间中的位置 (x, y, z),求三个关节角度 (q1, q2, q3)
  2. 求解策略:采用解析解法(封闭解)
  • 将三维问题分解为平面问题
  • 利用三角函数和几何关系直接求解
  1. 优势
  • 计算速度快(适合实时控制)
  • 解的唯一性有保证(在工作空间内)
  1. 局限性
  • 仅适用于特定的腿部结构(3自由度串联臂)
  • 存在奇异位形(如完全伸直或折叠)

总结

速度指令到逆运动学的转换分为四个层次

  1. 指令层:手柄输入 → 速度指令(坐标无关)
  2. 轨迹层:速度指令 → 足端轨迹(全局坐标系)
  3. 位置层:轨迹点 → 足端位置(身体坐标系)
  4. 关节层:足端位置 → 关节角度(逆运动学)

核心数学工具是几何解析法,通过三角函数和余弦定理直接求解三条关节角度,确保实时性和稳定性。