4、编码器信号采集:从STM32到FPGA的实战方案

编码器信号采集,说白了就是要把电机转子的位置和速度,从物理世界的脉冲信号,变成数字世界能读懂的数据。这一步要是做不好,后面什么FOC、什么伺服控制,全是空中楼阁。

我个人习惯把编码器采集分成三个层次来看:信号接口层(差分还是单端)、计数实现层(定时器还是FPGA)、抗干扰层(滤波与校验)。咱们一个一个聊。

4.1 差分信号处理:RS-422标准与实战

先说说差分信号。为什么工业现场几乎清一色用差分?

你想想看,电机运行时,驱动器IGBT开关会产生巨大的共模噪声。单端信号(比如5V TTL)在这种环境下,信号线上随便耦合个几伏的干扰,逻辑就翻车了。差分信号就不一样——它传的是两根线之间的电压差,共模噪声在两根线上同时出现,一相减就没了。

RS-422是编码器最常用的差分标准。我见过不少新手直接拿杜邦线接编码器,结果跑起来数据乱跳。嗯,这里要注意:RS-422必须用双绞屏蔽线,终端匹配电阻也不能省。

参数 RS-422差分 单端TTL
抗共模干扰 强(±7V共模范围) 弱
传输距离 可达1200米 通常<3米
典型接收器 AM26LS32、MAX3485 74HC14施密特
线缆要求 双绞屏蔽线+120Ω终端电阻 普通排线即可
我的经验:编码器A+/A-、B+/B-、Z+/Z-这六根线,一定要用独立双绞对。我曾经图省事把A+和B+放在同一对绞线里,结果相位串扰导致计数误差,查了两天才找到原因。

4.2 STM32定时器编码器模式:最常用的方案

STM32的定时器编码器模式,是我个人用得最多的方案。它不需要外部中断,硬件自动完成鉴相和计数,CPU几乎零开销。

原理其实很简单:定时器内部有个方向判别逻辑,根据A、B两相的相位关系,自动决定计数器是加还是减。你只需要配置好模式,然后读CNT寄存器就行。

// STM32定时器编码器模式配置示例(TIM3)
void Encoder_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    TIM_Encoder_InitTypeDef EncoderConfig = {0};

    // 1. 使能时钟
    __HAL_RCC_TIM3_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    // 2. 配置PA6(TIM3_CH1)、PA7(TIM3_CH2)为复用功能
    GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    GPIO_InitStruct.Alternate = GPIO_AF2_TIM3;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 3. 配置编码器模式:TI1和TI2都计数,4倍频
    EncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12;
    EncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING;
    EncoderConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI;
    EncoderConfig.IC1Prescaler = TIM_ICPSC_DIV1;
    EncoderConfig.IC1Filter = 0x0F;  // 硬件滤波,后面会讲
    EncoderConfig.IC2Polarity = TIM_ICPOLARITY_RISING;
    EncoderConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI;
    EncoderConfig.IC2Prescaler = TIM_ICPSC_DIV1;
    EncoderConfig.IC2Filter = 0x0F;

    HAL_TIM_Encoder_Init(&htim3, &EncoderConfig);
    HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
}
避坑指南:我曾经在配置编码器模式时,忘了设置GPIO的上下拉。结果编码器悬空时,引脚电平不定,计数器乱跳。建议:编码器输入引脚配置为上拉输入,或者外部加上拉电阻到3.3V。

4.3 FPGA高速计数方案:当STM32不够用时

什么时候需要上FPGA?我遇到过两个典型场景:

  • 超高分辨率编码器:比如每圈100万线的光学编码器,STM32定时器最高也就72MHz或168MHz,根本吃不下这么高的脉冲频率。
  • 多轴同步:同时采集8个、16个编码器,STM32的定时器数量有限,而且中断处理会互相干扰。

FPGA方案的核心思路是:用硬件描述语言实现一个正交解码计数器,把A、B相的逻辑状态直接映射成计数方向。

// Verilog:简单的正交解码与计数模块
module quadrature_decoder (
    input  wire clk,          // 系统时钟(比如50MHz)
    input  wire rst_n,        // 复位
    input  wire A,            // 编码器A相
    input  wire B,            // 编码器B相
    output reg [31:0] count   // 位置计数值
);

    reg A_dly, B_dly;         // 延迟一拍,用于边沿检测
    wire A_rise, A_fall;
    wire B_rise, B_fall;

    // 边沿检测
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            A_dly <= 1'b0;
            B_dly <= 1'b0;
        end else begin
            A_dly <= A;
            B_dly <= B;
        end
    end

    assign A_rise = A & ~A_dly;
    assign A_fall = ~A & A_dly;
    assign B_rise = B & ~B_dly;
    assign B_fall = ~B & B_dly;

    // 方向判别与计数
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 32'd0;
        else begin
            if (A_rise & B)      // A上升沿时B为高 → 正转
                count <= count + 1'b1;
            else if (A_rise & ~B) // A上升沿时B为低 → 反转
                count <= count - 1'b1;
            else if (A_fall & ~B) // A下降沿时B为低 → 正转
                count <= count + 1'b1;
            else if (A_fall & B)  // A下降沿时B为高 → 反转
                count <= count - 1'b1;
            // B相边沿同理,这里省略
        end
    end

endmodule
FPGA vs STM32 选型建议:
  • 编码器线数 ≤ 5000线,转速 ≤ 3000rpm → STM32定时器模式完全够用
  • 编码器线数 ≥ 10000线,或需要多轴同步 → 上FPGA
  • 如果既想灵活又想低成本,可以考虑STM32+CPLD的组合方案

4.4 抗干扰滤波设计:让数据不再"抖"

信号采集最头疼的问题就是抖动。我调试过一台伺服驱动器,电机静止时编码器读数在±3个脉冲之间跳,查到最后发现是电机线缆和编码器线缆绑在一起走的。

抗干扰滤波,我一般分三个层面来做:

4.4.1 硬件滤波

  • RC低通滤波:在编码器信号进入MCU之前,串一个100Ω电阻+100pF电容到地,截止频率约16MHz,能滤掉大部分高频噪声。
  • 施密特触发器:比如74HC14,能把缓慢变化的边沿整形成陡峭的方波。我习惯在长距离传输后加一级施密特。
  • 光耦隔离:如果编码器和控制器之间电位差大,或者有浪涌风险,必须用高速光耦(比如6N137)做电气隔离。

4.4.2 STM32内部滤波

STM32定时器的输入捕获自带数字滤波器。配置时设置ICxF参数,原理是:连续采样N个点,如果N个点电平一致,才认为信号有效。

// 滤波参数配置示例
// IC1Filter = 0x0F 表示连续采样8个时钟周期,8次一致才更新
// 时钟频率 = TIMxCLK / (IC1PSC+1) = 72MHz / 1 = 72MHz
// 采样周期 = 8 * (1/72MHz) ≈ 111ns
// 能滤掉宽度小于111ns的毛刺

htim3.Instance->CCMR1 |= TIM_CCMR1_IC1F_0 | TIM_CCMR1_IC1F_1 | TIM_CCMR1_IC1F_2 | TIM_CCMR1_IC1F_3;
// 等效于 IC1Filter = 0x0F

4.4.3 软件滤波

硬件滤波之后,如果还有零星跳变,我会在软件里做最后一道防线:

  • 限幅滤波:相邻两次采样值之差超过最大速度对应的脉冲数,就丢弃本次值。
  • 中值滤波:连续采5次,排序取中间值。适合电机低速运行时的稳态读数。
  • 滑动平均:取最近N次读数的平均值,适合速度环反馈。
我的经验:软件滤波不要做得太重。有一次我把滑动窗口设到32次,结果位置响应延迟了十几毫秒,电机一加速就振荡。后来改成4次滑动+限幅,效果就好多了。滤波的度,得根据你的控制周期来调。

4.5 本章知识体系总览

下面这张图,把编码器信号采集的完整链路画出来了。从编码器本体到控制器,每一层都有对应的处理手段。

编码器信号采集知识体系 编码器本体 增量式 / 绝对式 / 正余弦 信号接口层 RS-422差分 / 单端TTL 传输线缆 双绞屏蔽 / 终端匹配 硬件滤波 RC滤波 / 施密特 / 光耦 计数实现 STM32定时器 / FPGA 软件滤波 限幅 / 中值 / 滑动平均 STM32内部滤波 ICxF数字滤波器 多轴同步方案 FPGA并行采集 核心原则 • 差分传输是抗干扰的基础 • 滤波要分层:硬件→内部→软件 • 选型看线数×转速的乘积 常见误区 • 忽略终端匹配电阻 • 软件滤波窗口过大导致滞后

这张图把整个链路串起来了。从编码器本体开始,经过差分接口、传输线缆、硬件滤波,最后进入计数单元。右侧的软件滤波和STM32内部滤波,是硬件处理之后的补充手段。多轴同步方案则是在计数实现层面,用FPGA解决STM32的瓶颈。