W25Q128 FPGA 控制问答手册(管脚控制 + 命令格式 + 时序)
Q1:W25Q128 是什么?在 FPGA 里通常怎么用?
A:
• W25Q128 是 NOR Flash,按扇区/块擦除、按页编程、支持 SPI/QSPI。
• 它适合做:
• FPGA 配置镜像存储
• 参数/日志存储
• 固件升级镜像(A/B 分区)
• 关键特性(工程上要记住):
• 总容量:16MB
• 页(Page):256B(编程基本单位)
• 扇区(Sector):4KB(最常用擦除单位)
• 块(Block):32KB / 64KB(批量擦除更快)
• 擦除后值通常为 0xFF
• 编程只能把位从 1 -> 0,若需 0 -> 1 必须先擦除
---
Q2:FPGA 连接 W25Q128 的管脚怎么理解?
A:先按“标准 SPI”连接,再扩展到 QSPI。
2.1 基本管脚(标准 SPI)
• CS#:片选,低有效。一个命令事务通常从 CS# 拉低开始、拉高结束。
• CLK:串行时钟,由 FPGA 主机输出。
• DI(IO0/MOSI):主机到 Flash 的输入数据线。
• DO(IO1/MISO):Flash 到主机的输出数据线。
• WP#(IO2):写保护脚;在 Quad 模式下变为 IO2。
• HOLD# / RESET#(IO3):保持/复位脚;在 Quad 模式下变为 IO3。
• VCC/GND:电源和地。
2.2 QSPI 模式下的数据脚角色
• IO0~IO3 变成双向数据线。
• 常见相位宽度表达:
• 1-1-1:命令 1bit,地址 1bit,数据 1bit(标准 SPI)
• 1-1-4:命令/地址单线,数据四线(最常用折中)
• 1-4-4:命令单线,地址/数据四线(更高性能)
2.3 管脚控制要点(FPGA 实现视角)
1. CS# 是事务边界控制:
• 同一条命令的全部字节期间保持 CS#=0
• 结束时 CS# 拉高,命令才算“提交”
2. IO[3:0] 必须支持方向切换(tri-state):
• 发命令/地址时 FPGA 驱动输出
• 读数据时 FPGA 释放总线、采样输入
3. WP#、HOLD# 若不用相关功能,通常保持高电平;进入 Quad 模式后作为普通数据线。
4. 复位时序要保守:上电后先等待芯片就绪再发命令。
---
Q3:SPI 时序模式(CPOL/CPHA)怎么选?
A:W25Q128 常用 SPI Mode 0(CPOL=0, CPHA=0),部分系统也可用 Mode 3。
• Mode 0 常见规则:
• CLK 空闲为低
• 在上升沿采样,在下降沿更新输出
• 你在 FPGA 里应把“采样沿/驱动沿”参数化,便于与不同器件适配。
---
Q4:最小可用命令集有哪些?(先把系统跑起来)
A:建议先实现以下 12 条。
| 功能 | 命令 | Opcode | 地址 | Dummy | 数据方向 |
| ------------- | --------------- | ----------------: | ------ | ----- | ----------- |
| 读 JEDEC ID | RDID | 0x9F | 无 | 无 | Flash->FPGA |
| 读状态寄存器1 | RDSR1 | 0x05 | 无 | 无 | Flash->FPGA |
| 读状态寄存器2 | RDSR2 | 0x35 | 无 | 无 | Flash->FPGA |
| 写使能 | WREN | 0x06 | 无 | 无 | FPGA->Flash |
| 写失能 | WRDI | 0x04 | 无 | 无 | FPGA->Flash |
| 写状态寄存器 | WRSR | 0x01 | 无 | 无 | FPGA->Flash |
| 普通读 | READ | 0x03 | 24-bit | 无 | Flash->FPGA |
| 快速读 | FAST_READ | 0x0B | 24-bit | 8clk | Flash->FPGA |
| 页编程 | PAGE_PROGRAM | 0x02 | 24-bit | 无 | FPGA->Flash |
| 扇区擦除4KB | SECTOR_ERASE | 0x20 | 24-bit | 无 | FPGA->Flash |
| 块擦除64KB | BLOCK_ERASE_64K | 0xD8 | 24-bit | 无 | FPGA->Flash |
| 整片擦除 | CHIP_ERASE | 0xC7/0x60 | 无 | 无 | FPGA->Flash |
> 说明:W25Q128 容量 16MB,3 字节地址(24-bit)可覆盖全空间。
---
Q5:状态寄存器哪些位最关键?
A:先盯住这几个位就够了。
• SR1:
• BUSY(bit0):1=内部忙(编程/擦除进行中)
• WEL(bit1):1=写使能已置位
• SR2:
• QE(Quad Enable):1=允许 Quad 数据线模式
• 设计规则:
• 任何写/擦操作之前先确认 WEL=1
• 发完写/擦后轮询 BUSY 直到变 0
> 注:具体位定义请以你手头 W25Q128 版本 datasheet 为准,个别批次/系列在细节字段命名上可能存在差异。
---
Q6:一个命令事务的“控制命令格式”是什么?
A:统一抽象成 5 段,控制器最容易做。
[CMD] -> [ADDR] -> [MODE(optional)] -> [DUMMY(optional)] -> [DATA]
6.1 READ (0x03) 示例
• 格式:CMD(8) + ADDR(24) + DATA(N*8)
• 方向:前两段 FPGA 输出,最后一段 FPGA 输入。
6.2 FAST_READ (0x0B) 示例
• 格式:CMD(8) + ADDR(24) + DUMMY(8clk) + DATA
• Dummy 周期由器件和频率决定,高速时尤其重要。
6.3 PAGE_PROGRAM (0x02) 示例
• 格式:CMD(8) + ADDR(24) + DATA(1~256B)
• 限制:不可跨页;跨页必须拆分成多条命令。
6.4 SECTOR_ERASE (0x20) 示例
• 格式:CMD(8) + ADDR(24)
• 地址对齐:建议 4KB 对齐(低 12 位为 0)。
---
Q7:FPGA 端控制时序应如何组织?
A:建议做两层状态机:字节引擎 + 高层事务引擎。
7.1 高层事务状态(推荐)
1. IDLE:等待请求
2. ASSERT_CS:片选拉低,准备事务
3. SEND_CMD:发送命令字节
4. SEND_ADDR:发送 3 字节地址(若需要)
5. SEND_DUMMY:发送 Dummy(若需要)
6. TX_DATA / RX_DATA:发送或接收数据
7. DEASSERT_CS:片选拉高,结束事务
8. WAIT_BUSY:若是写/擦类,轮询 RDSR1.BUSY
9. DONE/ERROR
7.2 位级时序规则(以 Mode0 为例)
• CS# 先拉低再开始打时钟。
• 输出位在下降沿前稳定;上升沿采样输入位。
• 一字节 8bit,MSB first。
• 最后一个位结束后,再拉高 CS#。
7.3 关键延时与边界(实现时必须留参数)
• tCSS:CS# 下降到首个时钟沿的建立时间
• tCSH:最后时钟沿到 CS# 拉高保持时间
• tSHSL:两次 CS# 事务间隔
• tPP:页编程内部时间
• tSE/tBE/tCE:扇区/块/整片擦除时间
> 建议:这些时间统一做成“可配置计数参数”,默认取 datasheet 最大值留裕量。
---
Q7A:请给出“能直接照着写代码”的时序图体系
A:下面给你一套“中间变量 + 波形 + 触发动作”的完整模板。
> 思路:先用分频器产生“节拍脉冲”,再由高层事务状态机驱动 SPI 字节引擎,最后控制 CS#/CLK/IO0~IO3。
> 你只要按这些图把状态和寄存器写出来,就能落地。
Q7A-1:建议先统一中间变量命名
• clk_sys:FPGA 系统时钟(例:50MHz)
• cnt_div_spi:SPI 位时钟分频计数器
• tick_spi:SPI 位操作脉冲(1个 clk_sys 周期)
• cnt_div_poll:轮询等待分频计数器
• tick_poll:轮询触发脉冲
• op_req:上层请求(READ/PP/ERASE 等)
• op_busy:当前操作进行中
• fsm_state:高层状态机状态
• byte_shift[7:0]:当前待发/待收字节
• bit_cnt:位计数器(7->0)
• byte_cnt:字节计数器
• cs_n:片选
• spi_clk:输出到 W25Q128 的时钟
• io0_oe/io0_out:IO0 方向与输出
• io1_in:IO1 输入采样
• shift_done:单字节发送/接收完成脉冲
• cmd_done:整条命令完成脉冲
• busy_bit:读 SR1 后的 BUSY 位
• wel_bit:读 SR1 后的 WEL 位
Q7A-2:分频脉冲时序图(SPI 与轮询)
clk_sys : _|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_
cnt_div_spi : 0 1 2 ... SPI_DIV-1 0 1 2 ...
tick_spi : ________________|‾|_______________|‾|____
(每到阈值产生单周期脉冲)
cnt_div_poll : 0 1 2 ... POLL_DIV-1 0 1 2 ...
tick_poll : _____________________|‾|_________________
用途:
1. tick_spi 驱动位级发送/采样。
2. tick_poll 控制 RDSR1 轮询间隔,避免无意义高频轮询。
Q7A-3:SPI Mode0 位级时序图(1bit基础)
cs_n : ____|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|____
拉低后开始事务,拉高后结束事务
io0_out : ----D7---------D6---------D5--------- ... ----
(在 spi_clk 上升沿前稳定)
spi_clk : _______|‾|_____|‾|_____|‾|_____|‾|___________
^ ^ ^ ^
| | | |
上升沿采样(Mode0)
关键纪律:
• 输出数据先变,再给 spi_clk 上升沿。
• 输入数据在上升沿采样到 byte_shift。
Q7A-4:通用命令帧时序图(CMD->ADDR->DUMMY->DATA)
cs_n : ____|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|____ phase : [ CMD ] [ ADDR(3B) ] [ DUMMY ] [ DATA ] byte_cnt : 0 1 2 3 4 5....N io_dir : OUT OUT OUT OUT/IN
说明:
• READ:最后 DATA 是输入。
• PAGE_PROGRAM:最后 DATA 是输出。
• ERASE:通常无 DATA 段。
Q7A-5:WREN + PAGE_PROGRAM 完整时序图(最常用)
Step1: WREN(0x06)
cs_n : ____|‾‾‾‾|____
mosi(io0) : 0x06
Step2: RDSR1(0x05) 检查 WEL=1(可选但强烈建议)
cs_n : ____________|‾‾‾‾‾‾|____
mosi(io0) : 0x05
miso(io1) : [SR1] -> wel_bit=1?
Step3: PAGE_PROGRAM(0x02 + ADDR + DATA)
cs_n : __________________________|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|____
mosi(io0) : 0x02 A23..A0 D0..Dn
Step4: 轮询 BUSY(RDSR1)直到 busy_bit=0
cs_n : ____|‾‾|____|‾‾|____|‾‾|____ ... ____|‾‾|____
mosi(io0) : 05 05 05 05
miso(io1) : S1 S1 S1 S1
busy=1 ... busy=0
你写状态机时可拆成:
• PP_WREN -> PP_CHECK_WEL -> PP_SEND_CMD_ADDR_DATA -> PP_POLL_BUSY -> PP_DONE
Q7A-6:SECTOR_ERASE(0x20) 时序图
WREN
cs_n : ____|‾‾‾‾|____
io0_out : 0x06
ERASE CMD
cs_n : ____________|‾‾‾‾‾‾‾‾|____
io0_out : 0x20 + A23..A0
POLL BUSY
cs_n : ____|‾‾|____|‾‾|____ ... ____|‾‾|____
io0_out : 05 05 05
io1_in : S1 S1 ... S1
busy=1 ... busy=0
注意:擦除耗时远大于页编程,POLL 间隔可适当拉长。
Q7A-7:READ(0x03) 与 FAST_READ(0x0B) 对比时序
READ(0x03) cs_n : ____|‾‾‾‾‾‾‾‾‾‾‾‾|____ io0_out : 0x03 + A23..A0 io1_in : D0 D1 D2 ... FAST_READ(0x0B) cs_n : ____|‾‾‾‾‾‾‾‾‾‾‾‾‾‾|____ io0_out : 0x0B + A23..A0 + DUMMY(8clk) io1_in : D0 D1 D2 ...
区别核心:FAST_READ 比 READ 多 Dummy 周期。
Q7A-8:QSPI 1-1-4(以 0x6B 为例)时序图
cs_n : ____|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|____ phase : [CMD:1bit] [ADDR:1bit] [DUMMY] [DATA:4bit] io0 : cmd/addr out ............ q0<->data io1 : (input/idle) ............ q1<->data io2 : (input/idle) ............ q2<->data io3 : (input/idle) ............ q3<->data io_oe : 仅io0输出 全部输入(读场景)
实现关键:
1. CMD/ADDR 阶段仍可单线输出。
2. 到 DATA 阶段前,切换 IO 方向(tri-state)必须无冲突。
3. Dummy 周期结束后再采样 4 线数据。
Q7A-9:跨页写入(必须拆包)控制时序图
示例:addr=0x01F8,len=32B(跨 256B 页边界)。
Page0 可写剩余: 256 - 0xF8 = 8B 事务1: WREN + PP(addr=0x01F8, len=8) 事务2: WREN + PP(addr=0x0200, len=24) time ---> cs_n : __|‾|____|‾‾‾|____(poll)...__|‾|____|‾‾‾‾|____(poll)... op : WREN PP8B WREN PP24B
结论:
• 任何跨页写都要分解为多个 WREN+PP+POLL 子事务。
Q7A-10:从时序到代码的触发表(可直接用于 FSM)
| 触发条件 | 动作 | 关键变量 |
|---|---|---|
| op_req 到来且 op_busy=0 | 锁存请求并进入 ASSERT_CS | op_busy, fsm_state |
| tick_spi=1 且位发送中 | 执行 1bit 发送/采样 | spi_clk, bit_cnt, byte_shift |
| bit_cnt 结束 | 置 shift_done=1,准备下字节 | byte_cnt, shift_done |
| 命令帧最后字节结束 | cs_n 拉高 | cmd_done, cs_n |
| busy_poll 操作且 tick_poll=1 | 触发一次 RDSR1 | fsm_state |
| RDSR1 返回 busy_bit=0 | 退出轮询进入 DONE | fsm_state, op_busy |
Q7A-10A:通用事务状态机伪代码(建议直接照此搭框架)
on reset:
fsm_state <- IDLE
op_busy <- 0
cs_n <- 1
spi_clk <- 0
every clk_sys:
case fsm_state
IDLE:
if op_req_valid and op_busy==0:
latch(op_req)
op_busy <- 1
fsm_state <- ASSERT_CS
ASSERT_CS:
cs_n <- 0
wait tCSS cycles
load first byte (opcode)
fsm_state <- SEND_BYTE
SEND_BYTE:
if tick_spi:
do one bit shift according to Mode0
if bit_cnt done:
if more bytes in this phase:
load next byte
else:
fsm_state <- NEXT_PHASE
NEXT_PHASE:
switch current phase
CMD -> ADDR / DUMMY / DATA / END
ADDR -> DUMMY / DATA / END
DUMMY -> DATA / END
DATA -> END
if target phase exists: prepare phase and go SEND_BYTE
else: fsm_state <- DEASSERT_CS
DEASSERT_CS:
cs_n <- 1
wait tCSH cycles
if need_busy_poll:
fsm_state <- WAIT_POLL_TICK
else:
fsm_state <- DONE
WAIT_POLL_TICK:
if tick_poll:
prepare command 0x05 (RDSR1)
fsm_state <- ASSERT_CS
DONE:
op_busy <- 0
pulse cmd_done
fsm_state <- IDLE
ERROR:
op_busy <- 0
raise error flag/code
fsm_state <- IDLE
Q7A-11:初学者最容易犯错的时序点
1. CS# 在命令中途被拉高,导致命令被截断。
2. tick_spi 不是单周期,导致一个状态走两步。
3. 写操作忘记 WREN,或 WEL 未校验。
4. FAST_READ/QSPI 的 Dummy 周期配错。
5. QSPI 数据阶段 IO 方向切换过晚,引发总线争用。
---
Q8:读取流程怎么做最稳妥?
A:读取一般最简单,推荐如下顺序。
1. (可选)RDSR1 检查 BUSY=0
2. 发 READ(0x03) 或 FAST_READ(0x0B)
3. 连续读取 N 字节数据
4. CS# 拉高结束
5. 做数据校验(CRC/已知头)
读取异常处理
• 读到全 0xFF:可能地址空白、总线未驱动、片选/时钟异常。
• 读到固定错位数据:常见是 CPHA/CPOL 不匹配或 bit 对齐错误。
Q8A:READ/FAST_READ 伪代码(与 Q7A-7 时序图对应)
function flash_read(addr, length, use_fast_read):
wait_until_not_busy()
if use_fast_read:
opcode <- 0x0B
dummy_cycles <- 8
else:
opcode <- 0x03
dummy_cycles <- 0
start_transaction()
send_byte(opcode)
send_addr24(addr)
send_dummy(dummy_cycles)
for i in 0 .. length-1:
rx_buf[i] <- recv_byte()
end_transaction()
return rx_buf
---
Q9:写入(页编程)流程怎么做?
A:写入最容易错在 WREN 和跨页。
标准流程
1. 读状态 BUSY=0
2. 发 WREN(0x06)
3. 读 RDSR1 确认 WEL=1
4. 发 PAGE_PROGRAM(0x02) + addr + data(<=256B)
5. 轮询 BUSY 直到 0
6. 回读比对数据
必须遵守的规则
• 单次 PP 最多 256B。
• 若 addr[7:0] + len > 256,必须分包到下一页。
• 每次 PP 前都要重新 WREN(很多器件写后自动清除 WEL)。
Q9A:PAGE_PROGRAM 伪代码(含跨页拆包)
function flash_program(addr, data[], length):
offset <- 0
while offset < length:
page_room <- 256 - ((addr + offset) & 0xFF)
chunk_len <- min(page_room, length - offset)
wait_until_not_busy()
// Step1: WREN
start_transaction(); send_byte(0x06); end_transaction()
// Step2: 可选检查 WEL
sr1 <- read_sr1()
if sr1.WEL != 1: return ERROR_WEL_NOT_SET
// Step3: PAGE PROGRAM
start_transaction()
send_byte(0x02)
send_addr24(addr + offset)
for i in 0 .. chunk_len-1:
send_byte(data[offset + i])
end_transaction()
// Step4: 轮询 BUSY
if wait_until_not_busy_timeout(): return ERROR_PP_TIMEOUT
offset <- offset + chunk_len
return OK
---
Q10:删除(擦除)流程怎么做?
A:Flash 的“删除”就是擦除,通常按 4KB 扇区。
扇区擦除流程(推荐)
1. 确认 BUSY=0
2. 发 WREN
3. 发 SECTOR_ERASE(0x20) + 24-bit 地址
4. 轮询 BUSY 直到 0
5. 回读抽样校验是否为 0xFF
块/整片擦除
• 64KB:0xD8
• Chip:0xC7 或 0x60
• 注意:整片擦除时间很长,必须有超时机制和进度策略。
Q10A:ERASE 伪代码(4KB/64KB/整片统一框架)
function flash_erase(addr, erase_type):
wait_until_not_busy()
if erase_type == SECTOR_4K:
opcode <- 0x20
require_align(addr, 4KB)
else if erase_type == BLOCK_64K:
opcode <- 0xD8
require_align(addr, 64KB)
else if erase_type == CHIP:
opcode <- 0xC7
else:
return ERROR_BAD_TYPE
// WREN
start_transaction(); send_byte(0x06); end_transaction()
// ERASE
start_transaction()
send_byte(opcode)
if erase_type != CHIP:
send_addr24(addr)
end_transaction()
// BUSY polling
if wait_until_not_busy_timeout(): return ERROR_ERASE_TIMEOUT
return OK
---
Q11:QSPI 模式怎么切换和控制?
A:先用 SPI 做初始化,再开 QE,最后切 Quad 读写。
推荐步骤
1. 用标准 SPI 发 RDID 确认通信正常
2. WREN
3. 写状态寄存器使 QE=1
4. 改控制器数据相位:读命令改为 Quad Read(如 0x6B)
5. 在 IO0~IO3 上切换方向并验证读数据
QSPI 关键点
• 命令阶段常保持单线,数据阶段四线,调试最稳。
• 高速下 dummy cycle 必须严格匹配。
• IO 三态切换时机必须正确,否则总线争用。
Q11A:SPI -> QSPI 初始化伪代码(1-1-4 读)
function enable_quad_mode_and_read_test(test_addr): // 1) 先确认 SPI 基础通信 id <- read_jedec_id() // 0x9F if id invalid: return ERROR_NO_DEVICE // 2) WREN start_transaction(); send_byte(0x06); end_transaction() // 3) 写状态寄存器使 QE=1(具体位按datasheet) sr1 <- read_sr1() sr2 <- read_sr2() sr2.QE <- 1 write_status(sr1, sr2) if wait_until_not_busy_timeout(): return ERROR_QE_TIMEOUT // 4) 切到 1-1-4 读命令(如 0x6B) set_phase_lines(cmd=1, addr=1, data=4) set_dummy_cycles(per_datasheet) // 5) 做一次读回验证 data <- quad_read_1_1_4(test_addr, N) if data check fail: return ERROR_QSPI_VERIFY return OK
---
Q12:推荐的命令模板(便于你写控制器)
A:把每条命令抽象成参数表,控制器按表驱动。
| 字段 | 含义 |
| ---------------- | --------------------------- |
| opcode | 命令码 |
| addr_en | 是否带地址 |
| addr_len | 地址长度(W25Q128 通常 24) |
| dummy_cycles | Dummy 周期数 |
| data_dir | NONE / TX / RX |
| data_lines | 1 或 4 |
| require_wren | 是否要求先写使能 |
| busy_poll | 命令后是否需要轮询 BUSY |
这样你就能统一支持 READ/WRITE/ERASE,不必为每条命令写一套独立状态机。
---
Q13:如何做“可落地”的错误处理与保护?
A:至少实现以下 8 条。
1. 操作超时(BUSY 长时间不清零)自动报错。
2. 写前检查 WEL,失败则重发 WREN 并计数。
3. 关键操作后回读校验(页编程后比对)。
4. 擦除前地址对齐检查(4KB/64KB)。
5. 操作期间禁止并发发新命令(互斥锁/忙标志)。
6. 上电初始化先读取 ID,不通过则降级或报警。
7. 错误码分级:通信错误、协议错误、超时错误、校验错误。
8. 日志记录最近一次失败命令(opcode、addr、len、status)。
---
Q14:从 0 开始 bring-up,最短路径是什么?
A:按这个 7 步走,最快定位问题。
1. 先低频 SPI(如 5~10MHz)+ Mode0。
2. 打通 RDID(0x9F),确认 ID 正确。
3. 打通 RDSR1(0x05),确认能稳定读 BUSY/WEL。
4. 擦 1 个扇区(4KB)。
5. 编程 1 页(256B 以内)。
6. 回读并比对。
7. 再提频、再切 FAST_READ/QSPI。
---
Q15:你实现 Verilog 时最小模块划分建议是什么?
A:推荐 5 个模块。
1. spi_phy:时钟、移位、采样、IO 方向控制。
2. spi_byte_engine:字节发送接收与 bit 计数。
3. flash_cmd_engine:命令模板解释(CMD/ADDR/DUMMY/DATA)。
4. flash_op_fsm:读写擦高层流程(含 WREN、BUSY 轮询)。
5. flash_if:对上层暴露统一接口(read/write/erase/status)。
---
Q16:常见问题与快速定位
| 现象 | 优先检查 | 常见根因 |
| ------------- | ------------------- | ------------------------------------------- |
| RDID 读不通 | CS/CLK/IO0/IO1 波形 | 片选时序不对、模式错误、引脚复用冲突 |
| 读数据错位 | 采样边沿、bit 顺序 | CPHA/CPOL 配置不匹配 |
| 写命令无效 | WEL 位 | 忘记 WREN,或 WREN 与写命令间隔/CS 边界错误 |
| 擦除很慢/超时 | BUSY 持续时间 | 轮询逻辑错误、超时时间设置过短 |
| QSPI 读全错 | QE 位、IO 方向切换 | 未开启 QE、IO2/IO3 未正确 tri-state |
| 偶发失败 | 时钟频率、SI | 频率过高、布线完整性不足、时序裕量不够 |
---
Q17:参数建议(给 FPGA 初版)
• SPI 时钟初始:<= 10MHz(先保守)
• 成功后逐步升频:20MHz -> 40MHz -> 更高(看板级与时序)
• Dummy cycle:按目标频率和 datasheet 推荐值配置
• 超时计数:
• 页编程:按 tPP_max 留 20%~50% 裕量
• 扇区擦除:按 tSE_max 留裕量
• 整片擦除:单独超时策略(通常秒级到分钟级)
---
Q18:一页总结(可直接用于实现)
• 先做 SPI 1-1-1 打通:RDID -> RDSR1 -> ERASE -> PP -> READ校验。
• 再做 FAST_READ,最后做 QSPI(先 1-1-4)。
• 高层流程始终遵循:
• 写/擦前 WREN
• 操作后轮询 BUSY
• 关键数据回读校验
• 把命令抽象成“模板表驱动”,状态机可复用,最利于维护和扩展。
---
附录 A:建议你在文档旁边准备的实现清单
1. 命令表(opcode + phase 参数)
2. 状态寄存器位定义表
3. 时序参数表(按 datasheet)
4. 错误码定义表
5. bring-up 测试记录表(每步是否通过、波形截图编号)
---
> 最后提醒:本手册给的是工程通用控制框架。若你手头 W25Q128 的具体后缀型号(JV/FV 等)与封装版本不同,请以对应 datasheet 的“电气参数、QE 位定义、最大频率、dummy 建议值”为最终准则。