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

    1. #include "stm32f10x.h" // Device header
    2. void MySPI_W_CS(uint8_t BitValue)
    3. {
    4. GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);
    5. }
    6. void MySPI_W_CLK(uint8_t BitValue)
    7. {
    8. GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)BitValue);
    9. }
    10. void MySPI_W_MOSI(uint8_t BitValue)
    11. {
    12. GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)BitValue);
    13. }
    14. uint8_t MySPI_R_MISO(void)
    15. {
    16. return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
    17. }
    18. void MySPI_Init(void)
    19. {
    20. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    21. GPIO_InitTypeDef GPIO_Init_Structure;
    22. //要求配置为推挽输出,浮空或上拉输入
    23. GPIO_Init_Structure.GPIO_Mode=GPIO_Mode_IPU;
    24. GPIO_Init_Structure.GPIO_Pin=GPIO_Pin_6;
    25. GPIO_Init_Structure.GPIO_Speed=GPIO_Speed_50MHz;
    26. GPIO_Init(GPIOA,&GPIO_Init_Structure);
    27. GPIO_Init_Structure.GPIO_Mode=GPIO_Mode_Out_PP;
    28. GPIO_Init_Structure.GPIO_Pin=GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_7;
    29. GPIO_Init_Structure.GPIO_Speed=GPIO_Speed_50MHz;
    30. GPIO_Init(GPIOA,&GPIO_Init_Structure);
    31. MySPI_W_CS(1);
    32. MySPI_W_CLK(0);
    33. }
    34. //三个基本时序:起始,交换数据,终止
    35. void MySPI_Start(void)
    36. {
    37. MySPI_W_CS(0);
    38. }
    39. void MySPI_Stop(void)
    40. {
    41. MySPI_W_CS(1);
    42. }
    43. //通过掩码,依次挑出每一位操作,优点是保留了原数据的值
    44. uint8_t MySPI_SwapByte(uint8_t ByteSend)
    45. {
    46. uint8_t i,ByteReceive = 0x00;
    47. for(i = 0;i < 8;i ++)
    48. {
    49. MySPI_W_MOSI(ByteSend &(0x80>>i));
    50. MySPI_W_CLK(1);
    51. if(MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}
    52. MySPI_W_CLK(0);
    53. }
    54. return ByteReceive;
    55. }
    56. //还可以按照移位示意图中的方式交换数据,优点是效率高,但不能保存原数据值
    57. //uint8_t MySPI_SwapByte(uint8_t ByteSend)
    58. //{
    59. // uint8_t i;
    60. // for(i=0;i<8;i ++)
    61. // {
    62. // MySPI_W_MOSI(ByteSend &0x80);
    63. // ByteSend <<=1; //最高位移出,最后补0
    64. // MySPI_W_CLK(1);
    65. // if(MySPI_R_MISO() == 1) {ByteSend |= 0x01;} //输入的数据放在最低位
    66. // MySPI_W_CLK(0);
    67. // }
    68. // return ByteSend;
    69. //}

    0.电平翻转函数封装

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

    1.初始化

    2.起始信号

    3.终止信号

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

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

    1)SS设置为下降沿

    2)将数据读入到引脚

    3)SCK设置为上升沿

    4)将数据从引脚读出

    5)将SCK设置为下降沿

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

    3.W25Q64代码

    1. #include "stm32f10x.h" // Device header
    2. #include "MySPI.H"
    3. #include "W25Q64_INS.H"
    4. void W25Q64_Init(void)
    5. {
    6. MySPI_Init();
    7. }
    8. //用指针的方式来获取多个函数的值!!!
    9. void W25Q64_ReadID(uint8_t *MID,uint16_t *DID)
    10. {
    11. MySPI_Start();
    12. MySPI_SwapByte(W25Q64_JEDEC_ID);
    13. *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
    14. *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
    15. *DID <<= 8;
    16. *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);
    17. MySPI_Stop();
    18. }
    19. void W25Q64_WiteEnable(void)
    20. {
    21. MySPI_Start();
    22. MySPI_SwapByte(W25Q64_WRITE_ENABLE);
    23. MySPI_Stop();
    24. }
    25. /**
    26. * @brief 直到BUSY清零后结束
    27. * @param
    28. * @retval
    29. */
    30. void W25Q64_WaitBusy(void)
    31. {
    32. uint32_t Timeout=100000; //为了防止卡死
    33. MySPI_Start();
    34. MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
    35. //直到busy不为1
    36. while((MySPI_SwapByte(0xFF) & 0X01) == 0X01)
    37. {
    38. Timeout--;
    39. if(Timeout == 0)
    40. {
    41. break;
    42. }
    43. }
    44. MySPI_Stop();
    45. }
    46. //页编程写入,注意页编程写入一页的范围
    47. void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t count)
    48. {
    49. //没有24位,通过数组可以传多个字节。所以用32位,写入数据的数量范围0-256,所以用uint16不用uint8
    50. W25Q64_WiteEnable();
    51. uint16_t i;
    52. MySPI_Start();
    53. MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
    54. MySPI_SwapByte(Address >> 16);
    55. MySPI_SwapByte(Address >> 8); //高两位会丢弃
    56. MySPI_SwapByte(Address);
    57. for(i=0;i < count ;i++)
    58. {
    59. MySPI_SwapByte(DataArray[i]);
    60. }
    61. MySPI_Stop();
    62. W25Q64_WaitBusy();
    63. }
    64. void W25Q64_SectorErase(uint32_t Address)
    65. {
    66. //写使能仅对之后跟随的一条时序有效,结束之后会失能,所以每个函数加入这个就不用再写失能
    67. W25Q64_WiteEnable();
    68. MySPI_Start();
    69. MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
    70. MySPI_SwapByte(Address >> 16);
    71. MySPI_SwapByte(Address >> 8);
    72. MySPI_SwapByte(Address);
    73. MySPI_Stop();
    74. W25Q64_WaitBusy();
    75. }
    76. void W25Q64_ReadData(uint32_t Address,uint8_t *DataArray,uint32_t count) //改为32位
    77. {
    78. uint32_t i;
    79. MySPI_Start();
    80. MySPI_SwapByte(W25Q64_READ_DATA);
    81. MySPI_SwapByte(Address >> 16);
    82. MySPI_SwapByte(Address >> 8);
    83. MySPI_SwapByte(Address);
    84. for(i=0;i < count ;i++)
    85. {
    86. //调用交换读取之后,内部指针自动自增
    87. DataArray[i]=MySPI_SwapByte(W25Q64_DUMMY_BYTE);
    88. }
    89. MySPI_Stop();
    90. }

    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.测试代码

    1. #include "stm32f10x.h" // Device header
    2. #include "Delay.h"
    3. #include "LED.H"
    4. #include "Key.h"
    5. #include "OLED.H"
    6. #include "W25Q64.H"
    7. uint8_t MID;
    8. uint16_t DID;
    9. uint8_t ArrayWrite[]={0x55,0x66,0x77,0x88};
    10. uint8_t ArrayRead[4];
    11. int main(void)
    12. {
    13. OLED_Init();
    14. W25Q64_Init();
    15. OLED_ShowString(1,1,"MID: DID:");
    16. OLED_ShowString(2,1,"W:");
    17. OLED_ShowString(3,1,"R:");
    18. W25Q64_ReadID(&MID,&DID);
    19. OLED_ShowHexNum(1,5,MID,2);
    20. OLED_ShowHexNum(1,12,DID,4);
    21. //只擦除不写入,可以验证flash擦除之后变为ff
    22. //不擦除直接改写,可以测试不能由0到1,只能1到0
    23. //写之前先擦除。xxx000-xxffff
    24. W25Q64_SectorErase(0x000000);
    25. //页地址范围xxxx00-xxxxff
    26. W25Q64_PageProgram(0X000000,ArrayWrite,4);
    27. W25Q64_ReadData(0X000000,ArrayRead,4);
    28. OLED_ShowHexNum(2,3,ArrayWrite[0],2);
    29. OLED_ShowHexNum(2,6,ArrayWrite[1],2);
    30. OLED_ShowHexNum(2,9,ArrayWrite[2],2);
    31. OLED_ShowHexNum(2,12,ArrayWrite[3],2);
    32. OLED_ShowHexNum(3,3,ArrayRead[0],2);
    33. OLED_ShowHexNum(3,6,ArrayRead[1],2);
    34. OLED_ShowHexNum(3,9,ArrayRead[2],2);
    35. OLED_ShowHexNum(3,12,ArrayRead[3],2);
    36. while(1)
    37. {
    38. }
    39. }

    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写的时候,单次写是不能跨页的

    1. #include "main.h"
    2. #include "stm32f4xx_hal.h"
    3. SPI_HandleTypeDef hspi1;
    4. #define CS_PIN GPIO_PIN_4
    5. #define CS_PORT GPIOA
    6. #define PAGE_SIZE 256 // 假设一页的大小为256字节
    7. void SystemClock_Config(void);
    8. static void MX_GPIO_Init(void);
    9. static void MX_SPI1_Init(void);
    10. void W25Q64_WriteData(uint8_t* dataBuffer, uint32_t address, uint32_t dataSize);
    11. int main(void)
    12. {
    13. HAL_Init();
    14. SystemClock_Config();
    15. MX_GPIO_Init();
    16. MX_SPI1_Init();
    17. // 允许片选引脚
    18. HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET);
    19. // 写入数据的缓冲区
    20. uint8_t dataBuffer[512]; // 假设写入512字节的数据
    21. // 从地址0开始写入数据
    22. W25Q64_WriteData(dataBuffer, 0, sizeof(dataBuffer));
    23. // 关闭片选引脚
    24. HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET);
    25. while (1)
    26. {
    27. // Your application code here
    28. }
    29. }
    30. // 写入数据的函数
    31. void W25Q64_WriteData(uint8_t* dataBuffer, uint32_t address, uint32_t dataSize)
    32. {
    33. uint32_t currentPage, remainingBytes;
    34. // 计算当前页和剩余字节数
    35. currentPage = address / PAGE_SIZE;
    36. remainingBytes = dataSize;
    37. // 写入整页数据
    38. while (remainingBytes >= PAGE_SIZE) {
    39. // 发送写使能命令
    40. uint8_t writeEnableCommand = 0x06;
    41. HAL_SPI_Transmit(&hspi1, &writeEnableCommand, 1, HAL_MAX_DELAY);
    42. // 发送写命令和地址
    43. uint8_t writeCommand[] = {0x02, (uint8_t)((address >> 16) & 0xFF), (uint8_t)((address >> 8) & 0xFF), (uint8_t)(address & 0xFF)};
    44. HAL_SPI_Transmit(&hspi1, writeCommand, sizeof(writeCommand), HAL_MAX_DELAY);
    45. // 发送数据
    46. HAL_SPI_Transmit(&hspi1, dataBuffer, PAGE_SIZE, HAL_MAX_DELAY);
    47. // 等待写入完成
    48. while (W25Q64_IsWriteInProgress()) {
    49. HAL_Delay(1);
    50. }
    51. // 更新地址和剩余字节数
    52. address += PAGE_SIZE;
    53. dataBuffer += PAGE_SIZE;
    54. remainingBytes -= PAGE_SIZE;
    55. }
    56. // 写入剩余字节
    57. if (remainingBytes > 0) {
    58. // 发送写使能命令
    59. uint8_t writeEnableCommand = 0x06;
    60. HAL_SPI_Transmit(&hspi1, &writeEnableCommand, 1, HAL_MAX_DELAY);
    61. // 发送写命令和地址
    62. uint8_t writeCommand[] = {0x02, (uint8_t)((address >> 16) & 0xFF), (uint8_t)((address >> 8) & 0xFF), (uint8_t)(address & 0xFF)};
    63. HAL_SPI_Transmit(&hspi1, writeCommand, sizeof(writeCommand), HAL_MAX_DELAY);
    64. // 发送剩余数据
    65. HAL_SPI_Transmit(&hspi1, dataBuffer, remainingBytes, HAL_MAX_DELAY);
    66. // 等待写入完成
    67. while (W25Q64_IsWriteInProgress()) {
    68. HAL_Delay(1);
    69. }
    70. }
    71. }
    72. // 检查写入是否仍在进行中
    73. int W25Q64_IsWriteInProgress(void)
    74. {
    75. uint8_t statusReg;
    76. // 发送读取状态寄存器命令
    77. uint8_t readStatusCommand = 0x05;
    78. HAL_SPI_Transmit(&hspi1, &readStatusCommand, 1, HAL_MAX_DELAY);
    79. // 读取状态寄存器
    80. HAL_SPI_Receive(&hspi1, &statusReg, 1, HAL_MAX_DELAY);
    81. // 检查忙位 (Bit 0)
    82. return (statusReg & 0x01);
    83. }
    84. void HAL_SPI_MspInit(SPI_HandleTypeDef* hspi)
    85. {
    86. GPIO_InitTypeDef GPIO_InitStruct = {0};
    87. if (hspi->Instance == SPI1)
    88. {
    89. __HAL_RCC_SPI1_CLK_ENABLE();
    90. __HAL_RCC_GPIOA_CLK_ENABLE();
    91. /**SPI1 GPIO Configuration
    92. PA5 ------> SPI1_SCK
    93. PA6 ------> SPI1_MISO
    94. PA7 ------> SPI1_MOSI
    95. */
    96. GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
    97. GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    98. GPIO_InitStruct.Pull = GPIO_NOPULL;
    99. GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    100. GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
    101. HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    102. }
    103. }

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

    W25Q64读可跨页数据

    1. #include "main.h"
    2. #include "stm32f4xx_hal.h"
    3. SPI_HandleTypeDef hspi1;
    4. #define CS_PIN GPIO_PIN_4
    5. #define CS_PORT GPIOA
    6. #define PAGE_SIZE 256 // 假设一页的大小为256字节
    7. void SystemClock_Config(void);
    8. static void MX_GPIO_Init(void);
    9. static void MX_SPI1_Init(void);
    10. void W25Q64_ReadData(uint8_t* dataBuffer, uint32_t address, uint32_t dataSize);
    11. int main(void)
    12. {
    13. HAL_Init();
    14. SystemClock_Config();
    15. MX_GPIO_Init();
    16. MX_SPI1_Init();
    17. // 允许片选引脚
    18. HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET);
    19. // 读取数据的缓冲区
    20. uint8_t dataBuffer[512]; // 假设读取512字节的数据
    21. // 从地址0开始读取数据
    22. W25Q64_ReadData(dataBuffer, 0, sizeof(dataBuffer));
    23. // 关闭片选引脚
    24. HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET);
    25. while (1)
    26. {
    27. // Your application code here
    28. }
    29. }
    30. // 读取数据的函数
    31. void W25Q64_ReadData(uint8_t* dataBuffer, uint32_t address, uint32_t dataSize)
    32. {
    33. uint32_t currentPage, remainingBytes;
    34. // 计算当前页和剩余字节数
    35. currentPage = address / PAGE_SIZE;
    36. remainingBytes = dataSize;
    37. // 读取整页数据
    38. while (remainingBytes >= PAGE_SIZE) {
    39. // 发送读命令和地址
    40. uint8_t readCommand[] = {0x03, (uint8_t)((address >> 16) & 0xFF), (uint8_t)((address >> 8) & 0xFF), (uint8_t)(address & 0xFF)};
    41. HAL_SPI_Transmit(&hspi1, readCommand, sizeof(readCommand), HAL_MAX_DELAY);
    42. // 接收数据
    43. HAL_SPI_Receive(&hspi1, dataBuffer, PAGE_SIZE, HAL_MAX_DELAY);
    44. // 更新地址和剩余字节数
    45. address += PAGE_SIZE;
    46. dataBuffer += PAGE_SIZE;
    47. remainingBytes -= PAGE_SIZE;
    48. }
    49. // 读取剩余字节
    50. if (remainingBytes > 0) {
    51. // 发送读命令和地址
    52. uint8_t readCommand[] = {0x03, (uint8_t)((address >> 16) & 0xFF), (uint8_t)((address >> 8) & 0xFF), (uint8_t)(address & 0xFF)};
    53. HAL_SPI_Transmit(&hspi1, readCommand, sizeof(readCommand), HAL_MAX_DELAY);
    54. // 接收剩余数据
    55. HAL_SPI_Receive(&hspi1, dataBuffer, remainingBytes, HAL_MAX_DELAY);
    56. }
    57. }
    58. void HAL_SPI_MspInit(SPI_HandleTypeDef* hspi)
    59. {
    60. GPIO_InitTypeDef GPIO_InitStruct = {0};
    61. if (hspi->Instance == SPI1)
    62. {
    63. __HAL_RCC_SPI1_CLK_ENABLE();
    64. __HAL_RCC_GPIOA_CLK_ENABLE();
    65. /**SPI1 GPIO Configuration
    66. PA5 ------> SPI1_SCK
    67. PA6 ------> SPI1_MISO
    68. PA7 ------> SPI1_MOSI
    69. */
    70. GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
    71. GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    72. GPIO_InitStruct.Pull = GPIO_NOPULL;
    73. GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    74. GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
    75. HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    76. }
    77. }

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

    七、STM32内部Flash

    1、内部flash信息

    1.查数据手册的flash章节

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

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

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

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

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

    3.操作函数查HAL库

    未完成

  • 相关阅读:
    【Java 进阶篇】揭秘 Jackson:Java 对象转 JSON 注解的魔法
    [附源码]java毕业设计风景区管理系统
    一个自己用的复制对象的工具类
    Electron中加载百度地图api调用其中方法报错:Uncaught ReferenceError: md5 is not defined
    Java 中如何实现序列化?
    L18.linux命令每日一练 -- 第三章 文件过滤及内容编辑处理命令 -- head和tail命令
    2.20每日一题(被积函数带绝对值的定积分)
    秋招面经记录
    1086 就不告诉你
    python脚本合并多个pdf文件
  • 原文地址:https://blog.csdn.net/m0_63077733/article/details/134491959