一、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. 通信流程(简述)

  1. 主机拉低某个从机的CS。

  2. 主机产生SCLK,同时在MOSI上发送数据。

  3. 从机在MISO线上发送数据(回送)。

  4. 完成一帧后,主机拉高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);

六、实用小结

  1. STM32标准外设库(StdPeriph)下,所有SPI操作都以SPI_开头

  2. 最常用的是初始化、数据收发(SendData/ReceiveData)、状态查询(GetFlagStatus)、使能SPI。

  3. 寄存器操作和底层细节可通过库函数轻松实现,大大减少手写寄存器代码的出错率。

  4. 如果用的是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。

操作流程

  1. CS拉低,选中芯片

  2. 发送命令0x9F(JEDEC ID读命令)

  3. 连续读取3个字节(厂商ID、设备类型、容量)

  4. 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();
}

操作流程

  1. CS拉低

  2. 发送命令0x03(标准读)

  3. 发送3字节地址(高→低字节)

  4. 连续读取数据N字节(读多少收多少)

  5. CS拉高

5. 写使能(所有写/擦都需先发此命令)

void W25Q_WriteEnable(void)
{
    W25Q_CS_LOW();
    SPI_RW(SPI1, 0x06); // 写使能命令
    W25Q_CS_HIGH();
}

操作流程

  1. CS拉低

  2. 发送命令0x06

  3. 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不可逆)。

操作流程

  1. 写使能(见上面)

  2. CS拉低

  3. 发送命令0x02(页编程)

  4. 发送3字节目标地址(写入起始地址)

  5. 连续写入N个字节数据(最大256字节,不能跨页)

  6. CS拉高

  7. 轮询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();
}

操作流程

  1. 写使能

  2. CS拉低

  3. 发送命令0x20(擦除4K扇区)

  4. 发送3字节目标地址(扇区内任意地址)

  5. CS拉高

  6. 轮询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表示忙
}

操作流程

  1. CS拉低

  2. 发送命令0x05(读状态寄存器1)

  3. 读取一个字节(bit0为BUSY,1=忙)

  4. CS拉高

三、完整的写数据典型流程

  1. 擦除扇区

  2. 等待空闲

  3. 页编程写入

  4. 等待空闲

  5. 读数据校验


四、代码小结与注意

  • 每次写/擦都需先写使能

  • 写操作必须以页为单位,不能跨页。

  • 写入前要擦除目标区,否则会出现写入不成功(只能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

Logo

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

更多推荐