STM32系统学习——SPI(读写串行 FLASH)

一、SPI 协议简介
SPI 协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设备接口,是一种高速全双工的通信总线。它被广泛地使用在 ADC、LCD 等设备与 MCU 间,要求通讯速率较高的场合。
可与 I2C 章节对比阅读,体会两种通讯总线的差异以及 EEPROM 存储器与 FLASH 存储器的区别。
1、物理层

SPI通讯使用 3 条总线及片选线,3条总线分别为 SCK、MOSI、MISO,片选线为 SS,它们的作用介绍如下:
(1) SS( 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 时钟频率最大为f pclk /2,两个设备之间通讯时,通讯速率受限于低速设备。
(3) MOSI (Master Output, Slave Input):主设备输出/从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,即这条线上数据的方向为主机到从机。
(4) MISO(Master Input,,Slave Output):主设备输入/从设备输出引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。
2、协议层

这是一个主机的通讯时序。NSS、SCK、MOSI 信号都由主机控制产生,而 MISO 的信号由从机产生,主机通过该信号线读取从机的数据。MOSI 与 MISO 的信号只在 NSS 为低电平的时候才有效,在 SCK的每个时钟周期 MOSI和 MISO传输一位数据。
(1) 通讯的起始和停止信号
在图中的标号1处,NSS 信号线由高变低,是 SPI 通讯的起始信号。NSS 是每个从机各自独占的信号线,当从机在自己的 NSS 线检测到起始信号后,就知道自己被主机选中了,开始准备与主机通讯。在图中的标号处,NSS 信号由低变高,是 SPI 通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。
(2)数据有效性
SPI使用 MOSI及 MISO信号线来传输数据,使用 SCK信号线进行数据同步。MOSI及MISO 数据线在 SCK 的每个时钟周期传输一位数据,且数据输入输出是同时进行的。数据传输时,MSB 先行或 LSB 先行并没有作硬性规定,但要保证两个 SPI通讯设备之间使用同样的协定,一般都会采用图 中的 MSB先行模式。
观察图中的2345标号处,MOSI及 MISO的数据在 SCK的上升沿期间变化输出,在SCK 的下降沿时被采样。即在 SCK 的下降沿时刻,MOSI 及 MISO 的数据有效,高电平时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效,MOSI及 MISO为下一次表示数据做准备。
SPI每次数据传输可以 8 位或 16 位为单位,每次传输的单位数不受限制。
(3)CPOL/CPHA及通讯模式
上面讲述的图 中的时序只是 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的时序图。首先,根据 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的偶数边沿被采样,见下图。

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

上图即是SPI的四种模式。
二、STM32 的 SPI 特性及架构
1、STM32的 SPI外设简介
STM32 的 SPI 外设可用作通讯的主机及从机,支持最高的 SCK 时钟频率为 f pclk /2(STM32F103型号的芯片默认 f pclk1 为 72MHz,f pclk2 为 36MHz),完全支持 SPI协议的 4种模式,数据帧长度可设置为 8 位或 16 位,可设置数据 MSB 先行或 LSB 先行。它还支持双线全双工(前面小节说明的都是这种模式)、双线单向以及单线模式。其中双线单向模式可以同时使用 MOSI 及 MISO 数据线向一个方向传输数据,可以加快一倍的传输速度。而单线模式则可以减少硬件接线,当然这样速率会受到影响。我们只讲解双线全双工模式。
2、架构剖析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4hUiOTm7-1585793098998)(https://img-blog.csdn.net/20171209152154276?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvenhoMTU5MjAwMA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)]
1)通讯引脚
PI的所有硬件架构都从图 中左侧 MOSI、MISO、SCK及 NSS线展开的。STM32芯片有多个 SPI外设,它们的 SPI通讯信号引出到不同的 GPIO引脚上,使用时必须配置到这些指定的引脚。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CKBt1GKy-1585793098998)(https://img-blog.csdn.net/20171209152309905?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvenhoMTU5MjAwMA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)]
其中 SPI1 是 APB2上的设备,最高通信速率达 36Mbtis/s,SPI2、SPI3 是 APB1上的设备,最高通信速率为 18Mbits/s。除了通讯速率,在其它功能上没有差异。其中 SPI3用到了下载接口的引脚,这几个引脚默认功能是下载,第二功能才是 IO 口,如果想使用 SPI3 接口,则程序上必须先禁用掉这几个 IO 口的下载功能。一般在资源不是十分紧张的情况下,这几个 IO 口是专门用于下载和调试程序,不会复用为 SPI3。
2)时钟控制逻辑
SCK线的时钟信号,由波特率发生器根据“控制寄存器CR1”中的BR[0:2]位控制,该位是对 f pclk 时钟的分频因子,对 f pclk 的分频结果就是 SCK 引脚的输出时钟频率。

其中的 f pclk 频率是指 SPI所在的 APB总线频率,APB1为 f pclk1 ,APB2为 f pckl2 。
通过配置“控制寄存器 CR”的“CPOL 位”及“CPHA”位可以把 SPI 设置成前面分析的 4 种 SPI模式。
3)数据控制逻辑
SPI的 MOSI及 MISO 都连接到数据移位寄存器上,数据移位寄存器的数据来源及目标接收、发送缓冲区以及 MISO、MOSI 线。当向外发送数据的时候,数据移位寄存器以“发送缓冲区”为数据源,把数据一位一位地通过数据线发送出去;当从外部接收数据的时候,数据移位寄存器把数据线采样到的数据一位一位地存储到“接收缓冲区”中。通过写 SPI的“数据寄存器 DR”把数据填充到发送 F 缓冲区中,通讯读“数据寄存器 DR”,可以获取接收缓冲区中的内容。其中数据帧长度可以通过“控制寄存器 CR1”的“DFF 位”配置成 8位及 16位模式;配置“LSBFIRST位”可选择 MSB 先行还是 LSB 先行。

4)整体控制逻辑
整体控制逻辑负责协调整个 SPI 外设,控制逻辑的工作模式根据我们配置的“控制寄存器(CR1/CR2)”的参数而改变,基本的控制参数包括前面提到的 SPI 模式、波特率、LSB先行、主从模式、单双向模式等等。在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器(SR)”,我们只要读取状态寄存器相关的寄存器位,就可以了解 SPI 的工作状态了。除此之外,控制逻辑还根据要求,负责控制产生 SPI 中断信号、DMA 请求及控制NSS 信号线。
实际应用中,我们一般不使用 STM32 SPI外设的标准 NSS 信号线,而是更简单地使用普通的 GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。
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”中的数据。

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

1 typedef struct2 {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;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这些结构体成员说明如下,其中括号内的文字是对应参数在 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外设。
四、SPI—读写串行 FLASH 实验
FLSAH 存储器又称闪存,它与 EEPROM 都是掉电后数据不丢失的存储器,但 FLASH存储器容量普遍大于 EEPROM,现在基本取代了它的地位。我们生活中常用的 U 盘、SD卡、SSD 固态硬盘以及我们 STM32 芯片内部用于存储程序的设备,都是 FLASH 类型的存储器。在存储控制上,最主要的区别是 FLASH 芯片只能一大片一大片地擦写,而在“I2C章节”中我们了解到 EEPROM可以单个字节擦写。
本小节以一种使用 SPI 通讯的串行 FLASH 存储芯片的读写实验讲解 STM32 的SPI 使用方法。实验中 STM32 的 SPI 外设采用主模式,通过查询事件的方式来确保正常通讯。
1、硬件设计
本实验板中的 FLASH芯片(型号:W25Q64)是一种使用 SPI通讯协议的NOR FLASH存储 器 , 它 的 CS/CLK/DIO/DO 引 脚 分 别 连 接 到 了 STM32 对 应 的 SPI 引 脚NSS/SCK/MOSI/MISO 上,其中 STM32的 NSS 引脚虽然是其片上 SPI外设的硬件引脚,但实际上后面的程序只是把它当成一个普通的 GPIO,使用软件的方式控制 NSS 信号,所以在 SPI的硬件设计中,NSS 可以随便选择普通的 GPIO,不必纠结于选择硬件 NSS信号。
FLASH 芯片中还有 WP 和 HOLD 引脚。WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。
我们直接接电源,不使用通讯暂停功能。关于 FLASH 芯片的更多信息,可参考其数据手册《W25Q64》来了解。若实验板 FLASH的型号或控制引脚不一样,只需根据我们的工程修改即可,程序的控制原理相同。

2、软件设计
为了使工程更加有条理,我们把读写 FLASH相关的代码独立分开存储,方便以后移植。
在“工程模板”之上新建“bsp_spi_flash.c”及“bsp_spi_ flash.h”文件,这些文件也可根据您的喜好命名,它们不属于 STM32标准库的内容,是由我们自己根据应用需要编写的。
1) 编程要点
初始化通讯使用的目标引脚及端口时钟;
使能 SPI外设的时钟;
配置 SPI外设的模式、地址、速率等参数并使能 SPI外设;
编写基本 SPI按字节收发的函数;
编写对 FLASH 擦除及读写操作的的函数;
编写测试程序,对读写数据进行校验。
2) 代码分析
SPI 硬件相关宏定义
我们把 SPI硬件相关的配置都以宏的形式定义到 “bsp_spi_ flash.h”文件中

1 /*SPI 接口定义-开头****************************/2 #define FLASH_SPIx SPI13 #define FLASH_SPI_APBxClock_FUN RCC_APB2PeriphClockCmd4 #define FLASH_SPI_CLK RCC_APB2Periph_SPI15 6 //CS(NSS)引脚 片选选普通 GPIO 即可7 #define FLASH_SPI_CS_APBxClock_FUN RCC_APB2PeriphClockCmd8 #define FLASH_SPI_CS_CLK RCC_APB2Periph_GPIOA9 #define FLASH_SPI_CS_PORT GPIOA10 #define FLASH_SPI_CS_PIN GPIO_Pin_411 12 //SCK 引脚13 #define FLASH_SPI_SCK_APBxClock_FUN RCC_APB2PeriphClockCmd14 #define FLASH_SPI_SCK_CLK RCC_APB2Periph_GPIOA15 #define FLASH_SPI_SCK_PORT GPIOA16 #define FLASH_SPI_SCK_PIN GPIO_Pin_517 //MISO 引脚18 #define FLASH_SPI_MISO_APBxClock_FUN RCC_APB2PeriphClockCmd19 #define FLASH_SPI_MISO_CLK RCC_APB2Periph_GPIOA20 #define FLASH_SPI_MISO_PORT GPIOA21 #define FLASH_SPI_MISO_PIN GPIO_Pin_622 //MOSI 引脚23 #define FLASH_SPI_MOSI_APBxClock_FUN RCC_APB2PeriphClockCmd24 #define FLASH_SPI_MOSI_CLK RCC_APB2Periph_GPIOA25 #define FLASH_SPI_MOSI_PORT GPIOA26 #define FLASH_SPI_MOSI_PIN GPIO_Pin_727 28 #define FLASH_SPI_CS_LOW() GPIO_ResetBits( FLASH_SPI_CS_PORT, FLASH_SPI_CS_PIN )29 #define FLASH_SPI_CS_HIGH() GPIO_SetBits( FLASH_SPI_CS_PORT, FLASH_SPI_CS_PIN )30 31 /*SPI 接口定义-结尾****************************/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

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

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

1 /**2 * @brief SPI_FLASH 初始化3 * @param 无4 * @retval 无5 */6 void SPI_FLASH_Init(void)7 {8 SPI_InitTypeDef SPI_InitStructure;9 GPIO_InitTypeDef GPIO_InitStructure;10 11 /* 使能 SPI 时钟 */12 FLASH_SPI_APBxClock_FUN ( FLASH_SPI_CLK, ENABLE );13 14 /* 使能 SPI 引脚相关的时钟 */15 FLASH_SPI_CS_APBxClock_FUN ( FLASH_SPI_CS_CLK|FLASH_SPI_SCK_CLK|16 FLASH_SPI_MISO_PIN|FLASH_SPI_MOSI_PIN, ENABLE );17 18 /* 配置 SPI 的 CS 引脚,普通 IO 即可 */19 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_CS_PIN;20 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;21 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;22 GPIO_Init(FLASH_SPI_CS_PORT, &GPIO_InitStructure);23 24 /* 配置 SPI 的 SCK 引脚*/25 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN;26 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;27 GPIO_Init(FLASH_SPI_SCK_PORT, &GPIO_InitStructure);28 29 /* 配置 SPI 的 MF103-霸道引脚*/30 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN;31 GPIO_Init(FLASH_SPI_MISO_PORT, &GPIO_InitStructure);32 33 /* 配置 SPI 的 MOSI 引脚*/34 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;35 GPIO_Init(FLASH_SPI_MOSI_PORT, &GPIO_InitStructure);36 37 /* 停止信号 FLASH: CS 引脚高电平*/38 FLASH_SPI_CS_HIGH();39 //为方便讲解,以下省略 SPI 模式初始化部分40 }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

与所有使用到 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 外设中的这些参数配置一致。

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 /* SPI 模式配置 */13 // FLASH 芯片 支持 SPI 模式 0 及模式 3,据此设置 CPOL CPHA14 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_4;21 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;22 SPI_InitStructure.SPI_CRCPolynomial = 7;23 SPI_Init(FLASH_SPIx, &SPI_InitStructure);24 25 /* 使能 SPI */26 SPI_Cmd(FLASH_SPIx, ENABLE);27 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

这段代码中,把 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通讯了,复杂的数据通讯都是由单个字节数据收发组成的,我们看看它的代码实现。

1 #define Dummy_Byte 0xFF2 /**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_SPIx, SPI_I2S_FLAG_TXE) == RESET)13 {14 if ((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);15 }16 17 /* 写入数据寄存器,把要写入的数据写入发送缓冲区 */18 SPI_I2S_SendData(FLASH_SPIx, byte);19 20 SPITimeout = SPIT_FLAG_TIMEOUT;21 22 /* 等待接收缓冲区非空,RXNE 事件 */23 while (SPI_I2S_GetFlagStatus(FLASH_SPIx, SPI_I2S_FLAG_RXNE) == RESET)24 {25 if ((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);26 }27 28 /* 读取数据寄存器,获取接收缓冲区数据 */29 return SPI_I2S_ReceiveData(FLASH_SPIx);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 }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 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芯片的数据手册《W25Q64》,可了解各种它定义的各种指令的功能及指令格式

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

FLASH 指令编码表1 /*FLASH 常用命令*/2 #define W25X_WriteEnable 0x063 #define W25X_WriteDisable 0x044 #define W25X_ReadStatusReg 0x055 #define W25X_WriteStatusReg 0x016 #define W25X_ReadData 0x037 #define W25X_FastReadData 0x0B8 #define W25X_FastReadDual 0x3B9 #define W25X_PageProgram 0x0210 #define W25X_BlockErase 0xD811 #define W25X_SectorErase 0x2012 #define W25X_ChipErase 0xC713 #define W25X_PowerDown 0xB914 #define W25X_ReleasePowerDown 0xAB15 #define W25X_DeviceID 0xAB16 #define W25X_ManufactDeviceID 0x9017 #define W25X_JedecDeviceID 0x9F18 /*其它*/19 #define sFLASH_ID 0XEF401720 #define Dummy_Byte 0xFF
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

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

读取 FLASH 芯片 ID1 /**2 * @brief 读取 FLASH ID3 * @param 无4 * @retval FLASH ID5 */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 }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

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

FLASH 写使能以及读取当前状态
在向 FLASH 芯片存储矩阵写入数据前,首先要使能写操作,通过“Write Enable”命令即可写使能。

1 /**2 * @brief 向 FLASH 发送 写使能 命令3 * @param none4 * @retval none5 */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 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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


我们只关注这个状态寄存器的第0位“BUSY”,当这个位为“1”时,表明FLASH芯片处于忙碌状态,它可能正在对内部的存储矩阵进行“擦除”或“数据写入”的操作。
利用指令表中的“Read Status Register”指令可以获取 FLASH 芯片状态寄存器的内容,其时序见下图


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

1 /* WIP(busy)标志,FLASH 内部正在写入 */2 #define WIP_Flag 0x013 4 /**5 * @brief 等待 WIP(BUSY)标志被置 0,即等待到 FLASH 内部数据写入完毕6 * @param none7 * @retval none8 */9 void SPI_FLASH_WaitForWriteEnd(void)10 {11 u8 FLASH_Status = 0;12 13 /* 选择 FLASH: CS 低 */14 SPI_FLASH_CS_LOW();15 16 /* 发送 读状态寄存器 命令 */17 SPI_FLASH_SendByte(W25X_ReadStatusReg);18 19 /* 若 FLASH 忙碌,则等待 */20 do21 {22 /* 读取 FLASH 芯片的状态寄存器 */23 FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);24 }25 while ((FLASH_Status & WIP_Flag) == SET); /* 正在写入标志 */26 27 /* 停止信号 FLASH: CS 高 */28 SPI_FLASH_CS_HIGH();29 }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

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

FLASH 扇区擦除
由于 FLASH存储器的特性决定了它只能把原来为“1”的数据位改写成“0”,而原来为“0”的数据位不能直接改写为“1”。所以这里涉及到数据“擦除”的概念,在写入前,必须要对目标存储矩阵进行擦除操作,把矩阵中的数据位擦除为“1”,在数据写入的时候,如果要存储数据“1”,那就不修改存储矩阵 ,在要存储数据“0”时,才更改该位。通常,对存储矩阵擦除的基本操作单位都是多个字节进行,如本例子中的 FLASH芯片支持“扇区擦除”、“块擦除”以及“整片擦除”。
扇区擦除指令的第一个字节为指令编码,紧接着发送的 3 个字节用于表示要擦除的存
储矩阵地址。要注意的是在扇区擦除指令前,还需要先发送“写使能”指令,发送扇区擦
除指令后,通过读取寄存器状态等待扇区擦除操作完毕

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 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

这段代码调用的函数在前面都已讲解,只要注意发送擦除地址时高位在前即可。调用扇区擦除指令时注意输入的地址要对齐到 4KB。
FLASH 的页写入
目标扇区被擦除完毕后,就可以向它写入数据了。与 EEPROM 类似,FLASH 芯片也有页写入命令,使用页写入命令最多可以一次向 FLASH 传输 256个字节的数据,我们把这个单位为页大小。FLASH 页写入的时序见图 。


从时序图可知,第 1 个字节为“页写入指令”编码,2-4 字节为要写入的“地址 A”,接着的是要写入的内容,最多个可以发送 256 字节数据,这些数据将会从“地址 A”开始,按顺序写入到 FLASH的存储矩阵。若发送的数据超出 256个,则会覆盖前面发送的数据。与擦除指令不一样,页写入指令的地址并不要求按 256 字节对齐,只要确认目标存储单元是擦除状态即可(即被擦除后没有被写入过)。所以,若对“地址 x”执行页写入指令后,发送了 200 个字节数据后终止通讯,下一次再执行页写入指令,从“地址(x+200)”开始写入200个字节也是没有问题的(小于256均可)。 只是在实际应用中由于基本擦除单元是4KB,一般都以扇区为单位进行读写。

把页写入时序封装成函数

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 }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

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

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

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 整数倍,13 运算结果 Addr 值为 0*/14 Addr = WriteAddr % SPI_FLASH_PageSize;15 16 /*差 count 个数据值,刚好可以对齐到页地址*/17 count = SPI_FLASH_PageSize - Addr;18 /*计算出要写多少整数页*/19 NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;20 /*mod 运算求余,计算出剩余不满一页的字节数*/21 NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;22 23 /* Addr=0,则 WriteAddr 刚好按页对齐 aligned */24 if (Addr == 0)25 {26 /* NumByteToWrite < SPI_FLASH_PageSize */27 if (NumOfPage == 0)28 {29 SPI_FLASH_PageWrite(pBuffer, WriteAddr,30 NumByteToWrite);31 }32 else /* NumByteToWrite > SPI_FLASH_PageSize */33 {34 /*先把整数页都写了*/35 while (NumOfPage--)36 {37 SPI_FLASH_PageWrite(pBuffer, WriteAddr,38 SPI_FLASH_PageSize);39 WriteAddr += SPI_FLASH_PageSize;40 pBuffer += SPI_FLASH_PageSize;41 }42 /*若有多余的不满一页的数据,把它写完*/43 SPI_FLASH_PageWrite(pBuffer, WriteAddr,44 NumOfSingle);45 }46 }47 /* 若地址与 SPI_FLASH_PageSize 不对齐 */48 else49 {50 /* NumByteToWrite < SPI_FLASH_PageSize */51 if (NumOfPage == 0)52 {53 /*当前页剩余的 count 个位置比 NumOfSingle 小,一页写不完*/54 if (NumOfSingle > count)55 {56 temp = NumOfSingle - count;57 /*先写满当前页*/58 SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);59 60 WriteAddr += count;61 pBuffer += count;62 /*再写剩余的数据*/63 SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);64 }65 else /*当前页剩余的 count 个位置能写完 NumOfSingle 个数据*/66 {67 SPI_FLASH_PageWrite(pBuffer, WriteAddr,68 NumByteToWrite);69 }70 }71 else /* NumByteToWrite > SPI_FLASH_PageSize */72 {73 /*地址不对齐多出的 count 分开处理,不加入这个运算*/74 NumByteToWrite -= count;75 NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;76 NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;77 78 /* 先写完 count 个数据,为的是让下一次要写的地址对齐 */79 SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);80 81 /* 接下来就重复地址对齐的情况 */82 WriteAddr += count;83 pBuffer += count;84 /*把整数页都写了*/85 while (NumOfPage--)86 {87 SPI_FLASH_PageWrite(pBuffer, WriteAddr,88 SPI_FLASH_PageSize);89 WriteAddr += SPI_FLASH_PageSize;90 pBuffer += SPI_FLASH_PageSize;91 }92 /*若有多余的不满一页的数据,把它写完*/93 if (NumOfSingle != 0)94 {95 SPI_FLASH_PageWrite(pBuffer, WriteAddr,96 NumOfSingle);97 }98 }99 }100 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100

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

从 从 FLASH 读取数据
相对于写入,FLASH 芯片的数据读取要简单得多,使用读取指令“Read Data”即可。


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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

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

MAIN函数

 main 函数1 int main(void)2 {3 LED_GPIO_Config();4 LED_BLUE;5 6 /* 配置串口 1 为:115200 8-N-1 */7 USART_Config();8 printf('\r\n 这是一个 8Mbyte 串行 flash(W25Q64)实验 \r\n');9 10 /* 8M 串行 flash W25Q64 初始化 */11 SPI_FLASH_Init();12 13 /* 获取 Flash Device ID */14 DeviceID = SPI_FLASH_ReadDeviceID();15 Delay( 200 );16 17 /* 获取 SPI Flash ID */18 FlashID = SPI_FLASH_ReadID();19 printf('\r\n FlashID is 0x%X,\20 Manufacturer Device ID is 0x%X\r\n', FlashID, DeviceID);21 22 /* 检验 SPI Flash ID */23 if (FlashID == sFLASH_ID)24 {25 printf('\r\n 检测到串行 flash W25Q64 !\r\n');26 27 /* 擦除将要写入的 SPI FLASH 扇区,FLASH 写入前要先擦除 */28 // 这里擦除 4K,即一个扇区,擦除的最小单位是扇区29 SPI_FLASH_SectorErase(FLASH_SectorToErase);30 31 /* 将发送缓冲区的数据写到 flash 中 */32 // 这里写一页,一页的大小为 256 个字节33 SPI_FLASH_BufferWrite(Tx_Buffer, FLASH_WriteAddress, BufferSize);34 printf('\r\n 写入的数据为:%s \r\t', Tx_Buffer);35 36 /* 将刚刚写入的数据读出来放到接收缓冲区中 */37 SPI_FLASH_BufferRead(Rx_Buffer, FLASH_ReadAddress, BufferSize);38 printf('\r\n 读出的数据为:%s \r\n', Rx_Buffer);39 40 /* 检查写入的数据与读出的数据是否相等 */41 TransferStatus1 = Buffercmp(Tx_Buffer, Rx_Buffer, BufferSize);42 43 if ( PASSED == TransferStatus1 )44 {45 LED_GREEN;46 printf('\r\n 8M 串行 flash(W25Q64)测试成功!\n\r');47 }48 else49 {50 LED_RED;51 printf('\r\n 8M 串行 flash(W25Q64)测试失败!\n\r');52 }53 }// if (FlashID == sFLASH_ID)54 else// if (FlashID == sFLASH_ID)55 {56 LED_RED;57 printf('\r\n 获取不到 W25Q64 ID!\n\r');58 }59 60 while (1);61 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

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

本文引用《STM32库开发实战指南》

(0)

相关推荐

  • STM32无线通信——nRF24L01通信模块

    不同型号STM32的无线通信--基于一样的nRF24L01芯片模块 在此声明一下全部代码均不允许转发以及在商业上的行为等,-Mannix声明. 本次讲解主要内容 1.实验目的 2.实验硬件 3.芯片模 ...

  • nRF24L01 使用心得

    为了方便大家测试程序,先将程序开发环境说明一下,程序是在 STC15W4K56S4 上开发的,开发板,nRF24L01 无线模块都是某宝淘来的. 原理图 PDF: https://download.c ...

  • STM32系统学习——USART(串口通信)

    串口通信是一种设备间非常常用的串行通行方式,其简单便捷,大部分电子设备都支持. 一.物理层 常用RS-232标准,主要规定了信号的用途.通信接口以及信号的电平标准. "DB9接口" ...

  • 赛普拉斯代理4Mbit串行SPI铁电存储器CY15B104Q-LHXI

    赛普拉斯型号CY15B104Q-LHXI主要采用先进铁电工艺的4Mbit非易失性存储器.铁电随机存取存储器或FRAM是非易失性的,并且执行类似于RAM的读取和写入操作.它提供了151年的可靠数据保留, ...

  • 痞子衡嵌入式:串行NOR Flash的DQS信号功能简介

    https://www.cnblogs.com/henjay724/p/14438002.html 今天痞子衡给大家分享的是串行NOR Flash的DQS信号功能. 串行NOR Flash在嵌入式里的 ...

  • 64Mbit SOIC8封装SPI接口的国产串行SRAM EMI7064

    现在的电子系统应用,对SRAM要求越来越高,单片机或ARM内部的RAM越来越不够用.国产EMI公司的64Mbit SPI接口的SRAM芯片EMI7064.这样的IC用途一般是:数据采集或信号处理过程的 ...

  • Everspin代理非易失性1Mb串行SPI MRAM--MR25H10MDFR

    MRAM(磁性只读存储器)和FRAM(铁电RAM)都声称具有相似的性能优势:低电压操作.长寿命和非常高的速度.他们以不同的方式实现这些目标,尽管在每种情况下,性能突破背后都有创新的材料技术.   MR ...

  • Itrace跟踪进程调用库函数参数选项-linux系统学习心得

    Itrace能够跟踪进程的库函数调用,它会显现出调用了哪个库函数,而strace则是跟踪进程的每个系统调用.随着Linux运维技术的发展,各企业对于Linux运维工程师人才的需求也是逐渐增加,学习Li ...

  • 系统学习股票投资需要阅读的16本书 经常有朋友问我,有没有股票投资相关的书推荐。

    经常有朋友问我,有没有股票投资相关的书推荐. 下面就将自己看过的投资理念.操作系统.企业选择.财报分析.实践案例等方面的书做一个书单及简介,希望对于想较为系统学习股票投资的股友们有所帮助 一.< ...

  • 【教育读书】北京四中“3641系统学习法”

    让知识回家 一站式收藏您的阅读与创作 [教育读书] 北京四中"3641系统学习法"     "3641系统学习方法"是北京四中学生多用的一种学习方法.主要分为: ...

  • 北京四中“3641系统学习法”

    "3641系统学习方法"是北京四中学生多用的一种学习方法.主要分为 3个过程.4个环节.6个习惯.1个学习计划. 一.3个过程 3个过程实际上就是把基础知识或者新的知识点让学生通过 ...