4.STM32通信接口之SPI通信(含源码)---硬件SPI与W25Q64存储模块通信实战《精讲》
开胃简介
根据上一节对STM32的SPI介绍!本节将进行硬件SPI的实现,片选用软件实现!跟着Whappy走起!W25Q64的驱动层,我们不需要更改,仅仅需要更改一下SPI的协议,即:由软件实现改成硬件实现。跟着Whappy同志步伐学起来!
SPI硬件整体框架如下图,代码配置也是根据这个SPI的基本结构进行实现的,如下五步实现过程。
STM32的SPI相关库函数的介绍如下:
初始化和配置函数
函数名 功能 参数介绍 使用方法 SPI_I2S_DeInit
将 SPI/I2S 外设寄存器重置为默认值 SPIx
: SPI 外设地址在重新配置 SPI 前调用,清除所有之前的配置 SPI_Init
根据指定配置初始化 SPI SPIx
: SPI 外设地址<br>SPI_InitStruct
: SPI 配置结构体配置 SPI 通信参数,如时钟极性、数据大小等 I2S_Init
根据指定配置初始化 I2S SPIx
: SPI 外设地址<br>I2S_InitStruct
: I2S 配置结构体配置 I2S 通信参数,如标准、数据长度等 SPI_StructInit
填充 SPI 初始化结构体默认值 SPI_InitStruct
: SPI 配置结构体指针在手动配置 SPI 前,快速获取默认配置 SPI_I2S_StructInit
填充 I2S 初始化结构体默认值 I2S_InitStruct
: I2S 配置结构体指针在手动配置 I2S 前,快速获取默认配置
控制和命令函数
函数名 功能 参数介绍 使用方法 SPI_Cmd
使能或禁用 SPI SPIx
: SPI 外设地址<br>NewState
: 使能/禁用状态开启或关闭 SPI 外设 I2S_Cmd
使能或禁用 I2S SPIx
: SPI 外设地址<br>NewState
: 使能/禁用状态开启或关闭 I2S 外设 SPI_I2S_ITConfig
配置 SPI/I2S 中断 SPIx
: SPI 外设地址<br>SPI_I2S_IT
: 中断类型<br>NewState
: 使能/禁用状态配置中断,如发送完成、接收完成等 SPI_I2S_DMACmd
配置 SPI/I2S DMA 请求 SPIx
: SPI 外设地址<br>SPI_I2S_DMAReq
: DMA 请求类型<br>NewState
: 使能/禁用状态启用或禁用 DMA 传输
数据传输函数
函数名 功能 参数介绍 使用方法 SPI_I2S_SendData
通过 SPI/I2S 发送数据 SPIx
: SPI 外设地址<br>Data
: 待发送的 16 位数据发送单个数据字 SPI_I2S_ReceiveData
从 SPI/I2S 接收数据 SPIx
: SPI 外设地址读取接收缓冲区的 16 位数据
高级配置函数
函数名 功能 参数介绍 使用方法 SPI_NSSInternalSoftwareConfig
配置内部片选信号 SPIx
: SPI 外设地址<br>SPI_NSSInternalSoft
: NSS 信号配置软件控制从设备片选 SPI_SSOutputCmd
配置 SS 输出 SPIx
: SPI 外设地址<br>NewState
: 使能/禁用状态控制从设备片选输出 SPI_DataSizeConfig
配置数据大小 SPIx
: SPI 外设地址<br>SPI_DataSize
: 数据大小设置每次传输的数据位数 SPI_BiDirectionalLineConfig
配置双向线路方向 SPIx
: SPI 外设地址<br>SPI_Direction
: 传输方向设置单工或半双工模式下的传输方向
CRC 校验函数
函数名 功能 参数介绍 使用方法 SPI_TransmitCRC
发送 CRC SPIx
: SPI 外设地址手动发送 CRC 值 SPI_CalculateCRC
使能/禁用 CRC 计算 SPIx
: SPI 外设地址<br>NewState
: 使能/禁用状态开启或关闭 CRC 硬件计算 SPI_GetCRC
获取 CRC 值 SPIx
: SPI 外设地址<br>SPI_CRC
: CRC 类型读取发送或接收 CRC 值 SPI_GetCRCPolynomial
获取 CRC 多项式 SPIx
: SPI 外设地址读取当前使用的 CRC 多项式
状态和标志函数
函数名 | 功能 | 参数介绍 | 使用方法 |
---|---|---|---|
SPI_I2S_GetFlagStatus | 获取状态标志 | SPIx : SPI 外设地址<br>SPI_I2S_FLAG : 标志类型 | 检查传输、错误等各种状态 |
SPI_I2S_ClearFlag | 清除状态标志 | SPIx : SPI 外设地址<br>SPI_I2S_FLAG : 标志类型 | 手动清除特定状态标志 |
SPI_I2S_GetITStatus | 获取中断状态 | SPIx : SPI 外设地址<br>SPI_I2S_IT : 中断类型 | 检查中断是否触发 |
SPI_I2S_ClearITPendingBit | 清除中断挂起位 | SPIx : SPI 外设地址<br>SPI_I2S_IT : 中断类型 | 手动清除中断挂起状态 |
代码实现SPI步骤--五步实现(采用非连续传输发送)
GPIO 模式 | 十六进制值 | 模式说明 | 典型应用场景 |
---|---|---|---|
GPIO_Mode_AIN | 0x0 | 模拟输入模式 | 用于模拟信号输入,如传感器、ADC采样 |
GPIO_Mode_IN_FLOATING | 0x04 | 浮空输入模式 | 外部悬空输入,无上拉/下拉电阻 |
GPIO_Mode_IPD | 0x28 | 下拉输入模式 | 输入引脚默认为低电平 |
GPIO_Mode_IPU | 0x48 | 上拉输入模式 | 输入引脚默认为高电平 |
GPIO_Mode_Out_OD | 0x14 | 开漏输出模式 | 需要外部上拉电阻,常用于 I2C 通信 |
GPIO_Mode_Out_PP | 0x10 | 推挽输出模式 | 通用数字输出模式,驱动能力强 |
GPIO_Mode_AF_OD | 0x1C | 开漏复用功能 | 复用功能的开漏输出,如 I2C |
GPIO_Mode_AF_PP | 0x18 | 推挽复用功能 | 复用功能的推挽输出,如 USART、SPI |
第一步:开启时钟,开启SPI和GPIO的时钟;
第二步:初始化GPIO,其中SCK和MOSI是由硬件外设控制的输出信号,配置称复用推挽输出,MISO是硬件外设的输入信号,配置上拉输入。SN片选引脚,是由软件控制的输出信号,配置成通用推挽输出。
第三步:配置SPI外设,这一步,我们使用一个结构体进行配置(STM32的库函数)
第四步:使能SPI。
void MySPI_Init(void) { // 开启GPIOA时钟 /* 开启GPIOA的时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启GPIOA的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); // 开启SPI1的时钟 /* GPIO的配置 */ // 配置SCK和MOSI引脚 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 设置为复用推挽输出模式 (Alternate Function Push-Pull) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; // 选择引脚5 (SCK) 和 引脚7 (MOSI) GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置引脚速率为50MHz GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIOA的这两个引脚 // 配置MISO引脚 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 设置为上拉输入模式 (Input with Pull-Up) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; // 选择引脚6 (MISO) GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置引脚速率为50MHz GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIOA的MISO引脚 // 配置NSS引脚 (片选) GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 设置为推挽输出模式 (Push-Pull Output) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; // 选择引脚4 (NSS) GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置引脚速率为50MHz GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIOA的NSS引脚 /* SPI的配置 */ SPI_InitTypeDef SPI_InitStructure; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; // 设置SPI波特率预分频器为128,确定SPI通信速度 SPI_InitStructure.SPI_CRCPolynomial = 7; // 设置CRC多项式为7,用于数据完整性检测 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 设置数据位宽为8位 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 设置SPI为全双工模式(即同时发送和接收数据) SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 设置数据传输顺序为先发送最高有效位(MSB) SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 设置SPI为主模式 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 设置为软件控制的片选(NSS) SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 设置时钟相位 (CPHA) 为第一沿变化 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 设置时钟极性 (CPOL) 为低电平 SPI_Init(SPI1, &SPI_InitStructure); // 初始化SPI1外设 // 使能SPI SPI_Cmd(SPI1, ENABLE); // 启动SPI1外设,开始进行SPI通信 }
详细解释:
时钟使能:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE)
:启用GPIOA外设时钟,以便能够配置和使用GPIOA引脚。RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE)
:启用SPI1外设时钟,确保SPI外设可以正常工作。GPIO配置:
- SCK (Serial Clock) 和 MOSI (Master Out Slave In) 引脚配置为复用推挽输出模式(
GPIO_Mode_AF_PP
),通过这些引脚来传输数据。- MISO (Master In Slave Out) 引脚配置为上拉输入模式(
GPIO_Mode_IPU
),用于接收从外设发送来的数据。- NSS (Chip Select) 引脚配置为推挽输出模式(
GPIO_Mode_Out_PP
),用于控制SPI外设的选通。SPI配置:
- 波特率预分频器:设置为128,这会影响SPI的时钟频率,确保SPI通信速度适合与外设交互。
- CRC多项式:设置为7,SPI传输过程中可以使用CRC校验以提高数据的完整性。
- 数据位宽:设置为8位,即每次传输一个字节的数据。
- SPI方向:设置为全双工模式,意味着数据可以在同一时间同时进行接收和发送。
- 数据位顺序:设置为MSB先传输(
SPI_FirstBit_MSB
),即先发送最高有效位(MSB)。- SPI模式:设置为主模式(
SPI_Mode_Master
),意味着此外设将控制通信。- 片选控制:选择软件控制片选模式(
SPI_NSS_Soft
),即通过软件控制NSS信号的状态。- 时钟极性和相位:
SPI_CPOL_Low
表示时钟的空闲状态为低电平,SPI_CPHA_1Edge
表示数据将在时钟的第一沿变化时采样。使能SPI:
SPI_Cmd(SPI1, ENABLE)
:使能SPI1外设,开始SPI通信,允许SPI发送和接收数据。这段代码配置了STM32的SPI1外设,使其能够与其他SPI外设进行通信(例如传感器、外部存储设备等)。
第五步:最后,参考一下非连续传输,进行一个字节的交换!如下图
uint8_t MySPI_SwapByte(uint8_t Byte) { // 等待SPI1的发送数据寄存器空,确保可以发送数据 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET); // 发送数据 SPI_I2S_SendData(SPI1, Byte); // 通过SPI发送一个字节的数据 // 等待SPI1的接收数据寄存器非空,确保数据已接收 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET); // 读取接收到的数据并返回 return SPI_I2S_ReceiveData(SPI1); // 返回接收到的字节数据 }
详细解释:
等待发送寄存器空:
SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE)
:检查SPI1的数据发送寄存器(TXE标志)是否为空。如果为“空”(即可以发送数据),则返回SET
,否则继续等待。- 这是通过循环检查,确保数据寄存器准备好可以发送数据,避免数据溢出或丢失。
发送数据:
SPI_I2S_SendData(SPI1, Byte)
:将参数Byte
中的数据通过SPI总线发送出去。SPI的发送操作是同步的,当数据发送寄存器可用时,数据将被传送到外部设备。等待接收寄存器非空:
SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE)
:检查SPI1的接收数据寄存器(RXNE标志)是否非空。若接收寄存器有数据(即数据已经接收到),则返回SET
,否则继续等待。- 这确保了只有在数据完全接收后,才进行下一步操作。
读取接收数据:
SPI_I2S_ReceiveData(SPI1)
:从SPI1接收数据寄存器中读取接收到的字节并返回。- 在交换字节数据时,发送和接收操作是并行进行的。因此,读取接收到的数据通常就是发送数据时外设返回的数据。
总结:
该函数实现了SPI的数据发送与接收。它通过等待SPI硬件标志位来确保发送和接收过程的同步,确保数据的完整传输。在使用时,传入一个字节,发送后等待接收另一个字节并返回。这个函数可以用于SPI设备之间的字节交换操作。
源码
把上一节软件部分改一下即可
#include "stm32f10x.h" // Device header
//片选
void MySPI_W_CS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);
}
void MySPI_Init(void)
{
// 开启GPIOA时钟
/* 开启GPIOA的时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); // 开启SPI1的时钟
/* GPIO的配置 */
// 配置SCK和MOSI引脚
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 设置为复用推挽输出模式 (Alternate Function Push-Pull)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; // 选择引脚5 (SCK) 和 引脚7 (MOSI)
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置引脚速率为50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIOA的这两个引脚
// 配置MISO引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 设置为上拉输入模式 (Input with Pull-Up)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; // 选择引脚6 (MISO)
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置引脚速率为50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIOA的MISO引脚
// 配置NSS引脚 (片选)
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 设置为推挽输出模式 (Push-Pull Output)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; // 选择引脚4 (NSS)
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置引脚速率为50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIOA的NSS引脚
/* SPI的配置 */
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; // 设置SPI波特率预分频器为128,确定SPI通信速度
SPI_InitStructure.SPI_CRCPolynomial = 7; // 设置CRC多项式为7,用于数据完整性检测
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 设置数据位宽为8位
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 设置SPI为全双工模式(即同时发送和接收数据)
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 设置数据传输顺序为先发送最高有效位(MSB)
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 设置SPI为主模式
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 设置为软件控制的片选(NSS)
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 设置时钟相位 (CPHA) 为第一沿变化
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 设置时钟极性 (CPOL) 为低电平
SPI_Init(SPI1, &SPI_InitStructure); // 初始化SPI1外设
// 使能SPI
SPI_Cmd(SPI1, ENABLE); // 启动SPI1外设,开始进行SPI通信
}
//SPI模式0
/**
* @brief 开始SPI通信,拉低片选信号
* @note 通常在发送数据前调用,选中从设备
*/
void MySPI_Start(void)
{
MySPI_W_CS(0); // 拉低CS(片选)信号,选中从设备
}
/**
* @brief 结束SPI通信,拉高片选信号
* @note 通常在数据传输完成后调用,取消从设备选择
*/
void MySPI_Stop(void)
{
MySPI_W_CS(1); // 拉高CS(片选)信号,取消从设备选择
}
uint8_t MySPI_SwapByte(uint8_t Byte)
{
// 等待SPI1的发送数据寄存器空,确保可以发送数据
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);
// 发送数据
SPI_I2S_SendData(SPI1, Byte); // 通过SPI发送一个字节的数据
// 等待SPI1的接收数据寄存器非空,确保数据已接收
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);
// 读取接收到的数据并返回
return SPI_I2S_ReceiveData(SPI1); // 返回接收到的字节数据
}
知识普及:
SPI 的四种模式详细解析:
模式定义
SPI 模式由两个参数决定:
- CPOL (Clock Polarity):时钟极性
- CPHA (Clock Phase):时钟相位
详细模式说明
- 模式0 (CPOL=0, CPHA=0)
- 时钟空闲态为低电平
- 在上升沿采样数据
- 在下降沿发送数据
- 最常用的模式
- 模式1 (CPOL=0, CPHA=1)
- 时钟空闲态为低电平
- 在下降沿采样数据
- 在上升沿发送数据
- 模式2 (CPOL=1, CPHA=0)
- 时钟空闲态为高电平
- 在下降沿采样数据
- 在上升沿发送数据
- 模式3 (CPOL=1, CPHA=1)
- 时钟空闲态为高电平
- 在上升沿采样数据
- 在下降沿发送数据
时序图解释
SPI Timing Diagram
Click to open code
选择建议
- 确保主从设备的模式一致
- 根据从设备的具体通信要求选择
- 模式0最为通用
- 特定外设可能有特定模式要求
配置示例(STM32)
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 时钟极性 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 时钟相位
SPI的学习到此就圆满结束了!相较于I2C,尽管SPI需要多几条物理线路,但它的实现要简单得多。I2C在多设备通信时的复杂性和时序管理常常让人感到头疼,而SPI通过其清晰、简洁的同步时序,极大简化了实现过程。与此不同,USART通信尽管也能够由软件模拟,但相较于SPI和I2C,它的实现要复杂得多。原因在于,USART是一种异步通信协议,没有统一的时钟信号,数据传输完全依赖于发送和接收设备之间的同步,时序完全靠软件来控制和测量,带来了更高的实现难度和出错的风险。
在实际应用中,许多设备的基础通信都使用USART,这也是因为USART协议在很多情况下是最为通用且具备足够灵活性的标准。尽管它实现较为复杂,但其异步特性在一些特定场合(如无需时钟信号、长距离通信等)下仍具有不可替代的优势。因此,虽然USART的时序和同步管理比SPI和I2C复杂,但它仍在很多嵌入式应用中占有一席之地。
进军下一章节!!!!! 时间2024.12.26 20:51 地点:苏州