SPI:Serial Peripheral interface,串行外围设备接口。
SPI接口一般使用4条线通信:
MISO主设备数据输入,从设备数据输出。
MOSI主设备数据输出,从设备数据输入。
SCLK时钟信号,由主设备产生。
从图中可以看出,主机和从机都有一个串行移位寄存器,主机通过向它的SPI串行寄存器写入一个字节来发起一次传输。寄存器通过MOSI信号线将字节传送给从机,从机也将自己的移位寄存器中的内容通过MISO信号线返回给主机。这样,两个移位寄存器中的内容就被交换。外设的写操作和读操作是同步完成的。如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输,这个空字节通常称为dummy_byte。
SPI总线四种工作方式是为了和不同外设进行数据交换,其输出串行同步时钟极性和相位可以进行配置。
时钟极性CPOL=0,串行同步时钟的空闲状态为低电平;如果CPOL=1,串行同步时钟的空闲状态为高电平。
时钟相位CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;如果CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。
SPI主从设备的时钟相位和极性应该一致。
STM32F1的SPI时钟最多可以到18Mhz。可用DMA。
SPI配置示例
void SPI2_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB, GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15);
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI2, &SPI_InitStructure);
SPI_Cmd (SPI2, ENABLE);
SPI2_ReadWriteByte(0xff);
}
SPI_Direction:选择半双工、全双工。
SPI_Mode:选择主机模式、从机模式。
SPI_DataSize:选择8位还是16位帧格式传输。
SPI_CPOL:设置时钟极性为高电平还是低电平。
SPI_CPHA:设置时钟相位,选择在时钟的第几个跳变沿数据被采样。
SPI_NSS:设置NSS信号由硬件(NSS管脚)还是软件控制。
SPI_BaudRatePrescaler:设置SPI波特率预分频值。示例选择256即本次SPI传输速度=36M/256=140.625KHz。
SPI_FirstBit:设置数据传输顺序是MSB位在前还是LSB位在前。大部分应用是MSB在前。
SPI_CRCPolynomial:设置CRC校验多项式,提高通信可靠性,大于1即可。
标准库里SPI发送一个字节函数为
SPI_I2S_SendData(SPI2, data);
接受一个字节函数为
data = SPI_I2S_ReceiveData(SPI2);
因此需要我们再封装几个函数,SPI写任意字节,读任意字节,读写任意字节。
SPI最常见的应用场景是读写FLASH。
正点原子战舰板子上的FLASH为W25Q128。该款FLASH的容量为128Mb也就是16M字节。将16M的容量分为256个块(Block),每个块大小为64K字节,每个块又分为16个扇区(Sector),每个扇区4K字节。W25Q128最小擦除单位为一个扇区,也就是每次必须擦除4K字节。这样就需要开辟一个至少4K的缓存区,即单片机需要有4K以上的SRAM才能很好操作。W25Q128最大SPI时钟能达80Mhz。
FLASH拿到手通常先测试能否到它的ID
从W25QXX datasheet 里看到,主机给它发0x90,0x00,0x00,0x00,即可读取16位ID
u16 W25QXX_ReadID(void)
{
u16 Temp = 0;
W25QXX_CS = 0;
SPI2_ReadWriteByte(0x90);
SPI2_ReadWriteByte(0x00);
SPI2_ReadWriteByte(0x00);
SPI2_ReadWriteByte(0x00);
Temp |= SPI2_ReadWriteByte(0xFF) << 8;
Temp |= SPI2_ReadWriteByte(0xFF);
W25QXX_CS = 1;
return Temp;
}
这里要注意读ID时MOSI发送的是0xFF,这个就是DUMMY_BYTE,大多DUMMY_BYTE是0xFF,少部分设备会有特殊要求。
FLASH读取数据,先发0x03和读取数据的地址起点,然后即可读取。
代码如下
void W25QXX_Read(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead)
{
u16 i;
W25QXX_CS = 0;
SPI2_ReadWriteByte(0x03);
SPI2_ReadWriteByte((u8) ((ReadAddr) >> 16));
SPI2_ReadWriteByte((u8) ((ReadAddr) >> 8));
SPI2_ReadWriteByte((u8) ReadAddr);
for(i = 0; i < NumByteToRead; i++)
{
pBuffer[i] = SPI2_ReadWriteByte(0XFF);
}
W25QXX_CS = 1;
}
W25Q128写使能,要给flash写数据,必须先写使能。
从W25QXX datasheet 里看到,只要发0x06即可写使能。
void W25QXX_Write_Enable(void)
{
W25QXX_CS=0;
SPI2_ReadWriteByte(0x06);
W25QXX_CS=1;
}
写使能后还不能立即往FLASH里面写数据,需要将写入数据的FLASH地址擦除。
void W25QXX_Erase_Sector(u32 Dst_Addr)
{
Dst_Addr *= 4096;
W25QXX_Write_Enable();
W25QXX_Wait_Busy();
W25QXX_CS = 0;
SPI2_ReadWriteByte(0x20);
SPI2_ReadWriteByte((u8) ((Dst_Addr) >> 16));
SPI2_ReadWriteByte((u8) ((Dst_Addr) >> 8));
SPI2_ReadWriteByte((u8) Dst_Addr);
W25QXX_CS = 1;
W25QXX_Wait_Busy();
}
FLASH写数据需要考虑跨扇区写入的问题,因为FLASH只提供了往一个页page写数据的命令0x02。
void W25QXX_Write_Page(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
u16 i;
W25QXX_Write_Enable();
W25QXX_CS = 0;
SPI2_ReadWriteByte(0x02);
SPI2_ReadWriteByte((u8) ((WriteAddr) >> 16));
SPI2_ReadWriteByte((u8) ((WriteAddr) >> 8));
SPI2_ReadWriteByte((u8) WriteAddr);
for(i = 0; i < NumByteToWrite; i++)
{
SPI2_ReadWriteByte(pBuffer[i]);
}
W25QXX_CS = 1;
W25QXX_Wait_Busy();
}
void W25QXX_Write_NoCheck(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
u16 pageremain;
pageremain = 256 - WriteAddr % 256;
if(NumByteToWrite <= pageremain)
{
pageremain = NumByteToWrite;
}
while(1)
{
W25QXX_Write_Page(pBuffer , WriteAddr, pageremain);
if(NumByteToWrite == pageremain)
{
break;
}
else
{
pBuffer += pageremain;
WriteAddr += pageremain;
NumByteToWrite -= pageremain;
if(NumByteToWrite > 256)
{
pageremain = 256;
}
else
{
pageremain = NumByteToWrite;
}
}
};
}
u8 W25QXX_BUFFER[4096];
void W25QXX_Write(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;
}
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)
{
W25QXX_Erase_Sector(secpos);
for(i = 0; i < secremain; i++)
{
W25QXX_BUF[i + secoff] = pBuffer[i];
}
W25QXX_Write_NoCheck(W25QXX_BUF, secpos * 4096, 4096);
}
else
{
W25QXX_Write_NoCheck(pBuffer, WriteAddr, secremain);
}
if(NumByteToWrite == secremain)
{
break;
}
else
{
secpos++;
secoff = 0;
pBuffer += secremain;
WriteAddr += secremain;
NumByteToWrite -= secremain;
if(NumByteToWrite > 4096)
{
secremain = 4096;
}
else
{
secremain = NumByteToWrite;
}
}
}
}