W25Q16 read and write operations based on STM32 (spi)


foreword

We learned about flash memory before, which is more for internal data storage, and the capacity will be relatively small. This time let's learn more about the storage unit w25q16, and learn about spi——serial peripheral interface by the way.

1. W25Q16

1 Introduction

There is basically such a chip on our core boards, but some of them have a larger capacity. You can check the schematic diagram of the board, as shown in the figure: some boards may
insert image description here
be w25q32 or others, and the numbers behind them represent the capacity. , the usage is the same, from the picture or the product manual we can see that the chip communicates with the spi protocol.

2.SPI

2.1. Introduction

The Serial Peripheral Interface (SPI) enables half-duplex/full-duplex synchronous serial communication with external devices. The interface can be configured in master mode, in which case it provides the communication clock (SCK) to an external slave device. The interface is also capable of working in a multi-master configuration.

It can be used for a variety of purposes, including simplex synchronous transmission over two wires, one of which can be used as a bidirectional data line, or reliable communication using CRC checks.

2.2. Features

● 基于三条线的全双工同步传输
● 基于双线的单工同步传输,其中一条可作为双向数据线
● 8 位或 16 位传输帧格式选择
● 主模式或从模式操作
● 多主模式功能
● 8 个主模式波特率预分频器(最大值为 fPCLK/2)
● 从模式频率(最大值为 fPCLK/2)
● 对于主模式和从模式都可实现更快的通信
● 对于主模式和从模式都可通过硬件或软件进行 NSS 管理:动态切换主/从操作
● 可编程的时钟极性和相位
● 可编程的数据顺序,最先移位 MSB 或 LSB
● 可触发中断的专用发送和接收标志
● SPI 总线忙状态标志
● SPI TI 模式
● 用于确保可靠通信的硬件 CRC 功能:
— 在发送模式下可将 CRC 值作为最后一个字节发送
— 根据收到的最后一个字节自动进行 CRC 错误校验
● 可触发中断的主模式故障、上溢和 CRC 错误标志
● 具有 DMA 功能的 1 字节发送和接收缓冲器:发送和接收请求

2.3. Function Description

insert image description here
Generally, SPI is connected to external devices through 4 pins:

● MISO:主输入/从输出数据。此引脚可用于在从模式下发送数据和在主模式下接收数据。
● MOSI:主输出/从输入数据。此引脚可用于在主模式下发送数据和在从模式下接收数据。
● SCK:用于 SPI 主器件的串行时钟输出以及 SPI 从器件的串行时钟输入。
● NSS:从器件选择。这是用于选择从器件的可选引脚。

2.4. Working mode

The SPI bus has four working modes, the most widely used ones are mode 0 and mode 3.

CPOL (Clock Polarity): Clock polarity selection, when it is 0, when the SPI bus is idle, the clock line is low; when it is 1, when the SPI bus is idle, the clock line is high.

CPHA (Clock Phase): Clock phase selection, when it is 0, at the first transition edge of SCLK, the host samples the MISO pin level; when it is 1, at the second SCLK transition edge, the host samples the MISO pin level sampling.
insert image description here

insert image description here

2.5. Pin description

insert image description here

2. Code development

1. SPI initialization

In the schematic diagram, we can see that PB3, 4, 5, and 14 pins are directly used on the board, and then we initialize.

void w25qxx_init(void)
{
    
    
	//PB硬件时钟使能	
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
	
	//配置PB3 PB5 PB14为输出模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_5|GPIO_Pin_14;		//第3 4 5根引脚
	GPIO_InitStructure.GPIO_Mode= GPIO_Mode_OUT;	//输出模式
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;	//推挽输出,增加输出电流能力。
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//高速响应
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;	//没有使能上下拉电阻

	GPIO_Init(GPIOB,&GPIO_InitStructure);	
	
	//看时序图,工作在模式3,时钟线引脚PB3,初始电平为高电平
	PBout(3)=1;
	
	//看时序图,片选引脚PB14,初始电平为高电平	
	PBout(14)=1;	
	
	
	//PB4配置输入模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;		//第4根引脚
	GPIO_InitStructure.GPIO_Mode= GPIO_Mode_IN;		//输入模式
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;	//推挽输出,增加输出电流能力。
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//高速响应
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;	//没有使能上下拉电阻
	GPIO_Init(GPIOB,&GPIO_InitStructure);		
	
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //全双工
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;						//主机模式
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;				//8位数据位
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;						//SPI FLASH可以设置为高电平
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;					//MISO在第二边沿采样数据
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;						//片选引脚由代码控制
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; //clk=84MHZ/8=10.5MHz
	
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;		//以最高有效位发送
	
	
	SPI_Init(SPI1, &SPI_InitStructure);
	
	SPI_Cmd(SPI1, ENABLE);
	
}

2. Read the manufacturer ID

We need to do this according to the product manual. We can see such a picture (as shown below), which may seem a bit complicated. I don’t know where to start. We need to take it slowly.
insert image description here
We stitch together the upper and lower pictures, which is actually one picture, as shown below:

insert image description here
Here it can be divided into 7 parts.

2.1. Read and write bytes

insert image description here
Here we are using mode 3. It can be seen from the figure that it needs to be written from the high bit first, 8 bits each time, and the reading is the same.

uint8_t spi1_send_byte(uint8_t byte)
{
    
    
	
	int32_t i=0;
	uint8_t d=0;
	
	
	for(i=7; i>=0; i--)
	{
    
    
		//对byte每个bit进行判断
		if(byte & (1<<i))
		{
    
    
			//MOSI引脚输出高电平
			PBout(5)=1;
		}
		else
		{
    
    
		
			//MOSI引脚输出低电平
			PBout(5)=0;		
		
		}
		
		//时钟线输出低电平
		PBout(3)=0;
		
		//延时一会,MOSI引脚已经发送到对方
		delay_us(1);
		
	
		//时钟线输出高电平
		PBout(3)=1;
		
		//延时一会
		delay_us(1);	
	
		//读取MISO引脚电平
		if(PBin(4))
			d|=1<<i;
	
	}
	
	
	return d;
}

2.2. Read ID

According to the description of the sequence diagram, we write the code

void w25qxx_read_id(uint8_t *m_id,uint8_t *d_id)
{
    
    
	//片选引脚输出低电平
	PBout(14)=0;

	//发送90h命令
	spi1_send_byte(0x90);

	//发送24bit地址,该数值全为0
	spi1_send_byte(0x00);
	spi1_send_byte(0x00);
	spi1_send_byte(0x00);	
	
	//传递任意参数,读取厂商id
	*m_id=spi1_send_byte(0xFF);
	
	//传递任意参数,读取设备id
	*d_id=spi1_send_byte(0xFF);	
	
	//片选引脚输出高电平
	PBout(14)=1;	
}

3. Some other operations

insert image description here
There are many different instruction operations in the product manual. I won’t introduce them one by one here, but they are all similar to the operation of reading the ID above. If you are interested, you can write the code according to the product manual. The complete code will be given below, including some common ones. operate.

4. Complete code

#include "stm32f4xx.h"
#include "sys.h"
#include <stdio.h>


static GPIO_InitTypeDef 		GPIO_InitStructure;
static NVIC_InitTypeDef 		NVIC_InitStructure;
static USART_InitTypeDef 		USART_InitStructure;
static SPI_InitTypeDef  		SPI_InitStructure;

#pragma import(__use_no_semihosting_swi)
struct __FILE {
    
     int handle; /* Add whatever you need here */ };
FILE __stdout;
FILE __stdin;

int fputc(int ch, FILE *f) {
    
    
	
	USART_SendData(USART1,ch);
	while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
	USART_ClearFlag(USART1,USART_FLAG_TXE);
	
	
	return ch;
}
void _sys_exit(int return_code) {
    
    

}

void delay_us(uint32_t n)
{
    
    
	SysTick->CTRL = 0; 			// Disable SysTick,关闭系统定时器
	SysTick->LOAD = (168*n)-1; // 配置计数值(168*n)-1 ~ 0
	SysTick->VAL  = 0; 		// Clear current value as well as count flag
	SysTick->CTRL = 5; 		// Enable SysTick timer with processor clock
	while ((SysTick->CTRL & 0x10000)==0);// Wait until count flag is set
	SysTick->CTRL = 0; 		// Disable SysTick	
}

void delay_ms(uint32_t n)
{
    
    
	while(n--)
	{
    
    
		SysTick->CTRL = 0; 				// Disable SysTick,关闭系统定时器
		SysTick->LOAD = (168000)-1; 	// 配置计数值(168000)-1 ~ 0
		SysTick->VAL  = 0; 		// Clear current value as well as count flag
		SysTick->CTRL = 5; 		// Enable SysTick timer with processor clock
		while ((SysTick->CTRL & 0x10000)==0);// Wait until count flag is set
	}
	
	SysTick->CTRL = 0; 		// Disable SysTick	

}

void usart1_init(uint32_t baud)
{
    
    
	
	//打开PA硬件时钟	
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
	
	

	//打开串口1硬件时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

	//配置PA9和PA10为复用功能模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9|GPIO_Pin_10;		//第9 10根引脚
	GPIO_InitStructure.GPIO_Mode= GPIO_Mode_AF;	//多功能模式
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;	//推挽输出,增加输出电流能力。
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//高速响应
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;	//没有使能上下拉电阻

	GPIO_Init(GPIOA,&GPIO_InitStructure);


	//将PA9和PA10引脚连接到串口1的硬件
	GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1);
	GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1);	
	
	
	
	//配置串口1相关参数:波特率、无校验位、8位数据位、1个停止位......
	USART_InitStructure.USART_BaudRate = baud;										//波特率
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;						//8位数据位
	USART_InitStructure.USART_StopBits = USART_StopBits_1;							//1个停止位
	USART_InitStructure.USART_Parity = USART_Parity_No;								//无奇偶校验
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;	//无硬件流控制
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;					//允许收发数据
	USART_Init(USART1, &USART_InitStructure);
	
	
	//配置串口1的中断触发方法:接收一个字节触发中断
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
	
	
	//配置串口1的中断优先级
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
	
	
	//使能串口1工作
	USART_Cmd(USART1, ENABLE);
}

void w25qxx_init(void)
{
    
    
	//PB硬件时钟使能	
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
	
	//配置PB3 PB5 PB14为输出模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_5|GPIO_Pin_14;		//第3 4 5根引脚
	GPIO_InitStructure.GPIO_Mode= GPIO_Mode_OUT;	//输出模式
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;	//推挽输出,增加输出电流能力。
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//高速响应
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;	//没有使能上下拉电阻

	GPIO_Init(GPIOB,&GPIO_InitStructure);	
	
	//看时序图,工作在模式3,时钟线引脚PB3,初始电平为高电平
	PBout(3)=1;
	
	//看时序图,片选引脚PB14,初始电平为高电平	
	PBout(14)=1;	
	
	
	//PB4配置输入模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;		//第4根引脚
	GPIO_InitStructure.GPIO_Mode= GPIO_Mode_IN;		//输入模式
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;	//推挽输出,增加输出电流能力。
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//高速响应
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;	//没有使能上下拉电阻
	GPIO_Init(GPIOB,&GPIO_InitStructure);		
	
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //全双工
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;						//主机模式
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;				//8位数据位
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;						//SPI FLASH可以设置为高电平
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;					//MISO在第二边沿采样数据
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;						//片选引脚由代码控制
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; //clk=84MHZ/8=10.5MHz
	
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;		//以最高有效位发送
	
	
	SPI_Init(SPI1, &SPI_InitStructure);
	
	SPI_Cmd(SPI1, ENABLE);
	
}


uint8_t spi1_send_byte(uint8_t byte)
{
    
    
	
	int32_t i=0;
	uint8_t d=0;
	
	
	for(i=7; i>=0; i--)
	{
    
    
		//对byte每个bit进行判断
		if(byte & (1<<i))
		{
    
    
			//MOSI引脚输出高电平
			PBout(5)=1;
		}
		else
		{
    
    
		
			//MOSI引脚输出低电平
			PBout(5)=0;		
		
		}
		
		//时钟线输出低电平
		PBout(3)=0;
		
		//延时一会,MOSI引脚已经发送到对方
		delay_us(1);
		
	
		//时钟线输出高电平
		PBout(3)=1;
		
		//延时一会
		delay_us(1);	
	
		//读取MISO引脚电平
		if(PBin(4))
			d|=1<<i;
	
	}
	
	
	return d;
}



//读取w25qxxState Regist(状态寄存器)值
uint8_t w25qxx_read_SR()
{
    
    
	uint8_t RS1 = 0;


	//片选引脚输出低电平
	PBout(14)=0;

	//发送90h命令
	spi1_send_byte(0x05);
	
	//接收指定字节数据
	RS1=spi1_send_byte(0xFF);

	//片选引脚输出高电平
	PBout(14)=1;
	
	return RS1;
}



//w25qxx读取数据
void w25qxx_read_data(uint8_t *data,uint32_t dataaddr,uint32_t size)
{
    
    
	int8_t i=0;
	
	//片选引脚输出低电平
	PBout(14)=0;

	//发送90h命令
	spi1_send_byte(0x03);

	//发送24bit地址,该数值全为0
	//例如发送地址:0x123456
	spi1_send_byte((uint8_t)(dataaddr>>16));
	spi1_send_byte((uint8_t)(dataaddr>>8));
	spi1_send_byte((uint8_t)dataaddr);	
	
	
	//接收指定字节数据
	for(i=0;i<size;i++)
		//传递任意参数,读取数据
		data[i]=spi1_send_byte(0xFF);
	
	
	//片选引脚输出高电平
	PBout(14)=1;	
}

//w25qxx写使能
void w25qxx_write_enable()
{
    
    
	//片选引脚输出低电平
	PBout(14)=0;

	//发送90h命令
	spi1_send_byte(0x06);
	

	//片选引脚输出高电平
	PBout(14)=1;
		
}

//w25qxx写使不能
void w25qxx_write_disable()
{
    
    
	//片选引脚输出低电平
	PBout(14)=0;

	//发送90h命令
	spi1_send_byte(0x04);
	

	//片选引脚输出高电平
	PBout(14)=1;


}


//扇区擦除
void w25qxx_erase_sector(uint32_t sectoraddr)
{
    
    

	w25qxx_write_enable();
	
	PBout(14) = 0;
	
	//等待BUSY位清空
	//while(w25qxx_read_SR()&0x01 == 0x01);
	
	//发送扇区擦除命令
	spi1_send_byte(0x20);

	//发送24bit地址,该数值全为0
	spi1_send_byte((uint8_t)(sectoraddr>>16));
	spi1_send_byte((uint8_t)(sectoraddr>>8));
	spi1_send_byte((uint8_t)sectoraddr);


	PBout(14) = 1;	
	
	//等待BUSY位清空
	while(w25qxx_read_SR()&0x01 == 0x01);
	


}


void w25qxx_page_write(uint8_t *data,uint32_t dataaddr,uint16_t size)
{
    
    
	uint16_t i=0;
	
	//写使能
	w25qxx_write_enable();
	
	PBout(14) = 0;
	
	//发送页存储指令
	spi1_send_byte(0x02);
	
	//发送存储地址
	spi1_send_byte((uint8_t)(dataaddr>>16));
	spi1_send_byte((uint8_t)(dataaddr>>8));
	spi1_send_byte(dataaddr);
	
	//循环发送数据
	for(i=0;i<size;i++)
		spi1_send_byte(data[i]);
	

	PBout(14) = 1;
	
	//等待BUSY位清空
	while(w25qxx_read_SR()&0x01 == 0x01);	
	
	//添加写保护
	w25qxx_write_disable();
	
}

void w25qxx_read_id(uint8_t *m_id,uint8_t *d_id)
{
    
    
	//片选引脚输出低电平
	PBout(14)=0;

	//发送90h命令
	spi1_send_byte(0x90);

	//发送24bit地址,该数值全为0
	spi1_send_byte(0x00);
	spi1_send_byte(0x00);
	spi1_send_byte(0x00);	
	
	//传递任意参数,读取厂商id
	*m_id=spi1_send_byte(0xFF);
	
	//传递任意参数,读取设备id
	*d_id=spi1_send_byte(0xFF);	
	
	//片选引脚输出高电平
	PBout(14)=1;	
}


int main(void)
{
    
    
	int32_t i=0;
	uint8_t m_id,d_id;
	uint8_t data[256] = {
    
    0};
	
	//使能(打开)端口F的硬件时钟,就是对端口F供电
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);
	
	//串口1波特率:115200bps
	usart1_init(115200);	

	//初始化GPIO引脚
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;		//第9根引脚
	GPIO_InitStructure.GPIO_Mode= GPIO_Mode_OUT;	//输出模式
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;	//推挽输出,增加输出电流能力。
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//高速响应
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;	//没有使能上下拉电阻

	GPIO_Init(GPIOF,&GPIO_InitStructure);
	
	PFout(9)=1;
	
	//初始化SPI1连接的w25qxx
	w25qxx_init();
	w25qxx_read_id(&m_id,&d_id);
	
	
	printf("m_id=%x,d_id=%x\r\n",m_id,d_id);
	
	printf("read data at addr 0:\r\n");
	w25qxx_read_data(data,0,64);
	for(i=0;i<64;i++)
		printf("%02X ",data[i]);
	
	//扇区擦除
	printf("\r\nerase sector 0:\r\n");	
	w25qxx_erase_sector(0);
	
	printf("read data at addr 0:\r\n");	
	w25qxx_read_data(data,0,64);
	for(i=0;i<64;i++)
		printf("%02X ",data[i]);
	
	
	
	//页写入
	printf("write data at addr 0:\r\n");		
	for(i=0;i<256;i++)
		data[i]=0x88;
	w25qxx_page_write(data,0,256);
	
	
	printf("read data at addr 0:\r\n");	
	w25qxx_read_data(data,0,256);
	for(i=0;i<256;i++)
		printf("%02X ",data[i]);
		
	
	

		
	printf("\r\n");
	
	while(1)
	{
    
    
		
		
	}
}



void USART1_IRQHandler(void)
{
    
    
	uint8_t d;
	
	//检测标志位
	if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
	{
    
    
		//接收数据
		d=USART_ReceiveData(USART1);
		
		
	
		//清空标志位
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);
	}

}

3. Effect demonstration

Here we still use the serial port to debug to see the effect we made, as shown in the figure below:
insert image description here

Guess you like

Origin blog.csdn.net/weixin_46155589/article/details/128377305