简介:面向STM32F405与W25N01G的驱动示例工程,适合需要为MCU扩展大容量NOR Flash的嵌入式开发者。该Demo基于硬件SPI接口,完整展示W25N01G的初始化、状态寄存器读取、页编程和块擦除流程,并提供硬件连接与软件配置要点,可帮助快速实现1Gb闪存的数据管理。压缩包约812KB,共5个文件,包括2个C源文件、2个头文件和1份W25N01G芯片数据手册PDF,代码与手册配套,便于对照理解底层时序与指令操作。工程采用模块化组织,封装了写入、擦除、状态查询等操作函数,并考虑错误检测与数据可靠性,具备较强的可移植性。目前已有233人学习下载,该驱动实现可直接借鉴到真实项目的SPI Flash调试中。 前阵子做数据记录板,主控定了STM32F405,存储需求大概百来MB,要能存日志、固件备份,还要板面积小、采购省心。选来选去,最后锁定了华邦的W25N01G。这颗1Gbit的SPI NAND在工业仪表、音频采集、批量日志这类场景里很常见,容量是普通SPI NOR的几十倍,但引脚和连线基本一样,驱动逻辑也比想象中简单。这篇就把整套方案的选型思路、驱动写法、调试过程完整整理出来,给准备在F405这类MCU上接大容量Flash的朋友当一份参考。
1. 方案思路:为F405选一颗合适的SPI NAND
1.1 为什么不是NOR Flash,而是W25N01G
手上这颗F405原来的存储方案备选是W25Q128这类SPI NOR。SPI NOR的好处大家都清楚:随机读快、能XIP执行、坏块管理几乎不用操心,但容量一到几十MB甚至128MB这个档位,价格和供货都很尴尬,密度也上不去了。项目里的日志和固件备份基本是连续写入、顺序读回,访问模式非常规整,NAND这种“大块吞吐”的存储反而更契合。
W25N01G标称1Gbit,算下来128MB可用空间,主区每页2048字节,额外带128字节备用区,专门留给ECC校验、坏块标记这类元数据。它和NOR最大的区别在于:写入前必须先擦除整个块,擦除动作按块进行,不能单独擦一页。单页数据损坏或跨块读写的边界条件也比NOR严格,驱动代码里的状态机逻辑要更谨慎。
1.2 容量结构与寻址方式
W25N01G内部结构可以按三个层级理解:
- 页(Page):2048字节主区 + 128字节备用区。
- 块(Block):64页组成一块,所以一块约128KB。
- 整片布局:65536页,即1024块,总容量128MB。
寻址上,页地址占16位,列地址也就是页内偏移,用2字节表示。这里容易出问题的是,SPI NOR可以直接按列地址读任意字节,NAND则不一样,所有读写都要先经过内部的页缓冲寄存器。读数据时,芯片先把整页内容搬到页缓冲,再从缓冲里按列地址顺序移出;写数据时,先把主机发来的数据存进页缓冲,再执行命令把缓冲内容拷到阵列区。理解了这个“中间缓冲”机制,后面看命令时序会轻松很多。
我在demo阶段把整片当成一个大数组来用:页地址当作高维索引,列地址固定从0开始,整页整页操作。这个阶段先不碰文件系统和坏块管理,逻辑最清晰。
2. STM32F405侧硬件准备与工程配置
2.1 硬件连接与引脚安排
F405的SPI1可以复用PA5/PA6/PA7,也能复用PB3/PB4/PB5。我最终用的是PA5(SCK)、PA6(MISO)、PA7(MOSI),CS单独选了PA4这个普通GPIO。这里有一个重要建议:片选不要用SPI硬件NSS。W25N01G和普通NOR一样,每个命令周期都有严谨的时序要求,比如读ID必须把命令字节、地址字节一次性发完再拉高CS。硬件NSS很难按这种节奏去拼组多条命令,用软件GPIO控制最灵活,还方便逻辑分析仪抓波形。
引脚连接关系如下表:
| STM32F405 | W25N01G | 说明 |
|---|---|---|
| PA4 | /CS | 片选,软件控制,默认高 |
| PA5 | CLK | SPI时钟 |
| PA6 | DO | SPI MISO,读数据 |
| PA7 | DI | SPI MOSI,写命令和数据 |
W25N01G工作电压2.7V到3.6V,和F405的3.3V电平直接对接没有问题。除了VCC加0.1uF去耦电容,WP和HOLD这两个引脚必须上拉至VCC,否则芯片在SPI通讯过程中可能被意外拉成写保护或暂停状态。这个细节很容易被忽略,我手焊样板时吃过亏,丢帧丢得莫名其妙。
2.2 CubeMX配置与底层封装
在CubeMX里,SPI1设置为主模式,波特率预分频选/2,也就是42MHz,CPOL和CPHA都设为低,对应SPI模式0。W25N01G数据手册规格最高支持104MHz,但F405的SPI1挂在APB2总线上,APB2默认84MHz,SPI1最高只能跑42MHz,所以瓶颈反而在MCU这边。
底层代码建议封装成两个函数,后面所有命令都基于这两个函数来写:
static void nand_cs(uint8_t level) { HAL_GPIO_WritePin(NAND_CS_GPIO_Port, NAND_CS_Pin, level); } static uint8_t nand_spi_rw(uint8_t byte) { uint8_t rx = 0; HAL_SPI_TransmitReceive(&hspi1, &byte, &rx, 1, HAL_MAX_DELAY); return rx; }这样做的目的是把SPI收发细节收敛起来,后续改用DMA或者换引脚,只改这两个函数就行。
3. W25N01G驱动核心实现:让芯片开口说话
3.1 最小命令集
真正跑通一颗SPI NAND,需要的命令不多。我整理了一张最常用的清单,demo阶段照着用就行:
| 命令字 | 名称 | 作用 |
|---|---|---|
| 0x06 | Write Enable | 每次写/擦之前必须执行 |
| 0x9F | Read JEDEC ID | 读制造商ID和容量ID |
| 0x05 | Read Status Register | 查询芯片忙状态 |
| 0x03 | Read Data | 从页缓冲读取数据 |
| 0x02 | Program Data Load | 把数据装载进页缓冲 |
| 0x10 | Program Execute | 把页缓冲内容编程到阵列 |
| 0xD8 | Block Erase | 擦除整块 |
| 0xFF | Reset | 复位芯片 |
有些参考代码还会用到Set Feature/Get Feature(0x1F/0x0F)去配置内部ECC、四线模式之类,demo阶段可以先不碰,先把读写跑通。
3.2 读ID与忙状态等待
读ID是最先要验证的功能。发送0x9F后连续读3个字节,正常情况应返回EF AA 21。EF是Winbond的厂商ID,AA标识NAND类型,21表示1Gbit容量。代码实现相当直白:
uint8_t id[3]; nand_cs(0); nand_spi_rw(0x9F); id[0] = nand_spi_rw(0xFF); id[1] = nand_spi_rw(0xFF); id[2] = nand_spi_rw(0xFF); nand_cs(1); printf("Mfr=0x%02X Type=0x%02X Cap=0x%02X\r\n", id[0], id[1], id[2]);如果读到全FF或全00,大概率是接线、SPI模式或供电问题,下面第4节有详细排查思路。假如能读到EF AA 21,说明芯片已经正常响应,后面就顺利多了。
每次读写擦之后都要查询状态寄存器bit0,为0表示控制器空闲。W25N01G忙等待时间不长,整页编程大约几百微秒到1毫秒,整块擦除也就几毫秒,轮询时一定要加超时保护,我用的是循环计数,比如10万次还没变0就报错返回,避免程序卡死。
3.3 页读:从页缓冲拿数据
读一页数据的逻辑很简单:CS拉低,发送0x03,再发列地址2字节和页地址2字节,然后连续读数据。因为demo整页操作,列地址固定为0,也就是从页首开始。
static void w25n01g_read_page(uint32_t page, uint8_t *dst, uint32_t len) { uint8_t cmd[5]; cmd[0] = 0x03; cmd[1] = 0x00; // 列地址高字节 cmd[2] = 0x00; // 列地址低字节 cmd[3] = (page >> 8) & 0xFF; // 页地址高字节 cmd[4] = page & 0xFF; // 页地址低字节 nand_cs(0); HAL_SPI_Transmit(&hspi1, cmd, 5, HAL_MAX_DELAY); HAL_SPI_Receive(&hspi1, dst, len, HAL_MAX_DELAY); nand_cs(1); }如果想读备用区,把列地址改成2048起步就行。备用区一般放坏块标记和ECC校验,产品化阶段会用到。
3.4 页写与块擦除:两步走的精髓
NAND写流程和NOR区别很大,必须分两步:先把数据装进页缓冲,再发执行命令。装载命令是0x02,后面跟列地址和待写数据,一次最多装载2048字节;随后CS拉高,再发0x10和页地址,芯片才开始真正往阵列编程:
static void w25n01g_write_page(uint32_t page, uint8_t *src, uint32_t len) { uint8_t cmd[3]; // 1. 写使能 cmd[0] = 0x06; nand_cs(0); HAL_SPI_Transmit(&hspi1, cmd, 1, HAL_MAX_DELAY); nand_cs(1); // 2. 装载数据到页缓冲 cmd[0] = 0x02; cmd[1] = 0x00; // 列地址高字节 cmd[2] = 0x00; // 列地址低字节 nand_cs(0); HAL_SPI_Transmit(&hspi1, cmd, 3, HAL_MAX_DELAY); HAL_SPI_Transmit(&hspi1, src, len, HAL_MAX_DELAY); nand_cs(1); // 3. 执行编程 cmd[0] = 0x10; cmd[1] = (page >> 8) & 0xFF; cmd[2] = page & 0xFF; nand_cs(0); HAL_SPI_Transmit(&hspi1, cmd, 3, HAL_MAX_DELAY); nand_cs(1); // 4. 等待完成 w25n01g_wait_busy(); }这里有个关键点:0x06写使能和后面0x02装载命令,不能放在同一个CS低电平周期里。CS必须先拉高,让写使能生效,再开始下一个命令周期。我第一次写SPI NAND时就犯了这毛病,把0x06和0x02连在一起发,结果芯片一直不响应。
提示:写使能(0x06)和后续写/擦命令必须分为两个CS周期,中间CS要拉高一次,命令才真正生效。
擦除整块就更直接了,发送0xD8加块地址。块地址可以通过页地址右移6位得到,也可以直接用块号乘64换算成页地址再发给芯片:
static void w25n01g_erase_block(uint32_t block) { uint8_t cmd[4]; uint32_t page = block * 64; // 块号转页地址 cmd[0] = 0x06; nand_cs(0); HAL_SPI_Transmit(&hspi1, cmd, 1, HAL_MAX_DELAY); nand_cs(1); cmd[0] = 0xD8; // Block Erase cmd[1] = (page >> 16) & 0xFF; // 高位补0 cmd[2] = (page >> 8) & 0xFF; cmd[3] = page & 0xFF; nand_cs(0); HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY); nand_cs(1); w25n01g_wait_busy(); }擦除前同样要先写使能。擦除后这一整块的数据会全部变成0xFF。
3.5 demo主流程设计
我习惯把demo的main函数压缩成四个步骤:读ID、擦除第0块、向第0页写一串0x5A、读回比较。串口打印PASS或FAIL。一个完整的demo到这一步就算真正跑通了。
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); uart_init(); uint8_t tx_buf[2048]; uint8_t rx_buf[2048]; for (int i = 0; i < 2048; i++) tx_buf[i] = 0x5A; w25n01g_read_id(); // 步骤1:读ID w25n01g_erase_block(0); // 步骤2:擦除第0块 w25n01g_write_page(0, tx_buf, 2048); // 步骤3:写第0页 w25n01g_read_page(0, rx_buf, 2048); // 步骤4:读回 if (memcmp(tx_buf, rx_buf, 2048) == 0) printf("PASS\r\n"); else printf("FAIL\r\n"); while (1); }这一步通过后,再往上叠页拷贝、多页连续读写、坏块扫描都会顺手很多。
4. 调试过程中踩过的坑与后续扩展
4.1 读ID失败怎么定位
读ID全FF是最常见的现象,排查顺序我总结了一下:
- 检查接线:MISO和MOSI交叉错接非常常见,我画PCB时也翻过一次车。
- 确认SPI模式:示波器看波形不一定准确,可以直接把CPOL和CPHA在模式0和模式3之间切换试一下。
- 查WP和HOLD引脚:有没有上拉,没有上拉芯片会进入写保护或暂停状态,表现就是完全没响应。
- 确认供电:VCC是否稳定,去耦电容离芯片引脚够不够近。
如果读到了EF但后两位不对,重点查芯片型号是否被替换过,不同批次容量的ID存在差异。
4.2 写数据后读回全是FF
这个现象差点让我拆板。排查方向主要集中在三个地方:
- 写使能有没有成功发出,用逻辑分析仪看CS和MOSI,确认0x06发完后CS确实拉高了。
- 写流程有没有等待忙状态,写完立即读,芯片还没完成编程,读回来自然是旧数据或全FF。
- 页地址和块地址有没有算错,尤其块擦除如果误传了页地址而不是块地址,芯片会解析到完全不同的物理块。
擦除后也需要等状态寄存器清零再读,不然也会看到FF。这套检查流程核对一遍,绝大多数写入失败问题都能解决。
4.3 用DMA传输要注意片选时序
把读写改成DMA批量搬运后,最容易踩的坑是最后一个字节还没发完,CS就被拉高了,导致末尾丢数据。HAL库里可以在传输完成回调里再拉CS,或者启动DMA后轮询HAL_SPI_GetState,确认返回HAL_SPI_STATE_READY再操作GPIO。我用的是DMA传输完成回调里拉CS,逻辑上最干净。
4.4 后续扩展:坏块管理、ECC与文件系统
demo跑通之后,要做产品化还要补齐几件事,按优先级排:
- 坏块管理:NAND出厂可能带坏块,擦写过程中还会不断产生新坏块。简单办法是上电时全片扫描备用区第0字节,建立坏块表;进一步可以做动态坏块替换。
- ECC校验:W25N01G内部集成ECC功能,可以用Set Feature指令使能,读数据时通过状态寄存器判断是否有ECC错误,数据可靠性会好很多。
- 文件系统:想在W25N01G上挂FatFS,需要自己实现disk_read和disk_write,并正确处理块擦除时序,FatFS的底层接口和NOR的操作方式有差异,不能直接照搬SD卡驱动。
这三件事做完,这套驱动才算真正能干活。尤其是坏块管理,只要涉及频繁擦写,就绕不开。
最后再分享一点个人体会。SPI NAND的驱动流程看起来比NOR多了好几步,但只要抓住两个重点就不会乱:一切读写都先过页缓冲,一切写擦都要先写使能。数据手册里每个命令都带时序图,调试时对照着逐条比对,通常都能很快定位问题。这套代码我后面还会继续整理坏块管理和ECC部分,有进展再更新。
本文还有配套的精品资源,点击获取