【正点原子STM32连载】第三十六章 SPI实验 摘自【正点原子】STM32F103 战舰开发指南V1.2

news2024/11/26 0:27:51

1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html#

第三十六章 SPI实验

本章,我们将介绍如何使用STM32F103的SPI功能,并实现对外部NOR FLASH的读写并把结果显示在TFTLCD模块上。
本章分为如下几个小节:
36.1 SPI及NOR FLASH芯片介绍
36.2 硬件设计
36.3 程序设计
36.4 下载验证

36.1 SPI及NOR FLASH介绍

36.1.1 SPI介绍
我们将从结构、时序和寄存器三个部分来介绍SPI。
36.1.1.1 SPI框图
SPI是英语 Serial Peripheral interface 缩写,顾名思义就是串行外围设备接口。SPI通信协议是 Motorola公司首先在其 MC68HCXX 系列处理器上定义的。SPI 接口是一种高速的全双工同步的通信总线,已经广泛应用在众多MCU、存储芯片、AD转换器和LCD之间。大部分STM32是有有3个SPI接口,本实验使用的是SPI2。
我们先看SPI的结构框图,了解它的大致功能,如图36.1.1.1.1所示。
在这里插入图片描述

图36.1.1.1.1 SPI框图
围绕框图,我们展开介绍一下SPI的引脚信息、工作原理以及传输方式,把SPI的4种工作方式放在后面讲解。
SPI的引脚信息:
MISO(Master In / Slave Out)主设备数据输入,从设备数据输出。
MOSI(Master Out / Slave In)主设备数据输出,从设备数据输入。
SCLK(Serial Clock)时钟信号,由主设备产生。
CS(Chip Select)从设备片选信号,由主设备产生。
SPI的工作原理:在主机和从机都有一个串行移位寄存器,主机通过向它的SPI串行寄存器写入一个字节来发起一次传输。串行移位寄存器通过MOSI信号线将字节传送给从机,从机也将自己的串行移位寄存器中的内容通过MISO信号线返回给主机。这样,两个移位寄存器中的内容就被交换。外设的写操作和读操作是同步完成的。如果只是进行写操作,主机只需忽略接收到的字节。反之,若主机要读取从机的一个字节,就必须发送一个空字节引发从机传输。
SPI的传输方式:SPI总线具有三种传输方式:全双工、单工以及半双工传输方式。
全双工通信,就是在任何时刻,主机与从机之间都可以同时进行数据的发送和接收。
单工通信,就是在同一时刻,只有一个传输的方向,发送或者是接收。
半双工通信,就是在同一时刻,只能为一个方向传输数据。
36.1.1.2 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是偶数边沿采样,所以需要根据偶数边沿判断,假如第一个边沿即奇数边沿是下降沿,那么偶数边沿的边沿极性就是上升沿。不理解的,可以看一下下面4种SPI工作模式的图。
由于CPOL和CPHA都有两种不同状态,所以SPI分成了4种模式。我们在开发的时候,使用比较多的是模式0和模式3。下面请看表36.1.1.2.1 SPI工作模式表。
SPI工作模式 CPOL CPHA SCL空闲状态 采样边沿 采样时刻
0 0 0 低电平 上升沿 奇数边沿
1 0 1 低电平 下降沿 偶数边沿
2 1 0 高电平 下降沿 奇数边沿
3 1 1 高电平 上升沿 偶数边沿
表36.1.1.2.1 SPI工作模式表
下面分别对SPI的4种工作模式进行分析:
在这里插入图片描述

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

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

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

图36.1.1.2.4 串行时钟的偶数边沿上升沿采样图
图36.1.1.2.4是CPOL=1&&CPHA=1的情形,可以看到未发送数据和发送数据完毕,SCL的状态是高电平,奇数边沿的边沿极性是上升沿,偶数边沿的边沿极性是下降沿。因为CPHA=1,所以数据在偶数边沿上升沿被采样。在奇数边沿的时候MOSI和MISO会发生变化,在偶数边沿时候是稳定的。
36.1.1.3 SPI寄存器
在这里我们简单介绍一下本实验用到的寄存器。
 SPI控制寄存器1(SPI_CR1)
SPI 控制寄存器1描述如图36.1.1.3.1所示:
在这里插入图片描述

图36.1.1.3.1 SPI_CR1寄存器(部分)
该寄存器控制着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)
SPI 状态寄存器描述如图36.1.1.3.2所示:
在这里插入图片描述

图36.1.1.3.2 SPI_SR寄存器(部分)
该寄存器是查询当前SPI的状态的,我们在实验中用到的是TXE位和RXNE位,即发送完成和接收完成是否的标记。
 SPI数据寄存器(SPI_DR)
SPI 数据寄存器描述如图36.1.4.3所示:
在这里插入图片描述

图36.1.1.3.2 SPI_DR寄存器
该寄存器是SPI数据寄存器,是一个双寄存器,包括了发送缓存和接收缓存。当向该寄存器写数据的时候,SPI就会自动发送,当收到数据的时候,也是存在该寄存器内。
36.1.2 NOR FLASH简介
36.1.2.1 FLASH简介
FLASH是常见的用于存储数据的半导体器件,它具有容量大、可重复擦写、按“扇区/块”擦除、掉电后数据可继续保存的特性。常见的FLASH主要有NOR FLASH和NAND FLASH两种类型,它们的特性如表36.1.2.1.1所示。NOR和NAND是两种数字门电路,可以简单地认为Flash内部存储单元使用哪种门作存储单元就是哪类型的FLASH。U盘,SSD,eMMC等为NAND型,而NOR FLASH则根据设计需要灵活应用于各类PCB上,如BIOS,手机等。
在这里插入图片描述

表36.1.2.1.1 NOR FLASH和NAND FLASH特性对比
NOR与NAND在数据写入前都需要有擦除操作,但实际上NOR FLASH的一个bit可以从1变成0,而要从0变1就要擦除后再写入,NAND Flash这两种情况都需要擦除。擦除操作的最小单位为“扇区/块”,这意味着有时候即使只写一字节的数据,则这个“扇区/块”上之前的数据都可能会被擦除。
NOR的地址线和数据线分开,它可以按“字节”读写数据,符合CPU的指令译码执行要求,所以假如NOR上存储了代码指令,CPU给NOR一个地址,NOR就能向CPU返回一个数据让CPU执行,中间不需要额外的处理操作,这体现于表35.1.2.1.1中的支持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为例,认识一下具体的NOR Flash的特性。
NM25Q128是一款大容量SPI FLASH产品,其容量为16M。它将16M字节的容量分为256个块(Block),每个块大小为64K字节,每个块又分为16个扇区(Sector),每一个扇区16页,每页256个字节,即每个扇区4K个字节。NM25Q128的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节。这样我们需要给NM25Q128开辟一个至少4K的缓存区,这样对SRAM要求比较高,要求芯片必须有4K以上SRAM才能很好的操作。
NM25Q128的擦写周期多达10W次,具有20年的数据保存期限,支持电压为2.7~3.6V,NM25Q128支持标准的SPI,还支持双输出/四输出的SPI,最大SPI时钟可以到104Mhz(双输出时相当于208Mhz,四输出时相当于416Mhz)。
下面我们看一下NM25Q128芯片的管脚图,如图36.1.2.1.2所示。
在这里插入图片描述

图 36.1.2.1.2 NM25Q128芯片引脚图
芯片引脚连接如下:CS即片选信号输入,低电平有效;DO是MISO引脚,在CLK管脚的下降沿输出数据;WP是写保护管脚,高电平可读可写,低电平仅仅可读;DI是MOSI引脚,主机发送的数据、地址和命令从SI引脚输入到芯片内部,在CLK管脚的上升沿捕获捕获数据;CLK是串行时钟引脚,为输入输出提供时钟脉冲;HOLD是保持管脚,低电平有效。
STM32F103通过SPI总线连接到NM25Q128对应的引脚即可启动数据传输。
36.1.2.2 NOR FLASH工作时序
前面对于NM25Q128的介绍中也提及其存储的体系,NM25Q128有写入、读取还有擦除的功能,下面就对这三种操作的时序进行分析,在后面通过代码的形式驱动它。
下面先让我们看一下读操作时序,如图36.1.2.1所示:
在这里插入图片描述

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

图36.1.2.2.2 NM25Q128页写时序
在发送页写指令之前,需要先发送“写使能”指令。然后主机拉低CS引脚,然后通过MOSI引脚把02H发送到芯片,接着发送24位地址,最后你就可以发送你需要写的字节数据到芯片。完成数据写入之后,需要拉高CS引脚,停止数据传输。
下面介绍一下扇区擦除时序,如图36.1.2.2.3所示:
在这里插入图片描述

图36.1.2.2.3 扇区擦除时序图
扇区擦除指的是将一个扇区擦除,通过前面的介绍也知道,NM25Q128的扇区大小是4K字节。擦除扇区后,扇区的位全置1,即扇区字节为FFh。同样的,在执行扇区擦除之前,需要先执行写使能指令。这里需要注意的是当前SPI总线的状态,假如总线状态是BUSY,那么这个扇区擦除是无效的,所以在拉低CS引脚准备发送数据前,需要先要确定SPI总线的状态,这就需要执行读状态寄存器指令,读取状态寄存器的BUSY位,需要等待BUSY位为0,才可以执行擦除工作。
接着按时序图分析,主机先拉低CS引脚,然后通过MOSI引脚发送指令代码20h到芯片,然后接着把24位扇区地址发送到芯片,然后需要拉高CS引脚,通过读取寄存器状态等待扇区擦除操作完成。
此外还有对整个芯片进行擦除的操作,时序比扇区擦除更加简单,不用发送24bit地址,只需要发送指令代码C7h到芯片即可实现芯片的擦除。
在NM25Q128手册中还有许多种方式的读/写/擦除操作,我们这里只分析本实验用到的,其他大家可以参考NM25Q128手册。
36.2 硬件设计

  1. 例程功能
    通过KEY1按键来控制norflash的写入,通过按键KEY0来控制norflash的读取。并在LCD模块上显示相关信息。我们还可以通过USMART控制读取norflash的ID、擦除某个扇区或整片擦除。LED0闪烁用于提示程序正在运行。
  2. 硬件资源
    1)LED灯
    LED0 – PB5
    2)独立按键
    KEY0 – PE4
    KEY1 – PE3
    3)NOR FLASH NM25Q128
    4)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
    5)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)(USMART使用)
  3. 原理图
    我们主要来看看norflash和开发板的连接,如下图所示:
    在这里插入图片描述

图36.3.1 NOR FLASH与开发板的连接原理
通过上图可知,NM25Q128的CS、SCK、MISO和MOSI分别连接在PB12、PB13、PB14和PB15上。本实验还支持多种型号的SPI FLASH芯片,比如:BY25Q128/NM25Q128/W25Q128等等,具体请看norflash.h文件的宏定义,在程序上只需要稍微修改一下,后面讲解程序的时候会提到。
36.3 程序设计
36.3.1 SPI的HAL库驱动
SPI在HAL库中的驱动代码在stm32f1xx_hal_spi.c文件(及其头文件)中。

  1. HAL_SPI_Init函数
    SPI的初始化函数,其声明如下:
HAL_StatusTypeDef HAL_SPI_Init(SPI_HandleTypeDef *hspi);
  函数描述:
用于初始化SPI。
  函数形参:
形参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模式 */
  uint32_t CRCCalculation;     	/* 硬件CRC是否使能 */
  uint32_t CRCPolynomial;      	/* 设置CRC多项式*/
} SPI_InitTypeDef;

 函数返回值:
HAL_StatusTypeDef枚举类型的值。
使用SPI传输数据的配置步骤
1)SPI参数初始化(工作模式、数据时钟极性、时钟相位等)。
HAL库通过调用SPI初始化函数HAL_SPI_Init完成对SPI参数初始化,详见例程源码。
注意:该函数会调用:HAL_SPI_MspInit函数来完成对SPI底层的初始化,包括:SPI及GPIO时钟使能、GPIO模式设置等。
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寄存器来修改,具体实现方法请参考后面软件设计小节相关函数。
36.3.2 程序流程图
在这里插入图片描述

图36.3.2.1 SPI实验程序流程图
36.3.3 程序解析
本实验中,我们通过调用HAL库的函数去驱动SPI进行通信,所以需要在工程中的FWLIB分组下添加stm32f1xx_hal_spi.c文件去支持。实验工程中,我们新增了spi.c存放spi底层驱动代码,norflash.c文件存放W25Q128/NM25Q128驱动。

  1. SPI驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。SPI驱动源码包括两个文件:spi.c和spi.h。
    下面我们直接介绍SPI相关的程序,首先先介绍 spi.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)

#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)

#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)   

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

我们通过宏定义标识符的方式去定义SPI通信用到的三个管脚SCK、MISO和MOSI,同时还宏定义SPI2的相关信息。
接下来我们看一下spi.c代码中的初始化函数,代码如下:

/**
 * @brief   		SPI初始化代码
 *   @note      	主机模式,8位数据,禁止硬件片选
 * @param       	无
 * @retval      	无
 */
SPI_HandleTypeDef g_spi2_handler; 				/* SPI2句柄 */

void spi2_init(void)
{
    SPI2_SPI_CLK_ENABLE(); 						/* SPI2时钟使能 */

    g_spi2_handler.Instance = SPI2_SPI;      	/* SPI2 */
    g_spi2_handler.Init.Mode = SPI_MODE_MASTER;	/* 设置SPI工作模式,设置为主模式 */
/* 设置SPI单向或者双向的数据模式:SPI设置为双线模式 */
    g_spi2_handler.Init.Direction = SPI_DIRECTION_2LINES;             
/* 设置SPI的数据大小:SPI发送接收8位帧结构 */
    g_spi2_handler.Init.DataSize = SPI_DATASIZE_8BIT;                  
    /* 串行同步时钟的空闲状态为高电平 */
g_spi2_handler.Init.CLKPolarity = SPI_POLARITY_HIGH;
/* 串行同步时钟的第二个跳变沿(上升或下降)数据被采样 */
g_spi2_handler.Init.CLKPhase = SPI_PHASE_2EDGE;                    
/* NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制 */
    g_spi2_handler.Init.NSS = SPI_NSS_SOFT;                           
/* 定义波特率预分频的值:波特率预分频值为256 */
    g_spi2_handler.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; 
/* 指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始 */
    g_spi2_handler.Init.FirstBit = SPI_FIRSTBIT_MSB;                  
    g_spi2_handler.Init.TIMode = SPI_TIMODE_DISABLE; 	/* 关闭TI模式 */
/* 关闭硬件CRC校验 */
    g_spi2_handler.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;   
    g_spi2_handler.Init.CRCPolynomial = 7;              	/* CRC值计算的多项式 */
HAL_SPI_Init(&g_spi2_handler);                        	/* 初始化 */

    __HAL_SPI_ENABLE(&g_spi2_handler);                   	/* 使能SPI2 */
/* 启动传输, 实际上就是产生8个时钟脉冲, 达到清空DR的作用, 非必需 */
    spi2_read_write_byte(0Xff); 
}
在spi_init函数中主要工作就是对于SPI参数的配置,这里包括工作模式、数据模式、数据大小、时钟极性、时钟相位、波特率预分频值等。关于SPI的管脚配置就放在了HAL_SPI_MspInit函数里,其代码如下:
/**
 * @brief     	SPI底层驱动,时钟使能,引脚配置
 * @note      	此函数会被HAL_SPI_Init()调用
 * @param       	hspi:SPI句柄
 * @retval    	无
 */
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
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脚时钟使能 */

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

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

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

通过以上两个函数的作用就可以完成SPI初始化。接下来介绍SPI的发送和接收函数,其定义如下:

/**
 * @brief     	SPI2读写一个字节数据
 * @param      	txdata  : 要发送的数据(1字节)
 * @retval     	接收到的数据(1字节)
 */
uint8_t spi2_read_write_byte(uint8_t txdata)
{
    uint8_t rxdata;
    HAL_SPI_TransmitReceive(&g_spi2_handler, &txdata, &rxdata, 1, 1000);
    return rxdata; /* 返回收到的数据 */
}
这里的spi_read_write_byte函数直接调用了HAL库内置的函数进行接收发送操作。前面已经有介绍了,这里就不展开对HAL_SPI_TransmitReceive函数的解析。
由于不同的外设需要的通信速度不一样,所以这里我们定义了一个速度设置函数,通过操作寄存器的方式去实现,其代码如下:
/**
 * @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 */
}
  1. NOR FLASH驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。NOR FLASH驱动源码包括两个文件:norflash.c和norflash.h。
    在上一小节已经对SPI协议需要用到的东西都封装好了。那么现在就要在SPI通信的基础上,通过前面分析的NM25Q128的工作时序拟定通信代码。
    由于这部分的代码量比较多,这里就不一一贴出来介绍。介绍几个重点,其余的请自行查看源码。首先是norflash.h头文件,我们做了一个FLASH芯片列表(宏定义),这些宏定义是一些支持的FLASH芯片的ID。接下来是FLASH芯片指令表的宏定义,这个请参考FLASH芯片手册比对得到,这里就不将代码列出来了。
    下面介绍norflash.c文件几个重要的函数,首先是NOR FLASH初始化函数,其定义如下:
/**
 * @brief   		初始化SPI NOR FLASH
 * @param       	无
 * @retval      	无
 */
void norflash_init(void)
{
    uint8_t temp;

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

/* 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); 

    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);
            spi1_read_write_byte(FLASH_Enable4ByteAddr);    /* 使能4字节地址指令 */
            NORFLASH_CS(1);
        }
}

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

在初始化函数中,将SPI通信协议用到的CS引脚配置好,同时根据FLASH的通信要求,通过调用spi2_set_speed函数把SPI2切换到高速状态。然后尝试读取flash的ID,由于W25Q256的容量比较大,通信的时候需要4个字节,为了函数的兼容性,我们这里做了判断处理。当然,我们使用的NM25Q128是3字节地址模式的。如果能读到ID则说明我们的SPI时序能正常操作FLASH,便可以通过SPI接口读写NOR FLASH的数据了。
进行其它数据操作时,由于每一次读写操作的时候都需要发送地址,所以这里我们把这个板块封装成函数,函数名是norflash_send_address,实质上就是通过SPI的发送接收函数spi2_read_write_byte实现的,这里就不列出来了,大家可以查看光盘源码。
下面介绍一下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写函数,其定义如下:

/**
 * @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; 	/* 扇区剩余空间大小 */

    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;	/* 下一个扇区可以写完了 */
            }
        }
    }
}

该函数可以在NOR FLASH的任意地址开始写入任意长度(必须不超过NOR FLASH的容量)的数据。
我们这里简单介绍一下思路:先获得首地址(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的状态寄存器,可以通过寄存器相关位判断NM25Qxx的状态,下面是NM25Qxx状态寄存器表:
状态寄存器 Bit7 Bit6 Bit5 Bit4 Bit3 Bit2 Bit1 Bit0
状态寄存器1 SPR RV TB BP2 BP1 BP0 WEL BUSY
状态寄存器2 SUS CMP LB3 LB2 LB1 ® QE SRP1
状态寄存器3 HOLD/RST DRV1 DRV0 ® ® WPS ADP ADS
表36.3.3.1 NM25Qxx状态寄存器表
我们也定义了一个函数norflash_read_sr,去读取NM25Qxx状态寄存器的值,这里就不列出来了,主要实现的方式也是老套路:根据传参判断需要获取的是哪个状态寄存器,然后拉低片选线,调用spi2_read_write_byte函数发送该寄存器的命令,然后通过发送一字节空数据获取读取到的数据,最后拉高片选线,函数返回读取到的值。
在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个字节了 */
            }
        }
    }
}

上面函数的实现主要是逻辑处理,通过判断传参中的写入字节的长度与单页剩余的字节数,来决定是否是需要在新页写入剩下的字节。这里需要大家自行理解一下。通过调用该函数实现了norflash_write的功能。
下面简单介绍一下擦除函数norflash_erase_sector,前面工作时序中也有对此描述,现在就来看一下代码:

/**
 * @brief       擦除一个扇区
 *   @note      注意,这里是扇区地址,不是字节地址!!
 *               擦除一个扇区的最少时间:150ms
 *
 * @param       saddr : 扇区地址 根据实际容量设置
 * @retval      无
 */
void norflash_erase_sector(uint32_t saddr)
{
    saddr *= 4096;
    norflash_write_enable();    					/* 写使能 */
    norflash_wait_busy();        					/* 等待空闲 */
    NORFLASH_CS(0);
    spi2_read_write_byte(FLASH_SectorErase);    /* 发送写页命令 */
    norflash_send_address(saddr);   				/* 发送地址 */
    NORFLASH_CS(1);
    norflash_wait_busy();           				/* 等待扇区擦除完成 */
}

该代码也是老套路,通过发送擦除指令实现擦除功能,要注意的是使用扇区擦除指令前,需要先发送写使能指令,拉低片选线,发送扇区擦除指令之后,发送擦除的扇区地址,实现擦除,最后拉高片选线结束通信。在函数最后通过读取寄存器状态的函数,等待扇区擦除完成。
3. main.c代码
在main.c里面编写如下代码:

const uint8_t g_text_buf[] = {"STM32 SPI TEST"};	/* 要写到FLASH的字符串数组 */
#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按下,写入 */
        {   /* 从倒数第100个地址处开始,写入SIZE长度的数据 */
            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);     
            lcd_show_string(30, 150, 200, 16, 16, "FLASH Write Finished!", BLUE); 
        }

        if (key == KEY0_PRES) /* KEY0按下,读取字符串并显示 */
        {   /* 从倒数第100个地址处开始,读出SIZE个字节 */
            lcd_show_string(30, 150, 200, 16, 16, "Start Read FLASH...", BLUE);
            norflash_read(datatemp, flashsize - 100, TEXT_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!”。
36.4 下载验证
将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示的内容如图36.4.1所示:
在这里插入图片描述

图36.4.1 SPI实验程序运行效果图
通过先按下KEY1写入数据,然后再按KEY0读取数据,得到如图36.4.2所示:
在这里插入图片描述

图36.4.2 操作后的显示效果图
程序在开机的时候会检测NOR FLASH是否存在,如果不存在则会在 LCD 模块上显示错误信息,同时LED0慢闪。大家可以通过跳线帽把PB14和PB15短接就可以看到报错了。
该实验还支持USMART,在这里我们加入了norflash_read_id和norflash_erase_chip以及norflash_erase_sector函数。可以通过USMART调用norflash_read_id函数去读取SPI_FLASH的ID,也可以调用另外两个擦除函数。需要注意的是假如调用了norflash_erase_chip函数将会对整个SPI_FLASH进行擦除,一般情况不建议对整个SPI_FLASH进行擦除,因为会导致字库和综合例程所需要的系统文件全部丢失。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/666735.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

测试实战总结,性能测试-秒杀系统实战与优化,彻底打通性能测试...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 对于大并发量的系…

小程序跳转

小程序的路由跳转 一、路由跳转的方式 1.1 wx.navigateTo wx.navigateTo 保留当前页面&#xff0c;跳转到应用内的某个页面。会将页面缓放在页面栈中&#xff0c;最多十个。 wx.navigateTo({url: test })1.2 wx.redirectTo wx.redirectTo 关闭当前页面&#xff0c;跳转到应…

电商项目,订单如何测试?软件测试实战场景,所有测试点汇总...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 订单测试作为常见…

scanpy单细胞分析流程

梳理一下scanpy单细胞分析流程&#xff08;处理的是scRNA-seq&#xff09;。 先上一张流程图&#xff1a; scanpy单细胞分析流程 import scanpy as scRead data 常用的文件格式有两种&#xff0c;分别是h5ad和10X mtx # read h5ad adata sc.read()# read 10X mtx adata …

从小白到大神之路之学习运维第44天---第三阶段----拓展知识-----文件管理命令(find+sed+awk)、pycharm工具

第三阶段基础 时 间&#xff1a;2023年6月20日 参加人&#xff1a;全班人员 内 容&#xff1a; 目录 一、文件管理命令 find 1. 根据文件名查找文件 2. 根据文件类型查找文件 3. 根据文件大小查找文件 4. 根据时间戳查找文件 5. 组合多个条件查找文件 Sed 1. 替…

【Vue】学习笔记-创建Vue3.0工程

创建Vue3.0工程 使用vue-cli创建查看vue/cli版本&#xff0c;确保vue/cli版本在4.5.0以上安装或者升级你的vue/cli创建启动 使用vite创建创建工程进入工程目录安装依赖运行 使用vue-cli创建 官方文档&#xff1a;https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-…

【性能测试】压力测试指标全解之TPS、响应时间

hello&#xff0c;大家好&#xff01;我是磨磨唧唧小蘑菇~ 接上期阐述了《TP50/90/99/999》的含义及计算方式&#xff0c;本期将阐述压力测试的其他指标&#xff0c;如TPS、响应时间等。 目录 一、TPS 二、响应时间 三、TPS与响应时间RT的关系 一、TPS 1、TPS的含义 …

vscode折叠代码展开快捷键

1.折叠所有代码 (按住ctrl 分别点击k和0) ctrlk,ctrl0 2.展开所有代码 (按住ctrl 分别点击k和j) ctrlk,ctrlj 3. 折叠鼠标竖线所在位置的节点以及当前节点下的子节点&#xff08;递归折叠&#xff09; ctrlk,ctrl[ 4. 展开鼠标竖线所在位置的节点以及当前节点下的子节点&#x…

【Springboot】关于Spring和SpringBoot的那些事儿

参考javaguide的学习笔记~ 1 怎么那么多名字呀~ 一开始看到这个图太劝退了&#xff0c;但实际上一开始只需要理解它是一个框架&#xff0c;包含很多个模块&#xff0c;能够简化开发。 使得接收HTTP请求&#xff0c;查数据库&#xff0c;写业务逻辑三部分能够分开。 并且能很…

【2023最全最叼教程】Selenium 自动化测试环境搭建

【导语】Selenium是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中&#xff0c;就像真正的用户在操作一样。支持自动录制动作和自动生成 .Net、Java、Perl等不同语言的测试脚本。本文详细介绍了搭建自动化测试环境所需的工具&#xff0c;让你学习自动化测试不…

人脸检测——基于机器学习3】AdaBoost算法

简介 主要工作AdaBoost算法的人脸检测算法包含的主要工作:(1)通过积分图快速求得Haar特征;(2)利用AdaBoost算法从大量的特征中选择出判别能力较强的少数特征用于人脸检测分类;(3)提出一个级联结构模型,将若干个弱分类器集成一个强分类器,其能够快速排除非人脸区域,…

系统架构设计师-系统工程与信息系统基础(2)

一、电子政务类型 电子政务主要有3类角色&#xff1a;政府&#xff08;Government&#xff09;、企&#xff08;事&#xff09;业单位&#xff08;Business&#xff09;及公民&#xff08;Citizen&#xff09;。如果有第4类就是公务员&#xff08;Employee&#xff09;。 二、企…

CMIP6降尺度方法

气候变化关系到农业、生态系统、社会经济和人类生存与发展&#xff0c;是当今世界关注的重点问题之一。IPCC&#xff08;Intergovernmental Panel on Climate Change&#xff09;第6次评估报告指出&#xff0c;自 20 世纪 50 年代以来&#xff0c;从全球平均气温和海温升高、大…

Unity核心10——导航寻路系统

Unity 中的导航寻路系统是能够让我们在游戏世界当中&#xff0c;让角色能够从一个起点准确的到达另一个终点&#xff0c;并且能够自动避开两个点之间的障碍物选择最近最合理的路径进行前往 ​ Unity 中的导航寻路系统的本质&#xff0c;就是在 A 星寻路算法的基础上进行了拓展和…

国家加强互联网广告监管

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 5月1日《互联网广告管理办法》正式实施。 6月19日&#xff0c;市场监督总局要求加强广告监管。 主要针对直播带货广告、弹窗广告、“软文”广告等新型广告形式&#xff0c;加大互联网广告乱象清理…

HDLBits自学笔记3:Verilog language.Modules Hierarchy

Modules 在顶层模块中实例化模块mod_a&#xff0c;其端口描述&#xff1a; module mod_a ( input in1, input in2, output out ); module top_module ( input a, input b, output out );// 按信号名称连线mod_a u1(.in1(a),.in2(b),.out(out));// 按信号位置连线// mod_a u…

基于数据驱动 U-Net 模型的大气污染物扩散快速预测,提升计算速度近6000倍

项目背景 当前&#xff0c;常见的大气污染预测模型大多是基于物理机理构建的&#xff0c;比如空气质量预测模型 Calpuff、AERMOD、CMAQ 等。然而&#xff0c;这些模型运算较为复杂&#xff0c;对于输入数据的要求非常高&#xff0c;运算耗时也比较长&#xff0c;适合用于常规固…

【玩转Docker小鲸鱼叭】Docker镜像常用命令汇总

镜像是Docker中最为核心也是最具创造性的概念&#xff01;在理解Docker的核心概念文章中&#xff0c;我们理解了镜像的含义&#xff0c;下面让我们一起操作一些镜像&#xff0c;包括拉取、推送镜像等操作。 1、镜像支持哪些命令 通过 docker image help 查看 Docker 支持的镜像…

联想U盘重装系统Win10步骤和详细教程

最近很多用户都在问怎么利用U盘来给联想电脑重装Win10系统&#xff0c;用户需要准备一个8G以上的U盘&#xff0c;确保联想电脑能够正常联网&#xff0c;然后按照小编给大家分享的联想U盘重装系统Win10步骤和详细教程操作&#xff0c;即可快速轻松完成联想电脑Win10系统的重装。…

C++——cin输入

cin对象在抽取过程中会进行类型转换&#xff0c;这应该是根据字节数来的&#xff0c;对于每一种类型&#xff0c;都会有相应的指令合集&#xff0c;也就是cpu会按字节进行转换&#xff0c;区别在于大端模式或者是小端模式&#xff0c;cin对象从在了<<相应的函数被称为格式…