主控驱动不匹配 / 参数页对齐 · 样机排查手册
编制日期 2026-09-21 | 版本号 Rev 1.0
XTX 芯天下 · FAE 现场技术文档 | 换料 / 换批次后驱动不认或行为异常的分层定位与参数页对齐 | 2026-09 版
驱动表结构体名、宏名、字段偏移、设备树属性名全部以所用内核 / SDK 源码与器件 datasheet 为准;本文只回答「要查哪一类、按什么顺序改」。
- 本文讨论的是器件与主控驱动之间的「认识问题」:硬件连得上、波形正常,但驱动不认, 或者认下来了却用错参数。连 ID 都读不回来的硬件层问题,请先看《上电不识别_ID读不到_样机排查》。
- SFDP 的字段名与偏移以 JESD216 系列规范为准、ONFI 参数页以 ONFI 规范为准、 EXT_CSD 以 JEDEC JESD84 为准。本文不给出未经核实的具体偏移与位定义, 只给出「要核对哪些字段」,数值一律回查规范与 datasheet。
- SPI NAND 读参数页的命令码(ONFI 常见写法为
0xEC)与地址字节数在不同器件上并不统一, 动手前必须逐字节核对 datasheet 的指令表。 - 驱动表条目里的 擦除 opcode、编程 opcode、容量字段 写错会直接破坏数据,属高危操作, 见第 3 章的危险操作提示。
这份文档解决什么
解决的是换料、换批次、换主控平台之后,驱动层不认新器件,或者认下来了却用错几何参数、容量、时序与 ECC 策略 这一类问题的定位与根治方法。覆盖 SPI NOR(SFDP)、SPI NAND(ONFI)、eMMC(EXT_CSD)、SD / SD NAND(CSD / SCR) 四种介质的参数页对齐。
全文按「机制 → 决策树 → 分成因排查 → 速查表 → 核查清单」组织:第 2 章把问题收敛到 A / B / C 三类, 第 3~5 章给出各介质可直接粘贴的驱动表条目、设备树片段与回读命令,第 6 章速查,第 7 章核查与结案。
一句话结论:现代驱动不再靠「认 ID 查表」一件事搞定,而是三层串行—— 硬件层把 ID 读回来、参数页层把器件自描述表解析开、驱动表层命中一个条目。 换料失败时,绝大多数人只改了第三层(补一条驱动表),却忽略了第二层: 早期或特殊器件的 SFDP / ONFI 参数页字段可能缺失、填 0 或与默认约定不一致,驱动 fallback 到一套错误假设, 于是容量减半、地址回绕、ECC 冲突、高速读失败这些「玄学」现象就来了。 正确的动作顺序是:先回读参数页原始 hex,逐字段与 datasheet 比对,再决定是改驱动表还是改设备树; 临时兼容条目只用来证明「参数对了就能跑」,绝不能锁进量产参数卡。
1. 机制:驱动是怎么认识一颗芯片的
早期驱动靠一张静态表认芯片,现代驱动多了一层——先读器件的自描述参数页,再决定用什么参数。 理解这三层,换料失败就不再神秘:要么 ID 没读回,要么参数页解得不对,要么表里根本没这一项。
1.1 三层识别架构
第一层是硬件识别:控制器按各介质的约定发读 ID 命令,器件回厂商 ID 与器件 ID。 这一层不通后面全免谈,但它通常不是换料失败的堵点——换料前后硬件连接没变。
第二层是参数页,也是本文重点。器件内部存了一张自描述表,驱动读回它就能知道容量、擦除粒度、 读命令与 dummy cycle、Quad 能力、ECC 要求等。问题在于:不是所有器件的参数页都完整、都符合驱动期望的那个版本。 早期器件可能没有参数页,或关键字段填 0 / 填了驱动不认的值,此时驱动会 fallback—— 而 fallback 用的那套默认假设,往往与新器件并不相符。
第三层是驱动表。表里没有这一项,就退回默认参数或直接报错。 不同系统的表位置完全不同:linux 的 spi-nor 器件表、U-Boot 的 flash 表、RT-Thread SFUD 的表、 Zephyr 与 ESP-IDF 各自的 ID 表,文件名、结构体名、宏名都要按 SDK 源码核对,不能跨系统照抄。
1.2 四种介质的参数页对照
四种介质的自描述表名字不同、读法不同、包含的信息也不同。 排查前先确认你面对的是哪一种,用错入口会得到完全错误的结论。
| 介质 | 自描述表 | 常见读取入口 | 需要对齐的关键信息 | 缺失时的典型后果 |
|---|---|---|---|---|
| SPI NOR | SFDP(JESD216 系列) | 驱动探测时自动读取 | 容量、擦除粒度、读命令与 dummy cycle、4 字节地址、Quad 能力 | fallback 到默认参数,容量或读命令不匹配 |
| SPI NAND | ONFI 参数页 | Read Parameter Page 命令(常见 0xEC) | page / spare / block 几何、ECC 要求、时序参数 | OOB 布局与 ECC 策略按错误假设配置 |
| eMMC | EXT_CSD | CMD8(SEND_EXT_CSD) | 容量扇区数、速率能力位图、缓存与后台操作、分区配置 | 容量不符、速率上不去、缓存行为异常 |
| SD / SD NAND | CSD / SCR | CMD9(SEND_CSD) | 容量、速率等级、总线宽度能力 | 容量识别错误、宽度切换失败 |
eMMC 的 CMD8 是 SEND_EXT_CSD(读扩展寄存器),
SD 的 CMD8 是 SEND_IF_COND(电压与版本探测),
命令号相同、语义完全不同。跨介质套用命令码是排查时最常见的低级错误。
2. 决策树:不认 / 认错怎么收敛
拿到「换料后不认」的样机,先按 ID 读不读得回、读回来对不对、认下来行为正不正常 把问题收敛到 A / B / C 三类,再进对应章节。不要一上来就补驱动表条目—— 八成的坑在第二层参数页,不在第三层。
怎么用这棵树
- 先把 ID 读回来:串口日志、驱动调试打印、逻辑分析仪解码任选。 拿到 ID 之前不要改任何代码。
- ID 对不对,用 datasheet 核,不要用「上一颗料的 ID」核。 换料后 ID 变了是正常的,变了不认才是问题。
- ID 对但不认 → B 类:查驱动表的匹配字段(ID 序列、匹配长度、掩码)与设备树绑定。
- 认下来但行为异常 → C 类:回读参数页原始 hex 与 datasheet 逐字段比对,而不是继续猜。
2.1 对齐方法四步
无论哪种介质,参数页对齐的动作序列都是固定的这四步。跳过第二步直接改代码,是返工的根源。
- 回读原始 hex:用内核调试日志、U-Boot 命令或用户态 spidev 脚本, 把参数页原始字节读回来存档。只要原始字节,不要只看驱动解析后的结果—— 解析结果已经被驱动的假设污染过了。
- 与 datasheet 逐字段比对:签名、版本、几何字段、能力位、时序字段一项一项对, 对不上的记下来,就是要改的地方。
- 改驱动表或设备树:几何与能力类的差异改驱动表条目; 速率、总线宽度、匹配串、分区这类差异改设备树节点。
- 复测:大块读写 + 回读校验 + 冷启动一致性 + 温压拉偏。 只跑通 boot 不算复测通过。
为快速证明「参数对了就能跑」,可以克隆相近型号的条目做成临时兼容条目,但必须满足三条: 在名称或注释里标明「临时 / 非正式」、未核实的字段保持待确认而不是照抄、 正式补丁回来后立刻撤掉并回归。 把临时条目直接锁进量产参数卡是这类问题里后果最严重的做法——它会在几个月后以「批次间不一致」的形式重新暴露, 且那时已追溯不回原因。
3. 成因一:SPI NOR 的 SFDP 与驱动表
SPI NOR 侧最常见的情况是:驱动优先读 SFDP,读不到或字段不认就 fallback, fallback 的默认参数与新器件不符;或者干脆是驱动表里没有这个 ID。
3.1 原理:SFDP 优先,驱动表兜底
支持 SFDP 的驱动,探测流程大致是:读 JEDEC ID → 读 SFDP 参数表 → 用参数表里的信息填充几何与时序 → 如果 SFDP 缺失或解析失败,才退回驱动表里该条目的静态参数。 这意味着两件事:
- SFDP 存在但字段有问题时,驱动会用错误的自描述参数去工作,此时改驱动表条目可能完全不起作用, 因为驱动根本没走到查表那一步。
- SFDP 缺失时,驱动表条目是唯一的参数来源,此时条目里的每一个字段都必须来自 datasheet, 照抄相近型号必然出错。
几个高频的具体坑:
- 4 字节地址:容量超过 16 MB 必须用 4 字节地址(或地址模式切换机制)。 标志没开,访问高位地址时会回绕到低位、静默覆盖已有数据——最危险的一类,因为它不报错。
- Quad 能力:使能位与读命令需要配套。位没开但驱动按 Quad 读,读出来全错; 位开了但主控没按 Quad 时序发,同样错。
- dummy cycle:读命令与 dummy cycle 数必须成对。 配错表现为「低速读正常、高速读错」或反过来。
3.2 实操:回读比对与补条目
第一步:回读 SFDP 原始 hex
# 1) 打开 SPI NOR 子系统的动态调试(路径随内核版本不同,按实际调整)
echo 'file drivers/mtd/spi-nor/* +p' > /sys/kernel/debug/dynamic_debug/control
# 2) 重新触发一次探测(重新绑定驱动或重启),再抓日志
dmesg -w | grep -iE 'spi-nor|sfdp|nor0|unrecognized|unknown|jedec'
# 3) 若内核导出了 sysfs 节点,可直接看识别结果(路径随内核版本不同,不存在就回到 dmesg)
cat /sys/bus/spi/devices/spi0.0/spi-nor/jedec_id # ← 总线号/CS 号按实际改
cat /sys/bus/spi/devices/spi0.0/spi-nor/partname
# 4) 看容量是否与标称一致(mtd 大小不等于文件系统可用大小,要看 mtd 原始大小)
cat /proc/mtd
第二步:补驱动表条目(可直接粘贴,注释处按实际改)
/* 文件示例:drivers/mtd/spi-nor/<厂商>.c,或 SDK 自带的 spi-nor 器件表
* 具体文件名、结构体名、宏名以所用内核 / SDK 源码为准,不要照抄路径。
*
* 填写纪律:
* 1) 每个字段都要在 datasheet 上找到出处,没出处的先留 0 并标记为「待确认」;
* 2) erase / program 的 opcode 必须与 datasheet 的指令表逐字节核对;
* 3) 能力标志只开「已经实测验证过」的,不要一次全开。
*/
{
/* 名称:会打印在日志里,建议直接用完整料号,便于日后追溯到批次 */
.name = "xt25wxxxx", /* ← 按实际料号改 */
/* JEDEC ID:填读 ID 命令实际回读的厂商 ID + 器件 ID 序列
* 顺序与长度按驱动定义的匹配规则填,不要凭印象写 */
.id = { 0x00, 0x00, 0x00 }, /* ← 按实际回读值改 */
.id_len = 3, /* ← 按驱动的匹配长度定义改 */
/* 容量:单位字节,按 datasheet 标称容量填,不是「读出来的最大值」 */
.size = (1 << 24), /* ← 按标称容量改,例如 2 MB = 1<<21 */
/* 写粒度:页大小,必须来自 datasheet */
.page_size = 256, /* ← 按 datasheet 改 */
/* 擦除:opcode 与粒度来自 datasheet 的指令表,两者必须配套 */
.erase_opcode = 0x20, /* ← 常见为 4 KB 扇区擦除,务必核对 datasheet */
.sector_size = (1 << 12), /* ← 与该 opcode 对应的擦除粒度 */
/* 编程:opcode 来自 datasheet 的指令表 */
.program_opcode = 0x02, /* ← 常见为 Page Program,务必核对 datasheet */
/* 能力标志:宏名随内核版本变化(4 字节地址、Quad 使能、读命令各不相同)
* 未核实的标志一律留 0,确认一项开一项 */
.flags = 0, /* ← 逐项核实后再按位或上去,例如
* .flags = SPI_NOR_4B_OPCODES 之类,
* 宏名以所用内核源码为准 */
},
第三步:改设备树节点(可直接粘贴,注释处按实际改)
/* 文件:<板级>.dts —— 路径与节点名按实际平台改 */
&spi0 {
status = "okay";
/* 引脚复用必须与实际原理图一致;漏配会表现为「命令发出去没有回读」 */
pinctrl-names = "default";
pinctrl-0 = <&spi0_pins>; /* ← 按实际 pinctrl 节点名改 */
flash@0 {
/* 匹配串:不同内核版本写法不同,常见为 "jedec,spi-nor",
* 也有厂商自定义匹配串,以所用内核的驱动源码为准 */
compatible = "jedec,spi-nor"; /* ← 按内核支持的匹配串填 */
reg = <0>; /* ← CS# 编号,按原理图改 */
/* 频率:先填保守值,跑通后按实测波形逐步收敛,不要一上来就写器件标称上限 */
spi-max-frequency = <50000000>; /* ← 按实际改,调试阶段可先降到 10000000 */
/* 总线宽度:调试阶段先设 1,确认单线能跑通再改成 4 */
spi-rx-bus-width = <1>; /* ← 1 / 2 / 4 */
spi-tx-bus-width = <1>; /* ← 1 / 2 / 4 */
#address-cells = <1>;
#size-cells = <1>;
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
partition@0 {
label = "spl"; /* ← 分区名按实际启动布局改 */
reg = <0x0 0x100000>; /* ← 偏移与长度按实际改 */
read-only;
};
partition@100000 {
label = "uboot";
reg = <0x100000 0x200000>; /* ← 按实际改 */
};
/* 其余分区按实际布局继续补;
* 分区偏移必须与烧录器配置、量产脚本三者完全一致,否则会出现
* 「烧进去的地址与启动读取的地址不是一个」这类错位问题 */
};
};
};
- 擦除 opcode 写错:驱动用错误命令去擦,可能擦掉不该擦的区域,严重时擦成空白且不可恢复。
- 编程 opcode 写错:写入内容与校验结果不符,还可能触发意外的编程操作。
- 容量字段填大:驱动会访问超出物理范围的地址,最坏情况是回绕覆盖已写入的数据。
- 4 字节地址标志乱开 / 该开不开:前者在 3 字节器件上发出非法命令序列, 后者在超过 16 MB 的地址上静默回绕覆盖。
- 用
flash_eraseall/flashcp前不确认目标 mtd 编号: 选错编号会擦掉另一个分区,执行前先cat /proc/mtd核对。
3.3 验证:条目填对了的判据
- 容量等于标称容量:看块设备或 mtd 的原始大小,不是「看起来能跑」。
- 跨 16 MB 边界读写不回绕:边界前后各写不同数据再回读,确认没互相覆盖。 超过 16 MB 的器件这一项必做。
- 擦除粒度正确:按 datasheet 的最小擦除单位擦写,能成功且不影响相邻区域。
- Quad 读(若开了)与单线读结果一致:两种模式各读一遍同一区域比对。
- 全片回读校验通过:整片写入已知图案再回读比对,不是只测几个页。
- 连续 20 次冷启动识别结果一致,且打印出的器件名就是新料号。
4. 成因二:SPI NAND 的 ONFI 与 ECC
SPI NAND 比 SPI NOR 多两件必须谈拢的事:OOB(spare)区怎么布局、ECC 由谁来做。 这两件事谈不拢的典型表现是「能烧、能启动,但文件系统挂载就报错」。
4.1 原理:几何字段与 ECC 要求都在参数页里
ONFI 参数页里放着驱动需要的全部几何信息:page 大小、spare(OOB)大小、block 大小, 以及器件要求的最低纠错能力。驱动读回参数页后按这些信息配置布局与 ECC 策略。问题出在两处:
- 驱动没真正读参数页,而是按「上一个料号」的值硬编码。 换料后 page 从 2 KB 变 4 KB、spare 从 64 B 变 128 B,驱动还按旧几何算 ECC 位置, 于是数据被 ECC 校验字节挤掉,或 ECC 根本没覆盖到数据区。
- ECC 责任方没统一。SPI NAND 普遍带 on-die ECC(片内纠错), 主控侧也有硬件 ECC,两者不能同时承担同一份数据的纠错, 同时开会导致 spare 区被重复占用、回读出现不可纠正错。 正确做法是二选一并在全链路统一。
4.2 实操:回读参数页与核对 ECC 策略
回读 ONFI 参数页(示意脚本,需 root 与 spidev 权限)
# 通过 /dev/spidevX.Y 回读 ONFI 参数页 —— 示意脚本,需 root 与 spidev 权限
# 依赖:pip install spidev(部分平台需先在设备树里启用 spidev 节点)
import time
import spidev
spi = spidev.SpiDev()
spi.open(0, 0) # ← 按实际 SPI 总线号 / CS 号改
spi.max_speed_hz = 1_000_000 # 读参数页先用低速,保证稳定
spi.mode = 0 # ← 按 datasheet 的 CPOL / CPHA 改
# 1) 发 Read Parameter Page:命令码 + 地址 + 等待 tR
# 命令码与地址字节数以 datasheet 的指令表为准(ONFI 常见写法为 0xEC + 地址 0x00)
cmd = [0xEC, 0x00, 0x00] # ← 逐字节核对 datasheet
spi.xfer2(cmd)
# 2) 等 tR:器件把参数页搬到内部缓存的时长,量级见 datasheet
time.sleep(0.001) # ← 按 datasheet 的 tR 改
# 3) 读回前 128 字节,先看签名
buf = bytes(spi.readbytes(128))
print("signature :", buf[0:4]) # 期望为 b'ONFI'
print("raw[0:32] :", buf[0:32].hex(" "))
# 4) 按参数页字段偏移解析 page / spare / block 与 ECC 要求
# 偏移以 ONFI 规范与 datasheet 为准,禁止沿用「上一个料号的值」
spi.close()
在目标板上交叉确认
# 在目标板上交叉确认内核识别出的几何与 ECC 信息
cat /proc/mtd
mtdinfo /dev/mtd0 # 需 mtd-utils:看擦除块 / 最小写入单元 / OOB 大小
dmesg | grep -iE 'spi-nand|onfi|ecc|oob|bitflip|bad block'
# 若使用 UBI / UBIFS,看挂载时打出的几何信息
dmesg | grep -iE 'ubi[0-9]|physical eraseblock|logical eraseblock|sub-page'
# 三者必须一致:参数页回读值 = datasheet = mtdinfo 输出
ECC 与 OOB 的核对顺序
- 先确认 on-die ECC 是开还是关。查 datasheet 里相关配置位(或 feature 寄存器)的默认值, 再确认驱动 / 烧录工具有没有主动改过。状态要与量产脚本一致。
- 再确认主控侧 ECC 能力:主控硬件 ECC 能纠多少位,是否覆盖器件要求的最低纠错能力。 器件要求的能力必须不超过主控能力。
- 核对 OOB 布局:spare 区里 ECC 字节放哪、坏块标记放哪,必须与文件系统层的期望一致。
- 核对 page / block 几何:驱动解析出的值必须与参数页回读值一致,不能来自硬编码。
- 全链路统一:BootROM、SPL、U-Boot、内核、量产烧录工具五处策略必须一致。 只改内核不改烧录工具,是最常见的漏项。
4.3 验证:布局对不对,跑一遍就知道
- 参数页回读的几何值与 datasheet 一致,且与驱动打印出的值一致。
- 跨页写入验证:写超过一个 page 长度的数据再回读。 只写几十字节测不出 page 大小假设错误。
- 文件系统完整流程:挂载、写入、卸载、重新挂载、回读校验。
- ECC 纠错能力实测:在可控环境人为制造少量位翻转,确认能被纠正并上报正确位数。
- 坏块表一致性:驱动扫描出的坏块与量产工具扫描出的一致。
烧录器能烧、能启动,只证明烧录器与 BootROM 这一对谈拢了。 内核与文件系统层用的是另一套布局假设,不匹配时会在挂载阶段才暴露。 所以验收必须走到文件系统挂载 + 回读校验,停在「能启动」不够。