如果你刚拿到一块AS5047P磁编码器兴冲冲用STM32CubeMX配好SPI写了几行代码准备读角度大概率会经历这么一出发命令之前信心满满发完命令读回来一串0xFFFF于是开始怀疑芯片是坏的、杜邦线是假的、甚至怀疑自己买了个假开发板。这个场景我太熟了因为我自己就是这样过来的。这篇避坑指南就围绕一件事展开用STM32CubeMX把SPI配置好把AS5047P的寄存器读写链路彻底打通最终稳定输出角度。适合刚开始接触磁编码器、对SPI协议还不算特别熟、想直接把项目跑起来的读者。我会把CubeMX关键参数、SPI帧格式、代码封装、踩坑排查一条线讲完最后附上一些实际项目中的小技巧。文章里的代码以STM32F103为例但思路对所有STM32系列通用。1. 先搞清楚AS5047P这颗芯片到底怎么回事1.1 为什么选磁编码器而不是光电编码器AS5047P是ams OSRAM原奥地利微电子出的一款14位磁角编码器核心原理是芯片内部集成了霍尔传感器阵列通过检测上方磁铁旋转时磁场方向的变化解算出绝对角度。和光电编码器相比它最吸引人的地方是非接触、不怕油污粉尘、安装容差大。光电编码器对码盘和光轴的同心度要求很高稍微歪一点就会输出抖动而磁编码器只要磁铁中心和芯片中心大致对得上角度数据就能稳定出来。它的分辨率是14位也就是一圈16384个刻度换算到角度大概是360 / 16384 0.022度。虽然精度和高端光电编码器没法比但在绝大多数电机反馈、云台角度、机械臂关节场景下完全够用。加上单圈绝对角度输出上电就知道轴在什么位置不需要找零点归零项目初期非常省心。AS5047P还集成了PWM输出和ABI增量正交输出如果你不想用SPI读角度直接读PWM占空比也能拿到角度。不过PWM输出的角度更新率受载波频率限制通常不如SPI读寄存器快。我实际使用下来最常用、最灵活的方式还是SPI这也是本文要展开的内容。1.2 AS5047P的SPI工作模式与寄存器地图AS5047P在SPI总线上作为从机存在主机是STM32。芯片支持最大10MHz的SCK时钟支持SPI Mode 1CPOL0CPHA1这两个数字后面会反复提到。芯片内部有一组16位寄存器角度、磁场幅值、AGC增益、诊断信息都以寄存器形式暴露给主机。你不需要关心它内部CORDIC算法怎么算的只需要读写对应地址就行。下面是几个我实际项目中用得最频繁的寄存器地址寄存器名说明0x0000NOP空操作用于同步或热身0x0003DIAAGC诊断寄存器含AGC增益和CORDIC溢出标志0x0014MAGCORDIC幅值可以间接判断磁场强度0x0016ANGLEUNC未经过零位补偿的原始角度0x0017ANGLECOM经过零位和非线性补偿后的最终角度0x0018AGC单独的AGC增益值0x001BCTRL1控制寄存器1用于配置接口特性读角度最常用的就是读0x0017ANGLECOM这个寄存器返回的就是你最终要用的14位角度数据。0x0016返回的是未补偿角度两者只有在设置了零点后才会体现差异。我第一次用的时候直接在0x0016上卡了半天发现两个寄存器读出来的值不一样还以为是芯片坏了其实只是补偿开关的问题。2. CubeMX里SPI配置最容易翻车的几个参数2.1 SPI Mode 1别选成Mode 0这是AS5047P最经典的一个坑比接线错误还隐蔽。很多人的惯性思维是SPI都是Mode 0于是CubeMX里默认参数没动编译下载后发现读回来的数据要么全是0xFF要么偶尔蹦出一个看似正常的数字但下一次又变了。AS5047P明确要求SPI工作在Mode 1也就是CPOL0、CPHA1。CubeMX里对应两个下拉选项Clock Polarity (CPOL): LowClock Phase (CPHA): 2 Edge如果你在STM32的老库函数Standard Peripheral Library里配置对应宏是SPI_CPOL_Low和SPI_CPHA_2Edge。为什么要强调这个因为CPHA1意味着数据在SCK的第二个边沿被采样。对于CPOL0来说第一个边沿是上升沿第二个边沿是下降沿。也就是说AS5047P在SCK下降沿采样数据和常见的Mode 0上升沿采样正好错开半个周期。命令发出去没问题但返回数据被采样时电平还没稳定读出来自然不对。我建议在CubeMX配置完成后打开生成的spi.c确认一下hspi1.Init.CLKPolarity和hspi1.Init.CLKPhase这两个字段别只看CubeMX界面。2.2 16位数据、MSB First、软件NSS一个都不能错AS5047P的一帧数据是16位所以CubeMX里的Data Size要选16 bits。如果你的项目里其他SPI设备用8位建议把AS5047P单独挂一条SPI总线或者至少在进行AS5047P通信前动态修改SPI的数据长度否则切换起来很痛苦。First Bit要选MSB First。AS5047P的命令和数据都是从高位开始传输的如果选成LSB First即使时序和模式全对读回来的角度也是镜像乱序的。还有一个极其容易忽略的点硬件NSS。CubeMX中SPI配置界面默认会有一个Hardware NSS Signal选项里面有Disable、Output、Internal等。对于AS5047P我强烈建议选Disable然后用一个普通GPIO手动控制CS片选引脚。原因有两个。第一AS5047P要求严格的一帧帧传输每帧都要CS拉低、传32个SCK、CS拉高帧与帧之间CS必须释放。硬件NSS在某些单片机上是自动脉冲模式的时序不一定符合芯片要求。第二手动GPIO控制CS的时序是完全可控的排查问题也方便一根杜邦线看波形时一眼就能看出CS的拉低拉高是否干净。最后是分频系数。F103的SPI1挂在APB2上72MHz建议分到4.5MHz或2.25MHz也就是分频16或32。AS5047P虽然支持10MHz但如果你用的是杜邦线或者PCB布局一般高速下非常容易偶发位错误。后面我会专门讲这个排查过程。CubeMX里选Prescaler的时候注意看右侧显示的SCK实际频率不要只选倍数。2.3 硬件接线与电平匹配AS5047P供电是3.3VSTM32F103大多也跑3.3VIO电平可以直接匹配。但如果你用的是5V供电的STM32板子或者AS5047P模块上的VDD接了5V那就必须做电平转换最简单的用MOS管双向电平转换模块别硬连。引脚连线如下AS5047P引脚STM32引脚说明VDD3.3V电源需要加100nF去耦电容GNDGND共地别只靠杜邦线最好接同一块铜排CSn任意GPIO片选软件控制平时拉高SCKSPI_SCK时钟SDI/MOSISPI_MOSI数据输入到芯片SDO/MISOSPI_MISO数据输出给主机特别提醒一点AS5047P的SDOMISO是从机输出数据线在CSn为高的时候是高阻态。如果你把MISO接到了SPI总线上而总线上还有其他从机要确保其他从机的MISO也有类似的三态特性否则会互相打架。电源去耦是很多新手忽视的地方。AS5047P内部有ADC采样VDD的纹波会直接串到角度数据里。我实测过供电脚没有100nF电容的时候角度输出在静止状态下能跳动3到4个LSB加上100nF和10uF钽电容后跳动降到1个LSB以内。电容尽量靠近芯片电源脚放。磁铁安装也要注意AS5047P芯片表面中心有一个圆形参考点磁铁中心要对准这个圆点轴向距离控制在1到3毫米左右看磁铁具体尺寸。装偏了或气隙太大轻则角度非线性畸变重则AGC报警直接输出异常。后面会讲怎么用寄存器判断磁场状态。3. 寄存器读写先弄懂帧格式再写代码3.1 命令帧地址为什么要左移一位AS5047P的命令字是16位它的内部结构分配不是我想当然的那种地址在高字节、控制位在低字节而是bit15到bit1是寄存器地址bit0是读写控制位0表示读1表示写。所以如果你要读0x0017这个寄存器不能直接发送0x0017因为0x0017的bit0是1会把这次操作当成写命令。正确做法是把地址左移一位再与上0xFFFE保证bit0为0uint16_t cmd (0x0017 1) 0xFFFE; // 结果是0x002E这个坑我踩了很久。一开始代码里直接发送0x0017芯片完全没反应我以为是SPI没通拿着逻辑分析仪看波形才发现地址被芯片解读成了另一个寄存器。所有寄存器读操作都要遵循这个规则不是只有ANGLECOM。几个常用读命令的最终字节值列在下面方便你对照寄存器地址读命令字NOP0x00000x0000DIAAGC0x00030x0006MAG0x00140x0028ANGLECOM0x00170x002E顺便说一句网上有些AS5047P例程里直接发送0x3FFF或者0xFFFF来读角度这是利用了芯片内部对特定地址的快捷映射不见得每个版本、每颗芯片的默认行为都一样。如果你跟着老代码抄最好的兜底方案还是老老实实读0x0017文档白纸黑字写得清楚。3.2 响应帧14位数据、奇偶校验和错误标志AS5047P读操作返回的16位数据帧结构也要拆开看这是另一个新手高频翻车点。返回数据格式如下bit15是奇偶校验位even parity覆盖bit14到bit0共15位bit14到bit1是真正的14位寄存器数据bit0是错误标志位error flag。也就是说角度数据不是低14位而是中间14位。很多人的第一反应是raw 0x3FFF就完事了这样会把bit0错误标志混进角度数据里导致角度在某个值附近偶发跳变。正确做法是uint16_t angle (raw 1) 0x3FFF;如果bit0是1说明芯片检测到了通信错误、写保护错误或者内部CORDIC异常这一帧数据应该直接丢弃重读。在正常通信下bit0永远应该是0如果频繁出现1多半是硬件连接或SPI速率有问题。奇偶校验位可以用来验证数据完整性。因为是even parity所以包括校验位在内的整个16位数据中1的个数应该是偶数。检查逻辑写起来很简单uint8_t parity 0; for (int i 0; i 15; i) { if (raw (1u i)) { parity ^ 1; } } if (parity ! ((raw 15) 1)) { // 校验失败 }我一般在产品代码里不开这个校验因为正常通信很少出错校验逻辑本身也会占用CPU时间。但在调试阶段如果遇到偶发读值异常把它打开能帮你最快定位问题。3.3 连续32个SCK的读时序与流水线响应对齐AS5047P的读时序很特殊单发16位命令是读不到数据的。必须在一个CS拉低周期内连续发送至少32个SCK时钟前16个SCK发送读命令后16个SCK发送一个dummy命令全0即可同时从MISO上读出响应数据。更准确地说芯片的响应数据在第二个16位时钟周期内移出。你最终要的角度数据对应的是第二次transmit_receive拿到的那个16位值。我第一次写驱动时以为发完命令读一次就够了CS拉低、发0x002E、马上拉高结果MISO什么都没有。后来翻datasheet看到时序图才明白必须连续32个SCK少一个时钟都不行。实际代码实现有两种风格。一种是用ST的HAL库一次性发两个16位帧uint16_t tx_buf[2] { cmd, 0x0000 }; uint16_t rx_buf[2] { 0, 0 }; HAL_SPI_TransmitReceive(hspi1, (uint8_t *)tx_buf, (uint8_t *)rx_buf, 2, HAL_MAX_DELAY);这时rx_buf[1]才是有效响应rx_buf[0]是上一帧CS周期留下的旧数据直接忽略。另一种风格是分两次调用HAL API先HAL_SPI_Transmit发命令再HAL_SPI_Receive拿数据。也可以但中途可能会插进其他总线的操作导致时序不确定。我建议统一用一次TransmitReceive搞定干净利落。4. 保姆级代码实现从底层寄存器到角度输出4.1 基础SPI收发函数封装假设你在CubeMX里把SPI1配好CS引脚叫ENC_CS生成的代码里GPIO宏是ENC_CS_GPIO_Port和ENC_CS_Pin。下面是完整的基础读写函数/* 定义读方向读命令 bit0 为 0 */ #define AS5047P_CMD_READ 0x0000 /* 定义写方向写命令 bit0 为 1 */ #define AS5047P_CMD_WRITE 0x0001 void AS5047P_CS_Low(void) { HAL_GPIO_WritePin(ENC_CS_GPIO_Port, ENC_CS_Pin, GPIO_PIN_RESET); } void AS5047P_CS_High(void) { HAL_GPIO_WritePin(ENC_CS_GPIO_Port, ENC_CS_Pin, GPIO_PIN_SET); } uint16_t AS5047P_ReadRegister(uint16_t addr) { uint16_t cmd (addr 1) 0xFFFE; /* 地址左移一位bit0 置 0 表示读 */ uint16_t tx_buf[2] { cmd, 0x0000 }; /* 第二个16位是dummy用于产生时钟移出响应 */ uint16_t rx_buf[2] { 0, 0 }; AS5047P_CS_Low(); HAL_SPI_TransmitReceive(hspi1, (uint8_t *)tx_buf, (uint8_t *)rx_buf, 2, HAL_MAX_DELAY); AS5047P_CS_High(); return rx_buf[1]; }这里有个小细节tx_buf和rx_buf必须是uint16_t类型因为SPI配置的是16位数据帧。如果你用uint8_t数组再强转STM32小端模式下高字节和低字节会被拆散出来全是乱码。这也是16位SPI模式下最常见的代码错误。4.2 读ANGLECOM并转换成角度值有了寄存器读函数读角度就很简单了。我通常会封装两层一层返回原始14位无符号整数一层返回浮点角度值。#define AS5047P_REG_ANGLECOM 0x0017 #define AS5047P_REG_ANGLEUNC 0x0016 uint16_t AS5047P_ReadAngleRaw(void) { uint16_t raw AS5047P_ReadRegister(AS5047P_REG_ANGLECOM); if (raw 0x0001) { /* error flag 置位返回一个不可能的角度值表示异常 */ return 0xFFFF; } return (raw 1) 0x3FFF; } float AS5047P_ReadAngleDeg(void) { uint16_t raw AS5047P_ReadAngleRaw(); if (raw 0xFFFF) { return -1.0f; /* 调用方根据返回值判断读值失败 */ } return raw * 360.0f / 16384.0f; }如果你不需要浮点运算可以直接输出原始整数然后在上位机里换算。在一些对时序要求高的控制循环里浮点除法会占用几十微秒这时候用整数转发更划算。实际项目中我常用的是原始整数上位机浮点换算的组合。4.3 用DMA方式替代阻塞读可选如果你做的是电机速度环读角度频率很高比如10kHz以上阻塞式HAL_SPI_TransmitReceive会占用CPU时间。这时候可以用DMA方式让SPI在后台读取CPU可以同时算控制算法。CubeMX里开启SPI1的DMA发送和接收两个通道DMA数据宽度都设为Half Word然后在代码里写一个带回调的读函数volatile uint8_t as5047p_dma_done 0; void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { as5047p_dma_done 1; } } uint16_t AS5047P_ReadRegister_DMA(uint16_t addr) { static uint16_t tx_buf[2]; static uint16_t rx_buf[2]; uint16_t cmd (addr 1) 0xFFFE; tx_buf[0] cmd; tx_buf[1] 0x0000; rx_buf[0] 0; rx_buf[1] 0; as5047p_dma_done 0; AS5047P_CS_Low(); HAL_SPI_TransmitReceive_DMA(hspi1, (uint8_t *)tx_buf, (uint8_t *)rx_buf, 2); while (!as5047p_dma_done); /* 实际项目中建议放到状态机里等待避免阻塞 */ AS5047P_CS_High(); return rx_buf[1]; }DMA方式要注意一点CS拉高操作必须在DMA传输完成之后执行。上面例子里用了一个忙等循环工程上更好的做法是在DMA完成回调里把CS拉高。否则DMA还在搬运数据你就把CS拉了会截断最后一帧。其实如果只是读角度SPI运行在2.25MHz一次读取32个SCK也就14微秒左右除非你真在高频控制环里跑否则阻塞式完全够用。DMA带来的复杂度不小要权衡。4.4 写寄存器方法与验证AS5047P的写命令格式和读命令几乎一样区别只是命令字的bit0设为1同时第二个16位帧不是dummy而是要写入的值。void AS5047P_WriteRegister(uint16_t addr, uint16_t value) { uint16_t cmd (addr 1) | 0x0001; /* bit0 置 1 表示写 */ uint16_t tx_buf[2] { cmd, value }; uint16_t rx_buf[2] { 0, 0 }; AS5047P_CS_Low(); HAL_SPI_TransmitReceive(hspi1, (uint8_t *)tx_buf, (uint8_t *)rx_buf, 2, HAL_MAX_DELAY); AS5047P_CS_High(); }写寄存器是不是真的写成功了我习惯再读一次同一个地址对比值。注意有些寄存器是只读的写什么都不生效这种寄存器在写入后读回的值应该不变。如果你要配置CTRL1这类控制寄存器先读一下原始值再按位与或修改避免覆盖其他功能位。以前有位同事直接往CTRL1写了一个自己想当然的整值结果把芯片的PWM输出功能关了半天没找到原因。5. 实测中踩过的坑与排查思路5.1 读回来一直是0xFFFF这是最多的求助场景。遇到全0xFFFF我建议按下面这个链路一步步排查不要一上来就怀疑芯片。先用逻辑分析仪看SPI四根线的波形。CS有没有正常拉低再拉高SCK是不是连续32个脉冲MISO线上有没有数据翻转如果MISO一直悬空大概率是接线问题SDO和MISO没接对。如果MISO在CS拉高时被外部拉低或者一直拉低检查SDO是否和别的从机MISO冲突。如果波形看起来正常但数据就是0xFFFF接下来查CubeMX配置SPI Mode是不是Mode 1数据长度是不是16位这里如果有任何一项不对读回来的数据多半是0xFFFF或0x0000。如果这些都正确那问题很可能出在命令字上。检查你是不是直接发送了寄存器地址而不是地址左移的读命令。我第一次就是栽在这里。最后还有一种容易被忽略的情况AS5047P在刚上电时SPI接口可能需要几个NOP命令同步。上电后立刻读寄存器前几帧可能返回垃圾值连续发5个NOP读0x0000后再正常读就稳定了。下面这段初始化代码我每次都会加上void AS5047P_Init(void) { for (int i 0; i 5; i) { AS5047P_ReadRegister(0x0000); } }5.2 角度在某个点附近跳变如果静止状态下角度数据隔一小段时间跳一次先怀疑三件事电源纹波、SPI速率过高、响应帧的bit0错误标志没处理。电源纹波造成的跳变通常是不规律的范围可能有好几个LSB处理方式是加强去耦或者软件上做均值滤波。SPI速率过高造成的跳变通常表现为单次读出的值中有一两个bit错误比如角度应该是1000突然变成1008之类。这种可以打开奇偶校验函数如果校验失败的帧频繁出现就要降SPI时钟频率。我实际项目里一开始用4.5MHz SPI杜邦线连接时不时出现偶发跳变。后来把Prescaler调到32分频2.25MHz同时把CS帧与帧之间的间隔拉大一点跳变基本消失。如果你的PCB布局比较好4.5MHz也是可以的。还有一种跳变比较特殊在机械零位附近角度从16383回绕到0的瞬间如果你的处理代码用了if (angle 16383)这类判断或者直接拿原始值做减法会出现一个很大的突变。这是循环计数问题不是芯片的错解决方案是把角度转成带符号的增量值再处理。5.3 奇偶校验不过SPI稳定性排查奇偶校验函数是调试SPI稳定性的神器。我的做法是连续读1000次ANGLECOM统计校验失败次数如果失败率超过万分之一就要紧张了。按下面顺序逐个试第一降低SPI分频。72MHz主频下从16分频改成32分频甚至64分频看失败率是否下降。第二检查杜邦线长度超过20cm的线在高频下就是天线能换短线就换短线能焊接就焊接。第三检查地线回路传感器和MCU的地线要尽量短粗如果通过开关电源供电注意共地。第四在SCK和MISO上并联一个几十pF的电容滤高频干扰但这只是治标。如果这四步都做了还不行检查你的CS控制时序。CS拉低后至少要等几百纳秒再开始SCKCS拉高前也要等待最后一个SCK边沿完全过去。用HAL的TransmitReceive一帧代码这些时序由SPI外设保证问题不大如果你在CS拉低和HAL_SPI_TransmitReceive之间插了其他操作可能导致时序不对。5.4 上电后第一次读数不准的问题AS5047P在上电后内部数字逻辑需要一段时间稳定。我实测过大概几十毫秒后读值才正常但不同批次的芯片可能略有差异。建议主程序启动时延时50ms左右再初始化编码器。另一个现象是首次读ANGLECOM时返回的不是角度数据而是上次下电前锁存的一些随机值。这是因为读操作的响应有流水线特性当前响应其实是上一条命令的结果。我的NOP热身函数就是在解决这个问题连续读几次NOP把流水线清空然后正常读角度。第一次读的角度值无论好坏都丢掉只显示第二次以后的结果这样绝对干净。6. 让编码器更好用的额外处理6.1 角度换算、零点修正和增量式接口选择14位角度原始整数是0到16383换算成0到360度只需要乘以360再除以16384。如果你在单片机里不想用浮点可以输出原始值让上位机换算或者用整型定点算法。AS5047P支持零点修正可以把芯片的0度位置调整到机械结构的任意角度。这个修正值写入零位寄存器后ANGLECOM的输出就会自动偏置。要注意的是有些零点配置涉及OTP编程写一次就不能再改了量产前一定确认清楚需求。调试阶段临时改零位我建议只在RAM里的可写寄存器上做别碰OTP相关位。如果你对角度更新速率要求非常高比如几万转的高速电机SPI轮询会限制上限。这时可以考虑用ABI增量输出脉冲频率可配置支持最高10000rpm以上但这种输出只提供相对位置需要和一个已知参考点配合做绝对定位。两种接口各有适用场景我自己一般低速绝对角用SPI高速电机换向用ABI。6.2 磁场强度自检读MAG和DIAAGC装好磁铁后别急着跑控制算法先读一下0x0014MAG和0x0003DIAAGC确认磁场状态正常。MAG寄存器反映CORDIC输出幅值正常值一般在中间范围。DIAAGC里的AGC增益值如果特别大说明磁场偏弱如果特别小说明磁场偏强。AGC的值可以在datasheet的寄存器描述里查到一个建议范围各版本略有差异。我碰到过一次角度曲线明显畸形的情况换了磁铁后恢复。后来查代码发现那次的磁铁是轴向充磁的而AS5047P需要径向充磁的磁铁磁场方向完全不对。这个错误靠肉眼很难发现但读一下MAG就能看出来幅值异常低。建议在新项目中都用这个方法做上电自检比等电机转起来发现问题舒服得多。6.3 滤除毛刺与速度估算即便硬件调好了低速或静止时角度数据的低几位仍然会有轻微抖动这是正常的。对大部分应用做一个简单的滑动平均就够了#define ANGLE_FILTER_LEN 8 static uint32_t angle_sum 0; static uint8_t angle_count 0; uint16_t filtered_raw; angle_sum raw_angle; angle_count; if (angle_count ANGLE_FILTER_LEN) { filtered_raw (uint16_t)(angle_sum / ANGLE_FILTER_LEN); angle_sum 0; angle_count 0; /* 用 filtered_raw 做后续处理 */ }注意一点采样均值之前要处理回绕。如果原始角度在0和16383之间来回跨越直接平均会算出个中间值。解决方法是先把角度展开成连续计数均值之后再mod 16384。这个细节在我做云台限位时卡了很久。速度估算可以用两次读角度的差值除以时间间隔但也要处理回绕。最简单的方式是计算带符号的增量int16_t diff (int16_t)(new_raw - old_raw); /* diff 的自然回绕特性可以处理 16383 - 0 的情况 */由于14位数据的回绕周期是16384用int16_t存差值可以自动处理回绕。这是我做过多次电机测速后总结出来的最省事写法。最后再说一个我自己的使用习惯每次上电后先连续读5帧NOP让SPI接口完成同步再丢弃第一次ANGLECOM读数之后才进入正式读取循环。这个动作看起来多此一举但实际上帮我省下了非常多查偶发数据不对的时间。调试时把奇偶校验打开量产时再关掉也是一条值得记住的经验。
阅读完成 · 觉得有帮助?