datasheet

STM32学习之:SPI读写串行Flash

2018-10-21来源: eefocus关键字:STM32  SPI读写串  行Flash

24.1 SPI协议简介


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


学习本章时,可与I2C章节对比阅读,体会两种通讯总线的差异以及EEPROM存储器与FLASH存储器的区别。下面我们分别对SPI协议的物理层及协议层进行讲解。


24.1.1 SPI物理层


SPI通讯设备之间的常用连接方式见图 241。

图 241 常见的SPI通讯系统


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


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


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


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


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


24.1.2 协议层


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


1.    SPI基本通讯过程


先看看SPI通讯的通讯时序,见图 242。

图 242 SPI通讯时序


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


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


2.    通讯的起始和停止信号


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


3.    数据有效性


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


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


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


4.    CPOL/CPHA及通讯模式


上面讲述的图 242中的时序只是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的"偶数边沿"采样。见图 243及图 244。

图 243 CPHA=0时的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的偶数边沿被采样,见图 244。

图 244 CPHA=1时的SPI通讯模式


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


表 241 SPI的四种模式


SPI模式

CPOL

CPHA

空闲时SCK时钟

采样时刻

0

0

0

低电平

奇数边沿

1

0

1

低电平

偶数边沿

2

1

0

高电平

奇数边沿

3

1

1

高电平

偶数边沿

24.2 STM32的SPI特性及架构


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


24.2.1 STM32的SPI外设简介


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


STM32的SPI外设还支持I2S功能,I2S功能是一种音频串行通讯协议,在我们以后讲解MP3播放器的章节中会进行介绍。


24.2.2 STM32的SPI架构剖析

图 245 SPI架构图


1.    通讯引脚


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


其中SPI1、SPI4、SPI5、SPI6是APB2上的设备,最高通信速率达45Mbtis/s,SPI2、SPI3是APB1上的设备,最高通信速率为22.5Mbits/s。除了通讯速率,在其它功能上没有差异。


2.    时钟控制逻辑


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


表 243 BR位对fpclk的分频

BR[0:2]

分频结果(SCK频率)


BR[0:2]

分频结果(SCK频率)

000

fpclk/2


100

fpclk/32

001

fpclk/4


101

fpclk/64

010

fpclk/8


110

fpclk/128

011

fpclk/16


111

fpclk/256



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


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


3.    数据控制逻辑


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


4.    整体控制逻辑


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


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


24.2.3 通讯过程


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


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

图 246 主发送器通讯过程


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


(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"中的数据。


24.3 SPI初始化结构体详解


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


代码清单 241 SPI初始化结构体


1 typedef struct


2 {


3 uint16_t SPI_Direction; /*设置SPI的单双向模式 */


4 uint16_t SPI_Mode; /*设置SPI的主/从机端模式 */


5 uint16_t SPI_DataSize; /*设置SPI的数据帧长度,可选8/16位 */


6 uint16_t SPI_CPOL; /*设置时钟极性CPOL,可选高/低电平*/


7 uint16_t SPI_CPHA; /*设置时钟相位,可选奇/偶数边沿采样 */


8 uint16_t SPI_NSS; /*设置NSS引脚由SPI硬件控制还是软件控制*/


9 uint16_t SPI_BaudRatePrescaler; /*设置时钟分频因子,fpclk/分频数=fSCK */


10 uint16_t SPI_FirstBit; /*设置MSB/LSB先行 */


11 uint16_t SPI_CRCPolynomial; /*设置CRC校验的表达式 */


12 } SPI_InitTypeDef;


这些结构体成员说明如下,其中括号内的文字是对应参数在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外设。


24.4 SPI—读写串行FLASH实验


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


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


24.4.1 硬件设计

图 247 SPI串行FLASH硬件连接图


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


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


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


24.4.2 软件设计


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


1.    编程要点


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


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


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


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


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


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


2.    代码分析


SPI硬件相关宏定义


我们把SPI硬件相关的配置都以宏的形式定义到"bsp_spi_ flash.h"文件中,见代码清单 242。


代码清单 242 SPI硬件配置相关的宏


1 //SPI号及时钟初始化函数


2 #define FLASH_SPI SPI3


3 #define FLASH_SPI_CLK RCC_APB1Periph_SPI3


4 #define FLASH_SPI_CLK_INIT RCC_APB1PeriphClockCmd


5 //SCK引脚


6 #define FLASH_SPI_SCK_PIN GPIO_Pin_3


7 #define FLASH_SPI_SCK_GPIO_PORT GPIOB


8 #define FLASH_SPI_SCK_GPIO_CLK RCC_AHB1Periph_GPIOB


9 #define FLASH_SPI_SCK_PINSOURCE GPIO_PinSource3


10 #define FLASH_SPI_SCK_AF GPIO_AF_SPI3


11 //MISO引脚


12 #define FLASH_SPI_MISO_PIN GPIO_Pin_4


13 #define FLASH_SPI_MISO_GPIO_PORT GPIOB


14 #define FLASH_SPI_MISO_GPIO_CLK RCC_AHB1Periph_GPIOB


15 #define FLASH_SPI_MISO_PINSOURCE GPIO_PinSource4


16 #define FLASH_SPI_MISO_AF GPIO_AF_SPI3


17 //MOSI引脚


18 #define FLASH_SPI_MOSI_PIN GPIO_Pin_5


19 #define FLASH_SPI_MOSI_GPIO_PORT GPIOB


20 #define FLASH_SPI_MOSI_GPIO_CLK RCC_AHB1Periph_GPIOB


21 #define FLASH_SPI_MOSI_PINSOURCE GPIO_PinSource5


22 #define FLASH_SPI_MOSI_AF GPIO_AF_SPI3


23 //CS(NSS)引脚


24 #define FLASH_CS_PIN GPIO_Pin_8


25 #define FLASH_CS_GPIO_PORT GPIOI


26 #define FLASH_CS_GPIO_CLK RCC_AHB1Periph_GPIOI


27


28 //控制CS(NSS)引脚输出低电平


29 #define SPI_FLASH_CS_LOW() {FLASH_CS_GPIO_PORT->BSRRH=FLASH_CS_PIN;}


30 //控制CS(NSS)引脚输出高电平


31 #define SPI_FLASH_CS_HIGH() {FLASH_CS_GPIO_PORT->BSRRL=FLASH_CS_PIN;}


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


初始化SPI的 GPIO


利用上面的宏,编写SPI的初始化函数,见代码清单 243。


代码清单 243 SPI的初始化函数(GPIO初始化部分)


 1 

2 /**


3 * @brief SPI_FLASH初始化


4 * @param 无


5 * @retval 无


6 */


7 void SPI_FLASH_Init(void)


8 {


9 GPIO_InitTypeDef GPIO_InitStructure;


10


11 /* 使能 FLASH_SPI 及 GPIO 时钟 */


12 /*!< SPI_FLASH_SPI_CS_GPIO, SPI_FLASH_SPI_MOSI_GPIO,


13 SPI_FLASH_SPI_MISO_GPIO和 SPI_FLASH_SPI_SCK_GPIO 时钟使能 */


14 RCC_AHB1PeriphClockCmd (FLASH_SPI_SCK_GPIO_CLK | FLASH_SPI_MISO_GPIO_CLK|


15 FLASH_SPI_MOSI_GPIO_CLK|FLASH_CS_GPIO_CLK, ENABLE);


16


17 /*!< SPI_FLASH_SPI 时钟使能 */


18 FLASH_SPI_CLK_INIT(FLASH_SPI_CLK, ENABLE);


19


20 //设置引脚复用


21 GPIO_PinAFConfig(FLASH_SPI_SCK_GPIO_PORT,FLASH_SPI_SCK_PINSOURCE,


22 FLASH_SPI_SCK_AF);


23 GPIO_PinAFConfig(FLASH_SPI_MISO_GPIO_PORT,FLASH_SPI_MISO_PINSOURCE,


24 FLASH_SPI_MISO_AF);


25 GPIO_PinAFConfig(FLASH_SPI_MOSI_GPIO_PORT,FLASH_SPI_MOSI_PINSOURCE,


26 FLASH_SPI_MOSI_AF);


27


28 /*!< 配置 SPI_FLASH_SPI 引脚: SCK */


29 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN;


30 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;


31 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;


32 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;


33 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;


35


36 GPIO_Init(FLASH_SPI_SCK_GPIO_PORT, &GPIO_InitStructure);


37


38 /*!< 配置 SPI_FLASH_SPI 引脚: MISO */


39 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN;


40 GPIO_Init(FLASH_SPI_MISO_GPIO_PORT, &GPIO_InitStructure);


41


42 /*!< 配置 SPI_FLASH_SPI 引脚: MOSI */


43 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;


44 GPIO_Init(FLASH_SPI_MOSI_GPIO_PORT, &GPIO_InitStructure);


45


46 /*!< 配置 SPI_FLASH_SPI 引脚: CS */


47 GPIO_InitStructure.GPIO_Pin = FLASH_CS_PIN;


48 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;


49 GPIO_Init(FLASH_CS_GPIO_PORT, &GPIO_InitStructure);


50


51 /* 停止信号 FLASH: CS引脚高电平*/


52 SPI_FLASH_CS_HIGH();


53 /*为方便讲解,以下省略SPI模式初始化部分*/


54 //......


55 }


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


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


(2)    调用库函数RCC_AHB1PeriphClockCmd来使能SPI引脚使用的GPIO端口时钟,调用时使用"|"操作同时配置多个引脚。调用宏FLASH_SPI_CLK_INIT使能SPI外设时钟(该宏封装了APB时钟使能的库函数)。


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


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


配置SPI的模式


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


代码清单 244 配置SPI模式


1 /**


2 * @brief SPI_FLASH引脚初始化


3 * @param 无


4 * @retval 无


5 */


6 void SPI_FLASH_Init(void)


7 {


8 /*为方便讲解,省略了SPI的GPIO初始化部分*/


9 //......


10


11 SPI_InitTypeDef SPI_InitStructure;


12 /* FLASH_SPI 模式配置 */


13 // FLASH芯片支持SPI模式0及模式3,据此设置CPOL CPHA


14 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;


15 SPI_InitStructure.SPI_Mode = SPI_Mode_Master;


16 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;


17 SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;


18 SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;


19 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;


20 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;


21 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;


22 SPI_InitStructure.SPI_CRCPolynomial = 7;


23 SPI_Init(FLASH_SPI, &SPI_InitStructure);


24


25 /* 使能 FLASH_SPI */


26 SPI_Cmd(FLASH_SPI, ENABLE);


27 }


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


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


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


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


代码清单 245 使用SPI发送和接收一个字节的数据


1 #define Dummy_Byte 0xFF


2 /**


3 * @brief 使用SPI发送一个字节的数据


4 * @param byte:要发送的数据


5 * @retval 返回接收到的数据


6 */


7 u8 SPI_FLASH_SendByte(u8 byte)


8 {


9 SPITimeout = SPIT_FLAG_TIMEOUT;


10


11 /* 等待发送缓冲区为空,TXE事件 */


12 while (SPI_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_TXE) == RESET)


13 {


14 if ((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);


15 }


16


17 /* 写入数据寄存器,把要写入的数据写入发送缓冲区 */


18 SPI_I2S_SendData(FLASH_SPI, byte);


19


20 SPITimeout = SPIT_FLAG_TIMEOUT;


21


22 /* 等待接收缓冲区非空,RXNE事件 */


23 while (SPI_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_RXNE) == RESET)


24 {


25 if ((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);


26 }


27


28 /* 读取数据寄存器,获取接收缓冲区数据 */


29 return SPI_I2S_ReceiveData(FLASH_SPI);


30 }


31


32 /**


33 * @brief 使用SPI读取一个字节的数据


34 * @param 无


35 * @retval 返回接收到的数据


36 */


37 u8 SPI_FLASH_ReadByte(void)


38 {


39 return (SPI_FLASH_SendByte(Dummy_Byte));


40 }


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芯片的数据手册《W25Q128》,可了解各种它定义的各种指令的功能及指令格式。


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


在FLSAH芯片内部,存储有固定的厂商编号(M7-M0)和不同类型FLASH芯片独有的编号(ID15-ID0),见表 245。


表 245 FLASH数据手册的设备ID说明


FLASH型号

厂商号(M7-M0)

FLASH型号(ID15-ID0)

W25Q64

EF h

4017 h

W25Q128

EF h

4018 h


通过指令表中的读ID指令"JEDEC ID"可以获取这两个编号,该指令编码为"9F h",其中"9F h"是指16进制数"9F" (相当于C语言中的0x9F)。紧跟指令编码的三个字节分别为FLASH芯片输出的"(M7-M0)"、"(ID15-ID8)"及"(ID7-ID0)"。


此处我们以该指令为例,配合其指令时序图进行讲解,见图 248。



图 248 FLASH读ID指令"JEDEC ID"的时序(摘自规格书《W25Q128》)


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


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


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


定义FLASH指令编码表


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


代码清单 246 FLASH指令编码表


1 /*FLASH常用命令*/


2 #define W25X_WriteEnable 0x06


3 #define W25X_WriteDisable 0x04


4 #define W25X_ReadStatusReg 0x05


5 #define W25X_WriteStatusReg 0x01


6 #define W25X_ReadData 0x03


7 #define W25X_FastReadData 0x0B


8 #define W25X_FastReadDual 0x3B


9 #define W25X_PageProgram 0x02


10 #define W25X_BlockErase 0xD8


11 #define W25X_SectorErase 0x20


12 #define W25X_ChipErase 0xC7


13 #define W25X_PowerDown 0xB9


14 #define W25X_ReleasePowerDown 0xAB


15 #define W25X_DeviceID 0xAB


16 #define W25X_ManufactDeviceID 0x90


17 #define W25X_JedecDeviceID 0x9F


18 /*其它*/


19 #define sFLASH_ID 0XEF4018


20 #define Dummy_Byte 0xFF


读取FLASH芯片ID


根据"JEDEC"指令的时序,我们把读取FLASH ID的过程编写成一个函数,见代码清单 247。


代码清单 247 读取FLASH芯片ID


1 /**


2 * @brief 读取FLASH ID


3 * @param 无


4 * @retval FLASH ID


5 */


6 u32 SPI_FLASH_ReadID(void)


7 {


8 u32 Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0;


9


10 /* 开始通讯:CS低电平 */


11 SPI_FLASH_CS_LOW();


12


13 /* 发送JEDEC指令,读取ID */


14 SPI_FLASH_SendByte(W25X_JedecDeviceID);


15


16 /* 读取一个字节数据 */


17 Temp0 = SPI_FLASH_SendByte(Dummy_Byte);


18


19 /* 读取一个字节数据 */


20 Temp1 = SPI_FLASH_SendByte(Dummy_Byte);


21


22 /* 读取一个字节数据 */


23 Temp2 = SPI_FLASH_SendByte(Dummy_Byte);


24


25 /* 停止通讯:CS高电平 */


26 SPI_FLASH_CS_HIGH();


27


28 /*把数据组合起来,作为函数的返回值*/


29 Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2;


30


31 return Temp;


32 }


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


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


在向FLASH芯片存储矩阵写入数据前,首先要使能写操作,通过"Write Enable"命令即可写使能,见代码清单 248。


代码清单 248 写使能命令


1 /**


2 * @brief 向FLASH发送写使能命令


3 * @param none


4 * @retval none


5 */


6 void SPI_FLASH_WriteEnable(void)


7 {


8 /* 通讯开始:CS低 */


9 SPI_FLASH_CS_LOW();


10


11 /* 发送写使能命令*/


12 SPI_FLASH_SendByte(W25X_WriteEnable);


13


14 /*通讯结束:CS高 */


15 SPI_FLASH_CS_HIGH();


16 }


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

图 249 FLASH芯片的状态寄存器


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


利用指令表中的"Read Status Register"指令可以获取FLASH芯片状态寄存器的内容,其时序见图 2410。

图 2410 读取状态寄存器的时序


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


代码清单 249 通过读状态寄存器等待FLASH芯片空闲


1 /*WIP(BUSY)标志:FLASH内部正在写入*/


2 #define WIP_Flag 0x01


3


4 /**


5 * @brief 等待WIP(BUSY)标志被置0,即等待到FLASH内部数据写入完毕


6 * @param none


7 * @retval none


8 */


9 void SPI_FLASH_WaitForWriteEnd(void)


10 {


11 u8 FLASH_Status = 0;


12 /* 选择 FLASH: CS 低 */


13 SPI_FLASH_CS_LOW();


14


15 /* 发送读状态寄存器命令 */


16 SPI_FLASH_SendByte(W25X_ReadStatusReg);


17


18 SPITimeout = SPIT_FLAG_TIMEOUT;


19 /* 若FLASH忙碌,则等待 */


20 do


21 {


22 /* 读取FLASH芯片的状态寄存器 */


23 FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);


24 if ((SPITimeout--) == 0)


25 {


26 SPI_TIMEOUT_UserCallback(4);


27 return;


28 }


29 }


30 while ((FLASH_Status & WIP_Flag) == SET); /* 正在写入标志 */


31


32 /* 停止信号 FLASH: CS 高 */


33 SPI_FLASH_CS_HIGH();


34 }


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


FLASH扇区擦除


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


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


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


图 2411 FLASH芯片的存储矩阵


使用扇区擦除指令"Sector Erase"可控制FLASH芯片开始擦写,其指令时序见图 2414。

图 2412 扇区擦除时序


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


代码清单 2410 擦除扇区


1 /**


2 * @brief 擦除FLASH扇区


3 * @param SectorAddr:要擦除的扇区地址


4 * @retval 无


5 */


6 void SPI_FLASH_SectorErase(u32 SectorAddr)


7 {


8 /* 发送FLASH写使能命令 */


9 SPI_FLASH_WriteEnable();


10 SPI_FLASH_WaitForWriteEnd();


11 /* 擦除扇区 */


12 /* 选择FLASH: CS低电平 */


13 SPI_FLASH_CS_LOW();


14 /* 发送扇区擦除指令*/


15 SPI_FLASH_SendByte(W25X_SectorErase);


16 /*发送擦除扇区地址的高位*/


17 SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);


18 /* 发送擦除扇区地址的中位 */


19 SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);


20 /* 发送擦除扇区地址的低位 */


21 SPI_FLASH_SendByte(SectorAddr & 0xFF);


22 /* 停止信号 FLASH: CS 高电平 */


23 SPI_FLASH_CS_HIGH();


24 /* 等待擦除完毕*/


25 SPI_FLASH_WaitForWriteEnd();


26 }


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


FLASH的页写入


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

图 2413 FLASH芯片页写入


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


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


把页写入时序封装成函数,其实现见代码清单 2411。


代码清单 2411 FLASH的页写入


1 /**


2 * @brief 对FLASH按页写入数据,调用本函数写入数据前需要先擦除扇区


3 * @param pBuffer,要写入数据的指针


4 * @param WriteAddr,写入地址


5 * @param NumByteToWrite,写入数据长度,必须小于等于页大小


6 * @retval 无


7 */


8 void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)


9 {


10 /* 发送FLASH写使能命令 */


11 SPI_FLASH_WriteEnable();


12


13 /* 选择FLASH: CS低电平 */


14 SPI_FLASH_CS_LOW();


15 /* 写送写指令*/


16 SPI_FLASH_SendByte(W25X_PageProgram);


17 /*发送写地址的高位*/


18 SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);


19 /*发送写地址的中位*/


20 SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);


21 /*发送写地址的低位*/


22 SPI_FLASH_SendByte(WriteAddr & 0xFF);


23


24 if (NumByteToWrite > SPI_FLASH_PerWritePageSize)


25 {


26 NumByteToWrite = SPI_FLASH_PerWritePageSize;


27 FLASH_ERROR("SPI_FLASH_PageWrite too large!");


28 }


29


30 /* 写入数据*/


31 while (NumByteToWrite--)


32 {


33 /* 发送当前要写入的字节数据 */


34 SPI_FLASH_SendByte(*pBuffer);


35 /* 指向下一字节数据 */


36 pBuffer++;


37 }


38


39 /* 停止信号 FLASH: CS 高电平 */


40 SPI_FLASH_CS_HIGH();


41


42 /* 等待写入完毕*/


43 SPI_FLASH_WaitForWriteEnd();


44 }


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


不定量数据写入


应用的时候我们常常要写入不定量的数据,直接调用"页写入"函数并不是特别方便,所以我们在它的基础上编写了"不定量数据写入"的函数,基实现见代码清单 2412。


代码清单 2412不定量数据写入


1 /**


2 * @brief 对FLASH写入数据,调用本函数写入数据前需要先擦除扇区


3 * @param pBuffer,要写入数据的指针


4 * @param WriteAddr,写入地址


5 * @param NumByteToWrite,写入数据长度


6 * @retval 无


7 */


8 void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)


9 {


10 u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;


11


12 /*mod运算求余,若writeAddr是SPI_FLASH_PageSize整数倍,运算结果Addr值为0*/


13 Addr = WriteAddr % SPI_FLASH_PageSize;


14


15 /*差count个数据值,刚好可以对齐到页地址*/


16 count = SPI_FLASH_PageSize - Addr;


17 /*计算出要写多少整数页*/


18 NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;


19 /*mod运算求余,计算出剩余不满一页的字节数*/


20 NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;


21


22 /* Addr=0,则WriteAddr 刚好按页对齐 aligned */


23 if (Addr == 0)


24 {


25 /* NumByteToWrite < SPI_FLASH_PageSize */


26 if (NumOfPage == 0)


27 {


28 SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);


29 }


30 else /* NumByteToWrite > SPI_FLASH_PageSize */


31 {


32 /*先把整数页都写了*/


33 while (NumOfPage--)


34 {


35 SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);


36 WriteAddr += SPI_FLASH_PageSize;


37 pBuffer += SPI_FLASH_PageSize;


38 }


39


40 /*若有多余的不满一页的数据,把它写完*/


41 SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);


42 }


43 }


44 /* 若地址与 SPI_FLASH_PageSize 不对齐 */


45 else


46 {


47 /* NumByteToWrite < SPI_FLASH_PageSize */


48 if (NumOfPage == 0)


49 {


50 /*当前页剩余的count个位置比NumOfSingle小,写不完*/


51 if (NumOfSingle > count)


52 {


53 temp = NumOfSingle - count;


54


55 /*先写满当前页*/


56 SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);


57 WriteAddr += count;


58 pBuffer += count;


59


60 /*再写剩余的数据*/


61 SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);


62 }


63 else /*当前页剩余的count个位置能写完NumOfSingle个数据*/


64 {


65 SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);


66 }


67 }


68 else /* NumByteToWrite > SPI_FLASH_PageSize */


69 {


70 /*地址不对齐多出的count分开处理,不加入这个运算*/


71 NumByteToWrite -= count;


72 NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;


73 NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;


74


75 SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);


76 WriteAddr += count;


77 pBuffer += count;


78


79 /*把整数页都写了*/


80 while (NumOfPage--)


81 {


82 SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);


83 WriteAddr += SPI_FLASH_PageSize;


84 pBuffer += SPI_FLASH_PageSize;


85 }


86 /*若有多余的不满一页的数据,把它写完*/


87 if (NumOfSingle != 0)


88 {


89 SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);


90 }


91 }


92 }


93 }


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


从FLASH读取数据


相对于写入,FLASH芯片的数据读取要简单得多,使用读取指令"Read Data"即可,其指令时序见图 2414。

图 2414 SPI FLASH读取数据时序


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


代码清单 2413 从FLASH读取数据


1 /**


2 * @brief 读取FLASH数据


3 * @param pBuffer,存储读出数据的指针


4 * @param ReadAddr,读取地址


5 * @param NumByteToRead,读取数据长度


6 * @retval 无


7 */


8 void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead)


9 {


10 /* 选择FLASH: CS低电平 */


11 SPI_FLASH_CS_LOW();


12


13 /* 发送读指令 */


14 SPI_FLASH_SendByte(W25X_ReadData);


15


16 /* 发送读地址高位 */


17 SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);


18 /* 发送读地址中位 */


19 SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);


20 /* 发送读地址低位 */


21 SPI_FLASH_SendByte(ReadAddr & 0xFF);


22


23 /* 读取数据 */


24 while (NumByteToRead--)


25 {


26 /* 读取一个字节*/


27 *pBuffer = SPI_FLASH_SendByte(Dummy_Byte);


28 /* 指向下一个字节缓冲区 */


29 pBuffer++;


30 }


31


32 /* 停止信号 FLASH: CS 高电平 */


33 SPI_FLASH_CS_HIGH();


34 }


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


3.    main函数


最后我们来编写main函数,进行FLASH芯片读写校验,见代码清单 2414。


代码清单 2414 main函数


1 /* 获取缓冲区的长度 */


2 #define TxBufferSize1 (countof(TxBuffer1) - 1)


3 #define RxBufferSize1 (countof(TxBuffer1) - 1)


4 #define countof(a) (sizeof(a) / sizeof(*(a)))


5 #define BufferSize (countof(Tx_Buffer)-1)


6


7 #define FLASH_WriteAddress 0x00000


8 #define FLASH_ReadAddress FLASH_WriteAddress


9 #define FLASH_SectorToErase FLASH_WriteAddress


10


11


12 /* 发送缓冲区初始化 */


13 uint8_t Tx_Buffer[] = "感谢您选用秉火stm32开发板\r\n";


14 uint8_t Rx_Buffer[BufferSize];


15


16 //读取的ID存储位置


17 __IO uint32_t DeviceID = 0;


18 __IO uint32_t FlashID = 0;


19 __IO TestStatus TransferStatus1 = FAILED;


20


21 // 函数原型声明


22 void Delay(__IO uint32_t nCount);


23


24 /*


25 * 函数名:main


26 * 描述:主函数


27 * 输入:无


28 * 输出:无


29 */


30 int main(void)


31 {


32 LED_GPIO_Config();


33 LED_BLUE;


34


35 /* 配置串口1为:115200 8-N-1 */


36 Debug_USART_Config();


37


38 printf("\r\n这是一个16M串行flash(W25Q128)实验 \r\n");


39


40 /* 16M串行flash W25Q128初始化 */


41 SPI_FLASH_Init();


42


43 Delay( 200 );


44


45 /* 获取 SPI Flash ID */


46 FlashID = SPI_FLASH_ReadID();


47


48 /* 检验 SPI Flash ID */


49 if (FlashID == sFLASH_ID)


50 {


51 printf("\r\n检测到SPI FLASH W25Q128 !\r\n");


52


53 /* 擦除将要写入的 SPI FLASH 扇区,FLASH写入前要先擦除 */


54 SPI_FLASH_SectorErase(FLASH_SectorToErase);


55


56 /* 将发送缓冲区的数据写到flash中 */


57 SPI_FLASH_BufferWrite(Tx_Buffer, FLASH_WriteAddress, BufferSize);


58 printf("\r\n写入的数据为:\r\n%s", Tx_Buffer);


59


60 /* 将刚刚写入的数据读出来放到接收缓冲区中 */


61 SPI_FLASH_BufferRead(Rx_Buffer, FLASH_ReadAddress, BufferSize);


62 printf("\r\n读出的数据为:\r\n%s", Rx_Buffer);


63


64 /* 检查写入的数据与读出的数据是否相等 */


65 TransferStatus1 = Buffercmp(Tx_Buffer, Rx_Buffer, BufferSize);


66


67 if ( PASSED == TransferStatus1 )


68 {


69 LED_GREEN;


70 printf("\r\n16M串行flash(W25Q128)测试成功!\n\r");


71 }


72 else


73 {


74 LED_RED;


75 printf("\r\n16M串行flash(W25Q128)测试失败!\n\r");


76 }


77 }// if (FlashID == sFLASH_ID)


78 else


79 {


80 LED_RED;


81 printf("\r\n获取不到 W25Q128 ID!\n\r");


82 }


83


84 SPI_Flash_PowerDown();


85 while (1);


86 }


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


注意:


由于实验板上的FLASH芯片默认已经存储了特定用途的数据,如擦除了这些数据会影响到某些程序的运行。所以我们预留了FLASH芯片的"第0扇区(0-4096地址)"专用于本实验,如非必要,请勿擦除其它地址的内容。如已擦除,可在配套资料里找到"刷外部FLASH内容"程序,根据其说明给FLASH重新写入出厂内容。


24.4.3 下载验证


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


24.5 每课一问


1.    在SPI外设初始化部分,MISO引脚可以设置为输入模式吗?为什么?实际测试现象如何?


2.    尝试使用FLASH芯片存储int整型变量,float型浮点变量,编写程序写入数据,并读出校验。


3.    如果扇区未经擦除就写入,会有什么后果?请做实验验证。


4.    简述FLASH存储器与EEPROM存储器的区别。



关键字:STM32  SPI读写串  行Flash

编辑:什么鱼 引用地址:http://www.eeworld.com.cn/mcu/2018/ic-news102141953.html
本网站转载的所有的文章、图片、音频视频文件等资料的版权归版权所有人所有,本站采用的非本站原创文章及图片等内容无法一一联系确认版权者。如果本网所选内容的文章作者及编辑认为其作品不宜公开自由传播,或不应无偿使用,请及时通过电子邮件或电话通知我们,以迅速采取适当措施,避免给双方造成不必要的经济损失。

上一篇:STM32学习之:DMA详解
下一篇:STM32学习之:IAR中确认某段代码的执行时间

关注eeworld公众号 快捷获取更多信息
关注eeworld公众号
快捷获取更多信息
关注eeworld服务号 享受更多官方福利
关注eeworld服务号
享受更多官方福利

推荐阅读

STM32堆栈设置

1.堆和栈大小 定义大小在startup_stm32f2xx.sStack_Size      EQU     0x00000400                AREA    STACK, NOINIT, READWRITE, ALIGN=3Stack_Mem      
发表于 2019-04-16
STM32堆栈设置

STM32堆和栈(Heap & Stack)的资料理解

源起:在移植cjson的过程中,解析json包的时候发现动态内存分配不足而导致解析失败,为解决这一问题,而深入了解stm32的堆和栈。stm32的存储器结构。Flash,SRAM寄存器和输入输出端口被组织在同一个4GB的线性地址空间内。可访问的存储器空间被分成8个主要块,每个块为512MB。FLASH存储下载的程序。SRAM是存储运行程序中的数据。而SRAM一般分这几个部分:静态存储区:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据、全局数据和常量。栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率
发表于 2019-04-16
STM32堆和栈(Heap & Stack)的资料理解

STM32定义堆栈地址到ram区顶部

本设置针对stm32f103rbt6的设置,该芯片RAM大小为20kB,故RAM区地址范围为0x20000000—0x20005000,芯片信息如下图所示;第一步:设置.sct文件;;*************************************************************; *** Scatter-Loading Description Filegenerated by uVision ***; *************************************************************LR_IROM1 0x08000000 0x00020000  
发表于 2019-04-16
STM32定义堆栈地址到ram区顶部

STM32之程序如何防止堆栈溢出

近日为某个项目写了个草稿程序,即非正式程序,后来发现老是进入hardfaulthandler,原来是堆栈溢出,后仔细查看发现函数调用纵深太深,最多的时候可保持7个函数在堆栈中调用。因此有心得如下:一、函数调用不要纵深太深,即以下模式:main(){   fun1();}fun1(){  fun2();}fun2(){   fun3();}fun3(){  fun4();}fun4(){  fun5();}fun5(){  fun6();}fun6(){   fun7();}这样子main函数要调用fun1函数完成某个功能,则要一直调到
发表于 2019-04-16

stm32之堆栈

stm32中的堆栈设置keil编译完成时存储情况当编译成功时,会出现: BUILD://Program Size: Code=340 RO-data=252 RW-data=0 ZI-data=1632Code:程序代码部分RO-data: 程序定义的常量const tempRW-data:已初始化的全局变量ZI-data:未初始化的全局变量片中的:flash=Code+RO-data+RW-dataRAM=RW-data+ZI-data通过上面的BUILD可以看出,这个程序已经用了1600多的RAM,为什么会出用到这么多的RAM呢?在startup_stm32f10x_md.s文件中存在:St
发表于 2019-04-16

说说STM32的堆栈与内存

1.概念这里所说的堆栈,是针对单片机所说的“堆”与“栈”,指的是内存中一片特殊用途的区域。而不是数据结构中的堆栈(虽然其实规则一样)。这里所说的内存,是指RAM,RAM包括SRAM,DRAM等。而不是什么手机内存卡之类。这里所说的flash,指的是用作为ROM的存储器,保存代码与常量数据。而不是动画制作。。。栈的生长方向:指的是入栈方向,从高地址向低地址生长叫做向下生长,或逆向生长;反过来就叫向上生长,或正向生长。STM32的栈是向下生长。2.内存中的堆栈安排确切地说,是keil mdk根据STM32的特性,对stm32的RAM甚至flash进行部署。编译工程后,在生成的.map文件里可以看到具体的安排。双击工程界面的工程根目录
发表于 2019-04-16
说说STM32的堆栈与内存

小广播

何立民专栏

单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

电子工程世界版权所有 京ICP证060456号 京ICP备10001474号 电信业务审批[2006]字第258号函 京公海网安备110108001534 Copyright © 2005-2019 EEWORLD.com.cn, Inc. All rights reserved