(1)SPI程序层次
一、核心逻辑:"SPI Flash 操作" 是怎么跑起来的?
要读写 SPI Flash,需同时理解 硬件连接(怎么接线) 和 软件分层(谁负责发指令、谁负责控制逻辑),二者结合才能让数据正确读写。
二、逐图拆解(从 "硬件物理连接" 到 "软件逻辑分层")
1. 「硬件接线 - 实际底板」
- 内容 :展示 DShanMCU-F103 学习底板上,SPI Flash 模块的物理安装位置(丝印 "FLASH 模块" 的排母接口)。
- 作用:实操时知道 "模块插哪里",确保硬件连接正确。
- 关键细节 :
- 模块名称:"Flash 模块"(红框标注)。
- 底板标识:DShanMCU-F103 Base Board,确认自己用的开发板匹配。
2. 「软件层次 - 操作 Flash 的分工」
- 内容 :把 "操作 SPI Flash" 的软件拆成 3 层,明确每层职责:
- 应用程序 (最上层):决定 "读写什么数据、在 Flash 的哪个位置操作"(比如你写的
main.c
里,要存一个汉字字模到 Flash 地址0x0800
)。 - Flash 驱动 (中间层):把应用程序的 "读写需求" 转成 SPI 协议命令 (比如读操作要发
0x03
指令 + 地址,写操作要先发擦除命令再写)。 - SPI 控制器驱动(HAL)(最下层):最基础的 "SPI 硬件控制",负责发送具体的 SPI 时序信号(时钟、数据),跟硬件寄存器直接交互。
- 应用程序 (最上层):决定 "读写什么数据、在 Flash 的哪个位置操作"(比如你写的
- 作用:理解 "软件怎么分层协作",开发时知道改哪一层(比如换 Flash 型号,只需改 "Flash 驱动层";换主控芯片,可能改 "HAL 层")。
3. 「硬件框图 - 系统级连接逻辑」
- 内容 :从 SoC(芯片系统) 角度,展示 CPU、内存管理单元、SPI 控制器、Flash 之间的连接关系:
- CPU :发读写指令(比如 "读 Flash 地址
0x1000
的数据")。 - 内存管理单元:处理地址映射、片选信号(决定操作 RAM 还是 Flash 还是其他外设)。
- SPI 控制器:把 CPU 的指令转成 SPI 时序(时钟 SCK、数据 MOSI/MISO、片选 CS 等),发给外部 Flash。
- Flash:接收 SPI 信号,执行读写擦除操作,返回数据。
- CPU :发读写指令(比如 "读 Flash 地址
- 作用:理解 "硬件模块如何协同工作",比如为什么操作 Flash 时要控制片选信号(片选 3 对应 Flash),地址怎么通过内存管理单元转发。
4. 「硬件原理图 - 引脚级连接」
- 内容 :SPI Flash 模块与 STM32 主控的具体引脚连接 :
- PA7(DI/SPI1 MOSI):主控 → Flash 发数据(Master Out Slave In)。
- PA5(SCK/SPI1 SCK):主控给 Flash 提供时钟信号。
- PA6(DO/SPI1 MISO):Flash → 主控返回数据(Master In Slave Out)。
- PB9(CS):片选信号,拉低时选中 Flash 模块(同一总线上可能接多个 SPI 设备,靠 CS 区分)。
- VCC +3.3V :给 Flash 模块供电;GND:接地。
- 关键细节 :
- "M6/M12 要互斥操作":同一套 SPI 引脚可能接多个模块(比如 M6 和 M12),同一时间只能选一个(通过不同 CS 控制)。
- 引脚功能:MOSI 发命令 / 地址 / 数据,MISO 收数据,SCK 同步时钟,CS 选设备。
(2)SPI协议和SPI控制结构
一、知识地图:SPI 学习的 "原子级" 逻辑链
要彻底掌握 SPI,需理解 "硬件物理层 → 信号时序层 → 控制器内部逻辑层 → 软件驱动层" 的完整闭环
二、逐图拆解:从 "物理线" 到 "寄存器位" 的原子级解析
1. 「硬件接线 - SPI 外设连接」
- 核心作用 :明确 SPI 控制器如何外接多个设备 (SPI Flash、SPI OLED),理解 "共享总线 + 片选区分" 的设计。
- 关键细节(阴暗角落!) :
- 总线共享 :
SCK、DO(MOSI)、DI(MISO)
是共享总线,多个设备并联在这三根线上。 - 片选(CS)的灵魂 :
- 每个设备有独立
CS
(CS0 连 Flash,CS1 连 OLED),低电平有效(拉低对应 CS 才选中设备)。 - 同一时间只能有一个 CS 被拉低,否则总线冲突(比如同时选 Flash 和 OLED,MOSI 发的数据会乱套)。
- 每个设备有独立
- GPIO 的隐藏作用 :如果 SPI 控制器的
NSS
(硬件片选)不用,可通过 GPIO 模拟片选(灵活,但占 GPIO 资源)。
- 总线共享 :
- 关联下一张图 :知道设备咋连后,得理解 "SPI 信号咋交互" → 看时序图。
2. 「SPI 控制器内部框图」
- 核心作用 :揭秘 "SPI 控制器内部咋把 CPU 数据转成时序信号",理解移位寄存器、缓冲器的关键作用。
- 关键模块 & 阴暗细节 :
- 移位寄存器(Shift register) :
- 是 SPI 收发的核心 !发送时,CPU 把数据写入
Tx buffer
,移位寄存器逐位 把数据推到MOSI
;接收时,MISO
来的数据逐位移入 移位寄存器,装满后送到Rx buffer
。 - 不管是 8 位还是 16 位帧,都靠它按位 "搬运" 数据(比如发 0x56,会拆成 8 个比特,在 SCK 时钟下一位位发)。
- 是 SPI 收发的核心 !发送时,CPU 把数据写入
- Tx/Rx buffer(发送 / 接收缓冲) :
Tx buffer
:CPU 写数据到这里,移位寄存器从这里取数据发出去(先入先出,发完一个字节才会取下一个)。Rx buffer
:移位寄存器收完数据,存在这里,CPU 从这里读(必须及时读 ,否则新数据会覆盖旧数据,导致OVR
溢出错误)。
- 波特率发生器(Baud rate generator) :
- 决定
SCK
的频率(比如f_SCK = f_PCLK / (BR[2:0] + 1)
),直接影响传输速度(太快可能导致从机跟不上,出现数据错误)。
- 决定
- 控制寄存器(SPI_CR1、SPI_CR2) :
CPOL、CPHA
:决定时钟极性和相位(后面时序图重点讲)。MSTR
:设为 1 表示主控模式(开发板作为 SPI 主机控制外设)。LSB FIRST
:设为 1 则先传低位(默认先传高位,需看外设要求)。TXEIE、RXNEIE
:发送 / 接收中断使能(数据发完 / 收到时触发中断,异步通信常用)。
- 移位寄存器(Shift register) :
- 关联下一张图 :理解控制器内部后,得掌握 "SCK、MOSI、MISO 咋配合发数据" → 看时序图。
3. 「SPI 时序图(CPOL=1 系列)」
- 核心作用 :明确 "时钟极性(CPOL)、相位(CPHA)" 如何决定数据采样时机,是 SPI 通信的协议灵魂。
- 阴暗细节(逐周期解析!) :
- CPOL(时钟极性) :
CPOL=1
:SCK
空闲时是高电平 ;CPOL=0
:空闲时是低电平(决定时钟的 "基础电平")。
- CPHA(时钟相位) :
CPHA=0
:第一个时钟沿(上升沿 / 下降沿,看 CPOL)采样数据;CPHA=1
:第二个时钟沿采样数据。
- 以
CPOL=1, CPHA=0
(Format A)为例 :SCK
空闲高电平 → 第一个时钟沿是下降沿 (从高→低),此时采样MOSI/MISO
数据。- 数据传输:
MOSI
先发MSB
(最高位),MISO
同步返回数据,8 个时钟周期传完 1 字节。
- 以
CPOL=1, CPHA=1
(Format B)为例 :- 第一个时钟沿(下降沿)不采样,第二个时钟沿(上升沿) 采样数据。
- 注意
MISO
数据延迟:返回的LSB
是 "之前传输字符的低位"(时序对齐需要,外设必须支持)。
- CPOL(时钟极性) :
- 关键坑点 :
- 时序匹配 :主机和从机的
CPOL、CPHA
必须完全一致,否则数据采样错位(比如主机用 CPOL=1,从机用 CPOL=0,时钟沿对不上,数据全错)。 - MSB/LSB 顺序 :默认
LSB FIRST=0
(先传 MSB),如果外设要求先传 LSB,必须设LSB FIRST=1
(看 datasheet!)。
- 时序匹配 :主机和从机的
- 关联下一张图 :对比
CPOL=0
的时序,理解四种模式的差异 → 看CPOL=0 时序图(图 4)。
4. 「SPI 时序图(CPOL=0 系列)」
- 核心作用 :补充
CPOL=0
时的时序差异,理解 "四种 SPI 模式" 的完整逻辑。 - 阴暗细节(与 CPOL=1 对比) :
CPOL=0
:SCK
空闲时是低电平 ,第一个时钟沿是上升沿(从低→高)。CPOL=0, CPHA=0
(Format A) :- 第一个时钟沿(上升沿)采样数据,
MOSI
先发MSB
,MISO
同步返回。
- 第一个时钟沿(上升沿)采样数据,
CPOL=0, CPHA=1
(Format B) :- 第一个时钟沿(上升沿)不采样,第二个时钟沿(下降沿) 采样数据。
MISO
返回的LSB*
是 "之前传输字符的低位"(时序对齐逻辑和 CPOL=1 类似)。
- 关键总结 :
- 四种模式对应
CPOL
(0/1)和CPHA
(0/1)的组合,必须和外设手册一致(比如 SPI Flash 可能要求模式 0,OLED 可能要求模式 3)。 - 常用模式:模式 0(CPOL=0, CPHA=0) 和 模式 3(CPOL=1, CPHA=1) ,因为它们都在上升沿采样(不管空闲电平,只要沿对齐即可,兼容性好)。
- 四种模式对应
5. 「SPI 传输示例(0x56 时序)」
- 核心作用 :用实际数据(0x56 = 0b0101 0110)演示 "时序图如何对应二进制位",把抽象时序落地。
- 阴暗细节(逐位解析) :
- CS0 拉低:选中 SPI Flash,开始传输。
- SCK 时钟与数据的对应 :
- 0x56 的二进制是
0b0101 0110
,MSB
是第 7 位(0),LSB
是第 0 位(0)。 - 每个
SCK
周期对应 1 个比特:- 第 1 个 SCK 下降沿(CPOL=1, CPHA=0 时):发
0
(MSB),Flash 采样。 - 第 2 个 SCK 下降沿:发
1
,依此类推... - 第 8 个 SCK 下降沿:发
0
(LSB),传输结束。
- 第 1 个 SCK 下降沿(CPOL=1, CPHA=0 时):发
- 0x56 的二进制是
- 采样时机 :Flash 在每个 SCK 的上升沿采样(因为示例中可能用了模式 3?需要结合前面的模式图验证)。
- 关键验证 :
- 数一下 SCK 周期和数据位是否对应(8 个周期传 8 位),理解
MSB 先传
的规则。 - 对比前面的时序模式图,看这个示例属于哪种
CPOL、CPHA
组合(比如这里 SCK 空闲高电平 → CPOL=1;下降沿发数据,上升沿采样 → 可能是模式 2 或 3?需要细扣)。
- 数一下 SCK 周期和数据位是否对应(8 个周期传 8 位),理解
6. 「SPI 模式总结表」
- 核心作用 :把四种 SPI 模式的规则表格化,方便快速查询和配置。
- 阴暗细节(表格逐行解析) :
- 模式 0(CPOL=0, CPHA=0) :
- SCK 空闲低电平,第一个时钟沿(上升沿)采样数据 → 常用,很多外设默认支持。
- 模式 1(CPOL=0, CPHA=1) :
- SCK 空闲低电平,第二个时钟沿(下降沿)采样数据 → 部分外设(如某些传感器)可能用。
- 模式 2(CPOL=1, CPHA=0) :
- SCK 空闲高电平,第一个时钟沿(下降沿)采样数据 → 较少用,但某些旧设备可能要求。
- 模式 3(CPOL=1, CPHA=1) :
- SCK 空闲高电平,第二个时钟沿(上升沿)采样数据 → 常用(和模式 0 互补,覆盖上升沿采样场景)。
- 模式 0(CPOL=0, CPHA=0) :
- 关键技巧 :
- 记不住四种模式?记住 "常用模式 0 和 3",它们都在上升沿采样(不管空闲电平),配置时先试这两个模式,不行再查外设手册。
7. 「SPI 传输示例(0x56 时序图)」
- 核心作用 :和图 5 呼应,用更规范的时序图展示
0x56
传输,验证 "数据位与时钟沿的对应关系"。 - 阴暗细节(与图 5 对比) :
- 图 5 是手绘版,图 7 是规范版,都展示
0x56 = 0b0101 0110
的传输。 - 注意
CS0
拉低的时机(传输前拉低,传输后拉高),以及SCK
周期数(8 个周期传 1 字节)。
- 图 5 是手绘版,图 7 是规范版,都展示
- 关键验证 :
- 数
DO
线上的电平是否和0b0101 0110
一致(第 1 个 SCK 周期是0
,第 2 个是1
... 第 8 个是0
)。
- 数
8. 「SPI 主机模式配置步骤」
- 核心作用 :把 "软件配置 SPI 控制器" 的步骤标准化,指导代码怎么写。
- 阴暗细节(逐步骤解析) :
- 1. 配置波特率(BR [2:0]) :
- 决定
SCK
频率(f_SCK = f_PCLK / (BR + 1)
),不能超过外设最大频率(比如 SPI Flash 最大支持 80MHz,就不能设太高)。 - 示例:
BR[2:0] = 011
→ 分频系数 4 →f_SCK = 84MHz / 4 = 21MHz
(假设 PCLK=84MHz)。
- 决定
- 2. 配置 CPOL、CPHA :
- 根据外设手册选模式(比如 Flash 要求模式 0 → CPOL=0, CPHA=0)。
- 3. 配置数据帧格式(DFF) :
DFF=0
→ 8 位帧(常用);DFF=1
→ 16 位帧(某些设备如 OLED 可能用)。
- 4. 配置 LSBFIRST :
LSBFIRST=0
→ 先传 MSB(默认,大多数外设要求);LSBFIRST=1
→ 先传 LSB(少数外设如某些传感器可能用)。
- 5. 配置 NSS(片选) :
- 硬件模式:
NSS
引脚接高电平,靠硬件自动控制; - 软件模式:设
SSM=1, SSI=1
,用软件控制CS
(灵活,适合多设备)。
- 硬件模式:
- 6. 使能 SPI(MSTR、SPE) :
MSTR=1
→ 主机模式;SPE=1
→ 使能 SPI 控制器。
- 1. 配置波特率(BR [2:0]) :
- 关键坑点 :
- 波特率不能乱设:太高会导致从机收不到数据(比如 Flash 最大支持 30MHz,你设成 50MHz,就会丢数据)。
- NSS 配置易错 :软件模式下必须设
SSM=1
,否则NSS
引脚会自动拉低,导致总线冲突。
(3)SPI_HAL库编程
第一步:先搞懂 SPI 是啥 ------ 就像 "多人打电话"
SPI 是单片机(比如 STM32)和其他设备(比如传感器、显示屏)之间 "传数据" 的一种方式,就像几个人用电话通话:
- 主机:STM32(相当于发起通话的人)
- 从机:被控制的设备(比如传感器,相当于接电话的人)
- 线的作用 :
- SCK 线:时钟线(相当于 "喂喂喂" 的节奏,保证双方语速一致)
- MOSI 线:主机发、从机收(主机说话的线)
- MISO 线:从机发、主机收(从机回话的线)
- NSS 线:片选线(主机想跟哪个从机说话,就拉低对应从机的 NSS 线,相当于 "小明,听我说")
第二步:用 CubeMX "搭线路"------ 相当于 "插电话线"
在写代码前,需要用 STM32CubeMX 软件配置 SPI 的 "硬件线路",就像提前插好电话线、设置好通话规则。
1. 配置 SPI 核心参数
打开 CubeMX,找到 SPI 外设(比如 SPI1),配置以下参数:
- 模式:选 "全双工主机"(最常用,主机既能说也能听)
- 帧格式 :
- 数据长度:8 位(一次传 1 个字节,像一次说一个字)
- 高位在前(MSB First,像说话从第一个字开始)
- 时钟极性 / 相位:默认选 "低电平空闲,第一个边沿采样"(记不住没关系,CubeMX 默认值一般能用)
- 时钟分频:比如 "分频 8"(STM32 主频 72MHz 的话,SPI 时钟就是 9MHz,相当于说话的语速,不能太快否则从机听不懂)
- 下面会自动显示引脚:比如 SPI1 的 SCK=PA5、MOSI=PA7、MISO=PA6(这些是硬件固定的,不用改)
2. 配置片选引脚
NSS 线(片选)一般用软件控制(更灵活),需要手动配置一个普通 GPIO 当片选:
- 选一个引脚(比如 PB9),模式设为 "推挽输出"(能输出高低电平)
- 初始状态设为 "高电平"(默认不选中从机,相当于 "先不打电话")
3. 生成代码
配置完后,点 "Generate Code" 生成初始化代码,CubeMX 会自动帮我们写好 SPI 的基础设置(不用自己写)。
第三步:用查询方式 "发消息"------ 相当于 "对着电话一直说,等对方回应"
最基础的通信方式:发送数据时,STM32 会 "一直等" 到发送完成,再做下一步(类似打电话时一直说,直到说完才停)。
1. 发送数据函数
用HAL_SPI_Transmit()
函数发送,格式:
HAL_SPI_Transmit(&hspi1, 发送的数据地址, 数据长度, 超时时间);
&hspi1
:CubeMX 生成的 SPI1 结构体(相当于指定用哪部电话)- 发送的数据地址:比如
&data
(要发的数据存在哪里) - 数据长度:比如 1(发 1 个字节)
- 超时时间:比如 100(最多等 100ms,没发完就报错)
2. 接收数据函数
用HAL_SPI_Receive()
函数接收,格式类似:
HAL_SPI_Receive(&hspi1, 接收数据的缓冲区地址, 长度, 超时时间);
3. 收发同时进行
用HAL_SPI_TransmitReceive()
,一边发一边收(全双工的特点):
HAL_SPI_TransmitReceive(&hspi1, 要发的数据, 接收缓冲区, 长度, 超时时间);
举个例子:给从机发命令并读回数据
uint8_t send_data = 0x55; // 要发的命令
uint8_t recv_data; // 用来存接收的数据
HAL_GPIO_WritePin(PB9_GPIO_Port, PB9_Pin, GPIO_PIN_RESET); // 拉低PB9,选中从机("小明,听着")
HAL_SPI_TransmitReceive(&hspi1, &send_data, &recv_data, 1, 100); // 发命令同时收数据
HAL_GPIO_WritePin(PB9_GPIO_Port, PB9_Pin, GPIO_PIN_SET); // 拉高PB9,释放从机("说完了")
第四步:用中断方式 "发消息"------ 相当于 "说完一段话就挂电话,对方听完打回来"
查询方式会让 STM32 一直等着,效率低。中断方式是:STM32 发起发送后,就去做别的事,等数据发完了,硬件会 "打断" STM32,提醒它 "发送完成了"(类似发微信,不用一直盯着,收到回复再看)。
1. 中断函数怎么用
用带_IT
后缀的函数,比如:
- 发送:
HAL_SPI_Transmit_IT(&hspi1, 数据地址, 长度);
- 接收:
HAL_SPI_Receive_IT(&hspi1, 缓冲区, 长度);
- 收发同时:
HAL_SPI_TransmitReceive_IT(...);
这些函数调用后会立刻返回,STM32 可以去干别的(比如亮灯、读按键)。
2. 中断是怎么 "提醒" 的(对应 "图 4→图 3→图 5→图 2")
中断的流程像 "快递送货":
(SPI1_IRQHandler):硬件中断入口(相当于快递员到家门口按门铃),这是 STM32 芯片自带的函数,会自动调用 HAL 库的处理函数。
(HAL_SPI_IRQHandler):HAL 库的中断总处理(相当于家人听到门铃,去开门),它会检查是发送中断还是接收中断,然后调用具体的处理函数。
(中断初始化逻辑) :在调用
_IT
函数时,HAL 库会提前 "绑定" 好具体处理函数(比如 8 位数据对应SPI_2linesRxISR_8BIT
),相当于 "告诉家人,快递来了怎么处理"。(SPI_2linesRxISR_8BIT):实际处理接收的函数(相当于家人接过快递,拆开看),会把收到的字节存到缓冲区,直到收完所有数据。
3. 收到 "提醒" 后做什么 ------ 回调函数
当中断处理完数据(比如发送完成、接收完成),HAL 库会自动调用 "回调函数",我们可以在回调函数里写后续操作(比如收到数据后计算、点灯)。
常用回调函数:
HAL_SPI_TxCpltCallback()
:发送完成回调HAL_SPI_RxCpltCallback()
:接收完成回调HAL_SPI_ErrorCallback()
:出错时回调
这一步放 "图 6",它列出了各种回调函数,说明什么时候会被调用
举个例子:用中断接收数据
uint8_t recv_buf[5]; // 接收缓冲区
// 启动中断接收(收5个字节)
HAL_GPIO_WritePin(PB9_GPIO_Port, PB9_Pin, GPIO_PIN_RESET); // 选中从机
HAL_SPI_Receive_IT(&hspi1, recv_buf, 5); // 启动接收,立刻返回
// 接收完成后,自动调用这个回调函数
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
if (hspi == &hspi1) { // 确认是SPI1的中断
HAL_GPIO_WritePin(PB9_GPIO_Port, PB9_Pin, GPIO_PIN_SET); // 释放从机
// 这里可以处理收到的recv_buf数据,比如打印、计算
}
}
第五步:用 DMA 方式 "发消息"------ 相当于 "雇个快递员,自己不用管"
如果要传大量数据(比如发 1000 个字节),用查询或中断会占用 STM32 太多时间。DMA 方式相当于 "雇个快递员",让 DMA 控制器直接搬运数据,STM32 全程不用插手(效率最高)。
DMA 函数怎么用
用带_DMA
后缀的函数,比如:
- 发送:
HAL_SPI_Transmit_DMA(&hspi1, 数据地址, 长度);
- 接收:
HAL_SPI_Receive_DMA(&hspi1, 缓冲区, 长度);
- 收发同时:
HAL_SPI_TransmitReceive_DMA(...);
调用后,DMA 会自动搬数据,完成后通过中断通知 STM32(和中断方式一样,会调用回调函数)。
(4)SPI_Flash_W25Q64操作方法
一、基础概念与整体逻辑
W25Q64 是常用的 SPI 接口 Flash 存储芯片,要操作它(读、写、擦除等),需遵循 "先理解芯片存储结构 → 掌握 SPI 指令交互流程 → 按步骤实现读写擦操作" 的逻辑。
简单说:
- 存储结构:芯片像很多 "小格子",有页(256 字节)、扇区(4KB = 16 页 )等划分,操作要对应这些单位。
- SPI 交互:通过 SPI 总线发 "指令 + 地址 + 数据",让 Flash 执行读、写、擦除,还要用状态寄存器判断操作是否完成。
二、核心流程串联
1. 读数据流程(最基础,先学它!)
作用:从 Flash 指定地址把数据读出来,比如读取之前存的配置、日志。
(1)读操作时序
-
(9.5.1 读数据 - 时序图) :
这是读操作的完整 SPI 时序,步骤分解:
- 拉低 /CS:选中 Flash 芯片(相当于敲门说 "我要操作你啦" )。
- 发读指令(0x03) :通过 MOSI 线发指令
0x03
,告诉 Flash "我要读数据" 。 - 发 24 位地址 :接着发要读取的地址(比如
0x000000
),告诉 Flash "从这个位置开始读" 。 - 读数据:地址发完后,Flash 会从 MISO 线把数据传回来,读一个字节后,内部地址自动 +1,可连续读很多数据,直到 /CS 拉高。
-
补充解释了 "发完地址后,Flash 持续输出数据,直到操作结束",帮你理解 "连续读" 逻辑 ------ 读一个字节后,地址自动递增,能一直读到芯片末尾,不用每次重新发地址。
2. 写数据流程(要擦除后才能写,稍复杂 )
注意 :Flash 特性是 "写之前必须擦除"(因为只能从 1 改 0,擦除是把 0 改回 1 ),所以写操作分 "擦除扇区 → 写使能 → 烧写页" 三步。
(1)写使能
作用 :告诉 Flash "我要准备写数据 / 擦除了,打开写权限",是写、擦除操作的 必要前提 。
- (写使能时序) :
步骤:- 拉低 /CS → 选中芯片。
- 发写使能指令
0x06
→ 告诉 Flash "允许我写数据啦" 。 - 拉高 /CS → 结束操作。
为什么必须? :Flash 有 "写保护",发 0x06
是解除保护的钥匙,否则写、擦除会失败!
(2)擦除扇区
作用 :写数据前,必须把要写的区域擦成 "全 1",W25Q64 最小擦除单位是 扇区(4KB = 16 页 ) 。
- (擦除扇区时序) :
步骤:- 拉低 /CS → 选中芯片。
- 发擦除指令
0x20
→ 告诉 Flash "我要擦除扇区" 。 - 发 24 位地址 → 指定要擦除的扇区(比如地址
0x000000
对应第 0 扇区 )。 - 拉高 /CS → 启动擦除(擦除需要时间,不是立刻完成 )。
怎么判断擦除完成? :擦除时 Flash 内部忙,要读 状态寄存器(后面讲) 看 BUSY
位,BUSY=0
才代表擦除完!
(3)烧写页
作用 :把数据写入 Flash,最小写入单位是 页(256 字节 ) ,可从页内任意位置开始写,写超页末尾会 "绕回页开头" 。
- (烧写页时序) :
步骤:- 拉低 /CS → 选中芯片。
- 发页编程指令
0x02
→ 告诉 Flash "我要写数据到页里" 。 - 发 24 位地址 → 指定要写入的页起始地址(比如
0x000000
对应第 0 页 )。 - 发数据(最多 256 字节 )→ 把要存的数据通过 MOSI 发过去,写超 256 字节会覆盖页开头数据。
- 拉高 /CS → 启动写入(同样要等状态寄存器
BUSY=0
才完成 )。
3. 状态寄存器
作用 :不管是擦除还是写入,Flash 都需要时间完成(尤其是擦除,可能要几毫秒到几十毫秒 )。通过读 状态寄存器 里的 BUSY
位,能判断操作是否完成。
- (状态寄存器结构) :
这是状态寄存器的 8 位含义,重点看 最低位(S0)→ BUSY 位 :BUSY=1
:Flash 正在忙(擦除、写入中 ),不能执行新操作。BUSY=0
:操作完成,可执行下一个指令。
怎么读状态寄存器? :发专门的 "读状态寄存器指令(0x05
)",流程类似读数据:拉低 /CS → 发 0x05
→ 读 1 个字节(状态寄存器值 )→ 拉高 /CS 。
4. 芯片存储结构说明
作用:理解 "页、扇区、块" 的划分,知道操作单位,避免写错区域或擦除范围不对。
- (英文说明 - 存储结构) :
关键翻译:- W25Q64 有 32768 页 ,每页 256 字节(所以总容量 32768×256 = 8MB = 64Mbit,对应型号 )。
- 擦除单位:
- 扇区(4KB ):16 页擦除一次(
0x20
指令 )。 - 块(32KB ):128 页擦除一次(另一个指令,比如
0x52
,课程里没细讲但要知道有更大单位 )。 - 块(64KB ):256 页擦除一次(指令
0xD8
)。 - 整片擦除:全部内容擦除(指令
0xC7
)。
- 扇区(4KB ):16 页擦除一次(
实际用:小数据写入用 "页编程",大范围擦除用 "扇区 / 块擦除",根据需求选。
三、完整操作流程总结(从读 → 擦除 → 写 )
把上面的步骤串起来,比如 "要写数据到 Flash 某地址",完整流程是:
-
擦除对应扇区:
- 发写使能→ 发擦除扇区指令 + 地址→ 循环读状态寄存器,直到
BUSY=0
。
- 发写使能→ 发擦除扇区指令 + 地址→ 循环读状态寄存器,直到
-
烧写页数据:
- 发写使能→ 发页编程指令 + 地址 + 数据→ 循环读状态寄存器,直到
BUSY=0
。
- 发写使能→ 发页编程指令 + 地址 + 数据→ 循环读状态寄存器,直到
-
验证数据(读操作):
- 用读指令读刚才写的地址,对比数据是否正确。
(5)W25Q64 SPI Flash 驱动开发(内部函数篇)保姆级笔记
这篇笔记只关注 "如何实现功能",不纠结代码规范,带你你从 "新建文件" 到 "每个函数怎么用" 一步步看懂,零基础也能跟着做!
一、前期准备:新建文件并添加到工程
1. 新建文件

-
打开你的工程文件夹(比如叫 "STM32_Project"),右键新建两个文件:
- 一个叫
driver_spi_flash.c
(放具体代码逻辑) - 一个叫
driver_spi_flash.h
(放函数声明,后面会用到)
- 一个叫
2. 添加到工程
- 打开编程软件(比如 Keil MDK),右键 "Source Group"→"Add Files to Group",选中刚新建的
driver_spi_flash.c
,点 "Add"。
二、硬件配置回顾(代码能跑的前提)
在写代码前,要先通过 CubeMX 配置好硬件,这些配置是代码能正常运行的基础:
1. SPI 外设配置
- 简单说:STM32 作为 "主机",通过 SPI1 和 W25Q64 通信,用 "中断方式" 收发数据(后面代码里会用到
HAL_SPI_Transmit_IT
这类函数)。
2. 片选引脚配置
- 作用:PB9 是控制 W25Q64 "是否工作" 的开关,拉低就是 "选中它",拉高就是 "不选中"。
三、driver_spi_flash.c
代码逐行解析
从最基础的 "选芯片" 到 "发指令",每个函数都讲清楚怎么用、为什么这么写:
1. 头文件和变量声明
#include "driver_spi_flash.h" // 自己建的头文件,后面会放函数声明
#include "ascii_font.c" // 可能是字库文件,暂时用不到可以不管
#include "stm32f1xx_hal.h" // HAL库的核心文件,提供SPI、GPIO等函数
// 声明两个等待函数(在其他文件里实现,比如spi.c)
void Wait_spi1_txcplt(void); // 等SPI发送完成
void Wait_spi1_txrxcplt(void); // 等SPI收发完成
// 引用外部的SPI1配置结构体(CubeMX自动生成的,存着SPI的各种参数)
extern SPI_HandleTypeDef hspi1;
作用:引入必要的工具(函数、变量),让下面的代码能正常调用。
2. 内部函数:控制片选(选芯片 / 不选芯片)
用法 :每次和 W25Q64 通信前,先用 spiFlash_select()
选中它;通信结束后,用 spiFlash_deselect()
释放它。
3. 内部函数:写使能(允许芯片被写入 / 擦除)
// 发送"写使能"指令(0x06),让芯片允许后续的写入或擦除操作
static int spiFlash_writeEnable(void)
{
uint8_t BUF[1] = {0x06}; // 定义要发送的指令:0x06就是"写使能"的意思
// 用中断方式发送这个指令:启动发送后,函数会立刻返回,等发送完会触发中断
HAL_SPI_Transmit_IT(&hspi1, BUF, 1);
Wait_spi1_txcplt(); // 等待发送完成(这个函数会一直等,直到SPI发送完数据)
}
(片选拉低→发 0x06→片选拉高)
为什么要做:W25Q64 默认不允许写入或擦除,必须先发这个指令 "解锁",否则后续操作会失败。
4. 内部函数:读状态寄存器(判断芯片是否忙)
// 读芯片的状态寄存器,判断它是否在工作(比如正在擦除或写入)
static int spiFlash_readstatus(void)
{
uint8_t txBUF[2] = {0x05, 0xff}; // 要发送的内容:0x05是"读状态"指令,0xff是填充数
uint8_t rxBUF[2] = {0, 0}; // 用来存接收到的数据(芯片返回的状态)
// 用中断方式同时收发:发送指令的同时,接收芯片返回的状态
HAL_SPI_TransmitReceive_IT(&hspi1, txBUF, rxBUF, 2);
Wait_spi1_txrxcplt(); // 等待收发完成
return rxBUF[1]; // 返回状态寄存器的值(第二个字节才是有效状态)
}
(发 0x05 指令后,芯片返回状态值)
作用:芯片擦除或写入时会 "忙",状态寄存器的第 0 位是 "忙标志"(1 = 忙,0 = 空闲),读它能知道芯片是否准备好接受新指令。
5. 内部函数:等待芯片空闲
// 一直等,直到芯片不忙(状态寄存器的忙标志为0)
static int spiFlash_WriteRead(void)
{
while(spiFlash_readstatus() & 1 == 1); // 读状态,如果忙就一直等
}
用法:在擦除或写入操作后调用,等芯片完成当前工作,再进行下一步。
6. 宏定义与全局声明
// 定义SPI操作超时时间(单位:毫秒)
#define SPI_FLASH_TIMEOUT 1000
// 声明SPI等待函数(用于等待传输完成)
void Wait_spi1_txcplt(int timeout); // 等待发送完成
void Wait_spi1_txrxcplt(int timeout); // 等待收发完成
void Wait_spi1_rxcplt(int timeout); // 等待接收完成
// 声明SPI句柄(在其他文件中初始化,如main.c)
extern SPI_HandleTypeDef hspi1;
// 全局标志位(用于中断与主程序同步,需在.c文件中定义)
static volatile uint8_t spi1_tx_done = 0; // 发送完成标志
static volatile uint8_t spi1_rx_done = 0; // 接收完成标志
static volatile uint8_t spi1_txrx_done = 0; // 收发完成标志
作用:定义超时时间、声明等待函数和 SPI 句柄,以及用于中断同步的标志位。
7. 等待函数实现
cs
//SPI.C
static volatile int g_spil_tx_complete= 0;
static volatile int g_spil_txrx_complete= 0;
static volatile int g_spil_rx_complete= 0;
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
if(hspi == &hspi1)
{
g_spil_tx_complete =1;
}
}
void Wait_spi1_txcplt(int timeout)
{
while (g_spil_tx_complete == 0 && timeout--)
{
HAL_Delay(1);
}
g_spil_tx_complete = 0;
}
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
if(hspi == &hspi1)
{
g_spil_txrx_complete =1;
}
}
void Wait_spi1_txrxcplt(int timeout)
{
while (g_spil_txrx_complete == 0 && timeout--)
{
HAL_Delay(1);
}
g_spil_txrx_complete = 0;
}
void Wait_spi1_rxcplt(int timeout)
{
while (g_spil_rx_complete == 0 && timeout--)
{
HAL_Delay(1);
}
g_spil_rx_complete = 0;
}
8. 对外函数
这些函数是给用户直接调用的,比如读 ID、擦除扇区等,虽然现在是空的,但框架要清楚:
(1)读芯片 ID
cs
int spi_flash_readID(void)
{
uint8_t txBUF[2] = {0x9F, 0xff}; // 发送缓冲区:读ID命令+填充字节
uint8_t rxBUF[2] = {0, 0}; // 接收缓冲区
spiFlash_select(); // 选中SPI闪存(拉低CS信号)
// 以中断方式发送2字节数据,同时接收2字节数据
HAL_SPI_TransmitReceive_IT(&hspi1, txBUF, rxBUF, 2);
Wait_spi1_txrxcplt(SPI_FLASH_TIMEOUT); // 等待收发完成(带超时)
spiFlash_deselect(); // 取消选中(拉高CS信号)
return rxBUF[1]; // 返回接收的ID值
}
功能 :读取 SPI 闪存的芯片 ID,用于验证芯片是否正常连接。
关键细节:
- 命令
0x9F
是 SPI 闪存的 "读 ID 命令"(不同芯片可能有差异)。 - 发送缓冲区第二个字节
0xff
是占位符,SPI 通信为全双工,发送的同时会接收数据。 - 芯片 ID 通过
rxBUF[1]
返回(具体位置取决于芯片规格,部分芯片可能返回多字节 ID)
(2)擦除扇区
cs
int spi_flash_ErasesSector(uint32_t addr)
{
uint8_t txBUF[4] = {0x20}; // 发送缓冲区:扇区擦除命令
/* 写使能 */
spiFlash_writeEnable(); // 发送写使能命令,允许擦除操作
/* 填充地址 */
txBUF[1] = (addr >> 16) & 0xff; // 地址高8位
txBUF[2] = (addr >> 8) & 0xff; // 地址中8位
txBUF[3] = (addr >> 0) & 0xff; // 地址低8位
spiFlash_select(); // 选中芯片
// 以中断方式发送4字节命令(擦除命令+地址)
HAL_SPI_Transmit_IT(&hspi1, txBUF, 4);
Wait_spi1_txcplt(SPI_FLASH_TIMEOUT); // 等待发送完成
spiFlash_deselect(); // 取消选中
/* 等待擦除完成 */
spiFlash_WriteRead(); // 循环等待芯片空闲(状态寄存器忙标志位清零)
return 0; // 返回成功状态
}
功能 :擦除指定地址addr
所在的扇区(扇区大小由芯片决定,通常为 4KB/64KB)。
关键细节:
- 命令
0x20
是扇区擦除指令,必须配合地址使用。 - 擦除前必须调用
spiFlash_writeEnable()
,否则操作无效(硬件保护机制)。 - 擦除是耗时操作(毫秒级),
spiFlash_WriteRead()
会等待操作完成后再返回。
(3)写数据
cs
int spi_flash_writeData(uint32_t addr, uint8_t *data, uint32_t len)
{
uint8_t txBUF[4] = {0x02}; // 发送缓冲区:页写命令
/* 写使能 */
spiFlash_writeEnable(); // 允许写入操作
/* 填充地址 */
txBUF[1] = (addr >> 16) & 0xff; // 地址高8位
txBUF[2] = (addr >> 8) & 0xff; // 地址中8位
txBUF[3] = (addr >> 0) & 0xff; // 地址低8位
spiFlash_select(); // 选中芯片
/* 发送命令和地址 */
HAL_SPI_Transmit_IT(&hspi1, txBUF, 4);
Wait_spi1_txcplt(SPI_FLASH_TIMEOUT); // 等待命令发送完成
/* 发送数据 */
HAL_SPI_Transmit_IT(&hspi1, data, len); // 发送用户数据
Wait_spi1_txcplt(SPI_FLASH_TIMEOUT); // 等待数据发送完成
spiFlash_deselect(); // 取消选中
/* 等待写入完成 */
spiFlash_WriteRead(); // 等待芯片完成写入操作
return 0; // 返回成功状态
}
功能 :向指定地址addr
写入长度为len
的字节数据(*data
)。
关键细节:
- 命令
0x02
是 "页写命令",一次写入不能跨页(页大小通常为 256 字节)。 - 写入前必须确保目标扇区已擦除(擦除后数据为
0xff
,才能写入非0xff
值)。 - 分两步发送:先发送命令和地址,再发送实际数据,符合 SPI 闪存的写入时序要求。
(4)读数据
cs
int spi_flash_ReadData(uint32_t addr, uint8_t *datas, uint32_t len)
{
uint8_t txBUF[4] = {0x03}; // 发送缓冲区:读数据命令
/* 填充地址 */
txBUF[1] = (addr >> 16) & 0xff; // 地址高8位
txBUF[2] = (addr >> 8) & 0xff; // 地址中8位
txBUF[3] = (addr >> 0) & 0xff; // 地址低8位
spiFlash_select(); // 选中芯片
/* 发送命令和地址 */
HAL_SPI_Transmit_IT(&hspi1, txBUF, 4);
Wait_spi1_txcplt(SPI_FLASH_TIMEOUT); // 等待命令发送完成
/* 读取数据 */
HAL_SPI_Receive_IT(&hspi1, datas, len); // 接收数据到datas缓冲区
Wait_spi1_rxcplt(SPI_FLASH_TIMEOUT); // 等待接收完成
spiFlash_deselect(); // 取消选中
return 0; // 返回成功状态
}
功能 :从指定地址addr
读取长度为len
的字节数据,存储到*datas
缓冲区。
关键细节:
- 命令
0x03
是 "读数据命令",支持连续读取(不受页限制)。 - 读取操作无需写使能(只读操作无硬件保护)。
- 分两步执行:先发送命令和地址,再接收数据,符合 SPI 闪存的读取时序。
记得声明
9.主函数
cs
#include "main.h"
#include "dma.h"
#include "i2c.h"
#include "spi.h"
#include "usart.h"
#include "gpio.h"
#include "circle_buffer.h"
#include "driver_SPI_Flash.h"
void Wait_Tx_Complete(void); // 等待UART发送完成
void Wait_Rx_Complete(void); // 等待UART接收完成
void startuart1recv(void); // 启动UART1接收中断
int UART1getchar(uint8_t *pVal);// 从UART1获取一个字符
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c);//主设备发送完成回调函数
/* USER CODE END 1 */
void Wait_i2c1Tx_Complete(void);
void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c);//主设备接收完成回调函数
void Wait_i2c1Rx_Complete(void);
void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c);//主设备发送完成回调函数
void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c);//主设备发送完成回调函数
// 环形缓冲区相关变量:用于存储按键数据
static uint8_t g_data_buf[100]; // 缓冲区的实际存储空间(可以存100个字节)
static circle_buf g_key_bufs; // 环形缓冲区结构体(管理存储空间的读写)
int main(void)
{
int len; // 临时变量:存储字符串长度
// 定义要发送的字符串:\r\n是换行符(串口通信中常用)
char *str = "Please enter a char: \r\n";
char *str2 = "www.100ask.net";
char c; // 存储接收的字符
char flash_buf[20];
HAL_Init();
SystemClock_Config();
// 初始化环形缓冲区:circld_buf_init是拼写错误,正确应为circle_buf_init
// 参数:缓冲区结构体、大小100、存储空间g_data_buf
circld_buf_init(&g_key_bufs, 100, g_data_buf);
MX_GPIO_Init();
MX_DMA_Init();
MX_I2C1_Init();
MX_USART1_UART_Init();
MX_SPI1_Init();
OLED_Init(); // 初始化OLED屏幕
OLED_Clear(); // 清屏(清除OLED上的所有显示)
OLED_Printchinese(5,6);
int id =spi_flash_readID();
OLED_PrintHex(0,0,id,1);
startuart1recv(); // 启动UART1接收中断:让UART1准备好接收数据,收到数据后会触发中断
spi_flash_ErasesSector(0);//每个扇区大小是4096
spi_flash_writeData(0,str2,strlen(str2)+1);
spi_flash_ReadData(0,flash_buf,20);
OLED_PrintString(0,2,flash_buf);
while (1)
{
}
}
四、函数调用流程示例(以 "擦除扇区" 为例)
用一个实际操作演示这些内部函数怎么配合:
- 先调用
spiFlash_writeEnable()
发写使能指令(解锁); - 调用
spiFlash_select()
选中芯片; - 发送 "擦除扇区" 指令和地址;
- 调用
spiFlash_WriteRead()
等待擦除完成; - 调用
spiFlash_deselect()
释放芯片。
这样一步步看下来,你能从 "文件怎么建" 到 "每个函数干什么" 再到 "函数怎么配合工作",完全掌握这些内部函数的用法,后续完善对外函数时就会很轻松啦!
五、结果展示
