上个月调一个云台的角度环,低速时电机像打摆子一样来回抖,串口打印出来的编码器角度值在某个区间跳了几十个码,滤波、屏蔽、降压全都折腾了一遍,最后才发现问题出在CS拉低之后SCK起得太快,MT6835的MISO还没建立稳定,主机就已经采样了。这种问题在STM32读SPI磁编码器的项目里非常典型。MT6835本身是一个很稳定的芯片,难调的地方全在SPI时序细节和数据处理上。这篇我就按自己的调试顺序,把怎么读、怎么验证、读出来怎么用,以及几个容易掉进去的坑一次说清楚。
1. 先把MT6835的输出机制吃透:它是一颗SPI从机,不是模拟传感器
1.1 磁编码器为什么能直接读出绝对角度
MT6835属于磁角度编码器,芯片内部有磁阻或霍尔阵列,可以感知贴在转轴上的磁铁产生的旋转磁场。磁铁转动时,芯片内部会得到两路正交的正余弦信号,再通过CORDIC算法算出当前角度。这里的关键是“绝对角度”——上电那一刻,你就能读到0到360度范围内的一个确定位置,不需要找零位、不需要增量累积。
这个特性对电机FOC控制、云台、机器人关节非常友好。增量式编码器断电后再上电就不知道转子在哪,必须做一次找零动作;而磁编码器只要磁铁装好,一上电就什么都知道。
还有一个容易被忽略的优势:磁编码器对灰尘、油污不敏感,结构上允许一定程度的偏心,比光栅编码器耐造。MT6835的分辨率一般是14位,也就是一圈有16384个码,每个码对应的角度是:
360 / 16384 ≈ 0.02197度
这个精度对大多数运动控制场景已经够用了。追求更高分辨率当然可以上18位、20位的芯片,但控制环路能不能用上那么多位,还要看机械刚度和采样周期,不是位数越高越好。
1.2 SPI接口的访问方式:一次16位时钟换一个角度
MT6835把自己当作一个SPI从机,主控要拉低片选CS,然后给出16个SCK时钟,芯片就会把当前角度通过MISO线送回来。也就是说,读一次角度本质上就是一次完整的16位SPI全双工交换。
MOSI线上发送什么?对于标准的读角度操作,通常是发送0x0000命令。从机在收到命令后,把角度数据放在MISO线上返回。整个交互过程就是一问一答,非常直接。
这里要特别说一下返回数据的位定义。按我手头这批MT6835的数据手册,返回的16位数据大致是这样分布的:
- bit15:偶校验位
- bit14:状态位或保留位,正常读出来是0
- bit13到bit0:14位角度数据
不同批次、不同固件版本对高位定义可能会有细微差别,所以代码里不要写死,先对照手册确认一遍。我见过有人把高位状态位当成角度位参与运算,结果角度输出像被人为加了偏移一样,怎么校准都校不回来。处理方式很简单:每次拿到原始帧先做掩码操作,只保留低14位:
uint16_t angle_raw = frame & 0x3FFF; /* 只留 bit13:0 */这一步看起来不起眼,但能省掉后面一大堆莫名其妙的调试时间。
1.3 为什么选SPI而不是PWM或ABZ输出
MT6835这类芯片通常还支持PWM输出、ABZ正交输出等接口。有些工程师图省事直接用PWM读角度,接一个定时器输入捕获就能算。但从控制角度讲,SPI有几个不可替代的优势:
- 读取速度快,几十MHz的SPI时钟足够支撑高频率的角度采样
- 能够直接读到绝对角度,不需要PWM周期内的高电平计时
- 可以附带校验位和状态位,方便判断数据是否有效
- 一次SPI事务能同时传递命令和数据,扩展性强
ABZ接口适合那些已经有正交解码器硬件的系统,但它本质上是增量式的,断电后位置信息丢失。SPI则没有这个问题。所以如果项目对成本不太敏感,SPI接口基本是磁编码器场景下的最优解。
2. 硬件环节的三个高频翻车点:电源、引脚、磁铁
2.1 电源和电平匹配
MT6835的供电电压通常是3.3V,STM32也是3.3V系统,两者直连问题不大。但有些电机驱动板上为了迁就驱动芯片,把逻辑电平拉到了5V,这时候直接连STM32的SPI引脚就有风险。
需要确认一点:STM32的大多数IO是5V容忍的,但“5V容忍”不等于“推荐长期工作在5V”。更稳妥的做法是加电平转换芯片,或者用分压电阻把MT6835侧的MOSI、SCK、CS信号从5V降到3.3V。MISO方向因为是芯片输出3.3V,到STM32这边一般没问题。
另外注意芯片的电源引脚,如果VDD和VIO是分开的,两个都要接对。很多人只给VDD供电,忘了VIO,结果芯片怎么都不工作,MISO上永远是高阻态。这种低级错误用万用表量一下引脚电压就能发现。
2.2 SPI引脚分配和丝印确认
MT6835需要四根线:SCK、MOSI、MISO、CS。这个通用说法没问题,但具体到芯片丝印上要注意,不同厂家的命名习惯不一样,有的叫SDI/SDO,有的叫SDIN/SDOUT。SDI是芯片的串行数据输入,对应主机的MOSI;SDO是芯片的串行数据输出,对应主机的MISO。接反的话,读回来的数据要么全是0xFF,要么全是0x00,或者是毫无规律的乱码。
我自己的经验是:上电后先写一个简单的SPI读函数,把返回的原始帧通过串口打印出来,然后用手慢慢转动磁铁。如果读出来的数值在某个范围变化,说明信号路径基本通了;如果纹丝不动,大概率是MOSI和MISO交叉了,这时候去查原理图比在代码里猜更快。
还有一点,CS片选脚不要图省事直接接到GND。有一些从机确实支持CS一直拉低,但那样做会失去片选控制能力,以后想扩展成多片编码器或者读其他寄存器就很麻烦。用STM32的一个普通GPIO做软件片选,是最灵活的做法。
2.3 磁铁选型与安装是很多人忽视的“软件问题”
SPI读不到正常数据,很多时候不是SPI的问题,而是磁铁根本没装好。MT6835需要的是轴向充磁的圆盘磁铁,直径5到8毫米比较常见。充磁方向一定要是轴向的,也就是磁铁的N极和S极分布在上下两个端面,而不是分布在内外径。搞反了的话,芯片输出的角度信号会很弱,甚至读出来是乱的。
安装时注意三点:
- 磁铁中心尽量对准芯片封装中心的感磁区域
- 气隙控制在0.5到2毫米左右,不要太远也不要贴死在芯片表面
- 磁铁和转轴之间不能有可见的偏心
偏心会导致角度信号谐波误差增大,直观表现是:每转360度,角度误差出现周期性变化,某些位置线性度特别差。程序上可以查表和校准,但最好还是在机械安装阶段就把问题解决,软件校准只是补救手段。
3. CubeMX里的SPI配置,按这样填成功率最高
3.1 关键参数:数据宽度、时钟极性、分频系数
我用STM32F103C8T6加SPI1举例,其他系列配置思路完全一样。在CubeMX里新建工程后,把SPI1的Mode选为Full-Duplex Master,然后重点设置以下几个参数:
- Data Size:16 Bits
- First Bit:MSB First
- Prescaler:先选大分频,比如32分频,算下来SPI时钟大约2.25MHz
- CPOL:Low
- CPHA:1 Edge(对应SPI Mode 1)
CPOL和CPHA这里是重点中的重点。按照MT6835手册给出的时序,数据在SCK的下降沿更新,在上升沿采样,对应的就是CPOL=0、CPHA=1这种配置。实际使用中,你把CPOL配成High、CPHA保持1 Edge也能工作,但一旦采样的沿选错,读回来的数据会整体移位,角度值看起来像是真实值的一半或者两倍,很难第一时间反应过来。
分频系数不要一上来就追求高速。先把SPI时钟压到2到4MHz,跑通整个流程,再用示波器看MISO波形质量,确认没有明显振铃和毛刺以后,再逐步提高时钟。SPI通信速率不是越快越好,尤其在电机驱动板这种电磁环境恶劣的地方,过高的速率只会带来更多误码。
3.2 MISO引脚配置的真坑:不要手动改成输入模式
CubeMX在初始化SPI外设后,会自动把SCK、MOSI、MISO配置成复用推挽输出,即AF_PP模式。MISO虽然是接收数据的引脚,但在SPI外设工作模式下,引脚控制权已经交给外设,从机推挽驱动MISO,主机侧用复用功能接收即可。
有些参考代码里会把MISO设成GPIO输入上拉模式,理由是想让空闲电平稳定在高电平。这在某些场合有效,但在STM32的SPI外设接管下,这种配置反而可能导致采样异常。我自己的建议是:完全信任CubeMX生成的引脚配置,不要在代码里额外改MISO的模式。如果真的怀疑空闲电平不稳定,先查从机芯片的MISO驱动能力和外部上拉电阻,不要在引脚模式上乱动。
3.3 CS片选时序要自己在软件里补
CubeMX不会帮你管理软件片选。CS拉低之后,SCK什么时候开始,取决于你调HAL_SPI_TransmitReceive的时机。如果CS刚拉低就立刻启动SPI传输,MT6835的MISO可能还没来得及从高阻态切换到驱动态,导致第一个bit采到不确定的电平。
我通常会在CS拉低后加一段极短的延时,哪怕几个空指令也行:
ENC_CS_LOW(); for (volatile uint8_t i = 0; i < 4; i++); HAL_SPI_TransmitReceive(&hspi1, (uint8_t*)&tx, (uint8_t*)&rx, 1, 10); ENC_CS_HIGH();这里的延时不是随便加的,它的本质是满足从机的输出建立时间tCSD。不同手册给的参数不一样,没有示波器的情况下先给足余量,跑稳了再去压缩时间。CS拉高之后,两次读取之间也要留一小段时间,让芯片内部更新角度数据。连续高速读的时候,如果发现相邻两次读到的值是同一个数或者跳变很诡异,多半是CS释放得太快,芯片还没来得及刷新。
4. 代码实现的两种方式,以及数据拼装的坑
4.1 16位数据宽度:代码最简洁,但要注意两个细节
配置成16位Data Size以后,一组数据就是16个SCK,HAL库的写法很简洁:
uint16_t MT6835_ReadRaw(void) { uint16_t tx = 0x0000; uint16_t rx = 0; ENC_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, (uint8_t*)&tx, (uint8_t*)&rx, 1, 10); ENC_CS_HIGH(); return rx; }两个容易错的点:
第一,HAL_SPI_TransmitReceive的第三个参数是“数据个数”,不是“字节数”。配置成16位宽时,传1就表示传输1个16位的数据。很多人惯性写成2,结果一次传输了32个SCK,数据读出来完全错位。这是一个非常隐蔽的坑,因为函数不会报错,数据也有变化,但角度值就是不对。
第二,字节序。STM32是小端模式,而SPI外设是按MSB先移出的方式工作。正常情况下rx直接就是bit15到bit0,不需要手动交换字节。但如果发现转轴慢慢转时,角度值不是连续变化,而是每256个码一跳,那说明高位字节和低位字节对调了。这时可以用一个宏交换高低字节:
uint16_t swapped = (rx >> 8) | (rx << 8);判断到底是哪种字节序,用“角度值是否以256为单位跳变”这个方法最快,比对着原理图猜效率高多了。
4.2 8位数据宽度:更保守的兼容性写法
如果你不确定芯片是哪一种位序,或者想兼容后续换芯片的情况,用8位数据宽度分两次收发更直观:
uint8_t tx[2] = {0x00, 0x00}; uint8_t rx[2] = {0x00, 0x00}; ENC_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 10); ENC_CS_HIGH(); uint16_t raw = ((uint16_t)rx[0] << 8) | rx[1];这个写法假设芯片先发高位字节、再发低位字节。如果发现角度值跳变异常,就把拼装顺序反过来:
uint16_t raw = ((uint16_t)rx[1] << 8) | rx[0];8位方式的优势在于,即使以后换了一颗数据格式类似的芯片,只需要修改拼装逻辑,不需要改动SPI外设配置。对于量产项目要预留方案变更余地的场景,这种方式更稳。
4.3 角度换算与掩码处理
拿到raw值以后,第一件事是掩码,只保留低14位,然后换算成角度:
#define MT6835_RESOLUTION 16384.0f uint16_t angle_raw = raw & 0x3FFF; float angle_deg = (float)angle_raw / MT6835_RESOLUTION * 360.0f;如果你发现角度值始终偏离真实角度一个固定数值,而且怎么校准都校不回来,回头检查一下是不是没有掩码,把状态位和保留位都当成角度位了。这个原因在排查列表里出现的频率非常高。
5. 数据乱跳时的完整排查链路:从波形到校验
5.1 不要急着改代码,先上示波器看波形
读回来的角度不对,第一反应往往是回头改代码,但很多问题在时序上,不在代码逻辑里。最有效的做法是直接把示波器探头夹在CS、SCK、MISO三个引脚上,条件是CS下降沿触发,单次抓取16个SCK周期。
看什么?看三点:
- CS下降沿到SCK第一个上升沿之间,MISO是否已经变成有效驱动电平
- MISO上的数据在整个传输过程中是否稳定,有没有在SCK采样沿附近翻转
- CS上升沿是否在最后一个SCK结束之后才出现
如果MISO在SCK采样沿附近还在变化,说明从机输出建立时间不够,或者SPI时钟频率太高。这时候优先降低分频系数,把SPI时钟降下来,再观察波形是否改善。很多“随机跳变”的问题,本质上是时钟太快导致采样点落在数据变化沿上,属于亚稳态问题,不是磁铁问题。
5.2 常见故障现象和排查方向
我把实际调试中遇到的几种典型现象整理成一个表,方便快速定位:
| 故障现象 | 可能原因 | 优先排查方向 |
|---|---|---|
| 读回数据全是0xFFFF或0x0000 | MOSI和MISO接反、从机未供电 | 用万用表量引脚电平,对照原理图查线 |
| 角度值是实际值的一半或两倍 | CPHA采样沿选错,数据移位 | 修改CPHA,观察角度变化 |
| 角度值以256为单位跳变 | 高低字节序颠倒 | 交换高低字节 |
| 固定区间内来回跳几十个码 | CS拉低后SCK启动太快 | 在CS之后加建立延时 |
| 随机跳变且校验失败率很高 | 线路干扰、SPI时钟过快 | 降速、缩短连线、改善地线 |
| 每转一周出现固定位置的误差 | 磁铁偏心或气隙不均 | 机械校准,重新装磁铁 |
5.3 用奇偶校验判断帧是否有效
MT6835的返回帧带奇偶校验位,这是排查干扰的最有力工具。不要只在数据乱的时候才去看校验,而是要把它设计到每个读取流程里。
按偶校验来写一个简单的判断函数:
bool MT6835_CheckFrame(uint16_t frame) { uint8_t cnt = 0; for (uint8_t i = 0; i < 16; i++) { if (frame & (1U << i)) { cnt++; } } return (cnt % 2) == 0; }注意,校验位到底是奇校验还是偶校验,不同的资料可能会给出不同说法。上面这个函数是参考实现,实际用之前一定要拿手册确认校验算法。更稳妥的做法是:先用手缓慢转动磁铁,采集几十帧数据,观察校验结果是否和你预期一致,再决定用奇校验还是偶校验。
校验函数的价值在于定性判断。如果校验失败率很低,偶发一次失败,那可以直接丢弃该帧重读。如果连续失败几十帧,那说明硬件链路有严重干扰,光靠软件丢弃是遮羞布,必须回到示波器找根因。
5.4 机械因素导致的“伪干扰”
还有一种情况,SPI数据一切正常,校验也全过,但角度值就是有周期性波动。这时候要怀疑磁铁和芯片的机械安装。
一个典型的例子:磁铁中心偏离芯片感磁中心0.5毫米,结果每个转动周期里会出现两次明显的角度误差凸起。这种误差用示波器看不出来,因为SPI时序完全正确,纯粹是磁场分布畸变导致的。
处理方式是重新校准机械结构,或者在软件里做误差查表补偿。对于量产项目,软件补偿能省下不少机械装配成本,但前提是先确认误差是重复性的、可复现的。如果每次装完误差都不一样,补偿表就没有意义。
6. 工程化使用技巧:滤波、多圈累计、FOC对齐
6.1 滑动平均滤波以及360度跳变陷阱
对随机噪声,滑动平均是最直接的降噪手段:
#define FILTER_N 5 uint16_t mt6835_buf[FILTER_N]; uint8_t mt6835_idx = 0; uint32_t mt6835_sum = 0; uint16_t MT6835_FilterRead(void) { uint16_t raw = MT6835_ReadRaw(); mt6835_sum -= mt6835_buf[mt6835_idx]; mt6835_sum += raw; mt6835_buf[mt6835_idx] = raw; mt6835_idx = (mt6835_idx + 1) % FILTER_N; return (uint16_t)(mt6835_sum / FILTER_N); }但这个实现有个经典的坑:当角度在358度和1度之间切换时,平均结果会趋近于180度左右,产生一个完全错误的“穿越值”。这是因为角度信号本身的性质决定的,普通的算术平均不适用于圆形变量。
解决办法有两个方向。一个简单的方式是做角度差值判断,如果当前值和上一值的差超过半圈的一半,就认为是跨越了0度边界,把当前值加上或减去16384后再参与平均。另一个更通用的方式是转成sin和cos再平均:
float sin_sum = 0.0f; float cos_sum = 0.0f; for (int i = 0; i < FILTER_N; i++) { float rad = angle_deg[i] * 3.14159265f / 180.0f; sin_sum += sinf(rad); cos_sum += cosf(rad); } float avg_deg = atan2f(sin_sum / FILTER_N, cos_sum / FILTER_N) * 180.0f / 3.14159265f; if (avg_deg < 0) { avg_deg += 360.0f; }这个办法计算量稍大,但在0度边界附近不会出错,适合位置控制里的慢速高精度场景。如果是做速度环,更推荐直接用差分值滤波,而不是对绝对角度做平均。
6.2 多圈角度的累计方法
14位分辨率只有一圈。如果机器要处理多圈场景,比如机器人关节,就需要在软件里自己扩展圈数计数。
思路很简单:记录上一次的角度值,用当前值减上一次值,判断是否跨过零点:
int32_t mt6835_total = 0; uint16_t mt6835_last = 0; void MT6835_UpdateTotal(uint16_t current) { int16_t delta = (int16_t)(current - mt6835_last); if (delta > 8192) { delta -= 16384; } else if (delta < -8192) { delta += 16384; } mt6835_total += delta; mt6835_last = current; }加上圈数以后,多圈位置可以用下面的式子表示:
int32_t multi_turn_position = mt6835_total;这个方案的前提是,单次采样间隔内磁铁转过的角度不能超过半圈,也就是8192个码。对于大多数运动控制场景,这个条件都成立。如果转速非常快,采样频率又跟不上,就只能上更高分辨率或者更快读取周期,否则无法恢复整圈计数。
6.3 在FOC控制里怎么用
MT6835这类绝对角度磁编码器在FOC里最大的价值,是上电就能拿到转子机械角度,配合电机极对数就能计算出电角度,省去了传统增量编码器上电找零的过程。
要特别注意一点:磁铁安装在转轴上的角度,和电机三相绕组的电角度零点,通常不是天然对齐的。所以量产程序里一般会做一次标定,把读到的机械角度和电角度之间的固定偏置存到Flash里,每次上电后减去这个偏置再参与FOC计算。
以极对数为2的电机为例,电角度等于机械角度乘以2,再减去标定偏置:
float elec_angle = fmodf(mech_angle * 2.0f - offset_angle, 360.0f); if (elec_angle < 0) { elec_angle += 360.0f; }如果这个偏置标定不准,最明显的现象是电机出力变小,电流波形不对称。
最后一个经验:不要把SPI读取放到定时器中断里用阻塞方式跑。HAL_SPI_TransmitReceive内部有等待超时机制,在中断里调用会占用大量CPU时间。我最初把读取放到1kHz定时器中断里,结果中断执行时间太长,电机反而更抖。后来改成在RTOS任务里用5kHz频率读取,或者用SPI DMA配合双缓冲,读取周期稳定了,控制效果立刻改善。MT6835本身不是一个难伺候的芯片,真正决定成败的,是你有没有把握住SPI那些看起来不起眼的时序细节。
这里再分享一个验证的小技巧:调试时用串口把原始角度值打印出来,打印频率控制在5到10Hz就够了,然后手动缓慢转动磁铁,观察输出是不是从0线性增加到16383,再回到0。如果这一关能稳定通过,再上PID调试,效率会高很多。我第一次调的时候就因为太心急,一头扎进控制参数堆里,绕了很大的弯路。串口打印看到的数据曲线,很多时候比示波器更能说明问题。