SPI—读写串行FLASH

2023-05-16

1 SPI协议简介

SPI协议是由摩托罗拉公司提出的通讯协议(SerialPeripheralInterface),即串行外围设备接口,是一种高速全双工的通信总线。它被广泛地使用在ADC、LCD等设备与MCU间,要求通讯速率较高的场合。

1.1 SPI物理层

SPI通讯设备之间的常用连接方式

SPI通讯使用3条总线及片选线,3条总线分别为SCK、MOSI、MISO,片选线为,它们的作用介绍如下:

(1)(SlaveSelect):从设备选择信号线,常称为片选信号线,也称为NSS、CS,以下用NSS表示。当有多个SPI从设备与SPI主机相连时,设备的其它信号线SCK、MOSI及MISO同时并联到相同的SPI总线上,即无论有多少个从设备,都共同只使用这3条总线;而每个从设备都有独立的这一条NSS信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。I2C协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而SPI协议中没有设备地址,它使用NSS信号线来寻址,当主机要选择从设备时,把该从设备的NSS信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行SPI通讯。所以SPI通讯以NSS线置低电平为开始信号,以NSS线被拉高作为结束信号。

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

(3)MOSI(MasterOutput,SlaveInput):主设备输出/从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,即这条线上数据的方向为主机到从机。

(4)MISO(MasterInput,,SlaveOutput):主设备输入/从设备输出引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。

1.2 协议层

与I2C的类似,SPI协议定义了通讯的起始和停止信号、数据有效性、时钟同步等环节。

1.2.1 SPI基本通讯过程

SPI通讯的通讯时序

 这是一个主机的通讯时序。NSS、SCK、MOSI信号都由主机控制产生,而MISO的信号由从机产生,主机通过该信号线读取从机的数据。MOSI与MISO的信号只在NSS为低电平的时候才有效,在SCK的每个时钟周期MOSI和MISO传输一位数据。

以上通讯流程中包含的各个信号分解如下:

1.2.2 通讯的起始和停止信号

在图SPI通讯时序中的标号处,NSS信号线由高变低,是SPI通讯的起始信号。NSS是每个从机各自独占的信号线,当从机在自己的NSS线检测到起始信号后,就知道自己被主机选中了,开始准备与主机通讯。在图中的标号处,NSS信号由低变高,是SPI通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。

1.2.3 数据有效性

SPI使用MOSI及MISO信号线来传输数据,使用SCK信号线进行数据同步。MOSI及MISO数据线在SCK的每个时钟周期传输一位数据,且数据输入输出是同时进行的。数据传输时,MSB先行或LSB先行并没有作硬性规定,但要保证两个SPI通讯设备之间使用同样的协定,一般都会采用图SPI通讯时序中的MSB先行模式。

观察图中的标号处,MOSI及MISO的数据在SCK的上升沿期间变化输出,在SCK的下降沿时被采样。即在SCK的下降沿时刻,MOSI及MISO的数据有效,高电平时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效,MOSI及MISO为下一次表示数据做准备。

SPI每次数据传输可以8位或16位为单位,每次传输的单位数不受限制。

1.2.4 CPOL/CPHA及通讯模式

上面讲述的图SPI通讯时序中的时序只是SPI中的其中一种通讯模式,SPI一共有四种通讯模式,它们的主要区别是总线空闲时SCK的时钟状态以及数据采样时刻。为方便说明,在此引入“时钟极性CPOL”和“时钟相位CPHA”的概念。

时钟极性CPOL是指SPI通讯设备处于空闲状态时,SCK信号线的电平信号(即SPI通讯开始前、NSS线为高电平时SCK的状态)。CPOL=0时,SCK在空闲状态时为低电平,CPOL=1时,则相反。

时钟相位CPHA是指数据的采样的时刻,当CPHA=0时,MOSI或MISO数据线上的信号将会在SCK时钟线的“奇数边沿”被采样。当CPHA=1时,数据线在SCK的“偶数边沿”采样。见图CPHA=0时的SPI通讯模式及图CPHA=1时的SPI通讯模式。

 我们来分析这个CPHA=0的时序图。首先,根据SCK在空闲状态时的电平,分为两种情况。SCK信号线在空闲状态为低电平时,CPOL=0;空闲状态为高电平时,CPOL=1。

无论CPOL=0还是=1,因为我们配置的时钟相位CPHA=0,在图中可以看到,采样时刻都是在SCK的奇数边沿。注意当CPOL=0的时候,时钟的奇数边沿是上升沿,而CPOL=1的时候,时钟的奇数边沿是下降沿。所以SPI的采样时刻不是由上升/下降沿决定的。MOSI和MISO数据线的有效信号在SCK的奇数边沿保持不变,数据信号将在SCK奇数边沿时被采样,在非采样时刻,MOSI和MISO的有效信号才发生切换。

类似地,当CPHA=1时,不受CPOL的影响,数据信号在SCK的偶数边沿被采样,见图CPHA=1时的SPI通讯模式_。

 由CPOL及CPHA的不同状态,SPI分成了四种模式,见表SPI的四种模式,主机与从机需要工作在相同的模式下才可以正常通讯,实际中采用较多的是“模式0”与“模式3”。

 2  STM32的SPI特性及架构

与I2C外设一样,STM32芯片也集成了专门用于SPI协议通讯的外设。

2.1 STM32的SPI外设简介

STM32的SPI外设可用作通讯的主机及从机,支持最高的SCK时钟频率为fpclk/2(STM32F103型号的芯片默认fpclk1为36MHz,fpclk2为72MHz),完全支持SPI协议的4种模式,数据帧长度可设置为8位或16位,可设置数据MSB先行或LSB先行。它还支持双线全双工(前面小节说明的都是这种模式)、双线单向以及单线模式。其中双线单向模式可以同时使用MOSI及MISO数据线向一个方向传输数据,可以加快一倍的传输速度。而单线模式则可以减少硬件接线,当然这样速率会受到影响。我们只讲解双线全双工模式。

2.2 STM32的SPI架构剖析

2.2.1 通讯引脚

SPI的所有硬件架构都从图SPI架构图中左侧MOSI、MISO、SCK及NSS线展开的。STM32芯片有多个SPI外设,它们的SPI通讯信号引出到不同的GPIO引脚上,使用时必须配置到这些指定的引脚,见表STM32F10x的SPI引脚。关于GPIO引脚的复用功能,可查阅《STM32F10x规格书》,以它为准。

 其中SPI1是APB2上的设备,最高通信速率达36Mbtis/s,SPI2、SPI3是APB1上的设备,最高通信速率为18Mbits/s。除了通讯速率,在其它功能上没有差异。其中SPI3用到了下载接口的引脚,这几个引脚默认功能是下载,第二功能才是IO口,如果想使用SPI3接口,则程序上必须先禁用掉这几个IO口的下载功能。一般在资源不是十分紧张的情况下,这几个IO口是专门用于下载和调试程序,不会复用为SPI3。

2.2.2 时钟控制逻辑

SCK线的时钟信号,由波特率发生器根据“控制寄存器CR1”中的BR[0:2]位控制,该位是对fpclk时钟的分频因子,对fpclk的分频结果就是SCK引脚的输出时钟频率,计算方法见表BR位对fpclk的分频。

 其中的fpclk频率是指SPI所在的APB总线频率,APB1为fpclk1,APB2为fpckl2。

通过配置“控制寄存器CR”的“CPOL位”及“CPHA”位可以把SPI设置成前面分析的4种SPI模式。

2.2.3 数据控制逻辑

SPI的MOSI及MISO都连接到数据移位寄存器上,数据移位寄存器的数据来源及目标接收、发送缓冲区以及MISO、MOSI线。当向外发送数据的时候,数据移位寄存器以“发送缓冲区”为数据源,把数据一位一位地通过数据线发送出去;当从外部接收数据的时候,数据移位寄存器把数据线采样到的数据一位一位地存储到“接收缓冲区”中。通过写SPI的“数据寄存器DR”把数据填充到发送缓冲区中,通讯读“数据寄存器DR”,可以获取接收缓冲区中的内容。其中数据帧长度可以通过“控制寄存器CR1”的“DFF位”配置成8位及16位模式;配置“LSBFIRST位”可选择MSB先行还是LSB先行。

2.2.4 整体控制逻辑

整体控制逻辑负责协调整个SPI外设,控制逻辑的工作模式根据我们配置的“控制寄存器(CR1/CR2)”的参数而改变,基本的控制参数包括前面提到的SPI模式、波特率、LSB先行、主从模式、单双向模式等等。在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器(SR)”,我们只要读取状态寄存器相关的寄存器位,就可以了解SPI的工作状态了。除此之外,控制逻辑还根据要求,负责控制产生SPI中断信号、DMA请求及控制NSS信号线。

实际应用中,我们一般不使用STM32SPI外设的标准NSS信号线,而是更简单地使用普通的GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。

2.3 通讯过程

STM32使用SPI外设通讯时,在通讯的不同阶段它会对“状态寄存器SR”的不同数据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。

图主发送器通讯过程中的是“主模式”流程,即STM32作为SPI通讯的主机端时的数据收发过程。

 主模式收发流程及事件说明如下:

(1)控制NSS信号线,产生起始信号(图中没有画出);

(2)把要发送的数据写入到“数据寄存器DR”中,该数据会被存储到发送缓冲区;

(3)通讯开始,SCK时钟开始运行。MOSI把发送缓冲区中的数据一位一位地传输出去;MISO则把数据一位一位地存储进接收缓冲区中;

(4)当发送完一帧数据的时候,“状态寄存器SR”中的“TXE标志位”会被置1,表示传输完一帧,发送缓冲区已空;类似地,当接收完一帧数据的时候,“RXNE标志位”会被置1,表示传输完一帧,接收缓冲区非空;

(5)等待到“TXE标志位”为1时,若还要继续发送数据,则再次往“数据寄存器DR”写入数据即可;等待到“RXNE标志位”为1时,通过读取“数据寄存器DR”可以获取接收缓冲区中的内容。假如我们使能了TXE或RXNE中断,TXE或RXNE置1时会产生SPI中断信号,进入同一个中断服务函数,到SPI中断服务程序后,可通过检查寄存器位来了解是哪一个事件,再分别进行处理。也可以使用DMA方式来收发“数据寄存器DR”中的数据。

3  SPI初始化结构体详解

跟其它外设一样,STM32标准库提供了SPI初始化结构体及初始化函数来配置SPI外设。初始化结构体及函数定义在库文件“stm32f4xx_spi.h”及“stm32f4xx_spi.c”中,编程时我们可以结合这两个文件内的注释使用或参考库帮助文档。了解初始化结构体后我们就能对SPI外设运用自如了

SPI初始化结构体

 这些结构体成员说明如下,其中括号内的文字是对应参数在STM32标准库中定义的宏:(1)SPI_Direction

本成员设置SPI的通讯方向,可设置为双线全双工(SPI_Direction_2Lines_FullDuplex),双线只接收(SPI_Direction_2Lines_RxOnly),单线只接收(SPI_Direction_1Line_Rx)、单线只发送模式(SPI_Direction_1Line_Tx)。

(2)SPI_Mode

本成员设置SPI工作在主机模式(SPI_Mode_Master)或从机模式(SPI_Mode_Slave),这两个模式的最大区别为SPI的SCK信号线的时序,SCK的时序是由通讯中的主机产生的。若被配置为从机模式,STM32的SPI外设将接受外来的SCK信号。

(3)SPI_DataSize

本成员可以选择SPI通讯的数据帧大小是为8位(SPI_DataSize_8b)还是16位(SPI_DataSize_16b)。

(4)SPI_CPOL和SPI_CPHA

这两个成员配置SPI的时钟极性CPOL和时钟相位CPHA,这两个配置影响到SPI的通讯模式,关于CPOL和CPHA的说明参考前面“通讯模式”小节。时钟极性CPOL成员,可设置为高电平(SPI_CPOL_High)或低电平(SPI_CPOL_Low)。时钟相位CPHA则可以设置为SPI_CPHA_1Edge(在SCK的奇数边沿采集数据)或SPI_CPHA_2Edge(在SCK的偶数边沿采集数据)。

(5)SPI_NSS

本成员配置NSS引脚的使用模式,可以选择为硬件模式(SPI_NSS_Hard)与软件模式(SPI_NSS_Soft),在硬件模式中的SPI片选信号由SPI硬件自动产生,而软件模式则需要我们亲自把相应的GPIO端口拉高或置低产生非片选和片选信号。实际中软件模式应用比较多。

(6)SPI_BaudRatePrescaler

本成员设置波特率分频因子,分频后的时钟即为SPI的SCK信号线的时钟频率。这个成员参数可设置为fpclk的2、4、6、8、16、32、64、128、256分频。

(7)SPI_FirstBit

所有串行的通讯协议都会有MSB先行(高位数据在前)还是LSB先行(低位数据在前)的问题,而STM32的SPI模块可以通过这个结构体成员,对这个特性编程控制。

(8)SPI_CRCPolynomial

这是SPI的CRC校验中的多项式,若我们使用CRC校验时,就使用这个成员的参数(多项式),来计算CRC的值。

配置完这些结构体成员后,我们要调用SPI_Init函数把这些参数写入到寄存器中,实现SPI的初始化,然后调用SPI_Cmd来使能SPI外设。

4  SPI—读写串行FLASH实验

FLSAH存储器又称闪存,它与EEPROM都是掉电后数据不丢失的存储器,但FLASH存储器容量普遍大于EEPROM,现在基本取代了它的地位。我们生活中常用的U盘、SD卡、SSD固态硬盘以及我们STM32芯片内部用于存储程序的设备,都是FLASH类型的存储器。在存储控制上,最主要的区别是FLASH芯片只能一大片一大片地擦写,而在“I2C”中我们了解到EEPROM可以单个字节擦写。

本小节以一种使用SPI通讯的串行FLASH存储芯片的读写实验为大家讲解STM32的SPI使用方法。实验中STM32的SPI外设采用主模式,通过查询事件的方式来确保正常通讯。

4.1 硬件设计

 本实验板中的FLASH芯片(型号:W25Q64)是一种使用SPI通讯协议的NORFLASH存储器,它的CS/CLK/DIO/DO引脚分别连接到了STM32对应的SPI引脚NSS/SCK/MOSI/MISO上,其中STM32的NSS引脚是一个普通的GPIO,不是SPI的专用NSS引脚,所以程序中我们要使用软件控制的方式。

FLASH芯片中还有WP和HOLD引脚。WP引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。HOLD引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。

关于FLASH芯片的更多信息,可参考其数据手册《W25Q64》来了解。若您使用的实验板FLASH的型号或控制引脚不一样,只需根据我们的工程修改即可,程序的控制原理相同。

4.2 软件设计

为了使工程更加有条理,我们把读写FLASH相关的代码独立分开存储,方便以后移植。在“工程模板”之上新建“bsp_spi_flash.c”及“bsp_spi_flash.h”文件,这些文件也可根据您的喜好命名,它们不属于STM32标准库的内容,是由我们自己根据应用需要编写的。

4.2.1 编程要点

(1)初始化通讯使用的目标引脚及端口时钟;

(2)使能SPI外设的时钟;

(3)配置SPI外设的模式、地址、速率等参数并使能SPI外设;

(4)编写基本SPI按字节收发的函数;

(5)编写对FLASH擦除及读写操作的的函数;

(6)编写测试程序,对读写数据进行校验。

4.2.2 代码分析

SPI硬件相关宏定义

我们把SPI硬件相关的配置都以宏的形式定义到“bsp_spi_flash.h”文件中

SPI硬件配置相关的宏

 以上代码根据硬件连接,把与FLASH通讯使用的SPI号、GPIO等都以宏封装起来,并且定义了控制CS(NSS)引脚输出电平的宏,以便配置产生起始和停止信号时使用。

 初始化SPI的GPIO

利用上面的宏,编写SPI的初始化函数

SPI的初始化函数(GPIO初始化部分)

 

 与所有使用到GPIO的外设一样,都要先把使用到的GPIO引脚模式初始化,配置好复用功能。GPIO初始化流程如下:

(1)使用GPIO_InitTypeDef定义GPIO初始化结构体变量,以便下面用于存储GPIO配置;

(2)调用库函数RCC_APB2PeriphClockCmd来使能SPI引脚使用的GPIO端口时钟。

(3)向GPIO初始化结构体赋值,把SCK/MOSI/MISO引脚初始化成复用推挽模式。而CS(NSS)引脚由于使用软件控制,我们把它配置为普通的推挽输出模式。

(4)使用以上初始化结构体的配置,调用GPIO_Init函数向寄存器写入参数,完成GPIO的初始化。

配置SPI的模式

以上只是配置了SPI使用的引脚,对SPI外设模式的配置。在配置STM32的SPI模式前,我们要先了解从机端的SPI模式。本例子中可通过查阅FLASH数据手册《W25Q64》获取。根据FLASH芯片的说明,它支持SPI模式0及模式3,支持双线全双工,使用MSB先行模式,支持最高通讯时钟为104MHz,数据帧长度为8位。我们要把STM32的SPI外设中的这些参数配置一致。

配置SPI模式

 

 这段代码中,把STM32的SPI外设配置为主机端,双线全双工模式,数据帧长度为8位,使用SPI模式3(CPOL=1,CPHA=1),NSS引脚由软件控制以及MSB先行模式。代码中把SPI的时钟频率配置成了4分频,实际上可以配置成2分频以提高通讯速率,读者可亲自尝试一下。最后一个成员为CRC计算式,由于我们与FLASH芯片通讯不需要CRC校验,并没有使能SPI的CRC功能,这时CRC计算式的成员值是无效的。

赋值结束后调用库函数SPI_Init把这些配置写入寄存器,并调用SPI_Cmd函数使能外设。

使用SPI发送和接收一个字节的数据

初始化好SPI外设后,就可以使用SPI通讯了,复杂的数据通讯都是由单个字节数据收发组成的,我们看看它的代码实现

使用SPI发送和接收一个字节的数据

 

SPI_FLASH_SendByte发送单字节函数中包含了等待事件的超时处理,这部分原理跟I2C中的一样,在此不再赘述。

SPI_FLASH_SendByte函数实现了前面讲解的“SPI通讯过程”:

(1)本函数中不包含SPI起始和停止信号,只是收发的主要过程,所以在调用本函数前后要做好起始和停止信号的操作;

(2)对SPITimeout变量赋值为宏SPIT_FLAG_TIMEOUT。这个SPITimeout变量在下面的while循环中每次循环减1,该循环通过调用库函数SPI_I2S_GetFlagStatus检测事件,若检测到事件,则进入通讯的下一阶段,若未检测到事件则停留在此处一直检测,当检测SPIT_FLAG_TIMEOUT次都还没等待到事件则认为通讯失败,调用的SPI_TIMEOUT_UserCallback输出调试信息,并退出通讯;

(3)通过检测TXE标志,获取发送缓冲区的状态,若发送缓冲区为空,则表示可能存在的上一个数据已经发送完毕;

(4)等待至发送缓冲区为空后,调用库函数SPI_I2S_SendData把要发送的数据“byte”写入到SPI的数据寄存器DR,写入SPI数据寄存器的数据会存储到发送缓冲区,由SPI外设发送出去;

(5)写入完毕后等待RXNE事件,即接收缓冲区非空事件。由于SPI双线全双工模式下MOSI与MISO数据传输是同步的(请对比“SPI通讯过程”阅读),当接收缓冲区非空时,表示上面的数据发送完毕,且接收缓冲区也收到新的数据;

(6)等待至接收缓冲区非空时,通过调用库函数SPI_I2S_ReceiveData读取SPI的数据寄存器DR,就可以获取接收缓冲区中的新数据了。代码中使用关键字“return”把接收到的这个数据作为SPI_FLASH_SendByte函数的返回值,所以我们可以看到在下面定义的SPI接收数据函数SPI_FLASH_ReadByte,它只是简单地调用了SPI_FLASH_SendByte函数发送数据“Dummy_Byte”,然后获取其返回值(因为不关注发送的数据,所以此时的输入参数“Dummy_Byte”可以为任意值)。可以这样做的原因是SPI的接收过程和发送过程实质是一样的,收发同步进行,关键在于我们的上层应用中,关注的是发送还是接收的数据。

控制FLASH的指令

搞定SPI的基本收发单元后,还需要了解如何对FLASH芯片进行读写。FLASH芯片自定义了很多指令,我们通过控制STM32利用SPI总线向FLASH芯片发送指令,FLASH芯片收到后就会执行相应的操作。

 而这些指令,对主机端(STM32)来说,只是它遵守最基本的SPI通讯协议发送出的数据,但在设备端(FLASH芯片)把这些数据解释成不同的意义,所以才成为指令。查看FLASH芯片的数据手册《W25Q64》,可了解各种它定义的各种指令的功能及指令格式,见表FLASH常用芯片指令表。

 该表中的第一列为指令名,第二列为指令编码,第三至第N列的具体内容根据指令的不同而有不同的含义。其中带括号的字节参数,方向为FLASH向主机传输,即命令响应,不带括号的则为主机向FLASH传输。表中“A0~A23”指FLASH芯片内部存储器组织的地址;“M0~M7”为厂商号(MANUFACTURERID);“ID0-ID15”为FLASH芯片的ID;“dummy”指该处可为任意数据;“D0~D7”为FLASH内部存储矩阵的内容。

在FLSAH芯片内部,存储有固定的厂商编号(M7-M0)和不同类型FLASH芯片独有的编号(ID15-ID0),见表FLASH数据手册的设备ID说明。

 通过指令表中的读ID指令“JEDECID”可以获取这两个编号,该指令编码为“9Fh”,其中“9Fh”是指16进制数“9F”(相当于C语言中的0x9F)。紧跟指令编码的三个字节分别为FLASH芯片输出的“(M7-M0)”、“(ID15-ID8)”及“(ID7-ID0)”。此处我们以该指令为例,配合其指令时序图进行讲解,见图FLASH读ID指令JEDEC_ID的时序。

 主机首先通过MOSI线向FLASH芯片发送第一个字节数据为“9Fh”,当FLASH芯片收到该数据后,它会解读成主机向它发送了“JEDEC指令”,然后它就作出该命令的响应:通过MISO线把它的厂商ID(M7-M0)及芯片类型(ID15-0)发送给主机,主机接收到指令响应后可进行校验。常见的应用是主机端通过读取设备ID来测试硬件是否连接正常,或用于识别设备。

对于FLASH芯片的其它指令,都是类似的,只是有的指令包含多个字节,或者响应包含更多的数据。

实际上,编写设备驱动都是有一定的规律可循的。首先我们要确定设备使用的是什么通讯协议。如上一章的EEPROM使用的是I2C,本章的FLASH使用的是SPI。那么我们就先根据它的通讯协议,选择好STM32的硬件模块,并进行相应的I2C或SPI模块初始化。接着,我们要了解目标设备的相关指令,因为不同的设备,都会有相应的不同的指令。如EEPROM中会把第一个数据解释为内部存储矩阵的地址(实质就是指令)。而FLASH则定义了更多的指令,有写指令,读指令,读ID指令等等。最后,我们根据这些指令的格式要求,使用通讯协议向设备发送指令,达到控制设备的目标。

定义FLASH指令编码表

为了方便使用,我们把FLASH芯片的常用指令编码使用宏来封装起来,后面需要发送指令编码的时候我们直接使用这些宏即可

FLASH指令编码表


 读取FLASH芯片ID

根据“JEDEC”指令的时序,我们把读取FLASHID的过程编写成一个函数

读取FLASH芯片ID

 

这段代码利用控制CS引脚电平的宏“SPI_FLASH_CS_LOW/HIGH”以及前面编写的单字节收发函数SPI_FLASH_SendByte,很清晰地实现了“JEDECID”指令的时序:发送一个字节的指令编码“W25X_JedecDeviceID”,然后读取3个字节,获取FLASH芯片对该指令的响应,最后把读取到的这3个数据合并到一个变量Temp中,然后作为函数返回值,把该返回值与我们定义的宏“sFLASH_ID”对比,即可知道FLASH芯片是否正常。

FLASH写使能以及读取当前状态

在向FLASH芯片存储矩阵写入数据前,首先要使能写操作,通过“WriteEnable”命令即可写使能

写使能命令

 与EEPROM一样,由于FLASH芯片向内部存储矩阵写入数据需要消耗一定的时间,并不是在总线通讯结束的一瞬间完成的,所以在写操作后需要确认FLASH芯片“空闲”时才能进行再次写入。为了表示自己的工作状态,FLASH芯片定义了一个状态寄存器,见图FLASH芯片的状态寄存器。

 我们只关注这个状态寄存器的第0位“BUSY”,当这个位为“1”时,表明FLASH芯片处于忙碌状态,它可能正在对内部的存储矩阵进行“擦除”或“数据写入”的操作。

利用指令表中的“ReadStatusRegister”指令可以获取FLASH芯片状态寄存器的内容,其时序见图读取状态寄存器的时序。

 我们只关注这个状态寄存器的第0位“BUSY”,当这个位为“1”时,表明FLASH芯片处于忙碌状态,它可能正在对内部的存储矩阵进行“擦除”或“数据写入”的操作。

利用指令表中的“ReadStatusRegister”指令可以获取FLASH芯片状态寄存器的内容,其时序见图读取状态寄存器的时序。

 只要向FLASH芯片发送了读状态寄存器的指令,FLASH芯片就会持续向主机返回最新的状态寄存器内容,直到收到SPI通讯的停止信号。据此我们编写了具有等待FLASH芯片写入结束功能的函数

通过读状态寄存器等待FLASH芯片空闲

 

这段代码发送读状态寄存器的指令编码“W25X_ReadStatusReg”后,在while循环里持续获取寄存器的内容并检验它的“WIP_Flag标志”(即BUSY位),一直等待到该标志表示写入结束时才退出本函数,以便继续后面与FLASH芯片的数据通讯。

FLASH扇区擦除

由于FLASH存储器的特性决定了它只能把原来为“1”的数据位改写成“0”,而原来为“0”的数据位不能直接改写为“1”。所以这里涉及到数据“擦除”的概念,在写入前,必须要对目标存储矩阵进行擦除操作,把矩阵中的数据位擦除为“1”,在数据写入的时候,如果要存储数据“1”,那就不修改存储矩阵,在要存储数据“0”时,才更改该位。

通常,对存储矩阵擦除的基本操作单位都是多个字节进行,如本例子中的FLASH芯片支持“扇区擦除”、“块擦除”以及“整片擦除”,见表本实验FLASH芯片的擦除单位。

 FLASH芯片的最小擦除单位为扇区(Sector),而一个块(Block)包含16个扇区,其内部存储矩阵分布见图FLASH芯片的存储矩阵。

 使用扇区擦除指令“SectorErase”可控制FLASH芯片开始擦写,其指令时序见图扇区擦除时序。

 

 扇区擦除指令的第一个字节为指令编码,紧接着发送的3个字节用于表示要擦除的存储矩阵地址。要注意的是在扇区擦除指令前,还需要先发送“写使能”指令,发送扇区擦除指令后,通过读取寄存器状态等待扇区擦除操作完毕

擦除扇区

 

 这段代码调用的函数在前面都已讲解,只要注意发送擦除地址时高位在前即可。调用扇区擦除指令时注意输入的地址要对齐到4KB。

FLASH的页写入

目标扇区被擦除完毕后,就可以向它写入数据了。与EEPROM类似,FLASH芯片也有页写入命令,使用页写入命令最多可以一次向FLASH传输256个字节的数据,我们把这个单位为页大小。FLASH页写入的时序见图FLASH芯片页写入。

从时序图可知,第1个字节为“页写入指令”编码,2-4字节为要写入的“地址A”,接着的是要写入的内容,最多个可以发送256字节数据,这些数据将会从“地址A”开始,按顺序写入到FLASH的存储矩阵。若发送的数据超出256个,则会覆盖前面发送的数据。

与擦除指令不一样,页写入指令的地址并不要求按256字节对齐,只要确认目标存储单元是擦除状态即可(即被擦除后没有被写入过)。所以,若对“地址x”执行页写入指令后,发送了200个字节数据后终止通讯,下一次再执行页写入指令,从“地址(x+200)”开始写入200个字节也是没有问题的(小于256均可)。只是在实际应用中由于基本擦除单元是4KB,一般都以扇区为单位进行读写,想深入了解,可学习我们的“FLASH文件系统”相关的例子。

把页写入时序封装成函数

FLASH的页写入

 这段代码的内容为:先发送“写使能”命令,接着才开始页写入时序,然后发送指令编码、地址,再把要写入的数据一个接一个地发送出去,发送完后结束通讯,检查FLASH状态寄存器,等待FLASH内部写入结束。

不定量数据写入

应用的时候我们常常要写入不定量的数据,直接调用“页写入”函数并不是特别方便,所以我们在它的基础上编写了“不定量数据写入”的函数

不定量数据写入

 

 

 这段代码与EEPROM章节中的“快速写入多字节”函数原理是一样的,运算过程在此不再赘述。区别是页的大小以及实际数据写入的时候,使用的是针对FLASH芯片的页写入函数,且在实际调用这个“不定量数据写入”函数时,还要注意确保目标扇区处于擦除状态。

从FLASH读取数据

相对于写入,FLASH芯片的数据读取要简单得多,使用读取指令“ReadData”即可,其指令时序见图SPI_FLASH读取数据时序。

 发送了指令编码及要读的起始地址后,FLASH芯片就会按地址递增的方式返回存储矩阵的内容,读取的数据量没有限制,只要没有停止通讯,FLASH芯片就会一直返回数据。

 从FLASH读取数据

 由于读取的数据量没有限制,所以发送读命令后一直接收NumByteToRead个数据到结束即可。

4.2.3  main函数

最后我们来编写main函数,进行FLASH芯片读写校验

main函数

 

 

 函数中初始化了LED、串口、SPI外设,然后读取FLASH芯片的ID进行校验,若ID校验通过则向FLASH的特定地址写入测试数据,然后再从该地址读取数据,测试读写是否正常。

 4.3 下载验证

用USB线连接开发板“USBTOUART”接口跟电脑,在电脑端打开串口调试助手,把编译好的程序下载到开发板。在串口调试助手可看到FLASH测试的调试信息。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

SPI—读写串行FLASH 的相关文章

  • HTTP带用户名和密码请求

    import java io IOException import org apache commons codec binary Base64 import com cn mid system urls UrlUtils import o
  • n*m的格子中正方形个数和长方形个数

    问题描述 1 xff0e 设有一个nm方格的棋盘 xff08 1 m n 100 xff09 求出该棋盘中包含多少个正方形 多少个长方形 xff08 不包括正方形 xff09 例如 xff1a 当n 61 2 xff0c m 61 3时 正
  • Linux数字权限

    linux系统文件夹 从左至右 xff0c 第一位数字代表文件所有者的权限 xff1b 第二位数字代表同组用户的权限 xff1b 第三位数字代表其他用户的权限 而具体的权限是由数字来表示的 xff1a 读取的权限等于4 xff0c 用r表示
  • 八大排序算法C语言实现

    1 插入排序 1 1 直接插入排序 基本原理 xff1a 将第n个数插入已经排序好的 xff0c 长度为n 1的序列中 从n 1长度的序列中查找出待插入的元素应该插入的位置 xff1b 给插入元素腾出空间 操作方法 xff1a 从第2个数开
  • CLion下的gtest测试

    在mac环境中 xff0c 使用CLion编译的简单gtest程序 一 下载gtest源码 加入工程中 xff1a 二 编写CMakeList txt 文件 在文件中添加头文件和链接库文件 xff0c 并将链接库文件与目标文件进行链接 sp
  • python中json与dict的互相转换(编码与解码)及其简单实现

    在json模块中 将json转换为dict数据的方法有 xff1a load loads xff08 xff09 其中 xff0c load 方法从文件中提取数据进行转换 将dict转换为json数据的方法有 xff1a dump dump
  • gtest中ASSERT与EXPECT断言的区别

    参考资料查找到ASSERT断言与EXPECT断言的区别 xff1a ASSERT 系列的断言 xff0c 当检查点失败时 xff0c 退出当前函数 xff08 注意 xff1a 并非退出当前案例 xff09 EXPECT 系列的断言 xff
  • gtest参数化

    步骤 xff1a 1 创建一个类 xff0c 继承testing TestWithParam xff0c T是你需要参数化的参数类型 xff0c 比如参数类型为int 2 使用新宏TEST P替代TEST 在TEST P宏里 xff0c 可
  • gtest中字符串比较是否相等

    1 EXPECT EQ val1 xff0c val2 xff09 class StringCmpTest span class token punctuation span public testing span class token
  • 白盒测试——逻辑覆盖

    白盒测试中的逻辑覆盖有以下六种方法 xff1a 1 语句覆盖 xff1a 每个可执行语句至少被执行一次 2 判定覆盖 xff1a 每个判定的每个分支都至少执行一次 3 条件覆盖 xff1a 判定式中每个条件的每个分支至少执行一次 4 判定条
  • 计算机网络面试题整理

    面试很多时候被问到的问题 xff0c 感觉都没答好 xff0c 统一整理一下 更新 8 23 GET和POST的区别 xff1f GET和POST方法没有实质上区别 xff0c 只是报文格式不同 GET和POST是HTTP协议中的两种请求方
  • 【C语言】c/c++中常用的预定义宏:__LINE__, __func__, __FILE__, __DATE__, __TIME__

    ANSI C标准中的预定义宏 xff08 也是常用的 xff09 xff1a LINE xff1a 在代码中插入当前行号 func xff1a 在代码中插入当前行所在的函数的函数名 FILE xff1a 在代码中插入当前文件的文件名 DAT
  • 无人机入门知识

    无人机入门知识 无人机的定义 现在提到的 无人机 xff0c 通常是说 无人飞行载具 xff08 Unmanned Aerial Vehicle xff0c 简称UAV xff09 xff0c 或称无人飞机 无人飞机系统 xff08 Unm
  • http协议

    一 认识url url被称为统一资源定位符 xff0c 用来表示从互联网上得到的资源位置和访问这些资源的方法 他的表示方法一般为 xff1a span class token operator lt span 协议 span class t
  • 大小端区别和判断

    在代码中看到往寄存器写数据的时候 xff0c 使用到 volatile uint32 t address 61 cpu to le32 value xff0c 进一步追踪 xff0c if BYTE ORDER 61 61 LITTLE E
  • 结构体对齐详解

    1 结构体数据成员对齐的意义 许多实际的计算机系统对基本类型数据在内存中存放的位置有限制 xff0c 它们会要求这些数据的起始地址的值是某个数k的倍数 xff0c 这就是所谓的内存对齐 xff0c 而这个k则被称为该数据类型的对齐模数 al
  • C语言基础之格式化占位符

    在 printf 系列函数中 xff0c 下列哪个格式化占位符有可能导致内存任意写风险 A A n B d C p D s 使用printf修改变量的值 VS2008中使用 n输出遇到的问题及解决方法 include lt stdio h
  • 不常用的访问控制方式——http auth

    今天遇到一个http auth方式的访问控制 xff0c 访问页面时出现输入用户名与密码的验证 xff1a 使用ncrack爆破登陆后 xff0c 当想要爆破目录时 xff0c 必须每次请求都带着authorication头 xff0c 使
  • 数据库中字符串匹配函数like、rlike、instr、regexp_extract

    instr 简介 MySQL hive中函数 xff0c instr str substr position occurrence 其中str代表从哪个字符串中搜索 xff0c substr代表搜索哪个子字符串 xff0c 返回值为子字符串
  • 【C语言】实现linux下的基于C语言的一个简单的TCP客/服 端的通信

    对于基础的好的朋友可以直接取代码 xff0c 如果想要看详细解析的朋友可以详看下方的解析 TCP服务端代码 xff1a include lt stdio h gt include lt string h gt include lt stdl

随机推荐

  • 实验二 OpenGL的简单动画

    ZZU的学弟学妹们不要抄作业哦 一 实验目的 1 掌握OpenGL的闲置函数 2 掌握OpenGL的时间函数 3 掌握OpenGL的简单动画功能 4 了解OpengGL裁剪窗口 视区 显示窗口的概念和它们之间的关系 5 进一步掌握OpenG
  • 使用脚本(命令行)编译KEIL工程

    参考KEIL官网 http www keil com support man docs uv4 uv4 commandline htm Keil Build bat脚本 64 echo off set UV 61 D Keil v5 UV4
  • (二)五次多项式轨迹规划

    一 三次多项式轨迹规划的缺陷 上一篇文章说道 xff0c 三次多项式轨迹规划只能够保证速度和位移连续 xff0c 并不能保证加速度连续 加速度不连续将会对使电机抖动 甚至冲击 二 轨迹规划中的五次多项式 我们对加速度数值进行指定 xff0c
  • (三)抛物线过渡的线性函数规划

    前面说到 xff0c 无论是三次还是五次多项式进行规划存在以下缺点 xff1a 位移往返没有匀速段 这一节中 xff0c 我们的研究对象是初速度和末速度都为0关节运动 一 无过渡线性函数 假设时刻 t t t 和角度
  • GCC的学习(二)头文件及其库制作

    当前目录 43 相对路径 61 绝对路径大写i xff0c I头文件包含路径大写l xff0c L库文件路径小写l xff0c l库名字 xff08 去头去尾没有lib xff0c 也没有so ldd 查看动态库连接库存在性及其路径nm 查
  • VSCODE(八)launch 调试与运行

    前面讲了如何配置任务文件tasks json xff0c 调试功能在程序程序经常会用上 xff0c 那么VSCODE是完成调试功能的呢 xff1f 答 xff1a vscode文件夹内的launch json文件配置 xff0c 一些调试器
  • C++11线程库 (七) 线程的停止

    在这篇文章里 xff0c 我们将会讨论如何在C 43 43 11下停止和终止一个线程 为什么C 43 43 11不直接提供一个停止线程的方法 xff1f 这是因为线程在停止之前可能有一些资源需要被释放 关闭 xff0c 比如说 xff1a
  • QCustomplot(五)QCPAxisRect进行子绘图

    子绘图是比较复杂的 xff0c 你几乎需要掌握QCustomPlot所有类及其含义 那么需要掌握的类有 xff1a QCPAxis 描述轴行为 xff0c 轴需要依赖QCPAxisRectQCPAxisRect 描述矩形区域 xff0c 区
  • 关于C/C++的换行

    可以分为两种情况 xff1a 代码换行长字符串换行 一 C C 43 43 代码换行直接用enter键换行 xff0c 同时注意关键字不要切割 xff1a span class token keyword bool span a span
  • 31线性变换及其矩阵

    一 线性变换的概念 线性变换是数学中一种特殊的映射 xff0c 它具有将向量空间中的元素转换到另一向量空间元素的功能 xff0c 同时也保持了变换前后向量的线性不变的性质 线性变换的数学表达式如下 xff1a T c
  • 30奇异值分解

    在讲SVD之前 xff0c 我们先来看看计算是如何存储一个灰度图的 灰度图会被分割成一块块的小灰度像素 xff0c 然后进行存储 xff0c 数值的大小表示灰度的程度 一个灰度像素用8位进行存储 xff0c 也就是256种状态 xff0c
  • (二)PID控制的Anti-windup

    比例环节 xff1a 快速接近目标积分环节 xff1a 防止稳态误差微分环节 xff1a 减少振荡 被控对象可以分为两个部分 xff0c 分别是执行器 xff0c 用于产生力或者能量从而改变系统 xff0c 和处理 xff0c 比如说温控的
  • Socket的学习(一)什么是Socket?

    本文参考的是 Socket通信原理 https www cnblogs com wangcq p 3520400 html 一 TCP IP UDP是什么 xff1f TCP IP xff08 Transmission Control Pr
  • Socket的学习(二)AF_UNIX实现本地通信

    参考文章 xff1a 1 Unix Domain Socket IPC通信机制 2 How fast are Unix domain sockets 3 read 函数参数理解 一 Unix域的Socket通信及其优点 基于socket的框
  • 工程师笔记|在CMD窗口运行C语言

    cmd窗口大家可能都比较熟悉 xff0c 那如何在cmd窗口编译运行C语言程序呢 xff1f 其实很简单的 xff0c 只需要下载一个C C 43 43 编译器 xff0c 添加一下环境变量 xff0c 之后就能在cmd窗口中编译运行C语言
  • 推挽输出与开漏输出

    推挽输出 要理解推挽输出 xff0c 首先要理解好三极管 xff08 晶体管 xff09 的原理 下面这种三极管有三个端口 xff0c 分别是基极 xff08 Base xff09 集电极 xff08 Collector xff09 和发射
  • 固件、驱动、软件的区别

    不管我们使用什么操作系统 xff0c 无论是 Windows macOS xff0c 还是 Linux xff0c 里面都安装了许多软件 驱动程序和固件 但是 xff0c 这三者概念有什么区别 xff1f 转载微信公众号 xff1a 良许L
  • 51单片机与STM32的区别(为何51单片机IO引脚的驱动能力弱)

    1 51单片机的特性 51单片机之所以成为经典 xff0c 成为易上手的单片机主要有以下特点 xff1a 从内部的硬件到软件有一套完整的按位操作系统 xff0c 称作位处理器 xff0c 处理对象不是字或字节而是位 不但能对片内某些特殊功能
  • I2C—读写EEPROM

    1 I2C协议简介 I2C通讯协议 Inter xff0d IntegratedCircuit 是由Phiilps公司开发的 xff0c 由于它引脚少 xff0c 硬件实现简单 xff0c 可扩展性强 xff0c 不需要USART CAN等
  • SPI—读写串行FLASH

    1 SPI协议简介 SPI协议是由摩托罗拉公司提出的通讯协议 SerialPeripheralInterface xff0c 即串行外围设备接口 xff0c 是一种高速全双工的通信总线 它被广泛地使用在ADC LCD等设备与MCU间 xff