STM32实战:SPI通信指南
一、SPI通信基础
SPI(Serial Peripheral Interface,串行外设接口)是摩托罗拉公司推出的一种高速、全双工、同步的串行通信总线协议,广泛应用于微控制器与各种外设(如Flash存储器、SD卡、LCD显示屏、传感器等)之间的通信。
1.1 SPI通信特点
- 全双工同步通信:主从设备可以同时发送和接收数据
- 高速传输:通常可达几MHz到几十MHz的时钟频率
- 主从架构:一个主设备可控制多个从设备
- 硬件简单:仅需4根信号线即可实现通信
- 无寻址机制:通过片选信号选择从设备
1.2 SPI信号线
SPI通信需要4根基本信号线:
- SCK(Serial Clock):时钟信号,由主设备产生
- MOSI(Master Out Slave In):主设备输出,从设备输入
- MISO(Master In Slave Out):主设备输入,从设备输出
- NSS/CS(Slave Select):片选信号,低电平有效
二、STM32 SPI外设
STM32系列微控制器通常集成多个SPI接口(如SPI1、SPI2、SPI3等),支持主从模式、全双工/半双工通信、多种数据格式和时钟极性/相位配置。
2.1 SPI工作模式
STM32 SPI支持以下工作模式:
- 全双工模式:同时发送和接收数据(最常用)
- 半双工模式:同一时刻只能发送或接收
- 只发送模式:仅发送数据,不接收
- 只接收模式:仅接收数据,不发送
2.2 时钟配置
SPI时钟配置涉及两个关键参数:
- CPOL(Clock Polarity):时钟极性
- CPOL=0:空闲时SCK为低电平
- CPOL=1:空闲时SCK为高电平
- CPHA(Clock Phase):时钟相位
- CPHA=0:数据在第一个时钟边沿采样
- CPHA=1:数据在第二个时钟边沿采样
CPOL和CPHA的组合形成4种SPI模式(Mode 0-3),通信双方必须配置相同的模式。
三、STM32 SPI编程实战
下面以STM32F103系列为例,演示如何使用HAL库进行SPI通信。
3.1 SPI初始化配置
#include "stm32f1xx_hal.h" SPI_HandleTypeDef hspi1; void SPI1_Init(void) { __HAL_RCC_SPI1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); // GPIO配置 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; // SCK, MISO, MOSI GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // SPI配置 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 (Mode 0) hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 9MHz (72MHz/8) 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); }3.2 SPI数据收发函数
// 发送一个字节并接收一个字节 uint8_t SPI_TransmitReceive(uint8_t txData) { uint8_t rxData = 0; // 拉低片选(软件控制) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // 发送并接收数据 HAL_SPI_TransmitReceive(&hspi1, &txData, &rxData, 1, 1000); // 释放片选 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); return rxData; } // 发送多个字节 void SPI_Transmit(uint8_t *pData, uint16_t Size) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, pData, Size, 1000); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); } // 接收多个字节 void SPI_Receive(uint8_t *pData, uint16_t Size) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_SPI_Receive(&hspi1, pData, Size, 1000); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); }3.3 与W25Qxx Flash通信示例
// W25Qxx指令定义 #define W25X_WriteEnable 0x06 #define W25X_WriteDisable 0x04 #define W25X_ReadStatusReg 0x05 #define W25X_WriteStatusReg 0x01 #define W25X_ReadData 0x03 #define W25X_FastReadData 0x0B #define W25X_PageProgram 0x02 #define W25X_SectorErase 0x20 // 读取Flash ID uint32_t W25Q_ReadID(void) { uint32_t Temp = 0; uint8_t cmd[4] = {0x90, 0x00, 0x00, 0x00}; // 读取ID指令 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, cmd, 4, 1000); HAL_SPI_Receive(&hspi1, (uint8_t*)&Temp, 3, 1000); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); return Temp; } // 读取数据 void W25Q_Read(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead) { uint8_t cmd[4]; cmd[0] = W25X_ReadData; cmd[1] = (uint8_t)((ReadAddr & 0xFF0000) >> 16); cmd[2] = (uint8_t)((ReadAddr & 0xFF00) >> 8); cmd[3] = (uint8_t)(ReadAddr & 0xFF); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, cmd, 4, 1000); HAL_SPI_Receive(&hspi1, pBuffer, NumByteToRead, 1000); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); }四、特殊使用:软件模拟SPI通信
当MCU外设不够时,可以使用GPIO模拟SPI进行通信,对于调试SPI有着极大的灵活性,但会带来更高的 CPU 占用和更复杂的时序控制。
4.1 引脚初始化
配置GPIO引脚为输出模式(SCLK、MOSI、SS)和输入模式(MISO),初始化时钟线和片选线为高电平(根据SPI模式调整)。
// 示例代码(基于STM32 HAL库) void SPI_GPIO_Init() { GPIO_InitTypeDef GPIO_InitStruct = {0}; // 配置SCLK、MOSI、SS为输出 GPIO_InitStruct.Pin = SPI_SCLK_PIN | SPI_MOSI_PIN | SPI_SS_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(SPI_PORT, &GPIO_InitStruct); // 配置MISO为输入 GPIO_InitStruct.Pin = SPI_MISO_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; HAL_GPIO_Init(SPI_PORT, &GPIO_InitStruct); // 初始状态:片选高(不选中),时钟高(模式0) HAL_GPIO_WritePin(SPI_PORT, SPI_SS_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(SPI_PORT, SPI_SCLK_PIN, GPIO_PIN_SET); }4.2 数据传输函数
根据SPI模式(如模式0:时钟空闲为高,数据在上升沿采样)实现逐位读写:
uint8_t SPI_Sim_TransferByte(uint8_t tx_data) { uint8_t rx_data = 0; for (uint8_t i = 0; i < 8; i++) { if (tx_data & 0x80) { HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI2_MOSI_Pin, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI2_MOSI_Pin, GPIO_PIN_RESET); } HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI2_CLK_Pin, GPIO_PIN_SET); rx_data <<= 1; if (HAL_GPIO_ReadPin(SPI_GPIO_PORT, SPI2_MISO_Pin) == GPIO_PIN_SET) { rx_data |= 0x01; } HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI2_CLK_Pin, GPIO_PIN_RESET); tx_data <<= 1; } return rx_data; } uint32_t SCL3300_Sim_Transceive(uint32_t cmd) { uint32_t response = 0; uint8_t *cmd_bytes = (uint8_t*)&cmd; uint8_t *resp_bytes = (uint8_t*)&response; HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI2_CS_Pin, GPIO_PIN_RESET); for (int i = 3; i >= 0; i--) { resp_bytes[i] = SPI_Sim_TransferByte(cmd_bytes[i]); } HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI2_CS_Pin, GPIO_PIN_SET); HAL_Delay(1); return response; }4.3 时序调整
- 时钟频率:通过调整延时函数(如
Delay_us())控制SCLK速度,需匹配从设备要求。 - 模式支持:修改时钟极性和相位(CPOL/CPHA)以适配不同SPI模式。
4.4 注意事项
- 信号完整性:软件模拟SPI速率较低(通常<1MHz),高频应用需硬件SPI。
- 中断处理:避免在传输过程中被中断打断,可临时关闭中断。
- 多从设备:每个从设备需独立片选引脚,传输前选中对应设备。
4.5 验证方法
通过逻辑分析仪或示波器观察SCLK、MOSI/MISO波形,确保数据对齐时钟边沿,并检查从设备的响应数据。
五、SPI通信注意事项
5.1 时序匹配
- 主从设备必须配置相同的时钟极性和相位
- 注意时钟频率不能超过从设备的最大支持频率
- 考虑信号传输延迟,适当降低时钟频率
5.2 片选信号管理
- 片选信号必须在数据传输前拉低,传输完成后拉高
- 多从机系统中,同一时刻只能有一个片选有效
- 软件片选(NSS Soft)需要手动控制GPIO
5.3 数据对齐
- STM32支持8位和16位数据格式
- 注意MSB/LSB(高位在前/低位在前)设置
- 从设备的数据格式必须与主设备匹配
5.4 错误处理
- 检查SPI状态标志(BUSY、TXE、RXNE等)
- 实现超时机制,防止通信卡死
- 使用DMA传输时注意缓冲区管理
六、SPI与I2C、UART对比
| 特性 | SPI | I2C | UART |
|---|---|---|---|
| 通信方式 | 同步、全双工 | 同步、半双工 | 异步、全双工 |
| 信号线数量 | 4根(3+CS) | 2根 | 2根(TX、RX) |
| 传输速率 | 高(MHz级) | 中(KHz-MHz) | 低(通常<1Mbps) |
| 寻址方式 | 硬件片选 | 软件地址 | 点对点 |
| 硬件复杂度 | 简单 | 中等 | 简单 |
| 适用场景 | 高速外设 | 多设备低速通信 | 设备间通信 |
七、常见问题与调试技巧
7.1 通信失败排查步骤
- 检查电源和地线连接是否正常
- 确认时钟极性和相位配置一致
- 使用示波器或逻辑分析仪观察信号波形
- 检查片选信号时序是否正确
- 验证从设备是否处于就绪状态
7.2 性能优化建议
- 使用DMA传输减少CPU占用
- 合理设置SPI时钟分频器
- 批量传输数据,减少片选切换
- 使用硬件NSS信号提高稳定性
7.3 多从机系统设计
- 为每个从设备分配独立的片选引脚
- 考虑总线负载和信号完整性
- 实现从设备状态管理机制
- 注意从设备切换时的时序要求
八、总结
SPI是STM32与外部设备通信的重要接口,具有高速、全双工、硬件简单的优点。掌握SPI通信原理、STM32 SPI外设配置方法以及实际编程技巧,对于嵌入式开发至关重要。在实际项目中,需要根据具体外设的时序要求,合理配置SPI参数,并注意信号完整性和错误处理,确保通信的稳定可靠。
