智能硬件设备的MCU下面,常常会挂一个SPI Flash,用于存放字库等文件。容量不会太大,16MB左右。今天记录一下通过SPI接口对其进行操作。
这个图是SPI的接口结构图。主机写数据寄存器,通过 MOSI 信号线 传送给从机,从机也将自己的移位寄存器中的内容通过 MISO 信号线返回给主机。这样,两个移位寄存器中的内容就被交换。 如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。最后这句要理解,如果要读从机,除了发读命令,还要写空数据到从机,把从机中的数据挤出来。
SPI的配置中,有两个比特要注意。CPOL用来配置空闲的时候,CLK电平的高低。
CPHA用来控制采样时刻。CPHA=1的时候,采样发生在CS变低后的第二个沿,无论是下降沿还是上升沿。CPHA=0的时候,采样发生在CS变低后的第一个沿。这个需要查看从机的时序来确定怎么配置。ST的MCU,NSS管脚可以选择用硬件控制,也可以用软件控制,软件控制就是写GPIO,输出高低。ST的SPI口的其余配置就很简单了。
接下来介绍一下这颗SPI Flash。W25Q128 将 16MB 的容量分为 256 个块( Block),每个块大小为 64K 字节,每个块又分为16 个扇区( Sector),每个扇区 4K 个字节。 W25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。这样我们需要给 W25Q128 开辟一个至少 4K 的缓存区。每个扇区又分为16个页(page),每个page
256B, 可以对整个page进行写操作。
//数据读写函数,这个函数主要用来发送控制命令
u8 SPI1_ReadWriteByte(u8 TxData) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET){}//等待发送缓冲区为空,SR寄存器的TXE位 SPI_I2S_SendData(SPI1, TxData); //往DR寄存器写入要发送的值,即是发送数据 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET){} //等待接收缓冲区为空 return SPI_I2S_ReceiveData(SPI1); //缓冲区空了,数据已经到DR寄存器了,就可以读了。 }
//读状态寄存器
u8 W25QXX_ReadSR(void) { u8 byte=0; W25QXX_CS=0; SPI1_ReadWriteByte(W25X_ReadStatusReg); // W25X_ReadStatusReg是读状态寄存器指令,0x05; byte=SPI1_ReadWriteByte(0Xff); // 写个无效数据,把要读取的数据移出来 W25QXX_CS=1; // return byte; }
这个是读ID的指令,代码如下:
u16 W25QXX_ReadID(void) { u16 Temp = 0; W25QXX_CS=0; SPI1_ReadWriteByte(0x90);// 发指令 SPI1_ReadWriteByte(0x00); //dummy SPI1_ReadWriteByte(0x00); //dummy SPI1_ReadWriteByte(0x00); Temp|=SPI1_ReadWriteByte(0xFF)<<8; //读MF7-MF0 Temp|=SPI1_ReadWriteByte(0xFF); //读ID7-ID0 W25QXX_CS=1; return Temp; }
以上是读数据的时序,下面是代码
void W25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead) //要放入的数组;读地址;要读的数据个数 { u16 i; W25QXX_CS=0; // SPI1_ReadWriteByte(W25X_ReadData); // 03h SPI1_ReadWriteByte((u8)((ReadAddr)>>16)); // 地址23~16 SPI1_ReadWriteByte((u8)((ReadAddr)>>8)); // 地址15~8 SPI1_ReadWriteByte((u8)ReadAddr); // 地址7~0 for(i=0;i<NumByteToRead;i++) { pBuffer[i]=SPI1_ReadWriteByte(0XFF); // 发送dummy,移出读取数据 } W25QXX_CS=1; }
//这个函数是用来page写,page写需要满足下面的条件。page都已经被擦除了,而且写使能已经执行了
//The Page Program instruction allows from one byte to 256 bytes (a page) of data to be programmed at
//previously erased (FFh) memory locations. A Write Enable instruction must be executed before the device
//will accept the Page Program Instruction (Status Register bit WEL= 1).
//这个函数使用的前提是,这个page被擦干净了,所以这个函数是不会被单独调用的,会在另外一个函数中被引用
void W25QXX_Write_Page(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite) //NumByteToWrite不能超过一个page的大小 { u16 i; W25QXX_Write_Enable(); //写使能 W25QXX_CS=0; // SPI1_ReadWriteByte(W25X_PageProgram); // page编程指令 SPI1_ReadWriteByte((u8)((WriteAddr)>>16)); // 地址23~16 SPI1_ReadWriteByte((u8)((WriteAddr)>>8)); //地址15~8 SPI1_ReadWriteByte((u8)WriteAddr); //地址7~0 for(i=0;i<NumByteToWrite;i++)
SPI1_ReadWriteByte(pBuffer[i]); // 循环操作 W25QXX_CS=1; // W25QXX_Wait_Busy(); // }
下面这个函数,写入的数据要大于一个page。然后控制写入地址的偏移,把数据分割成小块,然后再调用上面的Page写函数。
pageremain表示这个page中要写入的数据个数
void W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite) { u16 pageremain; pageremain=256-WriteAddr%256; //要写入的地址所在的page,还剩余多少空间 if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;// 如果要写入的数据,连第一个page也填不满 while(1) { W25QXX_Write_Page(pBuffer,WriteAddr,pageremain); if(NumByteToWrite==pageremain)break;//一个page都没满,这就写完了 else //还需要写到下一个page { pBuffer+=pageremain; //地址偏移 WriteAddr+=pageremain; NumByteToWrite-=pageremain; //已经写掉的去除 if(NumByteToWrite>256)pageremain=256; // else pageremain=NumByteToWrite; // } }; }
以下是真正的写,会涉及到擦除,会调用上面的函数
u8 W25QXX_BUFFER[4096]; //先开辟一个4K的空间 void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite) { u32 secpos; u16 secoff; u16 secremain; u16 i; u8 * W25QXX_BUF; W25QXX_BUF=W25QXX_BUFFER; secpos=WriteAddr/4096;//获得sector号 secoff=WriteAddr%4096;// sector中的偏移 secremain=4096-secoff;// sector中剩余空间//printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用 if(NumByteToWrite<=secremain)secremain=NumByteToWrite;// 思路和上面的函数类似 while(1) { W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//因为后面可能需要擦除,所以要把sector读出来 for(i=0;i<secremain;i++)// { if(W25QXX_BUF[secoff+i]!=0XFF)break; //碰到非FF的,就需要擦除了 } if(i<secremain)//跳出了for循环,说明碰到非FF了 { W25QXX_Erase_Sector(secpos);// 擦除这个sector for(i=0;i<secremain;i++) // { W25QXX_BUF[i+secoff]=pBuffer[i]; //左边这个数据已经把整个sector读出来了,右边这个是需要写入的数据,右边把左边覆盖掉,这个是指针操作,所以可以这样 } W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096); //虽然真正写入的是sector后面一部分,但是由于整个都擦除了,所以需要都写 }else W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain); // 发现剩余部分没有非FF,那就直接全部写入 if(NumByteToWrite==secremain)break;// 要写入的都在一个sector里面,就一次写完了,可以跳出 else// { secpos++;// 转到下一个sector secoff=0;// 到了一个新的sector,就是从偏移地址0开始写 pBuffer+=secremain; // WriteAddr+=secremain;// NumByteToWrite-=secremain; // if(NumByteToWrite>4096)secremain=4096; // 这个和page操作类似 else secremain=NumByteToWrite; // } }; }