❤ 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的写指令。
首先是