STM32——“SPI Flash”
引入
在给单片机写程序的时候,有时会用到显示屏,就拿市面上的0.96寸单色显示器来说,一张全屏的图片就占用8x128=1024个字节,即1kb的空间,这对于单片机来说确实有点奢侈,于是我买了一个8Mb的SPI Flash,型号为华邦的W25Q64。
在手册里很容易看到他的介绍:
它支持四线的SPI,在很大程度上增加了读写速度,同时在H7系列中还可以用作扩展的Flash,自带的QSPI功能强大,但是在F1系列中没有QSPI的功能,因此这里只介绍用STM32的普通硬件SPI来驱动这块Flash。
想要驱动这块Flash,首先要配置STM32的硬件SPI:
一、配置SPI
SPI_HandleTypeDef g_W25Qxx_Handle;
void SPI_Init()
{
g_W25Qxx_Handle.Instance = SPIx;
g_W25Qxx_Handle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
g_W25Qxx_Handle.Init.CLKPhase = SPI_PHASE_1EDGE;
g_W25Qxx_Handle.Init.CLKPolarity = SPI_POLARITY_LOW;
g_W25Qxx_Handle.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
g_W25Qxx_Handle.Init.CRCPolynomial = 1;
g_W25Qxx_Handle.Init.DataSize = SPI_DATASIZE_8BIT;
g_W25Qxx_Handle.Init.Direction = SPI_DIRECTION_2LINES;
g_W25Qxx_Handle.Init.FirstBit = SPI_FIRSTBIT_MSB;
g_W25Qxx_Handle.Init.Mode = SPI_MODE_MASTER;
g_W25Qxx_Handle.Init.NSS = SPI_NSS_SOFT;
g_W25Qxx_Handle.Init.TIMode = SPI_TIMODE_DISABLE;
HAL_SPI_Init(&g_W25Qxx_Handle);
}
void SPI_GPIO_Init()
{
SPI_FLASH_SCLK_RCC();
SPI_FLASH_CS_RCC();
SPI_FLASH_MISO_RCC();
SPI_FLASH_MOSI_RCC();
SPI_FLASH_SPI_RCC();
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pin = FLASH_SCLK_PIN;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(SPI_FLASH_SCLK_PORT,&GPIO_InitStruct);
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pin = FLASH_MISO_PIN;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(SPI_FLASH_MISO_PORT, &GPIO_InitStruct);
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pin = FLASH_MOSI_PIN;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(SPI_FLASH_MOSI_PORT,&GPIO_InitStruct);
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 普通输出
GPIO_InitStruct.Pin = FLASH_CCS_PIN;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(SPI_FLASH_CCS_PORT, &GPIO_InitStruct);
}
void SelectChip()
{
HAL_GPIO_WritePin(SPI_FLASH_CCS_PORT,FLASH_CCS_PIN,GPIO_PIN_RESET);
}
void UnselectChip()
{
HAL_GPIO_WritePin(SPI_FLASH_CCS_PORT,FLASH_CCS_PIN,GPIO_PIN_SET);
}
引脚宏为:
#define SPIx SPI1
#define FLASH_CCS_PIN GPIO_PIN_4
#define FLASH_SCLK_PIN GPIO_PIN_5
#define FLASH_MISO_PIN GPIO_PIN_6
#define FLASH_MOSI_PIN GPIO_PIN_7
#define SPI_FLASH_SCLK_PORT GPIOA
#define SPI_FLASH_CCS_PORT GPIOA
#define SPI_FLASH_MISO_PORT GPIOA
#define SPI_FLASH_MOSI_PORT GPIOA
#define SPI_FLASH_SCLK_RCC() __HAL_RCC_GPIOA_CLK_ENABLE()
#define SPI_FLASH_CS_RCC() __HAL_RCC_GPIOA_CLK_ENABLE()
#define SPI_FLASH_MISO_RCC() __HAL_RCC_GPIOA_CLK_ENABLE()
#define SPI_FLASH_MOSI_RCC() __HAL_RCC_GPIOA_CLK_ENABLE()
#define SPI_FLASH_SPI_RCC() __HAL_RCC_SPI1_CLK_ENABLE()
在SPI的配置上使用模式0或者模式3的高位先发,在这里我使用模式0,即:极性为低,相位是在第一个上升沿。
体现在上边的代码为:
g_W25Qxx_Handle.Init.CLKPhase = SPI_PHASE_1EDGE;
g_W25Qxx_Handle.Init.CLKPolarity = SPI_POLARITY_LOW;
由于在我写硬件SPI代码的过程中,遇到了不能准确识别芯片的问题,研究了很久,才发现是片选代码的问题,即:在写了OLED驱动之后由于思维惯性,我的想法是只要持续拉低片选就一直可以通信,但现实是的确可以通信,只是Flash貌似识别不了我发的命令。最后的解决方法是在每次通信之前拉低片选,通信完之后拉高片选。这里的通信是有命令发出的时候。
解决完这个问题之后,首先要做的就是和Flash建立通信,即获取Flash的ID。
二、查询ID
在手册里,Flash的ID为:EF4017
对应的查询命令为:0x9F
第一个是厂商ID,后边的是芯片ID。
/*
**** 函数名 W25Q64GetID
**** 功能 W25Q64 读取设备ID号
**** 参数 无
****
*/
uint32_t W25Qxx_GetID()
{
uint32_t ID = 0;
uint8_t id[3];
uint8_t cmd = JEDEC_ID;
SelectChip();
HAL_SPI_Transmit(&g_W25Qxx_Handle,&cmd,1,1000);
HAL_SPI_Receive(&g_W25Qxx_Handle,id,3,1000);
ID = (((((ID | id[0]) << 8) | id[1]) << 8) | id[2]);
UnselectChip();
return ID;
}
这里发送命令,然后接收三个字节的ID,最后拼接ID并返回。
int main()
{
HAL_Init();
SystemClock_Config();
OLED_Init();
LED_Init();
W25Qxx_Init();
UsartInit(115200);
printf("现在进行硬件SPI实验!\n\n");
uint32_t id = W25Qxx_GetID();
printf("芯片ID为:%X\n\n",id);
while(1)
{
}
}
执行后的效果如下:
可见完全没问题。
三、读状态寄存器
接下来是写数据,但是在写数据之前需要写使能和擦除扇区,另外在进行这两个操作之前需要检查Flash是否繁忙,于是接下来是检查Flash的状态,即,检查状态寄存器1
我们主要检查第一个寄存器的第一个比特位。然而检查第一个寄存器状态的命令是0x05
返回之后对第一位进行检查:
/*
**** 函数名 W25Q64CheckBusy
**** 功能 W25Q64 读状态寄存器
**** 参数 无
****
*/
void W25Qxx_CheckBusy()
{
uint8_t ret = 0;
uint8_t cmd = ReadStatusRegister;
SelectChip();
HAL_SPI_Transmit(&g_W25Qxx_Handle,&cmd,1,1000);
do{
HAL_SPI_Receive(&g_W25Qxx_Handle,&ret,1,1000);
}while((ret & 0x01) == 0x01);
UnselectChip();
}
倘若Flash繁忙,则第一位为1,反之为0,为1就一直检查,直到芯片空闲。
接下来是写使能。
四、写使能
Flash手册里提到:在进行写入,擦除,写状态寄存器等操作之前必须进行写使能。
它对应的命令是0x06
/*
**** 函数名 W25Q64WriteEnable
**** 功能 W25Q64 写使能
**** 参数 无
****
*/
void W25Qxx_WriteEnable()
{
uint8_t cmd = WriteEnable;
SelectChip();
HAL_SPI_Transmit(&g_W25Qxx_Handle,&cmd,1,1000);
UnselectChip();
}
准备工作进行完之后就是擦除了。
五、擦除
W25Q64的擦除命令有四个:扇区擦除(4kb)-0x20,块擦除(32kb)-0x52,块擦除(64kb)-0xD8,全片擦除-0x60。
由于前三个的代码大同小异,因此只介绍一下第一个和最后一个。
1.扇区擦除(4kb)
这里是需要输入地址的,因此在发送命令以后需要发送地址,大小为24位:
/*
**** 函数名 W25Q64SectorErase
**** 功能 W25Q64 扇区擦除(4kb)
**** 参数 address:24位的地址
****
*/
void W25Qxx_SectorErase(uint32_t address)
{
uint8_t cmd[4];
cmd[0] = SectorErase;
cmd[1] = (address >> 16) & 0xFF;
cmd[2] = (address >> 8) & 0xFF;
cmd[3] = address & 0xFF;
W25Qxx_CheckBusy();
W25Qxx_WriteEnable();
W25Qxx_CheckBusy();
SelectChip();
HAL_SPI_Transmit(&g_W25Qxx_Handle,cmd,4,1000);
UnselectChip();
}
擦除之前记得写使能和检查芯片状态。
2.全片擦除
全片擦除不需要输入地址,但是全片擦除等待的时间很长。
/*
**** 函数名 W25Q64ChipErase
**** 功能 W25Q64 全片擦除
**** 参数 无
****
*/
void W25Qxx_ChipErase()
{
uint8_t cmd[1];
cmd[0] = ChipErase;
W25Qxx_CheckBusy();
W25Qxx_WriteEnable();
W25Qxx_CheckBusy();
SelectChip();
HAL_SPI_Transmit(&g_W25Qxx_Handle,cmd,1,1000);
UnselectChip();
W25Qxx_CheckBusy();
}
这里可以测试一下全片擦除的时间:
int main()
{
HAL_Init();
SystemClock_Config();
OLED_Init();
LED_Init();
W25Qxx_Init();
UsartInit(115200);
printf("现在进行硬件SPI实验!\n\n");
uint32_t id = W25Qxx_GetID();
printf("芯片ID为:%X\n\n",id);
uint32_t head = 0,tail = 0;
if(id == 0xEF4017)
{
head = HAL_GetTick();
W25Qxx_ChipErase();
tail = HAL_GetTick();
printf("全片擦除所用的时间为%d ms\n\n",tail - head);
}
while(1)
{
}
}
时间还是比较长的。另外值得注意的是,根据我的实验结果,擦除时候并不是按照你给的地址开始擦除,而是擦除你地址所在的扇区或者块。
六、页编程
页编程的命令是0x02
在此之前付下如图:
这个图介绍了编程的最小范围是一页,编程超过一页的需要手动变换地址,因为一页写满之后,地址并不会自动跳到下一页继续写,而是回到该页首地址继续写,这样会造成前后数据的覆盖。另外一页是256字节。
/*
**** 函数名 W25Q64PageProgram
**** 功能 W25Q64 页编程(一页256字节)
**** 参数 address:24位的地址
**** 参数 data: 写入的数据
**** 参数 Size:数据的大小,单位:字节
*/
void W25Qxx_PageProgram(uint32_t address,uint8_t* data,uint16_t Size)
{
uint8_t cmd[4];
cmd[0] = PageProgram;
cmd[1] = (address >> 16) & 0xFF;
cmd[2] = (address >> 8) & 0xFF;
cmd[3] = address & 0xFF;
W25Qxx_CheckBusy();
W25Qxx_WriteEnable();
W25Qxx_CheckBusy();
SelectChip();
HAL_SPI_Transmit(&g_W25Qxx_Handle,cmd,4,1000);
HAL_SPI_Transmit(&g_W25Qxx_Handle,data,Size,10000);
UnselectChip();
}
这个页编程介绍的东西不多,最重要的是下边的随意地址编程,最重要的就是解决写满一页以后需要手动解决地址偏移的问题。
七、写任意大小数据
由于页BUFF的限制,一次性只能写入256个字节,因此这个操作就是持续重复写入256及以下字节。
/*
**** 函数名 W25Q64WriteData
**** 功能 W25Q64 写入数据
**** 参数 address:24位的地址
**** 参数 databuffer:写入的数据
**** 参数 Size:读取数据的大小,单位:字节3
*/
void W25Qxx_WriteData(uint32_t address, uint8_t* data, uint32_t Size)
{
if (address > 0x7FFFFF || data == NULL) // 检查输入参数合法性
{
return;
}
uint8_t offset = address % 256; // 当前地址的页内偏移
uint16_t remainingInPage = 256 - offset; // 当前页剩余空间大小
// 判断数据是否跨页
if (Size <= remainingInPage) // 数据小于或等于当前页剩余空间
{
W25Qxx_PageProgram(address, data, Size); // 写入当前页
return;
}
// 数据跨页
// 先填满当前页
W25Qxx_PageProgram(address, data, remainingInPage);
// 更新地址和数据指针
address += remainingInPage;
data += remainingInPage;
Size -= remainingInPage;
// 写入完整页的数据
while (Size >= 256)
{
W25Qxx_PageProgram(address, data, 256);
address += 256;
data += 256;
Size -= 256;
}
// 写入最后不足一页的数据
if (Size > 0)
{
W25Qxx_PageProgram(address, data, Size);
}
}
在代码中,offset是计算的相对于该写入地址所在的页的首地址的偏移量,remainingInPage 用于计算该页剩余可写入空间大小。举个例子,一个地址是0x100即256,该地址所在的页是[256,511] 一共256个字节,因为前一个页是[0,255]。那么我要在256地址处写数据,那么它的offset = 256%256 = 0,剩余可写入空间为remainingInPage = 256 - 0 = 256。所以我们可以按照这样的算法,来先把当前页填充满,当前页填充满之后就可以成整数倍的填充256个字节,当剩余的数据小于256字节的时候,在单独填充,这样做的好处就是可以在任意地址写入任意大小的数据,且不用担心数据覆盖。
八、读任意大小数据
读数据的命令是0x03。
/*
**** 函数名 W25Q64ReadData
**** 功能 W25Q64 读取数据
**** 参数 address:24位的地址
**** 参数 databuffer:数据接收缓冲区
**** 参数 Size:读取数据的大小,单位:字节
*/
void W25Qxx_ReadData(uint32_t address,uint8_t* databuffer,uint32_t Size)
{
uint8_t cmd[4];
cmd[0] = ReadData;
cmd[1] = (address >> 16) & 0xFF;
cmd[2] = (address >> 8) & 0xFF;
cmd[3] = address & 0xFF;
W25Qxx_CheckBusy();
SelectChip();
HAL_SPI_Transmit(&g_W25Qxx_Handle,cmd,4,1000);
HAL_SPI_Receive(&g_W25Qxx_Handle,databuffer,Size,100000);
UnselectChip();
}
这个可讲解的地方不多,单纯发送读命令后再接收数据。
九、测试
最后测试一下代码效果:
void W25QxxTest()
{
uint16_t head,tail;
for(uint16_t i = 0;i < 8192;i++)
{
data[i] = '1';
}
data[8191] = '\0';
printf("现在进行硬件SPI实验!\n\n");
uint32_t id = W25Qxx_GetID();
printf("芯片ID为:%X\n\n",id);
if(id == W25Q64_ID)
{
printf("正在擦除...\n");
W25Qxx_BlockErase(0x000000);
printf("擦除成功!\n\n");
printf("正在写入...写入数据量为8kb\n");
head = HAL_GetTick();
W25Qxx_WriteData(0x00000A,data,8192);
tail = HAL_GetTick();
printf("写入成功!,写入花费的时间为:%d ms\n\n",tail - head);
printf("正在读取数据,读取数据量为8kb\n");
head = HAL_GetTick();
W25Qxx_ReadData(0x00000A,test,8192);
tail = HAL_GetTick();
if(memcmp(test,data,8192) == 0)
{
printf("读取成功,读取花费的时间为:%d ms\n\n",tail - head);
}
else
{
printf("读取失败\n");
}
printf("读取到的数据是: %s\n",test);
}
}
END............