凌晨两点四十七分,我被Prometheus发来的警报震醒。告警信息很简单:figure02_joint_temp_upper_bound,腕关节温度超过82℃,触发一级停机。我一边穿裤子一边摸出手机看Grafana面板——产线监控大屏上,Figure 02的右臂已经完全锁定,流水线停了19分钟。这批机器人从早上8点开始不间断运行,到故障发生时刚满18小时,远没达到设计的72小时连续作业指标。我在运维群里发了一句:“关节温度,又是3号机,密封圈有问题那台。”然后翻身骑上电动车往厂区赶。路上我一直在想,从实验室到宝马产线,我们到底在产品化这条路上踏进了多少这样的泥坑。
30秒速览
- - Figure 02在宝马产线的72小时连续运行暴露了关节密封失效、结构疲劳和实时调度延迟,硬件加固必须基于真实工况测试,不是简单的IP代码认证。
- - 实时控制安全停机状态机必须加入超时和故障恢复路径,否则中断嵌套会导致状态锁死,我们用看门狗机制彻底解决。
- - 任务级自动化接口通过技能抽象和行为树降低了MES集成复杂度,但频率牺牲了实时性,需要更强的边缘算力。
- - 软件供应链量产管理缺失会导致灾难性停机,Fast DDS的内存碎片bug让我们锁死所有依赖版本并强制长时间通信压力测试。
- - 远程诊断效率靠故障码归一化和传感器快照提升,平均修复时间从4小时降到37分钟,非计划停机占比大幅压缩。
从实验室到宝马产线:硬件加固不是加个壳子就能搞定
我在接Figure 02产线部署项目之前,以为人形机器人的量产无非就是把实验室那几台原型机复制粘贴。真正走进装配车间才发现,问题密度高得惊人,每一个都足以让产线停摆。
我们做的第一层加固:结构件从“够用”改成“耐操”
实验室原型用的是6061铝合金框架,减重优化到极致,单台成本控制得很好。但放到宝马冲压车间旁边连续跑,振动环境完全不同。我们第一周就碰上肩部结构件疲劳开裂,裂纹从安装孔扩散出去,一台机器人在抬举15kg工件时直接脱力。拆解后发现,原型机的安全系数只按1.5倍设计,到了产线,峰值负载会叠加振动冲击,瞬态过载超过2.8倍。研发团队给出的方案是换7075-T6并加厚腹板,但代价是单臂质量增加370克,对动态控制影响不小。最后我们妥协成局部加强:在危险截面贴钛合金补片,用应变片实时监测,监控阈值设在1500με。这个阈值不是拍脑袋出来的,是拉断了7个试件后取的70%屈服应变。贴片监控的代码接入我们自己的边缘采集器,数据流如下:
# /etc/telegraf/telegraf.d/figure_strain.conf
[[inputs.modbus]]
name = "figure_strain_gauge"
controller = "tcp://192.168.12.30:502"
holding_registers = [
{ name = "strain_shoulder_right", byte_order = "ABCD", data_type = "FLOAT32", scale=1.0, address = [0,1]},
{ name = "strain_elbow_right", byte_order = "ABCD", data_type = "FLOAT32", scale=1.0, address = [2,3]},
]
timeout = "500ms"
[[outputs.prometheus_client]]
listen = ":9275"
metric_version = 2
这个配置看起来平淡无奇,但我踩过一个大坑:默认的Modbus采集间隔是1秒,但在机器臂高速动作时,应变片信号里有大量高频噪声,1秒均值滤波根本滤不干净,经常误触发告警。我最后把Telegraf的interval调到100ms,再用流式聚合,才算把虚警率从每小时17次压到1次以下。这事告诉我,硬件传感器的数据采集中间层不能直接用现成配置,必须跟着机械特性做适配,否则你的告警通道就是狼来了的故事。
防水防尘:IP68的标签在产线面前就是一张纸
实验室环境下,Figure 02的整机防水是按IEC 60529测过IP68的,浸入1.5米水深30分钟没问题。但宝马产线不是水池,是油雾、金属粉尘和高压水枪交织的恶劣环境。我们第一个月报废了6台机器人的关节轴承,全是密封失效。拆开发现,唇形密封圈的材质是丁腈橡胶,耐矿物油但耐高温氧化性能不够。关节电机附近长期80℃运转,密封圈硬化、弹性丧失,然后微小的金属碎屑进入滚道,造成磨粒磨损。更糟的是,维修团队没有早期检测手段,每次都是等到关节扭矩异常升高、机器人自己报过载才停机,这时轴承滚道已经压出剥落坑了。(延伸阅读:我们用Bedrock多智能体搞定了差旅报销,但第一个版本差点把财务部搞崩)
我们做了一次彻底整改:密封圈全部换成氟橡胶,并且在每个关节腔增加正压空气回路,维持内部比环境高0.2bar的干燥空气压力。这个方案借鉴了ABB喷涂机器人的设计,但小型化到人形关节上费了很大功夫。同时,我在监控里加了一条规则,一旦腔体压力传感器跌落至0.05bar以下,立刻触发预警告而不是等到停机:
# PrometheusRule for joint chamber pressure
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: figure-joint-pressure
spec:
groups:
- name: figure_seal_integrity
rules:
- alert: JointChamberPressureLow
expr: figure_chamber_pressure_bar < 0.05
for: 5s
labels:
severity: critical
component: joint_seal
annotations:
summary: "Joint {{ $labels.joint_id }} chamber pressure critically low"
runbook_url: "https://wiki.internal.runbooks/figure-joint-seal-failure"
- alert: JointChamberPressureTrend
expr: deriv(figure_chamber_pressure_bar[10m]) < -0.002
for: 10m
labels:
severity: warning
annotations:
summary: "Joint {{ $labels.joint_id }} pressure declining, possible seal wear"
加了正压和监控之后,轴承失效率从每千小时12台降到0.8台,但成本上升了约22%。回过头看,如果研发阶段就把产线环境因素纳入密封选型,这6台报废的成本完全可以避免。防水防尘不要只看IP代码,你必须拿着真实环境介质做加速老化测试,否则量产后的可靠性账单会让你心脏疼。(延伸阅读:从KB到TB:我在256块B200上调度万亿参数训练的30天——每步延迟都刻进骨头里)
软件栈:实时控制环里藏着一个200微秒的死锁幽灵
人形机器人在产线上最大的挑战不是AI,是确定性。动作规划可以异步,但伺服控制环必须准时,一微秒的抖动都不行。
实时操作系统与安全停机:不是切了就完事
Figure 02的主控跑的是基于Linux 6.1的实时内核,补丁集用的是PREEMPT_RT。我们遇到的第一个软件问题不是功能bug,而是调度延迟毛刺。在物料搬运任务中,偶尔会出现手臂运动卡顿,持续大约200~300微秒。这个级别的抖动根本不会让系统崩溃,但累积起来会导致放置位置偏差超差,一个班次下来良品率下降1.2%。我用cyclictest跟踪了48小时,终于抓到元凶:某个非实时线程在做日志轮转时持有一把自旋锁,阻塞了高优先级的运动控制线程。日志系统用了默认的glibc写文件,没有做实时安全设计。修复方案是把日志推到共享内存环形缓冲区,由独立低优先级线程异步刷盘,并且用mlockall(MCL_CURRENT|MCL_FUTURE)锁住实时任务的内存页面,防止换页。
安全紧急停机机制是另一个血泪点。我们设计了三级停机:软停机(减速停止)、硬停机(断使能、动态制动)、紧急断电(切断动力电源)。但在第一周产线联调时,安全PLC触发硬停机后,机器人控制器里的状态机卡在ESTOP_ACTIVE与DISABLED之间,导致无法重新上使能,需要现场人员手动重启控制器。查代码发现,状态机缺少超时与故障恢复路径,一个意外的中断嵌套就让状态流转死锁了。最后我们重写了安全状态机,要求每个状态必须设置看门狗,超时自动退到安全状态,并在状态变换时记录结构化日志发送到监控系统。修改后的状态机伪代码逻辑成了整个团队的强制模板:
void safety_state_machine(SafetyEvent event) {
static State state = STATE_DISABLED;
static uint64_t wdog = 0;
switch(event) {
case EVT_ESTOP:
if(state != STATE_ESTOP) {
log_safety_event("ESTOP_ENTER", state, get_timestamp_us());
state = STATE_ESTOP;
wdog = get_timestamp_us() + 500000; // 500ms timeout
engage_brakes();
disable_servos();
}
break;
case EVT_CLEAR:
if(state == STATE_ESTOP && check_safety_clear()) {
log_safety_event("ESTOP_CLEAR", state, get_timestamp_us());
state = STATE_RECOVERY;
wdog = get_timestamp_us() + 1000000;
precharge_drivers();
}
break;
case EVT_TIMER:
if(state == STATE_ESTOP && get_timestamp_us() > wdog) {
log_safety_event("ESTOP_WDOG", state, get_timestamp_us());
state = STATE_LOCKOUT; // require manual reset
power_off_drives();
}
if(state == STATE_RECOVERY && get_timestamp_us() > wdog) {
state = STATE_ESTOP; // fallback
}
break;
// ...
}
}
这段代码在产线上跑了超过2000小时,再没出现过状态锁死。我们得出的结论是:安全停机不能依赖中断的完美时序,必须用超时机制兜底,否则量产时的意外中断会把你所有假设打碎。(延伸阅读:我把一个27万行的monorepo从Webpack切到Vite 6.0 Rolldown,CI构建从8分钟掉到了42秒)
任务级自动化接口:物料搬运的标准化不是协议,是语义对齐
宝马产线原本就有一套基于OPC UA的MES系统,用于调度AGV和机器人。我们在Figure 02上也复用了这套协议,但很快就发现语义层根本对不齐。AGV的任务是“移动料箱到工位A”,而人形机器人的任务是“用双手从料箱中抓取铸件毛坯,翻转180度放入压机模具”。这里面涉及多个子任务,且需要与压机、传送带协调。我们最初定义了十几个自定义OPC UA方法,比如PickPart、Rotate、PlaceInDie,但组合调用时,每个方法都要带一堆上下文参数,而且错误处理异常复杂——如果一个Rotate调用超时,上游MES根本不知道应该回滚到哪个步骤。最后我们干脆把任务抽象成“技能”资源,通过一个单一调用ExecuteSkill传递JSON描述,内部由行为树引擎管理子任务和异常恢复。接口简化成这样:
// OPC UA Method: ns=2;s=Figure.ExecuteSkill
{
"skill": "LoadDie",
"params": {
"source_location": "conveyor_3",
"target_press": "press_2",
"part_type": "alum_casting_17",
"flip_orientation": true
},
"timeout_ms": 120000,
"priority": 10
}
行为树内部在拿铸件之前先检查视觉定位质量,如果定位失败会重试两次,重试仍失败则回退到安全位姿并向MES报告SKILL_FAILED_PART_NOT_FOUND。这层抽象减少了90%的MES端集成时间,但我们也付出了代价:行为树引擎的循环周期从250Hz降到了50Hz,导致某些动态避障响应慢了。折中就是,对运动安全关键的避障依然运行在实时域,只有任务调度层用低频率循环。这种做法目前看是可行的,但我一直想把它重新优化到100Hz以上——前提是拿到更快的CPU,当前Jetson AGX Orin的负载已经跑到78%了。
故障率统计与远程诊断:我们终于把平均修复时间从4小时压到了37分钟
量产部署最怕的不是出故障,是不知道故障在哪。Figure 02有43个电机、37个温度传感器、12个压力传感器、6个六维力/力矩传感器,还有电池、驱动器、惯性测量单元等。每一台机器在72小时产线运行中产生约2.3TB的结构化遥测数据。如果不做分层处理,运维人员会淹没在数据里。
远程诊断的前提是高质量故障码和上下文快照
我们强制要求所有底层组件必须上报Fault Code,而不是简单的“错误”。一个完整的故障记录包含故障码、时间戳、故障发生前10秒的传感器快照、当前技能上下文和堆栈。这套格式定义在ROS 2的诊断消息之上,扩展成我们自己的数据模型。初期很多故障码都是“UNKNOWN_ERROR”,因为研发人员偷懒没分配子码,结果现场人员只能整机重启。我拉了一个Jira任务强制所有模块补充故障码,并且每发布一个新固件版本,自动化测试框架会模拟注入故障,检查未知错误率是否超过0.1%。超过就Block流程。这种做法带来了显著的改变:远程诊断一次的平均交互从7轮消息压缩到2轮,因为我们看到的不再是“机器人不动了”,而是“右肩关节驱动器报错0x1A05,过流,母线电流32A持续1.2秒,电机编码器无反馈”——基本能直指现场替换单元。
故障统计暴露的关节寿命短板
在连续运行超过10000小时后,我们统计了所有关节的故障分布。谐波减速器的寿命问题比电机更突出。厂家标称L10寿命是10000小时,但我们在7000小时左右就开始出现柔轮疲劳裂纹,表现是转矩波动增大,监控中可见加速度噪声增加。最初我们没有针对转矩波动做告警,等抖动大到影响动作精度时,减速器已经临近失效。后来我们加了一道FFT分析,跟踪特定频率带的能量,当三次谐波能量超过阈值就提前安排更换。这个频域监控跑在边缘计算板上,用Python的scipy.signal做实时STFT计算,虽然笨重,但总算把非计划停机占比从34%砍到11%。
数据表明,连续72小时满载作业时,关节平均温度比实验室循环测试高12℃,高温直接加速润滑脂劣化。我们不得不把单次连续运行调整为48小时后强制散热1小时,同时升级了合成润滑脂。这些调整背后全是监控数据在说话,没有数据,你根本不知道是哪颗螺丝先松。(延伸阅读:Copilot Chat免费了,我让我妈试了试自然语言编程,然后她真写出个网页来)
量产逼出来的软件供应链反思:我们不能再像搭积木一样拼开源组件了
做DevOps这么多年,我对依赖包的风险敏感度已经刻进骨子里。在Figure 02的软件栈里,我们用了大量开源组件:ROS 2 Humble、DDS、EtherCAT主站、PCL点云库、OpenCV、TensorRT等等。但直到量产,我们才意识到软件物料清单的管理有多薄弱。
一个DDS vendor的bug,让整个机队僵住12分钟
我们使用的是eProsima Fast DDS,一个常见选择。但在某个周五下午,所有运行超过6小时的机器人突然出现消息延迟暴增,控制命令从30微秒延迟飙升到800毫秒,安全看门狗超时触发大规模急停。分析发现,Fast DDS的内部碎片管理在长时间运行后会产生大量内存碎块,导致分配器阻塞。这个bug在官方GitHub上已经有人开了issue,但在我们采用的版本中没有修复。我们的CI管道并没有做长时间内存压力测试,因为单元测试只跑10分钟。出事后,我们连夜在CI加入24小时通信稳定性测试,模拟持续的消息收发,并监控RSS内存和分配延迟。同时,我们锁定了所有ROS 2依赖的精确版本哈希,并且用vcs export --exact导出完整的repo清单。任何依赖更新必须经过上述长时间稳定性测试。
# dependency lock file snippet: ros2.repos
repositories:
ros2/rmw_fastrtps:
type: git
url: https://github.com/ros2/rmw_fastrtps.git
version: 6f9b27a3a4c15b8b3f7e0a3fc1d25ab44cf6d012
eProsima/Fast-DDS:
type: git
url: https://github.com/eProsima/Fast-DDS.git
version: a3c5e8f29120d4b56e7f1a0b9d46c71e2af8b093
这个教训让我彻底认同一件事:做机器人量产,必须把软件供应链当成硬件BOM一样管理,每一个第三方库的版本、补丁、已知漏洞都必须是可追溯的,否则半夜停机的根源很可能是一个两年没人管的issue。(延伸阅读:Blackwell Ultra推理调优手记:我为何押注FP8量化与MIG分区,却差点输给显存带宽)
AI模型部署流水线:从笔记本到产线的最后一公里全是坑
Figure 02用到的视觉抓取模型是PyTorch训练的,然后通过TensorRT转换部署到Orin上。我们最初沿用研发阶段的模型部署方式:手动导出ONNX,手动优化,手动scp到机器人。一旦模型更新频繁,就彻底乱套,有过两台机器人跑着不同版本权重而产线上一片混乱的情况。最终我们搭了一条基于GitOps的模型部署流水线:模型训练完成后,由GitHub Actions触发导出和精度验证,然后打包成OCI镜像(用tritonserver作为推理服务),推送到私有注册表。机器人通过K3s上的轻量调度器定期拉取最新模型版本,版本号用Git commit hash标记,并且和产线MES的工单绑定。任何一次模型部署如果导致抓取成功率下降超过0.5%,自动回滚到上一个版本。这个自动回滚机制救过我们两次:有一次新的transformer模型在产线光照条件下置信度飘移,系统在45分钟内检测到并自动退回了上一版,才没造成批量报废。监控抓取成功率的指标是用Prometheus记录每次抓取结果,通过自定义Exporter统计每分钟成功率:
# HELP figure_grasp_success_rate 1-minute grasp success rate
# TYPE figure_grasp_success_rate gauge
figure_grasp_success_rate{skill="pick_and_place"} 0.993
figure_grasp_success_rate{skill="bin_picking"} 0.987
一旦低于阈值,Alertmanager触发回滚脚本,并且通知我。半夜被叫醒的次数从此减少了一大半,因为系统自己搞定了。
从Figure 02走进宝马产线的这半年,我手上的老茧多了,监控面板上的红色告警也从开始的每天几十次降到个位数。量产工程化没有魔法,只有一层层防呆设计、一次次深夜复盘,和每个关节里流淌的监控数据。现在我敢说,我们的机器人可以扛住72小时连续作业了——不是因为它不会坏,而是因为我们知道它什么时候可能坏,并且在它坏之前让它停下来休息一下。这或许就是一个老DevOps对人形机器人产业化的全部理解:稳定性不是设计出来的,是告警和回滚堆叠出来的肌肉记忆。