SPI 协议简介
SPI是串行外设接口(Serial Peripheral Interface)的缩写,是美国摩托罗拉公司(Motorola)最先推出的一种同步串行传输规范,也是一种单片机外设芯片串行扩展接口,是一种高速、全双工、同步通信总线,所以可以在同一时间发送和接收数据,SPI没有定义速度限制,通常能达到甚至超过10M/bps。
SPI有主、从两种模式,通常由一个主模块和一个或多个从模块组成(SPI不支持多主机),主模块选择一个从模块进行同步通信,从而完成数据的交换。提供时钟的为主设备(Master),接收时钟的设备为从设备(Slave),SPI接口的读写操作,都是由主设备发起,当存在多个从设备时,通过各自的片选信号进行管理。
SPI通信需要至少4根线
它们是MISO(主设备数据输入)、MOSI(主设备数据输出)、SCLK(时钟)和CS/SS(片选)。
MISO( Master Input Slave Output):主设备输入/从设备输出引脚。主机从这条信号线读入 数 据,从机的数据由这条信号线输出到主机,即在这 条线上数据的方向为从机到主机。;
MOSI(Master Output Slave Input):主设备输出/从设备输入引脚。主机的数据从这条信号 线输出,从机由这条信号线读入主机发送的数据,即 这条线上数据的方向为主机到从机。;
SCLK(Serial Clock):时钟信号线,用于通讯数据同步。它由通讯主机产生,决定了通讯 的速率, 不同的设备支持的最高时钟频率不一样,如 STM32 的 SPI 时钟频率最大为 fpclk/2,两个设备之间 通讯时,通 讯速率受限于低速设备。;
CS/SS(Chip Select/Slave Select):
从设备选择信号线,常称为片选信号线,也称为 NSS、CS,以下用 NSS 表 示。当有多个 SPI 从设备与 SPI 主机相连时,设备的其它信号线 SCK、MOSI 及 MISO 同时并联 到相同的 SPI 总线上,即无论有多少个从设备,都共同只使用这 3 条总线;而每个从设备都有独 立的这一条 NSS 信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选 信号线。从设备使能信号,由主设备控制,一主多从时,CS/SS是从芯片是否被主芯片选中的控制信号,只有片选信号为预先规定的使能信号时(高电位或低电位),主芯片对此从芯片的操作才有效。I2C 协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而 SPI 协议 中没有设备地址,它使用 NSS 信号线来寻址,当主机要选择从设备时,把该从设备的 NSS 信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行 SPI 通 讯。所以 SPI 通讯以 NSS 线置低电平为开始信号,以 NSS 线被拉高作为结束信号。
SPI时序图
这是一个主机的通讯时序。NSS、SCK、MOSI 信号都由主机控制产生,而 MISO 的信号由从机 产生,主机通过该信号线读取从机的数据。MOSI 与 MISO 的信号只在 NSS 为低电平的时候才有效,在 SCK 的每个时钟周期 MOSI 和 MISO 传输一位数据。
连接方式
一主设备一从设备模式
一主设备多从设备模式
通信原理
与 I2C 的类似,SPI 协议定义了通讯的起始和停止信号、数据有效性、时钟同步等环节。
NSS 信号线由高变低,是 SPI 通讯的起始信号。
NSS 是每个从机 各自独占的信号线,当从机在自己的 NSS 线检测到起始信号后,就知道自己被主机选中了,开 始准备与主机通讯。在图中的标号处,
NSS 信号由低变高,是 SPI 通讯的停止信号,表示本次通 讯结束,从机的选中状态被取消。
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 位为单位,每次传输的单位数不受限制。
SPI主设备和从设备都有一个串行移位寄存器,主设备通过向它的SPI串行寄存器写入一个字节来发起一次传输。
通信步骤
SPI数据通信的流程可以分为以下几步:
1、主设备发起信号,将CS/SS拉低,启动通信。
2、主设备通过发送时钟信号,来告诉从设备进行写数据或者读数据操作(采集时机可能是时钟信号的上升沿(从低到高)或下降沿(从高到低),因为SPI有四种模式,后面会讲到),它将立即读取数据线上的信号,这样就得到了一位数据(1bit)。
3、主机(Master)将要发送的数据写到发送数据缓存区(Menory),缓存区经过移位寄存器(缓存长度不一定,看单片机配置),串行移位寄存器通过MOSI信号线将字节一位一位的移出去传送给从机,同时MISO接口接收到的数据经过移位寄存器一位一位的移到接收缓存区。
4、从机(Slave)也将自己的串行移位寄存器(缓存长度不一定,看单片机配置)中的内容通过MISO信号线返回给主机。同时通过MOSI信号线接收主机发送的数据,这样,两个移位寄存器中的内容就被交换。
例如,下图示例中简单模拟SPI通信流程,主机拉低NSS片选信号,启动通信,并且产生时钟信号,上升沿触发边沿信号,主机在MOSI线路一位一位发送数据0X53,在MISO线路一位一位接收数据0X46,如下图所示:
这里有一点需要着重说明一下:
SPI只有主模式和从模式之分,没有读和写的说法,外设的写操作和读操作是同步完成的。若只进行写操作,主机只需忽略接收到的字节(虚拟数据);反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。也就是说,你发一个数据必然会收到一个数据;你要收一个数据必须也要先发一个数据。
通信特性
设备选择
SPI是单主设备(Single Master)通信协议,只有一支主设备能发起通信,当SPI主设备想读/写从设备时,它首先拉低从设备对应的SS线(SS是低电平有效)。接着开始发送工作脉冲到时钟线上,在相应的脉冲时间上,主设备把信号发到MOSI实现“写”,同时可对MISO采样而实现“读”。如下图所示:
设备时钟
SPI时钟特点主要包括:时钟速率、时钟极性和时钟相位三方面。
时钟速率
SPI总线上的主设备必须在通信开始时候配置并生成相应的时钟信号。从理论上讲,只要实际可行,时钟速率就可以是你想要的任何速率,当然这个速率受限于每个系统能提供多大的系统时钟频率,以及最大的SPI传输速率。
时钟极性
根据硬件制造商的命名规则不同,时钟极性通常写为CKP或CPOL。时钟极性和相位共同决定读取数据的方式,比如信号上升沿读取数据还是信号下降沿读取数据。
CKP可以配置为1或0。这意味着你可以根据需要将时钟的默认状态(IDLE)设置为高或低。极性反转可以通过简单的逻辑逆变器实现。你必须参考设备的数据手册才能正确设置CKP和CKE。
CKP = 0:时钟空闲IDLE为低电平 0;
CKP = 1:时钟空闲IDLE为高电平1。
时钟相位
根据硬件制造商的不同,时钟相位通常写为CKE或CPHA。顾名思义,时钟相位/边沿,也就是采集数据时是在时钟信号的具体相位或者边沿;
CKE = 0:在时钟信号SCK的第一个跳变沿采样;
CKE = 1:在时钟信号SCK的第二个跳变沿采样。
四种模式
时钟极性 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”。
根据SPI的时钟极性和时钟相位特性可以设置4种不同的SPI通信操作模式,它们的区别是定义了在时钟脉冲的哪条边沿转换(toggles)输出信号,哪条边沿采样输入信号,还有时钟脉冲的稳定电平值(就是时钟信号无效时是高还是低)。
四种模式详解
Mode0:CKP=0,CKE =0:当空闲态时,SCK处于低电平,数据采样是在第1个边沿,也就是SCK由低电平到高电平的跳变,所以数据采样是在上升沿(准备数据),(发送数据)数据发送是在下降沿。
Mode1:CKP=0,CKE=1:当空闲态时,SCK处于低电平,数据发送是在第2个边沿,也就是SCK由低电平到高电平的跳变,所以数据采样是在下降沿,数据发送是在上升沿。
Mode2:CKP=1,CKE=0:当空闲态时,SCK处于高电平,数据采集是在第1个边沿,也就是SCK由高电平到低电平的跳变,所以数据采集是在下降沿,数据发送是在上升沿。
Mode3:CKP=1,CKE=1:当空闲态时,SCK处于高电平,数据发送是在第2个边沿,也就是SCK由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿。
黑线为采样数据的时刻,蓝线为SCK时钟信号
举个例子,下图是SPI Mode0读/写时序,可以看出SCK空闲状态为低电平,主机数据在第一个跳变沿被从机采样,数据输出同理。
下图是SPI Mode3读/写时序,SCK空闲状态为高电平,主机数据在第二个跳变沿被从机采样,数据输出同理。
SPI优缺点
优点
- 无起始位和停止位,因此数据位可以连续传输而不会被中断;
- 没有像I2C这样复杂的从设备寻址系统;
- 数据传输速率比I2C更高(几乎快两倍);
- 分离的MISO和MOSI信号线,因此可以同时发送和接收数据;
- 极其灵活的数据传输,不限于8位,它可以是任意大小的字;
- 非常简单的硬件结构。从站不需要唯一地址(与I2C不同)。从机使用主机时钟,不需要精密时钟振荡器/晶振(与UART不同)。不需要收发器(与CAN不同)。
缺点
- 使用四根信号线(I2C和UART使用两根信号线);
- 无法确认是否已成功接收数据(I2C拥有此功能);
- 没有任何形式的错误检查,如UART中的奇偶校验位;
- 只允许一个主设备;
- 没有硬件从机应答信号(主机可能在不知情的情况下无处发送);
- 没有定义硬件级别的错误检查协议;
- 与RS-232和CAN总线相比,只能支持非常短的距离
STM32 的 SPI 外设简介
STM32 的 SPI 外设可用作通讯的主机及从机,支持最高的 SCK 时钟频率为 fpclk/2 (STM32F407 型 号的芯片默认 fpclk142MHz,fpclk2 为 84MHz),完全支持 SPI 协议的 4 种模式,数据帧长度可设置 为 8 位或 16 位,可设置数据 MSB 先行或 LSB 先行。
它还支持双线全双工 (前面小节说明的都是 这种模式)、双线单向以及单线模式。其中双线单向模式可以同时使用 MOSI 及 MISO 数据线向 一个方向传输数据,可以加快一倍的传输速度。而单线模式则可以减少硬件接线,当然这样速率 会受到影响。我们只讲解双线全双工模式。
STM32 的 SPI 外设还支持 I2S 功能,I2S 功能是一种音频串行通讯协议
STM32 的 SPI 架构剖析
通讯引脚
SPI 的所有硬件架构都从图 SPI 架构图 中左侧 MOSI、MISO、SCK 及 NSS 线展开的。STM32 芯 片有多个 SPI 外设,它们的 SPI 通讯信号引出到不同的 GPIO 引脚上,使用时必须配置到这些指 定的引脚,见表 STM32F4xx 的 SPI 引脚。关于 GPIO 引脚的复用功能,可查阅《STM32F4xx 规格 书》,以它为准。
STM32F4系列的SPI资源
时钟控制逻辑
SCK 线的时钟信号,由波特率发生器根据“控制寄存器 CR1”中的 BR[0:2] 位控制,该位是对 fpclk 时钟的分频因子,对 fpclk 的分频结果就是 SCK 引脚的输出时钟频率,计算方法见表 BR 位对 fpclk 的分频 。
数据控制逻辑
SPI 的 MOSI 及 MISO 都连接到数据移位寄存器上,数据移位寄存器的内容来源于接收缓冲区及 发送缓冲区以及 MISO、MOSI 线。当向外发送数据的时候,数据移位寄存器以“发送缓冲区”为 数据源,把数据一位一位地通过数据线发送出去;当从外部接收数据的时候,数据移位寄存器把 数据线采样到的数据一位一位地存储到“接收缓冲区”中。通过写 SPI 的“数据寄存器 DR”把 数据填充到发送缓冲区中,通过“数据寄存器 DR”,可以获取接收缓冲区中的内容。其中数据帧 长度可以通过“控制寄存器 CR1”的“DFF 位”配置成 8 位及 16 位模式;配置“LSBFIRST 位” 可选择 MSB 先行还是 LSB 先行。
整体控制逻辑
整体控制逻辑负责协调整个 SPI 外设,控制逻辑的工作模式根据我们配置的“控制寄存器 (CR1/CR2)”的参数而改变,基本的控制参数包括前面提到的 SPI 模式、波特率、LSB 先行、主 从模式、单双向模式等等。
在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器 (SR)”,我们只要读取状态寄存器相关的寄存器位,就可以了解 SPI 的工作状态了。除此之外,控 制逻辑还根据要求,负责控制产生 SPI 中断信号、DMA 请求及控制 NSS 信号线。
实际应用中,我们一般不使用 STM32 SPI 外设的标准 NSS 信号线,而是更简单地使用普通的 GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。
通讯过程
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 外设。初始 化结构体及函数定义在库文件“stm32f4xx_spi.h”及“stm32f4xx_spi.c”中,编程时我们可以结合 这两个文件内的注释使用或参考库帮助文档。了解初始化结构体后我们就能对 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 外设。
FLASH
FLSAH 存储器又称闪存,它与 EEPROM 都是掉电后数据不丢失的存储器,但 FLASH 存储器容量 普遍大于 EEPROM,现在基本取代了它的地位。我们生活中常用的 U 盘、SD 卡、SSD 固态硬盘 以及我们 STM32 芯片内部用于存储程序的设备,都是 FLASH 类型的存储器。在存储控制上,最 主要的区别是 FLASH 芯片只能一大片一大片地擦写,而在“I2C 章节”中我们了解到 EEPROM 可以单个字节擦写。
硬件设计
本实验板中的 FLASH 芯片 (型号:W25Q128) 是一种使用 SPI 通讯协议的 NORFLASH 存储器,它 的 CS/CLK/DIO/DO 引脚分别连接到了 STM32 对应的 SDI 引脚 NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚是一个普通的 GPIO,不是 SPI 的专用 NSS 引脚,所以程序中我们要使用软 件控制的方式。 FLASH 芯片中还有 WP 和 HOLD 引脚。WP 引脚可控制写保护功能,当该引脚为低电平时,禁止 写入数据。我们直接接电源,不使用写保护功能。HOLD 引脚可用于暂停通讯,该引脚为低电平 时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不 使用通讯暂停功能。 关于 FLASH 芯片的更多信息,可参考其数据手册《W25Q128》来了解。若您使用的实验板 FLASH 的型号或控制引脚不一样,只需根据我们的工程修改即可,程序的控制原理相同。
软件设计
为了使工程更加有条理,我们把读写 FLASH 相关的代码独立分开存储,方便以后移植。在“工 程模板”之上新建“bsp_spi_flash.c”及“bsp_spi_ flash.h”文件,这些文件也可根据您的喜好命名, 它们不属于 STM32 标准库的内容,是由我们自己根据应用需要编写的。
编程要点
(1) 初始化通讯使用的目标引脚及端口时钟;
(2) 使能 SPI 外设的时钟;
(3) 配置 SPI 外设的模式、地址、速率等参数并使能 SPI 外设;
(4) 编写基本 SPI 按字节收发的函数;
(5) 编写对 FLASH 擦除及读写操作的的函数;
(6) 编写测试程序,对读写数据进行校验。
SPI 硬件相关宏定义
#define sFLASH_ID 0XEF4018 //W25Q128
//#define SPI_FLASH_PageSize 4096
#define SPI_FLASH_PageSize 256
#define SPI_FLASH_PerWritePageSize 256
/*SPI接口定义-开头****************************/
#define FLASH_SPI SPI1
#define FLASH_SPI_CLK RCC_APB2Periph_SPI1
#define FLASH_SPI_CLK_INIT RCC_APB2PeriphClockCmd
#define FLASH_SPI_SCK_PIN GPIO_Pin_3
#define FLASH_SPI_SCK_GPIO_PORT GPIOB
#define FLASH_SPI_SCK_GPIO_CLK RCC_AHB1Periph_GPIOB
#define FLASH_SPI_SCK_PINSOURCE GPIO_PinSource3
#define FLASH_SPI_SCK_AF GPIO_AF_SPI1
#define FLASH_SPI_MISO_PIN GPIO_Pin_4
#define FLASH_SPI_MISO_GPIO_PORT GPIOB
#define FLASH_SPI_MISO_GPIO_CLK RCC_AHB1Periph_GPIOB
#define FLASH_SPI_MISO_PINSOURCE GPIO_PinSource4
#define FLASH_SPI_MISO_AF GPIO_AF_SPI1
#define FLASH_SPI_MOSI_PIN GPIO_Pin_5
#define FLASH_SPI_MOSI_GPIO_PORT GPIOB
#define FLASH_SPI_MOSI_GPIO_CLK RCC_AHB1Periph_GPIOB
#define FLASH_SPI_MOSI_PINSOURCE GPIO_PinSource5
#define FLASH_SPI_MOSI_AF GPIO_AF_SPI1
#define FLASH_CS_PIN GPIO_Pin_6
#define FLASH_CS_GPIO_PORT GPIOG
#define FLASH_CS_GPIO_CLK RCC_AHB1Periph_GPIOG
#define SPI_FLASH_CS_LOW() {FLASH_CS_GPIO_PORT->BSRRH=FLASH_CS_PIN;}
#define SPI_FLASH_CS_HIGH() {FLASH_CS_GPIO_PORT->BSRRL=FLASH_CS_PIN;}
/*SPI接口定义-结尾****************************/
/*等待超时时间*/
#define SPIT_FLAG_TIMEOUT ((uint32_t)0x1000)
#define SPIT_LONG_TIMEOUT ((uint32_t)(10 * SPIT_FLAG_TIMEOUT))
/*信息输出*/
#define FLASH_DEBUG_ON 1
#define FLASH_INFO(fmt,arg...) printf("<<-FLASH-INFO->> "fmt"\n",##arg)
#define FLASH_ERROR(fmt,arg...) printf("<<-FLASH-ERROR->> "fmt"\n",##arg)
#define FLASH_DEBUG(fmt,arg...) do{\
if(FLASH_DEBUG_ON)\
printf("<<-FLASH-DEBUG->> [%d]"fmt"\n",__LINE__, ##arg);\
}while(0)
以上代码根据硬件连接,把与 FLASH 通讯使用的 SPI 号、引脚号、引脚源以及复用功能映射都 以宏封装起来,并且定义了控制 CS(NSS) 引脚输出电平的宏,以便配置产生起始和停止信号时 使用。
SPI和GPIO结构体初始化
向 GPIO 初始化结构体赋值,把 SCK/MOSI/MISO 引脚初始化成复用推挽模式。而 CS(NSS) 引 脚由于使用软件控制,我们把它配置为普通的推挽输出模式。
void SPI_FLASH_Init(void)
{
SPI_InitTypeDef SPI_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能 FLASH_SPI 及GPIO 时钟 */
/*!< SPI_FLASH_SPI_CS_GPIO, SPI_FLASH_SPI_MOSI_GPIO,
SPI_FLASH_SPI_MISO_GPIO,SPI_FLASH_SPI_SCK_GPIO 时钟使能 */
RCC_AHB1PeriphClockCmd (FLASH_SPI_SCK_GPIO_CLK | FLASH_SPI_MISO_GPIO_CLK|FLASH_SPI_MOSI_GPIO_CLK|FLASH_CS_GPIO_CLK, ENABLE);
/*!< SPI_FLASH_SPI 时钟使能 */
FLASH_SPI_CLK_INIT(FLASH_SPI_CLK, ENABLE);
//设置引脚复用
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);
/*!< 配置 SPI_FLASH_SPI 引脚: SCK */
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(FLASH_SPI_SCK_GPIO_PORT, &GPIO_InitStructure);
/*!< 配置 SPI_FLASH_SPI 引脚: MISO */
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN;
GPIO_Init(FLASH_SPI_MISO_GPIO_PORT, &GPIO_InitStructure);
/*!< 配置 SPI_FLASH_SPI 引脚: MOSI */
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;
GPIO_Init(FLASH_SPI_MOSI_GPIO_PORT, &GPIO_InitStructure);
/*!< 配置 SPI_FLASH_SPI 引脚: CS */
GPIO_InitStructure.GPIO_Pin = FLASH_CS_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_Init(FLASH_CS_GPIO_PORT, &GPIO_InitStructure);
/* 停止信号 FLASH: CS引脚高电平*/
SPI_FLASH_CS_HIGH();
/* FLASH_SPI 模式配置 */
// FLASH芯片 支持SPI模式0及模式3,据此设置CPOL CPHA
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
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;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(FLASH_SPI, &SPI_InitStructure);
/* 使能 FLASH_SPI */
SPI_Cmd(FLASH_SPI, ENABLE);
}
对 SPI 外设模式的配置。在配置 STM32 的 SPI 模式前,我们要 先了解从机端的 SPI 模式。本例子中可通过查阅 FLASH 数据手册《W25Q128》获取。
根据 FLASH 芯片的说明,它支持 SPI 模式 0 及模式 3,支持双线全双工,使用 MSB 先行模式,支持最高通讯 时钟为 104MHz,数据帧长度为 8 位。我们要把 STM32 的 SPI 外设中的这些参数配置一致。
这段代码中,把 STM32 的 SPI 外设配置为主机端,双线全双工模式,数据帧长度为 8 位,使用 SPI 模式 3(CPOL=1,CPHA=1),NSS 引脚由软件控制以及 MSB 先行模式。最后一个成员为 CRC 计算式,由于我们与 FLASH 芯片通讯不需要 CRC 校验,并没有使能 SPI 的 CRC 功能,这时 CRC 计算式的成员值是无效的。 赋值结束后调用库函数 SPI_Init 把这些配置写入寄存器,并调用 SPI_Cmd 函数使能外设。
使用 SPI 发送和接收一个字节的数据
初始化好 SPI 外设后,就可以使用 SPI 通讯了,复杂的数据通讯都是由单个字节数据收发组成的, 我们看看它的代码实现
/**
* @brief 使用SPI发送一个字节的数据
* @param byte:要发送的数据
* @retval 返回接收到的数据
*/
u8 SPI_FLASH_SendByte(u8 byte)
{
SPITimeout = SPIT_FLAG_TIMEOUT;
/* 等待发送缓冲区为空,TXE事件 */
while (SPI_I2S_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_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_RXNE) == RESET)
{
if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);
}
/* 读取数据寄存器,获取接收缓冲区数据 */
return SPI_I2S_ReceiveData(FLASH_SPI);
}
/**
* @brief 使用SPI读取一个字节的数据
* @param 无
* @retval 返回接收到的数据
*/
u8 SPI_FLASH_ReadByte(void)
{
return (SPI_FLASH_SendByte(Dummy_Byte));
}
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》,可了解各种它定义的各种指令的功能及指令格式,见表 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 指令“JEDEC ID”可以获取这两个编号,该指令编码为“9F h”,其中“9F h”是指 16 进制数“9F”(相当于 C 语言中的 0x9F)。紧跟指令编码的三个字节分别为 FLASH 芯 片输出的“(M7-M0)”、“(ID15-ID8)”及“(ID7-ID0)”。 此处我们以该指令为例,配合其指令时序图进行讲解,见图 FLASH 读 ID 指令 JEDEC_ID 的时序。
主机首先通过 MOSI 线向 FLASH 芯片发送第一个字节数据为“9F h”,当 FLASH 芯片收到该数据 后,它会解读成主机向它发送了“JEDEC 指令”,然后它就作出该命令的响应:通过 MISO 线把 它的厂商 ID(M7-M0) 及芯片类型 (ID15-0) 发送给主机,主机接收到指令响应后可进行校验。
常见的应用是主机端通过读取设备 ID 来测试硬件是否连接正常,或用于识别设备。 对于 FLASH 芯片的其它指令,都是类似的,只是有的指令包含多个字节,或者响应包含更多的数据。
实际上,编写设备驱动都是有一定的规律可循的。首先我们要确定设备使用的是什么通讯协议。 如上一章的 EEPROM 使用的是 I 2C,本章的 FLASH 使用的是 SPI。那么我们就先根据它的通讯 协议,选择好 STM32 的硬件模块,并进行相应的 I 2C 或 SPI 模块初始化。
接着,我们要了解目标设备的相关指令,因为不同的设备,都会有相应的不同的指令。如 EEPROM 中会把第一个数据 解释为内部存储矩阵的地址 (实质就是指令)。而 FLASH 则定义了更多的指令,有写指令,读指令,读 ID 指令等等。最后,我们根据这些指令的格式要求,使用通讯协议向设备发送指令,达到控制设备的目标。
定义 FLASH 指令编码表
为了方便使用,我们把 FLASH 芯片的常用指令编码使用宏来封装起来,后面需要发送指令编码 的时候我们直接使用这些宏即可
/* Private define ------------------------------------------------------------*/
/*命令定义-开头*******************************/
#define W25X_WriteEnable 0x06
#define W25X_WriteDisable 0x04
#define W25X_ReadStatusReg 0x05
#define W25X_WriteStatusReg 0x01
#define W25X_ReadData 0x03
#define W25X_FastReadData 0x0B
#define W25X_FastReadDual 0x3B
#define W25X_PageProgram 0x02
#define W25X_BlockErase 0xD8
#define W25X_SectorErase 0x20
#define W25X_ChipErase 0xC7
#define W25X_PowerDown 0xB9
#define W25X_ReleasePowerDown 0xAB
#define W25X_DeviceID 0xAB
#define W25X_ManufactDeviceID 0x90
#define W25X_JedecDeviceID 0x9F
#define WIP_Flag 0x01 /* Write In Progress (WIP) flag */
#define Dummy_Byte 0xFF
/*命令定义-结尾*******************************/
读取 FLASH 芯片 ID
根据“JEDEC”指令的时序,我们把读取 FLASH ID 的过程编写成一个函数。
/**
* @brief 读取FLASH ID
* @param 无
* @retval FLASH ID
*/
u32 SPI_FLASH_ReadID(void)
{
u32 Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0;
/* 开始通讯:CS低电平 */
SPI_FLASH_CS_LOW();
/* 发送JEDEC指令,读取ID */
SPI_FLASH_SendByte(W25X_JedecDeviceID);
/* 读取一个字节数据 */
Temp0 = SPI_FLASH_SendByte(Dummy_Byte);
/* 读取一个字节数据 */
Temp1 = SPI_FLASH_SendByte(Dummy_Byte);
/* 读取一个字节数据 */
Temp2 = SPI_FLASH_SendByte(Dummy_Byte);
/* 停止通讯:CS高电平 */
SPI_FLASH_CS_HIGH();
/*把数据组合起来,作为函数的返回值*/
Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2;
return Temp;
}
这段代码利用控制 CS 引脚电平的宏“SPI_FLASH_CS_LOW/HIGH”以及前面编写的单字节收发 函数 SPI_FLASH_SendByte,很清晰地实现了“JEDEC ID”指令的时序:发送一个字节的指令编 码“W25X_JedecDeviceID”,然后读取 3 个字节,获取 FLASH 芯片对该指令的响应,最后把读 取到的这 3 个数据合并到一个变量 Temp 中,然后作为函数返回值,把该返回值与我们定义的宏 “sFLASH_ID” 对比,即可知道 FLASH 芯片是否正常。
FLASH 写使能以及读取当前状态
在向 FLASH 芯片存储矩阵写入数据前,首先要使能写操作,通过“Write Enable”命令即可写使能。
/**
* @brief 向FLASH发送 写使能 命令
* @param none
* @retval none
*/
void SPI_FLASH_WriteEnable(void)
{
/* 通讯开始:CS低 */
SPI_FLASH_CS_LOW();
/* 发送写使能命令*/
SPI_FLASH_SendByte(W25X_WriteEnable);
/*通讯结束:CS高 */
SPI_FLASH_CS_HIGH();
}
线通讯结束的一瞬间完成的,所以在写操作后需要确认 FLASH 芯片“空闲”时才能进行再次写 入。为了表示自己的工作状态,FLASH 芯片定义了一个状态寄存器,见图 FLASH 芯片的状态寄 存器。
我们只关注这个状态寄存器的第 0 位“BUSY”,当这个位为“1”时,表明 FLASH 芯片处于忙碌 状态,它可能正在对内部的存储矩阵进行“擦除”或“数据写入”的操作。 利用指令表中的“Read Status Register”指令可以获取 FLASH 芯片状态寄存器的内容,其时序见 图读取状态寄存器的时序。
只要向 FLASH 芯片发送了读状态寄存器的指令,FLASH 芯片就会持续向主机返回最新的状态寄 存器内容,直到收到 SPI 通讯的停止信号。
据此我们编写了具有等待 FLASH 芯片写入结束功能 的函数,
/**
* @brief 等待WIP(BUSY)标志被置0,即等待到FLASH内部数据写入完毕
* @param none
* @retval none
*/
void SPI_FLASH_WaitForWriteEnd(void)
{
u8 FLASH_Status = 0;
/* 选择 FLASH: CS 低 */
SPI_FLASH_CS_LOW();
/* 发送 读状态寄存器 命令 */
SPI_FLASH_SendByte(W25X_ReadStatusReg);
SPITimeout = SPIT_FLAG_TIMEOUT;
/* 若FLASH忙碌,则等待 */
do
{
/* 读取FLASH芯片的状态寄存器 */
FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);
{
if((SPITimeout--) == 0)
{
SPI_TIMEOUT_UserCallback(4);
return;
}
}
}
while ((FLASH_Status & WIP_Flag) == SET); /* 正在写入标志 */
/* 停止信号 FLASH: CS 高 */
SPI_FLASH_CS_HIGH();
}
这段代码发送读状态寄存器的指令编码“W25X_ReadStatusReg”后,在 while 循环里持续获取寄 存器的内容并检验它的“WIP_Flag 标志”(即 BUSY 位),一直等待到该标志表示写入结束时才退 出本函数,以便继续后面与 FLASH 芯片的数据通讯。
FLASH 扇区擦除
由于 FLASH 存储器的特性决定了它只能把原来为“1”的数据位改写成“0”,而原来为“0”的 数据位不能直接改写为“1”。所以这里涉及到数据“擦除”的概念,在写入前,必须要对目标存 储矩阵进行擦除操作,把矩阵中的数据位擦除为“1”,在数据写入的时候,如果要存储数据“1”, 那就不修改存储矩阵,在要存储数据“0”时,才更改该位。 通常,对存储矩阵擦除的基本操作单位都是多个字节进行,如本例子中的 FLASH 芯片支持“扇 区擦除”、“块擦除”以及“整片擦除”,见表本实验 FLASH 芯片的擦除单位。
FLASH 芯片的最小擦除单位为扇区 (Sector),而一个块 (Block) 包含 16 个扇区,其内部存储矩阵 分布见图 FLASH 芯片的存储矩阵。
扇区擦除指令的第一个字节为指令编码,紧接着发送的 3 个字节用于表示要擦除的存储矩阵地 址。要注意的是在扇区擦除指令前,还需要先发送“写使能”指令,发送扇区擦除指令后,通过 读取寄存器状态等待扇区擦除操作完毕。
/**
* @brief 擦除FLASH扇区
* @param SectorAddr:要擦除的扇区地址
* @retval 无
*/
void SPI_FLASH_SectorErase(u32 SectorAddr)
{
/* 发送FLASH写使能命令 */
SPI_FLASH_WriteEnable();
SPI_FLASH_WaitForWriteEnd();
/* 擦除扇区 */
/* 选择FLASH: CS低电平 */
SPI_FLASH_CS_LOW();
/* 发送扇区擦除指令*/
SPI_FLASH_SendByte(W25X_SectorErase);
/*发送擦除扇区地址的高位*/
SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);
/* 发送擦除扇区地址的中位 */
SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);
/* 发送擦除扇区地址的低位 */
SPI_FLASH_SendByte(SectorAddr & 0xFF);
/* 停止信号 FLASH: CS 高电平 */
SPI_FLASH_CS_HIGH();
/* 等待擦除完毕*/
SPI_FLASH_WaitForWriteEnd();
}
这段代码调用的函数在前面都已讲解,只要注意发送擦除地址时高位在前即可。调用扇区擦除指 令时注意输入的地址要对齐到 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 文件系统”相关的例子。
把页写入时序封装成函数
/**
* @brief 对FLASH按页写入数据,调用本函数写入数据前需要先擦除扇区
* @param pBuffer,要写入数据的指针
* @param WriteAddr,写入地址
* @param NumByteToWrite,写入数据长度,必须小于等于SPI_FLASH_PerWritePageSize
* @retval 无
*/
void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
/* 发送FLASH写使能命令 */
SPI_FLASH_WriteEnable();
/* 选择FLASH: CS低电平 */
SPI_FLASH_CS_LOW();
/* 写页写指令*/
SPI_FLASH_SendByte(W25X_PageProgram);
/*发送写地址的高位*/
SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
/*发送写地址的中位*/
SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
/*发送写地址的低位*/
SPI_FLASH_SendByte(WriteAddr & 0xFF);
if(NumByteToWrite > SPI_FLASH_PerWritePageSize)
{
NumByteToWrite = SPI_FLASH_PerWritePageSize;
FLASH_ERROR("SPI_FLASH_PageWrite too large!");
}
/* 写入数据*/
while (NumByteToWrite--)
{
/* 发送当前要写入的字节数据 */
SPI_FLASH_SendByte(*pBuffer);
/* 指向下一字节数据 */
pBuffer++;
}
/* 停止信号 FLASH: CS 高电平 */
SPI_FLASH_CS_HIGH();
/* 等待写入完毕*/
SPI_FLASH_WaitForWriteEnd();
}
这段代码的内容为:先发送“写使能”命令,接着才开始页写入时序,然后发送指令编码、地址, 再把要写入的数据一个接一个地发送出去,发送完后结束通讯,检查 FLASH 状态寄存器,等待 FLASH 内部写入结束。
不定量数据写入
应用的时候我们常常要写入不定量的数据,直接调用“页写入”函数并不是特别方便,所以我们 在它的基础上编写了“不定量数据写入”的函数,
/**
* @brief 对FLASH写入数据,调用本函数写入数据前需要先擦除扇区
* @param pBuffer,要写入数据的指针
* @param WriteAddr,写入地址
* @param NumByteToWrite,写入数据长度
* @retval 无
*/
void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
/*mod运算求余,若writeAddr是SPI_FLASH_PageSize整数倍,运算结果Addr值为0*/
Addr = WriteAddr % SPI_FLASH_PageSize;
/*差count个数据值,刚好可以对齐到页地址*/
count = SPI_FLASH_PageSize - Addr;
/*计算出要写多少整数页*/
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
/*mod运算求余,计算出剩余不满一页的字节数*/
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
/* Addr=0,则WriteAddr 刚好按页对齐 aligned */
if (Addr == 0)
{
/* NumByteToWrite < SPI_FLASH_PageSize */
if (NumOfPage == 0)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
}
else /* NumByteToWrite > SPI_FLASH_PageSize */
{
/*先把整数页都写了*/
while (NumOfPage--)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
WriteAddr += SPI_FLASH_PageSize;
pBuffer += SPI_FLASH_PageSize;
}
/*若有多余的不满一页的数据,把它写完*/
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
}
/* 若地址与 SPI_FLASH_PageSize 不对齐 */
else
{
/* NumByteToWrite < SPI_FLASH_PageSize */
if (NumOfPage == 0)
{
/*当前页剩余的count个位置比NumOfSingle小,写不完*/
if (NumOfSingle > count)
{
temp = NumOfSingle - count;
/*先写满当前页*/
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
WriteAddr += count;
pBuffer += count;
/*再写剩余的数据*/
SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);
}
else /*当前页剩余的count个位置能写完NumOfSingle个数据*/
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
}
}
else /* NumByteToWrite > SPI_FLASH_PageSize */
{
/*地址不对齐多出的count分开处理,不加入这个运算*/
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
WriteAddr += count;
pBuffer += count;
/*把整数页都写了*/
while (NumOfPage--)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
WriteAddr += SPI_FLASH_PageSize;
pBuffer += SPI_FLASH_PageSize;
}
/*若有多余的不满一页的数据,把它写完*/
if (NumOfSingle != 0)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
}
}
}
这段代码与 EEPROM 章节中的“快速写入多字节”函数原理是一样的,运算过程在此不再赘述。 区别是页的大小以及实际数据写入的时候,使用的是针对 FLASH 芯片的页写入函数,且在实际 调用这个“不定量数据写入”函数时,还要注意确保目标扇区处于擦除状态。
从 FLASH 读取数据
相对于写入,FLASH 芯片的数据读取要简单得多,使用读取指令“Read Data”即可,其指令时 序见图 SPI_FLASH 读取数据时序。
发送了指令编码及要读的起始地址后,FLASH 芯片就会按地址递增的方式返回存储矩阵的内容,读取的数据量没有限制,只要没有停止通讯,FLASH 芯片就会一直返回数据。代码实现见
/**
* @brief 读取FLASH数据
* @param pBuffer,存储读出数据的指针
* @param ReadAddr,读取地址
* @param NumByteToRead,读取数据长度
* @retval 无
*/
void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead)
{
/* 选择FLASH: CS低电平 */
SPI_FLASH_CS_LOW();
/* 发送 读 指令 */
SPI_FLASH_SendByte(W25X_ReadData);
/* 发送 读 地址高位 */
SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);
/* 发送 读 地址中位 */
SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);
/* 发送 读 地址低位 */
SPI_FLASH_SendByte(ReadAddr & 0xFF);
/* 读取数据 */
while (NumByteToRead--)
{
/* 读取一个字节*/
*pBuffer = SPI_FLASH_SendByte(Dummy_Byte);
/* 指向下一个字节缓冲区 */
pBuffer++;
}
/* 停止信号 FLASH: CS 高电平 */
SPI_FLASH_CS_HIGH();
}
由于读取的数据量没有限制,所以发送读命令后一直接收 NumByteToRead 个数据到结束即可。
测试
最后我们来编写 main 函数,进行 FLASH 芯片读写校验,main 函数
#include "stm32f4xx.h"
#include "./led/bsp_led.h"
#include "./usart/bsp_debug_usart.h"
#include "./flash/bsp_spi_flash.h"
typedef enum { FAILED = 0, PASSED = !FAILED} TestStatus;
/* 获取缓冲区的长度 */
#define TxBufferSize1 (countof(TxBuffer1) - 1)
#define RxBufferSize1 (countof(TxBuffer1) - 1)
#define countof(a) (sizeof(a) / sizeof(*(a)))
#define BufferSize (countof(Tx_Buffer)-1)
#define FLASH_WriteAddress 0x00000
#define FLASH_ReadAddress FLASH_WriteAddress
#define FLASH_SectorToErase FLASH_WriteAddress
/* 发送缓冲区初始化 */
uint8_t Tx_Buffer[] = "感谢您选用野火stm32开发板\r\nhttps://fire-stm32.taobao.com";
uint8_t Rx_Buffer[BufferSize];
//读取的ID存储位置
__IO uint32_t DeviceID = 0;
__IO uint32_t FlashID = 0;
__IO TestStatus TransferStatus1 = FAILED;
// 函数原型声明
void Delay(__IO uint32_t nCount);
TestStatus Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2, uint16_t BufferLength);
/*
* 函数名:main
* 描述 :主函数
* 输入 :无
* 输出 :无
*/
int main(void)
{
LED_GPIO_Config();
LED_BLUE;
/* 配置串口1为:115200 8-N-1 */
Debug_USART_Config();
printf("\r\n这是一个16M串行flash(W25Q128)实验 \r\n");
/* 16M串行flash W25Q128初始化 */
SPI_FLASH_Init();
/* 获取 Flash Device ID */
DeviceID = SPI_FLASH_ReadDeviceID();
Delay( 200 );
/* 获取 SPI Flash ID */
FlashID = SPI_FLASH_ReadID();
printf("\r\nFlashID is 0x%X, Manufacturer Device ID is 0x%X\r\n", FlashID, DeviceID);
/* 检验 SPI Flash ID */
if (FlashID == sFLASH_ID)
{
printf("\r\n检测到SPI FLASH W25Q128 !\r\n");
/* 擦除将要写入的 SPI FLASH 扇区,FLASH写入前要先擦除 */
SPI_FLASH_SectorErase(FLASH_SectorToErase);
/* 将发送缓冲区的数据写到flash中 */
SPI_FLASH_BufferWrite(Tx_Buffer, FLASH_WriteAddress, BufferSize);
printf("\r\n写入的数据为:\r\n%s", Tx_Buffer);
/* 将刚刚写入的数据读出来放到接收缓冲区中 */
SPI_FLASH_BufferRead(Rx_Buffer, FLASH_ReadAddress, BufferSize);
printf("\r\n读出的数据为:\r\n%s", Rx_Buffer);
/* 检查写入的数据与读出的数据是否相等 */
TransferStatus1 = Buffercmp(Tx_Buffer, Rx_Buffer, BufferSize);
if( PASSED == TransferStatus1 )
{
LED_GREEN;
printf("\r\n16M串行flash(W25Q128)测试成功!\n\r");
}
else
{
LED_RED;
printf("\r\n16M串行flash(W25Q128)测试失败!\n\r");
}
}// if (FlashID == sFLASH_ID)
else
{
LED_RED;
printf("\r\n获取不到 W25Q128 ID!\n\r");
}
SPI_Flash_PowerDown();
while(1);
}
/*
* 函数名:Buffercmp
* 描述 :比较两个缓冲区中的数据是否相等
* 输入 :-pBuffer1 src缓冲区指针
* -pBuffer2 dst缓冲区指针
* -BufferLength 缓冲区长度
* 输出 :无
* 返回 :-PASSED pBuffer1 等于 pBuffer2
* -FAILED pBuffer1 不同于 pBuffer2
*/
TestStatus Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2, uint16_t BufferLength)
{
while(BufferLength--)
{
if(*pBuffer1 != *pBuffer2)
{
return FAILED;
}
pBuffer1++;
pBuffer2++;
}
return PASSED;
}
void Delay(__IO uint32_t nCount)
{
for(; nCount != 0; nCount--);
}
函数中初始化了 LED、串口、SPI 外设,然后读取 FLASH 芯片的 ID 进行校验,若 ID 校验通过 则向 FLASH 的特定地址写入测试数据,然后再从该地址读取数据,测试读写是否正常。