SPI(Serial Peripheral Interface)是一种广泛应用的高速、全双工、同步串行通信协议。本质上是主从结构的总线协议,适用于MCU与各种外设间的短距离高速通信。
SPI协议基础
硬件连接
SPI通信需要四根信号线:
| 信号 | 全称 | 方向 | 说明 |
|---|---|---|---|
| SCLK | Serial Clock | Master→Slave | 时钟信号,由主机产生 |
| MOSI | Master Out Slave In | Master→Slave | 主机发送数据 |
| MISO | Master In Slave Out | Slave→Master | 主机接收数据 |
| CS/SS | Chip Select / Slave Select | Master→Slave | 片选信号,选择通信的从设备 |
MCU (Master) Slave Device
========== =============
+-------------+ +-------------+
| SCLK|------------->|SCLK |
| MOSI|------------->|MOSI |
| MISO|<-------------|MISO |
| CS |------------->|CS |
+-------------+ +-------------+
多从设备连接
MCU (Master)
==========
+---------------+
| SCLK|-----------------------------------+
| MOSI|-----------------------------------+
| MISO|<----------------------------------+
| CS0 |-------------------+ |
| CS1 |--------------------------+ |
| CS2 |-----------------------------+ |
+---------------+ | |
| | | | | |
v v v v v v
+-----+-----+-----+ +-----+-----+-----+
| CS0 | CS1 | CS2 | | SS0 | SS1 | SS2 |
+-----+-----+-----+ +-----+-----+-----+
- 独立片选模式:每从设备独占一个CS引脚,最多可连接n个从设备
- 级联模式:多个从设备共享同一CS,数据线串联
SPI工作模式
SPI有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)共同决定:
时钟极性 (CPOL)
| CPOL | 空闲时钟状态 | 时钟有效状态 |
|---|---|---|
| 0 | 低电平 | 高电平 |
| 1 | 高电平 | 低电平 |
时钟相位 (CPHA)
| CPHA | 数据采样时机 | 数据输出时机 |
|---|---|---|
| 0 | 第一个时钟边沿 | 第二个时钟边沿 |
| 1 | 第二个时钟边沿 | 第一个时钟边沿 |
四种模式详解
/*
* 四种SPI模式
*
* CPOL=0, CPHA=0 (Mode 0)
* - 空闲SCLK=0
* - 第一个边沿(上升沿)采样
* - 下降沿输出
*
* CPOL=0, CPHA=1 (Mode 1)
* - 空闲SCLK=0
* - 下降沿采样
* - 上升沿输出
*
* CPOL=1, CPHA=0 (Mode 2)
* - 空闲SCLK=1
* - 下降沿采样
* - 上升沿输出
*
* CPOL=1, CPHA=1 (Mode 3)
* - 空闲SCLK=1
* - 上升沿采样
* - 下降沿输出
*/
// 常用模式0的时序
/*
SCLK __|---|___|---|___|---|___|---|___
| | | | | | | |
MOSI ---< D7 >--< D6 >--< D5 >--< D4 >---
| | | | | | | |
MISO ---< D7 >--< D6 >--< D5 >--< D4 >---
^ ^ ^ ^
采样 采样 采样 采样
(上升沿)
*/
模式选择指南
| 模式 | CPOL | CPHA | 适用场景 |
|---|---|---|---|
| Mode 0 | 0 | 0 | 最常用,大多数SPI器件默认模式 |
| Mode 1 | 0 | 1 | 部分存储芯片 |
| Mode 2 | 1 | 0 | 较少使用 |
| Mode 3 | 1 | 1 | 高速通信,部分传感器 |
GPIO模拟SPI
当MCU硬件SPI不可用或需要更多控制时,可用GPIO模拟SPI协议:
/*
* GPIO模拟SPI - 基于STM32 HAL
*/
// 引脚定义
#define SPI_GPIO_PORT GPIOA
#define SPI_SCK_PIN GPIO_PIN_5
#define SPI_MOSI_PIN GPIO_PIN_7
#define SPI_MISO_PIN GPIO_PIN_6
#define SPI_CS_PORT GPIOA
#define SPI_CS_PIN GPIO_PIN_4
// GPIO初始化
void SPI_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIO时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
// SCK和MOSI配置为输出
GPIO_InitStruct.Pin = SPI_SCK_PIN | SPI_MOSI_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);
// MISO配置为输入
GPIO_InitStruct.Pin = SPI_MISO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);
// CS配置为输出
GPIO_InitStruct.Pin = SPI_CS_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(SPI_CS_PORT, &GPIO_InitStruct);
// 初始状态:CS=1, SCK=0
HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET);
}
// SPI模式0:CPOL=0, CPHA=0
void SPI_GPIO_Init_Mode0(void) {
// Mode 0: 空闲SCK=0, 第一个边沿采样
HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET);
}
// SPI模式3:CPOL=1, CPHA=1
void SPI_GPIO_Init_Mode3(void) {
// Mode 3: 空闲SCK=1, 第二个边沿采样
HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_SET);
}
// 发送一个字节
void SPI_GPIO_SendByte(uint8_t data) {
for (int i = 7; i >= 0; i--) {
// 设置MOSI数据
if (data & (1 << i)) {
HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_MOSI_PIN, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_MOSI_PIN, GPIO_PIN_RESET);
}
// 产生时钟边沿 - Mode 0先采样再输出
// 实际上Mode 0第一个边沿(上升沿)采样,所以先拉高SCK再拉低
HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_SET); // 采样点
delay_us(1);
HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET); // 完成一位
delay_us(1);
}
}
// 接收一个字节
uint8_t SPI_GPIO_RecvByte(void) {
uint8_t data = 0;
for (int i = 7; i >= 0; i--) {
// Mode 0: 上升沿采样
HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_SET);
delay_us(1);
if (HAL_GPIO_ReadPin(SPI_GPIO_PORT, SPI_MISO_PIN) == GPIO_PIN_SET) {
data |= (1 << i);
}
HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET);
delay_us(1);
}
return data;
}
// 全双工收发
uint8_t SPI_GPIO_Transfer(uint8_t data) {
uint8_t received = 0;
for (int i = 7; i >= 0; i--) {
// 设置MOSI
if (data & (1 << i)) {
HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_MOSI_PIN, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_MOSI_PIN, GPIO_PIN_RESET);
}
// 时钟上升沿采样MISO
HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_SET);
delay_us(1);
if (HAL_GPIO_ReadPin(SPI_GPIO_PORT, SPI_MISO_PIN)) {
received |= (1 << i);
}
// 时钟下降沿
HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET);
delay_us(1);
}
return received;
}
// 完整的SPI读写操作
uint16_t SPI_GPIO_WriteRead(uint8_t *tx_buf, uint8_t *rx_buf, uint16_t len) {
HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_RESET);
for (uint16_t i = 0; i < len; i++) {
rx_buf[i] = SPI_GPIO_Transfer(tx_buf[i]);
}
HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_SET);
return len;
}
硬件SPI配置
使用MCU内置的SPI控制器,效率更高:
/*
* STM32 HAL库硬件SPI配置
*/
// SPI句柄
SPI_HandleTypeDef hspi1;
// SPI1初始化 (APB2时钟 84MHz)
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; // 8位数据
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0, Mode 0
hspi1.Init.NSS = SPI_NSS_SOFT; // 软件片选
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 84/8=10.5MHz
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; // 高位先行
hspi1.Init.TIMode = SPI_TIMODE_DISABLE; // 非TI模式
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 7;
if (HAL_SPI_Init(&hspi1) != HAL_OK) {
Error_Handler();
}
}
// SPI引脚配置 (复用功能)
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (hspi->Instance == SPI1) {
__HAL_RCC_SPI1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// PA5: SCK, PA6: MISO, PA7: MOSI
GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
}
典型外设驱动实现
Flash存储芯片驱动 (W25Qxx)
/*
* W25Q64 Flash芯片驱动 (SPI接口)
* 容量: 8MB (64Mbit)
*/
#define W25Q_OK 0
#define W25Q_ERROR 1
// W25Qxx命令
#define W25Q_READ_STATUS_REG1 0x05
#define W25Q_WRITE_ENABLE 0x06
#define W25Q_WRITE_DISABLE 0x04
#define W25Q_READ_DATA 0x03
#define W25Q_FAST_READ 0x0B
#define W25Q_PAGE_PROGRAM 0x02
#define W25Q_SECTOR_ERASE_4KB 0x20
#define W25Q_BLOCK_ERASE_64KB 0xD8
#define W25Q_CHIP_ERASE 0xC7
#define W25Q_JEDEC_ID 0x9F
typedef struct {
SPI_HandleTypeDef *hspi;
GPIO_TypeDef *cs_port;
uint16_t cs_pin;
uint8_t status;
} W25Qxx_HandleTypeDef;
W25Qxx_HandleTypeDef g_w25q;
// 初始化
int W25Q_Init(SPI_HandleTypeDef *hspi, GPIO_TypeDef *cs_port, uint16_t cs_pin) {
g_w25q.hspi = hspi;
g_w25q.cs_port = cs_port;
g_w25q.cs_pin = cs_pin;
HAL_GPIO_WritePin(cs_port, cs_pin, GPIO_PIN_SET);
// 读取ID验证通信
uint8_t id[3];
W25Q_ReadJEDECID(id);
if (id[0] != 0xEF || id[1] != 0x40 || id[2] != 0x17) {
return W25Q_ERROR;
}
return W25Q_OK;
}
// 等待Flash空闲
void W25Q_WaitBusy(void) {
uint8_t status;
do {
W25Q_ReadStatus(&status, 1);
} while (status & 0x01);
}
// 读取状态寄存器
void W25Q_ReadStatus(uint8_t *status, uint8_t len) {
HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_RESET);
uint8_t cmd = W25Q_READ_STATUS_REG1;
HAL_SPI_Transmit(g_w25q.hspi, &cmd, 1, 100);
HAL_SPI_Receive(g_w25q.hspi, status, len, 100);
HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET);
}
// 发送命令
void W25Q_SendCommand(uint8_t cmd) {
HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_RESET);
HAL_SPI_Transmit(g_w25q.hspi, &cmd, 1, 100);
}
// 读取JEDEC ID
void W25Q_ReadJEDECID(uint8_t *id) {
W25Q_SendCommand(W25Q_JEDEC_ID);
HAL_SPI_Receive(g_w25q.hspi, id, 3, 100);
HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET);
}
// 使能写操作
void W25Q_WriteEnable(void) {
W25Q_SendCommand(W25Q_WRITE_ENABLE);
HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET);
}
// 扇区擦除 (4KB)
int W25Q_EraseSector(uint32_t addr) {
W25Q_WaitBusy();
W25Q_WriteEnable();
uint8_t cmd[4] = {W25Q_SECTOR_ERASE_4KB,
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
addr & 0xFF};
HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_RESET);
HAL_SPI_Transmit(g_w25q.hspi, cmd, 4, 100);
HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET);
W25Q_WaitBusy();
return W25Q_OK;
}
// 页编程 (256字节)
int W25Q_PageProgram(uint32_t addr, uint8_t *data, uint32_t len) {
if (len > 256) return W25Q_ERROR;
W25Q_WaitBusy();
W25Q_WriteEnable();
uint8_t cmd[4] = {W25Q_PAGE_PROGRAM,
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
addr & 0xFF};
HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_RESET);
HAL_SPI_Transmit(g_w25q.hspi, cmd, 4, 100);
HAL_SPI_Transmit(g_w25q.hspi, data, len, 100);
HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET);
W25Q_WaitBusy();
return W25Q_OK;
}
// 读取数据
int W25Q_ReadData(uint32_t addr, uint8_t *buf, uint32_t len) {
W25Q_WaitBusy();
uint8_t cmd[4] = {W25Q_READ_DATA,
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
addr & 0xFF};
HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_RESET);
HAL_SPI_Transmit(g_w25q.hspi, cmd, 4, 100);
HAL_SPI_Receive(g_w25q.hspi, buf, len, 100);
HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET);
return W25Q_OK;
}
常见问题与解决方案
1. 时钟速率过高
/*
* 问题:时钟速率过高导致数据错乱
*
* 原因:
* - MCU时钟过快
* - 线缆过长
* - 从设备不支持高速
*
* 解决:根据从设备最高支持频率降低波特率
*/
// STM32分频设置
#define SPI_BAUDRATEPRESCALER_2 0x00 // 42MHz (最快)
#define SPI_BAUDRATEPRESCALER_4 0x08 // 21MHz
#define SPI_BAUDRATEPRESCALER_8 0x10 // 10.5MHz
#define SPI_BAUDRATEPRESCALER_16 0x18 // 5.25MHz
#define SPI_BAUDRATEPRESCALER_256 0x70 // ~328KHz (最慢)
// 根据从设备选择合适分频
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 10.5MHz通常安全
2. 模式不匹配
/*
* 问题:读取数据全为0xFF或错误值
*
* 原因:主机与从机的CPOL/CPHA不匹配
*
* 解决:仔细阅读从设备数据手册,确认正确模式
*/
// 常见设备的SPI模式
/*
* W25Qxx Flash: Mode 0 或 Mode 3 (支持两种)
* SD卡: Mode 0
* NRF24L01: Mode 0
* OLED (SSD1306): Mode 0
* ADXL345: Mode 3
* MCP3008: Mode 0
*/
// 调试建议:先用示波器观察SCLK和MOSI/MISO波形
3. CS片选时序问题
/*
* 问题:片选时序不对导致通信失败
*
* 常见问题:
* - 片选提前拉高
* - 片选持续时间不够
* - 多字节传输时片选中途释放
*/
// 正确做法:整个传输过程保持CS低
void SPI_TransferFull(uint8_t *tx_buf, uint8_t *rx_buf, uint16_t len) {
HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET); // CS低
HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, len, 1000);
// 等待传输完成
while (HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY);
HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET); // CS高
}
// 反面例子:多字节分开传输
void BAD_SpiTransfer(uint8_t *data, uint16_t len) {
for (uint16_t i = 0; i < len; i++) {
HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, &data[i], 1, 100);
HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET); // 错误!每字节CS都释放
}
}
4. 数据对齐问题
/*
* 问题:16位/32位数据通信时高低字节错位
*
* 原因:大小端(MSB/LSB)设置不一致
*
* 解决:统一设置为高位先行(MSB First)
*/
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; // 高位先行
// 验证方法:发送0x80,观察第一位是否正确
uint8_t test = 0x80; // 二进制: 10000000
// 如果第一位是1,则MSB优先正确
性能优化
DMA加速
/*
* 使用DMA进行SPI传输,减少CPU干预
*/
SPI_HandleTypeDef hspi1;
DMA_HandleTypeDef hdma_spi1_tx;
DMA_HandleTypeDef hdma_spi1_rx;
// DMA初始化
void SPI_DMA_Init(void) {
__HAL_RCC_DMA2_CLK_ENABLE();
hdma_spi1_tx.Instance = DMA2_Stream3;
hdma_spi1_tx.Init.Channel = DMA_CHANNEL_3;
hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi1_tx.Init.Mode = DMA_NORMAL;
hdma_spi1_tx.Init.Priority = DMA_PRIORITY_MEDIUM;
HAL_DMA_Init(&hdma_spi1_tx);
__HAL_LINKDMA(&hspi1, hdmatx, hdma_spi1_tx);
// 同样配置RX DMA...
}
// DMA传输
HAL_StatusTypeDef SPI_TransmitDMA(uint8_t *data, uint16_t len) {
return HAL_SPI_Transmit_DMA(&hspi1, data, len);
}
// DMA完成回调
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
// 传输完成,可以通知其他任务
}
总结
使用SPI协议的关键点:
- 确认工作模式:仔细阅读从设备数据手册,选择正确的CPOL和CPHA
- 控制时钟速率:确保不超过从设备支持的最大频率
- 注意片选时序:整个传输过程保持CS有效
- 大小端一致:主机与从机的位顺序要匹配
- 错误处理:实现超时检测和错误恢复机制
希望这篇深度解析能帮助你更好地掌握SPI通信协议!