正面攻克NVMe阻塞启动之谜——RK3588移植内核6.12纪实
原创
Cris 陈显森
Cris 陈显森
格友
2026年10月16日 22:29
上海
去阅读
同一台幽兰、同一块 S3000,5.10 能启动,6.12 却停在 Logo显示。取回现场数据后发现,PCIe 已枚举到 SSD,NVMe 控制器却始终未报告就绪:旧系统约 918 毫秒的等待,在 6.12 中持续 128 秒仍失败。保持同一份 Image,仅在设备树增加 SSD 重新供电前的关闭间隔后,原盘分区恢复,读写及正常重启校验通过。
这次排查从张银奎老师的一条消息开始。瑞芯微在 GitHub 发布了 Linux 6.12 分支,老师编译后在幽兰笔记本上试跑,发现 NVMe 硬盘识别不到分区。 正好我手头也有一台幽兰 RK3588 笔记本,便提出国庆假期在本地搭建 6.12 编译环境,尝试复现这个问题,参与调试。 张老师随后发来幽兰的设备树文件 rk3588-yourland.dts,以及瑞芯微的 develop-6.12 分支地址。[2] 设备树描述板上的硬件连接;这份文件从 5.x 内核沿用过来,张老师提醒其中有些配置可能还需要调整。
张老师反馈NVMe分区识别问题并提供幽兰设备树
我这台幽兰原本运行太乙系统的%20Linux%205.10.110,能够从%20NVMe%20硬盘正常启动。换用%206.12%20后,同一台机器、同一块硬盘,屏幕却停在%20Logo,迟迟没有出现登录界面。
屏幕保留着开机画面,内核的执行位置仍需通过启动日志和%20CPU%20状态确认。接下来的排查重点是%20Linux%20接管硬盘后的初始化过程。%20本次使用幽兰%20RK3588%20笔记本,启动盘为时创意(SCY)的%20S3000%20512%20GB%20固态硬盘,测试内核为基于瑞芯微源码构建的%20Linux%206.12.69。[13]
图%201:本次排查使用的时创意%20S3000%20512%20GB%20NVMe%20硬盘。
这块硬盘插在幽兰的%20M.2%20插槽上,硬盘与%20RK3588%20之间则通过%20PCIe(PCI%20Express)通信。PCIe%20是高速串行接口,显卡、网卡也常用它。[10]%20读写这块硬盘时,Linux%20驱动按%20NVMe%20协议提交命令,并取得执行结果。
一、从上电到进入系统的三个阶段
这台幽兰的内核和系统文件都保存在%20NVMe%20硬盘上。U-Boot%20负责把内核读入内存,Linux%20运行后再用自己的驱动访问这块硬盘。
1.%20启动代码和固件准备环境,启动%20U-Boot
电脑上电后,RK3588%20首先执行芯片内置的启动代码,再由后续固件准备内存等基本环境。环境准备完成后,固件启动%20U-Boot。
2.%20U-Boot%20加载内核和设备树
U-Boot%20按启动配置,从%20NVMe%20硬盘读取内核镜像 Image 和设备树文件 DTB,把它们放进内存。Image 包含%20Linux%20的代码,设备树记录主板的硬件配置,供内核初始化设备时使用。
后面的诊断实验还会让%20U-Boot%20加载%20initramfs,即初始内存文件系统。它的程序和文件都在内存中,可以在%20NVMe%20根分区挂载前使用。
文件加载完成后,U-Boot%20传入设备树等启动信息,并把执行权交给%20Linux。
3.%20Linux%20初始化硬件,启动系统服务
Linux%20开始运行后,建立自己的内存管理和中断环境,再初始化设备驱动。驱动根据设备树,取得%20PCIe%20控制器所需的时钟、复位资源,以及控制%20SSD%20供电的%20GPIO%20管脚。GPIO%20是可以由软件控制的通用管脚。
对于这块启动盘,PCIe%20主机驱动先初始化%20RK3588%20一侧的控制器,识别连接的%20SSD。NVMe%20驱动随后配置%20SSD%20内部的存储控制器,等待它报告就绪。控制器就绪后,Linux%20继续识别存储空间、注册块设备,并读取盘上的分区表。
找到保存系统文件的根分区后,Linux%20将它挂载,开始使用其中的程序和文件。内核接着启动系统服务管理程序%20systemd,由它启动网络、图形登录等服务。图形登录服务启动后,屏幕出现登录界面,用户登录后进入桌面。
下图展示从这块硬盘启动到进入桌面的顺序。图中只列出启动盘相关的步骤,其他设备的初始化可以并行进行。
Linux启动流程
图%202:本机从%20NVMe%20启动到进入桌面的主要步骤。
U-Boot%20已经从硬盘读出内核,Linux%20仍可能在初始化同一块硬盘时失败。 Linux%20使用自己的驱动,重新配置供电、复位和命令队列;U-Boot%20读文件成功只能说明前一阶段的访问正常。
显示控制器可以继续输出%20U-Boot%20留下的%20Logo,此时%20CPU%20可能已经执行%20Linux%20的代码。画面仍停在%20Logo,实际进度需要通过%20CPU%20指令地址或内核日志确认。
ARM64%20启动接口规定了固件准备内存、传入设备树和进入内核的要求。[1]%20本次使用幽兰原有的%20U-Boot%20启动方式。
二、PCIe%20设备枚举后,NVMe%20驱动等待控制器就绪
1.%20PCIe%20主机驱动准备通信资源,Linux%20枚举%20SSD
RK3588%20一侧的%20PCIe%20主机控制器负责与%20SSD%20通信。主机驱动准备时钟、复位和物理接口后,双方建立%20PCIe%20连接,系统才能读取设备标识并分配地址资源。这个发现和配置设备的过程称为枚举。
SSD%20内部的%20NVMe%20控制器执行存储命令。设备枚举后,Linux%20的%20NVMe%20驱动访问它的寄存器,设置命令队列并请求启用控制器,再读取状态寄存器等待就绪。
我们后来取得的现场显示,PCIe%20连接已经建立,SSD%20标识能够读到,连接参数为%20Gen3%20×4。接下来的%20NVMe%20初始化却停在了等待就绪这一步:驱动发出启用请求后,控制器一直没有报告%20Ready。Gen3%20×4%20描述连接速率和通道数量,Ready%20则表示%20SSD%20的存储控制器已经准备好处理命令。
瑞芯微的%20RK3588%20TRM(Technical%20Reference%20Manual,技术参考手册)给出了主机侧的实际结构。下面原图左侧是芯片内部互连,中间是五个%20PCIe%20控制器,右侧是负责物理信号收发的%20PHY。本次%20NVMe%20使用设备树中的 pcie3x4,对应图中的四通道控制器%20PCIe30X4;其他控制器及其%20PHY%20选择也画在图里,便于核对具体端口。
瑞芯微RK3588%20TRM中的PCIe控制器子系统
图%203:RK3588%20的%20PCIe%20控制器子系统。Interconnect%20为片上互连,Bridge%20为桥接逻辑,PHY%20为物理接口。图描述主机侧,SSD%20内部的%20NVMe%20控制器另由前文说明。[9]
PCIe%20主机初始化时,需要准备参考时钟和%20PHY,释放相关复位,再开始连接训练,由两端协商通信状态。设备枚举后,还要打开内存访问和总线主控权限。[9]
RK3588手册中的PCIe主机上电复位步骤
图%203a:手册中的主机上电复位步骤:释放复位后,需要等待一段时间,或检查可用的锁相环(PLL)锁定标志。[9]
2.%20驱动配置管理队列,再请求控制器启用
NVMe%20驱动写入寄存器,设置管理队列参数并请求启用%20SSD%20控制器;再读取状态寄存器,检查控制器的就绪状态。
为了访问这些寄存器,Linux%20将%20SSD%20的%20BAR%200%20地址资源映射成内核可访问的地址,之后用 readl()、writel() 读取状态、写入配置。BAR%20是%20Base%20Address%20Register,基地址寄存器,用于描述%20PCI%20设备的地址资源;按地址访问设备寄存器的方式称为%20MMIO,即内存映射%20I/O。
接下来,驱动在主机内存中分配管理提交队列和管理完成队列。SSD%20从提交队列读取命令,把执行结果写入完成队列。为了让%20SSD%20访问这两块内存,驱动把设备可用的队列地址写进控制器寄存器。
SSD%20对主机内存的访问使用%20DMA,即直接内存访问:CPU%20准备队列和地址,设备通过%20PCIe%20自己读取命令、写回结果。初始化使用的管理队列承担%20Identify%20等操作,Identify%20用于取得控制器及存储空间的信息。普通文件读写稍后使用另外的%20I/O%20队列。
这里有两个方向的访问:主机写入%20SSD%20控制器的寄存器,SSD%20控制器则通过%20DMA%20读取命令、写回数据和完成项。
NVM%20Express%20的规范用下面这幅图展示队列与控制器的关系。左侧是一对管理队列,右侧是普通%20I/O%20队列;队列位于主机内存,SSD%20控制器读取提交项并写回完成项。
NVM%20Express官方管理队列与I/O队列图
图%204:NVMe%20管理队列与%20I/O%20队列。Host%20表示主机,Controller%20表示设备控制器;图展示队列一对一对应的示例,实际队列数量由驱动与硬件共同决定。[6]
初始化时,有几个寄存器直接关系到我们的故障:
CC是控制器配置寄存器,CC.EN表示主机要求启用控制器。CSTS是控制器状态寄存器,CSTS.RDY为%201%20时表示控制器已就绪。AQA保存管理队列的大小;ASQ和ACQ分别保存管理提交队列、管理完成队列的设备可访问地址。
这些寄存器属于%20SSD,访问要经过%20PCIe。设备树中的 pcie-apb 则供驱动配置%20RK3588%20本地控制器、读取连接状态。这两组寄存器记录分别用于检查%20PCIe%20连接状态和%20NVMe%20控制器状态。
队列准备完成后,主机写入 CC.EN=1,表示要求启用控制器。这只是请求,SSD%20还要完成内部准备;驱动随后反复读取 CSTS,等待 RDY 变成%201。只有观察到就绪,才继续提交%20Identify%20管理命令。
NVMe规范对控制器Ready状态的判定
图%204a:规范以 CSTS.RDY=1 判定控制器已准备好处理命令。[6]
NVMe%20把设备暴露的存储空间称为%20namespace。Linux%20在找到%20namespace%20后注册块设备,再识别其中的分区。我们的原盘对应 nvme0n1,系统使用其中的第二个分区作为根分区。
NVMe初始化流程
图%205:NVMe%20初始化的主要控制顺序。CC、CSTS%20等字段来自%20NVMe%20寄存器定义;本次故障停在等待控制器就绪的步骤,驱动尚未提交第一条%20Identify%20命令。
3.%20管理命令通过后,文件读写才能开始
前面的初始化让%20Linux%20找到块设备和分区。接下来,应用的文件读写经过文件系统和块设备层,交给%20NVMe%20驱动。驱动先把命令写入主机内存中的%20I/O%20提交队列,再把更新后的队列尾部位置编号写入%20SSD%20的通知寄存器。该寄存器在%20NVMe%20规范中称为%20Submission%20Queue%20Tail%20Doorbell,以下简称%20Doorbell%20寄存器。SSD%20根据队列位置的变化,确定需要读取的新命令。[6]
SSD%20读取队列里的命令,按命令提供的地址传输数据,然后写入完成队列,并通过中断通知%20Linux。驱动处理完成项后,上层读写请求才能结束。命令、文件数据和完成结果分别保存在自己的内存区域,下图把它们分开画出。
NVMe读写数据流
图%206:正常%20I/O%20的逻辑数据流。实线表示请求、命令、数据和结果的传递;虚线表示驱动写入新队列位置通知%20SSD,或%20SSD%20通过中断通知驱动。数据方向随读写请求变化;完成通知也可由驱动轮询取得,图中展示使用中断的路径。
回到本次故障,驱动发出启用请求后一直等不到%20Ready,尚未提交第一条%20Identify%20命令,原盘的块设备和分区也还没有注册。我们因此先追踪启用请求和%20Ready%20等待。普通%20I/O%20队列、文件系统和桌面服务,要等这一步通过后再验证。
这些步骤对应%20NVMe%20PCI%20驱动的 pci.c 和%20NVMe%20核心代码的 core.c;文末列出了本次源码版本及两个文件的原始链接。[3]
三、挥码枪从内存中取回了启动现场
现在已经知道应当观察%20NVMe%20初始化,但机器停在%20Logo,没有进入可以运行采集命令的系统。通常这时会先接串口,把%20U-Boot%20和内核的文字输出送到另一台电脑。
但接上串口后,电脑虽然识别到了探针的%20USB%20设备,终端却长时间没有启动日志。我们排查了设备树的引脚配置、SD%20转接线和探针固件,仍没能稳定收到输出。于是先用挥码枪的%20SWD%20接口读取%20CPU%20状态和内存中的日志,定位%20CPU%20所在函数,并取回已经保存的启动记录。
挥码枪是格蠹公司的硬件调试探针,正式产品名称是%20Nano%20Target%20Probe,简称%20NTP。NanoCode%20是配合探针使用的调试%20IDE。电脑上的调试软件通过%20USB%20发出请求,探针再通过%20SWD(Serial%20Wire%20Debug,串行线调试)访问目标芯片的调试单元。本次专用%20SD%20转接线把幽兰的%20SWD%20信号引到探针,两根信号分别承担时钟和数据,另需共同的地参考。
Arm%20的调试访问端口称为%20DAP。主机调试软件通过它访问芯片上的调试资源;CMSIS-DAP%20则规定调试软件与探针之间的通信方式。对于本次已验证的%20RK3588%20连接,调试工具可以读取%20CPU%20寄存器和内存,也可以设置硬件断点、暂停%20CPU,再恢复执行。这个过程不依赖目标系统的%20SSH%20服务。
Arm%20的官方图展示了调试软件、探针与片上调试资源之间的连接:左侧%20Debugger%20是电脑上的调试软件,中间%20CMSIS-DAP%20是探针中的协议实现,右侧%20CoreSight%20是%20Arm%20的片上调试与跟踪组件。图中展示多种接口,本次实际连接使用%20USB%20和%20SWD。
Arm%20CMSIS-DAP官方接口图
图%207:Arm%20CMSIS-DAP%20接口图。Cortex-M、Cortex-A%20表示可连接的处理器类别。[5]
格蠹二代挥码枪的官方资料还给出了专用五芯线的实物图。N%20端接探针,T%20端接%20SD%20转接头,五根线分别传递%20SWD%20数据、SWD%20时钟、地参考以及串口收发信号。
格蠹二代挥码枪五芯SD转接线
图%208:格蠹二代挥码枪五芯线。SWD→DA3、SWK→DA2、GND→GND、TX→DA0、RX→DA1;SWK%20是探针上时钟脚的标记。[7]
挥码枪SWD调试连接
图%209:本次使用的两种路径。UART%20传递目标输出的文字;SWD%20访问芯片的调试资源。两者共用探针%20USB%20连接,但观察对象不同。图为按本机连接和%20Arm%20CMSIS-DAP%20说明绘制的原理示意。
Linux%20的 printk 日志会先写入内存,再通过串口等接口输出。即使串口终端没有文字,内存中仍可能保存着启动记录。
读取这些记录,需要准备与运行内核匹配的 vmlinux。启动用的 Image 是内核镜像,vmlinux 则保留调试所需的符号和类型信息。调试器利用这些信息,把%20CPU%20指令地址对应到函数,定位日志缓冲区并解析日志结构。换一个构建,这些地址和布局可能变化,因此每轮取证都保存对应产物。
本次使用%20NanoCode,也使用支持%20CMSIS-DAP%20的%20OpenOCD%20配合%20GDB,读取%20CPU%20状态并保存内存中的日志。板级连接按%20NanoCode%20的幽兰内核调试教程完成。[5]
驱动等待设备时,执行初始化的任务可以睡眠,CPU%20转去执行其他任务或进入空闲状态。因此,停核时看到空闲函数,还要结合%20NVMe%20日志和寄存器,核对驱动最后记录的初始化操作与当时的控制器状态。本次%20SWD%20读取的是主机%20CPU%20和内存,SSD%20内部的执行位置需要设备自己的调试资料。
暂停%20CPU%20会改变时序。测量%20NVMe%20等待时,我们让初始化先自然运行到成功或超时,再停核保存已经写下的记录,随后恢复执行。这样取得的是故障发生前的日志;停核期间经过的时间单独记录。
后来,我们确认旧探针固件处理%20USB%20设置波特率请求时会超时。按官方流程升级固件后,已经配置好的%206.12%20串口可以稳定双向收发,逐字节比对一致,重启日志也能连续保存。升级发生在%20NVMe%20首次修复之后:早期排查主要依靠%20SWD%20取回的数据,后续重启验证才有了连续的串口记录。
四、用%20ftrace%20和%20kprobe%20记录%205.10%20的正常初始化时序
通过%20SWD%20保存%206.12%20的失败现场后,我们开始采集正常系统的时序。原%205.10.110%20能从同一块盘启动,适合记录控制器启用、就绪位变化和首条管理命令完成的时间,再与%206.12%20的相应记录比较。
当时缺少完整的太乙定制内核源码,公开源码之间的差异无法代替旧系统的实际执行记录。好在旧内核提供了%20ftrace%20和%20kprobe,允许我们在保持原内核与设备树的条件下增加启动采集。
1.%20先记录初始化函数、寄存器值和管理命令
ftrace%20是%20Linux%20内核提供的跟踪设施。这里使用它的%20function%20tracer%20记录指定函数的进入顺序,又启用%20NVMe%20已有的事件,记录命令准备和完成。NVMe%20驱动的 nvme_probe 是识别和初始化设备的入口。记录保存在每个%20CPU%20的环形缓冲区中,新的事件会持续写入;空间耗尽时,旧数据可能被覆盖。
函数跟踪记录保存函数名称和进入时间,寄存器参数与读值则由动态探针采集。kprobe%20允许在指定函数或指令位置设置动态探针,命中时采集参数或寄存器;kretprobe%20用来记录函数返回值。我们把这些探针生成的事件也写入%20ftrace%20缓冲区。
| 观察方式 | 本次观察对象 | 记录内容 |
|---|---|---|
| function%20tracer | PCIe%20初始化、NVMe%20probe、控制器关闭与启用、Ready%20等待 | 函数进入顺序和发生时间 |
| kprobe | 寄存器写入请求、读取偏移和实际读值 | 寄存器偏移、写入值和实际读值 |
| kretprobe | 关闭、启用、等待函数的返回码 | 函数返回码,结合对应函数的定义解释退出结果 |
| NVMe%20静态事件 | Identify%20等命令的准备和完成 | 控制器%20Ready%20后的管理命令执行记录 |
采集寄存器值前,需要确认函数参数存在哪个%20CPU%20寄存器中,以及读取指令执行后,硬件读数保存在哪里。我们核对了目标函数的参数原型和反汇编指令。原内核保存的%20BTF%20类型信息补充了相关结构体字段的类型与偏移;参数所在的寄存器和读取指令的位置,则以这次构建的反汇编为准。
下面是本次太乙%20ARM64%20构建上验证过的%20CC%20写入探针:
p:ulan_cc%20nvme_pci_reg_write32%20c=%x0:x64%20o=%x1:x32%20v=%x2:x32
p 表示普通%20kprobe,这个定义没有指定指令偏移,因此位于函数入口;ulan_cc 是事件名。后面的三项分别采集控制器指针、寄存器偏移和写入值。我们核对过这个构建的函数原型,参数对应%20ARM64%20的%20x0、x1、x2。当偏移为 0x14 时,写入目标是%20CC,v 字段保存驱动传入的写入值。
读取%20CSTS%20时,还需要区分函数返回值和寄存器读数。nvme_pci_reg_read32() 的返回类型是 int,第三个参数 val 是保存读数的地址。所对照的瑞芯微%206.12%20源码中,这个函数的实现如下:[3]
static int nvme_pci_reg_read32(struct%20nvme_ctrl%20*ctrl,%20u32%20off,%20u32%20*val)
{
*val%20=%20readl(to_nvme_dev(ctrl)->bar%20+%20off);
return 0;
}
readl() 返回设备寄存器的%2032%20位读数,赋值语句将它写到 val 指向的位置,随后函数直接返回%200。实际读数为%200%20或%201%20时,函数都返回%200。因此,kretprobe%20记录到返回值%200,只能说明函数执行到了这个返回位置;判断%20Ready%20需要取得实际读数,再检查其中的 CSTS.RDY 位。把函数返回的%200%20当成%20CSTS%20的值,会把不同的寄存器状态都误记为%200。
在%205.10%20的实际采集中,我们检查原内核的机器指令,把值探针放在该函数的 +0x24 位置;在这个构建里,x1%20此时保存实际读值。这样,函数返回码与%20CSTS%20实际读值就分别保存在对应事件中。指令偏移随构建变化,换%20Image%20后要重新核对。参数取值和返回探针使用%20Linux%205.10%20的%20kprobe%20事件接口。[4]
2.%20在%20NVMe%20初始化之前启用采集
如果等%20SSH%20上线再打开%20ftrace,NVMe%20初始化早已结束。我们把%20function%20filter、NVMe%20事件和动态探针放到下一次启动的配置中,并保留原内核、设备树和根分区。
这次采集使用 trace_clock=mono,让所有%20CPU%20的事件采用统一的单调时间;每个%20CPU%20配置%201%20MiB%20缓冲区,八个%20CPU%20合计约%208%20MiB。选择指定函数和事件,避免把整个内核的函数调用全部灌进缓冲区。
下面按参数分行展示这次启动设置的部分内容;动态探针通过 kprobe_event 另行注册。它们要追加到保留的原启动参数中,具体文件写法取决于目标机的启动程序:
trace_clock=mono
trace_buf_size=1M
ftrace=function
ftrace_filter=rk_pcie_really_probe,rk_pcie_establish_link,nvme_probe,nvme_reset_work,nvme_disable_ctrl,nvme_enable_ctrl,nvme_wait_ready
trace_event=nvme:nvme_setup_cmd,nvme:nvme_complete_rq
设置启动采集前,还做了在线试采:发出一条只读%20Identify,确认准备、完成和返回事件能够解析,并清理试采状态。随后才正常重启%205.10,取得真正的初始化记录。启动后的服务冻结跟踪,再导出文本、事件格式、探针统计和每%20CPU%20缓冲区统计。[4]
以下是其中几条原始记录的字段节选。为便于阅读,省略了进程显示栏和内核地址,时间与事件值保持原样:
2.528095:%20ulan_cc:%20o=0x14%20v=0x460001
3.444062:%20ulan_value:%20v=0x0
3.446116:%20ulan_value:%20v=0x1
3.446834:%20nvme_setup_cmd:%20qid=0,%20cmdid=4112,%20cmd=(nvme_admin_identify%20cns=1,%20ctrlid=0)
3.460348:%20nvme_complete_rq:%20qid=0,%20cmdid=16,%20status=0x0
这些记录需要结合入口事件解释。ulan_value 自身没有保存寄存器偏移,分析程序按同一个线程把它与前一条读取入口配对,确认读取的是偏移 0x1c 的%20CSTS。执行初始化的内核工作线程会在%20CPU%20之间迁移,因此配对依据是线程身份,单凭%20CPU%20编号会拆散同一次操作。
启用写入的值 0x460001 中,最低位的%201%20是%20EN。CSTS%20从%200%20变成%201,表示本次读到了%20Ready。管理命令随后提交并成功完成,顺序与前面介绍的正常初始化一致。
下图把这五条驱动事件放回初始化过程。队列配置、Doorbell%20寄存器写入、DMA%20和完成通知按协议补充,实际采集没有逐笔记录%20PCIe%20事务。
正常与失败的NVMe初始化时序对照
图%2010:5.10%20从启用控制器到完成第一条%20Identify%20的过程。带时间的五条消息来自%20trace,其余动作按%20NVMe%20协议说明。
3.%20等待时间计算与跟踪数据完整性检查
我们用同一次%20trace%20的时间相减:
首次观察到%20Ready%20的用时
=%203.446116%20s%20-%202.528095%20s
=%200.918021%20s
=%20918.021%20ms
第一条%20Identify%20从命令准备到完成的用时
=%203.460348%20s%20-%203.446834%20s
=%2013.514%20ms
“首次观察到”这个限定有实际意义。驱动每隔一段时间读取状态,设备可能在两次读取之间改变%20RDY。最后一次读到%200%20是%203.444062%20秒,第一次读到%201%20是%203.446116%20秒。因此,就绪变化发生在启用请求后的 (915.967,%20918.021] 毫秒之间,918.021%20毫秒是首次被软件观察到的时间。
这里的%2013.514%20毫秒以 nvme_setup_cmd 为起点,它记录驱动准备命令的阶段,包含随后提交队列、通知设备及处理完成的过程。它适合对比驱动观察到的命令用时;SSD%20内部的命令执行用时,需要通过设备侧记录或%20PCIe%20事务另行测量。
准备事件中的 cmdid=4112 和完成事件中的 cmdid=16 还需要配对。核对这版事件定义后可知,提交命令%20ID%20的低%2012%20位是请求%20tag,即请求编号,高%204%20位是%20generation,用于区分请求代次;完成记录只保存请求%20tag。因此,本次取准备事件%20ID%20的低%2012%20位:4112%20&%200xfff%20=%2016,确认两条记录属于同一请求后再计算时间差。这个规则来自实际事件实现,换版本要重新检查。
完整%20trace%20共保存了%208751%20条事件。七个动态探针的%20missed%20计数均为%200,各%20CPU%20的%20overrun、commit%20overrun%20和%20dropped%20events%20也均为%200。这些计数用于检查探针漏记、缓冲区覆盖和事件丢失。冻结跟踪时,还有一个普通%20I/O%20请求尚未完成;它的后续过程超出了本次采集窗口,因此保留为未完成记录,未计入命令失败。
这些统计支持本段记录完整,跟踪开销仍属于测量条件。比较新旧内核时,我们使用各自记录中的时间差,printk%20和%20trace%20的绝对时间分别分析。
4.%20验证%20SWD%20读回的跟踪记录与系统导出一致
正常系统能够导出跟踪文件,6.12%20失败时却可能连根分区都无法挂载。为继续使用同样的记录,我们还在正常系统上验证了%20SWD%20读回%20ftrace%20内存页的办法。启动采集已冻结后,停住%20CPU,读取一个%204096%20字节的%20ftrace%20内存页;又在 __arm64_sys_getpid 设置硬件断点,命中后读取同一页。两份数据逐字节一致,已提交区为%204080%20字节,解析出%20165%20条记录。第一条 nvme_reset_work 的时间与文本%20trace%20对应。
断点发生在初始化完成之后,没有混入前面的%20918%20毫秒测量。这次比对验证了从内存读取%20ftrace%20页的方法,供后续%206.12%20失联取证使用。
五、在%206.12%20中记录供电、复位与%20NVMe%20初始化
回到%206.12,接下来的采集覆盖初始化函数、寄存器状态以及此前的供电和复位操作,便于和正常样本逐项对照。
这次拥有可修改的源码,所以在%20PCIe%20主机驱动和%20NVMe%20初始化代码中增加定点日志:从%20SSD%20供电、复位和%20PCIe%20连接状态,一直记录到配置读取、管理队列地址、CC%20写入与读回,以及%20Ready%20等待的开始和结束。
ftrace%20继续记录函数顺序,源码日志补上字段值;失败后,SWD%20把两者从内存取回。这些记录按时间对应到供电、复位、队列配置和%20Ready%20等待,便于检查各步的返回结果与状态变化。
我们没有为每次状态轮询都打印一行。驱动原有循环约每%201~2%20毫秒读取一次,全部打印会制造大量输出,也可能明显扰动等待。日志只记录首次采样、状态变化、每十秒摘要和最终返回;额外的%20PCIe%20寄存器与错误计数采样也有数量上限。
其中一轮完整观测留下了如下结果:
| 阶段 | 实际观察 | 本轮判断 |
|---|---|---|
| PCIe%20配置访问 | 能读到%20SSD%20标识,两端显示%20Gen3%20×4 | SSD%20已被%20PCI%20枚举 |
| 控制器关闭 | 请求清除%20EN,等待%20RDY=0%20成功 | 关闭阶段返回成功 |
| 管理队列 | AQA、ASQ、ACQ%20读回预期值,队列页和设备地址已记录 | 主机完成了可观察的队列配置 |
| 控制器启用 | CC%20读回%20EN%20已设置 | 启用请求写入后能被主机读回 |
| Ready%20等待 | 64,029%20次轮询,约%20128.0005%20秒,CSTS%20始终为%200 | 初始化等待超时,驱动尚未提交%20Identify |
等待上限来自设备能力寄存器 CAP.TO。该轮字段为%20255,驱动对应上限为 (255%20+%201)%20/%202%20=%20128 秒。实验让等待完整运行,再取回现场。
和%205.10%20对照,最明显的差异已经出现:旧系统约%20918%20毫秒读到%20Ready,这轮%206.12%20直到%20128%20秒上限仍读到%200。与此同时,SSD%20能被枚举,队列配置和%20CC%20的启用位能够读回。排查因此集中到控制器启用后的准备条件。
这也把最初的%20Logo%20对应到了执行过程。NVMe%20初始化没有注册原盘分区,根设备 nvme0n1p2 没有出现;启动参数中的 rootwait 又要求系统继续等待根设备。后面的%20systemd%20和登录服务无从启动,显示控制器仍可以保留原来的画面。直接挡住启动的步骤已经找到了,下一项工作是解释控制器为何一直没有准备好。
我们先检查管理队列地址及%20PCIe%20地址转换窗口,因为%20SSD%20需要通过这些地址访问内存。失败样本的队列位于%204%20GiB%20以上,正常%205.10%20和后来成功的%206.12%20也使用高于%204%20GiB%20的地址,因此高地址本身不能区分成功与失败。
同次等待期间,所采硬件错误计数没有增加。软件记录了驱动发出的供电、复位请求和寄存器读回,但这些数据还不能还原%20SSD%20电源、参考时钟和复位信号的完整波形。为继续缩小范围,我们调整软件能控制的等待和复位条件,观察%20Ready%20是否随之改变;硬件波形和%20SSD%20内部状态仍作为待补充证据。
六、从调整等待和复位,查到重新供电的间隔
U-Boot%20刚刚用过这块盘,Linux%20随后重新初始化,却一直等不到%20Ready。我们接着测试两类条件:PCIe%20配置请求发出的时机,以及复位、重新供电后的设备状态。两组实验分别保存操作前后的寄存器读值,以及驱动等待%20Ready%20的结果。
复位实验涉及不同对象。PCIe%20主机内部有多种复位信号,各自作用于核心逻辑或总线接口;SSD%20外部供电则由本板设备树和电源驱动控制。[9]
| 对象 | 手册或本板源码中的作用 | 本次操作与记录 |
|---|---|---|
PCIe%20主机的 core_rst_n、AXI%20与%20DBI%20接口复位 |
分别作用于核心逻辑及相应总线时钟域 | 记录主机驱动操作的复位资源,分别对应核心逻辑和总线接口 |
| PHY%20与参考时钟 | PHY%20初始化和释放复位需要满足参考时钟等条件 | 完整卸载主机时需要同时记录%20PHY、时钟的停止与恢复 |
PERST#%20等设备复位信号 |
参与%20PCIe%20接口复位;功能级复位、总线复位还各有协议定义 | 按功能级复位、总线复位和供电关闭分别记录操作与结果 |
本板 vcc3v3_pcie30 |
固定电源节点通过使能%20GPIO%20控制%20SSD%20供电 | 关闭间隔由%20regulator%20配置和日志核对,三秒取值由后面的本机对照实验确定 |
1.%20PCIe%20的%20100%20毫秒等待完成了一项时序对照
我们先检查%20PCIe%20连接建立后,驱动隔多久才发出配置请求。在所对照的代码中,旧实现这里等待%20100%20毫秒,6.12%20对应位置等待%2050%20毫秒;附近注释也提到了高速连接建立后的等待要求。这使我们怀疑配置请求可能发得过早,于是把等待改为%20100%20毫秒,并通过日志检查实际访问时刻。
这%20100%20毫秒指高速连接训练完成后、首次配置请求前的等待。它与电源稳定后释放 PERST# 的等待属于不同条件。[12]
这轮修改后,NVMe%20仍然等待超时。日志显示,首次成功读取%20SSD%20标识发生在%20PCIe%20检测之后约%20166%20毫秒,也在复位释放之后约%20230%20毫秒。
从这组时间看,成功读取%20SSD%20标识已经晚于关注的%20100%20毫秒条件,而%20Ready%20依旧不出现。恢复这处等待没有解决当前故障,所以我们保留其日志,转向设备状态恢复的实验。
2.%20分别测试设备和总线复位后的%20Ready%20状态
如果设备从前一启动阶段继承了不合适的状态,让它重新复位是下一项可验证操作。我们分别测试%20PCI%20功能级复位和总线复位:前者针对设备功能,后者影响下游设备的复位过程。
两种复位后,日志都能看到%20CC%20被清除,驱动随后重新写入管理队列并启用控制器,CSTS%20仍保持%200。这说明这些复位动作实际发生了,但还没有使这块盘恢复就绪。
两种复位都没有恢复%20Ready,接下来准备停止整个%20PCIe%20主机,再重新初始化它管理的时钟、PHY%20和%20SSD%20电源。此时%20NVMe%20根分区仍不可用,诊断程序必须从内存运行。为此,我们把程序及其依赖放进小型%20initramfs,让它在原盘根分区挂载之前执行。
第一次%20NVMe%20初始化仍按原条件执行并留下失败记录;之后,内存中的诊断程序卸载和重新绑定主机驱动,观察停止与恢复的效果。
3.%20完整停止%20PCIe%20主机后,第一次看到了%20Ready
这次实验先让初次初始化超时,再卸载 fe150000.pcie 对应的%20Rockchip%20PCIe%20主机驱动。
按照源码,卸载过程会移除下游%20PCI%20设备,停止%20PHY%20和相应时钟,请求主机复位,并关闭%20SSD%20的供电使能。现场记录显示,供电使用计数归零,控制电源的%20GPIO%20变低。我们等待三秒,再重新绑定同一个主机设备,让初始化重新执行。
同一块盘、同一份内核,这次首次观察到%20Ready%20的等待约为%20734%20毫秒。随后管理命令继续执行,用于普通读写的%20I/O%20队列建立,日志终于出现:
nvme0n1:%20p1%20p2
这短短一行是排查中一个关键的转折。此前反复等待%20128%20秒都没变化的状态,终于在不到一秒的就绪等待后变了;系统找到了原来的两个分区。
同一块硬盘、同一份内核,在完整停止并恢复主机资源后读到了%20Ready。不过,卸载同时改变了供电、时钟、PHY%20和复位,单凭这轮结果还不能确定是哪一项起作用。
供电关闭间隔可以通过设备树单独设置。下一轮保留同一份内核代码,只让第一次初始化在重新供电前等待三秒,检查它能否直接成功。这样可以进一步区分供电间隔和其他恢复动作的作用。
4.%20沿用同一%20Image,只改设备树的供电关闭间隔
保持同一份%20Image,在%20SSD%20的固定电源节点 vcc3v3_pcie30 中新增:
/*%20本机验证值:重新打开SSD电源前,补足3秒关闭间隔。%20*/
off-on-delay-us%20=%20<3000000>;
这是%20Linux%20固定电源驱动已有的设备树属性,单位为微秒。原来的 startup-delay-us%20=%20<5000> 保留:它描述开启后的启动等待;新增属性约束关闭之后再次开启前的间隔,两者作用时点不同。[8]
本机节点未设置表示启动时已启用电源的 regulator-boot-on 属性。该版本固定电源驱动初始化时,把供电使能%20GPIO%20置低;Linux%20电源管理框架%20regulator%20随即记录初始关闭时间。
PCIe%20驱动随后请求供电时,regulator%20用 last_off%20+%20off_on_delay 计算最早开启时间;当前时间早于这个时点时,先补足三秒间隔的剩余等待,再开启供电。因此,这个属性也会影响%20Linux%20的第一次初始化:先关闭供电使能,再延后开启,让%20SSD%20获得更长的供电关闭间隔。[8]
这一回,供电开启日志从原样本约%201.577%20秒移到了约%204.834%20秒。NVMe%20等待约%20861%20毫秒后读到%20CSTS=1,432%20次轮询结束,两个分区随后注册成功。Image、NVMe%20轮询上限和队列地址策略没有因这次%20DTB%20对照改变。
增加%20SSD%20重新供电前的关闭间隔,是本机%20S3000%20在这组%206.12%20配置上的有效修复。 三秒是实验取值,会增加启动等待;最短可靠间隔、SSD%20内部状态和电源放电过程仍需设备资料或波形测量。
这里还要区分供电关闭间隔和上电后的复位时序。我们的三秒发生在关闭供电之后、再次开启之前。PCI-SIG%20的%20PCIe%20M.2%20上电与%20PERST#%20勘误讨论的则是上电到复位释放的间隔;它将 PERST# 的时序参数 TPVPGL 改为实现相关,并保留推荐值。[11]
这份勘误以设备端电源有效作为上电的计时参照。因此,主机%20GPIO%20切换与%20SSD%20端电压达到有效范围,需要分别记录。后续需要同步测量%20SSD%20电压和 PERST# 波形,核对实际的供电关闭与复位间隔;SSD%20内部状态的解释还需对应设备资料。
七、分区出现之后,还要真正读写并重新启动
看到原盘的两个分区,说明控制器初始化已经走过原来的停点。要成为可交付的启动修复,还要让系统从这些分区运行,并证明写入的数据在重启后仍然一致。
我们先撤去诊断用的内存根系统,改由原%20NVMe%20根分区启动%206.12。第一次%20SSH%20没有连回来,很容易让人以为又失败了;SWD%20保存的日志却已经显示%20ext4%20根分区挂载和%20systemd%20执行。存储阶段已经通过,这一次需要补齐的是网络驱动配置。
补齐相应网络配置后,机器从原盘进入系统。随后,在根分区写入一个%2064%20MiB%20文件,在启动分区写入一个%201%20MiB%20文件。测试使用直接%20I/O%20和 fsync,读回数据并核对%20SHA256%20校验值,再正常重启,读取同一批文件重新比对。校验值根据文件内容计算,用于检查这批数据在写入、读回和重启后是否一致。
直接%20I/O%20有助于减少页缓存对读回测试的影响,fsync 请求将文件数据同步到存储设备。这轮测试中,根分区和启动分区的文件写入、读回及重启后校验均通过。这个结果验证了本次修复后的分区读写和正常重启;突然断电恢复与长期可靠性需要另行测试。
后续用于%20GPU%20适配和数据采集的内核版本,也继续检查这批文件,并增加了小样本。正常重启后,四个文件合计%2067%20MiB%20的校验通过。
八、SWD%20取证与%20eMMC%20恢复让排查继续进行
这次排查最难熬的一段,是机器反复停在同一幅画面,而每次修改后只有“还是不行”这样的反馈。
后来我们逐渐把每轮实验组织成可重复的过程:保存原启动项和构建产物,启动独立测试镜像,让关键等待完成,保存日志、寄存器与跟踪数据,再恢复可工作的系统。每份结果关联对应的源码修改、构建产物和事件定义,供下一轮核对两次实验使用的代码、参数及观察结果。
挥码枪在这里的作用,第一是系统没有网络、串口没有输出时,读取%20CPU%20寄存器和内存中的日志;第二是通过%20SWD%20设置启动标记并复位,让幽兰进入%20eMMC%20板载闪存上的旧系统,再通过%20SSH%20恢复%20NVMe%20启动配置。
这套恢复操作依赖幽兰的启动固件、启动标记寄存器和%20eMMC%20恢复系统;这里使用的是已经在本机验证的步骤。强制复位时,文件系统可能仍有写入,因此需要记录复位前的状态,并在恢复后检查文件系统。系统能够正常运行时,优先通过系统命令重启。
我们也有过不合适的尝试:停核后,通过%20GDB%20主动调用内核函数,希望请求正常停机,结果产生了新的内核异常。此后撤下这一操作,恢复时使用已经验证的启动标记和复位流程。探针可以暂停和恢复%20CPU,但主动调用内核函数还需要满足该函数要求的执行上下文和返回条件。
最初看到两个分区时,我们已经松了一口气。等到原盘正常重启后再次通过文件校验,存储这一阶段才有了可以交付的结果。
原盘恢复后,桌面仍在用%20CPU%20绘图。下一篇将继续记录%20GPU%20适配及随之出现的启动异常,最后一篇再补充寄存器访问和%20DMA%20所经过的片上接口与地址转换。
参考资料
[1]%20Linux%206.12%20ARM64启动文档:固件准备内存、设备树与内核入口要求。
https://docs.kernel.org/6.12/arch/arm64/booting.html
[2]%20瑞芯微本次源码基准:本文内核基于该版本源码构建。
https://github.com/rockchip-linux/kernel/tree/470f9dccbdc42e7b8a824d0a5c5640a10e9457d2
develop-6.12 分支:
https://github.com/rockchip-linux/kernel/tree/develop-6.12
[3]%20NVMe%20寄存器定义与驱动实现:CC、CSTS、CAP、管理命令,以及寄存器读取和控制器初始化代码。
https://github.com/rockchip-linux/kernel/blob/470f9dccbdc42e7b8a824d0a5c5640a10e9457d2/include/linux/nvme.h
NVMe%20PCI%20驱动 pci.c,含 nvme_pci_reg_read32():
https://github.com/rockchip-linux/kernel/blob/470f9dccbdc42e7b8a824d0a5c5640a10e9457d2/drivers/nvme/host/pci.c
NVMe%20核心代码 core.c:
https://github.com/rockchip-linux/kernel/blob/470f9dccbdc42e7b8a824d0a5c5640a10e9457d2/drivers/nvme/host/core.c
[4]%20Linux%205.10%20kprobe事件与启动跟踪文档:动态参数取值与早期采集。
Linux%205.10%20kprobe事件:
https://www.kernel.org/doc/html/v5.10/trace/kprobetrace.html
启动跟踪文档:
https://www.kernel.org/doc/html/v5.10/trace/boottime-trace.html
[5]%20Arm%20CMSIS-DAP、接口原图与NanoCode%20幽兰调试教程:图%207%20的探针通信关系与本板调试连接。
Arm%20CMSIS-DAP:
https://arm-software.github.io/CMSIS-DAP/latest/index.html
接口原图:
https://arm-software.github.io/CMSIS-DAP/latest/CMSIS\_DAP\_INTERFACE.png
NanoCode 幽兰调试教程:
https://www.nanocode.cn/wiki/docs/youlan/ndbyl
[6] NVM Express Base Specification 1.4c:图 4 为第 8 页 Figure 1;图 4a 为第 299 页第 7.6.1 节的 Ready 判定。初始化步骤跨第 298—299 页,第 3.1.5、3.1.6 节分别定义 CC、CSTS。第 60—61 页第 3.1.25 节定义 Submission Queue Tail Doorbell,第 63 页第 4.1 节解释队列位置更新与命令提交的关系。© 2007 to 2021 NVM Express, Inc. ALL RIGHTS RESERVED.
NVM Express Base Specification 1.4c:
https://nvmexpress.org/wp-content/uploads/NVM-Express-1\_4c-2021.06.28-Ratified.pdf
第 299 页第 7.6.1 节的 Ready 判定:
https://nvmexpress.org/wp-content/uploads/NVM-Express-1\_4c-2021.06.28-Ratified.pdf#page=299
第 60—61 页第 3.1.25 节:
https://nvmexpress.org/wp-content/uploads/NVM-Express-1\_4c-2021.06.28-Ratified.pdf#page=60
[7] 格蠹二代挥码枪官方资料:探针功能与五芯线的引脚对应关系。
https://www.nanocode.cn/wiki/docs/gedu\_ntp\_wiki/gedu\_ntp\_wiki-20240328
[8] 固定电源的设备树定义与驱动实现:startup-delay-us、off-on-delay-us 的字段依据,以及供电关闭时间的记录与再次开启前的等待。
https://github.com/rockchip-linux/kernel/blob/470f9dccbdc42e7b8a824d0a5c5640a10e9457d2/Documentation/devicetree/bindings/regulator/fixed-regulator.yaml
电源管理框架 drivers/regulator/core.c:
https://github.com/rockchip-linux/kernel/blob/470f9dccbdc42e7b8a824d0a5c5640a10e9457d2/drivers/regulator/core.c
[9] Rockchip RK3588 TRM V1.0 Part2:瑞芯微编写,Stanford 镜像。图 3 为第 1187 页 Fig. 11-1,图 3a 摘自第 2155 页第 11.6.2.1 节的第 8 步。主机复位与时钟定义见第 2153 页第 11.6.1.3 节。版本日期为 2022-03-09。© 2022 Rockchip Electronics Co., Ltd.
Rockchip RK3588 TRM V1.0 Part2:
https://www.scs.stanford.edu/~zyedidia/docs/rockchip/rk3588\_part2.pdf
第 1187 页 Fig. 11-1:
https://www.scs.stanford.edu/~zyedidia/docs/rockchip/rk3588\_part2.pdf#page=1187
第 2155 页第 11.6.2.1 节的第 8 步:
https://www.scs.stanford.edu/~zyedidia/docs/rockchip/rk3588\_part2.pdf#page=2155
第 2153 页第 11.6.1.3 节:
https://www.scs.stanford.edu/~zyedidia/docs/rockchip/rk3588\_part2.pdf#page=2153
[10] PCI-SIG 的 PCIe、M.2 介绍与规范入口:PCIe 接口、模块外形与连接规格,以及 PCIe 基础规范的版本与勘误目录。
PCIe 接口介绍:
https://pcisig.com/what-pci-express-pcie-architecture
PCIe 模块外形介绍,含 M.2:
https://pcisig.com/blog/introduction-form-factors-pci-express%C2%AE
PCIe 基础规范入口:
https://pcisig.com/specification-overview/pci-express-base
[11] PCI-SIG M.2 上电与 PERST# 勘误:2014-09-18,针对 PCIe M.2 Spec 1.0,公开摘要解释复位参数和设备端电源有效的计时参照。
https://pcisig.com/PCIExpress/ECN/M.2/Power-uprequirementsforPCIesidebandsPERST\_etc.
[12] Linux PCI 主机驱动指南:资源、地址窗口、连接训练和移除顺序;部分接口晚于本文 6.12,实际行为以本文固定源码和日志为准。
https://cdn.kernel.org/doc/html/latest/PCI/controller/pci-controller-drivers.html
[13] 时创意 S3000 产品页:本文启动盘的品牌与产品系列。
https://www.shichuangyi.com/gutaiyingpan/4.html
(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生
扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物
也欢迎关注格友公众号
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论