❤ 2019.3.16

    事情是这样的。

    在很多天以前,老师接了个项目,问我有没有意向,我谨慎的表达了我对新事物的好奇心,对新知识的求知欲,同时又委婉的表达了我的能力有限的实际情况,然后我以为事情就这么过去了,直到半个多月之前,老师又找到我,我才知道,该来的总会来的。。。

    具体来说,我这次要做的事情其实很简单,就是读一个旋转编码器的度数,并且实现能设置零点的功能。

    至于编码器,就是这货:

 没什么难度嘛~哈哈哈~so easy~

    然后就piapiapia打脸,事情远远没有我想象的那么简单!

    首先老师给我的编码器是安装好的,我并看不到里面芯片的型号,同时对于其机械结构的畏惧,我又没敢拆开(当时还不知道传感器的型号),于是只好问老师,老师给了我一些资料,大概是这样的:

这样的:

和这样的:

 然后就是我把它当成5600调了大概两三天,其间把IIC通讯从头到尾学了一遍,我甚至一度认为是我能力不行(难道不是么?),后来和朱工反复确认,才发现原来是型号不对,人家是不支持iic总线的,(as5048b支持iic总线),只能用spi总线,然而商家只有as5600的iic例程,并不提供5048的spi例程,所以只好自己写了。

    我:“¥……@……*#……%#&”

❤ 2019.3.16

    好了下面终于进入正文了。

● 致谢

    我的调试平台是秉火STM32霸道开发板,程序模板用的是秉火开发板自带的spi总线读取串行flash的例程,在调试期间以下的文章对我帮助很大,先行致谢~

【STM32F407 SPI配置并读取磁角度传感器AS5048a笔记】

    文章里详细的记录了在STM32F4上调试AS5048A的重点内容,对于了解spi总线但是不了解as5048a的童鞋非常有帮助。我也是有好几个问题在这篇文章里找到了答案,比心~

【AS5048A SPI 14位磁旋转编码器】

    文章里记录了一个新手可能会范的小错误(虽然我没有范),同时解释了一下各个寄存器的功能,对于我等英文渣非常有帮助,比心+1~

   

我之前对spi总线是不了解的,除了知道他是个串行总线之外一无所知(主要是因为懒没有把stm32的课程学完)。于是我首先做的就是先去学习了下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笔记》这篇文章里看到了对数据进行的处理,主要是刚开始没意识到这个问题,文中的程序也没给出注释,所以没有及时发现问题。

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

    data = data & 0x3fff;    //屏蔽高两位
    代码已更新到上面读取数据函数中。

 

● 清除错误标记函数

    OK搞定了读取数据函数,下面还有清除错误标记函数,因为在通讯过程中难免出现错误,(根据墨菲定理。。。),所以清除错误标记是很重要的。

大概的意思是当出现错误时,返回值的错误表位会被置1,然后通过读取错误标记寄存器可以清零错误标记位。

    不过至于错误标记位有什么用呢?我的理解是在调试过程中判断通讯是否正常(可是不正常的话不就收不到信息了么。。。),调好之后在使用中就用不到了,毕竟前两位是被屏蔽的。。。

所以代码如下:

bsp_spi_AS5048A.c

 * 函数名:ClearAndNop
 * 描述  :清除错误标记位
 * 输入  :无
 * 输出  :无
 */
u16 ClearAndNop(void)
{
	SPI_AS5048A_CS_LOW();
	SPI_AS5048A_ReadWriteWord(CMD_CLAER);              // 附加偶校验的错误标志位清除命令
	SPI_AS5048A_CS_HIGH();
	delay_us(10);              // 两次命令之间有350ns的间隔,源自官方datasheet
	SPI_AS5048A_CS_LOW();;
	SPI_AS5048A_ReadWriteWord(CMD_NOP);               // 附加偶校验的错误标志位清除命令
	SPI_AS5048A_CS_HIGH();
}

● 写入寄存器函数

    到了这里虽然完成了寄存器角度信息的读取,但是还有个功能需要实现就是通过按键充值编码器的零点,这个好像还有点复杂,而且没找到相关的资料(懒。。。),所以研究下As5048A的写指令。

    首先是