野火电子论坛

 找回密码
 注册

QQ登录

只需一步,快速开始

查看: 30|回复: 0

[经验\资料] 记一次使用UEFI启动ArchLinux但无法使用NPU的问题和解决方案

[复制链接]
发表于 昨天 22:20 | 显示全部楼层 |阅读模式
本帖最后由 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
  1. <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下面。

或者是也可以通过阅读设备树源码得到。
  1. <font size="4" face="新宋体"><b><font color="#4169e1">&i2c1</font></b> {
  2.     pinctrl-names = "default";
  3.     pinctrl-0 = <&i2c1m2_xfer>;
  4.     status = "okay";
  5.     vdd_npu_s0: vdd_npu_mem_s0: rk8602@42 {
  6.         compatible = "rockchip,rk8602";
  7.         reg = <0x42>;
  8.         vin-supply = <&vcc5v0_sys>;
  9.         regulator-compatible = "rk860x-reg";
  10.         regulator-name = "vdd_npu_s0";
  11.         regulator-min-microvolt = <550000>;
  12.         regulator-max-microvolt = <950000>;
  13.         regulator-ramp-delay = <2300>;
  14.         rockchip,suspend-voltage-selector = <1>;
  15.         regulator-boot-on;
  16.         regulator-always-on;
  17.         regulator-state-mem {
  18.             regulator-off-in-suspend;
  19.         };
  20.         };
  21. </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原语
野火论坛202602252043499186..png
  • 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极早期,绕过所有标准驱动,直接“手动接管”引脚,强行配置电源,然后再把引脚还给系统。
  1. GpioPinSetFunction (I2C_BANK, I2C_SCL_PIN, 0);
  2. GpioPinSetFunction (I2C_BANK, I2C_SDA_PIN, 0);
复制代码
将这两个引脚(PD4, PD5)从默认的“I2C 控制器功能”强行切断,切换为普通GPIO模式。此时UEFI还没初始化好复杂的I2C硬件控制器。为了确保通信绝对可控,使用软件模拟的方式来操作。
  1. GpioPinSetDirection (I2C_BANK, I2C_SCL_PIN, GPIO_PIN_INPUT);
  2. GpioPinSetDirection (I2C_BANK, I2C_SDA_PIN, GPIO_PIN_INPUT);
复制代码
将引脚设为输入模式。I2C总线是开漏结构,默认需要外部上拉电阻拉高。将GPIO设为输入(高阻态),相当于“松手”,让外部上拉电阻把电平拉高至1。这是I2C总线的空闲状态。如果直接设为输出高电平,万一总线上有其他设备正在拉低,会造成短路。设为输入是最安全的“起手式”。
  1. 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稳定运行的黄金电压。
  1. <font face="新宋体" size="4">  Status = Rk8602WriteReg(0x05, 0x20);
  2. </font>
复制代码
寄存器0x05:这是PGOOD(Power Good)状态寄存器,属性是只读。
数值0x20:对应Bit5,即输入过压保护标志。
虽然手册说它是只读的,但在有些PMIC的设计惯例中,向状态寄存器写“1”往往用于清除中断标志。即便在RK860上这行代码无效(因为它是纯只读),它也是无害的。忽略即可(防御代码)。

  1. <font face="新宋体" size="4">Exit:
  2.   /* Release I2C bus, switch back to I2C function. */
  3.   GpioPinSetDirection (I2C_BANK, I2C_SCL_PIN, GPIO_PIN_OUTPUT);
  4.   GpioPinSetDirection (I2C_BANK, I2C_SDA_PIN, GPIO_PIN_OUTPUT);
  5.   GpioPinWrite (I2C_BANK, I2C_SCL_PIN, TRUE);
  6.   GpioPinWrite (I2C_BANK, I2C_SDA_PIN, TRUE);
  7.   MicroSecondDelay(5);
  8.   GpioPinSetFunction (I2C_BANK, I2C_SCL_PIN, 9);
  9.   GpioPinSetFunction (I2C_BANK, I2C_SDA_PIN, 9);
  10. </font>
复制代码
配置完成后,必须把现场清理干净,把控制权交还给系统。
  1. <font face="新宋体" size="4">  GpioPinSetDirection (I2C_BANK, I2C_SCL_PIN, GPIO_PIN_OUTPUT);
  2.   GpioPinSetDirection (I2C_BANK, I2C_SDA_PIN, GPIO_PIN_OUTPUT);
  3. </font>
复制代码
强制输出高电平。这在I2C协议中代表STOP状态或总线空闲。这确保了我们退出时,总线处于一个稳定的状态,不会让PMIC误以为还有数据要传输。
  1. <font face="新宋体" size="4">  GpioPinWrite (I2C_BANK, I2C_SCL_PIN, TRUE);
  2.   GpioPinWrite (I2C_BANK, I2C_SDA_PIN, TRUE);
  3. </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”,使用命令为
  1. 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的引用。

  1. # 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的测试也可以通过……

野火论坛202602252220124229..png
fig14.png
fig13.png
野火论坛202602252205388007..png
野火论坛202602252055174038..png
野火论坛202602252054476444..png
野火论坛202602252051133911..png
野火论坛202602252050351199..png
野火论坛202602252047063065..png
野火论坛202602252046363037..png
野火论坛202602252045435629..png
野火论坛202602252043014095..png
野火论坛202602252042149183..png
fig5.png
fig4.png
fig12.png
fig3.png
fig7.png
fig8.png
野火论坛202602252025413167..png
fig10.png
fig11.png
fig6.png
fig9.png
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

联系站长|手机版|野火电子官网|野火淘宝店铺|野火电子论坛 ( 粤ICP备14069197号 ) 大学生ARM嵌入式2群

GMT+8, 2026-2-26 11:24 , Processed in 0.117105 second(s), 28 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表