STM32实现软件SPI对W25Q64内存芯片实现读写操作

先看看本次实验的成果吧:

这么简单的一个程序,我学习了一个星期左右,终于把所有的关节都打通了。所有代码都能什么都不看背着敲出来了。为了使自己的记忆更为清晰,特意总结了一个思维导图,感觉自己即便是日后忘记了看一遍思维导图也就知道怎么写了。特此展示一下吧!

STM32内部集成了硬件SPI收发电路,

可以由硬件自动执行时钟生成、数据收发等功能,

减轻CPU的负担 可配置8位/16位数据帧、高位先行/低位先行

时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256) 支持多主机模型、主或从操作

可精简为半双工/单工通信 支持DMA 兼容I2S协议

STM32F103C8T6 硬件SPI资源:SPI1、SPI2

这个图应该是从右往左看才对的,我个人感觉是这个样子的,因为我写程序是这个顺序的,下面我就把所有的程序展示一下吧:

根据时序图会更好更快的理解程序的逻辑,一切都是要最后芯片的时序来编程,否则就不能和芯片通讯了。

首先是SPI.c的文件:

cs 复制代码
#include "stm32f10x.h"                  // Device header

#define GPIOX                      GPIOA                  //宏定义GPIOA,需要更改端口时只更改这里就好了
#define RCC_APB2Periph_GPIOX       RCC_APB2Periph_GPIOA   //宏定义时钟开启端口
#define MySPI_CS                   GPIO_Pin_4             //宏定义CS片选信号
#define MySPI_MOSI                 GPIO_Pin_7             //宏定义MOSI主机输出信号
#define MySPI_CLK                  GPIO_Pin_5             //宏定义时钟信号
#define MySPI_MISO                 GPIO_Pin_6             //宏定义主机输入信号

void MySPI_W_CS(uint8_t BitValue)                                   //位操作片选信号
{
	GPIO_WriteBit(GPIOX, MySPI_CS, (BitAction)BitValue);
}

void MySPI_W_MOSI(uint8_t BitValue)                                 //位操作主机输出信号
{
	GPIO_WriteBit(GPIOX, MySPI_MOSI, (BitAction)BitValue);
}

void MySPI_W_CLK(uint8_t BitValue)                                  //位操作时钟信号
{
	GPIO_WriteBit(GPIOX, MySPI_CLK, (BitAction)BitValue);
}

uint8_t MySPI_Read_MISO(void)                                        //读取主机输入信号
{
	return GPIO_ReadInputDataBit(GPIOX, MySPI_MISO);
}


void MySPI_Init(void)                                                     //SPI初始化
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOX, ENABLE);                   //开启时钟
	
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStruct.GPIO_Pin = MySPI_CS | MySPI_MOSI | MySPI_CLK;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOX, &GPIO_InitStruct);                                   //初始化片选,主机输出,时钟三个信号为推挽输出模式
	
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStruct.GPIO_Pin = MySPI_MISO;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOX, &GPIO_InitStruct);                                //初始化主机输入信号为上拉输入模式
	
	MySPI_W_CS(1);                                                   //片选信号高电平,没有选中从机
	MySPI_W_CLK(0);                                                  //时钟低电平
}

void MySPI_Start(void)                              //SPI开始函数
{
	MySPI_W_CS(0);  // 片选信号低电平,选中从机
}

void MySPI_Stop(void)                              //SPI结束函数
{
	MySPI_W_CS(1);  //片选信号高电平,没有选中从机
}

uint8_t MySPI_RW_Byte(uint8_t SendByte)           //SPI读写一个字节函数
{
	uint8_t i;
	for(i = 0; i < 8; i ++)                         //循环8次,一个字节8位,每次操作1位
	{
	MySPI_W_MOSI(SendByte & 0x80);                  // 主机输出信号写:发送数据的最高位
	SendByte <<= 1;                                 // 发送信号右移1位,下次循环到来时还是发送最高位
	MySPI_W_CLK(1);                                 //时钟信号高电平
	if(MySPI_Read_MISO() == 1){SendByte |= 0x01;}   // 如果接收到的从机信号为1:发送数据的最低位就写入1
	MySPI_W_CLK(0);                                 //时钟信号低电平
	}
	return SendByte;     //返回发送的数据:此时经过了八次从最低位往最高位写入读入的位值,此时就是收到的新的一个字节数据。
}

接着是SPI.h的文件:

cs 复制代码
#ifndef __MYSPI_H
#define __MYSPI_H


void MySPI_Init(void);                        //SPI初始化

void MySPI_Start(void);                       //SPI开始

void MySPI_Stop(void);                        //SPI结束

uint8_t MySPI_RW_Byte(uint8_t SendByte);      //SPI读写一个字节


#endif

写入操作时:

写入操作前,必须先进行写使能 每个数据位只能由1改写为0,不能由0改写为1

写入数据前必须先擦除,擦除后,所有数据位变为1 擦除必须按最小擦除单元进行

连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入

写入操作结束后,芯片进入忙状态,不响应新的读写操作

读取操作时:

直接调用读取时序,无需使能,无需额外操作,没有页的限制,

读取操作结束后不会进入忙状态,但不能在忙状态时读取

接着是W25Q64.c的文件:

cs 复制代码
#include "stm32f10x.h"                  // Device header
#include "MySPI.h" 
#include "W25Q64_Int.h" 

void W25Q64_Init(void)                                               //W25Q64初始化
{
	MySPI_Init();                                                               //SPI初始化
}

void W25Q64_Read_ID(uint8_t *MID, uint16_t *DID)                     //W25Q64 读取ID号
{
	MySPI_Start();                                   //SPI开始
	MySPI_RW_Byte(W25Q64_JEDEC_ID);                  //SPI写命令
	*MID = MySPI_RW_Byte(W25Q64_DUMMY_BYTE);         //SPI读取从机发过来的第一个字节就是MID号
	*DID = MySPI_RW_Byte(W25Q64_DUMMY_BYTE);         //SPI读取从机发过来的第二个字节就是DID的高8位
	*DID <<= 8;                                      //上面读到的是高8位所以向右移动8位就是高8位了
	*DID |= MySPI_RW_Byte(W25Q64_DUMMY_BYTE);        //低8位再写入从机发过来的第三个字节
	MySPI_Stop();                                    //SPI停止
}

void W25Q64_WaitBusy(void)                                     //W25Q64 等待忙函数
{
	uint32_t Timeout = 100000;                                          //定义一个超时变量
	MySPI_Start();                                                      //SPI开始
	MySPI_RW_Byte(W25Q64_READ_STATUS_REGISTER_1);                       //读取忙标志位命令
	while((MySPI_RW_Byte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)            //如果读取到的标志位最后1为是1就循环
	{
		Timeout --;                                                         //超时变量自减
		if(Timeout == 0)                                                    //如果超时变量为0了,while循环还没有结束
		{
			MySPI_Stop();                                                   //SPI停止
			break;                                                          //退出while循环(防止在While循环中出不去)
		}
	}
	MySPI_Stop();                                                       //SPI停止
}

void W25Q64_WriteENABLE(void)                                                  //W25Q64写使能
{
	MySPI_Start();                                                                  //SPI开始
	MySPI_RW_Byte(W25Q64_WRITE_ENABLE);                                             //W25Q64写使能命令
	MySPI_Stop();                                                                   //SPI停止
}

void W25Q64_Write_Data(uint32_t Address, uint8_t *DataArry, uint16_t Len)    //W25Q64写数据(参数:32位的地址,8位的数组,要写入数据的长度)
{
	uint16_t i;                                            //定义16位的i,循环用,一页可以写256个字节,8位的画最大255,差一个不够,所以用16位的
	
	W25Q64_WriteENABLE();                                  //W25Q64写使能(W25Q64需要写入数据前的必须操作)
	
	MySPI_Start();                                          //SPI开始
	MySPI_RW_Byte(W25Q64_PAGE_PROGRAM);                     //SPI写入操作页的命令
	MySPI_RW_Byte(Address >> 16);                           //SPI写入24位地址的高8位
	MySPI_RW_Byte(Address >> 8);                            //SPI写入24位地址的中间8位
	MySPI_RW_Byte(Address);                                 //SPI写入24位地址的低8位
	for(i = 0; i < Len; i ++)                               //循环 写入数据的长度 次
	{
		MySPI_RW_Byte(DataArry[i]);                             //SPI写入 写入数组的(从低到高)单个字节     
	}
	MySPI_Stop();                                           //SPI停止
	
	W25Q64_WaitBusy();                                      //W25Q64等待忙完
}

void W25Q64_SectorErase(uint32_t Address)                        // W25Q64擦除数据
{
	W25Q64_WriteENABLE();                                       //W25Q64写使能(W25Q64需要写入数据前的必须操作)
	
	MySPI_Start();                                                    //SPI开始
	MySPI_RW_Byte(W25Q64_SECTOR_ERASE_4KB);                           //SPI写入擦除页的命令
	MySPI_RW_Byte(Address >> 16);                                     //SPI写入24位地址的高8位
	MySPI_RW_Byte(Address >> 8);                                      //SPI写入24位地址的中间8位
	MySPI_RW_Byte(Address);                                           //SPI写入24位地址的低8位
	MySPI_Stop();                                                     //SPI停止
	
	W25Q64_WaitBusy();                                                //W25Q64等待忙完
}

void W25Q64_ReadData(uint32_t Address, uint8_t *DataArry, uint32_t Len)   // W25Q64读取数据
{
	uint32_t i;                                            // 定义读取数据的长度变量(读取不受页的空间限制,读多少地址都会自增) 所以i的容量要大   
	MySPI_Start();                                          //SPI开始    
	MySPI_RW_Byte(W25Q64_READ_DATA);                        //SPI写入读取的命令
	MySPI_RW_Byte(Address >> 16);                           //SPI写入24位地址的高8位
	MySPI_RW_Byte(Address >> 8);                            //SPI写入24位地址的中间8位 
	MySPI_RW_Byte(Address);                                 //SPI写入24位地址的低8位
	for(i = 0; i < Len; i ++)                               //循环  要读取的字节数  次
	{
		DataArry[i] = MySPI_RW_Byte(W25Q64_DUMMY_BYTE);        //数组的第i位赋值为 W25Q64传过来的字节
	}
	MySPI_Stop();                                             //SPI停止 
}                                                             //由于读完自动清除忙标志位,所以读完不用等待忙完

然后是W25Q64.h的文件:

cs 复制代码
#ifndef __W25Q64_H
#define __W25Q64_H

void W25Q64_Init(void);

void W25Q64_Read_ID(uint8_t *MID, uint16_t *DID);

void W25Q64_Write_Data(uint32_t Address, uint8_t *DataArry, uint16_t Len);

void W25Q64_SectorErase(uint32_t Address);

void W25Q64_ReadData(uint32_t Address, uint8_t *DataArry, uint16_t Len);

#endif

最后就是main.c的主函数文件了:

cs 复制代码
#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "W25Q64.h"

uint8_t MID;
uint16_t DID;

uint8_t DataW[] = {0x55, 0x66, 0x77, 0x88};
uint8_t DataR[4];

int main(void)
{
	OLED_Init();       //oled  屏幕初始化
	W25Q64_Init();     //W25Q64初始化
	
	OLED_ShowString(1,1, "MID:   DID:"); //OLED第一行第一列显示字符串
	
	W25Q64_Read_ID(&MID, &DID);           // W25Q64 读ID
	
	OLED_ShowHexNum(1,5, MID, 2);        //OLED显示MID
	OLED_ShowHexNum(1,13, DID, 4);       //OLED显示DID
	
	OLED_ShowString(2,1, "W:");          // OLED显示要写入的数据
	OLED_ShowString(3,1, "R:");          // OLED显示读出的数据
	
	W25Q64_SectorErase(0x000100);          //W25Q64擦除地址为0x000100开始的一页的数据
	W25Q64_Write_Data(0x000100, DataW, 4); //W25Q64从地址为0x000100的地方开始写入数据,共写入4个字节
	W25Q64_ReadData(0x000100, DataR, 4);   //W25Q64从地址为0x000100的地方开始读数据,共读出4个字节的数据
	
	OLED_ShowHexNum(3,3, DataR[0], 2);      //OLED显示读出的数据0
	OLED_ShowHexNum(3,6, DataR[1], 2);      //OLED显示读出的数据1
	OLED_ShowHexNum(3,9, DataR[2], 2);      //OLED显示读出的数据2
	OLED_ShowHexNum(3,12, DataR[3], 2);     //OLED显示读出的数据3
	
	OLED_ShowHexNum(2,3, DataW[0], 2);      //OLED显示要写入的数据0
	OLED_ShowHexNum(2,6, DataW[1], 2);      //OLED显示要写入的数据1
	OLED_ShowHexNum(2,9, DataW[2], 2);      //OLED显示要写入的数据2
	OLED_ShowHexNum(2,12, DataW[3], 2);     //OLED显示要写入的数据3
	
	while(1)
	{

	}
}

还有一个W25Q64芯片的命令文件W25Q64_Int.h:里面放的是写好的命令和值的对应关系都做好了宏定义:

cs 复制代码
#ifndef __W25Q64_INT_H
#define __W25Q64_INT_H

#define W25Q64_WRITE_ENABLE							0x06   //写使能命令值
#define W25Q64_WRITE_DISABLE						0x04   //写失能命令值
#define W25Q64_READ_STATUS_REGISTER_1				0x05   //读芯片忙状态寄存器,最后一位为1,代表忙
#define W25Q64_READ_STATUS_REGISTER_2				0x35
#define W25Q64_WRITE_STATUS_REGISTER				0x01
#define W25Q64_PAGE_PROGRAM							0x02   //页写入命令值
#define W25Q64_QUAD_PAGE_PROGRAM					0x32
#define W25Q64_BLOCK_ERASE_64KB						0xD8
#define W25Q64_BLOCK_ERASE_32KB						0x52
#define W25Q64_SECTOR_ERASE_4KB						0x20   //擦除4KB命令
#define W25Q64_CHIP_ERASE							0xC7
#define W25Q64_ERASE_SUSPEND						0x75
#define W25Q64_ERASE_RESUME							0x7A
#define W25Q64_POWER_DOWN							0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE				0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET			0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID		0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID				0x90
#define W25Q64_READ_UNIQUE_ID						0x4B
#define W25Q64_JEDEC_ID								0x9F    //读ID命令
#define W25Q64_READ_DATA							0x03    //读数据命令
#define W25Q64_FAST_READ							0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT				0x3B
#define W25Q64_FAST_READ_DUAL_IO					0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT				0x6B
#define W25Q64_FAST_READ_QUAD_IO					0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO				0xE3

#define W25Q64_DUMMY_BYTE							0xFF   // 置换一字节的数据命令





#endif

然后根据我的文件架构来布置代码编译后就能实现对W25Q64存储芯片的读写了:

相关推荐
时空自由民.31 分钟前
无人机系统耗电,低功耗管理问题解决方法(chatgpt)
单片机·嵌入式硬件·无人机
时空自由民.36 分钟前
无人机系统耗电,低功耗管理问题解决方法(腾讯元宝)
单片机·嵌入式硬件·无人机
清风6666662 小时前
基于单片机的双档输出数字直流电压源设计
单片机·mongodb·毕业设计·nosql·课程设计
牛马大师兄2 小时前
STM32独立看门狗IWDG与窗口看门狗WWDG知识梳理笔记
笔记·stm32·单片机·嵌入式硬件·嵌入式·看门狗
夜月yeyue2 小时前
STM32 Flash 访问加速器详解(ART Accelerator)
linux·单片机·嵌入式硬件·uboot·bootloard
A9better3 小时前
嵌入式开发学习日志37——stm32之USART
stm32·嵌入式硬件·学习
国科安芯6 小时前
ASP4644芯片低功耗设计思路解析
网络·单片机·嵌入式硬件·安全
充哥单片机设计6 小时前
【STM32项目开源】基于STM32的智能厨房火灾燃气监控
stm32·单片机·嵌入式硬件
CiLerLinux14 小时前
第四十九章 ESP32S3 WiFi 路由实验
网络·人工智能·单片机·嵌入式硬件
时光の尘14 小时前
【PCB电路设计】常见元器件简介(电阻、电容、电感、二极管、三极管以及场效应管)
单片机·嵌入式硬件·pcb·二极管·电感·三极管·场效应管