3. 编码器数据采集

大家好,我是你们的机器人工程师老张。今天我们来聊聊编码器数据采集这件事。

说实话,编码器这东西看着简单,但坑不少。我最早做机器人底盘时,就因为在编码器配置上偷了个懒,结果小车跑起来像个醉汉——左摇右晃,里程计数据完全没法用。后来花了整整两天排查,才发现是定时器模式没选对。

好,咱们从头捋一遍。

3.1 增量式编码器原理

增量式编码器,说白了就是一个「转了多少、往哪转」的传感器。它不像式编码器那样一上电就知道位置,但它便宜、精度高、响应快,是机器人底盘最常用的方案。

它的核心原理很简单:

  • A相和B相:两个相位相差90°的方波信号
  • 每转一圈:输出固定数量的脉冲(比如500线、1024线)
  • 方向判断:看A相和B相的先后顺序

你想想看,如果A相领先B相90°,那就是正转;反过来B相领先A相,那就是反转。这就是所谓的「正交编码信号」。

关键参数:

  • 线数(PPR):每转脉冲数,决定了角度分辨率
  • 最大响应频率:决定了你能测多快的转速
  • 输出类型:推挽、集电极开路、差分等

我在项目中遇到过一个问题:编码器线数选得太高,结果STM32的定时器计数溢出了。嗯,这个后面会细说。

3.2 STM32定时器编码器模式

STM32的定时器有个很贴心的功能——编码器模式。说白了,就是硬件帮你完成脉冲计数和方向判断,你只需要读寄存器就行。

我个人习惯用TIM3或TIM4来做编码器接口,因为它们有足够的通道。配置起来其实就几步:

// 编码器模式配置示例(基于HAL库)
void Encoder_Init(TIM_HandleTypeDef *htim)
{
    TIM_Encoder_InitTypeDef sEncoder = {0};
    
    // 配置编码器模式
    sEncoder.EncoderMode = TIM_ENCODERMODE_TI12;  // 双沿触发
    sEncoder.IC1Polarity = TIM_ICPOLARITY_RISING;
    sEncoder.IC2Polarity = TIM_ICPOLARITY_RISING;
    
    HAL_TIM_Encoder_Init(htim, &sEncoder);
    HAL_TIM_Encoder_Start(htim, TIM_CHANNEL_ALL);
}

这里有个细节:TIM_ENCODERMODE_TI12模式会在A相和B相的每个边沿都计数,相当于把分辨率翻了4倍。比如500线的编码器,用这个模式就能得到2000个计数/圈。

我的经验:

如果你用的是直流有刷电机+编码器,建议用TI12模式。但如果是步进电机,用TI1或TI2模式就够了,因为步进电机本身就有位置记忆。

3.3 脉冲计数与方向判断

配置好编码器模式后,STM32会自动处理计数和方向。你只需要关心两个寄存器:

  • CNT:当前计数值(16位或32位)
  • DIR:方向标志位(0正转,1反转)

但这里有个坑——CNT寄存器会溢出。16位定时器最大65535,32位定时器最大4294967295。如果你的机器人跑得久,计数值很容易溢出。

我曾经踩过的坑:

有一次做巡检机器人,连续跑了4个小时,编码器计数值溢出了3次。结果里程计数据直接跳变,定位全乱套了。后来我加了个溢出中断,每次溢出时记录一次,才解决问题。

正确的做法是:

// 读取编码器计数值(带溢出处理)
int32_t ReadEncoder(TIM_HandleTypeDef *htim)
{
    static int32_t last_count = 0;
    static int32_t total_count = 0;
    int32_t current_count = __HAL_TIM_GET_COUNTER(htim);
    int32_t delta = current_count - last_count;
    
    // 处理溢出
    if (delta > 32768) delta -= 65536;      // 正转溢出
    else if (delta < -32768) delta += 65536; // 反转溢出
    
    total_count += delta;
    last_count = current_count;
    
    return total_count;
}

方向判断其实不用你操心,硬件已经帮你做了。但如果你非要自己判断,可以读DIR位:

uint8_t direction = __HAL_TIM_IS_TIM_COUNTING_DOWN(htim) ? -1 : 1;

3.4 数据发布到ROS

好,现在编码器数据有了,怎么发给ROS?

ROS里常用的消息类型是nav_msgs/Odometry,但编码器数据通常先转成geometry_msgs/Twist或者自定义消息。我个人习惯用std_msgs/Int32MultiArray来发布原始脉冲数,然后在ROS节点里做转换。

为什么?因为这样更灵活。你可以在ROS端做滤波、标定、甚至切换不同的编码器模型。

发布代码示例(基于rosserial):

#include <ros.h>
#include <std_msgs/Int32MultiArray.h>

ros::NodeHandle nh;
std_msgs::Int32MultiArray encoder_msg;
ros::Publisher pub_encoder("encoder_ticks", &encoder_msg);

void setup()
{
    nh.initNode();
    nh.advertise(pub_encoder);
    
    // 初始化编码器数据数组
    encoder_msg.data_length = 2;
    encoder_msg.data = (int32_t*)malloc(2 * sizeof(int32_t));
}

void loop()
{
    // 读取左右轮编码器
    encoder_msg.data[0] = ReadEncoder(&htim3);  // 左轮
    encoder_msg.data[1] = ReadEncoder(&htim4);  // 右轮
    
    pub_encoder.publish(&encoder_msg);
    nh.spinOnce();
    
    delay(10);  // 100Hz发布
}

发布频率建议:

底盘编码器一般50-100Hz就够了。太快了浪费带宽,太慢了里程计更新跟不上。我通常用100Hz,配合IMU做融合时效果不错。

在ROS端,你可以写个节点订阅/encoder_ticks,然后转换成里程计数据。转换公式很简单:

// 脉冲数 -> 距离
float distance_per_tick = (2 * PI * wheel_radius) / (encoder_ppr * 4);
float left_distance = left_ticks * distance_per_tick;
float right_distance = right_ticks * distance_per_tick;

这里encoder_ppr * 4是因为我们用了TI12模式,分辨率翻了4倍。如果你用的是TI1或TI2模式,就不用乘4。

3.5 本章知识体系

下面这张图展示了编码器数据采集的完整链路:

编码器数据采集链路 物理层 增量式编码器 A相/B相/Z相 硬件层 STM32定时器 编码器模式 数据处理层 脉冲计数 方向判断/溢出处理 ROS 发布 关键参数对照表 参数 说明 建议值 编码器线数 每转脉冲数 500-1024线 定时器模式 TI12双沿触发 TI12 发布频率 数据发布到ROS 50-100Hz 溢出处理 防止计数溢出 必须实现

从这张图可以看到,编码器数据从物理层一路走到ROS层,每一层都有它的关键点。物理层选好编码器,硬件层配好定时器,数据处理层做好溢出处理,最后通过ROS发布出去。

嗯,这就是编码器数据采集的完整链路。下一节我们会讲如何把这些脉冲数据转换成真正的里程计信息——也就是机器人走了多远、转了多少角度。

本章要点回顾:

  • 增量式编码器通过A/B相相位差判断方向
  • STM32定时器编码器模式自动完成计数和方向判断
  • TI12模式将分辨率提升4倍
  • 必须处理计数器溢出问题
  • 通过rosserial以50-100Hz发布到ROS

可可精品资料库