ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MT6701磁角度传感器SSI接口调试:CRC-6校验参数陷阱解析

MT6701磁角度传感器SSI接口调试:CRC-6校验参数陷阱解析 1. 现象轴在转角度却像喝了假酒先交代一下背景。我在做一套伺服电机的位置闭环转子位置检测选了 MT6701 这颗 TMR 磁角度传感器12 bit 分辨率支持 SSI 接口成本比光学编码器低一截PCB 面积也省。理论上把磁铁贴在轴端、芯片对准中心SSI 引三根线到主控读出来就是 0~4095 的角度码值整个调试过程应该用不了一下午。实际呢我焊好板子、写完第一版读取程序上电之后用手慢慢拧电机轴串口打印出来的角度值直接让我怀疑人生——数值在 0~4095 之间疯狂跳变偶尔连续几个数看起来是递增的但紧接着就会跳回一个完全不挨着的值而且 CRC 校验结果几乎永远是错的。这里要解释一下MT6701 的 SSI 输出不是裸的角度数据帧尾带了一段 CRC 校验位。我在程序里设了“校验不通过就丢弃该帧”的逻辑所以串口里大量打印的是CRC ERR错误计数。当时我以为是接线松了、芯片焊歪了、SPI 外设配置错了……反正能想到的都排查了一遍最后才发现问题不在硬件也不在主控而是我对 CRC-6 校验码的理解出了偏差而且这个偏差特别隐蔽藏在一个几乎所有教程都不会讲清楚的细节里。这篇文章把这三天踩坑的完整过程写下来包括 SSI 时序怎么抓、数据帧怎么拆、CRC-6 到底怎么算、以及那个让我最头疼的“参数对不上”问题。如果你也在调带 SSI 接口的绝对值编码器尤其是 MT6701 这类带 CRC 校验的芯片这篇应该能帮你省下至少一晚上的时间。2. SSI 接口的三根线看着简单电平、时序、上拉一个都不能少2.1 SSI 和 SPI 的关系别把时序配置抄错MT6701 的 SSI 接口全称是 Synchronous Serial Interface同步串行接口。很多工程师习惯直接把主控的 SPI 外设拿来复用它因为波形上很像。但 SSI 和 SPI 有一个本质区别SPI 是双向的主机发数据的同时从机也能回数据SSI 是单向的主机只输出时钟从机在时钟沿把数据一位一位“推”出来。MT6701 的 SSI 时序大概是这个套路CSN 平时是高电平通信前拉低SCLK 空闲时也是高电平对应 SPI 的 CPOL1CSN 拉低后SCLK 的第一个下降沿芯片把最高位数据放到 SO 引脚上之后每个下降沿更新下一位主机在 SCLK 的上升沿去采样 SO读完一帧需要的 bit 数之后把 CSN 拉高芯片内部复位等待下一帧。我当时直接把主控 SPI 外设配置成了 CPOL1、CPHA1心想这不就是“空闲高、第二个边沿采样”嘛。但实际一抓波形发现有些芯片的“下降沿更新数据、上升沿采样”对应的是 CPHA0 而不是 CPHA1具体要看芯片手册怎么定义“第一个边沿”。MT6701 这颗芯片我最终验证下来是 CPOL1、CPHA1 能读对但这东西不能拍脑袋每个型号必须亲自抓波形确认。2.2 电平、上拉和线长的坑杜邦线飞出来的全是眼泪MT6701 是 3.3V 器件这个大家都懂但实际接的时候还是容易被忽略。我第一次测试用的是开发板 杜邦线飞线SCLK 大约 1MHzSO 引脚上没接外部上拉。结果就是时钟越快SO 的上升沿越圆波形就像被水泡过的卫生纸示波器上看不到干净的边沿。原因很简单芯片内部虽然有弱上拉但驱动能力有限加上杜邦线本身的寄生电容RC 时间常数变大信号的上升沿被拉长。主机在上升沿采样的时候SO 电压可能还在中间区域徘徊读回来的位自然就是随机的。解决方式也粗暴外部加上拉电阻4.7k 到 10k 都行我板子上最后放了 4.7k。然后把 SCLK 从 1MHz 降到 500kHz 试了一轮发现波形明显干净很多CRC 错误率从“每次都错”变成了“偶尔对”虽然还是没有完全解决但至少方向对了。之后再调回 1MHz配合上拉电阻波形也能稳住。如果你的测试环境更恶劣——比如线长超过 15cm或者旁边有电机驱动线在跑 PWM——建议直接把 SCLK 降到 250kHz 以下或者干脆把芯片和主控放同一块 PCB 上别用飞线。SSI 这玩意儿对时序的要求没有特别变态但它在电机控制器这种强干扰环境里对信号完整性是真敏感。2.3 示波器怎么抓才不误导自己调试那几天我犯过一个低级错误示波器的地线夹子接得很长结果探到的波形上全是毛刺。后来换成了弹簧地针紧贴着 SO 引脚测量波形瞬间干净了。还有一点很关键抓 SSI 波形时要把 CSN、SCLK、SO 三个通道一起抓触发放到 CSN 的下降沿。因为 SSI 一帧数据全靠 CSN 拉低作为起点如果你只看时钟和数据很难判断一帧到底从哪里开始、到哪里结束。而帧边界错位恰恰是我后面排查 CRC 失败的第一个怀疑对象。3. 数据帧解析24 个 bit 里角度只占一半多3.1 先把帧结构画出来再动手写代码我手头这颗 MT6701 通过配置选择了扩展 SSI 输出模式一帧 24 个 bit高位在前。帧格式大致是Bit23 ~ Bit1212 bit 角度数据范围 0~4095对应 0°~360°Bit11错误标志 ERR正常为 0Bit10磁场强度状态位通常为 1表示磁场强度在可用范围内Bit9 ~ Bit6保留位或状态位不同固件版本可能不一样Bit5 ~ Bit0CRC-6 校验值这里要特别提醒一下MT6701 不同型号、不同固件版本帧格式可能有差异有的版本 SSI 默认是 16 bit 帧有的版本支持扩展帧。我代码里最终写死的帧长是 24 bit但这个参数必须对着你手头那颗芯片的数据手册确认别直接抄网上的代码。我第一次就是在这一步偷懒了直接按 16 bit 解析低 6 位当成角度的一部分结果就是轴每转 64 个码值360°/4096 × 64 ≈ 5.6°左右角度数据就会出现一次明显的跳变。实际上那不是跳变是我把 CRC 位当成角度位了角度值本身是对的是低 6 位在“瞎跳”。3.2 解析代码掩码提角度CRC 做门卫正确的解析顺序应该是先把整帧 24 bit 收完然后按位掩码取出角度和状态标志再对“除了 CRC 之外的所有有效位”做 CRC-6 校验校验通过才认为这帧数据可用。我最终在 MCU 上用的读取逻辑大概是这样的伪代码 C 片段#define FRAME_BITS 24 #define ANGLE_MASK 0xFFF000 // bit23~bit12 #define ANGLE_SHIFT 12 #define ERR_MASK 0x000800 // bit11 #define MAG_OK_MASK 0x000400 // bit10 uint32_t ssi_read_frame(void) { uint32_t frame 0; csn_low(); for (int i FRAME_BITS - 1; i 0; i--) { sclk_low(); // 下降沿芯片更新 SO _nop_(); if (sdo_read()) { frame | (1u i); } sclk_high(); // 上升沿主机采样 _nop_(); } csn_high(); return frame; }拿到frame之后再做校验和解析uint8_t angle (frame ANGLE_MASK) ANGLE_SHIFT; uint8_t err_flag (frame ERR_MASK) ? 1 : 0; uint8_t mag_ok (frame MAG_OK_MASK) ? 1 : 0; uint8_t crc_rx frame 0x3F; // 帧里收到的 CRC-6 uint8_t crc_calc crc6_calc((uint8_t*)frame, 3, ...); // 对前 18 bit 算 CRC if (crc_rx ! crc_calc) { // 校验失败丢弃 }注意这里有个大坑CRC 到底对哪些位计算是整帧 24 bit 还是前 18 bit不同芯片做法不一样。MT6701 我验证下来是对“角度 状态位”这些有效数据位计算也就是前 18 bitBit23~Bit6CRC 本身不参与计算。但你要是不看手册光靠试很容易在“算 3 个字节”和“算 2 个字节再补 2 bit”之间反复横跳。3.3 为什么要在角度数据里塞一个 CRC可能有人会觉得一个角度传感器而已读错了下次再读呗搞个 CRC 不是增加复杂度吗但在电机控制场景里一个错误的角度值不是“显示错了”那么简单。位置环、速度环、电流环全都依赖这个角度做坐标变换一旦角度跳变电流环可能瞬间给出错误的电压矢量轻则电机嘎嘎响一下重则直接过流烧驱动。所以 SSI 接口的绝对值编码器普遍带 CRC就是为了让控制器能识别坏帧宁可保持上一次的值也不要用一个错得离谱的假数据。想通了这一点我就理解了为什么 MT6701 愿意为 6 个 bit 的 CRC 多花 6 个时钟周期——这不是浪费这是电气噪声环境下的保命设计。4. CRC-6 校验算法多项式、初始值、位序差一个参数就全盘皆输4.1 先把 CRC 基本概念对齐CRC循环冗余校验本质上就是把数据当成一个二进制多项式用另一个固定的多项式去做模 2 除法得到的余数就是校验值。CRC-6 就是用 6 位寄存器存余数最终校验值是 6 bit取值范围 0~63。描述一个完整的 CRC 算法需要四个参数缺一不可多项式Poly比如 x^6 x 1通常简写为 0x03表示“去掉最高项之后剩下的 bit 序列”初始值Init寄存器一开始的值常见 0x00 或 0x3F输入/输出是否反转RefIn / RefOut数据在参与计算前、校验值输出前是否要把 bit 顺序反过来结果异或值XorOut最终结果再异或一个值常见 0x00 或 0x3F很多人在这一步就开始踩坑了。因为 CRC-32 这种标准化程度高的算法大家用的参数基本一样但 CRC-6 没有统一标准光常见的就有 CRC-6-ITU、CRC-6-GSM、CRC-6-CDMA2000 等好几种多项式、初始值、是否反转全都不一样。MT6701 这套 SSI 帧里的 CRC-6我最终核实过的参数组合是参数值Poly0x03Init0x00RefInfalseRefOutfalseXorOut0x00Check0x??重点不是这个参数本身而是我要强调不同批次、不同固件版本可能有差异建议你以自己手头芯片手册的伪代码为准。真正让我折腾三天的是下面这个“参数表达方式”的陷阱。4.2 同一份多项式不同代码里长得不一样这是我整篇最想分享的一个坑。网上搜 CRC-6 实现你会看到无数版本有的代码检查寄存器的 bit5即 0x20有的代码检查 bit7即 0x80还有的代码把多项式写成了 0x0C、0x30 之类的看起来完全不一样的值。原因在于CRC 寄存器的宽度是 6 位但很多通用 CRC 代码框架是按 8 位字节来写的为了统一它们会把 6 位的多项式“对齐”到 8 位的最高位也就是左移 2 位。比如同一个多项式 x^6 x 1在标准 6 位算法里是 0x03在“左移对齐”的代码里就变成了 0x0C。两种写法都能算出正确结果但如果你从网上抄代码却没注意它用的哪种表示法参数就必然对不上。我当时犯的错就是先找了一个看起来很权威的 CRC-6 代码照着里面填了 poly0x1B、refintrue结果算出来的校验值永远和帧尾对不上。后来我静下心用手册给的伪代码自己写了一遍标准 6 位算法才发现手册里那个 x^6 x 1 在标准算法里对应的就是 0x03和我之前用的那套“对齐到字节高位”的实现完全是两码事。调试这类非字节对齐的 CRC最稳妥的办法是不要直接用网上抄来的通用 CRC 框架而是自己按标准逐位算法写一个参数只填“去掉最高项后的低 6 位多项式”。这样至少能保证代码表示法和手册伪代码一致。4.3 一个可以跑通的 CRC-6 参考实现下面这个 C 实现是标准逐位算法寄存器和多项式都放在 6 位宽度里处理poly 参数直接填 0x03 这种低 6 位值不用做任何左移#include stdint.h static uint8_t reflect6(uint8_t v) { uint8_t r 0; for (int i 0; i 6; i) { if (v (1u i)) { r | (1u (5 - i)); } } return r; } uint8_t crc6_calc(const uint8_t *data, uint8_t len, uint8_t poly, uint8_t init, uint8_t refin, uint8_t refout, uint8_t xorout) { uint8_t crc init 0x3F; for (uint8_t i 0; i len; i) { uint8_t byte data[i]; if (refin) { byte reflect8(byte); // 这里需要一个 8 位反转函数 } crc ^ byte; // 先把一个字节异或进寄存器 for (uint8_t j 0; j 8; j) { if (crc 0x20) { // 检查 6 位寄存器的最高位 bit5 crc (uint8_t)(((crc 1) ^ poly) 0x3F); } else { crc (uint8_t)((crc 1) 0x3F); } } } if (refout) { crc reflect6(crc); } return (crc ^ xorout) 0x3F; }对应的 Python 验证脚本方便你在电脑上对着一帧抓下来的原始数据调参def reflect6(v: int) - int: r 0 for i in range(6): if v (1 i): r | (1 (5 - i)) return r def crc6(data: bytes, poly: int 0x03, init: int 0x00, refin: bool False, refout: bool False, xorout: int 0x00) - int: crc init 0x3F for b in data: if refin: b int(f{b:08b}[::-1], 2) crc ^ b for _ in range(8): if crc 0x20: crc ((crc 1) ^ poly) 0x3F else: crc (crc 1) 0x3F if refout: crc reflect6(crc) return (crc ^ xorout) 0x3F有了这个脚本我当时是把逻辑分析仪抓到的几帧数据粘进去然后写个循环穷举 poly、init、refin、refout、xorout 的不同组合几十秒就能把正确参数试出来。这个方法强烈建议在你调任何带 CRC 的编码器时先用上别光靠肉眼看手册猜参数。5. 三天排查复盘从盲目重试到逐位核验这三天我到底踩了哪些坑、每一步是怎么排查的我觉得值得完整复盘一遍因为很多细节是单看结论看不出来的而这些细节才是以后遇到类似问题时的排查路径。5.1 第一天怀疑硬件结果换了三块板子第一天晚上我 90% 的精力花在怀疑硬件上。先是拿万用表量 SO 引脚的静态电平发现空闲时是高的说明上拉没问题然后怀疑是芯片虚焊拿风枪吹了一遍没用接着怀疑杜邦线接触不良换了一套新的没用最后甚至把主控从开发板换到了另一块板子上现象依旧。现在回头看第一天最大的问题是我在没有确认“芯片输出的波形是否正常”的情况下就盲目怀疑硬件。其实只要当时接上示波器看一眼 CSN 拉低之后的 SCLK 和 SO就能排除掉大部分硬件问题根本不用换板子。5.2 第二天怀疑 SPI 外设配置角度“变顺了”但 CRC 依旧全错第二天我把重点放到 SPI 外设的配置上。CPOL 和 CPHA 四个组合挨个试了一遍终于找到一组让角度值看起来连续了——也就是转轴的时候角度在递增不再乱跳。我当时心里一喜觉得问题快解决了。但紧接着就发现CRC 校验还是几乎全部失败。这就很诡异了角度连续说明数据位大概率是对的帧边界大概率也没问题那为什么 CRC 还是错的也就是在这个时候我才正儿八经地把“CRC 参数”这个变量拉进排查范围。我开始怀疑不是数据错了而是我计算 CRC 的方式和芯片不一致。有意思的是就在我准备深入 CRC 参数时我又发现了一个更隐蔽的问题角度连续只能在速度很低的时候看出来一旦轴转快了角度依然会出现偶发的整段跳变。后来定位到是两个原因叠加一是 CSN 拉高的保持时间太短芯片没有完全复位就开始了下一帧二是中断把读帧过程打断了导致一帧数据中间夹了几个微秒的停顿时序全乱了。这个问题我是在第三天配合逻辑分析仪才彻底确认的第二天只处理了表面症状。5.3 第三天逻辑分析仪抓裸数据Python 穷举参数第三天我实在不想盲猜了直接上逻辑分析仪把 CSN、SCLK、SO 三根线以 10MHz 采样率抓了几十帧数据。然后把 SO 的 bit 流导出来对着 MT6701 的手册逐位核对帧结构。这一步有个特别重要的发现我之前一直以为 SO 是“MSB first”也就是高位先出这样我在代码里按位从高往低填就行。但是逻辑分析仪抓到的一帧数据显示数据线上的 bit 顺序和我代码里拼出来的顺序在某些位上是反的——不是全反而是字节内的位反了。换句话说虽然 SSI 协议规定整帧 MSB first但芯片内部对“数据字节”的处理可能是 LSB first 的导致你在拼帧时如果按字节为单位去移位出来的 bit 序列就和芯片实际发送的 bit 序列不一致。这个细节不抓波形是根本看不出来的因为角度粗看仍然是连续的只有详细比对每一位才会发现端倪。然后我在 Python 里把一帧的完整 bit 流打印出来写了穷举脚本遍历 poly 0x03、0x1B、0x2D、0x39init 0x00、0x3Frefin/refout 的四种组合……最终才定位到那组“看起来最不起眼”的参数poly0x03、init0x00、不反转、不异或。那一刻我真想把电脑砸了——就这么简单的参数让我折腾了三天。但冷静下来想想这三天其实不算白费因为如果不懂 CRC-6 的“参数表示法陷阱”就算有人直接告诉我 poly0x03我也会因为代码框架不一致而再次失败。5.4 排查过程小结阶段现象怀疑方向验证方法结论第一天角度乱跳CRC 全错接线、虚焊、芯片万用表、换板换线硬件基本正常是数据解析问题第二天角度连续CRC 仍全错快速转动会跳变SPI 配置、帧边界改 CPOL/CPHA查 CSN 时序帧解析眉目有了CRC 参数不对第三天逐位核对发现字节内位序反CRC 参数、位序逻辑分析仪 Python 穷举参数应为 poly0x03、refinfalse6. 最终修复与验证连续跑一整天错误帧率降到 06.1 完整读取代码长什么样把前三天的成果汇总我最终在主控里固定下来的读取逻辑大致如下#define SSI_FRAME_BITS 24U #define CRC_POLY 0x03U #define CRC_INIT 0x00U uint16_t angle 0; // 从 MT6701 读一帧 24 bit 数据 uint32_t ssi_read_raw(void) { uint32_t raw 0; csn_low(); for (int bit SSI_FRAME_BITS - 1; bit 0; bit--) { sclk_low(); // 下降沿芯片更新 SO delay_ns(100); if (sdo_read()) { raw | (1UL bit); } sclk_high(); // 上升沿主机采样 delay_ns(100); } csn_high(); delay_us(5); // CSN 高电平保持时间确保芯片复位 return raw; } int ssi_read_angle(uint16_t *out_angle, uint8_t *out_mag_ok) { uint32_t raw ssi_read_raw(); uint16_t ang (uint16_t)((raw 0xFFF000UL) 12); uint8_t err (raw 0x000800UL) ? 1 : 0; uint8_t mag (raw 0x000400UL) ? 1 : 0; // CRC 只对前 18 bit角度 状态位计算 uint8_t buf[3]; buf[0] (uint8_t)((raw 16) 0xFF); buf[1] (uint8_t)((raw 8) 0xFF); buf[2] (uint8_t)((raw 2) 0xFC); // 低 2 位清零只保留 Bit9~Bit6 状态位 uint8_t crc_calc crc6_calc(buf, 3, CRC_POLY, CRC_INIT, 0, 0, 0); uint8_t crc_rx (uint8_t)(raw 0x3F); if (err || crc_calc ! crc_rx) { return -1; // 这帧不能用 } *out_angle ang; *out_mag_ok mag; return 0; }这段代码里最容易被忽略的是 buf[2] 的取值。因为 CRC 覆盖的是 Bit23~Bit6 这 18 个有效位在按 3 字节分组时第三字节的低 2 位其实是 CRC 位本身的最高两位必须清零之后再参与计算。如果这里图省事直接把 raw 的三个字节全丢进去CRC 永远算不对。实际上我第二天晚上卡住就是卡在这个细节上看起来是“凑巧”的参数问题本质是对帧格式理解不够细致。6.2 校验失败时的策略CRC 校验失败之后怎么办我最终的策略是单帧失败丢弃当前值沿用上一次有效的角度值连续失败 10 次认为传感器或接线异常置位系统故障标志停止电机输出磁场警告标志mag_ok为 0即使 CRC 通过也只记录角度但不参与闭环控制同时点亮警示灯这个策略是根据电机控制场景设计的不能让坏值进入电流环但也别因为偶发的单帧坏数据就立刻停机否则在强干扰现场会频繁跳闸比数据偶尔坏一次还麻烦。6.3 验证结果从“几乎全错”到“零错误”修复之后的测试结果我很满意用手缓慢旋转电机轴串口打印的角度曲线连续且平滑没有跳变CRC 错误计数长时间内保持为 0。我一直开着系统跑了 24 小时的连续循环测试累计读取了大概几百万帧数据错误帧率是 0。后来我还故意把磁铁往远离芯片的方向拉了几毫米让磁场警告位变成 0此时 CRC 开始偶尔出错角度输出也确实出现了精度下降——这说明 CRC 在弱磁场的边缘工况下确实起到了“哨兵”作用能提前暴露异常。硬件方面我做了个对比测试把外部上拉去掉SCLK 跑 1MHz错误帧率马上恢复到 0.1% 左右加上 4.7k 上拉错误帧率回到 0。所以对于 MT6701 这类芯片SO 的外部上拉不是可有可无的尤其在线长和干扰并存的环境下上拉电阻是稳定性的保底手段。6.4 一点后续扩展这次调试完之后我把这套逻辑抽成了一个通用的“SSI 编码器驱动框架”帧长、角度掩码、CRC 参数全部做成宏或结构体配置项下次不管换什么带 SSI 接口的编码器只需要改配置不用重写驱动。实测下来后来调另一颗带 24 位帧、CRC-8 的编码器时果然又碰上了 CRC 参数对不上的问题但因为有这个框架我直接跑一遍穷举脚本就搞定了前后不到半小时。7. 写在最后调这类接口先抓波形再算 CRC别用猜的回头看这三天最大教训其实是两个字别猜。第一天我在没有示波器波形的前提下怀疑硬件浪费了一整晚第二天在没核对帧格式的前提下反复改 CRC 参数又浪费了一下午。直到我真正用逻辑分析仪抓到完整帧把 bit 流打印出来逐位核对问题的真相才浮出水面。我也总结了一条以后通用的调机顺序送给所有准备碰 MT6701 或类似 SSI 编码器芯片的朋友先抓波形确认 CSN、SCLK、SO 三根线的时序和手册一致重点看 SO 在下降沿之后多久稳定。用逻辑分析仪抓 10~20 帧不同角度下的原始数据导出 bit 流在电脑上手工拆帧确认角度位、状态位、CRC 位的具体分布。写一个 Python 脚本把这批原始帧和最后的 CRC 字节配对穷举常见参数组合一次性锁定正确的 CRC 参数。最后再写 MCU 端的驱动代码把 CRC 参数做成可配置项并用连续长时间运行验证错误帧率。这套流程下来我再也不怕任何带 CRC 的编码器接口了。说句实在话CRC-6 本身不难难的是它在不同代码实现里长得不一样。你只要理解了“多项式表示法”和“位序反转”这两个坑其他都是纸老虎。
返回列表