zoukankan      html  css  js  c++  java
  • STM32-SPI读写串行FLASH

    SPI协议简介

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

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

    SPI物理层

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

    5d307a6ec0fc827076

    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 时钟频率最大为fpclk/2,两个设备之间通讯时,通讯速率受限于低速设备。

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

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

    协议层

    1. SPI基本通讯过程

    先看看 SPI 通讯的通讯时序,见图 24-2。

    5d307a79572a354909

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

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

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

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

    1. 数据有效性

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

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

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

    1. CPOL/CPHA 及通讯模式

    上面讲述的图 24-2 中的时序只是 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 的“偶数边沿” 采样。见图 24-3 及图 24-4。

    5d307a84f15d459674

    5d307a8a52bd694695
    我们来分析这个 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 的偶数边沿被采样,见图 24-4。

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

    5d307a96321ba13913

    STM32的SPI特性及架构

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

    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 播放器的章节中会进行介绍。

    STM32 的SPI架构剖析

    5d307aa03670f93156
    2. 通讯引脚

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

    5d307ac4db26c18216

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

    1. 时钟控制逻辑

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

    5d307af273a6926753

    其中的 fpclk频率是指 SPI 所在的 APB 总线频率, APB1 为 fpclk1, APB2 为 fpckl2。通过配置“控制寄存器 CR”的“CPOL 位”及“CPHA”位可以把 SPI 设置成前面分析的 4 种 SPI 模式。

    1. 数据控制逻辑

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

    1. 整体控制逻辑

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

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

    通讯过程

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

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

    5d307b02eb33343123

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

    (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 外设运用自如了, 见代码清单 24-1。

    代码清单 24-1 SPI 初始化结构体

    typedef struct
    {
        uint16_t SPI_Direction; /*设置 SPI 的单双向模式 */
        uint16_t SPI_Mode; /*设置 SPI 的主/从机端模式 */
        uint16_t SPI_DataSize; /*设置 SPI 的数据帧长度,可选 8/16 位 */
        uint16_t SPI_CPOL; /*设置时钟极性 CPOL,可选高/低电平*/
        uint16_t SPI_CPHA; /*设置时钟相位,可选奇/偶数边沿采样 */
        uint16_t SPI_NSS; /*设置 NSS 引脚由 SPI 硬件控制还是软件控制*/
        uint16_t SPI_BaudRatePrescaler; /*设置时钟分频因子, fpclk/分频数=fSCK */
        uint16_t SPI_FirstBit; /*设置 MSB/LSB 先行 */
        uint16_t SPI_CRCPolynomial; /*设置 CRC 校验的表达式 */
    } 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 外设。

    5d307b66672af98418

    本实验板中的 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 的型号或控制引脚不一样, 只需根据我们的工程修改即可,程序的控制原理相同。

    软件设计

    SPI 的接收过程和发送过程实质是一样的,收发同步进行,关键在于我们的上层应用中,关注的是发送还是接收的数据。

    控制 FLASH 的指令

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

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

    5d307b9806e8538305

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

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

    5d307ba74542688031

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

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

    5d307bb2929f238156

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

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

    5d307bd2342ab49192

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

    5d307bdf82b4d61832

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

    5d307beea204d72269

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

    代码清单 24-10 擦除扇区

    /**
    * @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 页写入的时序见图 24-13。

    5d307c3d59a2354647

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

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

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

    代码清单 24-11 FLASH 的页写入

    /**
    * @brief 对 FLASH 按页写入数据,调用本函数写入数据前需要先擦除扇区
    * @param pBuffer,要写入数据的指针
    * @param WriteAddr,写入地址
    * @param NumByteToWrite,写入数据长度,必须小于等于页大小
    * @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 内部写入结束。

    不定量数据写入

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

    代码清单 24-12 不定量数据写入

    /**
    * @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 值为
        */
        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”即可,其指令时序见图 24-14。

    5d307e254787555495

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

    代码清单 24-13 从 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 个数据到结束即可。

    参考引用:

    1. 野火---《零死角玩转STM32-F429挑战者》
    2. 《STM32F4xx中文参考手册》
    3. 《Cortex-M4内核编程手册》
  • 相关阅读:
    STM32-串口通信
    STM32-系统计时器(systick)
    字符串操作常用的函数
    基本MarkDown语法
    结构
    python入门
    贪心算法小结
    POJ1631_高深DP
    POJ3046ANT_COUNTING
    POJ1742coins
  • 原文地址:https://www.cnblogs.com/luoxiao23/p/11210375.html
Copyright © 2011-2022 走看看