第4章:里程计传感器接入

各位同学,今天我们来聊聊轮式里程计。说实话,里程计在导航系统里就像人的眼睛——虽然不完美,但没了它,机器人基本就是个瞎子。我做了这么多年导航,踩过最多的坑,就是里程计不准导致的定位漂移。

4.1 轮式里程计原理

轮式里程计,说白了就是通过轮子转了多少圈,来推算机器人走了多远。听起来简单,但实际做起来门道不少。

核心公式其实就一个:

位移 = 轮子周长 × 编码器脉冲数 / 每圈脉冲数

嗯,这里要注意——轮子周长不是一成不变的。轮胎气压、负载大小都会影响实际滚动半径。我在项目里遇到过,空载时里程计挺准,一装上负载就偏了10%以上。

对于差速驱动机器人,我们通常用这样的模型:

左轮速度 vL = (2πR × ΔN_L) / (Δt × PPR)
右轮速度 vR = (2πR × ΔN_R) / (Δt × PPR)

机器人线速度 v = (vL + vR) / 2
机器人角速度 ω = (vR - vL) / L

其中L是轮距,PPR是编码器每圈脉冲数。这个模型虽然简单,但够用。我见过有人用更复杂的模型,结果反而因为参数太多调不准。

关键点:里程计本质上是积分过程,误差会累积。所以它只适合短距离定位,长距离必须配合其他传感器。

4.2 编码器驱动实现

编码器驱动,我建议直接在ROS2的驱动层实现。为什么?因为这样能保证时间戳的准确性。时间戳不准,里程计就没法用。

下面是我常用的编码器驱动框架:

class WheelEncoderNode : public rclcpp::Node
{
public:
  WheelEncoderNode() : Node("wheel_encoder_node")
  {
    // 初始化编码器接口
    encoder_left_ = std::make_unique<Encoder>(LEFT_WHEEL_PIN);
    encoder_right_ = std::make_unique<Encoder>(RIGHT_WHEEL_PIN);
    
    // 创建里程计发布者
    odom_pub_ = this->create_publisher<nav_msgs::msg::Odometry>("odom", 10);
    
    // 定时器,10ms周期读取编码器
    timer_ = this->create_wall_timer(
      10ms, std::bind(&WheelEncoderNode::update_odometry, this));
  }

private:
  void update_odometry()
  {
    // 读取编码器脉冲数
    int32_t left_pulses = encoder_left_->read();
    int32_t right_pulses = encoder_right_->read();
    
    // 计算位移和角度变化
    double delta_left = (left_pulses / PPR_) * WHEEL_CIRCUMFERENCE_;
    double delta_right = (right_pulses / PPR_) * WHEEL_CIRCUMFERENCE_;
    
    double delta_s = (delta_left + delta_right) / 2.0;
    double delta_theta = (delta_right - delta_left) / WHEEL_BASE_;
    
    // 更新位姿
    x_ += delta_s * cos(theta_ + delta_theta / 2.0);
    y_ += delta_s * sin(theta_ + delta_theta / 2.0);
    theta_ += delta_theta;
    
    // 发布里程计消息
    auto odom_msg = nav_msgs::msg::Odometry();
    odom_msg.header.stamp = this->now();
    odom_msg.header.frame_id = "odom";
    odom_msg.child_frame_id = "base_footprint";
    
    odom_msg.pose.pose.position.x = x_;
    odom_msg.pose.pose.position.y = y_;
    odom_msg.pose.pose.orientation = createQuaternionFromYaw(theta_);
    
    // 设置协方差
    setCovariance(odom_msg);
    
    odom_pub_->publish(odom_msg);
  }
};

这段代码我用了很多年,基本没大改过。唯一要注意的是,编码器读取要用硬件中断,别用轮询。我曾经在树莓派上用轮询,结果CPU占用率直接飙到80%。

个人经验:编码器驱动里,一定要加防抖处理。机械振动会导致编码器产生毛刺脉冲,不加防抖的话,里程计会莫名其妙地漂移。

4.3 里程计协方差配置

协方差配置,这是很多人忽略的地方。说白了,协方差就是告诉滤波器:「我这个里程计有多可信」。

在ROS2的Odometry消息里,协方差是一个6×6的矩阵:

# 协方差矩阵排列顺序
# [x, y, z, roll, pitch, yaw]
# 对于轮式机器人,我们主要关注:
# x方向位置误差、y方向位置误差、yaw角度误差

odom_msg.pose.covariance = {
  // x方向协方差
  x_cov, 0.0, 0.0, 0.0, 0.0, 0.0,
  // y方向协方差
  0.0, y_cov, 0.0, 0.0, 0.0, 0.0,
  // z方向(固定为0)
  0.0, 0.0, 1e-9, 0.0, 0.0, 0.0,
  // roll(固定为0)
  0.0, 0.0, 0.0, 1e-9, 0.0, 0.0,
  // pitch(固定为0)
  0.0, 0.0, 0.0, 0.0, 1e-9, 0.0,
  // yaw角度协方差
  0.0, 0.0, 0.0, 0.0, 0.0, yaw_cov
};

协方差值怎么设?我一般这样估算:

误差来源 典型协方差值 说明
编码器量化误差 0.01 ~ 0.05 取决于编码器分辨率
轮子打滑 0.1 ~ 0.5 地面越滑,值越大
轮距误差 0.05 ~ 0.2 安装误差导致
轮胎变形 0.02 ~ 0.1 负载变化引起

避坑指南:我曾经把协方差设得太小,结果EKF完全信任里程计,IMU的数据基本被忽略。机器人一打滑,定位就飞了。后来我把协方差调大了3倍,效果好了很多。

实际调试时,我建议这样做:

  1. 先让机器人走直线,看x方向误差
  2. 再让机器人原地转圈,看yaw方向误差
  3. 根据实测数据调整协方差
  4. 最后在真实场景里跑一遍,微调参数

嗯,这里有个小技巧——协方差不是一成不变的。速度越快,误差越大。我习惯根据当前速度动态调整协方差:

double speed = sqrt(vx * vx + vy * vy);
double base_cov = 0.05;
double speed_factor = 1.0 + speed * 0.1;  // 速度越快,协方差越大

x_cov = base_cov * speed_factor;
y_cov = base_cov * speed_factor;
yaw_cov = 0.1 * speed_factor;

这样处理之后,机器人在低速时定位更稳,高速时也不会因为过度信任里程计而出问题。

总结一下:里程计接入看似简单,但要做好不容易。原理要懂,驱动要稳,协方差要调。这三样都做到位了,你的导航系统就成功了一半。

轮式里程计接入知识体系 轮式里程计原理 编码器驱动实现 协方差配置 差速模型:v = (vL+vR)/2 编码器脉冲 → 位移转换 积分累积误差特性 硬件中断读取编码器 ROS2定时器驱动框架 防抖处理与时间戳 6×6协方差矩阵结构 误差来源与典型值 动态协方差调整 输出:可靠的 /odom 话题 三个模块缺一不可,共同决定里程计质量

好了,里程计这块就讲这么多。记住,理论再漂亮,不如实际跑一圈。回去把代码写出来,在真实机器人上试试,你会发现很多书上没写的问题。

可可精品资料库