STM32的SPI通信原理与练习记录

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回环自测

回环测试通过短接开发板的MOSIMISO引脚,实现自发自收,无需外接任何从设备,是验证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 完整测试流程

  1. 初始化SPI1及GPIO;

  2. 擦除待写入的扇区(如地址0x000000所在扇区);

  3. 调用页写入函数写入一串测试数据;

  4. 从相同地址回读数据;

  5. 对比写入与读出的内容,若一致则说明通信正常。

注意: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设备的开发。

相关推荐
weixin_456808382 小时前
【沁恒蓝牙开发】IAP升级流程
c语言·单片机·嵌入式硬件
你有哈莫吗2 小时前
CMake构建学习笔记-iconv库的构建
笔记·学习
czhaii3 小时前
RTL8019AS、555,DS3231等芯片的跳线方式和引脚定义
嵌入式硬件
ctlover3 小时前
Python学习第4日
学习
geats人山人海3 小时前
数据结构第六章c语言 树的存储下二叉树的概念和存储
c语言·数据结构·算法
小弥儿4 小时前
GitHub今日热榜 | 2026-08-01:硬件安全工具与AI教育并行
人工智能·学习·开源·github
秋雨梧桐叶落莳4 小时前
计算器仿写总结
学习·macos·ios·objective-c·cocoa
影视飓风TIM4 小时前
Linux下C语言缓冲区原理 + Git版本控制
linux·c语言·git
统计学小王子5 小时前
《数学少年-从正负号到几何原本》(第一章:“刻度之上,零为分界“)--1.2 电梯与账单里的相反数字
学习·初中数学·初中数学科普·数学科普