前言
SPI(Serial Peripheral Interface)通信协议是一种高速、全双工、同步的串行通信协议
本篇文章就使用W25Q64模块来学习SPI,包括软件SPI代码的编写,硬件SPI,中断SPI和DMA+SPI
SPI的应用场景和模块
!这里是抄AI的,主要是也要了解学了之后有什么用
- 存储设备:如闪存芯片(如 W25Q64 等)与微控制器的连接。SPI 接口可实现高速的数据读写,用于程序代码存储、数据存储等,支持频繁的擦除和写入操作,满足设备对数据存储和快速访问的需求。
- 传感器:许多传感器如温度传感器、压力传感器、加速度传感器等,采用 SPI 接口与主控芯片通信。传感器将采集到的模拟信号转换为数字信号后,通过 SPI 协议快速传输给主控芯片进行处理,以实现对物理量的精确测量和实时监测。
- 显示设备:SPI 常用于连接微控制器与液晶显示器(LCD)或有机发光二极管显示器(OLED)的驱动芯片。能够高效地传输图像数据和控制指令,实现显示屏的快速刷新和高分辨率显示,满足不同显示场景的需求。
- 音频设备:在音频编解码器、数字音频放大器等音频设备中,SPI 用于传输音频数据和控制信息。可以实现高质量的音频数据传输,支持多种音频格式和采样率,确保音频信号的准确还原和播放。
- 无线通信模块:如 Wi-Fi 模块、蓝牙模块等与主控芯片之间常通过 SPI 接口进行通信。SPI 协议能够满足无线通信模块与主控芯片之间高速数据传输的需求,实现设备间的短距离无线数据传输和通信。
- 现场可编程门阵列(FPGA)配置:在一些系统中,通过 SPI 接口将配置数据加载到 FPGA 中,实现对 FPGA 的功能配置和初始化。这种方式简单可靠,能够在系统启动时快速完成 FPGA 的配置,确保其正常工作。
SPI信号线
- 主设备出、从设备入(MOSI):主设备通过这条线向从设备传输数据,也被称为从设备输入线(SI 或 SDI)。连接设备DI引脚
- 主设备入、从设备出(MISO):从设备利用这条线向主设备发送数据,也叫从设备输出线(SO 或 SDO)。连接设备DO引脚
- 串行时钟(SCLK):由主设备产生的时钟信号,用于同步主从设备之间的数据传输,从设备根据 SCLK 的节奏来接收或发送数据。
- 从设备选择(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搜集到的资料,稍微简单了解下模块
- 基本参数
- 存储容量:容量为 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。
页地址、扇区地址、块地址
都是在驱动模块前需要了解的基本资料
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就弄完了