软件
SPI通信

1. 定义与起源
- SPI(Serial Peripheral Interface)是由 Motorola(摩托罗拉)公司开发的一种通用数据总线。
2. 物理连接(四根通信线)
-
SCK (Serial Clock):串行时钟线,用于同步数据传输。
-
MOSI (Master Output Slave Input):主设备输出、从设备输入线(数据由主发往从)。
-
MISO (Master Input Slave Output):主设备输入、从设备输出线(数据由从发往主)。
-
SS (Slave Select):片选信号线(通常低电平有效),用于选中特定的从设备。
3. 通信特性
-
同步(Synchronous):数据传输由时钟信号(SCK)严格同步。
-
全双工(Full-duplex):由于有独立的MOSI和MISO线,主从设备可以同时发送和接收数据。
4. 总线拓扑结构
- 支持总线挂载多设备(一主多从):一个主机可以通过多个SS片选线连接多个从设备。
虽然SPI通信速度远快于UART和I2C,且是全双工,但它也有缺点:占用引脚较多。当挂载多个从设备时,每个从设备都需要一根独立的SS线(片选线),这在引脚资源有限的微控制器(MCU)上可能会造成压力。
硬件电路

1. 总线共享规则(SCK, MOSI, MISO)
-
所有SPI设备的 SCK(时钟) 、MOSI(主出从入) 、MISO(主入从出) 引脚分别连在一起。
-
这意味着这三根线构成了共享总线,所有设备都并联在这三根线上。
2. 片选控制规则(SS)
-
主机需要另外引出多条独立的SS控制线,分别连接到各从机的SS引脚。
-
这是为了解决多个从机共享一根MISO线时的冲突问题。
3. GPIO引脚配置(电气特性)
-
输出引脚 (主机端的SCK、MOSI、SS,以及从机端的MISO):配置为推挽输出(Push-Pull)。推挽输出具有强驱动能力,能快速拉高或拉低电平,保证高频通信信号的质量。
-
输入引脚 (主机端的MISO,以及从机端的SCK、MOSI、SS):配置为浮空输入或上拉输入。
🔍 电路原理图深度解析(一主三从架构)
原理图清晰地展示了一个SPI主机(Master)连接三个SPI从机(Slave 1, 2, 3)的标准电路结构:
-
共享连线:主机底部的 SCK、MOSI、MISO 三根线,直接并连到了三个从机的对应引脚上。
-
独立片选 :主机顶部引出了
SS1、SS2、SS3三根线,分别接入从机1、从机2、从机3的SS引脚。 -
数据流向:
-
SCK、MOSI、SS:从主机指向从机(主机作为控制方和输出方)。
-
MISO:从从机指向主机(当从机被选中且需要返回数据时)。
-
💡 关键原理解析与补充
1. 为什么MISO可以连在一起,而SS不行?
在SPI通信中,同一时刻只允许一个从机与主机通信(一主多从,但同一时间只能有一个从机响应)。
如果多个从机的MISO线强行连在一起且同时输出数据(一个输出高电平,一个输出低电平),会发生总线短路冲突 。因此,必须通过SS片选线来决定当前"听谁的"。
-
当某个从机的SS引脚被主机拉低(通常是低电平有效)时,该从机才会被激活,将其MISO引脚切换到推挽输出状态,向主机发送数据。
-
未被选中的从机,其MISO引脚处于高阻态(相当于断开)。
2. 为什么输入引脚推荐配置为浮空或上拉输入?
这与刚才提到的"高阻态"有关。当主机的MISO引脚连接了多个从机,且所有从机都没被选中(或当前从机尚未准备好)时,MISO线处于悬空状态。
-
如果不配置上拉,浮空的引脚容易受到外界电磁干扰,导致主机读取到乱码。
-
配置为上拉输入,可以确保在没有设备驱动MISO时,线路保持在一个确定的高电平状态,增加通信的稳定性。而主机端的SCK、MOSI、SS在空闲时通常保持高电平或低电平的固定状态,所以配置为推挽输出。
3. 优缺点体现
这张图完美体现了SPI总线的一个显著特点:速度快(推挽输出、全双工),但引脚消耗大。每增加一个从机,主机就要多牺牲一个GPIO引脚作为片选线。相比之下,I2C总线只需要两根线就能挂载多个设备(通过设备地址区分)。
移位示意图

这张图片("移位示意图")揭示了SPI通信最底层的核心硬件逻辑------基于移位寄存器(Shift Register)的全双工数据交换机制。
1. 内部核心组件
-
SPI主机(Master) :内部包含一个移位寄存器 (图中当前值为
10101010)和一个波特率发生器。 -
SPI从机(Slave) :内部同样包含一个移位寄存器 (图中当前值为
01010101)。 -
波特率发生器:主机内部产生SCK时钟信号,驱动双方的移位寄存器同步工作。
2. 连线逻辑(构成环形结构)
-
MOSI:主机寄存器的最高位(MSB,最左侧)连到从机寄存器的最低位(LSB,最右侧)。
-
MISO:从机寄存器的最高位(MSB,最左侧)连到主机寄存器的最低位(LSB,最右侧)。
-
SCK:主机的时钟信号同时连接到主从双方的移位寄存器。
-
关键理解 :在物理逻辑上,这两个8位寄存器通过MOSI和MISO连成了一个16位的环形移位寄存器。
⚙️ 工作原理动态演示(全双工交换)
在SCK时钟脉冲的驱动下,数据按位进行同步移动:
-
第一个时钟脉冲:
-
主机将自己最左边的一位(MSB,
1)通过MOSI发给从机,存入从机寄存器的最右边(LSB)。 -
从机将自己最左边的一位(MSB,
0)通过MISO发给主机,存入主机寄存器的最右边(LSB)。 -
双方的寄存器同时向左移动一位。
-
-
经过8个时钟脉冲后:
-
主机的8位数据
10101010全部推入了从机寄存器。 -
从机的8位数据
01010101全部推入了主机寄存器。 -
结果 :主机和从机的数据完成了完全交换 (主机变为
01010101,从机变为10101010)。
-
💡 关键原理解析与补充
1. 为什么SPI必然是"全双工"的?
因为主机在发送数据(通过MOSI)的同时,必然也在通过MISO接收从机的数据。你无法只发送不接收。如果主机只想发送,它会接收到从机发来的垃圾数据(通常选择忽略);如果主机只想接收,它也必须发送一个无效字节(如 0xFF 或 0x00)来驱动时钟。
2. 为什么从机没有波特率发生器?
这完美呼应了上一张图的"同步"特性。从机是被动的,它不需要自己产生时钟。主机的SCK线直接连接到从机寄存器的时钟输入端。主机给一个脉冲,从机就移位一次,这就保证了收发双方绝对同步,不需要像UART那样约定波特率。
3. 位序(MSB First vs LSB First)
图中展示的是典型的MSB先行(Most Significant Bit first) 模式。主机的最高位先移出。在代码配置中,我们经常会看到 SPI_FirstBit_MSB 或 SPI_FirstBit_LSB 的配置选项,这是根据外设芯片手册要求来设定的。如果换成LSB先行,移位方向就会反转。
4. 与片选信号(SS)的配合
结合第二张图,当主机需要和"从机1"通信时,主机会拉低"从机1"的SS。此时,"从机1"的移位寄存器被激活,参与这个环形网络。其他的从机未被选中,其寄存器处于高阻态(相当于脱离了这个环形网络),不会干扰数据交换。
总结 :SPI通信的本质就是"在时钟脉冲的驱动下,主从双方各自驱动一个移位寄存器,通过环形总线进行数据交换"。这种硬件机制极其简单,因此SPI通信速度可以做得非常快(通常可达几十MHz)。
SPI时序基本单元及其时序模式





SPI通信协议的时序基本单元,也就是SPI通信中最核心的四种工作模式(SPI Mode 0~3)以及通信的起始/终止条件。
-
CPOL (Clock Polarity 时钟极性):决定 SCK 空闲状态(即没有数据传输时)是高电平还是低电平。(0=低电平,1=高电平)
-
CPHA (Clock Phase 时钟相位):决定在 SCK 的第几个边沿采集(移入)数据。(0=第一个边沿,1=第二个边沿)
-
黄金法则 :数据必须在"采样边沿"前准备好,并在"采样边沿"保持稳定。因此,"移出(改变)数据"和"移入(采样)数据"总是发生在不同的边沿。
起始与终止条件
这是SPI通信的最外层框架。
-
起始条件 :SS(片选信号)从高电平切换到低电平。(主机拉低对应从机的SS线,唤醒该从机)
-
终止条件 :SS(片选信号)从低电平切换到高电平。(主机拉高SS线,结束通信)
-
图中展示了 SS 信号的下降沿和上升沿波形。在SS拉低到拉高的这段时间内,SCK、MOSI、MISO才会开始有条不紊地工作。
模式0 (Mode 0)
-
参数 :
CPOL=0(空闲状态时,SCK为低电平),CPHA=0(第一个边沿移入数据,第二个边沿移出数据)。 -
波形解析:
-
空闲时,SCK是低电平。
-
当第一个边沿到来(上升沿 ),此时数据已经准备好,双方进行采样(移入)。
-
当第二个边沿到来(下降沿 ),双方**改变(移出)**数据,准备下一位。
-
观察MOSI/MISO波形,数据位(B7)在SCK上升沿时处于稳定状态,在下降沿时发生翻转。
-
-
应用:Mode 0 是实际应用中最常用的模式。
第二张图(上半部分):模式1 (Mode 1)
-
参数 :
CPOL=0(空闲状态时,SCK为低电平),CPHA=1(第一个边沿移出数据,第二个边沿移入数据)。 -
波形解析:
-
空闲时,SCK是低电平。
-
当第一个边沿到来(上升沿 ),双方**改变(移出)**数据。
-
当第二个边沿到来(下降沿 ),数据已经稳定,双方进行采样(移入)。
-
观察波形,数据位(B7)在SCK上升沿时发生翻转,在下降沿时处于稳定状态。
-
模式2 (Mode 2)
-
参数 :
CPOL=1(空闲状态时,SCK为高电平),CPHA=0(第一个边沿移入数据,第二个边沿移出数据)。 -
波形解析:
-
空闲时,SCK是高电平。
-
当第一个边沿到来(下降沿 ),双方进行采样(移入)。
-
当第二个边沿到来(上升沿 ),双方**改变(移出)**数据。
-
观察波形,数据位在SCK下降沿时稳定(被采样),在上升沿时翻转。
-
模式3 (Mode 3)
-
参数 :
CPOL=1(空闲状态时,SCK为高电平),CPHA=1(第一个边沿移出数据,第二个边沿移入数据)。 -
波形解析:
-
空闲时,SCK是高电平。
-
当第一个边沿到来(下降沿 ),双方**改变(移出)**数据。
-
当第二个边沿到来(上升沿 ),数据稳定,双方进行采样(移入)。
-
观察波形,数据位在SCK下降沿时翻转,在上升沿时稳定。
-
SPI时序
-
绿线:SCK的上升沿,是数据的采样点(对应SPI模式0)。
-
红线:用来分隔不同的通信阶段(指令、地址、数据)。
-
SS:低电平有效,整个通信过程(无论持续多少个时钟)SS必须一直保持低电平,直到所有数据收发完毕才拉高。
发送指令(单字节指令)
-
文字内容:向SS指定的设备,发送指令(0x06)。通常对应Flash的"写使能(Write Enable)"操作。
-
波形解析:
-
SS拉低后,共有 8个时钟脉冲。
-
在MOSI线上,主机依次发送
0000 0110(即0x06)。 -
MISO线一直保持高电平(
0xFF),因为从机没有数据返回,主机的上拉电阻将其拉高。 -
发完8位后,SS拉高,通信结束。这就是最简单的单字节指令发送。
-

指定地址写(Page Program / Write)
-
文字内容:发送写指令(0x02),随后在指定地址(Address23:0)下,写入指定数据(Data)。
-
波形解析(看红色垂直分隔线) :整个通信由 40个时钟脉冲 组成,分为三个阶段:
-
指令阶段(8个时钟) :红线左侧。MOSI发送
0x02(二进制0000 0010),代表"我要写数据"。 -
地址阶段(24个时钟) :红线和红线之间。因为地址是
Address[23:0],即24位(3个字节),所以这里连续有24个时钟脉冲。MOSI依次发送地址的高8位、中8位、低8位。 -
数据阶段(8个时钟):最右侧红线之后。MOSI发送具体的8位数据(Data)。
-
注意MISO :在整个40个时钟周期内,MISO线依然保持高电平。因为是"写"操作,从机只接收,不发送有效数据,所以主机读回的是无效的
0xFF。 -
发完最后一个数据位,SS拉高,写操作完成。
-

指定地址读(Read Data)
-
文字内容:发送读指令(0x03),随后在指定地址(Address23:0)下,读取从机数据(Data)。
-
波形解析(看红色垂直分隔线) :同样由 40个时钟脉冲 组成,分三个阶段:
-
指令阶段(8个时钟) :MOSI发送
0x03(二进制0000 0011),代表"我要读数据"。 -
地址阶段(24个时钟):MOSI发送24位地址。
-
数据读取阶段(8个时钟):
-
在这个阶段,主机不需要再向从机发送数据了,所以MOSI线处于空闲状态 (通常保持高电平或发
0xFF)。 -
与此同时,MISO线上出现了高低变化的波形!这是从机在该地址下把内部的数据推到了MISO线上。主机通过这8个时钟脉冲,成功读取到了从机发来的1个字节的Data。
-
发完8位数据,SS拉高,读操作完成。
-
-
💡 总结与实战意义
-
SPI设备通信的通用帧格式 :往往是
指令(1字节) + 地址(N字节) + 数据(M字节)。图2和图3完美展示了这种结构化通信。 -
SS的特性:不能每发一个字节就拉高SS!图2和图3的40个时钟期间,SS必须持续拉低,一旦中途拉高,从机就会复位,之前发过的指令和地址就会失效。
-
全双工的本质 :不管你是"写"(图2)还是"读"(图3),主机和从机的移位寄存器始终在同时工作。图2是主机用指令+地址+数据,换回了从机的4个
0xFF;图3是主机用指令+地址+0xFF,换回了从机的1个有效数据字节。这是理解SPI底层机制的关键。
W25Q64简介

W25Q64 是一个典型的、完全依赖 SPI 协议进行通信的硬件设备(SPI Flash 芯片)。:
- 硬件引脚的完美对应
看图片右下角的实物模块,上面标着:VCC, CS, DO, GND, CLK, DI。这正好就是前面所讲的 SPI 四根通信线:
-
CS (片选)------ 对应我们前面讲的 SS。
-
CLK (时钟)------ 对应 SCK。
-
DI (数据输入)------ 对应 MOSI(主机输出,从机输入)。
-
DO (数据输出)------ 对应 MISO(主机输入,从机输出)。
-
VCC 和 GND 是电源和地。
- 协议与波形的完美印证
-
24位地址(Address23:0)的由来 :
PPT 中明确写了"存储容量(24位地址)"。W25Q64 的容量是 64Mbit = 8MByte。要访问这 8M 的空间,需要 223223 个地址。为了凑齐整数个字节,就用 3 个字节(24位)来表示地址。这就完美解释了为什么你上一张图(图2、图3)的时序中,指令后面紧跟着 24个时钟脉冲的地址段。
-
指令(0x02, 0x03, 0x06) :
你之前看到的
0x06(写使能)、0x02(页写入)、0x03(读取数据),这些正是 W25Q64 芯片手册(Datasheet)里规定的标准指令。-
0x06:Write Enable,每次写入前必须发,用来解除芯片的写保护。
-
0x02:Page Program,向指定地址写入数据。
-
0x03:Read Data,从指定地址读取数据。
-
-
时序模式:W25Q64 通常支持 SPI Mode 0 和 Mode 3。我们之前分析抓包图用的正是 Mode 0(CPOL=0, CPHA=0)。
- 关于速度(Dual SPI / Quad SPI)
PPT 中提到"80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)"。这就是我们在第一张图提到过的"SPI支持总线挂载多设备,但引脚多"的演变:
-
Standard SPI (标准SPI):就是我们前面讲的,一根 MOSI 发,一根 MISO 收(80MHz)。
-
Dual SPI (双线SPI):为了快点把数据读出来,把原本单向的 MOSI 和 MISO 借用来同时双向传输,变成两根数据线(160MHz)。
-
Quad SPI (四线SPI):再增加两根额外的数据线(WP 和 HOLD),四个通道同时传数据,速度翻倍(320MHz)。但它们的控制指令和基本时序依然是基于 SPI 的。
硬件电路

- 接口与引脚映射(核心连接关系)
-
左侧 J1 接口:这是一个标准的 6 针 SPI 模块接口。它直接引出了 VCC、CS、DO、GND、CLK、DI,方便接线。
-
中间 U1 芯片:W25QXX 芯片本体。
-
右侧引脚功能表:这是重中之重,它完全印证了之前的 SPI 理论:
-
CS (SS):SPI 片选。低电平有效(对应之前的 SS)。
-
CLK (SCK):SPI 时钟。由主机提供。
-
DI (MOSI):主机输出、从机输入。接收主机发来的指令和地址。
-
DO (MISO):主机输入、从机输出。向主机回传数据。
-
注意:这里为什么叫 DI/DO?因为它是站在芯片(从机)的角度命名的。 主机发数据给芯片,所以是 Data In;芯片发数据给主机,所以是 Data Out。
-
-
右上角的引脚分布图:展示了芯片的物理封装(通常是 SOP-8)。引脚 1 是 /CS,引脚 4 是 GND,引脚 8 是 VCC,完全对应电路图。
- 特殊控制引脚(WP 和 HOLD)
这是这张硬件图的一个关键知识点。看原理图中间,芯片的第 3 脚 (/WP) 和第 7 脚 (/HOLD) 被特殊处理了:
-
/WP (Write Protect):写保护。低电平有效。当它被拉低时,芯片的状态寄存器会被锁定,防止数据被意外篡改。
-
/HOLD (Hold):数据保持。低电平有效。当它被拉低时,芯片的通信会暂停,但不会复位时钟序列,用于多主机共享总线时的冲突避免。
-
电路设计逻辑 :在原理图中,这两个引脚通过电阻
R1连接到了VCC。将它们拉高(高电平),意味着禁用了写保护和数据保持功能,让芯片处于正常工作状态。 这也解释了为什么之前你发0x06(写使能)就可以直接写数据,因为硬件上没有写保护锁定。
- 外围辅助电路
-
C1 (104 电容):0.1μF 的去耦电容。连接在 VCC 和 GND 之间。这是所有数字芯片的标准配置,用于滤除电源的高频噪声,保证芯片在 80MHz 甚至更高频率工作时供电稳定。
-
R1 (1K 电阻) 和 D1 (LED):组成电源指示灯。VCC 接通时,D1 亮起,提示模块已上电。同时,R1 也充当了 WP 和 HOLD 引脚的上拉电阻。
- 硬件连接实战(如何一主多从)
结合之前学的"一主多从"理论:
-
如果你接多个 W25QXX 芯片,需要把 CLK、DI、DO 并联接在一起(接到主机的 SCK、MOSI、MISO)。
-
但是,每个芯片的 CS 必须单独接主机的一个 GPIO 引脚。
-
需要操作哪个芯片,就把哪个芯片的 CS 拉低,其他芯片的 CS 保持高电平(它们的 DO 引脚处于高阻态,不会干扰总线)。
- 供电注意事项
右下角表格标明了 VCC 电压范围(2.7 ~ 3.6V)。这是一个极其重要的硬件坑点!如果你使用 STM32 或 51 单片机(通常为 3.3V 或 5V):
-
如果主机是 5V,而 W25Q64 是 3.3V,那么 MOSI、CS、CLK 线必须加电平转换电路,否则会烧毁 Flash 芯片。
-
现在大多数单片机(如 STM32)都是 3.3V 供电,可以直接直连。
W25Q64框图

一、 右边:仓库区(存储阵列)
这里就是真正用来存数据的地方。注意看它的层级结构(对应图右上角):
-
总容量 :右上角标了
W25Q64BV,一共 64Mbit = 8MByte。 -
块 (Block) :8MB 被分成了 128个块 (Block 0 到 Block 127)。每个块的大小是 64KB 。(你可以看到右侧的地址从
000000h一直到7FFFFFh)。 -
扇区 (Sector) :每个 64KB 的块,又被分成了 16个扇区 。每个扇区大小是 4KB 。(看左侧小图
Sector 0 (4KB)到Sector 15 (4KB))。在Flash中,擦除的最小单位就是一个扇区(4KB)。 -
地址对应关系:
- 总地址范围
000000h-7FFFFFh。这就是为什么之前的时序图里,地址是24位(3个字节)。因为 223223 刚好覆盖 8MB 的寻址空间。
- 总地址范围
二、 左边:管理层(SPI 控制逻辑)
这里是芯片的大脑,负责执行你从 SPI 总线发来的指令。
-
SPI Command & Control Logic (核心指挥官)
-
图左下角的
CLK、/CS、DI、DO直接连入这里。这就是你之前学的 SPI 四根线在芯片内部的终点。 -
你发送的
0x06、0x03、0x02等指令,全部由这个逻辑块接收并解析。
-
-
Status Register (状态寄存器)
- 芯片当前是在忙(Busy)还是空闲?写使能(Write Enable)开了没有?都在这里记录。你发
0x06就是把这个寄存器里的某一位设为1。
- 芯片当前是在忙(Busy)还是空闲?写使能(Write Enable)开了没有?都在这里记录。你发
-
Write Protect Logic (写保护逻辑)
- 连接着芯片的
/WP引脚。前面硬件图里我们把/WP拉高了(无效),所以这里的写保护逻辑不会阻挡你的写操作。
- 连接着芯片的
-
Address Latch / Counter (地址锁存器与计数器)
- 分为
Page Address(页地址)和Byte Address(字节地址)。分别对应行地址(你在哪个块、哪个扇区)和列地址(你在那一页的哪个字节)。
- 分为
-
256-Byte Page Buffer (256字节页缓冲区) ------ 极其重要!
-
这是你在写数据时(0x02指令)的"中转站"。Flash 芯片不能直接把数据写进仓库,你必须先把数据发到这里暂存,凑齐一页(256字节),芯片内部才会一次性把它们搬进仓库。
-
读数据时(0x03指令),数据也是从仓库先搬到这个缓冲区,然后再通过
DO引脚一位一位发给你。
-
-
High Voltage Generators (高压发生器)
- 你可能会好奇,为什么 Flash 断电了还能存数据?因为写入(编程)和擦除需要给浮栅晶体管"充电",这需要高压(通常十几伏)。而你的单片机只给它 3.3V。这个模块就是负责把 3.3V 升压到内部需要的高压。
三、 串联起来:数据是如何流动的?
当你要写一个字节时(对应之前的图2):
-
CLK,DI将0x02指令传入 SPI控制逻辑。 -
接着传入 24位地址(行、列地址锁存)。
-
接着你要写入的数据,被一比特一比特地存进 256-Byte Page Buffer(页缓冲区)。
-
数据存满或你拉高了
/CS,高压发生器 启动,把 Page Buffer 里的数据真正烧录进右边的 仓库区(某个Block的某个Sector里)。
当你要读一个字节时(对应之前的图3):
-
传入
0x03指令和 24位地址。 -
行解码器(Row Decode)根据地址找到仓库(Block/Sector)。
-
数据被读出,经过 列解码器(Column Decode),送入缓冲区。
-
最后通过
DO引脚,在CLK的节拍下一位一位移出给主机。
💡 总结
不要被庞大的连线吓到。它就是一个微型的计算机系统:有接口(SPI控制逻辑)、有内存表(状态寄存器)、有缓存(Page Buffer)、有硬盘(存储阵列),甚至还有电源管理(高压发生器)。
软件读写W25Q64
cpp
main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"
int main(void)
{
/*uint8_t mid; // 准备一个8位变量,用来存"厂商ID (Manufacturer ID)"
uint16_t did; // 准备一个16位变量,用来存"设备ID (Device ID)"
// 1. 初始化外设
OLED_Init(); // 初始化 OLED 屏幕,准备用来显示信息
W25Q64_init(); // 初始化 SPI 底层硬件(包括你之前看的 spi_init)
// 2. 在 OLED 上画好"填空题"的标签
OLED_ShowString(1, 1, "MID: DID:"); // 第1行显示标签
OLED_ShowString(2, 1, "W:"); // 第2行准备显示写测试(预留)
OLED_ShowString(3, 1, "R:"); // 第3行准备显示读测试(预留)
// 3. 核心动作:读取芯片ID
W25Q64_read_id(&mid, &did); // 调用你上一个问题里写好的读取ID函数,传入mid和did的地址
// 4. 把读到的ID以"十六进制"格式显示在屏幕上
OLED_ShowHexNum(1, 5, mid, 2); // 在第1行第5列显示 mid,占2位十六进制(比如 EF)
OLED_ShowHexNum(1, 12, did, 4); // 在第1行第12列显示 did,占4位十六进制(比如 4017)
// 5. 死循环,程序停在这里,保持屏幕常亮显示
while(1)
{
}
return 0;
*/
uint8_t mid;
uint16_t did;
uint8_t array_w[4] = {0x11, 0x22, 0x33, 0x44};
uint8_t array_r[4];
//初始化
OLED_Init();
W25Q64_init();
//显示一个字符
//OLED_ShowChar(1, 1, 'A');
//显示字符串
//OLED_ShowString(1, 3, "SPI Test");
OLED_ShowString(1, 1, "MID: DID:");
OLED_ShowString(2, 1, "W:");
OLED_ShowString(3, 1, "R:");
W25Q64_read_id(&mid, &did);
OLED_ShowHexNum(1, 5, mid, 2);
OLED_ShowHexNum(1, 12, did, 4);
//擦除扇区
W25Q64_sector_erase(0x0);
//写扇区
W25Q64_page_program(0x0, array_w, 4);
//读数据
W25Q64_read_data(0x0, array_r, 4);
OLED_ShowHexNum(2, 3, array_w[0], 2);
OLED_ShowHexNum(2, 6, array_w[1], 2);
OLED_ShowHexNum(2, 9, array_w[2], 2);
OLED_ShowHexNum(2, 12, array_w[3], 2);
OLED_ShowHexNum(3, 3, array_r[0], 2);
OLED_ShowHexNum(3, 6, array_r[1], 2);
OLED_ShowHexNum(3, 9, array_r[2], 2);
OLED_ShowHexNum(3, 12, array_r[3], 2);
while(1)
{
}
}
SPI.c
#include "stm32f10x.h" // Device header
/*
CS: PA4
CLK: PA5
DO: PA6
DI: PA7
*/
//SS写 PA4
void spi_W_SS(uint8_t bitval)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)bitval);
}
//CLK写 PA5
void spi_W_SCK(uint8_t bitval)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)bitval);
}
//DI写 MOSI PA7
void spi_W_MOSI(uint8_t bitval)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)bitval);
}
//DO读 MISO PA6
uint8_t spi_R_MISO(void)
{
return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}
void spi_init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
//使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//A4 A5 A7 推挽输出
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
//A6 输入
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
spi_W_SS(1);
spi_W_SCK(0);
}
void spi_start(void)
{
spi_W_SS(0);
}
void spi_stop(void)
{
spi_W_SS(1);
}
uint8_t spi_swap_byte(uint8_t val)
//主设备与 SPI 从设备(进行"全双工"交换一个字节的数据。
{
uint8_t i = 0;
uint8_t value = 0;
//模式0 第一个边沿移入数据 第二个边沿移出数据
for (i = 0; i < 8; i++)
{
spi_W_MOSI(val & (0x80 >> i));
spi_W_SCK(1);
if (spi_R_MISO() == 1)//
{
value |= 0x80 >> i;
}
spi_W_SCK(0);//拉低时钟线
}
return value;
}
w25q64.c
#include "stm32f10x.h" // Device header
#include "SPI.h"
/* ==================== 写使能与状态寄存器控制 ==================== */
#define W25Q64_WRITE_ENABLE 0x06 // 写使能(每次写入/擦除前必须先发送此指令,否则芯片拒绝执行)
#define W25Q64_WRITE_DISABLE 0x04 // 写禁止(取消写使能状态)
#define W25Q64_READ_STATUS_REGISTER_1 0x05 // 读状态寄存器1(最常用!常用于轮询第0位BUSY,判断芯片是否忙)
#define W25Q64_READ_STATUS_REGISTER_2 0x35 // 读状态寄存器2(包含QE位等,用于QSPI模式配置)
#define W25Q64_WRITE_STATUS_REGISTER 0x01 // 写状态寄存器(用于配置保护区域、使能Quad模式等)
/* ==================== 数据写入(编程)指令 ==================== */
#define W25Q64_PAGE_PROGRAM 0x02 // 页编程(写入数据,一页最大256字节,超过会回卷覆盖)
#define W25Q64_QUAD_PAGE_PROGRAM 0x32 // 四线页编程(利用4根数据线同时写,提高写入速度)
/* ==================== 擦除指令(Flash写入新数据前必须先擦除) ==================== */
#define W25Q64_BLOCK_ERASE_64KB 0xD8 // 64KB块擦除
#define W25Q64_BLOCK_ERASE_32KB 0x52 // 32KB块擦除
#define W25Q64_SECTOR_ERASE_4KB 0x20 // 4KB扇区擦除(最常用,最小擦除单位)
#define W25Q64_CHIP_ERASE 0xC7 // 全片擦除(清空整个8MB芯片,耗时极长,慎用)
#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 // 连续读模式复位(同时也是复位指令)
/* ==================== 设备识别与ID读取 ==================== */
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB // 释放掉电模式 / 读设备ID
#define W25Q64_MANUFACTURER_DEVICE_ID 0x90 // 读厂商和设备ID
#define W25Q64_READ_UNIQUE_ID 0x4B // 读64位唯一ID(出厂固化,唯一不可改,常用于加密防抄板)
#define W25Q64_JEDEC_ID 0x9F // 读JEDEC ID(极其常用!返回厂商ID+容量ID+设备ID,如EF 40 17)
/* ==================== 读取数据指令 ==================== */
#define W25Q64_READ_DATA 0x03 // 普通读数据(最常用,发送指令+24位地址后,可连续读取)
#define W25Q64_FAST_READ 0x0B // 快读(需要发一个哑字节,支持更高的时钟频率)
#define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B // 双线输出快读(两根线接收数据,速度翻倍)
#define W25Q64_FAST_READ_DUAL_IO 0xBB // 双线IO快读(两根线既发地址又收数据)
#define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B // 四线输出快读(四根线接收数据,QSPI基础)
#define W25Q64_FAST_READ_QUAD_IO 0xEB // 四线IO快读(四根线全双工,速度极快,常用于XIP)
#define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3 // 八位字四线IO读(高级模式)
/* ==================== 通用常量 ==================== */
#define W25Q64_DUMMY_BYTE 0xFF // 哑字节(用于"踩时钟"产生SCK脉冲,把目标数据从MISO线上挤出来)
void W25Q64_init(void)
{
spi_init();
}
void W25Q64_read_id(uint8_t *mid, uint16_t *did)
{
spi_start(); // 拉低CS片选,选中芯片
spi_swap_byte(W25Q64_JEDEC_ID); // 发送 0x9F (读JEDEC ID指令)
*mid = spi_swap_byte(W25Q64_DUMMY_BYTE); // 接收第1个字节(厂商ID)
*did = spi_swap_byte(W25Q64_DUMMY_BYTE); // 接收第2个字节(设备ID高位)
*did <<= 8; // 高位左移8位
*did |= spi_swap_byte(W25Q64_DUMMY_BYTE); // 接收第3个字节(设备ID低位),并拼装
spi_stop(); // 拉高CS片选,释放芯片
}
//作用:发送 0x06 指令。
//重要性:这是 Flash 操作的硬性规则! Flash 芯片为了保护数据,默认是"写保护"状态。任何擦除或写入操作前,必须先调用这个函数"开锁",否则芯片会直接忽略后续的指令。
void W25Q64_write_enable(void) //写使能
{
spi_start();
spi_swap_byte(W25Q64_WRITE_ENABLE);
spi_stop();
}
//作用:轮询状态寄存器 1(发送 0x05 指令),检测最低位(BUSY 位,即 0x01)是否为 1。
//逻辑:
//擦除或写入需要时间(几毫秒到几秒),这段时间芯片内部在忙,不能接收新指令。
//循环发送 0xFF 读取状态,如果 & 0x1 为 1,说明还在忙。
//加了 timeout 超时机制,防止芯片故障导致死循环。
//重要性:每次擦除或写入后,必须调用此函数,确保芯片空闲后再进行下一步操作。
void W25Q64_wait_busy(void) //等待忙碌
{
uint32_t timeout;
spi_start();
spi_swap_byte(W25Q64_READ_STATUS_REGISTER_1);
timeout = 100000;
while((spi_swap_byte(W25Q64_DUMMY_BYTE) & 0x1) == 0x01)
{
timeout--;
if (0 == timeout)
{
break;
}
}
spi_stop();
}
//作用:向指定地址写入数据(最大 256 字节)。
//执行流程:
//W25Q64_write_enable() (开锁)。
//spi_start() (选中)。
//发送页编程指令 0x02。
//发送 24 位地址(addr>>16、addr>>8、addr,分别发送高、中、低 8 位)。
//循环发送 arr 里的数据。
//spi_stop() (释放)。
//W25Q64_wait_busy() (等待写入完成)。
//注意:Flash 是按页写的,一页 256 字节。
//这个函数没有处理"跨页写入"逻辑(len 超过 256 字节时会自动覆盖回页首,造成数据混乱)
//实际使用时需要应用层限制 len 或分段写入。
void W25Q64_page_program(uint32_t addr, uint8_t *arr, uint16_t len)
{
uint8_t i;
W25Q64_write_enable();
spi_start();
spi_swap_byte(W25Q64_PAGE_PROGRAM);
spi_swap_byte(addr >> 16);
spi_swap_byte(addr >> 8);
spi_swap_byte(addr);
for (i = 0; i < len; i++)
{
spi_swap_byte(arr[i]);
}
spi_stop();
W25Q64_wait_busy();
}
//作用:擦除指定地址所在的 4KB 扇区。
//执行流程:
//W25Q64_write_enable() (开锁)。
//spi_start()。
//发送扇区擦除指令 0x20。
//发送 24 位地址(告诉芯片擦哪里)。
//spi_stop()。
//W25Q64_wait_busy() (擦除非常耗时,必须等待)。
//重要性:Flash 物理特性决定:只能把 1 变成 0,不能把 0 变成 1。所以写入新数据前,必须先把目标扇区擦除(全部变成 0xFF)。
void W25Q64_sector_erase(uint32_t addr)
{
W25Q64_write_enable();
spi_start();
spi_swap_byte(W25Q64_SECTOR_ERASE_4KB);
spi_swap_byte(addr >> 16);
spi_swap_byte(addr >> 8);
spi_swap_byte(addr);
spi_stop();
W25Q64_wait_busy();
}
//作用:从指定地址开始,连续读取 len 个字节存入数组 arr。
//执行流程:
//spi_start()。
//发送读数据指令 0x03。
//发送 24 位地址。
//循环 len 次:arr[i] = spi_swap_byte(0xFF) (发送哑字节踩时钟,从 MISO 读取数据)。
//spi_stop()。
//注意:读取操作不需要"写使能",也不需要等待忙碌(除非刚好碰巧芯片在擦除/写入中)
void W25Q64_read_data(uint32_t addr, uint8_t *arr, uint16_t len)
{
uint8_t i = 0;
spi_start();
spi_swap_byte(W25Q64_READ_DATA);
spi_swap_byte(addr >> 16);
spi_swap_byte(addr >> 8);
spi_swap_byte(addr);
for (i = 0; i < len; i++)
{
arr[i] = spi_swap_byte(W25Q64_DUMMY_BYTE);
}
spi_stop();
}
硬件

1. 写入操作前,必须先进行写使能
- 原理解析 :这是安全机制。Flash 芯片默认是拒绝写入的。每次你要修改数据(写或擦除)之前,必须先发一条指令
0x06(Write Enable),把状态寄存器里的 WEL 位置 1。注意:每次写/擦除操作完成后,WEL 会自动清零 ,所以下一次写之前必须重新发0x06。
2. 每个数据位只能由 1 改写为 0,不能由 0 改写为 1
- 原理解析 :这是 NOR Flash 的物理特性。写入操作(Program)本质上是一个"与(AND)"操作。
1 AND 0 = 0,0 AND 0 = 0,但0 AND 1 = 0。也就是说,只要写进去,这一位就永久变成0了(除非擦除)。
3. 写入数据前必须先擦除,擦除后,所有数据位变为 1
-
原理解析 :因为上一条特性(只能1变0),如果原本是
0x00,你想写入0xFF是绝对不可能的。擦除(Erase)操作是唯一能让位从0变回1的方法。擦除后,整个区域变成0xFF(全1)。 -
通俗比喻:写入就像是用铅笔在纸上画"0",擦除就像是换了一张全新的白纸(全是1)。
4. 擦除必须按最小擦除单元进行
- 原理解析 :结合上一张图的 W25Q64 结构,最小擦除单元是扇区(Sector,4KB),甚至整块(Block,64KB)。你不能只擦除某一个字节。哪怕你只想修改1个字节,你也得把整个4KB的扇区读出来保存在内存里,擦除整个扇区,然后在内存里修改那1个字节,最后把4KB数据全部写回去(这叫"读-改-写"机制)。
5. 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入(页回卷)
-
原理解析 :这是新手最容易踩的坑!Flash 内部有一个 256 字节的页缓冲区(参考上一张图)。当你在同一页内连续写入超过 256 字节时,地址计数器不会自动跳到下一页,而是回卷(Wrap)到本页的头部,把你刚写进去的开头数据覆盖掉。
-
举例 :假设页大小256字节。你从第 250 字节开始写 10 个字节。最后 4 个字节不会写到下一页,而是强行写回本页的第 0、1、2、3 字节,把原有的数据覆盖得乱七八糟。跨页写入必须手动切分数据段。
6. 写入操作结束后,芯片进入忙状态,不响应新的读写操作
-
原理解析 :Flash 的写入(编程)和擦除需要很高的内部电压(高压发生器工作),耗时较长(通常几毫秒,擦除甚至几十到几百毫秒)。在这个期间,你必须等待。
-
代码实现 :写/擦除指令发完后,你需要不断发送"读状态寄存器"指令(
0x05),检查 Busy 位(第0位)。当 Busy 位变为 0 时,才代表芯片空闲,可以进行下一步操作。
📖 读取操作时的特别说明
相比写入,读取非常自由:
-
无需使能、无页限制 :读取就像流水一样,可以直接从地址 0 一直连续读到末尾(8MB 读完都行),不需要发
0x06。只要给时钟(SCK),芯片就会源源不断把数据吐出来。 -
读操作不产生忙状态:纯读取非常快,读完立刻结束,不需要等待。
-
不能在忙状态时读取 :这是唯一的限制。如果芯片正在擦除或写入(Busy=1),你去发读取指令(
0x03),芯片是不理你的,读回来的全是无效数据。必须等状态寄存器的 Busy 位清零才能读。
编程实战总结(写Flash的标准流程)
基于这页PPT,在写 C 语言代码(如 STM32 HAL库)操作 W25Q64 时,必须遵循以下流程:
-
读数据 :直接发
0x03+ 24位地址,拿数据走人。 -
写数据(修1个字节也要走全套):
-
计算出这个字节所在的扇区(4KB对齐)。
-
把这个扇区的 4KB 数据全部读进单片机的 RAM。
-
在 RAM 里修改你要改的字节。
-
发
0x06(写使能)。 -
发
0x20(扇区擦除指令)+ 扇区地址。等待 Busy 变 0。 -
发
0x06(再次写使能,因为擦除让 WEL 归零了)。 -
发
0x02(页写入指令)+ 24位地址 + RAM里的4KB数据(注意每次最多发256字节,然后切分新页继续发)。等待 Busy 变 0。 -
完成。
-
SPI外设简介

- 硬件SPI的优势
- 减轻CPU负担:如果你用普通GPIO(软件模拟SPI),发送一个字节需要CPU手动拉高拉低SCK、翻转MOSI,极度消耗CPU算力,且容易受中断干扰导致时钟变形。STM32内部集成了硬件SPI电路,你只要把数据丢进寄存器(DR),硬件会自动产生时钟、自动移位收发,CPU在此期间可以去干别的事。
- 高度可配置
-
数据帧大小:支持8位或16位。因为我们前面讲的 W25Q64 都是按8位(1字节)操作,所以配置为8位。
-
位序 :高位先行(MSB First)或低位先行。W25Q64 的时序图证明它需要高位先行。
-
时钟频率配置:公式是 fPCLK/分频系数fPCLK/分频系数。fPCLKfPCLK 是外设总线时钟频率。
- 实战注意 :STM32F103的 SPI1挂在APB2总线上(最高72MHz) ,SPI2挂在APB1总线上(最高36MHz)。分频系数有2~256。如果你的W25Q64最高支持80MHz,用SPI1(72MHz)刚好;如果你分频系数设为2,得到36MHz,这也是最常见的初始化频率。
- 多主机、主从模式
- STM32既可以做主设备(控制W25Q64、OLED),也可以做从设备(被其他单片机控制)。但在我们目前的场景里,STM32永远是主机(Master)。
- 半双工/单工通信
-
标准SPI是全双工(MOSI和MISO同时工作)。
-
为了节省引脚或提高速度,STM32的SPI可以被精简。
-
半双工:把MOSI和MISO合并成一根线(双向)。
-
单工:只用MOSI或只用MISO。
-
呼应前文 :这其实就演变出了我们之前提过的 Dual SPI(双线,为了更快读Flash) 和 Quad SPI(四线)。STM32F103内置的是标准SPI,如果要跑W25Q64的Quad SPI,往往需要结合DMA或直接用普通GPIO配合(因为STM32F103的硬件SPI不支持原生Quad模式)。
-
- 支持DMA
- 这是读大容量Flash的利器! 当你要从W25Q64读取 4KB 的图片或字库数据时,如果不用DMA,CPU就得中断几万次去搬数据。开启DMA后,你只需发一条读取指令,剩下搬运数据的活全自动完成,CPU甚至可以去睡一觉。
- 兼容I2S协议
- 这是STM32硬件设计的特色:SPI和I2S(音频总线)共用引脚和部分硬件逻辑。如果以后你玩音频(如接DAC播放音乐),可以把SPI外设配置成I2S模式。
- 具体的硬件资源
-
STM32F103C8T6 (也就是最常见的"蓝桥杯"或"最小系统板"芯片)具有 SPI1 和 SPI2。
-
常用引脚(可以通过CubeMX或查手册看):
-
SPI1:PA5(SCK), PA6(MISO), PA7(MOSI)。(PA4常用作SS)。
-
SPI2:PB13(SCK), PB14(MISO), PB15(MOSI)。
-
SPI框图

一、 数据收发主线
这是SPI通信的核心。注意看中间的三个方块:发送缓冲区 -> 移位寄存器 -> 接收缓冲区。
-
发送过程(写入):
-
当你把数据写入
SPI_DR寄存器时,数据首先进入**"发送缓冲区"(Send buffer)**。 -
然后数据会被转移到**"移位寄存器"(Shift register)**。
-
在波特率发生器 产生的时钟驱动下,移位寄存器把数据一位一位地从MOSI引脚推出去。
-
图中的
LSBFIRST控制位决定了是最高位先出(MSB)还是最低位先出(LSB),对应我们之前讲的W25Q64需要 MSB First。
-
-
接收过程(读出):
-
从MISO 引脚进来的数据,被一位一位地移入**"移位寄存器"**。
-
当接收完一个完整的字节后,数据会被整体转移到**"接收缓冲区"(Receive buffer)**。
-
最后,CPU通过"地址和数据总线"把数据读走。
-
💡 划重点:这就是之前强调"全双工"的硬件基础。发送和接收共用同一个时钟,但拥有独立的缓冲区和移位路径。
二、 时钟与控制主线
这部分负责给通信提供"节拍"和"指挥"。
-
波特率发生器 (Baud rate generator):
- 它接收外设总线时钟(fPCLKfPCLK),根据右侧
SPI_CR1寄存器里BR[2:0]的值进行分频(2~256分频),产生最终的SCK时钟信号。
- 它接收外设总线时钟(fPCLKfPCLK),根据右侧
-
主控制电路 (Master control circuit):
-
接收NSS (片选)引脚信号。图中
SSM和SSI控制位用来配置软件/硬件片选。 -
它还控制着通信状态和CRC校验逻辑。
-
三、 寄存器控制区(图右侧虚线框)
这是你在写代码(如HAL库初始化)时最常打交道的地方,分成了三个关键寄存器:
-
SPI_CR1(控制寄存器1) ------ 最核心的配置寄存器-
CPOL和CPHA:决定SPI的四种工作模式(Mode 0~3),对应我们之前看过的时序图。 -
MSTR:主从模式选择(1为主机,0为从机)。 -
SPE:SPI使能位(开启/关闭SPI外设)。 -
BR[2:0]:波特率分频系数。 -
LSBFIRST:位序选择。 -
BIDIMODE/BIDIOE/RXONLY:半双工/单工配置(之前PPT里提到的精简模式)。 -
DFF:数据帧格式(8位或16位)。
-
-
SPI_CR2(控制寄存器2) ------ 高级功能配置-
TXDMAEN/RXDMAEN:DMA使能位(之前PPT提到支持DMA,就是在这里开启的)。 -
TXEIE/RXNEIE/ERRIE:各种中断使能位(发送空中断、接收非空中断等)。
-
-
SPI_SR(状态寄存器) ------ 告诉CPU当前状态-
TXE:发送缓冲区为空(可以写下一个数据了)。 -
RXNE:接收缓冲区非空(有数据可以读了)。 -
BSY:忙标志(对应之前操作Flash时"芯片进入忙状态",其实就是检测这个位)。这也是CPU轮询发送/接收时判断的关键标志位。
-
🔗 与前面知识的完美闭环
结合之前的W25Q64硬件图:
-
你发
0x06时,代码把数据放进发送缓冲区,硬件自动通过移位寄存器从MOSI发出。 -
此时SPI_SR的
BSY位会置1(表示在忙)。 -
同时,从机返回的
0xFF会通过MISO进入移位寄存器,然后存入接收缓冲区,把RXNE置1。 -
你的代码检测到
RXNE=1,就把数据读走。
SPI基本结构

-
数据控制器(Data Controller)
- 位于最左侧,是整个 SPI 模块的"大脑"。它负责接收 CPU/总线的指令,控制数据的发送与接收流程,并管理移位寄存器的操作。
-
数据寄存器组(TDR 与 RDR)
-
发送数据寄存器 TDR(Transmit Data Register):用户(CPU)将准备发送的数据写入 TDR,等待被送入移位寄存器。
-
接收数据寄存器 RDR(Receive Data Register):接收到的数据从移位寄存器转入 RDR,等待 CPU 读取。
-
这两个寄存器起到了缓冲作用,使得 CPU 无需等待缓慢的串行传输,提高了效率。
-
-
移位寄存器(
<<移位寄存器<<)- 这是 SPI 实现"串行"与"并行"转换的核心。发送时,数据从 TDR 并行加载到移位寄存器,然后逐位(通常是 MSB 先行或 LSB 先行)移出;接收时,数据逐位移入,移满一个字节/字后再并行送入 RDR。
-
波特率发生器(Baud Rate Generator)
-
负责产生 SPI 通信所需的时钟信号。通过配置分频系数,它可以产生不同频率的时钟,控制数据传输的速率。
-
产生的时钟信号通过
GPIO输出到外部引脚 SCK(Serial Clock)。
-
-
GPIO 与外部引脚(SCK、MOSI、MISO)
-
图中的绿色方块代表通用输入输出接口,它们将内部的逻辑信号转换为外部物理引脚上的电平。
-
SCK:时钟线,由主设备控制。
-
MOSI(Master Out Slave In):主设备输出、从设备输入的数据线。
-
MISO(Master In Slave Out):主设备输入、从设备输出的数据线。
-
-
开关控制(Switch Control)
- 位于右下角,通常用于控制 SPI 模块的使能/禁用,或者用于多主从架构中的片选(CS/SS)信号管理。
二、 数据收发流程
根据图中的箭头,我们可以梳理出数据传输的路径:
-
发送流程 :
数据控制器->发送数据寄存器TDR->移位寄存器->GPIO->MOSI(发送至从设备)。同时,
波特率发生器->GPIO->SCK,提供同步时钟。 -
接收流程 :
MISO(从设备发送过来)->GPIO->移位寄存器->接收数据寄存器RDR->数据控制器读取。
三、 SPI 结构的几个关键特性
-
全双工通信:注意图中的接收和发送路径是独立的(MOSI 和 MISO 分离),这意味着 SPI 允许主从设备同时发送和接收数据(利用移位寄存器,发送和接收是同一个时钟周期内完成的)。
-
同步传输:由图中的波特率发生器可知,SPI 是同步通信,依赖时钟线 SCK 进行数据采样。
-
硬件开销小、效率高:因为使用硬件移位寄存器进行并串转换,不需要像 UART 那样在数据前后加起止位,也没有复杂的协议解析,软件开销非常小,传输速率通常很高
硬件读写W25Q64
cpp
main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"
int main(void)
{
uint8_t mid;
uint16_t did;
uint8_t array_w[4] = {0x11, 0x22, 0x33, 0x44};
uint8_t array_r[4];
//初始化
OLED_Init();
W25Q64_init();
OLED_ShowString(1, 1, "MID: DID:");
OLED_ShowString(2, 1, "W:");
OLED_ShowString(3, 1, "R:");
W25Q64_read_id(&mid, &did);
OLED_ShowHexNum(1, 5, mid, 2);
OLED_ShowHexNum(1, 12, did, 4);
//擦除扇区
W25Q64_sector_erase(0x0);
//写扇区
W25Q64_page_program(0x0, array_w, 4);
//读数据
W25Q64_read_data(0x0, array_r, 4);
OLED_ShowHexNum(2, 3, array_w[0], 2);
OLED_ShowHexNum(2, 6, array_w[1], 2);
OLED_ShowHexNum(2, 9, array_w[2], 2);
OLED_ShowHexNum(2, 12, array_w[3], 2);
OLED_ShowHexNum(3, 3, array_r[0], 2);
OLED_ShowHexNum(3, 6, array_r[1], 2);
OLED_ShowHexNum(3, 9, array_r[2], 2);
OLED_ShowHexNum(3, 12, array_r[3], 2);
while(1)
{
}
return 0;
}
spi.c
#include "stm32f10x.h" // Device header
/*
CS: PA4
CLK: PA5
DO: PA6
DI: PA7
*/
//SS写 PA4
void spi_W_SS(uint8_t bitval)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)bitval);
}
void spi_init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
SPI_InitTypeDef SPI_InitStruct;
//使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
//A4 CS
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
//A5 A7 CLK DI
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
//A6 DO
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 1. 波特率预分频器:设置 SCK 时钟频率 = PCLK / 128
// 作用:控制 SPI 的通信速度。128 分频意味着时钟很慢,适合长导线或低频外设。
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;
// 2. 时钟相位 (CPHA):选择第一个边沿采样 (1Edge)
// 作用:数据在第一个时钟边沿被采样(移入),第二个边沿移出。
SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge;
// 3. 时钟极性 (CPOL):空闲状态为低电平 (Low)
// 作用:SCK 空闲时为低。结合 CPHA,这就是经典的 SPI 模式0(Mode 0)。
// 与你之前逻辑分析仪上的波形(上升沿采样)完全对应!
SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low;
// 4. CRC 多项式:设置为 7
// 作用:用于 SPI 硬件 CRC 校验。如果不使用 CRC 功能,这个值随便填(通常写7),不影响正常通信。
SPI_InitStruct.SPI_CRCPolynomial = 7;
// 5. 数据大小:8位 (8b)
// 作用:每次收发一个标准的 8 位字节(正好对应 spi_swap_byte 里的一个字节)。
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
// 6. 方向:双线全双工 (2Lines_FullDuplex)
// 作用:启用 MISO 和 MOSI 两根数据线,允许同时收发。
// 对应你之前的疑问:全双工!发出去一位,同时收回一位。
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
// 7. 首位顺序:高位先行 (MSB First)
// 作用:先发送最高位(第7位)。对应你手写代码里的 `0x80 >> i` 或 `val << 1`。
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
// 8. 模式:主机模式 (Master)
// 作用:STM32 主动产生 SCK 时钟,控制整个通信过程。
SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
// 9. NSS 管理:软件模式 (Soft)
// 作用:不使用硬件 NSS 引脚自动拉低,而是由程序手动控制普通 GPIO 引脚充当片选(CS)。
// 对应你手写代码里的 spi_start() / spi_stop(),以及控制 W25Q64_CS 拉低拉高的操作。
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;
SPI_Init(SPI1, &SPI_InitStruct);
SPI_Cmd(SPI1, ENABLE);
spi_W_SS(1);
}
void spi_start(void)
{
spi_W_SS(0);
}
void spi_stop(void)
{
spi_W_SS(1);
}
uint8_t spi_swap_byte(uint8_t val)
{
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);
SPI_I2S_SendData(SPI1, val);
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);
return SPI_I2S_ReceiveData(SPI1);
}
w25q64.c
#include "stm32f10x.h" // Device header
#include "SPI.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
void W25Q64_init(void)
{
spi_init();
}
void W25Q64_read_id(uint8_t *mid, uint16_t *did)
{
spi_start();
spi_swap_byte(W25Q64_JEDEC_ID);
*mid = spi_swap_byte(W25Q64_DUMMY_BYTE);
*did = spi_swap_byte(W25Q64_DUMMY_BYTE);
*did <<= 8;
*did |= spi_swap_byte(W25Q64_DUMMY_BYTE);
spi_stop();
}
void W25Q64_write_enable(void)
{
spi_start();
spi_swap_byte(W25Q64_WRITE_ENABLE);
spi_stop();
}
void W25Q64_wait_busy(void)
{
uint32_t timeout;
spi_start();
spi_swap_byte(W25Q64_READ_STATUS_REGISTER_1);
timeout = 100000;
while((spi_swap_byte(W25Q64_DUMMY_BYTE) & 0x1) == 0x01)
{
timeout--;
if (0 == timeout)
{
break;
}
}
spi_stop();
}
void W25Q64_page_program(uint32_t addr, uint8_t *arr, uint16_t len)
{
uint8_t i;
W25Q64_write_enable();
spi_start();
spi_swap_byte(W25Q64_PAGE_PROGRAM);
spi_swap_byte(addr >> 16);
spi_swap_byte(addr >> 8);
spi_swap_byte(addr);
for (i = 0; i < len; i++)
{
spi_swap_byte(arr[i]);
}
spi_stop();
W25Q64_wait_busy();
}
void W25Q64_sector_erase(uint32_t addr)
{
W25Q64_write_enable();
spi_start();
spi_swap_byte(W25Q64_SECTOR_ERASE_4KB);
spi_swap_byte(addr >> 16);
spi_swap_byte(addr >> 8);
spi_swap_byte(addr);
spi_stop();
W25Q64_wait_busy();
}
void W25Q64_read_data(uint32_t addr, uint8_t *arr, uint16_t len)
{
uint8_t i = 0;
spi_start();
spi_swap_byte(W25Q64_READ_DATA);
spi_swap_byte(addr >> 16);
spi_swap_byte(addr >> 8);
spi_swap_byte(addr);
for (i = 0; i < len; i++)
{
arr[i] = spi_swap_byte(W25Q64_DUMMY_BYTE);
}
spi_stop();
}