【STM32】SPI通信协议&W25Q64Flash存储器芯片(学习笔记)-CSDN博客
SPI通信外设
SPI外设简介
- STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担
- 可配置8位/16位数据帧、高位先行/低位先行(标黑常用)
- 时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256),就是SCK,一般体现的是传输速度,单位为Hz或者bit/s,最大进行2分频,72M/2=36MHz,I2C最大400kHz,SPI最大频率比I2C大90倍,SPI1时钟频率比SPI2大一倍
- 支持多主机模型、主或从操作
- 可精简为半双工/单工通信
- 支持DMA,DMA自动帮我们搬运数据
- 兼容I2S协议,数字音频信号传输的专用协议
- STM32F103C8T6 硬件SPI资源:SPI1(APB2的外设)、SPI2(APB1的外设)
兼容I2S协议: 应用场景
三条主要总线
STM32F103C8T6 有三条主要总线,分别是 AHB(高级高性能总线)、APB1(高级外设总线 1)和 APB2(高级外设总线 2),各总线频率如下:
- AHB 总线:其最高频率为 72MHz,通常系统时钟(SYSCLK)会直接作为 AHB 总线的时钟。
- APB1 总线:它的最高频率是 36MHz,APB1 总线挂载着一些低速外设,如 UART2 - UART5、SPI2、SPI3、I2C1、I2C2 等。
- APB2 总线:最高频率为 72MHz,APB2 总线挂载着高速外设,像 GPIO 端口、USART1、SPI1 等。
SPI框图
1. 引脚部分
- MOSI(Master Out Slave In):主设备输出,从设备输入引脚。在 SPI 通信中,主设备通过此引脚向从设备发送数据。
- MISO(Master In Slave Out):主设备输入,从设备输出引脚。从设备通过此引脚向主设备发送数据。
- SCK(Serial Clock):串行时钟引脚,由主设备产生,用于同步主设备和从设备之间的数据传输。
- NSS(Slave Select):从设备选择引脚,主设备通过拉低此引脚电平来选中对应的从设备。
2. 数据传输相关模块
- 发送缓冲区:用于暂存要发送的数据,数据由软件写入,然后发送到移位寄存器。
- 接收缓冲区:用于暂存接收到的数据,数据从移位寄存器读出后存储在此,等待软件读取。
- 移位寄存器:是 SPI 数据传输的核心部件。在发送数据时,它从发送缓冲区获取数据,按照时钟信号一位一位地通过 MOSI 引脚发送出去;在接收数据时,通过 MISO 引脚一位一位地接收数据,然后将完整的数据存入接收缓冲区。“LSBFIRST 控制位” 决定数据是低位(LSB) 在前还是高位(MSB) 在前进行传输。
3. 控制寄存器部分
- SPI_CR1:控制寄存器 1,包含多个控制位:
- LSBFIRST:控制数据传输时是低位在前还是高位在前。
- SPE(SPI Enable):使能 SPI 外设。
- BR[2:0]:波特率控制位,用于设置 SPI 的时钟频率,即波特率。
- MSTR(Master Select):主从模式选择位,设置该 SPI 设备为主设备还是从设备。
- CPOL(Clock Polarity):时钟极性控制位,决定 SCK 时钟信号的空闲状态电平(高电平或低电平)。
- CPHA(Clock Phase):时钟相位控制位,用于选择 SPI 的两种不同时钟相位模式。
- 还有其他如 CRC 相关控制位、双线双向模式控制位等,用于设置 SPI 的各种工作模式和特性。
- SPI_CR2:控制寄存器 2,包含以下控制位:
- TXEIE(Transmit Buffer Empty Interrupt Enable):发送缓冲区空中断使能位。
- RXNEIE(Receive Buffer Not Empty Interrupt Enable):接收缓冲区非空中断使能位。
- ERRIE(Error Interrupt Enable):错误中断使能位。
- SSOE(Slave Select Output Enable):从设备选择输出使能位。
- TXDMAEN(Transmit DMA Enable):发送直接内存访问(DMA)使能位。
- RXDMAEN(Receive DMA Enable):接收 DMA 使能位。
- SPI_SR:状态寄存器,包含多个状态标志位:
- BSY(Busy):忙标志位,当 SPI 正在进行数据传输时,该位被置 1。
- OVR(Overrun):溢出标志位,当接收缓冲区已满,又有新的数据到来时,该位被置 1。
- MODF(Mode Fault):模式错误标志位,当 SPI 在主模式下 NSS 引脚被意外拉低时,该位被置 1。
- CRCERR(CRC Error):CRC 校验错误标志位。
- TXE(Transmit Buffer Empty):发送缓冲区空标志位。
- RXNE(Receive Buffer Not Empty):接收缓冲区非空标志位。
- 波特率发生器:根据 SPI_CR1 中 BR [2:0] 的设置,产生相应频率的时钟信号 SCK,以控制数据传输的速率。
- 主控制电路:负责协调 SPI 各个模块的工作,根据控制寄存器的设置来管理数据的发送和接收过程。
- 通信电路:在 SPI 通信过程中,处理数据的收发以及与其他模块的交互,确保通信的正常进行。
大致分为两部分,左上角就是数据寄存器和移位寄存器配合部分;和串口、I2C的设计思路具有异曲同工之妙,主要为了实现数据流传输,右下角部分就是控制逻辑了;下面来看看框图细节:
首先,左上角核心部分,就是移位寄存器,右边的数据从低位,一位一位的从MOSI移出,然后MSO,一位一位的移入左边的数据高位,显然移位寄存器是一个右移的状态,所以目前图上表示的是低位先行的配置,对应右下角有一个LSBFLRST控制位,这一位可以控制是低位先行还是高位先行,可以查一下数据手册
目前框图的LSBFIRST状态应该是1,低位先行,如果LSBFIRST给0的话,就是高位先行,这个框图还要变动一下,就移位寄存器变为左移,输出从左边一出去,输入,从右边移进来。然后就是两个缓冲区,这两个缓冲区实际上就是数据寄存器DR,下面发送缓冲区就是发送数据寄存器TDR,上面接收缓冲区就是接收数据寄存器RDR,和串口那里一样,TDR和RDR占用同一个地址,统一叫做DR;
连续发送数据流过程:在主设备中,将要发送的数据写入发送缓冲区(TDR)。当移位寄存器没有数据移位时,TDR的数据就会转入至移位寄存器进行移位,当发送缓冲区中的数据发送完成后,SPI 状态寄存器(SPI_SR)中的 TXE(发送缓冲区空)标志位置 1。然后移位寄存器工作
移位寄存器工作:发送缓冲区的数据会在 SCK 时钟信号的驱动下,一位一位地移入移位寄存器,然后通过 MOSI 引脚发送到从设备。同时,从设备在 SCK 时钟的同步下,通过 MISO 引脚将数据发送回主设备(如果是双向通信)。
数据接收与存储:在主设备接收数据时,从设备发送的数据在 SCK 时钟信号的同步下,通过 MISO 引脚一位一位地移入主设备的移位寄存器,然后再存入接收缓冲区。当接收缓冲区中有数据时,SPI_SR 中的 RXNE(接收缓冲区非空)标志位置 1。
数据读取:软件可以通过查询 RXNE 标志位或者利用接收缓冲区非空中断,从接收缓冲区(SPI_DR)中读取接收到的数据。如果是连续接收数据,需要及时处理已接收的数据,以便接收新的数据,保证接收过程的连续性。
数据传输设计思路SPI和I2C和串口的区别:设计思路区别
框图中的“波特率发生器”有什么作用?
1. 定义与基本功能
波特率发生器是一种用于生成特定频率时钟信号的电路模块。在 SPI(Serial Peripheral Interface,串行外设接口)通信中,它的主要功能是产生串行时钟信号 SCK(Serial Clock)。SCK 信号用于同步主设备和从设备之间的数据传输,确保数据能够按照预定的速率和时序准确地发送和接收。
2. 波特率的设置与控制
- 控制位设置:波特率发生器的工作受 SPI 控制寄存器(如 SPI_CR1 中的 BR [2:0] 位)的控制。通过对这些控制位进行编程设置,可以改变波特率发生器的输出时钟频率。例如,在 STM32 的 SPI 模块中,BR [2:0] 位有不同的组合,对应不同的分频系数。
- 具体示例:当 BR [2:0] 设置为不同的值时,波特率发生器会将系统时钟(通常是 AHB 总线时钟)按照相应的分频系数进行分频,从而得到不同频率的 SCK 时钟信号。如果系统时钟频率为 72MHz,当 BR [2:0] 设置为 “110” 时,对应的分频系数为 128,那么此时 SCK 的频率就是 72MHz / 128,约为 562.5kHz。
3. 对通信的影响
- 数据传输速率:波特率发生器生成的 SCK 时钟频率决定了 SPI 通信的数据传输速率。较高的 SCK 频率意味着数据可以在更短的时间内传输完成,从而提高通信效率。但同时,过高的频率可能会受到传输线的电气特性、设备的处理能力等因素的限制,导致数据传输错误。
- 设备兼容性:不同的 SPI 设备可能对 SCK 时钟频率有不同的要求。波特率发生器可以根据与之通信的从设备的特性,灵活地调整 SCK 时钟频率,以确保主设备和从设备之间能够正常通信。例如,一些低速的 SPI 从设备可能无法处理过高频率的时钟信号,此时就需要通过波特率发生器降低 SCK 的频率。
4. 与其他模块的协同工作
波特率发生器产生的 SCK 时钟信号与 SPI 的其他模块协同工作。在数据发送过程中,发送缓冲区的数据会在 SCK 时钟信号的驱动下,一位一位地通过 MOSI 引脚发送出去;在数据接收过程中,MISO 引脚上的数据会在 SCK 时钟信号的同步下,一位一位地被移入移位寄存器。同时,波特率发生器的工作也受到主控制电路的管理和协调,以确保整个 SPI 通信过程的稳定和准确。
NSS任何实现多主机切换功能?NSS多主机切换
SPI基本结构 
主模式全双工连续传输 
1. 时钟与数据传输
- SCK(时钟信号):由主设备产生,为数据传输提供同步时钟,确保主从设备数据收发时序一致。
- 数据发送(MOSI 输出):主设备通过 MOSI 依次发送数据
0xF1
、0xF2
、0xF3
,每个数据按位(b0~b7
)在 SCK 驱动下传输。 - 数据接收(MISO 输入):主设备通过 MISO 接收从设备返回的数据
0xA1
、0xA2
、0xA3
,同样按位同步接收。
2. 关键标志位变化
- TXE(发送缓冲区空标志):
- 硬件自动置位:当发送缓冲区数据发送到移位寄存器后,TXE 被硬件置 1,表示发送缓冲区空闲。
- 软件清除:需通过软件向
SPI_DR
写入新数据来清除该标志(如写入0xF1
后,TXE 置 1,再写入0xF2
即清除)。
- RXNE(接收缓冲区非空标志):
- 硬件自动置位:当接收缓冲区收到完整数据(如
0xA1
)后,RXNE 被硬件置 1。 - 软件清除:通过软件读取
SPI_DR
数据来清除该标志(如读取0xA1
后,RXNE 标志清除)。
- 硬件自动置位:当接收缓冲区收到完整数据(如
- BSY(忙标志):
- 硬件自动置位:SPI 开始数据传输时,BSY 置 1,表示外设处于忙状态。
- 硬件自动清除:所有数据传输完成后,BSY 由硬件自动清零。
3. 软件操作流程
- 发送数据:软件先写入第一个数据(如
0xF1
到SPI_DR
),等待 TXE=1(发送缓冲区空),再写入下一个数据(如0xF2
),循环直至数据发送完毕。 - 接收数据:软件等待 RXNE=1(接收缓冲区有数据),然后从
SPI_DR
读取数据(如读取0xA1
),完成一次接收操作,循环处理后续数据。
4. 整体时序逻辑
- 发送缓冲区、接收缓冲区与标志位
TXE
、RXNE
配合,确保数据连续发送和接收。BSY
标志则全程反映 SPI 外设的工作状态,直到所有数据传输结束才恢复空闲状态。通过这种机制,SPI 实现了主模式全双工连续数据传输的高效控制。
非连续传输 
这张图展示了 STM32 SPI 外设工作在 非连续传输发送模式(BIDIMODE=0
且 RXONLY=0
)时,TXE
(发送缓冲区空标志)和 BSY
(忙标志)的变化过程,具体解析如下:
1. 基础配置与信号
- 时钟配置:图中示例配置
CPOL=1
(时钟空闲状态为高电平)、CPHA=1
(数据在时钟第二个边沿采样),定义了 SPI 通信的时钟极性和相位。 - SCK(时钟信号):由主设备生成,为数据传输提供时序同步,驱动 MOSI 引脚按位发送数据。
- MOSI(输出):主设备通过 MOSI 依次发送数据
0xF1
、0xF2
、0xF3
,每个数据按b0~b7
位在 SCK 时钟下逐位传输。
2. 关键标志位变化
- TXE(发送缓冲区空标志):
- 当发送缓冲区数据转移到移位寄存器后,硬件自动将
TXE
置 1,表示缓冲区空闲。 - 软件需通过向
SPI_DR
写入新数据来清除该标志(如写入0xF1
后,TXE
置 1,写入0xF2
时清除)。
- 当发送缓冲区数据转移到移位寄存器后,硬件自动将
- BSY(忙标志):
- 硬件控制:SPI 开始数据传输时,
BSY
置 1,表示外设处于忙状态;所有传输结束后,BSY
自动清零。
- 硬件控制:SPI 开始数据传输时,
3. 非连续传输的软件操作特点
- 数据写入延迟:与连续传输不同,非连续传输中软件写入数据存在延迟。例如:
- 先写入
0xF1
到SPI_DR
,启动第一次传输。 - 等待
TXE=1
后,较晚写入0xF2
;再次等待TXE=1
后,又较晚写入0xF3
。
- 先写入
- 结束等待:最后需等待
BSY=0
,确认所有非连续的传输操作完全结束,SPI 外设回归空闲状态。
4. 整体时序逻辑
图中体现了非连续传输的 “间隔性”:每次数据发送依赖软件主动写入新数据触发,且两次传输之间存在等待 TXE
标志的间隔。BSY
标志全程反映 SPI 工作状态,仅在数据传输过程中保持为 1,其余时间(如等待软件写入新数据阶段)可能短暂清零,直到所有传输完成最终清零。
非连续传输在SCK频率低时无影响;
在SCK频率高时有缺点:不同SCK频率不同间隙的示波器波形
最后看手册很重要:坚持到最后都牛逼
硬件SPI读写W25Q64
本节代码在软件SPI读写W25Q64芯片基础上做修改,代码参考软件SPI读写W25Q64芯片:【江协科技STM32】软件SPI读写W25Q64芯片(学习笔记)-CSDN博客
接线图
因为这里是硬件读写SPI,涉及STM32内部硬件外设的引脚,都要查询引脚定义表
引脚定义表
引脚复用:
硬件SPI
硬件SPI代码实际上就两部分:
1、SPI外设初始化代码
Ⅰ卡其时钟,开启SPI和GPIO的时钟
Ⅱ 初始化GPIO口,其中,SCK和MOSI是由硬件外设控制的输出信号,所以配置为复用推挽输出;
MISO是由硬件外设控制的输入信号,所以配置为上拉或者浮空输入。
Ⅲ 配置SPI外设,初始化SPI
Ⅳ 使能SPI
void HerSPI_Init(void)
{/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); //开启SPI1的时钟/*GPIO初始化*/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); //将PA4引脚初始化为推挽输出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); //将PA5和PA7引脚初始化为复用推挽输出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); //将PA6引脚初始化为上拉输入/*SPI初始化*/SPI_InitTypeDef SPI_InitStructure; //定义结构体变量SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //模式,选择为SPI主模式SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //方向,选择2线全双工SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //数据宽度,选择为8位SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //先行位,选择高位先行SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; //波特率分频,选择128分频SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //SPI极性,选择低极性SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //SPI相位,选择第一个时钟边沿采样,极性和相位决定选择SPI模式0SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS,选择由软件控制SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC多项式,暂时用不到,给默认值7SPI_Init(SPI1, &SPI_InitStructure); //将结构体变量交给SPI_Init,配置SPI1/*SPI使能*/SPI_Cmd(SPI1, ENABLE); //使能SPI1,开始运行/*设置默认电平*/HerSPI_W_SS(1); //SS默认高电平
}
2、SPI外设操作时序,完成交换应该字节的流程
①等待TXE为1,等待发送寄存器位空,发送寄存器不为空就不着急写
②写入数据到发送数据寄存器,开始产生时序
③//等待RXNE 接收数据寄存器非空
④ 读取接收到的数据并返回
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET); //等待发送数据寄存器空SPI_I2S_SendData(SPI1, ByteSend); //写入数据到发送数据寄存器,开始产生时序while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET); //等待RXNE 接收数据寄存器非空return SPI_I2S_ReceiveData(SPI1); //读取接收到的数据并返回
}