14-SPI通信.md 11 KB

14-SPI通信

原理

SPI 协议

  • 类型:同步全双工
  • 线制:四线制 (SCK + MOSI + MISO + CS)
  • 设备:一主多从 (通过 CS 片选)

           +-------+
           |Master |---+ MISO
           |       |---+ MOSI
           |       |---+ SCK
           |       |---+ CS1
           |       |---+ CS2
           +-------+   |
             |  |  |   |
           +-------+   |
           |Slave1 |---+---+
           +-------+   |   |
           +-------+   |   |
           |Slave2 |---+---+
           +-------+
    

5 参数模型

参数 可选值 说明
波特率 f_PCLK / {2,4,8,16,32,64,128,256} 通信速率
数据位 8 位 / 16 位 每帧数据宽度
比特序 MSB first / LSB first 高位/低位先发
CPOL (时钟极性) 0=空闲低 / 1=空闲高 SCK 空闲电平
CPHA (时钟相位) 0=前沿采样 / 1=后沿采样 采样时刻

4 种 SPI 模式

Mode 0 (CPOL=0, CPHA=0) ★最常用
  SCK   ____/``\__/``\__/``\__
  MOSI      X---X---X---X
  MISO      X---X---X---X
  采样在上升沿 (前沿)

Mode 1 (CPOL=0, CPHA=1)
  SCK   ____/``\__/``\__/``\__
  采样在下降沿 (后沿)

Mode 2 (CPOL=1, CPHA=0)
  SCK   ```\____/``\____/``\__
  采样在下降沿 (前沿)

Mode 3 (CPOL=1, CPHA=1)
  SCK   ```\____/``\____/``\__
  采样在上升沿 (后沿)

W25Q64

参数
容量 64 Mbit = 8 MB
256 字节
扇区 (Sector) 4 KB (16 页)
块 (Block) 64 KB (16 扇区)
最大时钟 104 MHz
擦除寿命 约 10 万次

关键指令

指令 码值 说明
Write Enable 0x06 写使能 (每写之前必发)
Write Disable 0x04 写禁止
Read Status Register 0x05 读状态寄存器 (BUSY=bit0)
Read Data 0x03 读数据 (3 字节地址)
Page Program 0x02 页编程 (最多 256 字节)
Sector Erase 0x20 扇区擦除 (4KB)
Block Erase 64KB 0xD8 块擦除
Chip Erase 0xC7 整片擦除
Power Down 0xB9 掉电
Release Power Down 0xAB 唤醒
Read ID 0x90 读制造商/设备 ID
Read Unique ID 0x4B 读唯一 ID

Flash 特性:只能 1→0 (写 0),擦除恢复为 1。写入前必须擦除。


API 表格

软件 SPI

函数 说明
void SPI_Start(void) CS 拉低
void SPI_Stop(void) CS 拉高
uint8_t SPI_SwapByte(uint8_t Byte) 交换一个字节 (发送+接收)

硬件 SPI (STM32 SPI)

函数 说明
SPI_InitTypeDef 配置结构体
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct) 初始化 SPI
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState) 使能 SPI
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data) 发送数据
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx) 接收数据
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint32_t SPI_I2S_FLAG) 获取标志状态
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint32_t SPI_I2S_IT, FunctionalState NewState) 中断配置

SPI_InitTypeDef

成员 可选值 说明
SPI_Direction SPI_Direction_2Lines_FullDuplex 双线全双工
SPI_Mode SPI_Mode_Master / SPI_Mode_Slave 主/从模式
SPI_DataSize SPI_DataSize_8b / SPI_DataSize_16b 数据位
SPI_CPOL SPI_CPOL_Low / SPI_CPOL_High 时钟极性
SPI_CPHA SPI_CPHA_1Edge / SPI_CPHA_2Edge 时钟相位
SPI_NSS SPI_NSS_Soft / SPI_NSS_Hard NSS 控制方式
SPI_BaudRatePrescaler SPI_BaudRatePrescaler_* 波特率预分频
SPI_FirstBit SPI_FirstBit_MSB / SPI_FirstBit_LSB 比特序
SPI_CRCPolynomial 7 (默认) CRC 多项式

常用标志

标志 说明
SPI_FLAG_TXE 发送缓冲区空 (可写 DR)
SPI_FLAG_RXNE 接收缓冲区非空 (可读 DR)
SPI_FLAG_BSY 忙标志
SPI_FLAG_OVR 溢出错误

完整代码示例

硬件 SPI 读写 W25Q64

/*------------------------------------------------
 * SPI_W25Q64.c
 * 硬件 SPI1: PA5(SCK), PA6(MISO), PA7(MOSI), PA4(CS)
 *------------------------------------------------*/

#include "stm32f10x.h"
#include <stdint.h>
#include <string.h>

/* SPI1 引脚 */
#define W25Q64_CS_PORT      GPIOA
#define W25Q64_CS_PIN       GPIO_Pin_4

/* W25Q64 指令 */
#define W25X_WriteEnable    0x06
#define W25X_WriteDisable   0x04
#define W25X_ReadStatusReg  0x05
#define W25X_ReadData       0x03
#define W25X_PageProgram    0x02
#define W25X_SectorErase    0x20
#define W25X_ReadID         0x90

/* ---- CS 控制 ---- */
static void W25Q64_CS_Low(void)
{
    GPIO_ResetBits(W25Q64_CS_PORT, W25Q64_CS_PIN);
}
static void W25Q64_CS_High(void)
{
    GPIO_SetBits(W25Q64_CS_PORT, W25Q64_CS_PIN);
}

/* ---- 硬件 SPI 初始化 ---- */
static void SPI_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    SPI_InitTypeDef  SPI_InitStruct;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);

    /* PA5(SCK), PA7(MOSI): 复用推挽 */
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* PA6(MISO): 浮空输入 */
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* PA4(CS): 推挽输出 */
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    GPIO_SetBits(GPIOA, GPIO_Pin_4);   // CS 高 = 释放

    /* SPI1: Mode0, 主模式, 8位, 18MHz */
    SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
    SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
    SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low;
    SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge;
    SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;
    SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 72/4=18MHz
    SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
    SPI_InitStruct.SPI_CRCPolynomial = 7;
    SPI_Init(SPI1, &SPI_InitStruct);

    SPI_Cmd(SPI1, ENABLE);
}

/* ---- SPI 字节交换 ---- */
static uint8_t SPI_SwapByte(uint8_t Byte)
{
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
    SPI_I2S_SendData(SPI1, Byte);

    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
    return (uint8_t)SPI_I2S_ReceiveData(SPI1);
}

/* ---- 写指令/地址 (CS 已拉低) ---- */
static void W25Q64_WriteCmd(uint8_t cmd)
{
    SPI_SwapByte(cmd);
}

/* ---- 写使能 (每次擦/写之前必须) ---- */
static void W25Q64_WriteEnable(void)
{
    W25Q64_CS_Low();
    W25Q64_WriteCmd(W25X_WriteEnable);
    W25Q64_CS_High();
}

/* ---- 等待空闲 (BUSY=0) ---- */
static void W25Q64_WaitBusy(void)
{
    uint8_t sr;
    do {
        W25Q64_CS_Low();
        W25Q64_WriteCmd(W25X_ReadStatusReg);
        sr = SPI_SwapByte(0xFF);
        W25Q64_CS_High();
    } while (sr & 0x01);
}

/* ---- 读 ID ---- */
uint16_t W25Q64_ReadID(void)
{
    uint16_t id;
    W25Q64_CS_Low();
    W25Q64_WriteCmd(W25X_ReadID);
    SPI_SwapByte(0x00); SPI_SwapByte(0x00); SPI_SwapByte(0x00); // dummy
    id  = (uint16_t)SPI_SwapByte(0xFF) << 8;   // 制造商 ID
    id |= SPI_SwapByte(0xFF);                   // 设备 ID
    W25Q64_CS_High();
    return id;
    // W25Q64: 制造商=0xEF, 设备=0x16 (EF16)
}

/* ---- 读数据 ---- */
void W25Q64_ReadData(uint32_t addr, uint8_t* buf, uint32_t len)
{
    uint32_t i;
    W25Q64_CS_Low();
    W25Q64_WriteCmd(W25X_ReadData);
    SPI_SwapByte((addr >> 16) & 0xFF);
    SPI_SwapByte((addr >> 8)  & 0xFF);
    SPI_SwapByte(addr         & 0xFF);
    for (i = 0; i < len; i++)
        buf[i] = SPI_SwapByte(0xFF);
    W25Q64_CS_High();
}

/* ---- 扇区擦除 (4KB) ---- */
void W25Q64_SectorErase(uint32_t addr)
{
    W25Q64_WriteEnable();
    W25Q64_WaitBusy();
    W25Q64_CS_Low();
    W25Q64_WriteCmd(W25X_SectorErase);
    SPI_SwapByte((addr >> 16) & 0xFF);
    SPI_SwapByte((addr >> 8)  & 0xFF);
    SPI_SwapByte(addr         & 0xFF);
    W25Q64_CS_High();
    W25Q64_WaitBusy();
}

/* ---- 页编程 (最多 256 字节) ---- */
void W25Q64_PageProgram(uint32_t addr, const uint8_t* data, uint32_t len)
{
    uint32_t i;
    if (len > 256) len = 256;

    W25Q64_WriteEnable();
    W25Q64_WaitBusy();
    W25Q64_CS_Low();
    W25Q64_WriteCmd(W25X_PageProgram);
    SPI_SwapByte((addr >> 16) & 0xFF);
    SPI_SwapByte((addr >> 8)  & 0xFF);
    SPI_SwapByte(addr         & 0xFF);
    for (i = 0; i < len; i++)
        SPI_SwapByte(data[i]);
    W25Q64_CS_High();
    W25Q64_WaitBusy();
}

/* ---- 整片擦除 ---- */
void W25Q64_ChipErase(void)
{
    W25Q64_WriteEnable();
    W25Q64_WaitBusy();
    W25Q64_CS_Low();
    W25Q64_WriteCmd(0xC7);
    W25Q64_CS_High();
    W25Q64_WaitBusy();
}

/* ---- 主函数 ---- */
int main(void)
{
    uint8_t buf[256];
    uint16_t chipID;

    /* 初始化 SysTick 延时 (假设已在其他地方实现 Delay_ms) */
    SPI_Config();

    chipID = W25Q64_ReadID();

    /* 写入示例 */
    uint8_t writeData[8] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};

    W25Q64_SectorErase(0x000000);       // 擦除扇区 0
    W25Q64_PageProgram(0x000000, writeData, 8);  // 写 8 字节
    W25Q64_ReadData(0x000000, buf, 8);            // 读回验证

    while (1);
}

常见坑点

  1. SPI1 与 SPI2 挂载不同总线

    • SPI1 → APB2 (72MHz)
    • SPI2 → APB1 (36MHz)
    • 配置波特率时注意 APB 时钟不同,SPI2 最高 18MHz (36/2)
  2. NSS 软件控制更灵活

    • SPI_NSS_Soft 模式下,CS 用普通 GPIO 推挽输出手动拉低/拉高
    • 硬件 NSS 模式 (NSS_Hard) 在多从机时麻烦,不推荐
  3. 发送的同时也在接收

    • SPI 是全双工,发送一个字节的同时会收到一个字节
    • 读数据时也必须发送 (dummy byte = 0xFF)
    • 写数据时收到的字节应丢弃
  4. W25Q64 写入前必擦除

    • Flash 只能 1→0,写入前需要先擦除 (恢复全 1)
    • 擦除以扇区 (4KB) 为单位,不能单独擦字节
    • 只写 0 不擦除也能成功,但读回会不对
  5. 等待 BUSY

    • 每次擦除/写入后必须查状态寄存器 (0x05) bit0,直到为 0
    • 擦除扇区约 45~400ms,页编程约 1~3ms
  6. 页编程不可跨页

    • PageProgram 只能在 256 字节页内连续写
    • 跨页地址会自动回卷,不是续写到下一页
  7. Mode0 最常用

    • 绝大多数 SPI 芯片默认 Mode0 (CPOL=0, CPHA=0)
    • CPOL/CPHA 不匹配会导致通信失败
  8. GPIO 模式区分

    • MOSI/SCK/CS → 推挽输出 (GPIO_Mode_Out_PP) 或复用推挽 (GPIO_Mode_AF_PP)
    • MISO → 浮空输入 (GPIO_Mode_IN_FLOATING)