基于STM32平台的IC-MU磁绝对值编码器SPI通讯与码盘数据处理
说实话,第一次拿到IC-MU这颗磁绝对值编码器芯片时,我脑子里冒出来的第一个问题是:为什么一颗小小的磁性角度传感器,要叫“编码器”,还要带“码盘数据处理”这么重的名字?等我把SPI时序调通、把角度数据算出来之后才明白,这颗芯片的单圈绝对值输出只是开胃菜,真正决定项目成败的,是后面那套数据拼接、滤波和错误处理的功夫。
这篇文章把我从零开始调通的完整过程整理出来,包括硬件接线、SPI参数配置、数据帧解析、多圈拼接思路,以及我踩过的几个比较隐蔽的坑。适合正在用或准备用磁绝对值编码器做电机闭环、转台角度反馈、机械臂关节位置检测的工程师参考——不管你是用库函数还是寄存器操作,思路都是通用的。
1. 先搞明白IC-MU到底是个什么编码器
1.1 磁绝对值编码器的工作原理
磁绝对值编码器与传统光电编码器的本质区别在于检测方式。光电编码器靠码盘上的透光/遮光条纹配合发光管和接收管工作,码盘精度直接受限于光栅刻线密度;而IC-MU这类磁编码器,内部核心是一个磁阻或霍尔阵列,通过感知上方旋转磁铁的磁场方向来解算角度。
具体到工作原理,芯片内部通常有多个各向异性磁阻(AMR)或巨磁阻(GMR)元件,呈环形排列。当磁铁旋转时,每个元件感受到的磁通密度和方向都在变化,经过芯片内部的差分放大和CORDIC算法运算,就能解算出0°到360°的绝对角度值。
有一点值得注意:标题里的“码盘数据处理”这个说法,习惯上是从光电编码器那边带过来的。IC-MU本身并没有物理码盘,它输出的是经过内部数字处理后的角度码值。但在实际工程中,我们仍然沿用这个概念——把每一帧SPI读回来的数据当作“码值”来处理,后续的拼接、滤波、角度换算,本质上就是在处理这套码值流。
1.2 为什么项目里要选绝对值而非增量式
做项目选型时,最容易被忽略的问题就是“断电重启后位置还在不在”。增量式编码器输出的是A/B/Z脉冲,断电后位置信息完全丢失,必须重新找零位或回原点。绝对值编码器则不同,它每个位置对应唯一的码值,断电重启后直接读SPI就能拿到当前绝对角度。
IC-MU在精度和成本之间找到了一个比较好的平衡点。拿我用的这颗来说,单圈输出位数是14位,也就是16384个码值,对应约0.022°的角分辨率。这个精度用于大多数电机闭环和转台定位场景都足够了,而价格只有同精度光电绝对值编码器的几分之一。
| 编码器类型 | 断电记忆 | 分辨率 | 抗污染能力 | 体积 | 成本 |
|---|---|---|---|---|---|
| 增量式光电 | 无 | 可选,通常较高 | 怕油污粉尘 | 较大 | 低 |
| 光电绝对值 | 有 | 高,受限于码盘 | 怕油污粉尘 | 大 | 高 |
| 磁绝对值(IC-MU类) | 有 | 中高(12~14位常见) | 抗污染能力强 | 小 | 中低 |
另外,磁编码器对安装环境的适应能力明显更强。光电码盘一旦进油或者落灰,读数就会出错;磁编码器只要保证磁铁和芯片之间的间隙在手册范围内,表面有点脏东西问题不大。这是我选择IC-MU做转台反馈的直接原因——设备运行环境里免不了有切削液飞溅。
1.3 芯片输出的是什么形式的数据
IC-MU这类芯片对外接口通常支持SPI或SSI协议,高版本还会支持BISS-C、BiSS等更快更智能的总线协议。我们用的SPI模式,本质上是芯片作为从机,STM32作为主机发起时钟,芯片把内部的角度寄存器值按位送往MISO线。
从数据帧结构来看,一个完整的读取周期通常包含前导位、角度数据位、以及错误/警告标志位。常见的位宽有12位、14位、16位、18位甚至更高。要注意的是,这些位宽往往不是8的整数倍,所以直接按字节读会面临错位问题,需要在软件层做移位和掩码处理。这个问题我在第2章会详细讲。
2. STM32侧SPI通讯的关键设计与初始化
2.1 硬件接线:别把SPI和SSI时序混为一谈
很多人在第一步就踩坑。IC-MU的SPI接口和标准的SPI从机有一个很大的不同:它往往没有MOSI数据输入的需求,或者说命令字极短。芯片只需要主机给一个时钟沿,就会把数据从MISO线推出来。这种单线返回数据的模式在时序上更接近SSI协议,只是管脚定义沿用了SPI的SCK和SO。
我实测下来的接线方式如下:
- SCK - PA5(SPI1_SCK)
- MISO - PA6(SPI1_MISO)
- CS - 任意一个空闲GPIO,我用的是PA4
- MOSI - PA7(如果芯片不需要命令字,这根线可以不接,但为了方便调试我还是接了)
这里的一个核心问题是片选信号的处理。IC-MU的片选不是简单的低电平选中,它对CS的下降沿和SCK的第一个上升沿之间的间隔有要求,某些型号还要求CS低电平期间SCK必须是确定的初始极性。如果直接用STM32硬件SPI的NSS引脚,由于硬件NSS的时序是与SCK自动对齐的,有时反而会不满足芯片的要求。
我的做法是用普通GPIO模拟片选,先拉低CS,等几个微秒,再启动SPI通信。示波器观察下来,这个做法对IC-MU的兼容性最好。
注意:启动SPI传输之前,最好把片选拉低后做几个空时钟周期,有的芯片需要这段时间完成内部数据锁存。这个细节在后文"避坑记录"里还会提到。
2.2 CubeMX下的SPI参数配置要点
用STM32CubeMX配置SPI时,最容易纠结的是CPOL和CPHA怎么选。IC-MU这类磁编码器的SPI接口,数据手册里一般会给出时序图。如果时序图显示SCK空闲为低、采样沿在上升沿、数据在下降沿切换,那对应的就是CPOL=0、CPHA=0,也就是SPI Mode 0。
我用的是SPI Mode 0,主频压到2.25MHz左右。为什么不跑满SPI的18MHz或36MHz?因为IC-MU内部的数据输出寄存器从CS拉低到数据稳定需要一定的响应时间,时钟太快时MISO线上的数据还没稳定就被采样了,多读几次会出现数据跳变。
具体配置如下:
| 参数 | 值 |
|---|---|
| Mode | Full-Duplex Master |
| Data Size | 8 Bit |
| First Bit | MSB First |
| Prescaler | 32(在36MHz APB2总线下约1.125MHz)或16(约2.25MHz) |
| CPOL | Low |
| CPHA | 1 Edge |
| NSS | Software |
从实际调试来看,2.25MHz是一个比较稳妥的选择,既能满足绝大多数控制系统对更新率的带宽需求(按16位一帧算,约每秒十几万次读取),又能保证采样点避开信号边沿附近的毛刺区间。
2.3 数据帧的读取和位宽拼接处理
如果芯片输出的是16位数据(比如最高两位是错误标志和警告标志,低14位是角度值),那事情就简单了——直接用16位接收即可。但实际中很多IC-MU类芯片输出的帧长度不是标准的8位或16位。比如有的芯片一个输出帧是21位:1位前导位+2位错误/警告位+18位角度数据。
这种情况下,用标准SPI硬件收发会比较尴尬,因为STM32的SPI数据长度是固定的,早期F1系列只支持8位或16位帧格式。我的处理方式是把数据拆成3个字节来读:
uint8_t buf[3]; uint16_t angle_raw; uint8_t IC_MU_ReadFrame(uint16_t *angle) { GPIO_ResetBits(ENC_CS_GPIO_PORT, ENC_CS_PIN); for (volatile int i = 0; i < 8; i++); // CS拉低后等待, 让芯片准备好 HAL_SPI_Receive(&hspi1, buf, 3, 10); GPIO_SetBits(ENC_CS_GPIO_PORT, ENC_CS_PIN); // 假设帧结构: [bit20] 前导位, [bit19] 错误标志, [bit18] 警告标志, [bit17:0] 角度数据 // 三个字节拼成21位 uint32_t raw_frame = ((uint32_t)buf[0] << 16) | ((uint32_t)buf[1] << 8) | buf[2]; // 检查错误位 if (raw_frame & (1 << 19)) { return 1; // 错误 } *angle = raw_frame & 0x3FFFF; // 取低18位 return 0; }这段代码里有几个细节值得展开说一下。其一,HAL_SPI_Receive在CubeMX生成的代码里,如果配置的是全双工模式,接收过程中主机会持续发送0xFF;IC-MU在CS拉低后收到第一个时钟下降沿就开始返回数据,主机发什么不在乎,所以用Receive函数是可行的。其二,三位字节拼接的时候注意移位方向——芯片一般使用MSB First,也就是第一个字节是数据帧的最高位,所以要在代码里把第一个字节移位到最高位。
如果芯片输出的是14位数据,读两个字节就够了,然后做掩码和移位。这里用一个共用体或者位操作都可以,但为了可读性,我倾向于显式的<<和&运算,不要偷懒用指针强转——字节序问题在ARM上很容易搞乱。
3. 码盘数据处理:从原始码值到稳定角度
3.1 单圈角度值怎么算
拿到14位或18位的原始角度码值之后,第一步就是把它换算成我们熟悉的角度值。计算公式非常简单:
angle_raw / 2^N * 360°比如14位精度,最大码值是16384,那么码值4096对应的角度是4096/16384×360°=90°。在嵌入式代码里,浮点运算虽然方便,但会拖慢速度,特别是在控制环路里。我通常的做法是先把原始码值左移一定位数变成Q格式定点数,或者在精度要求不高的场景直接用整数运算:
// 14位编码器, 码值范围0~16383 // 角度 = raw * 360 / 16384 uint16_t angle_deg_x100 = (uint32_t)angle_raw * 36000u / 16384u;这样算出来的angle_deg_x100是角度的一百倍,比如12345表示123.45°。在LCD上显示或者通过串口上报时,只需要分别取商和余数即可。这是我在实际项目中最常用的处理方式,既保留了精度又避免了浮点运算的开销。
还要注意一点:IC-MU芯片输出的角度方向是和磁铁旋转方向相关的。如果安装后发现角度变化方向和实际转向相反,通常不需要改硬件,在软件里对原始码值取反即可:
angle_raw = (1 << N) - angle_raw;这个取反操作会带来一个新的问题——取反后码值范围变成了0到2^N-1,但是0点位置会偏移半个码值吗?不会,只是改变了零点的物理方向,而且依然保持了单圈码值的连续性,不会出现跳变。实际使用当中,配合零位校准使用完全没有问题。
3.2 多圈数据的软件拼接逻辑
单圈绝对值编码器在断电重启后能拿到0°到360°之间的绝对角度,但是对于机械臂关节、丝杠滑台这类需要记录累计行程的场合,单圈的值不够用。如果不想花钱买多圈绝对值编码器,可以在软件里做多圈拼接。
多圈拼接的核心思想是:相邻两次采样之间,单圈码值的差值就是这一段时间内转过的角度增量。把这个增量累加起来,就得到了相对某个起点的累计角度。关键代码如下:
int32_t multi_turn_count = 0; uint16_t last_raw = 0; void Update_MultiTurn(uint16_t current_raw) { int16_t delta = (int16_t)(current_raw - last_raw); // 处理单圈跨越的情况 // 如果delta超过半圈,说明跨过了0点 if (delta > 8192) delta -= 16384; else if (delta < -8192) delta += 16384; multi_turn_count += delta; last_raw = current_raw; }这段代码的原理不难理解:current_raw - last_raw的正常变化范围应该在半圈之内。如果两次采样间隔内转角超过半圈,那就无法判断是正转了(16384-delta)还是反转了(-delta),所以采样率必须高到足以保证相邻两次的角度差小于半圈。这个约束我在实际项目中用2kHz的中断轮询,配合2.25MHz的SPI时钟,完全不成问题。
拼接完成后的multi_turn_count是以码值为单位的累计值,如果14位编码器转了10圈,multi_turn_count的范围就是±163840。想显示成角度,再乘以360/16384就行。这里有个细节要提醒:multi_turn_count必须定义成有符号的int32_t,如果你用uint32_t,反向转动时会出现巨大的正数跳变。
3.3 滤波和防抖处理
码盘数据处理的最后一道工序是滤波。IC-MU虽然内部做了数字处理,但在电机启停瞬间、电磁干扰较强的环境下,SPI读回来的码值偶尔还是会跳一两下。这些毛刺如果直接送进速度环或位置环,会在控制端产生不必要的抖振。
我实测下来最有效的方案是滑动窗口中值滤波。取最近5到9次读数的码值,排序后取中间值作为有效值。中值滤波的好处是能干净利落地扔掉脉冲毛刺,同时不引入像均值滤波那样的相位滞后。
#define FILTER_SIZE 7 static uint16_t filter_buf[FILTER_SIZE]; static uint8_t filter_idx = 0; uint16_t Encoder_MedianFilter(uint16_t new_raw) { uint16_t tmp[FILTER_SIZE]; filter_buf[filter_idx] = new_raw; filter_idx = (filter_idx + 1) % FILTER_SIZE; memcpy(tmp, filter_buf, sizeof(tmp)); // 简单冒泡排序,数据量小,性能可接受 for (int i = 0; i < FILTER_SIZE - 1; i++) { for (int j = i + 1; j < FILTER_SIZE; j++) { if (tmp[j] < tmp[i]) { uint16_t t = tmp[i]; tmp[i] = tmp[j]; tmp[j] = t; } } } return tmp[FILTER_SIZE / 2]; }如果项目里用的是F103这类主频72MHz的芯片,在1kHz读取频率下跑一个7点排序完全没有压力。不过需要注意,滤波器的引入会带来群延迟——7点中值滤波大约滞后3~4个采样周期。在1kHz采样率下也就是3~4ms,对大多数位置环来说是可以接受的。
更高级的用法是同时维护两个滤波器通道:一个输出给显示和上位机用,延迟无所谓;另一个不做滤波,直接给电流环或者速度环前馈用——前提是你对信号质量足够有信心。我在实际项目里就是一脚踩两条船,上位机数值稳定,控制环响应也足够快。
4. 实测踩坑记录:波形、时序和机械安装
4.1 片选时间过短导致的数据错位
这是我调试过程中最折磨人的一个问题。现象是:用逻辑分析仪抓到的波形完全正常,SCK和MISO的数据位都对得上,但程序打印出来的角度值会在几个正常值之间随机跳动,偶尔还会出现一个完全离群的数值。
排查过程是这样的:先用SPI连续读1000次,把数据通过串口打印出来观察跳变规律,发现数据错误集中出现在芯片从休眠模式恢复后的前几帧。我怀疑是CS拉低后芯片还没准备好,查阅手册后确认,IC-MU在CS下降沿到第一个有效数据位之间存在一个典型值为几个微秒的建立时间。
解决办法是在CS拉低之后加一个微延时。用for循环的空转指令即可,实测约16个空循环在72MHz主频下大概是几微秒。这个延时看起来不起眼,但在高速读取时会消耗不少CPU时间。更好的做法是用单片机的一个定时器产生PWM来驱动片选,或者直接降低SPI时钟频率来"挤"出这段建立时间。对于简单项目,微延时最省事,我在项目中就是这样处理的。
4.2 时钟极性和相位选错时的表现
SPI模式的CPOL/CPHA如果选错,很多人以为读回来的数据会完全乱掉。但实际上IC-MU这类芯片的表现往往更隐蔽——数据在大多数情况下看起来是对的,只有在特定角度区间才会出现偶发的码值偏差。
原因是当采样沿落在数据切换沿附近时,采样电压正好处于中间电平,芯片输入端的施密特触发器可能采到下一bit也可能采到上一bit,造成某个位置的码值随机偏差。如果你只用低速旋转测试,可能完全发现不了这个问题;等系统跑高速时突然出问题,就会非常难查。
这个坑的排查方法很简单:用逻辑分析仪同时抓SCK和MISO,对比数据手册的时序图,确认采样沿是否稳定落在数据位的中间位置。我在调试过程中确认到,IC-MU在Mode 0下数据在下降沿变化、上升沿稳定,如果配置Mode 1就会出问题。
4.3 上电不稳定和磁铁安装偏心问题
SPI时序调通、数据也稳定之后,还有一个藏在物理层面的坑要处理:芯片上电后的输出稳定时间。
IC-MU内部有自检电路和数字滤波,上电瞬间磁场方向未稳定时输出的角度码值是不可信的。手册一般会给出一个上电稳定时间,通常是几毫秒到几十毫秒。如果主控复位后立刻去读编码器,读回来的初始角度可能是错的,而多圈拼接逻辑又会把这个错误的初始角度当成基准,导致整个系统零点偏移。
我的做法是在初始化时延时100ms之后再读第一次编码器值,并且连续读5帧确认数据没有跳变,才认为编码器真正就绪。这个“确认帧”的思路对于所有绝对编码器都适用。
另一个容易被忽略的问题是磁铁的安装偏心。IC-MU的精度极限是在磁铁几何中心和芯片感磁中心重合时达到的。如果磁铁安装偏心超过一定范围,输出角度会叠加一个周期性误差——每转一圈误差波动一个周期。对于要求不高的应用可以直接忽略,但如果你的转台定位精度要求很高,这会导致实际定位误差远大于编码器标称精度。
我用一个简单办法验证偏心:把电机匀速旋转,串口记录一圈内的角度误差。如果误差曲线呈现明显的正弦波,基本可以断定是偏心问题,需要重新设计磁铁座的安装精度,而不是去调滤波参数。
结束前的最后一段
做完这个项目后我最大的感悟是,磁绝对值编码器的数据链路看起来简单——不就是SPI读两个字节嘛——但真正做得稳定可靠,需要在时序、数据校验、多圈拼接和机械安装几个维度同时下功夫。我把这套流程沉淀成了一套固定的调试模板:先拿逻辑分析仪确认时序,再读裸数据做统计分布分析,最后才上滤波和控制环,每一步走稳了,后面问题自然就少了。如果你也正在调IC-MU或者其他类似磁编码器芯片,希望这篇文章能帮你少走几步弯路。