简介:《双足机器人设计》文档面向嵌入式机器人开发者、运动控制学习者及双足步态研究人员,围绕北京汉库科技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取反,看那个关节的摆动方向是不是跟着反。这一步过了,再往下看变速算法。
| 字段 | 取值 | 写错后的现场表现 |
|---|---|---|
| face | 0 / 1 | 抬腿时腿往侧面甩,机身跟着歪 |
| dir | 0 / 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 有上限 40,Ea 可能超过它,直接赋值会溢出。我一般在赋值处做一次截断,并在入口判一次合理性。
#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 周期是按系统时钟算出来的,时钟本身有偏差,占空比就跟着偏,多路叠加起来就是抖动加漂移。解决:工程选项里改成外部晶振,重新编译烧写。顺带核对芯片型号,型号选错也能烧进去,但定时器行为不对,症状和这个很像。
**坑三:下蹲到底“咚”一声,机身明显
本文还有配套的精品资源,点击获取