016 - STM32学习笔记 - SPI读写FLASH(一)

news/2025/1/10 19:41:32/

016 - STM32学习笔记 - SPI访问Flash(一)

之前csdn的名称是宥小稚,后来改成放学校门口见了,所以前面内容看到图片水印不要在意,都是自己学习过程中整理的,不涉及版权啥的。

1、什么是SPI?

SPI 协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设备接口,是一种高速全双工的通信总线。

在这里插入图片描述

在SPI总线中,一共有四条线:

SCLK:同步时钟信号线,用于通讯数据同步。由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,如 STM32 的 SPI 时钟频率最大为fpclk/2,两个设备之间通讯时,通讯速率由最低速率设备决定。

CS:片选信号线,或设备选择线,也称为SS、NSS,当多个SPI从机设备与SPI主机相连时,设备的其他信号线SCLK、MOSI、MISO同时并联到相同的SPI总线上,无论多少个从机设备,这三条总线都是公用的,唯独CS线都是从机设备与主机设备的一对一连接,SPI总线上有多少从机设备,就有多少根CS线。I2C中主机通过设备地址来寻址进行通讯;SPI中没有设备地址,只使用CS信号线进行片选,当主机要与某个从机设备进行通信时,则将该从机设备的CS信号线设置为低电平,就表示该设备被选中,接着就可以开始通讯了,当通讯结束后,CS线被拉高,则表示结束信号。

MOSI:Master Output,Slave Input,主设备输出/从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,数据方向为主机到从机

MISO:Master Input,Slave Output,主设备输入/从设备输出引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,数据方向为从机到主机

2、SPI协议

在这里插入图片描述

SPI通讯过程

在SPI通讯过程中,CS、SCK、MOSI信号都是由主机产生,MISO信号是由从机产生,其中MOSI和MISO信号只有在CS片选信号为低电平时有效,在SCLK每个信号周期,MOSI和MISO都可以传输一位数据,因此SPI总线可以同时收发数据。

a、通讯起始与结束信号:

根据SPI通讯过程的图来看,当SPI总线无通信时,CS片选信号线一直保持高电平,当开始通信时,CS信号线跳变为低电平,此时SCLK、MOSI、MISO信号线上的信号才有效,通信结束后,CS跳变为高电平,表示此次通信结束了。

b、有效数据:

在SPI中,当CS片选信号低电平时,MOSI和MISO数据线在SCLK的每个时钟周期都会传输一位数据,并且MOSI和MISO上的信号时同时进行传输的,MSB(高位)先行或LSB(地位)先行并没有硬性规定,但要保证两个SPI设备之间使用同样的协定,就跟上图一样,采用MSB先行模式了。

c、CPOL/CPHA

下面要学习关于SPI的四种通讯模式,上图中的时序时SPI的其中一种通讯模式,四种通讯模式的主要区别在于总线空闲时,SCLK的时钟状态以及数据采样时刻。在此之前先看一下CPOL(时钟极性)和CPHA(时钟相位)。

CPOL:是指SPI通讯设备处于空闲状态时,SCK信号线的的电平信号(就是CS片选信号是高电平时,SCLK的状态),当CS信号线为高电平时,CPOL = 0,SCLK则为低电平;CPOL = 1,SCLK则为高电平;

CPHA:时钟相位实际指的就是数据采样的时间,当CPHA = 0的时候,采样信号在SCLK时钟线的奇数边沿,当CPHA = 1的时候,采样信号则是在SCLK时钟线的偶数边沿。

在这里插入图片描述

CPHA = 0

在这里插入图片描述

CPHA = 1

所以根据CPOL和CPHA的不同状态可以组合出四种模式,如下表

模式CPOLCPHA空闲时SCLK采样时刻
000低电平奇数边沿
101低电平偶数边沿
210高电平奇数边沿
311高电平偶数边沿

在实际应用中,模式0模式3 用的比较多。

3、SPI框图

在这里插入图片描述

a、SPI引脚

前面已经介绍过SPI的四个引脚了,这里不多做赘述,主要看一下四个引脚在F429里面的分布,F429中,SPI一共由6组,其中SPI1、SPI4、SPI5、SPI6挂载在APB2总线上,最高通信速率可以达到45MBtis/s,SPI2、SPI3是挂载在APB1上,最高通信速率为22.5MBtis/s。

在这里插入图片描述

引脚SPI1 SPI2 SPI3 SPI4 SPI5 SPI6
MOSIPA7/PB5PB15/PC3/PI3PB5/PC12/PD6PE6/PE14PF9/PF11PG14
MISOPA6/PB4PB14/PC2/PI2PB4/PC11PE5/PE13PF8/PH7PG12
SCKPA5/PB3PB10/PB13/PD3PB3/PC10PE2/PE12PF7/PH6PG13
NSSPA4/PA15PB9/PB12/PI0PA4/PA15PE4/PE11PF6/PH5PG8
SPI引脚表

b、时钟控制逻辑

SPI的时钟信号由SCLK线发出,内部通过波特率发生器根据控制寄存器CR1中的BR[2:0]位控制,该位是对fpclk时钟的分频因子,对fpclk的分频结果就是SCLK引脚输出的频率。

在这里插入图片描述

BR[2:0]分频结果(SCLK频率)BR[2:0]分频结果(SCLK频率)
000fpclk/2100fpclk/32
001fpclk/4101fpclk/64
010fpclk/8110fpclk/128
011fpclk/16111fpclk/256

c、数据控制逻辑

在SPI框图中可以看到,SPI的MOSI和MISO都是连接到数据移位寄存器上,而数据移位寄存器中的数据,来源于接收缓冲区、发送缓冲区以及MOSI和MISO线,向外发送数据时,数据向外发送时,数据移位寄存器将“发送缓冲区”的数据作为数据源,将数据按位发送出去;接收数据时,数据移位寄存器从MISO数据线上将数据按位通过“数据移位寄存器”采集到“接收缓冲区”。

此处需要注意,数据帧的长度可以通过控制寄存器CR1DFF[11]位配置成8位或16位模式,通过配置“LSBFIRST [7]位可选择 MSB 先行还是 LSB 先行。

4、SPI结构体

SPI结构体声明在stm32f4xx_spi.h中,内容如下:

typedef struct
{uint16_t SPI_Direction;           /* SPI单、双向模式 */uint16_t SPI_Mode;                /* SPI主/从机模式 */uint16_t SPI_DataSize;            /* SPI数据帧长度 */uint16_t SPI_CPOL;                /* 时钟极性CPOL设置,可选高/低电平 */uint16_t SPI_CPHA;                /* 时钟相位CPHA设置,可选奇/偶边沿采样 */uint16_t SPI_NSS;                 /* 设置CS引脚由SPI硬件控制还是软件控制 */uint16_t SPI_BaudRatePrescaler;   /* 设置时钟分频因子,fpclk/分频=fsck */uint16_t SPI_FirstBit;            /* 设置MSB/LSB先行 */uint16_t SPI_CRCPolynomial;       /* 设置CRC校验的表达式 */
}SPI_InitTypeDef;

a、SPI单双向选择(SPI_Direction):

在固件库中提供了以下四种模式

  • 双线全双工(SPI_Direction_2Lines_FullDuplex)
  • 双线只接收(SPI_Direction_2Lines_RxOnly)
  • 单线只接收(SPI_Direction_1Line_Rx)
  • 单线只发送模式(SPI_Direction_1Line_Tx)

b、SPI片选信号控制选择(SPI_NSS)

可选有两种模式:

  • 硬件模式(SPI_NSS_Hard )
  • 软件模式(SPI_NSS_Soft )

其中硬件模式中的片选信号是由SPI硬件自动产生,而软件模式则是由用户将GPIO的引脚电平拉高或者拉低产生信号,常用的为软件模式。

5、编程测试

在这里插入图片描述

在F429的核心板上可以看到,PF6、7、8、9引脚分别对应SPI的CS(片选)、CLK(时钟)、MOSI(输入)、MISO(输出)功能,另外Flash还有两个引脚WP(写保护控制)和HOLD(暂停通讯控制),当WP为低电平时,禁止数据写入,HOLD为低电平时,通讯暂停,MISO输出为高阻态,时钟和输入无效,这里用不到,所以直接接电源拉成高电平。

相关宏定义,在bsp_spi_flash.h中

//SPI 号及时钟初始化函数
#define FLASH_SPI                       SPI5									//根据SPI引脚表可以看到PF6-9都是挂载在SPI5下
#define FLASH_SPI_CLK                   RCC_APB2Periph_SPI5						 //SPI5时钟
//SCK 引脚PF7
#define FLASH_SPI_SCK_PIN               GPIO_Pin_7
#define FLASH_SPI_SCK_GPIO_PORT         GPIOF
#define FLASH_SPI_SCK_GPIO_CLK          RCC_AHB1Periph_GPIOF
#define FLASH_SPI_SCK_PINSOURCE         GPIO_PinSource7
#define FLASH_SPI_SCK_AF                GPIO_AF_SPI5
//MISO 引脚PF8
#define FLASH_SPI_MISO_PIN              GPIO_Pin_8
#define FLASH_SPI_MISO_GPIO_PORT        GPIOF
#define FLASH_SPI_MISO_GPIO_CLK         RCC_AHB1Periph_GPIOF
#define FLASH_SPI_MISO_PINSOURCE        GPIO_PinSource8
#define FLASH_SPI_MISO_AF               GPIO_AF_SPI5
//MOSI 引脚PF9
#define FLASH_SPI_MOSI_PIN              GPIO_Pin_9
#define FLASH_SPI_MOSI_GPIO_PORT        GPIOF
#define FLASH_SPI_MOSI_GPIO_CLK         RCC_AHB1Periph_GPIOF
#define FLASH_SPI_MOSI_PINSOURCE        GPIO_PinSource9
#define FLASH_SPI_MOSI_AF               GPIO_AF_SPI5
//CS(NSS)引脚PF6
#define FLASH_CS_PIN                    GPIO_Pin_6
#define FLASH_CS_GPIO_PORT              GPIOF
#define FLASH_CS_GPIO_CLK               RCC_AHB1Periph_GPIOF#define SPI_FLASH_CS_LOW()              {FLASH_CS_GPIO_PORT->BSRRH=FLASH_CS_PIN;} 	/*控制CS片选信号输出低电平*/
#define SPI_FLASH_CS_HIGH()             {FLASH_CS_GPIO_PORT->BSRRL=FLASH_CS_PIN;}	/*控制CS片选信号输出高电平*/

相关GPIO配置及SPI的配置和初始化

void SPI_GPIO_Config(void)
{GPIO_InitTypeDef SPI_GPIO_Initstructure;SPI_InitTypeDef SPI_InitStructure;/*  SPI四个引脚时钟使能,只要是外设,第一步一定是开启时钟!!! */RCC_AHB1PeriphClockCmd(FLASH_SPI_SCK_GPIO_CLK | FLASH_SPI_MISO_GPIO_CLK|FLASH_SPI_MOSI_GPIO_CLK|FLASH_CS_GPIO_CLK, ENABLE);/* 将SCLK、MISO、MOSI引脚都连接到复用功能,CS片选信号由软件控制,不需要 */GPIO_PinAFConfig(FLASH_SPI_SCK_GPIO_PORT,FLASH_SPI_SCK_PINSOURCE,FLASH_SPI_SCK_AF);GPIO_PinAFConfig(FLASH_SPI_MISO_GPIO_PORT,FLASH_SPI_MISO_PINSOURCE,FLASH_SPI_MISO_AF);GPIO_PinAFConfig(FLASH_SPI_MOSI_GPIO_PORT,FLASH_SPI_MOSI_PINSOURCE,FLASH_SPI_MOSI_AF);/* 配置SCLK信号引脚 */SPI_GPIO_Initstructure.GPIO_Pin = FLASH_SPI_SCK_PIN;SPI_GPIO_Initstructure.GPIO_Speed = GPIO_Speed_50MHz;SPI_GPIO_Initstructure.GPIO_Mode = GPIO_Mode_AF;SPI_GPIO_Initstructure.GPIO_OType = GPIO_OType_PP;SPI_GPIO_Initstructure.GPIO_PuPd = GPIO_PuPd_NOPULL;/* 初始化SCLK引脚 */GPIO_Init(FLASH_SPI_SCK_GPIO_PORT, &SPI_GPIO_Initstructure);/* 配置并初始化MISO引脚 */SPI_GPIO_Initstructure.GPIO_Pin = FLASH_SPI_MISO_PIN;GPIO_Init(FLASH_SPI_MISO_GPIO_PORT, &SPI_GPIO_Initstructure);/* 配置并初始化MOSI引脚 */SPI_GPIO_Initstructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;GPIO_Init(FLASH_SPI_MOSI_GPIO_PORT, &SPI_GPIO_Initstructure);/* 配置CS引脚为输出模式并初始化,这里片选由软件自行控制,因此此处模式选择为输出 */SPI_GPIO_Initstructure.GPIO_Pin = FLASH_CS_PIN;SPI_GPIO_Initstructure.GPIO_Mode = GPIO_Mode_OUT;GPIO_Init(FLASH_CS_GPIO_PORT, &SPI_GPIO_Initstructure);/* 将CS片选信号置位高电平,非选中状态 */SPI_FLASH_CS_HIGH();/*------------------------以下为SPI配置内容------------------------*//* SPI时钟使能,只要是外设,第一步一定是开启时钟!!! *//* PF6-9是挂载在SPI5上,因此使能SPI5的时钟 */RCC_APB2PeriphClockCmd(FLASH_SPI_CLK, ENABLE);/* 配置SPI为双线全双工 */SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;/* 配置SPI为主机模式 */SPI_InitStructure.SPI_Mode = SPI_Mode_Master;/* 设置数据帧为8bit */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;/* 设置为2分频 *//*F429的主频是180MHz,那么APB2的时钟fpclk1就是90MHz,而SP1、4、5、6最高频率为45MHz,因此需要2分频;同理如果要用到SPI2、3,最高频率为22.5MHz,	而SPI2、3挂载在AP1,总线时钟为45MHz,如果要达到最高速度,同样要2分频需要注意的是,此处SPI的最大速度取决于总线中设备通讯速率最低的设备 */SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;/* 设置MSB先行 */SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;/* 设置CRC校验多项式,保证通信质量,大于1就行 */SPI_InitStructure.SPI_CRCPolynomial = 7;/* 初始化SPI */SPI_Init(FLASH_SPI, &SPI_InitStructure);/* 使能 FLASH_SPI */SPI_Cmd(FLASH_SPI, ENABLE);
}

首先测试一下通过SPI读取FALSH的Device_ID号,根据FALSH手册,可以看到设备ID是0xEF4018,如下图

在这里插入图片描述

另外在看一下SPI的指令表,在指令表中,可以看到查看FALSH的指令为0x9F:

在这里插入图片描述

如此根据指令表编写读取FALSH的设备ID,可以看到,读取设备ID时,需要下发指令0x9F,之后Byte2-Byte4中的内容组合则为设备ID,时序如下图:

在这里插入图片描述

u32 SPI_FLASH_ReadID(void)
{u8 temp[3] = {0x00,0x00,0x00};/* 开始通讯:拉低片选信号CS */SPI_FLASH_CS_LOW();/* 发送 JEDEC 指令,读取 ID */SPI_FLASH_SendByte(0x9F);/* 读取一个字节数据 */temp[0] = SPI_FLASH_SendByte(Dummy_Byte);/* 读取一个字节数据 */temp[1] = SPI_FLASH_SendByte(Dummy_Byte);/* 读取一个字节数据 */temp[2] = SPI_FLASH_SendByte(Dummy_Byte);/* 停止通讯:拉高片选信号CS */SPI_FLASH_CS_HIGH();/* 组合数据返回 */return (temp[0] << 16) | (temp[1] << 8) | temp[2];
}

这段程序中,如果直接使用库函数SPI_I2S_ReceiveData去读取数据的话,发现读取回来的数据不是我们要的0xEF4018,因为在每次数据发送完成后,需要等待发送缓冲区为空时,才可以发送下一个要发送的数据,因此跟I2C的一样,我们需要去检测一下发送缓冲区是否为空,当发送缓冲区为空时,硬件会将TXE标志置1,因此需要调用SPI_GetFlagStatus去获取TXE状态,为此在这里实现SPI_FLASH_SendByte来尝试读取设备ID:

u8 SPI_FLASH_SendByte(u8 byte)
{SPITimeout = SPIT_FLAG_TIMEOUT;/* 等待发送缓冲区为空, TXE 事件 */while (SPI_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_TXE) == RESET){if ((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);}/* 写入数据寄存器,把要写入的数据写入发送缓冲区 */SPI_I2S_SendData(FLASH_SPI, byte);SPITimeout = SPIT_FLAG_TIMEOUT;/* 等待接收缓冲区非空, RXNE 事件 */while (SPI_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_RXNE) == RESET){if ((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);}/* 读取数据寄存器,获取接收缓冲区数据 */return SPI_I2S_ReceiveData(FLASH_SPI);
}

实现方法有点类似I2C,代码大家看一下I2C那节的内容就能理解,关于超时检测和SPI_TIMEOUT_UserCallback回调函数的实现也可以看一下I2C那节的内容。

到这里在到main函数中测试一下:

#include "stm32f4xx.h"
#include "bsp_led.h"
#include "bsp_systick.h"
#include "bsp_usart_dma.h"
#include "bsp_spi_flash.h"
#include <stdio.h>int main(void)
{u32 device_id = 0;LED_Config();DEBUG_USART1_Config();SysTick_Init();  SPI_GPIO_Config();printf("这是SPI读取FLASH_Device_ID的测试实验!\n");device_id = SPI_FLASH_ReadID();printf("device_id = 0x%X\n",device_id);while(1){if(device_id == 0xef4018){LED_G_TOGGLEDelay_ms(1000);}else{LED_R_TOGGLEDelay_ms(1000);}}
}

在这里插入图片描述

OK,这节内容学习完毕!

根据上述内容,总给下SPI的操作顺序:

1、配置SPI对应的四个GPIO;(只要是外设,第一步一定要先打开时钟!!!)

2、将三个GPIO连接到SPI复用功能(CS引脚我们用软件自己控制,就不需要连接了!)

3、配置SPI相关参数(模式按照模式0或者模式3来配置,速度按照SPI最大速度来执行,记得要打开SPI的时钟!)

4、编写发送和读取数据的功能函数。


http://www.ppmy.cn/news/799258.html

相关文章

excel表格一个格子怎么分成两行打字mac

按住&#xff1a;control&#xff0c;option&#xff0c;enter

excel常用技巧:空格填充(根据上一格)

手机品牌通信品牌用户数HTC移动179 联通59 电信 华为移动12 联通200 电信162 表格如上&#xff08;注&#xff1a;所有数据全部虚构&#xff09; 1.选中手机平牌一列&#xff0c;ctrlg定位到空格 2.A2&#xff08;指第一个HTC那个空格&#xff09; 3.ctrlenter 手机品牌通…

在excel中,将 长*宽*高 这样的格式列,拆分成3列

在excel中,将 长*宽*高 这样的格式列,拆分成3列 MID(D6,1,FIND("*",D6)-1)长 MID($D6,FIND("*",D6)1,FIND("*",D6,FIND("*",D6)1)-FIND("*",D6)-1)宽 MID($D6,FIND("*",D6,FIND("*",D6)1)1,LEN(D6))高 …

EXCEL中如何根据标点符号进行表格分列

1.点击【数据】选项卡&#xff0c;在其中的【数据工具】选项中选择【分列】选项。 2.如下图&#xff0c;弹出【文本分列向导-步骤之1】对话框&#xff0c;勾选【分隔符号】选项&#xff0c;然后点击【下一步】选项 3.如下图&#xff0c;弹出【文本分列向导-步骤之2】对话框&a…

Excel单元格斜线分割线怎样插入?干货分享!如何将斜线分隔线加入单元格?

对于excel表格文件我们大家平时记录一些信息、数据经常会用的到&#xff0c;但是一些特殊的符号&#xff0c;斜线分割线应该怎样插入呢&#xff1f;下面小编分享给大家这一个小方法。 首先我们打开excel表格软件&#xff0c;进入需要编辑的表格当中&#xff0c;或者新建一个空白…

Excel表格左上角+绿色小标倒三角----

哎&#xff0c;写Excel的时候总是忘记加个绿色的小标 百度搜了半天不知道该用啥! 感觉好复杂的样子哦&#xff0c;又是半角&#xff0c;又是函数的 还是来个简单的 首先你写个1&#xff0c;是这样子的是吧 但是右键——设置单元格格式——点击文本——点击确定&#xff0c;之…

Excel按单元格背景颜色查找单个单元格的2种操作

今天小编要和大家分享的是&#xff0c;Excel按单元格背景颜色查找单个单元格的2种操作&#xff0c;比如说下图的黄色背景填充的单元格&#xff0c;该如何定位查找呢 (查找对话框) (方方格子插件) 1.先看动图演示吧 2.我们可以按CtrlF打开查找对话框&#xff0c;然后选择格式按…

三斜线表头表格HTML,excel三栏斜线表头的完美制作方法

在Excel中制作三栏斜线表头的方法有很多&#xff0c;今天介绍一种认为最完美最简单的制作excel三栏斜线表头的方法。请看本文教程&#xff1a;excel三栏斜线表头的完美制作方法。 操作步骤&#xff1a; 步骤一&#xff1a;边框线的添加。边框线的添加别无选择&#xff0c;就使用…