【STM32】W25Q64 && SPI(串行外设接口)

一、SPI通信

0.IIC与SPI的优缺点

https://blog.csdn.net/weixin_44575952/article/details/124182011

1.SPI介绍

同步(有时钟线),高速,全双工(数据发送和数据接收各占一条线)

1)SCK:时钟线-->SCLK,CLK,CK--->等价于IIC的SCK

2)MOSI(主机输出从机接收),MISO(主机接收从机输出):DO(Data Output),DI(Data Input)--->等价于IIC的SDA

3)SS(片选):NSS(Not Slave Select)-->低电平有效,CS(Chip Select)-->专门进行主机和该指定从机的通信线路(可能不只一条)

4)SPI只接受:一主多从

5)SPI没有应答数据

DO,DI的区别

先确定芯片的身份(主机/从机)

2.硬件电路

1)SCK是主机控制,SCK是主机输出,SCK是从机接收

2)MOSI(主机输出从机接收)

3)MISO(主机接收从机输出)

4)SS:从机选择线(低电平有效)

3.移位示意图

SPI的数据收发:基于字节交换

如果单纯想要接收或者发送------则将接收或者发送的数据自动屏蔽掉即可

只发送,只接收,既发送既接收

4.SPI时序基本单元

SPI发起指令操作的时候传输的数据单元是=指令码+寄存器地址+操作数

1.起始条件

SS低电平有效,通信时间段内一直保持低电平

起始条件:SS从高电平切换到低电平

2.终止条件

终止条件:SS从低电平切换到高电平

3.交换一个字节(模式1)

模式1:第一个边沿放数据,也可以描述成高电平放数据,第二个边沿采集数据,也可以描述成低电平采集数据(采集数据时数据不能更改)

下降沿采样 (将数据读入寄存器中)

4.交换一个字节(模式3)

与模式1的区别:SCK极性取反(CPOL=1)

5.交换一个字节(模式0)

相比于模式1,数据输出快了半个时钟

上升沿采样(将数据读入寄存器中)

6.交换一个字节(模式2)

与模式0的区别:SCK极性取反(CPOL=1)

7.注意点:

1)CPOL:用于设置极性(1表示高电平有效,0表示低电平有效)

2)CPHA:不是用于决定上升沿读取还是下降沿读取,而是决定第几个周期进行采样。

3)一般如果我们想要接收数据&读取数据,则我们可以随便写入&读出一个值即可,其他不用理会。(我们一般发送0xff或者0x00)

5.SPI时序

1.发送指令

使用模式0(在时序开始前存放数据,在上升沿读取数据)

发送0x06(芯片公司自己定义)--->W25Q64是写使能

接收到0xff不需要看(因为我们目的是主机发送给从机,所以从机传输的数据是什么无所谓)

2.指定地址写

1)向SS指定的设备,发送写指令(0x02),

2)随后在指定地址(Address[23:0])下,写入指定数据(Data)

由此图可知要在地址为:0x123456下写入0x55这个数据

3.指定地址读

1)向SS指定的设备,发送读指令(0x03),

2)随后在指定地址(Address[23:0])下,读取从机数据(Data)

二、单片机中用到的存储器

1.物理层存储器

1)磁存储原理:磁带,软盘,机械硬盘(磁盘)

2)光刻存储:DVD

3)半导体存储:EEPROM,NandFlash,NorFlash

2.Nand和Nor的差异

(1)Nand容量大,价格低,需要按块访问(不能按字节访问),需要专用时序接口访问 (不能直接接到地址总线上)

(2)Nor容量小,价格高,按块擦和写、按字节读需要专用时序接口访问

不同点

相同点

3.单片机系统常用存储解决方案

(1)单片机自身代码:存储在内部Flash中,本质是NorFlash

(2)存少量掉电不丢失数据,用EEPROM(一般都是比较小)--》IIC通信(速度较慢),典型24C02

(2)存中容量掉电不丢失数据,用SPINorFlash(使用SPI是为了减少引脚)****--》SPI通信(速度比IIC快),一般64k-32MB范围

(3)存大容量掉电不丢失数据,用SPINandFlash,一般32MB-1GB范围

(4)要便于插拔和扩展 ,用TF/SD卡,U盘等,一般容量在GB级别。

(5)现在还有新型的SDNand,就是芯片封装的SD卡,容量在nMB-1GB级别。

(6)更大容量板载存储,用eMMC芯片,一般容量4GB-256GB级别

(7)STM32内部Flash可以开放给程序用,存储少量掉电不丢失数据。

4、存储器总结

(1)多种可用,根据产品特点和需求选择,重点考虑:性价比、容量、寿命、速度、可靠性 等因素,大多数行业都有选型惯例。

(2)程序员不必过多关心内部存储颗粒特性,更多关心编程接口即可

三、W25Q64

1.W25Q64简介

开发板中的 FLASH 芯片型号:W25Q64。W25Q 系列为台湾华邦公司推出的是一种使用 SPI 通讯协议的 NOR FLASH 存储器。芯片型号后两位表示芯片容量,例如 W25Q64 的 64 就是指 64Mbit 也就是 8M 的容量。它的 CS/CLK/DIO/DO 引脚分别连接到了 STM32 对应的 SPI 引脚 NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚虽然是其片上 SPI 外设的硬件引脚,但实际上后面的程序只是把它当成一个普通的 GPIO,使用软件的方式控制 NSS 信号,所以在 SPI 的硬件设计中,NSS 可以随便选择普通的 GPIO,不必纠结于选择硬件 NSS 信号。

FLASH 芯片中还有 WP 和 HOLD 引脚。WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。

1)AT24C存储容量是KB级别的,W25Q64是MB级别

2)存储容量:24位地址

2.硬件电路

3、W25Q64框图

1)W25Q64使用的存储空间是8MB(128*64=8,192bit--->8,192/1024=8MByte****)(实际上可以使用16MB)-->所以地址从:0x00 00 00到0x7f ff ff

2)存储空间的划分:先划分为若干块,在划分为若干扇区,最后划分为若干页

1.分为Block

将8MB/128Block分为64KB(每一个大小为64KB,0-127)

2.分为Sector

将64KB/16分为4KB

3.分为page

将4K/25bit分为16bit

4.其他部分

SPI控制器,状态寄存器,数据缓存区

5.Flash操作的注意事项

1)如果我们没有对Flash进行擦除,则原来是(0xAA:1010 1010)如果想要修改为(0x55:0101 0101)--->实际上无法修改【因为数据位只能由1-->0,无法从0-->1

2)如果不进行擦除,则【读出数据=原始数据&写入数据】

3)因为要擦除(将全部数据位置为1),所以我们如果读写Flash输出为0xff,则表示该位置被擦除后未被重写过

4)擦除的最小单位:扇区(4096字节)为单位

5)一个写入时序,最多只能写一页的数据(不能跨页),页就是256字节【因为页缓冲区只有256字节】,超出部分会覆盖前面的位置部分

6)写入操作后,芯片会处于忙状态,因为要将缓冲区中的数据写入Flash中【所以我们在执行写操作的代码后,要检测芯片是否处于忙状态】

7)在要进行读操作之前也要先判断芯片是否处于忙状态

8)写入不能跨页,但是读取可以跨页

9)SPIFlash读写的最小单位是1个字节,而且地址不必对齐

四、SPIFlash(W25Q64)数据手册解读

https://www.aiema.cn/part/datasheet/w25q64dwzpig-fn195394276

1、主要SPIFlash厂家

(1)SPIFlash本质:SPI接口芯片+内部存储颗粒(Nand,Nor)

(2)台湾:Winbond华邦(W开头)、MXIC旺宏(M开头)

(3)国内:GD兆易创新(GD开头)

2.数据手册查看

1.标准SPI指令

2.状态寄存器

1)写入数据后,不需要我们手动将写失能【会自动失能】

2)一个写使能,只能保证后续一条写指令可以操作

BUSY 是状态寄存器 (S0) 中的只读位,当器件正在执行命令时,该位设置为 1 状态

页编程、四页编程、扇区擦除、块擦除、芯片擦除、写入状态寄存器或

擦除/编程安全寄存器指令。 在此期间设备将忽略进一步的指令

除了读取状态寄存器和擦除/编程暂停指令(参见 tW、tPP、tSE、tBE 和

交流特性中的 tCE)。 当编程、擦除或写入状态/安全寄存器指令有

完成后,BUSY 位将被清除为 0 状态,指示设备已准备好接受进一步的指令。

写使能锁存器 (WEL) 是状态寄存器 (S1) 中的一个只读位,在执行

写使能指令。 当器件写禁止时,WEL 状态位清零。 一个写

上电时或执行以下任何指令后会出现禁用状态:写入禁用、页面

编程、四页编程、扇区擦除、块擦除、芯片擦除、写状态寄存器、擦除

安全寄存器和程序安全寄存器。

3.指令表

五、软件SPI读写

1.硬件接线

2.SPI代码编写

cpp 复制代码
#include "stm32f10x.h"                  // Device header
 
void MySPI_W_CS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);
}
void MySPI_W_CLK(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)BitValue);
}
void MySPI_W_MOSI(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)BitValue);
}
uint8_t MySPI_R_MISO(void)
{
	return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}
 
void MySPI_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	GPIO_InitTypeDef GPIO_Init_Structure;
	//要求配置为推挽输出,浮空或上拉输入
	GPIO_Init_Structure.GPIO_Mode=GPIO_Mode_IPU;
	GPIO_Init_Structure.GPIO_Pin=GPIO_Pin_6;
	GPIO_Init_Structure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_Init_Structure);
	
	GPIO_Init_Structure.GPIO_Mode=GPIO_Mode_Out_PP;
	GPIO_Init_Structure.GPIO_Pin=GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_7;
	GPIO_Init_Structure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_Init_Structure);
 
	MySPI_W_CS(1);
	MySPI_W_CLK(0);
}
//三个基本时序:起始,交换数据,终止
void MySPI_Start(void)
{
	MySPI_W_CS(0);
}
void MySPI_Stop(void)
{
	MySPI_W_CS(1);
}
//通过掩码,依次挑出每一位操作,优点是保留了原数据的值
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t i,ByteReceive = 0x00;
	for(i = 0;i < 8;i ++)
	{
		MySPI_W_MOSI(ByteSend &(0x80>>i));
		MySPI_W_CLK(1);
		if(MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}
		MySPI_W_CLK(0);
	}
	return ByteReceive;
}
 
//还可以按照移位示意图中的方式交换数据,优点是效率高,但不能保存原数据值
//uint8_t MySPI_SwapByte(uint8_t ByteSend)
//{
//	 uint8_t i;
//   for(i=0;i<8;i ++)
//    {
//    	MySPI_W_MOSI(ByteSend &0x80);
//		ByteSend <<=1;			//最高位移出,最后补0
//    	MySPI_W_CLK(1);
//    	if(MySPI_R_MISO() == 1) {ByteSend |= 0x01;}	 //输入的数据放在最低位
//    	MySPI_W_CLK(0);
//    }
//    return ByteSend;
//}

0.电平翻转函数封装

因为W25Q64的频率很快,所以中间不需要添加延时函数

1.初始化

2.起始信号

3.终止信号

4.交换(发送/接收)一个字节(模式0)

主机发送数据给从机,从机发送数据给主机

1)SS设置为下降沿

2)将数据读入到引脚

3)SCK设置为上升沿

4)将数据从引脚读出

5)将SCK设置为下降沿

5.交换(发送/接收)一个字节(模式1)

3.W25Q64代码

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "MySPI.H"
#include "W25Q64_INS.H"
 
void W25Q64_Init(void)
{
	MySPI_Init();
}
//用指针的方式来获取多个函数的值!!!
void W25Q64_ReadID(uint8_t *MID,uint16_t *DID)
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_JEDEC_ID);
	*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
	*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
	*DID <<= 8;
	*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);
	MySPI_Stop();
}
void W25Q64_WiteEnable(void)
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_WRITE_ENABLE);
	MySPI_Stop();
}
/**
  * @brief 直到BUSY清零后结束
  * @param  
  * @retval 
  */
void W25Q64_WaitBusy(void)
{
	uint32_t Timeout=100000;  //为了防止卡死
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
	
	//直到busy不为1
	while((MySPI_SwapByte(0xFF) & 0X01) == 0X01)
	{
		Timeout--;
		if(Timeout == 0)
		{
			break;
		}
	}
	MySPI_Stop();
}
 
//页编程写入,注意页编程写入一页的范围
void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t count)	
{
	//没有24位,通过数组可以传多个字节。所以用32位,写入数据的数量范围0-256,所以用uint16不用uint8
	W25Q64_WiteEnable();
	
	uint16_t i;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);	//高两位会丢弃
	MySPI_SwapByte(Address);
	for(i=0;i < count ;i++)
	{
		MySPI_SwapByte(DataArray[i]);
	}
	MySPI_Stop();
	
	W25Q64_WaitBusy();
}
 
void W25Q64_SectorErase(uint32_t Address)	
{
	//写使能仅对之后跟随的一条时序有效,结束之后会失能,所以每个函数加入这个就不用再写失能
	W25Q64_WiteEnable();
	
	MySPI_Start();
	MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);	
	MySPI_SwapByte(Address);
	MySPI_Stop();
 
	W25Q64_WaitBusy();
}
 
void W25Q64_ReadData(uint32_t Address,uint8_t *DataArray,uint32_t count)	//改为32位
{	
	uint32_t i;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_DATA);
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);	
	MySPI_SwapByte(Address);
	for(i=0;i < count ;i++)
	{
		//调用交换读取之后,内部指针自动自增
		DataArray[i]=MySPI_SwapByte(W25Q64_DUMMY_BYTE);
	}
	MySPI_Stop();
}

0.初始化

1.获取ID

实际上是【抛砖引玉】

2.宏定义

3.写使能

4.读状态寄存器1

判断当前芯片是否处于忙状态

5.Page Program

这里我们传入数据为uint16_t,不能写uint8_t,因为int8最大是255,而我们page最大256,所以如果使用int8空间不足够
如果发送到设备的字节超过256个,寻址将封装到页的开头,并覆盖以前发送的数据。

DataArray:写入的数值

写入操作前,必须先进行写使能

6.Sector Erase (4KB)

写入操作前,必须先进行写使能

7.Read Data

DataArray:返回读取到的数值

4.测试代码

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "LED.H"
#include "Key.h"
#include "OLED.H"
#include "W25Q64.H"
 
uint8_t MID;
uint16_t DID;
 
uint8_t ArrayWrite[]={0x55,0x66,0x77,0x88};
uint8_t ArrayRead[4];
int main(void)
{
	OLED_Init();
	W25Q64_Init();
	OLED_ShowString(1,1,"MID:   DID:");
	OLED_ShowString(2,1,"W:");
	OLED_ShowString(3,1,"R:");
 
	W25Q64_ReadID(&MID,&DID);
	OLED_ShowHexNum(1,5,MID,2);
	OLED_ShowHexNum(1,12,DID,4);
	
	//只擦除不写入,可以验证flash擦除之后变为ff
	//不擦除直接改写,可以测试不能由0到1,只能1到0
 
    //写之前先擦除。xxx000-xxffff
	W25Q64_SectorErase(0x000000); 	
	
	//页地址范围xxxx00-xxxxff
	W25Q64_PageProgram(0X000000,ArrayWrite,4); 
	
	W25Q64_ReadData(0X000000,ArrayRead,4);
	
	OLED_ShowHexNum(2,3,ArrayWrite[0],2);
	OLED_ShowHexNum(2,6,ArrayWrite[1],2);
	OLED_ShowHexNum(2,9,ArrayWrite[2],2);
	OLED_ShowHexNum(2,12,ArrayWrite[3],2);
	
	OLED_ShowHexNum(3,3,ArrayRead[0],2);
	OLED_ShowHexNum(3,6,ArrayRead[1],2);
	OLED_ShowHexNum(3,9,ArrayRead[2],2);
	OLED_ShowHexNum(3,12,ArrayRead[3],2);
 
	while(1)
	{
 
	}
}

5.事前等待 VS 事后等待

1.事前等待

表示我们在编写一个函数之前,先判断此时芯片是否处于忙状态。但是需要每一个函数前都进行判断(读寄存器&&写寄存器都要进行判断)

2.事后等待

表示我们在写完一个执行写操作的函数后,在程序退出之前查看芯片是否处于忙状态。此时如果处于忙状态则我们可以停下来等待,如果不处于忙状态则直接退出。不用每一个函数中都调用。

3.小总结

事前等待:1、写入前先等待,等不忙了再写入 2、效率高。 3、在写入和读取操作之前都要等待。

事后等待:1、写入后立刻等待,不忙了退出。 2、这样最保险,函数结束后,函数之外的地方芯片肯定不忙。 3、只需在写入后等待。

六、W25Q64的HAL源代码解析

1、 CubeMX例程展示

【精选】STM32CubeMX学习笔记(10)------SPI接口使用(读写SPI Flash W25Q64)_stm32cubemx配置spi-CSDN博客

1.时钟设置

2.SPI设置

1)在 Connectivity 中选择 SPI1 设置,并选择 Full-Duplex Master 全双工主模式不开启 NSS 即不使用硬件片选信号

SPI 为默认设置不作修改。只需注意一下,Prescaler 分频系数最低为 4,波特率 (Baud Rate) 为 18.0 MBits/s。这里被限制了,SPI1 最高通信速率可达 36Mbtis/s。

  • Clock Polarity(CPOL):SPI 通讯设备处于空闲状态时,SCK 信号线的电平信号(即 SPI 通讯开始前、 NSS 线为高电平时 SCK 的状态)。CPOL=0 时, SCK 在空闲状态时为低电平,CPOL=1 时,则相反。
  • Clock Phase(CPHA):指数据的采样的时刻,当 CPHA=0 时,MOSI 或 MISO 数据线上的信号将会在 SCK 时钟线的"奇数边沿"被采样。当 CPHA=1 时,数据线在 SCK 的"偶数边沿"采样。

3.设置SS(CS:片选)

原理图中虽然将 CS 片选接到了硬件 SPI1 的 NSS 引脚,因为硬件 NSS 使用比较麻烦,所以后面直接把 PA4 配置为普通 GPIO,手动控制片选信号。

在右边图中找到 SPI1 NSS 对应引脚,选择 GPIO_Output。

修改输出高电平 High【因为SS是低电平有效,所以初始化为高电平】

2.MDK例程分析

3.HAL库中SPI库函数分析

4.SPIFlash驱动分析

https://www.cnblogs.com/wenhao-Web/p/13827313.html

STM32F405+CubeMX HAL库读写W25Q64 SPI Flash例程_hal库spi例程-CSDN博客

W25Q64写可跨页数据

1)SPIFlash允许跨页读,不允许跨页写

2)SPIFlash写的时候,单次写是不能跨页的

cpp 复制代码
#include "main.h"
#include "stm32f4xx_hal.h"

SPI_HandleTypeDef hspi1;

#define CS_PIN GPIO_PIN_4
#define CS_PORT GPIOA

#define PAGE_SIZE 256  // 假设一页的大小为256字节

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_SPI1_Init(void);

void W25Q64_WriteData(uint8_t* dataBuffer, uint32_t address, uint32_t dataSize);

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_SPI1_Init();

  // 允许片选引脚
  HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET);

  // 写入数据的缓冲区
  uint8_t dataBuffer[512];  // 假设写入512字节的数据

  // 从地址0开始写入数据
  W25Q64_WriteData(dataBuffer, 0, sizeof(dataBuffer));

  // 关闭片选引脚
  HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET);

  while (1)
  {
    // Your application code here
  }
}

// 写入数据的函数
void W25Q64_WriteData(uint8_t* dataBuffer, uint32_t address, uint32_t dataSize)
{
  uint32_t currentPage, remainingBytes;

  // 计算当前页和剩余字节数
  currentPage = address / PAGE_SIZE;
  remainingBytes = dataSize;

  // 写入整页数据
  while (remainingBytes >= PAGE_SIZE) {
    // 发送写使能命令
    uint8_t writeEnableCommand = 0x06;
    HAL_SPI_Transmit(&hspi1, &writeEnableCommand, 1, HAL_MAX_DELAY);

    // 发送写命令和地址
    uint8_t writeCommand[] = {0x02, (uint8_t)((address >> 16) & 0xFF), (uint8_t)((address >> 8) & 0xFF), (uint8_t)(address & 0xFF)};
    HAL_SPI_Transmit(&hspi1, writeCommand, sizeof(writeCommand), HAL_MAX_DELAY);

    // 发送数据
    HAL_SPI_Transmit(&hspi1, dataBuffer, PAGE_SIZE, HAL_MAX_DELAY);

    // 等待写入完成
    while (W25Q64_IsWriteInProgress()) {
      HAL_Delay(1);
    }

    // 更新地址和剩余字节数
    address += PAGE_SIZE;
    dataBuffer += PAGE_SIZE;
    remainingBytes -= PAGE_SIZE;
  }

  // 写入剩余字节
  if (remainingBytes > 0) {
    // 发送写使能命令
    uint8_t writeEnableCommand = 0x06;
    HAL_SPI_Transmit(&hspi1, &writeEnableCommand, 1, HAL_MAX_DELAY);

    // 发送写命令和地址
    uint8_t writeCommand[] = {0x02, (uint8_t)((address >> 16) & 0xFF), (uint8_t)((address >> 8) & 0xFF), (uint8_t)(address & 0xFF)};
    HAL_SPI_Transmit(&hspi1, writeCommand, sizeof(writeCommand), HAL_MAX_DELAY);

    // 发送剩余数据
    HAL_SPI_Transmit(&hspi1, dataBuffer, remainingBytes, HAL_MAX_DELAY);

    // 等待写入完成
    while (W25Q64_IsWriteInProgress()) {
      HAL_Delay(1);
    }
  }
}

// 检查写入是否仍在进行中
int W25Q64_IsWriteInProgress(void)
{
  uint8_t statusReg;

  // 发送读取状态寄存器命令
  uint8_t readStatusCommand = 0x05;
  HAL_SPI_Transmit(&hspi1, &readStatusCommand, 1, HAL_MAX_DELAY);

  // 读取状态寄存器
  HAL_SPI_Receive(&hspi1, &statusReg, 1, HAL_MAX_DELAY);

  // 检查忙位 (Bit 0)
  return (statusReg & 0x01);
}

void HAL_SPI_MspInit(SPI_HandleTypeDef* hspi)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  
  if (hspi->Instance == SPI1)
  {
    __HAL_RCC_SPI1_CLK_ENABLE();

    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**SPI1 GPIO Configuration    
    PA5     ------> SPI1_SCK
    PA6     ------> SPI1_MISO
    PA7     ------> SPI1_MOSI 
    */
    GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  }
}

首先计算当前页和剩余字节数,然后循环写入整页数据。在每个循环中,它发送写使能命令,写命令和地址,然后发送数据。在每次写入后,它等待写入完成,然后更新地址和剩余字节数。最后,如果有剩余字节,它再次发送写使能命令,写命令和地址,并发送剩余的数据。函数 W25Q64_IsWriteInProgress 用于检查写入是否仍在进行中。

W25Q64读可跨页数据

cpp 复制代码
#include "main.h"
#include "stm32f4xx_hal.h"

SPI_HandleTypeDef hspi1;

#define CS_PIN GPIO_PIN_4
#define CS_PORT GPIOA

#define PAGE_SIZE 256  // 假设一页的大小为256字节

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_SPI1_Init(void);

void W25Q64_ReadData(uint8_t* dataBuffer, uint32_t address, uint32_t dataSize);

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_SPI1_Init();

  // 允许片选引脚
  HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET);

  // 读取数据的缓冲区
  uint8_t dataBuffer[512];  // 假设读取512字节的数据

  // 从地址0开始读取数据
  W25Q64_ReadData(dataBuffer, 0, sizeof(dataBuffer));

  // 关闭片选引脚
  HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET);

  while (1)
  {
    // Your application code here
  }
}

// 读取数据的函数
void W25Q64_ReadData(uint8_t* dataBuffer, uint32_t address, uint32_t dataSize)
{
  uint32_t currentPage, remainingBytes;

  // 计算当前页和剩余字节数
  currentPage = address / PAGE_SIZE;
  remainingBytes = dataSize;

  // 读取整页数据
  while (remainingBytes >= PAGE_SIZE) {
    // 发送读命令和地址
    uint8_t readCommand[] = {0x03, (uint8_t)((address >> 16) & 0xFF), (uint8_t)((address >> 8) & 0xFF), (uint8_t)(address & 0xFF)};
    HAL_SPI_Transmit(&hspi1, readCommand, sizeof(readCommand), HAL_MAX_DELAY);

    // 接收数据
    HAL_SPI_Receive(&hspi1, dataBuffer, PAGE_SIZE, HAL_MAX_DELAY);

    // 更新地址和剩余字节数
    address += PAGE_SIZE;
    dataBuffer += PAGE_SIZE;
    remainingBytes -= PAGE_SIZE;
  }

  // 读取剩余字节
  if (remainingBytes > 0) {
    // 发送读命令和地址
    uint8_t readCommand[] = {0x03, (uint8_t)((address >> 16) & 0xFF), (uint8_t)((address >> 8) & 0xFF), (uint8_t)(address & 0xFF)};
    HAL_SPI_Transmit(&hspi1, readCommand, sizeof(readCommand), HAL_MAX_DELAY);

    // 接收剩余数据
    HAL_SPI_Receive(&hspi1, dataBuffer, remainingBytes, HAL_MAX_DELAY);
  }
}

void HAL_SPI_MspInit(SPI_HandleTypeDef* hspi)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  
  if (hspi->Instance == SPI1)
  {
    __HAL_RCC_SPI1_CLK_ENABLE();

    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**SPI1 GPIO Configuration    
    PA5     ------> SPI1_SCK
    PA6     ------> SPI1_MISO
    PA7     ------> SPI1_MOSI 
    */
    GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  }
}

W25Q64_ReadData 函数首先计算当前页和剩余字节数,然后循环读取整页数据。在每个循环中,它发送读命令和地址,然后接收数据。在每次读取后,它更新地址和剩余字节数。最后,如果有剩余字节,它再次发送读命令和地址,并接收剩余的数据。

七、STM32内部Flash

1、内部flash信息

1.查数据手册的flash章节

STM32F10xxx闪存编程参考手册.pdf · 林何/STM32F103C8 - 码云 - 开源中国 (gitee.com)

正常原来程序的代码从前往后写。

所以正常额外添加的代码从后往前写,防止把原来的程序覆盖掉。

2.查MDK工程编译后的map文件

从Flash往后数9324后开始就跨页写入数据

3.操作函数查HAL库

未完成

相关推荐
杰克逊的日记9 天前
MCU编程
单片机·嵌入式硬件
Python小老六9 天前
单片机测ntc热敏电阻的几种方法(软件)
数据库·单片机·嵌入式硬件
懒惰的bit9 天前
STM32F103C8T6 学习笔记摘要(四)
笔记·stm32·学习
HX科技9 天前
STM32给FPGA的外挂FLASH进行升级
stm32·嵌入式硬件·fpga开发·flash·fpga升级
Suagrhaha9 天前
驱动入门的进一步深入
linux·嵌入式硬件·驱动
国科安芯9 天前
基于ASP4644多通道降压技术在电力监测系统中集成应用与发展前景
嵌入式硬件·硬件架构·硬件工程
Li Zi9 天前
STM32 ADC(DMA)双缓冲采集+串口USART(DMA)直接传输12位原始数据到上位机显示并保存WAV格式音频文件 收藏住绝对实用!!!
经验分享·stm32·单片机·嵌入式硬件
进击的程序汪9 天前
触摸屏(典型 I2C + Input 子系统设备)从设备树解析到触摸事件上报
linux·网络·嵌入式硬件
damo王9 天前
Zephyr 系统深入解析:SoC 支持包结构与中断调度器调优实践
单片机·嵌入式硬件·zephyr
逼子格9 天前
硬件工程师笔试面试高频考点汇总——(2025版)
单片机·嵌入式硬件·面试·硬件工程·硬件工程师·硬件工程师真题·硬件工程师面试