STM32SPIFLASH读写
STM32SPIFLASH读写
1.1 SPI注意事项
SPI是同步通信,即通信双方每次信息交互必会带有一问一答,这代表在正常的单核MCU(例如STM32)中很难实现软件模拟的双向SPI通信(TFT屏幕一类的外设不算,那些顶多属于单向SPI),因为无法同时发送和接收数据。而在STM32中,硬件实现同步通信的办法是利用硬件缓冲区,以字节为单位,每次发送一个字节的数据,接收缓冲区就会缓存一个字节的接收数据,如此实现同时接收和发送。
1.2 SPI代码编写
SPI的代码需要引用如下的标准库头文件:
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_spi.h"
其中rcc头文件用于外设时钟的初始化,gpio头文件用于GPIO口的初始化,spi头文件用于SPI外设的初始化。
1.1.1 初始化结构体与定义变量
//定义SPI设备
#define FLASH_SPI SPI1
//定义CS引脚
#define FLASH_SPI_CS_PORT GPIOC
#define FLASH_SPI_CS_Pin GPIO_Pin_0
//定义SCK引脚
#define FLASH_SPI_SCK_PORT GPIOA
#define FLASH_SPI_SCK_Pin GPIO_Pin_5
//定义MISO引脚
#define FLASH_SPI_MISO_PORT GPIOA
#define FLASH_SPI_MISO_Pin GPIO_Pin_6
//定义MOSI引脚
#define FLASH_SPI_MOSI_PORT GPIOA
#define FLASH_SPI_MOSI_Pin GPIO_Pin_7
//定义CS引脚控制函数
#define SPI_FLASH_CS_High() GPIO_SetBits(FLASH_SPI_CS_PORT,FLASH_SPI_CS_Pin)
#define SPI_FLASH_CS_Low() GPIO_ResetBits(FLASH_SPI_CS_PORT,FLASH_SPI_CS_Pin)
//定义无意义字节,用于挤占同步通信以读取数据
#define Dummy_Byte 0xFF
void SPI_FLASH_Init() //SPI初始化函数
{
SPI_InitTypeDef FLASH_InitStructure; //定义SPI初始化结构体
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE); //使能SPI1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //使能GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE); //使能GPIOC时钟
GPIO_InitTypeDef GPIO_InitStructure; //定义GPIO初始化结构体
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_CS_Pin; //定义CS引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //定义CS引脚为推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //定义CS引脚速度为50MHz
GPIO_Init(FLASH_SPI_CS_PORT,&GPIO_InitStructure); //初始化CS引脚
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_Pin; //定义SCK引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //定义SCK引脚为复用推挽输出
GPIO_Init(FLASH_SPI_SCK_PORT,&GPIO_InitStructure); //初始化SCK引脚
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_Pin; //定义MOSI引脚
GPIO_Init(FLASH_SPI_MOSI_PORT,&GPIO_InitStructure); //初始化MOSI引脚
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_Pin; //定义MISO引脚
GPIO_Init(FLASH_SPI_MISO_PORT,&GPIO_InitStructure); //初始化MISO引脚
SPI_FLASH_CS_High(); //CS引脚打开后最好将其置高以释放CS片选线
FLASH_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //定义SPI为双线全双工模式
FLASH_InitStructure.SPI_Mode = SPI_Mode_Master; //定义SPI为主模式
FLASH_InitStructure.SPI_DataSize = SPI_DataSize_8b; //定义SPI数据大小为8位
FLASH_InitStructure.SPI_CPOL = SPI_CPOL_High; //定义时钟极性为高电平
FLASH_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //定义时钟相位为第二个时钟边沿
FLASH_InitStructure.SPI_NSS = SPI_NSS_Soft; //定义NSS信号由软件控制
FLASH_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; //定义波特率预分频为4
FLASH_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //定义数据传输从MSB位开始
FLASH_InitStructure.SPI_CRCPolynomial = 7; //定义CRC多项式为7
SPI_Init(FLASH_SPI,&FLASH_InitStructure); //初始化SPI
SPI_Cmd(FLASH_SPI,ENABLE);
}
其中SPI外设的主要参数为SPI_Direction、SPI_Mode、SPI_DataSize、SPI_NSS、SPI_BaudRatePrescaler、SPI_FirstBit。SPI_Direction决定了SPI外设的工作模式,可设置为双线全双工、双线只接收、单线只接收、单线只发送四个模式。SPI_Mode决定了SPI外设的工作模式,可设置为主模式、从模式。SPI_DataSize决定了每一帧数据帧的长度。SPI_NSS决定了NSS信号的来源,NSS信号即为CS信号,可设置为软件控制、硬件控制。SPI_BaudRatePrescaler决定了波特率预分频,可设置为主频的2、4、6、8、16、128、256分频。SPI_FirstBit决定了数据传输的起始位,可设置为MSB位或LSB位,即数据从左向右读取与从右向左读取。
其余参数也很重要,但若只是使用的话并没有上面那些起决定性作用。其中SPI_CPOL与SPI_CPHA决定了数据的读取模式,SPI_CPOL设置时钟信号为高电平有效或低电平有效,SPI_CPHA设置时钟相位为第一个时钟边沿或第二个时钟边沿有效。SPI_CRCPolynomial决定了CRC校验的中的多项式。
1.1.2 SPI发送与接收函数
uint8_t SPI_FLASH_SendRecive(uint8_t byte) //SPI发送接收函数
{
while(SPI_I2S_GetFlagStatus(FLASH_SPI,SPI_I2S_FLAG_TXE) == RESET); //等待发送缓冲区为空
SPI_I2S_SendData(FLASH_SPI,byte); //发送一个字节
while(SPI_I2S_GetFlagStatus(FLASH_SPI,SPI_I2S_FLAG_RXNE) == RESET); //等待接收缓冲区非空
return SPI_I2S_ReceiveData(FLASH_SPI); //从接收缓冲区读取一个字节
}
发送与接收函数没什么好说的,就单纯的等待SPI外设寄存器状态,然后发送或接收数据。不过这点与IIC不同的是,发送与接收无法分开,必须要同步进行。
2.1 FLASH注意事项
我所使用的开发板为野火STM32F103VET6开发板,其板载SPI FLASH芯片为W25Q64,若板载SPI FLASH芯片不同,则指令与扇区大小也不同,需查看手册确定。
在W25Q64中,有128个块(BLOCK),每个块有16个扇区(SECTOR),每个扇区有64个页(PAGE)。块 = 64KB,扇区 = 4KB,页 = 256B。每次擦除FLASH最小为整个扇区擦除,每次写入FLASH最大不超过一页,即256个字节。
2.2 FLASH代码编写
FLASH的头文件引用需要如下头文件:
#include "Spi.h"
Spi头文件内为文章上述SPI相关的代码。
2.2.1 定义变量与初始化
W25Q64为自带微型处理器的设备,STM32作为主设备若利用W25Q64读取与写入数据,需要对应其指令表进行操作,下列代码为W25Q64的指令定义:
#define W25X_WriteEnable 0x06 //写使能指令
#define W25X_WriteDisable 0x04 //写失能指令
#define W25X_ReadStatusReg 0x05 //读寄存器指令
#define W25X_WriteStatusReg 0x01 //写寄存器指令
#define W25X_ReadData 0x03 //读数据指令
#define W25X_FastReadData 0x0B //快速读取数据指令
#define W25X_FastReadDual 0x3B //快速读取两倍数据指令
#define W25X_PageProgram 0x02 //页编程指令
#define W25X_BlockErase 0xD8 //块擦除指令
#define W25X_SectorErase 0x20 //扇区擦除指令
#define W25X_ChipErase 0xC7 //整片擦除指令
#define W25X_PowerDown 0xB9 //掉电模式指令
#define W25X_ReleasePowerDown 0xAB //唤醒模式指令
#define W25X_DeviceID 0xAB //设备ID指令
#define W25X_ManufactDeviceID 0x90 //生产ID指令
#define W25X_JedecDeviceID 0x9F //JEDEC设备ID指令
#define sFlash_ID 0xEF4017 //JEDEC设备ID
#define WIP_Flag 0x01 //设备忙标志位
#define FLASH_Page_Size 0x1000//定义FLASH每页的大小,4K = 4096 = 0x1000
2.2.2 W25Q64通信验证
当STM32通过SPI向W25Q64发送W25X_JedecDeviceID命令时,W25Q64会返回一个24位的JEDECID,可通过此ID确定通信是否成功。
uint8_t FLASH_Device_Init(void) //初始化FLASH设备
{
uint32_t temp1 = 0,temp2 = 0,temp3 = 0; //定义三个临时变量
uint8_t status = 0; //定义状态变量
SPI_FLASH_CS_Low(); //拉低CS片选线,选中FLASH
SPI_FLASH_SendRecive(W25X_JedecDeviceID); //发送W25X_JedecDeviceID命令
temp1 = SPI_FLASH_SendRecive(Dummy_Byte); //接收一个字节
temp2 = SPI_FLASH_SendRecive(Dummy_Byte); //接收一个字节
temp3 = SPI_FLASH_SendRecive(Dummy_Byte); //接收一个字节
SPI_FLASH_CS_High(); //拉高CS片选线,取消选中FLASH
if( ((temp1<<16)|(temp2<<8)|temp3) == sFlash_ID) //比较接收到的JEDECID与预设的JEDECID
{
status = 0x01; //通信成功
return status; //返回通信状态
}
else
{
return 0; //通信失败
}
}
2.2.2 写入使能及写保护检测
W25Q64具有严格的写保护机制,若想要向其写入数据,必须保证写使能,且写入的页面必须为擦除后的页面。而且每次对FLASH内容进行修改后,W25Q64硬件会自动写失能,会触发写失能的操作有“写状态寄存器”、“页编程”、“扇区擦除”、“块区擦除”、“芯片擦除”。要想打开写使能,只需向W25Q64发送W25X_WriteEnable命令后释放CS片选线即可。
void FLASH_WriteEnable(void) //写入使能
{
SPI_FLASH_CS_Low(); //拉低CS片选线,选中FLASH
SPI_FLASH_SendRecive(W25X_WriteEnable); //发送W25X_WriteEnable命令
SPI_FLASH_CS_High(); //拉高CS片选线,取消选中FLASH
}
W25Q64对于自身的状态有一个8位的寄存器专门存放,当主设备向W25Q64发送状态寄存器查询时,不论此时W25Q64处于什么状态,都会将8位的状态寄存器返回给主设备。而这8位的状态寄存器中,第0位为写保护位,若该位为1,则表示W25Q64处于写保护状态,此时主设备无法向其写入数据。可以在此时写一个死循环重复读取其状态寄存器,直到该位为0,在进行后续操作。
void FLASH_WaitForWriteEnd(void) //等待写入结束
{
uint8_t FLASH_Status = 0xff; //初始化状态变量
SPI_FLASH_CS_Low(); //拉低CS片选线,选中FLASH
SPI_FLASH_SendRecive(W25X_ReadStatusReg); //发送W25X_ReadStatusReg命令
while((FLASH_Status & WIP_Flag) == SET) //若写入标志位为1,则表示写入未完成
{
FLASH_Status = SPI_FLASH_SendRecive(Dummy_Byte); //发送占位字节,接收状态寄存器
}
SPI_FLASH_CS_High(); //拉高CS片选线,取消选中FLASH
}
2.2.3 FLASH扇区擦除
在前文中提到过,W25Q64的最小擦除单位位扇区,每个扇区的大小为4KB。若想要擦除W25Q64中的数据,只需向其发送W25X_SectorErase命令,再将其24位的地址发送给W25Q64即可。
void FLASH_SecortErase(uint32_t addr) //扇区擦除
{
FLASH_WaitForWriteEnd(); //等待写入结束
FLASH_WriteEnable(); //写入使能
FLASH_WaitForWriteEnd(); //等待写入结束
SPI_FLASH_CS_Low(); //拉低CS片选线,选中FLASH
SPI_FLASH_SendRecive(W25X_SectorErase); //发送W25X_SectorErase命令
SPI_FLASH_SendRecive((addr & 0xFF0000) >>16); //发送24位地址中的高8位
SPI_FLASH_SendRecive((addr & 0xFF00)>>8); //发送24位地址中的中间8位
SPI_FLASH_SendRecive(addr & 0xFF); //发送24位地址中的低8位
SPI_FLASH_CS_High(); //拉高CS片选线,取消选中FLASH
FLASH_WaitForWriteEnd(); //等待写入结束
}
2.2.4 FLASH页写入
W25Q64的最小写入单位为页,每个页的大小为256字节。若想要向W25Q64中写入数据,只需向其发送W25X_PageProgram命令,再将其24位的地址发送给W25Q64,最后发送要写入的数据即可。但是有个限制,每次写入的数据大小不能超过256字节,即不能超过一页,超过一页就需要重新等待写入结束,写使能,发送写指令与地址。
void FLASH_Write(uint8_t* databuff,uint32_t addr,uint32_t data_length) //页写入
{
FLASH_WaitForWriteEnd(); //等待写入结束
FLASH_WriteEnable(); //写入使能
FLASH_WaitForWriteEnd(); //等待写入结束
SPI_FLASH_CS_Low(); //拉低CS片选线,选中FLASH
SPI_FLASH_SendRecive(0x02); //发送W25X_PageProgram命令
SPI_FLASH_SendRecive((addr & 0xFF0000) >>16); //发送24位地址中的高8位
SPI_FLASH_SendRecive((addr & 0xFF00)>>8); //发送24位地址中的中间8位
SPI_FLASH_SendRecive(addr & 0xFF); //发送24位地址中的低8位
while(data_length--) //循环发送数据
{
SPI_FLASH_SendRecive(*databuff); //发送一个字节的数据
databuff++; //指针后移
}
SPI_FLASH_CS_High(); //拉高CS片选线,取消选中FLASH
FLASH_WaitForWriteEnd(); //等待写入结束
}
2.2.5 FLASH不定页写入
不定页写入与页写入类似,区别在于不定页写入每次写入的数据大小通过处理将其划分为每一页依次写入,即可以调用该函数写入任意长度的数据。
void FLASH_PageWrite(uint8_t* databuff,uint32_t addr,uint32_t data_length) //不定页写入
{
uint32_t page_count,page_other,i,addr_start,addr_other; //定义变量
addr_start = addr % 256; //计算地址的起始位置
addr_other = 256 - addr_start; //计算地址的剩余位置
if(addr_start == 0) //如果地址的起始位置为0
{
if(data_length >= 256) //如果数据长度大于等于256
{
page_count = data_length/256; //计算页数
page_other = data_length%256; //计算剩余数据长度
for(i=0;i<page_count;i++) //循环写入页数据
{
FLASH_Write(databuff,(addr += (i * 256)),256); //写入256个字节的数据
*databuff += page_count * 256; //指针后移
}
FLASH_Write(databuff,(addr += (page_count * 256)),page_other); //写入剩余数据
}
else //如果数据长度小于256
{
FLASH_Write(databuff,addr,data_length); //写入数据
}
}
else //如果地址的起始位置不为0
{
FLASH_Write(databuff,addr,addr_other); //先写入地址的剩余位置数据
*databuff += addr_other; //指针后移
data_length -= addr_other; //数据长度减去不满一页地址的长度
if(data_length >= 256) //如果数据长度大于等于256
{
page_count = data_length/256; //计算页数
page_other = data_length%256; //计算剩余数据长度
for(i=0;i<page_count;i++) //循环写入页数据
{
FLASH_Write(databuff,(addr += (i * 256)),256); //写入256个字节的数据
*databuff += page_count * 256; //指针后移
}
FLASH_Write(databuff,(addr += (page_count * 256)),page_other); //写入剩余数据
}
else
{
FLASH_Write(databuff,addr,data_length); //写入数据
}
}
}
2.2.5 FLASH读取
虽然FLASH写入有256字节的限制,但是读取时没有限制,只要发送W25X_ReadData与读取起始即可一直接收数据。
void FLASH_Read(uint8_t* databuff,uint32_t addr,uint32_t data_length) //读取数据
{
FLASH_WaitForWriteEnd(); //等待写入完成
SPI_FLASH_CS_Low(); //拉低CS片选线,选中FLASH
SPI_FLASH_SendRecive(W25X_ReadData); //发送读取命令
SPI_FLASH_SendRecive((addr & 0xFF0000) >>16); //发送地址的高8位
SPI_FLASH_SendRecive((addr & 0xFF00)>>8); //发送地址的中8位
SPI_FLASH_SendRecive(addr & 0xFF); //发送地址的低8位
while(data_length--) //循环读取数据
{
*databuff = SPI_FLASH_SendRecive(Dummy_Byte); //读取数据
databuff++; //指针后移
}
SPI_FLASH_CS_High(); //拉高CS片选线,取消选中FLASH
FLASH_WaitForWriteEnd(); //等待写入完成
}