STM32环境下AS5048A14位磁旋转编码器SPI通讯调试记录——我学到的东西、遇到的问题、解决的过程
〇 SPI总线简要介绍
● SPI物理层
SSn:片选信号,主机控制,低电平有效。
SCK:时钟信号,主机控制。
MOSI:主机输出从机输入。
MISO:主机输入从机输出。
● 协议层
通过配置CPOL位(时钟极性)和CPHA位(时钟相位),SPI总线有四种工作模式:
● STM32的SPI特性
架构剖析
通讯引脚
● SPI初始化结构体
● 几个比较重要的库函数
SPI初始化函数
SPI使能函数
获取SPI状态标记函数
SPI发送数据函数
SPI接收数据函数
好了关于SPI的基本信息就是这样,下面真的开始正文了。
〇 AS5048A调试过程
● 硬件连接
这个是真的as5048a的接线的定义:
我选择了stm32的spi1口进行调试,对应的接口:
CSn———-PC13
CLK———-PA5
MOSI——–PA7
MISO——–PA6
● IO口初始化
首先定义各个功能对应的IO口,顺带定义了一下片选指令
bsp_spi_AS5048A.h
/*SPI接口定义-开头****************************/
#define AS5048A_SPIx SPI1
#define AS5048A_SPI_APBxClock_FUN RCC_APB2PeriphClockCmd
#define AS5048A_SPI_CLK RCC_APB2Periph_SPI1
//CS(NSS)引脚 片选选普通GPIO即可
#define AS5048A_SPI_CS_APBxClock_FUN RCC_APB2PeriphClockCmd
#define AS5048A_SPI_CS_CLK RCC_APB2Periph_GPIOC
#define AS5048A_SPI_CS_PORT GPIOC
#define AS5048A_SPI_CS_PIN GPIO_Pin_13
//SCK引脚
#define AS5048A_SPI_SCK_APBxClock_FUN RCC_APB2PeriphClockCmd
#define AS5048A_SPI_SCK_CLK RCC_APB2Periph_GPIOA
#define AS5048A_SPI_SCK_PORT GPIOA
#define AS5048A_SPI_SCK_PIN GPIO_Pin_5
//MISO引脚
#define AS5048A_SPI_MISO_APBxClock_FUN RCC_APB2PeriphClockCmd
#define AS5048A_SPI_MISO_CLK RCC_APB2Periph_GPIOA
#define AS5048A_SPI_MISO_PORT GPIOA
#define AS5048A_SPI_MISO_PIN GPIO_Pin_6
//MOSI引脚
#define AS5048A_SPI_MOSI_APBxClock_FUN RCC_APB2PeriphClockCmd
#define AS5048A_SPI_MOSI_CLK RCC_APB2Periph_GPIOA
#define AS5048A_SPI_MOSI_PORT GPIOA
#define AS5048A_SPI_MOSI_PIN GPIO_Pin_7
#define SPI_AS5048A_CS_LOW() GPIO_ResetBits( AS5048A_SPI_CS_PORT, AS5048A_SPI_CS_PIN )
#define SPI_AS5048A_CS_HIGH() GPIO_SetBits( AS5048A_SPI_CS_PORT, AS5048A_SPI_CS_PIN )
/*SPI接口定义-结尾****************************/
然后使能SPI时钟,使能GPIO口时钟,配置GPIO口属性。
bsp_spi_AS5048A.c
/* 使能SPI时钟 */
AS5048A_SPI_APBxClock_FUN ( AS5048A_SPI_CLK, ENABLE );
/* 使能SPI引脚相关的时钟 */
AS5048A_SPI_CS_APBxClock_FUN ( AS5048A_SPI_CS_CLK|AS5048A_SPI_SCK_CLK|AS5048A_SPI_MISO_CLK|AS5048A_SPI_MOSI_CLK, ENABLE );
/* 配置SPI的 CS引脚,普通IO即可 */
GPIO_InitStructure.GPIO_Pin = AS5048A_SPI_CS_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(AS5048A_SPI_CS_PORT, &GPIO_InitStructure);
/* 配置SPI的 SCK引脚*/
//【为什么注释掉这几个端口配置就好了?】
GPIO_InitStructure.GPIO_Pin = AS5048A_SPI_SCK_PIN;
// GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(AS5048A_SPI_SCK_PORT, &GPIO_InitStructure);
/* 配置SPI的 MISO引脚*/
GPIO_InitStructure.GPIO_Pin = AS5048A_SPI_MISO_PIN;
// GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
// GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(AS5048A_SPI_MISO_PORT, &GPIO_InitStructure);
/* 配置SPI的 MOSI引脚*/
GPIO_InitStructure.GPIO_Pin = AS5048A_SPI_MOSI_PIN;
// GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
// GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(AS5048A_SPI_MOSI_PORT, &GPIO_InitStructure);
在这里我有个疑问,一开始我定义了SPI的端口的属性,结果通讯不成功,然后我注释掉了,就可以了,不知道是怎么回事。
● SPI初始化结构体配置
配置SPI初始化结构体,需要根据AS5048A的属性来设置相关的参数。
从这段话可以得知,AS5048A需要16位SPI数据,在下降沿读数据,在上升沿写数据,每发送一次指令(16位数据)后片选信号需要拉高一次。
○ SPI时序图
从这里可以看出SPI总线工作在模式1,即CPOL=0,CPHA=1。
另外还有就是高位字节优先(MSB模式)。
时间特性
这个图的重点大概是两个350ns的延时,但是我还没有验证过。
○ 由上面的信息可以知道,SPI初始化结构体需要配置的参数了,代码如下。
bsp_spi_AS5048A.c
/* SPI 模式配置 */
// AS5048A芯片 支持SPI模式0及模式3,据此设置CPOL CPHA
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //双线全双工模式
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //SPI主模#式
SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b; //16位数据
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //CPOL=0
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //CPHA=1
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //软件控制片选信号
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;
//时钟16分频(这个分频主要看as5048a的最高工作频率,我在datasheet里并没有找到,
//我根据时间特性计算了一下大概是10M以下,所以选了个速度比较低的模式)
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //高位字节优先模式
SPI_InitStructure.SPI_CRCPolynomial = 15; //CRC位数,好像没用
SPI_Init(AS5048A_SPIx , &SPI_InitStructure);
/* SPI使能 */
SPI_Cmd(AS5048A_SPIx,ENABLE);
● 发送/接收函数
OK初始化工作基本上就完成了,下面是发送接收函数。
指令的发送和数据的接收本来是很重要的部分,但是其实也挺简单的,SPI总线的特点是发送接收同时进行,所以发送函数同时也是接收函数。
需要注意的是,发送函数的实质是向发送寄存器里写入数据,同理接收函数也是,所以在发送之前需要检测发送寄存器的状态,然而判断数据是否发送完成却要看接受寄存器的状态,因为发送接收是同时进行的。在发送完成之后实际上也完成了数据的接收,所以顺带return一个接收到的数据。
所以代码如下:
bsp_spi_AS5048A.c
/**
* @brief SPI_AS5048A读写函数,16位
* @param 无
* @retval 有
*/
u16 SPI_AS5048A_ReadWriteWord(u16 data)
{
/* 等待发送缓冲区为空,TXE事件 */
while(SPI_I2S_GetFlagStatus(AS5048A_SPIx, SPI_I2S_FLAG_TXE) == RESET);
/* 写入数据寄存器,把要写入的数据写入发送缓冲区 */
SPI_I2S_SendData(AS5048A_SPIx,data);
/* 等待接收缓冲区非空,RXNE事件 */
while(SPI_I2S_GetFlagStatus(AS5048A_SPIx, SPI_I2S_FLAG_RXNE) == RESET);
/* 读取数据寄存器,获取接收缓冲区数据 */
return SPI_I2S_ReceiveData(AS5048A_SPIx);
}
这段代码实现了数据的发送和接收,但是有个问题,因为里面有两个while循环等待,根据墨菲定理,死循环的情况是一定会发生的,这点在秉火的例程里通过加入了一个超时函数来解决,代码是这样的:
static __IO uint32_t SPITimeout = SPIT_LONG_TIMEOUT;
static uint16_t SPI_TIMEOUT_UserCallback(uint8_t errorCode);
/**
* @brief SPI_AS5048A读写函数,16位
* @param 无
* @retval 有
*/
u16 SPI_AS5048A_ReadWriteWord(u16 data)
{
SPITimeout = SPIT_FLAG_TIMEOUT;
/* 等待发送缓冲区为空,TXE事件 */
while(SPI_I2S_GetFlagStatus(AS5048A_SPIx, SPI_I2S_FLAG_TXE) == RESET)
{
if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(2);
}
/* 写入数据寄存器,把要写入的数据写入发送缓冲区 */
SPI_I2S_SendData(AS5048A_SPIx,data);
SPITimeout = SPIT_FLAG_TIMEOUT;
/* 等待接收缓冲区非空,RXNE事件 */
while(SPI_I2S_GetFlagStatus(AS5048A_SPIx, SPI_I2S_FLAG_RXNE) == RESET)
{
if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(3);
}
/* 读取数据寄存器,获取接收缓冲区数据 */
return SPI_I2S_ReceiveData(AS5048A_SPIx);
}
/**
* @brief 等待超时回调函数
* @param None.
* @retval None.
*/
static uint16_t SPI_TIMEOUT_UserCallback(uint8_t errorCode)
{
/* 等待超时后的处理,输出错误信息 */
AS5048A_ERROR("SPI 等待超时!errorCode = %d",errorCode);
return 0;
}
学习了学习了。
● 读取数据函数
发送/接收函数只是对SPI寄存器的底层操作,并不能读取到传感器的数据,这里专门为读取传感器数据编写一个函数。
读取数据的逻辑是首先发出片选信号,然后发送一段指令指定读取那个寄存器数据,然后再发送一段任意指令或者下一个读取指令,在发送的同时接收到上一个指令中指定的寄存器数据。
○ 发送指令格式
首先需要知道发送指令的格式。
这个是AS5048A的SPI指令包格式。指令由一个校验位(偶校验),一个读写控制位,和14位寄存器地址构成。
寄存器地址如下:
作为读数据指令,指令的内容是固定的,因此我们可以定义几个指令,需要的时候直接发送。
【更新↓↓↓】
○ 接收数据格式
接收到的数据最高位是校验位,第二位是错误标记位,所以需要对接收到的数据进行处理。
【更新↑↑↑】
于是读取数据函数的代码:
bsp_spi_AS5048A.h
/*命令定义-开头*******************************/
//这是附加了偶校验位和读写标志位的指令
#define CMD_ANGLE 0xffff
#define CMD_AGC 0x7ffd
#define CMD_MAG 0x7ffe
#define CMD_CLAER 0x4001
#define CMD_NOP 0xc000
/*命令定义-结尾*******************************/
bsp_spi_AS5048A.c
/**
* @brief SPI_AS5048A读取接收函数,通过发送相应指令读取AS5048A中寄存器的数值
* @param 无
* @retval 返回接收到的数据
*/
u16 SPI_AS5048A_ReadData(u16 TxData)
{
u16 data;
//delay_us(10); //datasheet里面说两个信号之间要间隔350ns,不知道这样可不可以
SPI_AS5048A_CS_LOW();
//delay_us(10); //datasheet里面说片选信号和时钟信号要间隔350ns,不知道这样可不可以
SPI_AS5048A_ReadWriteWord(TxData);
SPI_AS5048A_CS_HIGH();
delay_us(10); //datasheet里面说两个信号之间要间隔350ns,不知道这样可不可以
SPI_AS5048A_CS_LOW();
data = SPI_AS5048A_ReadWriteWord(CMD_NOP);
SPI_AS5048A_CS_HIGH();
data = data & 0x3fff; //屏蔽高两位【更新】
return data;
}
main.c
while(1)
{
Value = SPI_AS5048A_ReadData(CMD_ANGLE);
printf("%d\n",Value);
delay_ms(1000);
}
● 遇到了问题
到这里,理论上来说就可以正常的读取编码器的角度值了。但是!
但是!
出问题了!
○ 问题描述
问题是这样的。
在程序编译成功之后,我用串口调试助手接收stm32读取到的数据,结果出现了这样的情况:
简单来说,就是当旋钮位置不变时,理论上读取到的数值应该是不变的(实际上会有很小的变化),但是我读到的数值却有两种,一种是看起来比较正常的值,另一种是一个特别大的数值。而且两种数值随机出现,并没有什么规律。
○ 问题分析
首先我用万用表测量传感器的模拟量输出端(其实是PWM信号输出),确定了比较正常的那个值确实是正确的读数。也就是说在某个环节出现了干扰,使我读到的数据发生了某种变化。
我首先排除了是指令发送过程中出现的错误,因为在发送NOP指令后读到的数据都是0(至于为什么我也不知道),然后我换了其他的输出格式,输出的数据依然是有两种,所以不是显示的问题。
后来我分析了读到的这两种数据。我发现首先对应同一个旋钮的位置,这两种数据是确定的,他们之间总是相差一个几乎确定的数字,大概是30000多,所以我怀疑错误的数据是在正确的数据上叠加了一个确定的数。于是我灵机一动,把接收到的十进制数转换成了二进制,于是发现了真相:
真相应该已经很清晰了,因为我设置的是十进制显示,所以没有在第一时间发现问题,还因为这个苦想了大半夜,熬到了将近4点才睡觉。。。。
为什么会出现这种情况呢?
我查看了传感器的register map,我觉得应该是传感器里的寄存器是14位的,但是通过SPI发送的数据是16位的,也就是说虽然stm32接收到了一个14位的数据,但是存在寄存器里的依然是个16位的数据,没有定义的两位可能会因为某些原因随机的表现出0或者1的状态,具体是不是这样我也不知道,不过知道问题出在哪,就知道该怎样去避免了。
【更新↓↓↓】
我仔细查了查,发现其实这并不是随机出现的,因为最高位是校验位,所以根据读到的数据不同有规律的置0置1(受教了)。读回来的数据格式如下:
【更新↑↑↑】
○ 解决方法
我觉得最直观的解决方法就是屏蔽接收到的数据的高两位,其实后来我在《STM32F407 SPI配置并读取磁角度传感器AS5048a笔记》这篇文章里看到了对数据进行的处理,主要是刚开始没意识到这个问题,文中的程序也没给出注释,所以没有及时发现问题。
解决方式是给读取数据函数加一行:
传感器里的寄存器是14位的,但是通过SPI发送的数据是16位的,也就是说虽然stm32接收到了一个14位的数据,但是存在寄存器里的依然是个16位的数据,没有定义的两位可能会因为某些原因随机的表现出0或者1的状态,具体是不是这样我也不知道,不过知道问题出在哪,就知道该怎样去避免了。
【更新↓↓↓】
我仔细查了查,发现其实这并不是随机出现的,因为最高位是校验位,所以根据读到的数据不同有规律的置0置1(受教了)。读回来的数据格式如下:

【更新↑↑↑】
○ 解决方法
我觉得最直观的解决方法就是屏蔽接收到的数据的高两位,其实后来我在《STM32F407 SPI配置并读取磁角度传感器AS5048a笔记》这篇文章里看到了对数据进行的处理,主要是刚开始没意识到这个问题,文中的程序也没给出注释,所以没有及时发现问题。
解决方式是给读取数据函数加一行:
作者:普通网友