江科大STM32系列笔记:SPI通信及W25Q64闪存芯片详解

一、SPI 总线基础

(一)定义

SPI(Serial Peripheral Interface)由 Motorola 公司开发,是一种通用数据总线。

(二)通信线组成

  1. SCK(Serial Clock):串行时钟线,用于同步数据传输。
  2. MOSI(Master Output Slave Input):主设备输出从设备输入线,主设备经此向从设备发送数据。
  3. MISO(Master Input Slave Output):主设备输入从设备输出线,从设备经此向主设备发送数据。
  4. SS(Slave Select):从设备选择线,用于选定特定从设备进行通信。

(三)通信特性

  1. 同步:数据传输在时钟信号(SCK)同步下进行。
  2. 全双工:主设备和从设备可同时双向传输数据。

(四)设备连接特点

支持一主多从模式,即一个主设备可连接多个从设备,通过 SS 线选择与之通信的从设备。

二、SPI 设备连接与引脚配置

(一)设备连接方式

  1. 公共线连接:所有 SPI 设备的 SCK、MOSI、MISO 分别相连。其中 SCK 负责同步,MOSI 用于主机向从机发送数据,MISO 用于从机向主机发送数据。
  2. 从机选择连接:主机引出多条 SS 控制线,分别连接各从机的 SS 引脚,通过控制 SS 线电平高低(一般低电平选中)来选择特定从机通信。
  3. 注意事项

在有多个从机的情况下,会出现主机一个 MISO、从机多个 MISO 的情况。为保证从机接收到的数据的一致性,当 SS 为高位(该从机未被使用)时,从机的 MISO 需设置为高阻态(相当于断掉),以此确保正在调用的从机数据能够准确传入主机之中。这样可避免未被选中的从机干扰正在通信的从机数据传输,保障 SPI 通信系统在多从机环境下的稳定性和准确性。

(二)引脚配置

  1. 输出引脚:配置为推挽输出,可提供较强驱动能力,保证数据传输速度与稳定性。
  2. 输入引脚:配置为浮空或上拉输入,可增强抗干扰能力和信号检测准确性。

三、W25Q64 FLASH 存储芯片要点

(一)引脚对应关系

  1. CLK 引脚对应 SPI 总线的 SCK(串行时钟引脚)。
  2. DI 与 DO 引脚对应 SPI 总线的 MOSI 和 MISO,但对应关系取决于芯片是主设备还是从设备。

(二)主从设备下引脚功能

当 W25Q64 芯片作为从设备时:

  1. DI(数据输入)引脚:从设备的数据输入需连接主机的设备输出,即 DI 应连接到主机的 MOSI。
  2. DO(数据输出)引脚:从设备的数据输出应连接主机的 MISO,即 DO 连接到 MISO。

四、应用要点

  1. 应用 SPI 总线时,需牢记其同步、全双工、一主多从特性。
  2. 连接 W25Q64 芯片与主机时,要准确理解引脚功能、连接方式及配置方法,以保障通信正常。

五、移位示意图

  • SPI时序
  • 0模式是上升沿读取,下降沿切换字节,并且ss置0时同时开始传输数据

    1模式时下降沿读取,上升沿切换字节,并且SCK置1时才开始传输数据

    模式3与1对应,SCK波形取反,CPOL=1

    模式2与0对应,SCK波形取反,CPOL=1

    注意:模式0与模式3都是上升沿采入——模式1与模式2都是下降沿采入

    这两张图展示了 SPI(Serial Peripheral Interface,串行外设接口)的两种工作模式。

    共同点

  • 都在描述交换一个字节的过程。
  • 都设定了CPOL = 0,即空闲状态时,SCK(时钟信号)为低电平。
  • 不同点

  • 模式 0:CPHA = 0,SCK第一个边沿移入数据,第二个边沿移出数据。从波形图上看,在SCK的上升沿进行数据移入,下降沿进行数据移出。
  • 模式 1:CPHA = 1,SCK第一个边沿移出数据,第二个边沿移入数据 。从波形图上看,在SCK的上升沿进行数据移出,下降沿进行数据移入。
  • 简单来说,两张图都是 SPI 在不同CPHA设置下交换一个字节数据的波形图,区别在于SCK不同边沿上数据的移入和移出操作不同。

    初始化阶段

    将 SS(即 a)置 0 开始运行。

    SS 为片选信号,用于选择要通信的从设备,低电平有效。

    此操作目的是选中对应的从设备,使其进入准备接收和发送数据的状态,开启通信流程。

    数据传输同步阶段:(模式一)

    发送同步时钟(SCK,即 b)至高位。

    SCK 为串行时钟信号,在时钟信号的上升沿和下降沿进行数据的发送和接收操作。

    其目的是在上升沿时,为主从设备数据的发送和接收提供同步时机,让双方能够在同一时刻进行数据交互。

    数据发送与读取阶段

    在同步时钟 SCK 上升沿时,MOSI(即 c)和 MISO(即 d)开始读取当前自己。

    MOSI 是主输出从输入线,主设备通过它向从设备发送数据;

    MISO 是主输入从输出线,从设备通过它向主设备发送数据。

    此时,主设备将数据通过 MOSI 线发送,从设备将数据通过 MISO 线发送,双方同时读取各自线上的数据,实现数据的双向传输。

    主机读取阶段

    当同步时钟 SCK 置 0,即下降沿到来时,主机读取数据。

    因为在 SCK 下降沿时,数据在 MISO 线上处于稳定状态,主机可准确读取从设备通过 MISO 线发送过来的数据。

    整段数据交换完成阶段

    重复上述 SCK 上升沿发送数据、下降沿读取数据的操作,即可完成一整段数据交换。在此过程中,SS 保持低电平选中从设备,SCK 不断产生上升沿和下降沿,MOSI 和 MISO 逐位传输数据。通过多次重复这些操作,逐位完成一整段数据的双向交换。

    继续通信阶段

    若主设备和从设备之间还有数据需要传输,即继续接收数据时,重复上述所有操作。也就是继续利用 SS、SCK、MOSI、MISO 协同工作,重复时钟信号的上升沿发送数据、下降沿读取数据的过程,实现后续数据的交换。

    结束通信阶段

    若主设备完成数据交换,不需要继续通信,将 SS(即 a)置 1,从机停止运行,同时将 MISO(即 d)设置至高阻态。

    SS 拉高表示取消对从设备的选择,使从设备停止通信操作进入空闲状态;

    MISO 高阻态意味着该引脚在电气上与电路断开,不输出信号,避免对其他设备产生干扰。

  • SPI时序
  • 这里的0x06是写使能,0x03是读指令

    W25Q64简介

    W25Q64

    1. 存储区域划分(Block Segmentation)

    图的左上角展示了存储区域的划分。可以看到不同的扇区(Sector),每个扇区大小有 4KB 或 1KB ,并且有对应的十六进制地址范围。例如,Sector 15 的地址范围是 0xF000h – 0xFFFFh ,大小为 4KB。扇区是存储设备中用于数据擦除和编程的基本单元。

    2. 存储块(Block)

    图的右侧展示了不同的存储块(Block),每个块的大小为 64KB ,并且有对应的十六进制地址范围。例如,Block 127 的地址范围是 0x7F000h – 0x7FFFFh 。存储块是由多个扇区组成的,在闪存中,擦除操作通常是以块为单位进行的。

    3. 控制逻辑部分

  • Write Control Logic:写控制逻辑,负责管理数据写入操作,/WP(Write Protect)引脚用于控制写保护功能。
  • Status Register:状态寄存器,用于存储芯片的状态信息,如操作是否完成、是否有错误等。
  • SPI Command & Control Logic:SPI(Serial Peripheral Interface)命令和控制逻辑,用于处理通过 SPI 接口发送的命令和数据。相关引脚包括 /HOLD、CLK、/CS、DI(Data In)和 DO(Data Out)。
  • High Voltage Generators:高压发生器,用于在编程和擦除操作时提供所需的高电压。
  • Page Address Latch / Counter 和 Byte Address Latch / Counter:分别用于锁存页地址和字节地址,在闪存操作中,数据是以页为单位进行读写的。
  • 4. 解码和缓冲部分

  • Write Protect Logic and Row Decode:写保护逻辑和行解码,用于选择特定的存储行。
  • Column Decode And 256 – Byte Page Buffer:列解码和 256 字节页缓冲,用于在读写操作时缓存数据。页缓冲器可以提高数据传输的效率,在页编程和读取操作中起到重要作用。
  • 总体来说,这张图详细描述了存储芯片内部的地址组织结构以及实现数据读写、擦除等操作的控制逻辑和电路模块,有助于理解闪存芯片的工作原理和操作流程。

    疑问:为什么FLASH操作相对于RAM操作如此繁琐?

    1. Flash 存储器特性
      1. Flash 是一种掉电不丢失的存储器。
      2. 为保证掉电不丢失特性,同时实现足够大的存储容量和较低成本,在其他方面做出妥协,如操作便携性。
    2. Flash 与 RAM 在写入操作上的差异
      1. RAM 写入特点:写入操作简单直接,想写哪里就写哪里,想写多少都可以,并且支持覆盖写入。
      2. Flash 写入特点:写入操作不如 RAM 简单直接,不具备像 RAM 那样想写哪里就写哪里、想写多少就写多少以及覆盖写入的特性 ,需要遵守上面tu'pia

    一.SPI通信层配置

    1. 设置void MySPI_Init(void) –引脚初始化

    端口设置PA4.5.6.7,其中a6设置GPIO_Mode_IPU,其余设置GPIO_Mode_Out_PP(why)

    1. 从机选择(给这几个GPIO换个名字)

    void MySPI_W_SS(uint8_t BitValue)

    void MySPI_W_SCK(uint8_t BitValue)

    void MySPI_W_MOSI(uint8_t BitValue)

    uint8_t MySPI_R_MISO(void)

    1. 在void MySPI_Init(void) 函数中添加引脚初始化

    MySPI_W_SS(1);MySPI_W_SCK(0);

    1. 根据SPI基本时序单元图封装函数

    void MySPI_Start(void)

    void MySPI_Stop(void)

    uint8_t MySPI_SwapByte(uint8_t ByteSend)

    注意:MySPI_W_MOSI(ByteSend & (0x80 >> i));//取输入数据最高位(&只要有一个为0,都为0) 

     MySPI_W_SCK(1);//SCK置高位

      if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}

      //主机把从机的高位接受进来(|只要有一个为1,都为1),把第I位设置成1

      //关于MySPI_R_MISO() == 1?刚刚设置ByteReceive = 0x00了嘛,所以如若接收到的这位是0,直接不用管了呀,它本身这个位置上的数就是0

      MySPI_W_SCK(0);//SCK置低位

    //交换数据不就是既发送也接受,如果想读取就随便发送然后接收就行,想写入,就发送写入值,不操作接收的返回值即可(ByteReceive = 0x00;已经在函数中设置过了,如果想要读取就改成ByteSend = 0x00后进行逻辑上的调整即可,只要满足主从交换就没问题

    1. 补充:模式123如何改 

    模式1

    MySPI_W_SCK(1);
    
    MySPI_W_MOSI(ByteSend & (0x80 >> i));
    
    MySPI_W_SCK(0);
    
    if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}

    模式3(注意,源代码中MySPI_W_SCK(0改成MySPI_W_SCK(1))

    MySPI_W_SCK(0);
    
    MySPI_W_MOSI(ByteSend & (0x80 >> i));
    
    MySPI_W_SCK(1);
    
    if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}

    二.SPI上层配置——W25Q64

    1.初始化与读ID,保证SPI底层逻辑调用成功

    void W25Q64_Init(void)
    
    { MySPI_Init();}
    
    void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
    
    { // 1. 启动 SPI 通信
    
        MySPI_Start();
    
        // 2. 发送 JEDEC ID 读取命令
    
        MySPI_SwapByte(W25Q64_JEDEC_ID);
    
        // 3. 读取制造商 ID
    
        *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
    
        // 4. 读取设备 ID 的高 8 位
    
        *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
    
        // 5. 将高 8 位左移 8 位
    
        *DID <<= 8;
    
        // 6. 读取设备 ID 的低 8 位,并与高 8 位合并
    
        *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);
    
        // 7. 停止 SPI 通信
    
        MySPI_Stop();}

    解析:

    代码逐行解释

     

    1. MySPI_Start();

    2. 调用 MySPI_Start 函数,该函数的作用是启动 SPI 通信。通常,这意味着将 SPI 接口的片选信号(CS)拉低,以选中 W25Q64 芯片。
    3. MySPI_SwapByte(W25Q64_JEDEC_ID);

    4. 调用 MySPI_SwapByte 函数,发送 W25Q64_JEDEC_ID 命令。W25Q64_JEDEC_ID 是一个预定义的常量,表示读取 JEDEC ID 的命令码。MySPI_SwapByte 函数会在发送一个字节的同时接收一个字节的数据。
    5. *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);

    6. 发送一个虚拟字节 W25Q64_DUMMY_BYTE,并将接收到的字节存储到 MID 指针所指向的内存位置。这个接收到的字节就是 W25Q64 芯片的制造商 ID。
    7. *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);

    8. 再次发送一个虚拟字节 W25Q64_DUMMY_BYTE,并将接收到的字节存储到 DID 指针所指向的内存位置。这个接收到的字节是设备 ID 的高 8 位。
    9. *DID <<= 8;

    10. 将 DID 的值左移 8 位,为存储设备 ID 的低 8 位腾出空间。
    11. *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);

    12. 发送另一个虚拟字节 W25Q64_DUMMY_BYTE,并将接收到的字节(设备 ID 的低 8 位)与 DID 的高 8 位进行按位或运算,合并成一个完整的 16 位设备 ID。
    13. MySPI_Stop();

    14. 调用 MySPI_Stop 函数,停止 SPI 通信。通常,这意味着将 SPI 接口的片选信号(CS)拉高,取消对 W25Q64 芯片的选中。

    2.写使能与通过读状态寄存器判断是否在忙

    void W25Q64_WaitBusy(void)
    {
        // 1. 启动 SPI 通信
        MySPI_Start();
    
        // 2. 发送读取状态寄存器 1 的命令
        MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
    
        // 3. 循环检查状态寄存器 1 的最低位(忙碌标志位)
        while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)
        {
            // 若最低位为 1,表示芯片处于忙碌状态,继续循环等待
            // 最后一位为Busy位,具体参考芯片手册
        }
    
        // 4. 停止 SPI 通信
        MySPI_Stop();
    }

    注意:为了放置死循环意外卡死,可以设置变量Timeout

    void W25Q64_WaitBusy(void)
    {
    	uint32_t Timeout;
    	MySPI_Start();
    	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
    	Timeout = 100000;
    	while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)
    	{
    		Timeout --;
    		if (Timeout == 0)
    		{
    			break;
    		}
    	}
    	MySPI_Stop();
    }
    

    3.页编程函数

    解读:先发送指令码02,再发送三个地址(A0-A23),再发送数据

    注意:只能发送256个Byte,多出会覆盖第一个

    void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
  • *DataArray  为了调用效率提升,设置一个数组提升效率,数组需要指针进行传递,数据类型定义为指针函数
  • uint16_t Count   表示写多少个,注意要使用uint16,写入数据的数量范围为0-256
  • void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
    {
    	uint16_t i;
    	
    	W25Q64_WriteEnable();
    	
    	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();
    }
     问:为什么Address右移传输?

    W25Q64_PageProgram函数中,MySPI_SwapByte(Address >> 16);MySPI_SwapByte(Address >> 8); 和 MySPI_SwapByte(Address); 这几行代码使用右移操作是为了将 32 位的地址 Address 拆分成三个 8 位字节,以便通过 SPI 接口逐字节发送到 W25Q64 芯片。下面详细解释其原理和原因。

    1. W25Q64 芯片的通信协议

    W25Q64 是一款基于 SPI 接口的闪存芯片,当进行页编程(Page Program)操作时,需要向芯片发送一系列命令和数据。具体来说,首先要发送页编程命令(W25Q64_PAGE_PROGRAM),然后发送 3 字节的地址,最后发送要写入的数据。

    2. 地址拆分的必要性

    SPI 接口是一种串行通信接口,每次只能传输 8 位(1 字节)的数据。而 Address 是一个 32 位的无符号整数,为了将这个 32 位的地址通过 SPI 接口发送给 W25Q64 芯片,需要将其拆分成 3 个 8 位字节。

    3. 右移操作的作用

    右移操作(>>)是一种位运算,用于将一个数的二进制表示向右移动指定的位数。在这个函数中,右移操作的具体作用如下:

  • MySPI_SwapByte(Address >> 16);:将 Address 右移 16 位,这样就把 Address 的高 8 位移动到了低 8 位,然后通过 MySPI_SwapByte 函数将这 8 位发送出去。
  • MySPI_SwapByte(Address >> 8);:将 Address 右移 8 位,把 Address 的中间 8 位移动到了低 8 位,再通过 MySPI_SwapByte 函数发送这 8 位。
  • MySPI_SwapByte(Address);:直接使用 Address 的低 8 位,通过 MySPI_SwapByte 函数发送出去。
  • 假设 Address 的值为 0x123456,它的二进制表示为:
    0000 0000 0001 0010 0011 0100 0101 0110
    
    Address >> 16 的结果为 0x0012,二进制表示为:
    0000 0000 0000 0000 0000 0000 0001 0010
    发送的是 0x12
    
    Address >> 8 的结果为 0x1234,二进制表示为:
    0000 0000 0000 0000 0001 0010 0011 0100
    发送的是 0x34。
    
    Address 本身为 0x123456,发送的是 0x56
    
    这样,通过三次右移操作和 MySPI_SwapByte 函数调用,就将 32 位的地址 0x123456 拆分成三个 8 位字节 0x12、0x34 和 0x56 依次发送给了 W25Q64 芯片。
    
    综上所述,使用右移操作是为了满足 SPI 接口每次只能传输 8 位数据的要求,将 32 位的地址拆分成 3 个 8 位字节进行传输。
    

    4.擦除选项

    void W25Q64_SectorErase(uint32_t Address)//只演示扇区擦除
    {
        // 使能 W25Q64 的写操作,在进行擦除操作前需要先使能写操作
        W25Q64_WriteEnable();
    
        // 启动 SPI 通信,开始与 W25Q64 进行数据交互
        MySPI_Start();
        // 发送扇区擦除命令(4KB 扇区),通知 W25Q64 即将进行扇区擦除操作
        MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
        // 将 32 位地址的高 8 位通过 SPI 发送给 W25Q64,指定要擦除的扇区起始地址的高字节
        MySPI_SwapByte(Address >> 16);
        // 将 32 位地址的中间 8 位通过 SPI 发送给 W25Q64,指定要擦除的扇区起始地址的中间字节
        MySPI_SwapByte(Address >> 8);
        // 将 32 位地址的低 8 位通过 SPI 发送给 W25Q64,指定要擦除的扇区起始地址的低字节
        MySPI_SwapByte(Address);
        // 停止 SPI 通信,结束与 W25Q64 的本次数据交互
        MySPI_Stop();
    
        // 等待 W25Q64 完成扇区擦除操作,在此期间芯片处于忙碌状态,等待其操作完成
        W25Q64_WaitBusy();
    }

    5.读取数据

  • High Impedance 高阻态
  • 读没有256的限制,Data Out x可以一直读,所以后面的uint16_t Count改成了uint32_t Count,扩大了数字限制
  • void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
    {
        uint32_t i;
    
        // 启动 SPI 通信,准备与 W25Q64 进行数据交互
        MySPI_Start();
    
        // 发送读取数据的命令给 W25Q64,告知芯片接下来要进行读取操作
        MySPI_SwapByte(W25Q64_READ_DATA);
    
        // 将 32 位地址的高 8 位通过 SPI 发送给 W25Q64,指定要读取数据的起始地址的高字节
        MySPI_SwapByte(Address >> 16);
        // 将 32 位地址的中间 8 位通过 SPI 发送给 W25Q64,指定要读取数据的起始地址的中间字节
        MySPI_SwapByte(Address >> 8);
        // 将 32 位地址的低 8 位通过 SPI 发送给 W25Q64,指定要读取数据的起始地址的低字节
        MySPI_SwapByte(Address);
    
        // 循环读取指定数量的数据
        for (i = 0; i < Count; i++)
        {
            // 向 W25Q64 发送一个哑字节(无实际意义的字节),同时接收从该地址读取到的数据
            // 并将读取到的数据存储到 DataArray 数组中
            DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
        }
    
        // 停止 SPI 通信,结束与 W25Q64 的本次数据交互
        MySPI_Stop();
    }
    注意:关于 W25Q64_WaitBusy();位置相关问题
    事后等待(本次代码方法)
  • 优点:在函数之外等待,能确保芯片处于不忙状态,稳定性和可靠性高。
  • 缺点:等待时程序处于阻塞状态,无法执行其他代码,可能导致整体运行效率降低。
  • 事前等待
  • 优点:写完后无需等待,程序可立即执行其他代码,理论上可提高效率。有可能在执行其他代码的过程中,芯片完成忙碌状态,无需额外等待时间。
  • 缺点:无法完全确定等待时间,存在等待时间不足的风险。若在芯片忙碌时进行读取操作,可能导致数据读取错误。在写入操作和读取操作之前都需要等待,适用场景相对复杂,增加了代码编写的复杂性。
  • void ......
    {
    	uint16_t i;
    
    	W25Q64_WaitBusy();//事前等待
    
    	W25Q64_WriteEnable();
    	MySPI_Start();
    
    	.....
    	for (i = 0; i < Count; i ++)
    	{
    		....
    	}
    	MySPI_Stop();
    	
    }

    这样就配置完了,记得去头文件申明一下,并在主函数中调用啊。

    6.主函数配置

    #include "stm32f10x.h"                  // 包含STM32F10x系列微控制器的头文件,提供了该系列芯片的寄存器定义等相关信息
    #include "Delay.h"                      // 包含延时函数的头文件,可用于实现不同时长的延时操作
    #include "OLED.h"                       // 包含OLED显示屏驱动的头文件,用于对OLED屏幕进行初始化和显示操作
    #include "W25Q64.h"                     // 包含W25Q64闪存芯片驱动的头文件,用于对W25Q64芯片进行读写、擦除等操作
    
    // 定义变量用于存储W25Q64芯片的制造商ID(MID)和设备ID(DID)
    uint8_t MID;
    uint16_t DID;
    
    // 定义一个用于写入W25Q64芯片的数组,包含4个字节的数据
    uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04};
    // 定义一个用于从W25Q64芯片读取数据的数组,长度为4字节
    uint8_t ArrayRead[4];
    
    int main(void)
    {
        OLED_Init();
        W25Q64_Init();
    
        // 在OLED屏幕的第1行第1列开始显示字符串 "MID:   DID:",用于提示接下来要显示的信息
        OLED_ShowString(1, 1, "MID:   DID:");
        // 在OLED屏幕的第2行第1列开始显示字符串 "W:",表示接下来要显示写入的数据
        OLED_ShowString(2, 1, "W:");
        // 在OLED屏幕的第3行第1列开始显示字符串 "R:",表示接下来要显示读取的数据
        OLED_ShowString(3, 1, "R:");
    
        // 调用W25Q64_ReadID函数读取W25Q64芯片的制造商ID和设备ID,并将结果存储在MID和DID变量中
        W25Q64_ReadID(&MID, &DID);
        // 在OLED屏幕的第1行第5列开始以十六进制形式显示制造商ID,显示2位
        OLED_ShowHexNum(1, 5, MID, 2);
        // 在OLED屏幕的第1行第12列开始以十六进制形式显示设备ID,显示4位
        OLED_ShowHexNum(1, 12, DID, 4);
    
        // 调用W25Q64_SectorErase函数擦除W25Q64芯片地址为0x000000的扇区,为后续写入数据做准备
        W25Q64_SectorErase(0x000000);
        // 调用W25Q64_PageProgram函数将ArrayWrite数组中的4个字节数据写入W25Q64芯片地址为0x000000的页面
        W25Q64_PageProgram(0x000000, ArrayWrite, 4);
    
        // 调用W25Q64_ReadData函数从W25Q64芯片地址为0x000000的位置开始读取4个字节的数据,并存储到ArrayRead数组中
        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)
        {
            
        }
    }

    作者:三月雪落无痕

    物联沃分享整理
    物联沃-IOTWORD物联网 » 江科大STM32系列笔记:SPI通信及W25Q64闪存芯片详解

    发表回复