平常用uart和can比较多,今天偶然碰见一个spi的问题,借着这个机会总结一下吧。
参考资料:
https://blog.csdn.net/Dr_Haven/article/details/82689116
https://blog.csdn.net/qq_25814297/article/details/103934693
https://blog.csdn.net/wan1234512/article/details/112399842
https://blog.csdn.net/qq_34430371/article/details/103859159
https://blog.csdn.net/qq_25814297/article/details/86189574
1、spi硬件和通信协议
·网上没找到合适的图,凑活一下吧
(1)硬件连接
SPI接口一般使用4条线通信:
MISO:主设备数据输入,从设备数据输出。
MOSI:主设备数据输出,从设备数据输入。
SCLK:时钟信号,由主设备产生。
CS:选片使能
·这四条线中cs和sclk(sck)是控制线,另外两条是数据线。这一点和usrt或者can非常不同,spi并不真正具有类似于”数据包“的概念,没有帧头、验证位这些概念。只要控制线符合要求,数据线传送的所有位都是数据(命令数据、地址数据、数值数据)。但是,这种同步通信可能无法长距离传输。
(2)8位位移寄存器和发送/接受缓冲区
SPI通信的核心是这个8位移位寄存器,它遵循FIFO(先进先出)原则,即每有一位从低位输入寄存器,寄存器的最高位就会被输出。SPI将两个移位寄存器按照上图的方式连接在一起,每次通信(无论读写)都是两个寄存器数据的置换。也就是说在不考虑数据内容的情况下,SPI的读出和写入是同一个操作。
注意区分单片机执行的接受/发送与在SPI上执行的所谓的读/写的区别。SPI发送和接受函数都是对发送缓冲区和接受缓冲区进行的操作。而spi上的读和写都是由主机的一次发送函数引起的主从机移位寄存器的数据调换。
(3)一个完整的发送接受过程
执行发送函数将数据放入发送缓冲区。当控制线符合要求时,发送缓存区数据的第一位会先发出,之后剩下的数据并行进入移位寄存器串行发出。与此同时,从机的位移寄存器数据会被发过来,从低位进入主机移位寄存器。新数据输入完成后,会被存入接受缓冲区,通过接受函数存入其他变量。
(4)缓存区的DR和SR寄存器
DR:接收和发送的缓存区
SR:缓存区状态寄存器,包括两个缓存区是否有数据的标志
2、时序分析与代码思路
(1)SPI通信格式
SPI的读和写都是在16个或者更多的时钟周期内完成的,这两种操作有统一的格式如下:(命令+数据)
读/写命令(1位)+单字节/多字节操作(1位)+寄存器地址后六位(6位)+单字节数据/多字节数据
·想真正理解这个时序图就要理解之前提到的SPI四根线中控制线和数据线的分别。是否进行数据传输不是由传输的数据决定的,而是由控制线cs和sck决定的,只要cs拉低、sck有时钟、主机的发送缓存区有数据就会进行发送。
(2)两个问题
a、两次发送单字节和一次发送两个字节有什么区别
二者并没有区别,SPI的数据不存在帧头和验证位,所以在不改变控制线的情况下,发送一个二字节数据=发送两个单节数据。同理,发送一个n字节数据=n次发送单字节数据。
b、在进行多字节发送时如何区分命令字节和数据字节
每次控制线符合通信要求时,主机发送的第一个字节会被认为是命令字节,其他所有字节都是数据字节。
3、代码分析
这部分代码只是为了说明流程
(1)单字节数据读写
// SPI写指令
void SpiWriteCmd(u8 cmd,u8 data)
{
ADS_SPI_CS_LOW(); //cs拉低,开始通信
while((SPI2->SR &0x0002)==0); //SR是缓冲区状态寄存器,等待发送缓冲区没有数据
SPI2->DR = (cmd); //存入"写命令"的字节
while((SPI2->SR &0x0001)==0); //等待接受缓冲区有数据
SPI_I2S_ReceiveData(SPI2); //读取缓冲区数据,注意这个数据只是从机移位寄存器之前有的数据,并没有意义。但是如果不取出的话,会导致缓存区溢出。
while((SPI2->SR &0x0002)==0); //同上
SPI2->DR = (data); //向缓存区存入数据字节,这个字节会自动传到从机,并存入上一个”写命令“指定的地址
while((SPI2->SR &0x0001)==0);
SPI_I2S_ReceiveData(SPI2);
ADS_SPI_CS_HIGH(); //拉高cs,通信结束
}
// SPI读指令
u8 SpiReadCmd(u8 cmd)
{
ADS_SPI_CS_LOW();
unsigned char returnvalue;
while((SPI2->SR &0x0002)==0); //同上
SPI2->DR = (cmd|0x80); //存入“读命令”字节,这里之所以是0x08,是因为它的二进制是10000000,执行“|”操作,正好将最高位置1是读命令。(所有寄存器地址都是7位,7位中的最高位都是0)
while((SPI2->SR &0x0001)==0);
returnvalue = (unsigned char)(SPI_I2S_ReceiveData(SPI2));
while((SPI2->SR &0x0002)==0);
SPI2->DR = (0x00); //发送一个空子节,以便置换想要的数据
while((SPI2->SR &0x0001)==0);
returnvalue =(unsigned char)(SPI_I2S_ReceiveData(SPI2)); // Return the Byte read from the SPI bus
ADS_SPI_CS_HIGH(); //通信结束
return(returnvalue); //返回第二次返回的数据,即指定寄存器的数据
}
从单字节的读写操作可以看出SPI的通信方式就是通过一组发送+接受实现的。由于SPI通信的最小周期是16个时钟周期(一字节命令+一字节数据),这里是通过两次单字节通信实现的。但是,SPI->DR本来就有16位,也可以直接”一次发送两个字节+一次接受两个字节“实现最短的SPI通信。
(2)多字节数据读写
到此为止,如果以上都理解了的话,应该就很自然的写出多字节的代码了,这里简单说明一下读取的写法,写入其实相同。
ADS_SPI_CS_LOW();
unsigned char returnvalue;
while((SPI2->SR &0x0002)==0);
SPI2->DR = 0x32|0xC0; //发送数据。0x32是要读取的地址,0xc0正好是11000000,设置成多位读取
while((SPI2->SR &0x0001)==0);
Buf[0] = (unsigned char)(SPI_I2S_ReceiveData(SPI2)); //返回一下缓存区数据
while((SPI2->SR &0x0002)==0);
SPI2->DR = 0x00; //连续发送空字节数据,指定地址的新数据就会不断发出
while((SPI2->SR &0x0001)==0);
Buf[0] = (unsigned char)(SPI_I2S_ReceiveData(SPI2)); // Return the Byte read from the SPI bus// Buf[1] = (unsigned char)(SPI_I2S_ReceiveData(SPI2));
while((SPI2->SR &0x0002)==0);
SPI2->DR = 0x00;
while((SPI2->SR &0x0001)==0);
Buf[1] = (unsigned char)(SPI_I2S_ReceiveData(SPI2)); // Return the Byte read from the SPI bus
while((SPI2->SR &0x0002)==0);
SPI2->DR = 0x00;
while((SPI2->SR &0x0001)==0);
Buf[2] = (unsigned char)(SPI_I2S_ReceiveData(SPI2)); // Return the Byte read from the SPI bus
while((SPI2->SR &0x0002)==0);
SPI2->DR = 0x00;
while((SPI2->SR &0x0001)==0);
Buf[3] = (unsigned char)(SPI_I2S_ReceiveData(SPI2)); // Return the Byte read from the SPI bus
while((SPI2->SR &0x0002)==0);
SPI2->DR = 0x00;
while((SPI2->SR &0x0001)==0);
Buf[4] = (unsigned char)(SPI_I2S_ReceiveData(SPI2)); // Return the Byte read from the SPI bus
while((SPI2->SR &0x0002)==0);
SPI2->DR = 0x00;
while((SPI2->SR &0x0001)==0);
Buf[5] = (unsigned char)(SPI_I2S_ReceiveData(SPI2)); // Return the Byte read from the SPI bus
ADS_SPI_CS_HIGH(); //通信结束
————————————————
版权声明:本文为CSDN博主「张一西」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_34430371/article/details/103859159
4、飞思卡尔SPI发送和接受函数分析
(1)函数分析
bool SPI001_ReadData(const SPI001_HandleType* Handle, uint16_t* DataPtr)
{
bool Result = (bool)FALSE; //结果状态值初始化
USIC_CH_TypeDef* USICRegs = Handle->USICRegs; //结构体
/* <<>>*/
if(USIC_ubIsRxFIFOempty(USICRegs)) //如果接受缓存区是空的
{
Result = (bool)FALSE; //读取失败
}
else //否则
{
*DataPtr = (uint16_t)USICRegs->OUTR; //将缓存区数据存入指定地址
Result = (bool)TRUE; //成功
}
return Result;
}
//写入操作
bool SPI001_WriteData( const SPI001_HandleType* Handle, const uint16_t* DataPtr,SPI001_TransmitMode TrMode )
{
bool Result = (bool)FALSE; //状态初始化
uint32_t HpcenNew = 0x00U;
uint8_t TbufIndex = 0x00U; //发送缓存区引脚
USIC_CH_TypeDef* USICRegs = Handle->USICRegs; //结构体
/* <<>>*/
HpcenNew = ((uint32_t)((uint32_t)TrMode & SPI001_TRMODE_Msk) >> (uint32_t)3);
if(USIC_IsTxFIFOfull(USICRegs)) //发送区是满的
{
Result = (bool)FALSE; //失败
}
else //否则
{
USICRegs->CCR &= (~USIC_CH_CCR_HPCEN_Msk);
USICRegs->CCR |= ((HpcenNew << USIC_CH_CCR_HPCEN_Pos) & \
USIC_CH_CCR_HPCEN_Msk);
TbufIndex = (uint8_t)((uint8_t)TrMode & (uint8_t)SPI001_TBUFINDEX_Msk); //存入缓存区的位置
USICRegs->IN[TbufIndex] = *DataPtr; //指定数据存入缓存区
Result = (bool)TRUE;
}
return Result;
}
可以看出这两个函数其实都没有一个完整的发送+接受的过程,所以并不是SPI的读和写的操作只是对缓存区的操作函数。
发送函数相当于前面代码中的SPI2->DR =xxx
接收函数相当于前面代码中的SPI_I2S_ReceiveData()
与之前不同的是这里是一次发送和接受两个字节的数据,但是只要按照之前代码的结构,两次单字节的发送和接受==一次两字节的发送和接受,将其中的收发函数替换成这两个函数,就可以实现SPI读写指令。