SPI,是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议。
SPI 具有信号线少,协议简单,数据率高等优点。数据传送速率达几MB/s
标准的 SPI 使用 4 Pin 进行数据传送:
(1)MOSI – 主器设备数据输出,从器件数据输入
(2)MISO – 主器设备件数据输入,从器件数据输出
(3)SCLK – 时钟信号,由主器件产生, 最大为fPCLK/2,从模式频率最大为fCPU/2
(4)NSS – 从器件使能信号,由主器件控制,有的 IC 会标注为 CS (Chip select)
数据时在 CLK 时钟的驱动下,在数据线上按照一个 bit 一个 bit 的进行传送,数据可以在时钟的上升沿或者下降沿改变(或者采样)。
SPI 传输的缺点是,没有数据完整性校验,也没有流控机制。
既然称 SPI 为总线,则 SPI 就可以支持多个设备相连接,通过 CS 片选信号来指定期望通讯的设备。(多机通讯)
SPI 通讯有 4 中不同的通讯模式,通信双方需要配置成为一样的模式,才能够进行正常的数据传输,这里有两个概念:
CPOL:时钟极性
CPHA:时钟相位
CPOL:(时钟极性)控制在没有数据传输时,SPI 时钟的空闲状态电平。即,定义了总线空闲的工作状态(注意,和 UART 不同,SPI 是通过 CLK 的状态来表征当前的总线状态,即不发生任何数据交互的时候,时钟信号总是没有进行翻转的)
CPOL=0,表示当SCLK=0时处于空闲态
CPOL=1,表示当SCLK=1时处于空闲态
CPHA:(时钟相位)是用来配置数据采样是在第几个边沿。
CPHA=0,表示数据采样是在第1个边沿
CPHA=1,表示数据采样是在第2个边沿
所以 CPOL 和 CPHA 的不同组合,成为了 SPI 的四种传输模式:
Mode Defination | CPOL Value | CPHA Value |
Mode0 | 0 | 0 |
Mode1 | 0 | 1 |
Mode2 | 1 | 0 |
Mode3 | 1 | 1 |
四种传输模式,定义了不同时刻的总线启动,以及数据发送和采样时间:
CPOL=0,CPHA=0:此时空闲态时,SCLK处于低电平,数据采样是在第1个边沿,也就是 SCLK 由低电平到高电平的跳变,所以数据采样是在上升沿,数据发送是在下降沿。
CPOL=0,CPHA=1:此时空闲态时,SCLK处于低电平,数据发送是在第1个边沿,也就是 SCLK 由低电平到高电平的跳变,所以数据采样是在下降沿,数据发送是在上升沿。
CPOL=1,CPHA=0:此时空闲态时,SCLK处于高电平,数据采集是在第1个边沿,也就是 SCLK 由高电平到低电平的跳变,所以数据采集是在下降沿,数据发送是在上升沿。
CPOL=1,CPHA=1:此时空闲态时,SCLK处于高电平,数据发送是在第1个边沿,也就是 SCLK 由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿。
对应到波形上:
STM32 上支持 3 路 SPI:
单板上使用 SPI2 进行 SPI FLASH 的操作,使用的是 APB1 的时钟,最大配置为 36MHz。
硬件上,通过单板的 SPI2 引脚,连接到外部的 SPI FLASH (W25Q64)
所以,在配置的时候,需要针对 SPI2 进行配置。
硬件单板上,连接的是 WinBond 的 W25Q64BV 的 SPI Flash,此款 Flash 的特性如下:
由于暂时不需要硬件写保护和Hold功能,故,直接将 WP和HOLD引脚接到 VCC(3.3V)
(此款SPI FLASH 还支持双线和4线 QSPI 的读写,由于 STM32 不支持,所以不在多说)
根据 W25Q64BV 的 Datasheet 描述,在操作这块 FLASH 的时候,需要配置主机为:
故,在 SPI2 配置的时候,需要进行对应的配置,才能够继续正常数据通信。
W25Q64BV 存在两个寄存器可以被访问,为 Status Register-1 和 Status Register-2,其中描述的关于 Write-Protect 的部分,暂时不管。与读写相关的就是 BUSY 位了,因为对 SPI Flash 编程后,Flash 需要内部的 cylce 进行数据的写入,内部program的时候会将 BUSY 置成 1,写入完成后,会将 BUSY 位置 0,故,每次对 Flash 进行写(包括擦除)之前,均要进行 BUSY 位的判断。
好啦,现在就开始按照 DateSheet 进行配置我们的 STM32 了。
配置过程主要分为两步:GPIO 的配置,SPI2 的配置(如原理图所示,PB_12 的 GPIO 用于了 CS 片选,我们需要将其配置成为输出的 GPIO,拉低的时候,选中 Flash,拉高的时候释放 Flash 信号)
1. 开启 GPIO B 组的时钟
2. 开启 SPI2 的时钟
3. 按照 STM32 手册,配置 SCK 、MOSI 和 NSS 为复用推挽输出、MISO为浮空输入(有的代码将 MISO 配置成为的输出,虽然也可以运行,不过,您不觉得别扭么?还是遵循 Spec 的来吧)。同时将 GPIO_B _12配置为输出(CS信号)
4. 配置 SPI2 为全双工模式
5. SPI2 为 Master
6. SPI2 运行在 MODE3(按照 W25Q64BV 的 Timing 要求 )
7. SPI2 NSS 为软件模式(根本没用)
8. 预分频系数为 4 分频(APB1 为 36M,则 SPI2 的 SCK 为 9 MHz)
9. SPI2 传输 MSB(按照 W25Q64BV 的 Timing 要求)
10. 不启用 CRC
11. 开启 SPI 功能
此刻 SPI 的配置就基本完成了。接下来就是 按照 W25Q64BV 的 Timing 要求,写 FLASH 驱动咯.....
#define SK_SPI_FLASH_CS_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_12)
#define SK_SPI_FLASH_CS_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_12)
/*******************************************************************************
* Function Name : SK_SPIPortInit
* Description : Configure the I/O port for SPI2.
* Input : None
* Output : None
* Return : None
*******************************************************************************/
static void _SK_SPI2PortInit(void)
{
GPIO_InitTypeDef stGpioInit;
/*!< Configure pins: SCK */
stGpioInit.GPIO_Pin = GPIO_Pin_13;
stGpioInit.GPIO_Speed = GPIO_Speed_50MHz;
stGpioInit.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOB, &stGpioInit);
/*!< Configure pins: MISO */
stGpioInit.GPIO_Pin = GPIO_Pin_14;
stGpioInit.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOB, &stGpioInit);
/*!< Configure pins: MOSI */
stGpioInit.GPIO_Pin = GPIO_Pin_15;
stGpioInit.GPIO_Speed = GPIO_Speed_50MHz;
stGpioInit.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOB, &stGpioInit);
/*!< Configure pins: CS */
stGpioInit.GPIO_Pin = GPIO_Pin_12;
stGpioInit.GPIO_Speed = GPIO_Speed_50MHz;
stGpioInit.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOB, &stGpioInit);
}
/*******************************************************************************
* Function Name : _SK_SPI2BusInit
* Description : Configure the SPI2 Bus to adpte the W25Q64 Flash.
* Input : None
* Output : None
* Return : None
*******************************************************************************/
static void _SK_SPI2BusInit(void)
{
SPI_InitTypeDef SPI_InitStructure;
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_4; // PCLK = 36M, SPI2 CLK = PCLK/4 = 9M
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI2, &SPI_InitStructure);
SPI_Cmd(SPI2, ENABLE);
}
/*******************************************************************************
* Function Name : SK_SPIInit
* Description : Initializes the peripherals used by the SPI FLASH driver.
* Input : None
* Output : None
* Return : None
*******************************************************************************/
void SK_SPIFlashInit(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
_SK_SPI2PortInit();
SK_SPI_FLASH_CS_HIGH();
_SK_SPI2BusInit();
}
W25Q64BV 是 Flash 嘛,最主要的就是读和写。当然,除了这些,还有擦除功能,读 ID 等等。不着急,一步一步来,都是套路。
STM32 在进行 SPI 全双工数据传输的时候,在通过 MOSI 写出去数据后,可以立马进行 MISO 数据读取,通过轮询 TXE 标志来判断数据已经全部加载到移位寄存器,通过轮询 RXE 标志来得知本次 MISO 的数据已经全部到账。
针对 W25Q64BV Flash,Datasheet 中列出了支持的多种不同的命令:
对应上述表格,有不同的 Timing 进行描述,比如:Opcode 为 0x9F 的时候,是读取一个叫 JEDEC ID 的东西:
可以看到,主机首先将 CS 拉低,然后主机处在 Mode 0 或者 Mode 3 的时候,在 MOSI 信号上输出 0x9F 的数据(命令),然后接着写入 Dummy Data (随便写点东西),然后再 MISO 信号上就能够收到 manufacture ID 的信息(为 0xEF),在继续写入 Dummy Data,继续接收,继续写入,继续接收,这样便可以得到期望的数据了。
根据上述方式,便可以获取 ID 信息:
/********************** W25Q64 Flash Command Defination ***********************/
#define SPI_FLASH_PerWritePageSize 256
#define W25X_WriteEnable 0x06
#define W25X_WriteDisable 0x04
#define W25X_ReadStatusReg_1 0x05
#define W25X_ReadStatusReg_2 0x35
#define W25X_WriteStatusReg 0x01
#define W25X_ReadData 0x03
#define W25X_FastReadData 0x0B
#define W25X_FastReadDual 0x3B
#define W25X_PageProgram 0x02
#define W25X_64K_BlockErase 0xD8
#define W25X_32K_BlockErase 0x52
#define W25X_4K_SectorErase 0x20
#define W25X_ChipErase 0xC7
#define W25X_PowerDown 0xB9
#define W25X_ReleasePowerDown 0xAB
#define W25X_DeviceID 0xAB
#define W25X_ManufactDeviceID 0x90
#define W25X_JedecDeviceID 0x9F
#define Busy_Flag 0x01 /* Write In Progress (WIP) flag */
#define Dummy_Byte 0xFF /* Dummy Data */
typedef struct {
uint8_t manufacturer_id;
uint8_t memory_type_id;
uint8_t capacity_id;
uint8_t device_id;
} W25Q64_ID_t;
/*******************************************************************************
* Function Name : SPI_FLASH_SendByte
* Description : Sends a byte through the SPI interface and return the byte
* received from the SPI bus.
* Input : byte : byte to send.
* Output : None
* Return : The value of the received byte.
*******************************************************************************/
static uint8_t SPI_FLASH_SendByte(uint8_t byte)
{
/* Loop while DR register in not emplty */
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET);
/* Send byte through the SPI2 peripheral */
SPI_I2S_SendData(SPI2, byte);
/* Wait to receive a byte */
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET);
/* Return the byte read from the SPI bus */
return SPI_I2S_ReceiveData(SPI2);
}
/*******************************************************************************
* Function Name : SK_GetFlashID
* Description : Reads FLASH identification.
* Input : W25Q64_ID_t pointer
* Output : None
* Return : FLASH identification
*******************************************************************************/
void SK_GetFlashID(W25Q64_ID_t *stW25Q64_ID)
{
SK_SPI_FLASH_CS_LOW();
SPI_FLASH_SendByte(W25X_JedecDeviceID);
stW25Q64_ID->manufacturer_id = SPI_FLASH_SendByte(Dummy_Byte);
stW25Q64_ID->memory_type_id = SPI_FLASH_SendByte(Dummy_Byte);
stW25Q64_ID->capacity_id = SPI_FLASH_SendByte(Dummy_Byte);
SK_SPI_FLASH_CS_HIGH();
}
/*******************************************************************************
* Function Name : SK_GetFlashDeviceID
* Description : Reads FLASH Device identification.
* Input : W25Q64_ID_t pointer
* Output : None
* Return : FLASH identification
*******************************************************************************/
void SK_GetFlashDeviceID(W25Q64_ID_t *stW25Q64_ID)
{
SK_SPI_FLASH_CS_LOW();
SPI_FLASH_SendByte(W25X_DeviceID);
SPI_FLASH_SendByte(Dummy_Byte);
SPI_FLASH_SendByte(Dummy_Byte);
SPI_FLASH_SendByte(Dummy_Byte);
stW25Q64_ID->device_id = SPI_FLASH_SendByte(Dummy_Byte);
SK_SPI_FLASH_CS_HIGH();
}
Flash 的擦除分为三种,Sector 擦除(4K 为单位),Block擦除(32K or 64K 为单位)以及 Chip 擦除(全部擦除)
基本的方式都是一样的,支持命令不一样而已:
先拉低 CS,然后传输擦除的命令(0x20 or 0xD8 or 0x52),在传输期望擦除的地址起始值:24 bits Address,进行操作后,需要等待 BUSY 为 0 ,方可退出。
针对 BUSY 位的判断,是通过读取 Status Register-1 的最后一位来确定的:
/*******************************************************************************
* Function Name : SK_SPI_FLASH_WaitBusy
* Description : Polls the status of the Write In Progress (WIP) flag in the
* FLASH's status register and loop until write opertaion
* has completed.
* Input : None
* Output : None
* Return : None
*******************************************************************************/
void SK_SPI_FLASH_WaitBusy(void)
{
u8 FLASH_Status = 0;
/* Select the FLASH: Chip Select low */
SK_SPI_FLASH_CS_LOW();
/* Send "Read Status Register" instruction */
SPI_FLASH_SendByte(W25X_ReadStatusReg_1);
/* Loop as long as the memory is busy with a write cycle */
do
{
/* Send a dummy byte to generate the clock needed by the FLASH
and put the value of the status register in FLASH_Status variable */
FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);
}
while ((FLASH_Status & Busy_Flag) == SET); /* Write in progress */
/* Deselect the FLASH: Chip Select high */
SK_SPI_FLASH_CS_HIGH();
}
/*******************************************************************************
* Function Name : SK_SPI_FLASH_Erase
* Description : Erases the specified FLASH
* Input : SectorAddr: address of the sector to erase.
* Input : EraseType : The type of erase.
* : W25X_4K_SectorErase (4KB Erase)
* : W25X_32K_BlockErase (32KB Erase)
* : W25X_64K_BlockErase (64KB Erase)
* Output : None
* Return : None
*******************************************************************************/
void SK_SPI_FLASH_Erase(uint32_t SectorAddr, uint8_t EraseType)
{
/* Send write enable instruction */
SK_SPI_FLASH_WriteEnable();
SK_SPI_FLASH_WaitBusy();
/* Sector Erase */
/* Select the FLASH: Chip Select low */
SK_SPI_FLASH_CS_LOW();
/* Send Sector Erase instruction */
SPI_FLASH_SendByte(EraseType);
/* Send SectorAddr high nibble address byte */
SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);
/* Send SectorAddr medium nibble address byte */
SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);
/* Send SectorAddr low nibble address byte */
SPI_FLASH_SendByte(SectorAddr & 0xFF);
/* Deselect the FLASH: Chip Select high */
SK_SPI_FLASH_CS_HIGH();
/* Wait the end of Flash writing */
SK_SPI_FLASH_WaitBusy();
}
注意,在写 W25Q64BV Flash 的时候,Datesheet 规定,需要先写入 Write Enable 命令:
/*******************************************************************************
* Function Name : SK_SPI_FLASH_WriteEnable
* Description : Enables the write access to the FLASH.
* Input : None
* Output : None
* Return : None
*******************************************************************************/
void SK_SPI_FLASH_WriteEnable(void)
{
/* Select the FLASH: Chip Select low */
SK_SPI_FLASH_CS_LOW();
/* Send "Write Enable" instruction */
SPI_FLASH_SendByte(W25X_WriteEnable);
/* Deselect the FLASH: Chip Select high */
SK_SPI_FLASH_CS_HIGH();
}
此刻代表 SPI Flash 已经处在可被写的状态,然后对 SPI FLASH 的写操作,支持 0~256-bytes的写操作:
当然,写完同样需要 Check Buys 位:
/*******************************************************************************
* Function Name : SK_SPI_FLASH_PageWrite
* Description : Writes more than one byte to the FLASH with a single WRITE
* cycle(Page WRITE sequence). The number of byte can't exceed
* the FLASH page size.
* Input : - pBuffer : pointer to the buffer containing the data to be
* written to the FLASH.
* - WriteAddr : FLASH's internal address to write to.
* - NumByteToWrite : number of bytes to write to the FLASH,
* must be equal or less than "SPI_FLASH_PageSize" value.
* Output : None
* Return : None
*******************************************************************************/
void SK_SPI_FLASH_PageWrite(uint8_t *pBuffer, uint32_t WriteAddr, uint32_t NumByteToWrite)
{
/* Enable the write access to the FLASH */
SK_SPI_FLASH_WriteEnable();
/* Select the FLASH: Chip Select low */
SK_SPI_FLASH_CS_LOW();
/* Send "Write to Memory " instruction */
SPI_FLASH_SendByte(W25X_PageProgram);
/* Send WriteAddr high nibble address byte to write to */
SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
/* Send WriteAddr medium nibble address byte to write to */
SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
/* Send WriteAddr low nibble address byte to write to */
SPI_FLASH_SendByte(WriteAddr & 0xFF);
if(NumByteToWrite > SPI_FLASH_PerWritePageSize)
{
NumByteToWrite = SPI_FLASH_PerWritePageSize;
}
/* while there is data to be written on the FLASH */
while (NumByteToWrite--)
{
/* Send the current byte */
SPI_FLASH_SendByte(*pBuffer);
/* Point on the next byte to be written */
pBuffer++;
}
/* Deselect the FLASH: Chip Select high */
SK_SPI_FLASH_CS_HIGH();
/* Wait the end of Flash writing */
SK_SPI_FLASH_WaitBusy();
}
读数据的过程,同样也是先 CS 拉低,在写入读数据的指令(0x03),接着跟 24-bit 的地址信息。然后一直发送 Dummy Data,同时读到指定的数据到 BUFFER
/*******************************************************************************
* Function Name : SK_SPI_FLASH_BufferRead
* Description : Reads a block of data from the FLASH.
* Input : - pBuffer : pointer to the buffer that receives the data read
* from the FLASH.
* - ReadAddr : FLASH's internal address to read from.
* - NumByteToRead : number of bytes to read from the FLASH.
* Output : None
* Return : None
*******************************************************************************/
void SK_SPI_FLASH_BufferRead(uint8_t* pBuffer, uint32_t ReadAddr, uint32_t NumByteToRead)
{
/* Select the FLASH: Chip Select low */
SK_SPI_FLASH_CS_LOW();
/* Send "Read from Memory " instruction */
SPI_FLASH_SendByte(W25X_ReadData);
/* Send ReadAddr high nibble address byte to read from */
SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);
/* Send ReadAddr medium nibble address byte to read from */
SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);
/* Send ReadAddr low nibble address byte to read from */
SPI_FLASH_SendByte(ReadAddr & 0xFF);
while (NumByteToRead--)
{
*pBuffer = SPI_FLASH_SendByte(Dummy_Byte);
pBuffer++;
}
/* Deselect the FLASH: Chip Select high */
SK_SPI_FLASH_CS_HIGH();
}
在 main 函数中,初始化 SPI 后,进行 Flash 的 ID 读取,扇区擦除,数据读写的操作,同时,也可以进行数据写入后,断电,然后再次读出的操作,看看数据是否正确保存。
#define SPI_FLASH_DATA_LEN 100
#define SPI_FLASH_START_ADDR 0x00
W25Q64_ID_t g_stW25Q64_ID;
uint8_t g_spi_flash_rd_data[SPI_FLASH_DATA_LEN] = {0};
uint8_t g_spi_flash_wr_data[SPI_FLASH_DATA_LEN] = {0};
int main(void)
{
uint32_t i = 0;
/* SPI FLASH TEST */
SK_SPIFlashInit();
SK_GetFlashID(&g_stW25Q64_ID);
SK_GetFlashDeviceID(&g_stW25Q64_ID);
SK_SPI_FLASH_Erase(SPI_FLASH_START_ADDR, W25X_4K_SectorErase);
SK_SPI_FLASH_BufferRead(g_spi_flash_rd_data, SPI_FLASH_START_ADDR, SPI_FLASH_DATA_LEN);
for (i = 0; i < SPI_FLASH_DATA_LEN; i++)
g_spi_flash_wr_data[i] = i;
SK_SPI_FLASH_PageWrite(g_spi_flash_wr_data, SPI_FLASH_START_ADDR, SPI_FLASH_DATA_LEN);
SK_SPI_FLASH_BufferRead(g_spi_flash_rd_data, SPI_FLASH_START_ADDR, SPI_FLASH_DATA_LEN);
}