GD32F4学习之路第六章----SPI读写w25q128
本文档详细介绍了SPI协议与W25Q128 Flash存储器的技术实现。主要内容包括:SPI协议的通信原理、四种工作模式及硬件接口定义;W25Q128 Flash的产品特点、16MB存储结构及关键指令集;硬件连接方案与时序配置;软件驱动实现,涵盖SPI初始化、Flash识别与操作函数(如写使能、状态读取等)。文档还提供了性能分析及优化建议,为嵌入式系统开发提供了完整的技术参考方案。
1. SPI协议概述
1.1 SPI协议定义
SPI(Serial Peripheral Interface)是一种同步串行通信接口,主要用于微控制器和各种外围器件(如传感器、存储器、无线模块等)之间的短距离通信。SPI具有以下特点:
- 全双工通信(同时发送和接收)
- 主从架构(一个主设备控制多个从设备)
- 高速数据传输(可达数十MHz)
- 灵活的时钟极性和相位配置
1.2 通信原理
SPI使用4条信号线:
- SCLK(Serial Clock):时钟信号,由主设备产生
- MOSI(Master Out Slave In):主设备数据输出,从设备数据输入
- MISO(Master In Slave Out):主设备数据输入,从设备数据输出
- CS/SS(Chip Select/Slave Select):片选信号,用于选择要通信的从设备
1.3 工作模式
SPI有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)决定:
| 模式 | CPOL | CPHA | 说明 |
|---|---|---|---|
| 0 | 0 | 0 | 时钟空闲为低,第一个边沿采样 |
| 1 | 0 | 1 | 时钟空闲为低,第二个边沿采样 |
| 2 | 1 | 0 | 时钟空闲为高,第一个边沿采样 |
| 3 | 1 | 1 | 时钟空闲为高,第二个边沿采样 |
2. W25Q128 Flash概述
2.1 产品特点
W25Q128是一款128M-bit串行Flash存储器,具有以下特点:
- 存储容量:128M-bit(16MB)
- 支持标准SPI、双线SPI和四线SPI通信
- 时钟频率最高可达104MHz
- 支持扇区擦除(4KB)和块擦除(64KB)
- 页编程大小:256字节
- 擦除/编程周期:100,000次
- 数据保持时间:20年
2.2 存储特性
存储器组织结构:
- 总容量:16MB(128Mbit)
- 块大小:64KB
- 扇区大小:4KB
- 页大小:256字节
2.3 命令系统
主要指令集:
#define FLASH_WriteEnable 0x06 /* 写使能 */
#define FLASH_ReadStatusReg1 0x05 /* 读状态寄存器1 */
#define FLASH_ReadData 0x03 /* 读数据 */
#define FLASH_PageProgram 0x02 /* 页编程 */
#define FLASH_SectorErase 0x20 /* 扇区擦除 */
#define FLASH_ChipErase 0xC7 /* 芯片擦除 */
3. 硬件接口设计
3.1 引脚定义
在GD32F427ZGT6开发板上的连接:
/* SPI引脚定义 */
#define NORFLASH_CS_PIN GPIO_PIN_14
#define NORFLASH_CS_PORT GPIOB
#define NORFLASH_SCK_PIN GPIO_PIN_3
#define NORFLASH_MISO_PIN GPIO_PIN_4
#define NORFLASH_MOSI_PIN GPIO_PIN_5
3.2 时序要求
当前实现的时序配置:
spi_init_struct.clock_polarity_phase = SPI_CK_PL_HIGH_PH_2EDGE; /* 时钟空闲为高,第二个边沿采样 */
spi_init_struct.prescale = SPI_PSC_256; /* 默认使用256分频,速度较慢 */
4. 软件实现
4.1 SPI驱动
SPI初始化配置:
void spi0_init(void)
{
spi_parameter_struct spi_init_struct;
/* GPIO配置 */
gpio_af_set(GPIOB, GPIO_AF_5, GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5);
gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP,
GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5);
/* SPI参数配置 */
spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX; /* 全双工模式 */
spi_init_struct.device_mode = SPI_MASTER; /* 主机模式 */
spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT; /* 8位数据帧 */
spi_init_struct.nss = SPI_NSS_SOFT; /* 软件控制片选 */
spi_init_struct.endian = SPI_ENDIAN_MSB; /* MSB优先 */
spi_init(SPI0, &spi_init_struct);
spi_enable(SPI0);
}
4.2 W25Q128详细操作函数
4.2.1 基础操作函数
1. 初始化函数
void norflash_init(void)
{
uint8_t temp;
rcu_periph_clock_enable(RCU_GPIOB); /* NORFLASH CS脚 时钟使能 */
/* 设置CS引脚PB14 推挽输出 */
gpio_mode_set(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_PIN_14);
gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_14);
NORFLASH_CS(1); /* F_CS拉高,取消片选 */
spi0_init(); /* 初始化SPI0 */
spi0_set_speed(SPI_SPEED_4); /* SPI0 切换到高速状态 25Mhz */
g_norflash_type = norflash_read_id(); /* 读取FLASH ID. */
if (g_norflash_type == W25Q256) /* SPI FLASH为W25Q256, 必须使能4字节地址模式 */
{
temp = norflash_read_sr(3); /* 读取状态寄存器3,判断地址模式 */
if ((temp & 0X01) == 0) /* 如果不是4字节地址模式,则进入4字节地址模式 */
{
norflash_write_enable(); /* 写使能 */
temp |= 1 << 1; /* ADP=1, 上电4位地址模式 */
norflash_write_sr(3, temp); /* 写SR3 */
NORFLASH_CS(0);
spi0_read_write_byte(FLASH_Enable4ByteAddr); /* 使能4字节地址指令 */
NORFLASH_CS(1);
}
}
//printf("ID:%x\r\n", g_norflash_type);
}
- 功能:初始化SPI NOR FLASH,配置GPIO,设置SPI参数,识别Flash型号
- 参数:无
- 返回值:无
- 说明:
- 配置CS引脚为推挽输出
- 初始化SPI接口
- 读取Flash ID识别型号
- 对于W25Q256型号,需要特殊处理4字节地址模式
2. 等待空闲函数
static void norflash_wait_busy(void)
{
while ((norflash_read_sr(1) & 0x01) == 0x01); /* 等待BUSY位清空 */
}
- 功能:等待Flash处于空闲状态
- 参数:无
- 返回值:无
- 说明:通过读取状态寄存器1的BUSY位判断Flash是否处于忙状态
3. 写使能函数
void norflash_write_enable(void)
{
NORFLASH_CS(0);
spi0_read_write_byte(FLASH_WriteEnable); /* 发送写使能 */
NORFLASH_CS(1);
}
- 功能:使能Flash写操作
- 参数:无
- 返回值:无
- 说明:在进行写入、擦除等操作前必须先调用此函数
4. 发送地址函数
static void norflash_send_address(uint32_t address)
{
if (g_norflash_type == W25Q256) /* 只有W25Q256支持4字节地址模式 */
{
spi0_read_write_byte((uint8_t)((address)>>24)); /* 发送 bit31 ~ bit24 地址 */
}
spi0_read_write_byte((uint8_t)((address)>>16)); /* 发送 bit23 ~ bit16 地址 */
spi0_read_write_byte((uint8_t)((address)>>8)); /* 发送 bit15 ~ bit8 地址 */
spi0_read_write_byte((uint8_t)address); /* 发送 bit7 ~ bit0 地址 */
}
- 功能:向Flash发送地址
- 参数:
- address:要发送的地址(最大32bit)
- 返回值:无
- 说明:根据Flash型号发送24位或32位地址
4.2.2 状态寄存器操作
1. 读状态寄存器
uint8_t norflash_read_sr(uint8_t regno)
{
uint8_t byte = 0, command = 0;
switch (regno)
{
case 1:
command = FLASH_ReadStatusReg1; /* 读状态寄存器1指令 */
break;
case 2:
command = FLASH_ReadStatusReg2; /* 读状态寄存器2指令 */
break;
case 3:
command = FLASH_ReadStatusReg3; /* 读状态寄存器3指令 */
break;
default:
command = FLASH_ReadStatusReg1;
break;
}
NORFLASH_CS(0);
spi0_read_write_byte(command); /* 发送读寄存器命令 */
byte = spi0_read_write_byte(0Xff); /* 读取一个字节 */
NORFLASH_CS(1);
return byte;
}
- 功能:读取Flash状态寄存器
- 参数:
- regno:状态寄存器号(1~3)
- 返回值:状态寄存器值
- 说明:
- 状态寄存器1:包含BUSY、WEL、写保护等标志
- 状态寄存器2:包含QE(四线使能)等标志
- 状态寄存器3:包含地址模式等标志
2. 写状态寄存器
void norflash_write_sr(uint8_t regno, uint8_t sr)
{
uint8_t command = 0;
switch (regno)
{
case 1:
command = FLASH_WriteStatusReg1; /* 写状态寄存器1指令 */
break;
case 2:
command = FLASH_WriteStatusReg2; /* 写状态寄存器2指令 */
break;
case 3:
command = FLASH_WriteStatusReg3; /* 写状态寄存器3指令 */
break;
default:
command = FLASH_WriteStatusReg1;
break;
}
NORFLASH_CS(0);
spi0_read_write_byte(command); /* 发送写状态寄存器命令 */
spi0_read_write_byte(sr); /* 写入一个字节 */
NORFLASH_CS(1);
}
- 功能:写Flash状态寄存器
- 参数:
- regno:状态寄存器号(1~3)
- sr:要写入的值
- 返回值:无
- 说明:用于设置写保护、QE位、地址模式等
4.2.3 ID与数据读取
1. 读取芯片ID
uint16_t norflash_read_id(void)
{
uint16_t deviceid;
NORFLASH_CS(0);
spi0_read_write_byte(FLASH_ManufactDeviceID); /* 发送读 ID 命令 */
spi0_read_write_byte(0); /* 写入一个字节 */
spi0_read_write_byte(0);
spi0_read_write_byte(0);
deviceid = spi0_read_write_byte(0xFF) << 8; /* 读取高8位字节 */
deviceid |= spi0_read_write_byte(0xFF); /* 读取低8位字节 */
NORFLASH_CS(1);
return deviceid;
}
- 功能:读取Flash芯片ID
- 参数:无
- 返回值:Flash芯片ID
- 说明:通过ID可以识别Flash型号,如W25Q128的ID为0xEF17
2. 读取数据
void norflash_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
uint16_t i;
NORFLASH_CS(0);
spi0_read_write_byte(FLASH_ReadData); /* 发送读取命令 */
norflash_send_address(addr); /* 发送地址 */
for(i=0;i<datalen;i++)
{
pbuf[i] = spi0_read_write_byte(0XFF); /* 循环读取 */
}
NORFLASH_CS(1);
}
- 功能:从Flash读取数据
- 参数:
- pbuf:数据存储区指针
- addr:起始地址
- datalen:要读取的字节数(最大65535)
- 返回值:无
- 说明:使用标准读取命令(0x03)读取数据
4.3 读写操作
4.3.1 写入操作
1. 页写入函数
static void norflash_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
uint16_t i;
norflash_write_enable(); /* 写使能 */
NORFLASH_CS(0);
spi0_read_write_byte(FLASH_PageProgram); /* 发送写页命令 */
norflash_send_address(addr); /* 发送地址 */
for(i=0;i<datalen;i++)
{
spi0_read_write_byte(pbuf[i]); /* 循环读取 */
}
NORFLASH_CS(1);
norflash_wait_busy(); /* 等待写入结束 */
}
- 功能:在一页内写入数据
- 参数:
- pbuf:数据存储区指针
- addr:起始地址
- datalen:要写入的字节数(最大256)
- 返回值:无
- 说明:
- 一次最多写入256字节
- 写入前需要先调用写使能函数
- 写入后需要等待操作完成
2. 无检验写入函数
static void norflash_write_nocheck(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
uint16_t pageremain;
pageremain = 256 - addr % 256; /* 单页剩余的字节数 */
if (datalen <= pageremain) /* 不大于256个字节 */
{
pageremain = datalen;
}
while (1)
{
/* 当写入字节比页内剩余地址还少的时候, 一次性写完
* 当写入直接比页内剩余地址还多的时候, 先写完整个页内剩余地址, 然后根据剩余长度进行不同处理
*/
norflash_write_page(pbuf, addr, pageremain);
if (datalen == pageremain) /* 写入结束了 */
{
break;
}
else /* datalen > pageremain */
{
pbuf += pageremain; /* pbuf指针地址偏移,前面已经写了pageremain字节 */
addr += pageremain; /* 写地址偏移,前面已经写了pageremain字节 */
datalen -= pageremain; /* 写入总长度减去已经写入了的字节数 */
if (datalen > 256) /* 剩余数据还大于一页,可以一次写一页 */
{
pageremain = 256; /* 一次可以写入256个字节 */
}
else /* 剩余数据小于一页,可以一次写完 */
{
pageremain = datalen; /* 不够256个字节了 */
}
}
}
}
- 功能:无需检验直接写入数据
- 参数:
- pbuf:数据存储区指针
- addr:起始地址
- datalen:要写入的字节数(最大65535)
- 返回值:无
- 说明:
- 必须确保写入区域已被擦除(全为0xFF)
- 具有自动换页功能
- 内部调用norflash_write_page实现
3. 带擦除的写入函数
void norflash_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
uint32_t secpos;
uint16_t secoff;
uint16_t secremain;
uint16_t i;
uint8_t *norflash_buf;
norflash_buf = g_norflash_buf;
secpos = addr / 4096; /* 扇区地址 */
secoff = addr % 4096; /* 在扇区内的偏移 */
secremain = 4096 - secoff; /* 扇区剩余空间大小 */
//printf("ad:%X,nb:%X\r\n", addr, datalen); /* 测试用 */
if (datalen <= secremain)
{
secremain = datalen; /* 不大于4096个字节 */
}
while (1)
{
norflash_read(norflash_buf, secpos * 4096, 4096); /* 读出整个扇区的内容 */
for (i = 0; i < secremain; i++) /* 校验数据 */
{
if (norflash_buf[secoff + i] != 0XFF)
{
break; /* 需要擦除, 直接退出for循环 */
}
}
if (i < secremain) /* 需要擦除 */
{
norflash_erase_sector(secpos); /* 擦除这个扇区 */
for (i = 0; i < secremain; i++) /* 复制 */
{
norflash_buf[i + secoff] = pbuf[i];
}
norflash_write_nocheck(norflash_buf, secpos * 4096, 4096); /* 写入整个扇区 */
}
else /* 写已经擦除了的,直接写入扇区剩余区间. */
{
norflash_write_nocheck(pbuf, addr, secremain); /* 直接写扇区 */
}
if (datalen == secremain)
{
break; /* 写入结束了 */
}
else /* 写入未结束 */
{
secpos++; /* 扇区地址增1 */
secoff = 0; /* 偏移位置为0 */
pbuf += secremain; /* 指针偏移 */
addr += secremain; /* 写地址偏移 */
datalen -= secremain; /* 字节数递减 */
if (datalen > 4096)
{
secremain = 4096; /* 下一个扇区还是写不完 */
}
else
{
secremain = datalen;/* 下一个扇区可以写完了 */
}
}
}
}
- 功能:写入数据(带擦除操作)
- 参数:
- pbuf:数据存储区指针
- addr:起始地址
- datalen:要写入的字节数(最大65535)
- 返回值:无
- 说明:
- 写入前会检查目标区域是否需要擦除
- 如需擦除,会先读取整个扇区,修改后再写回
- 如不需擦除,直接调用norflash_write_nocheck写入
写入操作流程:
- 扇区检查:检查目标地址所在扇区是否需要擦除
- 扇区擦除:如果需要,执行扇区擦除操作
- 页编程:按页(256字节)写入数据
- 等待完成:等待写入操作完成
void norflash_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
uint32_t secpos = addr / 4096; /* 扇区地址 */
uint16_t secoff = addr % 4096; /* 扇区内偏移 */
uint16_t secremain = 4096 - secoff; /* 扇区剩余空间 */
uint16_t i;
if (datalen <= secremain) secremain = datalen;
while(1)
{
/* 读取整个扇区 */
norflash_read(g_norflash_buf, secpos * 4096, 4096);
/* 检查是否需要擦除 */
for(i = 0; i < secremain; i++)
{
if(g_norflash_buf[secoff + i] != 0XFF) break;
}
/* 需要擦除的情况 */
if(i < secremain)
{
norflash_erase_sector(secpos); /* 擦除这个扇区 */
/* 复制数据到扇区缓存 */
for(i = 0; i < secremain; i++)
{
g_norflash_buf[secoff + i] = pbuf[i];
}
/* 写入整个扇区 */
norflash_write_nocheck(g_norflash_buf, secpos * 4096, 4096);
}
else /* 不需要擦除,直接写入 */
{
norflash_write_nocheck(pbuf, addr, secremain);
}
/* 处理写入结束或继续写入下一个扇区 */
if(datalen == secremain) break; /* 写入结束 */
else /* 继续写入下一个扇区 */
{
secpos++; /* 扇区地址增1 */
secoff = 0; /* 偏移位置为0 */
pbuf += secremain; /* 指针偏移 */
addr += secremain; /* 写地址偏移 */
datalen -= secremain; /* 字节数递减 */
/* 计算下一个扇区需要写入的字节数 */
if(datalen > 4096) secremain = 4096;
else secremain = datalen;
}
}
}
4.3.2 擦除操作
1. 扇区擦除函数
void norflash_erase_sector(uint32_t saddr)
{
//printf("fe:%x\r\n", saddr); /* 监视flash擦除情况,测试用 */
saddr *= 4096;
norflash_write_enable(); /* 写使能 */
norflash_wait_busy(); /* 等待空闲 */
NORFLASH_CS(0);
spi0_read_write_byte(FLASH_SectorErase); /* 发送扇区擦除指令 */
norflash_send_address(saddr); /* 发送地址 */
NORFLASH_CS(1);
norflash_wait_busy(); /* 等待扇区擦除完成 */
}
- 功能:擦除一个扇区
- 参数:
- saddr:扇区地址(注意是扇区号,不是字节地址)
- 返回值:无
- 说明:
- 擦除一个4KB扇区
- 擦除时间约150ms
- 擦除后扇区内容全为0xFF
2. 芯片擦除函数
void norflash_erase_chip(void)
{
norflash_write_enable(); /* 写使能 */
norflash_wait_busy(); /* 等待空闲 */
NORFLASH_CS(0);
spi0_read_write_byte(FLASH_ChipErase); /* 发送芯片擦除命令 */
NORFLASH_CS(1);
norflash_wait_busy(); /* 等待芯片擦除结束 */
}
- 功能:擦除整个芯片
- 参数:无
- 返回值:无
- 说明:
- 擦除整个芯片内容
- 擦除时间非常长(数十秒)
- 擦除后所有内容全为0xFF
5. 性能优化
5.1 当前实现分析
当前实现存在以下可优化点:
-
SPI速度配置
- 默认使用256分频,速度较慢
- W25Q128支持最高104MHz时钟频率
-
数据传输效率
- 每次只传输一个字节
- 未使用DMA传输
- 未使用双线/四线模式
-
写入操作
- 每次写入都要检查整个扇区
- 小数据量写入也需要读取整个扇区
5.2 优化建议
- 提高SPI时钟频率
/* 根据实际情况选择合适的分频系数 */
void spi0_set_speed(uint8_t speed)
{
/* 建议从高速开始尝试,如果不稳定再降低速度 */
spi0_set_speed(SPI_SPEED_2); /* 50MHz */
}
- 使用DMA传输
/* 添加DMA传输支持 */
void norflash_read_dma(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
/* 配置DMA传输 */
dma_config();
/* 启动传输 */
NORFLASH_CS(0);
spi0_read_write_byte(FLASH_ReadData);
norflash_send_address(addr);
/* 使用DMA接收数据 */
dma_transfer(pbuf, datalen);
NORFLASH_CS(1);
}
- 实现双线/四线模式
/* 添加快速读取命令支持 */
#define FLASH_FastRead 0x0B /* 快速读取 */
#define FLASH_FastReadDual 0x3B /* 双线快速读取 */
#define FLASH_FastReadQuad 0x6B /* 四线快速读取 */
- 写入优化
/* 添加缓存机制 */
#define SECTOR_CACHE_SIZE 4 /* 缓存4个扇区 */
struct sector_cache {
uint32_t addr; /* 扇区地址 */
uint8_t data[4096]; /* 扇区数据 */
uint8_t dirty; /* 是否被修改 */
};
struct sector_cache sector_caches[SECTOR_CACHE_SIZE];
- 批量操作优化
/* 添加批量擦除支持 */
void norflash_erase_block(uint32_t block_addr)
{
/* 64KB块擦除,比多次执行4KB扇区擦除更快 */
norflash_write_enable();
NORFLASH_CS(0);
spi0_read_write_byte(FLASH_BlockErase);
norflash_send_address(block_addr);
NORFLASH_CS(1);
norflash_wait_busy();
}
- 状态检查优化
/* 使用轮询替代忙等待 */
void norflash_wait_busy_with_timeout(uint32_t timeout)
{
uint32_t start = get_systick();
while ((norflash_read_sr(1) & 0x01) == 0x01)
{
if (get_systick() - start > timeout)
{
/* 超时处理 */
break;
}
/* 可以添加延时或让出CPU */
delay_us(100);
}
}
这些优化建议可以根据实际应用场景选择性实施。在实施优化时需要注意:
- 可靠性测试:提高SPI速度后需要充分测试系统稳定性
- 资源平衡:DMA和缓存机制会占用更多系统资源
- 兼容性:部分优化可能需要修改上层应用代码
- 功耗考虑:更高的时钟频率会增加功耗
建议根据实际应用需求,选择合适的优化方案:
- 对于追求性能的应用:实现DMA传输和多线模式
- 对于小容量随机写入:实现扇区缓存机制
- 对于大块数据传输:使用块擦除和批量传输
- 对于低功耗要求:保持较低的时钟频率,实现休眠机制
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐

所有评论(0)