双足机器人步态代码拆解:关节映射、变速与PWM_16P联调 简介《双足机器人设计》文档面向嵌入式机器人开发者、运动控制学习者及双足步态研究人员围绕北京汉库科技HGR-3HV-C机型的步伐源代码展开。内容覆盖初始站立、双腿下蹲与起立的分族变速子程序、侧身和抬腿动作等模块并涉及姿态检测、传感器融合、动力学模型与控制算法可用于理解关节协同关系、梳理步态控制流程、调试动作参数并优化行走稳定性。包内共1个doc文件约9.07MB按目录分节组织便于对照源码逐段查阅、整理笔记和定位关键子程序。已有477人学习下载适合作为双足机器人运动控制入门、课程设计或工程实践的参考材料。1. 双足机器人步伐手册拆解101 页里真正要抄的是哪几段一台双足机器人站着纹丝不动指令一下就走两步往前扑——这种场面我见过太多次。多数人的第一反应是回头改 PID改完该扑还是扑问题常常不在闭环而在这套机器人程序里步伐时序和扫尾系数根本没配上。这份 101 页的双足机器人设计手册主体是一套能直接编译进单片机的嵌入式机器人步伐代码从初始站立、下蹲起立、左右侧身到 Z、P 面分立的原地踏步、带断点与无断点前进再到两类交互式前进的 PWM 合成逐个子程序拆开连积分步数怎么取、扫尾系数怎么递减都写了进去。它适合手上已有一台舵机驱动双足平台、正被“走不稳”折磨的开发者也适合拿课程设计、需要一套完整步态骨架做参照的人。完全没碰过舵机的不太合适——手册默认你知道什么是中位、什么是占空比。下面按“先对齐关节、再抠变速算法、然后拼步伐、最后烧写联调”的路子往下走中间会给出能直接抄的骨架代码和几个我踩过的坑。2. 关节映射先对齐Z 面、P 面和方向位的一次性配置很多人拿到这类手册翻开第一页就开始找“前进子程序”翻到了直接抄进工程结果一上电腿往侧面甩。原因不在前进代码而在最前面那节最容易被跳过的“舵机方向制定”。这一章先把关节、面、方向位这三样东西钉死后面几百行代码才不会白读。2.1 为什么按面分立而不是按关节序号排双足行走时同一时刻不同关节承担的角色完全不同有的负责把重心推到支撑脚上方有的负责抬腿跨越障碍。如果按物理位置顺序给舵机编号写步伐时每调用一次都要心算“这个号是左腿还是右腿、是推重心还是抬腿”几十个子程序写下来必错一次。手册按 Z 面、P 面分组本质是把动作语义编进索引——同一面的关节在同一个子程序里往往同步动作方向位也常常相反。常见做法是Z 面一组管左右方向的重心转移P 面一组管前后方向的抬腿落脚。分组的价值在于你调一个面的时候另一个面的输出可以完全冻结调试时变量少一半。我一般会先把两组关节分开单独跑Z 面只做左右摆、P 面只做抬落各自能稳定重复 30 次再合起来。按面分立的另一个好处是复用。下蹲、起立、侧身、抬腿、落腿这些单一动作本质上都是同一批关节在不同方向上的增量组合只要面分对了函数模板可以只改参数不改结构。2.2 方向位的三种写法选能标定的那种同一个 PWM 增量装在左腿和右腿的舵机转出来的方向是反的。这就是方向位存在的意义。工程里常见的写法有三种宏定义#define L_HIP_DIR 1、方向数组dir_tab[i]、结构体表加上电标定。前两种写起来快但换一台机器就废——哪怕是同型号装配时把舵机臂装反的情况太常见改宏就要重新编译烧写。我一般用第三种。理由很实际标定比重编译快而且标定过程本身能顺带验证接线有没有插错。typedef struct { uchar ch; /* 舵机通道号对应控制板排针顺序 */ uchar face; /* 0 Z 面, 1 P 面 */ uchar dir; /* 方向位: 1 正向增量, 0 反向增量 */ uchar zero; /* 机械零位 PWM 计数, 标定后写入 */ uchar limit; /* 单步最大增量, 防止扫尾阶段过冲 */ } joint_map_t; /* 按面分组排列, 数组下标即子程序内部的访问顺序 */ static joint_map_t jmap[JOINT_NUM] { /* Z 面: 重心左右转移 */ { 0, 0, 1, 0, 0 }, { 1, 0, 0, 0, 0 }, /* P 面: 抬腿与落脚 */ { 2, 1, 1, 0, 0 }, { 3, 1, 0, 0, 0 } };ch是写 PWM 时用的通道号face决定它进哪个子程序组dir参与增量符号计算写反了整套步伐会变成剪刀腿zero初值填 0 表示尚未标定任何用到它的子程序都应该先判断这个标志limit用来卡住扫尾阶段的单步增量舵机速度快的机型这个值要往下压。2.3 把映射表落成结构体上电自检一遍表建好之后别急着跑步伐先写一个标定函数上电第一件事就是它。/* 上电调用: 所有舵机推到中位, 读回实际 PWM 计数写入 zero */ void joint_calibrate(void) { uchar i; for (i 0; i JOINT_NUM; i) { pwm_set(jmap[i].ch, PWM_MID); /* 先给理论中位 */ delay_ms(300); /* 等舵机走完, 别省这一步 */ jmap[i].zero pwm_read(jmap[i].ch); /* 读回实际位置 */ } }逻辑不复杂关键是那句 300ms 延时。省掉它读回来的是舵机上一个位置标定值全废表现出来就是“站立时左右腿不等高”然后你会去怀疑机械装配查一下午。参数上PWM_MID用你控制板的中间计数值pwm_read如果硬件不支持回读就退化成“给中位后固定延时把理论值写进 zero”精度差一点但流程一样。标定完做一次单关节验证手动把某一路dir取反看那个关节的摆动方向是不是跟着反。这一步过了再往下看变速算法。字段取值写错后的现场表现face0 / 1抬腿时腿往侧面甩机身跟着歪dir0 / 1左右腿镜像反了走成剪刀步zero标定值初始站立时左右腿不等高重心偏limit按舵机速度定扫尾阶段过冲落地砸腿3. 变速子程序的两族划分Ea 与 Sa 配错就是砸腿这一章是整份手册密度最高的部分也是最值得逐行读的部分。手册把所有变速动作归纳成“两族”下蹲、起立、侧身、抬腿、落腿全部套这个模板一共十好几个子程序但骨架只有一个。搞懂这两族后面几十页基本可以跳着看。3.1 第 1 族和第 2 族到底差在哪先看两组物理描述。下蹲过程手册归纳成两种第一种只有一个阶段速度逐渐增加、加速度不变或递减到达下极限位置时腿部压力极大优点是能增加脚面附着力缺点是系统负荷大不宜经常用。第二种分两个阶段开始时速度最小运动到中点速度最大然后速度减小到达下极限位置时压力最小属于软着陆。这“两族”就是描述速度曲线的两个分立段第 1 族速度逐渐增大直到最大第 2 族速度逐渐减小直到最小。软着陆下蹲就是第 1 族接第 2 族。理解到这个层次你就明白为什么手册里每个动作都成对出现“第 1 族子程序、第 2 族子程序”——它们不是两个备选项是可以串起来的两个半段。对比项第 1 族第 2 族速度走向由慢到快由快到慢扫尾系数 Sa由初值递减由 1 递增端点压力极限位置压力最大极限位置压力最小典型用途动作起步段动作收尾段单独使用的风险落地砸腿、系统负荷大起步拖沓、响应慢3.2 Sa Ea 的初值约定与递减规则手册里给了三个具体数字值得停下来算一遍扫尾系数最大值 Sa 40下蹲过程积分步数最大值 Ea 76。而实测中步数并不总能到 76出现概率最大的是 67 的一半也就是 38。38 和 40 只差 2工程上可以认为等价于是算法上直接把 Ea 放进 Sa 的初值省掉一次查表和插值。这不是偷懒是把两个量纲不同的参数强行对齐到同一区间。代价是当 Ea 很小时 Sa 初值偏小、启动偏快Ea 很大时 Sa 初值偏大、启动偏慢。手册对此的解释是符合机械特性的积分步数小意味着腿部转动角度小启动可以快些系统仍稳步数大意味着角度大启动必须慢。所以“Sa 初值 Ea”这条规则在两端都是往正确方向偏的。需要加一条兜底Sa 有上限 40Ea 可能超过它直接赋值会溢出。我一般在赋值处做一次截断并在入口判一次合理性。#define EA_MAX 76 /* 积分步数上限 */ #define SA_MAX 40 /* 扫尾系数上限 */ /* 第 1 族: 加速段, Sa 从初值递减到 1, 速度越来越快 */ void xd_zu1(uchar foot) { uchar ea, sa; if (foot 0 || foot EA_MAX) { stable 0; return; } /* 入口合理性判断 */ ea foot; sa (foot SA_MAX) ? SA_MAX : foot; /* Sa 初值直接取 Ea, 超限截断 */ while (ea 0) { limb_step(FACE_ALL, sa); /* 按当前扫尾系数推进一个微分元 */ pos_integrate(); /* 位置积分, 自动装回初始位置 */ ea--; /* 剩余步数递减 */ if (sa 1) sa--; /* 第 1 族关键: Sa 递减, 速度加快 */ } stable 1; }foot是用户输入的积分步数同时充当两个角色循环次数和 Sa 初值。limb_step和pos_integrate是我按手册描述抽出来的两个动作层函数接自己的 HAL 时换成实际写 PWM 和读位置的语句即可。stable是手册里明确的程序出口之一1 表示系统稳定0 表示不稳定后面所有上层的步伐函数都要检查它。第 2 族把规则反过来/* 第 2 族: 减速段, Sa 从 1 递增到上限, 速度越来越慢 */ void xd_zu2(uchar foot) { uchar ea foot, sa 1; if (foot 0 || foot EA_MAX) { stable 0; return; } while (ea 0) { limb_step(FACE_ALL, sa); pos_integrate(); ea--; if (sa SA_MAX) sa; /* 第 2 族关键: Sa 递增, 速度减慢 */ } stable 1; }两族串起来就是软着陆/* 软着陆下蹲: 前段加速, 后段减速, 极限位置速度最小 */ void squat_soft(uchar total) { uchar half total / 2; xd_zu1(half); /* 0 - 中点, 加速 */ if (!stable) { hold_position(); return; } xd_zu2(total - half); /* 中点 - 底, 减速 */ if (!stable) hold_position(); /* 不稳定立刻锁住当前位置 */ }注意total - half而不是half奇数步数时后半段多一步避免总步数少一导致动作没走完。3.3 起立、侧身、抬落腿怎么复用同一模板下蹲讲完剩下三类动作基本是换关节组、换方向的复制。起立两族对应ql_zu1/ql_zu2侧身分左右各两族一共四个抬腿和落腿各分左右、各两族一共八个。手册目录里这十几个子程序排得整整齐齐本质就是同一段 while 循环套不同关节组。统一签名的价值在这里体现出来所有族函数都是void f(uchar foot)就可以建一张函数指针表按“运动方式编码”分发上层调用只认编码不认函数名。typedef void (*move_fn)(uchar); static const move_fn move_tab[] { xd_zu1, xd_zu2, /* 下蹲 */ ql_zu1, ql_zu2, /* 起立 */ ct_l1, ct_l2, ct_r1, ct_r2, /* 左/右侧身 */ tl_l1, tl_l2, tl_r1, tl_r2, /* 左/右抬腿 */ lu_l1, lu_l2, lu_r1, lu_r2 /* 左/右落腿 */ }; void move_exec(uchar code, uchar foot) { if (code MOVE_NUM) { stable 0; return; } move_tab[code](foot); /* 按编码分发, 统一入口 */ }这样上层描述一个复合动作只需要写一串“编码 步数”的序列不必关心具体是哪个关节组在动。调试的时候把编码打出来就知道卡在哪一段。3.4 参数怎么改看什么现象改 Sa 上限是最直接的调法。上限调大整段动作变慢变柔但响应拖沓调小动作干脆落地冲击变大。我习惯从 40 起步先保证不砸腿再往上压速度。改 Ea 的时候要同步看两件事总时长和稳定标志。Ea 加大而 Sa 上限没动第 1 族会很快撞到截断实际速度曲线变成“先匀速后加速”落地更冲。所以这两个参数要成对调别单独动一个。最后每次改完参数先空载跑十次看stable是不是每次都回 1。出现一次 0说明积分过程已经偏了后面所有位置都是错的别接着调先回去查入口判断。4. 从原地踏步到前进步伐断点、无断点与 PWM_16P 的三种拼法单一动作调顺了才算走到真正难的地方——让机器往前挪。手册把前进拆成三级Z、P 面分立的原地踏步、带断点前进、无断点前进最后是两类交互式前进。这个顺序不能乱每一级都是下一级的验收前提。4.1 原地踏步先把相位差跑通原地踏步不产生位移所以它是唯一能单独调相位差的动作。左右腿谁先抬、重心什么时候过中线、抬脚高度多少这些参数在这一步定下来后面前进只是把“原地”换成“往前一点”。手册在踏步这一章把“舵机方向制定”和“动作时序分析”并列放在最前面用意很明确方向错了时序分析全是白费。验收标准我给一个可量化的连续踏步 30 个周期机身水平位移不超过一个脚掌厚度左右脚落地声一致stable全程为 1。这一条不过别碰前进代码。调相位差时我一般只动一个参数——左右腿抬起的间隔步数。间隔太小两条腿同时离地机身直接下沉间隔太大重心来不及转移抬腿那一下会往支撑脚方向倒。4.2 带断点前进抬腿用第 1 族落腿用第 2 族手册里带断点前进新增的专用子程序是四个前进中右腿抬起第 1 族、右腿落下第 2 族、左腿抬起第 1 族、左腿落下第 2 族。为什么偏偏是这个组合回到上一章的结论第 1 族 Sa 递减、速度递增第 2 族 Sa 递增、速度递减。抬腿用加速段腿是“甩”出去的起步快、到位快落地用减速段脚是“放”下去的接触地面时速度已经收住。抬完腿速度在最大值落腿却从这个最大值重新往回减两段之间就出现了一个速度上的断点。这个断点让落地更稳代价是每步之间有一次顿挫走快了会看出来一顿一顿。/* 带断点前进: 抬腿加速, 落腿减速, 相位之间速度归零 */ void walk_break(uchar steps) { uchar n; for (n 0; n steps; n) { qj_you_tai1(); /* 右腿抬起 第 1 族 */ if (!stable) break; qj_you_luo2(); /* 右腿落下 第 2 族 */ if (!stable) break; qj_zuo_tai1(); /* 左腿抬起 第 1 族 */ if (!stable) break; qj_zuo_luo2(); /* 左腿落下 第 2 族 */ if (!stable) break; } }steps是完整步数一次循环包含左右各一次。关键点每个子程序内部自己跑积分循环外面的 for 只负责重复不要把 Ea 当参数传进来——这是两层不同粒度的循环混在一起会出现“步数平方”的怪现象表现为设 10 步走了 100 步的时间。4.3 无断点前进把族的编号整个对调无断点版新增的四个子程序正好反过来抬腿用第 2 族、落腿用第 1 族。抬腿时速度在收腿抬到位时最慢落腿时速度在放脚接触地面前已经在加速。两段的速度曲线能接上中间不归零所以叫无断点。走起来明显更顺、更快但对机械的要求高得多。速度没归零就换相任何一点舵机回差、连杆虚位都会累积成航向偏移走十步看不出来走五十步能偏出半米。对比项带断点前进无断点前进抬腿用族第 1 族Sa 递减第 2 族Sa 递增落腿用族第 2 族Sa 递增第 1 族Sa 递减速度曲线每步有一次归零相邻相位连续落地冲击小脚是放下去的取决于机械刚性适用阶段初调、地面摩擦不均参数稳定后追求速度两套代码结构完全一样只换调用顺序/* 无断点前进: 抬腿减速, 落腿加速, 速度曲线首尾相接 */ void walk_nobreak(uchar steps) { uchar n; for (n 0; n steps; n) { qj_you_tai2(); /* 右腿抬起 第 2 族 */ qj_you_luo1(); /* 右腿落下 第 1 族 */ qj_zuo_tai2(); /* 左腿抬起 第 2 族 */ qj_zuo_luo1(); /* 左腿落下 第 1 族 */ if (!stable) break; } }我的做法是先带断点跑通把方向位、零位、相位差全部锁定再整体切换到无断点版本对比。切换时只改这一处调用顺序别同时动别的参数否则出了问题分不清是族的锅还是参数的锅。4.4 交互式前进PWM_8P1 与 PWM_8P2 合成 PWM_16P第 1 类交互式的思路是把“侧身”和“抬腿”压进同一个相位。原本侧身一相、抬腿一相两相串行一步的时间是两段之和合成之后侧身的同时抬对侧腿重心转移和抬腿同步完成一步的时间压到接近原来的一半。手册新增的四个子程序是左侧身加抬右腿第 2 族、右侧身加落右腿第 1 族、右侧身加抬左腿第 2 族、左侧身加落左腿第 1 族。合成动作在 PWM 层完成——PWM_8P1管一组 8 路PWM_8P2管另一组 8 路拼成PWM_16P一次性输出。/* 两组 8 路拼成 16 路: 先填缓冲区, 再整块刷新 */ void pwm16_build(const uchar *p1, const uchar *p2, uchar *out) { uchar i; for (i 0; i 8; i) { out[i] p1[i]; /* 0..7 第一组 */ out[i 8] p2[i]; /* 8..15 第二组 */ } } void pwm16_flush(const uchar *buf) { pwm_write_block(0, buf, 16); /* 一次性写入, 保证同相 */ }千万别边算边写。分两次写会让两组输出之间有几十微秒的时间差单看没问题但在交互式步伐里这个差值会被放大成肉眼可见的机身晃动。pwm16_build只做拼装不改数值pwm16_flush只做输出不做计算职责分开出问题时能定位到是哪一层。第 2 类交互式改的是另外四个子程序抬右腿前进第 2 族、落右腿前进第 1 族、抬左腿前进第 2 族、落左腿前进第 1 族。区别在于第 1 类动的是相位结构把侧身和抬腿合并第 2 类动的是速度曲线把前进中抬落腿本身的族归属换掉。前者改时序后者改手感别混为一谈。5. 烧写与首步联调的五个坑现象、原因和改法代码写得再漂亮烧不进去或者烧进去不走都是白搭。这一章的五条都是我或者身边人真实翻过的车按“现象、原因、解决”记下来遇到时对号入座比重读手册快。5.1 五条踩坑记录坑一点了编程按钮没反应进度条不动。现象是点了下载软件停在“等待连接”控制板上的灯不闪。原因是上电顺序反了——手册写得很直白必须先点编程按钮再开控制板电源。顺序颠倒单片机上电后直接跑进用户程序根本不进编程状态。解决关电源点编程再上电等软件提示完成。坑二程序能跑但静止时舵机持续轻微抖动走几步航向就偏。现象很隐蔽静止看只是轻微嗡嗡声走起来越偏越多。原因是时钟源选了内部晶振。手册特意提醒过内部晶振下单片机照样工作但舵机控制会出问题。PWM 周期是按系统时钟算出来的时钟本身有偏差占空比就跟着偏多路叠加起来就是抖动加漂移。解决工程选项里改成外部晶振重新编译烧写。顺带核对芯片型号型号选错也能烧进去但定时器行为不对症状和这个很像。**坑三下蹲到底“咚”一声机身明显本文还有配套的精品资源点击获取