HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)
串行外设接口(Serial Peripheral Interface,SPI)是一种传输速率比较高的串行接口,一些ADC芯片、Flash存储器芯片采用SPI接口,MCU通过SPI接口与这些外围器件通信。通过本文讲解了SPI通信,其中涉及了SPI的原理、HAL库的相关驱动函数,其中涉及了SPI轮询、中断、DMA三种方式。然后又通过Flash芯片W25Q128作为示例来讲解SPI通信,讲解了W25Q1
前言
一、SPI 接口和通讯协议
1.1 什么是SPI
串行外设接口(Serial Peripheral Interface,SPI)是一种传输速率比较高的串行接口,一些ADC芯片、Flash存储器芯片采用SPI接口,MCU通过SPI接口与这些外围器件通信。
1.2 SPI 的引脚信息
SPI接口的设置分为主设备(Master)和从设备(Slave),一个主设备可以连接一个或多个从设备。SPI通信的连接方式如图1-1 所示,SPI的主设备也可以称为主机,从设备也可称为从机。
图1-1 SPI通信的连接方式
SP接口有3个基本信号,功能表述如下:
(1)MOSI(Master Output Slave Input),主设备输出/从设备输入信号。MOSI是主设备的串行数据输出,SI是从设备的串行数据输入,主设备和从设备的这两个信号相连。
(2)MISO(Master Iuput Slave Output),主设备输入/从设备输出信号,MI是主设备的串行数据输入,SO是从设备的串行数据输出,主设备和从设备的这两个信号连接。
(3)SCK,串行时钟信号。时钟信号总是由主设备产生。
除了这3个必需的信号,从设备还有一个从设备选择信号CS(Chip Select;也称NSS位,Slave Select),用于选择与主设备进行通信的特定从设备。低电平表示选中从设备,高电平表示未选中。当一个SPI通信网络里有多个SPI从设备时(如图1-1),主设备就通过控制各个从设备的CS信号来保证同一时刻只能一个SPI从设备在线通信,未被选中的SPI从设备的接口引脚是高阻态。SPI主设备可以使用普通的GPIO输出引脚连接从设备的CS引脚,控制从设备的片选信号。
1.3 SPI 的工作原理(了解即可)
在主机和从机都有一个串行移位寄存器,主机通过向它的 SPI 串行寄存器写入一个字节来发起一次传输。串行移位寄存器通过 MOSI 信号线将字节传送给从机,从机也将自己的串行移位寄存器中的内容通过 MISO 信号线返回给主机。这样,两个移位寄存器中的内容就被交换。外设的写操作和读操作是同步完成的。
图1-2 SPI通信原理
图1-3 SPI结构框图
1.4 SPI 传输协议
SPI数据传输是在时钟信号SCK驱动下的串行数据传输,SPI的传输协议定义了SPI通信的起始信号、结束信号、数据有效性时钟同步等环节。SPI每次传输的数据帧长度是8位或16位,一般是最高有效位(Most Significant Bit,MSB)先行。
SPI通信有四种时序,由SPI控制寄存器SPI_CR1中的CPOL位和CPOA位控制。
CPHL(Clock Polarity)时钟极性,控制SCK引脚在空闲状态的电平。如果CPOL为0,则空闲时SCK为低电平;如果CPOL为1,则空闲时SCK为高电平。
CPHA(Clock Phase)时钟相位。如果CPHA为0,则在SCK的第一个边沿对数据采样;如果CPHA为1,则在CSK的第二个边沿对数据采样。
表1-1 SPI的4种时序模式
SPI时序模式 | CPOL时钟极性 | CPHA时钟相位 | 空闲时SCK电平 | 采样时刻 |
---|---|---|---|---|
模式0 | 0 | 0 | 低电平 | 第1个跳变沿 |
模式1 | 0 | 1 | 低电平 | 第2个跳变沿 |
模式2 | 1 | 0 | 高电平 | 第1个跳变沿 |
模式3 | 1 | 1 | 高电平 | 第2个跳变沿 |
图1-4所示的是CPHA为0时的数据传输时序图。NSS位从高变低是数据传输的起始信号,NSS从低变高是数据数据传输的结束信号,图中是MSB先行的方式。
图1-4 CPHA为0时的数据传输时序图
CPHA设置为0表示在SCK的第一个边沿读取数据,读取数据的时刻(捕获选通时刻)就是图1-4中虚线表示的时刻。根据CPOL的取值不同,读取数据的时刻发生在SCK的下跳沿(CPOL为1)时刻或上跳沿(CPOL为0)时刻。MISO、MOSI上的数据是在读取数据的SCK前一个跳变沿时刻发生变化的。
图1-5所示的是CPHA为1时的数据传输时序图。CPHA为1表示在SCK的第2个边沿读取数据。也就是图1-5中虚线表示的时刻。根据CPOL的取值不同,读取数据的时刻发生在SCK上跳沿(CPOL为1)时刻或下跳沿(CPOL为0)时刻。MISO、MOSI上的数据是在读取数据的SCK前一个跳变沿时刻发生的。
图1-5 CPHA为1时的数据传输时序图
在使用SPI接口通信时,主设备和从设备的SPI时序一定要一致,否则无法正常通信,由CPOL和CPOA的不同组合构成了4种SPI时序模式,如表1-1所示。如果使用硬件SPI接口,只需要设置正确的SPI时序模式,底层的通信时序由SPI硬件处理。有时候需要用普通GPIO引脚模拟SPI接口,这称为软件模拟SPI结接口。
1.5 STM32F407的SPI接口
STM32F407新芯片上有3个硬件SPI接口,除了支持SPI通信协议,还支持I2S音频协议。STM32F407的SPI接口有如下的特性。
- 数据帧长度可选择8位或16位。
- 可设置为主模式或从模式。
- SPI支持全双工通信。
- 可设置8种预分频值用于产生通信波频率,波特率最高位为SPI所在APB总线的频率的二分之一。STM32F407上的SPI1在APB2总线上,SPI2和SPI3在APB1总线上。
- 可设置时钟极性(CPOL)和时钟相位(CPHA),也就是4种SPI时序模式都支持。
- 可设置MSB先行(Most Significant Bit,高位先行:首先发送数据最高位,依次发送到最低位)或LSB先行(Least Significant Bit,低位先行:先发送数据的最低位,然后依次发送到最高位)。一般会采用MSR(高位先行)的模式。
- 可以使用硬件CRC效验。
- 可触发中断的主模式故障、上溢和CRC错误标志。
- 发送和接收具有独立的DMA请求,DMA传输具有1字节发送和接收缓冲区。
MCU的SPI接口实现了SPI硬件通信协议,也就是保证数据帧的正确接收和发送,如同UART接口实现底层数据帧的收发一样。SPI主设备和从设备之间具体的通信内容则需要两者之间规定通信协议,如同串口设备之间的通信协议一样。
补充:全双工、单工以及半双工传输方式的理解
(1)全双工(Full Duplex):在全双工模式下,主设备和从设备可以同时进行数据的发送和接收。主设备通过MOSI线发送数据给从设备,并通过MISO线接收从设备返回的数据。这种模式允许双向传输,在同一时钟周期内可以同时进行发送和接收操作,如PI。
(2)半双工(Half Duplex):在半双工模式下,主设备和从设备交替进行数据的发送和接收。通信双方不能同时发送和接收数据,而是通过切换发送和接收模式来实现双向传输。在每次通信中,首先主设备发送数据给从设备,然后切换为接收模式,从设备发送数据给主设备,如UART、I2C、CAN。
(3)单工(Simplex):在单工模式下,数据只能在一个方向上进行传输。通常情况下,SPI总线用于双向通信,因此单工模式在SPI中并不常见。
二、SPI 的HAL库驱动程序
2.1 SPI 寄存器操作的宏函数
SPI的驱动程序头文件是 stm32f4xx_hal_spi.h。SPI寄存器操作的宏函数如表2-1所示,宏函数中的参数_HANDLE_是具体某个SPI接口的对象指针,参数 _ INTERRUPT_ 是SPI的中断事件类型,参数_FLAG_是时间中断标志。
表2-1 SPI寄存器操作的宏函数
函数名 | 功能描述 |
---|---|
__HAL_SPI_DISABLE( __ HANDLE __) | 禁用某个SPI接口 |
__ HAL_SPI_ENABLE(__ HANDLE __) | 启用某个SPI接口 |
__ HAL_SPI_DISABLE_IT ( __ HANDLE__, __ INTERRUPT __ ) ) | 禁用某个中断事件源,不允许事件产生硬件中断 |
__ HAL_SPI_ENABLE_IT(__ HANDLE __, __ INTERRUPT __ ) | 开启某个中断事件源,允许事件产生硬件中断 |
__ HAL_SPI_GET_IT_SOURCE(__ HANDLE __, __ INTERRUPT __ ) | 检查某个中断事件源是否被允许产生硬件中断 |
__ HAL_SPI_GET_FLAG (__ HANDLE__, __ FLAG __ ) | 获取某个事件的中断标志 ,检查事件是否发生 |
__ HAL_SPI_CLEAR_CRCERRFLAG(__ HANDLE __) | 清除CRC校验错误中断标志 |
__ HAL_SPI_CLEAR_FREFLAG( __ HANDLE__) | 清除主模式故障中断标志 |
__ HAL_SPI_CLEAR_MODFFLAG(__ HANDLE __) ) | 清除主模式故障中断标志 |
__ HAL_SPI_CLEAR_OVRFLAG(__ HANDLE__) ) | 清除溢出错误中断标志 |
2.2 SPI 初始化和阻塞器数据传输
SPI 接口初始化、状态查询和阻塞式数据传输的函数列表如表2-2所示。
表2-2 SPI接口初始化和阻塞式数据传输相关函数
函数名 | 功能描述 |
---|---|
HAL_SPI_Init() | SPI初始化,配置SPI接口函数 |
HAL_SPI_MspInit() | SPI的MSP初始化函数,重新实现时一般用于SPI接口引脚GPIO初始化和中断设置 |
HAL_SPI_GetState() | 返回SPI接口当前状态,返回值是枚举类型SPI_HandleTypeDef |
HAL_SPI_GetError() | 返回SPI接口最后的错误码,错误码有一组宏定义 |
HAL_SPI_Transmit() | 阻塞式发送一个缓冲区的数据 |
HAL_SPI_Receive() | 阻塞式接收指定长度的数据保存到缓冲区 |
HAL_SPI_TransmitReceive() | 阻塞式同时发送和接收一定长度的数据 |
2.2.1 SPI 接口初始化
函数 HAL_SPI_Init() 用于具体某个SPI接口的初始化,其原型定义如下:
HAL_StatusTypeDef HAL_SPI_Init(SPI_HandleTypeDef *hspi);
其中,参数hspi是SPI外设对象指针。hspi->Init是SPI_HandleTypeDef 结构体类型,存储了SPI接口的通信参数。
2.2.2 阻塞式数据发送和接收
SPI是一种主/行通信方式,通信完全由SPI主机控制,因为SPI主机控制了时钟信号SCK。SPI主机与从机之间一般是应答式通信,主机先用函数 HAL_SPI_Transmit() 在MOSI线上发送指令或数据,忽略MISO线上传入的数据;从机接收指令或数据后会返回响应数据,主机通过函数 HAL_SPI_Receive() 在MISO线上接收响应数据,接收时不会在MOSI线上发送有效数据。
函数 HAL_SPI_Transmit() 用于发送数据,其原型定义如下:
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
其中,参数hspi是SPI外设对象指针,pData是输出数据缓冲区指针;Size是缓冲区数据的字节数,Timeout是超时等待时间,单位是系统滴答信号节拍数,默认情况下就是ms。函数 HAL_SPI_Transmit() 是阻塞式执行的,也就直到数据发送完成或超时时间后才返回。函数HAL_OK表示发送成功,返回HAL_TIMOUT表示发送超时。
函数 HAL_SPI_Receive() 用于从SPI接口接收数据,其原型定义如下:
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
其中,参数pData是接收数据缓冲区,Size是要接收的字节数,Timeout是超时等待时间。
2.3 中断方式发送数据
SPI接口能以中断的方式传输数据,是非阻塞数据传输。中断方式数据传输的相关函数、产生的中断事件类型、对应的中断回调函数等如表2-3所示。中断事件类型用中断事件使能控制位的宏定义表示。
表2-3 SPI中断方式数据传输相关函数
函数名 | 功能描述 | 产生的中断事件类型 | 对应的回调函数 |
---|---|---|---|
HAL_SPI_Transmit_IT() | 中断方式发送一个缓冲区的数据 | SPI_IT_TXE | HAL_SPI_TxCpltCallback() |
HAL_SPI_Receive_IT() | 中断方式节后指定长度的数据保存到缓冲区 | SPI_IT_RXNE | HAL_SPI_RxCpltCallback() |
HAL_SPI_TransmitReceive_IT() | 中断方式接收指定长度的数据保存到缓冲区 | SPI_IT_TXE和SPI_IT_RXN | HAL_SPI_TxRxCpltCallback() |
前三个中断方式传输函数 | 前3个中断模式传输函数都可能产生SPI_IT_ERR中断事件 | SPI_IT_ERR | HAL_SPI_ErrorCallback() |
HAL_SPI_IRQHandler() | SPI中断ISR里调用的通道处理函数 | —— | —— |
HAL_SPI_Abort() | 取消非阻塞式数据传输,本函数以阻塞模式运行 | —— | —— |
HAL_SPI_Abort_IT() | 取消非阻塞式数据传输,本函数以阻塞模式运行 | —— | HAL_SPI_AbortCpltCallback() |
HAL_SPI_Transmit_IT() 用于发送一个缓冲区的数据,发送完成后,会产生发送完成中断事件(SPI_IT_TXE),对应的回调函数是HAL_SPI_TxCpltCallback() 。
函数HAL_SPI_Receive_IT() 用于接收指定长度的数据保存到缓冲区,接收完成后,会产生接收完成中断事件(SPI_IT_RXNE),对应的回调函数是HAL_SPI_RxCpltCallback() 。
函数HAL_SPI_TransmitReceive_IT() 是发送和接收同时进行,由它启动的数据传输会产生 SPI_IT_TXE 和 SPI_IT_RXN 中断事件,但是有专门的回调函数HAL_SPI_TxRxCpltCallback() 。
上面3个函数的原型定义如下:
HAL_StatusTypeDef HAL_SPI_Transmit_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_Receive_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_TransmitReceive_IT(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData,uint16_t Size);
上面的3个函数都是非阻塞式的,函数返回HAL_OK只是表示函数操作成功,并不表示数据传输完成,只有对应的回调函数被调用才表明数据传输完成 。
函数HAL_SPI_AbortCpltCallback()是SPI中断ISR里调用的通用处理函数,它会根据中断事件类型调用相应的回调函数。在SPI的HAL驱动程序中。回调函数是用SPI外设对象变量的函数指针重定向的,在启动传输的函数里,为回调函数指针赋值,使用时只需要了解表2-3的对应关系即可。
函数HAL_SPI_Abort() 用于取消非阻塞数据传输过程,包括中断方式和DMA方式,这个函数自身以阻塞模式运行。
函数HAL_SPI_Abort_IT() 用于取消非阻塞式数据传输过程,包括中断方式和DMA方式,这个函数自身以中断模式运行,所以有回调函数HAL_SPI_AbortCpltCallback() 。
2.4 DMA方式数据传输
SPI的发送和接收有各自的DMA请求,能以DMA方式进行数据的发送发送和接收。DMA方式传输时需要触发DMA流的中断事件,主要有DMA传输完成中断事件。SPI的DMA方式数据传输的相关函数如表2-4 所示。DMA流的中断事件的宏定义可查。
表2-4 SPI的DMA方式数据传输的系相关函数
DMA方式功能函数 | 函数功能 | DMA流中断事件 | 对应的回调函数 |
---|---|---|---|
HAL_SPI_Transmit_DMA() | DMA方式发送数据 | DMA传输完成 | HAL_SPI_TxCpltCallback() |
HAL_SPI_Transmit_DMA() | DMA方式发送数据 | DMA传输半完成 | HAL_SPI_TxHalfCpltCallback() |
HAL_SPI_Receive_DMA() | DMA方式接收数据 | DMA传输完成 | HAL_SPI_RxCpltCallback() |
HAL_SPI_Receive_DMA() | DMA方式接收数据 | DMA传输半完成 | HAL_SPI_RxHalfCpltCallback() |
HAL_SPI_TransmitReceive_DMA() | DMA方式发送/接收数据 | DMA传输完成 | HAL_SPI_TxRxCpltCallback() |
HAL_SPI_TransmitReceive_DMA() | DMA方式发送/接收数据 | DMA传输半完成 | HAL_SPI_TxRxHalfCpltCallback() |
前3个DMA方式传输函数 | () | DMA传输错误中断事件 | DMA传输错误 |
启动DMA方式发送和接收数据的两个函数的原型分别定义如下:
HAL_StatusTypeDef HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_Receive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
其中,hspi是SPI外设对象指针,pData是用于DMA数据发送或接收的数据缓冲区指针,Size是缓冲区的大小,因为SPI接口传输的基本数据单位是字节,所以缓冲区元素类型是uint8_t,缓冲区大小的单位是字节。
三、Flash存储芯片 W25Q128
为了进一步的了解SPI通信的原理,我们通过一款SPI通信的Flash存储芯片来进行实验。Flash 是常见的用于存储数据的半导体器件, 它具有容量大、可重复擦写、按“扇区/块” 擦除、掉电后数据可继续保存的特性。常见的 Flash 主要有 NOR Flash 和 Nand Flash 两种类型,它们的特性如表3-1所示。 NOR 和 NAND 是两种数字门电路, 可以简单地认为 Flash 内部存储单元使用哪种门作存储单元就是哪类型的 Flash。
表3-1 NOR Flash和Nand Flash对比
特性 | NOR FLASH | NAND FLASH |
---|---|---|
容量 | 较小 | 较大 |
同容量存储器成本 | 较贵 | 较便宜 |
擦除单元 | 以“扇区/块”擦除 | 以“扇区/块”擦除 |
读写单元 | 可以基于字节读写 | 必须以“块”为单位读写 |
读取速度 | 较高 | 较低 |
写入速度 | 较低 | 较高 |
集成度 | 较低 | 较高 |
介质类型 | 随机存储 | 连续存储 |
地址线和数据线 | 独立分开 | 共用 |
坏块 | 较少 | 较多 |
是否支持XIP | 支持 | 不支持 |
应用举例 | 25Qxx、程序ROM | EMMC、SSD、U盘等 |
3.1 硬件接口和连接
W25Q128是一个Flash存储芯片,容量为128Mbit(位),也就是16MB(字节)。W25Q128支持标准SPI,还支持Dual/Qual SPI。若W25Q128工作于Dual/Qual SPI通信模式,需要连接的MCU也支持Dual/Qual SPI通信。具有QUADSPI接口的MCU才支持Dual/Qual SPI通信,如STM32F214、STM32F469等。
STM32F407只有标准SPI接口,不支持Dual/Qual SPI通信通信。开发板上有一个W25Q128芯片,通过标准SPI接口与STM32F407的SPI1接口连接,电路如图3-1。
图3-1 W25Q128电路
W25Q128的各个引脚的功能描述如下
- SO、SI、CLK这3个SPI引脚与MCU的SPI1接口的相应引脚连接,占用PB4、PB5、PB3 引脚。
- 片选信号CS与MCU的PB14连接,由MCU通过GPIO引脚PB14的输出控制W25Q128的片选状态。
- WP是写保护设置引脚,WP为低电平时,禁止修改内部的状态寄存器,与状态寄存器的一些位配合使用,可以对内部的一些存储区域进行写保护。电路中将WP接高电平,也就是不使用此写保护信号。
- HOLD是硬件保持信号引脚,当器件被选中时,如果HOLD输入为低电平,那么DO引脚变为高组态,DI和CLK的输入被忽略。当HOLE引脚输入高电平时,SPI的操作又继续。这里将HOLD引脚接电源,就是不使用保持功能。
W25Q128支持SPI模式0和模式3。在MCU与W25Q128通信时,设置使用SPI1模式3,即设置CPOL = 1,CPOA = 1。
开发板上的W25Q128与STM32F407的SPI1连接,因为SPI1接口要用到PB4、PB5、PB3引脚,而5线的JTAG接口要用到PB3、PB4,所以在使用SPI1接口时,系统的Dubug接口不能设置为JTAG接口,只能设置为SW接口,所以,为避免出现错误,在程序中所有例程都用的SW调试接口。
注:
Flash 也有对应的缺点,我们在使用过程中需要尽量去规避这些问题: 一是 Flash 的使用寿命, 另一个是可能的位反转。使用寿命体现在: 读写上是 FLASH 的擦除次数都是有限的(NOR Flash 普遍是 10 万次左右),当它的使用接近寿命的时候,可能会出现写操作失败。 由于 NAND 通常是整块擦写,块内有一位失效整个块就会失效,这被称为坏块。使用 NAND Flash 最好通过算法扫描介质找出坏块并标记为不可用, 因为坏块上的数据是不准确的。位反转是数据位写入时为 1, 但经过一定时间的环境变化后可能实际变为 0 的情况, 反之亦然。 位反转的原因很多,可能是器件特性也可能与环境、 干扰有关。
3.2 存储空间划分
W25Q128总容量为16MB,使用24位地址线,地址范围为0x000 000 ~0xFFF FFF。
16MB分为256个块(Block),每个块的大小为64KB,16位偏移地址,快内偏移地址范围是0x0000~0XFFFF。
每个块又分为16个扇区(Sector),共4096个扇区,每个扇区的大小为4KB,12位偏移地址,扇区内偏移地址的范围是0x000~0xFFF。
每个扇区又分为16个页(Page),共65536个页,每个页的大小为256个字节,8位偏移地址,页内偏移地址范围是0x00~0xFF。
图3-2 存储空间划分
3.3 数据读写的原则
从W25Q128读取数据时,用户可以从任意地址开始读取任意长度的数据。从W25Q128写数据时,用户可以从任意地址开始写数据,但是一次SPI通信写入的数据范围不能超过一个页的边界。所以,如果从页的起始地址开始写数据,一次最多可写入一个页的数据,即256个字节。如果一次写入的数据超过页的边界,会再从页的起始位置开始写。
向存储区域写入数据时,存储区域必须是被擦除过的,也就是存储内容是0xFF,否则写入的数据操作无效。用户可以对整个器件、某个块、某个扇区进行擦除操作,但是不能对单个页进行擦除。
3.4 操作指令
SPI的硬件层和传输协议只是规定了传输一个数据帧的方法,对于具体的SPI期间的操作由器件规定的操作指令实现。W25Q128制定了很多的操作指令,用以实现各种功能。
W25Q128的操作指令由一字节或多字节组成,指令的第一个字节是指令码,其后跟随的是指令的参数或返回的数据。W25Q128常用的几个指令如表3-1所示,其全部指令和详细解释见W25Q128的数据手册。表3-1 中用括号表示的部分表示返回的数据,A23~A0是24位的全局地址,dummy表示必须发送的无效字节数据,一般发送0x00。
表3-1 W25Q128常用指令
指令名称 | BYTE1指令码 | BYTE2 | BYTE3 | BYTE4 | BYTE5 | BYTE6 |
---|---|---|---|---|---|---|
写使能 | 0x06 | —— | —— | —— | —— | —— |
读状态寄存器1 | 0x05 | (S7~S0) | —— | —— | —— | —— |
读状态寄存器2 | 0x35 | (S15~S8) | —— | —— | —— | —— |
读厂家和设备ID | 0x90 | dummy | dummy | 0x00 | (MF7~MF0) | (ID7~ID0) |
读64位序列号 | 0x4B | dummy | dummy | dummy | dummy | (ID63 ~ ID0) |
器件擦除 | 0xC7/0x60 | —— | —— | —— | —— | —— |
块擦除 | 0xD8 | A23~A16 | A15~A8 | A7~A0 | —— | —— |
扇区擦除 | 0x20 | A23~A16 | A15~A8 | A7~A0 | —— | —— |
写数据(页编程) | 0x02 | A23~A16 | A15~A8 | A7~A0 | D7~D0 | —— |
读数据 | 0x03 | A23~A16 | A15~A8 | A7~A0 | (D7~D0) | —— |
快速读数据 | 0x0B | A23~A16 | A15~A8 | A7~A0 | dummy | (D7~D0) |
下面以几个指令为例,说明指令传输的过程,以及返回数据的读取等原理。
3.4.1 "写使能"指令
“写使能”指令(指令码0x06)只有一个指令码,其传输过程如图3-1所示,一个指令总是从片选信号CS由高变低的跳变开始,片选信号CS由低变高的跳变中结束。
图3-1 单字节“写使能”指令的时序
CS变为低电平后,MCU向W25Q128传输1字节数据0x06,然后结束SPI传输即可。W25Q128接收数据后,根据指令码判断指令类型,并进行相应的处理。“写使能”指令是将状态寄存器1的WEL位设置为1,在擦除芯片、擦除扇区等操作前必须执行“写使能”指令。
无返回数据的指令的操作都有此类似,就是连续将指令码、指令参数发送给W25Q128即可。
3.4.2 “读数据”指令
“读数据”指令(指令码0x03)运用从某个地址开始读取一定个数的字节数据,其中时序如图3-2所示。地址A23~A0是24位全局地址,分为3个字节,在发送指令码0x03后,再发送3个字节的地址数据。然后MCU开始从DO线上读取数据,一次读取一个字节,可以连续读取,W25Q128会自动返回下一个地址的数据。
图3-2 “读数据”指令的时序
3.4.3 “写数据”指令
“ 写数据 ” 指令(指令码0x02)就是数据手册上的“页编程”指令,用于向任意地址写入一定长度的数据。“写数据”指令的时序如图3-3所示,图中是向一个页一次写入256个字节的数据。一个页的容量是256字节,写数据操作操作一次最多写256字节。如果数据长度超过256个字节,会从页的起始位置开始继续写。
图3-3 “写数据”指令的时序
“写数据”指令的起始地址可以是任意地址,数据长度也可以小于256,但如果写的过程中地址超过页的边界,就会从页的起始地址开始继续写。写数据操作的存储单元必须是被擦除过的,也就是内容是0xFF。如果存储单元的内容不是0xFF,那么重新写入数据无效。所以,已经写过的存储区域是不能重复写入的,需要擦除后才能再次写入。
3.5 状态寄存器
W125Q128有3个状态寄存器(status register),用于对器件的一些参数进行配置,或返回器件的当前状态信息。下面是W25Q128的状态寄存器SR1,其各个位的定义见表
表3-2 状态寄存器SR1各个位的定义
位编号 | 位名称 | 功能说明 | 存储特性 | 读/写特性 |
---|---|---|---|---|
S7 | SRP0 | 状态寄存器保护位0 | 非易失 | 可写 |
S6 | SEC | 扇区保护 | 非易失 | 可写 |
S5 | TB | 顶/底保护 | 非易失 | 可写 |
S4 | BP2 | 块保护2 | 非易失 | 可写 |
S3 | BP1 | 快保护1 | 非易失 | 可写 |
S2 | BP0 | 快保护0 | 非易失 | 可写 |
S1 | WEL | 写使能锁存 | 易失 | 可写 |
S0 | BUSY | 有正在进行的擦除或写操作 | 易失 | 只读 |
通过读状态寄存器SR1的指令(指令码0x05),我们可以读取SR1的内容。状态寄存器中的某些位是可写的,是指可以通过写状态寄存器的指令修改这些位的内容;这些位是非易失的,是指修改的内容可永久保存,掉电也不会丢失。
SR1中有2个位在编程中经常用到:WEL位与BUSY位。
写使能锁存(Write Enable Latch,WEL)位是只读的。器件上电后,WEL位是0。只有当WEL位是1时,才能进行擦除芯片、擦除扇区、页编程操作。这些操作执行完后,WEL位自动变为0。只有执行“写操作”指令(指令码0x06)后,WEL位才变1。所以,在进行擦除芯片、擦除扇区、页编程等操作之前,“写使能”指令是必须先执行的。
BUSY位是只读的,表示器件是否处于忙的状态。如果BUSY位是1,表示器件正在执行页编程、扇区擦除、器件擦除等操作。此时,除了“读状态寄存器”指令和“擦除/编程挂起”指令,器件会忽略其他任何指令。当正在执行的页编程、擦除等指令执行完成以后,BUSY位自动变为0,这意味着可以继续执行其他指令了。
四、示例:轮询方式读写W25Q128
4.1 实例功能
开发板上的W25Q128芯片的电路如图1-1所示,与STM32F407的SPI1接口连接,占用PB3、PB4、PB5引脚,W25Q128的片选信号CS与MCU的PB14连接。在本示例中,会根据这个接口电路,为W25Q128编写常用操作的驱动程序,并且测试轮询方式读写W25Q128。示例功能与操作流程如下。
- 使用SPI1接口读写SPI1接口读写Flash存储器W25Q128。
- 使用阻塞模式SPI传输函数编写W25Q128常用功能的驱动程序。
- 通过模拟菜单测试擦除整个芯片、擦除块、写入数据和读取数据等操作。
4.2 CubeMX配置
4.2.1 SPI1的CubeMx设置
SPI的模式和参数设置界面如图4-1所示。SPI的模式设置只有两个参数,用于设置SPI1的工作模式和硬件NSS信号。
图4-1 SPI1的模式和参数设置
(1)Mode,工作模式。有多种工作模式可选:作为主机时,一般选择为Full-Duplex Master(全双工主机);作为从机时,一般选择为Full-Duplex Slave(全双工从机)。所谓全双工(Full-Duplex),是指使用MISO线和MOSI线可以同时接收和发送,相应的还有Half-Duplex(半双工),就是只使用一根数据线,这根数据线既可以发送有可接收,但是需要分时使用发送和接收功能。在本例中,MCU作为主机,并且有MISO和MOSI两根串行信号线,所以选择Full-Duplex。
(2)Hardware NSS Output Singal ,硬件NSS信号。有3种选项,"Disable"选项表示不使用硬件NSS信号,而是使用软件方式控制NSS信号 ;Hardware NSS Intput Signal表示硬件NSS输入信号,Hardware Output Single表示硬件NSS输出信号,SPI主机输出片选信号时选择此选项。本示例用一个单独的GPIO引脚PB14作为主机的片选信号,所以设置为Disable。
SPI1的参数设置为3组,这些参数的设置应该与W25Q128的SPI通信参数对应。W25Q128的SPI通信使用8位数据,MSB先行,支持SPI0和SPI3。
(1)Basic Parameters组,基本参数。
① Frame Format,帧格式。有Motorola和TI两个选项。但只能选Motorla。这个参数对应控制控制寄存器SPI_CR2的FRF位。
"Motorola"选项表示使用Motorola SPI帧格式。在Motorola SPI帧格式中,数据传输是以两个时钟边沿进行的,其中一个时钟边沿用于数据采样,另一个时钟边沿用于数据传输。数据的有效位数可以在配置中指定。
"TI"选项表示使用TI SPI帧格式。在TI SPI帧格式中,数据传输是以一个时钟边沿进行的,数据在该时钟边沿上同时进行采样和传输。数据的有效位数可以在配置中指定。
②Data Size,数据大小。数据帧的位数,可选8位或16位。本示例选择8位。
③First Bit,首先要传的位。可选MSB First(高位先行)或LSB Frist(低位先行)。本示例选择MSB First。
(2)Clock Parameters组,时钟参数。
①Prescaler(for Buaud Rate),用于产生波特率的预分频系数。有8个可选预分频系数,从2到256。SPI的时钟频率就是所在APB总线的时钟频率,SPI1在APB2总线上。最高频率是84MHz。
②Baud Rate,波特率。设置预分频系数后。CubeMx会自动根据APB总线频率和分频系数计算波特率。本示例中的APB2总线频率为84MHz,分频系数为8,所以波特率为10.5 Mbit/s。另外,根据W25Q128的数据手册,读数据指令(0x03)支持的最高频率是33MHz,但是经过测试,如果波特率超过12.5 Mbit/s时,读取数据就会偶尔发生错误,而波特率为5.25Mbit/s(分频系数为16)时传输很稳定。
③Clock Polarity,时钟极性。可选项为High和Low。本示例使用SPI模式3,所以选择High,
④Clock Phase,时钟相位。可选项为1 Edge和 2Edge。本示例使用SPI模式3,即在第2跳变沿采样数据,所以选择2 Edge。
图1-1中的CPOL和CPHA的设置对应于SPI模式3,因为W25Q128同时也支持SPI模式0,所以设置CPOL为Low,CPHA为1Edge也是可以的。
(3) Advance Parameters组,高级参数。
①CRC Calcution,CRC(循环冗余校验)计算。 STM32F407的SPI通信可以在传输数据的最后加上1个字节的CRC计算结果,在发生CRC错误时可以产生中断。若不使用,就选择Disabled.
②NSS Signal Type,NSS信号类型。这个参数的选项是由模式设置里面的Hardware Nss Signal的选择结果决定的。当模式设置里选择Hardware NSS Signal 为Disabe时,这个参数的选项就只能是Sofware,表示用软件产生NSS输出信号,即本例用PB14输出信号作为从机的片选信号。
启用SPI1后,CubeMx将自动将自动分配PA5、PA6、PA7作为SPI1的3个信号引脚,但是从图3-1中可以看出,实际用到的引脚是PB3、PB4、PB5引脚(如图4-2),所以在配置引脚时一定要注意,要按照原理图上的引脚进行配置,或者在Categories配置后一定要检查一遍引脚是都正确。
图4-2 SPI1的GPIO引脚配置
本示例使用SPI的阻塞式数据传输方式,不使用SPI的中断,多以无需开启全局中断。
4.2.2 其余GPIO引脚的配置
该工程通过串口1将W25Q128的操作结果打印出来,串口1具体配置如图4-3所示,需要注意的是,串口工具的配置需要和CubeMx里面的USART配置保持一致。
图4-3 串口配置
GPIO引脚包括LED灯引脚配置(PF9、PF10),其余引脚包括按键、蜂鸣器配置和项目《HAL库STM32常用外设教程(二)—— GPIO输入\输出》里面的引脚配置相同(有源码提供),如图4-4所示,此处不再赘述。
图4-4 GPIO配置
4.3 程序设计
在CubeMx里面生成keil的代码以后,在keil里面打开项目,代码如下。
4.3.1 SPI1初始化
配置好CubeMx后,生成工程并打开,在spi.c的文件里是关于SPI外设的一些基础配置,其中定义了SPI1的初始化函数MX_SPI1_Init(),相关代码及其对应的STM32CubeMx里面的配置如下:
上述程序定义了一个SPI_HandleTypeDef 结构体类型变量hspi1,这是表示SPI1的外设对象变量。函数MX_SPI1_Init()设置了hspi1各成员变量的值,其代码与CubeMx的配置是对应的。程序中的注释说明了每个成员变量的意义。
HAL_SPI_MspInit()是SPI的 MSP函数(“MCU Support Package”,微控制器支持包),在函数MX_SPI1_Init()里被调用,其主要作用是开启SPI1的时钟,并对其3个复用引脚进行GPIO设置。程序中的注释说明了每个成员变量的意义。
4.3.3 W25Q128的驱动程序
为方便对W25Q128进行操作,我们将W25Q128常用的一些功能编写为函数,也就是实现3.4节介绍的W25Q128常用操作指令。例如擦除芯片、擦除扇区、读取数据、写入数据等,这就是W25Q128的驱动程序。
注意W25Q128驱动程序与SPI接口的HAL库驱动程序有区别。SPI的HAL驱动程序实现了SPI接口数据传输的基本功能,是SPI硬件层的驱动;而W25Q128驱动程序是根据W25Q128的指令定义,实现器件具体功能操作的一系列函数。W25Q128驱动程序要用到SPI硬件层的HAL驱动程序,要通过SPI的HAL驱动程序实现数据帧的收发。
我们在项目里创建一个名为User的子目录,创建文件w25flash.c和w25flash.h,驱动程序文件w25flash.c和w25flash.h是根据W25Q128的数据手册编写的,并将其存放在这个子目录里。将子目录User添加到项目的头文件和源文件搜索路径。具体的w25flash.c和w25flash.h在文章末尾的网盘文件里有提供。
4.3 W25Q128的功能描述
4.3.1 主程序
下面我们使用W25Q128的驱动程序。
(1)在项目中添加代码,对W25Q128进行功能测试。添加完w25flash文件,如图4-5所示。
(2)在主程序中添加用户代码,完成后文件main.c的代码如下:
程序在完成外设初始化之后,调用Flash_TestReadStatus()读取Flash芯片的器件ID、状态寄存器SR1和SR2的值,并进行打印,在进入while循环之前,打印出了如下的一个菜单,菜单内如如下:
在while循环里面,程序检测按键输入,对于4个按键分别进行响应,实现下面的功能:
- KEY0键按下时,调用函数Flash_EraseChip()擦除整个器件,擦除操作大约需要30s,不要经常才出整个扇区。
- KEY2键按下时,调用Flash_EraseBlock64K()擦除Block0 ,测试写入数据之前应该先擦除。
- KEY_UP键按下时,调用函数Flash_TestWrite() 从Page0和Page1写入数据。
- KEY1键按下时,调用函数Flash_TestRead()从Page0和Page1读取数据。
函数Flash_EraseChip()和Flash_EraseBlock64K()是驱动文件w25flash.h中定义的函数。
函数Flash_TestReadStatus()、Flash_TestWrite() 、Flash_TestRead()是在文件spi.h中定义的测试函数,在后续的步骤中就。
Flash_TestReadStatus(); /* 读取器件ID并分析芯片类型 */
KEYS curKey = ScanPressedKey(KEY_WAIT_ALWAYS); /* 获取按下的是哪一个按键 */
switch(curKey)
{
case KEY0:
LED0_Toggle(); /* 翻转LED0 */
Flash_EraseChip(); /* 擦除整个器件,大约需要25时间 */
printf("Chip is erased\n");
break;
case KEY2:
LED1_Toggle(); /* 翻转LED1 */
uint32_t globalAddr = 0;
Flash_EraseBlock64K(globalAddr);/* 擦除块 */
printf("Block0 is erased\n");
break;
case KEY_UP:
LED0_Toggle(); /* 翻转LED0 */
LED1_Toggle(); /* 翻转LED1 */
Flash_TestWrite(); /* Flash写操作 */
break;
case KEY1:
// Buzzer_Toggle(); /* 蜂鸣器 */
Flash_TestRead(); /* Flash读操作 */
break;
default:
break;
}
4.3.1 W25Q128功能测试函数的实现
接下来我们利用w25flash.c里面的的W25Q128驱动函数 在spi.c里面实现上一小节中提到的三个函数,即在W25Q128驱动函数上再封装一层来实现我们想要的功能:
(1)函数Flash_TestReadStatus()就是调用W25Q128驱动程序中的3个函数,分别读取器件ID、状态寄存器SR1和SR2。
函数Flash_ReadID()返回厂家和产品ID,这在器件的手册上可以查到,如W25Q128的ID是0xEF17,如果其他芯片可继续在case里面进行添加,
代码如下(示例):
/*
* @brief 读取器件ID、状态寄存器SR1和SR2
* @param
*/
void Flash_TestReadStatus(void)
{
uint16_t devID = Flash_ReadID(); /* 读取器件ID */
uint8_t tempStrDevID[50]; /* 用来接字符串 */
sprintf((char*)tempStrDevID,"Device ID = 0x%04X",devID); /* 将一个设备ID(devID)格式化为字符串,并存储在 tempStrDevID 变量中 */
printf("%s\n",tempStrDevID); /* 打印tempStrDevID */
switch(devID){ /* 判读flash的型号 */
case 0xEF17:
printf("The chip is W25q128\n");
break;
default:
printf("The chip is Unknow\n");
break;
}
/* 读寄存器SR1的内容 */
uint8_t tempStrSR1[50];
sprintf((char*)tempStrSR1,"Status Reg1 = 0x%x",Flash_ReadSR1()); /* 将Flash_ReadSR1函数的返回值直接进行拼接 */
printf("%s\n",tempStrSR1);
/* 读寄存器SR2的内容 */
uint8_t tempStrSR2[50];
sprintf((char*)tempStrSR2,"Status Reg1 = 0x%x",Flash_ReadSR2()); /* 将Flash_ReadSR1函数的返回值直接进行拼接 */
printf("%s\n",tempStrSR2);
}
(2)函数Flash_TestWrite()的功能是在Page0和Page1里写入数据,写入数据的存储空间必须是被擦除过的,在Page0里面写入的是两个字符串,分别在Page0的起始位置以及偏移100的位置,对一个页是可以分多次写入的,只要写入的存储的存储单元是被擦除过的。对于Page1则从页的起始地址写入了256字节的数据,写入的内容等于偏移地址的大小,即0~255。
/*
* @brief 写flash测试
* @param
*/
void Flash_TestWrite(void)
{
uint8_t BlockNo = 0; /* 表示块号 */
uint8_t SubSectorNo = 0; /* 表示扇区 */
uint8_t SubPageNo = 0; /* 表示页号 */
uint32_t memAddress = 0; /* 存储地址 */
memAddress = Flash_Addr_byBlockSectorPage( BlockNo,SubSectorNo,SubPageNo);/* 计算内存地址,并将结果存储在memAddress变量中 */
uint8_t bufStr1[30] = "Hello from brginning";
Flash_WriteInPage(memAddress,bufStr1,strlen("Hello from brginning")+1); /* 在指定的内存地址写入bufStr1(第0页的起始地址) */
printf("Write in page 0 = %s\n",bufStr1);
uint8_t bufStr2[30] = "Hello in page";
Flash_WriteInPage(memAddress + 100,bufStr2,strlen("Hello from brginning")+1); /* 上一个位置偏移100个位置写入字符bufStr2 */
printf("Write in page 100 = %s\n",bufStr2);
uint8_t bufPage[FLASH_PAGE_SIZE];
for(uint16_t i = 0;i < FLASH_PAGE_SIZE;++i){ /* 使用for循环将0 到 FLASH_PAGE_SIZE-1填充到bufPage数组 */
bufPage[i] = i;
}
SubPageNo = 1; /* 再到第1页 */
memAddress = Flash_Addr_byBlockSectorPage( BlockNo,SubSectorNo,SubPageNo);/* 计算第一页的地址 */
Flash_WriteInPage(memAddress ,bufPage,FLASH_PAGE_SIZE); /* 将填充好的 bufPage 数据写入到内存地址中 */
printf("Write 0~255 in page1\n ");
}
(3)函数Flash_TestRead()的功能是从Page0和Page1里读取数据 ,即从页的起始地址、偏移12的地址、偏移136的地址、偏移210的地址读出Flash_TestWrite()函数里写入的数值,可以测试读出的内容和写入的是否一致。
/*
* @brief 读flash测试
* @param
*/
void Flash_TestRead(void)
{
uint8_t BlockNo = 0; /* 表示块号 */
uint8_t SubSectorNo = 0; /* 表示扇区 */
uint8_t SubPageNo = 0; /* 表示页号 */
uint32_t memAddress = Flash_Addr_byBlockSectorPage( BlockNo,SubSectorNo,SubPageNo);
uint8_t bufStr[50];
Flash_ReadBytes(memAddress,bufStr,50); /* 读50字节 */
printf("Read in page 0 = %s\n",bufStr); /* 将50个字符串打印出来 */
Flash_ReadBytes(memAddress + 100,bufStr,50); /* 读50字节 */
printf("Read in page 100 = %s\n",bufStr); /* 将50个字符串打印出来 */
SubPageNo = 1;
memAddress = Flash_Addr_byBlockSectorPage( BlockNo,SubSectorNo,SubPageNo);/* 计算第一页的地址 */
uint8_t randData12 = Flash_ReadOneByte(memAddress + 12); /* 读取地址中偏移量为12的字节数据,并将其存储在randData12变量中 */
uint8_t randData136 = Flash_ReadOneByte(memAddress + 136); /* 读取地址中偏移量为136的字节数据,并将其存储在randData136变量中 */
uint8_t randData210= Flash_ReadOneByte(memAddress + 210); /* 读取地址中偏移量为210的字节数据,并将其存储在randData210变量中 */
char tempStrRandData[100]=""; /* 使用前先清空 */
sprintf((char*)tempStrRandData,"Page1[12] = %d,Page1[136] = %d,Page1[210] = %d",randData12,randData136,randData210);
printf("%s\n",tempStrRandData);
}
4.4 示例结果
将完成的程序下载到开发板上,如图5-1所示按下相应的按键,就会得到图5-2串口得到的结果,其中打印多次是因为按键没有进行消抖操作。
五、总结
通过本文讲解了SPI通信,其中涉及了SPI的原理、HAL库的相关驱动函数,其中涉及了SPI轮询、中断、DMA三种方式。然后又通过Flash芯片W25Q128作为示例来讲解SPI通信,讲解了W25Q128的部分指令,轮询方式读写W25Q128,其中涉及的SPI的CubeMx配置应当熟悉掌握。
项目源码:HAL库STM32常用外设教程(九)—— SPI (读写W25Q128)
参考书籍:
《STM32Cube高效开发教程(基础篇)》王维波
《STM32F4xx中文参考手册》
《STM32F407 探索者开发指南》
人最宝贵的东西是生命,生命对人来说只有一次.因此,人的一生应当这样度过:当一个人回首往事时,不因虚度年华而悔恨,也不因碌碌无为而羞愧;这样,在他临死的时候,能够说,我把整个生命和全部精力都献给了人生最宝贵的事业——为人类的解放而奋斗。
——《钢铁是怎样练成的》
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)