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

前言

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每秒。

cpp 复制代码
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 位用于页内偏移。例如,地址 0x0000000x0000FF 是第 0 页,0x0001000x0001FF 是第 1 页,依此类推。

2. 扇区(Sector)

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

3. 块(Block)

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

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

首先初始化和确定好引脚

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

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

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

hal_spi.h

cpp 复制代码
#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

cpp 复制代码
#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;  // 返回读取到的数据
}

初始化函数

cpp 复制代码
void W25Q64_Init(void)
{
    HAL_SPI_Init();  // 初始化SPI
    SPI_CS_HIGH();
    HAL_Delay(10);  // 延时10ms
}

获取ID函数

cpp 复制代码
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

cpp 复制代码
#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

cpp 复制代码
#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直接卡死

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

cpp 复制代码
HAL_SPI_TransmitReceive(&hspi1, &data, &rx_data, 1,100);  // 发送数据并接收 

就是这一个函数

cpp 复制代码
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的样子

cpp 复制代码
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

cpp 复制代码
#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

cpp 复制代码
#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的方式,其实也是一样的道理

中断的方式

cpp 复制代码
// 中断服务函数
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的方式

cpp 复制代码
// 启动 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就弄完了

相关推荐
电子艾号哲1 小时前
Vscode开发STM32标准库
ide·vscode·stm32
双叶8361 小时前
(51单片机)LCD显示数据存储(DS1302时钟模块教学)(LCD1602教程)(独立按键教程)(延时函数教程)(I2C总线认识)(AT24C02认识)
c语言·数据库·单片机·嵌入式硬件·mongodb·51单片机·nosql
wutianxin01 小时前
硬件测试项之电源纹波的测量和纹波的要求、纹波的抑制
嵌入式硬件·硬件工程
格里姆肖2 小时前
HAL库通过FATFS和SDIO+DMA写入SD卡数据错误
c语言·stm32·单片机
Vesan,2 小时前
无人机飞控运行在stm32上的RTOS实时操作系统上,而不是linux这种非实时操作系统的必要性
linux·stm32·无人机
嘿,二极管2 小时前
硬件工程师面试常见问题(4)
单片机·嵌入式硬件
哄娃睡觉2 小时前
独立ADC和MCU中ADC模块的区别
stm32
wenchm3 小时前
细说STM32单片机FreeRTOS任务管理API函数vTaskList()的使用方法
c语言·c++·stm32·单片机·嵌入式硬件
天天爱吃肉82184 小时前
新能源汽车零部件功率级测试方案搭建研究
单片机·嵌入式硬件·汽车·学习方法