教你手写SPI与FLASH通讯(看完这篇你就会手动写啦,保姆级讲解)---- 2020.3.13

Meta ·
更新时间:2024-11-13
· 893 次阅读

写这篇文章足足肝了我一天时间!!!,不过还算是有点收获,希望这篇文章能够帮助你!!! 关于SPI协议原理方面的文章

嵌入式stm32 复习(工作用)— SPI协议原理知识 2020.3.12
添加链接描述

先上完整SPI与FLASH通讯部分代码!!!

SPI.c部分

#include "spi2.h" /** * @brief SPI硬件初始化 * @param None * @retval None */ void SPI2_Init(void) { //1.时钟 RCC->APB2ENR |= 1 <APB1ENR |= 1 <CRH &= ~(0x0F <CRH |= 0x0B <CRH &= ~(0x0F <CRH |= 0x08 <CRH &= ~(0x0F <CRH |= 0x0B <CR1 = 0; //3.1 配置波特率 SPI2->CR1 &= ~(0x07 <CR1 &= ~(1 <CR1 &= ~(1 <CR1 &= ~(1 <CR1 &= ~(1 <CR1 |= 1 <CR2 |= 1 <CR1 |= 1 <SR & 0x02)) ; SPI2->DR = data; //2.判断RXNE是否为1 while (!(SPI2->SR & 0x01)) ; return SPI2->DR; }

SPI.h部分

#ifndef INC_SPI2_H_ #define INC_SPI2_H_ #include "ext.h" void SPI2_Init(void); u8 SPI2_WR_Byte(u8 data); #endif /* INC_SPI2_H_ */

w25qxx.c部分

#include "w25qxx.h" #include "spi2.h" #include "stdlib.h" #include "string.h" #define W25QXX_CS PBout(12) //W25QXX片选信号线 /** * @brief Flash 的初始化 * @param None * @retval None */ void W25QXX_Init(void) { //关于库函数的GPIO配置,这里课上不再重复讲解 GPIOB->CRH &= ~(0x0F <CRH |= 0x03 << 16; //PB12(CS/NSS),推免输出 W25QXX_CS = 1; //CS片选拉高 SPI2_Init(); //初始化SPI2 } /** * @brief 测试读取W25QXX的Manufacturer / Device ID * @param None * @retval Manufacturer / Device ID */ u16 W25QXX_GetMDID(void) { u16 tmp = 0; //1.CS拉低,表示开始 W25QXX_CS = 0; //2.数据操作 SPI2_WR_Byte(0x90); //发设备指令 0x90 SPI2_WR_Byte(0x00); //连续发送三次0x00 SPI2_WR_Byte(0x00); SPI2_WR_Byte(0x00); tmp |= SPI2_WR_Byte(0x00) <> 16)); SPI2_WR_Byte((u8) (addr >> 8)); SPI2_WR_Byte((u8) (addr >> 0)); SPI2_WR_Byte(0xFF); //假读 for (i = 0; i > 16) & 0xFF); SPI2_WR_Byte((addr >> 8) & 0xFF); SPI2_WR_Byte((addr >> 0) & 0xFF); W25QXX_CS = 1; //3.CS拉高,表示结束 W25QXX_WaiteBusy(); } /** * @brief W25QXX 擦除Block (32KB) * @param None * @retval None */ void W25QXX_BlockErase32(u32 addr) { W25QXX_WriteEnble(); //必须先发写使能 W25QXX_CS = 0; //1.CS拉低,表示开始 SPI2_WR_Byte(0x52); //Block Erase (32KB) SPI2_WR_Byte((addr >> 16) & 0xFF); SPI2_WR_Byte((addr >> 8) & 0xFF); SPI2_WR_Byte((addr >> 0) & 0xFF); W25QXX_CS = 1; //3.CS拉高,表示结束 W25QXX_WaiteBusy(); } /** * @brief W25QXX Block (64KB) * @param None * @retval None */ void W25QXX_BlockErase64(u32 addr) { W25QXX_WriteEnble(); //必须先发写使能 W25QXX_CS = 0; //1.CS拉低,表示开始 SPI2_WR_Byte(0xD8); //Block Erase (64KB) SPI2_WR_Byte((addr >> 16) & 0xFF); SPI2_WR_Byte((addr >> 8) & 0xFF); SPI2_WR_Byte((addr >> 0) & 0xFF); W25QXX_CS = 1; //3.CS拉高,表示结束 W25QXX_WaiteBusy(); } /** * @brief W25QXX Power-down * @param None * @retval None */ void W25QXX_PowerDown(void) { W25QXX_CS = 0; //1.CS拉低,表示开始 SPI2_WR_Byte(0xB9); //Power-down W25QXX_CS = 1; //3.CS拉高,表示结束 } /** * @brief W25QXX 写Page * @param None * @retval None */ void W25QXX_WritePage(u32 addr, u8* pData, u16 len) { u16 i = 0; W25QXX_WriteEnble(); //先WriteEnable W25QXX_CS = 0; //1.CS拉低,表示开始 SPI2_WR_Byte(0x02); //Page Program 命令 SPI2_WR_Byte((addr >> 16) & 0xFF); SPI2_WR_Byte((addr >> 8) & 0xFF); SPI2_WR_Byte((addr >> 0) & 0xFF); for (i = 0; i = len) last = len; while (1) { W25QXX_WritePage(addr, pBuffer, last); if (last == len) break; else { pBuffer += last; addr += last; len -= last; if (len > 256) last = 256; else last = len; } } } u8 W25QXX_BUF[4096] = { 0 }; //如果频繁访问读写操作,可开辟缓存区,如果外接SRAM,可以使用动态申请内存 /** * @brief W25QXX 写任意数据 * @param addr:数据地址;pData:要写的数据首地址;len:要写数据的有效长度。 * @retval None */ void W25QXX_WriteMutiBytes(u32 addr, u8* pData, u16 len) { u16 offset = addr % 4096; u16 last = 4096 - offset; u16 n = addr / 4096; u8* buffer = W25QXX_BUF; //malloc(4096); if (last >= len) last = len; while (1) { u16 i = 0; //1.先拷贝数据 W25QXX_ReadMutiBytes(n * 4096, buffer, 4096); for (i = 0; i < 4096; ++i) { if (buffer[i] != 0xFF) break; } //如果当有扇区不需要擦除时,写入速度明显提升,可以在进行大容量拷贝时,先将整个Flash擦除后再写入 if (i != 4096) { //2.擦除整个扇区 W25QXX_SectorErase(n * 4096); } //3.替换数据(使用C标准库的速度和下面循环的速度大致差不多) strncpy((char *) (buffer + offset), (const char *) pData, last); //等同于下面步骤 // for (i = 0; i 4096) last = 4096; else last = len; } // free(buffer); }

w25qxx.h部分

#ifndef INC_W25QXX_H_ #define INC_W25QXX_H_ #include "ext.h" void W25QXX_Init(void); u16 W25QXX_GetMDID(void); void W25QXX_ReadMutiBytes(u32 addr, u8* pData, u16 len); void W25QXX_WriteMutiBytes(u32 addr, u8* pData, u16 len); void W25QXX_Write(u8* pBuffer, u32 address, u16 len); void W25QXX_ChipErase(void); #endif /* INC_W25QXX_H_ */ 好!按照老样子,接下来开始详细讲解每行代码的用处,以及为什么这样写!

SPI.c部分
SPI.c初始化部分

//1.时钟 RCC->APB2ENR |= 1 <APB1ENR |= 1 << 14;//SPI2时钟

//在这里插入图片描述//因为这里stm32与flash通讯的接口是GPIOB端口,所以需要开启GPIOB的时钟和SPI2的时钟。
//
在这里插入图片描述
//在这里插入图片描述//
在这里插入图片描述
//由前几篇的IIC协议文章中设置一样,所以这里就不再赘述。

//2.GPIO GPIOB->CRH &= ~(0x0F <CRH |= 0x0B << 20;//PB13(CLK),推免输出,复用

//
在这里插入图片描述
//因为SCK端口是PB13,所以位于端口配置高寄存器。
//由上一篇文章的SPI协议可以得知:SCK是由波特率产生器产生的的,是产生固定时钟周期的。,所以这个端口需要设置成复用推免输出模式,则设置这四位为1011,运算如下:
//但是首先先将这四位清零,再赋值。
在这里插入图片描述

GPIOB->CRH &= ~(0x0F <CRH |= 0x08 << 24;//PB14(MISO),上拉或下拉输入

//由上篇文章可以得知:MISO:只能由从设备向主设备发数据,所以这里将MISO引脚设置成上拉或者下拉输入。
//
在这里插入图片描述

GPIOB->CRH &= ~(0x0F <CRH |= 0x0B << 28;//PB15(MOSI),推免输出,复用

//由上篇文章可以得知:MOSI:只能由主设备向从设备发数据,所以这里将MOSI引脚设置成推免输出,复用。
//
在这里插入图片描述

//3.SPI //3.1 配置波特率 SPI2->CR1 &= ~(0x07 << 3);//选择Fpcl/2时钟频率

//
在这里插入图片描述//
在这里插入图片描述//从上图可以得知,波特率控制是由fPCLK决定的。
//所以我们就得知道fPCLK是多大频率。
//
在这里插入图片描述//所以这里fPCLK为36MHz,所以SPI2的最大波特率是18MHz。这里设置最大,即BR[2:0]这三位全是0,那么设置的时候先全置1,然后再取反。
//
在这里插入图片描述

//3.2 选择时钟极性 SPI2->CR1 &= ~(1 <CR1 &= ~(1 << 0);//选择第一个时钟边沿

//由上一篇文章我们可以知道有两种时钟空闲状态。
//高电平空闲:是从低电平到高电平,发送或者接收数据。
//低电平空闲:是从高电平到低电平,发送或者接收数据。
//那么我们这里采用的是低电平空闲,也就相当于电平从低电平变成高电平时,才会传输数据。
//
在这里插入图片描述//在这里插入图片描述//从图可以得知,数据采样从第二个时钟边沿开始。

//3.3 选择数据传输长度 SPI2->CR1 &= ~(1 << 11);//8bit数据长度

//
在这里插入图片描述//一般我们传输数据时是以8位数据格式进行发送或者接收的,并且是第11位。

//3.4 配置 LSBFIRST 帧格式 SPI2->CR1 &= ~(1 << 7);//MSB开始,高位在前

//在这里插入图片描述
//举个例子,如上图所示,传输数据的时候总是高位在前。
//在这里插入图片描述

//3.5 配置MSTR SPI2->CR1 |= 1 << 2;//配置SPI为主设备

在这里插入图片描述

//3.6 配置硬件管理CS(NSS) SPI2->CR2 |= 1 << 2;

在这里插入图片描述在这里插入图片描述

//4.使能 SPE SPI2->CR1 |= 1 << 6;//使能SPI

//在这里插入图片描述

SPI.c部分
SPI2数据读和写部分

//1.先判断TxE是否为1 while (!(SPI2->SR & 0x02));

//在这里插入图片描述//由上图我们可以知道,只有当TXE标志被置位时,数据才会从发送缓冲区到移位寄存器,所以这里先判断TxE是否为1。
//在这里插入图片描述//在这里插入图片描述//在这里插入图片描述

SPI2->DR = data;

//当TXE置1时,才开始发数据。
//在这里插入图片描述

//2.判断RXNE是否为1 while (!(SPI2->SR & 0x01));

//在这里插入图片描述// 传送移位寄存器里的数据到接收缓冲器,并且RXNE标志被置位,也就相当于RXNE位为1时,传送移位寄存器的数据才能到接收寄存器中。
//在这里插入图片描述
//
在这里插入图片描述//所以这里就相当于主设备向从设备发数据的过程中,主设备同时也在接收从设备发过来的数据,还记不记得我们在关于IIC协议文章讲到的IIC协议通讯是半双工通信,发送或者接收数据只能单方向,但是SPI协议就不是半双工通讯方式了,而是全双工通讯,又因为IIC传输数据只在一根线上传输,但是SPI传输数据是在两条线上传输的,所以这也是为什么SPI传输数据的速度比IIC传输数据快的原因了!!!

w25qxx.c部分
w25qxx(Flash)初始化部分

GPIOB->CRH &= ~(0x0F <CRH |= 0x03 << 16; //PB12(CS/NSS),推免输出 W25QXX_CS = 1; //CS片选拉高 SPI2_Init(); //初始化SPI2

//在这里插入图片描述//因为在每次通讯的时候,刚开始CS片选信号线电平状态总是从高电平变到低电平,所以这也是我们为什么设置CS片选信号不是在SPI初始化里面设置,而是在SPI初始化之前进行设置的,并且将CS片选信号线电平状态置1。
//设置CS片选信号的时候也是先清零然后再赋值。

w25qxx.c部分
w25qxx( 测试读取W25QXX的Manufacturer / Device ID)部分

#define W25QXX_CS PBout(12) //W25QXX片选信号线

//因为已知CS片选信号线是PB12,并且是输出模式,这个操作相当于与IIC协议中SCL,SDA信号线的电平状态设置相同,也是采用宏定义的方式。

u16 tmp = 0; //1.CS拉低,表示开始 W25QXX_CS = 0;

//这里就是将CS片选信号线拉低,这样才能开始传输数据。相当于IIC协议中的起始条件和结束条件。

//2.数据操作 SPI2_WR_Byte(0x90); //发设备指令 0x90 SPI2_WR_Byte(0x00); //连续发送三次0x00 SPI2_WR_Byte(0x00); SPI2_WR_Byte(0x00); tmp |= SPI2_WR_Byte(0x00) << 8; //始终时0xEF tmp |= SPI2_WR_Byte(0x00); //Device ID //3.CS拉高,表示结束 W25QXX_CS = 1;

//在这里插入图片描述//由上图可知首次CS片选信号线拉低,开始传输数据,然后主设备开始向从设备发送0x90,同时没有接收到数据,然后再发送24位数据,分别是3次8位数据,都是0x00,注意是先发最高位,然后再随机发送一个数据,0x00也行,然后就接收到一个制造商ID地址0xEF数据,再然后就能接收到正确的设备ID了。
//发送和接收数据是同步进行的。
//因为tmp数据是16位的,同时接收制造商ID和设备ID地址,且制造商ID是高8位,设备ID地址是低8位。
//最后再片选拉高!!!

w25qxx.c部分
W25QXX读取len个数据长度(快读方式)部分

u16 i = 0; //1.CS拉低,表示开始 W25QXX_CS = 0;

//跟往常一样,片选拉低

//2.数据操作 SPI2_WR_Byte(0x0B); //发读数据命令(Fast Read) //连续发送三次0x00 SPI2_WR_Byte((u8) (addr >> 16)); SPI2_WR_Byte((u8) (addr >> 8)); SPI2_WR_Byte((u8) (addr >> 0));

//
在这里插入图片描述//由上图可知,先把CS片选信号线拉低,然后开始传输数据,主设备开始向从设备发送0x0B,然后继续发24位数据,此时没有成功接收数据,然后再等待8个时钟周期,最后主设备随机发送数据到从设备,此时就能成功接收数据并且是一直能够接收,直到数据接收完成。
//在这里插入图片描述//然后依次类推,成功发送24位数据到从设备。

SPI2_WR_Byte(0xFF); //假读

//然后再等待8个时钟周期,相当于发送8位数据,只不过是接收不到数据的。

for (i = 0; i < len; i++) { pData[i] = SPI2_WR_Byte(0xFF); //读数据 }

//然后就可以成功接收数据了,这里就是一个for循环。
//指针就是数组,数组就是指针。

//3.CS拉高,表示结束 W25QXX_CS = 1;

//CS拉高,表示结束

w25qxx.c部分
W25QXX写page部分
在这里插入图片描述//W25Q80/16/32阵列被组织成4,096/8,192/16,384个可编程页,每个页256字节。
//使用Page程序指令一次最多可编程256个字节。可以以16组(扇区擦除)、128组(32KB块擦除)、256组(64KB块擦除)或整个芯片(芯片擦除)的方式擦除页面。
//W25Q80/16/32有256/512/1024个可擦扇区和16/32/64个可擦块。

//在这里插入图片描述//不同型号的w25qxx主要区别就在于页数不同,比如w25q80有16个block,w25q16有32个block,w25q32有64个block。
//Flash内存结构中最小单元是page(页),所以进行主设备写数据到从设备的话,需要进行页操作。

在这里插入图片描述
//页程序指令允许从一个字节到256字节(一页)的数据在先前擦除(FFh)的内存位置被编程。
//写使能指令必须在设备接收擦除指令之前执行(状态寄存器位WEL= 1)。
//如果要对整个256字节的页面进行编程,则最后一个地址字节(8个最不重要的地址位)应该设置为0。
//如果最后一个地址字节不为零,并且时钟的数量超过了剩余的页面长度,则寻址将自动换行到页面的开头。
//在某些情况下,小于256字节(部分页面)可以在不影响同一页面内的其他字节的情况下进行编程。

w25qxx.c部分
W25QXX写使能部分

//既然我们写数据的时候会用到写使能和读取状态等函数,那么我们就在写数据之前先把这几个函数封装好。

W25QXX_CS = 0; //1.CS拉低,表示开始 SPI2_WR_Byte(0x06); //Write Enable W25QXX_CS = 1; //3.CS拉高,表示结束

//首先我们还是先把CS拉低,表示开始,然后主设备发送一个指令到从设备,代表已经成功写使能,最后CS拉高,表示结束。
//
在这里插入图片描述w25qxx.c部分
W25QXX写不使能部分

W25QXX_CS = 0; //1.CS拉低,表示开始 SPI2_WR_Byte(0x04); //Write Disable W25QXX_CS = 1; //3.CS拉高,表示结束

//在这里插入图片描述//和写使能原理一样,只是主设备向从设备发送的数据不同罢了。

w25qxx.c部分
W25QXX读取状态寄存器部分
//在这里插入图片描述
//BUSY是状态寄存器(S0)中的一个只读位,它在设备执行a时被设置为1状态
页程序,扇区擦除,块擦除,芯片擦除或写状态寄存器指令
//当程序、擦除或写状态寄存器指令完成后,忙位将被清除为0状态,表示设备已准备好接受进一步的指令。
//在这里插入图片描述

W25QXX_CS = 0; //1.CS拉低,表示开始 SPI2_WR_Byte(0x05); //读Reg1命令 tmp = SPI2_WR_Byte(0xFF); W25QXX_CS = 1; //3.CS拉高,表示结束

//写使能指令将状态寄存器中的写使能锁存器(WEL)位设置为1

w25qxx.c部分
W25QXX等待忙碌状态部分

while (W25QXX_ReadReg1() & 0x01);

//这里就是如果是忙碌状态未完成,那么busy是1,那么上述操作就是会一直在while循环里,直到忙碌状态完成。

w25qxx.c部分
W25QXX擦除整个芯片

W25QXX_WriteEnble(); //必须先发写使能

//一个写使能指令必须在设备接受芯片擦除指令之前执行(状态)。

W25QXX_CS = 0; //1.CS拉低,表示开始 SPI2_WR_Byte(0xC7); //擦除整个芯片 W25QXX_CS = 1; //3.CS拉高,表示结束

//这里就不再赘述。

W25QXX_WaiteBusy();

//等待擦除命令完成。

w25qxx.c部分
W25QXX擦除扇区

W25QXX_WriteEnble(); //必须先发写使能

//还是在执行擦除命令之前先进行写使能

W25QXX_CS = 0; //1.CS拉低,表示开始 SPI2_WR_Byte(0x20); //Sector Erase //发地址 SPI2_WR_Byte((addr >> 16) & 0xFF); SPI2_WR_Byte((addr >> 8) & 0xFF); SPI2_WR_Byte((addr >> 0) & 0xFF); W25QXX_CS = 1; //3.CS拉高,表示结束

//这个发地址就相当于是只取8位数据,然后发3遍,总共24位。

W25QXX_WaiteBusy();

//然后等待擦除命令完成

w25qxx.c部分
W25QXX擦除块(32KB)

W25QXX_WriteEnble(); //必须先发写使能

//还是在执行擦除命令之前先进行写使能

W25QXX_CS = 0; //1.CS拉低,表示开始 SPI2_WR_Byte(0x52); //Block Erase (32KB) SPI2_WR_Byte((addr >> 16) & 0xFF); SPI2_WR_Byte((addr >> 8) & 0xFF); SPI2_WR_Byte((addr >> 0) & 0xFF); W25QXX_CS = 1; //3.CS拉高,表示结束

//这个发地址就相当于是只取8位数据,然后发3遍,总共24位。

W25QXX_WaiteBusy();

//然后等待擦除命令完成

w25qxx.c部分
W25QXX擦除块(64KB)

W25QXX_WriteEnble(); //必须先发写使能 W25QXX_CS = 0; //1.CS拉低,表示开始 SPI2_WR_Byte(0xD8); //Block Erase (64KB) SPI2_WR_Byte((addr >> 16) & 0xFF); SPI2_WR_Byte((addr >> 8) & 0xFF); SPI2_WR_Byte((addr >> 0) & 0xFF); W25QXX_CS = 1; //3.CS拉高,表示结束 W25QXX_WaiteBusy();

//这里不再赘述。

w25qxx.c部分
W25QXX写Page

void W25QXX_WritePage(u32 addr, u8* pData, u16 len)
//这里的 u16 为什么不是u8类型的呢,主要是u8类型的话,就是0~255,而一页是256个字节,所以这里是u16类型。
//在这里插入图片描述

u16 i = 0; W25QXX_WriteEnble(); //先WriteEnable W25QXX_CS = 0; //1.CS拉低,表示开始 SPI2_WR_Byte(0x02); //Page Program 命令 SPI2_WR_Byte((addr >> 16) & 0xFF); SPI2_WR_Byte((addr >> 8) & 0xFF); SPI2_WR_Byte((addr >> 0) & 0xFF);

//这里不再赘述。

for (i = 0; i < len; i++) { SPI2_WR_Byte(pData[i]); }

//这里就是把需要写入的数据成功写到从设备当中。

W25QXX_CS = 1; //3.CS拉高,表示结束 W25QXX_WaiteBusy();

//这里不再赘述。

w25qxx.c部分
W25QXX写Page(可以在任意位置写任意数据)

//在之前也说过了:如果最后一个地址字节不为零,并且时钟的数量超过了剩余的页面长度,则寻址将自动换行到页面的开头

u8* pBuffer = pData;

//先把所写数据的首地址赋值给新的指针变量,存放地址。

u16 last = 256 - addr % 256;

//获得当前地址所在Page中的位置到Page尾部的剩余大小空间

if (last >= len) last = len; while (1) { W25QXX_WritePage(addr, pBuffer, last); if (last == len) break; else { pBuffer += last; addr += last; len -= last; if (len > 256) last = 256; else last = len;

//
在这里插入图片描述//在这里插入图片描述//看完上这两张图就看清楚了。

w25qxx.c部分
W25QXX写Page(可以在任意位置写任意数据)如果频繁访问读写操作,可开辟缓存区,如果外接SRAM,可以使用动态申请内存

u8 W25QXX_BUF[4096] = { 0 };

//如果频繁访问读写操作,可开辟缓存区,如果外接SRAM,可以使用动态申请内存

//一般如果写数据之前没有被擦除,相当与里面的数据不全是1的话,那么就先进行擦除,然后再写数据。

//扇区擦除指令将指定扇区(4k字节)内的所有内存设置为所有1 (FFh)的擦除状态。

//
在这里插入图片描述//在这里插入图片描述

u16 offset = addr % 4096; u16 last = 4096 - offset; u16 n = addr / 4096;

//如上图所示,n代表前面有多少个扇区。

u8* buffer = W25QXX_BUF; //malloc(4096);

//将缓冲区赋值给新的指针变量。

if (last >= len) last = len;

//同理,不再赘述。

W25QXX_ReadMutiBytes(n * 4096, buffer, 4096);

//将在扇区中的数据读出来

for (i = 0; i < 4096; ++i) { if (buffer[i] != 0xFF) break; }

//如果读出来原来的扇区里面的数据不都是1,则停止,相当于该扇区需要进行擦除操作。

if (i != 4096) { //2.擦除整个扇区 W25QXX_SectorErase(n * 4096); }

//如果当有扇区不需要擦除时,写入速度明显提升,可以在进行大容量拷贝时,先将整个Flash擦除后再写入

//3.替换数据(使用C标准库的速度和下面循环的速度大致差不多) for (i = 0; i < last; ++i) { buffer[i + offset] = pData[i]; }

//把需要写的数据放到缓冲区中

//4.回写数据 W25QXX_WritePageEx(n * 4096, buffer, 4096);

//回写数据

if (last == len) break; //递归终止条件

//这里不再赘述。

n++;

//每次扇区地址向后偏移1

pData += last; //数据指针向后偏移last offset = 0; //从第二次开始offset变成0 addr += last; //地址向后偏移last

//从第二次开始offset变成0

if (len > 4096) last = 4096; else last = len;

//这里就不再赘述了。

结束语

个人认为大家如果细心看完这篇文章,并且结合上一篇文章一起看(在文章的刚开始会将前几篇关于SPI协议原理部分的文章链接发出来),我相信大家会彻底掌握SPI与Flash通讯了!!!如果觉得这篇文章还不错的话,记得点赞 ,支持下!!!

以后我会继续推出关于嵌入式(stm32)的协议方面的讲解,下一讲会推出DMA部分的文章!敬请期待!!!

**我先休息去了~~╭(╯^╰)╮


作者:致敬!!!



flash spi

需要 登录 后方可回复, 如果你还没有账号请 注册新账号
相关文章