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(受教了)。读回来的数据格式如下:


![](https://i3.wp.com/img-blog.csdnimg.cn/20190316181919988.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L29vb3Jjemdj,size_16,color_FFFFFF,t_70)


【更新↑↑↑】


 


○ 解决方法


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


    解决方式是给读取数据函数加一行:



作者:普通网友

物联沃分享整理
物联沃-IOTWORD物联网 » STM32环境下AS5048A14位磁旋转编码器SPI通讯调试记录——我学到的东西、遇到的问题、解决的过程

发表回复