核心观点
本文主要介绍了SPI通信的两种实现方式:软件SPI和硬件SPI。详细阐述了SPI通信协议的基本概念、硬件电路连接方式、移位示意图、时序基本单元以及四种工作模式。同时,对W25Q64模块进行了详细介绍,包括其硬件电路、框图以及操作注意事项,并分别给出了软件SPI和硬件SPI读写W25Q64的代码示例。
一、SPI通信协议
1. SPI通信
SPI(Serial Peripheral Interface)是一种由Motorola公司开发的通用数据总线,广泛应用于微控制器与各种外围设备之间的通信。它具有同步、全双工的特点,支持总线挂载多设备(一主多从)。
- SPI通信使用四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)。其中,SCK为时钟线,MOSI为主设备到从设备的数据线,MISO为从设备到主设备的数据线,SS为从设备选择线。
- 在SPI通信中,主机通过控制SS线来选择与之通信的从机。主机的SS线为输出,从机的SS线为输入,低电平有效。当主机需要与某个从机通信时,只需将对应的SS线置为低电平即可。
- SPI通信具有多种工作模式,通过设置时钟极性(CPOL)和时钟相位(CPHA)来确定数据的采样和移位时机。常见的模式有模式0、模式1、模式2和模式3,其中模式0使用最为广泛。
- SPI通信的移位过程是通过移位寄存器实现的。主机和从机的移位寄存器都有时钟输入端,SPI采用高位先行的方式。每来一个时钟,移位寄存器向左移位。从机的时钟源由主机提供,即波特率发生器产生的时钟驱动主机的移位寄存器进行移位,同时,这个时钟也通过SCK引脚输出到从机的移位寄存器。
- 例如,假设主机有一个数据10101010要发送到从机,同时从机有一个数据01010101要发送到主机。当产生一个时钟上升沿时,主机和从机的移位寄存器的数据都会往左移动一位,移出去的最高位数据就会在输出数据寄存器上。MOSI的数据为1,高电平,MISO的数据是0,低电平。这就是第一个时钟上升沿执行的结果。接下来的下降沿,主机和从机会对数据进行采样输入,将上升沿时放到MOSI和MISO上的数据分别采样输入到从机的最低位和主机的最低位。当执行8个时钟周期后,就实现了主机和从机一个字节的数据交换。
- 在SPI通信中,需要注意避免多个从机同时输出数据到主机,以免造成冲突。解决办法是在SS未被选中的状态,从机的MISO引脚必须关断输出,即配置输出为高阻状态。
2. **硬件电路**
- 在SPI通信的硬件电路中,所有SPI设备的SCK、MOSI、MISO分别连在一起。主机另外引出多条SS控制线,分别接到各从机的SS引脚。
- 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入。这种连接方式确保了主机能够准确地控制与各个从机的通信,并且能够有效地避免数据传输过程中的冲突。
3. **移位示意图**
- 主机和从机中的移位寄存器都有时钟输入端,SPI采用高位先行的方式。波特率发生器产生的时钟驱动主机的移位寄存器进行移位,同时,这个时钟也通过SCK引脚输出到从机的移位寄存器。
- 工作原理是:波特率发生器时钟的上升沿,所有移位寄存器(主机和从机)向左移动一位,移出去的位放到引脚上。波特率发生器时钟的下降沿,引脚上的位采样输入到移位寄存器的最低位。这里的移入数据即采样数据。
- 例如,假设主机有一个数据10101010要发送到从机,同时从机有一个数据01010101要发送到主机。当产生一个时钟上升沿时,主机和从机的移位寄存器的数据都会往左移动一位,移出去的最高位数据就会在输出数据寄存器上。MOSI的数据为1,高电平,MISO的数据是0,低电平。这就是第一个时钟上升沿执行的结果。接下来的下降沿,主机和从机会对数据进行采样输入,将上升沿时放到MOSI和MISO上的数据分别采样输入到从机的最低位和主机的最低位。当执行8个时钟周期后,就实现了主机和从机一个字节的数据交换。
- 当只要发送或接收数据其中之一时,也是完成一个字节交换的基础上,读取需要的数据或接收需要的数据即可。
4. **SPI时序基本单元**
- **开始通信和结束通信**:起始条件是SS从高电平切换到低电平,终止条件是SS从低电平切换到高电平。在SPI通信时,SS信号始终要保持低电平。
- **模式0**:这是使用最为广泛的一种模式。交换一个字节(模式0)时,CPOL=0,表示空闲状态时,SCK为低电平;CPHA=0,表示SCK第一个边沿移入数据,第二个边沿移出数据。模式0相比于模式1,数据移出移入的时机提前半个时钟,即相位提前了。工作过程是:SS下降沿时,触发MOSI和MISO输出数据,到SCK上升沿时,开始采样输入数据,下一个SCK下降沿,输出数据B6,下一个上升沿采样输入数据B6。
- **模式1**:交换一个字节(模式1)时,CPOL=0,表示空闲状态时,SCK为低电平;CPHA=1,表示SCK第一个边沿移出数据,第二个边沿移入数据。工作过程是:SCK第一个边沿上升沿,主机和从机同时移出数据,主机通过MOSI移出最高位,此时MOSI的电平就表示了主机要发送数据的B7,从机通过MISO移出最高位,此时MISO表示从机要发送数据的B7。然后时钟运行,产生下降沿,此时主机和从机同时移入数据,也就是进行数据采样。主机移出的B7,进入从机移位寄存器的最低位,从机移出的B7,进入主机移位寄存器的最低位。如此一个时钟脉冲产生完毕,一个数据位传输完毕。在SS的上升沿,MOSI还可以再变化一次,将MOSI置到一个默认的高电平或低电平。SPI没有规定MOSI的默认电平,但MISO,从机必须得置回高阻态。此时如果主机的MISO为上拉输入的话,那MISO引脚的电平就是默认的高电平,如果主机MISO为浮空输入,那MISO引脚的电平不确定。若交换多个字节,则重复以上操作。
- **模式2**:交换一个字节(模式2)时,CPOL=1,表示空闲状态时,SCK为高电平;CPHA=0,表示SCK第一个边沿移入数据,第二个边沿移出数据。
- **模式3**:交换一个字节(模式3)时,CPOL=1,表示空闲状态时,SCK为高电平;CPHA=1,表示SCK第一个边沿移出数据,第二个边沿移入数据。
5. **SPI时序**
- **写使能**:发送指令,向SS指定的设备,发送指令(0x06)。
- **指定地址写**:向SS指定的设备,发送写指令(0x02),随后在指定地址(Address[23:0])下,写入指定数据(Data)。
- **指定地址读**:向SS指定的设备,发送读指令(0x03),随后在指定地址(Address[23:0])下,读取从机数据(Data)。
二、W25Q64模块介绍
1. **W25Q64简介**
- W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景。其存储介质为Nor Flash(闪存),具有较高的读写速度和可靠性。
- 该系列存储器支持多种时钟频率,包括80MHz的标准SPI模式、160MHz的双重SPI模式(Dual SPI)和320MHz的四重SPI模式(Quad SPI)。这些不同的模式使得W25Q64能够在不同的应用场景中提供高效的性能。
- 存储容量方面,W25Qxx系列提供了多种选择,从4Mbit到256Mbit不等。具体来说:
- W25Q40:4Mbit / 512KByte
- W25Q80:8Mbit / 1MByte
- W25Q16:16Mbit / 2MByte
- W25Q32:32Mbit / 4MByte
- W25Q64:64Mbit / 8MByte
- W25Q128:128Mbit / 16MByte
- W25Q256:256Mbit / 32MByte
- 以W25Q64为例,其64Mbit的存储容量能够满足大多数嵌入式系统和物联网设备对存储空间的需求,既可以存储大量的数据,也可以存放复杂的固件程序。
2. **硬件电路**
- 在普通SPI模式下,W25Q64的引脚连接相对简单,主要使用SCK、MOSI、MISO和SS四根通信线。然而,为了提高数据传输效率,W25Q64还支持双重SPI模式和四重SPI模式。
- 在双重SPI模式下,DI和DO引脚分别变成IO0和IO1,数据可以通过这两个引脚同时发送和接收,从而实现2个数据位的同时传输。这种模式下,数据传输速率相比普通SPI模式可以提高一倍。
- 四重SPI模式则进一步提升了数据传输效率。在这种模式下,DI、DO引脚变成IO0和IO1,同时WP和HOLD引脚也作为IO2和IO3使用。这样,一个时钟周期内可以传输4个数据位,大大提高了数据传输速度。这种模式特别适用于对数据传输速率要求较高的应用场景,如高清图像数据存储、高速数据采集等。
3. **W25Q64框图**
- W25Q64的内部存储空间划分为多个层次,包括block(64KB)、sector(4KB)和page(256字节)。一个block包含16个sector,一个sector包含16个page。这种层次化的存储结构使得数据的读写操作更加灵活和高效。
- 每个字节在存储器中都有唯一的地址,通过SPI通信协议,主控芯片可以精确地指定要操作的地址。W25Q64内部的SPI Command & Control Logic(SPI控制逻辑)负责对芯片内部进行地址锁存、数据读写等操作。控制逻辑会自动完成这些操作,主控芯片只需通过SPI协议发送指令和数据即可。
- Status Register(状态寄存器)用于体现芯片的当前状态,包括是否处于忙状态、是否写使能、是否写保护等。通过读取状态寄存器,主控芯片可以了解W25Q64的运行状态,从而进行相应的操作。
- 地址锁存计数器用于指定地址。通过SPI发送3个字节的地址,其中前2个字节进入页地址锁存计数器,最后一个字节进入字节地址锁存计数器。页地址通过写保护和行解码选择需要操作的页,字节地址通过列解码和256字节页缓存进行指定字节的读写操作。地址锁存计数器具有计数功能,读写操作后地址指针可以自动加1,方便连续读写操作。
- 页缓存区是一个256字节的RAM存储器,用于暂存写入的数据。写入操作时,数据首先存储在页缓存区,时序结束后,芯片将缓存区的数据复制到对应的Flash中进行永久保存。由于Flash的写入速度相对较慢,而页缓存区是RAM,速度与SPI相同,因此页缓存区的存在可以有效提高写入效率。需要注意的是,页缓存区的大小为256字节,因此连续写入的数据量不能超过256字节。写完后,芯片会进入忙状态,此时状态寄存器的BUSY位会被置1,芯片在忙状态时不会响应新的读写操作。
4. **Flash操作注意事项**
- **写入操作**:
- 写入操作前,必须先进行写使能。这是为了确保芯片处于可写状态,避免误操作导致数据损坏。
- 每个数据位只能由1改写为0,不能由0改写为1。这是Flash存储器的一个特性,因此在写入数据时需要特别注意数据的逻辑状态。
- 写入数据前必须先擦除,擦除后,所有数据位变为1。擦除操作是Flash存储器写入数据的前提,通过擦除操作可以将存储单元恢复到初始状态。
- 擦除必须按最小擦除单元进行。W25Q64的最小擦除单元是sector(4KB),因此在进行擦除操作时,需要按照sector的边界进行操作。
- 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入。这是由于页缓存区的大小限制,写入数据时需要注意不要超出页的范围。
- 写入操作结束后,芯片进入忙状态,不响应新的读写操作。此时可以通过读取状态寄存器的BUSY位来判断芯片是否完成写入操作。
- **读取操作**:
- 直接调用读取时序,无需使能,无需额外操作。读取操作相对简单,只需按照SPI协议发送读取指令和地址即可。
- 没有页的限制,读取操作结束后不会进入忙状态。这意味着可以连续读取多个页的数据,而不会受到忙状态的限制。
- 但不能在忙状态时读取。在芯片处于忙状态时,正在进行写入操作或其他操作,此时读取数据可能会导致数据错误或操作冲突。
#### 三、软件SPI读写W25Q64代码
#include <Arduino.h>
// 定义SPI引脚
#define SCK_PIN 13
#define MOSI_PIN 11
#define MISO_PIN 12
#define SS_PIN 10
// 初始化SPI引脚
void setupSPI() {
pinMode(SCK_PIN, OUTPUT);
pinMode(MOSI_PIN, OUTPUT);
pinMode(MISO_PIN, INPUT);
pinMode(SS_PIN, OUTPUT);
// 初始化引脚状态
digitalWrite(SCK_PIN, LOW);
digitalWrite(MOSI_PIN, LOW);
digitalWrite(SS_PIN, HIGH);
}
// 产生一个时钟脉冲
void spiClockPulse() {
digitalWrite(SCK_PIN, HIGH);
delayMicroseconds(1);
digitalWrite(SCK_PIN, LOW);
delayMicroseconds(1);
}
// 发送一个字节
void spiSendByte(uint8_t data) {
for (int i = 0; i < 8; i++) {
digitalWrite(MOSI_PIN, (data & 0x80) ? HIGH : LOW);
data <<= 1;
spiClockPulse();
}
}
// 接收一个字节
uint8_t spiReceiveByte() {
uint8_t data = 0;
for (int i = 0; i < 8; i++) {
data <<= 1;
spiClockPulse();
if (digitalRead(MISO_PIN)) {
data |= 0x01;
}
}
return data;
}
// 写使能
void writeEnable() {
digitalWrite(SS_PIN, LOW);
spiSendByte(0x06); // 写使能指令
digitalWrite(SS_PIN, HIGH);
}
// 写入数据
void writeData(uint32_t address, uint8_t* data, uint32_t length) {
writeEnable();
digitalWrite(SS_PIN, LOW);
spiSendByte(0x02); // 写指令
spiSendByte((address >> 16) & 0xFF); // 发送地址高位
spiSendByte((address >> 8) & 0xFF); // 发送地址中位
spiSendByte(address & 0xFF); // 发送地址低位
for (uint32_t i = 0; i < length; i++) {
spiSendByte(data[i]); // 发送数据
}
digitalWrite(SS_PIN, HIGH);
}
// 读取数据
void readData(uint32_t address, uint8_t* data, uint32_t length) {
digitalWrite(SS_PIN, LOW);
spiSendByte(0x03); // 读指令
spiSendByte((address >> 16) & 0xFF); // 发送地址高位
spiSendByte((address >> 8) & 0xFF); // 发送地址中位
spiSendByte(address & 0xFF); // 发送地址低位
for (uint32_t i = 0; i < length; i++) {
data[i] = spiReceiveByte(); // 接收数据
}
digitalWrite(SS_PIN, HIGH);
}
// 主函数
void setup() {
setupSPI();
// 示例:写入数据
uint8_t writeBuffer[4] = {0x12, 0x34, 0x56, 0x78};
writeData(0x000000, writeBuffer, 4);
// 示例:读取数据
uint8_t readBuffer[4];
readData(0x000000, readBuffer, 4);
// 打印读取的数据
for (int i = 0; i < 4; i++) {
Serial.print(readBuffer[i], HEX);
Serial.print(" ");
}
Serial.println();
}
void loop() {
// 主循环为空
}
### 核心观点
硬件SPI利用STM32内部集成的硬件电路自动执行时钟生成和数据收发,减轻CPU负担。它支持多种配置,包括8位/16位数据帧、高位/低位先行、多种时钟频率等,并兼容I2S协议。STM32F103C8T6具有SPI1和SPI2资源,分别挂载到APB2和APB1总线。硬件SPI支持全双工和非连续传输,通过接收缓冲区(RDR)和发送缓冲区(TDR)进行数据交换。硬件SPI读写W25Q64代码简洁,与软件SPI相比,主要修改了引脚配置和数据交换方式。
---
二、硬件SPI
硬件SPI是一种利用微控制器内部集成的专用硬件电路来实现SPI通信的方式。与软件SPI相比,硬件SPI能够自动执行时钟生成、数据收发等功能,从而显著减轻CPU的负担,提高通信效率。以下是硬件SPI的详细介绍:
1. **SPI外设简介**
- **硬件SPI的优势**:STM32微控制器内部集成了硬件SPI收发电路,这种硬件电路可以自动执行时钟生成和数据收发等操作,从而释放CPU资源,使其能够处理其他任务。这种自动化的处理方式不仅提高了通信的效率,还减少了软件编程的复杂性。
- **可配置性**:硬件SPI提供了高度的可配置性,支持8位和16位数据帧格式,以及高位先行和低位先行两种数据传输顺序。这使得硬件SPI能够适应不同的应用需求和设备要求。
- **时钟频率配置**:硬件SPI的时钟频率可以通过配置分频因子来调整,分频因子的范围是2到256。这意味着SPI时钟频率可以是系统时钟频率(PCLK)的1/2到1/256。例如,如果系统时钟频率为72MHz,那么SPI时钟频率可以在36MHz到288kHz之间选择。这种灵活的时钟配置使得硬件SPI能够适应不同的通信速度要求。
- **多主机模型支持**:硬件SPI支持多主机模型,这意味着多个主机可以共享SPI总线,通过适当的仲裁机制来避免冲突。此外,硬件SPI既可以工作在主模式下,也可以工作在从模式下,这使得它能够灵活地与其他SPI设备进行通信。
- **通信模式精简**:硬件SPI可以根据需要精简为半双工或单工通信模式。在某些应用场景中,可能只需要单向数据传输或半双工通信,硬件SPI的这种灵活性使得它能够适应这些特殊需求。
- **DMA支持**:硬件SPI支持直接内存访问(DMA),这使得数据传输可以直接在内存和SPI外设之间进行,而无需CPU的干预。这种支持大大提高了数据传输的效率,尤其适用于需要大量数据传输的应用场景。
- **I2S协议兼容**:硬件SPI还兼容I2S协议,这是一种用于音频数据传输的协议。这种兼容性使得硬件SPI能够用于音频处理等应用,进一步扩展了其应用范围。
- **STM32F103C8T6硬件SPI资源**:STM32F103C8T6微控制器提供了SPI1和SPI2两个硬件SPI资源。SPI1挂载到APB2总线,其时钟频率PCLK为72MHz;SPI2挂载到APB1总线,其时钟频率PCLK为36MHz。这种配置使得STM32F103C8T6能够同时支持两个SPI设备,或者在不同的总线频率下进行通信。
2. **SPI框图**
- **接收缓冲区和发送缓冲区**:硬件SPI的框图中包括接收缓冲区(RDR)和发送缓冲区(TDR)。接收缓冲区用于存储从SPI总线上接收到的数据,而发送缓冲区用于存储待发送到SPI总线上的数据。这两个缓冲区的存在使得数据的接收和发送能够高效地进行,避免了数据丢失和冲突。
- **数据寄存器**:SPI数据寄存器(SPI_DR)是硬件SPI中的一个重要组成部分。它用于存储发送和接收的数据。在发送数据时,数据被写入SPI_DR,然后由硬件SPI自动发送到SPI总线上;在接收数据时,从SPI总线上接收到的数据被自动存储到SPI_DR中,供CPU读取。
3. **SPI基本结构**
- **主模式和从模式**:硬件SPI的基本结构支持主模式和从模式。在主模式下,硬件SPI负责生成时钟信号(SCK),并通过MOSI线发送数据,同时通过MISO线接收数据。在从模式下,硬件SPI接收来自主设备的时钟信号,并通过MISO线发送数据,同时通过MOSI线接收数据。
- **数据帧格式**:硬件SPI支持多种数据帧格式,包括8位和16位数据帧。数据帧的格式可以通过配置寄存器来设置。此外,硬件SPI还支持高位先行和低位先行两种数据传输顺序,这使得它能够与不同的设备进行通信。
- **时钟极性和相位配置**:硬件SPI的时钟极性(CPOL)和时钟相位(CPHA)可以通过配置寄存器来设置。这使得硬件SPI能够支持四种不同的SPI工作模式,从而与各种SPI设备进行兼容。例如,模式0(CPOL=0,CPHA=0)是最常用的模式,它在时钟的上升沿移入数据,在下降沿移出数据。
4. **主模式全双工连续传输(少用)**
- **全双工传输示例**:在主模式下,硬件SPI可以进行全双工连续传输。例如,使用SPI模式3时,SCK默认为高电平。在第一个下降沿,MOSI和MISO线移出数据;之后,在上升沿移入数据。这种传输方式适用于需要连续数据交换的场景,但实际应用中较少使用。
- **数据传输过程**:在全双工传输过程中,数据位依次出现在MOSI和MISO线上。例如,如果发送的数据是60(二进制00111100),那么在MOSI线上依次出现的数据位是0、0、1、1、1、1、0、0。同时,从MISO线上接收的数据也会被依次存储到接收缓冲区中。发送寄存器空标志位(TXE)和接收数据寄存器非空标志位(RXNE)用于指示发送和接收缓冲区的状态。发送缓冲器用于存储待发送的数据,而接收缓冲器用于存储接收到的数据。
- **硬件状态管理**:硬件SPI会自动管理状态标志位,如BUSY位。当有数据传输时,BUSY位被置1,表示SPI外设正在忙。这种自动化的状态管理使得硬件SPI能够高效地进行数据传输,而无需CPU频繁干预。
5. **非连续传输(常用)**
- **非连续传输特点**:非连续传输是硬件SPI中常用的一种传输方式。在这种方式下,数据传输不是连续进行的,而是根据需要进行分段传输。这种方式适用于需要间歇性数据交换的场景,例如在数据采集和处理过程中。
- **引脚配置**:在非连续传输中,引脚配置可以根据需要进行调整。例如,可以将GPIO口配置为复用开漏输出或复用推挽输出。这种配置方式使得硬件SPI能够灵活地与其他设备进行通信,而无需额外的硬件电路。
#### 二、硬件SPI读写W25Q64代码
硬件SPI读写W25Q64的代码实现相对简单,主要利用了STM32硬件SPI的自动数据收发功能。与软件SPI相比,硬件SPI的代码主要修改了引脚配置和数据交换方式。以下是硬件SPI读写W25Q64的代码示例:
#include "stm32f10x.h"
// 初始化SPI外设
void SPI_Init(void) {
SPI_InitTypeDef SPI_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
// 使能SPI和GPIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
// 配置SPI引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置SPI
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure);
// 使能SPI
SPI_Cmd(SPI1, ENABLE);
}
// 写使能
void Write_Enable(void) {
SPI1->DR = 0x06; // 写使能指令
while (!(SPI1->SR & SPI_SR_TXE)); // 等待发送缓冲区空
while (SPI1->SR & SPI_SR_BSY); // 等待忙标志位清零
}
// 写入数据
void Write_Data(uint32_t address, uint8_t* data, uint32_t length) {
Write_Enable();
SPI1->DR = 0x02; // 写指令
while (!(SPI1->SR & SPI_SR_TXE));
while (SPI1->SR & SPI_SR_BSY);
SPI1->DR = (address >> 16) & 0xFF; // 发送地址高位
while (!(SPI1->SR & SPI_SR_TXE));
while (SPI1->SR & SPI_SR_BSY);
SPI1->DR = (address >> 8) & 0xFF; // 发送地址中位
while (!(SPI1->SR & SPI_SR_TXE));
while (SPI1->SR & SPI_SR_BSY);
SPI1->DR = address & 0xFF; // 发送地址低位
while (!(SPI1->SR & SPI_SR_TXE));
while (SPI1->SR & SPI_SR_BSY);
for (uint32_t i = 0; i < length; i++) {
SPI1->DR = data[i]; // 发送数据
while (!(SPI1->SR & SPI_SR_TXE));
while (SPI1->SR & SPI_SR_BSY);
}
}
// 读取数据
void Read_Data(uint32_t address, uint8_t* data, uint32_t length) {
SPI1->DR = 0x03; // 读指令
while (!(SPI1->SR & SPI_SR_TXE));
while (SPI1->SR & SPI_SR_BSY);
SPI1->DR = (address >> 16) & 0xFF; // 发送地址高位
while (!(SPI1->SR & SPI_SR_TXE));
while (SPI1->SR & SPI_SR_BSY);
SPI1->DR = (address >> 8) & 0xFF; // 发送地址中位
while (!(SPI1->SR & SPI_SR_TXE));
while (SPI1->SR & SPI_SR_BSY);
SPI1->DR = address & 0xFF; // 发送地址低位
while (!(SPI1->SR & SPI_SR_TXE));
while (SPI1->SR & SPI_SR_BSY);
for (uint32_t i = 0; i < length; i++) {
SPI1->DR = 0xFF; // 发送虚拟数据以触发读操作
while (!(SPI1->SR & SPI_SR_TXE));
while (SPI1->SR & SPI_SR_BSY);
data[i] = SPI1->DR; // 读取数据
}
}