Buck Blog · 博客正文

返回技术分享首页
W25Q128 FPGA控制管脚命令时序问答手册

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 建议值”为最终准则。