stm32学习笔记-11 SPI通信
11 SPI通信
文章目录
- 11 SPI通信
- 11.1 SPI通信协议
- 11.2 W25Q64简介
- 11.3 实验:软件SPI读写W25Q64
- 11.4 SPI通信外设
- 11.5 实验:硬件SPI读写W25Q64
注:笔记主要参考B站 江科大自化协 教学视频“ STM32入门教程-2023持续更新中”。
注:工程及代码文件放在了本人的 Github仓库。
11.1 SPI通信协议

SPI(Serial Peripheral Interface,串行外设接口)是由Motorola公司开发的一种通用数据总线,与IIC差不多,也是为了实现主控芯片和各种外挂芯片之间的数据交流。SPI和IIC都是常用的接口协议,只是根据其不同的特点,应用场景有所不同。
- 四根通信线(SPI官方文档名称):
- SCK:Serial Clock,串行时钟线。别称SCLK、CLK、CK。作用是提供时钟信号,数据位的输入和输出都是在时钟上升沿、下降沿进行的。
- MOSI:Master Output Slave Input,主机输出从机输入。别称DO(Data Output)。
- MISO:Master Input Slave Output,主机输入从机输出。别称DI(Data Input)。
- SS:Slave Select,从机选择。别称NSS(Not Slave Select)、CS(Chip Select)。SPI协议可以为每个从机都开辟一条SS线,专门用于控制该从机的选择(低电平有效)(SPI壕无人性)。
- 同步(时钟线快点慢点无所谓),全双工,高位先行。
- 支持总线挂载多设备,仅支持“一主多从”,不支持“多主机”。
IIC和SPI各有优缺点。IIC通过各种软硬件设置,使用最少的硬件资源(2根通信线)实现了最多的功能(双向通信、应答位等),相当于一个“精打细算、思维灵活”的协议;但是为了实现这些功能,其硬件采用开漏输出+上拉电阻的模式以防止电源短路,导致其高电平驱动能力不足,同时也导致其上升沿时间很长,这限制了其最高通信速率(IIC标准模式100kHz/快速模式400kHz)。相对的,SPI协议并没有规定最大传输速度,而是取决于外挂芯片厂商的设计需求,比如W25Q64芯片手册说明其SPI速率最高可达80MHz(甚至比stm32主频还要高)!但是,SPI的设计简单粗暴(学习起来更加简单),功能也不如IIC多,并且会消耗4根通信引脚,所以SPI相当于出手阔绰的土豪(我不在乎占了几根通信线,我只在乎我的任务有没有最简单、最快速的完成)。
注:IIC通过改进电路的方式,设计出高速模式3.4MHz,但目前并不普及。一般仍认为最高速率400kHz。
下面来介绍SPI的硬件规定:

- SPI主机:主机一般是控制器,如stm32。
- SPI从机:从机一般是存储器、显示屏、通信模块、传感器等。
- 时钟线和数据线:所有SPI设备的SCK、MOSI、MISO分别连在一起,即相同名称的管脚连接在一起。
- 片选线:主机另外引出多条SS控制线,分别接到各从机的SS引脚。注意主机在同一时间只能选择一个从机进行通信(低电平有效),否则会造成数据冲突。
注:SPI输出引脚配置为推挽输出(驱动能力强),输入引脚配置为浮空或上拉输入。对于从机来说,只有当其被选中时,MISO才配置为推挽输出,否则为高阻态。
下面来介绍SPI基本收发时序:
SPI通信的基础是交换字节。也就是说,每次SPI通信的过程中,通过各自的MOSI、MISO线,主机和从机的寄存器会形成一个循环移位操作,每个比特的通信都是转圈的循环移位,8个时钟周期完整的交换一个字节。那么根据需求有选择的忽略交换过来的数据,就可以实现(以主机举例,从机同理)主机只发送、主机只接收、主从机交换数据这三类操作。

工作原理:
- 波特率发生器上升沿:所有寄存器左移一位。
- 波特率发生器下降沿:将采样输入的数据放到寄存器的最低位。
- 重复8个时钟周期,便可以实现主机和从机的数据交换。
注:实际上,何时移位、何时采样、时钟极性都是可以设置的,下面将介绍。
功能介绍:显然存在资源浪费现象。
- 同时进行发送和接收:正常的交换字节。
- 只想发送、不想接收:不看接收过来的数据。
- 只想接收、不想发送:随便发一个数据,比如0x00/0xFF。
下面介绍SPI交换单个字节的时序:
- 起始条件和终止条件:起始条件是SS从高电平切换到低电平,终止条件是SS从低电平切换到高电平。
- 交换一个字节:两个配置位分别为CPOL(Clock Polarity, 时钟极性)规定空闲状态的时钟高低电平、CPHA(Clock Phase, 时钟相位)规定数据移入(数据采样)、移出的时机。
- 【模式0】[CPOL,CPHA] = [0,0],SCK低电平为空闲状态;SCK第一个边沿(上升沿)移入数据,第二个边沿(下降沿)移出数据。
- 【模式1】[CPOL,CPHA] = [0,1],SCK低电平为空闲状态;SCK第一个边沿移出数据,第二个边沿移入数据。
- 【模式2】[CPOL,CPHA] = [1,0],SCK高电平为空闲状态;SCK第一个边沿移入数据,第二个边沿移出数据。
- 【模式3】[CPOL,CPHA] = [1,1],SCK高电平为空闲状态;SCK第一个边沿移出数据,第二个边沿移入数据。
下面介绍几个SPI的通信实例:
上面仅介绍了最基本的交换字节的时序。实际上,SPI要想与从机完成真正的通信,也有更高维度的数据帧结构:指令码+读写数据。每个SPI从机芯片都规定了指令集,指令集中不同的指令码对应不同的功能。下一小节将详细介绍W25Q64的指令集,本节仅看三个演示(默认【模式0】):
- 发送指令:向SS指定的设备,发送指令(0x06)。

由于上图是软件模拟SPI时序,所以MOSI的数据变化(那个上升沿)没有紧贴SCK下降沿,但是在硬件模拟SPI中是紧贴的。
- 指定地址写:向SS指定的设备,发送写指令(0x02),随后在指定地址(Address[23:0])下,写入指定数据(Data)。

主机:发指令 0 x 02 + 发 A d d r e s s [ 23 : 16 ] + 发 A d d r e s s [ 15 : 8 ] + 发 A d d r e s s [ 7 : 0 ] + 发 D a t a 主机:发指令0x02+发Address[23:16]+发Address[15:8]+发Address[7:0]+发Data 主机:发指令0x02+发Address[23:16]+发Address[15:8]+发Address[7:0]+发Data
- 指定地址读:向SS指定的设备,发送读指令(0x03),随后在指定地址(Address[23:0])下,读取从机数据(Data)。

主机:发指令 0 x 03 + 发 A d d r e s s [ 23 : 16 ] + 发 A d d r e s s [ 15 : 8 ] + 发 A d d r e s s [ 7 : 0 ] + 收 D a t a 主机:发指令0x03+发Address[23:16]+发Address[15:8]+发Address[7:0]+收Data 主机:发指令0x03+发Address[23:16]+发Address[15:8]+发Address[7:0]+收Data
首先可以观察到,由于从机SPI协议由硬件控制,所以从机发送过来的数据,其数据变化边沿都是紧贴着时钟下降沿完成的。并且,如果最后接收完一个字节后时钟仍为低电平,那么从机会继续将下一个地址的数据发送过来,就实现了“连续地址读”。
11.2 W25Q64简介
W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储(电脑BIOS固件)等场景。也就是如果程序需要存储大量的数据,就可以考虑外挂这款芯片。
- 存储介质:Nor Flash(闪存)。还有Flash一种是Nand Flash。
- 时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)。两重SPI是将MOSI和MISO同时用于收发;四重SPI是再加上写保护WP、数据保持HOLD进行收发数据,共四根线同时收发数据。
- 存储容量(24位地址):
W25Q40: 4Mbit / 512KByte
W25Q80: 8Mbit / 1MByte
W25Q16: 16Mbit / 2MByte
W25Q32: 32Mbit / 4MByte
W25Q64: 64Mbit / 8MByte(本节所使用)
W25Q128:128Mbit / 16MByte
W25Q256:256Mbit / 32MByte

引脚 | 功能 |
---|---|
VCC、GND | 电源(2.7~3.6V) |
CS(SS) | SPI片选 |
CLK(SCK) | SPI时钟 |
DI(MOSI) | SPI主机输出从机输入 |
DO(MISO) | SPI主机输入从机输出 |
WP | 写保护 |
HOLD | 数据保持,用于SPI总线进入中断 |
上面原理图中可以看出,WP和HOLD两根线都接到了VCC正极,那就表示这两个功能暂时没有用到。

- 芯片层级结构:8MB存储空间–>128个64KB块–>16个4KB扇区–>16个256B页。
- SPI控制逻辑:芯片内部进行地址锁存、数据读写等操作,都可以由控制逻辑自动完成,外部芯片主要关注与控制逻辑进行数据交互即可。
- 状态寄存器:非常重要,指明芯片是否处于忙状态、是否写使能、是否写保护等。
- 写控制逻辑:配合WP引脚实现硬件写保护。
- 高电压生成器:配合Flash进行编程,用于击穿内部晶体管,以实现掉电不丢失数据的特性。
- 页地址锁存/寄存器:锁存3字节地址的高两个字节。通过写保护和行解码,来选择要操作哪一页。
- 字节地址锁存/寄存器:锁存3字节地址的最低一个字节。通过列解码和256字节页缓存,来进行指定特定地址的读写操作。由于配有计数器,所以可以很容易实现从指定地址开始,连续读写多个字节。
- 256字节页缓存区:是一个256字节的RAM存储器,通过这个RAM缓冲区以实现数据读写。写入数据时,为了跟上SPI通信速度,会先将数据放到RAM缓存区里,时序结束后,芯片才会将数据复制到Flash中,所以 单次连续写入数据禁止超过256个字节,并且写时序后芯片会进入忙状态(状态寄存器)。读取数据时由于只需要看Flash电路状态,所以没有限制。
与RAM支持直接读写、覆盖读写不同,为了兼顾掉电不丢失与存储容量大、成本低的特点,Flash会在操作的便捷性上做出一些妥协和让步。于是下面是 Flash操作注意事项:
写入操作时:
- 写入操作前必须先进行写使能,一个写使能只能保证后面一条写操作的执行。这样设置是防止误操作。
- 每个数据位只能由1改写为0,不能由0改写为1。所以写入数据前必须先擦除(发送擦除指令),擦除后所有数据位变为1。
- 擦除必须按照 最小擦除单元(扇区) 进行,可以选择的擦除单元有整个芯片、块、扇区。要想不丢失数据,就需要先将所有的数据读取出来,擦除后再统一写入;或者直接为单个字节数据占用一个扇区,就不需要先读取了。
- 最多连续写入一页的数据(256字节),超过页尾位置的数据,会回到页首覆盖写入。也就是注意地址不要跨越页尾。
- 写入操作结束后,芯片进入忙状态,不响应新的读写操作。可以使用“读状态寄存器指令”,BUSY为0时芯片空闲。
- 写入操作总结:写使能–>(备份数据)–>擦除–>等待BUSY位为0–>写入数据–>等待BUSY位为0–>其他操作。
读取操作时:
- 直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取。

- 更多指令集详细信息可以查看W25Q64芯片手册的“11.2.2 Instruction Set Table 1”。
11.3 实验:软件SPI读写W25Q64
需求:用stm32四个引脚控制高低电平,与W25Q64进行通信。在OLED上显示:
- 第一行:显示MID(Manufacturer)和DID(Device)。MID是厂商ID(0xEF),DID是设备ID(0x4017)。
- 第二行:显示写入的四个数据。
- 第三行:显示读出的四个数据。

实际上使用软件模拟SPI,是可以任意选择端口的。但是为了后续硬件SPI实验不用再拆线,所以这里选择和stm32上SPI外设引脚。

下面是代码展示:
- main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "W25Q64.h"uint8_t ArrayWrite[4] = {0x11,0x22,0x33,0x44};
uint8_t ArrayRead[4];int main(void){OLED_Init(); //OLED初始化W25Q64_Init(); //W25Q64初始化//初始化OLED显示OLED_ShowString(1,1,"MID:FF DID:FFFF ");OLED_ShowString(2,1,"W:FF FF FF FF");OLED_ShowString(3,1,"R:FF FF FF FF");//读取W25Q64的ID号W25Q64_ID W25Q64_ID_Structure;W25Q64_ReadID(&W25Q64_ID_Structure);OLED_ShowHexNum(1,5,W25Q64_ID_Structure.MID,2);OLED_ShowHexNum(1,12,W25Q64_ID_Structure.DID,4);//写入数据W25Q64_EraseSector(0x00000000); //写擦除W25Q64_PageProgram(0x00000000, ArrayWrite, 4);//写数据//读取数据W25Q64_ReadByte(0x00000000, ArrayRead, 4);//将读写数据显示到OLED上uint16_t i;for(i=0;i<4;i++){OLED_ShowHexNum(2,3+3*i,ArrayWrite[i],2);OLED_ShowHexNum(3,3+3*i,ArrayRead[i],2);}while(1){};
}
- SPI_User.h
#ifndef __SPI_USER_H
#define __SPI_USER_H//SPI初始化
void SPI_User_Init(void);
//SPI起始信号
void SPI_User_Start(void);
//SPI终止信号
void SPI_User_Stop(void);
//SPI交换一个字节(模式0)
uint8_t SPI_User_SwapByte(uint8_t SendByte);#endif
- SPI_User.c
#include "stm32f10x.h" // Device header//SPI-SS引脚写操作
void SPI_User_W_SS(uint8_t BitValue){GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
//SPI-SCK引脚写操作
void SPI_User_W_SCK(uint8_t BitValue){GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}
//SPI-MOSI引脚写操作
void SPI_User_W_MOSI(uint8_t BitValue){GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}
//SPI-MISO引脚读操作
uint8_t SPI_User_R_MISO(void){return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}//SPI初始化
void SPI_User_Init(void){RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//初始化CLK、SS、MOSI-推挽输出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);//初始化MISO-上拉输入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_User_W_SS(1); //SS默认高电平SPI_User_W_SCK(0);//模式0:SCK默认低电平
}//SPI起始信号
void SPI_User_Start(void){SPI_User_W_SS(0);
}//SPI终止信号
void SPI_User_Stop(void){SPI_User_W_SS(1);
}//SPI交换一个字节(模式0)
uint8_t SPI_User_SwapByte(uint8_t SendByte){uint8_t i;//实现方法一:使用掩码依次提取数据的每一位uint8_t RecByte = 0x00;for(i=0;i<8;i++){SPI_User_W_MOSI((0x80>>i) & SendByte);SPI_User_W_SCK(1);//SCK上升沿if(SPI_User_R_MISO()==1){RecByte = (0x80>>i) | RecByte;}SPI_User_W_SCK(0);//SCK下降沿}
// //实现方法二:使用循环移位模型
// for(i=0;i<8;i++){
// SPI_User_W_MOSI(0x80 & SendByte);
// SendByte <<= 1;
// SPI_User_W_SCK(1);//SCK上升沿
// if(SPI_User_R_MISO()){
// SendByte |= 0x01;
// }
// SPI_User_W_SCK(0);//SCK下降沿
// }return RecByte;
}
- W25Q64.h
#ifndef __W25Q64_H
#define __W25Q64_H//W25Q64的ID结构体
typedef struct{uint8_t MID;uint16_t DID;
}W25Q64_ID;//W25Q64的指令码
#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_EAD 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//W25Q64初始化
void W25Q64_Init(void);
//读取W25Q64的ID
void W25Q64_ReadID(W25Q64_ID* ID_struct);
//页编程
void W25Q64_PageProgram(uint32_t start_addr, uint8_t *wByteArray, uint16_t count);
//写擦除-扇区
void W25Q64_EraseSector(uint32_t erase_addr);
//读取数据-读数据可以跨页
void W25Q64_ReadByte(uint32_t start_addr, uint8_t *rByteArray, uint32_t count);#endif
- SPI_User.c
#include "stm32f10x.h" // Device header
#include "SPI_User.h"
#include "W25Q64.h"//W25Q64初始化
void W25Q64_Init(void){SPI_User_Init();
}//读取W25Q64的ID
void W25Q64_ReadID(W25Q64_ID* ID_struct){SPI_User_Start();SPI_User_SwapByte(W25Q64_JEDEC_ID);ID_struct->MID = SPI_User_SwapByte(W25Q64_DUMMY_BYTE);ID_struct->DID = SPI_User_SwapByte(W25Q64_DUMMY_BYTE);ID_struct->DID <<= 8;ID_struct->DID |= SPI_User_SwapByte(W25Q64_DUMMY_BYTE); SPI_User_Stop();
}//发送写使能指令
void W25Q64_WriteEnable(void){SPI_User_Start();SPI_User_SwapByte(W25Q64_WRITE_ENABLE);SPI_User_Stop();
}//等待W25Q64恢复成空闲状态
void W25Q64_WaitBusy(void){SPI_User_Start();SPI_User_SwapByte(W25Q64_READ_STATUS_REGISTER_1);//读状态寄存器1while((SPI_User_SwapByte(W25Q64_DUMMY_BYTE)&0x01) == 0x01);//Busy位为0就一直等待SPI_User_Stop();
}//页编程
void W25Q64_PageProgram(uint32_t start_addr, uint8_t *wByteArray, uint16_t count){uint16_t i;W25Q64_WaitBusy(); //等待忙状态置0W25Q64_WriteEnable(); //写使能SPI_User_Start();SPI_User_SwapByte(W25Q64_PAGE_PROGRAM); //写指令SPI_User_SwapByte(start_addr>>16); //写地址[23:16]SPI_User_SwapByte(start_addr>>8); //写地址[15:8]SPI_User_SwapByte(start_addr); //写地址[7:0]for(i=0;i<count;i++){SPI_User_SwapByte(*(wByteArray+i)); //写数据} SPI_User_Stop();
}//写擦除-扇区
void W25Q64_EraseSector(uint32_t erase_addr){W25Q64_WaitBusy(); //等待忙状态置0W25Q64_WriteEnable(); //写使能SPI_User_Start();SPI_User_SwapByte(W25Q64_SECTOR_ERASE_4KB); //擦除指令SPI_User_SwapByte(erase_addr>>16); //擦除地址[23:16]SPI_User_SwapByte(erase_addr>>8); //擦除地址[15:8]SPI_User_SwapByte(erase_addr); //擦除地址[7:0]SPI_User_Stop();
}//读取数据-读数据可以跨页
void W25Q64_ReadByte(uint32_t start_addr, uint8_t *rByteArray, uint32_t count){uint32_t i;W25Q64_WaitBusy(); //等待忙状态置0SPI_User_Start();SPI_User_SwapByte(W25Q64_READ_DATA); //读指令SPI_User_SwapByte(start_addr>>16); //读地址[23:16]SPI_User_SwapByte(start_addr>>8); //读地址[15:8]SPI_User_SwapByte(start_addr); //读地址[7:0]for(i=0;i<count;i++){*(rByteArray+i) = SPI_User_SwapByte(W25Q64_DUMMY_BYTE);//读数据}SPI_User_Stop();
}
编程感想:
- 初始化数组的时候,一定要记得加
0x
,否则默认就是十进制数。- GPIO引脚选中的时候,格式为
GPIO_Pin_1
,而不是GPIO_PinSource1
。
11.4 SPI通信外设
STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,不仅SPI性能更高、同时也减轻CPU的负担。下面是stm32中SPI的性能参数(粗体表示使用中的默认配置):
- 可配置8位/16位数据帧、高位先行/低位先行
- 时钟频率: fPCLK/(2,4,8,16,32,64,128,256),也就是外设时钟分频得来。APB2中,fPCLK=72MHz;APB1中,fPCLK=36MHz。
- 支持多主机模型、主机操作、从机操作。
- 可精简为半双工/单工通信,一般不用。
- 支持DMA
- 兼容I2S协议(一种数字音频信号传输的专用协议)。
- STM32F103C8T6 硬件SPI资源:SPI1(APB2)、SPI2(APB1)。
引脚 | SPI1 | SPI2 |
---|---|---|
NSS | PA4/PA15 | PB12 |
SCK | PA5/PA3 | PB13 |
MISO | PA6/PA4 | PB14 |
MOSI | PA7/PA5 | PB15 |
注:斜杠后引脚定义表示引脚重映射
下面介绍stm32中SPI外设的电路框图:

寄存器配合介绍:
- 移位寄存器:右侧的数据一位一位地从MOSI输出,MOSI的数据一位一位地移到左侧数据位。
- LSBFIRST控制位:用于控制移位寄存器是低位先行(1)还是高位先行(0)。
- MISO和MOSI的交叉:用于切换主从模式。不交叉时为主机模式,交叉时为从机模式。
- 接收缓冲区、发送缓冲区:实际上分别就是接收数据寄存器RDR、发送数据缓冲区TDR。TDR和RDR占用同一个地址,统一叫作DR。移位寄存器空时,TXE标志位置1,TDR移入数据,下一个数据移入到TDR;移位寄存器接收完毕(同时也标志着移出完成),RXNE标志位置1,数据转运到RDR,此时需要尽快读出RDR,以防止被下一个数据覆盖。
细节:SPI为全双工同步通信,所以为一个移位寄存器、两个缓冲区;IIC为单工通信,所以只需要一个移位寄存器、一个缓冲区;USRT为全双工异步通信,所以需要两个移位寄存器、两个缓冲区,且这两套分别独立。
控制逻辑介绍:
- 波特率发生器:本质是一个分频器,用于产生SCK时钟。输入时钟就是外设时钟fPCLK=72MHz/36MHz。每产生一个时钟,就移入/移出一个比特。SPI_CR1中的[BR2,BR1,BR0]用于产生分频系数。
- SPI_CR1:SPI控制寄存器1,下面简单介绍一下。详细可以参考中文数据手册“23.5 SPI和I2S寄存器描述”一节。
- SPE(SPI Enable):SPI使能,就是SPI_Cmd函数配置的位。
- BR(Baud Rate):配置波特率,也就是SCK时钟频率。
- MSTR(Master):配置主机模式(1)、从机(0)模式。
- CPOL、CPHA:用于选择SPI的四种模式。

- 波特率发生器:用于产生SCK时钟。
- 数据控制器:根据配置,控制SPI外设电路的运行。
- 字节交换过程:交换完毕,移位寄存器空,则TXE位置1、RXNE位置1,TDR会自动转运数据到移位寄存器,RDR数据等待用户读取。
- 开关控制【代码】:SPI外设使能。
- GPIO【代码】:用于各引脚的初始化。
- 从机使能引脚SS【代码】:并不存在于SPI硬件外设中,实际使用随便指定一个GPIO口(例如PA4)即可。在一主多从模式下,GPIO模拟SS是最佳选择。
上面介绍了stm32中SPI外设的基本原理,在实际书写代码的过程中,使用一个结构体便可以直接配置 波特率发生器 和 字节交换的默认模式,这是SPI外设内部便会自动工作,用户额外需要关心的只是何时读写DR。下面介绍读写时序的流程,分别是性能更高、使用复杂的“主模式全双工连续传输”,以及性能较低、常用且简单易学的“非连续传输”:

本模式可以实现数据的连续传输。
- 连续写入数据:只要TXE置1,就立马进中断写入数据(会同时清除TXE位);当写入到最后一个数据时,等待BSY位清除,发送流程完毕。
- 连续读出数据:只要RXNE位置1,就立马进中断读出数据(会同时清除RXNE位)。若不及时读出,现有数据就会被新的数据覆盖。
评价:连续数据流传输对于软件的配合要求较高,需要在每个标志位产生后及时读写数据,整个发送和接收的流程是交错的,但是传输效率是最高的。对传输效率有严格要求才会用到此模式,否则一般采用下面更为简单的“非连续传输”。

本模式对于程序设计非常友好。
- 字节交换流程:最开始等待TXE位置1,发送一个数据(会自动清除TXE);等待RXNE置1,读取数据。再进行下一次的字节交换。
评价:非连续传输会损失数据传输效率,数据传输速率越快,损失越明显。


从上面SPI软/硬件波形的对比来看,硬件SPI的一大特点就是数据变化紧贴SCK边沿,而不是像软件SPI那样,因为代码语句的执行会有一定的延迟(即使SCK边沿变化和输出数据变化的代码是挨在一起的)。
11.5 实验:硬件SPI读写W25Q64
需求:与软件SPI读写W25Q64相同,读出W25Q64的ID,并将ID和读写数据都显示在OLED上。
注:根据引脚定义表,选择SPI1的PA5、PA6、PA7进行通信,另外选择PA4作为片选引脚SS,于是引脚选择也就和软件SPI相同。
接线图、代码调用 与上一个实验——“软件SPI读写W25Q64”一致,只是将SPI_User
中的引脚变化使用硬件来实现了。

所以下面代码展示:main.c
、W25Q64.h
、W25Q64.c
、SPI_User.h
与源文件相同,只是SPI_User.c
中只是将涉及到SPI引脚变化的操作都替换成库函数了,具体的变化过程参考图11-15“非连续传输”时序图及其说明。
- SPI_User.c
#include "stm32f10x.h" // Device header//SPI-SS引脚写操作
void SPI_User_W_SS(uint8_t BitValue){GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}//SPI初始化
void SPI_User_Init(void){//1.开启外设时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//GPIO时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); //SPI1时钟//2.初始化端口//初始化SS-推挽输出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);//初始化CLK、MOSI-外设复用推挽输出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-上拉输入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);//3.配置SPISPI_InitTypeDef SPI_InitStructure;SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; //APB2的2分频-36MHzSPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //第一个边沿采样,第二个边沿输出SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //时钟空闲时低电平SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //数据位宽8bitSPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //SPI双线全双工SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //高位先行SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //主机模式SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //软件自定义片选信号SPI_InitStructure.SPI_CRCPolynomial = 0x0007; //CRC用不到,所以默认值7SPI_Init(SPI1, &SPI_InitStructure);//4.SPI使能SPI_Cmd(SPI1, ENABLE);SPI_User_W_SS(1);//默认不选中从机
}//SPI起始信号
void SPI_User_Start(void){SPI_User_W_SS(0);
}//SPI终止信号
void SPI_User_Stop(void){SPI_User_W_SS(1);
}//SPI交换一个字节(模式0)
uint8_t SPI_User_SwapByte(uint8_t SendByte){while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)!=SET); //等待TXE置1SPI_I2S_SendData(SPI1,SendByte); //发送数据到TDRwhile(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)!=SET);//等待RNXE置1return SPI_I2S_ReceiveData(SPI1); //从RDR接收数据
}
编程感想:
- Keil小技巧:没有代码自动补全时,就按
Ctrl+Alt+Space
,但记得先把系统中切换输入的Ctrl+Space
快捷键取消。- 关于是否清除标志位。虽然在11-14、11-15的SPI时序图讲解中,手册上写明了TXE和RXNE“由硬件置位并由软件清除”,但是这并不代表需要一条专门的语句来清除标志位,比如SPI中就是读写数据的过程中就自动清除了,所以具体还需要查看数据手册的描述。
相关文章:

stm32学习笔记-11 SPI通信
11 SPI通信 文章目录 11 SPI通信11.1 SPI通信协议11.2 W25Q64简介11.3 实验:软件SPI读写W25Q6411.4 SPI通信外设11.5 实验:硬件SPI读写W25Q64 注:笔记主要参考B站 江科大自化协 教学视频“ STM32入门教程-2023持续更新中”。 注:…...

“微商城”项目(3页面布局)
1.设置标题 设置页面头部标题,方便告诉用户当前显示的是哪一个页面。编辑src\router.js文件,示例代码如下。 routes: [{ path: /, redirect: /home, meta: { title: 首页 } },{ path: /home, component: Home, name: home, meta: { title: 首页 } } ] …...
Java 八股文 - MySQL
MySQL 1. MySQL 有几种锁? 三种锁的特点 表级锁:开销小,加锁快;不会出现死锁;锁定颗粒度大,发生锁冲突的概率最高,并发度最低。行级锁:开销大,加锁慢;会…...

周赛347(模拟、思维题、动态规划+优化)
文章目录 周赛347[2710. 移除字符串中的尾随零](https://leetcode.cn/problems/remove-trailing-zeros-from-a-string/)模拟 [2711. 对角线上不同值的数量差](https://leetcode.cn/problems/difference-of-number-of-distinct-values-on-diagonals/)模拟 [2712. 使所有字符相等…...

String AOP的使用
面向切面编程,面向特定方法编程,以方法为对象,在不修改原方法的基础上,对方法进行操作扩展等,底层是通过动态代理实现的 使用开发步骤: 1、创建一个类,加上Aspect声明为一个AOP切面类ÿ…...

华为芯片基地旁,龙华科技小镇大水坑片区城市更新单元旧改项目
项目位置:龙华观澜大水坑社区,位于梅观创新走廊九龙山产学研片区内 占地面积:总面积198万平方米,其中项目第一期60万平米开 发 商: 华润集团申报主体:华润置地项目:龙华科技小镇大水坑片区城市…...
论文阅读 | 频谱监测、认知电子战、网电攻击
文章目录 1.《超短波信号的频谱监测与信号源定位》1.1 信号预处理技术1.2 对指定频段的宽带信号截获、分析以及频率分选研究1.3 对指定频段的信号进行最佳分频段扫描分析并还原原信号1.4 总结2.《认知电子战理论及关键技术研究》2.1 认知电子战发展现状2.2 认知电子战发展趋势分…...

MySQL server安装记录
1 安装Notepad 运行下载的 npp.7.9.Installer.x64.exe 2 安装MySQL 将mysql-8.0.22-winx64.zip解压缩,我将其放置D盘根目录下。 进入文件夹,在目录中新建文件夹data和文件my.ini 用NotePad打开my.ini,输入以下内容并保存,其中目…...

平衡树原理讲解
平衡树——Treap 文章目录 平衡树——TreapBST定义性质操作插入insert(o, v)删除del(o, v)找前驱 / 后继get_prev(o)、get_next(o)查找最大 / 最小值get_min(o)、get_max(o)求元素排名get_rank(o)查找排名为 k k k的元素get_value_by_rank 平衡树左旋、右旋zag(o)、zig(o)左旋右…...
SpringMVC框架面试专题(初级-中级)-第七节
欢迎大家一起探讨~如果可以帮到大家请为我点赞关注哦~后续会持续更新 问题: 1.Spring MVC框架中的注解是什么?请举例说明如何使用注解。 解析: Spring MVC是一个基于MVC(Model-View-Controller…...
爬虫实战案例
预计更新 一、 爬虫技术概述 1.1 什么是爬虫技术 1.2 爬虫技术的应用领域 1.3 爬虫技术的工作原理 二、 网络协议和HTTP协议 2.1 网络协议概述 2.2 HTTP协议介绍 2.3 HTTP请求和响应 三、 Python基础 3.1 Python语言概述 3.2 Python的基本数据类型 3.3 Python的流程控制语句 …...
ConcurrentLinkedQueue非阻塞无界链表队列
ConcurrentLinkedQueue非阻塞无界链表队列 ConcurrentLinkedQueue是一个线程安全的队列,基于链表结构实现,是一个无界队列,理论上来说队列的长度可以无限扩大。 与其他队列相同,ConcurrentLinkedQueue 也采用的是先进先出&#…...
排序算法稳定性
稳定性: 用一句话总结排序算法的稳定性就是:同样的值,在排完序之后改不改变相对次序。 举例:arr[] {3,2,1,2,1,3},数组中共有1、2 、3各2个数,排完序之后arr1[] {1,1,2,2,3,3}。稳定性是指排完序之后&…...
统计学期末复习整理
统计学:描述统计学和推断统计学。计量尺度:定类尺度、定序尺度、定距尺度、定比尺度。 描述统计中的测度: 1.数据分布的集中趋势 2.数据分布的离散程度 3.数据分布的形状。 离散系数 也称为标准差系数,通常是用一组数据的标准差与…...

Sketch在线版免费使用,Windows也能用的Sketch!
Sketch 的最大缺点是它对 Windows/PC 用户不友好。它是一款 Mac 工具,无法在浏览器中运行。此外,使用 Sketch 需要安装其他插件才能获得更多响应式设计工具。然而,现在有了 Sketch 网页版工具即时设计替代即时设计! 即时设计几乎…...

详解uni-app项目运行在安卓真机调试
详解uni-app项目运行在安卓真机调试 uni-app项目运行在安卓真机调试 文章目录 详解uni-app项目运行在安卓真机调试前言为什么要用真机调试?真机调试操作步骤总结 前言 UNI-APP学习系列之详解uni-app项目运行在安卓真机调试 为什么要用真机调试? 因为安…...

体积小、无广告、超实用的5款小工具
大家好,我又来啦,今天给大家带来的5款软件,共同特点都是体积小、无广告、超实用,大家观看完可以自行搜索下载哦。 1.动态桌面——WinDynamicDesktop WinDynamicDesktop是一款用于根据时间和地点自动更换桌面壁纸的工具。它可以让…...
OZON好出单吗?新手如何做?注意事项是什么?
最近OZON的势头确实很猛,东哥后台也收到了很多关于OZON的咨询,很多想尝试跨境电商的新手卖家都对这个平台跃跃欲试,其中问最多的就是,“OZON好出单吗?”“新手做OZON需要注意什么?避开哪些坑?”…...

性能测试需求分析有哪些?怎么做?
目录 性能测试必要性评估 常见性能测试关键评估项如下: 性能测试工具选型 性能测试需求分析 性能测试需求评审 性能测试需求分析与传统的功能测试需求有所不同,功能测试需求分析重点在于从用户层面分析被测对象的功能性、易用性等质量特性ÿ…...
STM32F103RCT6 -- 基于FreeRTOS 的USART1 串口通讯
1. 在STM32F103RCT6 单片机上跑FreeRTOS 实时操作系统,使用串口USART1 通讯,发送 – 接收数据,实现上位机与下位机的通信 使用 FreeRTOS 提供的队列(Queue)机制来实现数据的接收和发送 2. USART1 配置: …...

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)
题目:3442. 奇偶频次间的最大差值 I 思路 :哈希,时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况,哈希表这里用数组即可实现。 C版本: class Solution { public:int maxDifference(string s) {int a[26]…...

华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...

最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...

MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...

Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...
Python 训练营打卡 Day 47
注意力热力图可视化 在day 46代码的基础上,对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...

C++实现分布式网络通信框架RPC(2)——rpc发布端
有了上篇文章的项目的基本知识的了解,现在我们就开始构建项目。 目录 一、构建工程目录 二、本地服务发布成RPC服务 2.1理解RPC发布 2.2实现 三、Mprpc框架的基础类设计 3.1框架的初始化类 MprpcApplication 代码实现 3.2读取配置文件类 MprpcConfig 代码实现…...

Vue3 PC端 UI组件库我更推荐Naive UI
一、Vue3生态现状与UI库选择的重要性 随着Vue3的稳定发布和Composition API的广泛采用,前端开发者面临着UI组件库的重新选择。一个好的UI库不仅能提升开发效率,还能确保项目的长期可维护性。本文将对比三大主流Vue3 UI库(Naive UI、Element …...

动态规划-1035.不相交的线-力扣(LeetCode)
一、题目解析 光看题目要求和例图,感觉这题好麻烦,直线不能相交啊,每个数字只属于一条连线啊等等,但我们结合题目所给的信息和例图的内容,这不就是最长公共子序列吗?,我们把最长公共子序列连线起…...