本帖最后由 OrionisLi 于 2026-2-25 23:57 编辑
今天的文章有点硬,建议就着汤吃。
前一篇文章:鲁班猫4移植UEFI+grub引导
0x00 碎碎念Timing
半年前买了鲁班猫4(RK3588S)4+0g,花了点时间改了16g+128g。原厂的只能跑ubuntu,debian,还有一些buildroot和yocto,但对于我来说一秒不用arch那可是浑身难受,如芒在背,如鲠在喉。
当然我也考虑过移植archlinux上去,自然是没啥问题的。但是用uboot对于我来说简直就是折磨!(并不
所以闲得没事干,就在几个月前我完成了UEFI的初步移植。可以引导debian,ubuntu,archlinux,ubuntu server,甚至是PVE和ESXI,变成一台真正的服务器。
但是由于其内核并未合并进入主线导致一系列问题——干啥都要先把BSP内核搞进去。当然我这段时间就在研究怎么把内核给他整到主线里头去。比如https://gitlab.collabora.com/hardware-enablement/rockchip-3588
呃呃……扯远了。
这几天打算利用一下这个鲁班猫的6T NPU整点paddle玩玩(之前遇到一个OpenMP的多核心调度问题的Segfault打算现在研究研究来着),然后就是一顿模型转换,一顿写py脚本
然后炸了
> python run_paddle_OCR.py
> --> Load RKNN model
> --> Init runtime environment
> I RKNN: [11:27:44.967] RKNN Runtime Information, librknnrt version: 2.1.0 (967d001cc8@2024-08-07T19:28:19)
> E RKNN: [11:27:44.967] Meet unsupported target type: 0xffffffff
> E RKNN: [11:27:44.968] Meet unsupported rknn target type: 0xffffffff
> [1] 2251 segmentation fault (core dumped) python run_paddle_OCR.py
在开始深入研究之前,我想强行启动设备供电来启动NPU- <font size="4" face="新宋体">echo on | sudo tee /sys/bus/platform/devices/fdab0000.npu/power/control</font>
复制代码 板子也是一点没让我失望——直接Kernel Panic亖了。
0x01 一顿操作猛如虎
我不明白为什么会这样。我试着写设备树的假节点欺骗驱动,设置假频率表,覆盖供电配置,禁用IOMMU检查,至打算放弃使用IOMMU转而使用更低级的CMA方式……但是很显然,问题没有解决。
终于,在思考了一上午(bushi,之后我怀疑是UEFI的锅……
0x02 看看倒底怎么个事先
我们都知道,一般的UEFI的驱动启动流程保存在RockchipPlatformLib/RockchipPlatformLib.c文件里。
一般的驱动上电都写在这个文件里面。
但是这个文件里面没有任何关于RK8602的初始化,别问我咋知道的——因为这是我写的哈哈(悲
NPU电源域由RK8602独立控制,而我在RockchipPlatformLib.c中配置了RK806的BUCK1/3/4等,完全没有涉及RK8602,UEFI启动后,NPU电源处于未知状态。
事实上Linux的NPU驱动依赖UEFI或Bootloader完成基础电源配置,假如RK8602的电源配置都没有起来的话,那自然Linux就无法完成剩余的初始化了。
那自然,Linux在尝试运行时电源管理Runtime PM时,硬件处于未定义状态,触发致命错误。
执行echo on之后:
- Linux强制打开NPU的时钟。
- Linux强制打开NPU的IOMMU。
- IOMMU驱动尝试去读写NPU的总线接口。
而此时IOMMU面对的是一个电压为0V的设备,CPU发出总线读写指令 -> 硬件无响应 -> 触发 SError -> Kernel Panic。
这大概就是kernel panic的触发流程。
0x03 准备一些参数
虽然代码里写了RK806(主PMIC),但RK8602是独立的。我们需要知道它挂在哪条I2C总线上,以及它的从机地址。
可以看到,这个挂载到了I2C1下面。
或者是也可以通过阅读设备树源码得到。 - <font size="4" face="新宋体"><b><font color="#4169e1">&i2c1</font></b> {
- pinctrl-names = "default";
- pinctrl-0 = <&i2c1m2_xfer>;
- status = "okay";
- vdd_npu_s0: vdd_npu_mem_s0: rk8602@42 {
- compatible = "rockchip,rk8602";
- reg = <0x42>;
- vin-supply = <&vcc5v0_sys>;
- regulator-compatible = "rk860x-reg";
- regulator-name = "vdd_npu_s0";
- regulator-min-microvolt = <550000>;
- regulator-max-microvolt = <950000>;
- regulator-ramp-delay = <2300>;
- rockchip,suspend-voltage-selector = <1>;
- regulator-boot-on;
- regulator-always-on;
- regulator-state-mem {
- regulator-off-in-suspend;
- };
- };
- </font>
复制代码 写得也很明显,是挂载到i2c1底下的。
通过这个设备树,我们发现了更多信息:
- I2C总线:&i2c1 -> I2C1
- I2C地址:reg = <0x42> -> 0x42
- 芯片型号:rockchip,rk8602 -> RK8602
- 电压目标:0.8V(800mV) -> 寄存器值计算:(800 - 500) / 6.25 = 48 -> 0x30
- 使能方式:DTS无enable-gpios -> 纯I2C寄存器控制
补充一下——这个I2C1是有外部上拉电阻(后面需要用到)。
我们接着可以看到这两个I2C1_M2连接到了RK3588S芯片上的GPIO0_D4和GPIO0_D5引脚。
当然也可以阅读RK3588S的TRM手册来得到一样的结果——不过我嫌读那个太浪费时间了……几千页来着……
那就知道了,RK8602的I2C信号线处于RK3588S的GPIO0号Bank上。
然后我们看RK8602的EN引脚,可以看到这个上面连接的叫做BIG/NPU_EN。
我们全局搜索这个BIG/NPU_EN,可以发现在VDD_CPU_BIG0底下倒是有一个BIG/NPU_EN的线路。
而且再看旁边的线路,这是一个典型的分压电阻电路(R28和R30)。
- 上拉电阻(R28):100KΩ,连接到VCC_3V3_S3。
- 下拉电阻(R30):120KΩ,连接到GND。
让我们计算一下电压:
再看看这个引脚的使能电压。
1.79V>1.1V, 芯片使能。
这个使能电压有俩电压值VIH和VIL,待一会儿我们会详细分析。目前只需要知道我们需要高电平来使能芯片即可。
只要板子的VCC_3V3_S3电源一出来也就是板子一开机,这个BIG/NPU_EN信号就会自动变成高电平。所有相关芯片的都会自动使能。
再看这个VSEL引脚,这个引脚有一根信号线是连接的是NPU_VSEL,连接到了RK3588S的GPIO0_C1_d引脚上。 这个GPIO0_C1_d是干啥的呢?我们这时候只能搜一搜TRM了。  - Module Pin: pmic_sleep_3
- Pin Name: PMIC_SLEEP3/GPIO0_C1_d
这意味着GPIO0_C1_d在电源管理系统中被复用为pmic_sleep_3功能信号。
在PMIC的设计中,SLEEP信号通常用于在“正常模式”和“休眠/省电模式”之间切换:
- 正常工作模式: 此时不需要休眠,信号处于无效状态。
- 休眠模式: 系统进入待机,信号处于有效状态。
0x05 这些参数怎么传进去?
外部的Input信号大概分析完了,我们现在分析分析这个东西该传些什么参数进去,怎么传参数。
我们需要先确定这个芯片的die-id。

我们的芯片是RK8602对应的参数如下:- I2C地址:0x42
- 输出电压范围:0.5V-1.5V
- 默认输出电压:0.8V
- 电压变化步长:6.25mV
- 寄存器对应的ID:0xA
RK8602正常工作,需要往俩寄存器里写数据,也就是VSEL0_B和VSEL1_B。
这俩寄存器该怎么确定呢?还记得我们的寄存器对应的ID吗?上面有写的……
可以看看这个寄存器的表

设计两个寄存器就涉及到RK860芯片的一个DVS动态电压调节的特性,这样可以更快地进行电压切换。
比如CPU在全速运行时需要0.9V,休眠时只需要0.6V。软件可以预先在0x06填入0.9V(原始数据),在0x07填入0.6V(原始数据)。当系统需要休眠时,只需要拉动一根连接到VSEL引脚的GPIO线,使其变高,电压就会瞬间切换到0.6V,而不需要慢吞吞地发一堆I2C指令。这在对时序要求极高的场景下非常有用。
上面有标明地址VSEL0_B(0x06)和VSEL1_B(0x07)。那究竟用哪个呢?
这时候之前的VSEL就有作用了……
回顾之前的寄存器定义:
- 寄存器 0x06(VSEL0):当VSEL = L(低电平)时生效。
- 寄存器 0x07(VSEL1):当VSEL = H(高电平)时生效。
看看我们的CoT:
- 我们需要让NPU正常工作。
- 处于正常工作状态时,pmic_sleep_3信号应该处于 **无效状态**,也就是休眠模式。
- 无效状态对应低电平。
- 低电平对应硬件引脚VSEL = L。
- VSEL = L对应寄存器0x06。
这样逻辑就通顺了,也就明白了这个寄存器到底用哪个——用的是0x06的那个。
但是吧……由于这个RK860的datasheet里面没有直接标明其I2C的通信频率,而I2C的通信频率有三种:
- 标准模式:100kHz
- 快速模式:400kHz
- 高速模式:3.4MHz
我们就使用最低的标准模式吧——100kHz。
0x06 所以写代码吧开始。
导入一些变量
这个里面之前说的参数都出现过了基本。唯一要解释的就是这个I2C_RETRY_CNT。这个是重试次数,也就是通信失败时自动重试的次数,是用来提高鲁棒性的。
在这里设置了3次重传,3次是公认的“失败容忍阈值”——如果连续3次都失败,基本可以判定为硬件故障。
模拟开漏输出
I2C总线要求开漏输出,允许多个设备通过“线与”方式拉低总线。如果GPIO配置为推挽输出并强行输出高电平,当从设备试图拉低应答时,会发生引脚对引脚短路。
这里的逻辑巧妙地模拟了开漏(吹牛中……:
- 输出1(Level=TRUE):将GPIO设为输入模式。此时引脚呈高阻态,靠外部上拉电阻拉高。
- 输出0(Level=FALSE):先写0,再将GPIO设为输出模式。此时引脚被强行拉低。
不依赖具体的I2C硬件控制器驱动,而且通用性强,只要能控制GPIO就能通信。
I2C原语
- I2cStart/I2cStop:严格遵循I2C时序标准(Start:SCL高时SDA拉;Stop:SCL高时SDA拉高)。
- I2cSendByte:按位发送数据,并在发送完8位后读取PMIC(也就是从机)的ACK信号。
寄存器写函数
向RK8602电源管理芯片写入一个字节。Start->发送设备地址(0x42<<1)->发送寄存器地址->发送数据->Stop。包含了I2C_RETRY_CNT的重试循环。即使I2C通信偶尔会受干扰失败,重试机制也可以保证初始化的稳定性。
然后就是最关键的NPU电源初始化,就是吧我们之前的所有参数全部用上面软件实现的I2C串起来。
通常,驱动程序会等待操作系统加载完I2C控制器驱动后,再通过标准的API去操作电源。但这个函数不一样,它选择在UEFI极早期,绕过所有标准驱动,直接“手动接管”引脚,强行配置电源,然后再把引脚还给系统。
- GpioPinSetFunction (I2C_BANK, I2C_SCL_PIN, 0);
- GpioPinSetFunction (I2C_BANK, I2C_SDA_PIN, 0);
复制代码 将这两个引脚(PD4, PD5)从默认的“I2C 控制器功能”强行切断,切换为普通GPIO模式。此时UEFI还没初始化好复杂的I2C硬件控制器。为了确保通信绝对可控,使用软件模拟的方式来操作。
- GpioPinSetDirection (I2C_BANK, I2C_SCL_PIN, GPIO_PIN_INPUT);
- GpioPinSetDirection (I2C_BANK, I2C_SDA_PIN, GPIO_PIN_INPUT);
复制代码 将引脚设为输入模式。I2C总线是开漏结构,默认需要外部上拉电阻拉高。将GPIO设为输入(高阻态),相当于“松手”,让外部上拉电阻把电平拉高至1。这是I2C总线的空闲状态。如果直接设为输出高电平,万一总线上有其他设备正在拉低,会造成短路。设为输入是最安全的“起手式”。
- Status = Rk8602WriteReg(0x06, 0x30);
复制代码 寄存器0x06:根据我们之前的分析,这是VSEL0_B寄存器。它专门用于控制RK8602在VSEL引脚为低电平时的输出电压。由于板子硬件上把sleep信号拉低了,芯片只听这个寄存器的话。
数值0x30:看Datasheet的Feature里面有一句话Programmable output voltage:0.7125V to 1.5V with 12.5mV/step or 0.5V to 1.5V with 6.25mV/step意思就是有两种输出电压的方式:
- 0.7125V至1.5V,步进12.5mV。起步0.7125V,每次增加12.5mV。
- 0.5V至1.5V,步进6.25mV。起步0.5V,每次增加6.25mV。
我们属于第二种,那么就可以简单推出公式:
目标电压是0.8V。那么反推步数就是:
48的十六进制是0x30。这行代码将NPU的核心电压锁定在了0.8V,这是NPU稳定运行的黄金电压。
- <font face="新宋体" size="4"> Status = Rk8602WriteReg(0x05, 0x20);
- </font>
复制代码 寄存器0x05:这是PGOOD(Power Good)状态寄存器,属性是只读。
数值0x20:对应Bit5,即输入过压保护标志。
虽然手册说它是只读的,但在有些PMIC的设计惯例中,向状态寄存器写“1”往往用于清除中断标志。即便在RK860上这行代码无效(因为它是纯只读),它也是无害的。忽略即可(防御代码)。
- <font face="新宋体" size="4">Exit:
- /* Release I2C bus, switch back to I2C function. */
- GpioPinSetDirection (I2C_BANK, I2C_SCL_PIN, GPIO_PIN_OUTPUT);
- GpioPinSetDirection (I2C_BANK, I2C_SDA_PIN, GPIO_PIN_OUTPUT);
- GpioPinWrite (I2C_BANK, I2C_SCL_PIN, TRUE);
- GpioPinWrite (I2C_BANK, I2C_SDA_PIN, TRUE);
- MicroSecondDelay(5);
- GpioPinSetFunction (I2C_BANK, I2C_SCL_PIN, 9);
- GpioPinSetFunction (I2C_BANK, I2C_SDA_PIN, 9);
- </font>
复制代码 配置完成后,必须把现场清理干净,把控制权交还给系统。
- <font face="新宋体" size="4"> GpioPinSetDirection (I2C_BANK, I2C_SCL_PIN, GPIO_PIN_OUTPUT);
- GpioPinSetDirection (I2C_BANK, I2C_SDA_PIN, GPIO_PIN_OUTPUT);
- </font>
复制代码 强制输出高电平。这在I2C协议中代表STOP状态或总线空闲。这确保了我们退出时,总线处于一个稳定的状态,不会让PMIC误以为还有数据要传输。
- <font face="新宋体" size="4"> GpioPinWrite (I2C_BANK, I2C_SCL_PIN, TRUE);
- GpioPinWrite (I2C_BANK, I2C_SDA_PIN, TRUE);
- </font>
复制代码 这是RK3588硬件手册定义的I2C控制器功能。执行完这一步后,这两个引脚就不再受GPIO寄存器控制了,而是连通到了RK3588内部的硬件I2C控制器。这样,当Linux启动加载驱动时,它能通过硬件控制器正常访问PMIC,而不会发现引脚被占用了。
板级初始化
它负责按正确的顺序唤醒板子上所有关键的非标准硬件。这个函数严格遵循了数字电路启动的流程:
- 先供电 (Power)
- 再给时钟 (Clock)
- 最后解复位 (Reset)
在这里用到了Rockchip CRU寄存器的两个特殊机制——Write Mask与Active Low,因为内容有点多,不在这里展开写了。
0x07 结束了?
我起初是这么想的。但是很显然,单纯这么改之后并不奏效……该ffff还是ffff,该炸还是得炸。
目前UEFI已通过SafeInitNpu成功配置RK8602,但是执行echo on > /sys/.../npu/power/control立即触发同步外部中止,PC指向rk_iommu_is_stall_active。后续系统出现大量I2C超时、CPU调压失败等系统性紊乱。
查看dmesg日志,显示了一些和电源相关的信息,比如vdd_npu_s0: could not add device link fdab0000.npu: -EEXIST
debugfs: Directory 'fdab0000.npu-rknpu' with parent 'vdd_npu_s0' already present!
-EEXIST表示设备链接已存在,debugfs目录已存在,那么内核就认为regulator已经被注册过了。
硬件没问题(电压给了),问题出在内核与硬件的交互层。-EEXIST提示有重复注册,但设备树里vdd_npu_s0节点只出现一次。那么是谁抢在Linux之前注册了这个regulator?
CoT:
- Linux内核的regulator框架会在设备驱动匹配时,根据设备树节点创建regulator设备。
- 如果启动时regulator设备已经存在(通过其他固件接口如ACPI、UEFI设备路径),内核就会跳过创建,并报 -EEXIST。
- UEFI完全有能力在Linux启动前注册regulator设备——它可以通过EFI_RUNTIME_SERVICES或直接写内存等方式,在内核的DeviceTree或ACPI表中预置设备信息。
那么就是UEFI中存在某个驱动,它初始化了RK8602并“顺便”在内核空间留下了regulator设备注册痕迹。
进入UEFI固件源码目录,决定用最朴素的关键词搜索。既然是PMIC电源管理芯片,而且设备树中compatible为rockchip,rk8602,那么UEFI中很可能存在名称包含rk8602、rk860x或regulator的驱动。
因为搜索浪费了太多时间,在这里就不详细写了,搜索关键词是“RK860X”,使用命令为- grep -r "RK860X" --include="*.inf" --include="*.c" --include="*.dsc"
复制代码
迅速锁定了目标——Silicon/Rockchip/Drivers/I2c/Rk860xRegulatorDxe/Rk860xRegulatorDxe.inf,再指向Silicon/Rockchip/Drivers/I2c/Rk860xRegulatorDxe/Rk860xRegulatorDxe.c
当驱动成功匹配设备后,Rk860xRegulatorStart被调用。这里发生了最致命的行为:
这条语句将gRk860xRegulatorProtocolGuid协议安装到控制器句柄上。从此,系统中就存在了一个可供其他UEFI应用程序或驱动查询的regulator控制接口。
更严重的是——这个协议会一直存在于内存中,即使UEFI调用ExitBootServices()之后,这个协议的数据结构仍然驻留在内存里。Linux内核在启动初期扫描固件设备时,会发现这个协议,并将其转换为内核设备。这就是-EEXIST错误的根源——内核在解析设备树之前,就已经看到了一个名为vdd_npu_s0的regulator设备。
我们需要禁用edk2-rockchip/Silicon/Rockchip/FvMainModules.fdf.inc和edk2-rockchip/Silicon/Rockchip/Rockchip.dsc.inc里面有关Rk860xRegulatorDxe的引用。
- # INF Silicon/Rockchip/Drivers/I2c/Rk860xRegulatorDxe/Rk860xRegulatorDxe.inf
复制代码
直接这样注释即可。
之后测试OCR脚本就可以得到……
> python run_paddle_OCR.py
> --> Load RKNN model
> --> Init runtime environment
> I RKNN: [14:19:30.141] RKNN Runtime Information, librknnrt version: 2.3.2 (429f97ae6b@2025-04-09T09:09:27)
> I RKNN: [14:19:30.141] RKNN Driver Information, version: 0.9.8
> I RKNN: [14:19:30.142] RKNN Model Information, version: 6, toolkit version: 2.3.2(compiler version: 2.3.2 (e045de294f@2025-04-07T19:48:25)), target: RKNPU v2, target platform: rk3588, framework name: ONNX, framework layout: NCHW, model inference type: static_shape
> W RKNN: [14:19:30.173] query RKNN_QUERY_INPUT_DYNAMIC_RANGE error, rknn model is static shape type, please export rknn with dynamic_shapes
> W Query dynamic range failed. Ret code: RKNN_ERR_MODEL_INVALID. (If it is a static shape RKNN model, please ignore the above warning message.)
> --> Running inference
> Output shape: (1, 1, 640, 640)
> Done! Check result_heatmap.jpg

和
使用RKNN的测试也可以通过……
|