第3章:编码器与反馈——伺服系统的“眼睛”
做伺服驱动这么多年,我越来越觉得,编码器就是整个系统的眼睛。你算法写得再漂亮,电流环调得再顺,如果反馈回来的位置数据是错的,那一切都是白搭。今天咱们就好好聊聊编码器这回事。
3.1 编码器类型:增量式 vs 绝对式
先说说最基础的问题。编码器分两大类:增量式和绝对式。这俩有啥区别?说白了,一个只知道“动了多少”,一个知道“在哪儿”。
增量式编码器
增量式编码器输出的是脉冲信号。电机一转,它就咔咔往外蹦脉冲。你通过数脉冲个数,就知道电机转了多少角度。
优点:
- 结构简单,成本低
- 响应速度快
- 分辨率可以做得比较高
缺点:
- 断电后位置丢失
- 上电需要找零位(回零操作)
- 抗干扰能力相对弱一些
绝对式编码器
绝对式编码器就高级多了。它每个位置都有一个唯一的编码值。你读到的数据直接就是当前位置,不需要累计,不需要回零。
优点:
- 断电不丢位置
- 上电即用,无需回零
- 抗干扰能力强
缺点:
- 价格贵
- 接口复杂
- 多圈绝对式编码器有机械齿轮,寿命有限
3.2 编码器分辨率与精度
这两个概念经常被混淆。我见过不少工程师把分辨率当精度用,结果系统怎么调都调不好。
分辨率 是编码器能分辨的最小角度变化。比如一个17位的编码器,分辨率就是 360° / 2^17 ≈ 0.0027°。说白了,就是它能“看到”多小的变化。
精度 是编码器实际输出值和真实值之间的误差。这个受机械安装、码盘刻划误差、电子噪声等因素影响。
| 参数 | 分辨率 | 精度 |
|---|---|---|
| 含义 | 能分辨的最小变化 | 实际值与真实值的偏差 |
| 单位 | 位(bit)或线数(CPR) | 角秒(arcsec)或角度 |
| 影响因素 | 码盘刻线数、电子细分 | 机械安装、码盘质量、温度 |
| 典型值 | 17位、23位 | ±20角秒、±5角秒 |
3.3 编码器接口:ABZ、SSI、BiSS
接口这块,我把它分成三类来说。每种接口都有自己的脾气。
ABZ 增量式接口
这是最经典的接口。A相和B相相差90°电角度,用来判断方向。Z相是零位信号,每转一圈出一个脉冲。
读取方式很简单:用定时器捕获或者外部中断计数。我一般用STM32的定时器编码器模式,硬件自动处理,省心。
// STM32 定时器编码器模式配置示例
void Encoder_Init(void)
{
TIM_EncoderInterfaceConfigTypeDef sEncoderConfig;
sEncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12; // 双沿触发
sEncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING;
sEncoderConfig.IC2Polarity = TIM_ICPOLARITY_RISING;
HAL_TIM_Encoder_Init(&htim3, &sEncoderConfig);
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
}
SSI 同步串行接口
SSI是绝对式编码器常用的接口。它用时钟线和数据线,主设备发时钟,编码器在时钟的驱动下逐位输出数据。
SSI的时序其实很简单:
- 主设备拉低时钟线,准备开始
- 时钟线产生一串脉冲(比如25个,对应25位数据)
- 编码器在每个时钟上升沿输出一位数据
- 传输完成后,时钟线保持高电平一段时间(叫“单稳态时间”)
// SSI 读取示例(伪代码)
uint32_t SSI_Read(void)
{
uint32_t data = 0;
// 拉低时钟,开始传输
CLK_LOW();
delay_us(1);
for(int i = 0; i < 25; i++)
{
CLK_HIGH();
delay_us(0.5);
data <<= 1;
if(DATA_READ() == HIGH)
data |= 0x01;
CLK_LOW();
delay_us(0.5);
}
// 等待单稳态时间
delay_us(20);
return data;
}
BiSS 双向同步串行接口
BiSS是这几年比较火的接口,可以理解为SSI的升级版。它支持双向通信,不仅能读位置,还能写参数、读诊断信息。
BiSS的时序和SSI类似,但多了几个特点:
- 支持CRC校验,数据可靠性高
- 支持多编码器级联
- 传输速率高,可达10MHz以上
- 有寄存器读写功能
我个人比较喜欢BiSS,尤其是做高精度伺服的时候。CRC校验能帮你省掉很多排查故障的时间。有一次系统偶尔报位置错误,查了三天没找到原因。后来发现是编码器线缆有轻微破损,BiSS的CRC校验直接报错,一下子就定位到了。
3.4 编码器数据读取与处理
数据读回来了,怎么用?这里有几个关键点。
数据滤波
编码器数据难免有噪声。尤其是增量式,一个毛刺就能让位置跳一大截。我常用的滤波方法:
- 限幅滤波: 相邻两次位置变化超过阈值就丢弃
- 滑动平均: 取最近N个值的平均
- 中值滤波: 取中间值,适合去除脉冲噪声
// 限幅滤波示例
#define MAX_DELTA 100 // 最大允许变化量
int32_t Position_Filter(int32_t new_pos, int32_t last_pos)
{
int32_t delta = new_pos - last_pos;
if(abs(delta) > MAX_DELTA)
{
// 变化太大,认为是噪声,返回上次值
return last_pos;
}
return new_pos;
}
速度计算
位置数据有了,速度怎么算?最直接的方法就是差分:
// M法测速:计算单位时间内的脉冲数
float Speed_M_Method(int32_t pos_now, int32_t pos_last, float dt)
{
float speed = (float)(pos_now - pos_last) / dt;
return speed;
}
但这里有个坑。如果dt太小,差分结果会放大噪声。我一般会加一个低通滤波器:
// 一阶低通滤波
float Speed_LPF(float speed_raw, float speed_last, float alpha)
{
// alpha 取 0.1~0.3 比较合适
return alpha * speed_raw + (1 - alpha) * speed_last;
}
多圈处理
用增量式编码器做位置控制,一定要处理多圈溢出。比如一个16位的编码器,一圈65536个脉冲。转两圈就溢出了。你得在软件里维护一个“圈数计数器”。
// 多圈位置处理
int32_t g_total_position = 0;
int16_t g_last_raw = 0;
void Position_Update(int16_t raw_value)
{
int16_t delta = raw_value - g_last_raw;
// 处理溢出
if(delta > 32767)
delta -= 65536;
else if(delta < -32768)
delta += 65536;
g_total_position += delta;
g_last_raw = raw_value;
}
嗯,今天就先聊到这儿。下一章咱们讲电流环,那才是真正考验算法功底的地方。