韦东山STM32_HAl库入门教程(SPI)学习笔记[09]内容

(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 驱动层";换主控芯片,可能改 "HAL 层")。
3. 「硬件框图 - 系统级连接逻辑」
  • 内容 :从 SoC(芯片系统) 角度,展示 CPU、内存管理单元、SPI 控制器、Flash 之间的连接关系:
    • CPU :发读写指令(比如 "读 Flash 地址 0x1000 的数据")。
    • 内存管理单元:处理地址映射、片选信号(决定操作 RAM 还是 Flash 还是其他外设)。
    • SPI 控制器:把 CPU 的指令转成 SPI 时序(时钟 SCK、数据 MOSI/MISO、片选 CS 等),发给外部 Flash。
    • Flash:接收 SPI 信号,执行读写擦除操作,返回数据。
  • 作用:理解 "硬件模块如何协同工作",比如为什么操作 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 时钟下一位位发)。
    • 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:发送 / 接收中断使能(数据发完 / 收到时触发中断,异步通信常用)。
  • 关联下一张图 :理解控制器内部后,得掌握 "SCK、MOSI、MISO 咋配合发数据" → 看时序图
3. 「SPI 时序图(CPOL=1 系列)」
  • 核心作用 :明确 "时钟极性(CPOL)、相位(CPHA)" 如何决定数据采样时机,是 SPI 通信的协议灵魂
  • 阴暗细节(逐周期解析!)
    • CPOL(时钟极性)
      • CPOL=1SCK 空闲时是高电平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、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=0SCK 空闲时是低电平 ,第一个时钟沿是上升沿(从低→高)。
    • CPOL=0, CPHA=0(Format A)
      • 第一个时钟沿(上升沿)采样数据,MOSI 先发 MSBMISO 同步返回。
    • 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 0110MSB 是第 7 位(0),LSB 是第 0 位(0)。
      • 每个 SCK 周期对应 1 个比特:
        • 第 1 个 SCK 下降沿(CPOL=1, CPHA=0 时):发 0(MSB),Flash 采样。
        • 第 2 个 SCK 下降沿:发 1,依此类推...
        • 第 8 个 SCK 下降沿:发 0(LSB),传输结束。
    • 采样时机 :Flash 在每个 SCK 的上升沿采样(因为示例中可能用了模式 3?需要结合前面的模式图验证)。
  • 关键验证
    • 数一下 SCK 周期和数据位是否对应(8 个周期传 8 位),理解 MSB 先传 的规则。
    • 对比前面的时序模式图,看这个示例属于哪种 CPOL、CPHA 组合(比如这里 SCK 空闲高电平 → CPOL=1;下降沿发数据,上升沿采样 → 可能是模式 2 或 3?需要细扣)。
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 和 3",它们都在上升沿采样(不管空闲电平),配置时先试这两个模式,不行再查外设手册。
7. 「SPI 传输示例(0x56 时序图)」
  • 核心作用 :和图 5 呼应,用更规范的时序图展示 0x56 传输,验证 "数据位与时钟沿的对应关系"
  • 阴暗细节(与图 5 对比)
    • 图 5 是手绘版,图 7 是规范版,都展示 0x56 = 0b0101 0110 的传输。
    • 注意 CS0 拉低的时机(传输前拉低,传输后拉高),以及 SCK 周期数(8 个周期传 1 字节)。
  • 关键验证
    • 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 控制器。
  • 关键坑点
    • 波特率不能乱设:太高会导致从机收不到数据(比如 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 时序,步骤分解:

    1. 拉低 /CS:选中 Flash 芯片(相当于敲门说 "我要操作你啦" )。
    2. 发读指令(0x03) :通过 MOSI 线发指令 0x03,告诉 Flash "我要读数据" 。
    3. 发 24 位地址 :接着发要读取的地址(比如 0x000000 ),告诉 Flash "从这个位置开始读" 。
    4. 读数据:地址发完后,Flash 会从 MISO 线把数据传回来,读一个字节后,内部地址自动 +1,可连续读很多数据,直到 /CS 拉高。
  • 补充解释了 "发完地址后,Flash 持续输出数据,直到操作结束",帮你理解 "连续读" 逻辑 ------ 读一个字节后,地址自动递增,能一直读到芯片末尾,不用每次重新发地址。

2. 写数据流程(要擦除后才能写,稍复杂 )

注意 :Flash 特性是 "写之前必须擦除"(因为只能从 1 改 0,擦除是把 0 改回 1 ),所以写操作分 "擦除扇区 → 写使能 → 烧写页" 三步。

(1)写使能

作用 :告诉 Flash "我要准备写数据 / 擦除了,打开写权限",是写、擦除操作的 必要前提

  • (写使能时序)
    步骤:
    1. 拉低 /CS → 选中芯片。
    2. 发写使能指令 0x06 → 告诉 Flash "允许我写数据啦" 。
    3. 拉高 /CS → 结束操作。

为什么必须? :Flash 有 "写保护",发 0x06 是解除保护的钥匙,否则写、擦除会失败!

(2)擦除扇区

作用 :写数据前,必须把要写的区域擦成 "全 1",W25Q64 最小擦除单位是 扇区(4KB = 16 页 )

  • (擦除扇区时序)
    步骤:
    1. 拉低 /CS → 选中芯片。
    2. 发擦除指令 0x20 → 告诉 Flash "我要擦除扇区" 。
    3. 发 24 位地址 → 指定要擦除的扇区(比如地址 0x000000 对应第 0 扇区 )。
    4. 拉高 /CS → 启动擦除(擦除需要时间,不是立刻完成 )。

怎么判断擦除完成? :擦除时 Flash 内部忙,要读 状态寄存器(后面讲)BUSY 位,BUSY=0 才代表擦除完!

(3)烧写页

作用 :把数据写入 Flash,最小写入单位是 页(256 字节 ) ,可从页内任意位置开始写,写超页末尾会 "绕回页开头" 。

  • (烧写页时序)
    步骤:
    1. 拉低 /CS → 选中芯片。
    2. 发页编程指令 0x02 → 告诉 Flash "我要写数据到页里" 。
    3. 发 24 位地址 → 指定要写入的页起始地址(比如 0x000000 对应第 0 页 )。
    4. 发数据(最多 256 字节 )→ 把要存的数据通过 MOSI 发过去,写超 256 字节会覆盖页开头数据。
    5. 拉高 /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 )。

实际用:小数据写入用 "页编程",大范围擦除用 "扇区 / 块擦除",根据需求选。

三、完整操作流程总结(从读 → 擦除 → 写 )

把上面的步骤串起来,比如 "要写数据到 Flash 某地址",完整流程是:

  1. 擦除对应扇区

    • 发写使能→ 发擦除扇区指令 + 地址→ 循环读状态寄存器,直到 BUSY=0
  2. 烧写页数据

    • 发写使能→ 发页编程指令 + 地址 + 数据→ 循环读状态寄存器,直到 BUSY=0
  3. 验证数据(读操作)

    • 用读指令读刚才写的地址,对比数据是否正确。

(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)
  {
  }
}

四、函数调用流程示例(以 "擦除扇区" 为例)

用一个实际操作演示这些内部函数怎么配合:

  1. 先调用 spiFlash_writeEnable() 发写使能指令(解锁);
  2. 调用 spiFlash_select() 选中芯片;
  3. 发送 "擦除扇区" 指令和地址;
  4. 调用 spiFlash_WriteRead() 等待擦除完成;
  5. 调用 spiFlash_deselect() 释放芯片。

这样一步步看下来,你能从 "文件怎么建" 到 "每个函数干什么" 再到 "函数怎么配合工作",完全掌握这些内部函数的用法,后续完善对外函数时就会很轻松啦!

五、结果展示

相关推荐
源远流长jerry5 分钟前
电路基础相关知识
stm32·单片机·嵌入式硬件
1+2单片机电子设计1 小时前
基于STM32的数控机床物联网改造研究
stm32·单片机·嵌入式硬件·51单片机
猫猫的小茶馆1 小时前
【STM32】HAL库中的实现(三):PWM(脉冲宽度调制)
stm32·单片机·嵌入式硬件·mcu·51单片机·智能硬件
国科安芯1 小时前
ASP3605I同步降压调节器的高频化设计与多相扩展技术优化方案
网络·单片机·嵌入式硬件·硬件架构
CodeCraft Studio4 小时前
图像处理控件Aspose.Imaging教程:使用 C# 将 SVG 转换为 EMF
图像处理·microsoft·c#·svg·aspose·图片格式转换·emf
yiqiqukanhaiba5 小时前
江协科技STM32学习笔记1
科技·stm32·学习
码小文7 小时前
Altium Designer 22使用笔记(4)---添加封装、ERC检查、PDF文档与BOM生成
笔记·嵌入式硬件·硬件工程·学习方法·硬件经验
猫猫的小茶馆8 小时前
【STM32】HAL库中的实现(四):RTC (实时时钟)
stm32·单片机·嵌入式硬件·mcu·51单片机·实时音视频·pcb工艺
初九之潜龙勿用8 小时前
技术与情感交织的一生 (十一)
服务器·笔记·microsoft·印象笔记