机器狗这类四足平台,第一天上手最容易产生的错觉是“它能走,所以控制它走应该很简单”。等真正把上位机连上去,你会发现横在面前的是三件事:接口选哪一层、指令怎么表达、出问题怎么收场。这套东西不是调一个 API 就完事,它本质上是一个实时控制系统,指令里任何一个符号搞反、任何一个频率没对齐,机器狗都可能在你面前直接趴下或者原地抽搐。这篇内容围绕 Go1 这个型号的运动控制开发展开,讲的是从“连得上”到“走得稳”之间的全部实操细节:三个控制层级各自的能力边界、通信链路里最容易被忽略的路由问题、高层速度指令的单位与符号、低层十二个关节的控制权交接,以及实测中真正会翻车的那些场景。适合已经能通过官方示例收到数据、想让机器狗按自己想法动起来的开发者,也适合还在犹豫该用高层还是低层的同学——选错层级,后面所有工作都是白费。
1. 拆开 Go1 的控制栈:三个层级各自能干什么、代价是什么
很多人拿到 SDK 之后第一反应是直接翻到 low-level 的示例代码,觉得“既然要开发,当然用最底层最自由的那个”。这个判断在绝大多数场景下是错的,而且错得代价不小。先把层级关系理清楚,后面的所有取舍才有依据。
1.1 高层、低层,以及那个藏在中间的 Sport 模式
Go1 对外暴露的控制接口,本质上分成两条通路。
第一条是高层通路(High-Level)。你发给机器狗的是一条“意图”——往前走多快、往左平移多快、转多快。机器狗内部自己完成步态规划、落脚点选择、平衡控制、关节逆解,最后输出十二个电机的力矩。这条通路的指令频率很低,20Hz 到 50Hz 就够用,普通 WiFi 或者一根网线就能跑。它的核心价值是:你不用懂四足机器人怎么保持平衡。
第二条是低层通路(Low-Level)。你直接给十二个关节下发目标角度、目标角速度、前馈力矩,以及 PD 增益。这时候机器狗不再替你思考,你给什么它执行什么,包括让它摔倒。指令频率要求 500Hz,也就是每 2ms 一帧,延迟抖动一旦超过几毫秒,控制环路就开始发散。
中间还夹着一个容易被混淆的东西:很多资料里提到的 Sport 模式,其实是高层通路里的一组预设运动原语,比如“站起来”“趴下”“切换到小跑”“跳跃”。它和高层速度控制是同一层的不同入口,不是独立的第三层。理解这一点很重要,因为你在代码里看到的mode字段,本质就是在这些入口之间切换。
1.2 三种入口的能力与代价对照
我把这三条实际可用的路径整理成一张表,这张表基本决定了你项目的第一行代码该怎么写。
| 入口 | 你能控制什么 | 指令频率 | 需要自己做的数学 | 典型适用场景 |
|---|---|---|---|---|
| 高层速度控制 | 机体三轴速度 + 转向角速度 + 姿态 + 身高 | 20–50Hz | 几乎为零 | 路径跟踪、遥控增强、视觉伺服、教学演示 |
| 高层运动原语 | 站立、趴下、步态切换、特定动作 | 事件触发 | 状态机管理 | 开机自检、演示流程、故障恢复 |
| 低层关节控制 | 12 个关节的 q/dq/tau/Kp/Kd | 500Hz | 运动学、动力学、平衡控制全套 | 算法研究、自定义步态、强化学习部署 |
看这张表就能明白一件事:低层不是“更高级的高层”,它是另一类工作。选低层,等于你接手了原本由机器狗固件承担的全部责任。
1.3 我的选型判断:九成需求从高层起步
我的一般建议是:只要你的目标里出现“让机器狗移动到某个位置”“跟着某个人走”“按规划路径跑”,就从高层开始,把整个链路跑通、把安全机制建好,再考虑往下降。原因很实在——高层通路的调试成本极低,你改一个数字重新跑一次,两秒钟就能看到效果;低层通路一次参数没给对,机器狗直接侧翻,轻则换腿,重则烧电机。
真正需要下沉到低层的场景其实很窄:你在做步态算法研究,你需要注入自定义的足端轨迹,你在部署一个训练好的策略网络,或者你需要精确控制某一条腿做特定动作。这些场景的共同点是——你对平衡本身有话说。
还有一个折中方案值得提一下:高层和低层可以在同一个程序里切换。先用高层让机器狗站起来进入稳定状态,再把levelFlag切到低层接管,退出时切回高层让它自己趴下。这个“交接”流程是低层开发的标配,后面第 4 节会详细写。
提示:不要在机器狗趴着的状态下直接切低层。低层的起始目标角度必须和当前实际关节角接近,否则第一帧就是一记猛烈的阶跃。
2. 通信链路:从网卡 IP 到 UDP 端口,那些让你怀疑人生的细节
链路问题占了新手卡壳时间的一大半,而且症状特别有迷惑性——程序不报错,就是收不到数据;或者能收到数据,但机器狗纹丝不动。这一节把这条链路从头到尾拆一遍。
2.1 有线直连的网段配置与多网卡路由陷阱
Go1 的通信是标准的 UDP,机器狗自己是一个网段里的固定地址(不同批次和配置会有差异,通常落在192.168.123.x这个段里,具体以你手上设备的说明为准)。上位机需要配置一块网卡,地址落在同一网段但不冲突,比如192.168.123.100,子网掩码255.255.255.0。
听起来简单,坑在这里:你的笔记本通常还有一块 WiFi 网卡,而且它是默认路由。当你向192.168.123.161发包时,如果系统路由表里没有一条明确指向有线网卡的同网段路由,UDP 包会从 WiFi 网卡出去,然后消失在空气里。表现出来就是send调用返回成功,recv永远超时。
判断方法很直接,看路由表:
ip route get 192.168.123.161如果输出里dev后面跟的是你的无线网卡名,问题就确认了。修复方式有两种,任选其一:
# 方式一:临时加一条明细路由,重启失效 sudo ip route add 192.168.123.0/24 dev eth0 # 方式二:直接关掉无线,最简单粗暴 sudo ip link set wlan0 down我自己更倾向方式一,因为调试时经常需要一边查资料一边跑程序。但要注意,网线拔插之后这条路由会消失,需要重新加,写个启动脚本更省事。
第二个细节是防火墙。某些发行版默认会拦掉非常规端口的入站 UDP,而 SDK 用的端口不是 80 也不是 443。排查时先把防火墙临时关掉验证,确认是它的锅之后再单独放行对应端口,别图省事一直关着。
2.2 端口、周期与收发时序
SDK 的通信模型是一问一答:你发一帧指令,机器狗回一帧状态。发和收是成对的,不是两条独立的流。
这里有个必须养成的习惯——先收后发,或者收发在同一循环里背靠背执行。我见过不少自己手写 socket 的代码,把发送放在一个线程、接收放在另一个线程,结果两个线程频率不一致,指令堆积在缓冲区里,机器狗执行的是几秒钟之前的旧指令。表现出来就是“动作延迟巨大且不规律”。
标准循环长这样:
import time from unitree_legged_sdk import HighLevel, UDP udp = UDP(8080, "192.168.123.161", 8007) cmd = HighLevel() state = HighLevel() cmd.levelFlag = 0 # 0 表示高层通路 period = 0.02 # 50Hz next_t = time.perf_counter() while True: udp.send(cmd) udp.recv() state = udp.get_recv() # 用绝对时间推进,避免累积漂移 next_t += period sleep = next_t - time.perf_counter() if sleep > 0: time.sleep(sleep) else: next_t = time.perf_counter() # 已经落后,重置基准注意最后那段sleep的写法。用time.sleep(0.02)直接睡,每轮会多出几毫秒的执行开销,几百轮之后频率就掉到 40Hz 以下了。用绝对时间基准推进,能让平均周期稳定在设定值附近。这个细节在高频低层控制里更关键,50Hz 的时候差别还不明显,500Hz 的时候就是生死线。
2.3 一次“能收到数据却发不出去”的排查复盘
这个现象值得单独说,因为它很难靠猜定位。症状是:状态包正常收到,打印出来的 IMU 数据在变,但机器狗对速度指令毫无反应。
排查顺序我一般这么走:
第一,确认levelFlag和mode字段的值。高层通路里mode是一个状态标志,不同的数值对应不同的行为,如果它还停在待机档,你给再大的速度也是零。这个字段在 SDK 的不同版本里定义有细微差别,最靠谱的做法是翻你手上那个版本自带的示例,看它把mode设成了几。
第二,确认遥控器没有抢走控制权。这是最隐蔽的一个。手持遥控器如果处于开机且被按过的状态,它会持续向机器狗发送指令,而机器狗对遥控器指令的优先级通常高于 SDK。你以为自己在控制,其实遥控器在控制。判断方法是看状态包里的遥控器字段,如果摇杆值非零,说明有人在跟你抢。把遥控器关掉或者放平不碰,问题立刻消失。
第三,检查速度字段的单位和量级。SDK 里速度的单位是米每秒,不是厘米每秒,也不是归一化的百分比。新手常犯的错是给0.5期待它“走一半速度”,实际是 0.5 m/s,机器狗在小跑步态下已经跑起来了。反过来,如果给的是0.005,那点速度在摩擦和死区面前就是零。
第四,确认指令字段名对得上。不同版本的 SDK 里,速度字段有的叫forwardSpeed / sideSpeed / turnSpeed,有的叫velocity[0] / velocity[1] / yawSpeed,还有的把机体坐标系下的速度放在velocity数组里。字段名写错不会报错,因为结构体初始化时就是零,你赋值给一个不存在的字段名,编译期可能就被忽略掉了。我看过不止一个人在这上面耗一下午。
提示:任何一次“指令无效”,都先用一个远大于死区的值试一次,比如 0.3 m/s。用极小值试,你永远分不清是“没生效”还是“生效了但被阻力吃掉”。
3. 让机器狗迈出第一步:高层速度指令的单位、符号与限幅
链路通了之后,第一件要干的事不是写复杂的控制逻辑,而是老老实实把“前进、平移、转向”这三个自由度在坐标系里的定义搞清楚。符号搞反是这个阶段最高频的错误。
3.1 机体坐标系与三轴速度的物理含义
Go1 用的是标准的机体右手坐标系:x 轴指向机头前方,y 轴指向机体左侧,z 轴竖直向上。这一点必须记死,因为后面所有的符号判断都从它推出来。
在这个坐标系下,三个速度量是:
- vx(前进速度):沿 x 轴正向,正值表示向机头方向前进,负值表示后退。单位 m/s。
- vy(侧移速度):沿 y 轴正向,正值表示向机体左侧平移,负值向右。单位 m/s。
- vyaw(转向角速度):绕 z 轴旋转,正值表示逆时针,也就是从上方俯视时向左转。单位 rad/s。
这三个量都是机体坐标系下的,不是世界坐标系。也就是说,机器狗转了 90 度之后,你再给vx = 0.3,它依然朝它自己的机头方向走,而不是朝着你初始朝向的北方走。想做“走向世界坐标系中某个点”,你得自己把世界坐标的目标换算成机体坐标的速度,这个换算放在闭环那一节讲。
转向那个符号我特别要强调一下。右手坐标系里绕 z 轴正向旋转,按右手定则,拇指朝上,四指从 x 轴转向 y 轴,也就是从前方转向左侧,俯视看就是逆时针。很多人下意识觉得“正数应该往右转”,结果一给正值机器狗就往左拐,还以为是固件有问题。
3.2 一段能直接跑起来的速度控制骨架
下面这段是我自己在调试时常用的最小骨架,去掉了一切不必要的装饰,方便你在上面加自己的逻辑。
import time import numpy as np from unitree_legged_sdk import HighLevel, UDP class Go1Mover: def __init__(self, local_port=8080, robot_ip="192.168.123.161", robot_port=8007): self.udp = UDP(local_port, robot_ip, robot_port) self.cmd = HighLevel() self.state = HighLevel() self.cmd.levelFlag = 0 self.cmd.mode = 2 # 可控运动档,具体数值以你的 SDK 示例为准 # 目标速度与当前下发速度分离,用于做斜坡平滑 self.target = np.zeros(3) self.current = np.zeros(3) self.acc_limit = np.array([0.5, 0.4, 1.0]) # 每周期最大增量 def set_target(self, vx, vy, vyaw): self.target[:] = [vx, vy, vyaw] def _ramp(self): delta = self.target - self.current step = np.clip(delta, -self.acc_limit, self.acc_limit) self.current += step def _apply(self): # 字段名按你手上 SDK 版本调整 self.cmd.forwardSpeed = float(self.current[0]) self.cmd.sideSpeed = float(self.current[1]) self.cmd.turnSpeed = float(self.current[2]) def tick(self): self._ramp() self._apply() self.udp.send(self.cmd) self.udp.recv() self.state = self.udp.get_recv() return self.state用起来就是:
mover = Go1Mover() mover.set_target(0.3, 0.0, 0.0) for _ in range(150): # 大约 3 秒 mover.tick() time.sleep(0.02) mover.set_target(0.0, 0.0, 0.0) for _ in range(50): # 平滑刹停 mover.tick() time.sleep(0.02)这段代码里最值钱的是_ramp那个函数,下一节专门讲它。
3.3 为什么必须做斜坡,以及限幅表怎么定
直接从 0 给到 0.5 m/s,会发生什么?机器狗收到一个阶跃指令,内部的步态规划器会试图瞬间加速,结果是机体俯仰角突变、支撑腿打滑、严重时直接前扑。这不是机器狗不结实,而是任何真实的物理系统都不能接受阶跃输入。
所以指令必须“喂”进去,每一帧只允许变化一点点。这就是斜坡限制。速度量用加速度限制,角速度量用角加速度限制。
我实测下来比较保守的一组起点值,供参考:
| 量 | 平台大致上限 | 新手起始值 | 每周期最大增量(50Hz) |
|---|---|---|---|
| vx | 3.5 m/s 左右 | 0.2 m/s | 0.03 m/s |
| vy | 1.5 m/s 左右 | 0.15 m/s | 0.02 m/s |
| vyaw | 2.5 rad/s 左右 | 0.3 rad/s | 0.05 rad/s |
这些平台上限随时会因为步态、负载、地面材质而变化,写死一个绝对值很危险。我建议的做法是在程序里做软限幅,把上限设成平台能力的六到七成,任何时候都不允许超过:
LIMITS = np.array([1.0, 0.6, 1.0]) v = np.clip(v, -LIMITS, LIMITS)这一步看起来多余,但当你的上层逻辑里出现除法、出现积分饱和、出现一个除零保护返回的巨大值时,这行代码就是最后一道保险。
地面材质的影响比想象中大。光面瓷砖上做转向,实际能用的角速度可能只有实验室木地板上的六成,因为足端摩擦不够,一转就打滑,打滑会触发内部的姿态保护,机器狗会自己减速甚至蹲下。地毯则是另一个极端,阻力大、足端容易陷进去,抬腿高度不够就会绊。同一套参数换场地要重调,这是常态。
3.4 步态、抬腿高度、身高对运动表现的连带影响
高层接口里通常还开放了几个“形状”参数,它们不直接是速度,但会显著改变运动表现。
步态类型决定了腿的节律。小跑是四条腿两两分组交替,速度快、效率高,但对不平地面适应性一般。爬行是三条腿支撑、一条腿抬起,慢但稳。你切步态时要考虑当前速度——低速时用小跑会很别扭,因为步频上不去,机器狗会一顿一顿的。
抬腿高度决定越障能力。调高能过坎,但代价是每条腿的腾空时间变长,支撑腿数量减少,稳定性下降,同时电机发热上升。我的经验是设成刚好能过你场地里最大障碍物的高度再留两成余量,不要一股脑拉到最大。
机体身高这个参数用途比较隐蔽,它其实是把机体重心抬高或压低。压低重心能提升稳定性、减少侧翻风险,但会限制腿部行程,走高速时反而容易够不到地面。调试时可以先压到一个中间偏低的值,把速度跑顺了再慢慢抬。
这三个参数有个共同特点:改动它们的瞬间就会有可感知的影响,但影响不会立刻表现为故障,而是慢慢累积成打滑、发热、抖动。所以别一次性全改,一次只动一个,跑三十秒以上再判断。
4. 拿到十二个关节的控制权:低层模式的接管流程与增益取值
如果你的需求确实需要低层,那就要接受一个事实:从切过去的那一刻起,机器狗的平衡由你负责。这一节讲怎么平稳地把控制权接过来,以及第一组 PD 增益该怎么给。
4.1 接管与交还的完整时序
低层模式不能凭空进入。标准流程是四步:
- 高层让机器狗站起来并稳定。发送站立或平衡模式指令,等状态包里的姿态角稳定下来,俯仰和横滚都在几度以内,持续两三秒。
- 缓慢切到低层标志位。把
levelFlag改掉,但这一帧下发的关节目标角度必须等于当前实际角度。你可以从状态包里读回来,也可以先读一次再发。 - 在低层下维持零误差保持。第一秒不要让关节动,只做 PD 保持,观察是否有异常抖动或下沉。
- 退出时反向操作。先把关节目标缓慢拉回站立位姿,再切回高层,让机器狗自己趴下。
第二步那个细节是整套流程的核心。如果你切过去的时候目标角度是零(很多示例里的初始值就是零),机器狗会以为你要它把四条腿全部收拢到机械零位,那一瞬间的动作是极其暴力的。
# 假想的接管片段,重点是读取当前角度作为初始目标 udp.send(cmd) udp.recv() state = udp.get_recv() for i in range(12): cmd.motorCmd[i].q = state.motorState[i].q cmd.motorCmd[i].dq = 0.0 cmd.motorCmd[i].tau = 0.0 cmd.motorCmd[i].Kp = 20.0 cmd.motorCmd[i].Kd = 0.5 cmd.levelFlag = 1 # 切低层4.2 关节索引、方向与零位的一致性检查
十二个关节的排列顺序是有约定的,通常是按“右前、左前、右后、左后”分组,每组三个关节依次是髋外摆、大腿、小腿。但不同 SDK 版本、不同型号的这个顺序必须逐个核对,不能凭记忆写。
核对方法很土但很有效:给某一个关节下发一个小的增量(比如 0.1 弧度),看机器狗哪条腿动了、往哪个方向动。逐个试一遍,把索引和实际物理关节的对应关系记在纸上。这个过程花二十分钟,能省掉后面几天的困惑。
方向也要确认。有些关节的正方向定义和你的直觉相反,比如大腿关节正值可能是向前抬,也可能是向后摆。如果你在算运动学的时候方向搞反了,FK 算出来的足端位置会跑到机体后面去,怎么调都调不对。
零位的问题更微妙。关节的“零度”是编码器的零位,不等于机械上腿伸直的姿态。很多控制算法需要的是“腿伸直”这个几何参考,你就得在代码里存一组偏置量。这组偏置我跟你说,不同机器个体之间会有差异,别指望从文档抄一个通用值。
4.3 Kp 和 Kd 的取值逻辑与阶跃测试
低层里每个关节的力矩是这样算出来的:
tau_out = Kp * (q_target - q_actual) + Kd * (dq_target - dq_actual) + tau_ffKp是位置刚度,Kd是阻尼。这两个数的物理意义很直白:Kp越大,关节越“硬”,越能抵抗外力把它推开;Kd越大,关节越“黏”,运动时越不容易震荡。
取值没有万能公式,但有一个可操作的流程:
第一步,Kp 从零开始加。先给一个很小的值(比如 5),用手推一下腿,感受阻力。逐步加到关节能撑住自身重量不下垂为止。腿部的三个关节负载差异很大,髋外摆通常需要的 Kp 最小,小腿最大。
第二步,加 Kd 抑制震荡。把 Kp 加到目标值之后,快速拨动一下腿然后松手,如果腿在目标位置附近来回晃,说明阻尼不够,逐步加 Kd 直到晃动在一两个周期内收敛。
第三步,做小阶跃测试。给一个 0.05 弧度的目标变化,观察响应曲线。理想的响应是快速上升、轻微超调、迅速稳定。超调大就加 Kd,响应慢就加 Kp。
有一组经验数值可以当作起点参考:髋外摆 Kp 约 20–30,大腿 Kp 约 30–40,小腿 Kp 约 40–50,Kd 统一在 0.3–1.0 之间。但这只是起点,实际值取决于你的机器狗负载、电池电压、地面,必须自己调。
提示:调 Kp/Kd 的时候,务必把机器狗吊起来或者放在软垫上,让四条腿悬空。地面调试一旦参数不对,腿会猛蹬地面,很容易伤到机构。
还有一个容易被忽略的点:低层模式下通信中断意味着什么。绝大多数固件都有看门狗——超过一定时间(通常是一两百毫秒)收不到合法指令,就自动停止输出力矩,机器狗会软塌塌地趴下去。这个设计是保护,但趴下去的瞬间如果姿态不正,也可能磕到。所以低层程序里必须有 try/finally,异常退出时把标志位切回高层。
5. 从开环到闭环:用状态反馈把“能走”变成“走得准”
只会发指令不会读状态,等于蒙着眼睛开车。状态包里的信息比大多数人用的要多,关键是知道哪些值得信、哪些需要处理。
5.1 状态消息里真正有用的字段
状态包内容不少,但日常调试真正高频使用的就那么几组:
IMU 姿态角(roll / pitch / yaw),单位弧度。前两个是判断稳定性的关键——roll 超过十几度基本就是侧翻前兆,pitch 大幅变化说明在急加速或急减速。yaw 是你的航向,做转向闭环全靠它。
IMU 角速度和线加速度。角速度做阻尼很有用,尤其在低层控制里;线加速度可以用来检测碰撞和打滑,突然出现一个远超正常值的尖峰,基本就是撞到什么了。
十二个关节的实测角度和角速度。低层闭环必需,高层调试时也可以拿来看步态是否正常。
机体速度估计。这个值通常是内部融合出来的,精度一般,短时间用可以,长时间积分会漂。
电池电压和电流。这个字段千万别忽略,第 6 节会专门讲。
5.2 一个最小可用的“定角直行”闭环
机器狗走直线天生不直,因为四条腿的摩擦、装配误差、地面不平都会导致航向缓慢漂移。加一个很简单的偏航闭环就能显著改善:
target_yaw = 0.0 kp_yaw = 1.2 while running: state = mover.tick() yaw_err = target_yaw - state.imu.rpy[2] # 角度归一化到 [-pi, pi],否则过 ±180 度会跳变 while yaw_err > np.pi: yaw_err -= 2 * np.pi while yaw_err < -np.pi: yaw_err += 2 * np.pi vyaw_cmd = np.clip(kp_yaw * yaw_err, -0.5, 0.5) mover.set_target(0.25, 0.0, vyaw_cmd)这段代码里最容易被忽略的是角度归一化。yaw 从3.14跳到-3.14的时候,误差会瞬间变成 6.28,控制器以为偏了一大圈,输出一个满舵转向,机器狗直接原地打转。这个 bug 特别经典,几乎所有做航向控制的人都踩过一次。
kp_yaw这个增益不要给大。转向是有惯性的,增益过大必然震荡。从 0.8 开始往上试,找到刚好能压住漂移又不抖的值。
如果要走“到点”,就把目标点的世界坐标减去当前估计位置,得到世界坐标下的位移向量,再用当前 yaw 做一次二维旋转矩阵变换,转成机体坐标系下的(vx, vy):
def world_to_body(dx, dy, yaw): c, s = np.cos(yaw), np.sin(yaw) vx = c * dx + s * dy vy = -s * dx + c * dy return vx, vy注意这里的旋转方向。很多人第一次写会把矩阵的符号搞反,表现出来就是机器狗朝着目标的反方向走,越走越远。
5.3 延迟这件事必须量化
闭环控制里,延迟不搞清楚就是瞎调。系统总延迟由几部分组成:指令发送到机器狗接收(网络,通常几毫秒)、机器狗内部处理(固件周期)、状态包回传(网络)、你的程序从收到到处理完。
量它的方法很简单:让你的程序记录每一帧的发送时间戳,在状态包里找对应的时间字段(有些版本提供),或者干脆用外部手段——把机器狗放在地上让它原地转,你在程序里打印“发出转向指令的时刻”和“IMU 角速度开始变化的时刻”,差值就是一次往返的近似延迟。
我实测下来,有线连接下这个值通常在十几到二十几毫秒。这个数字很重要:如果你的控制周期是 20ms,那你的相位滞后差不多就是一整个周期,增益就不能按“理想无延迟”来给,要往下压三成左右。
提示:用无线连接调试时,延迟会从十几毫秒涨到几十甚至上百毫秒,而且抖动剧烈。所有涉及闭环调参的工作,一律用有线。
6. 实测中最容易翻车的场景,以及怎么把损失降到最低
前面讲的都是“怎么让它动”,这一节讲“怎么让它别乱动”。这些东西文档里基本不写,但每一条都是真金白银换来的。
6.1 程序异常退出之后的连锁反应
这是最危险的一类问题。你的程序在跑,机器狗在走,然后——一个未捕获的异常抛出来了,进程挂了。此时有两种情况:
如果机器狗还在高层模式,固件内部的看门狗会在几百毫秒内让速度指令归零,机器狗停下。相对安全。
如果是低层模式,看门狗会让它卸力趴下。如果你的程序挂掉的时候机器狗正在快速移动或者姿态不正,趴下的过程就可能磕碰。
所以低层程序必须写成这样:
try: run_low_level() finally: # 无论怎么退出,先回零速,再切回高层 for i in range(12): cmd.motorCmd[i].q = state.motorState[i].q cmd.motorCmd[i].Kp = 0.0 cmd.motorCmd[i].Kd = 2.0 for _ in range(20): udp.send(cmd) time.sleep(0.01) cmd.levelFlag = 0 for _ in range(20): udp.send(cmd) time.sleep(0.01)注意这里先把 Kp 降到零、Kd 保留,让关节从“硬撑”过渡到“有阻尼的放松”,而不是瞬间卸力。这一步能让机器狗缓缓趴下而不是砸下去。
另外,Ctrl+C也会触发KeyboardInterrupt,同样走finally,所以这个写法能覆盖手动中断。真正覆盖不了的是kill -9,那种情况只能靠固件的看门狗。所以调试时永远不要用kill -9结束控制进程。
6.2 遥控器的优先级你必须搞清楚
前面提过一次,这里展开说,因为它涉及安全边界。
这类平台的遥控器通常拥有最高优先级,目的是让人在算法失控的时候能一键接管。这意味着:你的程序再怎么努力,只要遥控器上有人在操作,机器狗就听遥控器的。
这个特性在调试时是好事,在现场部署时是个必须处理的问题。如果你的程序要在无人值守的情况下长期运行,就得确保遥控器处于不被误触的状态(收起来、锁住、或者干脆不放电池),同时你的程序要能检测到“控制权被抢”,做出相应的降级处理——比如检测到速度指令发出但实际速度为零,就进入暂停等待状态,而不是继续盲目发指令。
反过来,如果你在调试过程中发现“程序明明在发指令,机器狗却不听”,第一反应就应该是看一眼遥控器。
6.3 电池、温度和长时间运行的降额问题
这一类问题最阴险的地方在于:它不会在你调试的前十分钟出现,而是在你跑了半小时之后突然冒出来,让你以为是自己的控制逻辑有 bug。
电池电压:满电和低电状态下,电机能输出的力矩是不一样的。同样的 Kp,满电时关节很硬,电量掉到 20% 以下时明显变软,支撑力不足,机器狗会开始下沉、步态变形。所以所有涉及力控的参数,都应该在中等电量下标定,并且在程序里监控电压,低于阈值就主动降低速度上限或者直接结束任务。
电机温度:持续小跑、频繁转向、或者让机器狗在粗糙地面上拖行,都会让关节电机快速升温。温度高了之后,固件通常会自动降额保护,表现就是“刚才还好好的,突然就不听使唤了,速度提不上来”。解决方式无他——降低运动强度,给它休息时间。如果你要做长时间任务,就得在规划层就把占空比考虑进去,跑一段停一段。
通信稳定性:无线连接跑久了会因为信道拥挤出现丢包,丢包多了看门狗会误判。有线连接相对稳定,但接头松动、线材被踩、机器狗自己拖着线跑,都会造成瞬时中断。我现在的做法是在程序里统计连续收包失败的次数,超过阈值就主动降速并报警,而不是等看门狗来兜底。
我踩过最亏的一次坑,是让机器狗在一个有小坡的场地做定点往返,参数是在平地上调好的。上坡的时候需要的力矩大,电池掉得快,跑了二十来分钟电压下去之后,上坡那一段开始打滑,航向闭环又不停加大转向指令,最后在地上画圈。后来加了电压监控和力矩饱和检测,问题就没了。
7. 调参这件事,最后拼的是耐心和方法
上面这些内容拆开看都不复杂,真正难的是把它们组合起来用。我自己在这类平台上折腾下来,最深刻的体会是:所有看起来“玄学”的抖动和漂移,追到底都是某个具体的物理量没对齐——可能是延迟没算进去,可能是单位搞错了,可能是零位偏了,可能是电量不够。
给一个具体的工作方法。每次只改一个参数,改完至少跑三十秒,记录下来。用一个简单的表格管理你的实验:
| 日期 | 改动项 | 原值 | 新值 | 场地 | 表现变化 |
|---|---|---|---|---|---|
| 第一次 | 偏航增益 | 1.2 | 1.0 | 木地板 | 直线漂移略增,但过弯不抖了 |
| 第二次 | 坡道加速度限制 | 0.5 | 0.3 | 带坡 | 上坡不再打滑 |
这个表格看起来笨,但两周之后你会感谢自己。因为参数之间是互相影响的,你以为的“最优组合”往往是某一个参数在特定场地下的偶然匹配,换个场景就崩了,有记录才能快速退回去。
还有一个我个人很推荐的习惯:给机器狗准备一个“安全姿态”。不管你在做什么实验,只要检测到姿态角异常、或者连续若干帧收不到状态包、或者电压过低,就立刻进入一个已知安全的动作序列——先降速,再蹲低重心,最后趴下。这个兜底逻辑写起来不到五十行,但它救过我好几条腿。
最后提一句运动学的事。如果你打算往低层深挖,正运动学和逆运动学是绕不开的。正解是把三个关节角映射成足端在髋坐标系下的位置,逆解反过来。这部分数学不复杂,但符号约定和零位定义是全部难点,八成的错误都出在这两处,而不是公式本身。我的建议是先在纸上画清楚每条腿的坐标系,标好每个关节的正方向,用已知角度手算几个点,再写代码。跳过这一步直接抄公式,你会花三倍时间 debug。