前阵子给一套伺服驱动器换位置反馈方案,从 AS5047P 换到 MT6816,理由很直接:成本更低、现货也更稳。结果电机一挂上去跑开环,电流波形明显比原来毛躁,低速还有周期性的噪声。一开始怀疑是驱动参数没调好,查了一圈,把 MT6816 原始角度和光栅尺对着读,才发现这颗芯片的原始角度存在明显的非线性误差——同一个机械角度,转到不同扇区,误差能差出去零点几度。对伺服这种要跑电流环、速度环的应用,这零点几度最后都会变成电流谐波和力矩波动。于是花了几天时间,把 MT6816 的非线性校准从头做了一遍。这篇文章把整个从原理到代码的过程完整复盘一遍:误差怎么来、标定台怎么搭、查找表怎么生成、STM32 上怎么查表补偿,以及我在这个过程中踩过的坑。适合正在做电机控制、机器人关节、云台反馈,或者刚从光编码器切到磁编码器的朋友参考。
1. 先弄清楚误差从哪里来,再去想怎么校准
1.1 磁编码器内部在做什么
MT6816 属于磁旋转编码器,内部是一组磁场传感单元,一般基于磁阻或霍尔阵列。安装时,旋转轴上贴一颗径向充磁的磁铁,芯片固定在轴端正上方或者侧面。电机转轴旋转时,磁铁产生的磁场方向跟着转,芯片内部各个传感单元的输出电压随磁场角度呈周期性变化,再由片内电路算出反正切,最终输出一个 0°~360° 的角度值。MT6816 是 14 位分辨率,也就是一圈被分成 16384 个 LSB,每个 LSB 对应 360/16384 ≈ 0.022°。
这里有个容易忽略的点:芯片输出的角度本质上是"磁场方向的角度",而不是"机械转轴的实际角度"。磁场方向是否和机械轴严格同步,取决于磁铁安装、磁场质量和芯片位置。只要磁场相对于芯片的旋转不是理想均匀的,输出角度就会偏离真实机械角度,这就是非线性误差的来源。
1.2 误差的主要贡献来源
从工程实测来看,MT6816 的非线性误差不是随机噪声,而是典型的系统误差。也就是说,你把轴转到某一个固定角度,每次读出来的误差基本是同一个值,重复性往往不错;但是换一个角度,误差可能变成另一样。这种"跟着角度走、每圈重复出现"的误差,非常适合用校准表来补偿。
误差主要来自几个地方:
- 磁铁安装偏心。磁铁中心没有和转轴轴心重合,磁场旋转中心偏移,产生的误差大体上是 1 次谐波,也就是一转一个周期的正弦形状。
- 磁铁轴向倾斜或者芯片与磁铁间隙不一致。磁场在传感平面的投影会变成椭圆,典型表现是 2 次谐波。
- 磁铁本身充磁不均匀,或者磁铁内部存在瑕疵。这会带来更高次谐波,甚至局部毛刺。
- 外部杂散磁场干扰,尤其是附近有大电流母排、线圈或者铁磁物体移动时。
- 温度变化。磁铁剩磁和芯片灵敏度都会随温度漂移,这部分误差在不同温度下会变。
1.3 误差对控制系统的影响
在位置环里,误差直接表现为定位不准;在速度环里,轴匀速旋转时编码器读数不是匀速增长的,速度反馈会出现周期性波动;在电流环/FOC 里,电角度有误差意味着 dq 变换角度偏移,电流谐波变大,电机低速时能听到周期性噪声,力矩也不再平滑。
如果一个 20 对极的电机,机械角度误差 0.3°,映射到电角度就是 6° 电角度,这个偏差在 FOC 里已经不小了。所以在做伺服级应用时,把 MT6816 的非线性校准做掉,不是锦上添花,而是必要步骤。
2. 校准方案的取舍:查找表是主干,谐波补偿是延伸
2.1 为什么"可以校准"
这里先建立一个概念:MT6816 的误差是系统误差,也就是重复出现的部分远大于随机噪声。系统误差可以用标定的方式建立映射关系,然后在运行中反向补偿。剩下的随机噪声才真正决定这个编码器能做到的最终测量极限。
所以我做校准的第一步是评估重复性。固定一个角度,连续读几千次,看读数的标准差。如果 RMS 噪声在 1~2 个 LSB 以内,这个芯片就值得做校准表;如果噪声本身就飘到十几个 LSB,那再校线性误差也没有意义。实测下来,MT6816 在静态位置的读数稳定性是够用的,这也是它能做伺服反馈的前提。
2.2 误差曲线可以拆成谐波
把一圈内每个角度的误差画成曲线,你会看到它并不是乱糟糟的,而是有明显的规律性。对误差序列做一次频域分解,通常会看到这些成分:
- 1 次谐波:主要来自磁铁偏心和轴心偏离,幅度往往最大。
- 2 次谐波:来自椭圆磁场、芯片平面与磁铁不平行。
- 3 次及更高次谐波:来自磁铁磁化不均匀,幅度通常依次下降。
因此,理论上有两种补偿路线:一种是用几个谐波系数在运行时实时计算补偿值,另一种是直接记录一张误差查找表。谐波补偿参数少、存储小,但对非谐波的毛刺无能为力。查找表则不管误差长什么样都能补偿,存储占用稍大,换来的效果更彻底。
2.3 查找表与谐波补偿对比
| 维度 | 查找表 | 谐波补偿 |
|---|---|---|
| 校准速度 | 需要逐点采一圈,工作量较大 | 采集少量点后拟合参数即可 |
| 存储占用 | 512~1024 个 int16,约 1~2 KB Flash | 两三个系数,可忽略 |
| 对高次谐波/毛刺 | 能完整补偿 | 基本无能为力 |
| 运行期计算量 | 查表加一次插值,很小 | 需要算多次 sin/cos,成本高得多 |
| 调试直观性 | 表可以直接画出来检查 | 系数不直观,出错不好定位 |
现实中绝大部分项目,我会直接选查找表。谐波补偿更适合没有标定台、手工转几圈采几十个点就想快速降误差的场景,或者做理论分析时用。
2.4 我的选择
我最终采用的是"1024 项 int16 误差表 + 线性插值"。理由很简单:1024 项每项之间对应 360°/1024≈0.35°的区间,插值后表项引起的残差远小于编码器本身的噪声;存储只要 2 KB,对 STM32 来说毫无压力;线性插值在定点环境下只有几次乘加,控制环里调用非常快。
如果你 Flash 紧张,512 项也能用。再少就不建议了,因为误差曲线的高次谐波在局部可能变化比较快,表太稀疏,插值残差会被拉高。
3. 标定台怎么搭,数据怎么采才靠谱
3.1 基准源精度决定校准上限
校准这件事的本质,是把 MT6816 的误差向一个更高精度的基准对齐。基准如果不准,MT6816 的误差会被"校正"成基准的误差。所以基准编码器/光栅尺的精度,至少要比目标精度高 5~10 倍。比如你想把校准后做到 0.05°以内,基准最好能到 0.005°~0.01°或者更好。
我用的是一个带高精度圆光栅的转台,配一套绝对角度读取模块。如果你没有这种条件,也可以用手分度转台配合高精度磁栅尺,但每一步都要确认重复定位精度。用步进电机开环带动来凑合标定,原则上不推荐,除非你的目标精度只有 0.1°量级。
3.2 装配和预处理
标定之前,先尽量把机械装好。磁铁偏心是误差的头号来源,先调偏心,再谈校准。我自己用的办法:轴端打表测磁铁外圆跳动,控制到 0.03mm 以内;轴向间隙按数据手册推荐的中间值装。装配越差,校准表越难兜底,而且高次谐波和局部毛刺会变大,查找表规格被迫加大。
另外确认磁铁充磁方向正确。如果磁铁贴反或者充磁方向偏了,角度输出可能翻转或者出现奇怪的分段错误,这类问题校准表也救不了。装好后先跑一整圈,看一眼原始角度曲线是否平滑、有没有突变。
3.3 数据采集:等间隔定位 + 多次平均
我的采集流程如下:转台每转到一个预定角度,停下稳定片刻,然后用 STM32 同一时刻读取 MT6816 原始角度和基准编码器角度,每个点连续采 64 次取平均,用来把芯片本身的读数噪声压掉。一圈从 0°到 360°,按 1024 个位置采集。
核心伪代码如下:
#define LUT_SIZE 1024u #define SAMPLE_NUM 64u int16_t err_lut[LUT_SIZE]; for (uint16_t i = 0; i < LUT_SIZE; i++) { float target_angle = 360.0f * i / LUT_SIZE; rotate_stage_to(target_angle); // 转台转到目标角度 wait_for_stable(); // 等待机械稳定 float err_sum = 0.0f; for (uint16_t j = 0; j < SAMPLE_NUM; j++) { uint16_t raw_lsb = mt6816_read_raw_angle(); // 0~16383 float ref_deg = read_ref_angle(); // 基准角度,单位° float raw_deg = raw_lsb * 360.0f / 16384.0f; float delta = wrap_pi(ref_deg - raw_deg); // 折到[-180,180] err_sum += delta; } err_lut[i] = (int16_t)(err_sum / SAMPLE_NUM * 16384.0f / 360.0f); }这里wrap_pi非常重要,它的作用是把误差差值折到 [-180°, +180°],避免跨零点时出现"基准是 359.9°,原始是 0.1°,直接相减得到 359.8°"这种离谱结果:
float wrap_pi(float deg) { deg = fmodf(deg + 540.0f, 360.0f) - 180.0f; return deg; }3.4 从采集数据到最终 LUT
上面的伪代码假设转台逐点定位后,MT6816 原始读数就落在第i个分区附近。严格来说,因为 MT6816 自身有误差,raw_lsb不会刚刚好等于i * 16,而是会偏移十几个 LSB。比较严谨的上位机处理方式是:把整圈 16384 LSB 按每 16 LSB 一个分区,把每个原始读数按raw_lsb / 16归到对应分区,对每个分区内的误差取平均,最后生成 1024 项表。
如果某些分区因为采样点分布原因没有数据,用相邻分区线性插值补齐。上位机脚本的大致逻辑是这样的:
import numpy as np raw_arr = [] # 所有采样点的 MT6816 原始角度 ref_arr = [] # 所有采样点的基准角度换算成 LSB,0~16383 err_arr = np.array(ref_arr) - np.array(raw_arr) LUT_SIZE = 1024 STEP = 16384 // LUT_SIZE # 16 # 每个分区收集误差 buckets = [[] for _ in range(LUT_SIZE)] for raw, err in zip(raw_arr, err_arr): idx = int(raw) // STEP buckets[idx].append(err) err_lut = [] for b in buckets: if len(b) > 0: err_lut.append(int(round(np.mean(b)))) else: err_lut.append(None) # 空分区线性插值 ...生成后还有一个必须检查的点:err_lut[0]和err_lut[LUT_SIZE-1]的差不能太大。如果差值超过 3~5 个 LSB,说明跨零点附近的误差不连续,可能是标定数据在 0°附近采样有问题,也可能是装配畸变更严重,这时候做出来的表在跨零处插值会有跳变,需要回头补采零点附近的数据。
4. STM32 端实现:SPI 读数与查表补偿
4.1 SPI 初始化,先求稳再求快
MT6816 通过 SPI 读角度,示例代码基于 HAL 库,SPI 配置成主机模式。第一版建议不要追求高速,先按 2~4 MHz 左右调通,再逐步提高。很多"角度读出来是乱的"问题,其实只是 SPI 速率太高或者线序不对。
hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; // 72MHz/16=4.5MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; if (HAL_SPI_Init(&hspi1) != HAL_OK) { Error_Handler(); }SPI 模式通常对应模式 0(CPOL=0, CPHA=0)。如果你用的是某些特殊封装的 MT6816 或者定制 OTP 版本,读不到数据时可以试试把 CPOL/CPHA 切到模式 1。
4.2 读取 14 位原始角度
MT6816 的 SPI 读角度是一帧两个字节,CS 拉低后直接收数。第一字节高 2 位是状态或校验位,低 6 位是角度的高 6 位;第二字节是角度的低 8 位。合起来是 14 位角度值。
#define MT6816_CS_LOW() HAL_GPIO_WritePin(MT6816_CS_GPIO_Port, MT6816_CS_Pin, GPIO_PIN_RESET) #define MT6816_CS_HIGH() HAL_GPIO_WritePin(MT6816_CS_GPIO_Port, MT6816_CS_Pin, GPIO_PIN_SET) uint16_t MT6816_ReadRaw(void) { uint8_t tx[2] = {0x00, 0x00}; uint8_t rx[2] = {0x00, 0x00}; uint16_t angle = 0; MT6816_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 10); MT6816_CS_HIGH(); // 按我手头这颗料的帧格式:第一字节低6位为角度高6位,第二字节为角度低8位 angle = ((uint16_t)(rx[0] & 0x3F) << 8) | rx[1]; return angle & 0x3FFF; }如果你的驱动读出来总在 0~1023 或者 0~255 范围内,大概率是这个帧格式里的掩码位取错了。拿逻辑分析仪抓一下 MISO 数据,对照手册确认角度位的位置,不要凭感觉试。
4.3 查表与线性插值
误差表直接定义成const int16_t数组,放在 Flash 里。每项表示该分区起点处需要的补偿量,单位是 LSB。比如err_lut[i] = 14,表示当原始读数落在第i个分区起点附近时,真实角度大约比原始读数大 14 个 LSB。
#define LUT_SIZE 1024u #define LUT_FRAC_BITS 4u // 16384 / 1024 = 16,所以 raw 的低4位是插值比例 #define LUT_FRAC_MASK 0x0Fu #define ANGLE_MAX 16384u // 由标定脚本生成的误差表,单位:LSB const int16_t err_lut[LUT_SIZE] = { // ... 标定生成后填入 }; uint16_t MT6816_GetCompensatedRaw(uint16_t raw) { uint16_t idx = raw >> LUT_FRAC_BITS; uint16_t frac = raw & LUT_FRAC_MASK; uint16_t nextIdx = (idx + 1 < LUT_SIZE) ? (idx + 1) : 0; int32_t diff = (int32_t)err_lut[nextIdx] - (int32_t)err_lut[idx]; int32_t compErr = (int32_t)err_lut[idx] + diff * (int32_t)frac / (int32_t)(1 << LUT_FRAC_BITS); int32_t compensated = (int32_t)raw + compErr; if (compensated >= (int32_t)ANGLE_MAX) { compensated -= ANGLE_MAX; } else if (compensated < 0) { compensated += ANGLE_MAX; } return (uint16_t)(compensated & (ANGLE_MAX - 1)); }这里有个细节:因为查找表按 2 的整数次幂分区,idx直接由右移得到,frac取低 4 位,除法/16编译器会优化成移位,速度很快。如果你的表项固定但不想用位运算,直接写除法也可以,效果一样。
实际控制环里的用法很简单:
uint16_t raw = MT6816_ReadRaw(); uint16_t corrected = MT6816_GetCompensatedRaw(raw); // 后续 FOC 或位置环里全部用 corrected,不再用 raw之所以补偿后仍然输出 0~16383 的 LSB,而不是输出浮点角度,是因为电机控制算法普遍直接用 LSB 做角度归一化,省一次浮点换算。如果你需要角度,再乘一个360.0f / 16384.0f即可。
4.4 集成到控制环的注意事项
SPI 读角度是一次阻塞式HAL_SPI_TransmitReceive,示例里可以这么写,量产代码建议改成 DMA 或者中断方式,避免在高速电流环里阻塞太久。如果你把校准后的角度更新放到 10 kHz 中断里,一次 SPI 事务大约几微秒,实际上影响不大,但和 ADC 采样、SVPWM 更新挤在一起时,时序要量化看,不能想当然。
另外,如果控制环需要速度反馈,不要在控制器里去差分"校准后角度"。角度差分会把编码器噪声放大,建议用两个连续周期的角度差做低通,或者用 M/T 测速法处理。校准表的收益主要在位置和电流环,速度环能间接感受到噪声变小。
5. 校准效果怎么验证才算数
5.1 必须用独立数据验证
校准表是拿一组数据拟合出来的,如果评估效果时又拿同一组数据来算,属于开卷考试,得到的结果一定偏乐观,因为表项里的噪声也被"记住"了。 正确做法是:整个采集过程做两圈,第一圈数据生成 LUT,第二圈数据只用来验证,不参与生成表。第二圈的每个点,用同一时刻的 MT6816 原始读数和基准读数代入,分别计算校准前的误差和校准后的误差。
5.2 用统计数据说话
采集完验证数据后,统计两组指标:
- 峰值误差:所有误差绝对值里的最大值,反映最差情况。
- RMS 误差:所有误差的均方根值,反映整体水平。
我在某颗样片、常温、装配良好条件下的实测结果大致是这样的:
| 指标 | 校准前 | 校准后 |
|---|---|---|
| 峰值误差 | 0.31° | 0.021° |
| RMS 误差 | 0.083° | 0.007° |
这个数据只代表那一颗样片,不能当芯片规格书用。但趋势是稳定的:做了查找表补偿后,误差至少能降一个数量级,最后剩下的大头是芯片读数噪声和基准自身的误差。
5.3 在电机波形上观察
数值验证过关后,再把电机挂上去跑。校准时序不要变,重新上下电,你看低速匀速转动时的电流波形,高次谐波明显变小,速度环给定一个小的正弦波激励时,反馈跟随更平滑。这个时候校准才算真正生效。
6. 实操中容易翻车的五个细节
6.1 标定数据和验证数据混用
这是最容易犯也最隐蔽的错误。很多人在转台上采完一组数据,既用来生成表,又用来画校准前后的误差曲线,误差降得特别漂亮,很高兴。实际上你把噪声也学进去了。我建议标定和验证数据分开采集、分开存储,哪怕麻烦一点也值得。
6.2 跨零点表项不连续
LUT 是首尾相接的,err_lut[0]和err_lut[LUT_SIZE-1]本质上是同一个地方的误差。如果这两项差距过大,校准后会在 0°边界附近出现一个跳变,电机转到那里会产生一次明显的顿挫。生成表后第一件事就是检查这两个值的差值,通常在几个 LSB 以内。如果差值大,回头处理采集数据而不是硬凑表。
6.3 装配太差,表兜不住
校准表能补偿的是稳定的空间误差,不是安装事故。磁铁偏心 0.2mm 以上时,误差曲线会出现很陡的局部变化,表项之间插值追不上;轴串动大了,读数随机噪声变大,表补偿完残差还是很大。我的经验顺序是:先把机械安装做到位,再做标定,而不是靠标定去掩盖机械问题。
6.4 温度漂移会让表失效
磁编码器的误差里有一个温度相关分量,常温下标定的表,到了高温或者低温时,残差会变大。如果产品只要求室温工作,问题不大。如果工作温度范围很宽,建议做两个温度点的标定表,再根据当前温度做线性插值,或者至少要在产品最极端温度点验证一下校准后的残差。这个容易被忽略,但返工成本很高。
6.5 SPI 帧格式别想当然
MT6816 的 SPI 帧格式,不同封装、不同 OTP 选项下可能略有差异。网上搜到的代码也有两种掩码写法,一种是(rx[0] & 0x3F) << 8,一种是(rx[0] & 0x03) << 8,两个差得很远,如果你套了错的代码,读出来的角度要么不连续,要么范围明显不对。老老实实抓波形,确认角度位在哪几个 bit 上,再写驱动。
我在实际项目里做这套校准,从搭台到生成第一版可用 LUT,大概花了两天。第一天全在折腾机械对中和基准读取,第二天才真正进入采集和代码。如果你手头已经有转台和高精度基准,一天能跑完。最后提醒一句:每次更换磁铁批次、改动机械结构或者换芯片封装,都意味着误差曲线可能变了,需要重新标一次。校准表不是一劳永逸的,它对应的是某一套具体装配。