SPI通信协议详解与STM32实战应用驱动W25Q128(实战篇)

1. W25Q128简介

W25Q128 是Winbond推出的128M-bit(16MB)SPI接口Flash存储器,支持标准SPI、Dual-SPI和Quad-SPI模式。关键特性:

  • 工作电压:2.7V~3.6V
  • 分页结构:256页/块,每块16KB,共1024块
  • 支持页编程(256字节/页)
  • 擦除操作支持:扇区擦除(4KB)、块擦除(32/64KB)、全片擦除
  • 最高104MHz时钟频率
  • 超过10万次擦写周期
  • 1.1 W25Q128存储架构

    W25Q128存储器采用分级存储架构,总容量为16MB(128Mbit)。其物理存储结构按以下层级划分:

  • 顶层划分:256个存储块(Block),每块容量64KB
  • 块内结构:每个存储块包含16个扇区(Sector),每扇区容量4KB
  • 扇区组成:每个扇区细分为16个存储页(Page),每页容量256字节
  • 该存储器的擦除机制具有以下特性:

  • 最小擦除单位为单个扇区(4KB)
  • 执行擦除操作时必须完整处理整个扇区
  • 系统需预置4KB对齐的缓冲区,以满足物理擦除粒度要求
  • 1.2 W25Q128常用指令

    W25Q128 有非常多的指令,我们介绍几个常用的指令。

    指令 (HEX) 名称 作用
    0x06 写使能 写入数据/擦除之前,必须先发送该指令
    0x05 读 SR1 判定 FLASH 是否处于空闲状态,擦除用
    0x03 读数据 读取数据
    0x02 页写 写入数据,最多写256字节
    0x20 扇区擦除 扇区擦除指令,最小擦除单位

    1.3 W25Q128 操作时序详解

    1. 写使能(06h)- 指令功能‌:执行编程/擦除操作的必要使能信号
    2. 操作流程:
      ① CS引脚置低(通信起始)
      ② 发送1字节指令码06h(MSB先发)
      ③ CS引脚置高(通信终止)
    3. 状态影响‌:WEL位(SR1)置1,有效时间约50ms

    1. 状态寄存器读取(05h)
    2. 指令格式‌:
      ┌──────┬─────────────┐
      | 指令码 | 响应数据(持续可读) |
      └──────┴─────────────┘
    3. 操作时序**‌:
      ① CS置低 → 发送05h → 连续读取SR1 → CS置高‌
    4. 技术要点‌:
      BUSY位(SR1)监控:0=空闲,1=操作中
      支持全双工SPI模式(MOSI/MISO同步传输)

    1. 数据读取(03h)
    2. 指令结构‌:
      ┌──────┬──────────┬─────────┐
      | 03h | 24bit地址 | 数据流(≥1字节) |
      └──────┴──────────┴─────────┘
    3. 执行步骤‌:
      1. 起始地址对齐(按页边界自动递增)
      2. 单次读取最大长度:256字节(单页容量)
      3. 跨页读取需重新发送地址指令

    1. 页编程(02h)
    2. 技术限制‌:
    3. 目标区域必须处于擦除状态(全FFh)
    4. 单次写入跨度≤256字节(禁止跨页写入)
    5. 操作序列‌:
      [CS↓] → 02h → A23-A0 → Data_1~Data_n → [CS↑]
    6. 物理特性‌:
      实际编程时间约0.7-1.2ms(需BUSY状态轮询)

    1. 扇区擦除(20h)
    2. 约束条件‌:
    3. 擦除粒度:4KB(地址自动对齐到扇区起始)
    4. 擦除后状态:全FFh(电平=1)
    5. 时序流程‌:
      [CS↓] → 20h → 目标地址(A23-A0) → [CS↑]
    6. 耗时范围‌:
      典型值400ms(温度25℃),最大600ms

    2. 硬件连接

    这篇文章,使用的是W25Q128模块,W25Q128 的模块各个厂家做的各有不同,只是长得不一样而已,使用方式、引脚都是一样的。
    通过产品手册如下:

    STM32F103C8T6W25Q128 接线方式:

    STM32引脚 W25Q128引脚 功能
    PA5 CLK SPI时钟
    PA6 MISO 主机输入
    PA7 MOSI 主机输出
    PA4 CS 片选信号
    3.3V VCC 电源
    GND GND

    注意:CS引脚需通过GPIO控制,不可固定接低电平

    3. 代码实现

    3.1 SPI及GPIO初始化

    在进行初始化时, 我们需要查看参考手册:
    按照手册和实际情况进行GPIO口的配置

    SPI_HandleTypeDef spi_handle = {0};
    void w25q128_spi_init(void)
    {
        spi_handle.Instance = SPI1;
        spi_handle.Init.Mode = SPI_MODE_MASTER;
        spi_handle.Init.Direction = SPI_DIRECTION_2LINES;
        spi_handle.Init.DataSize = SPI_DATASIZE_8BIT;
        spi_handle.Init.CLKPolarity = SPI_POLARITY_LOW;              /* CPOL = 0 */
        spi_handle.Init.CLKPhase = SPI_PHASE_1EDGE;                  /* CPHA = 0 */
        spi_handle.Init.NSS = SPI_NSS_SOFT;
        spi_handle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;
        spi_handle.Init.FirstBit = SPI_FIRSTBIT_MSB;
        spi_handle.Init.TIMode = SPI_TIMODE_DISABLE;
        spi_handle.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
        spi_handle.Init.CRCPolynomial = 7;
        HAL_SPI_Init(&spi_handle);
    }
    
    void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
    {
        if(hspi->Instance == SPI1)
        {
            GPIO_InitTypeDef gpio_initstruct;
            //打开时钟
            __HAL_RCC_GPIOA_CLK_ENABLE();                           // 使能GPIOB时钟
            __HAL_RCC_SPI1_CLK_ENABLE();
            
            //调用GPIO初始化函数
            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);
            
            gpio_initstruct.Pin = GPIO_PIN_5 | GPIO_PIN_7;          
            gpio_initstruct.Mode = GPIO_MODE_AF_PP;           
            HAL_GPIO_Init(GPIOA, &gpio_initstruct);
            
            gpio_initstruct.Pin = GPIO_PIN_6;          
            gpio_initstruct.Mode = GPIO_MODE_INPUT;           
            HAL_GPIO_Init(GPIOA, &gpio_initstruct);
        }
    }
    

    3.2 W25Q128驱动函数

    w251128.c
    
    // 交换一个字节
    uint8_t w25q128_spi_swap_byte(uint8_t data)
    {
        uint8_t recv_data = 0;
        HAL_SPI_TransmitReceive(&spi_handle, &data, &recv_data, 1, 1000);
        return recv_data;
    }
    
    // 初始化:
    void w25q128_init(void)
    {
        w25q128_spi_init();
    }
    
    // 读取iD值
    uint16_t w25q128_read_id(void)
    {
        uint16_t device_id = 0;
        W25Q128_CS(0);
        
        w25q128_spi_swap_byte(FLASH_ManufactDeviceID);
        w25q128_spi_swap_byte(0x00);
        w25q128_spi_swap_byte(0x00);
        w25q128_spi_swap_byte(0x00);
        device_id = w25q128_spi_swap_byte(FLASH_DummyByte) << 8;
        device_id |= w25q128_spi_swap_byte(FLASH_DummyByte);
        
        W25Q128_CS(1);
        return device_id;
    }
    
    // 写使能
    
    void w25q128_writ_enable(void)
    {
        W25Q128_CS(0);
        w25q128_spi_swap_byte(FLASH_WriteEnable);
        W25Q128_CS(1);
    }
    
    // 读取状态寄存器1
    uint8_t w25q128_read_sr1(void)
    {
        uint8_t recv_data = 0;
        
        W25Q128_CS(0);
        w25q128_spi_swap_byte(FLASH_ReadStatusReg1);
        recv_data = w25q128_spi_swap_byte(FLASH_DummyByte);
        W25Q128_CS(1);
        
        return recv_data;
    }
    
    
    // 等待BUSY
    void w25q128_wait_busy(void)
    {
        while((w25q128_read_sr1() & 0x01) == 0x01);
    }
    // 发送地址
    void w25q128_send_address(uint32_t address)
    {
        w25q128_spi_swap_byte(address >> 16);
        w25q128_spi_swap_byte(address >> 8);
        w25q128_spi_swap_byte(address);
    }
    // 读取数据
    void w25q128_read_data(uint32_t address, uint8_t *data, uint32_t size)
    {
        uint32_t i = 0;
        W25Q128_CS(0);
        w25q128_spi_swap_byte(FLASH_ReadData);
        w25q128_send_address(address);
        
        for(i = 0; i < size; i++)
            data[i] = w25q128_spi_swap_byte(FLASH_DummyByte);
        W25Q128_CS(1);
    }
    
    // 页写
    void w25q128_write_page(uint32_t address, uint8_t *data, uint16_t size)
    {
        uint16_t i = 0;
        w25q128_writ_enable();
        
        W25Q128_CS(0);
        w25q128_spi_swap_byte(FLASH_PageProgram);
        w25q128_send_address(address);
        
        for(i = 0; i < size; i++)
            w25q128_spi_swap_byte(data[i]);
        
        W25Q128_CS(1);
        //等待空闲
        w25q128_wait_busy();
    }
    
    // 擦除
    void w25q128_erase_sector(uint32_t address)
    {
        //写使能
        w25q128_writ_enable();
        //等待空闲
        w25q128_wait_busy();
        //拉低片选
        W25Q128_CS(0);
        //发送扇区擦除指令
        w25q128_spi_swap_byte(FLASH_SectorErase);
        //发送地址
        w25q128_send_address(address);
        //拉高片选
        W25Q128_CS(1);
        //等待空闲
        w25q128_wait_busy();
    }
    
    
    w25q128.h
    #ifndef __W25Q128_H__
    #define __W25Q128_H__
    
    #include "sys.h"
    
    #define W25Q128_CS(x)   do{ x ? \
                                    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET): \
                                    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); \
                            }while(0)
    
    /* 指令表 */
    #define FLASH_ManufactDeviceID                  0x90
    #define FLASH_WriteEnable                       0x06
    #define FLASH_ReadStatusReg1                    0x05
    #define FLASH_ReadData                          0x03
    #define FLASH_PageProgram                       0x02
    #define FLASH_SectorErase                       0x20
    #define FLASH_DummyByte                         0xFF
    
    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, uint8_t *data, uint16_t size);
    void w25q128_erase_sector(uint32_t address);
    
    #endif
    
    

    3.3 使用示例

    #include "sys.h"
    #include "delay.h"
    #include "led.h"
    #include "uart1.h"
    #include "w25q128.h"
    
    uint8_t data_write[4] = {0xAA, 0xBB, 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);
    
    //    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)
        { 
    
        }
    }
    
    

    4. 注意事项

    1. 擦除操作必需:Flash写入前必须擦除对应区域
    2. 页边界限制:单次写入不可跨页(每页256字节)
    3. 时钟配置:确保SPI时钟不超过芯片规格(建议初始使用较低频率)
    4. 状态检测:重要操作后需检查BUSY位
    5. 电源稳定:Flash操作期间需保持3.3V稳定

    5. 常见问题排查

  • 无法读取ID:检查SPI模式(CPOL=0/CPHA=0),确认CS信号时序
  • 写入失败:确保已执行写使能命令,检查电源电压
  • 数据错误:注意地址对齐,排除SPI时钟干扰
  • 完整工程代码可在Gitee获取:https://gitee.com/bad-lemon/mcu-development-record.git


    实现效果:通过上述代码可实现W25Q128的读写操作,实测写入速度可达600KB/s,读取速度1.2MB/s(SPI时钟18MHz)。该方案适用于需要大容量非易失存储的物联网设备、数据采集系统等场景。

    作者:坏柠

    物联沃分享整理
    物联沃-IOTWORD物联网 » SPI通信协议详解与STM32实战应用驱动W25Q128(实战篇)

    发表回复