网站如何加入百度网盟,vi设计公司哪里,网站建设的技术路线,wordpress 编辑器设置参考#xff1a; 1.正点原子 前言#xff1a; SPI一般用在中高速的外围器件上#xff0c;如FLASH, GPS模块等。很常用的一种通信方式#xff0c;学习总结很有必要。 1.SPI的概念及时序。 2.通过SPI操作Flash芯片。
37.1 SPI 及 NOR Flash 介绍
37.1.1 SPI 介绍
我们将从…参考 1.正点原子 前言 SPI一般用在中高速的外围器件上如FLASH, GPS模块等。很常用的一种通信方式学习总结很有必要。 1.SPI的概念及时序。 2.通过SPI操作Flash芯片。
37.1 SPI 及 NOR Flash 介绍
37.1.1 SPI 介绍
我们将从结构、时序和寄存器三个部分来介绍 SPI。
37.1.1.1 SPI 框图
SPI 是英语 Serial Peripheral interface 缩写顾名思义就是串行外围设备接口。SPI 通信协议是 Motorola 公司首先在其 MC68HCXX 系列处理器上定义的。SPI 接口是一种高速的全双工同步的通信总线已经广泛应用在众多 MCU、存储芯片、AD 转换器和 LCD 之间。大部分 STM32有 3 个 SPI 接口本实验使用的是 SPI1。 我们先看 SPI 的结构框图了解它的大致功能如图 37.1.1.1.1 所示。
围绕框图我们展开介绍一下 SPI 的引脚信息、工作原理以及传输方式把 SPI 的 4 种工作方式放在后面讲解。 SPI 的引脚信息 MISOMaster In / Slave Out主设备数据输入从设备数据输出。 MOSIMaster Out / Slave In主设备数据输出从设备数据输入。 SCLKSerial Clock时钟信号由主设备产生。 CSChip Select从设备片选信号由主设备产生。 SPI 的工作原理在主机和从机都有一个串行移位寄存器主机通过向它的 SPI 串行寄存器写入一个字节来发起一次传输。串行移位寄存器通过 MOSI 信号线将字节传送给从机从机也将自己的串行移位寄存器中的内容通过 MISO 信号线返回给主机。这样两个移位寄存器中的内容就被交换。外设的写操作和读操作是同步完成的。如果只是进行写操作主机只需忽略接收到的字节。反之若主机要读取从机的一个字节就必须发送一个空字节引发从机传输。 SPI 的传输方式SPI 总线具有三种传输方式全双工、单工以及半双工传输方式。 全双工通信在任何时刻主机与从机之间都可以同时进行数据的发送和接收。 单工通信在同一时刻只有一个传输的方向发送或者是接收。 半双工通信在同一时刻只能为一个方向传输数据。
37.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。下面请看表 37.1.1.2.1 SPI 工作模式表。
下面分别对 SPI 的 4 种工作模式进行分析
我们分析一下 CPOL 0CPHA 0 的时序图 37.1.1.2.1 就是串行时钟的奇数边沿上升沿采样的情况首先由于配置了 CPOL 0可以看到当数据未发送或者发送完毕SCL 的状态是低电平再者 CPHA 0 即是奇数边沿采集。所以传输的数据会在奇数边沿上升沿被采集MOSI 和 MISO 数据的有效信号需要在 SCK 奇数边沿保持稳定且被采样在非采样时刻MOSI和 MISO 的有效信号才发生变化。 现在分析一下 CPOL 0 CPHA 1 的时序图 37.1.1.2.2 是串行时钟的偶数边沿下降沿采样的情况。由于 CPOL 0所以 SCL 的空闲状态依然是低电平CPHA 1 数据就从偶数边沿采样至于是上升沿还是下降沿从上图就可以知道是下降沿。这里有一个误区空闲状态是低电平的情况下不是应该上升沿吗为什么这里是下降沿首先我们先明确这里是偶数边沿采样那么看图就很清晰SCL 低电平空闲状态下上升沿是在奇数边沿上下降沿是在偶数边沿上。 图 37.1.1.2.3 这种情况和第一种情况相似只是这里是 CPOL 1即 SCL 空闲状态为高电平在 CPHA 0奇数边沿采样的情况下数据在奇数边沿下降沿要保持稳定并等待采样。 图 37.1.1.2.4 是 CPOL 1CPHA 1 的情形可以看到未发送数据和发送数据完毕SCL的状态是高电平奇数边沿的边沿极性是下降沿偶数边沿的边沿极性是上升沿。因为 CPHA 1,所以数据在偶数边沿上升沿被采样。在奇数边沿的时候 MOSI 和 MISO 会发生变化在偶数边沿时候是稳定的。
37.1.1.3 SPI 寄存器
在这里我们简单介绍一下本实验用到的寄存器。
⚫ SPI 控制寄存器 1SPI_CR1 SPI 控制寄存器 1 描述如图 37.1.1.3.1 所示
该寄存器控制着 SPI 很多相关信息包括主设备模式选择传输方向数据格式时钟极性、时钟相位和使能等。下面讲解一下本实验配置的位 在位 CPHA 置 1数据采样从第二个时钟边沿开始 在位 CPOL 置 0在空闲状态时SCK 保持低电平 在位 MSTR 置 1配置为主设备 在位 BR[2:0]置 7使用 256 分频速度最低 在位 SPE 置 1开启 SPI 设备 在位LSBFIRST 置 0MSB 先传输 在位 SSI 置 1禁止软件从设备即做主机 在位 SSM 置 1软件片选 NSS 控制 在位 RXONLY 置 0传输方式采用的是全双工模式 在位 DFF 置 0使用 8 位数据帧格式。
⚫ SPI 状态寄存器SPI_SR SPI 状态寄存器描述如图 37.1.1.3.2 所示
该寄存器是查询当前 SPI 的状态的我们在实验中用到的是 TXE 位和 RXNE 位即发送完成和接收完成是否的标记。
⚫ SPI 数据寄存器SPI_DR SPI 数据寄存器描述如图 37.1.3.3 所示
该寄存器是 SPI 数据寄存器是一个双寄存器包括了发送缓存和接收缓存。当向该寄存器写数据的时候SPI 就会自动发送当收到数据的时候也是存在该寄存器内。
37.1.2 NOR Flash 简介
37.1.2.1 Flash 简介
Flash 是常见的用于存储数据的半导体器件它具有容量大、可重复擦写、按“扇区/块”擦除、掉电后数据可继续保存的特性。常见的 Flash 主要有 NOR Flash 和 Nand Flash 两种类型它们的特性如表 37.1.2.1.1 所示。NOR 和 NAND 是两种数字门电路可以简单地认为 Flash 内部存储单元使用哪种门作存储单元就是哪类型的 Flash。U 盘SSDeMMC 等为 NAND 型而 NOR Flash 则根据设计需要灵活应用于各类 PCB 上如 BIOS手机等。
NOR 与 NAND 在数据写入前都需要有擦除操作但实际上 NOR Flash 的一个 bit 可以从 1变成 0而要从 0 变 1 就要擦除后再写入NAND Flash 这两种情况都需要擦除。擦除操作的最小单位为“扇区/块”这意味着有时候即使只写一字节的数据则这个“扇区/块”上之前的数据都可能会被擦除。 NOR 的地址线和数据线分开它可以按“字节”读写数据符合 CPU 的指令译码执行要求所以假如 NOR 上存储了代码指令CPU 给 NOR 一个地址NOR 就能向 CPU 返回一个数据让 CPU 执行中间不需要额外的处理操作这体现于表 37.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 才能很好的操作。 16M 16 * 1024K 256(Block) * 64K ----Block 256*16(Sector) * 4K ----Sector 256 * 16 * 16Page * 256 Byte ----Page NM25Q128 的擦写周期多达 10W 次具有 20 年的数据保存期限支持电压为 2.7~3.6VNM25Q128 支持标准的 SPI还支持双输出/四输出的 SPI最大 SPI 时钟可以到 80Mhz双输出时相当于 160Mhz四输出时相当于 320M。 下面我们看一下 NM25Q128 芯片的管脚图如图 37.1.2.1.2 所示。
芯片引脚连接如下 CS 即片选信号输入低电平有效 DO 是 MISO 引脚在 CLK 管脚的下降沿输出数据 WP 是写保护管脚高电平可读可写低电平仅仅可读 DI 是 MOSI 引脚主机发送的数据、地址和命令从 SI 引脚输入到芯片内部在 CLK 管脚的上升沿捕获数据 CLK是串行时钟引脚为输入输出提供时钟脉冲 HOLD 是保持管脚低电平有效。 STM32F407 通过 SPI 总线连接到 NM25Q128 对应的引脚即可启动数据传输。
37.1.2.2 NOR FLASH 工作时序
前面对于 NM25Q128 的介绍中也提及其存储的体系NM25Q128 有写入、读取还有擦除的功能下面就对这三种操作的时序进行分析在后面通过代码的形式驱动它。
下面先让我们看一下读操作时序如图 37.1.2.1 所示
从上图可知读数据指令是 03H可以读出一个字节或者多个字节。发起读操作时先把 CS片选管脚拉低然后通过 MOSI 引脚把 03H 发送芯片之后再发送要读取的 24 位地址这些数据在 CLK 上升沿时采样。芯片接收完 24 位地址之后就会把相对应地址的数据在 CLK 引脚下降沿从 MISO 引脚发送出去。从图中可以看出只要 CLK 一直在工作那么通过一条读指令就可以把整个芯片存储区的数据读出来。当主机把 CS 引脚拉高数据传输停止。
接着我们看一下写时序这里我们先看页写时序如图 37.1.2.2.2 所示
在发送页写指令之前需要先发送“写使能”指令.主机先拉低 CS 引脚然后通过 MOSI引脚把 02H 发送到芯片接着发送 24 位地址最后你就可以发送你需要写的字节数据到芯片。完成数据写入之后需要拉高 CS 引脚停止数据传输。
下面介绍一下扇区擦除时序如图 37.1.2.2.3 所示
扇区擦除指的是将一个扇区擦除通过前面的介绍也知道NM25Q128 的扇区大小是 4K字节。擦除扇区后扇区的位全置 1即扇区字节为 FFh。同样的在执行扇区擦除之前需要先执行写使能指令。这里需要注意的是当前 SPI 总线的状态假如总线状态是 BUSY那么这个扇区擦除是无效的所以在拉低 CS 引脚准备发送数据前需要先要确定 SPI 总线的状态这就需要执行读状态寄存器指令读取状态寄存器的 BUSY 位需要等待 BUSY 位为 0才可以执行擦除工作。 接着按时序图分析主机先拉低 CS 引脚然后通过 MOSI 引脚发送指令代码 20h 到芯片然后接着把 24 位扇区地址发送到芯片然后需要拉高 CS 引脚通过读取寄存器状态等待扇区擦除操作完成。此外还有对整个芯片进行擦除的操作时序比扇区擦除更加简单不用发送 24bit 地址只需要发送指令代码 C7h 到芯片即可实现芯片的擦除。 在 NM25Q128 手册中还有许多种方式的读/写/擦除操作我们这里只分析本实验用到的其他大家可以参考 NM25Q128 手册。
37.2 硬件设计
1. 例程功能 通过串口输入指令读取flash id, 写数据读取数据。 2. 硬件资源 1串口 1(PA9/PA10 连接在板载 USB 转串口芯片 CH340 上面)(USMART 使用) 2SPI1(PB3/PB4/PB5/PB14) 3norflash(本例程使用的是 W25Q128连接在 SPI1 上) 3. 原理图 我们主要来看看 norflash 和开发板的连接如下图所示
通过上图可知NOR FLASH 的 CS、SCK、MISO 和 MOSI 分别连接在 PB14、PB3、PB4和 PB5 上。本实验还支持多种型号的 SPI FLASH 芯片比如BY25Q128/NM25Q128/W25Q128等等具体请看 norflash.h 文件的宏定义在程序上只需要稍微修改一下后面讲解程序的时候会提到。
37.3 程序设计
37.3.1 SPI 的 HAL 库驱动
SPI 在 HAL 库中的驱动代码在 stm32f4xx_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 传输数据的配置步骤 1SPI 参数初始化(工作模式、数据时钟极性、时钟相位等)。 HAL 库通过调用 SPI 初始化函数 HAL_SPI_Init 完成对 SPI 参数初始化详见例程源码。 注意该函数会调用HAL_SPI_MspInit 函数来完成对 SPI 底层的初始化包括SPI 及GPIO 时钟使能、GPIO 模式设置等。 2使能 SPI 时钟和配置相关引脚的复用功能。 本实验用到 SPI1使用 PB3、PB4 和 PB5 作为 SPI_SCK、SPI_MISO 和 SPI_MOSI因此需要先使能 SPI1 和 GPIOB 时钟。参考代码如下 __HAL_RCC_SPI1_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); IO 口复用功能是通过函数 HAL_GPIO_Init 来配置的。 3使能 SPI 通过__HAL_SPI_ENABLE 函数使能 SPI便可进行数据传输。 4SPI 传输数据 通过 HAL_SPI_Transmit 函数进行发送数据。 通过 HAL_SPI_Receive 函数进行接收数据。 也可以通过 HAL_SPI_TransmitReceive 函数进行发送与接收操作。 5设置 SPI 传输速度 SPI 初始化结构体 SPI_InitTypeDef 有一个成员变量是 BaudRatePrescaler该成员变量用来设置 SPI 的预分频系数从而决定了 SPI 的传输速度。但是 HAL 库并没有提供单独的 SPI 分频系数修改函数如果我们需要在程序中偶尔修改速度那么我们就要通过设置 SPI_CR1 寄存器来修改具体实现方法请参考后面软件设计小节相关函数。
37.3.2 程序解析
本实验中我们通过调用 HAL 库的函数去驱动 SPI 进行通信。norflash.c 文件存放 W25Q128/NM25Q128/BY25Q128 驱动。
1. SPI 驱动代码 我们看一下 spi.c 代码中的初始化函数代码如下STM32CubeMX配置自动生成
/**
* brief SPI 初始化代码
* note 主机模式,8 位数据,禁止硬件片选
* param 无
* retval 无
*/
SPI_HandleTypeDef hspi1; /* SPI1 句柄 */
void MX_SPI1_Init(void)
{/* USER CODE BEGIN SPI1_Init 0 *//* USER CODE END SPI1_Init 0 *//* USER CODE BEGIN SPI1_Init 1 *//* USER CODE END SPI1_Init 1 */hspi1.Instance SPI1; /* SPI1 */hspi1.Init.Mode SPI_MODE_MASTER; /* 设置 SPI 工作模式设置为主模式 */hspi1.Init.Direction SPI_DIRECTION_2LINES; /* 设置 SPI 单向或者双向的数据模式:SPI 设置为双线模式 */hspi1.Init.DataSize SPI_DATASIZE_8BIT; /* 设置 SPI 的数据大小:SPI 发送接收 8 位帧结构 */hspi1.Init.CLKPolarity SPI_POLARITY_HIGH; /* 串行同步时钟的空闲状态为高电平 */hspi1.Init.CLKPhase SPI_PHASE_2EDGE; /* 串行同步时钟的第二个跳变沿上升或下降数据被采样 */hspi1.Init.NSS SPI_NSS_SOFT; /* NSS信号由硬件NSS 管脚还是软件使用SSI位管理:内部NSS信号由SSI位控制 */hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_32;/* 定义波特率预分频的值:波特率预分频值为 32 */hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; /* 指定数据传输从 MSB 位还是 LSB 位开始:数据传输从 MSB 位开始 */hspi1.Init.TIMode SPI_TIMODE_DISABLE; /* 关闭 TI 模式 */hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; /* 关闭硬件 CRC 校验 */hspi1.Init.CRCPolynomial 10; /* CRC 值计算的多项式 */if (HAL_SPI_Init(hspi1) ! HAL_OK) /* 初始化 */{Error_Handler();}/* USER CODE BEGIN SPI1_Init 2 *//* USER CODE END SPI1_Init 2 */}在 MX_SPI1_Init 函数中主要工作就是对于 SPI 参数的配置这里包括工作模式、数据模式、数据大小、时钟极性、时钟相位、波特率预分频值等。 关于 SPI 的管脚配置就放在了 HAL_SPI_MspInit函数里其代码如下
/**
* brief SPI 底层驱动时钟使能引脚配置
* note 此函数会被 HAL_SPI_Init()调用
* param hspi : SPI 句柄
* retval 无
*/
void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle)
{GPIO_InitTypeDef GPIO_InitStruct {0};if(spiHandle-InstanceSPI1){/* USER CODE BEGIN SPI1_MspInit 0 *//* USER CODE END SPI1_MspInit 0 *//* SPI1 clock enable */__HAL_RCC_SPI1_CLK_ENABLE();/* SPI1 时钟使能 */__HAL_RCC_GPIOB_CLK_ENABLE();/**SPI1 GPIO ConfigurationPB3 ------ SPI1_SCKPB4 ------ SPI1_MISOPB5 ------ SPI1_MOSI*/GPIO_InitStruct.Pin GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5;GPIO_InitStruct.Mode GPIO_MODE_AF_PP; /* SPI 引脚模式设置(复用输出) */ GPIO_InitStruct.Pull GPIO_NOPULL;GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH;GPIO_InitStruct.Alternate GPIO_AF5_SPI1;HAL_GPIO_Init(GPIOB, GPIO_InitStruct);/* USER CODE BEGIN SPI1_MspInit 1 *//* USER CODE END SPI1_MspInit 1 */}
}通过以上两个函数的作用就可以完成 SPI 初始。接下来介绍 SPI 的发送和接收函数其定义如下
/**
* brief SPI1 读写一个字节数据
* param txdata : 要发送的数据(1 字节)
* retval 接收到的数据(1 字节)
*/
uint8_t spi1_read_write_byte(uint8_t txdata)
{uint8_t rxdata; HAL_SPI_TransmitReceive(g_spi1_handler, txdata, rxdata, 1, 1000);return rxdata; /* 返回收到的数据 */
}这里的 spi_read_write_byte 函数直接调用了 HAL 库内置的函数进行接收发送操作。前面已经有介绍了这里就不展开对 HAL_SPI_TransmitReceive 函数的解析。 由于不同的外设需要的通信速度不一样所以这里我们定义了一个速度设置函数通过操作寄存器的方式去实现其代码如下
/**
* brief SPI1 速度设置函数
* note SPI1 时钟选择来自 APB1, 即 PCLK1, 为 42MHz
* SPI 速度 PCLK1 / 2^(speed 1)
* param speed : SPI1 时钟分频系数取值为 SPI_BAUDRATEPRESCALER_2~SPI_BAUDRATEPRESCALER_2 256
* retval 无
*/
void spi1_set_speed(uint8_t speed)
{assert_param(IS_SPI_BAUDRATE_PRESCALER(speed)); /* 判断有效性 */__HAL_SPI_DISABLE(g_spi1_handler); /* 关闭 SPI */g_spi1_handler.Instance-CR1 0XFFC7; /* 位 3-5 清零用来设置波特率 */g_spi1_handler.Instance-CR1 | speed 3; /* 设置 SPI 速度 */__HAL_SPI_ENABLE(g_spi1_handler); /* 使能 SPI */
}2. norflash 驱动代码 这里我们只讲解核心代码详细的源码请参考光盘本实验对应源码。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脚 时钟使能 */// 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); /* 取消片选 */// spi1_init(); /* 初始化SPI1 */// spi1_set_speed(SPI_SPEED_4); /* SPI1 切换到高速状态 21Mhz */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; /* ADP1, 上电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);
}在初始化函数中主要是读取flash id来判断spi flash通信是否正常若是能读取到id就是正常通信。如果能读到 ID 则说明我们的 SPI 时序能正常操作 Flash便可以通过 SPI 接口读写 NOR FLASH 的数据了。 进行其它数据操作时由于每一次读写操作的时候都需要发送地址所以这里我们把这个板块封装成函数函数名是 norflash_send_address实质上就是通过 SPI 的发送接收函数spi1_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);spi1_read_write_byte(FLASH_ReadData); /* 发送读取命令 */norflash_send_address(addr); /* 发送地址 */for(i0;idatalen;i){pbuf[i] spi1_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 的容量的数据。我们这里简单介绍一下思路先获得首地址WriteAddr所在的扇区并计算在扇区内的偏移然后判断要写入的数据长度是否超过本扇区所剩下的长度如果不超过再先看看是否要擦除如果不要则直接写入数据即可如果要则读出整个扇区在偏移处开始写入指定长度的数据然后擦除这个扇区再一次性写入。当所需要写入的数据长度超过一个扇区的长度的时候我们先按照前面的步骤把扇区剩余部分写完再在新扇区内执行同样的操作如此循环直到写入结束。这里我们还定义了一个 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);spi1_read_write_byte(FLASH_PageProgram); /* 发送写页命令 */norflash_send_address(addr); /* 发送地址 */for (i 0; i datalen; i){spi1_read_write_byte(pbuf[i]); /* 循环写入 */}NORFLASH_CS(1);norflash_wait_busy(); /* 等待写入结束 */
}在页写功能的代码中先发送写使能命令才发送页写命令然后发送写入的地址再把写入的内容通过一个 for 循环写入发送完后拉高片选 CS 引脚结束通信等待 flash 内部写入结束。检测 flash 内部的状态可以通过查看 NM25Qxx 状态寄存器 1 的位 0。在这里科普一下NM25Qxx 的状态寄存器可以通过寄存器相关位判断 NM25Qxx 的状态,下面是 NM25Qxx 状态寄存器表
我们也定义了一个函数 norflash_read_sr去读取 NM25Qxx 状态寄存器的值这里就不列出来了主要实现的方式也是老套路根据传参判断需要获取的是哪个状态寄存器然后拉低片选线调用 spi1_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);spi1_read_write_byte(FLASH_SectorErase); /* 发送写页命令 */norflash_send_address(saddr); /* 发送地址 */NORFLASH_CS(1);norflash_wait_busy(); /* 等待扇区擦除完成 */
}该代码也是老套路通过发送擦除指令实现擦除功能要注意的是使用扇区擦除指令前需要先发送写使能指令拉低片选线发送扇区擦除指令之后发送擦除的扇区地址实现擦除最后拉高片选线结束通信。在函数最后通过读取寄存器状态的函数等待扇区擦除完成。
3. main.c 代码 在 main.c 里面编写如下代码
int main(void)
{/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_DMA_Init();MX_USART1_UART_Init();MX_TIM6_Init();MX_RTC_Init();MX_ADC1_Init();MX_ADC3_Init();MX_SPI1_Init();/* USER CODE BEGIN 2 *//* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){uart_debug_task();XL_TIME6_time_show();/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}37.4 下载验证
将程序下载到开发板后在串口输入指令 “cmd_flash 0” 测试flash多扇区写入和读取数据是否一致。 “cmd_flash 1” 获取flash的ID,可以检查stm32 和 flash是否正常通信。 “cmd_flash 2 9 99” 2表示写flash, 9:在地址9写入数据99向地址9写入数据99.默认写入和读取的是uint32_t 4字节数据 “cmd_flash 3 9” 3表示读flash9读取地址9的数据。默认写入和读取的是uint32_t 4字节数据
37.5 STM32CubeMX
1.spi配置
2.cs片选引脚
37.6 源代码路径
git clone gitgitee.com:xiaoliangliangcong/stm32.git STM32F407ZGT6/14.SPI