SPI与W25Q通信
一、SPI与IIC对比
| 对比点 | SPI | IIC (I²C) |
|---|---|---|
| 线数 | 4根线(最常用) | 2根线 |
| 连接方式 | 主从式,多从少主 | 主从式,可多主多从 |
| 硬件开销 | 线多,资源占用高 | 线少,资源占用低 |
| 通信速率 | 高,常见几MHz~几十MHz | 相对低,标准模式100kHz,高速400kHz |
| 通信距离 | 一般适合板内或短距离 | 支持较长距离,几十厘米~米级 |
| 传输模式 | 全双工(同时收发) | 半双工(收或发) |
| 地址方式 | 不用地址,用片选信号区分从机 | 需分配地址,每个从机唯一地址 |
| 简单性 | 简单,时序易分析 | 时序复杂,需要应答信号 |
| 应用场景 | 高速、短距离、多数据量 | 简单外设挂载、慢速控制 |
| 常见设备 | Flash、SD卡、ADC、DAC | EEPROM、RTC、温湿度传感器 |
二、SPI是什么?
SPI(Serial Peripheral Interface,串行外设接口)
是一种高速、全双工、同步的串行通信总线协议,由Motorola公司提出,常用于MCU与各种外设(如Flash、AD/DA芯片、传感器等)间的短距离高速通信。
三、SPI物理架构
1. 基本硬件连线
通常有四根信号线(标准全功能):
-
SCLK(Serial Clock,时钟):由主机产生的时钟信号。
-
MOSI(Master Out Slave In):主机输出、从机输入的数据线。
-
MISO(Master In Slave Out):主机输入、从机输出的数据线。
-
SS/CS(Slave Select/Chip Select,片选):主机用来选择某一个从机,低电平有效。
基本结构图:

每个从机有自己的CS线,主机只拉低要通讯的从机对应CS线。
四、SPI工作原理
1. 主从模式
-
主机:控制时钟(SCLK),拉低CS选择从机,发起通信。
-
从机:被动响应主机,CS被拉低后才参与数据传输。
2. 数据传输
-
同步时序:数据在SCLK时钟边沿发送和采样。
-
全双工:发送和接收可以同时进行(MOSI、MISO独立)。
-
数据长度:一般8位一帧,也可设为16位、32位等。
3. 通信流程(简述)
-
主机拉低某个从机的CS。
-
主机产生SCLK,同时在MOSI上发送数据。
-
从机在MISO线上发送数据(回送)。
-
完成一帧后,主机拉高CS。
五、SPI工作模式
SPI有四种工作模式(Mode 0~3),由CPOL(时钟极性)和CPHA(时钟相位)两个参数确定:
| 模式 | CPOL | CPHA | SCLK空闲状态 | 数据采样/输出时刻 |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低电平 | 第一个时钟沿采样 |
| Mode 1 | 0 | 1 | 低电平 | 第二个时钟沿采样 |
| Mode 2 | 1 | 0 | 高电平 | 第一个时钟沿采样 |
| Mode 3 | 1 | 1 | 高电平 | 第二个时钟沿采样 |
CPOL = 0/1: 决定SCLK空闲时是低电平还是高电平
CPHA = 0/1: 决定数据在第一个/第二个时钟沿采样
示例:
-
STM32默认SPI是Mode 0(CPOL=0, CPHA=0)。
-
不同外设工作模式要和主机一致,否则数据会出错。
六、总结与应用建议
-
SPI适合高速、大数据量、短距离、对时序有较高要求的场合。
-
IIC适合简化硬件连线、多设备、中低速的场景。
-
SPI结构简单、效率高,但片选线随从机数量线性增加。
-
STM32、51、Arduino等平台都广泛支持SPI和IIC,通常用硬件外设模块+软件库函数实现。
库函数
SPI在不同MCU平台(如STM32、51、Arduino)下都有自己的库函数。
一、STM32 HAL库 SPI常用函数
头文件:#include "stm32f1xx_hal_spi.h"
(具体视芯片系列而定,F4系列为stm32f4xx_hal_spi.h)
1. 初始化相关
-
HAL_SPI_Init(&hspiX)
初始化SPI外设,hspiX为SPI句柄(配置见CubeMX或手动初始化)。
2. 发送接收函数
同步(阻塞式,常用)
-
HAL_SPI_Transmit(&hspiX, uint8_t *pData, uint16_t Size, uint32_t Timeout);
仅发送(主/从均可) -
HAL_SPI_Receive(&hspiX, uint8_t *pData, uint16_t Size, uint32_t Timeout);
仅接收 -
HAL_SPI_TransmitReceive(&hspiX, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout);
同时发送和接收(全双工)
非阻塞(中断方式)
-
HAL_SPI_Transmit_IT(&hspiX, ...) -
HAL_SPI_Receive_IT(&hspiX, ...) -
HAL_SPI_TransmitReceive_IT(&hspiX, ...)
DMA方式(大数据量、效率高)
-
HAL_SPI_Transmit_DMA(&hspiX, ...) -
HAL_SPI_Receive_DMA(&hspiX, ...) -
HAL_SPI_TransmitReceive_DMA(&hspiX, ...)
3. 其他辅助函数
-
HAL_SPI_DeInit(&hspiX)// 反初始化SPI -
HAL_SPI_GetState(&hspiX)// 获取SPI状态 -
HAL_SPI_MspInit(&hspiX)// 底层硬件初始化,通常用户重载 -
__HAL_SPI_ENABLE(&hspiX)// 使能SPI -
__HAL_SPI_DISABLE(&hspiX)// 禁用SPI
二、51单片机常用SPI操作函数(C51常见写法)
通常手动操作寄存器,但有些库(如STC-ISP带的SPI驱动库)会封装函数:
-
SPI_Init();// 初始化SPI -
SPI_SendByte(uint8_t dat);// 发送一个字节,返回接收到的字节 -
SPI_CS_Low();// 片选拉低 -
SPI_CS_High();// 片选拉高
注意:51平台常常直接通过寄存器(如SPDAT, SPSTAT, SPCON等)读写实现SPI通信。
三、Arduino平台SPI常用函数
头文件:#include <SPI.h>
-
SPI.begin();// 初始化SPI总线 -
SPI.beginTransaction(SPISettings(clock, bitOrder, mode));// 设置SPI参数 -
SPI.transfer(uint8_t data);// 发送并接收一个字节 -
SPI.transfer(buffer, size);// 发送并接收多个字节 -
SPI.endTransaction();// 结束SPI事务 -
SPI.end();// 关闭SPI
片选CS的控制通常通过digitalWrite完成。
四、常用代码片段举例(以STM32 HAL为例)
// 发送并接收一个字节(同步阻塞式)
uint8_t SPI_RW(uint8_t dat)
{
uint8_t rx;
HAL_SPI_TransmitReceive(&hspi1, &dat, &rx, 1, 100);
return rx;
}
发送多字节数据(同步发送)
uint8_t tx_buf[10] = {0xAA, 0x55, ...};
HAL_SPI_Transmit(&hspi1, tx_buf, 10, 100);
发送+接收多字节数据(全双工)
uint8_t rx_buf[10];
HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, 10, 100);
五、注意事项
-
STM32 HAL函数名中IT表示中断,DMA表示DMA,普通函数为阻塞式(最常用)。
-
使用前需CubeMX或手动配置SPI外设和GPIO,参数要匹配外设要求(时序、模式)。
-
Arduino和C51常需自己控制片选引脚(digitalWrite/IO口),STM32通常用片选引脚配合手动拉低/拉高。
标准外设库
一、STM32标准外设库(StdPeriph)SPI常用函数
1. SPI初始化相关
-
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
SPI外设初始化。需要先配置好SPI_InitTypeDef结构体参数。 -
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);
将SPI_InitTypeDef结构体参数初始化为默认值(全双工主机模式、8位数据、低极性低相位等)。 -
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
使能/失能SPI外设(ENABLE/DISABLE)。
2. 数据收发
-
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
发送数据(8位或16位,看配置)。 -
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);
接收数据。
3. 状态查询
-
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
查询状态标志(如发送缓冲区空、接收缓冲区非空、忙等)。 -
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
清除指定标志位。
4. 中断操作
-
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint16_t SPI_I2S_IT, FunctionalState NewState);
开启/关闭指定SPI中断。 -
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_IT);
查询中断状态。 -
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint16_t SPI_I2S_IT);
清除中断挂起位。
5. 外设使能/禁用
-
void SPI_NSSInternalSoftwareConfig(SPI_TypeDef* SPIx, uint16_t SPI_NSSInternalSoft);
选择片选(NSS)软件管理模式。 -
void SPI_SSOutputCmd(SPI_TypeDef* SPIx, FunctionalState NewState);
控制主机模式下NSS输出。
二、常用收发流程举例
1. 主机发送与接收一个字节
// 发送一个字节并接收返回的数据
uint8_t SPI_RW(SPI_TypeDef* SPIx, uint8_t data)
{
// 等待发送缓冲区空
while (SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_TXE) == RESET);
// 发送数据
SPI_I2S_SendData(SPIx, data);
// 等待接收缓冲区非空
while (SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_RXNE) == RESET);
// 返回接收到的数据
return SPI_I2S_ReceiveData(SPIx);
}
三、SPI常用相关结构体
-
SPI_InitTypeDef
配置结构体,包含SPI模式、数据宽度、极性相位等参数。
四、常用标志位
-
SPI_I2S_FLAG_TXE// 发送缓冲区空 -
SPI_I2S_FLAG_RXNE// 接收缓冲区非空 -
SPI_I2S_FLAG_BSY// SPI忙标志 -
SPI_I2S_FLAG_OVR// 溢出错误
五、典型初始化流程(伪代码)
SPI_InitTypeDef SPI_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
// 配置相关参数
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure);
// 使能SPI
SPI_Cmd(SPI1, ENABLE);
六、实用小结
-
STM32标准外设库(StdPeriph)下,所有SPI操作都以
SPI_开头。 -
最常用的是初始化、数据收发(
SendData/ReceiveData)、状态查询(GetFlagStatus)、使能SPI。 -
寄存器操作和底层细节可通过库函数轻松实现,大大减少手写寄存器代码的出错率。
-
如果用的是HAL库,那就以
HAL_SPI_开头,结构和思路类似。
W25Q系列
W25Q系列(如W25Q16、W25Q32、W25Q64、W25Q128等)SPI NOR Flash的型号、参数、地址、模块引脚、存储架构、常用指令和状态寄存器等知识。
一、W25Q系列常见型号与容量
| 型号 | 容量 | 存储空间(字节) | 设备ID/厂家ID |
|---|---|---|---|
| W25Q16 | 16M bit | 2MB | 0xEF15 |
| W25Q32 | 32M bit | 4MB | 0xEF16 |
| W25Q64 | 64M bit | 8MB | 0xEF17 |
| W25Q128 | 128M bit | 16MB | 0xEF18 |
| W25Q256 | 256M bit | 32MB | 0xEF19 |
-
厂家ID(JEDEC ID):
0xEF(Winbond厂商ID) -
设备ID由
0xEF + xx组成(后面的xx随容量而变)
二、SPI设备地址说明
-
SPI Flash没有IIC那种“设备地址”概念,用片选引脚CS来选中设备,通常直接拉低CS即可通信。
-
多个SPI Flash并联时,每个有独立的CS片选线。
三、W25Q128模块参数及引脚介绍
1. 主要参数(以W25Q128为例)
-
容量:128Mbit(16MB)
-
存储结构:256字节/页,16页/扇区,16扇区/块,256块/芯片
-
电源电压:2.7V~3.6V
-
工作温度:-40~+85℃
-
SPI接口速率:最高104MHz
2. 模块引脚说明(标准6脚封装/SOP8)
| 引脚名 | 功能 | 通用MCU接法 |
|---|---|---|
| CS (或/SS) | 片选(低有效) | 任意GPIO |
| CLK (SCK) | SPI时钟 | SPI SCK |
| MOSI (DI) | 主机数据输出 | SPI MOSI |
| MISO (DO) | 主机数据输入 | SPI MISO |
| WP | 写保护(低有效) | 拉高或MCU控制 |
| HOLD | 暂停(低有效) | 拉高或MCU控制 |
| VCC | 电源 | 3.3V |
| GND | 地 | GND |
通常WP和HOLD脚可以直接上拉到VCC(不用暂停和写保护时)。
四、W25Q128存储架构
-
存储容量:16MB
-
每页(Page):256字节
-
每扇区(Sector):4KB = 16页
-
每块(Block):64KB = 16扇区
-
芯片包含:256个Block,4096个Sector,65536个Page
地址空间:24位地址,支持0x000000 ~ 0xFFFFFF(共16MB)
结构示意:
芯片
└─ 256 Block(块)
└─ 16 Sector/Block
└─ 16 Page/Sector
└─ 256 Byte/Page
五、W25Q128常用指令集
1. 基本指令(8位命令码)
| 功能 | 指令码(Hex) | 说明 |
|---|---|---|
| 读数据 | 0x03 | 低速读(标准读) |
| 高速读 | 0x0B | 支持更高SCK,需加1个dummy字节 |
| 页编程 | 0x02 | 写入数据(256字节/页) |
| 扇区擦除 | 0x20 | 4KB擦除 |
| 块擦除64KB | 0xD8 | 64KB擦除 |
| 芯片擦除 | 0xC7或0x60 | 整片全部擦除 |
| 写使能 | 0x06 | 写前须发此命令 |
| 写禁止 | 0x04 | 禁止写入 |
| 读状态寄存器1 | 0x05 | 查看忙/写保护等状态 |
| 写状态寄存器1 | 0x01 | 修改写保护等状态 |
| 读设备ID | 0x90, 0x9F | 读取JEDEC ID等 |
| 使能4字节地址 | 0xB7 | 用于大容量芯片 |
| 退出4字节地址 | 0xE9 |
六、W25Q128状态寄存器(Status Register)
W25Q128有3个状态寄存器(Status Register 1/2/3),最常用的是SR1。
1. 状态寄存器1(SR1, 地址0x05)
| Bit | 名称 | 说明 |
|---|---|---|
| 7 | SRP0 | 状态寄存器保护 |
| 6 | SEC | 扇区保护 |
| 5 | TB | 块保护区选择 |
| 4 | BP2 | 写保护 |
| 3 | BP1 | 写保护 |
| 2 | BP0 | 写保护 |
| 1 | WEL | 写使能锁定位(1=可写) |
| 0 | BUSY | 忙状态(1=忙,0=空闲) |
-
BUSY(bit0): 1=忙(擦写/编程进行中),0=空闲可操作
-
WEL(bit1): 写使能锁(1=写使能,0=禁止写)
-
BP0BP2(bit24): 区域写保护设置
-
TB、SEC、SRP0:更高阶的保护管理
2. 状态寄存器2(0x35)
主要用于四线制(QUAD)SPI模式、页保护等
3. 状态寄存器3(0x15)
主要扩展存储管理等
七、W25Q128模块应用建议
-
通信方式:SPI,典型速率0~50MHz
-
工作电压3.3V
-
软件控制CS片选,一组SPI可带多个Flash
-
常见MCU/开发板直接支持,无需外部驱动
-
擦写前要写使能、等待BUSY位清零
SPI如何与W25Q通信
SPI与W25Q128(或同类W25Q系列Flash)通信的原理、基本操作步骤,并结合STM32标准库代码举例说明
一、SPI与W25Q128通信原理
1. 通信接口
-
W25Q128是标准SPI外设,主机(如STM32)通过SCK、MOSI、MISO、CS这四线与Flash通信。
-
片选CS拉低,主机发命令→发地址→收发数据→CS拉高。
-
数据都是先发命令字节,再发三字节(或四字节)地址,最后是数据区。
2. 操作流程
常用的基本操作流程为:
-
读取ID:拉低CS,发
0x9F命令,收回3个字节。 -
读数据:拉低CS,发
0x03,发3字节地址,接收N字节数据。 -
写数据:拉低CS,发
0x06写使能,CS拉高,
再拉低CS,发0x02,发3字节地址,发送N字节数据(最大256字节一页)。 -
擦除:
0x20扇区擦除(4K),需先0x06写使能。 -
其他操作如读/写状态寄存器,读忙状态等。
二、W25Q128基本操作代码实现(以STM32标准库为例)
1. SPI读写一个字节函数
// SPI发送并接收一个字节
uint8_t SPI_RW(SPI_TypeDef* SPIx, uint8_t data)
{
// 等待发送缓冲区空
while (SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_TXE) == RESET);
SPI_I2S_SendData(SPIx, data);
// 等待接收缓冲区非空
while (SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_RXNE) == RESET);
return SPI_I2S_ReceiveData(SPIx);
}
2. 拉低/拉高CS片选(假设CS用GPIO控制)
#define W25Q_CS_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_4)
#define W25Q_CS_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_4)
实际根据你接的CS引脚调整GPIO端口与引脚。
3. 读取W25Q128芯片ID
uint32_t W25Q_ReadID(void)
{
uint32_t id = 0;
W25Q_CS_LOW();
SPI_RW(SPI1, 0x9F); // 发送读ID命令
id |= SPI_RW(SPI1, 0x00) << 16; // Manufacturer
id |= SPI_RW(SPI1, 0x00) << 8; // Memory type
id |= SPI_RW(SPI1, 0x00); // Capacity
W25Q_CS_HIGH();
return id;
}
返回值如0xEF18为W25Q128。
操作流程
-
CS拉低,选中芯片
-
发送命令0x9F(JEDEC ID读命令)
-
连续读取3个字节(厂商ID、设备类型、容量)
-
CS拉高,结束
4. 读数据(标准读命令0x03)
void W25Q_ReadData(uint8_t* buf, uint32_t addr, uint16_t len)
{
W25Q_CS_LOW();
SPI_RW(SPI1, 0x03); // 读数据命令
SPI_RW(SPI1, (addr >> 16) & 0xFF); // 地址高字节
SPI_RW(SPI1, (addr >> 8) & 0xFF); // 地址中字节
SPI_RW(SPI1, addr & 0xFF); // 地址低字节
for(uint16_t i = 0; i < len; i++)
buf[i] = SPI_RW(SPI1, 0xFF); // 读数据,发空数据收实数据
W25Q_CS_HIGH();
}
操作流程
-
CS拉低
-
发送命令0x03(标准读)
-
发送3字节地址(高→低字节)
-
连续读取数据N字节(读多少收多少)
-
CS拉高
5. 写使能(所有写/擦都需先发此命令)
void W25Q_WriteEnable(void)
{
W25Q_CS_LOW();
SPI_RW(SPI1, 0x06); // 写使能命令
W25Q_CS_HIGH();
}
操作流程
-
CS拉低
-
发送命令0x06
-
CS拉高
6. 页编程(0x02)
void W25Q_PageProgram(uint8_t* buf, uint32_t addr, uint16_t len)
{
W25Q_WriteEnable(); // 先写使能
W25Q_CS_LOW();
SPI_RW(SPI1, 0x02); // 页编程命令
SPI_RW(SPI1, (addr >> 16) & 0xFF);
SPI_RW(SPI1, (addr >> 8) & 0xFF);
SPI_RW(SPI1, addr & 0xFF);
for(uint16_t i = 0; i < len; i++)
SPI_RW(SPI1, buf[i]);
W25Q_CS_HIGH();
}
-
注意:
len不能超过256,且写前目标区必须擦除(否则会变成0/1不可逆)。
操作流程
-
写使能(见上面)
-
CS拉低
-
发送命令0x02(页编程)
-
发送3字节目标地址(写入起始地址)
-
连续写入N个字节数据(最大256字节,不能跨页)
-
CS拉高
-
轮询BUSY(等待写操作完成)
7. 擦除扇区(0x20)
void W25Q_SectorErase(uint32_t addr)
{
W25Q_WriteEnable();
W25Q_CS_LOW();
SPI_RW(SPI1, 0x20); // 扇区擦除命令
SPI_RW(SPI1, (addr >> 16) & 0xFF);
SPI_RW(SPI1, (addr >> 8) & 0xFF);
SPI_RW(SPI1, addr & 0xFF);
W25Q_CS_HIGH();
}
操作流程
-
写使能
-
CS拉低
-
发送命令0x20(擦除4K扇区)
-
发送3字节目标地址(扇区内任意地址)
-
CS拉高
-
轮询BUSY(等待擦除完成)
8. 等待空闲(查询BUSY)
uint8_t W25Q_ReadSR(void)
{
uint8_t status;
W25Q_CS_LOW();
SPI_RW(SPI1, 0x05); // 读状态寄存器命令
status = SPI_RW(SPI1, 0xFF);
W25Q_CS_HIGH();
return status;
}
void W25Q_WaitBusy(void)
{
while(W25Q_ReadSR() & 0x01); // BUSY=1表示忙
}
操作流程
-
CS拉低
-
发送命令0x05(读状态寄存器1)
-
读取一个字节(bit0为BUSY,1=忙)
-
CS拉高
三、完整的写数据典型流程
-
擦除扇区
-
等待空闲
-
页编程写入
-
等待空闲
-
读数据校验
四、代码小结与注意
-
每次写/擦都需先
写使能。 -
写操作必须以页为单位,不能跨页。
-
写入前要擦除目标区,否则会出现写入不成功(只能1变0,不能0变1)。
-
擦写操作时间较长,须等待
BUSY位为0后再进行下一步。
五、实用参考(最简测试)
// 写入数据
uint8_t buf[256];
for(int i=0;i<256;i++) buf[i]=i;
W25Q_SectorErase(0x000000); // 擦除首个扇区
W25Q_WaitBusy();
W25Q_PageProgram(buf, 0x000000, 256);
W25Q_WaitBusy();
// 读取并校验
uint8_t rbuf[256];
W25Q_ReadData(rbuf, 0x000000, 256);
// 检查rbuf与buf是否一致
总结要点
-
所有**命令操作都是"CS拉低→发命令→发地址→数据收发→CS拉高"**的顺序
-
写/擦都需先写使能,并轮询BUSY位等待操作完成
-
写操作必须页对齐、扇区先擦除
HAL库
w25q128.c
#include "w25q128.h"
// 全局SPI句柄
SPI_HandleTypeDef spi_handle = {0};
/*
* @brief 初始化SPI1外设与相关GPIO
* @note SPI参数可根据实际需求进行选择和优化,具体说明见下方每行注释
*/
void w25q128_spi_init(void)
{
spi_handle.Instance = SPI1; // 选择SPI1(可选:SPI1/SPI2/SPI3)
spi_handle.Init.Mode = SPI_MODE_MASTER; // 主机模式(SPI_MODE_MASTER/SPI_MODE_SLAVE)
spi_handle.Init.Direction = SPI_DIRECTION_2LINES; // 全双工(2LINES:MOSI+MISO,1LINE仅单向)
spi_handle.Init.DataSize = SPI_DATASIZE_8BIT; // 每帧8位(可选:8/16位,Flash一般用8位)
spi_handle.Init.CLKPolarity = SPI_POLARITY_LOW; // 时钟空闲低电平CPOL=0(LOW/高CPOL=1)
spi_handle.Init.CLKPhase = SPI_PHASE_1EDGE; // 第1个时钟沿采样(CPHA=0,SPI标准模式0)
spi_handle.Init.NSS = SPI_NSS_SOFT; // 软件NSS,自己用GPIO控制片选(SOFT/HARD)
spi_handle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; // 波特率分频16,实际SPI时钟=主频/16。可选2/4/8/16/32/64/128/256(根据芯片最大时钟和Flash规格选用,W25Q128最高104MHz,但STM32 SPI1建议不超过主频/2)
spi_handle.Init.FirstBit = SPI_FIRSTBIT_MSB; // 先发高位(MSB/LSB,W25Q系列要求MSB先行)
spi_handle.Init.TIMode = SPI_TIMODE_DISABLE; // 禁用TI模式(一般不用)
spi_handle.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; // 禁用CRC校验
spi_handle.Init.CRCPolynomial = 7; // CRC多项式,禁用CRC时无实际意义
HAL_SPI_Init(&spi_handle);
}
/*
* @brief HAL库SPI底层初始化函数
* @note 配置SPI管脚、时钟等,注意CS片选管脚是软件控制
*/
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
if(hspi->Instance == SPI1)
{
GPIO_InitTypeDef gpio_initstruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能GPIOA时钟
__HAL_RCC_SPI1_CLK_ENABLE(); // 使能SPI1时钟
// PA4: CS,片选(推挽输出,用户手动控制)
gpio_initstruct.Pin = GPIO_PIN_4;
gpio_initstruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
gpio_initstruct.Pull = GPIO_PULLUP; // 上拉
gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高频率
HAL_GPIO_Init(GPIOA, &gpio_initstruct);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 初始拉高片选
// PA5: SCK(SPI时钟),PA7: MOSI(主机数据输出)
gpio_initstruct.Pin = GPIO_PIN_5 | GPIO_PIN_7;
gpio_initstruct.Mode = GPIO_MODE_AF_PP; // 复用推挽输出
HAL_GPIO_Init(GPIOA, &gpio_initstruct);
// PA6: MISO(主机数据输入)
gpio_initstruct.Pin = GPIO_PIN_6;
gpio_initstruct.Mode = GPIO_MODE_INPUT; // 输入模式
HAL_GPIO_Init(GPIOA, &gpio_initstruct);
}
}
/*
* @brief SPI收发一个字节(阻塞模式)
* @param data 要发送的数据(uint8_t,0~255)
* @retval 接收到的数据(uint8_t,0~255)
*/
uint8_t w25q128_spi_swap_byte(uint8_t data)
{
uint8_t recv_data = 0;
HAL_SPI_TransmitReceive(&spi_handle, &data, &recv_data, 1, 1000); // 最后一个参数为超时时间(ms)
return recv_data;
}
/*
* @brief 初始化W25Q128(包括SPI初始化)
*/
void w25q128_init(void)
{
w25q128_spi_init();
}
/*
* @brief 读取W25Q128设备ID
* @retval 16位设备ID,高8位为厂商ID,低8位为设备ID
*/
uint16_t w25q128_read_id(void)
{
uint16_t device_id = 0;
W25Q128_CS(0);
w25q128_spi_swap_byte(FLASH_ManufactDeviceID); // 0x90,读厂家/设备ID指令
w25q128_spi_swap_byte(0x00); // 24位地址(全0)
w25q128_spi_swap_byte(0x00);
w25q128_spi_swap_byte(0x00);
device_id = w25q128_spi_swap_byte(FLASH_DummyByte) << 8; // 读厂商ID
device_id |= w25q128_spi_swap_byte(FLASH_DummyByte); // 读设备ID
W25Q128_CS(1);
return device_id;
}
/*
* @brief 写使能(所有写/擦除操作前都要调用)
*/
void w25q128_write_enable(void)
{
W25Q128_CS(0);
w25q128_spi_swap_byte(FLASH_WriteEnable); // 0x06
W25Q128_CS(1);
}
/*
* @brief 读取状态寄存器1
* @retval 状态寄存器1(BUSY/WEL/保护位等)
*/
uint8_t w25q128_read_sr1(void)
{
uint8_t recv_data = 0;
W25Q128_CS(0);
w25q128_spi_swap_byte(FLASH_ReadStatusReg1); // 0x05
recv_data = w25q128_spi_swap_byte(FLASH_DummyByte);
W25Q128_CS(1);
return recv_data;
}
/*
* @brief 等待W25Q128空闲(忙状态自动查询)
*/
void w25q128_wait_busy(void)
{
while((w25q128_read_sr1() & 0x01) == 0x01); // BUSY=1为忙
}
/*
* @brief 发送24位地址(高字节→低字节)
* @param address 24位物理地址,0~0xFFFFFF
*/
static void w25q128_send_address(uint32_t address)
{
w25q128_spi_swap_byte((address >> 16) & 0xFF); // 发送高8位
w25q128_spi_swap_byte((address >> 8) & 0xFF); // 发送中8位
w25q128_spi_swap_byte(address & 0xFF); // 发送低8位
}
/*
* @brief 读取数据
* @param address 起始地址(0~0xFFFFFF)
* @param data 数据缓存指针
* @param size 要读取的长度(字节数),最大0x1000000
*/
void w25q128_read_data(uint32_t address, uint8_t *data, uint32_t size)
{
W25Q128_CS(0);
w25q128_spi_swap_byte(FLASH_ReadData); // 0x03,标准读取指令
w25q128_send_address(address);
for(uint32_t i = 0; i < size; i++)
data[i] = w25q128_spi_swap_byte(FLASH_DummyByte); // 连续读取
W25Q128_CS(1);
}
/*
* @brief 写入一页数据(最大256字节,不能跨页)
* @param address 起始地址(需保证不跨页,即address%256+size<=256)
* @param data 待写入数据指针
* @param size 写入长度,1~256
*/
void w25q128_write_page(uint32_t address, const uint8_t *data, uint16_t size)
{
if (size == 0 || size > 256)
return; // 非法长度
if (((address & 0xFF) + size) > 256)
return; // 跨页写入,非法!
w25q128_write_enable(); // 写使能
W25Q128_CS(0);
w25q128_spi_swap_byte(FLASH_PageProgram); // 0x02
w25q128_send_address(address);
for(uint16_t i = 0; i < size; i++)
w25q128_spi_swap_byte(data[i]); // 写入每一字节
W25Q128_CS(1);
w25q128_wait_busy(); // 等待空闲
}
/*
* @brief 擦除一个扇区(4KB,地址需4KB对齐)
* @param address 扇区内任意地址,建议为0x0000/0x1000/0x2000...
*/
void w25q128_erase_sector(uint32_t address)
{
w25q128_write_enable();
w25q128_wait_busy(); // 擦除前保证不忙
W25Q128_CS(0);
w25q128_spi_swap_byte(FLASH_SectorErase); // 0x20
w25q128_send_address(address);
W25Q128_CS(1);
w25q128_wait_busy(); // 擦除操作一般较慢,需要等待完成
}
w25q128.h
#ifndef __W25Q128_H__
#define __W25Q128_H__
#include "sys.h"
#include "stm32f1xx_hal.h"
// 片选宏,x=0拉低选中(使能),x=1拉高释放(禁用)
#define W25Q128_CS(x) \
do { \
if (x) \
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); \
else \
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); \
} while(0)
/*
* ================= SPI指令集(部分常用,更多请查手册) =================
*/
// 0x90: 读取厂家/设备ID(需跟三个0x00,然后返回厂商ID+设备ID,共2字节)
#define FLASH_ManufactDeviceID 0x90 // 读取厂家/设备ID指令
// 0x9F: 读取JEDEC ID(直接读,返回三字节:厂商ID、存储类型、容量)
#define FLASH_JEDEC_ID 0x9F // 读取JEDEC ID指令
// 0x06: 写使能(所有编程/擦除前都要先发一次)
#define FLASH_WriteEnable 0x06 // 写使能指令
// 0x04: 写禁止(防止误操作写入/擦除)
#define FLASH_WriteDisable 0x04 // 写禁止指令
// 0x05: 读状态寄存器1(BUSY/WEL/保护位等状态)
#define FLASH_ReadStatusReg1 0x05 // 读状态寄存器1
// 0x03: 读取数据(标准读,后跟3字节地址,连续读出N字节数据)
#define FLASH_ReadData 0x03 // 标准读取数据指令
// 0x02: 页编程(写入数据,最大256字节,后跟3字节地址+数据流)
#define FLASH_PageProgram 0x02 // 页编程指令
// 0x20: 扇区擦除(擦除4KB,后跟3字节地址)
#define FLASH_SectorErase 0x20 // 扇区擦除(4KB)指令
// 0xC7/0x60: 整片擦除(清空全部数据)
#define FLASH_ChipErase 0xC7 // 整片擦除指令(C7/60等效)
// 0xFF: Dummy字节(用于SPI读时“占位”,通常发0xFF即可收到有效数据)
#define FLASH_DummyByte 0xFF // 占位字节(用于SPI收发时)
/*
* 其他常用指令说明(可扩展):
* 0x35:读状态寄存器2
* 0x01:写状态寄存器1/2
* 0x75:进入掉电模式
* 0xAB:释放掉电
* 0xD8:块擦除(64KB)
* 详见Winbond原厂数据手册
*/
// ==== 接口函数声明 ====
void w25q128_init(void);
uint16_t w25q128_read_id(void);
void w25q128_read_data(uint32_t address, uint8_t *data, uint32_t size);
void w25q128_write_page(uint32_t address, const uint8_t *data, uint16_t size);
void w25q128_erase_sector(uint32_t address);
#endif
#include "sys.h"
#include "delay.h"
#include "led.h"
#include "uart1.h"
#include "w25q128.h"
//还有哪些指令
uint8_t data_write[4] = {0x11, 0x22, 0xCC, 0xDD};
uint8_t data_read[4] = {0};
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
led_init(); /* 初始化LED灯 */
uart1_init(115200);
w25q128_init();
printf("hello world!\r\n");
uint16_t device_id = w25q128_read_id();
printf("device id: %X\r\n", device_id);//我读出来是EF15,良读出来是EF17
// w25q128_erase_sector(0x000000);
// w25q128_write_page(0x000000, data_write, 4);
w25q128_read_data(0x000000, data_read, 4);
printf("data read: %X, %X, %X, %X\r\n", data_read[0], data_read[1], data_read[2], data_read[3]);
while(1)
{
}
}

标准库
main.c
/**
******************************************************************************
* @ 名称 STM32 学习模板
* @ 版本 STD 库 V3.5.0
* @ 描述 适用于嵌入式虚拟仿真平台
*
* @ 注意 本程序只供学习使用
******************************************************************************
*/
#include "sys.h"
#include "stm32f10x_conf.h"
#include "delay.h"
#include "w25q128.h"
#include "IIC_OLED.h"
#include <string.h> // 标准C字符串操作
int main(void)
{
char write_buf[] = "I LOVE YOU"; // 要写入的数据(字符串)
uint8_t read_buf[32] = {0}; // 读出用的缓存,需足够大
uint32_t flash_addr = 0x000100; // Flash目标地址(4K对齐更安全,实验中这里随便选)
//delay_init(); // 延时初始化(确保delay_ms有效)
w25q128_init(); // 初始化W25Q128(SPI、GPIO等)
oled_init(); // 初始化OLED(I2C、字库、寄存器等)
// ---------- 1. 写入W25Q128 ----------
w25q128_erase_sector(flash_addr); // 擦除目标扇区(写入前必须擦除,否则数据可能会出错)
delay_ms(10); // 等待擦除完成,建议加一点延时
w25q128_write_page(flash_addr, (uint8_t*)write_buf, strlen(write_buf));
delay_ms(2); // 写入完成等待,保险起见
// ---------- 2. 读取W25Q128 ----------
w25q128_read_data(flash_addr, read_buf, strlen(write_buf));
read_buf[strlen(write_buf)] = '\0'; // 确保字符串以0结尾
// ---------- 3. OLED显示 ----------
oled_fill(0x00); // 清屏
oled_show_string(0, 2, (char*)read_buf, 16); // 第2行显示,字号16
while(1)
{
// 如果要动态刷新,可以在这里添加代码
}
}
w25q128.c
#include "w25q128.h"
/**********************************************************************
* @brief SPI1和片选GPIO初始化
* SCK-PA5 MISO-PA6 MOSI-PA7 CS-PA4
***********************************************************************/
void w25q128_gpio_spi_init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
// 1. 打开GPIOA和SPI1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能GPIOA
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); // 使能SPI1
// 2. 配置SCK/MOSI为复用推挽输出,MISO为浮空输入,CS为推挽输出
// 2.1 SCK-PA5, MOSI-PA7
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 2.2 MISO-PA6
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 2.3 CS-PA4
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_4); // 默认拉高(不选中)
// 3. 配置SPI1工作模式、参数(模式0:CPOL=0,CPHA=0,8位,主机)
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主机模式
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8位数据帧
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 时钟空闲低(CPOL=0)
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 第1个沿采样(CPHA=0)
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件NSS
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;// 分频16
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // MSB先行
SPI_InitStructure.SPI_CRCPolynomial = 7; // CRC多项式,默认
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE); // 使能SPI1
}
/**********************************************************************
* @brief SPI收发一个字节
* @param data 要发送的数据(uint8_t,0~255)
* @retval 收到的数据(uint8_t,0~255)
***********************************************************************/
uint8_t w25q128_spi_rw(uint8_t data)
{
// 1. 等待发送缓冲区空
while (SPI_I2S_GetFlagStatus(W25Q128_SPI, SPI_I2S_FLAG_TXE) == RESET);
// 2. 发送数据
SPI_I2S_SendData(W25Q128_SPI, data);
// 3. 等待接收缓冲区非空
while (SPI_I2S_GetFlagStatus(W25Q128_SPI, SPI_I2S_FLAG_RXNE) == RESET);
// 4. 返回收到的数据
return SPI_I2S_ReceiveData(W25Q128_SPI);
}
/**********************************************************************
* @brief W25Q128初始化(包含SPI、GPIO初始化)
***********************************************************************/
void w25q128_init(void)
{
w25q128_gpio_spi_init();
}
/**********************************************************************
* @brief 读取W25Q128设备ID
* @retval 16位设备ID(高8位=厂商ID,低8位=设备ID)
***********************************************************************/
uint16_t w25q128_read_id(void)
{
uint16_t device_id = 0;
W25Q128_CS(0); // 片选拉低
w25q128_spi_rw(FLASH_ManufactDeviceID); // 0x90,命令
w25q128_spi_rw(0x00); // 24位地址,读ID用0x00
w25q128_spi_rw(0x00);
w25q128_spi_rw(0x00);
device_id = w25q128_spi_rw(FLASH_DummyByte) << 8; // 读厂商ID
device_id |= w25q128_spi_rw(FLASH_DummyByte); // 读设备ID
W25Q128_CS(1); // 片选拉高
return device_id;
}
/**********************************************************************
* @brief 写使能(写/擦除前必须调用)
***********************************************************************/
static void w25q128_write_enable(void)
{
W25Q128_CS(0);
w25q128_spi_rw(FLASH_WriteEnable); // 0x06
W25Q128_CS(1);
}
/**********************************************************************
* @brief 读取状态寄存器1
* @retval 状态寄存器1(BUSY/WEL等状态位)
***********************************************************************/
static uint8_t w25q128_read_sr1(void)
{
uint8_t status = 0;
W25Q128_CS(0);
w25q128_spi_rw(FLASH_ReadStatusReg1); // 0x05
status = w25q128_spi_rw(FLASH_DummyByte);
W25Q128_CS(1);
return status;
}
/**********************************************************************
* @brief 等待W25Q128芯片空闲(BUSY=0)
***********************************************************************/
void w25q128_wait_busy(void)
{
while (w25q128_read_sr1() & 0x01); // BUSY=1为忙,0为空闲
}
/**********************************************************************
* @brief 发送24位地址(高8位→中8位→低8位)
* @param address 24位物理地址
***********************************************************************/
static void w25q128_send_address(uint32_t address)
{
w25q128_spi_rw((address >> 16) & 0xFF); // 发送高8位
w25q128_spi_rw((address >> 8) & 0xFF); // 发送中8位
w25q128_spi_rw(address & 0xFF); // 发送低8位
}
/**********************************************************************
* @brief 读取数据(标准读指令)
* @param address 起始地址(0~0xFFFFFF)
* @param data 缓存区指针
* @param size 读取字节数
***********************************************************************/
void w25q128_read_data(uint32_t address, uint8_t *data, uint32_t size)
{
W25Q128_CS(0);
w25q128_spi_rw(FLASH_ReadData); // 0x03
w25q128_send_address(address);
for (uint32_t i = 0; i < size; i++)
data[i] = w25q128_spi_rw(FLASH_DummyByte);
W25Q128_CS(1);
}
/**********************************************************************
* @brief 页编程(写入最大256字节,不能跨页)
* @param address 起始地址(需保证同一页)
* @param data 数据指针
* @param size 写入字节数,1~256
***********************************************************************/
void w25q128_write_page(uint32_t address, const uint8_t *data, uint16_t size)
{
if (size == 0 || size > 256)
return; // 长度非法
if (((address & 0xFF) + size) > 256)
return; // 跨页写入,不支持!
w25q128_write_enable(); // 写使能
W25Q128_CS(0);
w25q128_spi_rw(FLASH_PageProgram); // 0x02
w25q128_send_address(address);
for (uint16_t i = 0; i < size; i++)
w25q128_spi_rw(data[i]);
W25Q128_CS(1);
w25q128_wait_busy(); // 等待操作完成
}
/**********************************************************************
* @brief 擦除一个扇区(4KB,地址4K对齐)
* @param address 扇区内任意地址
***********************************************************************/
void w25q128_erase_sector(uint32_t address)
{
w25q128_write_enable();
w25q128_wait_busy();
W25Q128_CS(0);
w25q128_spi_rw(FLASH_SectorErase); // 0x20
w25q128_send_address(address);
W25Q128_CS(1);
w25q128_wait_busy(); // 擦除时间较长
}
w25q128.h
#ifndef __W25Q128_H__
#define __W25Q128_H__
#include "stm32f10x.h" // STM32F10x标准库头文件
/********** W25Q128 芯片的引脚定义和片选宏 **********/
#define W25Q128_SPI SPI1 // SPI外设
#define W25Q128_CS_PORT GPIOA // 片选端口
#define W25Q128_CS_PIN GPIO_Pin_4 // 片选引脚
// 片选宏,x=0拉低选中,x=1拉高释放
#define W25Q128_CS(x) do { \
if(x) GPIO_SetBits(W25Q128_CS_PORT, W25Q128_CS_PIN); \
else GPIO_ResetBits(W25Q128_CS_PORT, W25Q128_CS_PIN); \
} while(0)
/********** W25Q128 常用指令集(附详细注释) **********/
#define FLASH_ManufactDeviceID 0x90 // 读厂家/设备ID(0x90+3字节地址+2字节ID)
#define FLASH_JEDEC_ID 0x9F // 读JEDEC ID(0x9F+3字节ID)
#define FLASH_WriteEnable 0x06 // 写使能(必须,写/擦前发)
#define FLASH_WriteDisable 0x04 // 写禁止
#define FLASH_ReadStatusReg1 0x05 // 读状态寄存器1(BUSY/WEL/保护等)
#define FLASH_ReadData 0x03 // 标准读数据
#define FLASH_PageProgram 0x02 // 页编程(最多256字节)
#define FLASH_SectorErase 0x20 // 扇区擦除(4KB)
#define FLASH_ChipErase 0xC7 // 整片擦除
#define FLASH_DummyByte 0xFF // Dummy字节(SPI收发时填充)
/********** 接口函数声明 **********/
void w25q128_gpio_spi_init(void); // SPI、GPIO初始化
uint8_t w25q128_spi_rw(uint8_t data); // SPI读写1字节
void w25q128_init(void); // W25Q128初始化
uint16_t w25q128_read_id(void); // 读设备ID
void w25q128_read_data(uint32_t address, uint8_t *data, uint32_t size); // 读数据
void w25q128_write_page(uint32_t address, const uint8_t *data, uint16_t size); // 页写入
void w25q128_erase_sector(uint32_t address); // 扇区擦除
void w25q128_wait_busy(void); // 等待芯片空闲
#endif

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)