06-SPI.md 6.1 KB

06 SPI 通信(HAL)

目标:SPI1 驱动 W25Q64(外置 Flash),实现按键切换 LED 状态并断电保存

原理简述

总线结构

SPI 总线结构

SPI 是 全双工、同步 串行通信协议,核心四线制:

  • SCK(Serial Clock)— 时钟线,由主机驱动
  • MOSI(Master Out Slave In)— 主机输出,从机输入
  • MISO(Master In Slave Out)— 主机输入,从机输出
  • CS/NSS(Chip Select)— 片选线,低电平有效,每个从机独占一根

数据传输过程中,一个时钟周期同时完成一位数据的发送和接收:

MOSI: 主机数据线 → 每个 SCK 周期移出一位
MISO: 从机数据线 → 每个 SCK 周期移出一位
全双工:收和发同时进行,互不干扰

时钟极性 (CPOL) 与时钟相位 (CPHA)

SPI 有 5 个可配置参数(铁头山羊课程中的 5 参数模型):

# 参数 说明
1 波特率 SCK 频率,由主机分频产生
2 数据位顺序 MSB First / LSB First
3 数据位长度 8 位 / 16 位
4 时钟极性 (CPOL) 空闲时 SCK 电平:0=低, 1=高
5 时钟相位 (CPHA) 数据采样边沿:0=第一边沿, 1=第二边沿

CPOL(时钟极性)

CPOL 示意图

  • CPOL = 0:SCK 空闲时为低电平
  • CPOL = 1:SCK 空闲时为高电平

CPHA(时钟相位) —— 数据传输分为"发送"和"采集"两个阶段:

CPHA 示意图

  • CPHA = 0:第一个边沿采集(采样),第二个边沿发送
  • CPHA = 1:第一个边沿发送,第二个边沿采集(采样)

4 种模式的完整时序

SPI 四种模式

模式 CPOL CPHA 说明
Mode 0 0 0 空闲低 → 上升沿采样(最常用)
Mode 1 0 1 空闲低 → 下降沿采样
Mode 2 1 0 空闲高 → 下降沿采样
Mode 3 1 1 空闲高 → 上升沿采样

CubeMX 配置

  • SPI1 → 模式 Full-Duplex Master
  • 参数:
    • Frame Format:Motorola
    • Data Size:8 Bits
    • First Bit:MSB First
    • Prescaler:32
    • 默认 8MHz 系统时钟 → SCK = 8MHz/32 = 250kHz
    • 配置 72MHz 后 → SCK = 72MHz/32 = 2.25MHz
    • W25Q64 支持最高 104MHz,2.25MHz 完全没问题
    • CPOL:Low
    • CPHA:1 Edge
    • NSS:Software(软件控制 CS)
  • 额外将 PA4 设为 GPIO_Output(用作软件 CS)

关键 HAL API

函数 说明
HAL_SPI_Transmit(&hspi, pData, Size, Timeout) 发送数据
HAL_SPI_Receive(&hspi, pData, Size, Timeout) 接收数据(内部发 0xFF 产生时钟)
HAL_SPI_TransmitReceive(&hspi, pTx, pRx, Size, Timeout) 同时收发

W25Q64 操作(完整示例)

/* CS 控制 */
#define W25Q64_CS_LOW()  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
#define W25Q64_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)

/* 写使能 (0x06) */
static void W25Q64_WriteEnable(void)
{
    uint8_t cmd = 0x06;
    W25Q64_CS_LOW();
    HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
    W25Q64_CS_HIGH();
}

/* 读状态寄存器 (0x05) */
static uint8_t W25Q64_ReadSR(void)
{
    uint8_t cmd[2] = {0x05, 0xFF};
    uint8_t rx[2] = {0};
    W25Q64_CS_LOW();
    HAL_SPI_TransmitReceive(&hspi1, cmd, rx, 2, HAL_MAX_DELAY);
    W25Q64_CS_HIGH();
    return rx[1];
}

/* 等待 BUSY 位清零(读状态寄存器 bit0) */
static void W25Q64_WaitBusy(void)
{
    uint8_t sr;
    do {
        uint8_t cmd[2] = {0x05, 0xFF};      // 读状态寄存器 1
        uint8_t rx[2] = {0};
        W25Q64_CS_LOW();
        HAL_SPI_TransmitReceive(&hspi1, cmd, rx, 2, HAL_MAX_DELAY);
        W25Q64_CS_HIGH();
        sr = rx[1];
    } while (sr & 0x01);                    // bit0=1 表示正在忙
}

/* 扇区擦除 (4KB, 0x20) — 地址自动按 4KB 对齐 */
static void W25Q64_SectorErase(uint32_t addr)
{
    uint8_t cmd[4] = {0x20, (addr>>16)&0xFF, (addr>>8)&0xFF, addr&0xFF};
    W25Q64_WriteEnable();
    W25Q64_CS_LOW();
    HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY);
    W25Q64_CS_HIGH();
    W25Q64_WaitBusy();                      // 比固定延时更可靠
}

/* 页写入 (256字节以内, 0x02) — 地址必须按 256 字节对齐 */
static void W25Q64_PageProgram(uint32_t addr, uint8_t *data, uint16_t len)
{
    uint8_t cmd[4] = {0x02, (addr>>16)&0xFF, (addr>>8)&0xFF, addr&0xFF};
    if (len > 256) len = 256;               // 页编程最多 256 字节
    W25Q64_WriteEnable();
    W25Q64_CS_LOW();
    HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY);
    HAL_SPI_Transmit(&hspi1, data, len, HAL_MAX_DELAY);
    W25Q64_CS_HIGH();
    W25Q64_WaitBusy();
}

/* 读取数据 (0x03) */
static void W25Q64_ReadData(uint32_t addr, uint8_t *buf, uint16_t len)
{
    uint8_t cmd[4] = {0x03, (addr>>16)&0xFF, (addr>>8)&0xFF, addr&0xFF};
    W25Q64_CS_LOW();
    HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY);
    HAL_SPI_Receive(&hspi1, buf, len, HAL_MAX_DELAY);
    W25Q64_CS_HIGH();
}

按键 + Flash 保存 LED 状态

uint8_t pre_key = 1, cur_key = 1;
uint8_t led_status;

led_status = LoadLEDState();               // 上电从 Flash 读取
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13,
    led_status ? GPIO_PIN_SET : GPIO_PIN_RESET);

while (1)
{
    pre_key = cur_key;
    cur_key = (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET) ? 0 : 1;

    if (pre_key != cur_key)
    {
        HAL_Delay(10);
        if (cur_key == 1)                   // 松开沿
        {
            led_status = !led_status;
            HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13,
                led_status ? GPIO_PIN_SET : GPIO_PIN_RESET);
            SaveLEDState(led_status);       // 保存到 Flash
        }
    }
}

常见坑点

  • ⚠️ SPI 的 NSS 用软件控制更简单,PA4 作为普通 GPIO 拉高拉低
  • ⚠️ W25Q64 写入前必须先擦除(只能 1→0,不能 0→1)
  • ⚠️ 每次写操作前必须发送 写使能命令 (0x06)
  • ⚠️ 分频后波特率不要超过从机支持的最大频率