一、软件SPI和硬件SPI的区别
🍏软件SPI:用IO模拟SPI时序,这个模拟过程全部是CPU在负责执行,为了其稳定的存取数据,你可能会插入软件延时,这个时间在读取数据量不大的情况下并不明显,但是基本上你在读取过程中,其他非中断非异常程序是无法得到执行。
🍎硬件SPI:首先这个数据存道储的过程是不需要CPU参与得,程序中配置好SPI的访问时序,开启中断,CPU就可以在中断函数中搬移数据,省下了软件模拟IO得存取时间。
仔细研究就会发现,CPU在进行SPI中断服务程序还版是需要耽误时间得,这个过程在大数据量传输中还是很耗时,ARM中Cortex-M3内核得处理器在硬件SPI上加入了DMA,这个DMA直接从SPI的数据寄存器,软件配置好DMA之后,基本上整个传输都不要权CPU参与,软件设计得好的话,整个数据传输都不要CPU参与,此时CPU就可以做更多其他有意义的事了。
🀄上文介绍过软件SPI的收发,这次来看下硬件SPI的收发
这个是字节的发送和读取,所以不需要像软件SPI那样循环发送8个位
//SPIx 读写一个字节 //TxData:要写入的字节 //返回值:读取到的字节 u8 SPI1_ReadWriteByte(u8 TxData) { u8 retry=0; while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) //检查指定的SPI标志位设置与否:发送缓存空标志位 { retry++; if(retry>200)return 0; } SPI_I2S_SendData(SPI1, TxData); //通过外设SPIx发送一个数据 retry=0; while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET) //检查指定的SPI标志位设置与否:接受缓存非空标志位 { retry++; if(retry>200)return 0; } return SPI_I2S_ReceiveData(SPI1); //返回通过SPIx最近接收的数据 }
二、 W25Q64简介
🍉 W25Q64(64Mbit)是为系统提供一个最小的空间、引脚和功耗的存储器解决方案的串行FLASH存储器。25Q系列比普通的串行FLASH存储器更灵活,性能更优越。基于双倍/四倍的SPI,它们能够可以立即完成提供数据给RAM,包括声音、文本和数据。
🍇 W25Q64由每页256字节组成,每页的256字节用一次页编程指令即可完成。每次可以擦除16页(一个扇区),128页(32KB块),256页(64KB块)和全片擦除。
🍓 W25Q64的内存空间结构:一页256字节,4k(4096字节)为一个扇区,16个扇区为一块,总容量为8M字节,共有128个块即2048个扇区。SPI最高支持80MHZ,当使用快读双倍/四倍指令时,相当于双倍输出时最高速率160MHZ,四倍输出时最高速率320MHZ。
🔔特点:
- 标准、双倍和四倍SPI
- 高性能串行FLASH存储器
- 灵活的4KB扇区结构
统一的扇区擦除和块擦除, 一次编程256字节,至少100000写/擦除周期,数据保存20年。
- 高级的安全特点
- 低功耗、宽温度范围
单电源2.7~3.6V,工作电流4mA,-40℃~85℃工作。
💢封装
🌀引脚描述
引脚编号 | 引脚名称 | I/O | 功能 |
1 | /CS | I | 片选端输入 |
2 | DO(IO1) | I/O | 数据输出 |
3 | /WP(IO2) | I/O | 写保护输入 |
4 | GND | 地 | |
5 | DI(IO0) | I/O | 数据输入 |
6 | CLK | I | 串行时钟输入 |
7 | /HOLD(IO3) | I/O | 保持端输入 |
8 | VCC | 电源 |
1.1 片选端(/CS)
SPI片选(/CS)引脚使能和禁止芯片操作。当/CS为高电平时,芯片未被选择,串行数据输出(IOx)引脚为高阻态,芯片处于待机状态下的低功耗,除非芯片内部在擦除。当/CS变成低电平,芯片功耗将增长到正常工作,能够从芯片上读写数据。上电后,在接收新的指令前,/CS必须由高电平变成低电平。
1.2 串行数据输入输出(DI、DO)
标准的 SPI 传输用单向的 DI(输入)引脚连续的写命令、地址或者数据在串行时钟(CLK)的上升沿时写入到芯片内。标准的SPI 用单向的 DO(输出)在 CLK 的下降沿从芯片内读出数据或状态。
1.3 写保护(/WP)
写保护引脚(/WP)用来保护状态寄存器。和状态寄存器的块保护位(SEC、TB、BP2、BP1 和BP0)和状态寄存器保护位(SRP)对存储器进行一部分或者全部的硬件保护。/WP 引脚低电平有效。当状态寄存器 2 的 QE 位被置位了,/WP 引脚(硬件写保护)的功能不可用,被用作了 IO2。
1.4 保持端(/HOLD)
当/HOLD 引脚是有效时,允许芯片暂停工作。在/CS 为低电平时,当/HOLD 变为低电平,DO 引脚将变为高阻态,在 DI 和 CLK 引脚上的信号将无效。当/HOLD 变为高电平,芯片恢复工作。/HOLD功能用在当有多个设备共享同一 SPI 总线时。/HOLD 引脚低电平有效。当状态寄存器 2 的 QE 位被置位了,/ HOLD 引脚的功能不可用,被用作了 IO3。
1.5 串行时钟(CLK)
串行时钟输入引脚为串行输入和输出操作提供时序。设备数据传输是从高位开始,数据传输的格式为 8bit,数据采样从上升沿开始。
1.6 结构框图
三、SPI 通信
有关SPI的介绍已经在上两篇文章介绍了,我这就不说了,直接上配置代码
1.配置SPI初始化
- IO,SPI结构体变量定义
- 使能IO、SPI时钟
- 映射SPI引脚,CS不用
- IO参数配置,初始化
- SPI参数配置,初始化
- SPI使能
- 拉高CS
/******************************************* *@函数名 : W25Q64_Init *@函数功能 : W25Q64初始化---SPI1 *@函数参数 : None *@Data 2023-09-11 *@函数返回值: None *@函数描述 : PA4--CS PA5--SCK PA6--MISO PA7--MOSI *********************************************/ void W25Q64_Init(void) { //结构体变量创建 GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; //使能GPIO时钟 RCC_AHB1PeriphClockLPModeCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能SPI时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE); //映射SPI1引脚 PA7 PA5 PA6 GPIO_PinAFConfig(GPIOA,GPIO_PinSource5,GPIO_AF_SPI1); GPIO_PinAFConfig(GPIOA,GPIO_PinSource6,GPIO_AF_SPI1); GPIO_PinAFConfig(GPIOA,GPIO_PinSource7,GPIO_AF_SPI1); //GPIO参数配置--配置 SPI引脚: SCK、MISO、MOSI GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF; //复用模式 GPIO_InitStructure.GPIO_OType=GPIO_OType_PP; //推挽输出 GPIO_InitStructure.GPIO_Pin=GPIO_Pin_5 | GPIO_Pin_6 |GPIO_Pin_7; GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP; //上拉 GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz; GPIO_Init(GPIOA,&GPIO_InitStructure); //CS 片选---PC7 GPIO_InitStructure.GPIO_Mode=GPIO_Mode_OUT; //普通模式 GPIO_InitStructure.GPIO_OType=GPIO_OType_PP; //推挽输出 GPIO_InitStructure.GPIO_Pin=GPIO_Pin_7; GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP; //上拉 GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz; GPIO_Init(GPIOC,&GPIO_InitStructure); W25Q64_CS_HIGH; // CS引脚拉高:停止信号 //SPI1参数配置 SPI_InitStructure.SPI_BaudRatePrescaler=SPI_BaudRatePrescaler_2;//波特率预分频值 SPI_InitStructure.SPI_CPHA=SPI_CPHA_2Edge;//串行时钟进行偶次采样 SPI_InitStructure.SPI_CPOL=SPI_CPOL_High;//时钟空闲为高电平---模式3-上升沿采集数据 SPI_InitStructure.SPI_CRCPolynomial=7;//CRC值计算多项式--7--复位值 SPI_InitStructure.SPI_Direction=SPI_Direction_2Lines_FullDuplex;//SPI双向全双工 SPI_InitStructure.SPI_DataSize=SPI_DataSize_8b;//8位数据帧 SPI_InitStructure.SPI_FirstBit=SPI_FirstBit_MSB;//数据高位在前 SPI_InitStructure.SPI_Mode=SPI_Mode_Master;// 主从模式: 1 = 主配置 SPI_InitStructure.SPI_NSS=SPI_NSS_Soft; // 软件从器件管理 : 1 = 使能软件从器件管理(软件NSS) SPI_Init(SPI1,&SPI_InitStructure); SPI_Cmd(SPI1,ENABLE);// SPI使能 1 = 使能外设 readID();//读取芯片 }
2.读写数据
通过接收完成和发送完成两个标志位实现一个字节的发送
******************************************* *@函数名 : sendByte *@函数功能 : 发送1字节,返回1字节 *@函数参数 : data: 数据 *@Data 2023-09-11 *@函数返回值: 返回接收到的字节 *@函数描述 : SPI通信,只一个动作:向DR写入从设命令值,同步读出数据! 写读组合,按从设时序图来. 作为主设,因为收发同步,连接收发送中断也不用开, *********************************************/ uint8_t sendByte(uint8_t data) { uint16_t retry = 0; while (SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET) // 等待发送区为空 { retry++; if (retry > 1000) return 0; } SPI_I2S_SendData(SPI1,data); retry = 0; while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) == RESET)//等待接收缓存区非空 { retry++; if (retry > 1000) return 0; } return SPI_I2S_ReceiveData(SPI1); }
W25Q的指令
👉写使能:0x06
👉禁使能:0x04
👉读状态寄存器指令:0x05
1.CS片选拉低发送指令,CS片选拉高结束读指令结束
2.读状态寄存器指令任何时候都能用
3.高位数据在前,先发送指令,再读取数据
4.时钟有主控控制,读取数据同时,要发送任意得数据给从机。
👉写状态寄存器指令:0x01
- CS片选拉低发送指令,CS片选拉高结束写指令结束,片选拉晚或者没有拉高,写入的值将无效
- 先写使能(0x06)
- 只有SRP TB BP2 BP1 BP0 可以被写入
- TB BP2 BP1 BP0写1的时候只能读,写0才能对块区进行操作
👉读数据命令:0x03
- CS片选拉低发送指令,CS片选拉高结束读指令结束
- 先发送0x03指令,后面跟24位的起始数据地址
- 只要片选不拉高,就会一直去读取芯片的数据,指导整个芯片数据读取完成
页编程指令:0x02 256字节
- CS片选拉低发送指令,CS片选拉高结束读指令结束
- 先写使能(0x02)
- 写入之前先把写入的扇区擦除,擦除写0xff
- 先发送0x02指令,后面跟24位的起始数据地址
- 注意不要超出一个页的范围(256)个字节,除非写完一个页后把地址改为0
- 页写指令不能跨页,所以写数据地址要保持256的倍数
👉扇区擦除指令:0x20 擦除4K
- CS片选拉低发送指令,CS片选拉高结束读指令结束
- 先写使能(0x02)
- 先发送0x20指令,后面跟24位的起始数据地址
- CS片选拉高结束写指令,片选拉晚或者没有拉高,写入的值将无效
👉块区擦除指令:0xD8 擦除64K
- CS片选拉低发送指令,CS片选拉高结束读指令结束
- 先写使能(0x02)
- 先发送0xD8指令,后面跟24位的起始数据地址
- CS片选拉高结束写指令,片选拉晚或者没有拉高,写入的值将无效
👉芯片擦除指令:0xC7 擦除8M
- CS片选拉低发送指令,CS片选拉高结束读指令结束
- 先写使能(0x02)
- 先发送0xC7指令
- CS片选拉高结束写指令,片选拉晚或者没有拉高,写入的值将无效
👉读取/制造器件号指令:0x90
- CS片选拉低发送指令,CS片选拉高结束读指令结束
- 先发送0x90指令
- 发送24位0地址(0x0)
- 发送8位任意数据产生时钟,接收8位返回ID,高位在前
3.指令发送
👽写使能
void W25QXX_WriteEnable(void) { W25Q64_CS_LOW; sendByte(0x06); // 命令: Write Enable : 06h W25Q64_CS_HIGH; }
👹写禁能
void W25QXX_WriteDisable(void) { W25Q64_CS_LOW; sendByte(0x04); // 命令: Write Disable : 04h W25Q64_CS_HIGH; }
👻读状态寄存器,空闲状态
void W25QXX_WaitBusy(void) { W25Q64_CS_LOW ; sendByte(0x05); while (sendByte(0xFF) & 1) {} W25Q64_CS_HIGH ; }
👦擦除扇区
void W25QXX_EraseSector(uint32_t addr) { addr = addr * 4096; // 从第几扇区开始 W25QXX_WriteEnable(); //写使能 W25QXX_WaitBusy(); //等待空闲 W25Q64_CS_LOW; //拉低 sendByte(0x20); //发送扇区擦除指令 sendByte((uint8_t)(addr >> 16));//发送24位地址的最高8位 sendByte((uint8_t)(addr >> 8));//发送24位地址的中间8位 sendByte((uint8_t)addr);//发送24位地址的最低8位 W25Q64_CS_HIGH; //拉高 W25QXX_WaitBusy(); //等待空闲 printf("\r\n擦除完成!\r\n"); }
👧页写
//SPI在一页(0~65535)内写入少于256个字节的数据--分扇区写进去 //在指定地址开始写入最大256字节的数据 //pBuffer:数据存储区 //WriteAddr:开始写入的地址(24bit) //NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!! void W25QXX_Write_Page(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite) { u16 i; W25QXX_WriteEnable(); //SET WEL W25QXX_WaitBusy(); W25Q64_CS_LOW ; //使能器件 sendByte(0x02); //发送写页命令 sendByte((u8)((WriteAddr)>>16)); //发送24bit地址 sendByte((u8)((WriteAddr)>>8)); sendByte((u8)WriteAddr); for(i=0;i<NumByteToWrite;i++)sendByte(pBuffer[i]);//循环写数 W25Q64_CS_HIGH ; //取消片选 W25QXX_WaitBusy(); //等待写入结束 }
🙅无校验写
/***********************************函数5.7****************************************************/ //无检验写SPI FLASH //必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败! //具有自动换页功能 //在指定地址开始写入指定长度的数据,但是要确保地址不越界! //pBuffer:数据存储区 //WriteAddr:开始写入的地址(24bit) //NumByteToWrite:要写入的字节数(最大65535) //CHECK OK void W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite) { u16 pageremain; pageremain=256-WriteAddr%256; //单页剩余的字节数 if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;//不大于256个字节 while(1) { W25QXX_Write_Page(pBuffer,WriteAddr,pageremain); if(NumByteToWrite==pageremain)break;//写入结束了 else //NumByteToWrite>pageremain { pBuffer+=pageremain; WriteAddr+=pageremain; NumByteToWrite-=pageremain; //减去已经写入了的字节数 if(NumByteToWrite>256)pageremain=256; //一次可以写入256个字节 else pageremain=NumByteToWrite; //不够256个字节了 } }; }
💁读取数据
/************************************函数5.10************************************************************** * 函 数 名 : W25QXX_ReadDatas * 功能说明 : 读取数据 * 形 参 : buffer:数据缓存区,addr:开始读取的地址(24bit),num_data:要读取的字节数(最大32768) * 返 回 值 : 无 * 备 注 : 无 指令---03H *********************************************************************************************************/ void W25QXX_ReadDatas(uint8_t *buffer,uint32_t addr, uint16_t num) { W25Q64_CS_LOW ; sendByte(0x03); // 发送读取命令 03h sendByte((uint8_t)(addr >> 16)); sendByte((uint8_t)(addr >> 8)); sendByte((uint8_t)addr); for (uint32_t i = 0; i < num; i++) { buffer[i] = sendByte(0xFF); } W25Q64_CS_HIGH ; }
👮读取设备ID
/****************************************函数5.11*********************************************************** * 函 数 名 : W25QXX_ReadID * 功能说明 : 读器件ID号 * 形 参 : 0XEF17,表示芯片型号为W25Q128 * 返 回 值 : 0XEF13,表示芯片型号为W25Q08 0XEF14,表示芯片型号为W25Q16 * 备 注 : 0XEF15,表示芯片型号为W25Q32 0XEF16,表示芯片型号为W25Q64 *********************************************************************************************************/ uint32_t readID(void) { uint16_t Temp = 0; W25Q64_CS_LOW; sendByte(0x90);//发送读取ID命令 sendByte(0x00); sendByte(0x00); sendByte(0x00); Temp |= sendByte(0xFF) << 8; Temp |= sendByte(0xFF); W25Q64_CS_HIGH; xW25Q64.FlagInit = 1; //字库判断,防止被擦除 if(Temp==W25Q64) //0XEF16 { sprintf((char *)xW25Q64.type, "%s", "W25Q64"); } else { sprintf((char *)xW25Q64.type, "%s", "Flash设备失败 !!!"); xW25Q64.FlagInit = 0; printf("读取到的错误型号数据:%x\r\n", Temp); } if (xW25Q64.FlagInit == 1) printf("Flash存储 检测... ID为:0x%x 型号:%s\r\n",Temp ,xW25Q64.type); else printf("数据存储检测: 型号读取错误,设备不可用!\r\n"); return Temp; }
四、字库烧写
/********************************************************************************************************* * 函 数 名 : USART1_W25QXX * 功能说明 : 通过串口往W25Q烧写数据 * 形 参 : sector:起止扇区编号,n:准备写入的数据占据的扇区个数 * 返 回 值 : 无 * 备 注 : 无 *********************************************************************************************************/ void USART1_W25QXX(uint16_t sector,uint16_t n) { uint8_t buf;//缓冲区 uint16_t i; uint32_t addr = sector * 4096; //擦除数据 for(i=0;i<n;i++) { W25QXX_EraseSector(sector+i); //删除扇区数据 } USART_ITConfig(USART1,USART_IT_RXNE,DISABLE);//关闭串口中断 USART_ITConfig(USART1,USART_IT_IDLE,DISABLE);//关闭空闲中断 while(1) { if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE)) { buf=USART_ReceiveData(USART1); W25QXX_Write_shaoxie(&buf,addr++,1); printf("写入成功\r\n"); } } }
根据字库的大小,计算需要写的扇区,一个扇区占4K的数据,256K的字库,需要64个扇区的空间,所以需要写64个扇区。,烧写之前先擦除数据
烧写二进制文件,bin为后缀的
读取字库的字
W25QXX_ReadDatas(New_Ch,((buf[j]-0xA1)*94+(*(&buf[j]+1)-0xA1))*32+(num*4096),32);
buf:表示你要读取的字
num:你烧写的字库的扇区起始位置