STM32单片机入门学习——第38节: [11-3] 软件SPI读写W25Q64

发布于:2025-04-18 ⋅ 阅读:(30) ⋅ 点赞:(0)

写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难,但我还是想去做!

本文写于:2025.04.16

前言

   本次笔记是用来记录我的学习过程,同时把我需要的困难和思考记下来,有助于我的学习,同时也作为一种习惯,可以督促我学习,是一个激励自己的过程,让我们开始32单片机的学习之路。
   欢迎大家给我提意见,能给我的嵌入式之旅提供方向和路线,现在作为小白,我就先学习32单片机了,就跟着B站上的江协科技开始学习了.
   在这里会记录下江协科技32单片机开发板的配套视频教程所作的实验和学习笔记内容,因为我之前有一个开发板,我大概率会用我的板子模仿着来做.让我们一起加油!
   另外为了增强我的学习效果:每次笔记把我不知道或者问题在后面提出来,再下一篇开头作为解答!

开发板说明

   本人采用的是慧净的开发板,因为这个板子是我N年前就买的板子,索性就拿来用了。另外我也购买了江科大的学习套间。
   原理图如下
1、开发板原理图
在这里插入图片描述
2、STM32F103C6和51对比
在这里插入图片描述
3、STM32F103C6核心板
在这里插入图片描述

视频中的都用这个开发板来实现,如果有资源就利用起来。另外也计划实现江协科技的套件。

下图是实物图
在这里插入图片描述

引用

【STM32入门教程-2023版 细致讲解 中文字幕】
还参考了下图中的书籍:
STM32库开发实战指南:基于STM32F103(第2版)
在这里插入图片描述
数据手册
在这里插入图片描述

解答和科普

一、硬件接线

CS片选接在了PA4,DO从机输出接在了PA6,CLK接在了PA5,DI从机输入,接在PA7.
在这里插入图片描述
程序整体框架:先建一个MySPI模块,这个模块主要包含通信引脚封装、初始化,以及SPI通信的3个拼图,起始、终止和交换一个字节,这是SPI通信层的内容,然后基于SPI层,再建一个W25Q64的模块,在这个模块里,调用底层SPI的拼图,来拼接各种指令和功能的完整时序,比如写使能、擦除、页编程、读数据等,这一块可以叫作W25Q64的硬件驱动层,最后在主函数里,调用驱动层的函数,完成想要实现的功能。

二、软件实现SPI

初始化:

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	GPIO_InitTypeDef   GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4| GPIO_Pin_5|GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);	
	
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);

把置引脚高低电平的函数都封装换个名字:

void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);
}

之后还有两个输出引脚,复制一下,

void MySPI_W_SCK(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)BitValue);
}

void MySPI_W_MOSI(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)BitValue);
}
uint8_t MySPI_R_MISO(void)
{
	return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}

当然也可以用宏定义来完成相同的功能。这里用函数包装一下,之后如果要移植到其他类型单片机里,或者要加一个延迟,会更方便一些。这里SPI的速度非常快,所以操作完引脚之后,暂时就不需要加延时了。到这里引脚的封装就完成了。
置一下初始化之后引脚的默认电平, 那在初始化GPIO之后,SS引脚应该置位高电平,

MySPI_W_SS(1);		//默认不选中从机
MySPI_W_SCK(0);		//计划使用SPI模式0,所以默认是低电平
void MySPI_Start(void)
{
	MySPI_W_SS(0);
}

void MySPI_Stop(void)
{
	MySPI_W_SS(1);
}

接下来写SPI的3个时序基本单元:
在这里插入图片描述
这里图上画的是,SS下降沿和数据移出去是同时发生的,包括后面,这个SCK下降沿和数据移出也是用时的,但这并不代表我们程序上要同时执行两条代码,这里实际上是有先后顺序的,是先SS下降沿或SCK下降沿,再数据移出,这个下降沿是触发数据移出这个动作的条件,现有了下降沿,之后才会有数据移出这个动作,对于硬件SPI来说,由于使用了硬件的移位寄存器电路,所以这两个动作几乎是同时发生的,而对于软件SPI来说,由于程序是一条条执行的,不可能同时完成两个动作,所以软件SPI,这就直接躺平,直接看成先后执行的逻辑,流程就是;先SS下降沿,再移出数据,再SCK上升沿,再移入数据,再SCK下降沿,再移出数据,以这个具有先后顺序的流程来进行,这样才能对于一条条依次执行的程序代码。
SS下降沿之后,第一步,主机和从机同时移出数据,就是主机移出我的数据最高位放到MOSI上,从机移出它的数据最高位放到MISO上,当然MISO数据变化,是从机的事,不归我们管,所以这里第一步是写MOSI,然后上升沿,从机会自动把MOSI的数据读走,主机的任务就是把刚才放到MISO上的数据读进来,所以这里要读MISO,接着就是SCK产生下降沿,主机和从机移出下一位,所以这里,之后是写SCK,为0,产生下降沿,下降沿后,主机的任务是,移出B6,

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t  ByteReceive=0x00;
	MySPI_W_MOSI(ByteSend&0x80);
	MySPI_W_SCK(1);
	if(MySPI_R_MISO()==1){ByteReceive|=0x80;}
	MySPI_W_SCK(0);
	MySPI_W_MOSI(ByteSend&0x40);
	
	return ByteReceive;
}

显然要用for循环。

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t  i,ByteReceive=0x00;
	for(i=0;i<8;i++)
	{
	MySPI_W_MOSI(ByteSend&(0x80>>i));
	MySPI_W_SCK(1);
	if(MySPI_R_MISO()==1){ByteReceive|=(0x80>>i);}
	MySPI_W_SCK(0);
	}

	return ByteReceive;
}

通过掩码,依次挑出每一位进行操作,这种不会改变传入变量本身,之后还想用ByteSend,可以继续使用。
第二种方法,是用移位数据本身来进行的操作,好处就是效率高,但是ByteSend这个数据,在移位工程中改变了,for循环执行完,原始传入的参数就没有了。

uint8_t MySPI_SwapByte_1 (uint8_t ByteSend)
{
	uint8_t  i;
	for(i=0;i<8;i++)
	{
	MySPI_W_MOSI(ByteSend&0x80);
	ByteSend<<=1;		//最低位空出来了0
		
	MySPI_W_SCK(1);
	if(MySPI_R_MISO()==1){ByteSend|=0x01;}
	MySPI_W_SCK(0);
	}

	return ByteSend;
}

模式1:

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t  i,ByteReceive=0x00;
	for(i=0;i<8;i++)
	{
	MySPI_W_SCK(1);
	MySPI_W_MOSI(ByteSend&(0x80>>i));
	MySPI_W_SCK(0);
	if(MySPI_R_MISO()==1){ByteReceive|=(0x80>>i);}
	}

	return ByteReceive;
}

模式3:

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t  i,ByteReceive=0x00;
	for(i=0;i<8;i++)
	{
	MySPI_W_SCK(0);
	MySPI_W_MOSI(ByteSend&(0x80>>i));
	MySPI_W_SCK(1);
	if(MySPI_R_MISO()==1){ByteReceive|=(0x80>>i);}
	}

	return ByteReceive;
}

模式2:

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t  i,ByteReceive=0x00;
	for(i=0;i<8;i++)
	{
	MySPI_W_MOSI(ByteSend&(0x80>>i));
	MySPI_W_SCK(0);
	if(MySPI_R_MISO()==1){ByteReceive|=(0x80>>i);}
	MySPI_W_SCK(1);
	}

	return ByteReceive;
}

把这些函数声明者.H文件。

三、W25Q64驱动层

然后在SPI通信基础上建立W25Q64的驱动层。

#include "stm32f10x.h"                  // Device header
#include "MySPI.h"
void W25Q64_Init(void)
{
	MySPI_Init();
}

接下来拼接完整的时序,先读取ID号测试:起始,先交换发送指令9F,随后连续交换接收3个字节,停止,我咋知道那个是交换发送,那个是交换接收呢,那连续接收三个字节,第一个字节是厂商,表示那个厂家生产的,后两个字节是设备ID,其中设备高8位,表示存储器类型,低8位表示容量。
由于计划这个函数是有两个返回值的,所以还是使用指针来实现多返回值。写上两个输出参数。
连续调用连个相同的参数,返回值是不一样的;因为是在通信,通信是有时序的,不同时间调用相同函数,它的意义是不一样的。

void W25Q64_ReadID(uint8_t *MID ,uint16_t *DID)
{
	MySPI_Start();
	MySPI_SwapByte(0x9F);				//读ID号的指令,抛玉引砖,返回值没有意义没用
	*MID = MySPI_SwapByte(0xFF);		//0XFF没有意义,抛砖引玉,就是为了把对面有意义的数据置换过来
	*DID = MySPI_SwapByte(0xFF);		// 读取设备ID高8位
	*DID <<=8;
	*DID |= MySPI_SwapByte(0xFF);		//获得16位的DID
	MySPI_Stop();
}

在这里插入图片描述
指令码单独放一下,然后写指令

void W25Q64_WriteEnable(void)
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_WRITE_ENABLE);
	MySPI_Stop();
}

最好实现一个等待BUSY为0的函数

while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE)& 0x01)==0x01);		//用掩码取出低位

如果BUSY为1,就会进入循环,再读出一次状态寄存器,继续判断,直到BUSY为0,挑出判断,这就是利用连续读出状态寄存器。实现等待BUSY的功能。最后STOP,终止时序。

void W25Q64_WaiteBusy(void)
{
	uint32_t TimeOut;
	
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
	TimeOut=10000;
	while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE)& 0x01)==0x01)		//用掩码取出低位
	{
		TimeOut--;
		if(TimeOut==0)
		{
		break;
		}
	
	}
	MySPI_Stop();
}

页编程的函数:先发一个指令码,再发3个字节的地址,最后发数据,这里的发数据是发送方向的。
在这里插入图片描述

void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t Count)
{
	uint16_t  i;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
	MySPI_SwapByte(Address>>16);			//0x123456 变为 0x12
	MySPI_SwapByte(Address>>8);				//0x123456 变为 0x1234  高位自动舍去就是0x34	
	MySPI_SwapByte(Address);				//0x123456 高位自动舍去就是0x56	
	for(i=0;i<Count;i++)
	{
	MySPI_SwapByte(DataArray[i]);
	}
	MySPI_Stop();
}

擦除:扇区

void W25Q64_SectorErase(uint32_t Address)
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
	MySPI_SwapByte(Address>>16);			//0x123456 变为 0x12
	MySPI_SwapByte(Address>>8);				//0x123456 变为 0x1234  高位自动舍去就是0x34	
	MySPI_SwapByte(Address);
	MySPI_Stop();
}

这里写了dummy你就发个FF就行了。

读出数据
在这里插入图片描述

void W25Q64_ReadData(uint32_t Address,uint8_t *DataArray,uint32_t Count)
{
	uint32_t  i;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_DATA);
	MySPI_SwapByte(Address>>16);			//0x123456 变为 0x12
	MySPI_SwapByte(Address>>8);				//0x123456 变为 0x1234  高位自动舍去就是0x34	
	MySPI_SwapByte(Address);	
	for(i=0;i<Count;i++)
	{
	DataArray[i]=MySPI_SwapByte(W25Q64_DUMMY_BYTE);
	}
	MySPI_Stop();
	
}

等待:事前等待和事后等待,事前等待的话读写操作都能等待。

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"
#include "OLED.h"
#include "W25Q64.h"


uint8_t	MID;
uint16_t DID;
uint8_t  ArrayWrite[]={0x55,0x66,0x77,0x88};
uint8_t  ArrayRead[4];

int main(void)
{
							
	OLED_Init();
	W25Q64_Init();
	
	OLED_ShowString(1,1,"MID:   DID:");
	OLED_ShowString(2,1,"W:");
	OLED_ShowString(3,1,"R:");
	W25Q64_ReadID(&MID,&DID);
	
	OLED_ShowHexNum(1,5 ,MID,2);
	OLED_ShowHexNum(1,12 ,DID,4);

//	W25Q64_SectorErase(0x000000);	//最好对齐起始地址
	W25Q64_PageProgram(0x000000,ArrayWrite,4);
	W25Q64_ReadData(0x000000,ArrayRead,4);

	OLED_ShowHexNum(2,3,ArrayWrite[0],2);
	OLED_ShowHexNum(2,6,ArrayWrite[1],2);
	OLED_ShowHexNum(2,9,ArrayWrite[2],2);
	OLED_ShowHexNum(2,12,ArrayWrite[3],2);
	
	OLED_ShowHexNum(3,3,ArrayRead[0],2);
	OLED_ShowHexNum(3,6,ArrayRead[1],2);
	OLED_ShowHexNum(3,9,ArrayRead[2],2);
	OLED_ShowHexNum(3,12,ArrayRead[3],2);
	
	while(1)
	{
	
		

	}
}

测试
在这里插入图片描述
擦除为FF
在这里插入图片描述
写入ABCD
在这里插入图片描述
不擦除写55 66 77 88
在这里插入图片描述
因为只能1变0.按&
页地址的范围:xxxx00 到xxxxFF,
W25Q64_PageProgram(0x0000FF,ArrayWrite,4);
在这里插入图片描述
在这里插入图片描述
说明写入并没有跨页,这个55写入到地址0FF,后面的66并没有跨页到下一个地址100,而是,返回页首000了,又因为读取时能跨页的,所以这三个FF是第二页的数据,擦除默认是FF,看000是不是写入覆盖了。
在这里插入图片描述
在这里插入图片描述
自己从软件上,分批次写入,先计算,这个数组总共要跨多少页,然后该擦除的擦除,最后再分批次,一页一写,这个操作可以封装成一个函数,之后调用封装的函数就可以跨页连续写入了,这个功能就自己写。

问题

总结

本节课主要学习了软件SPI读写W25Q64,先完成了底层软件SPI,手动翻转电平实现SPI实现。而且还用来函数用来封装引脚电平封装,更容易进行移植,后在SPI的基础上完成了对W25Q64的书写。完成了软件SPI对W25Q64的功能。


网站公告

今日签到

点亮在社区的每一天
去签到