声明
文中内容为观看 BiliBili 视频【STM32入门教程-2023版 细致讲解 中文字幕】后学习并扩展总结。
本文章为个人学习使用,版面观感若有不适请谅解,文中知识仅代表个人观点,若出现错误,欢迎各位批评指正。
一、SPI 通信外设
1.1 SPI 外设简介
SPI(Serial Peripheral Interface,串行外设接口) 是一种高速、同步、全双工的串行通信总线,广泛用于STM32 微控制器与外部外设的短距离数据传输。STM32 内部集成硬件 SPI 收发电路,可自动完成时钟生成、数据收发等操作,大幅减轻 CPU 负担。其核心特性包括:可配置 8 位 / 16 位数据帧、高位先行/低位先行两种传输顺序;时钟频率由 fPCLK 经 2、4、8 等 8 种分频系数得到,适配不同外设需求;支持多主机模型、主从模式切换,可精简为半双工/单工通信,同时兼容 I2S 协议并支持 DMA 高速无阻塞传输。其中 STM32F103C8T6 集成 SPI1、SPI2 两个独立硬件 SPI 外设,SPI1 挂载 APB2 总线(最高 36MHz),SPI2 挂载 APB1 总线(最高 18MHz),可满足不同速率的通信需求,是嵌入式开发中常用的核心外设。
1.2 SPI 框图
串行外设接口(Serial Peripheral Interface, SPI)为全双工同步串行通信模块,其内部架构如图所示,主要由数据通路模块、时钟与控制模块及寄存器组构成,支持高速主从设备间的数据交互。SPI 外设的核心功能模块及关键寄存器配置如下表所示:
| 模块 / 寄存器 | 核心组成与功能说明 |
|---|---|
| 数据通路模块 | 由发送缓冲区、移位寄存器与接收缓冲区组成。CPU 通过地址和数据总线向发送缓冲区写入数据,数据经移位寄存器在时钟驱动下通过 MOSI 引脚串行输出;同时,外部数据通过 MISO 引脚移入移位寄存器,锁存至接收缓冲区供 CPU 读取。LSBFIRST位控制数据传输顺序(高位 / 低位优先)。 |
| 时钟与控制模块 | 波特率发生器基于系统时钟分频产生 SCK 同步时钟,分频系数由 SPI_CR1寄存器的 BR[2:0]位配置;主控制电路负责主 / 从模式切换、时钟极性与相位控制,实现 SPI 时序逻辑的调度。 |
| 控制寄存器组 | SPI_CR1用于配置外设使能(SPE)、主从模式(MSTR)、时钟模式(CPOL/CPHA)及数据格式;SPI_CR2控制中断与 DMA 使能,支持 TXEIE/RXNEIE中断及 TXDMAEN/RXDMAENDMA 传输;SPI_SR提供工作状态反馈,包含忙标志(BSY)、收发缓冲区状态(TXE/RXNE)及错误标志(OVR/CRCERR)。 |
SPI 通信采用主从同步机制,主机通过 NSS 信号选中从机后,在 SCK 时钟驱动下实现数据的双向移位传输:主机写入发送缓冲区的数据经移位寄存器逐位输出,同时从机返回的数据同步移入移位寄存器并锁存至接收缓冲区。传输完成后,状态寄存器置位 TXE / RXNE 标志,CPU 可通过查询标志或中断方式处理收发数据,支持全双工、半双工及带 CRC 校验的多种工作模式。

1.3 SPI 基本结构
SPI 模块由波特率发生器、数据控制器、移位寄存器组及 GPIO 接口构成。波特率发生器通过 GPIO 输出串行时钟(SCK)信号,为整个通信链路提供同步时序;数据控制器负责调度数据流向,发送数据寄存器(TDR)与接收数据寄存器(RDR)通过移位寄存器完成数据的串并转换,分别实现主出从入(MOSI)与主入从出(MISO)方向的串行数据传输,GPIO 接口则作为物理层通道,配合开关控制实现通信的启停与模式切换。

1.4 主模式全双工连续传输
在主模式全双工(BIDIMODE=0、RXONLY=0)连续传输场景下,SPI 主设备可同时通过 MOSI/MISO 线与从机进行双向数据交换,其核心时序与标志位行为如下:
(1)工作机制与同步控制
主设备输出 SCK 时钟(示例中 CPOL=1、CPHA=1,空闲时 SCK 为高,数据在第一个下降沿移出、第二个上升沿锁存),驱动数据低位先行传输。发送数据(如 0xF1、0xF2)由主机通过 MOSI 发出,同时从机数据(如 0xA1、0xA2)通过 MISO 同步回传,实现全双工同步通信。
BSY标志位在数据传输期间由硬件自动置 1,传输完成后硬件自动清零,软件无需干预,用于标识总线忙状态。
(2)发送流程与 TXE 标志
TXE(发送缓冲器空)标志位初始为 1,表示发送数据寄存器(TDR)为空。当软件向SPI_DR写入数据(如 0xF1)后,TXE自动清零;当 TDR 数据移入移位寄存器后,TXE再次由硬件置 1,通知软件可写入下一个数据(如 0xF2),以此实现连续发送。
该标志需软件轮询或触发中断以维持数据写入节奏,避免缓冲区溢出。
(3)接收流程与 RXNE 标志
RXNE(接收缓冲器非空)标志位在移位寄存器完成数据接收后由硬件置 1,表示接收数据寄存器(RDR)已收到有效数据(如 0xA1)。当软件读取SPI_DR后,RXNE自动清零,准备下一次接收。
全双工模式下,每次 SCK 时钟周期完成 1 字节的双向交换,因此主机发送与接收同步进行,需在RXNE置位时及时读取数据,防止后续数据覆盖。
(4)连续传输实现逻辑
软件需配合标志位完成无缝数据交换:在TXE=1时写入下一个发送数据,在RXNE=1时读取已接收数据,确保 TDR 和 RDR 始终保持可用状态,实现多字节无间断传输。此过程中,主机始终作为时钟源,从机被动同步数据收发。

1.5 非连续传输
在全双工主模式(BIDIMODE=0、RXONLY=0)下,SPI 非连续传输的核心特征为软件写入数据的节奏慢于硬件传输速度,导致传输间出现间隙:
(1)时序与标志行为: 主机按 CPOL/CPHA 配置输出 SCK 时钟,MOSI 上的数据传输完成后,若软件未及时向SPI_DR写入下一个数据,TXE标志虽已置 1,但BSY标志会提前清零,SCK 时钟暂停,总线进入空闲状态,直到新数据写入才重启传输。
(2)控制逻辑: 软件需等待TXE=1后写入下一个数据,但写入延迟会造成帧间停顿,传输不再无缝衔接;传输结束后需等待BSY=0,确保总线完全空闲再进行后续操作。

二、硬件 SPI 读写 W25Q64
2.1 硬件 SPI 读写 W25Q64 的实现
-
首先,按下图接线方式,搭建面包板电路连接 OLED 显示屏,并将 W25Q64 的 CS、DO、CLK 和 DI 分别与 PA4、PA5、PA6 和 PA7 连接,然后将 DAP-Link / ST-Link 连接到 STM32 最小系统板上,为使 OLED 显示屏的 VCC 和 GND 正确连接正负极,请先连接对应正负极跳线(或直接使用 GPIO 口进行供电)。

-
直接复制先前演示的已有文件目录,重命名并双击后缀名为 .uvprojx 的文件打开工程文件,并对 main.c 进行修改,工程中所使用的全部头文件其详细内容已放于文末。
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "HardSPI.h"
#include "W25Q64.h"uint8_t MID;
uint16_t DID;uint8_t ArrayWrite[] = {0xA1, 0xB2, 0xC3, 0xD4};
uint8_t ArrayRead[4];int main(void)
{
OLED_Init();
OLED_ShowString(1, 5, "Hard SPI");
OLED_ShowString(2, 1, "MID: DID:");
OLED_ShowString(3, 1, "W:");
OLED_ShowString(4, 1, "R:");W25Q64_Init(); W25Q64_ReadID(&MID, &DID); OLED_ShowHexNum(2, 5, MID, 2); OLED_ShowHexNum(2, 12, DID, 4); W25Q64_SectorErase(0x000000); W25Q64_PageProgram(0x000000, ArrayWrite, 4); W25Q64_ReadData(0x000000, ArrayRead, 4); OLED_ShowHexNum(3, 3, ArrayWrite[0], 2); OLED_ShowHexNum(3, 6, ArrayWrite[1], 2); OLED_ShowHexNum(3, 9, ArrayWrite[2], 2); OLED_ShowHexNum(3, 12, ArrayWrite[3], 2); OLED_ShowHexNum(4, 3, ArrayRead[0], 2); OLED_ShowHexNum(4, 6, ArrayRead[1], 2); OLED_ShowHexNum(4, 9, ArrayRead[2], 2); OLED_ShowHexNum(4, 12, ArrayRead[3], 2); while (1) { }}
-
由于仅将软件 SPI 读写方式替换为硬件 SPI 读写方式,因此最终实现效果与上一节保持一致。

三、演示代码关联的头文件与源文件说明
-
OLED 相关头文件请从 STM32 学习 ------ 个人学习笔记4(OLED 显示屏及调试工具) 文末查看,此处不重复展示。
-
HardSPI.c
#include "stm32f10x.h" // Device header
void HardSPI_W_SS(uint8_t BitValue){
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}void HardSPI_Init(void){
// 提前声明需要使用的结构体 GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; // 配置 GPIO RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置 SPI SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); HardSPI_W_SS(1);}
void HardSPI_Start(void){
HardSPI_W_SS(0);
}void HardSPI_Stop(void){
HardSPI_W_SS(1);
}uint8_t HardSPI_SwapByte_Mode_0(uint8_t ByteSend){
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);SPI_I2S_SendData(SPI1, ByteSend); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET); return SPI_I2S_ReceiveData(SPI1);}
-
HardSPI.h
#ifndef __HARDSPI_H
#define __HARDSPI_Hvoid HardSPI_Init(void);
void HardSPI_Start(void);
void HardSPI_Stop(void);
uint8_t HardSPI_SwapByte_Mode_0(uint8_t ByteSend);#endif
-
W25Q64.c
#include "stm32f10x.h" // Device header
#include "HardSPI.h"
#include "W25Q64_Ins.h"void W25Q64_Init(void){
HardSPI_Init();}
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID){
HardSPI_Start();
HardSPI_SwapByte_Mode_0(W25Q64_JEDEC_ID);*MID = HardSPI_SwapByte_Mode_0(W25Q64_DUMMY_BYTE); *DID = HardSPI_SwapByte_Mode_0(W25Q64_DUMMY_BYTE); *DID <<= 8; *DID |= HardSPI_SwapByte_Mode_0(W25Q64_DUMMY_BYTE); HardSPI_Stop();}
void W25Q64_WriteEnable(void){
HardSPI_Start();
HardSPI_SwapByte_Mode_0(W25Q64_WRITE_ENABLE);
HardSPI_Stop();
}void W25Q64_WaitBusy(void){
uint32_t Timeout;
HardSPI_Start();
HardSPI_SwapByte_Mode_0(W25Q64_READ_STATUS_REGISTER_1);
Timeout = 100000;
while ((HardSPI_SwapByte_Mode_0(W25Q64_DUMMY_BYTE) & 0x01) == 0x01){
Timeout--;
if (Timeout == 0){
break;
}
}
HardSPI_Stop();
}void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count){
uint16_t i;W25Q64_WriteEnable(); HardSPI_Start(); HardSPI_SwapByte_Mode_0(W25Q64_PAGE_PROGRAM); HardSPI_SwapByte_Mode_0(Address >> 16); HardSPI_SwapByte_Mode_0(Address >> 8); HardSPI_SwapByte_Mode_0(Address); for (i=0; i<Count; i++){ HardSPI_SwapByte_Mode_0(DataArray[i]); } HardSPI_Stop(); W25Q64_WaitBusy();}
void W25Q64_SectorErase(uint32_t Address){
W25Q64_WriteEnable();HardSPI_Start(); HardSPI_SwapByte_Mode_0(W25Q64_SECTOR_ERASE_4KB); HardSPI_SwapByte_Mode_0(Address >> 16); HardSPI_SwapByte_Mode_0(Address >> 8); HardSPI_SwapByte_Mode_0(Address); HardSPI_Stop(); W25Q64_WaitBusy();}
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count){
uint32_t i;HardSPI_Start(); HardSPI_SwapByte_Mode_0(W25Q64_READ_DATA); HardSPI_SwapByte_Mode_0(Address >> 16); HardSPI_SwapByte_Mode_0(Address >> 8); HardSPI_SwapByte_Mode_0(Address); for (i=0; i<Count; i++){ DataArray[i] = HardSPI_SwapByte_Mode_0(W25Q64_DUMMY_BYTE); } HardSPI_Stop();}
-
W25Q64.h
#ifndef __W25Q64_H
#define __W25Q64_Hvoid W25Q64_Init(void);
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID);
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count);#endif
-
W25Q64_Ins.h
#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H#define W25Q64_WRITE_ENABLE 0x06
#define W25Q64_WRITE_DISABLE 0x04
#define W25Q64_READ_STATUS_REGISTER_1 0x05
#define W25Q64_READ_STATUS_REGISTER_2 0x35
#define W25Q64_WRITE_STATUS_REGISTER 0x01
#define W25Q64_PAGE_PROGRAM 0x02
#define W25Q64_QUAD_PAGE_PROGRAM 0x32
#define W25Q64_BLOCK_ERASE_64KB 0xD8
#define W25Q64_BLOCK_ERASE_32KB 0x52
#define W25Q64_SECTOR_ERASE_4KB 0x20
#define W25Q64_CHIP_ERASE 0xC7
#define W25Q64_ERASE_SUSPEND 0x75
#define W25Q64_ERASE_RESUME 0x7A
#define W25Q64_POWER_DOWN 0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE 0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID 0x90
#define W25Q64_READ_UNIQUE_ID 0x4B
#define W25Q64_JEDEC_ID 0x9F
#define W25Q64_READ_DATA 0x03
#define W25Q64_FAST_READ 0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B
#define W25Q64_FAST_READ_DUAL_IO 0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B
#define W25Q64_FAST_READ_QUAD_IO 0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3#define W25Q64_DUMMY_BYTE 0xFF
#endif
文中部分知识参考:B 站 ------ 江协科技;百度百科