2. SPI Flash 硬件基础:SPI 协议简介与 Flash 芯片选型

做嵌入式 UI,图片资源动不动就几百 KB。LVGL 跑起来,内部 Flash 根本塞不下。这时候,外挂一颗 SPI Flash 就成了最实在的方案。

这一章,我们先把硬件底子打牢。说白了,就是搞明白两件事:SPI 总线怎么干活,以及Flash 芯片怎么挑。我以 W25Q64 为例,带你走一遍选型和理解的过程。

2.1 SPI 协议:四根线怎么聊天的?

SPI 全称 Serial Peripheral Interface,同步串行外设接口。它由 Motorola 公司推出,后来成了事实标准。

SPI 总线只需要 4 根信号线:

信号 全称 方向 作用
SCLK Serial Clock 主机 → 从机 时钟信号,由主机产生
MOSI Master Out Slave In 主机 → 从机 主机发送数据给从机
MISO Master In Slave Out 从机 → 主机 从机发送数据给主机
CS Chip Select 主机 → 从机 片选信号,低电平有效

SPI 是 全双工 的。什么意思?主机发数据的同时,也能收数据。这一点比 I2C 快不少。

通信过程很简单:

  1. 主机把 CS 拉低,选中从机。
  2. 主机在 SCLK 上产生时钟脉冲。
  3. 每个时钟周期,主机通过 MOSI 发 1 bit,同时从机通过 MISO 回 1 bit。
  4. 传输结束,主机把 CS 拉高。
我的习惯: 调试 SPI 时,先拿逻辑分析仪抓 CS 和 SCLK。CS 有没有拉低?SCLK 有没有波形?这两根线对了,基本就成功了一半。

2.2 SPI 的四种模式:别搞错时钟极性

SPI 有 4 种工作模式,由 时钟极性 (CPOL) 和 时钟相位 (CPHA) 决定。

  • CPOL:空闲时 SCLK 的电平。0 是低电平,1 是高电平。
  • CPHA:数据在哪个边沿采样。0 是第一个边沿,1 是第二个边沿。
模式 CPOL CPHA 采样边沿 常用场景
模式 0 0 0 上升沿 W25Q64、SD 卡
模式 1 0 1 下降沿 部分传感器
模式 2 1 0 下降沿 部分 LCD 驱动
模式 3 1 1 上升沿 部分射频芯片
我曾经踩过的坑: 第一次用 W25Q64 时,没仔细看数据手册,默认用了模式 3。结果读 ID 全错。折腾了半天,才发现 W25Q64 只支持 模式 0 和模式 3。后来我学乖了,每次拿到新芯片,第一件事就是翻 SPI 章节,确认 CPOL 和 CPHA。

2.3 W25Q64:一颗经典的 SPI Flash

W25Q64 是 Winbond 公司生产的 SPI Flash 芯片。容量 64 Mbit,也就是 8 MB。为什么选它?

  • 性价比高:8 MB 容量,价格才一两块钱。
  • 生态成熟:几乎所有 MCU 厂商的驱动库都支持它。
  • 速度够用:支持最高 80 MHz 时钟频率,Quad SPI 模式下更快。

它的内部结构是这样的:

W25Q64 内部结构框图 W25Q64 内部结构框图 SPI 接口 (SCLK / MOSI / MISO / CS) 控制逻辑与状态寄存器 Flash 存储阵列 (64 Mbit) 256 个块 (Block) × 16 个扇区 (Sector) × 16 页 (Page) × 256 字节 页缓冲 (Page Buffer, 256 字节) 写入数据时,先写入页缓冲,再触发编程命令写入存储阵列

你想想看,8 MB 能存多少张图片?一张 320×240 的 16 位色 BMP 大约 150 KB。8 MB 能存 50 多张。对于 LVGL 的 UI 项目,绰绰有余。

2.4 芯片选型:除了 W25Q64,还有哪些选择?

市面上 SPI Flash 芯片很多。我列几个常见的,方便你对比:

型号 容量 最高频率 Quad SPI 价格 (参考)
W25Q16 2 MB 80 MHz 支持 ¥1.5
W25Q64 8 MB 80 MHz 支持 ¥2.5
W25Q128 16 MB 80 MHz 支持 ¥4.0
GD25Q64 8 MB 80 MHz 支持 ¥2.0
MX25L64 8 MB 86 MHz 支持 ¥3.0
我的建议: 如果项目对成本敏感,选 GD25Q64,和 W25Q64 完全兼容。如果追求稳定,选 W25Q64,文档最全,社区支持最好。

2.5 硬件连接:怎么接到 MCU 上?

连接很简单。以 STM32 为例:

// W25Q64 与 STM32F407 的连接
// PA5 - SCLK
// PA7 - MOSI
// PA6 - MISO
// PB0 - CS

SPI_HandleTypeDef hspi1;

void MX_SPI1_Init(void)
{
    hspi1.Instance = SPI1;
    hspi1.Init.Mode = SPI_MODE_MASTER;
    hspi1.Init.Direction = SPI_DIRECTION_2LINES;
    hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;      // CPOL = 0
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;          // CPHA = 0
    hspi1.Init.NSS = SPI_NSS_SOFT;                  // 软件控制 CS
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 42 MHz / 4 = 10.5 MHz
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
    hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
    hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    hspi1.Init.CRCPolynomial = 10;
    HAL_SPI_Init(&hspi1);
}
避坑指南: 硬件上,CS 引脚不要用硬件 NSS,用普通 GPIO 软件控制。为什么?因为硬件 NSS 在多从机场景下容易出问题。我习惯用 PB0 做 CS,手动拉高拉低,心里踏实。

2.6 读芯片 ID:验证通信的第一步

拿到新板子,怎么确认 SPI 通信正常?读芯片的 制造商 ID 和 设备 ID。

W25Q64 的读 ID 命令是 0x9F。发送流程:

  1. 拉低 CS。
  2. 发送命令字节 0x9F。
  3. 连续读取 3 个字节:制造商 ID、设备 ID 高字节、设备 ID 低字节。
  4. 拉高 CS。
uint8_t manufacturer_id;
uint8_t device_id_high;
uint8_t device_id_low;

void W25Q64_Read_ID(void)
{
    uint8_t cmd = 0x9F;
    uint8_t rx_buffer[3] = {0};
    
    CS_LOW();                           // 拉低 CS
    HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
    HAL_SPI_Receive(&hspi1, rx_buffer, 3, 100);
    CS_HIGH();                          // 拉高 CS
    
    manufacturer_id = rx_buffer[0];
    device_id_high = rx_buffer[1];
    device_id_low = rx_buffer[2];
    
    // W25Q64 的 ID 应该是:0xEF, 0x40, 0x17
    if (manufacturer_id == 0xEF && device_id_high == 0x40 && device_id_low == 0x17)
    {
        // 通信正常
    }
    else
    {
        // 通信异常,检查硬件连接
    }
}
我曾经遇到的坑: 读 ID 一直返回 0xFF。查了半天,发现是 CS 引脚没配置成推挽输出。默认是开漏,拉不低。嗯,硬件初始化一定要检查 GPIO 模式。

2.7 本章小结

这一章我们搞清楚了:

  • SPI 协议的四根线和四种模式。
  • W25Q64 的内部结构和选型要点。
  • 硬件连接和读 ID 的代码实现。