智能硬件设备的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
//这个函数是用来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
下面这个函数,写入的数据要大于一个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;i4096)secremain=4096; // 这个和page操作类似 else secremain=NumByteToWrite; // } }; }