SPI通信
0. 前言
通讯速率较高,适用于ADC、LCD等设备与MCU间通信
1. SPI基础知识
物理层
-
SCL:时钟信号线
-
CS:片选信号线/从设备选择信号线,低电平有效
-
MOSI:主设备输出/从设备输入引脚
-
MISO:主设备输入/从设备输出引脚
通信过程
-
MOSI和MISO数据线在SCK的每个时钟周期传输一位数据,且数据输入输出同时进行
-
在SCK
上升沿
时触发,在下降沿
时采样,即在SCK
上升沿
期间,MOSI和MISO的数据变化输出在SCK
下降沿
期间,MOSI和MISO的数据有效 -
一般在SPI通讯时序中选择MSB先行模式
-
SPI每次传输数据以8位或16位为单位
-
在双线全双工模式下,MOSI与MISO数据传输是同步的
2. SPI应用-stm32
SPI外设
- 可作为主机或从机
- 支持最高的SCK时钟频率为
fpclk/2
- 支持SPI协议的4种模式
- 数据帧长度可设置为8位或16位
- 可设置MSB先行或LSB先行
通讯模式
SPI模式 | CPOL | CPHA | 空闲时SCK时钟 | 采样时刻 |
---|---|---|---|---|
0 | 0 | 0 | 低电平 | 奇数边沿 |
1 | 0 | 1 | 低电平 | 偶数边沿 |
2 | 1 | 0 | 高电平 | 奇数边沿 |
3 | 1 | 1 | 高电平 | 偶数边沿 |
判断SCL信号空闲状态时的电平
- CPOL=0时,SCL=0
- CPOL=1时,SCL=1
判断数据采样和触发(转换)
- CPHA=0时,数据在奇数边沿采样,数据在偶数边沿转换
- CPHA=1时,数据在偶数边沿采样,数据在奇数边沿转换
可以通过上面判断通讯模式,下面这个是CPOL=0、CPHA=1的SPI通信
在模拟时序中,采样不一定在边沿,但是边沿时刻,MOSI状态要维持好,MISO在边沿之后获取引脚状态就行
Code BLock
1. 结构体
typedef struct
{
uint16_t SPI_Direction; //设置SPI的单双向模式
uint16_t SPI_Mode; //设置SPI的主/从机端模式
uint16_t SPI_DataSize; //设置SPI的数据帧长度
uint16_t SPI_CPOL; //设置时钟极性
uint16_t SPI_CPHA; //设置时钟相位
uint16_t SPI_NSS; //设置NSS引脚由硬件还是软件控制
uint16_t SPI_BaudRatePrescaler; //设置时钟分频因子,fpclk/分频数=fsck
uint16_t SPI_FirstBit; //设置MSB/LSB先行
uint16_t SPI_CRCPolynomial; //设置CRC检验表达式
}SPI_InitTypeDef;
flash芯片
页写入
使用页写入命令最多可以一次向FLASH传输256个字节的数据,我们把这个单位为页大小
若发送数据超出256个,则会覆盖前面发送的数据
不定量数据写入
在页写入时,常常要写入不定量的数据,代码如下
/**
* @brief 对flash写入数据,调用本函数写入数据前需要先擦除扇区
* @param pBuffer,要写入数据的指针
WriteAddr,写入地址
NumByteToWrite,写入数据长度
* @retval none
*/
u8 W25QXX_BUFFER[4096];
void SPI_FLASH_BufferWrite(u8 *pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
u32 secpos;
u16 secoff;
u16 secremain;
u16 i;
u8 *W25QXX_BUF;
W25QXX_BUF = W25QXX_BUFFER;
secpos = WriteAddr/4096; //扇区地址
secoff = WriteAddr%4096; //在扇区内的偏移
secremain = 4096-secoff; //扇区剩余空间大小
if(NumByteToWrite <= secremain)
secremain = NumByteToWrite; //不大于4096个字节
while(1)
{
W25QXX_Read(W25QXX_BUF,secpos*4096,4096); //读取整个扇区的内容
for(i=0;i<secremain;i++) //校验数据,是否需要擦除
{
if(W25QXX_BUF[secoff+i] != 0xFF)
break; //需要擦除
}
//需要擦除
if(i<secremain)
{
SPI_FLASH_SectorErase(secpos*4096);
#if 1
for(i=0;i<secremain;i++)
{
W25QXX_BUF[i+secoff] = pBuffer[i];
}
//写入整个扇区,从扇区起始地址写
//前secoff数据不用考虑,W25QXX_BUF[i+secoff] = pBuffer[i];
W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);
#else
//也可以用这个
W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);
#endif
}
//可以直接写入扇区剩余区间
else
W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);
//写入结束了
if(NumByteToWrite == secremain)
break;
//写入未结束
else
{
secpos++; //扇区地址增1
secoff = 0; //偏移位置为0
pBuffer += secremain; //指针偏移
WriteAddr += secremain; //写地址偏移
NumByteToWrite -= secremain; //字节数递减
//下一个扇区仍写不完
if(NumByteToWrite>4096)
secremain = 4096;
//下一个扇区可以写完
else
secremain = NumByteToWrite;
}
}
}
//写入时分为两种情况
//1、如果待写入指定的长度数据,在第一个扇区都够写完
if(NumByteToWrite <= secremain)
secremain = NumByteToWrite;
//2、如果第一个扇区写不完,则,第一个扇区,中间完整的扇区,最后一个扇区分别写入
//参考上面
//4KB = 2^12Byte,即4096个字节