【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代码编写

#include "stm32f10x.h"                  // Device headervoid 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代码

#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不为1while((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不用uint8W25Q64_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.测试代码

#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-xxffffW25Q64_SectorErase(0x000000); 	//页地址范围xxxx00-xxxxffW25Q64_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写的时候,单次写是不能跨页的

#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_SCKPA6     ------> SPI1_MISOPA7     ------> 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读可跨页数据

#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_SCKPA6     ------> SPI1_MISOPA7     ------> 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库

未完成

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/155798.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring Boot要如何学习?【云驻共创】

Spring Boot 是由 Pivotal 团队提供的全新框架&#xff0c;其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置&#xff0c;从而使开发人员不再需要定义样板化的配置。我这里会分享一些学习Spring Boot的方法和干货&#xff0c;包括…

计算机毕业设计选题推荐-内蒙古旅游微信小程序/安卓APP-项目实战

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

Spring Cloud Alibaba Sentinel 简单使用

Sentinel Sentinel 主要功能Sentinel 作用常见的流量控制算法计数器算法漏桶算法 令牌桶算法Sentinel 流量控制Sentinel 熔断Sentinel 基本使用添加依赖定义资源定义限流规则定义熔断规则如何判断熔断还是限流自定义 Sentinel 异常局部自定义异常全局自定义异常系统自定义异常…

基于Apache部署虚拟主机网站

文章目录 Apache释义Apache配置关闭防火墙和selinux 更改默认页内容更改默认页存放位置个人用户主页功能基于口令登录网站虚拟主机功能基于ip地址相同ip不同域名相同ip不同端口 学习本章完成目标 1.httpd服务程序的基本部署。 2.个人用户主页功能和口令加密认证方式的实现。 3.…

树与二叉树堆:树

目录 树&#xff1a; 树的概念&#xff1a; 树的相关概念&#xff1a; 1、结点的度&#xff1a; 2、叶节点&#xff1a;度为0的节点 3、非终端节点或分支节点&#xff1a; 4、父节点和子节点&#xff1a; 5、兄弟节点&#xff1a; 6、树的度&#xff1a; 7、树的层次或…

JS--localStorage设置过期时间的方案(有示例)

原文网址&#xff1a;JS--localStorage设置过期时间的方案(有示例)_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍如何使用localStorage设置数据的过期时间。 问题描述 localStorage是不支持设置过期时间的&#xff0c;cookie虽然支持设置过期时间但它存的数据量很小。所…

Deep Learning for Monocular Depth Estimation: A Review.基于深度学习的深度估计

传统的深度估计方法通常是使用双目相机&#xff0c;计算两个2D图像的视差&#xff0c;然后通过立体匹配和三角剖分得到深度图。然而&#xff0c;双目深度估计方法至少需要两个固定的摄像机&#xff0c;当场景的纹理较少或者没有纹理的时候&#xff0c;很难从图像中捕捉足够的特…

网工内推 | 字节原厂,正式编,网络工程师,最高30K*15薪

01 字节跳动 招聘岗位&#xff1a;网络虚拟化高级研发工程师 职责描述&#xff1a; 1、负责字节跳动虚拟网络产品的研发&#xff0c;包括但不局限于网络VPC、NAT、LB负载均衡等&#xff1b; 2、负责字节跳动网络基础平台的研发&#xff0c;包括但不局限于网络控制面系统、容器…

如何通过算法模型进行数据预测

当今数据时代背景下更加重视数据的价值&#xff0c;企业信息化建设会越来越完善&#xff0c;越来越体系化&#xff0c;以数据说话&#xff0c;通过数据为企业提升渠道转化率、改善企业产品、实现精准运营&#xff0c;为企业打造自助模式的数据分析成果&#xff0c;以数据驱动决…

Sentinel 系统规则 (SystemRule)

Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件&#xff0c;主要以流量为切入点&#xff0c;从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。 SpringbootDubboNacos 集成 Sentinel&…

Nacos介绍与使用

Nacos介绍与使用 文章目录 Nacos介绍与使用一. 什么是Nacos1 Nacos功能1.1 配置中心1.2 注册中心 2.为什么要使用Nacos 二.Nacos 部署安装1. Nacos 部署方式2. Nacos 安装3. 配置数据源4. 开启控制台授权登录&#xff08;可选&#xff09; 三. Nacos配置中心的使用1. 创建配置信…

2023年【T电梯修理】考试题及T电梯修理考试报名

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 T电梯修理考试题是安全生产模拟考试一点通总题库中生成的一套T电梯修理考试报名&#xff0c;安全生产模拟考试一点通上T电梯修理作业手机同步练习。2023年【T电梯修理】考试题及T电梯修理考试报名 1、【多选题】GB/T1…

红队攻防之Goby反杀

若结局非我所愿&#xff0c;那就在尘埃落定前奋力一搏。 本文首发于先知社区&#xff0c;原创作者即是本人 一、弹xss 为了方便&#xff0c;本次直接使用 PhpStudy 进行建站&#xff0c;开启的web服务要为MySQLNginx&#xff0c;这里的 PhpStudy 地址为 http://x.x.x.x&…

Hibernate 一级缓存,二级缓存,查询缓存

概念&#xff1a; 1.什么是缓存呢&#xff1f; 缓存&#xff1a;是计算机领域的概念&#xff0c;它介于应用程序和永久性数据存储源之间。 缓存&#xff1a;一般人的理解是在内存中的一块空间&#xff0c;可以将二级缓存配置到硬盘。用白话来说&#xff0c;就是一个存储数据的…

Web前端—移动Web第三天(移动Web基础、rem、less、综合案例—极速问诊)

版本说明 当前版本号[20231120]。 版本修改说明20231120初版 目录 文章目录 版本说明目录移动 Web 第三天01-移动 Web 基础谷歌模拟器屏幕分辨率视口二倍图适配方案 02-rem简介媒体查询rem 布局flexible.jsrem 移动适配 03-less注释运算嵌套变量导入导出禁止导出 04-综合案例…

GNSS技术在灾害监测与应急响应中的关键作用

全球导航卫星系统&#xff08;GNSS&#xff09;技术在灾害监测与应急响应领域发挥着重要作用&#xff0c;为预防、监测和应对自然灾害提供了关键数据支持。本文将深入探讨GNSS技术在灾害监测与应急响应中的作用&#xff0c;并分析其对提高应对灾害能力的重要性。 一、GNSS在灾害…

InnoDB 的一次更新事务是怎么实现的?

大体流程&#xff1a; 步骤: 1.加载数据到缓存中&#xff08;Buffer Pool&#xff09;&#xff1a; 在进行数据更新时&#xff0c;InnoDB首先会在缓冲池&#xff08;Buffer Pool&#xff09;中查找该记录是否已经在内存中。如果记录不在内存中&#xff0c;会将需要更新的数据…

2021年03月 Scratch(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 小猫在沙漠中旅行好不容易找到了一杯水,初始位置如下图所示,下面哪个程序可以帮助它成功喝到水? A: B: C: D:

基于像素特征的kmeas聚类的图像分割方案

kmeans聚类代码 将像素进行聚类&#xff0c;得到每个像素的聚类标签&#xff0c;默认聚类簇数为3 def seg_kmeans(img,clusters3):img_flatimg.reshape((-1,3))# print(img_flat.shape)img_flatnp.float32(img_flat)criteria(cv.TERM_CRITERIA_MAX_ITERcv.TERM_CRITERIA_EPS,2…

stack和queue简单实现(容器适配器)

容器适配器 stack介绍stack模拟实现queue 介绍queue模拟实现deque stack介绍 stack模拟实现 以前我们实现stack&#xff0c;需要像list,vector一样手动创建成员函数&#xff0c;成员变量。但是stack作为容器适配器&#xff0c;我们有更简单的方法来实现它。 可以利用模板的强大…