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

cpp 复制代码
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);
}

引脚宏为:

cpp 复制代码
#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,即:极性为低,相位是在第一个上升沿。

体现在上边的代码为:

cpp 复制代码
	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。

cpp 复制代码
/*
**** 函数名 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并返回。

cpp 复制代码
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

返回之后对第一位进行检查:

cpp 复制代码
/*
**** 函数名 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

cpp 复制代码
/*
**** 函数名 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位:

cpp 复制代码
/*
**** 函数名 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.全片擦除

全片擦除不需要输入地址,但是全片擦除等待的时间很长。

cpp 复制代码
/*
**** 函数名 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();
}

这里可以测试一下全片擦除的时间:

cpp 复制代码
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字节。

cpp 复制代码
/*
**** 函数名 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及以下字节。

cpp 复制代码
/*
**** 函数名 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。

cpp 复制代码
/*
**** 函数名 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();

}

这个可讲解的地方不多,单纯发送读命令后再接收数据。

九、测试

最后测试一下代码效果:

cpp 复制代码
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............

相关推荐
-Springer-5 小时前
STM32 学习 —— 个人学习笔记5(EXTI 外部中断 & 对射式红外传感器及旋转编码器计数)
笔记·stm32·学习
LS_learner5 小时前
树莓派(ARM64 架构)Ubuntu 24.04 (Noble) 系统 `apt update` 报错解决方案
嵌入式硬件
来自晴朗的明天6 小时前
16、电压跟随器(缓冲器)电路
单片机·嵌入式硬件·硬件工程
钰珠AIOT6 小时前
在同一块电路板上同时存在 0805 0603 不同的封装有什么利弊?
嵌入式硬件
代码游侠6 小时前
复习——Linux设备驱动开发笔记
linux·arm开发·驱动开发·笔记·嵌入式硬件·架构
代码游侠17 小时前
学习笔记——设备树基础
linux·运维·开发语言·单片机·算法
xuxg200519 小时前
4G 模组 AT 命令解析框架课程正式发布
stm32·嵌入式·at命令解析框架
CODECOLLECT20 小时前
京元 I62D Windows PDA 技术拆解:Windows 10 IoT 兼容 + 硬解码模块,如何降低工业软件迁移成本?
stm32·单片机·嵌入式硬件
BackCatK Chen21 小时前
STM32+FreeRTOS:嵌入式开发的黄金搭档,未来十年就靠它了!
stm32·单片机·嵌入式硬件·freertos·低功耗·rtdbs·工业控制