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共用了同一个回调函数

5661

被折叠的 条评论
为什么被折叠?



