收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
11 /* 使能 SPI 时钟 / 12 FLASH_SPI_APBxClock_FUN ( FLASH_SPI_CLK, ENABLE ); 13 14 / 使能 SPI 引脚相关的时钟 / 15 FLASH_SPI_CS_APBxClock_FUN ( FLASH_SPI_CS_CLK|FLASH_SPI_SCK_CLK| 16 FLASH_SPI_MISO_PIN|FLASH_SPI_MOSI_PIN, ENABLE ); 17 18 / 配置 SPI 的 CS 引脚,普通 IO 即可 / 19 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_CS_PIN; 20 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 21 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 22 GPIO_Init(FLASH_SPI_CS_PORT, &GPIO_InitStructure); 23 24 / 配置 SPI 的 SCK 引脚*/ 25 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN; 26 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; 27 GPIO_Init(FLASH_SPI_SCK_PORT, &GPIO_InitStructure); 28 29 /* 配置 SPI 的 MF103-霸道引脚*/ 30 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN; 31 GPIO_Init(FLASH_SPI_MISO_PORT, &GPIO_InitStructure); 32 33 /* 配置 SPI 的 MOSI 引脚*/ 34 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN; 35 GPIO_Init(FLASH_SPI_MOSI_PORT, &GPIO_InitStructure); 36 37 /* 停止信号 FLASH: CS 引脚高电平*/ 38 FLASH_SPI_CS_HIGH(); 39 //为方便讲解,以下省略 SPI 模式初始化部分 40 }
与所有使用到 GPIO的外设一样,都要先把使用到的 GPIO引脚模式初始化,配置好复用功能。GPIO初始化流程如下:
(1) 使用GPIO\_InitTypeDef定义 GPIO初始化结构体变量,以便下面用于存储GPIO配置;
(2) 调用库函数 RCC\_APB2PeriphClockCmd 来使能 SPI引脚使用的 GPIO 端口时钟。
(3) 向 GPIO 初始化结构体赋值,把 SCK/MOSI/MISO 引脚初始化成复用推挽模式。而CS(NSS)引脚由于使用软件控制,我们把它配置为普通的推挽输出模式。
(4) 使用以上初始化结构体的配置,调用 GPIO\_Init 函数向寄存器写入参数,完成 GPIO 的初始化。
配置 SPI 的模式
以上只是配置了 SPI 使用的引脚,对 SPI 外设模式的配置。在配置 STM32 的 SPI 模式前,我们要先了解从机端的 SPI 模式。本例子中可通过查阅 FLASH 数据手册《W25Q64》获取。根据 FLASH 芯片的说明,它支持 SPI模式 0及模式 3,支持双线全双工,使用 MSB先行模式,支持最高通讯时钟为 104MHz,数据帧长度为 8 位。我们要把 STM32 的 SPI 外设中的这些参数配置一致。
1 /** 2 * @brief SPI_FLASH 引脚初始化 3 * @param 无 4 * @retval 无 5 / 6 void SPI_FLASH_Init(void) 7 { 8 /为方便讲解,省略了 SPI 的 GPIO 初始化部分/ 9 //...... 10 11 SPI_InitTypeDef SPI_InitStructure; 12 / SPI 模式配置 / 13 // FLASH 芯片 支持 SPI 模式 0 及模式 3,据此设置 CPOL CPHA 14 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; 15 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; 16 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; 17 SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; 18 SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; 19 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; 20 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; 21 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; 22 SPI_InitStructure.SPI_CRCPolynomial = 7; 23 SPI_Init(FLASH_SPIx, &SPI_InitStructure); 24 25 / 使能 SPI */ 26 SPI_Cmd(FLASH_SPIx, ENABLE); 27 }
这段代码中,把 STM32 的 SPI 外设配置为主机端,双线全双工模式,数据帧长度为 8位,使用 SPI 模式 3(CPOL=1,CPHA=1),NSS 引脚由软件控制以及 MSB 先行模式。代码中把 SPI的时钟频率配置成了 4分频,实际上可以配置成 2分频以提高通讯速率,读者可亲自尝试一下。最后一个成员为 CRC 计算式,由于我们与 FLASH 芯片通讯不需要 CRC 校验,并没有使能 SPI的 CRC功能,这时 CRC计算式的成员值是无效的。
赋值结束后调用库函数 SPI\_Init 把这些配置写入寄存器,并调用 SPI\_Cmd 函数使能外设。
使用 SPI 发送和接收一个字节的数据
初始化好SPI外设后,就可以使用SPI通讯了,复杂的数据通讯都是由单个字节数据收发组成的,我们看看它的代码实现。
1 #define Dummy_Byte 0xFF 2 /** 3 * @brief 使用 SPI 发送一个字节的数据 4 * @param byte:要发送的数据 5 * @retval 返回接收到的数据 6 / 7 u8 SPI_FLASH_SendByte(u8 byte) 8 { 9 SPITimeout = SPIT_FLAG_TIMEOUT; 10 11 / 等待发送缓冲区为空,TXE 事件 / 12 while (SPI_I2S_GetFlagStatus(FLASH_SPIx, SPI_I2S_FLAG_TXE) == RESET) 13 { 14 if ((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0); 15 } 16 17 / 写入数据寄存器,把要写入的数据写入发送缓冲区 / 18 SPI_I2S_SendData(FLASH_SPIx, byte); 19 20 SPITimeout = SPIT_FLAG_TIMEOUT; 21 22 / 等待接收缓冲区非空,RXNE 事件 / 23 while (SPI_I2S_GetFlagStatus(FLASH_SPIx, SPI_I2S_FLAG_RXNE) == RESET) 24 { 25 if ((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1); 26 } 27 28 / 读取数据寄存器,获取接收缓冲区数据 / 29 return SPI_I2S_ReceiveData(FLASH_SPIx); 30 } 31 32 /* 33 * @brief 使用 SPI 读取一个字节的数据 34 * @param 无 35 * @retval 返回接收到的数据 36 */ 37 u8 SPI_FLASH_ReadByte(void) 38 { 39 return (SPI_FLASH_SendByte(Dummy_Byte)); 40 }
SPI\_FLASH\_SendByte 发送单字节函数中包含了等待事件的超时处理,这部分原理跟I2C 中的一样,在此不再赘述。
SPI\_FLASH\_SendByte 函数实现了前面讲解的“SPI通讯过程”:
(1) 本函数中不包含 SPI 起始和停止信号,只是收发的主要过程,所以在调用本函数前后要做好起始和停止信号的操作;
(2) 对 SPITimeout 变量赋值为宏 SPIT\_FLAG\_TIMEOUT。这个 SPITimeout 变量在下面的 while 循环中每次循环减 1,该循环通过调用库函数 SPI\_I2S\_GetFlagStatus 检测事件,若检测到事件,则进入通讯的下一阶段,若未检测到事件则停留在此处一直检测,当检测 SPIT\_FLAG\_TIMEOUT次都还没等待到事件则认为通讯失败,调用的 SPI\_TIMEOUT\_UserCallback输出调试信息,并退出通讯;
(3) 通过检测 TXE 标志,获取发送缓冲区的状态,若发送缓冲区为空,则表示可能存在的上一个数据已经发送完毕;
(4) 等待至发送缓冲区为空后,调用库函数 SPI\_I2S\_SendData 把要发送的数据“byte”写入到 SPI 的数据寄存器 DR,写入 SPI 数据寄存器的数据会存储到发送缓冲区,由 SPI外设发送出去;
(5) 写入完毕后等待 RXNE 事件,即接收缓冲区非空事件。由于 SPI 双线全双工模式下 MOSI 与 MISO 数据传输是同步的(请对比“SPI 通讯过程”阅读),当接收缓冲区非空时,表示上面的数据发送完毕,且接收缓冲区也收到新的数据;
(6) 等待至接收缓冲区非空时,通过调用库函数 SPI\_I2S\_ReceiveData 读取 SPI 的数据寄存器 DR,就可以获取接收缓冲区中的新数据了。代码中使用关键字“return”把接收到的这个数据作为 SPI\_FLASH\_SendByte 函数的返回值,所以我们可以看到在下面定义的 SPI 接收数据函数 SPI\_FLASH\_ReadByte,它只是简单地调用了SPI\_FLASH\_SendByte 函数发送数据“Dummy\_Byte”,然后获取其返回值(因为不关注发送的数据,所以此时的输入参数“Dummy\_Byte”可以为任意值)。可以这样做的原因是 SPI 的接收过程和发送过程实质是一样的,收发同步进行,关键在于我们的上层应用中,关注的是发送还是接收的数据。
控制 FLASH 的指令
搞定 SPI 的基本收发单元后,还需要了解如何对 FLASH 芯片进行读写。FLASH 芯片自定义了很多指令,我们通过控制 STM32 利用 SPI 总线向 FLASH 芯片发送指令,FLASH芯片收到后就会执行相应的操作。
而这些指令,对主机端(STM32)来说,只是它遵守最基本的 SPI通讯协议发送出的数据,但在设备端(FLASH芯片)把这些数据解释成不同的意义,所以才成为指令。查看 FLASH芯片的数据手册《W25Q64》,可了解各种它定义的各种指令的功能及指令格式
定义 FLASH 指令编码表
为了方便使用,我们把 FLASH芯片的常用指令编码使用宏来封装起来,后面需要发送
指令编码的时候我们直接使用这些宏即可
FLASH 指令编码表 1 /FLASH 常用命令/ 2 #define W25X_WriteEnable 0x06 3 #define W25X_WriteDisable 0x04 4 #define W25X_ReadStatusReg 0x05 5 #define W25X_WriteStatusReg 0x01 6 #define W25X_ReadData 0x03 7 #define W25X_FastReadData 0x0B 8 #define W25X_FastReadDual 0x3B 9 #define W25X_PageProgram 0x02 10 #define W25X_BlockErase 0xD8 11 #define W25X_SectorErase 0x20 12 #define W25X_ChipErase 0xC7 13 #define W25X_PowerDown 0xB9 14 #define W25X_ReleasePowerDown 0xAB 15 #define W25X_DeviceID 0xAB 16 #define W25X_ManufactDeviceID 0x90 17 #define W25X_JedecDeviceID 0x9F 18 /其它/ 19 #define sFLASH_ID 0XEF4017 20 #define Dummy_Byte 0xFF
读取 FLASH 芯片 ID
根据“JEDEC”指令的时序,我们把读取 FLASH ID 的过程编写成一个函数。
读取 FLASH 芯片 ID 1 /** 2 * @brief 读取 FLASH ID 3 * @param 无 4 * @retval FLASH ID 5 / 6 u32 SPI_FLASH_ReadID(void) 7 { 8 u32 Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0; 9 10 / 开始通讯:CS 低电平 / 11 SPI_FLASH_CS_LOW(); 12 13 / 发送 JEDEC 指令,读取 ID / 14 SPI_FLASH_SendByte(W25X_JedecDeviceID); 15 16 / 读取一个字节数据 / 17 Temp0 = SPI_FLASH_SendByte(Dummy_Byte); 18 19 / 读取一个字节数据 / 20 Temp1 = SPI_FLASH_SendByte(Dummy_Byte); 21 22 / 读取一个字节数据 / 23 Temp2 = SPI_FLASH_SendByte(Dummy_Byte); 24 25 / 停止通讯:CS 高电平 */ 26 SPI_FLASH_CS_HIGH(); 27 28 /把数据组合起来,作为函数的返回值/ 29 Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2; 30 31 return Temp; 32 }
这段代码利用控制 CS 引脚电平的宏“SPI\_FLASH\_CS\_LOW/HIGH”以及前面编写的单字节收发函数 SPI\_FLASH\_SendByte,很清晰地实现了“JEDEC ID”指令的时序:发送一个字节的指令编码“W25X\_JedecDeviceID”,然后读取 3 个字节,获取 FLASH 芯片对该指令的响应,最后把读取到的这 3 个数据合并到一个变量 Temp 中,然后作为函数返回值,把该返回值与我们定义的宏“sFLASH\_ID”对比,即可知道 FLASH 芯片是否正常。
FLASH 写使能以及读取当前状态
在向 FLASH 芯片存储矩阵写入数据前,首先要使能写操作,通过“Write Enable”命令即可写使能。
1 /** 2 * @brief 向 FLASH 发送 写使能 命令 3 * @param none 4 * @retval none 5 / 6 void SPI_FLASH_WriteEnable(void) 7 { 8 / 通讯开始:CS 低 / 9 SPI_FLASH_CS_LOW(); 10 11 / 发送写使能命令*/ 12 SPI_FLASH_SendByte(W25X_WriteEnable); 13 14 /*通讯结束:CS 高 */ 15 SPI_FLASH_CS_HIGH(); 16 }
与 EEPROM 一样,由于 FLASH 芯片向内部存储矩阵写入数据需要消耗一定的时间,并不是在总线通讯结束的一瞬间完成的,所以在写操作后需要确认 FLASH芯片“空闲”时才能进行再次写入。为了表示自己的工作状态,FLASH 芯片定义了一个状态寄存器。

我们只关注这个状态寄存器的第0位“BUSY”,当这个位为“1”时,表明FLASH芯片处于忙碌状态,它可能正在对内部的存储矩阵进行“擦除”或“数据写入”的操作。
利用指令表中的“Read Status Register”指令可以获取 FLASH 芯片状态寄存器的内容,其时序见下图

只要向 FLASH 芯片发送了读状态寄存器的指令,FLASH 芯片就会持续向主机返回最新的状态寄存器内容,直到收到 SPI通讯的停止信号。据此我们编写了具有等待 FLASH 芯片写入结束功能的函数。
1 /* WIP(busy)标志,FLASH 内部正在写入 / 2 #define WIP_Flag 0x01 3 4 /* 5 * @brief 等待 WIP(BUSY)标志被置 0,即等待到 FLASH 内部数据写入完毕 6 * @param none 7 * @retval none 8 / 9 void SPI_FLASH_WaitForWriteEnd(void) 10 { 11 u8 FLASH_Status = 0; 12 13 / 选择 FLASH: CS 低 / 14 SPI_FLASH_CS_LOW(); 15 16 / 发送 读状态寄存器 命令 / 17 SPI_FLASH_SendByte(W25X_ReadStatusReg); 18 19 / 若 FLASH 忙碌,则等待 / 20 do 21 { 22 / 读取 FLASH 芯片的状态寄存器 / 23 FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte); 24 } 25 while ((FLASH_Status & WIP_Flag) == SET); / 正在写入标志 / 26 27 / 停止信号 FLASH: CS 高 */ 28 SPI_FLASH_CS_HIGH(); 29 }
这段代码发送读状态寄存器的指令编码“W25X\_ReadStatusReg”后,在 while 循环里持续获取寄存器的内容并检验它的“WIP\_Flag 标志”(即 BUSY 位),一直等待到该标志表示写入结束时才退出本函数,以便继续后面与 FLASH 芯片的数据通讯。
FLASH 扇区擦除
由于 FLASH存储器的特性决定了它只能把原来为“1”的数据位改写成“0”,而原来为“0”的数据位不能直接改写为“1”。所以这里涉及到数据“擦除”的概念,在写入前,必须要对目标存储矩阵进行擦除操作,把矩阵中的数据位擦除为“1”,在数据写入的时候,如果要存储数据“1”,那就不修改存储矩阵 ,在要存储数据“0”时,才更改该位。通常,对存储矩阵擦除的基本操作单位都是多个字节进行,如本例子中的 FLASH芯片支持“扇区擦除”、“块擦除”以及“整片擦除”。
扇区擦除指令的第一个字节为指令编码,紧接着发送的 3 个字节用于表示要擦除的存
储矩阵地址。要注意的是在扇区擦除指令前,还需要先发送“写使能”指令,发送扇区擦
除指令后,通过读取寄存器状态等待扇区擦除操作完毕
1 /** 2 * @brief 擦除 FLASH 扇区 3 * @param SectorAddr:要擦除的扇区地址 4 * @retval 无 5 / 6 void SPI_FLASH_SectorErase(u32 SectorAddr) 7 { 8 / 发送 FLASH 写使能命令 / 9 SPI_FLASH_WriteEnable(); 10 SPI_FLASH_WaitForWriteEnd(); 11 / 擦除扇区 / 12 / 选择 FLASH: CS 低电平 / 13 SPI_FLASH_CS_LOW(); 14 / 发送扇区擦除指令*/ 15 SPI_FLASH_SendByte(W25X_SectorErase); 16 /发送擦除扇区地址的高位/ 17 SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16); 18 /* 发送擦除扇区地址的中位 / 19 SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8); 20 / 发送擦除扇区地址的低位 / 21 SPI_FLASH_SendByte(SectorAddr & 0xFF); 22 / 停止信号 FLASH: CS 高电平 / 23 SPI_FLASH_CS_HIGH(); 24 / 等待擦除完毕*/ 25 SPI_FLASH_WaitForWriteEnd(); 26 }
这段代码调用的函数在前面都已讲解,只要注意发送擦除地址时高位在前即可。调用扇区擦除指令时注意输入的地址要对齐到 4KB。
FLASH 的页写入
目标扇区被擦除完毕后,就可以向它写入数据了。与 EEPROM 类似,FLASH 芯片也有页写入命令,使用页写入命令最多可以一次向 FLASH 传输 256个字节的数据,我们把这个单位为页大小。FLASH 页写入的时序见图 。

从时序图可知,第 1 个字节为“页写入指令”编码,2-4 字节为要写入的“地址 A”,接着的是要写入的内容,最多个可以发送 256 字节数据,这些数据将会从“地址 A”开始,按顺序写入到 FLASH的存储矩阵。若发送的数据超出 256个,则会覆盖前面发送的数据。与擦除指令不一样,页写入指令的地址并不要求按 256 字节对齐,只要确认目标存储单元是擦除状态即可(即被擦除后没有被写入过)。所以,若对“地址 x”执行页写入指令后,发送了 200 个字节数据后终止通讯,下一次再执行页写入指令,从“地址(x+200)”开始写入200个字节也是没有问题的(小于256均可)。 只是在实际应用中由于基本擦除单元是4KB,一般都以扇区为单位进行读写。
把页写入时序封装成函数
FLASH 的页写入 1 /** 2 * @brief 对 FLASH 按页写入数据,调用本函数写入数据前需要先擦除扇区 3 * @param pBuffer,要写入数据的指针 4 * @param WriteAddr,写入地址 5 * @param NumByteToWrite,写入数据长度,必须小于等于页大小 6 * @retval 无 7 / 8 void SPI_FLASH_PageWrite(u8 pBuffer, u32 WriteAddr, u16 NumByteToWrite) 9 { 10 /* 发送 FLASH 写使能命令 / 11 SPI_FLASH_WriteEnable(); 12 13 / 选择 FLASH: CS 低电平 / 14 SPI_FLASH_CS_LOW(); 15 / 写送写指令*/ 16 SPI_FLASH_SendByte(W25X_PageProgram); 17 /发送写地址的高位/ 18 SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16); 19 /发送写地址的中位/ 20 SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8); 21 /发送写地址的低位/ 22 SPI_FLASH_SendByte(WriteAddr & 0xFF); 23 24 if (NumByteToWrite > SPI_FLASH_PerWritePageSize) 25 { 26 NumByteToWrite = SPI_FLASH_PerWritePageSize; 27 FLASH_ERROR("SPI_FLASH_PageWrite too large!"); 28 } 29 30 /* 写入数据*/ 31 while (NumByteToWrite--) 32 { 33 /* 发送当前要写入的字节数据 / 34 SPI_FLASH_SendByte(pBuffer); 35 / 指向下一字节数据 / 36 pBuffer++; 37 } 38 39 / 停止信号 FLASH: CS 高电平 / 40 SPI_FLASH_CS_HIGH(); 41 42 / 等待写入完毕/ 43 SPI_FLASH_WaitForWriteEnd(); 44 }
这段代码的内容为:先发送“写使能”命令,接着才开始页写入时序,然后发送指令编码、地址,再把要写入的数据一个接一个地发送出去,发送完后结束通讯,检查 FLASH状态寄存器,等待 FLASH 内部写入结束。
不定量数据写入
应用的时候我们常常要写入不定量的数据,直接调用“页写入”函数并不是特别方便,所以我们在它的基础上编写了“不定量数据写入”的函数。
1 /** 2 * @brief 对 FLASH 写入数据,调用本函数写入数据前需要先擦除扇区 3 * @param pBuffer,要写入数据的指针 4 * @param WriteAddr,写入地址 5 * @param NumByteToWrite,写入数据长度 6 * @retval 无 7 / 8 void SPI_FLASH_BufferWrite(u8 pBuffer, u32 WriteAddr, u16 NumByteToWrite) 9 { 10 u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0; 11 12 /mod 运算求余,若 writeAddr 是 SPI_FLASH_PageSize 整数倍, 13 运算结果 Addr 值为 0/ 14 Addr = WriteAddr % SPI_FLASH_PageSize; 15 16 /差 count 个数据值,刚好可以对齐到页地址/ 17 count = SPI_FLASH_PageSize - Addr; 18 /计算出要写多少整数页/ 19 NumOfPage = NumByteToWrite / SPI_FLASH_PageSize; 20 /mod 运算求余,计算出剩余不满一页的字节数/ 21 NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize; 22 23 /* Addr=0,则 WriteAddr 刚好按页对齐 aligned / 24 if (Addr == 0) 25 { 26 / NumByteToWrite < SPI_FLASH_PageSize / 27 if (NumOfPage == 0) 28 { 29 SPI_FLASH_PageWrite(pBuffer, WriteAddr, 30 NumByteToWrite); 31 } 32 else / NumByteToWrite > SPI_FLASH_PageSize / 33 { 34 /先把整数页都写了/ 35 while (NumOfPage--) 36 { 37 SPI_FLASH_PageWrite(pBuffer, WriteAddr, 38 SPI_FLASH_PageSize); 39 WriteAddr += SPI_FLASH_PageSize; 40 pBuffer += SPI_FLASH_PageSize; 41 } 42 /若有多余的不满一页的数据,把它写完/ 43 SPI_FLASH_PageWrite(pBuffer, WriteAddr, 44 NumOfSingle); 45 } 46 } 47 / 若地址与 SPI_FLASH_PageSize 不对齐 / 48 else 49 { 50 / NumByteToWrite < SPI_FLASH_PageSize / 51 if (NumOfPage == 0) 52 { 53 /当前页剩余的 count 个位置比 NumOfSingle 小,一页写不完/ 54 if (NumOfSingle > count) 55 { 56 temp = NumOfSingle - count; 57 /先写满当前页/ 58 SPI_FLASH_PageWrite(pBuffer, WriteAddr, count); 59 60 WriteAddr += count; 61 pBuffer += count; 62 /再写剩余的数据/ 63 SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp); 64 } 65 else /当前页剩余的 count 个位置能写完 NumOfSingle 个数据/ 66 { 67 SPI_FLASH_PageWrite(pBuffer, WriteAddr, 68 NumByteToWrite); 69 } 70 } 71 else / NumByteToWrite > SPI_FLASH_PageSize / 72 { 73 /地址不对齐多出的 count 分开处理,不加入这个运算/ 74 NumByteToWrite -= count; 75 NumOfPage = NumByteToWrite / SPI_FLASH_PageSize; 76 NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize; 77 78 / 先写完 count 个数据,为的是让下一次要写的地址对齐 / 79 SPI_FLASH_PageWrite(pBuffer, WriteAddr, count); 80 81 / 接下来就重复地址对齐的情况 */ 82 WriteAddr += count; 83 pBuffer += count; 84 /把整数页都写了/ 85 while (NumOfPage--) 86 { 87 SPI_FLASH_PageWrite(pBuffer, WriteAddr, 88 SPI_FLASH_PageSize); 89 WriteAddr += SPI_FLASH_PageSize; 90 pBuffer += SPI_FLASH_PageSize; 91 } 92 /若有多余的不满一页的数据,把它写完/ 93 if (NumOfSingle != 0) 94 { 95 SPI_FLASH_PageWrite(pBuffer, WriteAddr, 96 NumOfSingle); 97 } 98 } 99 } 100 }
这段代码与 EEPROM 章节中的“快速写入多字节”函数原理是一样的,运算过程在此不再赘述。区别是页的大小以及实际数据写入的时候,使用的是针对 FLASH芯片的页写入函数,且在实际调用这个“不定量数据写入”函数时,还要注意确保目标扇区处于擦除状态。
从 从 FLASH 读取数据
相对于写入,FLASH 芯片的数据读取要简单得多,使用读取指令“Read Data”即可。

发送了指令编码及要读的起始地址后,FLASH 芯片就会按地址递增的方式返回存储矩阵的内容,读取的数据量没有限制,只要没有停止通讯,FLASH 芯片就会一直返回数据。
1 /** 2 * @brief 读取 FLASH 数据 3 * @param pBuffer,存储读出数据的指针 4 * @param ReadAddr,读取地址 5 * @param NumByteToRead,读取数据长度 6 * @retval 无 7 / 8 void SPI_FLASH_BufferRead(u8 pBuffer, u32 ReadAddr, u16 NumByteToRead) 9 { 10 /* 选择 FLASH: CS 低电平 / 11 SPI_FLASH_CS_LOW(); 12 13 / 发送 读 指令 / 14 SPI_FLASH_SendByte(W25X_ReadData); 15 16 / 发送 读 地址高位 / 17 SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16); 18 / 发送 读 地址中位 / 19 SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8); 20 / 发送 读 地址低位 / 21 SPI_FLASH_SendByte(ReadAddr & 0xFF); 22 23 / 读取数据 / 24 while (NumByteToRead--) 25 { 26 / 读取一个字节*/ 27 pBuffer = SPI_FLASH_SendByte(Dummy_Byte); 28 / 指向下一个字节缓冲区 / 29 pBuffer++; 30 } 31 32 / 停止信号 FLASH: CS 高电平 */ 33 SPI_FLASH_CS_HIGH(); 34 }
由于读取的数据量没有限制,所以发送读命令后一直接收 NumByteToRead 个数据到结束即可。
MAIN函数
main 函数
1 int main(void)
2 {
3 LED_GPIO_Config();
4 LED_BLUE;
5
6 /* 配置串口 1 为:115200 8-N-1 /
7 USART_Config();
8 printf("\r\n 这是一个 8Mbyte 串行 flash(W25Q64)实验 \r\n");
9
10 / 8M 串行 flash W25Q64 初始化 /
11 SPI_FLASH_Init();
12
13 / 获取 Flash Device ID /
14 DeviceID = SPI_FLASH_ReadDeviceID();
15 Delay( 200 );
16
17 / 获取 SPI Flash ID /
18 FlashID = SPI_FLASH_ReadID();
19 printf("\r\n FlashID is 0x%X,
20 Manufacturer Device ID is 0x%X\r\n", FlashID, DeviceID);
21
22 / 检验 SPI Flash ID */
23 if (FlashID == sFLASH_ID)
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新