写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难,但我还是想去做!
本文写于: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的功能。