正点原子STM32F1系列学习笔记之SPI通信+NM25Q128(HAL库)

一、SPI介绍
SPI简介

​ SPI 是英语 Serial Peripheral interface 缩写,顾名思义就是串行外围设备接口。SPI 通信协 议是 Motorola 公司首先在其 MC68HCXX 系列处理器上定义的。SPI 接口是一种高速的全双工同步的通信总线,已经广泛应用在众多 MCU、存储芯片、AD 转换器和 LCD 之间。

SPI框图
image-20231010214959688
image-20231013163421637

SPI 的引脚信息:

  • MISO(Master In / Slave Out)主设备数据输入,从设备数据输出。
  • MOSI(Master Out / Slave In)主设备数据输出,从设备数据输入。
  • SCLK(Serial Clock)时钟信号,由主设备产生。
  • CS(Chip Select 或NSS,低电平有效)从设备片选信号,由主设备产生。

SPI 的传输方式:

​ SPI 总线具有三种传输方式:全双工、单工以及半双工传输方式。

  • 全双工通信,就是在任何时刻,主机与从机之间都可以同时进行数据的发送和接收。
  • 单工通信,就是在同一时刻,只有一个传输的方向,发送或者是接收。
  • 半双工通信,就是在同一时刻,只能为一个方向传输数据。

SPI 的工作原理:

​ 在主机和从机都有一个串行移位寄存器,主机通过向它的 SPI 串行寄器写入一个字节来发起一次传输。串行移位寄存器通过 MOSI 信号线将字节传送给从机,从机也将自己的串行移位寄存器中的内容通过 MISO 信号线返回给主机。这样,两个移位寄存器中的内容就被交换。外设的写操作和读操作是同步完成的。如果只是进行写操作,主机只需忽略接收到的字节。反之,若主机要读取从机的一个字节,就必须发送一个空字节引发从机传输。

​ 下图是以主机模式为例的数据发送与接受流程:通过SPI_SR寄存器的TXE判断发送缓冲区是否为空进而判断数据发送是否完成,通过SPI_SR寄存器的RXNE判断接受缓冲区是否不为空进而判断数据接受是否完成

image-20231010220142478

以8bit的MSB方式的全双工数据传输为例:

传输前:

image-20231010220532352

传输中(边沿触发):

image-20231010220601751

传输后:

image-20231010220646546

​ 外设的写操作和读操作是同步完成的。如果只是进行写操作,主机只需忽略接收到的字节。反之,若主机要读取从机的一个字节,就必须发送一个空字节引发从机传输。

​ (IIC是电平协议,而SPI是边沿协议)

SPI外设对应的引脚

​ STM32芯片有多个SPI外设,每个SPI外设输出的信号会到不同的GPIO口。STM32F1有三个SPI(大部分 STM32 是有 3 个 SPI 接口,不同系列的SPI外设用到的引脚是不一样的,具体可以查看芯片的datasheet),如下:

引脚SPI1SPI2SPI3
NSSPA4PB12PA15
CLKPA5PB13PB3
MISOPA6PB14PB4
MOSIPA7PB15PB5

​ 本实验使用的是 SPI2。由于本实验使用的NSS(就是CS)是用软件实现的,不是硬件,所以对应的引脚就没有用(连接任何一个IO都行),SPI2用的PB12,其他IO口也行。

SPI与IIC对比
功能说明SPI总线IIC总线
通信方式同步 串行 全双工同步 串行 半双工
总线接口MOSI、MISO、SCL、CSSDA、SCL
拓扑结构一主多从/一主一从多主从
从机选择片选引脚选择SDA上设备地址片选
通信速率一般50MHz以下100kHz、400kHz、3.4MHz
数据格式8位/16位8位
传输顺序MSB/LSBMSB
SPI工作模式

​ STM32 要与具有 SPI 接口的器件进行通信,就必须遵循 SPI 的通信协议。每一种通信协议都有各自的读写数据时序,当然 SPI 也不例外。SPI 通信协议就具备 4 种工作模式,在讲这 4 种 工作模式前,首先先知道两个单词 CPOL 和 CPHA。

​ CPOL,详称 Clock Polarity,就是时钟极性,当主从机没有数据传输的时候(空闲状态)的SCL 线电平状态,假如空闲状态是高电平,CPOL=1;若空闲状态是低电平,那么 CPOL = 0。

​ CPHA,详称 Clock Phase,就是时钟相位。在这里先科普一下数据传输的常识: 同步通信时,数据的变化和采样都是在时钟边沿上进行的,每一个时钟周期都会有上升沿和下降沿两个边沿,那么数据的变化和采样就分别安排在两个不同的边沿,由于数据在产生和到它稳定是需要一定的时间,那么假如我们在第 1 个边沿信号把数据输出了,从机只能从第 2 个边沿信号去采样这个数据。

​ CPHA 实质指的是数据的采样时刻,CPHA= 0 的情况就表示数据的采样是从第 1 个边沿信号上即奇数边沿,具体是上升沿还是下降沿的问题,是由 CPOL 决定的。这里就存在一个问题: 当开始传输第一个 bit 的时候,第 1 个时钟边沿就采集该数据了,那数据是什么时候输出来的呢?那么就有两种情况:一是 CS 使能的边沿,二是上一帧数据的最后一个时钟沿。

​ CPHA=1 的情况就是表示数据采样是从第 2 个边沿即偶数边沿,它的边沿极性要注意一点, 不是和上面 CPHA=0 一样的边沿情况。前面的是奇数边沿采样数据,从 SCL 空闲状态的直接跳变,空闲状态是高电平,那么它就是下降沿,反之就是上升沿。由于 CPHA=1 是偶数边沿采样,所以需要根据偶数边沿判断,假如第一个边沿即奇数边沿是下降沿,那么偶数边沿的边沿极性就是上升沿。总的来说,就是CPHA决定时钟线在第几个时钟边沿采样数据。

  • 0:SCK的第一(奇数)边沿进行数据位采样,数据在第一个时钟边沿被锁存
  • 1:SCK的第二(偶数)边沿进行数据位采样,数据在第二个时钟边沿被锁存

​ 不理解的,可以看一下下面 4 种 SPI 工作模式的图。

​ 由于 CPOL 和 CPHA 都有两种不同状态,所以 SPI 分成了 4 种模式。我们在开发的时候, 使用比较多的是模式 0 和模式 3。下面请看工作模式表。

SPI工作模式CPOLCPHASCL空闲状态采样边沿采样时刻
000低电平上升沿奇数边沿
101低电平下降沿偶数边沿
210高电平下降沿奇数边沿
311高电平上升沿偶数边沿

下面分别对 SPI 的 4 种工作模式进行分析:

模式0:CPOL=0&&CPHA=0

image-20231010232825558

​ 我们分析一下 CPOL=0&&CPHA=0 的时序,上图就是串行时钟的奇数边沿上升沿采样的情况,首先由于配置了 CPOL=0,可以看到当数据未发送或者发送完毕,SCL 的状态是低电平,再者 CPHA=0 即是奇数边沿采集。所以传输的数据会在奇数边沿上升沿被采集,MOSI 和 MISO 数据的有效信号需要在 SCK 奇数边沿保持稳定且被采样,在非采样时刻,MOSI 和 MISO 的有效信号才发生变化。

模式1:CPOL=0&&CPHA=1

image-20231010233056421

​ 现在分析一下 CPOL=0&CPHA=1 的时序,上图是串行时钟的偶数边沿下降沿采样的情况。由于 CPOL=0,所以 SCL 的空闲状态依然是低电平,CPHA=1 数据就从偶数边沿采样, 至于是上升沿还是下降沿,从上图就可以知道,是下降沿。这里有一个误区,空闲状态是低电平的情况下,不是应该上升沿吗,为什么这里是下降沿?首先我们先明确这里是偶数边沿采样, 那么看图就很清晰,SCL 低电平空闲状态下,上升沿是在奇数边沿上,下降沿是在偶数边沿上。

模式2:CPOL=1&&CPHA=0

image-20231010233400097

​ 上图这种情况和第一种情况相似,只是这里是 CPOL=1,即 SCL 空闲状态为高电平,在 CPHA=0,奇数边沿采样的情况下,数据在奇数边沿下降沿要保持稳定并等待采样。

模式3:CPOL=1&&CPHA=1

image-20231010233509952

​ 上图是 CPOL=1&&CPHA=1 的情形,可以看到未发送数据和发送数据完毕,SCL 的状态是高电平,奇数边沿的边沿极性是上升沿,偶数边沿的边沿极性是下降沿。因为 CPHA=1, 所以数据在偶数边沿上升沿被采样。在奇数边沿的时候 MOSI 和 MISO 会发生变化,在偶数边沿时候是稳定的。

SPI 寄存器
寄存器名称作用
SPI_CR1SPI控制寄存器1用于配置SPI工作参数
SPI_SRSPI状态寄存器用于查询当前SPI传输状态(TXE、RXNE)
SPI_DRSPI数据寄存器用于存放待发送数据或接收数据,有两个缓冲区(TX/RX)
SPI 控制寄存器 1(SPI_CR1)
image-20231010234152607

​ 该寄存器控制着 SPI 很多相关信息,包括主设备模式选择,传输方向,数据格式,时钟极性、时钟相位和使能等。下面讲解一下本实验配置的位:

  • 位 CPHA 置 1,数据采样从第二个时钟边沿开始;
  • 位 CPOL 置 1,在空闲状态时,SCK 保持高电平;
  • 位 MSTR 置 1,配置为主机模式;
  • 位 BR[2:0]置 7,使用 256 分频,速度最低;
  • 位 SPE 置 1,开启 SPI 设备;
  • 位 LSBFIRST 置 0,MSB 先传输;
  • 位 SSI 置 1,禁止软件从设备,即做主机;
  • 位 SSM 置 1,软件片选 NSS 控制;
  • 位 RXONLY 置 0,传输方式采用的是全双工模式;
  • 位 DFF 置 0,使用 8 位数据帧格式。
SPI 状态寄存器(SPI_SR)
image-20231010234253419

​ 该寄存器是查询当前 SPI 的状态的,我们在实验中用到的是 TXE 位和 RXNE 位,即发送 完成和接收完成是否的标记。

  • 当TXE位置1时,表示数据已经全部发送出去
  • 当RXE位置1时,表示已经接收到数据
SPI 数据寄存器(SPI_DR)
image-20231010234358835

​ 该寄存器是 SPI 数据寄存器,是一个双寄存器,包括了发送缓存和接收缓存。当向该寄存器写数据的时候,SPI 就会自动发送,当收到数据的时候,也是存在该寄存器内。

二、NOR FLASH
flash简介

​ FLASH 是常见的用于存储数据的半导体器件,它具有容量大、可重复擦写、按“扇区/块” 擦除、掉电后数据可继续保存的特性。常见的 FLASH 主要有 NOR FLASH 和 NAND FLASH 两种类型(NOR和NAND是两种数字门电路),它们的特性如下表所示。NOR 和 NAND 是两种数字门电路,可以简单地认为 FLASH 内部存储单元使用哪种门作存储单元就是哪类型的 FLASH。U 盘,SSD,eMMC 等为 NAND 型,而 NOR FLASH 则根据设计需要灵活应用于各类 PCB 上,如 BIOS,手机等。

类型特点应用举例
NOR FLASH基于字节读写,读取速度快,独立地址/数据线,无坏块,支持XIP25Qxx、程序ROM
NAND FLASH基于块读写,读取速度稍慢,地址数据线共用,有坏块,不支持XIPEMMC、SSD、U盘等

​ NOR FLASH与NAND FLASH的区别详见https://blog.csdn.net/ffdia/article/details/87437872

​ NOR 与 NAND 在数据写入前都需要有擦除操作,但实际上 NOR FLASH 的一个 bit 可以从 1 变成 0,而要从 0 变 1 就要擦除后再写入,NAND FLASH 这两种情况都需要擦除。擦除操作 的最小单位为“扇区/块”,这意味着有时候即使只写一字节的数据,则这个“扇区/块”上之前 的数据都可能会被擦除。

​ NOR 的地址线和数据线分开,它可以按“字节”读写数据,符合 CPU 的指令译码执行要 求,所以假如 NOR 上存储了代码指令,CPU 给 NOR 一个地址,NOR 就能向 CPU 返回一个数 据让 CPU 执行,中间不需要额外的处理操作,这体现于上表中的支持 XIP 特性(eXecute In Place)。因此可以用 NOR FLASH 直接作为嵌入式 MCU 的程序存储空间。

​ NAND 的数据和地址线共用,只能按“块”来读写数据,假如 NAND 上存储了代码指令, CPU 给 NAND 地址后,它无法直接返回该地址的数据,所以不符合指令译码要求。

​ 若代码存储在 NAND 上,可以把它先加载到 RAM 存储器上,再由 CPU 执行。所以在功能上可以认为 NOR 是一种断电后数据不丢失的 RAM,但它的擦除单位与 RAM 有区别,且读写速度比 RAM 要慢得多。 FLASH 也有对应的缺点,我们在使用过程中需要尽量去规避这些问题:一是 FLASH 的使用寿命,另一个是可能的位反转。

​ 使用寿命体现在:读写上是 FLASH 的擦除次数都是有限的(NOR FLASH 普遍是 10 万次左右),当它的使用接近寿命的时候,可能会出现写操作失败。由于 NAND 通常是整块擦写,块内 有一位失效整个块就会失效,这被称为坏块。使用 NAND FLASH 最好通过算法扫描介质找出 坏块并标记为不可用,因为坏块上的数据是不准确的。

​ 位反转是数据位写入时为 1,但经过一定时间的环境变化后可能实际变为 0 的情况,反之亦然。位反转的原因很多,可能是器件特性也可能与环境、干扰有关,由于位反转的的问题可 能存在,所以 FLASH 存储器需要“探测/错误更正(EDC/ECC)”算法来确保数据的正确性。

​ FLASH 芯片有很多种芯片型号,在我们的 norflash.h 头文件中有定义芯片 ID 的宏定义,对 应的就是不同型号的 NOR FLASH 芯片,比如有:W25Q128、BY25Q128、NM25Q128,它们是来自不同的厂商的同种规格的 NOR FLASH 芯片,内存空间都是 128M 字,即 16M 字节。它们 的很多参数、操作都是一样的,所以我们的实验都是兼容它们的。

NM25Q128 简介

​ NM25Q128,串行闪存器件,属于NOR FLASH中的一种,NM25Q128 是一款大容量 SPI FLASH 产品,其容量为 16M。它将 16M 字节的容量分为 256 个块(Block),每个块大小为 64K 字节,每个块又分为 16 个扇区(Sector),每一个扇区 16 页, 每页 256 个字节,即每个扇区 4K 个字节。NM25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。这样我们需要给 NM25Q128 开辟一个至少 4K 的缓存区,这样对 SRAM 要求比较高,要求芯片必须有 4K 以上 SRAM 才能很好的操作。

image-20231011105926666

​ NM25Q128 的擦写周期多达 10W 次,具有 20 年的数据保存期限,支持电压为 2.7~3.6V, NM25Q128 支持标准的 SPI,还支持双输出/四输出的 SPI,最大 SPI 时钟可以到 104Mhz(双输 出时相当于 208Mhz,四输出时相当于 416Mhz)。

  • SPI数据传输时序:支持模式0(CPOL = 0 , CPHA = 0)和模式3(CPOL = 1, CPHA = 1)
  • 数据格式:数据长度8位大小,先发高位,再发低位
  • 传输速度:支持标准模式104M bit/s

​ 下面我们看一下 NM25Q128 芯片的管脚图,如图所示。

image-20231011105615881

​ 芯片引脚连接如下:CS 即片选信号输入,低电平有效;DO 是 MISO 引脚,在 CLK 管脚 的下降沿输出数据;WP 是写保护管脚,高电平可读可写,低电平仅仅可读;DI 是 MOSI 引脚, 主机发送的数据、地址和命令从 SI 引脚输入到芯片内部,在 CLK 管脚的上升沿捕获捕获数据; CLK 是串行时钟引脚,为输入输出提供时钟脉冲;HOLD 是保持管脚,低电平有效。

​ STM32F103 通过 SPI 总线连接到 NM25Q128 对应的引脚即可启动数据传输。

NM25Q128常用指令

​ NOR FLASH的指令总数比较多, 但是如果只需要实现基本操作, 还是比较简单的。一般我们只需要:5条指令即可完成对NOR FLASH的基本使用(以NM25Q128为例),还有很多其他指令,可见NM25Q128.pdf

指令(HEX)名称作用
0X06写使能写入数据/擦除之前,必须先发送该指令
0X05读SR1判定FLASH是否处于空闲状态,擦除用
0X03读数据用于读取NOR FLASH数据
0X02页写用于写入NOR FLASH数据,最多写256字节
0X20扇区擦除扇区擦除指令,最小擦除单位(4096字节)
NOR FLASH 工作时序

读操作时序,如图所示:

image-20231011113617623

​ 从上图可知读数据指令是 03H,可以读出一个字节或者多个字节。发起读操作时,先把 CS 片选管脚拉低,然后通过 MOSI 引脚把 03H 发送芯片,之后再发送要读取的 24 位地址,这些数据在 CLK 上升沿时采样。芯片接收完 24 位地址之后,就会把相对应地址的数据在 CLK 引脚下降沿从 MISO 引脚发送出去。从图中可以看出只要 CLK 一直在工作,那么通过一条读指令就可以把整个芯片存储区的数据读出来。当主机把 CS 引脚拉高,数据传输停止。

​ 接着我们看一下写时序,这里我们先看页写时序,如图所示:

image-20231011113915642

​ 在发送页写指令之前,需要先发送“写使能”指令。然后主机拉低 CS 引脚,然后通过 MOSI 引脚把 02H 发送到芯片,接着发送 24 位地址,最后你就可以发送你需要写的字节数据到芯片。完成数据写入之后,需要拉高 CS 引脚,停止数据传输。

注意:页写命令最多可以向FLASH传输256个字节的数据

​ 下面介绍一下扇区擦除时序,如图所示:

image-20231011145245658

​ 扇区擦除指的是将一个扇区擦除,通过前面的介绍也知道,NM25Q128 的扇区大小是 4K 字节。擦除扇区后,扇区的位全置 1,即扇区字节为 FFh(FLASH存储器的特性决定了它只能把原来为“1”的数据位改写成“0”,而原来为“0”的数据位不能直接改写为“1”)。同样的,在执行扇区擦除之前,需要先执行写使能指令。这里需要注意的是当前 SPI 总线的状态,假如总线状态是 BUSY,那么这个扇区擦除是无效的,所以在拉低 CS 引脚准备发送数据前,需要先要确定 SPI 总线的状态, 这就需要执行读状态寄存器指令,读取状态寄存器的 BUSY 位,需要等待 BUSY 位为 0,才可以执行擦除工作。

​ 写入数据前,检查内存空间情况是否满足,不满足需擦除。如果内存已经是ffh了,那就不需要擦除了。

​ 接着按时序图分析,主机先拉低 CS 引脚,然后通过 MOSI 引脚发送指令代码 20h 到芯片, 然后接着把 24 位扇区地址发送到芯片,然后需要拉高 CS 引脚,通过读取寄存器状态等待扇区擦除操作完成。

​ 此外还有对整个芯片进行擦除的操作,时序比扇区擦除更加简单,不用发送 24bit 地址,只需要发送指令代码 C7h 到芯片即可实现芯片的擦除。 在 NM25Q128 手册中还有许多种方式的读/写/擦除操作,我们这里只分析本实验用到的, 其他大家可以参考 NM25Q128 手册。

​ 补充:

​ 读状态寄存器Read Status Reg1(05H)

image-20231011145539006

​ 05h为SR1,35h为SR2,15h为SR3

​ 写使能 Write Enable (06H):Write Status Register写状态寄存器等指令前,需要写使能

image-20231013170636915
三、实验:驱动NM25Q128实现读和写1字节数据
功能

​ 驱动NM25Q128实现读和写1字节数据

​ 注意,不同于IIC,本实验使用的是硬件spi

原理图
image-20231011150325142

​ 通过上图可知,NM25Q128 的 CS、SCK、MISO 和 MOSI 分别连接在 PB12、PB13、PB14 和 PB15 上。

SPI 传输数据的配置步骤

1)SPI 参数初始化(工作模式、数据时钟极性、时钟相位等)

​ HAL 库通过调用 SPI 初始化函数 HAL_SPI_Init 完成对 SPI 参数初始化。 注意:该函数会调用:HAL_SPI_MspInit 函数来完成对 SPI 底层的初始化,包括:SPI 及 GPIO 时钟使能、GPIO 模式设置等。

HAL_StatusTypeDef HAL_SPI_Init(SPI_HandleTypeDef *hspi);

​ 形参 1 是 SPI_HandleTypeDef 结构体类型指针变量,其定义如下:

typedef struct __SPI_HandleTypeDef
{
     SPI_TypeDef *Instance; /* SPI 寄存器基地址 */
     SPI_InitTypeDef Init; /* SPI 通信参数 */
     uint8_t *pTxBuffPtr; /* SPI 的发送缓存 */
     uint16_t TxXferSize; /* SPI 的发送数据大小 */
     __IO uint16_t TxXferCount; /* SPI 发送端计数器 */
     uint8_t *pRxBuffPtr; /* SPI 的接收缓存 */
     uint16_t RxXferSize; /* SPI 的接收数据大小 */
     __IO uint16_t RxXferCount; /* SPI 接收端计数器 */
     void (*RxISR)(struct __SPI_HandleTypeDef *hspi); /* SPI 的接收端中断服务函数 */
     void (*TxISR)(struct __SPI_HandleTypeDef *hspi); /* SPI 的发送端中断服务函数 */
     DMA_HandleTypeDef *hdmatx; /* SPI 发送参数设置(DMA) */
     DMA_HandleTypeDef *hdmarx; /* SPI 接收参数设置(DMA) */
     HAL_LockTypeDef Lock; /* SPI 锁对象 */
     __IO HAL_SPI_StateTypeDef State; /* SPI 传输状态 */
     __IO uint32_t ErrorCode; /* SPI 操作错误代码 */
} SPI_HandleTypeDef;

​ 我们这里主要讲解第二个成员变量 Init,它是 SPI_InitTypeDef 结构体类型,该结构体定义如下:

typedef struct
{
     uint32_t Mode; /* 模式:主(SPI_MODE_MASTER) 从(SPI_MODE_SLAVE)*/
     uint32_t Direction; /* 方向: 只接收模式 单线双向通信数据模式 全双工 */
     uint32_t DataSize; /* 数据帧格式: 8 位/16 位 */
     uint32_t CLKPolarity; /* 时钟极性 CPOL 高/低电平 */
     uint32_t CLKPhase; /* 时钟相位 奇/偶数边沿采集 */
     uint32_t NSS; /* SS 信号由硬件(NSS)管脚控制还是软件控制 */
     uint32_t BaudRatePrescaler; /* 设置 SPI 波特率预分频值*/
     uint32_t FirstBit; /* 起始位是 MSB 还是 LSB */
     uint32_t TIMode; /* 帧格式 SPI motorola 模式还是 TI 模式 实验用motorola模式*/
     uint32_t CRCCalculation; /* 硬件 CRC 是否使能 */
     uint32_t CRCPolynomial; /* 设置 CRC 多项式*/
} SPI_InitTypeDef;

2)使能 SPI 时钟和配置相关引脚的复用功能

​ 本实验用到 SPI2,使用 PB13、PB14 和 PB15 作为 SPI_SCK、SPI_MISO 和 SPI_MOSI,因此需要先使能 SPI2 和 GPIOB 时钟。参考代码如下:

 __HAL_RCC_SPI2_CLK_ENABLE(); 
 __HAL_RCC_GPIOB_CLK_ENABLE(); 

​ IO 口复用功能是通过函数 HAL_GPIO_Init 来配置的。

3)使能 SPI

​ 通过__HAL_SPI_ENABLE 函数使能 SPI,便可进行数据传输。

4)SPI 传输数据

​ 通过 HAL_SPI_Transmit 函数进行发送数据。

​ 通过 HAL_SPI_Receive 函数进行接收数据。

​ 也可以通过 HAL_SPI_TransmitReceive 函数进行发送与接收操作。

5)设置 SPI 传输速度(实验没有设置,正点原子例程有设置)

​ SPI 初始化结构体 SPI_InitTypeDef 有一个成员变量是 BaudRatePrescaler,该成员变量用来 设置 SPI 的预分频系数,从而决定了 SPI 的传输速度。但是 HAL 库并没有提供单独的 SPI 分频系数修改函数,如果我们需要在程序中偶尔修改速度,那么我们就要通过设置 SPI_CR1 寄存器来修改,具体实现方法请参考后面软件设计小节相关函数。

实验代码

​ spi.c 存放 spi 底层驱动代码,norflash.c 文件存放 NM25Q128 驱动。

1)SPI 参数初始化(工作模式、数据时钟极性、时钟相位等)

spi.h

#ifndef __SPI_H
#define __SPI_H

#include "./SYSTEM/sys/sys.h"


/******************************************************************************************/
/* SPI2 引脚 定义 */

#define SPI2_SCK_GPIO_PORT              GPIOB
#define SPI2_SCK_GPIO_PIN               GPIO_PIN_13
#define SPI2_SCK_GPIO_CLK_ENABLE()      do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */

#define SPI2_MISO_GPIO_PORT             GPIOB
#define SPI2_MISO_GPIO_PIN              GPIO_PIN_14
#define SPI2_MISO_GPIO_CLK_ENABLE()     do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */

#define SPI2_MOSI_GPIO_PORT             GPIOB
#define SPI2_MOSI_GPIO_PIN              GPIO_PIN_15
#define SPI2_MOSI_GPIO_CLK_ENABLE()     do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */

/* SPI2相关定义 */
#define SPI2_SPI                        SPI2
#define SPI2_SPI_CLK_ENABLE()           do{ __HAL_RCC_SPI2_CLK_ENABLE(); }while(0)    /* SPI2时钟使能 */

/******************************************************************************************/

void spi2_init(void);
uint8_t spi2_read_write_byte(uint8_t data);

#endif

​ 我们通过宏定义标识符的方式去定义 SPI 通信用到的三个管脚 SCK、MISO 和 MOSI,同 时还宏定义 SPI2 的相关信息。

spi2_init函数(spi.c)

#include "./BSP/SPI/spi.h"

SPI_HandleTypeDef g_spi2_handler; /* SPI2句柄 */

void spi2_init(void)
{
    g_spi2_handler.Instance                 = SPI2_SPI;                     /* SPI2 */
    g_spi2_handler.Init.Mode                = SPI_MODE_MASTER;              /* 设置SPI模式(主机模式) */
    g_spi2_handler.Init.Direction           = SPI_DIRECTION_2LINES;         /* 设置SPI工作方式(全双工) */
    g_spi2_handler.Init.DataSize            = SPI_DATASIZE_8BIT;            /* 设置数据格式(8bit长度) */
    g_spi2_handler.Init.CLKPolarity         = SPI_POLARITY_HIGH;            /* 设置时钟极性(CPOL = 1) */
    g_spi2_handler.Init.CLKPhase            = SPI_PHASE_2EDGE;              /* 设置时钟相位(CPHA = 1) */
    g_spi2_handler.Init.NSS                 = SPI_NSS_SOFT;                 /* 设置片选方式(软件片选,自定义GPIO) */
    g_spi2_handler.Init.BaudRatePrescaler   = SPI_BAUDRATEPRESCALER_256;    /* 设置SPI时钟波特率分频(256分频) */
    g_spi2_handler.Init.FirstBit            = SPI_FIRSTBIT_MSB;             /* 设置大小端模式(MSB高位在前) */
    g_spi2_handler.Init.TIMode              = SPI_TIMODE_DISABLE;           /* 设置帧格式(关闭TI模式) */
    g_spi2_handler.Init.CRCCalculation      = SPI_CRCCALCULATION_DISABLE;   /* 设置CRC校验(关闭CRC校验) */
    g_spi2_handler.Init.CRCPolynomial       = 7;                            /* 设置CRC校验多项式(范围:1~65535)默认是7 */

    HAL_SPI_Init(&g_spi2_handler);
}

​ 在 spi_init 函数中主要工作就是对于 SPI 参数的配置,这里包括工作模式、数据模式、数据 大小、时钟极性、时钟相位、波特率预分频值等。关于 SPI 的管脚配置就放在了 HAL_SPI_MspInit 函数里。

2)使能 SPI 时钟和配置相关引脚的复用功能 3)使能 SPI

HAL_SPI_MspInit函数(spi.c)

/**
 * @brief       SPI2底层驱动,时钟使能,引脚配置
 *   @note      此函数会被HAL_SPI_Init()调用
 * @param       hspi:SPI句柄
 * @retval      无
 */
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
    SPI2_SPI_CLK_ENABLE();
    
    GPIO_InitTypeDef gpio_init_struct;

    if (hspi->Instance == SPI2_SPI)
    {
        SPI2_SCK_GPIO_CLK_ENABLE();     /* SPI2_SCK脚时钟使能 */
        SPI2_MISO_GPIO_CLK_ENABLE();    /* SPI2_MISO脚时钟使能 */
        SPI2_MOSI_GPIO_CLK_ENABLE();    /* SPI2_MOSI脚时钟使能 */

        /* SCK引脚模式设置(复用输出) */
        gpio_init_struct.Pin = SPI2_SCK_GPIO_PIN;
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;
        gpio_init_struct.Pull = GPIO_PULLUP;
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
        HAL_GPIO_Init(SPI2_SCK_GPIO_PORT, &gpio_init_struct);

        /* MISO引脚模式设置(复用输出) */
        gpio_init_struct.Pin = SPI2_MISO_GPIO_PIN;
        HAL_GPIO_Init(SPI2_MISO_GPIO_PORT, &gpio_init_struct);

        /* MOSI引脚模式设置(复用输出) */
        gpio_init_struct.Pin = SPI2_MOSI_GPIO_PIN;
        HAL_GPIO_Init(SPI2_MOSI_GPIO_PORT, &gpio_init_struct);
    }
}

4)SPI 传输数据(spi.c)

​ HAL_SPI_TransmitReceive函数定义

HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout)

​ 这里的 spi_read_write_byte 函数直接调用了 HAL 库内置的函数HAL_SPI_TransmitReceive进行接收发送操作

uint8_t spi2_read_write_byte(uint8_t data)
{
    uint8_t rec_data = 0;
    //主机发送数据的同时也会接受到数据,大小为1byte,超时时间为1000ms
    HAL_SPI_TransmitReceive(&g_spi2_handler, &data, &rec_data, 1, 1000);
    
    return rec_data;
}

norflash.h

#ifndef __norflash_H
#define __norflash_H

#include "./SYSTEM/sys/sys.h"

/******************************************************************************************/
/* NORFLASH 片选 引脚 定义 */
#define NORFLASH_CS_GPIO_PORT           GPIOB
#define NORFLASH_CS_GPIO_PIN            GPIO_PIN_12
#define NORFLASH_CS_GPIO_CLK_ENABLE()   do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */

/******************************************************************************************/
/* NORFLASH 片选信号 */
#define NORFLASH_CS(x)      do{ x ? \
                                  HAL_GPIO_WritePin(NORFLASH_CS_GPIO_PORT, NORFLASH_CS_GPIO_PIN, GPIO_PIN_SET) : \
                                  HAL_GPIO_WritePin(NORFLASH_CS_GPIO_PORT, NORFLASH_CS_GPIO_PIN, GPIO_PIN_RESET); \
                            }while(0)


void norflash_init(void);
uint8_t norflash_rd_sr1(void);
uint8_t norflash_read_data(uint32_t addr);
void norflash_erase_sector(uint32_t addr);
void norflash_write_page(uint8_t data, uint32_t addr);



                            
#endif

norflash的初始化(norflash.c)

#include "./BSP/SPI/spi.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/NORFLASH/norflash.h"


void norflash_init(void)
{
    NORFLASH_CS_GPIO_CLK_ENABLE();      /* NORFLASH CS脚 时钟使能 */

    GPIO_InitTypeDef gpio_init_struct;
    gpio_init_struct.Pin = NORFLASH_CS_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
    gpio_init_struct.Pull = GPIO_PULLUP;
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(NORFLASH_CS_GPIO_PORT, &gpio_init_struct);    /* SCK引脚模式设置(复用输出) */

    spi2_init();
    spi2_read_write_byte(0xFF); /* 清除DR的作用 */
    
    NORFLASH_CS(1);/* 取消片选 */
}

norflash读取数据

uint8_t norflash_read_data(uint32_t addr)
{
    uint8_t rec_data = 0;
    
    NORFLASH_CS(0);
    
    /* 1 发送读命令 */
    spi2_read_write_byte(0x03);
    
    /* 2 发送地址 24位地址,分三次发送*/
    spi2_read_write_byte(addr >> 16);
    spi2_read_write_byte(addr >> 8);
    spi2_read_write_byte(addr);
       
    /* 3 读取数据 发送空字节,就是不写数据,只读数据*/
    rec_data = spi2_read_write_byte(0xFF);
    
    NORFLASH_CS(1);
    return rec_data;
}

norflash擦除

uint8_t norflash_rd_sr1(void)
{
    uint8_t rec_data = 0;
    
    NORFLASH_CS(0);
    spi2_read_write_byte(0x05);     /* 读状态寄存器1命令 */
    rec_data = spi2_read_write_byte(0xFF);/* 读状态寄存器1 */
    NORFLASH_CS(1);
    
    return rec_data;
}

void norflash_erase_sector(uint32_t addr)
{
    /* 1 写使能 */
    NORFLASH_CS(0);
    spi2_read_write_byte(0x06);
    NORFLASH_CS(1);
    
    /* 2 等待空闲 */
    while(norflash_rd_sr1()&0x01);//SR1的第1位
    
    /* 3 发送扇区擦除指令 */
    NORFLASH_CS(0);
    spi2_read_write_byte(0x20);
    
    /* 4 发送地址 */
    spi2_read_write_byte(addr >> 16);
    spi2_read_write_byte(addr >> 8);
    spi2_read_write_byte(addr);
    NORFLASH_CS(1);
    
    /* 5 等待空闲 */
    while(norflash_rd_sr1()&0x01);
}

norflash页写

void norflash_write_page(uint8_t data, uint32_t addr)
{
    /* 1 擦除扇区 */
    norflash_erase_sector(addr);
    
    /* 2 写使能 */
    NORFLASH_CS(0);
    spi2_read_write_byte(0x06);
    NORFLASH_CS(1);
    
    /* 3 发送页写指令 */
    NORFLASH_CS(0);
    spi2_read_write_byte(0x02);
    
    /* 4 发送地址 */
    spi2_read_write_byte(addr >> 16);
    spi2_read_write_byte(addr >> 8);
    spi2_read_write_byte(addr);
    
    /* 5 要写入的数据 */
    spi2_read_write_byte(data);
    NORFLASH_CS(1);
    
    /* 6 等待写入完成(等待空闲) */
    while(norflash_rd_sr1()&0x01);
}

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./USMART/usmart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/NORFLASH/norflash.h"

int main(void)
{
    uint8_t key;
    uint16_t i = 0;
    uint8_t rec_data = 0;
    
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72);                     /* 延时初始化 */
    usart_init(115200);                 /* 串口初始化为115200 */
    usmart_dev.init(72);                /* 初始化USMART */
    led_init();                         /* 初始化LED */
    lcd_init();                         /* 初始化LCD */
    key_init();                         /* 初始化按键 */
    norflash_init();
    
    while (1)
    {
        key = key_scan(0);

        if (key == KEY1_PRES) /* KEY1按下,写入 */
        {
            norflash_write_page('5', 0x123456); /* 地址范围0~0xFFFFFF */
            printf("write finish \r\n");
        }

        if (key == KEY0_PRES) /* KEY0按下,读取数据 */
        {
            rec_data = norflash_read_data(0x123456);
            printf("read data : %c \r\n", rec_data);
        }

        i++;

        if (i == 20)
        {
            LED0_TOGGLE(); /* LED0闪烁 */
            i = 0;
        }

        delay_ms(10);
    }
}

​ 当往0x123456写入数据后,从该地址可读取数据,且掉电不丢失。

​ 将写入数据地址改为0x123457

norflash_write_page('5', 0x123457); /* 地址范围0~0xFFFFFF */

​ 此时,写入数据后,读取0x123456的数据发现数据丢失,这是因为往0x123457写入数据时,会将其所在的页全部擦除。

​ 在正点原子的例程中,会有解决这个问题。

四、正点原子例程源码
功能

​ 通过 KEY1 按键来控制 norflash 的写入,通过按键 KEY0 来控制 norflash 的读取。并在 LCD 模块上显示相关信息。我们还可以通过 USMART 控制读取 norflash 的 ID、擦除某个扇区或整片擦除。LED0 闪烁用于提示程序正在运行。

代码

​ 在spi.c中增加了一个速度设置函数,通过操作寄存器的方式去实现,设置之前需要关闭SPI,设置之后需要开启SPI。其代码如下:

/**
 * @brief       SPI2速度设置函数
 *   @note      SPI2时钟选择来自APB1, 即PCLK1, 为36Mhz
 *              SPI速度 = PCLK1 / 2^(speed + 1)
 * @param       speed   : SPI2时钟分频系数
                        取值为SPI_BAUDRATEPRESCALER_2~SPI_BAUDRATEPRESCALER_2 256
 * @retval      无
 */
void spi2_set_speed(uint8_t speed)
{
    assert_param(IS_SPI_BAUDRATE_PRESCALER(speed)); /* 判断有效性 */
    __HAL_SPI_DISABLE(&g_spi2_handler);             /* 关闭SPI */
    g_spi2_handler.Instance->CR1 &= 0XFFC7;         /* 位3-5清零,用来设置波特率 */
    g_spi2_handler.Instance->CR1 |= speed << 3;     /* 设置SPI速度 */
    __HAL_SPI_ENABLE(&g_spi2_handler);              /* 使能SPI */
}

​ 在norflash.h中添加了FLASH芯片列表和其他的指令,用于兼容其他flash芯片,这里不列出。

norflash初始化函数

uint16_t g_norflash_type = NM25Q128;     /* 默认是NM25Q128 */

/**
 * @brief       初始化SPI NOR FLASH
 * @param       无
 * @retval      无
 */
void norflash_init(void)
{
    uint8_t temp;

    NORFLASH_CS_GPIO_CLK_ENABLE();      /* NORFLASH CS脚 时钟使能 */

    GPIO_InitTypeDef gpio_init_struct;
    gpio_init_struct.Pin = NORFLASH_CS_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
    gpio_init_struct.Pull = GPIO_PULLUP;
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(NORFLASH_CS_GPIO_PORT, &gpio_init_struct); /* CS引脚模式设置(复用输出) */

    NORFLASH_CS(1);                     /* 取消片选 */

    spi2_init();                        /* 初始化SPI2 */
    spi2_set_speed(SPI_SPEED_2);        /* SPI2 切换到高速状态 18Mhz */
    
    g_norflash_type = norflash_read_id();   /* 读取FLASH ID. */
    
    if (g_norflash_type == W25Q256)     /* SPI FLASH为W25Q256, 必须使能4字节地址模式 */
    {
        temp = norflash_read_sr(3);     /* 读取状态寄存器3,判断地址模式 */

        if ((temp & 0X01) == 0)         /* 如果不是4字节地址模式,则进入4字节地址模式 */
        {
            norflash_write_enable();    /* 写使能 */
            temp |= 1 << 1;             /* ADP=1, 上电4位地址模式 */
            norflash_write_sr(3, temp); /* 写SR3 */
            
            NORFLASH_CS(0);
            spi2_read_write_byte(FLASH_Enable4ByteAddr);    /* 使能4字节地址指令 */
            NORFLASH_CS(1);
        }
    }

    //printf("ID:%x\r\n", g_norflash_type);
}

​ 在初始化函数中,将 SPI 通信协议用到的 CS 引脚配置好,同时根据 FLASH 的通信要求, 通过调用 spi2_set_speed 函数把 SPI2 切换到高速状态(SPI_SPEED_2在spi.h中定义)。然后尝试读取 flash 的 ID,由于 W25Q256 的容量比较大,通信的时候需要 4 个字节(如果是三字节地址,会造成空间浪费,大部分空间都无法访问),为了函数的兼容性,我们这里做了判断处理。当然, 我们使用的 NM25Q128 是 3 字节地址模式的。如果能读到 ID 则说明我们的 SPI 时序能正常操 作 FLASH,便可以通过 SPI 接口读写 NOR FLASH 的数据了。

读芯片ID时序及代码:

image-20231013153633752
/**
 * @brief       读取芯片ID
 * @param       无
 * @retval      FLASH芯片ID
 *   @note      芯片ID列表见: norflash.h, 芯片列表部分
 */
uint16_t norflash_read_id(void)
{
    uint16_t deviceid;

    NORFLASH_CS(0);
    spi2_read_write_byte(FLASH_ManufactDeviceID);   /* 发送读 ID 命令 */
    spi2_read_write_byte(0);    /* 写入一个字节 */
    spi2_read_write_byte(0);
    spi2_read_write_byte(0);
    deviceid = spi2_read_write_byte(0xFF) << 8;     /* 读取高8位字节 */
    deviceid |= spi2_read_write_byte(0xFF);         /* 读取低8位字节 */
    NORFLASH_CS(1);

    return deviceid;
}

​ 进行其它数据操作时,由于每一次读写操作的时候都需要发送地址,所以这里我们把这个 板块封装成函数,函数名是 norflash_send_address,实质上就是通过 SPI 的发送接收函数 spi2_read_write_byte 实现的。

/**
 * @brief       25QXX发送地址
 *   @note      根据芯片型号的不同, 发送24ibt / 32bit地址
 * @param       address : 要发送的地址
 * @retval      无
 */
static void norflash_send_address(uint32_t address)
{
    if (g_norflash_type == W25Q256) /*  只有W25Q256支持4字节地址模式 */
    {
        spi2_read_write_byte((uint8_t)((address)>>24)); /* 发送 bit31 ~ bit24 地址 */
    } 
    spi2_read_write_byte((uint8_t)((address)>>16));     /* 发送 bit23 ~ bit16 地址 */
    spi2_read_write_byte((uint8_t)((address)>>8));      /* 发送 bit15 ~ bit8  地址 */
    spi2_read_write_byte((uint8_t)address);             /* 发送 bit7  ~ bit0  地址 */
}

NOR FLASH 读取函数:

​ 与之前实验代码不同,这个可以在指定地址开始读取指定长度的数据

/**
 * @brief       读取SPI FLASH
 *   @note      在指定地址开始读取指定长度的数据
 * @param       pbuf    : 数据存储区
 * @param       addr    : 开始读取的地址(最大32bit)
 * @param       datalen : 要读取的字节数(最大65535)
 * @retval      无
 */
void norflash_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t i;

    NORFLASH_CS(0);
    spi2_read_write_byte(FLASH_ReadData);       /* 发送读取命令 */
    norflash_send_address(addr);                /* 发送地址 */
    
    for(i=0;i<datalen;i++)
    {
        pbuf[i] = spi2_read_write_byte(0XFF);   /* 循环读取 */
    }
    
    NORFLASH_CS(1);
}

​ 该函数用于从 NOR FLASH 的指定位置读出指定长度的数据,由于 NOR FLASH 支持以任 意地址(但是不能超过 NOR FLASH 的地址范围)开始读取数据,所以,这个代码相对来说比 较简单。首先拉低片选信号,发送读取命令,接着发送 24 位地址之后,程序就可以开始循环读数据,其地址就会自动增加,读取完数据后,需要拉高片选信号,结束通信。

NOR FLASH 写函数

​ 该函数可以在 NOR FLASH 的任意地址开始写入任意长度(必须不超过 NOR FLASH 的容 量)的数据。

/**
 * @brief       写SPI FLASH
 *   @note      在指定地址开始写入指定长度的数据 , 该函数带擦除操作!
 *              SPI FLASH 一般是: 256个字节为一个Page, 4Kbytes为一个Sector, 16个扇区为1个Block
 *              擦除的最小单位为Sector.
 *
 * @param       pbuf    : 数据存储区
 * @param       addr    : 开始写入的地址(最大32bit)
 * @param       datalen : 要写入的字节数(最大65535)
 * @retval      无
 */
uint8_t g_norflash_buf[4096];   /* 扇区缓存 */

void norflash_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint32_t secpos;
    uint16_t secoff;
    uint16_t secremain;
    uint16_t i;
    uint8_t *norflash_buf;

    norflash_buf = g_norflash_buf;
    secpos = addr / 4096;       /* 扇区地址 */
    secoff = addr % 4096;       /* 在扇区内的偏移 */
    secremain = 4096 - secoff;  /* 扇区剩余空间大小 */

    //printf("ad:%X,nb:%X\r\n", addr, datalen); /* 测试用 */
    if (datalen <= secremain)
    {
        secremain = datalen;    /* 不大于4096个字节 */
    }

    while (1)
    {
        norflash_read(norflash_buf, secpos * 4096, 4096);   /* 读出整个扇区的内容 */

        for (i = 0; i < secremain; i++)   /* 校验数据 */
        {
            if (norflash_buf[secoff + i] != 0XFF)
            {
                break;      /* 需要擦除, 直接退出for循环 */
            }
        }

        if (i < secremain)   /* 需要擦除 */
        {
            norflash_erase_sector(secpos);  /* 擦除这个扇区 */

            for (i = 0; i < secremain; i++)   /* 复制 */
            {
                norflash_buf[i + secoff] = pbuf[i];
            }

            norflash_write_nocheck(norflash_buf, secpos * 4096, 4096);  /* 写入整个扇区 */
        }
        else        /* 写已经擦除了的,直接写入扇区剩余区间. */
        {
            norflash_write_nocheck(pbuf, addr, secremain);  /* 直接写扇区 */
        }

        if (datalen == secremain)
        {
            break;  /* 写入结束了 */
        }
        else        /* 写入未结束 */
        {
            secpos++;               /* 扇区地址增1 */
            secoff = 0;             /* 偏移位置为0 */

            pbuf += secremain;      /* 指针偏移 */
            addr += secremain;      /* 写地址偏移 */
            datalen -= secremain;   /* 字节数递减 */

            if (datalen > 4096)
            {
                secremain = 4096;   /* 下一个扇区还是写不完 */
            }
            else
            {
                secremain = datalen;/* 下一个扇区可以写完了 */
            }
        }
    }
}

​ 我们这里简单介绍一下思路:先获得首地址(addr)所在的扇区,并计算在扇区内的偏移, 然后判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,再先看看是否要擦除, 如果不要,则直接写入数据即可。如果要则读出整个扇区,在偏移处开始写入指定长度的数据, 然后擦除这个扇区,再一次性写入。当所需要写入的数据长度超过一个扇区的长度的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此循环,直到写入结束。这里我们还定义了一个 g_norflash_buf 的全局变量,用于擦除时缓存扇区内的数据。

​ 简单介绍一下写函数的实质调用,它用到的是通过无检验写 SPI_FLASH 函数实现的,而 最终是用到页写函数 norflash_write_page,在前面也对页写时序进行了分析,现在看一下代码:

/**
 * @brief       SPI在一页(0~65535)内写入少于256个字节的数据
 *   @note      在指定地址开始写入最大256字节的数据
 * @param       pbuf    : 数据存储区
 * @param       addr    : 开始写入的地址(最大32bit)
 * @param       datalen : 要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
 * @retval      无
 */
static void norflash_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t i;

    norflash_write_enable();    /* 写使能 */

    NORFLASH_CS(0);
    spi2_read_write_byte(FLASH_PageProgram);    /* 发送写页命令 */
    norflash_send_address(addr);                /* 发送地址 */

    for(i=0;i<datalen;i++)
    {
        spi2_read_write_byte(pbuf[i]);          /* 循环写入 */
    }
    
    NORFLASH_CS(1);
    norflash_wait_busy();       /* 等待写入结束 */
}

​ 在页写功能的代码中,先发送写使能命令,才发送页写命令,然后发送写入的地址,再把 写入的内容通过一个 for 循环写入,发送完后拉高片选 CS 引脚结束通信,等待 flash 内部写入 结束。检测 flash 内部的状态可以通过查看 NM25Qxx 状态寄存器 1 的位 0。

NM25Qxx 状 态寄存器表:

状态寄存器Bit7Bit6Bit5Bit4Bit3Bit2Bit1Bit0
状态寄存器1SPRRVTBBP2BP1BP0WELBUSY
状态寄存器2SUSCMPLB3LB2LB1®QESRP1
状态寄存器3HOLD/RSTDRV1DRV0®®WPSADPADS
  • BUSY位 指示当前状态 0:空闲状态(硬件自动清除) 1:当前处于忙碌状态
  • WEL位 执行WriteEnable指令该位为1,可以页写/扇区or块or片擦除/写状态寄存器

​ 0:写禁止,不能页编程/扇区or块or片擦除/写状态寄存器

在 norflash_write_page 函数的基础上,增加了 norflash_write_nocheck 函数进行封装解决写 入字节可能大于该页剩下的字节数问题,方便解决写入错误问题,其代码如下:

/**
 * @brief       无检验写SPI FLASH
 *   @note      必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!
 *              具有自动换页功能
 *              在指定地址开始写入指定长度的数据,但是要确保地址不越界!
 *
 * @param       pbuf    : 数据存储区
 * @param       addr    : 开始写入的地址(最大32bit)
 * @param       datalen : 要写入的字节数(最大65535)
 * @retval      无
 */
static void norflash_write_nocheck(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t pageremain;
    pageremain = 256 - addr % 256;  /* 单页剩余的字节数 */

    if (datalen <= pageremain)      /* 不大于256个字节 */
    {
        pageremain = datalen;
    }

    while (1)
    {
        /* 当写入字节比页内剩余地址还少的时候, 一次性写完
         * 当写入直接比页内剩余地址还多的时候, 先写完整个页内剩余地址, 然后根据剩余长度进行不同处理
         */
        norflash_write_page(pbuf, addr, pageremain);

        if (datalen == pageremain)   /* 写入结束了 */
        {
            break;
        }
        else     /* datalen > pageremain */
        {
            pbuf += pageremain;         /* pbuf指针地址偏移,前面已经写了pageremain字节 */
            addr += pageremain;         /* 写地址偏移,前面已经写了pageremain字节 */
            datalen -= pageremain;      /* 写入总长度减去已经写入了的字节数 */

            if (datalen > 256)          /* 剩余数据还大于一页,可以一次写一页 */
            {
                pageremain = 256;       /* 一次可以写入256个字节 */
            }
            else     /* 剩余数据小于一页,可以一次写完 */
            {
                pageremain = datalen;   /* 不够256个字节了 */
            }
        }
    }
}
image-20231013162610498

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./USMART/usmart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/NORFLASH/norflash.h"

/* 要写入到FLASH的字符串数组 */
const uint8_t g_text_buf[] = {"STM32 SPI TEST"};

#define TEXT_SIZE sizeof(g_text_buf) /* TEXT字符串长度 */

int main(void)
{
    uint8_t key;
    uint16_t i = 0;
    uint8_t datatemp[TEXT_SIZE];
    uint32_t flashsize;
    uint16_t id = 0;

    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72);                     /* 延时初始化 */
    usart_init(115200);                 /* 串口初始化为115200 */
    usmart_dev.init(72);                /* 初始化USMART */
    led_init();                         /* 初始化LED */
    lcd_init();                         /* 初始化LCD */
    key_init();                         /* 初始化按键 */
    norflash_init();                    /* 初始化NORFLASH */

    lcd_show_string(30,  50, 200, 16, 16, "STM32", RED);
    lcd_show_string(30,  70, 200, 16, 16, "SPI TEST", RED);
    lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "KEY1:Write  KEY0:Read", RED); /* 显示提示信息 */

    id = norflash_read_id(); /* 读取FLASH ID */

    while ((id == 0) || (id == 0XFFFF)) /* 检测不到FLASH芯片 */
    {
        lcd_show_string(30, 130, 200, 16, 16, "FLASH Check Failed!", RED);
        delay_ms(500);
        lcd_show_string(30, 130, 200, 16, 16, "Please Check!      ", RED);
        delay_ms(500);
        LED0_TOGGLE(); /* LED0闪烁 */
    }

    lcd_show_string(30, 130, 200, 16, 16, "SPI FLASH Ready!", BLUE);
    flashsize = 16 * 1024 * 1024; /* FLASH 大小为16M字节 */

    while (1)
    {
        key = key_scan(0);

        if (key == KEY1_PRES) /* KEY1按下,写入 */
        {
            lcd_fill(0, 150, 239, 319, WHITE); /* 清除半屏 */
            lcd_show_string(30, 150, 200, 16, 16, "Start Write FLASH....", BLUE);
            sprintf((char *)datatemp, "%s%d", (char *)g_text_buf, i);
            norflash_write((uint8_t *)datatemp, flashsize - 100, TEXT_SIZE);      /* 从倒数第100个地址处开始,写入SIZE长度的数据 */
            lcd_show_string(30, 150, 200, 16, 16, "FLASH Write Finished!", BLUE); /* 提示传送完成 */
        }

        if (key == KEY0_PRES) /* KEY0按下,读取字符串并显示 */
        {
            lcd_show_string(30, 150, 200, 16, 16, "Start Read FLASH... . ", BLUE);
            norflash_read(datatemp, flashsize - 100, TEXT_SIZE);                   /* 从倒数第100个地址处开始,读出SIZE个字节 */
            lcd_show_string(30, 150, 200, 16, 16, "The Data Readed Is:   ", BLUE); /* 提示传送完成 */
            lcd_show_string(30, 170, 200, 16, 16, (char *)datatemp, BLUE);         /* 显示读到的字符串 */
        }

        i++;

        if (i == 20)
        {
            LED0_TOGGLE(); /* LED0闪烁 */
            i = 0;
        }

        delay_ms(10);
    }
}

​ 在 main 函数前面,我们定义了 g_text_buf 数组,用来存放要写入到 FLASH 的字符串。main 函数代码和 IIC 实验那部分代码大同小异,具体流程大致是:在完成系统级和用户级初始化工作后,读取 FLASH 的 ID,然后通过 KEY0 去读取倒数第 100 个地址处开始的数据并把数据显示在 LCD 上;另外还可以通过 KEY1 去倒数第 100 个地址处写入 g_text_buf 数据并在 LCD 界 面中显示传输中,完成后并显示“FLASH Write Finished!”。

​ 程序在开机的时候会检测 NOR FLASH 是否存在,如果不存在则会在 LCD 模块上显示错 误信息,同时 LED0 慢闪。大家可以通过跳线帽把 PB14 和 PB15 短接就可以看到报错了。

​ 其余部分与实验代码相近,不做过多叙述。正点原子例程源码由于过长,就不放在这里了。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值