STM32软件spi和硬件spi
核心观点
本文主要介绍了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);// 配置SPISPI_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);// 使能SPISPI_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; // 读取数据}
}
相关文章:

STM32软件spi和硬件spi
核心观点 本文主要介绍了SPI通信的两种实现方式:软件SPI和硬件SPI。详细阐述了SPI通信协议的基本概念、硬件电路连接方式、移位示意图、时序基本单元以及四种工作模式。同时,对W25Q64模块进行了详细介绍,包括其硬件电路、框图以及操作注意事…...
MATLAB实战:人脸检测与识别实现方案
我们要用电脑识别照片或视频中的人脸,并知道是谁的脸。就像手机相册能自动识别照片里的人是谁一样。 🔍 人脸检测(找脸) 目标:在图片中找到人脸的位置 怎么做: 用MATLAB的"人脸扫描仪"ÿ…...

深度刨析树结构(从入门到入土讲解AVL树及红黑树的奥秘)
目录 树的表示 二叉树的概念及结构(重点学习) 概念 : 特点: 树与非树 特殊的二叉树 二叉树的性质(重点) 二叉树的存储结构 堆的概念及结构 建堆方式: 向下调整算法 向上调整算法 建堆第一步初始化 建…...

【Linux】shell的条件判断
目录 一.使用逻辑运算符判定命令执行结果 二.条件判断方法 三.判断表达式 3.1文件判断表达式 3.2字符串测试表达式 3.3整数测试表达式 3.4逻辑操作符 一.使用逻辑运算符判定命令执行结果 && 在命令执行后如果没有任何报错时会执行符号后面的动作|| 在命令执行后…...

第九天:java注解
注解 1 什么是注解(Annotation) public class Test01 extends Object{//Override重写的注解Overridepublic String toString() {return "Test01{}";} }2 内置注解 2.1 Override Override重写的注解 Override public String toString() {ret…...

十一、【核心功能篇】测试用例管理:设计用例新增编辑界面
【核心功能篇】测试用例管理:设计用例新增&编辑界面 前言准备工作第一步:创建测试用例相关的 API 服务 (src/api/testcase.ts)第二步:创建测试用例编辑页面组件 (src/views/testcase/TestCaseEditView.vue)第三步:配置测试用例…...
react-native的token认证流程
在 React Native 中实现 Token 认证是移动应用开发中的常见需求,它用于验证用户的身份并授权其访问受保护的 API 资源。 Token 认证的核心流程: 用户登录 (Login): 用户在前端输入用户名和密码。前端将这些凭据发送到后端 API。后端验证凭据。如果验证成…...
ERP系统中商品定价功能设计:支持渠道、会员与批发场景的灵活定价机制
在现代零售、批发与电商环境下,商品的定价策略日益复杂。一个优秀的ERP系统不仅需要管理商品基础信息、库存与订单,还必须提供一套灵活且可扩展的商品定价机制,以满足: 不同销售渠道(如线上平台、线下门店、分销商&…...

Spring是如何实现属性占位符解析
Spring属性占位符解析 核心实现思路1️⃣ 定义占位符处理器类2️⃣ 处理 BeanDefinition 中的属性3️⃣ 替换具体的占位符4️⃣ 加载配置文件5️⃣ Getter / Setter 方法 源码见:mini-spring 在使用 Spring 框架开发过程中,为了实现配置的灵活性…...
数据结构之ArrayList
系列文章目录 目录 系列文章目录 前言 一、数据结构的前置语法 1. 时空复杂度 2. 包装类 3. 泛型 二、ArrayList 和顺序表 1. 顺序表的模拟实现 2. 源码 3. ArrayList 的优缺点 前言 本文介绍数据结构的前置算法,以及 ArrayList 的模拟实现,部…...

DDR4读写压力测试
1.1测试环境 1.1.1整体环境介绍 板卡: pcie-403板卡 主控芯片: Xilinx xcvu13p-fhgb2104-2 调试软件: Vivado 2018.3 代码环境: Vscode utf-8 测试工程: pcie403_user_top 1.1.2硬件介绍 UD PCIe-403…...
uniapp 开发企业微信小程序时,如何在当前页面真正销毁前或者关闭小程序前调用一个api接口
在 UniApp 开发企业微信小程序时,若需在页面销毁或小程序关闭前调用 API 接口,需结合页面生命周期和应用生命周期实现。以下是具体实现方案及注意事项: 一、在页面销毁前调用 API(页面级) 通过页面生命周期钩子 onUnl…...
WPF 按钮点击音效实现
WPF 按钮点击音效实现 下面我将为您提供一个完整的 WPF 按钮点击音效实现方案,包含多种实现方式和高级功能: 完整实现方案 MainWindow.xaml <Window x:Class"ButtonClickSound.MainWindow"xmlns"http://schemas.microsoft.com/win…...

编写测试用例
测试用例(Test Case)是用于测试系统的要素集合 目录 编写测试用例作用 编写测试用例要包含七大元素 测试用例的设计方法 1、等价类法 2、边界值法 3、正交表法 4、判定表法 5、错误推测法 6、场景法 编写测试用例作用 1、确保功能全面覆盖…...
解释程序(Python)不需要生成机器码 逐行解析 逐行执行
在计算机组成原理中,解释程序(Interpreter)通常不会生成独立的机器码,但具体情况取决于解释器的实现方式。以下是详细分析: 1. 传统解释程序:不生成机器码 直接逐行执行: 经典的解释器ÿ…...

每日Prompt:隐形人
提示词 黑色棒球帽,白色抹胸、粉色低腰短裙、白色襪子,黑色鞋子,粉紅色背包,衣服悬浮在空中呈现动态姿势,虚幻引擎渲染风格,高清晰游戏CG质感,户外山林背景,画面聚焦在漂浮的衣服上…...

TensorFlow深度学习实战(19)——受限玻尔兹曼机
TensorFlow深度学习实战(19)——受限玻尔兹曼机 0. 前言1. 受限玻尔兹曼机1.1 受限玻尔兹曼机架构1.2 受限玻尔兹曼机的数学原理 2. 使用受限玻尔兹曼机重建图像3. 深度信念网络小结系列链接 0. 前言 受限玻尔兹曼机 (Restricted Boltzmann Machine, RB…...

告别手动绘图!基于AI的Smart Mermaid自动可视化图表工具搭建与使用指南
以下是对Smart Mermaid的简单介绍: 一款基于 AI 技术的 Web 应用程序,可将文本内容智能转换为 Mermaid 格式的代码,并将其渲染成可视化图表可以智能制作流程图、序列图、甘特图、状态图等等,并且支持在线调整、图片导出可以Docke…...

【Oracle】安装单实例
个人主页:Guiat 归属专栏:Oracle 文章目录 1. 安装前的准备工作1.1 硬件和系统要求1.2 检查系统环境1.3 下载Oracle软件 2. 系统配置2.1 创建Oracle用户和组2.2 配置内核参数2.3 配置用户资源限制2.4 安装必要的软件包 3. 目录结构和环境变量3.1 创建Ora…...
C++测开,自动化测试,业务(第一段实习)
目录 🌼前言 一,实习经历怎么写简历 🌹业务理解 🎂结构化表达 二,实习 🦂技术和流程卡点 🔑实习收获 / 代码风格 三,测试理论,用例设计,工具链 &…...

QT中更新或添加组件时出现“”qt操作至少需要一个处于启用状态的有效资料档案库“解决方法”
在MaintenanceTool.exe中点击下一步 第一个: 第二个: 第三个: 以上任意一个放入资料库中...

论文速读《UAV-Flow Colosseo: 自然语言控制无人机系统》
论文链接:https://arxiv.org/abs/2505.15725项目主页:https://prince687028.github.io/UAV-Flow/ 0. 简介 近年来,无人机技术蓬勃发展,但如何让无人机像智能助手一样理解并执行人类语言指令,仍是一个前沿挑战。现有研…...

ES6+中Promise 中错误捕捉详解——链式调用catch()或者async/await+try/catch
通过 unhandledrejection 捕捉未处理的 Promise 异常,手动将其抛出,最终让 window.onerror 捕捉,从而统一所有异常的处理逻辑 规范代码:catch(onRejected)、async...awaittry...catch 在 JavaScript 的 Pro…...
CDN安全加速:HTTPS加密最佳配置方案
CDN安全加速的HTTPS加密最佳配置方案需从证书管理、协议优化、安全策略到性能调优进行全链路设计,以下是核心实施步骤与注意事项: 一、证书配置与管理 证书选择与格式 证书类型:优先使用受信任CA机构颁发的DV/OV/EV证…...

解常微分方程组
Euler法 function euler_method % 参数设置 v_missile 450; % 导弹速度 km/h v_enemy 90; % 敌艇速度 km/h % 初始条件 x0 0; % 导弹初始位置 x y0 0; % 导弹初始位置 y xe0 120; % 敌艇初始位置 y t0 0; % 初始时间 % 时间步长和总时间 dt 0.01; % 时间步长 t_final …...

C++实现汉诺塔游戏自动完成
目录 一、汉诺塔的规则二、数学递归推导式三、步骤实现(一)汉诺塔模型(二)递归实现(三)显示1.命令行显示2.SDL图形显示 四、处理用户输入及SDL环境配置五、总结六、源码下载 一、汉诺塔的规则 游戏由3根柱子和若干大小不一的圆盘组成,初始状态下,所有的…...
在 ABP VNext 中集成 Serilog:打造可观测、结构化日志系统
🚀 在 ABP VNext 中集成 Serilog:打造可观测、结构化日志系统 📚 目录 🚀 在 ABP VNext 中集成 Serilog:打造可观测、结构化日志系统1. 为什么要使用结构化日志? 🤔2. 核心集成步骤 Ὦ…...

pikachu靶场通关笔记07 XSS关卡03-存储型XSS
目录 一、XSS 二、存储型XSS 三、源码分析 四、渗透实战 1、输入mooyuan试一试 2、注入Payload 3、查看数据库 4、再次进入留言板页面 本系列为通过《pikachu靶场通关笔记》的XSS关卡(共10关)渗透集合,通过对XSS关卡源码的代码审计找到XSS风险的…...
GitLab CI、GitHub Actions和Jenkins进行比较
特性/工具JenkinsGitLab CIGitHub Actions架构设计哲学Master/Agent分布式架构,通过插件扩展功能代码与CI/CD强耦合,内置Git仓库,基于Runner注册机制事件驱动,与GitHub深度集成,基于虚拟机的Job执行单元核心运行机制支…...
strcat及其模拟实现
#define _CRT_SECURE_NO_WARNINGS strcat 追加字符串 str "string"(字符串) cat "concatenate"(连接 / 追加) char* strcat(char* destination, const char* source); strcat的应用 方法一ÿ…...