STM32CubeMX-H7-15-SPI通信协议读写W25Q64

news2025/4/19 9:19:13

前言

SPI(Serial Peripheral Interface)通信协议是一种高速、全双工、同步的串行通信协议

本篇文章就使用W25Q64模块来学习SPI,包括软件SPI代码的编写硬件SPI中断SPIDMA+SPI

SPI的应用场景和模块

        !这里是抄AI的,主要是也要了解学了之后有什么用

  • 存储设备:如闪存芯片(如 W25Q64 等)与微控制器的连接。SPI 接口可实现高速的数据读写,用于程序代码存储、数据存储等,支持频繁的擦除和写入操作,满足设备对数据存储和快速访问的需求。
  • 传感器:许多传感器如温度传感器、压力传感器、加速度传感器等,采用 SPI 接口与主控芯片通信。传感器将采集到的模拟信号转换为数字信号后,通过 SPI 协议快速传输给主控芯片进行处理,以实现对物理量的精确测量和实时监测。
  • 显示设备:SPI 常用于连接微控制器与液晶显示器(LCD)或有机发光二极管显示器(OLED)的驱动芯片。能够高效地传输图像数据和控制指令,实现显示屏的快速刷新和高分辨率显示,满足不同显示场景的需求。
  • 音频设备:在音频编解码器、数字音频放大器等音频设备中,SPI 用于传输音频数据和控制信息。可以实现高质量的音频数据传输,支持多种音频格式和采样率,确保音频信号的准确还原和播放。
  • 无线通信模块:如 Wi-Fi 模块、蓝牙模块等与主控芯片之间常通过 SPI 接口进行通信。SPI 协议能够满足无线通信模块与主控芯片之间高速数据传输的需求,实现设备间的短距离无线数据传输和通信。
  • 现场可编程门阵列(FPGA)配置:在一些系统中,通过 SPI 接口将配置数据加载到 FPGA 中,实现对 FPGA 的功能配置和初始化。这种方式简单可靠,能够在系统启动时快速完成 FPGA 的配置,确保其正常工作。

SPI信号线

  1. 主设备出、从设备入(MOSI):主设备通过这条线向从设备传输数据,也被称为从设备输入线(SI 或 SDI)。连接设备DI引脚
  2. 主设备入、从设备出(MISO):从设备利用这条线向主设备发送数据,也叫从设备输出线(SO 或 SDO)。连接设备DO引脚
  3. 串行时钟(SCLK):由主设备产生的时钟信号,用于同步主从设备之间的数据传输,从设备根据 SCLK 的节奏来接收或发送数据。
  4. 从设备选择(SS):也称为片选信号线,低电平有效。主设备通过控制这条线来选择要与之通信的从设备。当有多个从设备时,每个从设备都有独立的 SS 引脚与主设备相连,主设备通过拉低相应从设备的 SS 引脚来使能该从设备进行数据传输。

SPI时序

        spi的时序有四种,但是我们会一种就够了。

        我们学习IIC的时候知道,在

        CLK高电平的时候是设备读取数据的时间,此时SDA不能改变数据

        只有在CLK为低电平的时候,SDA才能改变数据电平

       

        那么SPI也是一样的道理

        这里是SPI的一种模式

        就是在CLK上升沿的时候,从设备和主设备读数据

        在下降沿的时候,从设备和主设备也修改数据

        然后都是从最高位开始发送

        因为是上升沿和下降沿的时候实现读写,而且时钟线又是独立的可快可慢,所以SPI的速度可以到几M甚至几十M每秒。

        

uint8_t SPI_read_write_byte(uint8_t byte)
{
    uint8_t i;
    uint8_t data = 0;
    // 发送一个字节数据
    for (i = 0; i < 8; i++)
    {
        SPI_CLK_LOW();  // 拉低时钟
        if (byte & 0x80)  // 判断最高位是否为1
        {
            SPI_MOSI_HIGH();  // 发送1
        }
        else
        {
            SPI_MOSI_LOW();  // 发送0
        }
        byte <<= 1;  // 左移一位
        SPI_CLK_HIGH();  // 拉高时钟
        data <<= 1;  // 左移一位
        if (SPI_MISO_READ())  // 读取MISO信号
        {
            data |= 0x01;  // 读取到1
        } 
        else
        {
            data &= 0xFE;  // 读取到0
        }
    }
    return data;  // 返回读取到的数据
}

        这个是SPI的核心时序代码,是执行一次读写八位的操作

        跟IIC不同

        IIC是一条8位指令,只能写或者读

        CLK高电平的时候,不是主设备写从设备读,就是从设备写主设备读

        

        但是SPI是可以同时写和读的

        一条八位指令,同时完成读和写

        CLK高电平的时候,主设备从设备

        CLK低电平的时候,从设备主设备

        可以有多总理解方法,像我这样理解也行

        SPI通信就是围绕上面的一条函数来执行

        基本上,后面我们就使用这一个函数,来实现对W25Q64的读写

W25Q64--EEPROM数据存储模块

       以下是跟AI搜集到的资料,稍微简单了解下模块

W25Q64 是华邦公司推出的一款大容量 SPI FLASH 产品,以下将从其基本参数、性能特点、应用场景等方面进行详细介绍:

  • 基本参数
    • 存储容量:容量为 64Mb,即 8M 字节。它将 8M 字节的容量分为 128 个块,每个块大小为 64K 字节,每个块又分为 16 个扇区,每个扇区 4K 字节2
    • 封装形式:常见的有 8 引脚 SOIC 208 mil、8 焊盘 XSON 4x4 mm、8 焊垫 USON 4x3 mm、8 垫 WSON 6x5 mm/8x6 mm、24 球 TFBGA 8x6 mm(5x5 球阵列)、12 球 WLCSP 等3
  • 性能特点
    • 多种 SPI 接口支持:支持标准的 SPI 接口,还支持双输出 / 四输出的 SPI 接口,可满足不同系统对数据传输速率的要求。在双输出 SPI 模式下,等效时钟频率可达 160MHz;在四输出 SPI 模式下,等效时钟频率可达 320MHz256
    • 高性能串行闪存:高达普通串行闪存性能的 6 倍,支持最高 133MHz 的 SPI 时钟频率,当使用 SPI 快速读取 Quad I/O 指令时,允许 Quad I/O 的等效时钟速率为 532MHz(133MHz x 4),数据连续传输速率可达 66MB/S23
    • 高效的连续读取模式:具有低指令开销,仅需 8 个时钟周期处理内存,允许 XIP(Execute In Place)操作,即可以直接从闪存中执行代码,性能优于 X16 并行闪存2
    • 低功耗与宽温度范围:单电源供电,电压范围为 1.7V 至 1.95V,断电时电流消耗低至 1µA。正常运行温度范围为 - 40°C 至 + 85°C,部分型号可支持到 + 105°C,适用于各种恶劣环境3
    • 灵活的擦写操作:扇区统一擦除大小为 4KB,支持 1 到 256 个字节编程,擦写周期多达 10 万次以上,数据可保存达 20 年之久23
    • 高级安全功能:支持 JEDEC 标准制造商和设备 ID、64 位唯一序列号和三个 256 字节的安全寄存器。提供软件和硬件写保护、特殊 OTP 保护、顶部 / 底部互补阵列保护、单个块 / 扇区阵列保护等多种数据保护功能3
  • 应用场景
    • 嵌入式系统:作为嵌入式系统中的非易失性存储解决方案,可存储固件、操作系统和应用程序代码,如智能家居设备、工业控制器、医疗设备等。
    • 消费电子:在智能手机、平板电脑、数码相机、智能手表、健康监测设备等消费电子产品中,用于存储用户数据、应用程序、媒体文件和用户设置等。
    • 汽车电子:适用于汽车电子系统,如存储发动机控制单元(ECU)的固件、导航系统的数据和其他关键信息。
    • 通信设备:可用于通信基站、路由器等设备中,存储网络配置和固件更新。
    • 计算机外设:在打印机、扫描仪等计算机外设中,存储设备固件和驱动程序。
    • 智能卡和安全令牌:因其具有安全性特性,可用于智能卡和安全令牌中存储敏感信息。
    • 数据记录器:在需要数据记录和回放的应用中,如安全监控系统、医疗设备等,可用于存储数据。

字节、扇区、块、页的区别

        这个是存储空间的概念,基本上后面都会涉及,还是要做一些了解吧,比如这个模块擦除是以扇区为单位,不知道扇区就不好理解

  • 字节:是存储容量的基本单位,1 字节等于 8 位二进制数。W25Q64 的存储容量为 64Mbit,换算成字节就是 8MByte,即该芯片可以存储 8×1024×1024 个字节的数据,每个字节都有唯一的地址,通过地址可以对芯片中的数据进行读写操作1
  • :是闪存芯片中最小的可擦除单元,在 W25Q64 中,每页大小为 256 字节。页适用于需要频繁读写且存储小量数据的场景,如缓存、寄存器、配置信息等。可以对页进行独立的读取、写入和擦除操作,但写入时一次最多只能写入 256 字节的数据,如果超过 256 字节,就需要分多次写入或者进行跨页操作45
  • 扇区:是存储器中的逻辑分区,由多个页组成。在 W25Q64 中,16 个页组成一个扇区,所以一个扇区的大小是 4096 字节(即 4KB)。扇区适用于中等大小的数据存储和操作,如文件系统、日志记录等。扇区也是闪存芯片中常用的擦除单位,通常不能单独擦除一个页,而是要以扇区为单位进行擦除,这意味着在擦除扇区内的数据时,会将整个扇区的内容都清除245
  • :是存储器中的逻辑分块,由多个扇区组成。W25Q64 中,16 个扇区组成一个块,所以每个块的大小为 64KB。块适用于大容量数据存储,如磁盘分区、应用程序和媒体文件等。块通常是最大可擦除单元,擦除一个块将清除该块内的所有数据4

        W25Q64 将 8M 的容量分为 128 个块,每个块又分为 16 个扇区,每个扇区包含 16 个页2。这种分级结构有助于更高效地管理和操作闪存芯片中的数据,根据不同的应用场景和数据量大小,可以选择不同的存储单元进行读写和擦除操作。

页地址、扇区地址、块地址

都是在驱动模块前需要了解的基本资料

1. 页(Page)

  • 每页大小:256 字节($2^8$ 字节)。
  • 页数量:总容量除以每页大小,即 $\frac{8\times1024\times1024}{256}= 32768$ 页。
  • 地址表示:页地址由高 16 位确定,低 8 位用于页内偏移。例如,地址 0x000000 到 0x0000FF 是第 0 页,0x000100 到 0x0001FF 是第 1 页,依此类推。

2. 扇区(Sector)

  • 每扇区大小:4 KB($4\times1024 = 2^{12}$ 字节)。
  • 扇区数量:总容量除以每扇区大小,即 $\frac{8\times1024\times1024}{4\times1024}= 2048$ 个扇区。
  • 地址表示:扇区地址由高 12 位确定,低 12 位用于扇区内偏移。例如,地址 0x000000 到 0x000FFF 是第 0 扇区,0x001000 到 0x001FFF 是第 1 扇区。

3. 块(Block)

  • 每块大小:64 KB($64\times1024 = 2^{16}$ 字节)。
  • 块数量:总容量除以每块大小,即 $\frac{8\times1024\times1024}{64\times1024}= 128$ 个块。
  • 地址表示:块地址由高 8 位确定,低 16 位用于块内偏移。例如,地址 0x000000 到 0x00FFFF 是第 0 块,0x010000 到 0x01FFFF 是第 1 块。

        

使用SPI获取W25Q64制造商 ID和设备 ID 

        首先初始化和确定好引脚

        在.h文件定义引脚和功能函数

这样子可以就是复制代码,然后直接在.h处修改函数就可以改引脚就可以使用了

其实也可以自己在Cubemx配置,只不过我是觉得这样子使用方便

hal_spi.h

#ifndef HAL_SPI_H
#define HAL_SPI_H

#include "main.h"

#define SPI_CS_PIN    GPIO_PIN_0   //连接模块cs
#define SPI_CS_PORT   GPIOA        //连接模块cs

#define SPI_CLK_PIN   GPIO_PIN_2   //连接模块clk
#define SPI_CLK_PORT  GPIOA        //连接模块clk

#define SPI_MOSI_PIN  GPIO_PIN_3   //连接模块mosi
#define SPI_MOSI_PORT GPIOA        //连接模块mosi

#define SPI_MISO_PIN  GPIO_PIN_1   //连接模块miso
#define SPI_MISO_PORT GPIOA        //连接模块miso
// 片选操作宏定义
#define SPI_CS_LOW()    HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_RESET)
#define SPI_CS_HIGH()   HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_SET)

// 时钟信号操作宏定义
#define SPI_CLK_LOW()   HAL_GPIO_WritePin(SPI_CLK_PORT, SPI_CLK_PIN, GPIO_PIN_RESET)
#define SPI_CLK_HIGH()  HAL_GPIO_WritePin(SPI_CLK_PORT, SPI_CLK_PIN, GPIO_PIN_SET)

// MOSI信号操作宏定义
#define SPI_MOSI_LOW()  HAL_GPIO_WritePin(SPI_MOSI_PORT, SPI_MOSI_PIN, GPIO_PIN_RESET)
#define SPI_MOSI_HIGH() HAL_GPIO_WritePin(SPI_MOSI_PORT, SPI_MOSI_PIN, GPIO_PIN_SET)

// MISO信号操作宏定义
#define SPI_MISO_READ() HAL_GPIO_ReadPin(SPI_MISO_PORT, SPI_MISO_PIN)

// 初始化SPI
void HAL_SPI_Init(void);

// 发送一个字节数据
uint8_t SPI_read_write_byte(uint8_t byte);

#endif

在.c处添加初始化函数和SPI通信函数

第二个函数就是核心
 

hal_spi.c

#include "hal_spi.h"
#include "gpio.h"

// 软件SPI初始化
void HAL_SPI_Init(void)
{
    // 初始化GPIO
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    __HAL_RCC_GPIOA_CLK_ENABLE();  // 使能GPIOA时钟
    
    // 配置CS引脚为输出模式
    GPIO_InitStruct.Pin = SPI_CS_PIN ;  // 连接模块cs
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;  // 推挽输出模式
    GPIO_InitStruct.Pull = GPIO_NOPULL;  // 无上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;  // 高速
    HAL_GPIO_Init(SPI_CS_PORT, &GPIO_InitStruct);
    SPI_CS_HIGH();  // 初始状态为高电平

    // 配置CLK引脚为输出模式
    GPIO_InitStruct.Pin = SPI_CLK_PIN;  // 连接模块clk
    HAL_GPIO_Init(SPI_CLK_PORT, &GPIO_InitStruct);
    SPI_CLK_HIGH();  // 初始状态为高电平

    // 配置MOSI引脚为输出模式
    GPIO_InitStruct.Pin = SPI_MOSI_PIN;  // 连接模块mosi
    HAL_GPIO_Init(SPI_MOSI_PORT, &GPIO_InitStruct);

    // 配置MISO引脚为输入模式
    GPIO_InitStruct.Pin = SPI_MISO_PIN;  // 连接模块miso
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;  // 输入模式
    HAL_GPIO_Init(SPI_MISO_PORT, &GPIO_InitStruct);

    SPI_CLK_HIGH();  // 初始状态为高电平
    SPI_MOSI_HIGH();  // 初始状态为高电平
}


// 软件SPI读写一个字节
uint8_t SPI_read_write_byte(uint8_t byte)
{
    uint8_t i;
    uint8_t data = 0;
    // 发送一个字节数据
    for (i = 0; i < 8; i++)
    {
        SPI_CLK_LOW();  // 拉低时钟
        if (byte & 0x80)  // 判断最高位是否为1
        {
            SPI_MOSI_HIGH();  // 发送1
        }
        else
        {
            SPI_MOSI_LOW();  // 发送0
        }
        byte <<= 1;  // 左移一位
        SPI_CLK_HIGH();  // 拉高时钟
        data <<= 1;  // 左移一位
        if (SPI_MISO_READ())  // 读取MISO信号
        {
            data |= 0x01;  // 读取到1
        } 
        else
        {
            data &= 0xFE;  // 读取到0
        }
    }
    return data;  // 返回读取到的数据
}

初始化函数

void W25Q64_Init(void)
{
    HAL_SPI_Init();  // 初始化SPI
    SPI_CS_HIGH();
    HAL_Delay(10);  // 延时10ms
}

获取ID函数

void get_w25q64_id(uint8_t * ManufacturerID, uint16_t * DeviceID )
{
    SPI_CS_LOW();  // 片选拉低,开始通信
    SPI_read_write_byte(0x9F);  // 发送读取Jedec ID指令
    *ManufacturerID = SPI_read_write_byte(0x00);  // 读取Manufacturer ID
    *DeviceID = SPI_read_write_byte(0x00) ;
    *DeviceID <<= 8;
    *DeviceID |= SPI_read_write_byte(0x00);  // 读取Device ID
    SPI_CS_HIGH();  // 片选拉高,结束通信
}

然后直接在主函数调用就OK了

利用串口助手,可以看见已经成功获取ID了

后面还有对页,快,扇区的擦除和写

然后直接给上完整的软件SPI驱动W25Q64

w25164.c

#include "w25q64.h"
#include "hal_spi.h"
#include <stdio.h>

void W25Q64_Init(void)
{
    HAL_SPI_Init();  // 初始化SPI
    SPI_CS_HIGH();
    HAL_Delay(10);  // 延时10ms
}


    void get_w25q64_id(uint8_t * ManufacturerID, uint16_t * DeviceID )
    {
        SPI_CS_LOW();  // 片选拉低,开始通信
        SPI_read_write_byte(0x9F);  // 发送读取Jedec ID指令
        *ManufacturerID = SPI_read_write_byte(0x00);  // 读取Manufacturer ID
        *DeviceID = SPI_read_write_byte(0x00) ;
        *DeviceID <<= 8;
        *DeviceID |= SPI_read_write_byte(0x00);  // 读取Device ID
        SPI_CS_HIGH();  // 片选拉高,结束通信
    }

void W25Q64_WriteEnable(void)
{
    SPI_CS_LOW();  // 片选拉低,开始通信
    SPI_read_write_byte(0x06);  // 发送写使能指令
    SPI_CS_HIGH();  // 片选拉高,结束通信
}

void W25Q64_WriteDisable(void)
{
    SPI_CS_LOW();  // 片选拉低,开始通信
    SPI_read_write_byte(0x04);  // 发送写禁止指令
    SPI_CS_HIGH();  // 片选拉高,结束通信
}

void W25Q64_ReadStatusReg(uint8_t * StatusReg)//读取状态寄存器
{
    SPI_CS_LOW();  // 片选拉低,开始通信
    SPI_read_write_byte(0x05);  // 发送读取状态寄存器指令
    *StatusReg = SPI_read_write_byte(0x00);  // 读取状态寄存器
    SPI_CS_HIGH();  // 片选拉高,结束通信
}

void W25Q64_WriteStatusReg(uint8_t StatusReg)//写状态寄存器
{
    SPI_CS_LOW();  // 片选拉低,开始通信
    SPI_read_write_byte(0x01);  // 发送写状态寄存器指令
    SPI_read_write_byte(StatusReg);  // 写入状态寄存器
    SPI_CS_HIGH();  // 片选拉高,结束通信
}
void W25Q64_ReadData(uint8_t * Data, uint32_t Address, uint32_t Size)//读取数据
{
    uint8_t i;
    SPI_CS_LOW();  // 片选拉低,开始通信
    SPI_read_write_byte(0x03);  // 发送读取数据指令
    SPI_read_write_byte((Address >> 16) & 0xFF);  // 发送地址高8位
    SPI_read_write_byte((Address >> 8) & 0xFF);  // 发送地址中8位
    SPI_read_write_byte(Address & 0xFF);  // 发送地址低8位
    for (i = 0; i < Size; i++)  // 读取数据
    {
        Data[i] = SPI_read_write_byte(0x00);  // 读取数据
    }
    SPI_CS_HIGH();  // 片选拉高,结束通信 
}

void W25Q64_WriteData(uint8_t * Data, uint32_t Address, uint32_t Size)//写入数据
{
    uint8_t i;
    W25Q64_WriteEnable();  // 写使能
    SPI_CS_LOW();  // 片选拉低,开始通信 
    SPI_read_write_byte(0x02);  // 发送写数据指令
    SPI_read_write_byte((Address >> 16) & 0xFF);  // 发送地址高8位
    SPI_read_write_byte((Address >> 8) & 0xFF);  // 发送地址中8位
    SPI_read_write_byte(Address & 0xFF);  // 发送地址低8位
    for (i = 0; i < Size; i++)  // 写入数据
    {
        SPI_read_write_byte(Data[i]);  // 写入数据
    }
    SPI_CS_HIGH();  // 片选拉高,结束通信

    // 轮询状态寄存器,等待写操作完成
    uint8_t status;
    do {
        W25Q64_ReadStatusReg(&status);
    } while (status & 0x01); // 当状态寄存器的第0位为1时,表示芯片正在忙
}

void W25Q64_EraseSector(uint32_t Address)//擦除扇区
{
    W25Q64_WriteEnable();  // 写使能
    SPI_CS_LOW();  // 片选拉低,开始通信
    SPI_read_write_byte(0x20);  // 发送扇区擦除指令
    SPI_read_write_byte((Address >> 16) & 0xFF);  // 发送地址高8位
    SPI_read_write_byte((Address >> 8) & 0xFF);  // 发送地址中8位
    SPI_read_write_byte(Address & 0xFF);  // 发送地址低8位
    SPI_CS_HIGH();  // 片选拉高,结束通信

    // 轮询状态寄存器,等待擦除完成
    uint8_t status;
    do {
        W25Q64_ReadStatusReg(&status);
    } while (status & 0x01); // 当状态寄存器的第0位为1时,表示芯片正在忙
}

void W25Q64_EraseBlock(uint32_t Address)//擦除块
{
    W25Q64_WriteEnable();  // 写使能
    SPI_CS_LOW();  // 片选拉低,开始通信
    SPI_read_write_byte(0xD8);  // 发送块擦除指令
    SPI_read_write_byte((Address >> 16) & 0xFF);  // 发送地址高8位
    SPI_read_write_byte((Address >> 8) & 0xFF);  // 发送地址中8位
    SPI_read_write_byte(Address & 0xFF);  // 发送地址低8位
    SPI_CS_HIGH();  // 片选拉高,结束通信
    HAL_Delay(50);  // 等待擦除完成  
}
void W25Q64_EraseChip(void)//擦除芯片
{
    W25Q64_WriteEnable();  // 写使能
    SPI_CS_LOW();  // 片选拉低,开始通信
    SPI_read_write_byte(0xC7);  // 发送芯片擦除指令
    SPI_CS_HIGH();  // 片选拉高,结束通信
    HAL_Delay(50);  // 等待擦除完成   
}




void W25Q64_Verify(void) {
    //这个是测试程序
    uint8_t ManufacturerID;
    uint16_t DeviceID;
    uint8_t writeData[] = {0x11, 0x22, 0x33, 0x44};
    uint8_t readData[4];
    uint32_t address = 0x000000;
    uint8_t status;
    int i;

    // 初始化 W25Q64
    W25Q64_Init();

    // 读取芯片 ID
    get_w25q64_id(&ManufacturerID, &DeviceID);
    printf("Manufacturer ID: 0x%02X, Device ID: 0x%04X\n", ManufacturerID, DeviceID);

    // 擦除扇区
    W25Q64_EraseSector(address);

    // 读取状态寄存器
    W25Q64_ReadStatusReg(&status);
    printf("Status Register after sector erase: 0x%02X\n", status);

    // 写入数据
    W25Q64_WriteData(writeData, address, sizeof(writeData));
    
    // 读取状态寄存器
    W25Q64_ReadStatusReg(&status);
    printf("Status Register after write data: 0x%02X\n", status);

    // 读取数据
    W25Q64_ReadData(readData, address, sizeof(readData));

    // 验证数据
    printf("Read data: ");
    for (i = 0; i < sizeof(readData); i++) {
        printf("0x%02X ", readData[i]);
    }
    printf("\n");

    if (memcmp(writeData, readData, sizeof(writeData)) == 0) {
        printf("Data verification passed!\n");
    } else {
        printf("Data verification failed!\n");
    }
}

w25164.h

#ifndef W25Q64_H
#define W25Q64_H

#include "main.h"

void W25Q64_Init(void);
void get_w25q64_id(uint8_t * ManufacturerID, uint16_t * DeviceID );//读取ID
void W25Q64_WriteEnable(void);//写使能
void W25Q64_WriteDisable(void);//写禁止
void W25Q64_ReadStatusReg(uint8_t * StatusReg);//读取状态寄存器
void W25Q64_WriteStatusReg(uint8_t StatusReg);//写状态寄存器
void W25Q64_ReadData(uint8_t * Data, uint32_t Address, uint32_t Size);//读取数据
void W25Q64_WriteData(uint8_t * Data, uint32_t Address, uint32_t Size);//写数据
void W25Q64_EraseSector(uint32_t Address);//擦除扇区
void W25Q64_EraseBlock(uint32_t Address);//擦除块
void W25Q64_Verify(void);
#endif

调用测试程序的结果

可以发现成功实现读写

    一般我们使用这个模块主要是存储变量数据,可以直接参考我 我之前的文章

    https://blog.csdn.net/m0_74211379/article/details/146343170?fromshare=blogdetail&sharetype=blogdetail&sharerId=146343170&sharerefer=PC&sharesource=m0_74211379&sharefrom=from_link

    使用模块常见问题

         1.如果扇区和块使用超了会怎么样

            

    • 数据覆盖:W25Q64 芯片的存储容量是固定的,当扇区和块被全部使用后,若继续写入新数据,会覆盖之前存储在芯片中的旧数据。例如,在对一个已经写满数据的扇区再次写入数据时,新数据会直接覆盖该扇区原有的数据,导致旧数据丢失。
    • 写入失败:部分系统可能会对存储操作进行检查,当检测到没有可用的扇区或块时,会拒绝新的数据写入请求,并返回错误信息,告知用户存储已满无法写入。

            2.如果给定的地址超了会怎么样

            W25Q64 芯片本身不会对超出有效地址范围的输入进行检查和提示。当你发送一个超出块数量的地址(如对应块号 129 的地址)进行擦除、写入等操作时,芯片可能会忽略这个非法地址,不执行相应操作,或者将操作映射到内部合法的地址空间,但这并非预期行为,可能导致数据混乱。

     硬件SPI驱动

    基本的方式

    软件模拟时序结束了,接下来我们来弄硬件SPI

    首先是在Cubemx这样配置

    注意那个速度要在几M,不要太快,之前我68M直接卡死

    然后编写框架代码,其实使用硬件的一个函数就够了,我们只需要进行简单封装,就能像软件那样调用了。

    HAL_SPI_TransmitReceive(&hspi1, &data, &rx_data, 1,100);  // 发送数据并接收 

    就是这一个函数

    uint8_t hal_SPI_txrx(uint8_t data) {
        uint8_t rx_data = 0;
        HAL_SPI_TransmitReceive(&hspi1, &data, &rx_data, 1,100);  // 发送数据并接收 
        return rx_data;  // 返回接收到的数据
    }

    封装成我们软件SPI的样子

    void GET_W25Q64_JEDEC_ID(uint8_t *manufacturer_id, uint16_t *device_id) 
    {
        CS_LOW();  // 拉低片选信号,选中 W25Q64
        hal_SPI_txrx(0x9F);  // 发送JEDEC ID命令
        *manufacturer_id = hal_SPI_txrx(0x00);  // 接收制造商ID
        *device_id = (hal_SPI_txrx(0x00) << 8) | hal_SPI_txrx(0x00);  // 接收设备ID
        // 组合设备ID的高8位和低8位
        CS_HIGH();  // 拉高片选信号,取消选中 W25Q64
    }

    获取ID函数

    后面也是成功获取

    完整代码

    hal_w25q64.c

    #include "hal_w25q64.h"
    #include "gpio.h"  // 假设GPIO操作头文件
    #include "spi.h"
    
    
    uint8_t hal_SPI_txrx(uint8_t data) {
        uint8_t rx_data = 0;
        HAL_SPI_TransmitReceive(&hspi1, &data, &rx_data, 1,100);  // 发送数据并接收 
        return rx_data;  // 返回接收到的数据
    }
    
    void GET_W25Q64_JEDEC_ID(uint8_t *manufacturer_id, uint16_t *device_id) 
    {
        CS_LOW();  // 拉低片选信号,选中 W25Q64
        hal_SPI_txrx(0x9F);  // 发送JEDEC ID命令
        *manufacturer_id = hal_SPI_txrx(0x00);  // 接收制造商ID
        *device_id = (hal_SPI_txrx(0x00) << 8) | hal_SPI_txrx(0x00);  // 接收设备ID
        // 组合设备ID的高8位和低8位
        CS_HIGH();  // 拉高片选信号,取消选中 W25Q64
    }
    
    // 其他函数的实现可以参考上面的代码

    hal_w25q64.h

    #ifndef HAL_W25Q64_H
    #define HAL_W25Q64_H
    
    #include "main.h"
    
    #define CS_PIN GPIO_PIN_4 // 片选引脚
    #define CS_PORT GPIOA // 片选引脚所在的 GPIO 端口
    // 片选引脚控制函数
    #define CS_LOW() HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET) // 片选低电平
    #define CS_HIGH() HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET) // 片选高电平
    
    // 函数声明
    uint8_t hal_SPI_txrx(uint8_t data); // SPI 数据发送和接收函数
    
    void GET_W25Q64_JEDEC_ID(uint8_t *manufacturer_id, uint16_t *device_id) ; // 获取 W25Q64 的 JEDEC ID
    #endif /* HAL_W25Q64_H */

    然后如果要使用中断和DMA的方式,其实也是一样的道理

    中断的方式

    // 中断服务函数
    void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
    {
        // 处理发送完成事件
    }
    
    void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
    {
        // 处理接收完成事件
    }
    
    // 启动传输
    HAL_SPI_Transmit_IT(&hspi1, tx_buffer, tx_length);
    HAL_SPI_Receive_IT(&hspi1, rx_buffer, rx_length);

    DMA的方式

    // 启动 DMA 传输
    HAL_SPI_Transmit_DMA(&hspi1, tx_buffer, tx_length);
    HAL_SPI_Receive_DMA(&hspi1, rx_buffer, rx_length);
    
    // DMA 传输完成回调函数
    void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
    {
        // 处理发送完成事件
    }
    
    void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
    {
        // 处理接收完成事件
    }

    这样子我们软件和硬件SPI就弄完了

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

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

    相关文章

    【软考】论devops在企业信息系统开发中的应用

    摘要&#xff1a; 随着互联网的不断发展&#xff0c;各行各业都在建设自己的企业信息系统&#xff0c;而随着业务的不断升级和复杂化&#xff0c;系统的更新迭代速度越来越快&#xff0c;系统也越来越复杂。对于信息系统开发者&#xff0c;架构师&#xff0c;管理者&#xff0c…

    生物化学笔记:医学免疫学原理22 肿瘤及肿瘤治疗

    肿瘤及肿瘤治疗 免疫疗法 CAR-T细胞介绍

    JVM考古现场(二十二):降维打击·用二向箔优化内存模型

    "警报&#xff01;三维堆内存正在经历二维化坍缩&#xff01;" 我腰间的玄铁令突然震动&#xff0c;在蜀山剑派的量子剑阵中投射出诡异的曼德博分形——这是三体文明发动降维打击的铁证&#xff01; 楔子&#xff1a;二向箔奇点降临 昆仑镜监控日志&#xff1a; // …

    操作系统-PV

    &#x1f9e0; 背景&#xff1a;为什么会有 PV&#xff1f; 类比&#xff1a;内存&#xff08;生产者&#xff09; 和 CPU&#xff08;消费者&#xff09; 内存 / IO / 磁盘 / 网络下载 → 不断“生产数据” 例如&#xff1a;读取文件、下载视频、从数据库加载信息 CPU → 负…

    医院数据中心智能化数据上报与调数机制设计

    针对医院数据中心的智能化数据上报与调数机制设计,需兼顾数据安全性、效率性、合规性及智能化能力。以下为系统性设计方案,分为核心模块、技术架构和关键流程三部分: 一、核心模块设计 1. 数据上报模块 子模块功能描述多源接入层对接HIS/LIS/PACS/EMR等异构系统,支持API/E…

    【MATLAB代码例程】AOA与TOA结合的高精度平面地位,适用于四个基站的情况,附完整的代码

    本代码实现了一种基于到达角(AOA) 和到达时间(TOA) 的混合定位算法,适用于二维平面内移动或静止目标的定位。通过4个基站的协同测量,结合最小二乘法和几何解算,能够有效估计目标位置,并支持噪声模拟、误差分析和可视化输出。适用于室内定位、无人机导航、工业监测等场景…

    PC主板及CPU ID 信息、笔记本电脑唯一 MAC地址获取

    &#x1f947; 版权: 本文由【墨理学AI】原创首发、各位读者大大、敬请查阅、感谢三连 &#x1f389; 声明: 作为全网 AI 领域 干货最多的博主之一&#xff0c;❤️ 不负光阴不负卿 ❤️ 文章目录 PC主板及CPU ID 信息物理 MAC地址获取win11 新电脑 wmic 安装❤️ 欢迎一起学AI…

    C++17 信号量模拟实现

    C17 信号量模拟实现 一、实现原理 C17 标准库没有原生信号量(C20才有)&#xff0c;但可以通过 std::mutex std::condition_variable 模拟实现。以下是核心逻辑&#xff1a; #include <mutex> #include <condition_variable>class CountingSemaphore { private:…

    web后端语言中篇

    #作者&#xff1a;允砸儿 #日期&#xff1a;乙巳青蛇年 三月十八 笔者本来打算隔一天给它更完的&#xff0c;但是事情有点多这几天&#xff0c;实在是抱歉。废话不多说直接进入正题。 PHP流程控制语句 什么是流控:流程控制语句用于决定代码的执行顺序。 #注意流程控制语句…

    Spine-Leaf 与 传统三层架构:全面对比与解析

    本文将详细介绍Spine-Leaf架构&#xff0c;深入对比传统三层架构&#xff08;Core、Aggre、Access&#xff09;&#xff0c;并探讨其与Full-mesh网络和软件定义网络&#xff08;SDN&#xff09;的关联。通过通俗易懂的示例和数据中心网络分析&#xff0c;我将帮助您理解Spine-L…

    Vmware esxi 查看硬盘健康状况

    起因 硬盘掉盘 - - 使用自带的命令esxcli 列出所有硬盘 esxcli storage core device list[rootlocalhost:~] esxcli storage core device list t10.NVMe____INTEL_MEMPEK1W016GAL____________________PHBT83660BYP016D____00000001Display Name: Local NVMe Disk (t10.NVMe…

    4.黑马学习笔记-SpringMVC(P43-P47)

    1.SpringMVC简介 SpringMVC技术&#xff08;更少的代码&#xff0c;简便&#xff09;与servlet技术功能相同&#xff0c;属于web层开发技术。 SpringMVC是一种基于java实现MVC模型的轻量级web框架。 轻量级指的是&#xff08;内存占用比较低&#xff0c;运行效率高&#xff09;…

    【文件操作与IO】详细解析文件操作与IO (一)

    本篇博客给大家带来的是文件操作的知识点. &#x1f40e;文章专栏: JavaEE初阶 &#x1f680;若有问题 评论区见 ❤ 欢迎大家点赞 评论 收藏 分享 如果你不知道分享给谁,那就分享给薯条. 你们的支持是我不断创作的动力 . 王子,公主请阅&#x1f680; 要开心要快乐顺便进步 一. …

    PMP考试费能报销吗?报销流程是什么?

    最近也是到了6月和8月PMP考试的报名高峰期&#xff0c;后台有小伙伴最常问的问题就是&#xff0c;PMP考试费比较贵&#xff0c;能不能报销&#xff1f;报销流程是什么&#xff1f; 先给大家分享一下最新PMP报名消息和考试信息&#xff1a; 添加图片注释&#xff0c;不超过 140…

    学习threejs,使用EffectComposer后期处理组合器(采用RenderPass、GlitchPass渲染通道)

    &#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️THREE.EffectComposer 后期…

    docker部署springboot(eureka server)项目

    打jar包 使用maven&#xff1a; <build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><source>17</source><target>17&…

    第 7 期:DDPM 采样提速方案:从 DDPM 到 DDIM

    本期关键词:采样加速、DDIM 推导、可控性提升、伪逆过程、代码实战 前情回顾:DDPM 的采样瓶颈 在前几期中,我们构建了一个完整的 DDPM 生成流程。但是你可能已经发现: 生成一张图像太慢了!!! 原因是: DDPM 要在 T 个时间步中一步步地去噪,从 x_T → x_0。而通常 T 至…

    1panel第三方应用商店(本地商店)配置和使用

    文章目录 引言资源网站实战操作说明 引言 1Panel 提供了一个应用提交开发环境&#xff0c;开发者可以通过提交应用的方式将自己的应用推送到 1Panel 的应用商店中&#xff0c;供其他用户使用。由此衍生了一种本地应用商店的概念&#xff0c;用户可以自行编写应用配置并上传到自…

    七牛使用任务工作流对音频进行转码

    最近工作中有对音频转码的需求&#xff0c;比如 iOS 设备中对 ogg 格式的语音支持力度不够&#xff0c;那么可以讲ogg转码成mp3格式,下面来介绍一下&#xff0c;如果通过七牛&#xff0c;后端自行转码&#xff0c;不需要前端做任何事情。 假设我们存在一个音频的 url&#xff…

    基于springBoot+vue的PC 端学习系统(源码+lw+部署文档+讲解),源码可白嫖!

    摘要 随着我国经济的高速发展与人们生活水平的日益提高&#xff0c;人们对生活质量的追求也多种多样。尤其在人们生活节奏不断加快的当下&#xff0c;人们更趋向于足不出户解决生活上的问题&#xff0c;线上管理系统展现了其蓬勃生命力和广阔的前景。与此同时&#xff0c;在疫…