SPIFLASH

SPIflash读写

一.spiflash介绍

FLASH 是常见的用于存储数据的半导体器件,它具有容量大、可重复擦写、按“扇区/块”擦除、掉电后数据可继续保存的特性。常见的 FLASH 主要有 NOR FLASH 和 NAND FLASH 两种类型。
NOR 与 NAND 在数据写入前都需要有擦除操作,但实际上 NOR FLASH 的一个 bit 可以从
1 变成 0,而要从 0 变 1 就要擦除后再写入,NAND FLASH 这两种情况都需要擦除。擦除操作
的最小单位为“扇区/块”,这意味着有时候即使只写一字节的数据,则这个“扇区/块”上之前
的数据都可能会被擦除。

二.实验器件

本次实验使用的是STM32F103ZET6与NM25Q128进行实验。

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

支持SPI模式0和模式3

三.SPI通信

我们对SPI就不进行过多的说明了,主要将一下SPI的具体配置

#include "stm32f1xx_hal.h"
#include "spi.h"

SPI_HandleTypeDef SPI2_struct;

void spi_init()
{
	SPI2_struct.Instance = SPI2;
	SPI2_struct.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;  //SPI2是APB1外设,时钟时PCLK1,最大36MHz
	SPI2_struct.Init.CLKPhase = SPI_PHASE_1EDGE;  //串行同步时钟的第一个跳变沿(上升或下降)数据被采样
	SPI2_struct.Init.CLKPolarity = SPI_POLARITY_LOW;  //串行同步时钟的空闲状态为高电平
	SPI2_struct.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; //硬件CRC校验
	SPI2_struct.Init.CRCPolynomial = 0xff;   
	SPI2_struct.Init.DataSize = SPI_DATASIZE_8BIT;      //数据帧格式
	SPI2_struct.Init.Direction = SPI_DIRECTION_2LINES;      //数据方向,全双工
	SPI2_struct.Init.FirstBit = SPI_FIRSTBIT_MSB;  //高位在前
	SPI2_struct.Init.Mode = SPI_MODE_MASTER;    //工作模式,主模式
	SPI2_struct.Init.NSS = SPI_NSS_SOFT;
	SPI2_struct.Init.TIMode = SPI_TIMODE_DISABLE;
	HAL_SPI_Init(&SPI2_struct);
	
	__HAL_SPI_ENABLE(&SPI2_struct);
	CS(1);
}


void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
	GPIO_InitTypeDef GPIO_Initstruct;
	if(hspi->Instance == SPI2)
	{
		__HAL_RCC_SPI2_CLK_ENABLE();
		__HAL_RCC_GPIOB_CLK_ENABLE();
		
		GPIO_Initstruct.Pin = GPIO_PIN_12;  //CS片选引脚
		GPIO_Initstruct.Mode = GPIO_MODE_OUTPUT_PP;
		GPIO_Initstruct.Speed = GPIO_SPEED_FREQ_HIGH;
		HAL_GPIO_Init(GPIOB,&GPIO_Initstruct);
		
		GPIO_Initstruct.Pin = GPIO_PIN_14;  //MISO引脚
		GPIO_Initstruct.Mode = GPIO_MODE_AF_INPUT;
		GPIO_Initstruct.Pull = GPIO_NOPULL;
		GPIO_Initstruct.Speed = GPIO_SPEED_FREQ_HIGH;
		HAL_GPIO_Init(GPIOB,&GPIO_Initstruct);
		
		GPIO_Initstruct.Pin = GPIO_PIN_15;  //MOSI引脚
		GPIO_Initstruct.Mode = GPIO_MODE_AF_PP;
		GPIO_Initstruct.Speed = GPIO_SPEED_FREQ_HIGH;
		HAL_GPIO_Init(GPIOB,&GPIO_Initstruct);
		
		GPIO_Initstruct.Pin = GPIO_PIN_13;  //SCK引脚
		GPIO_Initstruct.Mode = GPIO_MODE_AF_PP;
		GPIO_Initstruct.Speed = GPIO_SPEED_FREQ_HIGH;
		HAL_GPIO_Init(GPIOB,&GPIO_Initstruct);
		
	}
}

注意此处的spi配置的模式0,spi的CR1状态寄存器的CPOL位为1,CPHA位为0。
在这里插入图片描述

四.NM25Q128的读写操作

NM25Q128的读写操作是建立在SPI通信上的,我们来梳理一下NM25Q128的基本读写流程

首先是读操作,读操只需要先拉低CS片选线,然后向NM25Q128写入读指令,之后在随便发一条数据,目的是给NM25Q128一个时钟信号,此时,NM25Q128就会将你想要读取的数据返回给你。

然后是写操作,对NM25Q128写入之前必须先拉低片选,写使能,再擦除之后才能进行写入。

下面我们就具体来介绍一下NM25Q128的读写操作的流程,以及常用的指令集

在这里插入图片描述

4.1等待芯片繁忙

我们在进行读写操作前,我们先需要判读,芯片是否正处于繁忙状态,我们可以通过读取
在这里插入图片描述
在这里插入图片描述

得知逻辑
1.先拉低CS片选信号
2.发送0x05或者0x35的指令
3.循环发送Flash发送数据(无所谓什么数据,只是为了个25Q128提供时钟,置换出FLash状态寄存器的值,判断状态寄存器的第0位busy是否为0,为0代表空闲,为1代表忙碌)
4.拉高片选信号

void W25Q128_wait_busy()
{
		uint8_t cmd[2],data[2];
	
	cmd[0] = 0x05;
	cmd[1] = 0xFF;
	
	do{
		CS(0);
		HAL_SPI_TransmitReceive(&SPI2_struct,cmd,data,2,1000);
		CS(1);		
	}while((data[1]&0x01)==0x01);
}
4.2写使能

再对25Q128进行写入数据时,我们必须先写使能才可以写入数据
在这里插入图片描述

void W25Q128_Write_ENABLE()
{
	uint8_t cmd,rxdata;
	cmd = 0x06;
	CS(0);
	HAL_SPI_TransmitReceive(&SPI2_struct,&cmd,&rxdata,1,100);
	CS(1);
}
4.3读取指令和读取时序

1.拉低片选信号
2.写入指令0x03
3.写入一个24位的地址
4.写入一个无用数据(一般无用数据都是0xff)给NM25Q128提供时钟,让他返回数据。
5.拉高片选
在这里插入图片描述
从上图可知读数据指令是 03H,可以读出一个字节或者多个字节。发起读操作时,先把 CS
片选管脚拉低,然后通过 MOSI 引脚把 03H 发送芯片,之后再发送要读取的 24 位地址,这些
数据在 CLK 上升沿时采样。芯片接收完 24 位地址之后,就会把相对应地址的数据在 CLK 引脚
下降沿从 MISO 引脚发送出去。从图中可以看出只要 CLK 一直在工作,那么通过一条读指令就
可以把整个芯片存储区的数据读出来。当主机把 CS 引脚拉高,数据传输停止。

void W25Q128_Read_Page(uint8_t *rx_data,uint32_t add,uint16_t len)
{
	uint8_t cmd[2],rxdata;
	cmd[0] = 0x03;
	cmd[1] = 0xff;
	
	uint8_t add_1 = (add&0x00ff0000)>>16;
	uint8_t add_2 = (add&0x0000ff00)>>8;
	uint8_t add_3 = (add&0x000000ff);
	
	W25Q128_Write_ENABLE();
	CS(0);
	HAL_SPI_TransmitReceive(&SPI2_struct,&cmd[0],&rxdata,1,100);
	HAL_SPI_TransmitReceive(&SPI2_struct,&add_1,&rxdata,1,100);
	HAL_SPI_TransmitReceive(&SPI2_struct,&add_2,&rxdata,1,100);
	HAL_SPI_TransmitReceive(&SPI2_struct,&add_3,&rxdata,1,100);
	HAL_SPI_TransmitReceive(&SPI2_struct,&cmd[1],rx_data,len,1000);
	CS(1);
	W25Q128_wait_busy();
}
4.4写指令和擦除指令

擦除指令
逻辑:
1.檫除操作实际上也是写入操作,是往F刊ash中写入0
FF。所以此时也要先开写使能。
2.拉低CS,发送指令20。
3发送三个字节。
4.拉高电平,等待操作完成。
在这里插入图片描述

/擦除一页
void W25Q128_Erase_Page(uint32_t add)
{
	uint8_t cmd[2],rxdata;
	cmd[0] = 0x20;
	
	uint8_t add_1 = (add&0x00ff0000)>>16;
	uint8_t add_2 = (add&0x0000ff00)>>8;
	uint8_t add_3 = (add&0x000000ff);
	
	W25Q128_Write_ENABLE();
	CS(0);
	HAL_SPI_TransmitReceive(&SPI2_struct,&cmd[0],&rxdata,1,100);
	HAL_SPI_TransmitReceive(&SPI2_struct,&add_1,&rxdata,1,100);
	HAL_SPI_TransmitReceive(&SPI2_struct,&add_2,&rxdata,1,100);
	HAL_SPI_TransmitReceive(&SPI2_struct,&add_3,&rxdata,1,100);
	CS(1);
	W25Q128_wait_busy();
}

写一页时序,
顾名思义,就是往FLash中写入一页(256Byte)的操作。最大写入数据不能超过256,超过就会失效
在这里插入图片描述
得出逻辑:
1.由于要进行写操作,所以要先进行写使能。
2.拉低CS,发送02指令。
3.发送三个字节,即24位的地址,每次发8位分3次
发送。为了告知写入的位置。
4.发送数据,8位8位发,最多256。
5.调用FLash等待函数,等待写入完成。
6写入完成后,拉高CS。


void W25Q128_Write_Page(uint8_t *write_data,uint32_t add,uint16_t len)
{
	uint8_t cmd,rxdata;
	cmd = 0x02;
	
	uint8_t add_1 = (add&0x00ff0000)>>16;
	uint8_t add_2 = (add&0x0000ff00)>>8;
	uint8_t add_3 = (add&0x000000ff);
	
	W25Q128_Write_ENABLE();
	W25Q128_wait_busy();
	CS(0);
	HAL_SPI_TransmitReceive(&SPI2_struct,&cmd,&rxdata,1,100);
	HAL_SPI_TransmitReceive(&SPI2_struct,&add_1,&rxdata,1,100);
	HAL_SPI_TransmitReceive(&SPI2_struct,&add_2,&rxdata,1,100);
	HAL_SPI_TransmitReceive(&SPI2_struct,&add_3,&rxdata,1,100);
	HAL_SPI_Transmit(&SPI2_struct,write_data,len,500);

	CS(1);
}

最后要注意的是,写操作如果超过了一页的最大数据量,则会发送回卷,比如你写入了257个数据,多余的一个数据就会回到开头。但是读操作不会

#include "stm32f1xx_hal.h"
#include "rcc.h"
#include "key.h"
#include "uart.h"
#include <string.h>
#include "spi.h"
#include "25Q128.h"
#include <String.h>

uint8_t tx_data[4096];  
uint8_t rx_data[4096];


uint16_t i = 0;
int main()
{
	HAL_Init();
	Rcc_Clock_Init();
	LED_Init();
	usart_init();
	spi_init();
	
	W25Q128_Erase_Page(0);
	for(uint16_t i = 0;i<4096;i++)
	{
		tx_data[i] = 0x20;
	}
	W25Q128_Write_Page(&tx_data,0,1);
	
	W25Q128_Read_Page(rx_data,0,4096);
	for(uint16_t i = 0;i<4096;i++)
	{
		My_printf_DR("rx %d = %x\r\n",i,rx_data[i]);
	}
	
	
	while(1)
	{
		if(state == 1){
			My_printf_DR("%s",U_RX_buff);
			memset(U_RX_buff, 0, sizeof(U_RX_buff));  // Clear buffer for next use
			state = 0;
		}
	}
}

五.DMA搬运spi数据

因为spi是收发同时进行的,所以我们必须配置SPI的DMA发送和SPI的DMA接收

void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
	GPIO_InitTypeDef GPIO_Initstruct;
	if(hspi->Instance == SPI2)
	{
		__HAL_RCC_SPI2_CLK_ENABLE();
		__HAL_RCC_GPIOB_CLK_ENABLE();
		__HAL_RCC_DMA1_CLK_ENABLE();
		
		GPIO_Initstruct.Pin = GPIO_PIN_12;  //CS片选引脚
		GPIO_Initstruct.Mode = GPIO_MODE_OUTPUT_PP;
		GPIO_Initstruct.Speed = GPIO_SPEED_FREQ_HIGH;
		HAL_GPIO_Init(GPIOB,&GPIO_Initstruct);
		
		GPIO_Initstruct.Pin = GPIO_PIN_14;  //MISO引脚
		GPIO_Initstruct.Mode = GPIO_MODE_AF_INPUT;
		GPIO_Initstruct.Pull = GPIO_NOPULL;
		GPIO_Initstruct.Speed = GPIO_SPEED_FREQ_HIGH;
		HAL_GPIO_Init(GPIOB,&GPIO_Initstruct);
		
		GPIO_Initstruct.Pin = GPIO_PIN_15;  //MOSI引脚
		GPIO_Initstruct.Mode = GPIO_MODE_AF_PP;
		GPIO_Initstruct.Speed = GPIO_SPEED_FREQ_HIGH;
		HAL_GPIO_Init(GPIOB,&GPIO_Initstruct);
		
		GPIO_Initstruct.Pin = GPIO_PIN_13;  //SCK引脚
		GPIO_Initstruct.Mode = GPIO_MODE_AF_PP;
		GPIO_Initstruct.Speed = GPIO_SPEED_FREQ_HIGH;
		HAL_GPIO_Init(GPIOB,&GPIO_Initstruct);
		
		dmatx_struct.Instance = DMA1_Channel5;
		dmatx_struct.Init.Direction = DMA_MEMORY_TO_PERIPH;  //DMA方向,内存到外设
		dmatx_struct.Init.MemDataAlignment = DMA_PDATAALIGN_BYTE;        //存储区数据宽度,一个字节
		dmatx_struct.Init.MemInc = DMA_MINC_ENABLE;   //内存地址自增
		dmatx_struct.Init.Mode = DMA_NORMAL;
		dmatx_struct.Init.PeriphDataAlignment = DMA_MDATAALIGN_BYTE;  //外设宽度
		dmatx_struct.Init.PeriphInc = DMA_PINC_DISABLE;
		dmatx_struct.Init.Priority = DMA_PRIORITY_MEDIUM;
		__HAL_LINKDMA(hspi,hdmatx,dmatx_struct);  
		HAL_DMA_Init(&dmatx_struct);
		
		dmarx_struct.Instance = DMA1_Channel4;
		dmarx_struct.Init.Direction = DMA_PERIPH_TO_MEMORY;
		dmarx_struct.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
		dmarx_struct.Init.MemInc = DMA_MINC_ENABLE;
		dmarx_struct.Init.Mode = DMA_NORMAL;
		dmarx_struct.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
		dmarx_struct.Init.PeriphInc = DMA_PINC_DISABLE;
		dmarx_struct.Init.Priority = DMA_PRIORITY_MEDIUM;
		__HAL_LINKDMA(hspi,hdmarx,dmarx_struct);  
		HAL_DMA_Init(&dmarx_struct);
		
		HAL_NVIC_SetPriority(DMA1_Channel4_IRQn,3,0);
		HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
	
		HAL_NVIC_SetPriority(DMA1_Channel5_IRQn,3,0);
		HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn);
	}
}

void DMA1_Channel4_IRQHandler(void)
{
	HAL_DMA_IRQHandler(SPI2_struct.hdmarx);
}
/*-------------------------------------------------*/
/*函数名:DMA通道5中断                             */
/*参  数:无                                       */
/*返回值:无                                       */
/*-------------------------------------------------*/
void DMA1_Channel5_IRQHandler(void)
{
	HAL_DMA_IRQHandler(SPI2_struct.hdmatx);
}


//接收完成回调函数
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
{
	uint32_t i;
	
	if(hspi->Instance == SPI2){
		CS(1);
//		for(i=0;i<4096;i++){
//			("rbuff[%d]=%x\r\n",i,rbuff[i]);
		}
//	}
}

以上是DMA配置,必须要清除中断标志位,否则就会死在中断标志位里


void W25Q128_Read_Page(uint8_t *rx_data,uint32_t add,uint16_t len)
{
	uint8_t cmd[2],rxdata;
	cmd[0] = 0x03;
	cmd[1] = 0xff;
	
	uint8_t add_1 = (add&0x00ff0000)>>16;
	uint8_t add_2 = (add&0x0000ff00)>>8;
	uint8_t add_3 = (add&0x000000ff);
	
	W25Q128_wait_busy();
	CS(0);
	HAL_SPI_TransmitReceive(&SPI2_struct,&cmd[0],&rxdata,1,100);
	HAL_SPI_TransmitReceive(&SPI2_struct,&add_1,&rxdata,1,100);
	HAL_SPI_TransmitReceive(&SPI2_struct,&add_2,&rxdata,1,100);
	HAL_SPI_TransmitReceive(&SPI2_struct,&add_3,&rxdata,1,100);
//	HAL_SPI_TransmitReceive(&SPI2_struct,&cmd[1],rx_data,len,50000);
	HAL_SPI_Receive_DMA(&SPI2_struct,rx_data,len);
}

当我们调用HAL_SPI_Receive_DMA(&SPI2_struct,rx_data,len);这个函数时,只是开启了DMA,跟轮询不一样(HAL_SPI_TransmitReceive()),轮询是直到数据发送完毕,或者超时时才会跳出这个函数。我们进入HAL_SPI_Receive_DMA()这个函数的内部可以看到,首先,先绑定了DMA的半完成回调函数,完成回调函数
在这里插入图片描述
随后打开了SPI的中断
在这里插入图片描述
一次DMA搬运会触发两次中断,第一次时半完成时触发了中断,第二次是完成时触发了中断,在中断函数里我们可以看到HAL_DMA_IRQHandler(SPI2_struct.hdmarx);
在这里插入图片描述在这里插入图片描述
会分别进入半完成和完成的回调函数

最后我们发现,半完成回调函数其实是个SPI共用了同一个回调函数
在这里插入图片描述

### ### SPI Flash 的工作原理 SPI Flash 是一种通过 SPI(Serial Peripheral Interface)接口与主控制器进行数据交互的非易失性存储器,通常用于存储固件、配置数据或代码。其工作原理基于 SPI 协议的串行通信机制,主控制器通过发送指令和地址,控制 Flash 芯片进行读写操作。SPI Flash 内部包含存储阵列、控制逻辑、地址译码器以及数据缓存区,能够支持页写入(Page Program)、扇区擦除(Sector Erase)、整体擦除(Bulk Erase)等操作。SPI Flash 的读写速度通常受限于 SPI 总线的时钟频率,但因其结构简单、成本低、功耗小,广泛应用于嵌入式系统中[^3]。 ### ### SPI Flash 的使用方法 SPI Flash 的使用主要涉及指令发送、地址定位和数据传输三个步骤。主控制器首先通过片选信号(CS)使能 Flash 芯片,随后通过 MOSI(主出从入)引脚发送操作指令和地址信息,再根据操作类型决定是否读取或写入数据。例如,读取操作通常使用“Read Data”指令(0x03),写入操作则需先发送“Write Enable”指令(0x06),再执行“Page Program”指令(0x02)完成数据写入。擦除操作则需发送“Sector Erase”指令(0x20)并指定擦除地址。由于 Flash 写入前必须先擦除,因此需特别注意写入流程的控制[^3]。 ### ### SPI Flash 的驱动开发 在 Linux 系统中,SPI Flash 的驱动开发依赖于 SPI 总线驱动框架。SPI 总线由 `spi_master` 管理,负责与 `spi_device` 通信,而 `spi_driver` 则负责实现对具体设备的操作逻辑。开发过程中,需实现 `spi_driver` 的 `probe` 和 `remove` 函数,注册字符设备或 MTD(Memory Technology Device)设备,实现对 Flash 的读写、擦除等操作。此外,还需处理 Flash 的状态寄存器读取、写保护控制、擦写延时等问题。例如,以下是一个简化的 SPI Flash 驱动初始化代码片段: ```c static int spi_flash_probe(struct spi_device *spi) { struct flash_device *flash; flash = devm_kzalloc(&spi->dev, sizeof(*flash), GFP_KERNEL); if (!flash) return -ENOMEM; spi_set_drvdata(spi, flash); // 初始化 Flash 相关寄存器 flash->spi = spi; flash->mtd.size = FLASH_SIZE; flash->mtd.erasesize = SECTOR_SIZE; flash->mtd.writesize = PAGE_SIZE; // 注册 MTD 设备 return mtd_device_register(&flash->mtd, NULL, 0); } ``` SPI 驱动开发需确保时序控制准确,尤其是写入和擦除操作的等待时间,避免数据损坏或写入失败[^2]。 ### ### SPI Flash 的硬件设计 在硬件设计中,SPI Flash 通常通过四根信号线与主控制器连接:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)和 CS(片选)。部分 Flash 芯片支持 Dual SPI 或 Quad SPI 模式,通过复用 MOSI 和 MISO 实现多线并行传输,提升读写速度。硬件设计需注意以下几点:电源去耦、时钟频率匹配、片选信号拉低有效、防止信号干扰。此外,SPI Flash 的容量和擦写寿命也是选型时的重要考量因素,例如常见的 25QXX 系列 Flash 支持 10 万次擦写周期和 20 年数据保持[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值