STM32——SPI通信+W25Q64

目录

一、SPI通信

1.1SPI通信简介

1.2SPI与I2C通信对比

1.2.1核心特性对比表

1.2.2如何选择?

1.2.3总结

1.3SPI硬件电路

1.4SPI移位示意图

1.4.1移位原理示意图

1.4.2详细步骤解析

1.4.2.1初始状态(CS拉低后)

1.4.2.2时钟周期0(第一个上升沿)

1.4.2.3时钟周期1(第二个上升沿)

1.4.2.4后续周期(重复直到8位完成)

1.4.2.5传输完成后的结果

1.4.2.6移位寄存器内部工作过程

1.4.3总结

1.4.4实际工程中的关键点

1.5SPI时序基本单元

1.5.1四种SPI模式的时序对比

1.6SPI时序(指令码+读写数据模型)

1.6.1发送指令

1.6.2指定地址写

1.6.3指定地址读

1.7看时序图时的疑惑:

1.7.1总结:

二、W25Q64

2.1W25Q64简介

2.2硬件电路

2.3W25Q64框图

2.3.1框图重点

2.3.2数据写入执行流程和指令与擦除指令

2.3.3读取数据流程和相关指令

⭐2.4Flash注意事项

2.4.1写入操作时:

2.4.2读取操作时:

⭐2.4.3写入一页数据(256字节)的典型流程:

⭐2.4.4擦除一个扇区(4KB)的典型流程:

⭐2.4.5总结

三、软件模拟SPI读写W25Q64

3.1My_SPI.c代码

3.2W25Q64.c代码

3.3获取ID号相关知识点

3.3.1读取流程 (软件模拟SPI)

3.3.2为什么发送0xFF就可以得到ID?

3.3.3ID是原先存储在从机里的吗?

3.4读取ID号main.c代码

3.5其他指令的代码

3.5.1手册中各种指令代码的思路展现

3.5.2手册中的注意点:

3.5.3其他指令的代码书写思路

3.5.4其他指令代码展示

3.6W25Q64指令地址

3.7其他指令main.c代码

四、硬件SPI

4.1SPI外设简介

4.2SPI框图

4.3SPI基本结构

4.4主模式全双工连续传输

4.4.1工作流程:

4.4.2流程详解(结合图表):

4.4.3关键特点:

4.4.4为什么要用这种模式?

4.5非连续传输(默认/常见模式)

4.5.1工作流程--老板主机,秘书从机:

4.5.2流程详解(结合图表):

4.6总结与对比:非连续 vs 连续

五、软硬件SPI对比

5.1对比表格:硬件SPI vs 软件模拟SPI

5.2如何选择?

六、硬件SPI读写W25Q64

6.1外设引脚连接

6.2硬件SPI初始化

6.3硬件SPI外设相关库函数

6.4硬件SPI代码


一、SPI通信

1.1SPI通信简介

SPISerial Peripheral Interface串行设备接口)是由Motorola公司开发的一种通用数据总线

四根通信线:SCKSerial Clock)、MOSIMaster Output Slave Input)、MISOMaster Input Slave Output)、SSSlave Select

  1. SCLK (Serial Clock):

    • 主设备(Master) 产生并输出给所有从设备(Slave)。

    • 它是数据传输的节拍器,决定了数据传输的速度。

    • 所有数据位的采样和输出都发生在SCLK的边沿(具体是上升沿还是下降沿由配置决定)。

    • SCLK: 时钟线 - 老板(主设备)手里的节拍器。老板控制敲击的节奏(频率)。这根线决定了沟通的速度有多快。老板敲一下,数据就传输一位。

  2. MOSI (Master Out Slave In) / SDO (Serial Data Out - Master):

    • 主设备输出数据,从设备接收数据的线路。

    • MOSI: 老板(主设备)说,员工(从设备)听 - 老板通过这根线把数据(指令)一个字一个字地告诉员工。

  3. MISO (Master In Slave Out) / SDI (Serial Data In - Master):

    • 从设备输出数据,主设备接收数据的线路。

    • MISO: 员工说,老板听 - 员工通过这根线把数据(工作汇报)一个字一个字地告诉老板。

  4. NSS (Slave Select) / CS (Chip Select)/SS:

    • 主设备控制,用于选择要通信的特定从设备

    • 通常是低电平有效(当为低电平时,对应的从设备被选中并响应通信)。

    • 关键点: 主设备在开始与某个从设备通信前,必须拉低该从设备的NSS引脚;通信结束后,拉高该NSS引脚。同一时间只能有一个从设备的NSS被拉低。

    • CS/SS: 选人线 - 老板(主设备)用来决定现在跟哪个员工(从设备)通话。这根线平常是“高电平”(想象成挂断电话)。当老板想找某个特定员工时,就把对应这根线拉成“低电平”(想象成拿起电话拨通),相当于喊:“喂,张三!现在听我说!” 沟通结束后,老板再把这条线拉高(挂断电话)。如果只有一个员工,这根线有时可以简化处理(比如直接接地,表示老板随时能喊他),但最好还是用。

同步,全双工

  1. 同步: 老板(主设备STM32)和员工(从设备)说话必须步调一致。老板手里有个节拍器(时钟信号,SCLK),他说一个字就敲一下节拍器。员工必须在老板敲节拍器的那个瞬间,听清老板说的字(接收数据),或者说出自己要说的字(发送数据)。这样就不会乱套。

  2. 全双工: 他们可以同时说和听(两条线:一根发送一根接收)!老板(主设备)一边给员工(从设备)下指令(发送数据),员工可以一边给老板汇报工作(发送数据)。这比一次只能一个人说话(半双工)快多了。

支持总线挂载多设备(一主多从)

点对点: 通常是一个老板(STM32)直接指挥一个员工(外设)。如果老板要指挥多个员工,他需要给每个员工单独拉一条“电话线”(片选线),每次只跟一个员工通话。

1.2SPI与I2C通信对比

1.2.1核心特性对比表

特性

SPI (Serial Peripheral Interface)

I2C (Inter-Integrated Circuit)

全称

Serial Peripheral Interface

Inter-Integrated Circuit

通信类型

全双工 (同时收发)

半双工 (同一时刻只能收或发)

同步方式

同步 (主设备提供时钟 SCLK)

同步 (主设备提供时钟 SCL)

信号线数量

4 条基本线
SCLK, MOSI, MISO, CS

2 条线
SCL (时钟), SDA (数据)

拓扑结构

主从结构,一主多从

主从结构,多主多从

设备寻址方式

硬件片选 (CS 引脚)
每个从机需独立CS引脚

软件地址
7位/10位地址广播寻址

数据速率

高速 (通常 10-100+ Mbps)

中低速 (标准模式 100 Kbps, 高速模式 400 Kbps/3.4 Mbps)

协议复杂度

简单 (无固定数据帧格式)

复杂 (需地址帧、ACK/NACK、起始/停止位)

功耗

较高 (需持续时钟驱动)

较低 (时钟可停止,支持睡眠模式)

抗干扰能力

较强 (独立时钟同步)

较弱 (依赖开漏输出,易受干扰)

典型应用场景

高速设备:Flash、屏幕、ADC/DAC、传感器

中低速设备:EEPROM、温度传感器、RTC

1.2.2如何选择?

场景

推荐协议

原因

高速数据传输(>1 Mbps)

SPI

全双工+无协议开销,速度优势明显

多设备连接(>3个)

I2C

节省GPIO引脚,布线简单

低功耗设备(传感器、RTC)

I2C

支持睡眠模式,静态电流更低

长距离通信(>30 cm)

慎用

两者均易受干扰,建议改用UART+RS485

实时性要求高(如ADC连续采样)

SPI

无总线仲裁,延迟确定

1.2.3总结

  • 选 SPI 当:需要高速、实时、点对点或少量设备通信(如Flash存储、显示屏)。

  • 选 I2C 当:设备较多、引脚紧张、速度要求不高(如传感器网络、配置芯片)。

1.3SPI硬件电路

所有SPI设备的SCKMOSIMISO分别连在一起

主机另外引出多条SS控制线,分别接到各从机的SS引脚

输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入

信号线

连接方式

说明

SCLK

主机SCLK → 所有从机SCLK

时钟线共享

MOSI

主机MOSI → 所有从机MOSI

主机输出数据线共享

MISO

主机MISO ← 所有从机MISO

主机输入数据线共享

CS

主机GPIO1 → 从机1的CS

独立片选(低电平有效)

主机GPIO2 → 从机2的CS

主机GPIO3 → 从机3的CS

关键特点:

  1. 片选隔离:每个从机使用独立的GPIO作为片选(CS),同一时间只有一个CS为低电平

  2. 数据线复用:MOSI/MISO/SCLK三根线并联到所有从机。

  3. 从机MISO处理:未被选中的从机必须将其MISO引脚置高阻态(Hi-Z),避免总线冲突(通常由从机内部自动处理)。

1.4SPI移位示意图

SPI的数据收发基于字节交换进行。(模式1)数据在时钟上升沿采样,下降沿改变。高位先行。在只接收或者只发送存在资源浪费。

1.4.1移位原理示意图

时钟周期: 0     1     2     3     4     5     6     7
SCLK   : _/‾‾‾\_/‾‾‾\_/‾‾‾\_/‾‾‾\_/‾‾‾\_/‾‾‾\_/‾‾‾\_/‾‾‾\_
         ↑     ↑     ↑     ↑     ↑     ↑     ↑     ↑   (上升沿采样)
MOSI   : D7---D6---D5---D4---D3---D2---D1---D0--      (主机输出)
MISO   : S7---S6---S5---S4---S3---S2---S1---S0--      (从机输出)

1.4.2详细步骤解析

——假设主机发送0xAA = 10101010,从机返回0x55 = 01010101)

1.4.2.1初始状态(CS拉低后)
  • SCLK处于低电平(空闲)。

  • 主机将第一个数据位(MSB)放在MOSI线上(0xAA的MSB是1,所以MOSI=1)。

  • 从机将第一个数据位(MSB)放在MISO线上(0x55的MSB是0,所以MISO=0)。

  • CS片选信号为低(选中从机)。


主机移位寄存器: [1][0][1][0][1][0][1][0]  <-- 0xAA
从机移位寄存器: [0][1][0][1][0][1][0][1]  <-- 0x55
MOSI线: 1 (主机输出MSB)
MISO线: 0 (从机输出MSB)
SCLK: 低电平

1.4.2.2时钟周期0(第一个上升沿)

下降沿(数据变化):

  • 主机在SCLK下降沿将MOSI切换为下一个数据位(第2位,0xAA的第2位是0,所以MOSI=0)。

  • 从机在SCLK下降沿将MISO切换为下一个数据位(第2位,0x55的第2位是1,所以MISO=1)。

上升沿(数据采样):

  • 主机在SCLK上升沿采样MISO线,读取到初始状态时采样的数据0(从机发送的MSB)。

  • 从机在SCLK上升沿采样MOSI线,读取到初始状态时采样的数据1(主机发送的MSB)。


动作:

  • 主机在上升沿采样MISO线:读取到0 → 移入接收寄存器LSB位置

  • 从机在上升沿采样MOSI线:读取到1 → 移入接收寄存器LSB位置

移位后:

  • 主机寄存器: [0][1][0][1][0][1][0][?]  ?=新移入的0

  • 从机寄存器: [1][0][1][0][1][0][1][?]  ?=新移入的1

MOSI: 0 (主机输出下一位)
MISO: 1 (从机输出下一位)

1.4.2.3时钟周期1(第二个上升沿)

下降沿(数据变化):

  • 主机将MOSI切换为第3位(0xAA的第3位是1,MOSI=1)。

  • 从机将MISO切换为第3位(0x55的第3位是0,MISO=0)。

上升沿(数据采样):

  • 主机采样MISO线,读取到时钟周期0时采样的数据1(从机发送的第2位)。

  • 从机采样MOSI线,读取到时钟周期0时采样的数据0(主机发送的第2位)。


动作:

  • 主机采样MISO:读取到1 → 移入寄存器

  • 从机采样MOSI:读取到0 → 移入寄存器

移位后:

  • 主机寄存器: [1][0][1][0][1][0][?][0] 

  • 从机寄存器: [0][1][0][1][0][1][?][1]  

MOSI: 1 (主机输出下一位)
MISO: 0 (从机输出下一位)

1.4.2.4后续周期(重复直到8位完成)
  • 每个时钟周期,主机和从机在下降沿改变数据(输出下一位),在上升沿采样数据(读取当前位)。

  • 8个时钟周期后,主机和从机都完成了一个字节的发送和接收。


时钟周期 | 主机动作                | 从机动作                | MOSI | MISO
-----------------------------------------------------------------------
   2    | 采样MISO=0 → 移入0      | 采样MOSI=1 → 移入1      |  0   |  1
   3    | 采样MISO=1 → 移入1      | 采样MOSI=0 → 移入0      |  1   |  0
   4    | 采样MISO=0 → 移入0      | 采样MOSI=1 → 移入1      |  0   |  1
   5    | 采样MISO=1 → 移入1      | 采样MOSI=0 → 移入0      |  1   |  0
   6    | 采样MISO=0 → 移入0      | 采样MOSI=1 → 移入1      |  0   |  1
   7    | 采样MISO=1 → 移入1      | 采样MOSI=0 → 移入0      |  -   |  -

1.4.2.5传输完成后的结果

主机移位寄存器: [0][1][0][1][0][1][0][1] = 0x55 (收到从机数据)
从机移位寄存器: [1][0][1][0][1][0][1][0] = 0xAA (收到主机数据)

MOSI线: 高阻态
MISO线: 高阻态
SCLK: 保持低电平(CPOL=0)

1.4.2.6移位寄存器内部工作过程

在主机和从机内部都有一个8位移位寄存器。传输开始时:

  • 主机将要发送的数据(0xAA)加载到其移位寄存器。

  • 从机将要发送的数据(0x55)加载到其移位寄存器。

每个时钟周期(下降沿时),移位寄存器移动一位:

  • 主机:移位寄存器最高位(MSB)从MOSI输出,同时MISO输入的数据移入最低位(LSB)。

  • 从机:移位寄存器最高位(MSB)从MISO输出,同时MOSI输入的数据移入最低位(LSB)。

8个时钟周期后:

  • 主机的移位寄存器中原本的0xAA被移出,同时移入了0x55(从机发送的数据)。

  • 从机的移位寄存器中原本的0x55被移出,同时移入了0xAA(主机发送的数据)。

1.4.3总结

SPI的移位过程是主机和从机同时通过移位寄存器进行数据交换的过程。每个时钟周期交换一位,8个时钟周期完成一个字节的交换。主机的移位寄存器和从机的移位寄存器通过MOSI和MISO线连接,形成一个循环移位路径。时钟信号(SCLK)严格同步了这个移位过程。

①若只想发送,不想接收:调用交换字节的时序,同时发送同时接收,不看接收到的数据

②若只想接收,不想发送:调用交换字节的时序,同时发送同时接收,在主机随便发送一个数据,一般0x00或0xFF。

1.4.4实际工程中的关键点

  1. 时钟同步性:所有设备必须使用同一个SCLK边沿采样

  2. 位序对齐:MSB/LSB先传输需主从设备一致

  3. 建立保持时间:数据需在采样边沿前稳定(通常>10ns)

  4. 移位完成中断:当8/16位移位完成时触发中断读取数据

  5. 缓冲区机制:现代SPI控制器有双缓冲区,允许连续传输

1.5SPI时序基本单元

起始条件:SS从高电平切换到低电平(选定从机状态)

终止条件:SS从低电平切换到高电平(结束选中从机状态)

CPHA决定第几个边沿数据采样(读取数据),并不能单独决定是上升沿还是下降沿

交换一个字节(模式0*应用多

CPOL(时钟极性)=0:空闲状态时,SCK为低电平

CPHA(时钟相位)=0SCK第一个边沿移入数据,第二个边沿移出数据(数据的移位移出提前半个时钟相对模式1)

1. 时钟起始状态

  • SCLK:保持 低电平(CPOL=0定义)

  • 数据线:MOSI/MISO在第一个跳变沿(SCLK上升沿)前,SS下降沿,输出一个数据位(MSB)

2. 第一个跳变沿(上升沿)→ 采样时刻

  • 主机动作:在上升沿 采样MISO线,读取从机发送的位

  • 从机动作:在上升沿 采样MOSI线,读取主机发送的位

3. 时钟高电平期间 → 数据保持

  • 采样完成后,MOSI/MISO数据必须保持稳定

  • 下一个下降沿的数据输出做准备

4. 第二个跳变沿(下降沿)→ 输出时刻

  • 主机动作:在下降沿将下一个数据位输出到MOSI线

  • 从机动作:在下降沿将下一个数据位输出到MISO线

完整周期总结
1个时钟周期 = 上升沿(采样) + 下降沿(输出)

交换一个字节(模式1

CPOL=0:空闲状态时,SCK为低电平

CPHA=1SCK第一个边沿移出数据,第二个边沿移入数据

①SS从机选择:通信开始前SS高电平,通信过程中SS保持低电平,通信结束SS恢复高电平


②MISO主机输入从机输出:SS高电平,MISO高阻状态,SS下降沿之后,从机MOSI被允许开启输出,SS上升沿之后,从机MISO必须置回高阻态。


③SCK:上升沿,主机从机同时移出数据,主机通过MOSI移出最高位,此时MOSI电平表示主机要发送数据的B7;从机通过MISO移出最高位,此时MISO表示从机要发送数据的B7;下降沿,主机从机同时移入数据,进行数据采样:主机移出B7,进入从机移位寄存器最低位;从机移出B7,进入主机移位寄存器的最低位。一个时钟脉冲产生完毕,一个数据位传输完毕。

交换一个字节(模式2)-模式0SCLK极性取反

CPOL=1:空闲状态时,SCK为高电平

CPHA=0SCK第一个边沿移入数据,第二个边沿移出数据

交换一个字节(模式3)-模式1SCLK极性取反

CPOL=1:空闲状态时,SCK为高电平

CPHA=1SCK第一个边沿移出数据,第二个边沿移入数据

1.5.1四种SPI模式的时序对比

以下为不同CPOL/CPHA组合下的行为差异(箭头表示采样时刻):

模式

CPOL

CPHA

SCLK空闲电平

采样边沿

输出边沿

时序图示例

Mode 0

0

0

低电平

↑ 上升沿

↓ 下降沿

_/‾‾‾\_ 采样在第一个跳变沿

Mode 1

0

1

低电平

↓ 下降沿

↑ 上升沿

_/‾‾‾\_ 采样在第二个跳变沿

Mode 2

1

0

高电平

↓ 下降沿

↑ 上升沿

‾\____/‾ 采样在第一个跳变沿

Mode 3

1

1

高电平

↑ 上升沿

↓ 下降沿

‾\____/‾ 采样在第二个跳变沿

1.6SPI时序(指令码+读写数据模型)

1.6.1发送指令

向SS指定的设备,发送指令(0x06

为了实现指定地址的读写,SPI通信通常包含一个指令阶段。这个阶段主机通过MOSI线发送一个或多个字节的指令码给从机,告诉它接下来要做什么操作。

  • 常见指令码

指令名称指令码 (Hex)功能描述重要说明
Write Enable0x06写使能在执行任何修改操作前必须发送
Write Disable0x04写失能保护性操作,防止误写
Read Data0x03读取数据最常用的读取指令,需24位地址
Fast Read0x0B快速读取比标准读更快,需24位地址+1个哑元字节
Page Program0x02页编程写入数据,需24位地址,最大256字节
Sector Erase0x20扇区擦除 (4KB)最常用的擦除指令,需24位地址
Block Erase (32KB)0x52块擦除 (32KB)擦除较大区块
Block Erase (64KB)0xD8块擦除 (64KB)擦除更大区块
Chip Erase0xC7 / 0x60整片擦除擦除整个芯片,谨慎使用
Read Status Register-10x05读状态寄存器1必用:检查BUSYWEL
Read Status Register-20x35读状态寄存器2检查其他状态位(如QE位)

通俗解释:

发送指令 (Send Command) - 老板让员工干活!

  • 场景: 老板想让员工做一件简单的事情,不需要指定具体位置(地址),也不需要立刻传递大量数据。

  • 例子: “启动温度转换!”、“复位!”、“查询设备ID”。

  • 通俗流程:

    1. 老板拨通电话: STM32拉低对应员工的CS线。

    2. 老板发指令: STM32通过MOSI线发送一个或多个字节。这些字节就是指令码(Command Code)。员工会仔细听。

      • 比如:发送 0x06 代表 “允许写入(Write Enable)” 指令。

    3. 员工执行: 员工收到完整的指令码后,立刻开始执行这个命令(比如打开写保护开关)。

    4. 老板挂电话: STM32拉高CS线。

1.6.2指定地址写

向SS指定的设备,发送写指令(0x02),随后在指定地址(Address[23:0])下,写入指定数据(Data

  1. 拉低片选(CS↓): 主机拉低目标从机的片选线,选中该外设,通信开始。

  2. 发送指令码(Command):

    • 主机通过MOSI线发送写操作指令码(例如 0x02)。

    • 每个SCLK周期发送一位(通常MSB first)。

    • 指令码长度通常是1字节(8位),但有些设备可能不同。

    • 此时MISO线通常无效(高阻态)或可能包含状态信息(取决于设备),主机可以忽略或读取。

  3. 发送目标地址(Address):

    • 主机通过MOSI线发送要写入的内存地址

    • 地址长度非常重要! 常见的有1字节(256字节设备)、2字节(64KB设备)、3字节(16MB设备,如SPI Flash)、甚至4字节。查阅外设手册确定STM32所支持的数据长度! (地址 0x001234 是24位/3字节)。

    • 地址字节也是MSB first传输。

    • MISO线状态同上。

  4. 发送数据(Data):

    • 主机通过MOSI线连续发送要写入该地址(及后续地址)的一个或多个字节的数据。

    • 每个字节MSB first。

    • 很多设备支持连续写入(Page Program),在发送起始地址后,后续写入的数据会自动递增地址(在同一个“页”内)。写入范围受设备页大小限制(无应答机制)。

    • 此时MISO线可能仍然无效,或者如果设备在忙,可能输出状态寄存器的忙位(需要查询)。

  5. 拉高片选(CS↑): 主机拉高片选线,结束本次通信。片选信号的拉高通常标志着写操作的真正开始执行(设备内部开始编程)。主机需要等待一段时间(查询状态寄存器或延时)直到设备完成内部写操作才能进行下一次操作(尤其是写操作),否则可能失败。写使能(Write Enable - 0x06)命令通常在写指令之前发送一次,使能设备的写操作。在一次写使能后,可以执行一次写操作(发送地址和数据),之后写使能状态会自动清除。下次写之前需要再次发送写使能。


通俗解释:

 指定地址写 (Write Data to Specific Address) - 老板把东西存到员工仓库的指定位置!

  • 场景: 老板要把一份文件(数据)存到员工仓库的某个特定货架(地址)上。

  • 例子: 把数据 0xA5 写入Flash芯片的 0x000100 地址。

  • 通俗流程:

    1. 老板拨通电话: STM32拉低CS

    2. 老板发指令: “我要存东西了!” (发送写指令码)。比如Flash芯片的页写指令可能是 0x02

    3. 老板发地址: “存到这个位置!” (发送地址字节,通常是3字节:0x000x010x00 代表地址 0x000100)。

    4. 老板发数据: “这是要存的东西!” (发送一个或多个字节的实际数据 0xA5)。

    5. 员工存数据: 员工收到地址和数据后,会把这些数据存到它内部地址对应的位置

    6. 老板挂电话: STM32拉高CS

    7. (可选:员工存数据可能需要时间,老板可能需要稍等一下(查询状态或延时)才能进行下次操作)

    8. 指令→地址→数据

1.6.3指定地址读

向SS指定的设备,发送读指令(0x03),随后在指定地址(Address[23:0])下,读取从机数据(Data

  1. 拉低片选(CS↓): 主机拉低目标从机的片选线,选中该外设。

  2. 发送指令码(Command):

    • 主机通过MOSI线发送读操作指令码(例如 0x03)。

    • 每个SCLK周期发送一位(MSB first)。

    • MISO线通常无效(高阻态),主机忽略。

  3. 发送目标地址(Address):

    • 主机通过MOSI线发送要读取的起始内存地址(例如 0x12(高位)+0x34+0x56=0x123456,发送三个字节=24位地址)。

    • 地址长度同样必须查阅手册确定! (示例中为3字节——24位地址)。

    • 地址字节MSB first传输。

    • MISO线通常仍然无效,主机忽略。

  4. 发送虚拟字节 / 等待周期(Dummy Bytes / Cycles):

    • 关键点! 在发送完地址后,主机需要继续产生一定数量的SCLK时钟周期,但这些周期上主机发送的数据(MOSI)是无效的(Dummy Bytes,通常发送 0x00 或 0xFF

    • 为什么需要Dummy Cycles? 外设收到读指令和地址后,需要时间从内部存储器中取出对应地址的数据并准备好输出到MISO线上。Dummy Cycles给了设备这个准备时间。

    • Dummy Cycles的数量是设备特定的! 常见的有0个、1个字节(8个时钟)、2个字节甚至更多。必须查阅外设手册! (示例假设需要1个Dummy Byte)。

    • 在这个阶段,MISO线开始变得有效! 设备会在这些Dummy Cycles期间(通常在某个特定的SCLK边沿)将第一个请求的数据字节的最高位(MSB)放到MISO线上

  5. 接收数据(Data):

    • 在Dummy Cycles之后(或者如果不需要Dummy Cycles,则在地址发送完后立即开始),主机继续产生SCLK时钟

    • 主机此时将MOSI线设置为发送无效数据(如 0x00)或保持某个状态(不重要),但更重要的是主机在每个SCLK的采样边沿(由CPHA决定)读取MISO线上的数据位

    • 设备会在每个SCLK周期将下一位数据(从MSB开始)放到MISO线上(无应答机制)。

    • 主机连续读取所需数量的字节。设备通常支持连续读取(Burst Read),读取的数据来自连续的递增地址

    • 主机需要知道要读取多少字节。

  6. 拉高片选(CS↑): 主机在接收到最后一个字节的最后一位后(或之后某个时间点),拉高片选线,结束本次通信。读操作通常没有像写操作那样的长等待时间。


通俗解释:

指定地址读 (Read Data from Specific Address) - 老板让员工从仓库的指定位置拿东西!

  • 场景: 老板想让员工从它仓库的某个特定货架(地址)上拿一份文件(数据)出来给自己看。

  • 例子: 从Flash芯片的 0x000200 地址读取1字节数据。

  • 通俗流程:

    1. 老板拨通电话: STM32拉低CS

    2. 老板发指令: “我要读东西了!” (发送读指令码)。比如Flash芯片的读指令可能是 0x03

    3. 老板发地址: “从这个位置读!” (发送地址字节,3字节:0x000x020x00 代表地址 0x000200)。

    4. 老板发“假字节”(Dummy Byte): 员工需要一点时间找到货架上的东西。老板这时随便说点什么(比如 0xFF 或 0x00)占个时间,目的是让员工准备好数据。员工在老板说这个“废话”的时候,其实是在找数据。

    5. 员工发数据: 员工找到数据后,通过MISO线把数据(一个或多个字节)发送给老板。老板认真听(MISO)。

    6. 老板挂电话: STM32拉高CS

    7. 指令→地址→假字节→数据

1.7看时序图时的疑惑:

模式0为例:

  • “MOSI先是传输主机最高位,最后展示的时序图就是主机的要发送的指令是嘛?”

    • 是的,完全正确。 如果配置为MSB first,那么MOSI时序图上第一个跳变/稳定的电平就代表主机要发送的那个字节的最高位(MSB),最后一个时钟周期代表的则是最低位(LSB)。整个MOSI波形连起来,就是主机发送的完整指令、地址或数据字节的每一位。

  • “MISO是从机发送最高位,所以MISO的时序图所展示的也是从机原先数据位的数据是吗?”

    • 基本正确。 如果配置为MSB first,那么MISO时序图上第一个跳变/稳定的电平就代表从机要发送给主机的那个字节的最高位(MSB),最后一个时钟周期代表的则是最低位(LSB)。整个MISO波形连起来,就是从机发送的完整数据或响应的每一位。

    • “原先数据位”这个说法可以理解为“从机准备好要发送给主机的数据位”。这些数据位可能是从机内部寄存器读出的值、传感器采集到的值,或者是根据主机命令计算出的结果。

  • “在SS下降沿时发送数据”: 严格来说,是在SS下降沿之后第一个SCLK下降沿(或稍前)主机和从机开始输出第一个数据位(MSB)到MOSI和MISO线上。SS下降沿是传输开始的触发信号。

  • “SCLK上升沿是接收数据”: 完全正确! SCLK的上升沿是主机和从机 采样(读取/接收)对方发送过来的数据位的时刻。 这是数据被“真正”读取并锁存的时刻。

  • “我所看的数据的上升沿下降沿怎么看,只看上升沿还是俩个都要看,哪一个才是他的真正数据”:

    • 两个边沿都要看,但它们作用不同。

    • SCLK下降沿: 看的是数据线的变化/更新。这是主机和从机输出下一个要发送的数据位的时刻。你可以在这个边沿观察数据位是如何被设置的(主机设置MOSI,从机设置MISO)。

    • SCLK上升沿: 看的是数据的有效性/锁存。这是主机和从机读取对方数据位的时刻。这个时刻数据线上的电平就是被传输的“真正数据”位!

  • 判断“真正数据”: 要确定一个数据位(比如第N位)的值是多少,你必须去看 采样该位 的那个SCLK上升沿时刻,数据线(MOSI或MISO)上的电平。

    • 对于MOSI线,在上升沿的电平,就是从机在那个时钟周期读到的、主机发送的位

    • 对于MISO线,在上升沿的电平,就是主机在那个时钟周期读到的、从机发送的位

1.7.1总结:

  • MOSI时序图 = 主机发送给从机的数据位流。

  • MISO时序图 = 从机发送给主机的数据位流。

  • 传输顺序(MSB/LSB first)必须相同且由主机配置。

  • 传输是同步且全双工的: 每一个时钟周期,一位数据在MOSI上从主机传到从机,同时另一位数据在MISO上从从机传到主机。

  • SCLK上升沿数据为真正读取的数据,高电平保持阶段可以看似为读取到的数据

二、W25Q64

2.1W25Q64简介

W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器(数据掉电不丢失),常应用于数据存储、字库存储、固件程序存储等场景,使用SPI通信的芯片之一。

存储介质:Nor Flash(闪存)

时钟频率:80MHz / 160MHz (Dual双重 SPI) / 320MHz (Quad四重 SPI)

存储容量(24位地址最大寻址空间16MB):

W25Q40

4Mbit / 512KByte(4M/8)

W25Q80

8Mbit / 1MByte

W25Q16

16Mbit / 2MByte

W25Q32

32Mbit / 4MByte

W25Q64

64Mbit / 8MByte

W25Q128

128Mbit / 16MByte

W25Q256

256Mbit / 32MByte

2.2硬件电路

引脚

功能

VCCGND

电源(2.7~3.6V

CSSS低电平有效

SPI片选

CLKSCK

SPI时钟

DIMOSI

SPI主机输出从机输入

DOMISO

SPI主机输入从机输出

WP(低电平有效)

写保护(1不保护可写,0保护不可写)

HOLD(低电平有效)

数据保持(相当于SPI总线进行中断)

W25Q64硬件电路连接如下👇

W25Q64 引脚名称引脚号方向功能描述典型连接
CS (Chip Select)1输入片选信号(低电平有效)。通信开始时拉低,结束后拉高。连接 MCU 的任意 GPIO 输出
DO (SO / MISO)2输出串行数据输出(主设备输入)。芯片通过此引脚向主机发送数据。连接 MCU 的 MISO 引脚
WP (Write Protect)3输入写保护(低电平有效)。拉低时,禁止写入状态寄存器(部分型号可硬件写保护存储阵列)。通常可接高电平(VCC)或悬空(内部有上拉) 以禁用写保护。接 VCC 或 MCU GPIO
GND4-电源地接系统 GND
DI (SI / MOSI)5输入串行数据输入(主设备输出)。主机通过此引脚向芯片发送指令和数据。连接 MCU 的 MOSI 引脚
CLK (SCK)6输入串行时钟。由主机产生,用于同步数据移位。连接 MCU 的 SCK 引脚
HOLD7输入保持(低电平有效)。拉低时,芯片暂停当前操作但保持通信状态,释放总线;拉高后继续操作。用于MCU需处理更高优先级中断时。通常可接高电平(VCC)或悬空 以禁用此功能。接 VCC 或 MCU GPIO
VCC8-电源正极2.7V ~ 3.6V)。接 3.3V 电源

2.3W25Q64框图

2.3.1框图重点

①存储器规划:整个存储空间,首先划分若干块,对于每个快,再划分为若干个扇区。对于整个空间,会划分为很多页,每页256字节。

存储阵列组织:总容量 8MB(64Mb),采用分层结构:

  • 块(Block)128 个块,每块 64KB整片擦除(Chip Erase) 会擦除所有块。
  • 扇区(Sector)每块分为 16 个扇区,每个扇区 4KB。这是 最小的擦除单元
  • 页(Page)每扇区分为 16 页,每页 256 字节。这是 最大的连续写入单元

②SPI控制逻辑:芯片的管理员,执行指令,读写数据

③状态寄存器:与忙状态,写使能,写保护等功能有关

④256字节页缓存:对一次性写入的数据量产生限制


256字节页缓存区=256字节的RAM存储器


2.3.2数据写入执行流程和指令与擦除指令

数据读写通过RAM缓存区进行:写入数据→缓存区RAM速度快,但存储只有256字节,因此连续写入的数据量不可超过256字节→时序结束后,芯片将缓存区的数据复制到对应的flash区进行永久保存,而数据从缓存区到flash需要时间,这时的芯片进入忙状态,此时会给状态寄存器的BUSY位置1,此刻,芯片不会响应新的读写时序

疑问:为什么不直接写入flash呢?

SPI写入频率高,而flash写入因为掉电不丢失频率慢

2.3.3读取数据流程和相关指令

通过缓存区读取,但读取只需要查看电路状态即可,基本不花时间,因此,读取的限制很少,速度也很快

⭐2.4Flash注意事项

2.4.1写入操作时:

写入操作前,必须先进行写使能

  • 在执行任何修改存储数据的操作(如页编程、扇区擦除、块擦除、芯片擦除)或写状态寄存器操作之前必须先发送 写使能指令(0x06)

  • 成功执行写使能指令后,状态寄存器中的 WEL 位 会被置 1

  • 一次写使能通常只生效一条指令,之后 WEL 位会自动清零。如需连续写入多条指令,需要在每条指令前重新发送写使能。

每个数据位只能由1改写为0,不能由0改写为1

写入数据前必须先擦除,擦除后,所有数据位变为1

擦除必须按最小擦除单元(一个扇区4KB=4096字节)进行

  • 上电后,先把Flash数据读出来,放到RAM里,当数据有变动,统一把数据备份到Flash里,或者把使用频繁的扇区放到RAM里,当使用频率降低,再把整个扇区备份到Flash里
  • 若数据量非常少,只存储几个字节的参数,可以直接一个字节一个扇区

连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入

  • 由于页缓存只有 256 字节,一次页编程(Page Program, 0x02)操作连续写入的数据量不能超过一页(256 字节)

  • 如果写入的起始地址不是页首(0xXX XX 00),且要写入的数据长度会跨越页边界超过当前页末尾的数据会自动卷绕(Wrap Around)到当前页的开头覆盖写入,而不是写到下一页。规划写入地址和长度至关重要

写入操作结束后或者芯片发送擦除指令后,芯片进入忙状态(BUSY=1),不响应新的读写操作

  • 在发起任何写操作(页编程)、擦除操作(扇区/块/芯片擦除)或写状态寄存器操作后,芯片内部需要时间完成物理操作,此时 BUSY 位会变为 1 。

  • 在 BUSY=1 期间,芯片不会响应任何指令(除了读状态寄存器指令 0x05 和擦除暂停指令)。强行发送指令是无效的。

  • 因此,在发出上述指令后,主机必须不断地读取状态寄存器(0x05)并检查 BUSY 位,直到其变为 0,才能进行下一步操作 。

2.4.2读取操作时:

直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取

⭐2.4.3写入一页数据(256字节)的典型流程:

  1. 发送 写使能指令 (0x06)

  2. (可选)读取状态寄存器 (0x05),确认 WEL 位是否已置 1。

  3. 发送 页编程指令 (0x02),紧接着发送 3 字节的目标起始地址,然后发送要写入的数据(最多 256 字节)。

  4. 等待操作完成:循环读取状态寄存器 (0x05),直到 BUSY 位变为 0。

  5. (可选)发送 写失能指令 (0x04) 增加安全性。

⭐2.4.4擦除一个扇区(4KB)的典型流程:

  1. 发送 写使能指令 (0x06)

  2. 发送 扇区擦除指令 (0x20),紧接着指定要擦除扇区的 3 字节起始地址(必须是 4096 的倍数)。

  3. 等待擦除完成:循环读取状态寄存器 (0x05),直到 BUSY 位变为 0。

⭐2.4.5总结

操作前写使能、写入前需擦除(最小4KB)、写入莫超256字节、操作之后等忙完

三、软件模拟SPI读写W25Q64

3.1My_SPI.c代码

#include "stm32f10x.h"                  // Device header

//置引脚高低电平的函数封装
//从机选择,主机输出引脚
void SPI_MAIN_SS(uint8_t Bitvalue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)Bitvalue);
}

//时钟
void SPI_MAIN_SCLK(uint8_t Bitvalue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)Bitvalue);
}

//主机输出从机输入
void SPI_MAIN_MOSI(uint8_t Bitvalue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)Bitvalue);
}

//主机输入从机输出
uint8_t SPI_FOLLOW_MISO(void)
{
	return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}


void My_SPI_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	//PA4 PA5 PA7主机输出 PA6主机输入 输出引脚推挽输出 输入引脚浮空或上拉输入
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4|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_MAIN_SS(1);//默认不选择从机
	SPI_MAIN_SCLK(0);//使用SPI模式0,SCLK默认低电平
	//MOSI没有明确规定和MISO输入引脚,不用输出电平
	
}



//SPI三个时序基本单元:起始信号 终止信号 交换一个字节模式0
//起始信号
void MY_SPI_Start(void)
{
	SPI_MAIN_SS(0);
}

void MY_SPI_Stop(void)
{
	SPI_MAIN_SS(1);
}


//ByteSend时传进来的参数,通过交换一个字节的时序发送出去
//返回值时ByteReceive 时通过交换一个字节接收到的数据
//通过返回值,传递给调用函数的地方
/*
SS下降沿之后,开始交换字节
(但真实情况是:SS下降沿和数据移出同时进行;SCLK下降沿和数据移出同时进行)
先SS下降沿,移出数据
SCLK上升沿,移入数据
再SCLK下降沿,移出数据……

①在SS下降沿之后(主机和从机同时移出数据的最高位分别放在MOSI和MISO上)
写MOSI,发送位为BteSend的最高位&0x80,此时要保证函数是非0即1的特征
②SCK上升沿之后,主机和从机同时移入数据(SCK上升沿,从机自动读走MOSI上的数据B7,主机只需读取MISO数据)
写SCK上升沿,读取MISO数据
③SCK下降沿,主机从机移出下一位
写SCK下降沿,主机移出ByteSend的B6位……循环8次,读取一个字节
*/
uint8_t MY_SPI_ChangeByte(uint8_t ByteSend)
{
	uint8_t ByteReceive=0x00;
//	SPI_MAIN_MOSI(ByteSend & 0x80);
//	SPI_MAIN_SCLK(1);
//	if(SPI_FOLLOW_MISO()==1)
//	{
//		ByteReceive |=0x80;
//	}
//	SPI_MAIN_SCLK(0);
//	SPI_MAIN_MOSI(ByteSend & 0x40);//循环8次,发送一个字节
	uint8_t i;
	for(i=0;i<8;i++)
	{
		SPI_MAIN_MOSI(ByteSend & (0x80>>i));//通过掩码,依次跳出每一位进行操作
		/*
		代码优化
		SPI_MAIN_MOSI(ByteSend&0x80);//第二次循环,由于前一次已经向右移动一位,因此,此时最高位为次高位
		ByteSend <<=1;将数据最高位移出到MOSI,最低位自动补0
		SPI_MAIN_SCLK(1);
		if(SPI_FOLLOW_MISO()==1)
		{
			ByteSend |=0x01;
		}
		SPI_MAIN_SCLK(0);
		*/	
		SPI_MAIN_SCLK(1);
		if(SPI_FOLLOW_MISO()==1)
		{
			ByteReceive |=(0x80>>i);
		}
		SPI_MAIN_SCLK(0);
	}
	return ByteReceive;
}

3.2W25Q64.c代码

#include "stm32f10x.h"                  // Device header
#include "My_SPI.h"
void W25Q64_Init(void)
{
	My_SPI_Init();
}

//获取ID号时序:起始,交换发送指令0x9F,连续交换接收三个字节,停止
//函数有俩个返回值,指针实现多返回值,先输出8位厂商ID,再输出16位设备ID
void W25Q64_readID(uint8_t *MID,uint16_t *DID)
{
	MY_SPI_Start();
	MY_SPI_ChangeByte(0x9F);//从机接收读取ID号指令,下一次交换,会把ID号还给主机
	*MID=MY_SPI_ChangeByte(0XFF);//从机的上次接收到ID号和此次主机发送的垃圾号交换
	*DID=MY_SPI_ChangeByte(0XFF);//设备ID的高8位
	//连续俩次相同的函数,接收到的数据不同,因为正在进行通信,通信有时序,不同时间调用不同的函数数据
	*DID<<=8;
	*DID |=MY_SPI_ChangeByte(0XFF);	
	MY_SPI_Stop();
}


3.3获取ID号相关知识点

3.3.1读取流程 (软件模拟SPI)

整个过程完全由主机的时钟(SCLK)驱动。

  1. 起始通信:将 CS# (片选) 引脚从高电平拉至低电平。

  2. 发送指令码:主机通过 MOSI 线,在 SCLK 的每个时钟边沿,逐位发送 1 个字节的指令码 0x9F

  3. 读取数据:指令发送完毕后,主机继续产生时钟信号。同时,从机(W25Q64)开始通过 MISO 线依次发送 3 个字节的数据:

    • 第1个字节:制造商ID (Manufacturer ID)

    • 第2个字节:存储器类型 (Memory Type)

    • 第3个字节:容量 (Capacity)

  4. 结束通信:将 CS# 引脚从低电平拉回高电平。

关键点

在整个过程中,主机在需要从机返回数据的阶段(即步骤3),会继续向MOSI线发送“哑元”数据(通常是 0xFF)来为从机提供输出数据所需的时钟信号。从机则完全忽略主机在此时发送的内容。


3.3.2为什么发送0xFF就可以得到ID?

这个问题的核心在于理解 SPI协议是“同步”和“全双工”的

  • 同步:数据的传输完全由主机的时钟(SCLK)控制,每一个比特位都是在SCLK的一个边沿被采样或输出。没有时钟,数据就无法移动

  • 全双工:在每一个时钟周期内,主机和从机同时进行一项操作:

    • 主机在MOSI线上输出1个比特。

    • 从机在MISO线上输出1个比特。

现在我们来分析 0x9F 指令的时序:

  1. 阶段一:主机发送指令 (0x9F)

    主机在MOSI上输出指令位,从机在MISO上输出无意义的数据(通常是高阻态或随机值,主机忽略它)。此时通信的目的是让从机接收指令。

  2. 阶段二:主机读取ID数据

    • 指令发送完毕,从机已经知道主机要读取JEDEC ID,并准备好了数据。

    • 主机必须继续产生时钟才能把这些数据位“读”回来。

    • 为了产生时钟,主机必须在MOSI线上发送一些东西。发送什么内容呢?

      • 发送 0xFF (二进制 11111111) 是最安全、最通用的选择。因为它不会意外构成任何其他有效指令(大部分指令的最高位都是0或1,但全1不会误触发写使能等危险操作)。

    • 所以,主机“假装”要发送数据 0xFF,从而产生8个时钟周期。从机则利用这8个时钟周期,将ID数据的第一个字节(制造商ID)的一位一位地输出到MISO线上,主机同时进行采样。

    • 重复这个过程三次,即可读完三个字节的ID。

总结

主机发送0xFF的目的不是为了传递数据,而是为了“购买”时钟周期。主机用这些“无用”的比特位,换取从机输出“有用”的比特位。SPI的这种特性意味着读取任何寄存器,本质上都是一个“先写后读”的过程,即使写的内容是无效的。


3.3.3ID是原先存储在从机里的吗?

是的,完全正确。

这个ID信息在芯片生产封装时就已经被永久地固化在硅片内部的一个只读存储器(ROM)中。用户无法通过任何指令修改它。它的作用就是让主机(MCU)能够识别出与之通信的是哪个厂家、哪种型号的Flash芯片,以便驱动程序选择正确的操作参数(如页大小、扇区大小、容量等)。

  • 对于W25Q64

    • 字节1 (制造商ID)0xEF - 代表Winbond(华邦)。

    • 字节2 (存储器类型)0x40 - 代表这是一个SPI Flash产品。

    • 字节3 (容量)0x17 - 0x17 specifically对应64M-bit的容量。0x16对应32M-bit (W25Q32),0x18对应128M-bit (W25Q128)。

3.4读取ID号main.c代码

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"

uint8_t MID;
uint16_t DID;
int main(void)
{	
	
    OLED_Init();
	W25Q64_Init();
	W25Q64_readID(&MID,&DID);
	OLED_ShowHexNum(1,1,MID,2);
	OLED_ShowHexNum(2,1,DID,8);
	while(1)
	{

	}
}

3.5其他指令的代码

3.5.1手册中各种指令代码的思路展现

3.5.2手册中的注意点:

①带括号“()”中的数据的字节字段表示正在从DO引脚上的设备读取数据,读取数据就要发送0xFF无用数据交换到设备上有用的字节。
②(S7-S0)状态寄存器的内容将不断重复,直到/CS终止该指令,需要发送0xFF无用数据交换到有用数据,判断S0Busy位。

③没有括号“()”的数据,如A23-A70,需要我们发送三个字节地址,再发送数据(最大256个字节,若继续发送会覆盖初始写入的字节)(改错Page Program中应该是D7-D0无括号)

④表格中出现的Dummy,直接发送0xFF无用数据即可,可能为电路延时做准备

3.5.3其他指令的代码书写思路

①标准读取 (Read Data - 0x03)

  • 功能:从指定地址开始读取数据。无需使能,无需等待。

  • 时序

    1. 拉低 CS

    2. 发送指令码 0x03

    3. 发送 3字节(24位)的地址(A23-A0)。

    4. 然后芯片会从该地址开始,从 DO (MISO) 引脚逐字节输出数据

    5. 每输出一个字节,内部地址指针会自动加一,从而实现连续读取。只要 CS 保持为低,就可以一直读下去,甚至跨越扇区和页的边界。

    6. 拉高 CS 结束读取。

  • 特点:最常用、最简单的读取方式。

②快速读取 (Fast Read - 0x0B)

  • 功能:比标准读取更快的读取方式,通常用于更高时钟频率下。

  • 时序:与 0x03 类似,但在发送完地址后,需要再发送一个额外的“哑元(Dummy)字节”(通常为 0xFF),之后芯片才开始输出数据。

  • 特点:这个哑元周期给了芯片内部电路更多的准备时间,从而允许主设备使用更高的 SPI 时钟频率。

③页编程 (Page Program - 0x02)

  • 功能:向指定地址开始写入数据。写入之前,必须先写使能。

  • 时序

    1. 发送 0x06 (Write Enable)。

    2. 拉低 CS

    3. 发送指令码 0x02

    4. 发送 3字节(24位)的起始地址

    5. 然后逐字节发送要写入的数据(指针)

    6. 拉高 CS

    7. 等待状态寄存器BUSY位置0

  •  极其重要的限制

    1. 必须先发送 0x06 (Write Enable)。

    2. 一次写入的数据量不能超过一页(256字节)

    3. 不能跨页写入:如果写入的起始地址不是页首(如 0x123405),且要写入的数据长度会导致地址跨越当前页的边界(如从地址 0x1234FF 开始写 10 字节,会写到 0x123509),超过当前页末尾的数据会自动“卷绕”到当前页的开头覆盖写入,而不是写到下一页。编程时必须计算好地址和长度!

    4. 发送完指令后,必须通过读状态寄存器检查 BUSY 位,等待写入操作完成

②扇区擦除 (Sector Erase - 0x20)

  • 功能:擦除一个 4KB 的扇区。这是最常用的擦除单位.写入之前,必须先写使能。

  • 时序

    1. 发送 0x06 (Write Enable)。

    2. 拉低 CS

    3. 发送指令码 0x20

    4. 发送要擦除的扇区的起始地址(该地址必须是 4096 的倍数,即低 12 位为 0,例如 0x0000000x0010000x002000)。

    5. 拉高 CS

    6. 读状态寄存器,等待 BUSY 位变0

  • 原理:Flash 只能将 1 变为 0,擦除是唯一能将 0 变回 1 的操作。写入数据前,必须确保目标区域已经被擦除(全为 0xFF)

③整片擦除 (Chip Erase - 0xC7 / 0x60)

  • 功能:擦除整个芯片的所有存储单元(恢复为全 0xFF)。

  • 时序:类似擦除指令,但不需要发送地址

  • 警告:此操作耗时很长(可能几十秒),且会清除所有数据除非特殊情况,否则应尽量避免使用

④总的写入数据流程:

  1. 擦除目标扇区 (0x20) -> 等待 BUSY=0(写入之前,必须擦除扇区)

  2. 写使能 (0x06)。

  3. 页编程 (0x02) -> 发送地址和数据 -> 等待 BUSY=0

  4. (可选)写失能 (0x04)。(一次写使能通常只生效一条指令,之后 WEL 位会自动清零。如需连续写入多条指令,需要在每条指令前重新发送写使能。因此,for循环中,写使能在内部)

3.5.4其他指令代码展示

#include "stm32f10x.h"                  // Device header
#include "My_SPI.h"
#include "W25Q64_INS.h"
void W25Q64_Init(void)
{
	My_SPI_Init();
}

//获取ID号时序:起始,交换发送指令0x9F,连续交换接收三个字节,停止
//函数有俩个返回值,指针实现多返回值,先输出8位厂商ID,再输出16位设备ID
void W25Q64_readID(uint8_t *MID,uint16_t *DID)
{
	MY_SPI_Start();
	MY_SPI_ChangeByte(W25Q64_JEDEC_ID);//从机接收读取ID号指令,下一次交换,会把ID号还给主机
	*MID=MY_SPI_ChangeByte(W25Q64_DUMMY_BYTE);//从机的上次接收到ID号和此次主机发送的垃圾号交换
	*DID=MY_SPI_ChangeByte(W25Q64_DUMMY_BYTE);//设备ID的高8位
	//连续俩次相同的函数,接收到的数据不同,因为正在进行通信,通信有时序,不同时间调用不同的函数数据
	*DID<<=8;
	*DID |=MY_SPI_ChangeByte(W25Q64_DUMMY_BYTE);	
	MY_SPI_Stop();
}

//写使能,后续可以不跟任何数据
void W25Q64_WriteEnable(void)
{
	MY_SPI_Start();							
	MY_SPI_ChangeByte(W25Q64_WRITE_ENABLE);	//写使能指令	
	MY_SPI_Stop();							
}

//状态寄存器1主要判断主机是否在忙状态,BUSY=1忙,BUSY!=1不忙,其中需要等待为0的函数
//先发送指令码,再接收状态寄存器,若时序不停,还要继续接收,芯片会将最新的状态寄存器发送(状态寄存器支持连续读出,方便执行等待的功能)
void W25Q64_WaitBusy(void)//等待BUSY=0
{
	uint32_t Timeout;
	MY_SPI_Start();								
	MY_SPI_ChangeByte(W25Q64_READ_STATUS_REGISTER_1);//交换发送读状态寄存器1的指令
//	while ((MY_SPI_ChangeByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01);//掩码取出最低位,若等于0x01,即BUSY=1,进入循环等待为不忙状态
	Timeout = 100000;
	while ((MY_SPI_ChangeByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)	
	{
		Timeout --;	
		if (Timeout == 0)					
		{	
			break;								
		}
	}
	MY_SPI_Stop();							
}


//W25Q64页编程,写入范围0-256, uint8_t count范围是0-255
//格式:先发送指令码02,再发送三个字节地址,最后发数据DataByte1,DataByte2,DataByte3……最大为256,若继续发,会覆盖一开始发送的数据
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
	uint16_t i;
	W25Q64_WriteEnable();//写入操作前,必须先写使能
	//写使能仅对之后跟随的一条时序有效,一条时序后会自动失能,因此在每次写入操作前都加入写使能,不写失能
	MY_SPI_Start();						
	MY_SPI_ChangeByte(W25Q64_PAGE_PROGRAM);//页编程的指令
	MY_SPI_ChangeByte(Address >> 16);//最高位 0x123456中的12
	MY_SPI_ChangeByte(Address >> 8);//中位 0x123456中的34
	MY_SPI_ChangeByte(Address);//最低位 0x123456中的56
	for (i = 0; i < Count; i ++)//写入数据
	{
		MY_SPI_ChangeByte(DataArray[i]);
	}
	MY_SPI_Stop();	
	W25Q64_WaitBusy();//写入操作之后,都会进入忙状态,等待状态寄存器的BUSY=0
}

//W25Q64扇区擦除,就是写入0xFF,所以也是写代码
void W25Q64_SectorErase(uint32_t Address)
{
	W25Q64_WriteEnable();//写入操作前,必须先写使能		
	MY_SPI_Start();								
	MY_SPI_ChangeByte(W25Q64_SECTOR_ERASE_4KB);	//扇区擦除指令
	MY_SPI_ChangeByte(Address >> 16);//与页编程发送数据类似
	MY_SPI_ChangeByte(Address >> 8);	
	MY_SPI_ChangeByte(Address);				
	MY_SPI_Stop();						
	W25Q64_WaitBusy();							
}

//文件中出现dummy,直接发送0xFF无用数据即可,可能为电路延时做准备
//W25Q64读取数据,在忙状态,数据读取也不行,事后等待,写入数据都等待过了,因此读取时肯定不忙
//流程:发送指令03,发送三个字节地址,转入接收,就可以接收数据
//DO一直高阻态,发送完三个字节地址后,DO开启输出,主机可以接收有用数据DataOut1……读取没有限制,可以跨页一直读
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
	uint32_t i;
	MY_SPI_Start();					
	MY_SPI_ChangeByte(W25Q64_READ_DATA);	//读数据指令
	MY_SPI_ChangeByte(Address >> 16);			
	MY_SPI_ChangeByte(Address >> 8);			
	MY_SPI_ChangeByte(Address);					
	for (i = 0; i < Count; i ++)//开始读地址		
	{
		DataArray[i] = MY_SPI_ChangeByte(W25Q64_DUMMY_BYTE);//发送无用数据,抛砖引玉,返回值为读到的数据
	}
	MY_SPI_Stop();
}

3.6W25Q64指令地址

#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

3.7其他指令main.c代码

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"

uint8_t MID;
uint16_t DID;
uint8_t ArrayWrite[]={0x01,0x02,0x03,0x04};
uint8_t ReadArray[4];

int main(void)
{	
	
  OLED_Init();
	W25Q64_Init();
	W25Q64_readID(&MID,&DID);
	OLED_ShowString(1,1,"MID:   DID:");
	OLED_ShowString(2,1,"W:");
	OLED_ShowString(3,1,"R:");

	OLED_ShowHexNum(1,5,MID,2);
	OLED_ShowHexNum(1,12,DID,4);
	
	//写之前,先擦除扇区
	//若只擦除不写入,读取数据都是FF
	//若改写数据,却不擦除数据,读取的数据是错的
	W25Q64_SectorErase(0x000000);//后三位为0扇区起始地址
	//页写,不可以跨页写入
	W25Q64_PageProgram(0x000000,ArrayWrite,4);//起始地址,存入数组,四个字节
	//读数据,可以跨页读取
	W25Q64_ReadData(0x000000,ReadArray,4);
	OLED_ShowHexNum(2,3,ArrayWrite[0],2);
	OLED_ShowHexNum(2,6,ArrayWrite[1],2);
	OLED_ShowHexNum(2,9,ArrayWrite[2],2);
	OLED_ShowHexNum(2,12,ArrayWrite[3],2);

    OLED_ShowHexNum(3,3,ReadArray[0],2);
	OLED_ShowHexNum(3,6,ReadArray[1],2);
	OLED_ShowHexNum(3,9,ReadArray[2],2);
	OLED_ShowHexNum(3,12,ReadArray[3],2);
	while(1)
	{

	}
}

四、硬件SPI

4.1SPI外设简介

STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担

可配置8/16位数据帧、高位先行/低位先行

时钟频率: fPCLK(外设时钟) / (2, 4, 8, 16, 32, 64, 128, 256)

APB2的PCLK为72MHz,APB1的PCLK为36MHz

支持多主机模型、主或从操作

可精简为半双工/单工通信

支持DMA

兼容I2S协议(音频传输协议)

STM32F103C8T6 硬件SPI资源:SPI1(APB2外设)SPI2(APB1外设)

4.2SPI框图

数据发送和接收模可查看之前的串口详情STM32——通信接口(串口通信)_stm32串口 默认先传高位再传低位吗-CSDN博客

SPI全双工,发送和接收同步进行——数据寄存器,发送和接收分离,移位寄存器发送和接受公用

I2C半双工,发送和接收不会同时进行——发送接收寄存器和移位寄存器都是公用的

串口全双工,发送和接收异步进行——发送和接收寄存器和发送接收移位寄存器都是分离的

4.3SPI基本结构

核心部分为发送接收寄存器(TDR_TXE标志位,RDR_RXNE标志位)+移位寄存器(高位先行),数据通过GPIO口传输/传入到MOSI和MISO上

波特率发生器,产生时钟,通过GPIO口,输出到SCK引脚

数据控制器,控制所有电路运行

开关控制Cmd

SS引脚通过GPIO口模拟

4.4主模式全双工连续传输

传输快,操作较复杂,示例为模式3,因此SCK初始为高电平

4.4.1工作流程:

  1. 老板按下秘书的呼叫按钮(NSS线拉低),表示通话开始。

  2. 老板开始以固定的节奏(SCLK时钟)说话,同时听报告。

  3. 在发送完1个字节/字的数据后,时钟SCLK不会停止,NSS信号也始终保持有效(低电平)。老板毫不停顿地继续说下一个字节/字。

  4. 只有当所有数据都说完了,老板才会松开呼叫按钮(NSS线拉高),通话彻底结束。

4.4.2流程详解(结合图表):

  1. 启动传输 (Start):

    • 你(程序)调用函数启动传输。硬件会自动将NSS信号拉低(如果是软件NSS模式),并开始产生SCLK时钟BUSY标志立即被置1

  2. 首次写入 (First Write):

    • 你将第一个要发送的数据(例如 Data1)写入 TDR 寄存器。

    • 一旦写入,硬件会立刻将 Data1 从 TDR 搬运到内部的移位寄存器中。由于TDR空了,TXE标志被置1

  3. 发送与接收开始 (Shift Out/In):

    • 移位寄存器在 SCLK 的每个时钟节拍下,开始将 Data1 的每一位从 MOSI 引脚移出。

    • 同时,它也在每个SCLK节拍下,从 MISO 引脚读取一位数据,存入移位寄存器。

  4. 填充流水线 (Keep Feeding):

    • 因为TXE已经为1,你知道TDR是空的,可以立即写入第二个数据 Data2 到 TDR

    • 此时,Data1 正在发送中,Data2 已经在TDR里排队等候了。这就是双缓冲区的优势,实现了“流水线”作业,效率极高。

  5. 数据移位完成 (Frame Transfer Complete):

    • Data1 的最后一位被发送出去,同时,从设备返回数据的最后一位也被接收进来。此时,一个完整的数据帧传输完毕。

    • 硬件自动将接收移位寄存器里刚收满的数据(我们称为 RxData1)搬运到 RDR 寄存器中。

    • 由于RDR中有了新数据,RXNE标志被置1

    • 同时,硬件自动将已在TDR中排队的 Data2 搬运到移位寄存器中,开始下一帧的发送和接收。TXE再次被置1,提示你可以写入 Data3 了。

  6. 读取数据 (Read Data):

    • 你的程序检测到 RXNE == 1,知道一个数据已经收到。你从 RDR 寄存器中读取 RxData1

    • 读取RDR的操作会自动将 RXNE标志清零

  7. 循环往复 (Loop):

    • 重复步骤4-6:你不断地利用TXE==1的信号写入下一个发送数据,并利用RXNE==1的信号读取接收到的数据,直到所有数据都发送完毕。

  8. 传输结束 (End of Transfer):

    • 当你发送完最后一个数据后,需要等待最后一个数据的接收完成(即最后一次 RXNE==1 并读取)。

    • 之后,你还需要等待 TXE==1(确保最后一个数据已从TDR进入移位寄存器)。

    • 最后,必须等待 BUSY标志变为0。BUSY=0意味着最后一位数据也已经处理完毕,SCLK时钟停止,硬件自动将NSS信号拉高(如果配置为软件NSS),标志着整个连续传输过程正式结束。在BUSY变0前,不要操作SPI寄存器。

4.4.3关键特点:

  • NSS信号在整个多字节传输期间始终保持有效(低电平)

  • 时钟SCLK在字节与字节之间是连续产生的,没有停顿。

  • 数据流是一个紧密连续的波形。

  • RDR数据要及时读走(RXNE=1之前),否则会被覆盖

  • 交换流程交错,发送数据1,数据2,接收数据1,发送数据3,接收数据2……

4.4.4为什么要用这种模式?

  • 极高的效率:减少了NSS信号频繁切换和时钟重启带来的时间开销,适合高速大数据流传输。

  • 特定协议要求:有些通信协议(如某些型号的SD卡、ADC芯片、LCD驱动芯片)规定数据必须是连续发送的,字节之间不能有空隙。

4.5非连续传输(默认/常见模式)

易封装,易理解,好用,丢失一些性能

4.5.1工作流程--老板主机,秘书从机:

  1. 老板按下秘书的呼叫按钮(NSS线拉低),表示通话开始。

  2. 老板开始以固定的节奏(SCLK时钟)说话。

    • 每滴答一次(一个时钟脉冲),老板通过MOSI线发送1 bit的指令。

    • 同时,秘书也通过MISO线向老板发送1 bit的报告。

    • 经过8个或16个脉冲(取决于数据帧格式),老板发送完1个字节/字的指令,也同时接收了1个字节/字的报告。

  3. 此时,如果老板没有更多的话要说,他就会松开呼叫按钮(NSS线拉高),表示这次通话结束。

4.5.2流程详解(结合图表):

  1. 启动单次传输 (Initiating a Frame):

    • 你的程序准备发送一帧数据(比如1个字节)。硬件会自动将NSS信号拉低(选中从设备),并开始产生SCLK时钟BUSY标志被置1

  2. 写入数据 (Writing Data):

    • 你将数据(Data1)写入 TDR 寄存器。

    • 硬件立刻将 Data1 从TDR搬运到移位寄存器TXE标志被置1。此时,即使TXE=1,你也可以先不写下一个数据(因为模式不同,处理方式也不同)。

  3. 发送与接收 (Shifting Data):

    • 移位寄存器在 SCLK 的驱动下,将 Data1 的位从MOSI移出,同时从MISO移入位数据。

  4. 一帧传输完成 (End of a Frame):

    • 当第8个(或16个)SCLK脉冲结束后,一帧数据传输完毕。

    • 关键步骤来了! 硬件会做以下几件事:

      • 将接收移位寄存器中的数据搬运到 RDR,并将 RXNE标志置1

      • 停止SCLK时钟

      • 将NSS信号拉高(释放从设备,表示“我这话说完了”)。

      • 将 BUSY标志清零(表示一次完整的传输会话结束了)。

  5. 读取数据 (Reading Data):

    • 你的程序检测到 RXNE == 1,于是从 RDR 中读取接收到的数据RxData1。读取操作会自动清除RXNE标志。

  6. 传输间隔 (The Gap):

    • 此时,SPI总线处于“休息”状态:SCLK无时钟,NSS为高,BUSY为0。总线上的电平是静止的。这就是“非连续”中的那个“间隔”。

  7. 下一次传输 (Next Frame):

    • 当你的程序需要发送第二帧数据(Data2)时,必须重新启动一次完整的传输过程

      • 硬件再次拉低NSS,启动SCLK,置位BUSY。

      • 你写入Data2到TDR。

      • 重复步骤2-6。

关键特点:

  • NSS信号在每次传输一个数据帧(如8bit)后都会无效(拉高)。

  • 传输每个数据帧之间都有明显的“间隔”或“停顿”。

  • 非常适合与需要“片选”信号的传统SPI设备通信。每次传输数据前都要先选中设备(拉低NSS)。

4.6总结与对比:非连续 vs 连续

特性

非连续传输 (Non-Continuous)

连续传输 (Continuous)

传输单元

单帧(如1字节)

多帧(如整个数组)

NSS 信号

每传完一帧就拉高,下次传输再拉低。频繁切换。

整个多帧传输期间一直保持低电平,最后才拉高。

SCLK 时钟

每传完一帧就停止

帧与帧之间不间断,产生一个连续、均匀的时钟流。

BUSY 标志

每传输一帧,BUSY从置1到清零一次

只有整个多帧传输开始和结束时,BUSY才变化一次

数据流波形

数据包之间有明显的空闲间隔

数据包之间紧密相连,无间隔

编程模型

通常是多次调用HAL_SPI_TransmitReceive,每次发一帧或一小批数据。

通常是单次调用HAL_SPI_TransmitReceive并传入整个数组,硬件自动完成流式传输。

效率

相对较低,因为有多余的NSS切换和时钟起停开销。

极高,消除了帧间开销,适合高速流数据。

应用场景

读写普通寄存器、发送单条命令等低速、交互式通信。

读写音频数据、显示屏帧缓存、大容量存储等高速、流式通信。

五、软硬件SPI对比

5.1对比表格:硬件SPI vs 软件模拟SPI

特性维度硬件SPI (Hardware SPI)软件模拟SPI (Software/Bit-Banging SPI)
本质使用STM32芯片内部专用的硬件电路来实现SPI协议。用普通的GPIO引脚CPU内核通过代码“模仿”SPI的时序。
速度与性能。速度仅受芯片手册规定的最高频率限制(可达数十MHz)。
稳定,时钟(SCLK)由硬件产生,精度高,波形整齐。
。速度受限于CPU执行指令的速度,通常最快也就1-2MHz。
不稳定,时钟信号由循环和延时产生,容易受中断、其他任务干扰,产生抖动。
CPU占用率极低。CPU只需配置好SPI外设,将数据放入发送寄存器,就可以去处理其他任务,传输过程由DMA或SPI硬件自动完成。极高。CPU必须全程参与每一个时钟跳变和每一位数据的读写,期间无法高效处理其他任务。
代码复杂度。通常使用库函数(如HAL库的HAL_SPI_Transmit),几行代码即可完成配置和传输。。需要自己编写底层的时序控制代码,包括时钟拉高拉低、数据位读写等,代码量大且容易出错。
灵活性。引脚是芯片硬件固定的(如STM32F1的SPI1_SCK必须在PA5引脚)。修改引脚需要查手册,有时甚至无法修改。极高任意通用GPIO引脚都可以用来模拟SPI,可以自由定义SCK、MOSI、MISO、NSS是哪几个引脚。
功能完整性。天然支持全双工、主从模式、各种时钟极性和相位(CPOL/CPHA)、中断、DMA等高级功能。实现复杂。实现全双工非常困难且低效。实现从模式几乎不可能。中断和DMA支持也需额外编程,非常繁琐。
可靠性。由硬件保证时序,抗干扰能力强,通信稳定可靠。较低。容易受到中断、任务调度、编译器优化等因素影响,导致时序错乱,在复杂系统中可能不稳定。
应用场景高速、大数据量、高可靠性的应用。
例如:驱动高清屏、读取SD卡、与高速ADC/DAC通信、连接无线模块等。
低速、简单、引脚资源紧张或需要引脚重映射的应用。
例如:读写一个低速的传感器、驱动一个简单的LED驱动芯片、硬件SPI引脚被其他功能占用时。

5.2如何选择?

  1. 首选硬件SPI:只要硬件SPI引脚没有被占用,且数量够用,永远优先使用硬件SPI。它的性能和可靠性是软件模拟无法比拟的。

  2. 不得已而用之:在以下情况下才考虑软件模拟SPI:

    • 项目需要的SPI设备数量超过了芯片自带的硬件SPI外设数量(比如芯片只有2个SPI,但你需要接3个SPI设备)。

    • 硬件SPI的默认引脚由于PCB布线等原因无法使用,又没有重映射到其他引脚的功能。

    • 通信速度要求极低(比如几KHz),且对CPU占用不敏感。

    • 为了调试和学习,亲手模拟一下SPI时序可以帮助你深刻理解协议的本质。

总而言之,硬件SPI是“专业团队”,软件模拟是“自己动手”。

六、硬件SPI读写W25Q64

6.1外设引脚连接

注意要根据复用功能选择引脚

若以上引脚被占用,可以选择重定义那一栏的SPI1的引脚重定义。

注意:PA15、PB3、PB4在默认情况下是作为JTAG的调试端口使用,若要使用原本的GPIO功能,或者使用重定义的外设引功能,需要先解除调试端口的复用(否则GPIO或者外设引脚不会正常工作)。方法在STM32——TIM定时器输出比较+PWM+舵机+直流电机-CSDN博客

6.2硬件SPI初始化

硬件SPI需要完成两步
①SPI外设初始化代码
Ⅰ开启时钟,开启SPI和GPIO时钟
Ⅱ初始化GPIO口

其中SCK和MOSI由硬件外设控制的输出信号,配置复用推挽输出;
MISO是硬件外设的输入信号,配置成上拉输入
SS软件控制的输出引脚,配置成通用推挽输出
Ⅲ配置SPI外设,用结构体
Ⅳ开关控制,使能SPI

②SPI外设操作时序,完成交换一个字节的流程
根据图218完成相关代码编写

6.3硬件SPI外设相关库函数

1.恢复缺省配置
void SPI_I2S_DeInit(SPI_TypeDef* SPIx);
2.初始化
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
3.结构体变量初始化
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);
4.外设使能
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);


5.中断使能
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);
6.DMA使能
void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);


7.写DR数据寄存器
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
8.读DR数据寄存器
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);


9.NSS引脚配置
void SPI_NSSInternalSoftwareConfig(SPI_TypeDef* SPIx, uint16_t SPI_NSSInternalSoft);
void SPI_SSOutputCmd(SPI_TypeDef* SPIx, FunctionalState NewState);
10.8/16位数据帧配置
void SPI_DataSizeConfig(SPI_TypeDef* SPIx, uint16_t SPI_DataSize);


11.CRC校验配置
void SPI_TransmitCRC(SPI_TypeDef* SPIx);
void SPI_CalculateCRC(SPI_TypeDef* SPIx, FunctionalState NewState);
uint16_t SPI_GetCRC(SPI_TypeDef* SPIx, uint8_t SPI_CRC);
uint16_t SPI_GetCRCPolynomial(SPI_TypeDef* SPIx);


12.半双工时,双向线的方向配置
void SPI_BiDirectionalLineConfig(SPI_TypeDef* SPIx, uint16_t SPI_Direction);


13.获取标志位
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
14.清除标志位
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
15.获取中断标志位
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
16.清除中断标志位
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);


6.4硬件SPI代码

#include "stm32f10x.h"                  // Device header
//使用非连续性传输

//置引脚高低电平的函数封装
//从机选择,主机输出引脚,继续使用软件模拟
void SPI_MAIN_SS(uint8_t Bitvalue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)Bitvalue);
}

void My_SPI_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
	
	//SS从机选择引脚PA4 输出引脚通用推挽输出
	GPIO_InitTypeDef GPIO_InitStructure;
	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);
	//SCK和MOSI外设控制的输出,配置为复用推挽输出 PA5和PA7
	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);
	//MISO配置上拉输入模式 PA6
	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_InitTypeDef SPI_InitStructure;
	SPI_InitStructure.SPI_BaudRatePrescaler=SPI_BaudRatePrescaler_128;//波特率预分频器,配置SCK时钟频率,分频系数越大,SCK时钟频率越小,传输越慢
	SPI_InitStructure.SPI_CPHA=SPI_CPHA_1Edge;//SPI模式0123
	SPI_InitStructure.SPI_CPOL=SPI_CPOL_Low;//SPI模式0123
	SPI_InitStructure.SPI_CRCPolynomial=7;//CRC 校验多项式 默认值7
	SPI_InitStructure.SPI_DataSize=SPI_DataSize_8b;//配置8/16位数据帧
	SPI_InitStructure.SPI_Direction=SPI_Direction_2Lines_FullDuplex;//配置SPI裁剪引脚
	/*
	#define SPI_Direction_2Lines_FullDuplex 全双线全双工发送模式
	#define SPI_Direction_2Lines_RxOnly     全双线只接收模式
	#define SPI_Direction_1Line_Rx      单线单双工接收模式   
	#define SPI_Direction_1Line_Tx      单线单双工发送模式   
	*/
	SPI_InitStructure.SPI_FirstBit=SPI_FirstBit_MSB;//高/低位先行
	SPI_InitStructure.SPI_Mode=SPI_Mode_Master;//SPI模式:决定SPI主机还是从机
	SPI_InitStructure.SPI_NSS=SPI_NSS_Soft;//硬/软件NSS模式
	SPI_Init(SPI1,&SPI_InitStructure);
	
	SPI_Cmd(SPI1,ENABLE);
	
	//调用SS函数,默认高电平,不选择从机
	SPI_MAIN_SS(1);
}




//SPI三个时序基本单元:起始信号 终止信号 交换一个字节模式0
//起始信号
void MY_SPI_Start(void)
{
	SPI_MAIN_SS(0);
}

void MY_SPI_Stop(void)
{
	SPI_MAIN_SS(1);
}


uint8_t MY_SPI_ChangeByte(uint8_t ByteSend)
{	
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)!=1);
	SPI_I2S_SendData(SPI1,ByteSend);
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)!=1);//RXNE=0,接收数据空了。RXNE=1,接收数据非空,接收到数据
	return SPI_I2S_ReceiveData(SPI1);
}

调用交换字节的函数,硬件SPI外设可以自动控制SCK,MISO和MISO引脚

交换字节硬件SPI步骤:
①等待TXE=1,发送寄存器空;若发送寄存器非空,就不写
②软件写入数据置SPI_DR
③等待RXNE=1,数据接收到
④读取RDR接收数据,置换接收的一个字符

注意:
①此时发送和接收时同时进行的,要想接收必须先发送
②手册上写:TXE和RXNE由硬件设置,软件清除,实际可以直接由后续硬件措施自动清除


不需要手动清除标志位
TXE:当标志位为1表明发送缓冲器空,可以写入下一个待发送的数据进入缓冲器。当写入数据寄存器中,TXE标志位被清除。
RXNE:当标志位为1表明在接收缓冲器中包含有效的接收数据,当读取SPI数据寄存器时可以清除此标志位。  


 ***等待BUSY不忙也可进行尝试
 

根据引用\[1\]中提供的信息,可以使用STM32的HAL库来模拟SPI方式驱动W25Q128存储芯片。首先需要进行模拟SPI方式的IO配置,然后使用相应的驱动代码来实现功能。 W25Q128是一款SPI接口的NOR Flash芯片,具有128 Mbit的存储空间,相当于16M字节。NOR Flash是一种常用的用于存储数据的半导体器件,具有容量大、可重复擦写、按扇区/块擦除、掉电后数据可继续保存的特性。Flash的物理特性是只能写0,不能直接写1,写1需要进行擦除操作。 根据引用\[3\]中的实验,可以通过硬件接线将W25Q128模块与STM32连接起来,其中VCC接3.3V,CS接PA4,CLK接PA5,DO接PA6,DI接PA7。然后可以使用CubeMX进行相应的配置。 要获取W25Q128的ID,可以使用SPI通信协议来读取芯片的ID寄存器。具体的代码实现可以参考引用\[1\]中提供的驱动代码。 #### 引用[.reference_title] - *1* [STM32CubeMX | STM32使用HAL库模拟SPI方式驱动W25Q128存芯片](https://blog.csdn.net/qq153471503/article/details/106895933)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [STM32SPI和W25Q128](https://blog.csdn.net/weixin_49001476/article/details/130909856)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值