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Ω终端电阻 | 普通排线即可 |
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);
}
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
- 编码器线数 ≤ 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次读数的平均值,适合速度环反馈。
4.5 本章知识体系总览
下面这张图,把编码器信号采集的完整链路画出来了。从编码器本体到控制器,每一层都有对应的处理手段。
这张图把整个链路串起来了。从编码器本体开始,经过差分接口、传输线缆、硬件滤波,最后进入计数单元。右侧的软件滤波和STM32内部滤波,是硬件处理之后的补充手段。多轴同步方案则是在计数实现层面,用FPGA解决STM32的瓶颈。