
做伺服关节和云台电机的时候我在磁编码器上踩过不少坑。最早一批用的是AS5047P性能和资料都没话说只是价格和供货在批量化之后越来越难受后面就慢慢换成了MT6816。单看数据手册14bit分辨率、SPI/ABZ/PWM输出、支持多圈计数指标很漂亮但真正装到电机轴上之后你会发现一个绕不开的问题芯片自身INL确实很低可你最终读回来的角度误差可能比手册写的还大一个数量级。这个误差主要不是来自芯片而是来自整个机械装配系统。磁铁安装偏心、磁铁与芯片间距波动、PCB贴片位置偏差、外部磁场干扰都会让芯片测到的磁场方向与真实机械角度之间出现非线性误差。想要让编码器真正达到“14bit的准头”必须自己做一次非线性校准。这篇文章就从误差来源、数学建模、数据采集、系数提取一直讲到STM32端的完整代码实现整个过程我在步进电机闭环、直流无刷伺服关节和云台项目里都实际验证过适合正在做运动控制、机器人关节或者电机驱动校准的工程师参考。1. 项目概述与需求拆解1.1 MT6816是什么为什么非要做非线性校准MT6816是MagnTek麦歌恩推出的一款基于TMR隧道磁阻原理的磁编码器芯片支持14bit单圈分辨率部分型号还带12bit多圈计数。和常见的霍尔式磁编码器相比TMR方案的灵敏度更高、温漂更小对磁铁磁感应强度的要求也更宽松。输出接口支持SPI、ABZ、UVW和PWM基本覆盖了伺服电机和机器人关节的常见需求。但是要注意“分辨率14bit”和“精度14bit”是两回事。分辨率指的是输出角度能被细分到16384份精度则是你的输出角度与真实机械角度之间的差值。MT6816自身的非线性误差典型值在0.5°以内这个指标在磁编码器里已经算不错了。不过你把它贴到电机后端磁铁用胶水粘在转轴上外壳一盖情况就完全变了。磁铁几何中心与芯片感光中心实际上TMR是感应磁场方向之间哪怕只有0.1mm的偏心都会在角度误差里引入明显的基波分量。轴端的跳动、磁铁充磁不均匀又会叠加二次谐波。这些误差叠加起来实测峰峰值到两三度一点也不奇怪。对于伺服应用来说两三度的位置误差意味着低速运行时电流波动、定位抖动走速度环的时候还会产生周期性转速脉动。所以只要你的项目对位置精度有要求非线性校准基本是必须做的一步。1.2 适用场景与校准预期这个方法适合什么场景只要是用MT6816或者类似磁编码器芯片做角度反馈并且希望把绝对精度从“能用”提升到“好用”的项目都可以套用。常见的有直流无刷电机FOC伺服控制需要高精度转子位置舵轮、AGV驱动轮的角度反馈机器人关节模组中的多圈绝对角度测量云台、机械臂关节的角度闭环校准后的效果取决于你的机械装配水平和基准设备的精度。用光学编码器或者精密分度台做基准把机械角度的误差从2°~3°压到0.1°以内在工程上是完全可以实现的。如果机械结构本身装得比较规矩误差来源主要是磁铁偏心那么单纯做一次谐波和二次谐波补偿就能把大部分误差消掉。这也是这篇文章里代码采用1次2次谐波模型的原因。2. 非线性误差的来源与数学建模2.1 误差从哪里来磁铁、装配与芯片的三角关系要理解非线性校准先得知道误差是怎么产生的。磁编码器内部测量的其实是磁铁旋转时在芯片表面产生的磁场方向变化芯片并没有直接去量“转轴转了多少度”。因此凡是能让磁场方向与机械角度关系发生偏移的因素都会造成测量误差。第一类误差源是磁铁本身的安装问题。磁铁中心与转轴回转中心不重合就是我们常说的偏心。偏心存在时芯片所在位置的磁场除了旋转分量之外还会叠加上一个随角度变化的平移分量最终测到的磁场方向会呈周期性偏离真实角度偏离量近似为基波正弦。这类误差在磁铁粘接、压装工艺不稳定的产线上非常突出。第二类是轴端跳动。转轴在旋转过程中如果轴承间隙大轴端会上下摆动相当于磁铁与芯片之间的气隙在周期变化。气隙变化会改变磁场强度进而影响TMR传感器的输出通常表现为二次谐波误差。第三类是磁铁充磁不均匀。烧结钕铁硼或者橡胶磁体在生产过程中很难做到完全理想的多极充磁谐波分量天然存在。这类误差在角度域里主要呈现为2次、3次甚至更高次谐波。第四类则是PCB贴片和芯片封装带来的固定偏差。芯片没有焊正、焊盘偏移相当于传感器的敏感中心与理想中心有个固定偏移这个偏移在角度域表现为常数项误差和一次谐波误差。这四类误差叠加在一起最终的角度误差就不是一个简单的常数而是一个以360°机械角为周期的周期函数。这也是我们能用谐波模型或者查找表去拟合它的根本原因。2.2 误差的频域特征为什么一次和二次谐波是主角把磁编码器的角度误差展开成傅里叶级数你会发现能量主要集中在基波1次和二次谐波上。这个规律我验证过好几台样机无论是不同批次的MT6816还是AS5047P结果都差不太多。基波误差主要来自磁铁偏心和芯片感应的固定偏移表现为 e1(θ) A1·cos(θ) B1·sin(θ)二次谐波主要来自轴端跳动和磁铁充磁不均匀表现为 e2(θ) A2·cos(2θ) B2·sin(2θ)更高次谐波不是说没有而是在常见装配条件下量级很小。我做过对比测试一台误差峰值2.6°的样机用一次二次谐波模型去拟合残差峰值能压到0.15°以内。再加三次谐波残差能降到0.08°左右但系数提取和代码实现的复杂度都会增加一点。大多数项目做到二次谐波就够用了这也是本文代码示例默认到2次谐波的原因。2.3 校准模型推导谐波补偿与查找表校准的本质是建立一个误差模型然后用这个模型去修正原始输出角度。假设我们用高精度基准设备测出了每个原始角度对应的真实角度那么误差可以定义为err(θ_raw) θ_raw - θ_true校准后的角度就是θ_cal θ_raw - err(θ_raw)这里要注意符号方向。如果你把误差定义成“参考角度减编码器角度”那公式会反过来。我用的是工程上最容易理解的定义编码器读数比真实值大误差为正校准就把它减掉。只要你保持定义一致不管哪种符号结果都一样。接下来有两种实现路径。一种是把误差拟合成谐波函数用几个系数计算误差另一种是把误差离散成查找表运行时查表插值。谐波法的优势是内存占用极小只有5个float参数适合嵌入式端直接计算。查找表的优势是不管误差有多少高次谐波都能无差别拟合精度上限更高。实际工程里我推荐先用上位机把误差数据做FFT看看能量集中在哪几次谐波再决定用纯谐波模型还是查找表。如果三次以上谐波误差很小用谐波法就够了。如果高次谐波比较多那就用查找表。下面的章节我会把两条路都讲清楚并且给出STM32能直接用的代码。3. 校准数据采集从硬件到数据3.1 采集环境搭建你需要一台“可信的尺子”做非线性校准最关键的不是代码而是基准。你需要一套能给出“真实角度”的设备。最理想的是光学编码器或精密分度台。光学编码器的精度通常在角秒级远高于磁编码器可以作为校准基准。如果你实验室没有现成的光学编码器也可以用一个更高精度的磁编码器比如同样是14bit但装配更好的模组或者一台精度足够的步进电机加细分驱动配合减速机但采集效果会打折扣。我自己的做法是用一台带高精度光编码器的伺服电机作为基准轴把待校准的MT6816模组通过联轴器与基准轴同轴连接然后让电机以恒定低速旋转同时同步采集光编码器角度和MT6816的SPI输出角度。这样绕一圈下来就能得到一组“参考角度-原始角度”的对应数据。这个环节最容易踩的坑是同轴度。联轴器如果有间隙或者两轴中心不重合基准轴的角度和编码器轴的角度本身就存在偏差这个偏差会被当成磁编码器的误差拟合进去校准结果自然不准。所以连接机构要尽量刚性最好用无间隙弹性联轴器并且在安装后用百分表确认两轴同轴度控制在0.05mm以内。3.2 数据采集与预处理转一圈对齐起点采集数据时建议整圈等角度采样。常见的做法是编码器原始角度每增加一个固定步长比如5°或10°记录一次参考角度。如果基准设备输出分辨率足够也可以让基准轴每转固定角度触发一次采样。假设你一圈采了72个点即每5°一个点。理想情况下第i个采样点的参考角度就是i乘以5°。但由于基准轴起始位置和MT6816的零位不一定重合两者之间会有一个固定的零位偏差。处理方法有两种第一种在数据处理时把参考角度的起点对齐到与MT6816原始角度0°对应的位置。做法是让MT6816输出0°时记录下基准轴的角度作为零偏后面所有参考角度都减去这个零偏。第二种更简单直接对误差序列做FFT时把直流分量也提取出来。前面公式里误差err包含了常数项a0这个a0就是零位偏差和固定安装偏差的叠加。校准代码里只要同时减去a0起点对齐不齐的问题就自动解决了。我的代码示例中直流分量a0是单独存在的这个处理方式非常实用。采集前还要注意一点整个转圈过程中速度不要忽快忽慢。如果速度波动大采样点对应的角度间隔会不均匀做FFT时会产生泄漏影响谐波系数提取的准确性。低速匀速数据更干净。4. 上位机数据处理提取校正系数4.1 计算误差序列与去噪数据采集完成后拿到的是一组“参考角度ref_deg[i]”和“原始角度raw_deg[i]”。首先计算误差序列err_deg[i] raw_deg[i] - ref_deg[i]然后检查有没有异常跳变。如果某个点误差突然比其他点大很多多半是SPI通信毛刺或者采样错位要剔除或者平滑掉。我习惯的做法是先用中值滤波处理一遍把单个异常点拉回来再做FFT。中值滤波窗口取3就足够窗口太大反而会把真实误差细节抹掉。这里还要注意角度跳变问题。MT6816输出范围是0°到360°如果误差很大在0°/360°附近raw_deg和ref_deg可能会出现突然跳变。比如真实角度358°编码器读数为1°误差就是1-358-357°这个数据放进FFT里会产生巨大假谐波。遇到这种情况需要先对误差序列做相位展开也就是判断误差是否超过±180°如果超过就加减360°把它拉回来。4.2 傅里叶系数提取5个参数搞定大部分误差误差序列准备好之后就可以做FFT或相关运算提取谐波系数了。这里我给出一个Python脚本输入是角度数组和误差数组输出直流分量a0、一次谐波系数a1/b1、二次谐波系数a2/b2。import numpy as np def extract_harmonics(raw_deg, err_deg, order2): theta np.deg2rad(raw_deg) n len(raw_deg) a0 np.mean(err_deg) a np.zeros(order 1) b np.zeros(order 1) for k in range(1, order 1): a[k] 2.0 / n * np.sum(err_deg * np.cos(k * theta)) b[k] 2.0 / n * np.sum(err_deg * np.sin(k * theta)) print(fa0 {a0:.5f}) for k in range(1, order 1): print(fa{k} {a[k]:.5f}, b{k} {b[k]:.5f}) return a0, a, b def error_model(angle_deg, a0, a, b): theta np.deg2rad(angle_deg) e np.full_like(angle_deg, a0) for k in range(1, len(a)): e e a[k] * np.cos(k * theta) b[k] * np.sin(k * theta) return e实际操作的时候用Python跑完这个脚本会得到一组系数。比如我自己一台样机跑出来是a0 0.2105a1 1.2634, b1 -0.5321a2 -0.1867, b2 0.0428看到a1有1.26°说明磁铁偏心是主要误差来源。这正是前面分析过的装配偏心贡献了误差曲线的基波峰值。4.3 生成查找表把误差曲线离散化如果你不想在STM32里做三角运算可以生成查找表。查表法的思路是把360°机械角等分成N份比如1024份每一份存一个校准误差值。运行时根据MT6816原始角度找到对应的两个表项做线性插值得到误差再修正角度。生成表格的Python代码也很简单lut_size 1024 lut_angles np.linspace(0.0, 360.0 * (1 - 1 / lut_size), lut_size) lut_err error_model(lut_angles, a0, a, b) # 放大100倍存成int16节省Flash空间 lut_scaled np.round(lut_err * 100).astype(np.int16) print(const int16_t err_lut[1024] {) for i in range(0, lut_size, 8): print( , .join(map(str, lut_scaled[i:i8])) ,) print(};)这里有一个细节查找表的索引一般用“原始角度”而不是“真实角度”。因为运行时你手里只有原始角度查表必须用原始角度作为索引才能闭环。误差模型的输入也要用原始角度这一点很多人会弄反。用我的公式err(θ_raw)θ_raw-θ_true模型输入就是θ_raw逻辑是自洽的。5. STM32端实现SPI读取与实时校准5.1 硬件连接与SPI初始化先把硬件接口理清楚。MT6816的SPI接口通常使用4根线SCK、MOSI、MISO、CS。接STM32的时候建议SPI时钟频率先控制在1MHz以内调通之后再往上提。MT6816的SPI最高速率能到10MHz左右但STM32的SPI分频往往取整后不容易刚好落在芯片允许的范围保守一点先用2.25MHz左右跑稳定性更好。GPIO配置上CS片选一定要用软件控制不要用SPI硬件NSS。原因是MT6816的CS时序要求比较严格软件控制更灵活不容易踩坑。void MT6816_SPI_Init(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); gpio.Pin GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5; gpio.Mode GPIO_MODE_AF_PP; gpio.Pull GPIO_PULLUP; gpio.Speed GPIO_SPEED_FREQ_HIGH; gpio.Alternate GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOB, gpio); gpio.Pin GPIO_PIN_12; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOB, gpio); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET); 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_32; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi1); }关于SPI模式MT6816的数据手册里描述的时序通常是CPOL0、CPHA1或者CPOL0、CPHA0不同资料版本会有差异。我建议先按CPOL0、CPHA0Mode 0初始化如果读回来的数据不稳定再切换成CPOL0、CPHA1Mode 1试一下。本质上就是SCK空闲电平和采样沿的差异两个模式都试一遍哪个读出来数据符合角度变化规律就用哪个。5.2 MT6816 SPI读取函数MT6816的SPI读命令以16bit帧为单位主机发送命令从机在MISO上返回数据。读取未锁定角度值的典型做法是发送0x0000。返回的16bit数据帧中角度值一般占用高14位也就是bit15到bit2低2位可以作为状态位忽略。具体位定义要对照你所拿到的数据手册来确认不同型号和封装可能有微小区别。uint16_t MT6816_ReadAngleRaw(void) { uint16_t cmd 0x0000; uint16_t rx 0; HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(hspi1, (uint8_t *)cmd, (uint8_t *)rx, 2, 10); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET); // 14bit角度位于高14位右移2位得到0~16383 return (rx 2) 0x3FFF; } float MT6816_ReadAngleDeg(void) { uint16_t raw MT6816_ReadAngleRaw(); return (float)raw * 360.0f / 16384.0f; }注意这里我用了8bit数据长度的SPI配置连续收发两个字节。有些芯片对帧格式要求高如果你发现自己发送的16bit命令数据读回来总是错位可以尝试16bit数据长度模式直接transmit一个uint16_t。两种方式我都见过选择哪种取决于你的HAL库版本和MCU的SPI外设支持情况。还有一个容易忽略的点CS拉低后需要留一点点时间再启动SCK让芯片准备就绪。在HAL_SPI_TransmitReceive之前加一个简单的延时比如几个空指令可以提升稳定性。如果SPI时钟不高这个延时通常不是必须的但如果遇到边界情况值得试一下。5.3 查找表插值校准实现拿到原始角度之后进入校准环节。我用的是1024点查找表表项放大100倍存成int16_t避免直接使用float数组占太多Flash。extern const int16_t err_lut[1024]; float MT6816_GetCalibratedAngle(float raw_deg) { float pos raw_deg / 360.0f * 1024.0f; int idx (int)pos; if (idx 1023) { idx 1022; } float frac pos - idx; float e0 (float)err_lut[idx] * 0.01f; float e1 (float)err_lut[idx 1] * 0.01f; float error e0 (e1 - e0) * frac; float cal raw_deg - error; if (cal 0.0f) { cal 360.0f; } else if (cal 360.0f) { cal - 360.0f; } return cal; }查表插值的精度取决于表项数量和误差曲线的平滑程度。1024点对应每0.3516°一个点对于主要成分是1次和2次谐波的误差曲线来说线性插值的误差基本可以忽略。这个函数可以直接返回float类型角度给FOC算法或者位置环使用也可以用uint16_t角度格式换算回来看你的工程需要。如果不想用查找表而是想用谐波补偿代码更精简只需要传入5个系数#define PI_F 3.14159265358979f float MT6816_HarmonicCorrect(float raw_deg, float a0, float a1, float b1, float a2, float b2) { float rad raw_deg * PI_F / 180.0f; float err a0 a1 * cosf(rad) b1 * sinf(rad) a2 * cosf(2.0f * rad) b2 * sinf(2.0f * rad); float cal raw_deg - err; if (cal 0.0f) { cal 360.0f; } else if (cal 360.0f) { cal - 360.0f; } return cal; }谐波补偿更节省Flash而且模型参数是连续的不会存在查找表离散化误差。代价是每次计算需要调用cosf和sinf在FOC中断频率比较高的场景下会占用一些CPU时间。如果你的主控跑在72MHzFOC频率20kHz四次三角函数仍然可以接受如果主频低或者中断负载高建议直接用查找表。5.4 校准效果验证数据会说话代码写完一定要做效果验证。最简单的验证方法是让电机转一整圈把校准前后的角度误差都记录下来对比峰峰值。我建议做成一个“误差测试模式”电机匀速旋转同时记录MT6816原始角度、校准后角度和参考角度串口或上位机日志输出。根据我自己的实测数据一台原始误差峰峰值2.35°的样机做完二次谐波校准后误差峰峰值降到了0.12°。这个数据是在装配没有动过的情况下测的说明误差模型中基波和二次谐波部分确实抓住了主要矛盾。如果你校准完之后误差没有明显下降不要先怀疑芯片先检查你的误差数据采集环节。尤其是参考角度和原始角度是否严格一一对应采样延迟是否造成了恒定角度差这两点是校准失败的高发原因。另外要提醒的是校准模型是“针对当前这一套机械装配”的。如果你把编码器模组拆下来重新安装或者换了一块磁铁误差曲线会变原校准参数就不一定适用了。量产的时候要么控制装配一致性要么每台产品都做一次在线标定。6. 常见问题与调试技巧6.1 SPI读取异常排查现象可能原因处理方法读取数据恒为0x0000CS时序不对或SPI模式错误检查CS拉低时序切换SPI Mode 0/1读取数据跳动很大SPI时钟过快降低SPI分频保证时序裕量读取数据有规律跳变命令字/返回帧位偏移检查数据位定义右移位数是否错误角度在0°/360°附近跳变正常角度回绕做角度展开或者用增量方式处理我遇到最多的问题其实是SPI模式配错。MT6816的手册样例和网上代码模板经常不一致有的用Mode 0有的用Mode 1。最简单的方法是隔离问题先固定角度读一串数据看是不是稳定值再手动缓慢转动磁铁看数据是不是平滑变化。这样基本能快速判断通信是否正常工作。6.2 校准无效或误差变大的原因有时候校准做完误差不但没变小反而变大了这种情况我遇到得很少但也不是没有。排查思路如下。第一检查误差公式的符号。如果你把误差定义成raw-true校准就要减去误差如果你在电脑上拟合时用的是true-raw在校准端又减了一次误差就变成原来的两倍了。解决方案是把标定脚本和校准代码放在一起用一个固定输入值做推演确认方向一致。第二检查谐波模型输入角度是否正确。前面提过误差模型的自变量是raw_deg。如果你误用了参考角度作为输入模型本身就有偏差。第三检查有没有做相位展开。误差序列里如果存在±180°附近的跳变点FFT提取出的系数会严重失真校准后误差自然无法收敛。6.3 几点补充建议我个人的体会是磁编码器校准这件事花在机械装配上的时间应该比花在校准算法上的时间更多。磁铁安装偏心是头号误差来源哪怕校准能把这部分误差补偿掉偏心的存在还会引入径向力、振动等其他工程问题。所以在做校准之前先用合适的工装保证磁铁中心和转轴中心尽量重合这个收益比任何算法都大。另外标定完的系数建议存到STM32内部Flash或者外部EEPROM里在产品出厂时写入。不要把系数硬编码在固件里否则一台产品的机械装配变了就得重新编译固件后期维护会很痛苦。我习惯的做法是在Bootloader或者出厂测试工装里先做标定标定完成后把系数写入Flash的指定区域应用程序启动时读取。这样每台设备可以有独一无二的校准参数而固件是同一份。如果你后续想进一步提高校准效果可以把三、四次谐波也加入模型代价是标定脚本和MCU端要多算两个谐波项。再或者把查找表点数从1024提高到2048或者4096对高次谐波占比较大的场景会有帮助。不过对大多数伺服和机器人应用来说做到二次谐波加1024点查找表已经能拿到很满意的效果了。