# 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 ```c /*------------------------------------------------ * SPI_W25Q64.c * 硬件 SPI1: PA5(SCK), PA6(MISO), PA7(MOSI), PA4(CS) *------------------------------------------------*/ #include "stm32f10x.h" #include #include /* 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)