引用:笔者对SPI通信有一定了解,但是没有用SPI通信过W25Q64。初次使用时,用MCU对W25Q64进行读写操作时,软件设计需要知道W25Q64哪些内容呢?笔者总结如下
一、W25Q64 核心硬件参数
| 参数项 | 数值 | 软件设计意义 |
|---|---|---|
| 总容量 | 64M-bit = 8MB = 8388608 字节 | 最大地址:0x7FFFFF(24 位地址) |
| 最小写入单位 | 页 (Page) | 1 页 = 256 字节(=0.25KB),写入不能跨页! |
| 总页数 | 32768 页 | 8MB ÷ 256B = 32768 |
| 最小擦除单位 | 扇区 (Sector) | 1 扇区 = 4096 字节(4KB),不能按字节 / 页擦除 |
| 总扇区数 | 2048 扇区 | 8MB ÷ 4KB = 2048 |
| 块 (Block) | 大块:64KB / 块,共 128 块小块:32KB / 块,共 256 块 | 大块擦除更快,适合批量数据 |
1.1 容量计算
存储芯片的 M不是10的6次方,而是1024KB。1MB=1024KB,1KB=1024B
W25Q64的64指的是64MBit(8MB) ,64M位,64M = 64 * 1024 * 1024 = 67,108,864 bit(位)。
67,108,864 / 8 = ++8388608 B(字节)++
8388608转16进制为0x800000,总容量为0x800000,地址可取范围为0~0x7FFFFF(8388607)
1页可以存储256个字节,8388608 / 256 = 32768 页
1扇区擦除4096 B,4096 / 256 = 16 页,1扇区擦除16页
1.2 如何得到每一页的首尾地址
W25Q64采用24位地址,++低8位为页内偏移地址,高15位为页号++
0x [高15位] [低8位],++我们存储的数据仅在低8位++
即:
第1页的首地址为0x 000000 第1页的尾地址为0x 0000FF
第2页的首地址为0x 000100 第2页的尾地址为0x 0001FF
第3页的首地址为0x 000200 第3页的尾地址为0x 0002FF
++第n页的首地址为0x (n-1)00++ 第n页的尾地址为0x (n-1)FF
二、W25Q64相关常识
2.1 W25Q64核心指令
| 功能 | 指令码 | 软件操作 |
|---|---|---|
| 芯片ID | 0x9F | 读取芯片ID |
| 写使能 | 0x06 | 写入 / 擦除前必须先发 |
| 状态寄存器(等待忙) | 0x05 | 查「忙位」,判断操作是否完成 |
| 读数据 | 0x03 | 任意地址、任意长度读取 |
| 页编程(写入) | 0x02 | 最多写 256 字节(1 页) |
| 扇区擦除 | 0x20 | 擦除 4KB(最小擦除) |
| 全片擦除 | 0xC7 | 擦除整个芯片 |
2.2 SPI通信
以软件SPI为例,相关程序:
4根通信线的定义
cpp
void MySPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
void MySPI_W_SCK(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}
void MySPI_W_MOSI(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}
uint8_t MySPI_R_MISO(void)
{
return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}
SPI协议
cpp
void MySPI_Start(void)
{
MySPI_W_SS(0); //拉低SS,开始时序
}
void MySPI_Stop(void)
{
MySPI_W_SS(1); //拉高SS,终止时序
}
/**
* 函 数:SPI交换传输一个字节,使用SPI模式0
* 参 数:ByteSend 要发送的一个字节
* 返 回 值:接收的一个字节
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
uint8_t i, ByteReceive = 0x00; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
for (i = 0; i < 8; i ++) //循环8次,依次交换每一位数据
{
MySPI_W_MOSI(ByteSend & (0x80 >> i)); //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线
MySPI_W_SCK(1); //拉高SCK,上升沿移出数据
if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);} //读取MISO数据,并存储到Byte变量
//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0
MySPI_W_SCK(0); //拉低SCK,下降沿移入数据
}
return ByteReceive; //返回接收到的一个字节数据
}
SPI通信实际就只用到3条函数:
MySPI_Start()、MySPI_SwapByte()、MySPI_Stop()
SPI起始信号(拉低CS片选)、交换字节、SPI结束信号(拉高CS片选)
因而SPI收发一次数据流的流程:
SPI起始信号->交换字节*n->SPI结束信号
2.3
三、 W25Q64部分功能读写操作汇总
前提:
写使能 的执行流程:起始信号-------[发送] 写使能功能码 (8 位)-------结束信号
等待忙 的执行流程:起始信号-------[发送] 等待忙功能码 (8 位)-------主机状态寄存器标志位-------结束信号
3.1 读取 ID 功能执行流程
起始信号
发送\] 读取 ID 功能码 (8 位) \[读取\] 24 位 ID 数据 结束信号 ## 3.2 页编程 (页写入) 功能执行流程 写使能 起始信号 \[发送\] 页编程功能码 (8 位) \[发送\] 24 位写入起始地址 \[发送\] 8 位数据 \* n 结束信号 等待忙 ## 3.3 读取数据功能执行流程 起始信号 \[发送\]读取数据功能码 (8 位) \[发送\] 24 位读取起始地址 \[读取\] 8 位数据 \* n 结束信号 ## 3.4 扇区擦除 (4KB) 功能执行流程 写使能 起始信号 \[发送\] 扇区擦除功能码 (8 位) \[发送\] 24 位扇区地址 结束信号 等待忙 ## 3.5 全片擦除功能执行流程 写使能 起始信号 \[发送\] 全片擦除功能码 (8 位) 结束信号 等待忙 ### 总结规律: 所有功能的执行流程都是: **起始信号-----发送功能码-----数据流(接收or发送or无)-----结束信号** 至于涉及写使能和等待忙的功能: **写使能** -----**起始信号-----发送功能码-----数据流(接收or发送or无)-----结束信号-----等待忙** 在头和尾添加写使能和等待忙的操作 只读类(读 ID、读数据),无需写使能、无需等待忙 写入 / 擦除类(页写入、扇区擦除、块擦除、全片擦除),必前置写使能、必后置等待忙 ## 四、写使能与等待忙 ### 为什么需要写使能和等待忙的操作? W25Q64 默认**处于写保护锁定状态** ,**禁止任何写入、擦除操作**。进行写入擦除操作时,必须先使能。 主机给W25Q64写入批量数据时,W25Q64需要时间读取,因此需要主机接收到W25Q64接收完成的信号(等待忙后空闲),才能进行下一步操作。 ## 五、代码示例 ```cpp #include "stm32f10x.h" // Device header #include "MySPI.h" #include "W25Q64_Ins.h" /** * 函 数:W25Q64初始化 * 参 数:无 * 返 回 值:无 */ void W25Q64_Init(void) { MySPI_Init(); //先初始化底层的SPI } /** * 函 数:MPU6050读取ID号 * 参 数:MID 工厂ID,使用输出参数的形式返回 * 参 数:DID 设备ID,使用输出参数的形式返回 * 返 回 值:无 */ void W25Q64_ReadID(uint8_t *MID, uint16_t *DID) { MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_JEDEC_ID); //交换发送读取ID的指令 *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收MID,通过输出参数返回 *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收DID高8位 *DID <<= 8; //高8位移到高位 *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); //或上交换接收DID的低8位,通过输出参数返回 MySPI_Stop(); //SPI终止 } /** * 函 数:W25Q64写使能 * 参 数:无 * 返 回 值:无 */ void W25Q64_WriteEnable(void) { MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_WRITE_ENABLE); //交换发送写使能的指令 MySPI_Stop(); //SPI终止 } /** * 函 数:W25Q64等待忙 * 参 数:无 * 返 回 值:无 */ void W25Q64_WaitBusy(void) { uint32_t Timeout; MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //交换发送读状态寄存器1的指令 Timeout = 100000; //给定超时计数时间 while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) //循环等待忙标志位 { Timeout --; //等待时,计数值自减 if (Timeout == 0) //自减到0后,等待超时 { /*超时的错误处理代码,可以添加到此处*/ break; //跳出等待,不等了 } } MySPI_Stop(); //SPI终止 } /** * 函 数:W25Q64页编程 * 参 数:Address 页编程的起始地址,范围:0x000000~0x7FFFFF * 参 数:DataArray 用于写入数据的数组 * 参 数:Count 要写入数据的数量,范围:0~256 * 返 回 值:无 * 注意事项:写入的地址范围不能跨页 */ void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count) { uint16_t i; W25Q64_WriteEnable(); //写使能 MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_PAGE_PROGRAM); //交换发送页编程的指令 MySPI_SwapByte(Address >> 16); //交换发送地址23~16位 MySPI_SwapByte(Address >> 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 for (i = 0; i < Count; i ++) //循环Count次 { MySPI_SwapByte(DataArray[i]); //依次在起始地址后写入数据 } MySPI_Stop(); //SPI终止 W25Q64_WaitBusy(); //等待忙 } /** * 函 数:W25Q64扇区擦除(4KB) * 参 数:Address 指定扇区的地址,范围:0x000000~0x7FFFFF * 返 回 值:无 */ void W25Q64_SectorErase(uint32_t Address) { W25Q64_WriteEnable(); //写使能 MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); //交换发送扇区擦除的指令 MySPI_SwapByte(Address >> 16); //交换发送地址23~16位 MySPI_SwapByte(Address >> 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 MySPI_Stop(); //SPI终止 W25Q64_WaitBusy(); //等待忙 } /** * 函 数:W25Q64读取数据 * 参 数:Address 读取数据的起始地址,范围:0x000000~0x7FFFFF * 参 数:DataArray 用于接收读取数据的数组,通过输出参数返回 * 参 数:Count 要读取数据的数量,范围:0~0x800000 * 返 回 值:无 */ void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count) { uint32_t i; MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_READ_DATA); //交换发送读取数据的指令 MySPI_SwapByte(Address >> 16); //交换发送地址23~16位 MySPI_SwapByte(Address >> 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 for (i = 0; i < Count; i ++) //循环Count次 { DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //依次在起始地址后读取数据 } MySPI_Stop(); //SPI终止 } ``` ## 六、使用示例 举例子,写3张128\*64像素的图片,从0x000000的地址开始写 128\*64像素的图片,128 \* 64 = 8192 bit(位),8192 / 8 = 1024 B(字节) 一张128\*64像素的图片,占用1024个字节,3张占用3 \* 1024 = 3072 B(字节) **因此定义数组,Image\[1024\]来装一张图片的数据** **W25Q64一页能写256个字节,1024个字节需要用4页,即一张图片需要用到4页纸** | 图片编号 | 起始地址 | 结束地址 | 大小 | |-------|----------|----------|-------| | 第 1 张 | 0x000000 | 0x0003FF | 1024B | | 第 2 张 | 0x000400 | 0x0007FF | 1024B | | 第 3 张 | 0x000800 | 0x000BFF | 1024B | ```cpp uint8_t Image1[1024]; // 第1张图片 uint8_t Image2[1024]; // 第2张图片 uint8_t Image3[1024]; // 第3张图片 uint8_t ReadImageBuf[1024];// 图片读取缓存(用于验证写入是否成功) int main(void) { W25Q64_Init(); W25Q64_SectorErase(0x000000); // 擦除第1个扇区(4KB) // 单张1024字节 = 4次页写入(每次256字节,不跨页) // 写入第1张:地址0x000000 W25Q64_PageProgram(0x000000, Image1, 256);//第0页 W25Q64_PageProgram(0x000100, Image1+256, 256);//第1页 W25Q64_PageProgram(0x000200, Image1+512, 256);//第2页 W25Q64_PageProgram(0x000300, Image1+768, 256);//第3页 // 写入第2张:地址0x000400 W25Q64_PageProgram(0x000400, Image2, 256); W25Q64_PageProgram(0x000500, Image2+256, 256); W25Q64_PageProgram(0x000600, Image2+512, 256); W25Q64_PageProgram(0x000700, Image2+768, 256); // 写入第3张:地址0x000800 W25Q64_PageProgram(0x000800, Image3, 256); W25Q64_PageProgram(0x000900, Image3+256, 256); W25Q64_PageProgram(0x000A00, Image3+512, 256); W25Q64_PageProgram(0x000B00, Image3+768, 256); // 读取第1张图片验证 W25Q64_ReadData(0x000000, ReadImageBuf, 1024); // 读取第3张图片验证 W25Q64_ReadData(0x000800, ReadImageBuf, 1024); } ```