SPI通信原理与练习记录
SPI(串行外设接口)是一种高速、全双工、同步串行通信总线,因其协议简洁、配置灵活、传输速率高,被广泛用于FLASH、触摸屏、OLED、传感器等外设的通信场景。本文以野火霸道V2(STM32F103ZET6)开发板为载体,从底层原理讲起,逐步解析SPI硬件架构、四种工作模式、标准库配置流程,并给出回环自测与W25Q64串行FLASH读写两个实战案例,帮助读者快速掌握STM32平台下的SPI通信开发。
一、SPI通信核心原理
1.1 总线物理结构
SPI采用四线制标准接口,无设备地址寻址机制,通过独立的片选线区分从设备,硬件架构简洁高效。四条信号线功能如下:
-
SCK(串行时钟):由主设备产生,用于同步数据传输,时钟频率可软件配置。STM32F103的SPI时钟最高可达18 MHz(主频72 MHz、4分频)。
-
MOSI(主发从收):主设备数据输出线,用于向从设备发送指令和数据。
-
MISO(主收从发):主设备数据输入线,用于接收从设备返回的状态或数据。
-
CS/NSS(片选):从设备使能信号,低电平有效。只有片选被拉低的从设备才会响应主设备,该机制支持一主多从架构。

SPI为全双工通信,MOSI和MISO可在同一时钟周期内同时收发数据,相比I²C等半双工总线,传输效率显著提升,非常适合高速外设通信。
1.2 四种工作模式
SPI的核心配置参数为时钟极性(CPOL) 和时钟相位(CPHA),二者组合决定数据采样和移位的时刻。通信双方必须工作在完全相同的模式下,否则数据会错乱。
| 模式 | CPOL(空闲电平) | CPHA(采样边沿) | 通信特性 | 典型应用 |
|---|---|---|---|---|
| 模式0 | 0(低) | 0(第一个边沿) | 空闲SCK低,上升沿采样,下降沿移位 | W25Q系列FLASH、OLED |
| 模式1 | 0(低) | 1(第二个边沿) | 空闲SCK低,下降沿采样,上升沿移位 | 部分传感器 |
| 模式2 | 1(高) | 0(第一个边沿) | 空闲SCK高,下降沿采样,上升沿移位 | 部分触摸芯片 |
| 模式3 | 1(高) | 1(第二个边沿) | 空闲SCK高,上升沿采样,下降沿移位 | XPT2046触摸屏 |
野火霸道V2板载的W25Q64 FLASH默认使用模式0,这也是嵌入式开发中最常用的配置。
二、野火霸道V2 SPI硬件资源
STM32F103ZET6内置3个硬件SPI外设(SPI1、SPI2、SPI3),各外设的引脚分配、总线时钟域和适用场景有所不同。板载外设已固定连接至特定SPI接口,开发者可直接使用。
2.1 SPI外设引脚映射
| SPI外设 | SCK引脚 | MOSI引脚 | MISO引脚 | 总线时钟 | 典型用途 |
|---|---|---|---|---|---|
| SPI1 | PA5 | PA7 | PA6 | APB2(高速) | 板载W25Q64 FLASH(默认) |
| SPI2 | PB13 | PB15 | PB14 | APB1 | 外接触摸屏、传感器 |
| SPI3 | PB3 | PB5 | PB4 | APB1 | 拓展接口,支持引脚重映射,可用于回环测试 |
2.2 板载默认外设配置
-
SPI1 :与板载W25Q64 FLASH硬件绑定,片选引脚为PA4(软件控制),无需外接任何线缆即可进行FLASH读写实验,是新手入门的最佳实践对象。
-
SPI2:可用于连接XPT2046电阻触摸屏,适配触控交互开发场景。
三、SPI标准库开发配置流程
以下以最常用的**SPI1(连接W25Q64)**为例,完整展示标准库下的初始化与收发函数封装。
3.1 时钟使能
SPI1挂载于APB2总线,对应GPIOA也需开启时钟:
c
cpp
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA, ENABLE);
3.2 GPIO引脚模式配置
SPI引脚需设置为复用功能模式,其中SCK和MOSI为推挽输出,MISO为浮空或上拉输入,CS作为普通GPIO输出(软件片选):
c
cpp
GPIO_InitTypeDef GPIO_InitStruct;
// SCK(PA5) 和 MOSI(PA7) → 复用推挽输出
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);
// MISO(PA6) → 上拉输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// CS(PA4) → 普通推挽输出(软件控制)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 默认片选拉高,不选中从设备
GPIO_SetBits(GPIOA, GPIO_Pin_4);
3.3 SPI核心参数配置
根据W25Q64的模式0要求,配置SPI为主设备、全双工、8位数据格式、软件NSS,时钟分频设置为4(得到18 MHz):
c
cpp
SPI_InitTypeDef SPI_InitStruct;
SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low; // 空闲低电平
SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge; // 第一个边沿采样(模式0)
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; // 软件控制NSS
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 18 MHz
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB; // 高位先传
SPI_InitStruct.SPI_CRCPolynomial = 7; // CRC多项式(不使用时可忽略)
SPI_Init(SPI1, &SPI_InitStruct);
SPI_Cmd(SPI1, ENABLE);
3.4 数据收发函数封装
SPI的收发是同步进行的:发送一个字节的同时会接收一个字节。封装如下基础函数,后续所有操作均依赖它:
c
cpp
/**
* @brief 通过SPI1发送一个字节并同时接收一个字节
* @param data: 待发送的数据
* @retval 接收到的数据
*/
uint8_t SPI1_SendRecvByte(uint8_t data)
{
while (SPI_GetFlagStatus(SPI1, SPI_FLAG_TXE) == RESET); // 等待发送缓冲区空
SPI_SendData(SPI1, data);
while (SPI_GetFlagStatus(SPI1, SPI_FLAG_RXNE) == RESET); // 等待接收完成
return SPI_ReceiveData(SPI1);
}
四、练习案例1:SPI回环自测
回环测试通过短接开发板的MOSI 与MISO引脚,实现自发自收,无需外接任何从设备,是验证SPI硬件和软件配置正确性的快速手段。
4.1 硬件连接
使用跳线帽短接野火霸道V2板上SPI3 对应的PB5(MOSI) 和**PB4(MISO)**引脚(若使用SPI1,则短接PA7与PA6,但注意SPI1已板载FLASH,可能产生冲突,故推荐用SPI3)。
4.2 测试原理
主设备发送特定数据,数据经外部短接线路直接返回至接收引脚。若收发一致,则说明SPI时钟、引脚、配置均正常。
4.3 核心代码
c
cpp
void SPI3_Loopback_Test(void)
{
uint8_t tx_data = 0xA5;
uint8_t rx_data = 0;
SPI3_Init(); // 初始化SPI3(模式0,全双工)
rx_data = SPI3_SendRecvByte(tx_data);
if (rx_data == tx_data)
printf("SPI回环测试通过!发送:0x%02X 接收:0x%02X\r\n", tx_data, rx_data);
else
printf("SPI回环测试失败!发送:0x%02X 接收:0x%02X\r\n", tx_data, rx_data);
}
若输出"通过",则可确信SPI底层驱动工作正常。
五、练习案例2:W25Q64 FLASH读写
野火霸道V2板载W25Q64(8 MB)串行FLASH,通过SPI1连接。本节演示如何对FLASH指定地址进行数据写入与读取,验证实际外设通信能力。

5.1 FLASH操作基本时序
W25Q系列通信遵循固定指令序列:
片选拉低 → 发送指令码 → 发送地址(3字节) → 读/写数据 → 片选拉高。常用指令包括:
-
读数据 (0x03)
-
页写入 (0x02) ------ 需先发写使能 (0x06)
-
扇区擦除 (0x20) ------ 写入前必须擦除目标扇区
5.2 核心函数实现
c
cpp
#define SPI_FLASH_CS_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_4)
#define SPI_FLASH_CS_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_4)
/**
* @brief 从W25Q指定地址读取1字节
*/
uint8_t W25Q_ReadByte(uint32_t addr)
{
uint8_t data;
SPI_FLASH_CS_LOW();
SPI1_SendRecvByte(0x03); // 读指令
SPI1_SendRecvByte((addr >> 16) & 0xFF); // 地址高8位
SPI1_SendRecvByte((addr >> 8) & 0xFF); // 地址中8位
SPI1_SendRecvByte(addr & 0xFF); // 地址低8位
data = SPI1_SendRecvByte(0xFF); // 发送任意字节,同时读取
SPI_FLASH_CS_HIGH();
return data;
}
/**
* @brief 向W25Q指定地址页写入数据(长度不超过256字节)
*/
void W25Q_PageWrite(uint32_t addr, uint8_t *pBuf, uint16_t len)
{
uint16_t i;
// 写使能
SPI_FLASH_CS_LOW();
SPI1_SendRecvByte(0x06);
SPI_FLASH_CS_HIGH();
delay_ms(1);
// 页写入指令 + 地址
SPI_FLASH_CS_LOW();
SPI1_SendRecvByte(0x02);
SPI1_SendRecvByte((addr >> 16) & 0xFF);
SPI1_SendRecvByte((addr >> 8) & 0xFF);
SPI1_SendRecvByte(addr & 0xFF);
for (i = 0; i < len; i++) {
SPI1_SendRecvByte(pBuf[i]);
}
SPI_FLASH_CS_HIGH();
delay_ms(10); // 等待页编程完成
}
5.3 完整测试流程
-
初始化SPI1及GPIO;
-
擦除待写入的扇区(如地址0x000000所在扇区);
-
调用页写入函数写入一串测试数据;
-
从相同地址回读数据;
-
对比写入与读出的内容,若一致则说明通信正常。
注意:FLASH写入前必须先擦除(将目标扇区全部置为0xFF),否则非0xFF的位无法被改写为0。
六、常见问题与排查方法
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 收发数据错乱 | CPOL/CPHA不匹配;时钟频率过高;数据位/顺序配置错误 | 核对从设备数据手册,确保模式一致;降低分频系数;检查LSB/MSB设置 |
| 读不到外设数据 | CS未拉低;MISO引脚模式错误(如设为输出);硬件虚焊 | 用逻辑分析仪检查CS时序;确认GPIO模式为输入;检查电气连接 |
| 回环测试失败 | MOSI与MISO未正确短接;SPI时钟未使能;外设未使能 | 检查跳线;确认SPI_Cmd(ENABLE)已调用;检查时钟使能函数 |
| FLASH写入读出不一致 | 未先擦除目标扇区;写使能未生效;地址越界 | 写入前执行扇区擦除;检查写使能指令是否发送成功;确保地址在有效范围 |
七、总结
本文从SPI总线原理出发,结合野火霸道V2(STM32F103ZET6)开发板,详细讲解了SPI的硬件资源、四种工作模式、标准库配置流程,并给出了回环自测和W25Q64 FLASH读写的完整实战代码。SPI开发的关键在于模式匹配、时序正确、引脚配置无误。借助回环测试可快速验证驱动基础,而板载FLASH实验则能完整检验外设通信能力。读者可基于本文提供的代码框架,轻松扩展至OLED、触摸屏、各类传感器等更多SPI设备的开发。