0 / 8 个检查点已完成
LEVEL 5 · 进阶

定位轮(Tracking Wheels)

前置:底盘控制、传感器基础、PID 概念
开始之前,确认你已经会这些:

以上内容会在 Level 0-4 课件中系统教学(制作中)。如果你已经有比赛经验,可以直接开始。

分层说明

必学 的是所有人必学,标 实战 的是实战进阶(比赛前再学也行),标 深入 的是深入理解(学有余力再看)

必学 为什么需要定位轮?

想象一下:自动阶段,你让机器人前进 100cm,但轮子打滑了,实际只走了 80cm。程序以为到位了,后面所有动作全部偏移。

核心问题:驱动轮会打滑,用驱动轮的编码器算距离不准。

解决方案:装几个不连接电机的小轮子(被动轮),它们只是贴在地上滚动,不会打滑。通过它们的编码器,我们可以精确知道机器人实际走了多远。

这就是定位轮(Tracking Wheels),也叫追踪轮、里程计轮。

▶ 视频讲解
What is Odometry? — An Introduction to Robot Odometry
Rex Liu · 6 分钟 · 英文 · 用动画讲解为什么需要定位轮
检查点 1
定位轮为什么比驱动轮编码器更准?

必学 一、定位轮是什么?

▶ 视频讲解
VEX Auton Tutorial Part 1 — Tracking Wheels
EthanMik · 11 分钟 · 英文 · 定位轮硬件设计和安装实操

硬件组成

一套定位轮系统通常包括:

┌─────────────────────────────┐
│         机器人俯视图          │
│                              │
│    ║        ↑ y正(前)   ║    │  ← 驱动轮(左右各3个)
│    ║    ┃          ┃    ║    │
│    ║    ┃竖轮      ┃    ║    │  ← 竖轮:测量前后移动
│    ║    ┃(Vertical)┃    ║    │
│    ║                    ║    │
│    ║  ━━━━━━━━━━━━━━━━  ║    │  ← 横轮:测量左右移动
│    ║    (Horizontal)    ║    │
│    ║                → x正(右) │
│    ║                    ║    │
│         [V5 大脑]         │
└─────────────────────────────┘
部件作用数量
竖轮(Vertical)测量前后方向移动距离1 个
横轮(Horizontal)测量左右方向移动距离1 个
陀螺仪(IMU)测量旋转角度1 个
旋转传感器(Rotation Sensor)装在定位轮上,读取旋转角度2 个
机器人底座定位轮布局示意图
图:定位轮布局俯视图 — 左右竖轮 + 后横轮 + 旋转中心(来源:Team 5225 E-Bots Pilons)

坐标系约定

在开始之前,必须先统一"方向"的定义,否则后面的公式正负号会乱:

约定定义
x 正方向机器人的右边
y 正方向机器人的前方(车头方向)
θ 正方向顺时针旋转
竖轮正方向向前滚 = 正值
横轮正方向向右滚 = 正值
重要:如果你的轮子装反了(向前滚读出负值),在代码里把编码器值取反,或者在传感器设置里 reverse。不要去改公式的正负号。

为什么需要两个轮 + 陀螺仪?

三个信息结合,就能算出机器人在场地上的精确坐标 (x, y, θ)

就像 GPS 告诉你在地图上的位置一样,定位轮告诉机器人在场地上的位置。
检查点 2
一套最基本的定位系统需要哪些?

必学 二、数学模型(核心思想)

▶ 视频讲解
Learn the Math behind Odometry — Essence of Robot Odometry Pt. 2
Rex Liu · 11 分钟 · 英文 · 弧线模型和弦长计算的完整推导

2.1 基本想法

我们要做的事情很简单:

  1. 每隔很短的时间(5 毫秒),读一次传感器
  2. 算出这段时间里机器人移动了多少
  3. 把移动量加到之前的位置上
核心公式
新位置 = 旧位置 + 这一小段时间的移动量

反复执行这个过程,就能一直知道机器人在哪里。

2.2 直线情况(简单)

如果机器人走直线,没有转弯:

       前 (y+)
       ↑
       |  竖轮走了 Δv
       |
  ← ──┼── → (x+)
横轮走了 Δh

这很直觉。但机器人大部分时间不是走直线,而是在转弯

2.3 从直线到弧线的过渡

想象一下:机器人不是走完美直线,而是一边走一边微微右转。

关键洞察:转弯时,机器人走的是一段弧线,不是直线。定位轮测到的是弧长(弯曲的路径长度),但我们需要的是位移(起点到终点的直线距离)。
机器人弧线运动示意图
图:弧线运动示意 — 红色/绿色分别是左右轮的弧线路径,灰色是机器人终点位置(来源:Team 5225)

弧线越弯,弧长和位移的差距越大。所以我们需要一个方法,把弧长转换成位移。

必学 2.4 转弯情况(圆弧模型)

不用背公式:这一节看起来数学多,但你只需要理解思路(弧长 → 半径 → 弦长),代码会帮你算。后面的完整版代码就是基于这个模型写的。

关键假设:在很短的时间内,机器人的运动轨迹可以看作一段圆弧。

为什么?因为两边电机功率不同,机器人会绕一个点画弧线。时间越短,这个假设越准确。

         B (新位置)
        ╱
      ╱  弦长(我们要求的位移)
    ╱
  A (旧位置)
   \
    \  半径 R
     \
      O (圆心)

角度变化 = Δθ(陀螺仪告诉我们的)
弧长 = 定位轮走的距离

推导过程(不需要背,理解思路就好):

  1. 陀螺仪告诉我们角度变了 Δθ
  2. 竖轮告诉我们走了弧长 弧长v
  3. 圆弧公式:弧长 = 半径 × 角度,所以 半径 = 弧长v ÷ Δθ
  4. 弦长公式(几何):弦长 = 2 × 半径 × sin(Δθ/2)
  5. 弦长就是实际位移,再分解到 x 和 y 方向

第 5 步为什么要"分解"? 因为机器人是斜着走的(一边前进一边转弯),位移不是纯 x 或纯 y 方向,需要用三角函数拆成两个分量。用的角度是这段时间的平均朝向 θ旧 + Δθ/2(起点和终点的中间值)。

最终公式
Δx = 弦长v × sin(平均角度) + 弦长h × cos(平均角度)
Δy = 弦长v × cos(平均角度) - 弦长h × sin(平均角度)
为什么是 sin 和 cos 这样搭配? 这其实是"旋转矩阵" —— 把机器人视角的前后左右,转换成场地视角的 x 和 y。车头朝前(y+方向)时,前进 = y 增加,所以竖轮对 y 的贡献用 cos(cos(0)=1);前进不改变 x,所以竖轮对 x 的贡献用 sin(sin(0)=0)。转了角度之后,cos 和 sin 的值随之变化,自动完成了坐标转换。
试一试:拖动滑块,观察坐标旋转

机器人走了 10cm,拖动角度看 Δx 和 Δy 怎么变

y+ x+

Δx = 10 × sin() = 0.00 cm
Δy = 10 × cos() = 10.00 cm

0° = 正前方,90° = 正右方
红色 = x 分量,绿色 = y 分量

实战 2.5 偏移补正

理想情况下,竖轮应该装在机器人的旋转中心上。但实际上很难做到,轮子往往偏了一点。

偏移会导致:机器人原地转圈时,定位轮也会走一段弧,程序误以为机器人移动了。

补正方法:在计算半径时,加上定位轮到旋转中心的偏移距离。

偏移补正公式
真实半径 = 弧长 ÷ Δθ + 偏移距离

这个偏移距离需要量出来(后面会讲怎么测量)。

检查点 3
机器人朝 0 度(正前方)直走 30cm,没有转弯。x 变化了多少?

必学 三、代码实现

从公式到代码:在看 C++ 代码之前,先用自然语言理解算法在做什么:
  1. 读传感器 — 一次性读取竖轮、横轮、陀螺仪的当前值
  2. 算弧长 — 用编码器变化量 × 轮子半径,得到竖轮和横轮各走了多远
  3. 算角度变化 — 当前角度 - 上次角度 = Δθ
  4. 判断直线还是弧线 — 如果 Δθ 几乎为零 → 当直线,弧长就是位移;否则 → 用圆弧模型算弦长
  5. 坐标旋转 — 用 sin/cos 把"机器人视角的前后左右"转成"场地视角的 x 和 y"
  6. 更新坐标 — 把位移加到全局坐标上
  7. 保存当前值 — 存起来给下次循环用

3.1 准备工作:常量定义

C++
// 定位轮参数
#define WHEEL_DIAMETER_V 5.08   // 竖轮直径(cm),2 英寸全向轮
#define WHEEL_DIAMETER_H 5.08   // 横轮直径(cm)
#define OFFSET_V 4.0            // 竖轮到旋转中心的水平偏移(cm)
#define OFFSET_H 10.0           // 横轮到旋转中心的纵向偏移(cm)

// 角度转弧度
#define DEG_TO_RAD (M_PI / 180.0)

// 极小角度阈值(小于这个值视为直线)
#define ANGLE_THRESHOLD 0.001   // 约 0.06 度
为什么用弧度? 三角函数 sin()cos() 要求输入弧度,不是角度。
弧度 = 角度 × π ÷ 180

3.2 先看极简版(只处理直线)

如果机器人只走直线不转弯,定位代码只需要 5 行:

C++
// 极简版:假设不转弯
void updatePosition_simple() {
    double delta_v = (VerticalEncoder.position(deg) - last_vertical)
                     * DEG_TO_RAD * (WHEEL_DIAMETER_V / 2.0);
    double delta_h = (HorizontalEncoder.position(deg) - last_horizontal)
                     * DEG_TO_RAD * (WHEEL_DIAMETER_H / 2.0);
    double angle = IMU.rotation(deg) * DEG_TO_RAD;

    // 直接用当前角度做坐标旋转
    pos_x += delta_v * sin(angle) + delta_h * cos(angle);
    pos_y += delta_v * cos(angle) - delta_h * sin(angle);

    last_vertical = VerticalEncoder.position(deg);
    last_horizontal = HorizontalEncoder.position(deg);
}

这个版本在机器人走直线时完全正确,转弯时有误差但也能用。很多队伍一开始就用这个版本。

3.3 完整版:加上圆弧修正

在极简版基础上,加上转弯时的圆弧修正(新增部分用 // ★ 标注):

C++
void updatePosition() {
    // === 第一步:一次性读取所有传感器(保证数据一致性) ===

    double cur_vertical = VerticalEncoder.position(deg);     // ★ 局部变量
    double cur_horizontal = HorizontalEncoder.position(deg); // ★ 局部变量
    double angle = IMU.rotation(deg) * DEG_TO_RAD;

    // 编码器角度转成轮子走过的距离(弧长)
    double arc_v = (cur_vertical - last_vertical)
                   * DEG_TO_RAD * (WHEEL_DIAMETER_V / 2.0);
    double arc_h = (cur_horizontal - last_horizontal)
                   * DEG_TO_RAD * (WHEEL_DIAMETER_H / 2.0);

    // 陀螺仪角度变化
    double delta_angle = angle - last_angle;

    // === 第二步:算弦长(实际位移) ===

    double chord_v, chord_h;

    if (fabs(delta_angle) < ANGLE_THRESHOLD) {               // ★ 用阈值,不用 == 0
        // 几乎没转弯,弧长 ≈ 弦长
        chord_v = arc_v;
        chord_h = arc_h;
    } else {
        // 有转弯,用圆弧模型
        double radius_v = arc_v / delta_angle + OFFSET_V;    // ★ 补正偏移
        double radius_h = arc_h / delta_angle + OFFSET_H;    // ★ 补正偏移

        chord_v = 2 * radius_v * sin(delta_angle / 2.0);
        chord_h = 2 * radius_h * sin(delta_angle / 2.0);
    }

    // === 第三步:转换到全局坐标 ===

    double avg_angle = last_angle + delta_angle / 2.0;       // ★ 平均角度

    pos_x += chord_v * sin(avg_angle) + chord_h * cos(avg_angle);
    pos_y += chord_v * cos(avg_angle) - chord_h * sin(avg_angle);

    // === 第四步:保存本次值,给下次循环用 ===

    last_vertical = cur_vertical;                             // ★ 用局部变量
    last_horizontal = cur_horizontal;                         // ★ 用局部变量
    last_angle = angle;
}

对比极简版,完整版多了什么?

改进作用不加会怎样
传感器一次性读取到局部变量保证计算和保存用的是同一组数据偶尔数据不一致
fabs(delta_angle) < 阈值避免除以接近零的数半径算出天文数字,坐标飞走
圆弧弦长计算转弯时更精确走弧线误差大
偏移补正消除轮子不在旋转中心的影响原地转圈坐标会漂移
平均角度用中间时刻的角度更准走弧线时 x/y 分配有偏差

3.4 后台线程:持续更新

定位系统需要在后台不停地跑,不能被其他代码打断:

C++
int trackingTask() {
    while (true) {
        updatePosition();
        task::delay(5);  // 每 5ms 更新一次(一秒 200 次)
    }
    return 0;
}

main() 里启动它:

C++
// VEXcode 写法
task tracking = task(trackingTask);

// PROS 写法(如果你用 PROS)
// pros::Task tracking(trackingTask);

3.5 逐行解读

为什么编码器值要乘 DEG_TO_RAD * (直径/2)

轮子走的距离 = 转过的角度(弧度) × 半径

编码器返回角度(度) → 乘 π/180 → 角度(弧度)
弧度 × 半径 = 弧长(距离)

为什么不能用 delta_angle == 0

因为陀螺仪总有微小噪声,返回值可能是 0.00003 而不是精确的 0。用 == 0 判断几乎永远为 false,导致每次都走圆弧分支 —— 当 delta_angle 极小时,arc_v / delta_angle 会算出一个巨大的半径,坐标直接飞走。用阈值 fabs(delta_angle) < 0.001 就安全了。

为什么用 sin(avg_angle)cos(avg_angle)

这是坐标旋转。定位轮测到的是机器人自己视角的前后左右,我们需要转换成场地视角的 x 和 y。用平均角度做旋转最准。

试一试:回到数学基础的三角函数交互组件,拖动角度从 0° 到 45° 到 90°,观察 sin 和 cos 的值怎么变化。你会发现:角度为 0° 时 cos=1、sin=0(前进只增加 y);角度为 90° 时 cos=0、sin=1(前进只增加 x)。这就是坐标旋转在做的事。
检查点 4
代码里为什么用 fabs(delta_angle) < 0.001 而不是 delta_angle == 0?

必学 四、定位轮安装指南

提示:如果你正在学原理阶段,还没有实体机器人,可以先跳到第五节(LemLib)继续学代码,等装车时再回来看这一节。
▶ 视频讲解
VEX Auton Tutorial Part 1 — Tracking Wheels(安装部分)
EthanMik · 11 分钟 · 英文 · 含 CAD 文件和弹簧安装细节
VEX Auton Tutorial Part 2 — Setting Up Odometry Template
EthanMik · 13 分钟 · 英文 · 轮径测量和偏移量计算

安装质量直接决定定位精度。

4.1 位置

要求原因
竖轮尽量在旋转中心的纵线上减少原地转圈时的误差
横轮尽量在旋转中心的横线上同上
离旋转中心越近越好偏移越大,补正误差越大
旋转中心在哪? 一般在底盘的几何中心附近。可以让机器人原地旋转,看哪个点不动,那就是旋转中心。

4.2 机械要求

要求原因不达标的后果
轮子必须完全平行/垂直于底盘哪怕偏 2 度,走远了误差很大走 3 米偏 10cm+
左右晃动极小(掰不动的程度)晃动 = 每次读数方向不一致误差快速积累
轮子始终贴地离地就读不到数据定位直接失效
用皮筋或弹簧向下压保证过颠簸时不离地弹起瞬间数据丢失
旋转阻力越小越好阻力大 = 轮子跟不上机器人读数滞后
皮筋力方向竖直向下斜着拉会给轮子施加扭矩轮子被拉歪
怎么检查轮子是否平行? 把机器人放在场地上,手推着它走一条直线(3米),看竖轮读数是否一直增长(不应该有来回跳动)。如果跳动,说明轮子歪了或者有晃动。

4.3 最常见的错误

常见安装错误

轮子歪了 2-3 度,以为没影响
→ 走一圈回来坐标偏了 20cm

轮子没压紧,过坎时弹起来
→ 弹起的瞬间数据丢失,位置跳变

在定位线程里打印数据到遥控器屏幕
→ 打印很慢,拖慢刷新频率(从 200Hz 降到 50Hz),精度下降

编码器接线松动
→ 偶尔读到 0,坐标瞬间飘走

横轮装反了(向右滚读出负值)
→ x 坐标方向反了,机器人以为自己在镜像位置
→ 解决:传感器设置里 reverse,或代码里取反

实战 4.4 偏移距离怎么测?

测量定位轮到旋转中心的距离:

  1. 设定初始位置 (0, 0),先把偏移距离设为 0
  2. 让机器人原地顺时针转 10 圈
  3. 看程序报告的 (x, y) —— 理论上应该还是 (0, 0)
  4. 如果 x 偏了,调整竖轮偏移(OFFSET_V);如果 y 偏了,调整横轮偏移(OFFSET_H)
  5. 反复调整直到转 10 圈后坐标几乎不变(x, y 都在 2cm 以内)
偏移值有正负:轮子在旋转中心右边就是正,左边就是负。可以先量个大概值,再用上面的方法微调。
检查点 5
定位轮安装时最重要的是?

实战 五、和 LemLib 的关系

▶ 视频讲解
How to Set Up LemLib + PROS Like a Pro
BennyBuildsRobots · 19 分钟 · 英文 · LemLib 完整安装配置教程
VEX Auton Tutorial Part 5 — Writing Autonomous
EthanMik · 23 分钟 · 英文 · 用 odometry 写自动程序实战

上面的代码是从头写的,帮助理解原理。实际比赛中,很多队伍用 LemLib 库,它帮你封装好了定位系统。

LemLib 的核心逻辑和我们写的基于同一个数学模型(圆弧近似),但实现细节有差异:

C++
// LemLib 的核心算法(简化版)
localX = 2 * sin(deltaHeading / 2) * (deltaX / deltaHeading + horizontalOffset);
localY = 2 * sin(deltaHeading / 2) * (deltaY / deltaHeading + verticalOffset);

// 转换到全局坐标
odomPose.x += localY * sin(avgHeading) + localX * (-cos(avgHeading));
odomPose.y += localY * cos(avgHeading) + localX * sin(avgHeading);
注意看符号差异:LemLib 的横轮项用了 -cos+sin,我们的代码用的是 +cos-sin。这不是谁写错了 —— 而是横轮正方向的约定不同。LemLib 定义横轮"向左为正",我们定义"向右为正",所以符号自然相反。

核心公式是一样的: 弦长 = 2R sin(Δθ/2)R = 弧长/Δθ + 偏移。区别只在坐标轴方向的约定。

LemLib 额外提供的功能:

建议:先用自己写的代码理解原理,比赛时再切换到 LemLib 享受它的额外功能。遇到定位不准时,你知道该查什么。
检查点 6
我们的代码和 LemLib 的横轮符号不同,这说明?

实战 六、用定位数据做什么? —— MoveTo 函数

▶ 视频讲解
VEX Robotics PID Control: An Introduction
Seaquam Robotics · 15 分钟 · 英文 · PID 基础概念在 VEX 中的应用
VEX Auton Tutorial Part 4 — Tuning PID Controllers
EthanMik · 16 分钟 · 英文 · PID 调参实操(含仿真器演示)

知道了 (x, y, θ),最直接的应用是:让机器人自动走到指定坐标

6.1 思路

把底盘功率拆成两部分:

左轮功率 = 直行功率 + 转弯功率
右轮功率 = 直行功率 - 转弯功率
                T (目标点)
               ╱|
              ╱ |
            ╱   | Δy
          ╱     |
     O ─────────┘
     (当前位置) Δx

     α = atan2(Δx, Δy)     ← 目标方向
     误差角度 = α - 当前角度  ← 车头偏了多少
     距离 = √(Δx² + Δy²)   ← 还有多远

6.2 简化代码

C++
// 角度归一化到 [-180, 180]
double normalizeAngle(double angle) {
    while (angle > 180) angle -= 360;
    while (angle < -180) angle += 360;
    return angle;
}

void moveTo(double target_x, double target_y, int timeout_ms) {
    timer t;

    while (t.time(msec) < timeout_ms) {
        // 当前位置(从定位系统读取)
        double cur_x = pos_x;
        double cur_y = pos_y;
        double cur_angle = IMU.rotation(deg);

        // 计算误差
        double err_x = target_x - cur_x;
        double err_y = target_y - cur_y;
        double distance = sqrt(err_x * err_x + err_y * err_y);

        // 目标方向角度(度)
        double target_angle = atan2(err_x, err_y) / DEG_TO_RAD;

        // 角度误差,归一化防止转大圈
        double angle_error = normalizeAngle(target_angle - cur_angle);

        // 投影距离(沿车头方向还要走多远)
        double forward_error = distance * cos(angle_error * DEG_TO_RAD);

        // PID 计算功率(这里简化为 P 控制)
        double power_linear = forward_error * 2.0;   // kP = 2.0
        double power_turn = angle_error * 1.5;       // kP = 1.5

        // 限幅
        if (fabs(power_linear) > 100)
            power_linear = (power_linear > 0) ? 100 : -100;
        if (fabs(power_turn) > 100)
            power_turn = (power_turn > 0) ? 100 : -100;

        // 快到了就停止转弯(防止结尾抖动)
        if (distance < 10) power_turn = 0;

        // 分配给左右轮
        double left = power_linear + power_turn;
        double right = power_linear - power_turn;

        // 等比缩放,防止超过 100
        double max_power = fmax(fabs(left), fabs(right));
        if (max_power > 100) {
            left = left / max_power * 100;
            right = right / max_power * 100;
        }

        moveLeft(left);
        moveRight(right);

        // 到达退出
        if (distance < 3) break;

        task::delay(10);
    }

    // 停下
    moveLeft(0);
    moveRight(0);
}

为什么需要 normalizeAngle 假如机器人转了 350°,陀螺仪读数 350,目标方向 10°。不归一化的话 10 - 350 = -340,机器人会傻傻地转 340 度大圈。归一化后变成 +20,只转 20 度小圈。

6.3 使用方式

C++
void autonomous() {
    // 机器人从 (0, 0) 出发
    moveTo(60, 0, 3000);     // 往右走 60cm,3 秒超时
    moveTo(60, 60, 3000);    // 再往前走 60cm
    moveTo(0, 0, 5000);      // 回到起点
}
对比以前用 TimerForward(127, 1000) 凭时间走路 —— 现在是告诉机器人"去这个坐标",它自己算怎么走。精确度和灵活性完全不是一个量级。

深入 6.4 实战细节

上面的 moveTo 是最简版,实际使用中还需要注意几个问题:

功率比例问题:直行功率 400 + 转弯功率 300,如果直接加起来就是 700,但电机最大 100。不做等比缩放的话,(100, 100) 变成直走,转弯完全丢失。代码里的等比缩放就是解决这个问题。

起步时强制转弯:刚开始时距离很远,直行功率很大,容易把转弯功率盖过去。可以在距离远的时候给转弯功率一个最低值:

C++
if (distance > 10 && fabs(power_turn) < 60) {
    power_turn = (power_turn > 0) ? 60 : -60;
}

收尾时停止转弯:快到目标时 Δx 和 Δy 都很小,任何微小变化都会导致目标方向剧烈跳动,机器人会原地左右摇摆。所以接近目标时直接把转弯功率清零。

功率平滑(Slew Rate):功率突变会让机器人抖动,限制每次循环最大功率变化:

C++
// 每次循环功率变化不超过 10
float slew(float current, float last, float max_change) {
    float change = current - last;
    if (change > max_change) change = max_change;
    if (change < -max_change) change = -max_change;
    return last + change;
}
检查点 7
机器人转了 350 度,目标方向 10 度,不做角度归一化会怎样?

必学 七、常见问题

理解原理后,比赛建议用成熟的库(LemLib、EZ-Template、JAR-Template 等)。但理解原理能帮你调参和排查问题。

校准时机器人不能动。如果漂移严重,可以用两个竖轮的差来算角度(代替陀螺仪),但需要两个竖轮左右对称安装。

定位轮解决"我在哪",Pure Pursuit 解决"怎么沿曲线走"。Pure Pursuit 需要定位轮提供实时位置,然后计算应该往前方路径上哪个点开。像开车时眼睛看前方一段距离的路面,而不是盯着脚下。

Vehicle Path Tracking Using Pure Pursuit Controller
MATLAB · 11 分钟 · 英文 · Pure Pursuit 算法原理动画讲解

5ms(每秒 200 次)是实践中比较好的值。太慢会丢精度,太快传感器跟不上也没意义。注意:不要在定位线程里做其他事(打印、复杂计算),会拖慢频率。

这确实会造成微小误差(一个更新了另一个还没更新)。目前没有完美解决方案,但 5ms 的循环频率下这个误差很小,实践中可以接受。

这是累积误差 —— 每次循环的微小误差会叠加。2 分钟比赛后可能累积 5-10cm。常见缓解方法:

  • 墙面重置:让机器人靠到场地墙边,直接把对应坐标设为墙的已知位置
  • 起始位置校准:每次自动阶段开始前,在代码里设好起始坐标
  • 减少误差源:把安装做到极致(见第四节)

必学 八、练习

练习 1:理解坐标系(纸上练习)

机器人在 (0, 0),车头朝 y 轴正方向(0°)。

  1. 直走 50cm,现在坐标是?
  2. 右转 90°,再直走 30cm,现在坐标是?
  3. 左转 45°(现在朝向 45°),再直走 20cm,现在坐标大约是?
  1. (0, 50) —— 只沿 y 轴走了 50
  2. (30, 50) —— 右转 90° 后车头朝 x+,直走 30cm 只增加 x
  3. (30, 50) 出发,朝向 45°(东北方向),走 20cm:
    • Δx = 20 × sin(45°) = 20 × 0.707 ≈ 14.1
    • Δy = 20 × cos(45°) = 20 × 0.707 ≈ 14.1
    • 最终 ≈ (44.1, 64.1)

练习 2:补全代码

下面的 updatePosition() 函数缺了关键部分,补全它:

C++
void updatePosition() {
    double cur_v = VerticalEncoder.position(deg);
    double cur_h = HorizontalEncoder.position(deg);

    double arc_v = (cur_v - last_vertical)
                   * DEG_TO_RAD * (WHEEL_DIAMETER_V / 2.0);
    double arc_h = /* (A) */;

    double angle = IMU.rotation(deg) * DEG_TO_RAD;
    double delta_angle = /* (B) */;

    double chord_v, chord_h;

    if (/* (C) */) {
        chord_v = /* (D) */;
        chord_h = /* (E) */;
    } else {
        double radius_v = /* (F) */;
        double radius_h = /* (G) */;
        chord_v = 2 * radius_v * sin(delta_angle / 2.0);
        chord_h = 2 * radius_h * sin(delta_angle / 2.0);
    }

    double avg_angle = /* (H) */;

    pos_x += /* (I) */;
    pos_y += /* (J) */;

    last_vertical = cur_v;
    last_horizontal = cur_h;
    last_angle = angle;
}

提示:答案全在第三节的完整版代码里,但试着先自己想再对照。

点击查看答案
  • (A) (cur_h - last_horizontal) * DEG_TO_RAD * (WHEEL_DIAMETER_H / 2.0)
  • (B) angle - last_angle
  • (C) fabs(delta_angle) < ANGLE_THRESHOLD
  • (D) arc_v
  • (E) arc_h
  • (F) arc_v / delta_angle + OFFSET_V
  • (G) arc_h / delta_angle + OFFSET_H
  • (H) last_angle + delta_angle / 2.0
  • (I) chord_v * sin(avg_angle) + chord_h * cos(avg_angle)
  • (J) chord_v * cos(avg_angle) - chord_h * sin(avg_angle)

练习 3:找 Bug(调试练习)

下面 4 段代码各有一个 Bug,找出来并说明会导致什么问题:

Bug 1:

C++
// 编码器角度转距离
double arc_v = (cur_v - last_v) * (WHEEL_DIAMETER_V / 2.0);
点击查看答案

缺少 DEG_TO_RAD。编码器返回的是角度(度),直接乘半径得到的不是弧长。正确写法:(cur_v - last_v) * DEG_TO_RAD * (WHEEL_DIAMETER_V / 2.0)。不修的话,算出的距离会偏大约 57 倍(因为 1 rad ≈ 57.3°)。

Bug 2:

C++
// 判断是否转弯
if (delta_angle == 0) {
    chord_v = arc_v;
} else {
    double radius_v = arc_v / delta_angle + OFFSET_V;
    chord_v = 2 * radius_v * sin(delta_angle / 2.0);
}
点击查看答案

用了 == 0 而不是阈值。陀螺仪有噪声,delta_angle 几乎不会精确等于 0,所以几乎每次都走 else 分支。当 delta_angle 极小(比如 0.00001)时,arc_v / delta_angle 算出天文数字,坐标直接飞走。正确写法:fabs(delta_angle) < 0.001

Bug 3:

C++
void updatePosition() {
    double arc_v = (VerticalEncoder.position(deg) - last_vertical) * DEG_TO_RAD * (WHEEL_DIAMETER_V / 2.0);
    double angle = IMU.rotation(deg) * DEG_TO_RAD;
    double delta_angle = angle - last_angle;
    // ... 计算 chord_v, chord_h, 更新 pos_x, pos_y ...
    last_vertical = VerticalEncoder.position(deg);  // 注意这一行
    last_angle = angle;
}
点击查看答案

传感器读了两次。开头用 VerticalEncoder.position(deg) 算 arc_v,结尾又读了一次存到 last_vertical。两次读取之间传感器可能变了,导致下次循环的 arc_v 计算不准。正确做法:开头用局部变量存一次 double cur_v = VerticalEncoder.position(deg);,后面都用 cur_v

Bug 4:

C++
// 偏移补正
double radius_v = arc_v / delta_angle + OFFSET_V;
// OFFSET_V = -4.0(竖轮在旋转中心左边 4cm)
// 但实际安装时竖轮在旋转中心右边
点击查看答案

偏移量符号错了。竖轮在旋转中心右边,OFFSET_V 应该是正数(+4.0),不是 -4.0。符号写反会导致补正方向相反——原地转圈时坐标不但不归零,反而偏得更远。检查方法:让机器人原地转 10 圈,看坐标是否回到 (0,0) 附近。

练习 4:调参挑战(实机练习)

  1. 装好定位轮,运行定位程序
  2. 让机器人手动控制走一个正方形(每边 60cm),回到起点
  3. 看程序报告的坐标离 (0, 0) 差多少
  4. 目标:走完正方形后,x 和 y 误差都在 5cm 以内

深入 练习 5:写你自己的 MoveTo(分三步)

不要一口气写完整的 moveTo。按下面三步来,每步写完测试通过再进下一步:

第一步:写 turnTo(targetAngle)

第二步:写 driveForward(distance)

第三步:合并成 moveTo(x, y)

提示:每步都先在纸上画图 → 写伪代码 → 再写 C++。

检查点 8
机器人在 (0,0),朝 90 度走 30cm,到哪?x 坐标是多少?

参考资料

文档资料(可下载)
视频教程
在线资源
核心收获:定位轮让机器人从"凭感觉走"变成"看着地图走"。数学模型虽然看起来复杂,但核心就一句话 —— 每隔一小段时间,用传感器算出移动了多少,加到坐标上。剩下的都是让这个过程更准确的细节。