当前位置: 首页 > article >正文

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根据指定配置初始化 SPISPIx: SPI 外设地址<br>SPI_InitStruct: SPI 配置结构体配置 SPI 通信参数,如时钟极性、数据大小等
I2S_Init根据指定配置初始化 I2SSPIx: SPI 外设地址<br>I2S_InitStruct: I2S 配置结构体配置 I2S 通信参数,如标准、数据长度等
SPI_StructInit填充 SPI 初始化结构体默认值SPI_InitStruct: SPI 配置结构体指针在手动配置 SPI 前,快速获取默认配置
SPI_I2S_StructInit填充 I2S 初始化结构体默认值I2S_InitStruct: I2S 配置结构体指针在手动配置 I2S 前,快速获取默认配置

控制和命令函数

 
 
函数名功能参数介绍使用方法
SPI_Cmd使能或禁用 SPISPIx: SPI 外设地址<br>NewState: 使能/禁用状态开启或关闭 SPI 外设
I2S_Cmd使能或禁用 I2SSPIx: 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发送 CRCSPIx: 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_AIN0x0模拟输入模式用于模拟信号输入,如传感器、ADC采样
GPIO_Mode_IN_FLOATING0x04浮空输入模式外部悬空输入,无上拉/下拉电阻
GPIO_Mode_IPD0x28下拉输入模式输入引脚默认为低电平
GPIO_Mode_IPU0x48上拉输入模式输入引脚默认为高电平
GPIO_Mode_Out_OD0x14开漏输出模式需要外部上拉电阻,常用于 I2C 通信
GPIO_Mode_Out_PP0x10推挽输出模式通用数字输出模式,驱动能力强
GPIO_Mode_AF_OD0x1C开漏复用功能复用功能的开漏输出,如 I2C
GPIO_Mode_AF_PP0x18推挽复用功能复用功能的推挽输出,如 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通信
}

详细解释:

  1. 时钟使能

    • RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE):启用GPIOA外设时钟,以便能够配置和使用GPIOA引脚。
    • RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE):启用SPI1外设时钟,确保SPI外设可以正常工作。
  2. 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外设的选通。
  3. 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 表示数据将在时钟的第一沿变化时采样。
  4. 使能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);  // 返回接收到的字节数据
}

详细解释:

  1. 等待发送寄存器空

    • SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE):检查SPI1的数据发送寄存器(TXE标志)是否为空。如果为“空”(即可以发送数据),则返回SET,否则继续等待。
    • 这是通过循环检查,确保数据寄存器准备好可以发送数据,避免数据溢出或丢失。
  2. 发送数据

    • SPI_I2S_SendData(SPI1, Byte):将参数 Byte 中的数据通过SPI总线发送出去。SPI的发送操作是同步的,当数据发送寄存器可用时,数据将被传送到外部设备。
  3. 等待接收寄存器非空

    • SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE):检查SPI1的接收数据寄存器(RXNE标志)是否非空。若接收寄存器有数据(即数据已经接收到),则返回SET,否则继续等待。
    • 这确保了只有在数据完全接收后,才进行下一步操作。
  4. 读取接收数据

    • 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):时钟相位

详细模式说明

  1. 模式0 (CPOL=0, CPHA=0)
  • 时钟空闲态为低电平
  • 在上升沿采样数据
  • 在下降沿发送数据
  • 最常用的模式
  1. 模式1 (CPOL=0, CPHA=1)
  • 时钟空闲态为低电平
  • 在下降沿采样数据
  • 在上升沿发送数据
  1. 模式2 (CPOL=1, CPHA=0)
  • 时钟空闲态为高电平
  • 在下降沿采样数据
  • 在上升沿发送数据
  1. 模式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 地点:苏州


http://www.kler.cn/a/429892.html

相关文章:

  • 如何实现多级缓存?
  • 代码随想录算法训练营第三十二天|509.斐波那契数、70.爬楼梯、746.使用最小花费爬楼梯
  • 湘潭大学人机交互复习
  • 【网络篇】TCP知识
  • 嵌入式驱动开发详解13(IIC驱动架构实现)
  • 掌握小程序地理位置服务插件,让用户体验再升级
  • 搭建Node.js后端
  • EasyExcel改名为FastExce做了那些改变呢
  • 【深度学习】深入解析卷积神经网络(CNNs)
  • 【语音识别】搭建本地的语音转文字系统:FunASR(离线不联网即可使用)
  • Kubernetes(K8s)
  • 从爱尔兰歌曲到莎士比亚:LSTM文本生成模型的优化之旅
  • Github 2024-12-06Java开源项目日报Top10
  • C#实现的ACCESS的增删改查基本功能
  • SVN Update 报错解决三部曲
  • CV(3)--噪声滤波和特征
  • 大数据-245 离线数仓 - 电商分析 缓慢变化维 与 拉链表 SCD Slowly Changing Dimensions
  • 项目中使用AntV L7地图(五)添加飞线
  • Linux驱动开发(12):中断子系统–按键中断实验
  • 超标量处理器设计笔记(5)虚拟存储器、地址转换、page fault
  • LeetCode:459.重复的子字符串
  • 提升网站流量的关键:AI在SEO关键词优化中的应用
  • Halcon 深度学习目标分类:原理与应用全解析