资讯动态

STM32 HAL库读取MT6816磁编码器:SPI通信实现与排错

发布时间:2026/10/21 9:21:25 来源:尧图企业网站定制
做电机控制或者机器人关节的时候总绕不开一个问题怎么把电机转子的绝对角度读回来。光电编码器精度高但贵且娇气接触式的电位器更是没法用。这两年我和几个朋友做项目经常选磁编码器其中MT6816这颗芯片出现频率很高。刚好最近一个电机项目里需要把MT6816的角度数据通过SPI接口接到STM32上用的是标准HAL库。整个过程走下来有些坑确实值得记一笔这篇就把基于STM32 HAL库的MT6816 SPI通信方案完整梳理一遍从选型、接线、协议到代码和排错希望对你有点帮助。如果你正在做FOC矢量控制、云台、机械臂关节或者需要高精度转子位置的场合这篇文章基本覆盖了让STM32通过SPI读回MT6816角度这条链路里所有关键细节。即使是刚接触HAL库的新手跟着文章把CubeMX配置完成、代码移植过去也能很快跑起来。1. 为什么是MT6816磁编码器选型里我比较在意的几件事1.1 磁编码器相比光栅编码器的实际差距先说说为什么选磁编码器而不是光栅编码器。多数做电机控制的人第一反应是光栅编码器分辨率高、精度好确实如此但光栅编码器对安装要求极高码盘稍微被油污灰尘遮住一点读数就可能跳变。磁编码器就没有这个毛病芯片和磁铁之间只需要保持一个相对稳定的气隙一般2到3毫米以内都能正常工作抗污染能力很强而且体积非常小可以直接贴在电机轴端背面。MT6816属于其中比较能打的一类。它内部是霍尔阵列加上信号处理电路输出的是14位绝对角度也就是一圈分成了16384份理论分辨率差不多0.022度。对FOC控制来说这个分辨率已经完全够用了大多数无感算法实际使用角度传感器时14位数据比10位或12位的芯片在低速下的表现好很多。1.2 SPI接口相比ABZ、UVW、PWM输出的优势MT6816并不只有SPI一种输出方式它还支持ABZ增量输出、UVW换相信号以及PWM输出。很多做电机控制的同学习惯直接用ABZ或者UVW因为接上就能产生编码器信号。但我个人更推荐用SPI原因很直接ABZ和UVW都是增量性质的信息断电重启之后你得重新找零位而SPI读出的是绝对角度上电即知道当前转子位置这对于FOC启动非常重要。另一个场景是云台和机械臂。这类设备对线缆根数敏感ABZ接口至少需要6根线UVW也需要好几根而SPI这边只需要四根线CSN、SCLK、SDI、SDO再加上电源和地也不过六根。系统一紧凑接口数量就成了很现实的约束。SPI的另外一个优势是数据可靠性ABZ在长线传输时脉冲容易被干扰但SPI传输的是带校验位的数字帧配合奇偶校验位可以及时发现问题。1.3 一张表看清MT6816的关键参数我把自用和查手册过程中比较核心的参数整理成了一张表参数数值/说明分辨率14位一圈16384个计数接口SPI、ABZ、UVW、PWMSPI最大时钟最高约10MHz不同批次略有差异供电电压3.3V/5V均可数字接口逻辑跟随VDD角度更新率与SPI读取频率相关最快速率见手册时序要求工作气隙建议2~3mm磁铁选择径向充磁方式输出模式出厂可能配置为SPI、PWM或ABZ需要确认版本选型的时候最容易忽略的是芯片版本和默认输出模式。有些用户买回来的MT6816默认工作在PWM模式这时候你直接接SPI是读不到数据的需要先通过OTP配置把输出切到SPI或者选择明确标注为SPI版本的型号。后面我会专门讲这个坑。2. 先把硬件接对引脚分配与电路细节2.1 MT6816与STM32的引脚对应关系这部分我不太想画电路图直接用表格描述即可实际接线时按表对号入座MT6816引脚功能接STM32位置VDD电源正3.3V或5V配套滤波电容GND电源地GNDCSNSPI片选低有效任意GPIO输出推荐推挽SCLKSPI时钟SPI对应SCK引脚SDI主机从设备的数据输入SPI对应的MOSI引脚SDO数据输出SPI对应的MISO引脚我在项目里用的是某开发板的STM32F4系列SPI1引脚刚好排在一起PA5、PA6、PA7分别分配给SCK、MISO、MOSICSN放在PB0上。如果你换了其它型号只要CubeMX里重新映射一下引脚就行代码逻辑完全一样。2.2 电源、去耦和片选上拉这些容易被忽略的点MT6816虽然支持5V供电但SPI逻辑电平会跟着供电电压走。如果MCU是3.3V我建议直接给MT6816也用3.3V避免电平不匹配的问题。如果项目里必须用5V给传感器供电那么SCLK、SDI、CSN这三个方向由MCU到芯片的引脚都要注意电平匹配最省事的办法还是统一用3.3V。芯片VDD附近我放了两个电容一个10uF钽电容加一个100nF陶瓷电容这是为了应对电机启停造成电压波动的问题。选磁编码器有一个通病就是它对电源噪声敏感电源纹波大的时候角度数据会出现周期性波动。这个现象早期排查时特别容易误判成算法问题实际上源头只是电源不干净。CSN引脚上最好加一个10kΩ上拉电阻让它在空闲时稳定在高电平。虽然STM32 GPIO初始化为推挽输出高电平也没问题但加个上拉电阻能避免上电瞬间GPIO配置未完成时CSN处于浮空状态这种瞬间的误触发会让芯片进入错误的协议模式。2.3 走线长度和地线处理的实战建议SPI本身是同步串行协议对时序的要求不算苛刻但放在电机控制器这种环境里还是要注意。如果电机和MCU板子靠得近传感器线缆最好用带屏蔽的双绞线屏蔽层单端接地。我之前在无刷电机的三相线旁边走过SPI线缆电机一加速角度数据就跳好几个LSB后来把线缆挪开并加上屏蔽才稳定。另外MCU板和电机驱动器之间的地线一定要连好。共地不良会导致SPI通信偶发失败而且现象非常随机用示波器看SCLK和MISO都正常但读回来的值偶尔是坏的。这种情况下先量一下两个板子之间的地线压差超过0.3V就说明地线处理不合格。3. MT6816的SPI协议命令帧、返回帧和角度怎么拼3.1 芯片SPI事务的基本单位MT6816在SPI模式下每一次通信的基本单位是16bit也就是主机在这条线上通过MOSI发出的数据是16bit从机在MISO上同时回传的也是16bit。这和很多寄存器型SPI芯片不一样那些芯片通常以字节为单位操作而MT6816需要你按16bit的粒度去组织收发。常说发8位收8位在这个芯片上是不完全对的。实际代码里你经常看到的是这样的形式uint8_t tx_data[2] {command_byte, 0x00}; uint8_t rx_data[2] {0x00, 0x00}; HAL_SPI_TransmitReceive(hspi1, tx_data, rx_data, 2, 10);这里一次交换两个字节正好16bit。这样做并不是某种多余的封装而是芯片协议本身要求如此。如果只发一个字节就拉高CSN读回来的往往是无效数据。所以后面写驱动的时候我会把一次事务两个字节当成默认操作。3.2 读角度需要操作的寄存器位置角度数据在芯片内部不是直接放在一个寄存器里的而是以两个字节分布在相邻地址上。通常在MT6816手册里你读角度数据时需要关心0x03和0x04这两个寄存器地址一个保存14位角度的高字节部分另一个保存低字节部分的少量有效位。访问方式也比较直接主机先通过MOSI发出要读的寄存器地址芯片在返回帧里把对应寄存器的值放到MISO链路上。这里有个细节是命令字节和返回的数据并不是同一个节拍对应的有些芯片是命令发出后下一个帧才返回真正的数据但MT6816的这一系列芯片通常在一个16bit事务内就能返回对应地址的数据。所以代码里要注意从返回帧的哪个字节位置取值不同批次芯片在帧内字节排列上可能有细微差别实际调试时以手册为准。3.3 14位角度如何拼接以及奇偶校验的处理角度数据是14位精度也就是说一圈360度对应16384个LSB。当我们把两个字节都读回来后需要做移位和拼接。很多现成例程会写成这样uint16_t raw ((uint16_t)rx_data[0] 8) | rx_data[1]; uint16_t angle raw 2; // 取高14位把16bit原始数据右移两位得到的就是14位角度值。注意不要直接返回raw至少要做一次 0x3FFF的掩码操作把超出14位的高位清掉同时也能避免后续算法被无用位干扰。MT6816的返回帧里还有奇偶校验位用于校验数据在传输过程中有没有位翻转。调通基本读取之后我建议把奇偶校验也加上代码逻辑很简单统计接收到的16bit数据中1的个数判断是否为偶数或奇数按手册定义。如果校验失败这次读取可以舍弃等下一次采样再读。这样做在电机大电流干扰的环境下明显减少异常角度值进入控制环的概率。4. 用CubeMX把SPI配置好从时钟树到模式参数4.1 时钟树里SPI时钟频率到底怎么算STM32的SPI外设时钟不是直接等于CPU主频的它挂在APB总线上。比如F4的SPI1挂在APB2上F1的SPI1也挂在APB2上而SPI2/SPI3通常挂在APB1上。APB总线频率往往又需要从系统时钟预分频得到所以在CubeMX的Clock Configuration里先把APB时钟确认好再看CubeMX自动算出来的SPI Baudrate是否满足你的需求。我习惯直接在CubeMX左侧选中SPI1然后打开外设参数配置界面软件会根据当前时钟树自动算出最终SPI时钟。如果速度不合适修改APB总线预分频器或者SPI Baudrate Prescaler就行。4.2 SPI参数怎么填全双工、8bit、MSB必须一致下面是我在CubeMX里最终使用的SPI参数做一个参考配置项数值ModeFull-Duplex MasterData Size8 BitsFirst BitMSB FirstPrescaler根据目标速率调整CPOLLowCPHA1 EdgeNSSDisable软件控制Data Size选8Bit是因为我们前面说了按字节收发First Bit一定选MSB First如果选成LSB First地址和数据位全都会被反转读出来的值完全不对。CPOL和CPHA我这里填了CPOLLow、CPHA1 Edge也就是SPI Mode 0这一步看起来简单实际上最容易出问题。4.3 SPI Mode 0还是Mode 3以及NSS怎么处理才是重点MT6816的数据手册会明确给出它的SPI模式实际上我接触过的多数盘装都需要在Mode 0附近工作。但不同成品的模块板上可能加了反相器或者改了时序导致你用Mode 0读出来全0而换成Mode 3反而正常。处理这种情况最快的方法是写一个很小的测试函数把角度寄存器读回来如果低位一直在变就说明时序对了。NSS这个配置项要格外说一下。把芯片的CSN引脚接到STM32的硬件NSS引脚上理论上也可以工作但我从来不让HAL库去自动控制NSS。原因是MT6816这类的片选信号需要在每次事务前后精确保持低电平和恢复高电平如果硬件NSS被SPI外设自动拉低拉高访问时序不够灵活尤其是后面要连续读写多个寄存器时会麻烦。所以我一直使用GPIO软控CSNCubeMX里NSS选择Disable然后在初始化代码中手动控制片选引脚的高低电平。5. HAL库代码实现读角度函数与DMA优化5.1 最简版本写一个阻塞式读角度函数确定CubeMX配置没问题以后生成工程然后直接定义一个读角度函数。我用的是标准HAL库的HAL_SPI_TransmitReceive这个函数是阻塞式的适合前期调试代码最直观#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_ReadAngle(void) { uint8_t tx[2] {0x03, 0x00}; uint8_t rx[2] {0x00, 0x00}; uint16_t raw 0; MT6816_CS_LOW(); HAL_SPI_TransmitReceive(hspi1, tx, rx, 2, 100); MT6816_CS_HIGH(); raw ((uint16_t)rx[0] 8) | rx[1]; return (raw 2) 0x3FFF; }这段代码的意图很明确一次函数调用完成一次完整的事务CSN在事务开始时拉低传输两个字节结束后拉高。如果你想读0x04寄存器直接把tx[0]改成0x04然后看返回的rx[0]或rx[1]哪个是有效数据。第一版驱动建议先这样跑起来把数据读到后打印到串口确认角度随电机旋转而变化再继续做优化。5.2 片选时序里隐藏的CSN低电平持续时间要求上面调用里CSN拉低之后立即开始传输传输完成后立刻拉高看起来没啥问题但有一个细节芯片在CSN拉高之后需要一点时间内部处理数据两个连续SPI事务之间不能没有间隔。如果你的主程序没有做延时在一个极短的循环里反复调用MT6816_ReadAngle芯片有可能处理不过来导致某几次返回的数据是上一条命令的旧值或者无效值。我之前在一个快速速度环上踩过这个坑循环里读角度的频率在几十kHz结果角度曲线偶尔冒出一个不合理的跳变点。后来翻手册才发现CSN释放后有一个最小间隔要求因此在高速连续读取时建议在CSN拉高之后加一个非常短的延时或者直接让SPI时钟降一点留出足够的处理窗口。单纯阻塞式轮询里也可以加个简单的u秒级延时函数大多数场景下能解决。5.3 用DMA方式做定时连续采样阻塞式读一个角度只需要几十微秒单独看着不慢但如果你还要读取电流ADC、跑FOC算法每多一次阻塞都可能挤占控制周期。我一般会改成DMA版本把读取交给硬件外设CPU不用等数据传完。先简单介绍一下思路每次启动一次DMA传输之前手动拉低CSN然后调用HAL_SPI_TransmitReceive_DMA并注册回调函数。在回调中拉高CSN并对收到的数据做处理。如果需要定时触发可以用一个基本定时器周期性产生中断在中断里启动DMA传输这样采样频率可以做到完全固定对后续滤波融合特别有好处。volatile uint16_t mt6816_raw_data 0; void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { uint16_t raw ((uint16_t)rx_buffer[0] 8) | rx_buffer[1]; mt6816_raw_data (raw 2) 0x3FFF; MT6816_CS_HIGH(); } } void MT6816_StartDMARead(void) { rx_buffer[0] 0x00; rx_buffer[1] 0x00; MT6816_CS_LOW(); tx_buffer[0] 0x03; tx_buffer[1] 0x00; HAL_SPI_TransmitReceive_DMA(hspi1, tx_buffer, rx_buffer, 2); }需要注意的是DMA模式下tx_buffer和rx_buffer必须是全局变量或者静态变量不能是局部变量因为DMA传输是异步的函数返回时局部变量已经销毁数据缓冲区就悬空了。这一点新手非常容易踩值得专门记住。6. 实测数据与踩坑记录6.1 角度随机跳变的排查过程我调试时遇到过这样一个现象电机静止不动角度数据始终在某个值附近抖动抖动的幅度并不是1到2个LSB而是几十个LSB看起来像随机跳变。第一步排查电源。给MT6816的VDD加了一个100nF电容并且用示波器观察发现开关电源供电时纹波峰值有几十毫伏。换成线性稳压供电后跳变幅度确实变小了但还没有完全消失。第二步排查SPI线缆。我的板子上SCLK和MISO两条线靠得比较近用示波器看MISO的时候能观察到明显的耦合噪声。把这两条线分开缩短走线距离跳变问题基本消失。所以如果你的角度数据在静止时跳动明显优先怀疑电源和布线不要急着怀疑芯片本身。6.2 读回来的数据全0或全1时怎么定位全0的情况比较常见多半是CSN没被真正拉低。如果你用的是硬件NSS查一下CubeMX里是否设成了软件模式很多例程默认会配成硬件NSS但实际CSN引脚并没有被正确拉到低电平。还有一种可能就是MT6816芯片版本默认工作在PWM模式SPI接口根本没有被激活这时无论如何收发得到的都是0。全1则更偏向于MOSI和MISO方向接反。MT6816上的SDO一定要接STM32的MISOSDI接STM32的MOSI。接线端子加上防反插设计之前我至少有一次因为转接板的SDO/MISO顺序印反导致一直读回全1耗费了不少时间。6.3 零位校准和过零点问题的实战处理角度数据读回来并不代表机械安装角度就是对的。电机装到设备上以后转子零位和磁编码器的零位往往有一个固定的角度偏移。解决方法是程序启动时让转子找一个已知的机械位置记录此时的传感器读数作为零位偏移量以后每次读到的角度减去这个偏移量。减法过程也不是直接用int16相减就行。0x3FFF是最大值角度继续增加时会跳回到0如果当前值小于零位偏移量直接减会得到负数。简单处理方式是int16_t angle_after_zero (int16_t)(angle - zero_offset); if (angle_after_zero 0) { angle_after_zero 16384; }这样保证最终角度在0到16383之间。如果后续还要计算角速度就不能直接用差值除以时间了因为角度过零点会产生一个巨大的假速度。这时候需要做环形差分处理把两个角度的差值限制在-8192到8192之间再除以采样周期这才是正确的速度计算方式。关于滤波手动旋转时数据很稳但电机大电流运行时角度噪声仍然会变大。我的经验是先做一次中值滤波取最近3个原始角度排中然后通过一个带宽合适的一阶低通滤波器。不过低通滤波会给FOC速度环引入滞后带宽要控制在100Hz到500Hz左右具体数值取决于你的系统响应要求不能照搬。我习惯先把原始数据、滤波后数据都通过串口打印到上位机观察一段时间后再确定滤波强度这样比盲调参数靠谱得多。最后再分享一个调试技巧用定时器每隔10ms触发一次角度读取并把读到的14位原始角度实时画成散点图。如果点密集且连续说明SPI通信链路和芯片本身都正常如果曲线出现断崖、毛刺多半是单次通信错误或者时序问题。我每次换一块新的MT6816板卡、换一段新的SPI线缆都会先用这个办法做一个快速验证确认链路干净后再去动算法。否则一上来就调PID出现问题根本分不清是传感器问题还是控制算法问题。这个习惯帮我省下过很多查了半天发现是线松了的时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑