这个Pages展示本项目的开发计划和项目文档

RK3588 NPU 驱动开发路线图

进度总览

当前状态:电源域管理 (Power Domain) 目标:将添加了npu驱动的starry编译拷贝到开发板验证探测

实际计划

第一阶段:环境验证与基础初始化

1. 理论与环境准备

  • 阅读手册与 Demo
    • 研读 RK3588 技术参考手册中 NPU 章节。
    • 分析 RKNN Toolkit2 的官方 Demo,理解从模型加载到推理的完整数据流
  • 实体机环境验证 (基于 StarryOS 比赛版)
    • 编译并烧录比赛版本的 StarryOS 代码到 RK3588 实体板。
    • 成功运行一个简单的文本生成模型,确保硬件和工具链正常。
  • 整理 StarryOS 对 RK3588 NPU 驱动的逆向成果
    • 寄存器地图
      • 按模块梳理:PC / CORE / CNA / DPU / PPU / SDMA / DDMA / GLOBAL
      • 对每个寄存器记录:offset、字段含义、读写属性、默认值/复位值、关联流程
      • 标注来源:TRM / Linux rknpu 驱动
    • 提交协议与数据结构
      • 整理 DRM_IOCTL_RKNPU_* ioctl 列表与语义(Action / MemCreate / MemMap / MemDestroy / MemSync / Submit)
      • 对齐结构体布局:rknpu_mem_create / rknpu_mem_map / rknpu_task / rknpu_submit(字段意义与对齐)
      • 梳理 mmap(offset) 规则(handle 与 offset 的编码/解码约定)
    • 任务提交流程(时序 + 状态机)
      • 从“用户态提交”到“硬件执行完成”的完整时序
      • 失败路径:超时、异常中断状态、非法参数

第二阶段:异步推理支持

目标:将当前同步阻塞轮询的任务提交改为中断驱动的异步模式,提交任务后 CPU 不再空转等待,NPU 完成后通过中断通知。

实现情况

  • 中断处理 — 实现 IRQ 驱动模式,CPU 通过 WFI 指令进入低功耗等待,NPU 完成后触发中断唤醒 CPU
  • 异步等待机制 — 实现 wait_all_npucore 函数,支持多核心并行等待,每个核心独立触发中断
  • 并发安全 — 每个核心维护独立的 irq_status 原子变量,避免多核并发访问冲突
  • 超时与错误恢复 — 实现中断状态检查和错误传播机制,异常时正确上报错误码

第三阶段:NPU多任务并发运行

目标:支持多个NPU计算任务同时在NPU多核上进行

实现情况

  • 硬件资源隔离 — 验证 3 个 NPU 核心的 PC/CORE/CNA 等寄存器 per-core 独立,GLOBAL 寄存器通过 core_idx 参数隔离
  • GEM 内存隔离 — 实现 per-core 的 regcmd 缓冲区分配,每个核心使用独立的 DMA 地址空间
  • 核心分配策略 — 实现 subcore_task 机制,支持灵活的核心分配,QKV 三核并行、Wo 单核等混合调度

项目日志

记录 RKNN NPU 驱动开发进展。

RKNPU技术报告

一、项目内容与用途

这个项目的对象是 RK3588 NPU 驱动及其在 StarryOS 上的系统集成。它的作用:让上层推理程序、benchmark 或后续 runtime 能稳定地把任务提交给 NPU 执行,而不是只能依赖 Linux 或闭源用户态库环境。

当前驱动已经覆盖了几条基础链路:

  1. 寄存器/MMIO 访问:能够映射 RK3588 RKNPU 三个 core 的寄存器窗口,并通过寄存器接口完成 PC、CNA、DPU 等硬件模块的配置。
  2. GEM/DMA 缓冲管理:支持用户态创建、映射、同步和销毁 NPU 可访问的 DMA buffer。
  3. 任务描述符组织:支持 RknpuTask、regcmd、输入/权重/输出 buffer 的组合,并把这些描述符作为 submit 的基本执行单元。
  4. ioctl 服务:支持 SubmitMemCreateMemMapMemDestroyMemSyncAction 等 RKNPU 专用入口。
  5. 中断与 completion 回收:IRQ handler 负责读取硬件完成状态,调度器再根据 core 绑定关系把 completion 还原到具体任务。
  6. 多核调度:在一个 submit 中按 lane 把任务切到不同 core,同时支持多个 submit 进入队列等待。

二、本轮二次开发重点

2.1 支持 RK3588 三核 NPU 执行

RK3588 的 RKNPU 不是单个执行核心,而是三个可以并行工作的 NPU core。早期单核路径只证明了“任务能跑”,但没有把硬件并行能力发挥出来。本轮二次开发首先把提交路径改成能够识别 core_masksubcore_task,让同一批任务可以被拆成多个 lane 分发到不同 core。

当前的三核执行模型可以概括为:

  1. 用户态仍然通过一个 RknpuSubmit 提交任务批次。
  2. subcore_task[] 描述每个 lane 的任务范围;如果用户态没有显式填写 lane,队列层会把任务归一化到默认 lane。
  3. core_mask 决定这次 submit 允许使用哪些物理 core,例如 0x1 表示只用 core0,0x7 表示三个 core 都可用。
  4. 调度器为每个空闲 core 挑选一个可派发 lane,然后调用底层 driver 对该 core 编程。
  5. core 完成后,IRQ 路径发布 raw completion,调度器再回收并推进对应 lane 的 cursor。

这部分工作的重点不是简单把同一条命令发三遍,而是要保证每个 core 跑的是正确的 task slice,同一条 lane 不会被重复派发,completion 也能回到正确的 submit 和 task index。只有这些状态对齐,多核数据才有意义。

2.2 任务调度器与多线程共享 NPU

第二个是任务调度器。NPU 是共享硬件资源,不能让多个线程各自直接碰寄存器,否则很容易出现 core 状态、任务进度和 completion 归属混乱。当前实现保留外部的 blocking submit 语义,但内部引入了调度队列和 worker 线程:

  1. 调用线程进入 Submit ioctl 后,不直接独占 NPU 跑完整批任务,而是把 submit 放进 scheduler。
  2. 每个 submit 有自己的 waiter。调用线程只等待“自己的 submit 是否完成”。
  3. 全局 worker 负责真正的 dispatch 和 harvest。它被 kick 唤醒后,先回收已完成 core,再给空闲 core 下发新任务。
  4. scheduler 维护 ready、running、complete 这些状态。ready 表示还没开始跑的 submit,running 表示已有 lane 在跑或还有 lane 可继续派发,complete 表示终态结果等待 ioctl 路径取回。
  5. 如果 running submit 还有可派发 lane,会优先继续推进 running,而不是马上切到一个全新的 ready submit。这样可以减少同一 submit 内部的等待空洞。

service 层的 blocking submit 通过 per-submit waiter 阻塞:调用线程在 wait_for_submit() 上等待,直到 worker 将该 submit 移入 complete 并调用 waiter.complete() 唤醒它,再通过 take_terminal_submit() 取回终态结果。多个用户线程同时提交 NPU 任务时,线程可以各自阻塞在自己的 waiter 上,NPU 由全局 worker 串行管理硬件状态并并行利用多个 core。换句话说,NPU 对外看起来仍是阻塞式设备,对内已经具备”多线程共享、队列化提交、三核流式执行”的基础形态。

这版调度器的价值就在这里:它没有把用户态 ABI 改复杂,但把内核侧的执行模型从"谁提交谁跑"推进到了"统一排队、统一分发、统一回收"。

2.3 引入 rknpu-regs 寄存器库

寄存器访问层也做了工程化整理,引入了基于 SVD / svd2rust 生成的 drivers/rknpu-regs。驱动开发里手写寄存器偏移和位域非常容易出错,尤其是 NPU 这种寄存器块多、字段分散、不同 core 地址重复的硬件。

rknpu-regs

  1. 用类型化寄存器接口替代裸地址和 magic number。
  2. 减少手工抄写偏移、位宽、mask 时的错误。
  3. 让寄存器访问代码更接近硬件文档,后续查错更方便。
  4. 把”访问寄存器”和”调度策略”拆开,避免调度器里混入大量底层地址细节。

三、benchmark 测试内容

3.1 测量范围与方法

本次性能数据来自 log.txt 中运行的 ./core_scaling_benchmark,对应测试程序是 core_scaling_benchmark.c

测量窗口:仅覆盖 blocking DRM_IOCTL_RKNPU_SUBMIT 的完整往返时间,即 submit ioctl 从进入到返回。operand packing 和 regcmd generation 单独打印,不计入对比窗口。因此下面的数据反映的是 driver submit、scheduler dispatch/harvest、硬件执行和 completion 回收的综合耗时

指标定义

  • speedup = T_1core / T_3core(同一批任务 1-core 与 3-core 的平均 submit 时间之比)
  • parallel efficiency = speedup / 3(理想三核加速为 3x,efficiency 衡量实际利用率)
  • GFLOP/s = 2 × GMAC/s(1 MAC = 1 乘 + 1 加 = 2 FLOP)
  • jitter span = (T_max - T_min) / T_avg(衡量单次 submit 时间的波动幅度)

正确性校验:第一轮 measured round 会对输出抽样与 CPU reference 比较,并检查每个 task 的 int_status == 0x300。这是抽样校验,不是全量验证。

3.2 测试环境

  • 硬件平台:RK3588 SoC(三核 NPU)
  • 操作系统:StarryOS比赛版本
  • Benchmark 程序core_scaling_benchmark(4 场景 × 2 operand 模式)
  • 测量轮次:每场景 warmup 2 轮 + measured 5-12 轮(场景相关)
  • 频率/电源控制:未控制(默认 governor)
  • 热稳态控制:未控制
  • IRQ 亲和性:未设置

3.3 测试场景

每个场景一般又分两种 operand 模式:

  1. shared-operands:所有任务复用同一份 input/weight,只有 output slice 私有。这更偏向测试调度和计算本身,DMA footprint 较小。
  2. unique-operands:每个 task 有自己的 input/weight/output slice。这更接近多任务独立数据的情况,内存占用和准备成本更高。

测试程序里固定了四个场景。它们不是随便取的矩阵形状,而是分别压不同瓶颈:

场景矩阵形状shared tasksunique taskswarmupmeasured主要验证点
tiny_dispatchM=4 K=32 N=169696212小矩阵,submit/scheduler 固定开销占比最高,用来观察多核调度在短任务下是否会被开销吞掉。
mid_balancedM=64 K=512 N=512481228中等矩阵,调度开销和计算吞吐都会影响结果,用来判断调度器是否进入稳定可用区间。
throughput_heavyM=128 K=1024 N=102424425大矩阵,目标是把瓶颈推向 NPU 计算吞吐;unique 任务数较少,是为了控制 DMA footprint。
llama_decode_likeM=1 K=4096 N=409648028低 M、高 K、高 N,接近 LLM decode 阶段线性层投影形状,更关注长任务延迟和尾部波动。

tiny_dispatch 主要看调度器的“底噪”。如果这个场景三核退化,说明每次 dispatch、IRQ 回收、worker yield、waiter 唤醒的成本已经压过了硬件并行收益。它使用 96 个 task,任务数足够多,可以持续给三个 core 喂任务,但单 task 计算量非常小。

mid_balanced 是更接近日常 benchmark 的中间档。shared 模式有 48 个 task,unique 模式只有 12 个 task,因为 unique 模式每个 task 都有独立 input/weight/output,内存占用增长更快。这个场景用来观察调度器在“既有计算量、又有一定任务数量”的情况下是否稳定。

throughput_heavy 则故意把矩阵放大。shared 模式有 24 个 task,unique 模式只有 4 个 task。这里不是为了追求任务数,而是为了让每个 task 本身足够重,看三核并行能否把 GFLOP/s 拉上去。unique 任务数少也会暴露另一个问题:当任务批次太短时,三核并行窗口会变窄。

llama_decode_like 只跑 shared 模式,unique tasks 在测试程序里设为 0,所以日志里会跳过 unique。它模拟的是 decode 阶段常见的投影类 workload:M 很低,但 K 和 N 很大。这个场景不一定追求最高吞吐,更关心一次 submit 的阻塞时间能不能被多核明显压下来。

输入和权重不是随机数,而是由 deterministic_input_value()deterministic_weight_value() 生成的整数模式;输出会抽样和 CPU reference 比较。每个 task 的 int_status 也会检查,期望值是 0x300。任务分发时,distribute_tasks_to_cores() 会按 core 数把 task range 平均切到 subcore_task[],1 core 时只填 core0,3 cores 时填 core0/core1/core2 并设置对应 core_mask。因此这个 benchmark 测到的不是单纯的 C 程序循环,而是完整覆盖了 submit ABI、调度器 lane 切分、三核 dispatch、IRQ completion 和 copy-back 这些路径。

四、benchmark 结果与性能分析

4.1 总体结论

所有有效场景里,三核都比单核快,没有出现最终退化。最好的场景是 llama_decode_like/shared-operands,从 344.354 ms 降到 137.000 ms,speedup 达到 2.514x,parallel efficiency 为 83.78%。这说明调度器已经能把 RK3588 三个 NPU core 的并行能力用起来。

三核都有正收益,但大多数场景的 parallel efficiency 仍在 57%68% 左右

4.2 结果汇总

场景模式任务数1-core avg submit3-core avg submitspeedupparallel efficiency
tiny_dispatchshared962.533 ms1.423 ms1.780x59.32%
tiny_dispatchunique963.478 ms1.739 ms2.000x66.67%
mid_balancedshared4832.496 ms18.746 ms1.733x57.78%
mid_balancedunique1210.459 ms6.065 ms1.724x57.48%
throughput_heavyshared2442.656 ms20.864 ms2.044x68.15%
throughput_heavyunique46.781 ms3.939 ms1.721x57.38%
llama_decode_likeshared48344.354 ms137.000 ms2.514x83.78%

4.3 tiny_dispatch:小任务也有明显收益

tiny_dispatch 的矩阵规模很小,单 task 的计算量低。当前结果里,shared 模式从 2.533 ms 降到 1.423 ms,unique 模式从 3.478 ms 降到 1.739 ms。这说明当前调度器的固定开销没有大到完全吞掉三核收益,小任务也能加速。

但是这个场景的性能不能过度解读。shared 模式 parallel efficiency 是 59.32%,unique 模式是 66.67%,距离理想三核加速2x以上仍有明显差距。小任务下,每次 dispatch 和 completion 的成本占比很高,真正花在矩阵计算上的时间太短。后续如果要优化小任务,需要减少每 task 的下发/回收成本,或者把更多小 task 合并成更粗粒度的 batch。

4.4 mid_balanced:中等任务证明调度器进入显著加速区间

mid_balanced 是这次比较有代表性的场景。shared 模式中,1 core 平均 submit 为 32.496 ms,3 core 为 18.746 ms,speedup 为 1.733x。unique 模式中,1 core 为 10.459 ms,3 core 为 6.065 ms,speedup 为 1.724x

这个结果说明,调度器在中等任务规模下已经进入稳定可用区间。两种 operand 模式都接近 1.7x,没有只在某一种特殊数据复用方式下才有效。它也说明当前瓶颈不只是数据准备,因为 benchmark 的 measured window 没把 operand packing 算进去;submit 阶段本身确实因为多核并行缩短了。

4.5 throughput_heavy:吞吐型任务收益更明显

throughput_heavy 的 shared 模式从 42.656 ms 降到 20.864 ms,speedup 达到 2.044x,parallel efficiency 为 68.15%。同时 GFLOP/s 从 151.033 提升到 308.786,基本也是 2.044x 的增益。这个场景最能说明三核调度的直接价值:任务足够重之后,固定调度开销被摊薄,硬件并行执行开始成为主导因素。

unique 模式任务数只有 4 个,这是为了控制 DMA footprint。它仍然从 6.781 ms 降到 3.939 ms,speedup 为 1.721x。任务数少会限制三核利用率,因为 worker 能同时派发的 lane 数量和后续补发机会都变少。这个结果没有 shared 模式好,但仍然是稳定正收益。

4.6 llama_decode_like:长任务场景收益最好,但 jitter 较大

llama_decode_like 是本次最强的结果。它的 shared 模式从 344.354 ms 降到 137.000 ms,speedup 达到 2.514x,parallel efficiency 为 83.78%,GFLOP/s 从 4.677 提升到 11.756。这说明在低 M、高 K、高 N 的投影类 workload 下,当前三核调度能显著降低 submit 阻塞时间。

但这个场景也反应出在3-core 的 min/max 为 126.027 / 139.504 ms时,jitter span 达到 9.84%,明显高于 1-core 的 0.03%。也就是说,平均值很好,但三核路径下仍有一定波动。可能原因包括 worker harvest 时机、core completion 到达顺序、yield 后重新调度的时间差,以及更长任务下不同 core 之间的尾部等待。

4.7 这轮结果说明

  1. 三核支持已经有效。所有有效 benchmark 场景都显示 3-core submit 时间低于 1-core。
  2. 调度器设计已在真实 benchmark 中转化为性能收益。ready/running 队列推进、core binding 和 completion 回收这套机制能够有效利用三核并行。需要注意的是,当前 benchmark 验证的是单次 blocking submit 内的多核扩展收益,而非多线程并发提交的竞争场景——后者是调度器设计支持的能力,但尚未专项测试。
  3. 软件开销仍然存在。大多数场景没有接近理想 3x,说明 dispatch、harvest、同步和调度唤醒成本仍然需要优化。

五、功能模块分层与实现思想

当前实现可以按从上到下的链路理解。

最上层是用户态 benchmark 或 runtime。它负责准备输入、权重、输出、regcmd 和 task array,然后通过 ioctl 提交任务。benchmark 还负责构造不同矩阵规模和不同 operand 共享模式,用来观察调度器在不同负载下的行为。

再往下是 ioctl / service 边界层。它负责把用户态传进来的 RknpuSubmitRknpuTask[] 和内存管理请求拷入内核,转换成驱动内部可以处理的数据。这里不应该保存太多调度状态,否则 ioctl 层会变成第二个 scheduler。

scheduler 层负责 submit 生命周期。它维护 ready、running、complete 三个 bucket,管理每个 submit 的 waiter,也负责唤醒全局 worker。waiter 和 kick 的职责要分清:waiter 是 per-submit 的,回答"这个 submit 完成了吗";kick 是全局 worker 信号,回答"现在有没有新活需要 worker 醒来处理"。这两个东西混在一起,调度器会很难读,也很容易漏唤醒。

driver / dispatch / IRQ 层负责硬件事实。driver 给某个 core 编程,IRQ handler 读取和清除硬件中断,completion 只表示"某个 core 有原始完成状态"。至于这个 completion 属于哪个 submit、哪个 lane、哪个 task index,应由 scheduler 根据 core_binding 还原,而不是让 driver 反向保存一堆队列语义。

最底层是寄存器访问层,也就是 rknpu-regs。它提供类型化 register API。调度器不应该知道太多寄存器细节,寄存器层也不应该知道队列策略。这个分层让后续工作可以分开推进:寄存器访问继续补全,调度策略继续优化,ioctl ABI 保持稳定。

六、当前限制与问题分析

  1. 三核收益仍然低于理想值。除了 llama_decode_like 达到 83.78% efficiency,多数场景仍在 57%68% 左右。说明当前并行并不是完全线性扩展,软件路径还有明显成本。
  2. 小任务场景仍然敏感。tiny_dispatch 虽然这次有正收益,但它的绝对 submit 时间只有毫秒级,任何一次额外调度、yield、IRQ 回收或锁竞争都会改变结果。后续如果要跑大量小算子,必须继续压低固定开销。
  3. 3-core 路径存在尾延迟波动。llama_decode_like 的平均 speedup 很好,但 3-core jitter span 到了 9.84%。这提示我们不能只盯 avg submit,还要看 min/max 和尾延迟。

七、调度器的执行时序

本节描述调度器的内部机制和执行流程,内容基于 drivers/rknpu/src/service/scheduler.rsdrivers/rknpu/src/task/taskqueen.rs 的实现。

7.1 核心数据结构

NpuSchedulerState(调度器全局状态,受单一 mutex 保护):

  • tasks: BTreeMap<RknpuQueueTaskId, RknpuQueueTask> — 所有活跃 submit 的唯一所有者
  • ready: BTreeMap<i32, VecDeque<RknpuQueueTaskId>> — 按优先级分桶,尚未开始执行的 submit
  • running: BTreeMap<i32, VecDeque<RknpuQueueTaskId>> — 按优先级分桶,已有 lane 在执行或还有 lane 可派发的 submit
  • complete: BTreeMap<RknpuQueueTaskId, RknpuQueueTask> — 已终态,等待 ioctl 路径取回
  • core_binding: BTreeMap<usize, CoreRunBinding> — 物理 core → {task_id, lane_slot, task_index} 的归属映射
  • waiters: BTreeMap<RknpuQueueTaskId, Arc<W>> — per-submit 的阻塞等待器

RknpuQueueTask(单个 submit 的运行时状态):

  • lane_isrun: [bool; 5] — 每条 lane 是否有任务在飞
  • subcore_cursors: [u32; 5] — 每条 lane 已完成的 task 数量
  • last_error: Option<RknpuError> — 错误状态

submit 的生命周期状态(ready/running/complete)不是单独维护的枚举,而是由 lane_isrunsubcore_cursorslast_error 的组合派生,并通过 reclassify_task() 决定 submit 应归入哪个 bucket。

7.2 调度策略

running-before-readydispatch_idle_cores() 为每个空闲 core 选择候选时,优先从 running bucket 中找可继续派发的 submit(prepare_dispatch_from_running),只有当没有 running submit 可用时,才从 ready 提升新 submit(promote_ready_and_prepare_dispatch)。这减少了同一 submit 内部的等待空洞,但对等待中的 ready submit 不是严格公平的。

内存同步边界

  • comfirm_write_all():在一次 dispatch_idle_cores() 调用中,对某个 submit 首次派发前调用,确保 DMA 写入对硬件可见
  • prepare_read_all():在 terminal submit 唤醒 waiter 前调用,确保硬件写回对 CPU 可见

7.3 图一:Submit 生命周期

注意:函数签名可能随着更新而改变实际函数签名以代码仓库中为准,此处只做流程解读

下图展示单个 submit 从 ioctl 入队到 copy-back 返回的完整路径,以及多个并发 submit 线程如何通过独立 waiter 共享同一个 worker。

Logo

7.4 图二:Worker 主循环与多核派发

注意:函数签名可能随着更新而改变实际函数签名以代码仓库中为准,此处只做流程解读

下图展示 worker 线程的主循环结构,以及 harvest 和 dispatch 如何交替推进多个 submit 的执行。

Logo

相关仓库

驱动主仓库

RKNPU

StarryOS与驱动的桥接层

rknpu-starry-adapter

演示视频

Video

PPT

PPT

第一周开发日志(1.25-1.31)

工作总结

  • 基础知识学习 — 学习 NPU 原理、AI 模型运行原理、驱动在整个推理链条中的角色
  • RKNN Toolkit2 仿真验证 — 下载套件,阅读 demo 代码,在 x86 仿真器上跑通测例

基础知识学习

刚接到 NPU 驱动开发任务时,虽然听说过 NPU,但对它实际是什么、解决什么问题、工作原理、如何运行都不了解。花了一天时间系统学习了以下内容:

  • NPU 是什么 — 神经网络处理单元,专为矩阵运算和推理加速设计的硬件
  • AI 模型的本质 — 本质上是一个函数,是大量矩阵乘法配上激活函数。训练是拟合参数,推理是执行前向传播
  • 模型格式与转换 — ONNX → RKNN 格式转换,量化,以及模型在 NPU 上的执行流程
  • 驱动在整个链条中的角色 — 用户态库(librknnrt.so)通过 ioctl 与内核驱动通信,驱动负责任务调度、DMA 搬运、寄存器操作

RKNN Toolkit2 套件与仿真验证

上 Rockchip 官网下载了 rknn-toolkit2 套件,发现里面包含一个 仿真器,可以在 x86 主机上模拟 RKNPU 的部分功能。阅读了 demo 代码,理解了 RKNN 推理的基本 API 调用流程:

rknn_init()来初始化npu执行上下文 → rknn_inputs_set()读写npu寄存器设置输出输出参数 → rknn_run()写npu寄存器提醒npu开始工作和运行 → rknn_outputs_get()npu通过中断通知操作系统,用户库从约定位置读取结果 → rknn_destroy()销毁释放npu执行上下文

第二周开发日志(2.1-2.7)

工作总结

  • 开发板验证通过 — 在 OrangePi 5 Plus(RK3588)上部署 StarryOS(NPU 版本),成功跑通 RKNN 推理测例
  • 闭源库逆向文档整理 — 整理了 librknnrt.so 逆向成果、完整寄存器语义、模型推理全链条,发布至 GitHub Pages
  • 驱动骨架搭建 — 创建 axnpu-rknn 独立 crate,编写 DTB 设备探测代码,通过 rdrive 框架自动匹配 NPU 设备
  • DRM 框架初步实现 — 创建 axdrm crate,实现 GEM 内存对象管理和 ioctl 编码解析与分发机制

开发板验证

搭载 RK3588 芯片的 OrangePi 5 Plus 开发板到了之后,将带有 NPU 支持的 StarryOS 版本(atomgit 仓库)部署到开发板上,成功跑通测例,验证了 NPU 驱动在 StarryOS 上的可行性。


闭源库逆向与文档整理

花了 3 天时间系统整理了以下内容:

  • librknnrt.so 闭源库逆向成果 — 梳理了用户态库的内部调用流程、ioctl 命令、内存管理机制
  • 寄存器语义 — 从官方 TRM 手册和 RKNN 开源内核驱动(C 语言版本)中,整理了完整的寄存器描述和语义
  • 模型推理全链条 — 从 rknn_initrknn_destroy 的完整生命周期,包括任务提交流程、状态机、DMA Fence 路径等

文档链接https://qc-gpu-driver.github.io/starryos-pulsar/documents/docs.html

这份文档不仅方便自己开发时查阅,也相当于一份社区参考文档,我也会一直维护和验证的,方便更多对 RKNN NPU 驱动感兴趣的开发者查阅和使用。


驱动骨架编写

最初尝试直接在 arceos/modules/ 下创建 axnpu-rknn 驱动模块,但遇到了 workspace 循环依赖问题——ArceOS 的 axdriver_block 锁定的版本缺少 ahci feature,而 Cargo workspace 会全量解析所有 members 的依赖,导致整个 workspace 编译失败。

参考 StarryOS(NPU 版本)的做法,将驱动模块移动到 Starry 根目录下独立开发,等完善后再集成回 arceos。

编写了 dtbparse.rs,利用 ArceOS 的 rdrive 框架通过设备树(DTB)自动探测 NPU 设备:

#![allow(unused)]
fn main() {
module_driver! {
    name: "RKNPU",
    level: ProbeLevel::PostKernel,
    priority: ProbePriority::DEFAULT,
    probe_kinds: &[
        ProbeKind::Fdt {
            compatibles: &["rockchip,rk3588-rknn"],
            on_probe: probe_rknpu
        }
    ],
}

fn probe_rknpu(info: FdtInfo<'_>, dev: PlatformDevice) -> Result<(), OnProbeError> {
    let name = info.node.name();
    // 提取 MMIO 基址: 0xfdab0000, 大小: 0x9000
    let mut regs = info.node.reg().ok_or_else(|| { /* ... */ })?;
    let base_reg = regs.next().ok_or_else(|| { /* ... */ })?;
    let mmio_base = base_reg.address as usize;
    let mmio_size = base_reg.size.unwrap_or(0x9000);
    // 提取中断号: SPI 110, 111, 112(3 个 NPU 核心)
    // ...
    Ok(())
}
}

module_driver! 宏会将驱动自动注册到 rdrive 框架,内核启动时遍历 DTB,匹配 compatible = "rockchip,rk3588-rknn" 后自动调用 probe_rknpu,无需手动调用。


DRM 框架搭建

创建了 driver/axdrm crate,作为用户态接口适配层:

GEM 内存对象管理 — 实现了 GemNumberAllocator(handle 编号分配/回收)、GemObject(物理地址、虚拟地址、fake offset)、GemHandle(自动回收的 RAII handle)

ioctl 编码解析 — 实现了 DRM ioctl 命令的解码函数:

函数作用
ioctl_nr(cmd)提取 ioctl 编号(bits [7:0])
ioctl_type(cmd)提取类型字节(DRM = 'd' = 0x64)
ioctl_size(cmd)提取参数大小(bits [29:16])
is_driver_ioctl(cmd)判断是否为驱动私有命令(nr >= 0x40)

ioctl 分发机制 — 驱动通过 register_driver() 注册自己的 ioctl handler,dispatch_ioctl() 根据命令编号自动分发到对应驱动。NPU 驱动只需注册一个回调函数即可处理私有 ioctl。


第三周开发日志(2.8-2.14)

工作总结

  • 修复 Axvisor 上 rsext4 文件系统导致的磁盘数据不一致问题 — 客户机写文件后重启出现 "block is free" 错误,定位为 rsext4 缓存未同步,在写操作后增加 sync_to_disk 调用修复 PR #368
  • svd2rust 寄存器库重构 — 基于 svd2rust 工具生成类型安全的寄存器访问库,重构 rknpu 驱动核心,减少手动操作寄存器的出错概率 rknpu-regs rknpu
  • 辅助注释与文档 — 利用 AI 为驱动代码添加详细注释,兼顾学习与开发效率
  • 基于 WFI 的异步中断处理 — 实现了 NPU 中断的异步等待机制

修复 Axvisor 上 rsext4 文件系统磁盘数据不一致问题

PR rgba(51, 95, 136, 1)

问题

  1. 客户机执行文件写操作后重启,出现 block is free(双重释放,原来没有正确把1写回位图) (位图实际扫描使用的Block和超级块中的Block不一致)错误,磁盘数据损坏
  2. 如果启动的是 Linux 客户机,Linux 在启动过程中会因文件系统完整性校验失败而无法启动

分析

Linux 客户机的问题是由于 Axvisor 在启动客户机的最后时刻调用 mount_virtual_fs 时没有刷新文件系统,导致文件系统不一致。Linux 启动时进行文件系统完整性校验,发现不一致后拒绝启动,随后关机导致缓存丢失,最终造成文件系统损坏。

根源在于 rsext4 为提升运行时性能设计了多级缓存机制,但 Axvisor 在集成 rsext4 时,没有在文件写操作后进行数据同步。如果客户机修改文件后立即关机,缓存仍停留在内存中,不可避免地导致磁盘数据不一致。

解决方案

在 rsext4 的 ext4fs.rs 访问层中,对所有写操作调用 sync_to_disk 函数,确保数据及时同步到磁盘。虽然会带来一定的性能损失,但保证了文件系统的一致性。


开发板环境踩坑

本周在板端遇到了一系列棘手的环境问题,花了不少时间排查:

U-Boot 与启动链问题

  • U-Boot 缺少网卡驱动 — 板载 U-Boot 不支持网络传输,只能通过串口 loady 加载内核,debug 内核每次都要通过串口传内核很慢,每次修改都需要完整烧录StarryOS内核
  • SPI 残留环境变量 — 从 OrangePi 官方 Ubuntu 镜像中提取干净的 U-Boot,制作 SPI 镜像刷入开发板。但刷完后发现 SPI 中残留了旧的 U-Boot 环境变量,导致启动失败,只能重新刷回 SPI 镜像
  • eMMC 与 SD 卡混淆 — StarryOS 在开发板上扫描到板载 eMMC,但驱动却强制探测 SD 卡。起初误以为使用的是 SD 卡,经分析后确认实际使用的是板载 eMMC

后续 NPU 驱动工作开发路线决策

最初尝试将 NPU 驱动集成到 StarryOS 主线,但 HAL 层架构 axhalsomehal 存在冲突,RK3588 平台级代码集成涉及整个架构的大修改。为专注于 NPU 驱动核心功能开发,决定先在 StarryOS NPU 版本上开发,待主要功能完成后再移植回主线。


svd2rust 寄存器库重构

基于 svd2rust 工具重构 rknpu 驱动的寄存器访问层。

#![allow(unused)]
fn main() {
// 旧方式:手动偏移 + 裸指针
let status = unsafe { ptr.add(0x20).read_volatile() };
}

改进

通过 svd2rust 从 SVD 描述文件生成类型安全的寄存器库 rknpu-regs,每个寄存器字段都有明确的类型和文档:

#![allow(unused)]
fn main() {
// 新方式:类型安全 + 自动补全 + 文档
let status = core.pc().interrupt_status().read().bits();
core.pc().interrupt_clear().write(|w| unsafe { w.bits(INT_CLEAR_ALL) });
}

成果

  • 生成了完整的寄存器定义,覆盖 PC、CNA、CORE、DPU、PPU、DDMA 等所有功能块
  • RknpuCore 结构体封装了各功能块的访问方法,提供统一接口
  • 编译期类型检查杜绝了寄存器偏移量写错、位域宽度搞混等常见错误

AI 辅助注释

利用 AI 为 axnpu 驱动代码添加了详细的中文注释(关键函数的调用流程和参数说明)

既方便自己学习 NPU 硬件细节,也降低了后续开发和他人协作的门槛。


基于 WFI 的异步中断处理

实现了 NPU 任务完成的异步等待机制:

  • NPU 核心执行完任务后触发中断
  • CPU 通过 WFI(Wait For Interrupt)指令进入低功耗等待状态
  • 中断到来时 CPU 被唤醒,读取中断状态寄存器,清除中断标志
  • 返回任务执行结果

这为后续多核并发任务提交打下了基础——每个 NPU 核心可以独立触发中断,CPU 侧可以并行等待多个核心的完成通知。


第五周开发日志(2.22-2.28)

工作总结

  • RK3588 NPU 三核并行矩阵乘法实现 — 扩展 Transformer 结构体支持 3 套 regcmd 缓冲区,实现 matmul_npu_3core_qkv 函数,将 QKV 三个矩阵乘法并行提交到 3 个 NPU 核心,板端验证推理成功
  • 驱动多核提交流程重构 — 修改 submit_ioctrl 实现批量任务分配,支持一次 ioctl 向多个核心提交任务,新增 wait_all_npucore 并行等待机制

验证结果

板端运行日志显示多核并行成功:

[261.640936] Total tasks to submit: 3, active cores: 3, max batch size: 4095
[261.650710] Total tasks to submit: 1, active cores: 1, max batch size: 4095
[262.295035] Total tasks to submit: 3, active cores: 3, max batch size: 4095
...
Once upon a time...
  • 3 tasks, 3 cores 表示 QKV 三核并行成功
  • 1 tasks, 1 core 表示后续的 wo 矩阵乘法(单核)
  • 模型推理输出 "Once upon a time..." 验证结果正确

驱动多核提交流程重构

批量任务分配

重构 submit_ioctrl 函数,支持将用户空间的任务数组自动分配到多个 NPU 核心:

#![allow(unused)]
fn main() {
pub fn submit_ioctrl(&mut self, args: &mut RknpuSubmit) -> Result<(), RknpuError> {
    // 1. 刷新缓存,确保 NPU 能看到 CPU 写入的数据
    self.gem.comfirm_write_all()?;
    
    // 2. 提取活跃的核心任务
    let active_subcore: Vec<&RknpuSubcoreTask> = args.subcore_task.iter()
        .filter(|s| s.task_number > 0).collect();
    
    // 3. 批量提交到多个核心
    while task_iter < task_iter_end {
        let task_batch = active_subcore.len().min(task_iter_end - task_iter);
        let submit_tasks = unsafe { 
            core::slice::from_raw_parts_mut(task_ptr.add(task_iter), task_batch) 
        };
        
        // 并行启动每个核心的任务
        for idx in 0..active_subcore.len().min(task_batch) {
            self.base[idx].start_execute_one(idx, &self.data, &mut submit_tasks[idx], args)?;
        }
        
        // 并行等待所有核心完成
        self.wait_all_npucore(self.wait_fn, int_mask, submit_tasks)?;
        task_iter += task_batch;
    }
    
    // 4. 使缓存无效,确保 CPU 能读取 NPU 写入的结果
    self.gem.prepare_read_all()?;
    Ok(())
}
}

并行等待机制

新增 wait_all_npucore 函数,实现多核心并行等待:

#![allow(unused)]
fn main() {
pub fn wait_all_npucore(&self, normal_wait_fn: Option<fn()>, 
                        int_mask: Vec<u32>, 
                        submit_tasks: &mut [RknpuTask]) -> Result<(), RknpuError> {
    let mut done: [bool; 3] = [false; 3];
    
    if let Some(wait) = normal_wait_fn {
        // IRQ+WFI 模式:CPU 休眠等待中断
        loop {
            let status: Vec<u32> = self.base.iter()
                .map(|core| core.irq_status.load(Ordering::Acquire)).collect();
            
            // 检查每个核心的完成状态
            for idx in 0..submit_tasks.len() {
                if status[idx] & int_mask[idx] > 0 {
                    self.base[idx].clean_interrupts();
                    self.base[idx].irq_status.store(0, Ordering::Release);
                    submit_tasks[idx].int_status = int_mask[idx] & status[idx];
                    done[idx] = true;
                }
            }
            
            // 所有核心都完成则退出
            if done[..submit_tasks.len()].iter().filter(|&d| !d).count() == 0 {
                break;
            }
            
            // CPU 进入低功耗等待
            (wait)();
        }
    } else {
        panic!("[NPU] busy-poll mode not implemented for multi-core wait");
    }
    Ok(())
}
}

第六周开发日志(3.1-3.7)

工作总结

  • NPU驱动移植到主线 — 将比赛版本上的 NPU 驱动和 aarch64 动态平台配置成功移植到 StarryOS 主线版本,系统启动成功,NPU 三核中断注册成功。不足:eMMC 驱动初始化失败导致系统崩溃,需要进一步排查;

  • WFI 异步调度优化 — 将 WFI 函数替换为 yield 函数,实现真正的异步调度,NPU 工作时 CPU 可以执行其他任务,提升系统并发性能 不足:异步调度目前仅实现单核版本,SMP 完整实现仍在主线版本中

第七周开发日志(3.8-3.14)

  • 工作进展:本周主要做的是规划,无实习任务相关的实质性代码工作

  • 本周做出了对下周工作的详细规划以及个人对aarch64的详细学习

-规划如下:

NPU 协处理器化架构设计 (NPU as FPU)

1. 概述

本项目提出并实现一种创新的异构计算架构,旨在将 NPU 从传统的、基于 VFS/ioctl 的“设备”模型,提升为与 CPU 核心紧耦合的“协处理器”。其设计目标是通过极致的软硬件协同,实现 NPU 算力调度

2. 核心设计哲学

彻底废弃传统基于 VFS(虚拟文件系统)和 ioctl 的松耦合设备驱动模型。将 NPU 提升为与 CPU 平级的计算协处理器(类比 FPU 浮点运算单元)。通过共享 DMA 内存、定制化系统调用(Syscall)与“懒汉式”上下文切换(Lazy Context Switch)实现算力调度。

3. 内核数据结构扩展 (TCB 升级)

为了让操作系统的调度器(Scheduler)能够接管 NPU 资源,必须扩展内核的任务控制块(TCB / task_struct):

  • npu_isdirty: bool

    • 含义:NPU 硬件上下文“脏”标识位。
    • true:表示当前物理 NPU 硬件的内部 SRAM 和寄存器中,依然保留着该任务的数据和状态。
    • false:表示该任务的 NPU 状态已被换出,或者尚未使用过 NPU。
  • npu_context: struct

    • 含义:NPU 硬件上下文结构体。追加在原生 CPU 任务上下文之后。
    • 作用:用于存储 NPU 被强行打断或切换时的完整硬件状态机。需查阅具体 NPU 芯片的闭源驱动源码及手册,提取并保存完整的寄存器集合(如命令队列指针、当前算子偏移量、中断屏蔽字等)。

4. 用户态基础框架 (libnpu)

在用户层封装底层细节,向应用程序开发者提供极简的 C/C++ 接口。

  • 专属内存分配 (dma_malloc 系统调用)

    • 抛弃标准 malloc。用户通过 dma_malloc 直接向内核申请一段物理地址连续的 DMA 内存。
    • 目的:绕过 IOMMU 的复杂页表配置,让 NPU 硬件的 DMA 控制器可以直接通过物理地址(PA)高速拉取数据。
  • 强制中断注入

    • 用户将计算参数、模型权重写入申请好的 DMA 内存区域。
    • 在构建 NPU 任务描述符(Task Descriptor)链表时,libnpu 会在底层介入,将每个微小可配置算子任务结构体中的中断控制位(IRQ Enable)强制设为开启。这是后续内核能够精准捕获算子边界、实现状态保存的关键。
  • 内联汇编触发 (npu_work API)

    • 利用 AArch64 调用约定,将装载了任务描述符链表的 DMA 内存物理地址指针塞入约定的寄存器(如 X0)。
    • 执行定制的内联汇编(包含特定的 SVC 陷入指令),直接触发定制的 resolve_npu 系统调用。

5. 内核态调度核心 (resolve_npu 系统调用)

这是整个架构的调度心脏。当 CPU 通过 SVC 陷入内核态后,严格按照以下流水线执行:

阶段 1:内存转换与前置检查

  1. 地址翻译:将 X0 寄存器传入的用户态虚拟地址(VA),通过查询页表或 DMA 分配记录,转换为 NPU 硬件所需的物理地址(PA)。
  2. 硬件状态机检查:检查全局 NPU 硬件状态(IDLEBUSY)。

阶段 2:懒汉式上下文切换 (Lazy Context Switch) 调度器需要仲裁当前物理 NPU 硬件的归属权。

  1. 碰撞检测:检查系统中是否存在一个“前任任务”,其 npu_isdirty == true,且该任务并非当前请求任务。
  2. 等待安全边界:如果存在这样的“前任任务”(说明 NPU 硬件中存有他人数据),内核绝不能强行复位硬件。必须检查 NPU 当前是否为 IDLE。如果是 BUSY,当前 CPU 线程必须挂起睡眠(加入等待队列),直到 NPU 算完当前算子并触发硬件中断。
  3. 保存前任状态:当 NPU 确认处于 IDLE 后,内核立刻将 NPU 硬件内的全量寄存器状态抽取出来,保存到那个“前任任务”的 npu_context 中。
  4. 剥夺前任所有权:将“前任任务”的 npu_isdirty 标志位修改为 false

阶段 3:恢复当前状态并启动硬件

  1. 检查当前记录:检查当前请求任务的 TCB 中,是否存有尚未跑完的 npu_context(即之前被切换出去的状态)。
  2. 状态恢复:如果存在,则将 npu_context 里的数据原封不动地写回 NPU 的物理控制寄存器。
  3. 宣誓主权:将当前任务的 npu_isdirty 标志位置为 true
  4. 硬件启动 (Kick the Doorbell):将阶段1中转换好的任务描述符物理地址指针,写入 NPU 的“任务执行寄存器”,并写入特定的启动命令(“踢门铃”)。
  5. 状态标记与返回:将全局 NPU 状态标记为 BUSY,CPU 退出系统调用,返回用户态继续执行或调度其他普通线程。

6. 硬件中断处理程序 (NPU IRQ Handler)

由于 libnpu 强制开启了每个算子的中断,NPU 每算完一个微小算子就会发出硬件 IRQ。

  1. 进入 ISR:CPU 陷入内核中断服务例程。
  2. 确认完成:读取 NPU 硬件状态寄存器,确认当前算子计算完成。
  3. 释放路权:将全局 NPU 状态强制标记为 IDLE(空闲)。
  4. 唤醒等待者唤醒resolve_npu 系统调用中,因为 NPU BUSY 而挂起睡眠的其他任务线程,让它们得以重新参与 NPU 的抢占与上下文切换流程。

第八周开发日志(3.15-3.21)

工作总结

  • 阶段性代码提交 — 本周完成一次阶段性代码提交 1d4e8fe,核心是把 rknpu 提交路径收敛为“单 task 边界步进 + 外层调度器轮转推进”,并补齐最小 DMA syscall 与用户态试验链路。提交链接:Commit
  • NPU 提交路径支持单 task 边界切换 — 驱动只保留 submit_ioctrl_step_with_owner() 这条步进入口,在每个 task 完成 IRQ 边界返回,让多个进程可以轮流推进各自未完成的 submit
  • DMA syscall 与用户态辅助库打通 — 保留 sys_dma_mallocsys_dma_free 和对应轻量用户态封装,用于板端分配与释放 DMA 缓冲区
  • 新增多进程/混合工作负载验证 — 新增 matmul_multi_processmatmul_llama_concurrent 两个测试程序,板端验证多进程交错提交和 llama 推理并发场景
  • 寄存器级保存/恢复路径彻底下线 — 当前主线不再保留完整寄存器快照、恢复镜像、毒值写坏和读回校验逻辑,IRQ 路径只更新最小 owner/task 状态

单 task 边界调度改造

本周核心工作是把原来“一次 ioctl 一口气跑完整批任务”的同步提交逻辑,改造成“每次只推进一个当前 task-batch,完成后返回调度器”的步进式路径。

当前驱动只保留两类最小状态:

  • NpuOwnerIds — 由外层调度器提供的 owner 标识,使用“线程 ID + 进程 ID + 地址空间指针”唯一标定一次 submit 的归属
  • 每核 owner 槽位 — 每个硬件 core 只保留一个 NpuOwnerState,记录该 core 当前绑定的 owner、当前 task、task 指针、task 索引,以及最近一次 IRQ 的观测结果

不再保留:

  • resident owner
  • owner 级 submit/context 快照
  • (owner, core) 共享状态表
  • 完整寄存器保存/恢复镜像

新的路径大致如下:

  1. card1 在进入 RKNPU_SUBMIT 时,用“当前线程 ID + 当前进程 ID + 当前地址空间指针”组装 owner
  2. 驱动执行 submit_ioctrl_step_with_owner(),本次最多只推进一个 task-batch
  3. 每个参与的 core 在下发前先绑定一个最小 NpuOwnerState
  4. 每个核心收到预期完成中断后,驱动更新对应 task 的 int_status,同步回写 owner 槽位里的 IRQ 字段,然后立即清空该 core 槽位
  5. 如果整次 submit 还没结束,就 yield_now() 主动让出,允许别的 owner 进来推进它自己的 submit
  6. 下次同一个 owner 再进来时,只根据 task_counter 从上次稳定完成的位置继续

这样之后,进程级切换点就固定在了“单个 task 完成 IRQ 边界”上,而不是“整个 submit 全部完成”之后。

板端验证已经可以观察到这种交错推进:

[NPU]   batch done: int_status=0x300
...
matmul_multi_process: pass

这说明当前已经不是“一个进程一次性独占跑完整次 submit”,而是由外层 loop 在 task 边界轮流推进多个 owner。


多进程与混合负载测试

为了验证当前这套“单 task 边界切换”不是只在单个 demo 里自娱自乐,本周新增了两个更贴近真实使用方式的测试程序。

1. matmul_multi_process

这个测试会同时启动 4 个子进程。每个子进程只发起 1 次 submit,但这次 submit 里串了 20 个相关联的 matmul task:

  • task 0 的输出作为 task 1 的输入
  • task 1 的输出再作为 task 2 的输入
  • 最终做逐 task、逐元素 CPU 参考校验

它验证的不是“多个短任务抢一个锁”,而是:

  • 单个 submit 内还有剩余 task 时,驱动能否在 IRQ 边界切出去
  • 之后切回来时,能否从正确的 task_counter 继续推进
  • 多个进程交错执行后,中间状态和最终输出是否仍然正确

最终板端结果为:

matmul_multi_process: pass

中断边界实验的收敛

本周原本也同步尝试了更激进的路线:在 IRQ 完成边界读取 live 寄存器快照,把准备恢复的寄存器先写入固定毒值,再按镜像恢复,最后读回校验。

但在板端验证时发现,至少有一部分 task-window 配置寄存器 在“任务刚完成、IRQ 已到达”的这个边界之后,并不保证还能按提交时的原值稳定读回。例如曾经观察到:

first_task_shadow_mismatch={ offset=0x100c, expected=0x120, got=0x0 }
first_task_shadow_mismatch={ offset=0x4058, expected=0xf, got=0x0 }

这说明两个事实:

  • 当前边界对“task 已完成”是稳定的
  • 但它不一定对“所有任务窗口寄存器还能按原值读回”稳定

因此当前版本做了一个更明确的收敛:

  • 不再尝试保存/恢复整套寄存器镜像
  • 不再保留毒值验证、恢复镜像、读回校验的主线逻辑
  • IRQ 路径只保留“读取 live IRQ、清中断、更新最小 owner/task 槽位状态”

也就是说,当前主线解决的是“可运行、可验证、可继续扩展”的 task 粒度协作式调度基础设施;任意寄存器级/算子级抢占 不在本次提交范围内。


本周结论

本周最重要的进展,是把 单任务级边界(核 IRQ 中断边界)切换 + 多进程交错推进 + 结果正确 这一层跑通,并进一步把驱动内部状态压缩到了最小模型。

当前已经可以确认:

  • 抢占/切换粒度已经从“整次 submit”下沉到了“单个 task 完成 IRQ 边界”
  • 多个进程提交各自含有多 task 的 submit 时,驱动可以轮流推进
  • 驱动内部不再承担 owner 上下文保存/恢复,也不再提供完整 NPU 状态导出接口

下一步的重点,是继续在当前 task 粒度调度稳定的基础上,围绕更细的调度与恢复能力评估真实可行的硬件边界,而不是继续维护一套过重的软件镜像模型。

第九周开发日志(3.22-3.29)

工作总结

本周的主要工作是实现一个基于 Clang front-end 的 RKNPU 代码生成工具。该工具面向“显式标注 + 代码生成”的使用模式:用户在 C 源码中对可加速代码段进行手工标注,工具负责识别被标注的算法结构,并将其转换为对应的 NPU 任务提交流程,从而把原本由 CPU 执行的计算卸载到 NPU 上。

当前版本已经完成从前端识别到代码生成的基础闭环,但支持范围仍较为有限,仅支持标准形态的 matmul 算子。 代码仓库


本周进展

  • 完成了基于 Clang front-end 的前端识别方案,可以对带有显式标记的 C 代码进行分析。
  • 完成了标准矩阵乘法代码模式的识别与验证,能够从纯 CPU 风格实现中提取出可下沉到 NPU 的计算区域。
  • 实现了面向 RKNPU 的基础代码生成流程,能够将识别后的 matmul 区域转换为对应的 NPU 任务构造与提交流程。
  • 建立了“手工标注 -> 识别分析 -> 代码生成”的最小闭环,为后续扩展更多算子和更复杂的调度机制打下基础。

遇到的问题与分析

本周原计划继续将 NPU 的任务调度粒度进一步细化到单个算子级别,以增加可调度时刻的数量,使单个进程占用 NPU 的最小时间片进一步缩小。

但在分析后发现,这一路径存在明显问题:如果将调度边界压缩到单个算子级别,将显著提高中断触发频率。虽然这样能够提升调度灵活性,但中断处理开销也会同步增大,整体性能收益并不理想,甚至可能得不偿失。

在此基础上,我进一步尝试思考和验证“任意时刻 checkpoint”方案,即在 NPU 运行过程中的任意时刻保存执行状态,并在之后恢复执行。当前主要遇到以下两个核心困难:

  1. 硬件层面缺少统一暂停机制

    现有 RKNPU 硬件接口中,没有看到能够让多个功能部件在同一时刻一致性暂停的机制。因此,即使希望在某一时刻保存状态,也难以保证采集到的是一个全局一致、可恢复的硬件状态快照。

  2. 恢复执行难以做到无副作用

    RKNPU 的启动方式本质上依赖于 PC 寄存器的启动脉冲。也就是说,恢复时并不是“从某个硬件内部断点继续执行”,而更接近一次重新触发执行。如果保存状态时任务正处于某个算子的执行过程中,那么恢复后很可能会导致该算子被重复执行,从而产生任务重放问题,破坏执行语义。


本周结论

本周完成了 RKNPU 代码生成工具链的第一版原型,实现了从 C 代码显式标注到 NPU 任务生成的基本流程,证明了“标准矩阵乘法代码经识别后自动生成 RKNPU 调用代码”这一方向是可行的。


第十周开发日志(3.30-4.5)

工作总结

本周的主要工作是完成 rknpu 任务调度器的正式落地。

这一周把提交、排队、分发、完成、唤醒这一整条路径实现了,让单次 submit 能在多核上并行推进,并在结束后正确返回到用户态。 提交链接

本周进展

  • 完成了 rknpu 调度器主路径的搭建,路径为 card1 -> scheduler -> queue -> driver.
  • 保留阻塞式 submit ,在内核内部为每次 submit 维护独立 waiter,使用户线程可以在提交后挂起等待,并在任务真正结束后被唤醒。
  • 引入 worker 驱动的后台推进模型,通过全局 kick 机制唤醒调度线程,避免提交线程自己承担全部硬件推进逻辑。
  • 完成多核分发路径,支持把同一批任务按 lane 切分后投递到多个 core 上并行执行。
  • 把队列层的职责收敛到“维护 ready submit、lane 游标和完成进度”这几个核心状态上,调度器负责选择可下发任务,驱动负责实际硬件提交和中断观测。

本周结论

本周已经完成 rknpu 任务调度器的主体实现。当前版本具备了完整的阻塞提交、后台 worker 推进、多核 lane 分发、terminal 唤醒能力。


第十一周开发日志(4.6-4.12)

工作总结

本周的工作进入了收尾阶段,围绕已经完成的 rknpu 调度器做最后测试、benchmark 验证


本周进展

  • 完成了多轮 benchmark 测试,覆盖 tiny_dispatchmid_balancedthroughput_heavyllama_decode_like 等场景,重点对比 1-core 与 3-core 的提交时间、吞吐和并行效率。
  • 对调度器关键路径补充了更细的日志,覆盖 enqueue、worker 唤醒、dispatch、harvest、terminal wake 和 blocking wait 等节点。

遇到的问题与分析

本周最麻烦的问题,是 benchmark 过程中出现了偶发性的卡住或返回不稳定

从现象上看,有些测试可以正常打印 benchmark complete status=0 并返回 shell;但也有一些运行过程中,会出现串口长时间停住、需要手动退出终端的情况。它并不是每次都能稳定复现。

结合当前benchmark日志,至少可以得到两个判断:

  1. 小任务场景下,多核并行并不一定带来正收益

    tiny_dispatch 这类尺寸很小的 workload 上,submit/scheduler 开销占比很高,3-core 的效率明显达不到理想值,甚至可能比 1-core 更慢。这说明当前瓶颈更多在调度与提交流程,而不是硬件算力本身。

  2. 中大型任务场景下,多核并行已经能稳定带来正收益

    结合这轮 benchmark 记录,可以直接看到几组比较明确的结果:

    • mid_balanced 场景下,1-core avg submit56.158 ms 降到 39.204 msspeedup = 1.432xparallel efficiency = 47.75%
    • throughput_heavy 场景下,1-core avg submit76.605 ms 降到 40.824 msspeedup = 1.876xparallel efficiency = 62.55%
    • llama_decode_like 场景下,1-core avg submit423.358 ms 降到 231.858 msspeedup = 1.826xparallel efficiency = 60.86%

本周结论

本周完成了调度器收尾阶段最重要的一轮验证工作。当前版本已经能够支撑 benchmark 跑通,并能产出比较完整的日志和性能数据;


项目月报

2月技术报告-周雨

目标与问题

主要目标

  1. 阅读 RKNPU 驱动相关源码,学习 NPU/AI 相关知识,将 RKNPU 硬件手册总结为 markdown 文档
  2. 实现NPU驱动异步中断支持
  3. 实现NPU驱动多核多任务并发

主要问题

  1. 环境与工具链问题:开发板 U‑Boot 缺少网卡驱动,只能通过串口加载内核,调试效率低;U-boot网卡驱动编译困难,依赖太多,将会逐步解决
  2. 驱动集成冲突:尝试将 NPU 驱动集成到 StarryOS 主线时,HAL 层架构 axhalsomehal 存在冲突,RK3588 平台级代码集成涉及整个架构的大修改。当前阶段将继续在当前已有驱动基础上开发。暂时不会集成到主线

方案或思路

面对上述问题,我制定了以下开发路线:

  1. 分阶段学习与验证

    • 第一阶段:学习 NPU 原理、AI 模型推理流程,利用 RKNN Toolkit2 仿真器在 x86 上验证推理链条。
    • 第二阶段:在真实硬件(OrangePi 5 Plus RK3588)上部署 StarryOS(NPU 版本),验证驱动可行性。
    • 第三阶段:逆向闭源用户态库 librknnrt.so,整理完整的寄存器语义和任务提交流程,形成社区参考文档。
  2. 驱动架构设计

    • 采用 rdrive 框架实现设备树(DTB)自动探测,避免硬编码设备地址。
    • 创建独立的 axdrm crate 实现 DRM 框架,提供 GEM 内存管理和 ioctl 编码解析/分发机制,保持用户态接口兼容性。
    • 将驱动模块放在 StarryOS 根目录独立开发,规避 workspace 循环依赖问题,待功能稳定后再集成回 ArceOS。
  3. 寄存器访问安全

    • 使用 svd2rust 工具从 SVD 描述文件生成类型安全的寄存器库 rknpu-regs,覆盖 PC、CNA、CORE、DPU、PPU、DDMA 等所有功能块。
    • 通过编译期类型检查杜绝寄存器偏移量写错、位域宽度混淆等错误。
  4. 异步中断处理

    • 利用 WFI(Wait For Interrupt)指令实现低功耗等待,NPU 核心完成任务后触发中断,CPU 被唤醒并读取中断状态寄存器。
    • 为后续多核并发任务提交奠定基础——每个 NPU 核心可独立触发中断,CPU 侧可并行等待多个核心的完成通知。
  5. 多核并发任务调度

    • 扩展驱动数据结构,支持多套寄存器命令缓冲区(regcmd)。
    • 重构 submit_ioctrl 函数,实现批量任务分配,一次 ioctl 可向多个核心提交任务。
    • 新增 wait_all_npucore 并行等待机制,循环检查各核心中断状态,直到所有核心完成任务。
  6. 问题规避与迂回

    • 暂时放弃将驱动集成到 StarryOS 主线,专注于在 NPU 版本上开发核心功能,减少架构冲突带来的干扰。
    • 通过刷写干净的 SPI 镜像解决 U‑Boot 环境变量残留问题,明确使用 eMMC 作为存储介质。

实现情况

1. 基础知识学习与仿真验证(第一周)

  • NPU 原理学习:理解了 NPU 作为神经网络处理单元的本质,掌握了 AI 模型训练与推理的基本流程,以及驱动在推理链条中的角色(用户态库通过 ioctl 与内核驱动通信,驱动负责任务调度、DMA 搬运、寄存器操作)。
  • RKNN Toolkit2 仿真验证:在 x86 主机上利用 RKNN Toolkit2 的仿真器跑通测例,梳理出 RKNN 推理的 API 调用流程:rknn_initrknn_inputs_setrknn_runrknn_outputs_getrknn_destroy

2. 开发板验证与文档整理(第二周)

  • 硬件验证:在 OrangePi 5 Plus(RK3588)开发板上成功部署 StarryOS(NPU 版本),并跑通 RKNN 推理测例,验证了 NPU 驱动在真实硬件上的可行性。
  • 闭源库逆向与文档:系统逆向分析了 librknnrt.so 用户态库,结合官方 TRM 手册和开源内核驱动,整理了完整的寄存器语义、任务提交流程、状态机与 DMA Fence 路径,形成 GitHub Pages 文档,为社区提供参考。

3. 驱动骨架与 DRM 框架搭建(第二周)

  • 设备探测:编写 dtbparse.rs,利用 rdrive 框架通过设备树(DTB)自动探测 NPU 设备(compatible = "rockchip,rk3588-rknn"),提取 MMIO 基址(0xfdab0000,大小 0x9000)和中断号(SPI 110‑112)。
  • DRM 框架:创建 axdrm crate,实现 GEM 内存对象管理(handle 分配/回收、物理地址映射)和 ioctl 编码解析/分发机制,为 NPU 驱动提供兼容 Linux DRM 的用户态接口。

4. 寄存器库重构与 AI 辅助注释(第三周)

  • svd2rust 寄存器库:基于 SVD 描述文件生成类型安全的寄存器库 rknpu-regs,覆盖 PC、CNA、CORE、DPU、PPU、DDMA 等所有功能块。将原有的手动偏移+裸指针访问改为类型安全、带自动补全和文档的 API,例如:
    #![allow(unused)]
    fn main() {
    let status = core.pc().interrupt_status().read().bits();
    core.pc().interrupt_clear().write(|w| unsafe { w.bits(INT_CLEAR_ALL) });
    }
  • AI 辅助注释:利用 AI 为 axnpu 驱动代码添加详细的中文注释,明确关键函数的调用流程和参数含义,降低后续开发和协作门槛。

5. 异步中断处理实现(第三周)

  • WFI 等待机制:实现基于 WFI(Wait For Interrupt)指令的低功耗异步等待。NPU 核心完成任务后触发中断,CPU 进入休眠状态;中断到来时 CPU 被唤醒,读取中断状态寄存器并清除标志位,返回任务执行结果。
  • 多核中断基础:该机制为多核并发任务提交打下基础,每个 NPU 核心可独立触发中断,CPU 侧可并行等待多个核心的完成通知。

6. 多核并行矩阵乘法实现(第五周)

  • 三核并行 QKV 计算:扩展 Transformer 结构体,支持 3 套 regcmd 缓冲区,实现 matmul_npu_3core_qkv 函数,将 Transformer 中的 Q、K、V 三个矩阵乘法并行提交到 3 个 NPU 核心。
  • 批量任务分配:重构 submit_ioctrl 函数,支持将用户空间的任务数组自动分配到多个 NPU 核心,一次 ioctl 可向多个核心提交任务。
  • 并行等待机制:新增 wait_all_npucore 函数,循环检查各核心中断状态,利用 WFI 等待所有核心完成,实现真正的多核并行等待。

7. 板端验证结果

板端运行日志显示多核并行成功:

[261.640936] Total tasks to submit: 3, active cores: 3, max batch size: 4095
[261.650710] Total tasks to submit: 1, active cores: 1, max batch size: 4095
[262.295035] Total tasks to submit: 3, active cores: 3, max batch size: 4095
...
Once upon a time...
  • 3 tasks, 3 cores 表示 QKV 三核并行成功。
  • 1 tasks, 1 core 表示后续的 wo 矩阵乘法(单核)。
  • 模型推理输出 "Once upon a time..." 验证结果正确。

8. 其他问题修复(第三周)

  • Axvisor 文件系统同步问题:修复了 rsext4 缓存未同步导致的磁盘数据不一致问题,在写操作后增加 sync_to_disk 调用,确保文件系统一致性(PR #368)。

下一步的计划/建议

基于 1 月末和 2 月份的开发进展,后续工作可围绕以下方向展开:

  1. 驱动稳定性与健壮性

    • 当前驱动还有很多unimpliment函数和特性,我将会继续完善它
    • 完善错误处理机制,增强驱动健壮性
  2. 多核调度优化

    • 实现动态负载均衡,根据各 NPU 核心的利用率自动分配任务。(当前多核并发是简单的任务切片)
    • 支持任务优先级调度,确保高优先级任务优先执行。
    • 探索任务依赖关系(DAG)支持,实现复杂模型层的流水线并行。
  3. 主线集成与代码重构

    • 待 NPU 驱动功能稳定后,重新评估集成到 StarryOS 主线的可行性,解决 HAL 层冲突。
    • axnpu-rknn 驱动模块迁移回 ArceOS 的 modules/ 目录,遵循项目模块化规范。
  4. 文档与社区建设

    • 持续更新 GitHub Pages 文档,补充更多寄存器细节、性能调优指南和故障排查案例。

3月技术报告-周雨

目标与问题

主要目标

  1. 将比赛版本中的 NPU 驱动和 aarch64 动态平台配置迁回 StarryOS 主线,先验证主线环境能不能真正承载 RKNPU 驱动,而不是一直停留在比赛分支上单独维护。
  2. 将原来一次性跑完整个 submit 的同步提交路径,改造成在 task 完成边界主动让出的协作式调度路径,让多个进程在共享 NPU 时有明确的切换点。
  3. 在驱动工作逐步稳定之后,继续往上层工具链推进,探索从带标注的 C 代码自动生成 RKNPU 调用代码,减少手工拼装任务描述符和提交流程的工作量。

主要问题

  1. 主线移植后虽然系统能够启动,NPU 三核中断也能注册成功,但目前平台适配层还有一些问题,实际调用 NPU 时会出现卡死。这个问题我还在继续排查,也说明主线环境离稳定开发还有一段距离。
  2. 更细粒度的调度与状态恢复并不只是软件设计问题。我在实验过程中已经明显碰到 RKNPU 的硬件边界。基于目前已经能稳定工作的单 task 粒度,我也认为没有必要继续追求任意时刻的调度,这件事现在看起来性能收益和工程收益都不大。
  3. 自动代码生成工具虽然已经跑通了 matmul 的最小闭环,但目前识别和生成能力都还非常有限

方案或思路

面对这些问题,我在 3 月的思路比 2 月更聚焦一些。2 月更多是在补驱动能力,3 月开始,我更在意这些能力能不能放回主线、能不能被调度,以及在确认硬件边界之后,哪些事情值得继续做,哪些事情应该及时收口。

  1. 将NPU驱动从比赛版本移植回主线

    我是先把比赛版本上的 NPU 驱动和平台适配层移植回了主线,然后在实际使用NPU驱动时会导致系统卡死,目前问题正在排查中

  2. 从单线程任务独占改为多线程task step 协作式调度

    3 月中期我花了一段时间重新想调度模型。当时我甚至考虑过把 NPU 做成类似 FPU 的协处理器,同时尽量把 ioctl 路径收得很薄。我往下做时,我是先把最重要的部分抽出来:不要再让一个 submit 一次性独占 NPU 到结束,而是把提交拆成 step,每次只推进一个当前 task-batch,在 IRQ 边界返回,由外层调度器决定谁继续。后面的工作重点也不是把切换做得越来越激进,而是继续把驱动层这套 task step 调度机制做扎实、做到能稳定的跑为止。

  3. 先用实验确认硬件边界,再主动收敛模型

    我不想在还没确认硬件边界的时候,就先堆一套很重的软件上下文模型。所以 3 月里我专门对寄存器快照、恢复镜像、算子级中断和 checkpoint(也就是执行中途保存状态,之后再恢复继续跑)做了实验。实验结果如果不支持,我就直接暂时搁置这条路,而不是继续维护一套看起来完整、实际上很难跑通的设计,而且实验看来在如果要做到任意时刻打断和恢复的话必定需要NPU硬件本身支持checkpoint的一种恢复机制。现在看下来,在单 task 粒度已经能稳定切换的前提下,没有必要再把目标扩展到任意时刻调度,继续往那个方向做,开销会更高,收益却不明显。

  4. 向工具链上移,减少手工写驱动调用

    到 3 月后半段,我开始把目光往上移,不再只盯着驱动内部。具体做法是基于 Clang front-end,也就是利用 Clang 编译器前端的语法分析能力,对带有显式标注的 C 代码做识别,然后尝试自动生成对应的 RKNPU 调用代码。第一步只做标准 matmul,先把最小闭环跑通。

实现情况

1. NPU驱动移植回 StarryOS 主线并完成基础验证

3 月一开始,我做的不是新功能,而是把比赛版本上的 NPU 驱动和 aarch64 动态平台配置迁回 StarryOS 主线。

实际验证结果比我预想得好一些。系统能够正常启动,NPU 三核中断注册也成功了。

问题:目前平台适配层还有一些问题,实际调用 NPU 时会出现卡死,具体原因我还在继续排查。

2. npu驱动中任务提交路径改造成单 task 边界协作式调度

这一部分是我 3 月最核心的工作。之前的模型很直接:一次 ioctl 进去,驱动把单个线程提交的整个 submit 从头跑到尾,跑完才返回。这样做实现起来不难,但问题也很明显,单个线程会长时间独占 NPU。只要 submit 足够长,别的线程就只能一直等。

我后来把这条路径改成了单步推进。具体做法是,在 card1 的提交路径里保留外层 loop + yield_now(),而驱动内部只保留 submit_ioctrl_step_with_owner() 这个 step 入口。这个函数每次最多只推进一个当前 task-batch,然后就在 task 完成后的 IRQ 边界返回。为了让它能重新进入,我把 task_counter 作为唯一恢复游标,下次进入时直接从上次稳定完成的位置继续算,不再另外维护一套复杂的快照。

驱动内部的状态也一起收窄了。每次 step 下发前,只给参与执行的 core 绑定一个最小的 owner/task 状态,用来记录“这个 core 当前在给谁跑哪个 task,以及最近看到了什么 IRQ”。任务完成后,驱动更新对应 task 的 int_status 和最近一次 IRQ 观测值,然后立刻把 core 槽位清掉。这样做的好处:切换点清楚,状态模型也足够小。

板端验证能说明这条路是通的。多进程 matmul_multi_process 和混合负载场景已经跑通,submit 不再是一家独占跑完,而是多个 owner 在 task 边界交错推进。这里的切换本质上还是协作式调度,不是任意时刻抢占。它解决的是多个线程怎么共享 NPU,不是怎么把 NPU 做成一个可以像 CPU/FPU 那样随时保存和恢复的通用协处理器。

3. NPU驱动更细粒度调度与状态恢复实验带来的收敛

我一开始其实想得更激进。我不满足于 task 边界调度,还想继续往下压,看看能不能做到更细粒度的切换,甚至尝试类似 checkpoint 的执行中途保存与恢复。为此,我在 IRQ 边界做过 live 寄存器快照、毒值写坏和恢复校验,也专门分析过算子级调度的可能性。

板端实验给出的反馈并不乐观。我当时观察到过这样的日志:

first_task_shadow_mismatch={ offset=0x100c, expected=0x120, got=0x0 }
first_task_shadow_mismatch={ offset=0x4058, expected=0xf, got=0x0 }

这说明一个很麻烦的问题:IRQ 虽然已经到了,task 也确实跑完了,但并不是所有 task-window 配置寄存器都会在这个边界上稳定地按提交值读回。也就是说,这个边界对“任务完成”是可靠的,对“完整寄存器状态可恢复”却不是。再往下走,如果把调度边界压到单个算子,中断频率会明显变高,灵活性是提高了,但中断和调度开销也会一起涨上来。更关键的是,当前硬件接口里我没有看到一个能让多个功能部件在同一时刻一致性暂停的机制,而恢复执行也不像 CPU/FPU 那样能从内部断点继续,更像重新触发一次执行,这就带来了任务重放的风险。

这些实验让我最后收敛得很明确:当前最稳妥的边界,仍然是 task 完成后的 IRQ 边界。所以 3 月我做出的判断不是完整寄存器保存/恢复写不下去,而是这条路在当前硬件边界下不值得继续当主线推进。再往任意时刻调度上做,软件会更复杂,中断和调度开销也会更高,但我现在看不到足够明显的收益。这也是为什么后来我把主线彻底收敛到了task 粒度协作式调度,并开始把精力放回驱动层任务调度本身的完善和优化。

4. 基于 Clang front-end 的 RKNPU 代码生成工具原型

3 月后半段,我开始结合ai工具做一个基于 Clang front-end 的 RKNPU 代码生成工具。这里的想法很直接:写纯cpu计算的代码,通过手工标注一段代码,让代码生成工具来进行中间的具体的NPU平台侧的代码生成和优化,这样我们我们不用写一行npu库相关的代码 而是让我们的代码生成工具来分析代码生成更优,性能更好的能跑在npu上的代码,这也是当时我想的最贴近让npu当作cpu的一个协处理器在软件层面的一个思路.

当前的做法是“显式标注 + 前端识别 + 代码生成”。用户先在 C 代码里标出可加速的区域,工具再利用 Clang front-end 对这些区域做语法级分析。目前我已经把标准形态的 matmul 识别跑通,并且能够把识别出的计算区域转换成对应的 RKNPU 任务构造与提交流程,形成一个最小闭环。代码仓库见 rknpu_gen

当然,这一步离“通用自动生成”还差得很远。现在它能说明的,只是标准 matmul 这条链可以走通,不能说明复杂算子、复杂控制流或者更随意的代码风格也都能自动处理。但这一步还是很重要,因为它把我 3 月的工作从“让驱动能跑”往前推到了“让工具帮人用驱动”。这两个方向其实不是一回事。

下一步的计划/建议

  1. 继续排查和修复 StarryOS 主线下的平台适配问题,重点把当前“调用 NPU 会卡死”的原因定位清楚,让主线环境真正适合稳定调试。
  2. 在现有 task 边界协作式调度已经稳定的前提下,继续完善驱动层的任务调度和优化工作,把队列化、优先级和多任务推进模型做得更规范。
  3. 继续把多进程和混合工作负载测试做得更系统,确认当前 task step 调度在不同负载下的行为和开销,不再把任意时刻寄存器恢复作为当前主线目标。
  4. 最后做一个benchmark来验证我优化前后驱动性能的提升

4月技术报告-周雨

目标与问题

主要目标

  1. rknpu 任务调度器的正式落地:可以基于优先级调度处理多个线程提交的 submit。
  2. 驱动的工程化重构:将 RKNPU 驱动抽象为一个独立且通用的 Rust crates 库,外部 OS 通过实现驱动所需的 trait 来使用驱动,实现内核与驱动逻辑的解耦。
  3. 调度器性能验证:对完成的调度器进行全面的 Benchmark 验证,重点对比多核(3-core)与单核(1-core)在不同尺度 workload 下的提交时间与并行效率,产出完整的性能数据。

具体方案

1. 搭建后台 Worker 推进模型,解耦用户提交与调度器

为了避免用户线程长时间阻塞并承担硬件推进逻辑,我重新设计了提交流程:

  • 后台 Worker 线程:设计了专门用于任务调度的 worker 线程。该线程在后台循环推进和调度任务。当没有任务时,worker 线程会进入两阶段睡眠(基于 listen 和 wait),挂起在监听新任务的信号量上;当有新任务入队时,通过全局的 notify_one (kick) 机制去唤醒调度线程。
  • 用户态阻塞与唤醒:用户线程只需要调用 ioctl 接口将 NPU 任务入队即可睡眠等待。为此,我为每次 submit 维护了独立的 waiter(基于 RknpuSubmitWaiter trait)。用户线程在提交任务后直接挂起 (wait),直到任务在多核上真正结束并触发 terminal,再由底层调用 complete 唤醒用户线程,安全返回用户态。

2. 将 RKNPU 驱动抽象为通用 Crate

为了让 RKNPU 驱动不依赖于特定的 OS 环境(如 StarryOS),我在 crate 层设计了一套平台相关的 Trait 接口,外部 OS 只需要实现 RknpuPlatform 即可接入驱动。具体拆分如下:

  • RknpuDeviceAccess:负责底层设备的临时可变借用(with_device),由 OS 决定低层级 RKNPU 实例的存储和锁机制。
  • RknpuUserMemory:抽象了内核与用户态之间的内存拷贝操作(copy_from_user 和 copy_to_user),处理不同 OS 内存管理模型的差异。
  • RknpuSchedulerRuntime:抽象了调度器所需的 OS 运行时能力,包括创建等待原语(new_waiter)、创建 Worker 唤醒信号(new_worker_signal)、生成后台 Worker 线程(spawn_worker)以及在硬件执行或调度停滞时的出让机制(yield_now)。

3. 多尺度 workload 的 Benchmark 测试

设计并执行覆盖多种场景的基准测试,包括:

  • tiny_dispatch(极小任务调度)
  • mid_balanced(中等规模平衡负载)
  • throughput_heavy(重吞吐负载)
  • llama_decode_like(模拟大模型解码负载)

实现结果

1. 调度模型与并发机制成功运行

成功实现了基于 Worker 线程和任务信号量的调度模型。底层通过 RknpuWorkerSignal 和 RknpuSubmitWaiter 实现了可靠的两阶段休眠与唤醒机制,彻底解决了用户态提交与底层硬件执行的深度耦合,多进程争抢 NPU 时的调度变得更加平滑。

2. 驱动 crate 化解耦完成

完成 rknpu crate 的重构,成功将平台强相关的内存拷贝、设备锁等待以及线程调度抽离到 RknpuPlatform trait 中。这证明了驱动的主体逻辑能够以纯库(Library)的形式独立演进,大幅提升了后续移植和维护的工程效率。

3. Benchmark 测试与多核性能验证

完成了全量 Benchmark 测试。结合日志数据,得出了明确的结论:在中大型任务场景下,多核并行已经能够稳定地带来明显的正向收益。 具体的几组明确结果如下:

  • mid_balanced 场景:1-core 的平均提交时间从 56.158 ms 降到了 3-core 的 39.204 ms,加速比达到了 1.432x,并行效率为 47.75%
  • throughput_heavy 场景:1-core 的平均提交时间从 76.605 ms 降到了 40.824 ms,加速比提升至 1.876x,并行效率达到了 62.55%
  • llama_decode_like 场景:1-core 平均提交时间从 423.358 ms 降到了 231.858 ms,加速比达到 1.826x,并行效率为 60.86%结论:数据有力地证明了,在重负载(如吞吐密集型和模拟大模型推理场景)下,当前的 crate 化调度机制和多核分发路径是稳定且具备实战价值的。但在极小任务(如 tiny_dispatch)下,由于调度本身的软件开销,多核加速比不明显甚至可能出现负收益,这也为下阶段(如探索 fast-path 或 batching 提交)指明了优化方向。

RK3588 RKNPU 开发文档

本文档基于 RockChip官方开源仓库和 StarryOS 仓库中对 RK3588 NPU 运行时库(闭源 librknnrt.so)及内核侧 RKNPU DRM 驱动的逆向与复现成果,整理为三个核心章节:

文档结构

章节内容适用场景
RKNN 硬件特性 三核架构、数据精度与算力、支持的推理框架 了解硬件能力边界
寄存器地图 按模块梳理rknpu内部各个寄存器的位域、读写属性 写驱动、调试硬件交互
IOCTL协议与数据结构 DRM_IOCTL_RKNPU_* 命令表,结构体布局,flags枚举,mmap 规则 实现ioctl分发,对齐用户态ABI
任务提交流程 从用户态提交到硬件执行完成的完整时序逻辑,包含状态机和各种失败路径 查询job生命周期
当前提交-IRQ 边界快照恢复系统 总结 step-submit、owner 切换、IRQ 边界快照/写坏/恢复/校验,以及相关 struct 的职责 整理当前实验实现、查数据结构分工、回顾抢占边界语义

来源标注约定

文档中对每条信息标注来源,使用以下标记:

  • Linux rknpu 驱动 — 来自Rockchip官方仓库中rk3588-npu内核驱动代码
  • rknpu-ioctl.h — 来自 Linux rknpu 驱动include目录的 ioctl 头文件
  • StarryOS Rust 驱动 — 来自 drivers/rknpu/src/ 的 Rust 复刻实现
  • 逆向推断 — 基于代码行为推断,无官方文档确认

术语速查

术语含义
GEMGraphics Execution Manager,DRM 子系统的内存对象管理框架
PCProgram Counter / 任务控制器,NPU 的命令流执行引擎
CNAConvolution Neural-network Accelerator,卷积加速单元
DPUData Processing Unit,数据后处理单元
PPUPooling Processing Unit,池化处理单元
DDMA/SDMAData DMA / System DMA,数据搬运引擎
IOVAI/O Virtual Address,IOMMU 映射后的设备侧虚拟地址
fenceDMA fence,用于 job 完成通知与跨设备同步的内核原语

RKNN 手册

本栏汇总 RK3588 NPU 的硬件参考与软件接口文档。

硬件手册

NPU 三核架构、数据精度、寄存器位域详解。

硬件手册

IOCTL 协议

DRM_IOCTL_RKNPU_* 命令表、结构体布局、mmap 规则。

IOCTL 协议

任务提交流程

从用户态提交到硬件执行完成的完整时序。

任务提交流程

RKNN 硬件手册

来源:RK3588 TRM Chapter 36 RKNN

本章汇总 RK3588 NPU 硬件相关文档,包括硬件特性概览与完整寄存器参考。

RKNN 硬件特性

  • 三核 NPU,支持三核协同 / 双核协同 / 单核独立
  • 每核 384KB 内部缓冲,AHB 配置接口 + AXI 数据接口
  • 支持 INT4 / INT8 / INT16 / FP16 / BF16 / TF32 多精度推理
  • 功能流水线:CNA(卷积)→ CORE(MAC)→ DPU(后处理)→ PPU(池化)
  • 激活函数:ReLU / Leaky ReLU / ReLUx / Sigmoid / Tanh / Softmax
  • 池化:Average / Max / Min Pooling

完整硬件特性

寄存器图

每个核心拥有独立 64KB 寄存器空间,按功能模块划分:

模块地址范围功能
PC0x0000 ~ 0x0FFF任务控制器 / 命令流引擎
CNA0x1000 ~ 0x1FFF卷积神经网络加速单元
CORE0x3000 ~ 0x3FFFMAC 核心控制
DPU0x4000 ~ 0x4FFF数据后处理单元
DPU_RDMA0x5000 ~ 0x5FFFDPU 读 DMA
PPU0x6000 ~ 0x6FFF池化处理单元
PPU_RDMA0x7000 ~ 0x7FFFPPU 读 DMA
DDMA0x8000 ~ 0x8FFFData DMA 引擎
SDMA0x9000 ~ 0x9FFFSystem DMA 引擎
GLOBAL0xF000 ~ 0xFFFF全局使能掩码

寄存器总览

RKNN 硬件特性概览

来源:RK3588 TRM Chapter 36 RKNN

RKNN 是专用于神经网络的处理单元,旨在加速人工智能(AI)领域的神经网络运算,涵盖机器视觉和自然语言处理等方向。AI 的应用范围正在不断扩大,目前已在多个领域提供功能支持,包括人脸追踪、手势与肢体追踪、图像分类、视频监控、自动语音识别(ASR)以及高级驾驶辅助系统(ADAS)。

核心特性

特性说明
核心数量三核 NPU(Triple NPU CORE)
协作模式支持三核协同、双核协同、单核独立工作
配置接口AHB 接口,仅用于寄存器配置(单次访问)
数据接口AXI 接口,用于从内存取数据
内部缓冲384KB × 3(每核 384KB)

rknn多核架构参考图 rknn单个核心架构参考图

功能模块描述

AHB / AXI 接口

AXI 主接口用于从挂载在 SoC AXI 互联总线上的内存中取数据。AHB 从接口用于访问寄存器,进行配置、调试和测试。

神经网络加速引擎(Neural Network Accelerating Engine)

该引擎是神经网络运算的核心处理单元,包含卷积预处理控制器、内部缓冲区、MAC 阵列和累加器。它为识别功能提供并行卷积 MAC 运算,支持 INT4、INT8、INT16、FP16、BF16 和 TF32 数据类型。

数据处理单元(Data Processing Unit, DPU)

数据处理单元主要负责单数据运算,如 Leaky ReLU、ReLU、ReLUx、Sigmoid、Tanh 等激活函数。同时提供 Softmax、转置(Transpose)、数据格式转换等功能。

平面处理单元(Planar Processing Unit, PPU)

平面处理单元在数据处理单元输出之后执行平面操作,支持平均池化(Average Pooling)、最大池化(Max Pooling)、最小池化(Min Pooling)等。

寄存器配置取数单元(Register File Fetch Unit, PC)

寄存器配置取数单元通过 AXI 接口从外部系统内存中获取寄存器配置(即命令流),实现硬件自动配置各功能模块寄存器。

支持的数据精度与算力

数据类型每周期 MAC 操作数(三核合计)
INT42048 × 3 = 6144
INT81024 × 3 = 3072
INT16512 × 3 = 1536
FP16512 × 3 = 1536
BF16512 × 3 = 1536
TF32256 × 3 = 768

支持的推理框架

TensorFlow、Caffe、TFLite、PyTorch、ONNX NN、Android NN 等。

寄存器地图

RK3588 NPU 每个核心(共 3 核)拥有独立的寄存器空间,内部按功能模块划分为以下区域:

地址空间总览

Base[15:12]模块大小地址范围功能
4'h0PC4KB0x0000 ~ 0x0FFF任务控制器 / 命令流引擎
4'h1CNA4KB0x1000 ~ 0x1FFF卷积神经网络加速单元
4'h3CORE4KB0x3000 ~ 0x3FFFMAC 核心控制
4'h4DPU4KB0x4000 ~ 0x4FFF数据后处理单元
4'h5DPU_RDMA4KB0x5000 ~ 0x5FFFDPU 读 DMA
4'h6PPU4KB0x6000 ~ 0x6FFF池化处理单元
4'h7PPU_RDMA4KB0x7000 ~ 0x7FFFPPU 读 DMA
4'h8DDMA4KB0x8000 ~ 0x8FFFData DMA 引擎
4'h9SDMA4KB0x9000 ~ 0x9FFFSystem DMA 引擎
4'hFGLOBAL4B0xF000 ~ 0xF004全局使能掩码

来源说明:地址映射来自 RK3588 TRM Table 1-1 RKNN Address Mapping。


PC 寄存器块(Program Counter / 任务控制器)

基址CORE_BASE + 0x0000 | 地址范围0x0000 ~ 0x0FFF

来源:RK3588 TRM §36.4.3 Detail Registers Description

PC 是 NPU 的命令流执行引擎,负责:从 DMA 地址读取寄存器命令流 → 按序写入各功能模块寄存器 → 触发执行 → 产生完成中断。


RKNN_pc_version(0x0000)

硬件版本寄存器(只读)。驱动通过 version + (version_num & 0xFFFF) 计算完整版本号。

Bit属性复位值字段名描述
31:0RO0x0version硬件版本标识

RKNN_pc_version_num(0x0004)

硬件版本号寄存器(只读)。

Bit属性复位值字段名描述
31:16RO0x0保留
15:0RO0x0version_num硬件版本编号

RKNN_pc_operation_enable(0x0008)

操作使能寄存器。

Bit属性复位值字段名描述
31:1RO0x0保留
0RW0x0op_enPC 操作使能。0:禁用 PC 模块;1:使能 PC 模块,为每个 task 取寄存器配置

RKNN_pc_base_address(0x0010)

PC 基址寄存器,指定 DMA 指令流所在的内存地址。

Bit属性复位值字段名描述
31:4RW0x0pc_source_addrPC 基址。DMA 指令流所在的内存地址
3:1RO0x0保留
0RW0x0pc_selPC 模式选择。0:PC 模式,通过 AXI DMA 取寄存器配置;1:Slave 模式,通过 AHB 设置寄存器

RKNN_pc_register_amounts(0x0014)

每个 task 需要取的寄存器数量。

Bit属性复位值字段名描述
31:16RO0x0保留
15:0RW0x0pc_data_amount数据量。一个 task 需要取的寄存器数量

每条寄存器指令占 64 bit,格式如下:

位域含义
[63:48]目标模块选择(哪个 block)
[47:16]寄存器值
[15:0]各 block 内的偏移地址

模块选择位

Bit目标模块
56PC
57CNA
59CORE
60DPU
61DPU_RDMA
62PPU
63PPU_RDMA
55设置各 block 的 op_en

示例64'h0081_0000_007f_0008 将设置各 block 的 op_en(CNA, CORE, ..., PPU_RDMA)。

注意op_en 强烈建议放在寄存器列表末尾。在 op_en 之前,必须先设置 64'h0041_xxxx_xxxx_xxxx


RKNN_pc_interrupt_mask(0x0020)

中断掩码寄存器。置 1 使能对应中断。

Bit属性复位值字段名描述
31:17RO0x0保留
16:0RW0x1FFFFint_mask中断掩码(见下表)
Bit中断源
0CNA feature group 0
1CNA feature group 1
2CNA weight group 0
3CNA weight group 1
4CNA csc group 0
5CNA csc group 1
6CORE group 0
7CORE group 1
8DPU group 0
9DPU group 1
10PPU group 0
11PPU group 1
12DMA read error
13DMA write error

注意:在 PC 模式下,int_mask 设置的是最后一个 task 的中断掩码。


RKNN_pc_interrupt_clear(0x0024)

中断清除寄存器。写 1 清除对应中断位。

Bit属性复位值字段名描述
31:17RO0x0保留
16:0W1C0x0int_clr中断清除(位定义同 int_mask

INT_CLEAR_ALL = 0x1FFFF(清除 bit0~bit16 全部中断)rknpu-ioctl.h


RKNN_pc_interrupt_status(0x0028)

中断状态寄存器(经过 mask 后的状态)。

Bit属性复位值字段名描述
31:17RO0x0保留
16:0W1C0x0int_st中断状态,与 mask 位做 AND(位定义同 int_mask

RKNN_pc_interrupt_raw_status(0x002C)

中断原始状态寄存器(未经 mask 的原始状态)。

Bit属性复位值字段名描述
31:17RO0x0保留
16:0W1C0x0int_raw_st中断原始状态(位定义同 int_mask

RKNN_pc_task_con(0x0030)

任务控制寄存器。

Bit属性复位值字段名描述
31:14RO0x0保留
13W1C0x0task_count_clear任务计数器清除。清除当前 task 计数器,建议在 task 启动前清除
12RW0x0task_pp_enPing-pong 模式使能。0:关闭,第二组寄存器在第一组 task 完成后才取;1:开启,第二组寄存器在第一组取完后立即开始取
11:0RW0x0task_number要执行的 task 总数

RKNN_pc_task_dma_base_addr(0x0034)

任务 DMA 基址寄存器。

Bit属性复位值字段名描述
31:4RW0x0dma_base_addr任务基址。各 DMA(feature DMA、weight DMA、DPU DMA、PPU DMA)的地址设为偏移地址,AXI 总线上的最终地址 = 基址 + 偏移地址
3:0RO0x0保留

RKNN_pc_task_status(0x003C)

任务状态寄存器(只读)。

Bit属性复位值字段名描述
31:28RO0x0保留
27:0RW0x0task_status任务状态(见下表)
位域含义
[11:0]当前 task 计数器值
[12]指示第一个 task 正在执行 / 第一个 task 的寄存器正在取
[13]指示最后一个 task 正在执行 / 最后一个 task 的寄存器正在取

附:驱动层补充

中断状态归一化(rknpu_fuzz_status()

StarryOS Rust 驱动 在判定完成前,对 interrupt_status 做如下归一化处理:

位组掩码归一化规则对应模块
bit[1:0]0x03任一非零 → 置 0x03CNA_FG
bit[3:2]0x0C任一非零 → 置 0x0CCNA_WG
bit[5:4]0x30任一非零 → 置 0x30CNA_CSC
bit[7:6]0xC0任一非零 → 置 0xC0CORE
bit[9:8]0x300任一非零 → 置 0x300DPU
bit[11:10]0xC00任一非零 → 置 0xC00PPU

含义 逆向推断:每个功能模块有 2 个中断 bit(G0/G1),硬件可能只置其中一个,但驱动判定完成时需要两个都为 1,因此做归一化。

CNA 寄存器块(Convolution Neural-network Accelerator)

基址CORE_BASE + 0x1000 | 地址范围0x1000 ~ 0x1FFF

来源:RK3588 TRM §36.4.3 Detail Registers Description

CNA 是卷积加速单元,包含特征数据加载、权重加载、384KB 内部缓冲(CBUF)、序列扫描控制器(CSC)。


RKNN_cna_s_status(0x1000)

执行器状态寄存器(只读)。

Bit属性复位值字段名描述
31:18RO0x0保留
17:16RO0x0status_1执行器 1 状态。0:空闲;1:正在执行;2:正在执行且执行器 1 等待执行;3:保留
15:2RO0x0保留
1:0RO0x0status_0执行器 0 状态。0:空闲;1:正在执行;2:正在执行且执行器 1 等待执行;3:保留

RKNN_cna_s_pointer(0x1004)

寄存器组指针与 ping-pong 控制。

Bit属性复位值字段名描述
31:17RO0x0保留
16RO0x0executer当前使用的寄存器组。0:执行器组 0;1:执行器组 1
15:6RO0x0保留
5W1C0x0executer_pp_clear清除执行器组指针,写 1 清零
4W1C0x0pointer_pp_clear清除寄存器组指针,写 1 清零
3RW0x0pointer_pp_modePing-pong 模式。0:按执行器切换(executer 0 完成后切到 1);1:按指针切换(pointer 0 完成后切到 1)
2RW0x0executer_pp_en执行器组 ping-pong 使能。0:禁用;1:使能
1RW0x0pointer_pp_en寄存器组 ping-pong 使能。0:禁用;1:使能
0RW0x0pointer当前待设置的寄存器组。0:组 0;1:组 1

RKNN_cna_operation_enable(0x1008)

操作使能寄存器。写入此寄存器将触发 CNA 模块开始执行。此寄存器及之后的寄存器均为 ping-pong 影子寄存器。

Bit属性复位值字段名描述
31:1RO0x0保留
0RW0x0op_enCNA 操作使能。0:禁用;1:使能

RKNN_cna_conv_con1(0x100C)

卷积控制寄存器 1:精度、模式、反卷积等。

Bit属性复位值字段名描述
31RO0x0保留
30RW0x0nonalign_dmaCNA DMA 非对齐模式。0:禁用;1:使能(ARGB 模式下请开启,使 DMA 连续取特征数据)
29RW0x0group_line_off组行取数关闭。0:使能组行取数;1:禁用(仅影响取数效率)
28:17RO0x0保留
16RW0x0deconv反卷积使能。0:禁用;1:使能
15:12RW0x0argb_in非对齐通道层控制。8:1 通道输入;9:2 通道;10:3 通道;11:4 通道
11:10RO0x0保留
9:7RW0x0proc_precision处理精度。0:int8;1:int16;2:fp16;3:bf16;6:int4;7:tf32
6:4RW0x0in_precision输入精度。编码同 proc_precision
3:0RW0x0conv_mode卷积模式。0:直接卷积;3:深度可分离卷积(Depthwise)

RKNN_cna_conv_con2(0x1010)

卷积控制寄存器 2:kernel 分组、feature grain、CSC 控制。

Bit属性复位值字段名描述
31:24RO0x0保留
23:16RW0x0kernel_groupKernel 分组数。int8 下 32 个 kernel 为 1 组,int16/fp16 下 16 个为 1 组。例:256 个 kernel,int8 下设为 256/32−1=7
15:14RO0x0保留
13:4RW0x0feature_grains卷积开始前需缓冲的特征数据行数。建议设为 y_stride + weight_height + 1
3RO0x0保留
2RW0x0csc_wo_en权重扫描控制。0:使能 CSC 输出权重到 CORE;1:禁用
1RW0x0csc_do_en数据扫描控制。0:使能 CSC 输出特征数据到 CORE;1:禁用
0RW0x0cmd_fifo_srst命令 FIFO 软复位(调试用)

RKNN_cna_conv_con3(0x1014)

卷积控制寄存器 3:多核模式、空洞卷积、反卷积步长、卷积步长。

Bit属性复位值字段名描述
31RO0x0保留
30:28RW0x0nn_mode多核协作模式。0:32×32(单核);1:64×32;2:96×32;4:32×64;5:32×96。单核模式保持 0
27:26RO0x0保留
25:21RW0x0atrous_y_dilation空洞卷积 Y 方向膨胀值(列方向两像素间插入的 pad 数)
20:16RW0x0atrous_x_dilation空洞卷积 X 方向膨胀值(行方向两像素间插入的 pad 数)。>0 时启用空洞卷积
15:14RO0x0保留
13:11RW0x0deconv_y_stride反卷积 Y 步长
10:8RW0x0deconv_x_stride反卷积 X 步长
7:6RO0x0保留
5:3RW0x0conv_y_stride卷积 Y 步长
2:0RW0x0conv_x_stride卷积 X 步长

RKNN_cna_data_size0(0x1020)

输入特征数据宽高。

Bit属性复位值字段名描述
31:27RO0x0保留
26:16RW0x0datain_width输入特征数据宽度
15:11RO0x0保留
10:0RW0x0datain_height输入特征数据高度

RKNN_cna_data_size1(0x1024)

输入特征数据通道数。

Bit属性复位值字段名描述
31:30RO0x0保留
29:16RW0x0datain_channel_real真实通道数。当输入通道不是 8(int8)或 4(int16/fp16)的整数倍时,设置此字段
15:0RW0x0datain_channel输入通道数。int8 须为 8 的整数倍;int16/fp16 须为 4 的整数倍

RKNN_cna_data_size2(0x1028)

卷积后输出数据宽度。

Bit属性复位值字段名描述
31:11RO0x0保留
10:0RW0x0dataout_width卷积后数据宽度

RKNN_cna_data_size3(0x102C)

卷积后输出数据 surface 模式与总像素数。

Bit属性复位值字段名描述
31:24RO0x0保留
23:22RW0x0surf_modeSurface 串行模式。0/1:1 surf;2:2 surf;3:4 surf
21:0RW0x0dataout_atomics卷积后输出总像素数

RKNN_cna_weight_size0(0x1030)

权重总字节数。

Bit属性复位值字段名描述
31:0RW0x0weight_bytes本次卷积的权重总字节数

RKNN_cna_weight_size1(0x1034)

单个 kernel 的权重字节数。

Bit属性复位值字段名描述
31:19RO0x0保留
18:0RW0x0weight_bytes_per_kernel单个 kernel 的权重字节数

RKNN_cna_weight_size2(0x1038)

Kernel 宽高与数量。

Bit属性复位值字段名描述
31:29RO0x0保留
28:24RW0x0weight_widthKernel 宽度
23:21RO0x0保留
20:16RW0x0weight_heightKernel 高度
15:14RO0x0保留
13:0RW0x0weight_kernelsKernel 数量

RKNN_cna_cbuf_con0(0x1040)

CBUF(内部缓冲)控制寄存器 0:数据/权重复用、Bank 分配。

Bit属性复位值字段名描述
31:14RO0x0保留
13RW0x0weight_reuse权重数据复用使能。0:禁用;1:使能,直接从内部缓冲取权重
12RW0x0data_reuse特征数据复用使能。0:禁用;1:使能,直接从内部缓冲取数据
11RO0x0保留
10:8RW0x0fc_data_bankFC 零跳过模式的特征数据 Bank 数。FC 零跳过模式设为 1,否则必须为 0
7:4RW0x0weight_bank权重数据占用的 Bank 数。1:Bank 7;2:Bank 6-7;…;7:Bank 1-7
3:0RW0x0data_bank特征数据占用的 Bank 数。0:Bank 0;1:Bank 0-1;…;6:Bank 0-6

RKNN_cna_cbuf_con1(0x1044)

CBUF 控制寄存器 1。

Bit属性复位值字段名描述
31:13RO0x0保留
12:0RW0x0data_entries存储一行特征图所需的 Bank 空间

RKNN_cna_cvt_con0(0x104C)

输入转换控制寄存器 0:CVT 截断值、符号、舍入、旁路。

Bit属性复位值字段名描述
31:28RO0x0保留
27:22RW0x0cvt_truncate_3CVT 截断值 3
21:16RW0x0cvt_truncate_2CVT 截断值 2
15:10RW0x0cvt_truncate_1CVT 截断值 1
9:4RW0x0cvt_truncate_0CVT 截断值 0
3RW0x0data_sign特征数据符号。0:无符号;1:有符号
2RW0x0round_type舍入类型。0:奇入偶不入;1:0.5 向上进 1
1RW0x0cvt_type转换运算顺序。0:先乘后加;1:先加后乘
0RW0x0cvt_bypass旁路输入转换。0:使能 CVT;1:禁用 CVT

RKNN_cna_cvt_con1(0x1050)

输入转换控制 1:第 1 通道的 scale 和 offset。

Bit属性复位值字段名描述
31:16RW0x0cvt_scale0CVT 缩放 0(第 1 通道乘法操作数)
15:0RW0x0cvt_offset0CVT 偏移 0(第 1 通道加法操作数)

RKNN_cna_cvt_con2(0x1054)

输入转换控制 2:第 2 通道。

Bit属性复位值字段名描述
31:16RW0x0cvt_scale1CVT 缩放 1(第 2 通道乘法操作数)
15:0RW0x0cvt_offset1CVT 偏移 1(第 2 通道加法操作数)

RKNN_cna_cvt_con3(0x1058)

输入转换控制 3:第 3 通道。

Bit属性复位值字段名描述
31:16RW0x0cvt_scale2CVT 缩放 2(第 3 通道乘法操作数)
15:0RW0x0cvt_offset2CVT 偏移 2(第 3 通道加法操作数)

RKNN_cna_cvt_con4(0x105C)

输入转换控制 4:第 4 通道。

Bit属性复位值字段名描述
31:16RW0x0cvt_scale3CVT 缩放 3(第 4 通道乘法操作数)
15:0RW0x0cvt_offset3CVT 偏移 3(第 4 通道加法操作数)

RKNN_cna_fc_con0(0x1060)

全连接零跳过控制 0。

Bit属性复位值字段名描述
31:16RW0x0fc_skip_dataFC 零跳过数据值(通常设为 0)
15:1RO0x0保留
0RW0x0fc_skip_enFC 零跳过使能。0:禁用;1:使能。当某像素特征数据为 0 时,跳过对应权重的取数

RKNN_cna_fc_con1(0x1064)

全连接零跳过控制 1。

Bit属性复位值字段名描述
31:17RO0x0保留
16:0RW0x0data_offsetFC 零跳过模式下的特征数据偏移

RKNN_cna_pad_con0(0x1068)

Pad 控制寄存器 0。

Bit属性复位值字段名描述
31:8RO0x0保留
7:4RW0x0pad_left特征图左侧 pad 数
3:0RW0x0pad_top特征图顶部 pad 数

RKNN_cna_feature_data_addr(0x1070)

输入特征数据基址。

Bit属性复位值字段名描述
31:0RW0x0feature_base_addr特征数据地址

RKNN_cna_fc_con2(0x1074)

权重数据地址偏移。

Bit属性复位值字段名描述
31:17RO0x0保留
16:0RW0x0weight_offset权重数据地址偏移

RKNN_cna_dma_con0(0x1078)

AXI DMA 控制寄存器 0:burst 长度。

Bit属性复位值字段名描述
31RW0x0ov4k_bypass超 4K burst 拆分。0:使能(将超 4K 的 burst 拆为 2 个);1:旁路
30:20RO0x0保留
19:16RW0x0weight_burst_len权重 DMA AXI burst 长度。3:burst=4;7:burst=8;15:burst=16
15:4RO0x0保留
3:0RW0x0data_burst_len特征 DMA AXI burst 长度。编码同上

RKNN_cna_dma_con1(0x107C)

行步长(Line stride)。

Bit属性复位值字段名描述
31:28RO0x0保留
27:0RW0x0line_stride行步长。含虚拟框(Virtual box)的特征宽度

RKNN_cna_dma_con2(0x1080)

Surface 步长。

Bit属性复位值字段名描述
31:28RO0x0保留
27:0RW0x0surf_strideSurface 步长。特征图实际 surface 面积

RKNN_cna_fc_data_size0(0x1084)

FC 模式下 AXI DMA 的特征输入宽高。

Bit属性复位值字段名描述
31:30RO0x0保留
29:16RW0x0dma_widthAXI DMA 特征输入宽度
15:11RO0x0保留
10:0RW0x0dma_heightAXI DMA 特征输入高度

RKNN_cna_fc_data_size1(0x1088)

FC 模式下 AXI DMA 的特征输入通道数。

Bit属性复位值字段名描述
31:16RO0x0保留
15:0RW0x0dma_channelAXI DMA 特征输入通道数

RKNN_cna_clk_gate(0x1090)

时钟门控控制寄存器。

Bit属性复位值字段名描述
31:5RO0x0保留
4RW0x0cbuf_cs_disable_clkgateCBUF 自动时钟门控。0:使能自动门控;1:禁用 CBUF 时钟门控
3RO0x0保留
2RW0x0csc_disable_clkgateCSC 自动时钟门控。0:使能;1:禁用 CSC 时钟门控
1RW0x0cna_weight_disable_clkgate权重取数自动时钟门控。0:使能;1:禁用
0RW0x0cna_feature_disable_clkgate特征取数自动时钟门控。0:使能;1:禁用

RKNN_cna_dcomp_ctrl(0x1100)

权重解压控制寄存器。

Bit属性复位值字段名描述
31:4RO0x0保留
3RW0x0wt_dec_bypass旁路权重解压
2:0RW0x0decomp_control权重解压控制

RKNN_cna_dcomp_regnum(0x1104)

权重解压寄存器数。

Bit属性复位值字段名描述
31:0RW0x0dcomp_regnum权重解压寄存器数量

RKNN_cna_dcomp_addr0(0x1110)

权重基址。

Bit属性复位值字段名描述
31:4RW0x0decompress_addr0权重基址
3:0RO0x0保留

RKNN_cna_dcomp_amount0~15(0x1140 ~ 0x117C)

权重解压量寄存器,共 16 个,偏移 0x1140 + N×4(N = 0~15)。

Bit属性复位值字段名描述
31:0RW0x0dcomp_amountN第 N 次解压的权重数据量

RKNN_cna_cvt_con5(0x1180)

按通道 CVT 使能。

Bit属性复位值字段名描述
31:0RW0x0per_channel_cvt_en按通道使能 CVT 功能。int4 共 32 通道(128 bit),int8 共 16 通道

RKNN_cna_pad_con1(0x1184)

Pad 值寄存器。

Bit属性复位值字段名描述
31:0RW0x0pad_valuePad 填充值

CORE 寄存器块(MAC 核心控制)

基址CORE_BASE + 0x3000 | 地址范围0x3000 ~ 0x3FFF

来源:RK3588 TRM §36.4.3 Detail Registers Description

CORE 模块包含 MAC 阵列和累加器,负责卷积乘累加运算。


RKNN_core_s_status(0x3000)

执行器状态寄存器(只读)。

Bit属性复位值字段名描述
31:18RO0x0保留
17:16RO0x0status_1执行器 1 状态。0:空闲;1:正在执行;2:正在执行且执行器 1 等待执行;3:保留
15:2RO0x0保留
1:0RO0x0status_0执行器 0 状态。编码同 status_1

RKNN_core_s_pointer(0x3004)

寄存器组指针与 ping-pong 控制。

Bit属性复位值字段名描述
31:17RO0x0保留
16RO0x0executer当前使用的寄存器组。0:执行器组 0;1:执行器组 1
15:6RO0x0保留
5W1C0x0executer_pp_clear清除执行器组指针,写 1 清零
4W1C0x0pointer_pp_clear清除寄存器组指针,写 1 清零
3RW0x0pointer_pp_modePing-pong 模式。0:按执行器切换;1:按指针切换
2RW0x0executer_pp_en执行器组 ping-pong 使能。0:禁用;1:使能
1RW0x0pointer_pp_en寄存器组 ping-pong 使能。0:禁用;1:使能
0RW0x0pointer当前待设置的寄存器组。0:组 0;1:组 1

RKNN_core_operation_enable(0x3008)

操作使能寄存器。写入此寄存器将触发 CORE 模块开始执行。此寄存器及之后的寄存器均为 ping-pong 影子寄存器。

Bit属性复位值字段名描述
31:1RO0x0保留
0RW0x0op_enCORE 操作使能。0:禁用;1:使能

RKNN_core_mac_gating(0x300C)

MAC 软时钟门控寄存器。

Bit属性复位值字段名描述
31:27RO0x0保留
26:0RW0x7800800slcg_op_en软时钟门控信号

注意:复位值为 0x07800800,与其他寄存器不同。


RKNN_core_misc_cfg(0x3010)

杂项配置寄存器:精度、深度可分离、量化使能。

Bit属性复位值字段名描述
31:20RO0x0保留
19:14RW0x0soft_gating累加器软门控信号
13:11RO0x0保留
10:8RW0x0proc_precision处理精度。0:int8;1:int16;2:fp16;3:bf16;6:int4;7:tf32
7:2RO0x0保留
1RW0x0dw_en深度可分离模式使能。0:禁用;1:使能 Depthwise 模式
0RW0x0qd_en量化特征数据计算使能。0:禁用;1:使能

RKNN_core_dataout_size_0(0x3014)

输出特征尺寸寄存器 0:宽高。

Bit属性复位值字段名描述
31:16RW0x0dataout_height激活后输出数据高度
15:0RW0x0dataout_width激活后输出数据宽度

RKNN_core_dataout_size_1(0x3018)

输出特征尺寸寄存器 1:通道数。

Bit属性复位值字段名描述
31:16RO0x0保留
15:0RW0x0dataout_channel激活后输出数据通道数

RKNN_core_clip_truncate(0x301C)

截断与舍入控制寄存器。

Bit属性复位值字段名描述
31:7RO0x0保留
6RW0x0round_type舍入类型。0:奇入偶不入;1:0.5 向上进 1
5RO0x0保留
4:0RW0x0clip_truncate截断位数

DPU 寄存器块(Data Processing Unit)

基址CORE_BASE + 0x4000 | 地址范围0x4000 ~ 0x4FFF

来源:RK3588 TRM §36.4.3 Detail Registers Description

DPU 负责后处理运算,包含三级流水线核心:BS CORE(Bias/Scale)→ BN CORE(Batch Norm)→ EW CORE(Element-Wise),以及输出转换器、LUT 引擎、转置/重组等功能。


RKNN_dpu_s_status(0x4000)

执行器状态寄存器(只读)。

Bit属性复位值字段名描述
31:18RO0x0保留
17:16RO0x0status_1执行器 1 状态。0:空闲;1:正在执行;2:正在执行且等待执行;3:保留
15:2RO0x0保留
1:0RO0x0status_0执行器 0 状态。编码同 status_1

RKNN_dpu_s_pointer(0x4004)

寄存器组指针与 ping-pong 控制。

Bit属性复位值字段名描述
31:17RO0x0保留
16RO0x0executer当前使用的寄存器组。0:组 0;1:组 1
15:6RO0x0保留
5W1C0x0executer_pp_clear清除执行器组指针,写 1 清零
4W1C0x0pointer_pp_clear清除寄存器组指针,写 1 清零
3RW0x0pointer_pp_modePing-pong 模式。0:按执行器切换;1:按指针切换
2RW0x0executer_pp_en执行器组 ping-pong 使能
1RW0x0pointer_pp_en寄存器组 ping-pong 使能
0RW0x0pointer当前待设置的寄存器组。0:组 0;1:组 1

RKNN_dpu_operation_enable(0x4008)

操作使能。写入触发 DPU 执行,此寄存器及之后均为 ping-pong 影子寄存器。

Bit属性复位值字段名描述
31:1RO0x0保留
0RW0x0op_enDPU 操作使能。0:禁用;1:使能

RKNN_dpu_feature_mode_cfg(0x400C)

特征模式配置:flying mode、输出目标、卷积模式、burst、非对齐、转置、重组。

Bit属性复位值字段名描述
31RW0x0comb_use组合使用,同 DPU_RDMA comb_use[0]
30RW0x0tp_en转置使能
29:26RW0x0rgp_type重组类型。0:全部 128bit;1:4bit;2:8bit;3:16bit;4:32bit;5:64bit
25RW0x0nonalign非对齐模式使能(输出数据流与输入相同时可用)
24:9RW0x0surf_len非对齐模式下存储的 8 字节数
8:5RW0x0burst_lenBurst 长度。3:Burst4;7:Burst8;15:Burst16
4:3RW0x0conv_mode卷积模式。0:普通卷积;3:Depthwise
2:1RW0x0output_mode输出目标。[0]:输出到 PPU;[1]:输出到外部
0RW0x0flying_modeFlying 模式。0:主数据来自卷积输出;1:主数据来自 MRDMA

RKNN_dpu_data_format(0x4010)

数据格式配置:输入/输出/处理精度、负数移位值。

Bit属性复位值字段名描述
31:29RW0x0out_precision输出精度。0:int8;1:int16;2:fp16;3:bf16;4:int32;5:fp32;6:int4
28:26RW0x0in_precision输入精度(同 DPU_RDMA)。编码同上
25:16RW0x0ew_truncate_negEW CORE 负数移位值
15:10RW0x0bn_mul_shift_value_negBN CORE 负数移位值
9:4RW0x0bs_mul_shift_value_negBS CORE 负数移位值
3RW0x0mc_surf_out多 surface 输出。0:每像素 16 字节对齐;1:可输出 2/4 surface 串行
2:0RW0x0proc_precision处理精度。编码同 out_precision

RKNN_dpu_offset_pend(0x4014)

额外通道填充值。

Bit属性复位值字段名描述
31:16RO0x0保留
15:0RW0x0offset_pend额外通道设置值

RKNN_dpu_dst_base_addr(0x4020)

目标基址。

Bit属性复位值字段名描述
31:4RW0x0dst_base_addr目标基址
3:0RO0x0保留

RKNN_dpu_dst_surf_stride(0x4024)

输出 surface 步长。

Bit属性复位值字段名描述
31:4RW0x0dst_surf_stride输出 shape 的 surface 步长
3:0RO0x0保留

RKNN_dpu_data_cube_width(0x4030)

输入 cube 宽度。

Bit属性复位值字段名描述
31:13RO0x0保留
12:0RW0x0width输入 cube 宽度

RKNN_dpu_data_cube_height(0x4034)

输入 cube 高度 + minmax 控制。

Bit属性复位值字段名描述
31:25RO0x0保留
24:22RW0x0minmax_ctlMinMax 配置。[0]:使能;[1]:类型;[2]:仅概率
21:13RO0x0保留
12:0RW0x0height输入 cube 高度

RKNN_dpu_data_cube_notch_addr(0x4038)

Notch 地址(宽度末尾到 shape 行末的像素数)。

Bit属性复位值字段名描述
31:29RO0x0保留
28:16RW0x0notch_addr_1Notch 地址 1
15:13RO0x0保留
12:0RW0x0notch_addr_0Notch 地址 0

RKNN_dpu_data_cube_channel(0x403C)

输入 cube 通道数。

Bit属性复位值字段名描述
31:29RO0x0保留
28:16RW0x0orig_channel原始输出通道数
15:13RO0x0保留
12:0RW0x0channelCube 通道数

RKNN_dpu_bs_cfg(0x4040)

BS CORE 配置:ALU 算法、操作数来源、ReLU/PRELU/RELUX 控制。

Bit属性复位值字段名描述
31:20RO0x0保留
19:16RW0x0bs_alu_algoBS ALU 运算类型。2:Add;4:Minus
15:9RO0x0保留
8RW0x0bs_alu_srcALU 操作数来源。0:寄存器;1:外部
7RW0x0bs_relux_enRELUX 使能
6RW0x0bs_relu_bypass旁路 BS RELU。0:不旁路;1:旁路
5RW0x0bs_mul_preluMUL PRELU 使能
4RW0x0bs_mul_bypass旁路 BS MUL
3:2RO0x0保留
1RW0x0bs_alu_bypass旁路 BS ALU
0RW0x0bs_bypass旁路整个 BS CORE

RKNN_dpu_bs_alu_cfg(0x4044)

BS ALU 操作数。

Bit属性复位值字段名描述
31:0RW0x0bs_alu_operandBS CORE ALU 操作数

RKNN_dpu_bs_mul_cfg(0x4048)

BS MUL 配置:操作数、移位值、来源。

Bit属性复位值字段名描述
31:16RW0x0bs_mul_operandBS MUL 操作数
15:14RO0x0保留
13:8RW0x0bs_mul_shift_valueBS 正数移位值
7:2RO0x0保留
1RW0x0bs_truncate_src移位值来源。0:寄存器;1:外部
0RW0x0bs_mul_srcMUL 操作数来源。0:寄存器;1:外部

RKNN_dpu_bs_relux_cmp_value(0x404C)

BS RELUX 比较值。

Bit属性复位值字段名描述
31:0RW0x0bs_relux_cmp_datRELUX 比较值

RKNN_dpu_bs_ow_cfg(0x4050)

BS OW(CPEND)配置 + 重组计数器 + 转置。

Bit属性复位值字段名描述
31:28RW0x0rgp_cnter重组计数器。0:全选;1:每 2 选 1;2:每 4 选 1;3:每 8 选 1
27RW0x0tp_org_en原始转置使能
26:11RO0x0保留
10:8RW0x0size_e_2最后一行输出每行 8 通道数(−1)
7:5RW0x0size_e_1中间行输出每行 8 通道数(−1)
4:2RW0x0size_e_0第一行输出每行 8 通道数(−1)
1RW0x0od_bypass旁路 CPEND
0RW0x0ow_srcCPEND 操作数来源。0:寄存器;1:外部

RKNN_dpu_bs_ow_op(0x4054)

CPEND 操作数。

Bit属性复位值字段名描述
31:16RO0x0保留
15:0RW0x0ow_opCPEND 操作数

RKNN_dpu_wdma_size_0(0x4058)

DPU WDMA 尺寸 0:转置精度、通道。

Bit属性复位值字段名描述
31:28RO0x0保留
27RW0x0tp_precision转置精度。0:8bit;1:16bit
26:16RW0x0size_c_wdmaWDMA 的 size_c
15:13RO0x0保留
12:0RW0x0channel_wdmaWDMA 通道数

RKNN_dpu_wdma_size_1(0x405C)

DPU WDMA 尺寸 1:宽高。

Bit属性复位值字段名描述
31:29RO0x0保留
28:16RW0x0height_wdmaWDMA 高度
15:13RO0x0保留
12:0RW0x0width_wdmaWDMA 宽度

RKNN_dpu_bn_cfg(0x4060)

BN CORE 配置:ALU 算法、操作数来源、ReLU/PRELU/RELUX 控制。

Bit属性复位值字段名描述
31:20RO0x0保留
19:16RW0x0bn_alu_algoBN ALU 运算类型。2:Add;4:Minus
15:9RO0x0保留
8RW0x0bn_alu_srcALU 操作数来源。0:寄存器;1:外部
7RW0x0bn_relux_enRELUX 使能
6RW0x0bn_relu_bypass旁路 BN RELU
5RW0x0bn_mul_preluMUL PRELU 使能
4RW0x0bn_mul_bypass旁路 BN MUL
3:2RO0x0保留
1RW0x0bn_alu_bypass旁路 BN ALU
0RW0x0bn_bypass旁路整个 BN CORE

RKNN_dpu_bn_alu_cfg(0x4064)

BN ALU 操作数。

Bit属性复位值字段名描述
31:0RW0x0bn_alu_operandBN CORE ALU 操作数

RKNN_dpu_bn_mul_cfg(0x4068)

BN MUL 配置。

Bit属性复位值字段名描述
31:16RW0x0bn_mul_operandBN MUL 操作数
15:14RO0x0保留
13:8RW0x0bn_mul_shift_valueBN 正数移位值
7:2RO0x0保留
1RW0x0bn_truncate_src移位值来源。0:寄存器;1:外部
0RW0x0bn_mul_srcMUL 操作数来源。0:寄存器;1:外部

RKNN_dpu_bn_relux_cmp_value(0x406C)

BN RELUX 比较值。

Bit属性复位值字段名描述
31:0RW0x0bn_relux_cmp_datBN RELUX 比较数据

RKNN_dpu_ew_cfg(0x4070)

EW CORE 配置:ALU 算法(Max/Min/Add/Div/Minus/Abs/Neg/Floor/Ceil)、LUT、转换器、PRELU 等。

Bit属性复位值字段名描述
31RW0x0ew_cvt_typeEW 输入转换类型。0:先乘后加;1:先加后乘
30RW0x0ew_cvt_roundEW 输入转换舍入。0:奇入偶不入;1:0.5 向上进 1
29:28RW0x0ew_data_modeERDMA 数据模式
27:24RO0x0保留
23:22RW0x0edata_sizeERDMA cube 数据大小。0:4bit;1:8bit;2:16bit;3:32bit
21RW0x0ew_equal_enMinMax 相等使能
20RW0x0ew_binary_enMinMax 二值使能
19:16RW0x0ew_alu_algoEW ALU 运算。0:Max;1:Min;2:Add;3:Div;4:Minus;5:Abs;6:Neg;7:Floor;8:Ceil
15:11RO0x0保留
10RW0x0ew_relux_enRELUX 使能
9RW0x0ew_relu_bypass旁路 EW RELU
8RW0x0ew_op_cvt_bypass旁路 EW 输入转换器
7RW0x0ew_lut_bypass旁路 LUT
6RW0x0ew_op_src操作数来源。0:寄存器;1:外部
5RW0x0ew_mul_preluMUL PRELU 使能
4:3RO0x0保留
2RW0x0ew_op_type运算类型。0:ALU;1:MUL
1RW0x0ew_op_bypass旁路 EW ALU 和 MUL
0RW0x0ew_bypass旁路整个 EW CORE

RKNN_dpu_ew_cvt_offset_value(0x4074)

EW 输入转换偏移。

Bit属性复位值字段名描述
31:0RW0x0ew_op_cvt_offsetEW 转换偏移

RKNN_dpu_ew_cvt_scale_value(0x4078)

EW 转换缩放与移位。

Bit属性复位值字段名描述
31:22RW0x0ew_truncateEW CORE 移位值
21:16RW0x0ew_op_cvt_shiftEW 转换移位值
15:0RW0x0ew_op_cvt_scaleEW 转换缩放

RKNN_dpu_ew_relux_cmp_value(0x407C)

EW RELUX 比较值。

Bit属性复位值字段名描述
31:0RW0x0ew_relux_cmp_datEW RELUX 比较数据

RKNN_dpu_out_cvt_offset(0x4080)

输出转换偏移。

Bit属性复位值字段名描述
31:0RW0x0out_cvt_offset输出转换器偏移

RKNN_dpu_out_cvt_scale(0x4084)

输出转换缩放 + fp32→fp16 使能。

Bit属性复位值字段名描述
31:17RO0x0保留
16RW0x0fp32tofp16_en使能输出 fp32→fp16 转换
15:0RW0x0out_cvt_scale输出转换器缩放

RKNN_dpu_out_cvt_shift(0x4088)

输出转换移位、舍入、指数。

Bit属性复位值字段名描述
31RW0x0cvt_type输出转换类型。0:先乘后加;1:先加后乘
30RW0x0cvt_round输出转换舍入。0:奇入偶不入;1:0.5 向上进 1
29:20RO0x0保留
19:12RW0x0minus_exp输出 CVT 减指数
11:0RW0x0out_cvt_shift输出转换器移位

RKNN_dpu_ew_op_value_0~7(0x4090 ~ 0x40AC)

EW CORE 操作数寄存器,共 8 个,偏移 0x4090 + N×4(N = 0~7)。

Bit属性复位值字段名描述
31:0RW0x0ew_operand_N第 N+1 个 EW 操作数

RKNN_dpu_surface_add(0x40C0)

Surface 加法器。

Bit属性复位值字段名描述
31:4RW0x0surf_add一行中有多少个 surface
3:0RO0x0保留

RKNN_dpu_lut_access_cfg(0x4100)

LUT 访问配置。

Bit属性复位值字段名描述
31:18RO0x0保留
17RW0x0lut_access_type访问类型。0:读;1:写
16RW0x0lut_table_id访问 ID。0:LE LUT;1:LO LUT
15:10RO0x0保留
9:0RW0x0lut_addr访问地址

RKNN_dpu_lut_access_data(0x4104)

LUT 访问数据。

Bit属性复位值字段名描述
31:16RO0x0保留
15:0RW0x0lut_access_dataLUT 访问数据

RKNN_dpu_lut_cfg(0x4108)

LUT 配置。

Bit属性复位值字段名描述
31:8RO0x0保留
7RW0x0lut_cal_selLUT 计算选择(仅 lut_expand_en=1 时有效)
6RW0x0lut_hybrid_priority混合流优先级。0:LE LUT;1:LO LUT
5RW0x0lut_oflow_priority上溢优先级。0:LE;1:LO
4RW0x0lut_uflow_priority下溢优先级。0:LE;1:LO
3:2RW0x0lut_lo_le_muxLO/LE LUT 复用
1RW0x0lut_expand_en扩展两个小 LUT 为一个大 LUT
0RW0x0lut_road_selLUT 路径选择。0:第 1 路;1:第 2 路

RKNN_dpu_lut_info(0x410C)

LUT 索引选择。

Bit属性复位值字段名描述
31:24RO0x0保留
23:16RW0x0lut_lo_index_selectLO LUT 索引选择(索引生成器中选择哪些位作为索引)
15:8RW0x0lut_le_index_selectLE LUT 索引选择
7:0RO0x0保留

RKNN_dpu_lut_le_start(0x4110)

Bit属性复位值字段名描述
31:0RW0x0lut_le_startLE LUT 起始点

RKNN_dpu_lut_le_end(0x4114)

Bit属性复位值字段名描述
31:0RW0x0lut_le_endLE LUT 终止点

RKNN_dpu_lut_lo_start(0x4118)

Bit属性复位值字段名描述
31:0RW0x0lut_lo_startLO LUT 起始点

RKNN_dpu_lut_lo_end(0x411C)

Bit属性复位值字段名描述
31:0RW0x0lut_lo_endLO LUT 终止点

RKNN_dpu_lut_le_slope_scale(0x4120)

LE LUT 斜率缩放(上溢/下溢)。

Bit属性复位值字段名描述
31:16RW0x0lut_le_slope_oflow_scaleLE LUT 上溢斜率缩放
15:0RW0x0lut_le_slope_uflow_scaleLE LUT 下溢斜率缩放

RKNN_dpu_lut_le_slope_shift(0x4124)

LE LUT 斜率移位。

Bit属性复位值字段名描述
31:10RO0x0保留
9:5RW0x0lut_le_slope_oflow_shiftLE LUT 上溢斜率移位
4:0RW0x0lut_le_slope_uflow_shiftLE LUT 下溢斜率移位

RKNN_dpu_lut_lo_slope_scale(0x4128)

LO LUT 斜率缩放。

Bit属性复位值字段名描述
31:16RW0x0lut_lo_slope_oflow_scaleLO LUT 上溢斜率缩放
15:0RW0x0lut_lo_slope_uflow_scaleLO LUT 下溢斜率缩放

RKNN_dpu_lut_lo_slope_shift(0x412C)

LO LUT 斜率移位。

Bit属性复位值字段名描述
31:10RO0x0保留
9:5RW0x0lut_lo_slope_oflow_shiftLO LUT 上溢斜率移位
4:0RW0x0lut_lo_slope_uflow_shiftLO LUT 下溢斜率移位

DPU_RDMA 寄存器块

基址CORE_BASE + 0x5000 | 地址范围0x5000 ~ 0x5FFF

来源:RK3588 TRM §36.4.3 Detail Registers Description

DPU_RDMA 负责为 DPU 从外部内存读取输入数据,包含四路 DMA 引擎:MRDMA(主数据)、BRDMA(BS 操作数)、NRDMA(BN 操作数)、ERDMA(EW 操作数)。


RKNN_dpu_rdma_s_status(0x5000)

执行器状态寄存器(只读)。

Bit属性复位值字段名描述
31:18RO0x0保留
17:16RO0x0status_1执行器 1 状态。0:空闲;1:正在执行;2:正在执行且等待执行;3:保留
15:2RO0x0保留
1:0RO0x0status_0执行器 0 状态。编码同 status_1

RKNN_dpu_rdma_s_pointer(0x5004)

寄存器组指针与 ping-pong 控制。

Bit属性复位值字段名描述
31:17RO0x0保留
16RO0x0executer当前使用的寄存器组。0:组 0;1:组 1
15:6RO0x0保留
5W1C0x0executer_pp_clear清除执行器组指针,写 1 清零
4W1C0x0pointer_pp_clear清除寄存器组指针,写 1 清零
3RW0x0pointer_pp_modePing-pong 模式。0:按执行器切换;1:按指针切换
2RW0x0executer_pp_en执行器组 ping-pong 使能
1RW0x0pointer_pp_en寄存器组 ping-pong 使能
0RW0x0pointer当前待设置的寄存器组。0:组 0;1:组 1

RKNN_dpu_rdma_operation_enable(0x5008)

操作使能。写入触发 DPU_RDMA 执行,此寄存器及之后均为 ping-pong 影子寄存器。

Bit属性复位值字段名描述
31:1RO0x0保留
0RW0x0op_enDPU_RDMA 操作使能。0:禁用;1:使能

RKNN_dpu_rdma_data_cube_width(0x500C)

输入特征宽度。

Bit属性复位值字段名描述
31:13RO0x0保留
12:0RW0x0width输入特征宽度(需减 1)

RKNN_dpu_rdma_data_cube_height(0x5010)

输入特征高度 + EW line notch。

Bit属性复位值字段名描述
31:29RO0x0保留
28:16RW0x0ew_line_notch_addrEW 行 notch
15:13RO0x0保留
12:0RW0x0height输入特征高度(需减 1)

RKNN_dpu_rdma_data_cube_channel(0x5014)

输入特征通道数。

Bit属性复位值字段名描述
31:13RO0x0保留
12:0RW0x0channel输入特征通道数(需减 1)

RKNN_dpu_rdma_src_base_addr(0x5018)

Flying 模式源地址。

Bit属性复位值字段名描述
31:0RW0x0src_base_addrFlying 模式源地址

RKNN_dpu_rdma_brdma_cfg(0x501C)

BRDMA(BS 操作数读取 DMA)配置。

Bit属性复位值字段名描述
31:5RO0x0保留
4:1RW0x0brdma_data_use读取数据类型。[0]:ALU 操作数;[1]:CPEND 操作数;[2]:MUL 操作数;[3]:TRT 操作数
0RO0x0保留

RKNN_dpu_rdma_bs_base_addr(0x5020)

BS 操作数基址。

Bit属性复位值字段名描述
31:0RW0x0bs_base_addr读取 BS ALU、BS CPEND、BS MUL 操作数的基址

RKNN_dpu_rdma_nrdma_cfg(0x5028)

NRDMA(BN 操作数读取 DMA)配置。

Bit属性复位值字段名描述
31:5RO0x0保留
4:1RW0x0nrdma_data_use读取数据类型。[0]:ALU 操作数;[1]:CPEND 操作数(固定为 0,BN 无 CPEND);[2]:MUL 操作数;[3]:TRT 操作数
0RO0x0保留

RKNN_dpu_rdma_bn_base_addr(0x502C)

BN 操作数基址。

Bit属性复位值字段名描述
31:0RW0x0bn_base_addr读取 BN ALU、BN MUL 操作数的基址

RKNN_dpu_rdma_erdma_cfg(0x5034)

ERDMA(EW 操作数读取 DMA)配置。

Bit属性复位值字段名描述
31:30RW0x0erdma_data_mode数据模式。0:按通道;1:按像素;2:按通道×像素;3:保留
29RW0x0erdma_surf_modeSurface 模式。0:1 surface 串行;1:2 surface 串行
28RW0x0erdma_nonalign非对齐模式。0:禁用;1:使能
27:4RO0x0保留
3:2RW0x0erdma_data_sizeERDMA 读取精度。0:4bit;1:8bit;2:16bit;3:32bit
1RW0x0ov4k_bypass超 4K burst 拆分。0:使能;1:旁路
0RW0x0erdma_disable禁用 ERDMA。0:不禁用;1:禁用

RKNN_dpu_rdma_ew_base_addr(0x5038)

EW 操作数基址。

Bit属性复位值字段名描述
31:0RW0x0ew_base_addr读取 EW 操作数的基址

RKNN_dpu_rdma_ew_surf_stride(0x5040)

EW 特征图 surface 步长。

Bit属性复位值字段名描述
31:4RW0x0ew_surf_strideEW 特征图 surface 步长。若 erdma_data_mode 为按通道模式,需设为 1
3:0RO0x0保留

RKNN_dpu_rdma_feature_mode_cfg(0x5044)

特征模式配置:精度、burst、组合使用、flying mode、unpooling。

Bit属性复位值字段名描述
31:18RO0x0保留
17:15RW0x0in_precision输入数据精度。0:int8;1:int16;2:fp16;3:bf16;4:int32;5:fp32;6:int4
14:11RW0x0burst_lenBurst 长度。3:Burst4;7:Burst8;15:Burst16
10:8RW0x0comb_use组合使用。[0]:MRDMA 和 ERDMA 读同一数据;[1]:数据送 MRDMA;[2]:数据送 ERDMA
7:5RW0x0proc_precision处理精度。编码同 in_precision
4RW0x0mrdma_disable禁用 MRDMA。0:不禁用;1:禁用
3RW0x0mrdma_fp16tofp32_en使能 DPU 输入 fp16→fp32 转换
2:1RW0x0conv_mode卷积模式。0:DC;3:Depthwise
0RW0x0flying_modeFlying 模式。0:主数据来自卷积输出;1:主数据来自 MRDMA

RKNN_dpu_rdma_src_dma_cfg(0x5048)

源 DMA 配置:line notch、unpooling 参数。

Bit属性复位值字段名描述
31:19RW0x0line_notch_addr宽度末尾到 shape 特征行末的像素数
18:14RO0x0保留
13RW0x0pooling_method池化方法。0:平均池化(上采样可用此模式);1:最小/最大池化
12RW0x0unpooling_en反池化使能
11:9RW0x0kernel_stride_height反池化 kernel 步长高度(−1)
8:6RW0x0kernel_stride_width反池化 kernel 步长宽度(−1)
5:3RW0x0kernel_height反池化 kernel 高度(−1)
2:0RW0x0kernel_width反池化 kernel 宽度(−1)

RKNN_dpu_rdma_surf_notch(0x504C)

Surface notch。

Bit属性复位值字段名描述
31:4RW0x0surf_notch_addr当前处理特征图末尾到 shape 特征图末尾的像素数
3:0RO0x0保留

RKNN_dpu_rdma_pad_cfg(0x5064)

反池化 Pad 配置。

Bit属性复位值字段名描述
31:16RW0x0pad_valuePad 填充值
15:7RO0x0保留
6:4RW0x0pad_top反池化顶部 pad
3RO0x0保留
2:0RW0x0pad_left反池化左侧 pad

RKNN_dpu_rdma_weight(0x5068)

四路 DMA 仲裁权重。

Bit属性复位值字段名描述
31:24RW0x0e_weightERDMA 仲裁权重
23:16RW0x0n_weightNRDMA 仲裁权重
15:8RW0x0b_weightBRDMA 仲裁权重
7:0RW0x0m_weightMRDMA 仲裁权重

RKNN_dpu_rdma_ew_surf_notch(0x506C)

EW surface notch。

Bit属性复位值字段名描述
31:4RW0x0ew_surf_notchEW surface notch
3:0RO0x0保留

PPU 寄存器块(Planar Processing Unit)

基址CORE_BASE + 0x6000 | 地址范围0x6000 ~ 0x6FFF

来源:RK3588 TRM §36.4.3 Detail Registers Description

PPU 负责池化运算,支持平均池化、最大池化、最小池化,可与 DPU 流水线级联或独立 flying 模式运行。


RKNN_ppu_s_status(0x6000)

执行器状态寄存器(只读)。

Bit属性复位值字段名描述
31:18RO0x0保留
17:16RO0x0status_1执行器 1 状态。0:空闲;1:正在执行;2:正在执行且等待执行;3:保留
15:2RO0x0保留
1:0RO0x0status_0执行器 0 状态。编码同 status_1

RKNN_ppu_s_pointer(0x6004)

寄存器组指针与 ping-pong 控制。

Bit属性复位值字段名描述
31:17RO0x0保留
16RO0x0executer当前使用的寄存器组。0:组 0;1:组 1
15:6RO0x0保留
5W1C0x0executer_pp_clear清除执行器组指针,写 1 清零
4W1C0x0pointer_pp_clear清除寄存器组指针,写 1 清零
3RW0x0pointer_pp_modePing-pong 模式。0:按执行器切换;1:按指针切换
2RW0x0executer_pp_en执行器组 ping-pong 使能
1RW0x0pointer_pp_en寄存器组 ping-pong 使能
0RW0x0pointer当前待设置的寄存器组。0:组 0;1:组 1

RKNN_ppu_operation_enable(0x6008)

操作使能。写入触发 PPU 执行,此寄存器及之后均为 ping-pong 影子寄存器。

Bit属性复位值字段名描述
31:1RO0x0保留
0RW0x0op_enPPU 操作使能。0:禁用;1:使能

RKNN_ppu_data_cube_in_width(0x600C)

池化输入 cube 宽度。

Bit属性复位值字段名描述
31:13RO0x0保留
12:0RW0x0cube_in_width池化输入宽度(需减 1)

RKNN_ppu_data_cube_in_height(0x6010)

池化输入 cube 高度。

Bit属性复位值字段名描述
31:13RO0x0保留
12:0RW0x0cube_in_height池化输入高度(需减 1)

RKNN_ppu_data_cube_in_channel(0x6014)

池化输入 cube 通道数。

Bit属性复位值字段名描述
31:13RO0x0保留
12:0RW0x0cube_in_channel池化输入通道数(需减 1)

RKNN_ppu_data_cube_out_width(0x6018)

池化输出 cube 宽度。

Bit属性复位值字段名描述
31:13RO0x0保留
12:0RW0x0cube_out_width池化输出宽度(需减 1)

RKNN_ppu_data_cube_out_height(0x601C)

池化输出 cube 高度。

Bit属性复位值字段名描述
31:13RO0x0保留
12:0RW0x0cube_out_height池化输出高度(需减 1)

RKNN_ppu_data_cube_out_channel(0x6020)

池化输出 cube 通道数。

Bit属性复位值字段名描述
31:13RO0x0保留
12:0RW0x0cube_out_channel池化输出通道数(需减 1)

RKNN_ppu_operation_mode_cfg(0x6024)

操作模式配置:池化方法、flying mode、notch、索引输出。

Bit属性复位值字段名描述
31RO0x0保留
30RW0x0index_en使能输出每个 kernel 的位置索引
29RO0x0保留
28:16RW0x0notch_addr宽度末尾到 shape 行末的像素数
15:8RO0x0保留
7:5RW0x0use_cntuse_cnt
4RW0x0flying_mode池化 cube 来源。0:DPU;1:外部
3:2RO0x0保留
1:0RW0x0pooling_method池化方法。0:平均池化;1:最大池化;2:最小池化;3:保留

RKNN_ppu_pooling_kernel_cfg(0x6034)

池化 kernel 大小与步长。

Bit属性复位值字段名描述
31:24RO0x0保留
23:20RW0x0kernel_stride_heightKernel 步长高度(需减 1)
19:16RW0x0kernel_stride_widthKernel 步长宽度(需减 1)
15:12RO0x0保留
11:8RW0x0kernel_heightKernel 高度(需减 1)
7:4RO0x0保留
3:0RW0x0kernel_widthKernel 宽度(需减 1)

RKNN_ppu_recip_kernel_width(0x6038)

Kernel 宽度倒数(用于平均池化计算)。

Bit属性复位值字段名描述
31:17RO0x0保留
16:0RW0x0recip_kernel_widthShape kernel 宽度的倒数 × 2^16

RKNN_ppu_recip_kernel_height(0x603C)

Kernel 高度倒数。

Bit属性复位值字段名描述
31:17RO0x0保留
16:0RW0x0recip_kernel_heightShape kernel 高度的倒数 × 2^16

RKNN_ppu_pooling_padding_cfg(0x6040)

池化四边 padding。

Bit属性复位值字段名描述
31:15RO0x0保留
14:12RW0x0pad_bottom底部 pad
11RO0x0保留
10:8RW0x0pad_right右侧 pad
7RO0x0保留
6:4RW0x0pad_top顶部 pad
3RO0x0保留
2:0RW0x0pad_left左侧 pad

RKNN_ppu_padding_value_1_cfg(0x6044)

Pad 填充值低 32 位。

Bit属性复位值字段名描述
31:0RW0x0pad_value_0pad_value×1 [31:0]

RKNN_ppu_padding_value_2_cfg(0x6048)

Pad 填充值高 3 位。

Bit属性复位值字段名描述
31:3RO0x0保留
2:0RW0x0pad_value_1pad_value×1 [34:32]

RKNN_ppu_dst_base_addr(0x6070)

输出 cube 目标基址。

Bit属性复位值字段名描述
31:4RW0x0dst_base_addr输出 cube 目标基址
3:0RO0x0保留

RKNN_ppu_dst_surf_stride(0x607C)

输出 surface 步长。

Bit属性复位值字段名描述
31:4RW0x0dst_surf_stride输出 shape 面积
3:0RO0x0保留

RKNN_ppu_data_format(0x6084)

数据格式配置。

Bit属性复位值字段名描述
31:4RW0x0index_addindex_en 使能,值为 dst_surface_stride × cube surface 数(每 surface 8 字节),否则等于 dst_surface_stride
3RW0x0dpu_flyin数据来自 DPU 且 DPU 数据来自外部时置 1
2:0RW0x0proc_precision处理精度

RKNN_ppu_misc_ctrl(0x60DC)

杂项控制:非对齐模式、多 surface 输出、burst。

Bit属性复位值字段名描述
31:16RW0x0surf_lenSurface 计数长度
15:9RO0x0保留
8RW0x0mc_surf_out多 surface 输出使能
7RW0x0nonalign非对齐模式使能
6:4RO0x0保留
3:0RW0x0burst_lenBurst 长度。3:Burst4;7:Burst8;15:Burst16

PPU_RDMA 寄存器块

基址CORE_BASE + 0x7000 | 地址范围0x7000 ~ 0x7FFF

来源:RK3588 TRM §36.4.3 Detail Registers Description

PPU_RDMA 负责为 PPU 从外部内存读取池化输入特征数据(flying 模式下使用)。


RKNN_ppu_rdma_s_status(0x7000)

执行器状态寄存器(只读)。

Bit属性复位值字段名描述
31:18RO0x0保留
17:16RO0x0status_1执行器 1 状态。0:空闲;1:正在执行;2:正在执行且等待执行;3:保留
15:2RO0x0保留
1:0RO0x0status_0执行器 0 状态。编码同 status_1

RKNN_ppu_rdma_s_pointer(0x7004)

寄存器组指针与 ping-pong 控制。

Bit属性复位值字段名描述
31:17RO0x0保留
16RO0x0executer当前使用的寄存器组。0:组 0;1:组 1
15:6RO0x0保留
5W1C0x0executer_pp_clear清除执行器组指针,写 1 清零
4W1C0x0pointer_pp_clear清除寄存器组指针,写 1 清零
3RW0x0pointer_pp_modePing-pong 模式。0:按执行器切换;1:按指针切换
2RW0x0executer_pp_en执行器组 ping-pong 使能
1RW0x0pointer_pp_en寄存器组 ping-pong 使能
0RW0x0pointer当前待设置的寄存器组。0:组 0;1:组 1

RKNN_ppu_rdma_operation_enable(0x7008)

操作使能。写入触发 PPU_RDMA 执行,此寄存器及之后均为 ping-pong 影子寄存器。

Bit属性复位值字段名描述
31:1RO0x0保留
0RW0x0op_enPPU_RDMA 操作使能。0:禁用;1:使能

RKNN_ppu_rdma_cube_in_width(0x700C)

池化输入 cube 宽度。

Bit属性复位值字段名描述
31:13RO0x0保留
12:0RW0x0cube_in_width池化 cube 宽度(需减 1)

RKNN_ppu_rdma_cube_in_height(0x7010)

池化输入 cube 高度。

Bit属性复位值字段名描述
31:13RO0x0保留
12:0RW0x0cube_in_height池化 cube 高度(需减 1)

RKNN_ppu_rdma_cube_in_channel(0x7014)

池化输入 cube 通道数。

Bit属性复位值字段名描述
31:13RO0x0保留
12:0RW0x0cube_in_channel池化 cube 通道数(需减 1)

RKNN_ppu_rdma_src_base_addr(0x701C)

池化 cube 源基址。

Bit属性复位值字段名描述
31:0RW0x0src_base_addr池化 cube 基址

RKNN_ppu_rdma_src_line_stride(0x7024)

源行步长(shape 宽度)。

Bit属性复位值字段名描述
31:4RW0x0src_line_stride池化 cube shape 宽度
3:0RO0x0保留

RKNN_ppu_rdma_src_surf_stride(0x7028)

源 surface 步长(shape 面积)。

Bit属性复位值字段名描述
31:4RW0x0src_surf_stride池化 cube shape 面积
3:0RO0x0保留

RKNN_ppu_rdma_data_format(0x7030)

输入数据格式。

Bit属性复位值字段名描述
31:2RO0x0保留
1:0RW0x0in_precision输入精度。0:4bit;1:8bit;2:16bit;3:32bit

DDMA / SDMA 寄存器块(Data DMA / System DMA)

DDMA 基址CORE_BASE + 0x8000 | 地址范围0x8000 ~ 0x8FFF SDMA 基址CORE_BASE + 0x9000 | 地址范围0x9000 ~ 0x9FFF

来源:RK3588 TRM §36.4.3 Detail Registers Description

DDMA 和 SDMA 寄存器布局完全一致,仅基址不同(DDMA 0x8xxx,SDMA 0x9xxx)。DDMA 用于数据搬运,SDMA 用于系统级搬运。以下以 DDMA 为例展开位域,SDMA 将偏移 0x8xxx 替换为 0x9xxx 即可。


cfg_outstanding(DDMA: 0x8000 / SDMA: 0x9000)

读写 outstanding 数配置。

Bit属性复位值字段名描述
31:16RO0x0保留
15:8RW0x0wr_os_cnt最大写 outstanding 数
7:0RW0x0rd_os_cnt最大读 outstanding 数

rd_weight_0(DDMA: 0x8004 / SDMA: 0x9004)

读仲裁权重 0:各模块读 burst 权重。

Bit属性复位值字段名描述
31:24RW0x0rd_weight_pdpPPU 读 burst 权重
23:16RW0x0rd_weight_dpuDPU 读 burst 权重
15:8RW0x0rd_weight_kernel权重读 burst 权重
7:0RW0x0rd_weight_feature特征读 burst 权重

wr_weight_0(DDMA: 0x8008 / SDMA: 0x9008)

写仲裁权重。

Bit属性复位值字段名描述
31:16RO0x0保留
15:8RW0x0wr_weight_pdpPPU 写权重
7:0RW0x0wr_weight_dpuDPU 写权重

cfg_id_error(DDMA: 0x800C / SDMA: 0x900C)

错误 ID 记录。

Bit属性复位值字段名描述
31:10RO0x0保留
9:6RW0x0wr_resp_id错误写 ID
5RO0x0保留
4:0RW0x0rd_resp_id错误读 ID

rd_weight_1(DDMA: 0x8010 / SDMA: 0x9010)

读仲裁权重 1:PC 读 burst 权重。

Bit属性复位值字段名描述
31:8RO0x0保留
7:0RW0x0rd_weight_pcPC 读 burst 权重

cfg_dma_fifo_clr(DDMA: 0x8014 / SDMA: 0x9014)

清除 DMA FIFO。

Bit属性复位值字段名描述
31:1RO0x0保留
0RW0x0dma_fifo_clr清除 DMA FIFO

cfg_dma_arb(DDMA: 0x8018 / SDMA: 0x9018)

DMA 仲裁模式配置。

Bit属性复位值字段名描述
31:10RO0x0保留
9RW0x0wr_arbit_model写仲裁模型
8RW0x0rd_arbit_model读仲裁模型
7RO0x0保留
6:4RW0x0wr_fix_arb写固定仲裁
3RO0x0保留
2:0RW0x0rd_fix_arb读固定仲裁

cfg_dma_rd_qos(DDMA: 0x8020 / SDMA: 0x9020)

各模块读 QoS 配置。

Bit属性复位值字段名描述
31:10RO0x0保留
9:8RW0x0rd_pc_qosPC 读 QoS
7:6RW0x0rd_ppu_qosPPU 读 QoS
5:4RW0x0rd_dpu_qosDPU 读 QoS
3:2RW0x0rd_kernel_qosKernel 读 QoS
1:0RW0x0rd_feature_qosFeature 读 QoS

cfg_dma_rd_cfg(DDMA: 0x8024 / SDMA: 0x9024)

AXI 读通道信号配置。

Bit属性复位值字段名描述
31:13RO0x0保留
12RW0x0rd_arlockAXI arlock
11:8RW0x0rd_arcacheAXI arcache
7:5RW0x0rd_arprotAXI arprot
4:3RW0x0rd_arburstAXI arburst
2:0RW0x0rd_arsizeAXI arsize

cfg_dma_wr_cfg(DDMA: 0x8028 / SDMA: 0x9028)

AXI 写通道信号配置。

Bit属性复位值字段名描述
31:13RO0x0保留
12RW0x0wr_awlockAXI awlock
11:8RW0x0wr_awcacheAXI awcache
7:5RW0x0wr_awprotAXI awprot
4:3RW0x0wr_awburstAXI awburst
2:0RW0x0wr_awsizeAXI awsize

cfg_dma_wstrb(DDMA: 0x802C / SDMA: 0x902C)

AXI 写选通。

Bit属性复位值字段名描述
31:0RW0x0wr_wstrbAXI 写选通信号

cfg_status(DDMA: 0x8030 / SDMA: 0x9030)

DMA 状态。

Bit属性复位值字段名描述
31:9RO0x0保留
8RW0x0idel空闲状态
7:0RO0x0保留

dt_wr_amount(DDMA: 0x8034 / SDMA: 0x9034)

数据写入量统计。用于 ioctl GetDtWrAmount

Bit属性复位值字段名描述
31:0RO0x0dt_wr_amount数据写入量

dt_rd_amount(DDMA: 0x8038 / SDMA: 0x9038)

数据读取量统计。用于 ioctl GetDtRdAmount

Bit属性复位值字段名描述
31:0RO0x0dt_rd_amount数据读取量

wt_rd_amount(DDMA: 0x803C / SDMA: 0x903C)

权重读取量统计。用于 ioctl GetWtRdAmount

Bit属性复位值字段名描述
31:0RO0x0wt_rd_amount权重读取量

清除操作:写 rd_weight_1(0x8010) 值 0x80000101 再写 0x00000101(两次写入进行 latch/clear)。

GLOBAL 寄存器块(全局使能)

基址CORE_BASE + 0xF000 | 地址范围0xF000 ~ 0xFFFF

来源:RK3588 TRM §36.4.3 Detail Registers Description

GLOBAL 模块仅包含一个寄存器,用于一次性组合使能各功能模块的操作。


RKNN_global_operation_enable(0xF008)

组合操作使能:一次写入同时触发多个模块开始执行。

Bit属性复位值字段名描述
31:7RO0x0保留
6RW0x0ppu_rdma_op_enPPU_RDMA 操作使能
5RW0x0ppu_op_enPPU 操作使能
4RW0x0dpu_rdma_op_enDPU_RDMA 操作使能
3RW0x0dpu_op_enDPU 操作使能
2RW0x0core_op_enCORE 操作使能
1RO0x0保留
0RW0x0cna_op_enCNA 操作使能

IOCTL 协议与数据结构

本章整理 RKNPU 驱动的全部 ioctl 命令、传参结构体布局、flags 枚举,以及 mmap(offset) 编码规则。


1. IOCTL 命令表

RKNPU 驱动提供 6 个 ioctl 命令,同时定义了两套编号:

命令编号DRM 路径宏非 DRM 路径宏方向结构体功能
ACTION0x00 DRM_IOCTL_RKNPU_ACTION IOCTL_RKNPU_ACTION IOWR rknpu_action 查询/设置:版本、频率、电压、电源、统计、复位等
SUBMIT0x01 DRM_IOCTL_RKNPU_SUBMIT IOCTL_RKNPU_SUBMIT IOWR rknpu_submit 提交 NPU 任务(PC job)
MEM_CREATE0x02 DRM_IOCTL_RKNPU_MEM_CREATE IOCTL_RKNPU_MEM_CREATE IOWR rknpu_mem_create 分配 DMA buffer(GEM 对象)
MEM_MAP0x03 DRM_IOCTL_RKNPU_MEM_MAP IOCTL_RKNPU_MEM_MAP IOWR rknpu_mem_map 获取 mmap 用的 fake offset
MEM_DESTROY0x04 DRM_IOCTL_RKNPU_MEM_DESTROY IOCTL_RKNPU_MEM_DESTROY IOWR rknpu_mem_destroy 释放 DMA buffer
MEM_SYNC0x05 DRM_IOCTL_RKNPU_MEM_SYNC IOCTL_RKNPU_MEM_SYNC IOWR rknpu_mem_sync Cache 同步(flush / invalidate)

1.1 两套编号的区别

DRM 路径非 DRM 路径
编码方式 DRM_IOWR(DRM_COMMAND_BASE + nr, type) _IOWR('r', nr, type)
设备节点 /dev/dri/cardX /dev/rknpu(如果驱动注册了)
调用方式 drmIoctl(fd, DRM_IOCTL_RKNPU_*, &arg) ioctl(fd, IOCTL_RKNPU_*, &arg)
闭源库使用 librknnrt.so 主要走此路径 备选

rknpu-ioctl.hRKNPU_IOC_MAGIC = 'r'DRM_COMMAND_BASE 来自 <libdrm/drm.h>(通常为 0x40)。


2. 结构体布局

2.1 struct rknpu_action(8 字节)

用于 ACTION ioctl,查询/设置各种属性。

偏移大小类型字段说明
0x004__u32flagsAction 编号(见 §3.4)
0x044__u32valueGET 时为返回值,SET 时为输入值

2.2 struct rknpu_mem_create(40 字节)

偏移大小类型字段方向说明
0x004__u32handleOUTGEM 对象句柄(驱动分配)
0x044__u32flagsIN内存类型 flags(见 §3.1)
0x088__u64sizeIN申请大小(内部页对齐)
0x108__u64obj_addrOUT内核对象地址(opaque token)
0x188__u64dma_addrOUT设备侧 DMA 地址(IOVA 或物理地址)
0x208__u64sram_sizeIN可选 SRAM 申请大小

StarryOS Rust 驱动 扩展了额外字段:iommu_domain_id: i32core_mask: u32,用于多核/IOMMU 域管理。

2.3 struct rknpu_mem_map(16 字节)

偏移大小类型字段方向说明
0x004__u32handleINGEM 句柄
0x044__u32reserved64 位对齐填充
0x088__u64offsetOUTfake offset,用于 mmap(fd, ..., offset)

2.4 struct rknpu_mem_destroy(16 字节)

偏移大小类型字段方向说明
0x004__u32handleINGEM 句柄
0x044__u32reserved填充
0x088__u64obj_addrIN内核对象地址

2.5 struct rknpu_mem_sync(32 字节)

偏移大小类型字段方向说明
0x004__u32flagsIN同步方向(见 §3.2)
0x044__u32reserved填充
0x088__u64obj_addrIN要同步的内核对象地址
0x108__u64offsetIN同步起始偏移(字节)
0x188__u64sizeIN同步区域大小

2.6 struct rknpu_task(36 字节,__packed

单个任务描述,由 PC 命令流引擎解释执行。

偏移大小类型字段说明
0x004__u32flags任务标志
0x044__u32op_idx算子索引
0x084__u32enable_mask模块使能掩码
0x0C4__u32int_mask期望的完成中断掩码
0x104__u32int_clear中断清除值
0x144__u32int_status完成后驱动写回实际中断状态
0x184__u32regcfg_amount寄存器配置项数量
0x1C4__u32regcfg_offset寄存器配置在命令流中的偏移
0x208__u64regcmd_addr寄存器命令流 DMA 地址

__packed:编译器不插入 padding,保证与内核侧布局一致。

2.7 struct rknpu_subcore_task(8 字节)

偏移大小类型字段说明
0x004__u32task_start该子核心的起始 task 索引
0x044__u32task_number该子核心要执行的 task 数量

2.8 struct rknpu_submit(96 字节)

任务提交主结构体,驱动最核心的入口。

偏移大小类型字段说明
0x004__u32flagsJob 模式(见 §3.3)
0x044__u32timeout超时时间(ms)
0x084__u32task_start全局起始 task 索引
0x0C4__u32task_number全局 task 数量
0x104__u32task_counter计数/序列号
0x144__s32priority调度优先级
0x188__u64task_obj_addrtask 数组的内核对象地址
0x208__u64regcfg_obj_addr寄存器配置对象地址
0x288__u64task_base_addrtask 基址(设备侧)
0x308__u64user_data可选用户数据(透传/调试)
0x384__u32core_mask核心选择掩码(bit0=core0, bit1=core1, bit2=core2)
0x3C4__s32fence_fddma-fence fd(IN: fence_in, OUT: fence_out)
0x4040subcore_task[5]subcore_task5 组子核心任务分配

StarryOS Rust 驱动 变体:将 regcfg_obj_addr 替换为 iommu_domain_id + reserved,并增加 hw_elapse_time 字段。


3. Flags 枚举

3.1 enum e_rknpu_mem_type(内存类型,位掩码)

名称说明
0 << 0CONTIGUOUS物理连续(默认)
1 << 0NON_CONTIGUOUS物理不连续
0 << 1NON_CACHEABLE不可缓存(默认)
1 << 1CACHEABLE可缓存
1 << 2WRITE_COMBINEWrite-Combine 映射
1 << 3KERNEL_MAPPING内核态映射
1 << 4IOMMUIOMMU 映射
1 << 5ZEROING分配后清零
1 << 6SECURE安全内存
1 << 7NON_DMA32不限于 DMA32 区域
1 << 8TRY_ALLOC_SRAM尝试分配 SRAM

MEM_MASK = 0x1FF(bit0~bit8 的合法组合集)

3.2 enum e_rknpu_mem_sync_mode(Cache 同步方向)

名称说明
1 << 0SYNC_TO_DEVICECPU → 设备:flush cache
1 << 1SYNC_FROM_DEVICE设备 → CPU:invalidate cache

3.3 enum e_rknpu_job_mode(Job 模式,位掩码)

名称说明
0 << 0SLAVE从模式(默认)
1 << 0PCPC(Program Counter)模式
0 << 1BLOCK阻塞等待完成(默认)
1 << 1NONBLOCK非阻塞返回
1 << 2PINGPONG双缓冲/流水模式
1 << 3FENCE_IN等待输入 fence
1 << 4FENCE_OUT返回输出 fence fd

3.4 enum e_rknpu_action(Action 编号)

编号名称方向说明
0GET_HW_VERSIONGET读取硬件版本
1GET_DRV_VERSIONGET读取驱动版本(编码:major*10000+minor*100+patch)
2GET_FREQGET读取当前频率
3SET_FREQSET设置频率
4GET_VOLTGET读取电压
5SET_VOLTSET设置电压
6ACT_RESETACTNPU 软复位
7GET_BW_PRIORITYGET带宽优先级
8SET_BW_PRIORITYSET设置带宽优先级
9GET_BW_EXPECTGET期望带宽
10SET_BW_EXPECTSET设置期望带宽
11GET_BW_TWGET带宽时间窗
12SET_BW_TWSET设置带宽时间窗
13ACT_CLR_TOTAL_RW_AMOUNTACT清除读写量统计
14GET_DT_WR_AMOUNTGET数据写入量
15GET_DT_RD_AMOUNTGET数据读取量
16GET_WT_RD_AMOUNTGET权重读取量
17GET_TOTAL_RW_AMOUNTGET总读写量
18GET_IOMMU_ENGETIOMMU 是否启用
19SET_PROC_NICESET进程优先级
20POWER_ONACTNPU 上电
21POWER_OFFACTNPU 下电
22GET_TOTAL_SRAM_SIZEGETSRAM 总大小
23GET_FREE_SRAM_SIZEGETSRAM 空闲大小
24GET_IOMMU_DOMAIN_IDGETStarryOS 扩展
25SET_IOMMU_DOMAIN_IDSETStarryOS 扩展

4. mmap 规则(handle → offset 编码)

4.1 流程

sequenceDiagram
    participant U as 用户态
    participant K as 内核驱动

    U->>K: ioctl(MEM_CREATE, {size, flags})
    K-->>U: {handle, obj_addr, dma_addr}

    U->>K: ioctl(MEM_MAP, {handle})
    K-->>U: {offset}

    U->>U: ptr = mmap(fd, size, PROT_RW, MAP_SHARED, fd, offset)
    Note over U: 现在 ptr 指向 DMA buffer 的用户态映射

    U->>K: ioctl(MEM_SYNC, {obj_addr, offset=0, size, flags=TO_DEVICE})
    Note over K: flush CPU cache → 设备可见

    Note over U,K: ... 使用 buffer(填充数据 / 读取结果)...

    U->>K: ioctl(MEM_DESTROY, {handle, obj_addr})
    Note over K: 释放 GEM 对象 + DMA 内存

4.2 offset 编码约定

Linux rknpu 驱动 中,MEM_MAP 返回的 offset 是 DRM GEM 的 fake offset

  • drm_gem_create_mmap_offset() 生成
  • 编码方式:offset = handle_to_node_offset(DRM 内部维护的映射表)
  • 用户态拿到 offset 后,传给 mmap() 的最后一个参数
  • 内核在 drm_gem_mmap() 中根据 offset 查找对应的 GEM 对象,建立页表映射

StarryOS 实现:由于没有完整 DRM 框架,需要自行维护 handle → 物理页的映射表,并在 sys_mmapDeviceMmap::Physical 路径中完成映射。

4.3 对齐要求

  • size:内部页对齐(4KB)
  • offset:必须页对齐
  • dma_addr:取决于是否启用 IOMMU
    • 有 IOMMU:返回 IOVA(设备虚拟地址)
    • 无 IOMMU:返回物理地址(需物理连续)

当前提交-IRQ 边界快照恢复系统

当前 NPU 队列调度器配合说明

这份文档讲的是当前这版 NPU 调度链路到底怎么配合。重点不是复盘历史方案,而是把已经落到代码里的职责边界讲清楚:card1 做什么,StarryOS 里的 scheduler 做什么,驱动和 IRQ 路径现在只剩下什么责任,以及一次 Submit 是怎么从入队一路走到返回用户态的。

这版实现保留了一个很明确的核心模型:

  • 对外还是阻塞 Submit ioctl
  • 对内是一整次 submit 入队
  • 真正下发给硬件时是 per-core、per-task streaming dispatch
  • 每个 submit 自己有 waiter
  • 全局只有一个 kick,用来唤醒 worker
  • in-flight dispatch 的 owner 只保留在 scheduler 一处

所以从用户态看,语义没变;从内核实现看,控制流已经不是“调用线程自己 loop 推完整次 submit”了,而是“调用线程阻塞,后台 worker 负责推进和补发”。

1. 先说结论

当前链路可以概括成四句话:

  1. 用户线程发起 Submitcard1 先把 RknpuSubmitRknpuTask[] 拷到内核 shadow。
  2. 整次 submit 作为一个队列项入队,但真正发给硬件时是按 core、按 task 一次一次地下发。
  3. 哪个 core 先完成,就先 harvest 哪个 core,再尽快给它补下一个可运行 task。
  4. 原始提交线程睡在自己的 waiter 上,等 submit 进入 terminal 状态后被唤醒,再把最终结果拷回用户态。

这就是“阻塞 submit + 内部异步调度”的准确含义。

2. 为什么要收成现在这个形状

之前的问题主要有两个。

第一个问题是职责缠在一起。card1 既处理 ioctl,又自己循环推进 task,还要主动 yield_now() 给别的执行流机会。路径长,问题一多就很难沿着职责边界查。

第二个问题是内部状态太散。一份 submit 的信息会在 card1 / scheduler / queue / driver 之间换着名字存几次;in-flight dispatch 也会在 queue 和 driver 两边都留一份反向绑定,最后软件模型比真正需要的边界还重。

所以现在的收口原则很直接:

  • RknpuSubmit 只留在 ioctl 边界和最终 copy-back 边界
  • queue 只维护“这个 submit 还剩什么能调度”
  • scheduler 独占 in-flight dispatch ownership
  • driver / IRQ 只发布 per-core 原始完成状态

3. 各层现在分别负责什么

3.1 card1:阻塞 ioctl 入口

card1 现在只做三件事:

  1. 从用户态拷贝 RknpuSubmitRknpuTask[]
  2. 构造 RknpuQueuedSubmit 并调用 enqueue_submit(...)
  3. 等待完成,取回 CompletedSubmit,再统一拷回用户态

它不再自己步进调度,也不再负责保存任何调度期内部状态。它只保留阻塞设备调用入口该有的职责。

3.2 NpuScheduler:真正的调度器

StarryOS 里的 NpuScheduler 是当前链路的控制中心。它手里有四类核心状态:

  • queue: RknpuTaskQueue
  • waiters: BTreeMap<RknpuQueueTaskId, Arc<NpuSubmitWaiter>>
  • inflight: [Option<InflightDispatch>; NPU_MAX_CORES]
  • kick: Event

分工很明确:

  • queue 管 submit 级进度
  • waiters 管阻塞线程的睡眠和唤醒
  • inflight 管当前每个硬件 core 在跑哪一个 dispatch
  • kick 只负责把 worker 从睡眠态唤醒

也就是说,waiter 是 per-submit 的,kick 是全局的;二者分别解决“谁该睡”和“worker 什么时候该起来干活”两个完全不同的问题。

3.3 drivers/rknpu:单次硬件下发原语

驱动层现在退回到最小硬件编程层。它保留的核心原语就是:

  • submit_ioctrl_step(...)
  • harvest_completed_dispatches()

其中:

  • submit_ioctrl_step(...) 只负责把一个 task 发给一个 core
  • harvest_completed_dispatches() 只负责把 IRQ top-half 已经发布好的 raw completion 收回来

驱动不再负责维护完整 submit 生命周期,也不再保存 queue 语义的绑定关系。

3.4 IRQ top-half:只发布 raw per-core completion

IRQ handler 现在只做最小动作:

  • 读取硬件 IRQ 状态
  • fuzz status
  • 清硬件中断
  • 把结果按 core 写入共享原子

它不直接继续调度,也不持有 queue 语义的数据结构。这样 top-half 足够短,行为也足够稳定。

4. 内部数据模型怎么收口

这版重构的核心不是“加了多少新类型”,而是把每个类型的边界收窄了。

4.1 RknpuSubmit 只留在边界

RknpuSubmit 现在只用于两处:

  • ioctl 入口时承接用户态 header
  • submit 终态时重新组装 copy-back 给用户态

调度期间不再把它当成一个全程可变的内部状态容器到处传、到处改。

4.2 SubmitMeta 是内部不可变输入视图

调度期真正使用的是 SubmitMeta。它只保留调度所需的固定字段:

  • flags
  • priority
  • core_mask
  • task_dma_base
  • lane_ranges
  • task_total

lane_ranges 在入队时就做一次归一化:

  • 如果 subcore_task[] 全空,就默认变成 slot0 = [0, task_total)
  • 否则只按已有非空 lane 解释

这样后面的 queue / scheduler 不需要再在运行期反复猜 lane layout。

4.3 CompletedSubmit 是 terminal 返回模型

调度器在 terminal 时返回的不是整个 queue entry,而是一个更明确的结果模型:

  • submit: RknpuSubmit
  • tasks: Vec<RknpuTask>
  • last_error: Option<RknpuError>

这个边界很重要,因为 card1 真正需要的只有这三样,不需要知道 queue 内部状态机细节。

4.4 InflightDispatch 是唯一的 in-flight owner

每个正在硬件上飞的 dispatch 现在只在 scheduler 的 inflight[core] 里有一份记录,里面保存:

  • queue_task_id
  • core_slot
  • subcore_slot
  • task_index
  • task_ptr
  • expected_irq_mask

这份记录回答的就是一个问题:

“这个 core 当前跑的是哪个 submit 的哪个 task;如果 completion 回来,我该把结果记到哪里。”

queue 不再保存 per-core 反向绑定,driver 也不再保存 queue-facing 绑定。

4.5 CoreCompletion 是 driver 向 scheduler 提交的最小完成记录

driver harvest 之后只返回:

  • core_slot
  • observed_irq_status

scheduler 拿到它以后,再结合 InflightDispatch.expected_irq_mask 计算:

  • last_task_int_status
  • task_error

然后推进 queue 状态机。这一步把“硬件观测”与“队列语义”干净地分开了。

5. queue 是怎么维护游标和分发任务的

这部分是当前调度模型的核心。

5.1 queue 维护的是 submit 级进度,不是 per-core 绑定

RknpuQueueTask 里真正和调度推进相关的字段只有这些:

  • meta
  • tasks
  • subcore_cursors
  • subcore_running_mask
  • completed_task_count
  • inflight_core_mask
  • last_error
  • ready_queued

语义分别是:

  • subcore_cursors:每个逻辑 lane 已经推进到该 lane 的第几个 task
  • subcore_running_mask:某个 lane 当前是否已经有 task 在飞
  • completed_task_count:这次 submit 已经完成了多少 task
  • inflight_core_mask:当前有哪些物理 core 正在执行这个 submit
  • ready_queued:这个 submit 当前是否已经挂在 ready 队列里,避免重复入队

5.2 reserve_next_dispatch() 只产出最小 reservation token

queue 不再给 scheduler 一整份“大计划”,而是一次只给一个最小 reservation:

  • queue_task_id
  • subcore_slot
  • task_index

这意味着 queue 只负责说“下一个可以发的是谁”,不负责持有这个 dispatch 之后在 driver 侧的绑定信息。

5.3 游标推进规则

worker 调度时,queue 会按下面的规则挑任务:

  1. 先按 priority 从 ready 队列里挑 submit
  2. 再在这个 submit 内部按 subcore_slot 扫描可运行 lane
  3. 跳过已经在飞的 lane
  4. subcore_cursors[slot] 算出当前 lane 的下一个 task_index
  5. 成功 reservation 后,设置:
    • subcore_running_mask
    • inflight_core_mask

completion 回来以后,再清掉对应 bit,并把该 lane 的 cursor 加一。

5.4 为什么同一个 submit 能同时占多个 core

因为 subcore_running_mask 限制的是“同一个 lane 不能并发重复下发”,不是“同一个 submit 只能占一个 core”。

所以只要:

  • 这个 submit 还有别的 lane 可跑
  • 目标 core 在 core_mask 允许范围内
  • 当前有空闲 core

那么同一个 submit 完全可以在一轮里占多个 core。

5.5 为什么 faulted submit 还要等 inflight core 排空

Faulted 不等于立即 terminal。

当前实现里,一个 submit 即便已经 faulted,只要 inflight_core_mask != 0,它还不能算 terminal。因为还有别的 core 上的 in-flight task 没收干净,waiter 这时不能提前醒。

所以 terminal 的判定是:

  • Completed
  • 或者 Faulted && inflight_core_mask == 0

6. 一次完整的 Submit 现在怎么走

第 1 步:用户态发起 ioctl

用户态传入 RknpuSubmitRknpuTask[]card1 把它们拷进内核 shadow。

第 2 步:构造 RknpuQueuedSubmit

card1 把原始 submit 拆成两部分:

  • 边界专用的 RknpuSubmit
  • 调度专用的 SubmitMeta + Vec<RknpuTask>

然后包装成 RknpuQueuedSubmit 入队。

第 3 步:提交线程阻塞

enqueue_submit(...) 为这个 submit 建立自己的 NpuSubmitWaiter。随后提交线程执行 wait_for_submit(...),睡到 waiter 的 WaitQueue 上。

第 4 步:worker 被 kick 唤醒

新 submit 入队后,scheduler 调一次 kick.notify_relaxed(1)。worker 如果在睡眠,就会被唤醒开始工作。

这里的 kick 不是“某个 submit 的完成事件”,它只是一个全局“有活了,起来看队列”的唤醒信号。

第 5 步:worker 给空闲 core 分发任务

worker 会循环做两件事:

  • harvest_completed_cores()
  • dispatch_idle_cores()

在 dispatch 路径里,它会:

  1. 找一个当前没有 inflight 的 core
  2. 让 queue 给出一个 reservation token
  3. 从 queue task 里取出对应 task,组装 InflightDispatch
  4. 先写入 state.inflight[core]
  5. 如果这是本轮第一次给这个 submit 发任务,就做一次 confirm_write_all()
  6. 调用 driver 的 submit_ioctrl_step(...)

如果 driver dispatch 失败,就回滚 state.inflight[core] 和 queue reservation。

第 6 步:IRQ 到来,只发布原始完成状态

哪个 core 先完成,就哪个 core 的 top-half 先把 observed_irq_status 发布出来。此时不会直接继续调度。

第 7 步:worker harvest completion

worker 下次循环里会:

  1. 从 driver 拿到 CoreCompletion
  2. 通过 state.inflight[core] 找到对应 InflightDispatch
  3. 计算:
    • last_task_int_status = observed_irq_status & expected_irq_mask
    • task_error
  4. 回写 task.int_status
  5. 调用 queue 的 complete_dispatch(...) 推进 submit 状态

如果 submit 已经 terminal,就把 task_id 记到 terminal 列表里。

第 8 步:terminal path 唤醒 waiter

terminal path 里,scheduler 会:

  • 先做一次 prepare_read_all()
  • 然后唤醒该 submit 对应的 waiter

提交线程被唤醒后,再从 scheduler 取回 CompletedSubmit,并把最终的 task 数组和 submit header 一起拷回用户态。

7. 现在比旧模型少了什么

少掉的其实就是这次刻意拿掉的那些冗余状态。

7.1 不再有 submit 信息的多份影子副本

现在不会再在内部反复保存这些重复字段:

  • task_dma_addrsubmit.task_base_addr 的双存
  • submit.task_obj_addr 的内部影子
  • submit.task_counter / submit.hw_elapse_time 的调度期原地写回版本
  • queue 上额外存一份 priority

这些都被收回到更单一的边界里了。

7.2 不再有 queue 和 driver 的双重反向绑定

以前 queue 和 driver 都想回答“这个 core 当前到底在跑谁”。现在只有 scheduler 的 InflightDispatch 回答这个问题。

7.3 不再有只负责搬字段的 helper

像旧的 submit view 刷新、ready bit 翻转、active core 反查这类 helper,现在都不需要了。保留下来的 helper 都是状态机规则本身的一部分。

8. 当前实现里还需要注意的点

8.1 worker 还不是 IRQ 直接唤醒

当前 worker 在有 inflight core 时,还是通过轻量 yield_now() 轮询 harvest,而不是让 IRQ 直接把 worker 唤醒。

这不影响语义正确性,但如果以后继续优化内核态延迟,这里还有继续收紧的空间。

8.2 confirm_write_all() / prepare_read_all() 还是全局同步

现在还是按 GEM 池全局同步,不是按单 submit 精细同步。这版先保证路径和 ownership 干净,性能细化可以后面再做。

8.3 当前切换边界仍然是 task 完成后的 IRQ 边界

这版已经是多 submit、per-core streaming dispatch,但它仍然不是“任意时刻抢占正在执行的 NPU 指令流”。真正稳定的切换点还是 task 完成后的 IRQ 边界。

9. 队列相关结构体作用

这一节单独列一下和 queue / scheduler 直接相关的结构体,后面查代码时可以快速对照。

9.1 SubmitMeta

作用:submit 的内部不可变调度视图。

它只保留调度期真正要用的元数据:flag、priority、core_mask、DMA base、lane_ranges、task_total。它的意义是把 RknpuSubmit 从“内部运行时状态对象”降回“边界协议头”。

9.2 RknpuQueuedSubmit

作用:从 ioctl 边界进入 queue 时的输入容器。

它持有:

  • meta
  • reply-only 的 submit 字段
  • shadow tasks

它只活在“刚入队”这道边界上,目的就是把边界输入整理成 queue 可以直接接管的形式。

9.3 RknpuQueueTask

作用:一个已经入队、正在被调度器管理的 submit 实体。

它维护 submit 的生命周期状态、lane cursor、running mask、完成计数、错误状态,以及最终把终态重新组装回 RknpuSubmit 所需的 reply 信息。

9.4 RknpuTaskQueue

作用:scheduler 内部的 submit 容器和 ready 选择器。

它维护:

  • 全部 queue task 总表
  • 按 priority 分桶的 ready 队列
  • 下一个 queue task id

它只做 submit 选择和 reservation,不做 per-core in-flight ownership。

9.5 RknpuDispatchReservation

作用:queue 交给 scheduler 的最小“可发任务 token”。

它只说明:

  • 哪个 submit
  • 哪个 lane
  • 哪个 task_index

不带 driver 绑定,不带 task 指针,不带 IRQ 期望值。

9.6 InflightDispatch

作用:scheduler 独占的 per-core in-flight 记录。

它把 reservation 扩成真正能完成一次硬件 dispatch 和一次 completion 匹配所需的最小信息,包括 task 指针和 expected IRQ mask。

9.7 CoreCompletion

作用:driver 向 scheduler 返回的最小 raw completion。

它只表达“哪个 core 观察到了什么 IRQ 状态”,不掺杂 queue 语义。

9.8 CompletedSubmit

作用:terminal submit 返回给 card1 的结果模型。

它把 scheduler 内部状态机收敛成用户态真正需要的三样结果:

  • 最终 submit
  • 最终 tasks
  • 最终 last_error

9.9 NpuSubmitWaiter

作用:一个 submit 对应一个阻塞原语。

原始 ioctl 线程就睡在这里,直到 terminal path 把它唤醒。它解决的是“这个 submit 的提交线程什么时候返回”。

9.10 kick: Event

作用:全局 worker 唤醒器。

它不对应某个 submit,也不携带完成结果。它只负责在“有新活可做”时把 worker 从睡眠态叫起来。

10. 用一句话收尾

这版实现真正做成的事情其实很具体:把 Submit 从“调用线程自己 loop 推完整次 submit 的独占路径”,改成了“对外阻塞、对内由 queue + worker 按 core streaming dispatch 推进”的模型。queue 只管 submit 进度,scheduler 独占 in-flight ownership,driver 和 IRQ 只保留原始硬件完成边界,最终结果再由原始提交线程统一回填用户态。

RKNN 用户库手册

概述

librknnrt.so 是 Rockchip 提供的闭源 NPU 运行时库,负责将神经网络模型编译为硬件命令流并提交给内核驱动执行。它是用户态与 NPU 硬件之间的桥梁。

架构层次

┌─────────────────────────────────────────────────┐
│              用户应用程序                          │
├─────────────────────────────────────────────────┤
│  rknn_api.h          │  rknn_matmul_api.h       │  ← 闭源库公开头文件
├──────────────────────┼──────────────────────────┤
│  librknnrt.so(闭源)                             │
│  ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│  │模型解析器 │ │图优化/编译│ │命令流生成 + Task │ │
│  └──────────┘ └──────────┘ └──────────────────┘ │
├─────────────────────────────────────────────────┤
│  DRM ioctl (DRM_IOCTL_RKNPU_*)                  │  ← 开源 ABI
├─────────────────────────────────────────────────┤
│  rknpu 内核驱动(开源 GPL)                       │
│  任务调度 │ GEM 内存 │ 中断处理 │ PC 寄存器写入   │
├─────────────────────────────────────────────────┤
│  NPU 硬件(CNA / CORE / DPU / PPU)              │
└─────────────────────────────────────────────────┘

两条 NPU 使用路径

路径依赖操作方式代表项目
闭源 API 路径librknnrt.sorknn_init → rknn_run → rknn_outputs_getyolov8 demo
裸 ioctl 路径libdrm + 内核驱动用户自行构造命令流 + DRM_IOCTL_RKNPU_SUBMITnpu_benchmark / npu_llama

闭源库封装了命令流生成、内存规划、多核调度等全部复杂逻辑;裸 ioctl 路径则要求用户自己实现这些。

闭源库提供的三套 API

头文件功能说明
rknn_api.h模型推理加载 .rknn 模型 → 设置输入 → 推理 → 获取输出
rknn_matmul_api.h矩阵乘法加速独立于模型推理的通用 matmul 接口
rknn_custom_op.h自定义算子注册用户实现的 CPU/GPU 算子回调

闭源库二进制

仓库中 test/starrynpu/demo/yolov8/3rdparty/ 包含实际二进制:

文件说明
rknpu2/Linux/aarch64/librknnrt.soRKNPU2 闭源运行时(当前版本)
rknpu1/Linux/aarch64/librknn_api.soRKNPU1 旧版 API 库
rknpu2/include/rknn_api.h推理 API 头文件(805 行)
rknpu2/include/rknn_matmul_api.hMatmul API 头文件(544 行)
rknpu2/include/rknn_custom_op.h自定义算子 API 头文件(145 行)

文档结构

推理 API 参考(rknn_api.h)

来源:test/starrynpu/demo/yolov8/3rdparty/rknpu2/include/rknn_api.h 以下资料均来着StarryOS或社区收集。大多数暂未经过验证!。


一、类型定义

1.1 上下文句柄

#ifdef __arm__
typedef uint32_t rknn_context;
#else
typedef uint64_t rknn_context;

所有 API 围绕 rknn_context 句柄操作,内部指向闭源库维护的不透明状态。

1.2 错误码

常量说明
RKNN_SUCC0成功
RKNN_ERR_FAIL-1通用失败
RKNN_ERR_TIMEOUT-2执行超时
RKNN_ERR_DEVICE_UNAVAILABLE-3设备不可用
RKNN_ERR_MALLOC_FAIL-4内存分配失败
RKNN_ERR_PARAM_INVALID-5参数无效
RKNN_ERR_MODEL_INVALID-6模型无效
RKNN_ERR_CTX_INVALID-7上下文无效
RKNN_ERR_INPUT_INVALID-8输入无效
RKNN_ERR_OUTPUT_INVALID-9输出无效
RKNN_ERR_DEVICE_UNMATCH-10设备不匹配(需更新 SDK/驱动)
RKNN_ERR_INCOMPATILE_PRE_COMPILE_MODEL-11预编译模型不兼容
RKNN_ERR_INCOMPATILE_OPTIMIZATION_LEVEL_VERSION-12优化级别版本不兼容
RKNN_ERR_TARGET_PLATFORM_UNMATCH-13目标平台不匹配

1.3 张量类型

typedef enum _rknn_tensor_type {
    RKNN_TENSOR_FLOAT32 = 0,
    RKNN_TENSOR_FLOAT16,      // 1
    RKNN_TENSOR_INT8,         // 2
    RKNN_TENSOR_UINT8,        // 3
    RKNN_TENSOR_INT16,        // 4
    RKNN_TENSOR_UINT16,       // 5
    RKNN_TENSOR_INT32,        // 6
    RKNN_TENSOR_UINT32,       // 7
    RKNN_TENSOR_INT64,        // 8
    RKNN_TENSOR_BOOL,         // 9
    RKNN_TENSOR_INT4,         // 10
    RKNN_TENSOR_BFLOAT16,     // 11
} rknn_tensor_type;

1.4 量化类型

typedef enum _rknn_tensor_qnt_type {
    RKNN_TENSOR_QNT_NONE = 0,              // 无量化
    RKNN_TENSOR_QNT_DFP,                   // 动态定点(fractional length)
    RKNN_TENSOR_QNT_AFFINE_ASYMMETRIC,     // 非对称仿射(zero_point + scale)
} rknn_tensor_qnt_type;

1.5 数据格式

typedef enum _rknn_tensor_format {
    RKNN_TENSOR_NCHW = 0,
    RKNN_TENSOR_NHWC,
    RKNN_TENSOR_NC1HWC2,    // NPU 原生格式
    RKNN_TENSOR_UNDEFINED,
} rknn_tensor_format;

1.6 核心掩码

typedef enum _rknn_core_mask {
    RKNN_NPU_CORE_AUTO   = 0,       // 自动选择
    RKNN_NPU_CORE_0      = 1,       // 核心 0
    RKNN_NPU_CORE_1      = 2,       // 核心 1
    RKNN_NPU_CORE_2      = 4,       // 核心 2
    RKNN_NPU_CORE_0_1    = 3,       // 核心 0+1 联合
    RKNN_NPU_CORE_0_1_2  = 7,       // 三核联合
    RKNN_NPU_CORE_ALL    = 0xffff,  // 平台自动选择多核
} rknn_core_mask;

1.7 初始化标志

标志说明逆向推断的内部行为
RKNN_FLAG_PRIOR_HIGH0x0高优先级(默认)设置 nice -19
RKNN_FLAG_PRIOR_MEDIUM0x1中优先级
RKNN_FLAG_PRIOR_LOW0x2低优先级
RKNN_FLAG_ASYNC_MASK0x4异步模式rknn_outputs_get 返回上一帧结果
RKNN_FLAG_COLLECT_PERF_MASK0x8性能采集启用逐层计时,降低帧率
RKNN_FLAG_MEM_ALLOC_OUTSIDE0x10外部内存分配用户负责分配 weight/internal/IO 内存
RKNN_FLAG_SHARE_WEIGHT_MEM0x20权重共享多上下文共享同一权重内存
RKNN_FLAG_FENCE_IN_OUTSIDE0x40外部输入 fence传入 DMA fence fd
RKNN_FLAG_FENCE_OUT_OUTSIDE0x80外部输出 fence获取 DMA fence fd
RKNN_FLAG_COLLECT_MODEL_INFO_ONLY0x100仅采集模型信息不实际加载,仅查询 weight/internal 大小
RKNN_FLAG_INTERNAL_ALLOC_OUTSIDE0x200外部分配内部内存
RKNN_FLAG_EXECUTE_FALLBACK_PRIOR_DEVICE_GPU0x400GPU 回退NPU 不支持的算子回退 GPU(OpenCL)
RKNN_FLAG_ENABLE_SRAM0x800启用 SRAM尝试在 SRAM 分配缓冲
RKNN_FLAG_SHARE_SRAM0x1000共享 SRAM多上下文共享 SRAM
RKNN_FLAG_DISABLE_PROC_HIGH_PRIORITY0x2000禁用高优先级不设置 nice -19
RKNN_FLAG_DISABLE_FLUSH_INPUT_MEM_CACHE0x4000禁用输入 cache flush用户自行保证 cache 一致性
RKNN_FLAG_DISABLE_FLUSH_OUTPUT_MEM_CACHE0x8000禁用输出 cache flush输出由 GPU/RGA 消费时使用
RKNN_FLAG_MODEL_BUFFER_ZERO_COPY0x10000模型缓冲零拷贝模型数据由 NPU 直接访问

二、核心结构体

2.1 张量属性 rknn_tensor_attr

typedef struct _rknn_tensor_attr {
    uint32_t index;                        // 输入/输出索引
    uint32_t n_dims;                       // 维度数
    uint32_t dims[RKNN_MAX_DIMS];          // 维度数组(最多 16 维)
    char     name[RKNN_MAX_NAME_LEN];      // 张量名(最长 256)
    uint32_t n_elems;                      // 元素总数
    uint32_t size;                         // 字节大小
    rknn_tensor_format fmt;                // 数据格式(NCHW/NHWC/NC1HWC2)
    rknn_tensor_type   type;               // 数据类型
    rknn_tensor_qnt_type qnt_type;         // 量化类型
    int8_t   fl;                           // DFP 小数位长度
    int32_t  zp;                           // 仿射量化零点
    float    scale;                        // 仿射量化缩放因子
    uint32_t w_stride;                     // 宽度方向步长(只读)
    uint32_t size_with_stride;             // 含步长的字节大小
    uint8_t  pass_through;                 // 直通模式标志
    uint32_t h_stride;                     // 高度方向步长(只写)
} rknn_tensor_attr;

2.2 张量内存 rknn_tensor_mem

typedef struct _rknn_tensor_memory {
    void*    virt_addr;     // 虚拟地址
    uint64_t phys_addr;     // 物理地址
    int32_t  fd;            // DMA buffer fd
    int32_t  offset;        // 内存偏移
    uint32_t size;          // 缓冲大小
    uint32_t flags;         // 标志
    void*    priv_data;     // 私有数据(闭源库内部使用)
} rknn_tensor_mem;

逆向推断priv_data 内部指向闭源库维护的 rknpu_mem_create 返回的 obj_addr,用于后续 mem_destroymem_sync 操作。

2.3 查询命令 rknn_query_cmd

命令返回结构体说明
RKNN_QUERY_IN_OUT_NUM0rknn_input_output_num输入/输出数量
RKNN_QUERY_INPUT_ATTR1rknn_tensor_attr输入张量属性
RKNN_QUERY_OUTPUT_ATTR2rknn_tensor_attr输出张量属性
RKNN_QUERY_PERF_DETAIL3rknn_perf_detail逐层性能(需 PERF_MASK)
RKNN_QUERY_PERF_RUN4rknn_perf_run推理总耗时
RKNN_QUERY_SDK_VERSION5rknn_sdk_versionSDK/驱动版本
RKNN_QUERY_MEM_SIZE6rknn_mem_size权重/内部内存大小
RKNN_QUERY_CUSTOM_STRING7rknn_custom_string自定义字符串
RKNN_QUERY_NATIVE_INPUT_ATTR8rknn_tensor_attr原生输入属性(NC1HWC2)
RKNN_QUERY_NATIVE_OUTPUT_ATTR9rknn_tensor_attr原生输出属性
RKNN_QUERY_DEVICE_MEM_INFO12设备内存信息
RKNN_QUERY_INPUT_DYNAMIC_RANGE13rknn_input_range动态 shape 范围
RKNN_QUERY_CURRENT_INPUT_ATTR14rknn_tensor_attr当前输入 shape(动态模型)
RKNN_QUERY_CURRENT_OUTPUT_ATTR15rknn_tensor_attr当前输出 shape(动态模型)

三、函数签名与逆向还原

3.1 生命周期管理

rknn_init

int rknn_init(rknn_context* context, void* model, uint32_t size, uint32_t flag, rknn_init_extend* extend);
参数说明
context[out] 上下文句柄指针
modelsize > 0 时为模型数据指针;size = 0 时为模型文件路径
size模型数据大小(0 表示从文件加载)
flag初始化标志组合(见 1.7)
extend扩展信息(可选,含 real_model_offsetmodel_buffer_fd 等)

逆向还原的内部流程

rknn_init()
├── 1. 打开 /dev/dri/card* 或 /dev/rknpu(获取 fd)
├── 2. ioctl(ACTION, RKNPU_GET_HW_VERSION) → 检查硬件版本
├── 3. 解析 .rknn 模型文件头
│   ├── 提取网络拓扑(层数、连接关系)
│   ├── 提取权重数据
│   └── 提取预编译命令流(如果是 pre-compile 模型)
├── 4. ioctl(MEM_CREATE) × N → 分配权重/内部/IO 内存
│   ├── 权重内存(RKNPU_MEM_KERNEL_MAPPING)
│   ├── 内部中间缓冲
│   └── 命令流缓冲(regcmd)
├── 5. ioctl(MEM_MAP) + mmap() → 映射到用户空间
├── 6. 将权重数据拷贝到 DMA 内存(转换为 native layout)
├── 7. 编译模型 → 生成寄存器命令流
│   ├── 为每层生成 NPUOP 指令序列
│   ├── 计算 CBUF bank 分配
│   └── 生成 Task 数组
├── 8. 如果 flag & RKNN_FLAG_PRIOR_HIGH → ioctl(ACTION, RKNPU_SET_PROC_NICE)
└── 9. 返回 context 句柄

rknn_destroy

int rknn_destroy(rknn_context context);

逆向还原

rknn_destroy()
├── 1. 释放所有 Task 数组内存
├── 2. munmap() + ioctl(MEM_DESTROY) × N → 释放所有 DMA 内存
├── 3. close(drm_fd)
└── 4. 释放上下文结构体

rknn_dup_context

int rknn_dup_context(rknn_context* context_in, rknn_context* context_out);

复制上下文,新上下文与原上下文共享权重内存(RKNN_FLAG_SHARE_WEIGHT_MEM 语义)。


3.2 推理流程

rknn_inputs_set

int rknn_inputs_set(rknn_context context, uint32_t n_inputs, rknn_input inputs[]);

逆向还原

rknn_inputs_set()
├── 1. 遍历 inputs[]
│   ├── 如果 pass_through == TRUE → 直接拷贝到输入 DMA 内存
│   └── 如果 pass_through == FALSE →
│       ├── 格式转换(NHWC → NCHW 或 NC1HWC2)
│       ├── 类型转换(FP32 → INT8/FP16,应用 scale/zp)
│       └── 拷贝到输入 DMA 内存
└── 2. ioctl(MEM_SYNC, SYNC_TO_DEVICE) → flush cache

rknn_run

int rknn_run(rknn_context context, rknn_run_extend* extend);
参数说明
extend->frame_id[out] 当前帧 ID
extend->non_block0=阻塞,1=非阻塞
extend->timeout_ms阻塞模式超时(毫秒)
extend->fence_fd外部 fence fd

逆向还原(核心路径):

rknn_run()
├── 1. 构造 rknpu_submit 结构体
│   ├── flags = RKNPU_JOB_PC | RKNPU_JOB_BLOCK(或 NONBLOCK)| RKNPU_JOB_PINGPONG
│   ├── task_obj_addr = tasks DMA 对象地址
│   ├── core_mask = 根据 rknn_set_core_mask() 设置
│   ├── subcore_task[] = 根据多核切分策略填充
│   └── timeout = extend->timeout_ms 或默认值
├── 2. ioctl(DRM_IOCTL_RKNPU_SUBMIT, &submit)
│   └── 内核驱动:
│       ├── 分配 job → 调度到核心
│       ├── 写 PC 寄存器(base_addr, amount, task_control, op_enable)
│       ├── 等待中断
│       └── 写回 task[].int_status
└── 3. 如果阻塞模式 → 等待 ioctl 返回
    如果非阻塞 → 立即返回,后续 rknn_wait() 或 rknn_outputs_get() 等待

rknn_wait

int rknn_wait(rknn_context context, rknn_run_extend* extend);

等待非阻塞推理完成。内部轮询或等待 fence 信号。

rknn_outputs_get

int rknn_outputs_get(rknn_context context, uint32_t n_outputs, rknn_output outputs[], rknn_output_extend* extend);

逆向还原

rknn_outputs_get()
├── 1. 如果异步模式 → 返回上一帧结果(不等待当前帧)
├── 2. ioctl(MEM_SYNC, SYNC_FROM_DEVICE) → invalidate output cache
├── 3. 遍历 outputs[]
│   ├── 如果 want_float == TRUE →
│   │   ├── 反量化(INT8 → FP32,应用 scale/zp)
│   │   └── 格式转换(NC1HWC2 → NCHW/NHWC)
│   ├── 如果 is_prealloc == TRUE → 拷贝到用户提供的 buf
│   └── 如果 is_prealloc == FALSE → 分配 buf 并拷贝
└── 4. extend->frame_id = 当前帧 ID

rknn_outputs_release

int rknn_outputs_release(rknn_context context, uint32_t n_ouputs, rknn_output outputs[]);

释放 rknn_outputs_getis_prealloc == FALSE 时分配的 buf。


3.3 查询接口

rknn_query

int rknn_query(rknn_context context, rknn_query_cmd cmd, void* info, uint32_t size);

通用查询接口。info 指向对应结构体,size 为结构体大小。

关键查询结构体

typedef struct _rknn_input_output_num {
    uint32_t n_input;
    uint32_t n_output;
} rknn_input_output_num;

typedef struct _rknn_perf_detail {
    char*    perf_data;      // 性能数据字符串(闭源库内部分配)
    uint64_t data_len;
} rknn_perf_detail;

typedef struct _rknn_perf_run {
    int64_t run_duration;    // 推理耗时(微秒)
} rknn_perf_run;

typedef struct _rknn_sdk_version {
    char api_version[256];
    char drv_version[256];
} rknn_sdk_version;

typedef struct _rknn_mem_size {
    uint32_t total_weight_size;
    uint32_t total_internal_size;
    uint64_t total_dma_allocated_size;
    uint32_t total_sram_size;
    uint32_t free_sram_size;
    uint32_t reserved[10];
} rknn_mem_size;

3.4 多核控制

rknn_set_core_mask

int rknn_set_core_mask(rknn_context context, rknn_core_mask core_mask);

逆向推断:设置后续 rknn_run 提交时 rknpu_submit.core_mask 的值。对于联合核心模式(CORE_0_1CORE_0_1_2),闭源库内部会将 Task 数组切分到 subcore_task[] 中。

rknn_set_batch_core_num

int rknn_set_batch_core_num(rknn_context context, int core_num);

设置批量推理时使用的核心数。


3.5 动态 Shape

rknn_set_input_shapes

int rknn_set_input_shapes(rknn_context ctx, uint32_t n_inputs, rknn_tensor_attr attr[]);

设置所有输入张量的 shape。仅对动态 shape 模型有效。调用后闭源库内部会重新编译命令流。

rknn_set_input_shape(已废弃)

int rknn_set_input_shape(rknn_context ctx, rknn_tensor_attr* attr);

3.6 扩展结构体

rknn_init_extend

typedef struct _rknn_init_extend {
    rknn_context ctx;
    int32_t      real_model_offset;    // 模型文件内偏移(零拷贝模式)
    uint32_t     real_model_size;      // 模型实际大小
    int32_t      model_buffer_fd;      // 模型缓冲 fd
    uint32_t     model_buffer_flags;   // 模型缓冲标志
    uint8_t      reserved[112];
} rknn_init_extend;

rknn_run_extend

typedef struct _rknn_run_extend {
    uint64_t frame_id;       // [out] 帧 ID
    int32_t  non_block;      // 0=阻塞,1=非阻塞
    int32_t  timeout_ms;     // 超时(毫秒)
    int32_t  fence_fd;       // 外部 fence fd
} rknn_run_extend;

rknn_output_extend

typedef struct _rknn_output_extend {
    uint64_t frame_id;       // [out] 输出对应的帧 ID
} rknn_output_extend;

矩阵乘法 API(rknn_matmul_api.h)

来源:test/starrynpu/demo/yolov8/3rdparty/rknpu2/include/rknn_matmul_api.h(544 行)

独立于模型推理的通用矩阵乘法加速接口。公式:C = A × B


一、支持的计算类型

typedef enum _rknn_matmul_type {
    RKNN_FLOAT16_MM_FLOAT16_TO_FLOAT32 = 1,   // FP16 × FP16 → FP32
    RKNN_INT8_MM_INT8_TO_INT32         = 2,   // INT8 × INT8 → INT32
    RKNN_INT8_MM_INT8_TO_INT8          = 3,   // INT8 × INT8 → INT8
    RKNN_FLOAT16_MM_FLOAT16_TO_FLOAT16 = 4,   // FP16 × FP16 → FP16
    RKNN_FLOAT16_MM_INT8_TO_FLOAT32    = 5,   // FP16 × INT8 → FP32
    RKNN_FLOAT16_MM_INT8_TO_FLOAT16    = 6,   // FP16 × INT8 → FP16
    RKNN_FLOAT16_MM_INT4_TO_FLOAT32    = 7,   // FP16 × INT4 → FP32
    RKNN_FLOAT16_MM_INT4_TO_FLOAT16    = 8,   // FP16 × INT4 → FP16
    RKNN_INT8_MM_INT8_TO_FLOAT32       = 9,   // INT8 × INT8 → FP32
    RKNN_INT4_MM_INT4_TO_INT16         = 10,  // INT4 × INT4 → INT16
    RKNN_INT8_MM_INT4_TO_INT32         = 11,  // INT8 × INT4 → INT32
    RKNN_FLOAT16_MM_INT4_TO_BFLOAT16   = 12,  // FP16 × INT4 → BF16
    RKNN_INT8_MM_INT4_TO_FLOAT16       = 15,  // INT8 × INT4 → FP16
} rknn_matmul_type;

二、对齐要求

RK3588 / RK3576

精度K 对齐N 对齐K 最大值
INT432 字节64 字节10240
INT832 字节32 字节10240
FP1632 字节16 字节10240

RK3566 / RK3568

精度K 对齐N 对齐
INT832 字节16 字节
FP1616 字节8 字节

RK3562

精度K 对齐N 对齐
INT832 字节16 字节
FP1632 字节8 字节

三、Native Layout 规范

硬件要求输入/输出数据按特定分块格式排列。使用 native layout 可避免闭源库内部的格式转换开销。

3.1 A 矩阵(M × K)

Normal layout(M, K) — 行主序。

Native layout(RK3588/3576)

精度布局说明
INT4(K/32, M, 32)每 32 个 K 元素为一组
INT8(K/16, M, 16)每 16 个 K 元素为一组
FP16(K/8, M, 8)每 8 个 K 元素为一组

示例(FP16):

[K1M1, K2M1, ..., K8M1,
 K1M2, K2M2, ..., K8M2,
 ...
 K(k-7)Mm, K(k-6)Mm, ..., KkMm]

3.2 B 矩阵(K × N)

Normal layout(K, N) — 行主序。

Native layout(RK3588/3576)

精度布局说明
INT4(N/64, K/32, 64, 32)64×32 分块
INT8(N/32, K/32, 32, 32)32×32 分块
FP16(N/16, K/32, 16, 32)16×32 分块

示例(INT8, RK3588):

[K1N1,  K2N1,  ..., K32N1,
 K1N2,  K2N2,  ..., K32N2,
 ...
 K1N32, K2N32, ..., K32N32,    ← 第一个 32×32 块
 K33N1, K34N1, ..., K64N1,
 ...
 K(k-31)N32, ..., KkN32,      ← 第二个 K 块
 K1N33, K2N33, ..., K32N33,    ← 第二个 N 块
 ...]

3.3 C 矩阵(M × N)

Normal layout(M, N) — 行主序。

Native layout

平台精度布局
通用通用(N/4, M, 4)
RK3588INT4(N/8, M, 8)

3.4 K 分段规则

当 K 超过硬件限制时,B 矩阵自动分段:

平台分段阈值分段数
RK3588K > 8192T = ceil(K / 8192)
RK3576K > 4096T = ceil(K / 4096)

分段后每段独立按 native layout 排列。推荐使用 rknn_B_normal_layout_to_native_layout() 进行自动转换。


四、函数签名

4.1 创建与销毁

rknn_matmul_create

int rknn_matmul_create(rknn_matmul_ctx* ctx, rknn_matmul_info* info, rknn_matmul_io_attr* io_attr);
参数说明
ctx[out] matmul 上下文句柄
info[in] matmul 配置(M/K/N、类型、布局、量化)
io_attr[out] 输入/输出属性(含实际维度和大小)

rknn_matmul_create_dynamic_shape

int rknn_matmul_create_dynamic_shape(rknn_matmul_ctx* ctx, rknn_matmul_info* info,
    int shape_num, rknn_matmul_shape dynamic_shapes[], rknn_matmul_io_attr io_attrs[]);

创建支持动态 M/K/N 的 matmul 上下文。info.M/K/N 无效,以 dynamic_shapes[] 为准。

rknn_matmul_destroy

int rknn_matmul_destroy(rknn_matmul_ctx ctx);

4.2 IO 绑定

rknn_matmul_set_io_mem

int rknn_matmul_set_io_mem(rknn_matmul_ctx ctx, rknn_tensor_mem* mem, rknn_matmul_tensor_attr* attr);

绑定 A/B/C 矩阵的内存。attr 来自 rknn_matmul_create 返回的 io_attr

4.3 执行

rknn_matmul_run

int rknn_matmul_run(rknn_matmul_ctx ctx);

阻塞执行矩阵乘法。

逆向推断的内部流程

rknn_matmul_run()
├── 1. 根据 M/K/N 和精度生成命令流
│   ├── 填充 CNA 描述符(卷积参数映射为 matmul)
│   ├── 填充 CORE 描述符
│   └── 填充 DPU 描述符(输出转换)
├── 2. 构造 rknpu_task 数组
│   ├── enable_mask = 0xd(CNA + CORE + DPU)
│   ├── int_mask = 0x300(DPU 完成中断)
│   └── regcmd_addr = 命令流 DMA 地址
├── 3. 构造 rknpu_submit
│   ├── flags = RKNPU_JOB_PC | RKNPU_JOB_BLOCK | RKNPU_JOB_PINGPONG
│   └── core_mask = 根据 rknn_matmul_set_core_mask() 设置
└── 4. ioctl(DRM_IOCTL_RKNPU_SUBMIT)

4.4 核心控制

rknn_matmul_set_core_mask

int rknn_matmul_set_core_mask(rknn_matmul_ctx context, rknn_core_mask core_mask);

4.5 量化参数

rknn_matmul_set_quant_params

int rknn_matmul_set_quant_params(rknn_matmul_ctx context, rknn_quant_params* params);

设置量化参数。仅支持 INT8_MM_INT8_TO_INT8INT8_MM_INT8_TO_INT32 类型。

rknn_matmul_get_quant_params

int rknn_matmul_get_quant_params(rknn_matmul_ctx ctx, rknn_quant_params* params, float* scale);

4.6 动态 Shape

rknn_matmul_set_dynamic_shape

int rknn_matmul_set_dynamic_shape(rknn_matmul_ctx ctx, rknn_matmul_shape* shape);

运行时切换 M/K/N(目前仅支持 M 动态)。

4.7 布局转换

rknn_B_normal_layout_to_native_layout

int rknn_B_normal_layout_to_native_layout(void* B_input, void* B_output, int K, int N, rknn_matmul_info* info);

将 B 矩阵从 normal layout 转换为 native layout。处理 K 分段和平台差异。


五、配置结构体

rknn_matmul_info

typedef struct rknn_matmul_info_t {
    int32_t M;
    int32_t K;
    int32_t N;
    rknn_matmul_type type;       // 计算类型
    int16_t B_layout;            // 0=normal, 1=native
    int16_t B_quant_type;        // 0=per-layer, 1=per-channel, 2=per-group
    int16_t AC_layout;           // 0=normal, 1=native
    int16_t AC_quant_type;       // 仅支持 0
    int32_t iommu_domain_id;     // IOMMU 域 ID
    int16_t group_size;          // per-group 量化的组大小
    int8_t  reserved[34];
} rknn_matmul_info;

rknn_quant_params

typedef struct _rknn_quant_params {
    char     name[RKNN_MAX_NAME_LEN];
    float*   scale;              // 缩放因子数组
    int32_t  scale_len;
    int32_t* zp;                 // 零点数组
    int32_t  zp_len;
} rknn_quant_params;

六、与裸 ioctl demo 的对应关系

npu_benchmarknpu_llama 中的手工 matmul 实现,本质上是 rknn_matmul_run() 内部逻辑的开源复刻:

闭源 API裸 ioctl demo 对应
rknn_matmul_create()gen_matmul_fp16() / gen_matmul_int8() 生成命令流
rknn_matmul_set_io_mem()mem_allocate() + weight_fp16() / feature_data() 排列数据
rknn_matmul_run()填充 rknpu_task + rknpu_submit + ioctl(SUBMIT)
rknn_matmul_destroy()munmap() + mem_destroy()
native layoutweight_fp16() / weight_int8() / feature_data() 函数

自定义算子 API(rknn_custom_op.h)

来源:test/starrynpu/demo/yolov8/3rdparty/rknpu2/include/rknn_custom_op.h(145 行)

当模型中包含 NPU 不支持的算子时,闭源库提供自定义算子机制,允许用户注册 CPU 或 GPU(OpenCL)回调函数来实现这些算子。


一、核心概念

模型推理流程中遇到不支持的算子:
    NPU 执行 layer 0~5
    → 自定义算子回调执行 layer 6(CPU/GPU)
    → NPU 继续执行 layer 7~N

闭源库在推理过程中自动调度:NPU 可执行的层在 NPU 上运行,不支持的层调用用户注册的回调。


二、类型定义

2.1 执行后端

typedef enum _rknn_target_type {
    RKNN_TARGET_TYPE_CPU = 1,    // CPU 后端
    RKNN_TARGET_TYPE_GPU = 2,    // GPU 后端(OpenCL)
} rknn_target_type;

2.2 GPU 上下文

typedef struct _rknn_gpu_op_context {
    void* cl_context;          // OpenCL context
    void* cl_command_queue;    // OpenCL command queue
    void* cl_kernel;           // OpenCL kernel
} rknn_gpu_op_context;

2.3 算子上下文

typedef struct _rknn_custom_op_context {
    rknn_target_type               target;        // 后端类型
    rknn_custom_op_interal_context internal_ctx;   // 闭源库内部上下文
    rknn_gpu_op_context            gpu_ctx;        // GPU 上下文
    void*                          priv_data;      // 用户私有数据
} rknn_custom_op_context;

2.4 算子张量

typedef struct _rknn_custom_op_tensor {
    rknn_tensor_attr attr;    // 张量属性(维度、类型、量化参数)
    rknn_tensor_mem  mem;     // 张量内存(虚拟地址、物理地址、fd)
} rknn_custom_op_tensor;

2.5 算子属性

typedef struct _rknn_custom_op_attr {
    char             name[RKNN_MAX_NAME_LEN];   // 属性名
    rknn_tensor_type dtype;                      // 数据类型
    uint32_t         n_elems;                    // 元素数量
    void*            data;                       // 属性数据指针
} rknn_custom_op_attr;

三、算子注册结构体

typedef struct _rknn_custom_op {
    uint32_t         version;                         // 版本号
    rknn_target_type target;                          // CPU 或 GPU
    char             op_type[RKNN_MAX_NAME_LEN];      // 算子类型名

    // GPU(OpenCL)专用字段
    char     cl_kernel_name[RKNN_MAX_NAME_LEN];       // OpenCL kernel 名
    char*    cl_kernel_source;                         // kernel 源码或文件路径
    uint64_t cl_source_size;                           // 源码大小(0=文件路径)
    char     cl_build_options[RKNN_MAX_NAME_LEN];     // 编译选项

    // 回调函数
    int (*init)(rknn_custom_op_context* op_ctx,
                rknn_custom_op_tensor* inputs, uint32_t n_inputs,
                rknn_custom_op_tensor* outputs, uint32_t n_outputs);
                // [可选] 初始化回调

    int (*prepare)(rknn_custom_op_context* op_ctx,
                   rknn_custom_op_tensor* inputs, uint32_t n_inputs,
                   rknn_custom_op_tensor* outputs, uint32_t n_outputs);
                   // [可选] 准备回调

    int (*compute)(rknn_custom_op_context* op_ctx,
                   rknn_custom_op_tensor* inputs, uint32_t n_inputs,
                   rknn_custom_op_tensor* outputs, uint32_t n_outputs);
                   // [必须] 计算回调

    int (*compute_native)(rknn_custom_op_context* op_ctx,
                          rknn_custom_op_tensor* inputs, uint32_t n_inputs,
                          rknn_custom_op_tensor* outputs, uint32_t n_outputs);
                          // [可选] 原生属性计算回调(当前不支持)

    int (*destroy)(rknn_custom_op_context* op_ctx);
                   // [可选] 销毁回调
} rknn_custom_op;

特殊返回值init 回调返回 RKNN_WARNING_SKIP_CUSTOM_OP_COMPUTE(-14)时,如果该算子类型被 RKNN 内部支持,则使用内部实现而非自定义回调。


四、函数签名

rknn_register_custom_ops

int rknn_register_custom_ops(rknn_context ctx, rknn_custom_op* op, uint32_t custom_op_num);
参数说明
ctxrknn 上下文(必须在 rknn_init 之后调用)
op自定义算子数组
custom_op_num数组长度

使用步骤

  1. 创建 rknn_custom_op 结构体数组
  2. 填写 op_typetarget、回调函数
  3. rknn_init() 之后调用 rknn_register_custom_ops()

rknn_custom_op_get_op_attr

void rknn_custom_op_get_op_attr(rknn_custom_op_context* op_ctx,
                                 const char* attr_name,
                                 rknn_custom_op_attr* op_attr);

在回调函数内部调用,获取模型中定义的算子属性(如 kernel_size、stride 等)。


五、动态加载机制

闭源库支持通过 dlopen 加载自定义算子 .so

typedef rknn_custom_op* (*get_custom_op_func)();

自定义算子 .so 需导出 get_custom_op_func 类型的函数,返回 rknn_custom_op 指针。需使用 RKNN_CUSTOM_OP_EXPORT__attribute__((visibility("default"))))标记导出函数。

模型推理全链条分析

从加载 .rknn 模型到推理完成到资源释放的完整逻辑链条。

逆向来源:闭源库头文件 rknn_api.h、开源内核驱动 rknpu/、裸 ioctl demo npu_benchmark/npu_llama、StarryOS Rust 驱动。


一、全链条总览

┌──────────────────────────────────────────────────────────────────┐
│ 阶段 1:初始化                                                    │
│   rknn_init()                                                    │
│   ├── 打开设备 → 检查硬件版本                                      │
│   ├── 解析 .rknn 模型                                             │
│   ├── 分配 DMA 内存(权重/内部/IO/命令流)                          │
│   ├── 权重转换 → native layout → 拷贝到 DMA 内存                   │
│   └── 编译模型 → 生成命令流 + Task 数组                             │
├──────────────────────────────────────────────────────────────────┤
│ 阶段 2:配置(可选)                                               │
│   rknn_set_core_mask()      → 选择 NPU 核心                      │
│   rknn_register_custom_ops() → 注册自定义算子                      │
│   rknn_set_input_shapes()   → 设置动态 shape                     │
├──────────────────────────────────────────────────────────────────┤
│ 阶段 3:推理循环(可重复执行)                                      │
│   rknn_inputs_set()  → 格式/类型转换 → 拷贝到 DMA → flush cache   │
│   rknn_run()         → 构造 submit → ioctl(SUBMIT) → 等待中断     │
│   rknn_outputs_get() → invalidate cache → 反量化 → 格式转换       │
│   rknn_outputs_release() → 释放输出缓冲                           │
├──────────────────────────────────────────────────────────────────┤
│ 阶段 4:销毁                                                      │
│   rknn_destroy()                                                 │
│   ├── 释放所有 DMA 内存                                           │
│   ├── 关闭设备 fd                                                 │
│   └── 释放上下文                                                  │
└──────────────────────────────────────────────────────────────────┘

二、阶段 1:初始化(rknn_init

2.1 设备打开

rknn_init()
└── open("/dev/dri/card0") 或 open("/dev/dri/renderD128")
    └── 获取 DRM fd

逆向证据npu_interface.cnpu_open() 遍历 /dev/dri/card*,用 drmGetVersion() 检查驱动名是否为 "rknpu"

2.2 硬件版本检查

struct rknpu_action action = { .flags = RKNPU_GET_HW_VERSION };
ioctl(fd, DRM_IOCTL_RKNPU_ACTION, &action);
// action.value → 硬件版本号

闭源库据此选择对应的硬件配置(核心数、CBUF 大小、支持的精度等)。

2.3 模型解析

.rknn 是 Rockchip 私有的模型格式,包含:

内容说明
文件头魔数、版本、段表标识文件格式
网络拓扑层定义、连接关系计算图描述
权重数据量化后的权重可能已是 native layout
预编译命令流寄存器命令序列pre-compile 模型专用
量化参数scale/zp 表每层或每通道
自定义字符串用户元数据可选

模型格式完全闭源,无公开文档。

2.4 内存分配

闭源库通过 ioctl 分配多块 DMA 内存:

ioctl(MEM_CREATE, { size, flags }) → { handle, obj_addr, dma_addr }
ioctl(MEM_MAP, { handle }) → { offset }
mmap(fd, offset) → virt_addr

分配的内存块:

用途flags说明
权重内存RKNPU_MEM_KERNEL_MAPPING内核态也需要访问
内部中间缓冲0 或 RKNPU_MEM_CACHEABLE层间数据传递
输入缓冲0用户写入输入数据
输出缓冲0硬件写入输出数据
命令流缓冲(regcmd)0存放 NPUOP 指令序列
Task 数组RKNPU_MEM_KERNEL_MAPPING内核驱动直接读取

逆向证据llama0.cbuild_transformer() 分配了 regcmd(1024 字节)和 tasks(1024 字节,带 RKNPU_MEM_KERNEL_MAPPING)。

2.5 权重转换

闭源库将模型权重从存储格式转换为 NPU native layout:

原始权重(行主序)
    ↓ 量化(如果需要)
量化权重(INT8/INT4)
    ↓ 重排列
Native Layout(按硬件分块要求)
    ↓ memcpy
DMA 内存

逆向证据llama0.ccreate_weight_cache() 将 FP32 权重转为 FP16 并用 feature_data() 函数重排列到 native layout。

2.6 命令流编译

对于非预编译模型,闭源库为每层生成寄存器命令流:

网络层描述
    ↓ 参数计算
CNA/CORE/DPU 描述符
    ↓ NPUOP 编码
64-bit 命令序列(regcmd buffer)
    ↓
Task 数组(每个 task 指向一段命令流)

每条命令格式:NPUOP(模块ID, 寄存器值, 寄存器偏移)

// 编码:[15:0]=寄存器偏移, [47:16]=寄存器值, [63:48]=模块ID
#define NPUOP(op, value, reg) \
    (((uint64_t)(op & 0xffff)) << 48) | \
    (((uint64_t)(value & 0xffffffff)) << 16) | \
    (uint64_t)(reg & 0xffff)

逆向证据npu_matmul.cgen_matmul_task() 生成 108 条 NPUOP 指令,覆盖 CNA(30 条)、CORE(6 条)、DPU(70+ 条)寄存器。


三、阶段 2:配置

3.1 核心选择

rknn_set_core_mask(ctx, RKNN_NPU_CORE_0_1_2);

影响后续 rknn_run()rknpu_submit.core_masksubcore_task[] 的填充。

多核切分逻辑(闭源库内部):

  • 单核模式:所有 task 在一个核心上顺序执行
  • 双核/三核模式:将 task 数组按层切分,分配到不同核心的 subcore_task[]

3.2 自定义算子注册

rknn_custom_op ops[1] = { ... };
rknn_register_custom_ops(ctx, ops, 1);

详见 自定义算子 API


四、阶段 3:推理循环

4.1 设置输入(rknn_inputs_set

rknn_input inputs[1];
inputs[0].index = 0;
inputs[0].buf = image_data;
inputs[0].size = 640 * 640 * 3;
inputs[0].type = RKNN_TENSOR_UINT8;
inputs[0].fmt = RKNN_TENSOR_NHWC;
inputs[0].pass_through = 0;

rknn_inputs_set(ctx, 1, inputs);

闭源库内部流程

用户数据(NHWC, UINT8)
    ↓ pass_through == FALSE
格式转换: NHWC → NC1HWC2(NPU 原生格式)
    ↓
类型转换: UINT8 → INT8(应用 zp 偏移)
    ↓
memcpy → 输入 DMA 缓冲
    ↓
ioctl(MEM_SYNC, SYNC_TO_DEVICE)  // flush CPU cache → 设备可见

如果 pass_through == TRUE,跳过格式/类型转换,直接拷贝。

4.2 执行推理(rknn_run

rknn_run_extend extend = { .non_block = 0, .timeout_ms = 5000 };
rknn_run(ctx, &extend);

4.2.1 闭源库侧:构造 rknpu_submit

rknn_run()
├── 构造 rknpu_submit
│   ├── flags:
│   │   ├── RKNPU_JOB_PC        (1)  ← PC 模式
│   │   ├── RKNPU_JOB_BLOCK     (0)  ← 阻塞(或 NONBLOCK)
│   │   └── RKNPU_JOB_PINGPONG  (4)  ← 乒乓模式
│   ├── timeout = 5000
│   ├── task_start = 0
│   ├── task_number = N(总 task 数)
│   ├── task_obj_addr = tasks DMA 对象地址
│   ├── core_mask = 0x1(单核)或 0x7(三核)
│   └── subcore_task[]:
│       ├── [0] = { start=0, number=N }     ← 核心 0 的 task 范围
│       ├── [1] = { start=N, number=0 }     ← 核心 1(如果启用)
│       └── [2] = { start=N, number=0 }     ← 核心 2(如果启用)
└── ioctl(DRM_IOCTL_RKNPU_SUBMIT, &submit)

4.2.2 内核驱动侧:任务提交完整时序

sequenceDiagram
    participant U as 用户态 (librknnrt.so)
    participant S as sys_ioctl
    participant D as DeviceOps::ioctl
    participant I as submit_ioctrl()
    participant O as submit_one(idx)
    participant P as submit_pc()
    participant H as NPU 硬件

    U->>S: ioctl(fd, DRM_IOCTL_RKNPU_SUBMIT, &rknpu_submit)
    S->>D: dispatch by cmd
    D->>I: submit_ioctrl(&submit_args)

    Note over I: 解析 subcore_task[0..5]

    loop idx = 0..4 (跳过 task_number == 0)
        I->>O: submit_one(idx, args)

        Note over O: 按 max_submit_number 分批

        loop 每批 task
            O->>P: submit_pc(batch_args)

            Note over P: 1. 写 CNA.s_pointer / CORE.s_pointer
            Note over P: 2. 写 PC.base_address = regcmd_base_addr
            Note over P: 3. 写 PC.register_amounts
            Note over P: 4. 写 PC.interrupt_mask / interrupt_clear
            Note over P: 5. 写 PC.task_control (task_number + flags)
            Note over P: 6. 写 PC.task_dma_base_addr
            Note over P: 7. 写 GLOBAL.enable_mask
            Note over P: 8. 写 PC.operation_enable = 1
            Note over P: 9. 写 PC.operation_enable = 0

            P->>H: 硬件开始执行命令流

            loop 轮询等待
                O->>H: 读 PC.interrupt_status
                Note over O: status = rknpu_fuzz_status(raw_status)
                alt status & int_mask != 0
                    H-->>O: 完成
                else 未完成 & 未超时
                    Note over O: 继续轮询
                else 超时
                    Note over O: 返回错误
                end
            end

            O->>H: 写 PC.interrupt_clear = INT_CLEAR_ALL
            Note over O: 写回最后一个 task 的 int_status
        end
    end

    I-->>D: 返回结果
    D-->>S: Ok / Err
    S-->>U: ioctl 返回值

4.2.3 状态机

stateDiagram-v2
    [*] --> IDLE: 驱动初始化完成

    IDLE --> PARSING: 收到 SUBMIT ioctl
    PARSING --> DISPATCHING: 解析 subcore_task[0..5]

    DISPATCHING --> SUBMITTING: submit_one(idx)
    Note right of DISPATCHING: 跳过 task_number == 0 的子核心

    SUBMITTING --> HW_RUNNING: submit_pc() 写寄存器 + OP_EN=1
    HW_RUNNING --> POLLING: 轮询 interrupt_status

    POLLING --> COMPLETED: status & int_mask != 0
    POLLING --> POLLING: 未完成 & 未超时
    POLLING --> TIMEOUT: 超过 timeout

    COMPLETED --> CLEARING: 写 interrupt_clear
    CLEARING --> NEXT_BATCH: 还有剩余批次?

    NEXT_BATCH --> SUBMITTING: 是,提交下一批
    NEXT_BATCH --> NEXT_CORE: 否,当前子核心完成

    NEXT_CORE --> DISPATCHING: 还有子核心?
    NEXT_CORE --> DONE: 全部完成

    DONE --> IDLE: 返回成功

    TIMEOUT --> ERROR: 返回超时错误
    ERROR --> IDLE: 错误恢复

4.2.4 解析 subcore_task[5]

rknpu_submit 包含 subcore_task[5],每个元素描述一个子核心要执行的 task 切片:

subcore_task[0]: { task_start: 0,  task_number: 10 }  → core 0 执行 task 0~9
subcore_task[1]: { task_start: 10, task_number: 5  }  → core 1 执行 task 10~14
subcore_task[2]: { task_start: 15, task_number: 0  }  → 跳过
subcore_task[3~4]: 跳过

驱动遍历 idx = 0..5,对 task_number > 0 的子核心调用 submit_one(idx, args)

4.2.5 分批提交(max_submit_number

每个子核心的 task 可能很多,但硬件一次能处理的 task 数量有上限(max_submit_number,取决于硬件版本)。因此 submit_one() 将 task 分批:

总 task_number = 100, max_submit_number = 12

批次 1: task[0..12]   → submit_pc()
批次 2: task[12..24]  → submit_pc()
...
批次 9: task[96..100]  → submit_pc()

4.2.6 submit_pc() 寄存器写入序列

步骤寄存器写入值说明
1CNA.s_pointer / CORE.s_pointer0xe + 0x10000000 * core_idx初始化子模块指针
2PC.base_addressregcmd_base_addr命令流 DMA 基址
3PC.register_amounts计算值命令/寄存器项数量
4PC.interrupt_masktask.int_mask设置期望的完成中断
5PC.interrupt_cleartask.int_clear清除残留中断
6PC.task_control((0x6 | pp_en) << bits) | task_num任务数量 + 控制位
7PC.task_dma_base_addrtask buffer DMA 地址task 描述符数组基址
8GLOBAL.enable_masktask.enable_mask使能相关功能模块
9PC.operation_enable1触发执行
10PC.operation_enable0清边沿(脉冲触发)

步骤 9→10 构成一个上升沿脉冲,硬件在检测到 OP_EN 从 0→1 时启动命令流执行。

4.2.7 等待完成与中断处理

Linux 内核驱动(中断驱动):

rknpu_irq_handler()
├── 读 INT_STATUS 寄存器
├── rknpu_fuzz_status() → 归一化中断状态
├── 写 INT_CLEAR → 清除中断
└── 唤醒 wait_queue / 信号 dma_fence

rknpu_job_done()
├── 写回 task[i].int_status
└── 信号 DMA fence / 唤醒用户态

StarryOS Rust 驱动(轮询模式):

loop {
    raw_status = read(PC.interrupt_status)
    status = rknpu_fuzz_status(raw_status)

    if status & int_mask != 0 {
        break  // 完成
    }
    if elapsed > timeout {
        return Error::Timeout
    }
    // 继续轮询(yield / sleep)
}
write(PC.interrupt_clear, INT_CLEAR_ALL)  // 0x1ffff
// 写回最后一个 task 的 int_status

rknpu_fuzz_status():将每个模块的 2-bit 中断组归一化(任一非零 → 全置 1),确保与 int_mask 的比较不会因为硬件只置了部分 bit 而误判为未完成。

4.2.8 失败路径

超时

触发条件行为恢复
轮询 interrupt_status 超过 submit.timeout 毫秒返回超时错误interrupt_clear = 0x1ffff,可能需要 NPU 软复位(ACTION::ACT_RESET

异常中断

触发条件说明检测
DMA_RD_ERR (bit12) 或 DMA_WR_ERR (bit13)DMA 读写错误(地址非法/IOMMU 映射缺失)interrupt_status & 0x3000

非法参数

参数非法条件行为
task_number== 0(某个 subcore_task)跳过该子核心(非错误)
core_mask无效核心选择返回 EINVAL
task_obj_addr无效内核对象地址返回 EFAULT
timeout== 0使用默认超时值

4.2.9 Linux 内核驱动 vs StarryOS 驱动差异

方面Linux rknpu 驱动StarryOS Rust 驱动
等待机制中断驱动 + wait_queue + dma_fence轮询 interrupt_status
多任务调度内核 job 队列 + 优先级调度串行提交
fence 支持完整 dma_fence + sync_file未实现
IOMMURKIOMMU domain attach/detach未实现(物理连续内存)
电源管理pm_runtime + devfreq直接寄存器操作(最小集)

4.3 获取输出(rknn_outputs_get

rknn_output outputs[3];
memset(outputs, 0, sizeof(outputs));
for (int i = 0; i < 3; i++) {
    outputs[i].index = i;
    outputs[i].want_float = 0;
}
rknn_outputs_get(ctx, 3, outputs, NULL);

闭源库内部流程

rknn_outputs_get()
│
├── ioctl(MEM_SYNC, SYNC_FROM_DEVICE)  // invalidate cache
│
├── 遍历 outputs[i]:
│   ├── 从输出 DMA 缓冲读取原始数据
│   ├── 如果 want_float:
│   │   ├── 反量化: INT8 → FP32(val = (qval - zp) * scale)
│   │   └── 格式转换: NC1HWC2 → NCHW/NHWC
│   ├── 如果 is_prealloc:
│   │   └── memcpy → 用户提供的 buf
│   └── 否则:
│       ├── malloc(size) → 分配 buf
│       └── memcpy → buf
│
└── 返回

4.4 释放输出

rknn_outputs_release(ctx, 3, outputs);

释放 rknn_outputs_getis_prealloc == FALSE 时分配的 buf


五、阶段 4:销毁(rknn_destroy

rknn_destroy(ctx);

内部流程

rknn_destroy()
├── 释放命令流缓冲
│   └── munmap() + ioctl(MEM_DESTROY)
├── 释放 Task 数组
│   └── munmap() + ioctl(MEM_DESTROY)
├── 释放权重内存
│   └── munmap() + ioctl(MEM_DESTROY)
├── 释放内部中间缓冲
│   └── munmap() + ioctl(MEM_DESTROY)
├── 释放输入/输出缓冲
│   └── munmap() + ioctl(MEM_DESTROY)
├── close(drm_fd)
└── free(context)

六、零拷贝推理路径

使用 RKNN_FLAG_MEM_ALLOC_OUTSIDE 时的高级路径:

// 1. 初始化(仅解析模型,不分配 IO 内存)
rknn_init(&ctx, model, size, RKNN_FLAG_MEM_ALLOC_OUTSIDE, NULL);

// 2. 查询内存需求
rknn_mem_size mem_size;
rknn_query(ctx, RKNN_QUERY_MEM_SIZE, &mem_size, sizeof(mem_size));

// 3. 用户分配内存
rknn_tensor_mem* weight_mem = rknn_create_mem(ctx, mem_size.total_weight_size);
rknn_tensor_mem* internal_mem = rknn_create_mem(ctx, mem_size.total_internal_size);

// 4. 绑定内存
rknn_set_weight_mem(ctx, weight_mem);
rknn_set_internal_mem(ctx, internal_mem);

// 5. 查询原生输入/输出属性
rknn_tensor_attr input_attr, output_attr;
input_attr.index = 0;
rknn_query(ctx, RKNN_QUERY_NATIVE_INPUT_ATTR, &input_attr, sizeof(input_attr));

// 6. 分配并绑定 IO 内存
rknn_tensor_mem* input_mem = rknn_create_mem(ctx, input_attr.size_with_stride);
rknn_set_io_mem(ctx, input_mem, &input_attr);

// 7. 直接写入 native layout 数据(无格式转换开销)
memcpy(input_mem->virt_addr, native_data, size);

// 8. 推理
rknn_run(ctx, NULL);

// 9. 直接读取输出(无拷贝)
float* output = (float*)output_mem->virt_addr;

此路径避免了 rknn_inputs_setrknn_outputs_get 中的格式转换和数据拷贝。


七、异步推理路径

// 初始化时启用异步
rknn_init(&ctx, model, size, RKNN_FLAG_ASYNC_MASK, NULL);

// 帧 0
rknn_inputs_set(ctx, 1, inputs_frame0);
rknn_run(ctx, NULL);
// rknn_outputs_get 返回的是上一帧结果(第一次调用返回空)
rknn_outputs_get(ctx, n, outputs, NULL);

// 帧 1
rknn_inputs_set(ctx, 1, inputs_frame1);
rknn_run(ctx, NULL);
// 返回帧 0 的结果
rknn_outputs_get(ctx, n, outputs, NULL);

异步模式下 rknn_outputs_get 不等待当前帧完成,直接返回上一帧结果,提高单线程帧率。


八、DMA Fence 路径

// 初始化时启用 fence
rknn_init(&ctx, model, size,
    RKNN_FLAG_FENCE_IN_OUTSIDE | RKNN_FLAG_FENCE_OUT_OUTSIDE, NULL);

// 推理时传入/获取 fence fd
rknn_run_extend extend;
extend.fence_fd = input_fence_fd;  // 等待输入数据就绪
extend.non_block = 1;
rknn_run(ctx, &extend);

// extend.fence_fd 现在是输出 fence fd
// 可以传给 GPU/RGA 等下游设备

对应内核驱动的 RKNPU_JOB_FENCE_IN / RKNPU_JOB_FENCE_OUT 标志。

内存管理与零拷贝

闭源库的内存管理策略,包括 DMA 内存分配、cache 同步、零拷贝机制。

逆向来源:rknn_api.hrknpu_ioctl.hnpu_interface.cllama0.c


一、内存分配路径

1.1 闭源库内部分配

闭源库通过 DRM ioctl 分配 DMA 内存:

rknn_create_mem(ctx, size)
    ↓
ioctl(DRM_IOCTL_RKNPU_MEM_CREATE, {
    .size = size,
    .flags = flags
})
    ↓ 返回
{ handle, obj_addr, dma_addr }
    ↓
ioctl(DRM_IOCTL_RKNPU_MEM_MAP, { handle })
    ↓ 返回
{ offset }
    ↓
mmap(fd, size, offset)
    ↓ 返回
virt_addr
    ↓
填充 rknn_tensor_mem {
    .virt_addr = virt_addr,
    .phys_addr = dma_addr,    // 实际是 IOVA
    .fd = drm_fd,
    .size = size,
    .priv_data = 内部状态指针
}

1.2 内存类型标志

enum e_rknpu_mem_type {
    RKNPU_MEM_CONTIGUOUS      = 0 << 0,  // 物理连续(默认)
    RKNPU_MEM_NON_CONTIGUOUS  = 1 << 0,  // 物理不连续
    RKNPU_MEM_NON_CACHEABLE   = 0 << 1,  // 不可缓存(默认)
    RKNPU_MEM_CACHEABLE       = 1 << 1,  // 可缓存
    RKNPU_MEM_WRITE_COMBINE   = 1 << 2,  // 写合并
    RKNPU_MEM_KERNEL_MAPPING  = 1 << 3,  // 内核态映射
    RKNPU_MEM_IOMMU           = 1 << 4,  // IOMMU 映射
    RKNPU_MEM_ZEROING         = 1 << 5,  // 零初始化
    RKNPU_MEM_SECURE          = 1 << 6,  // 安全缓冲
    RKNPU_MEM_NON_DMA32       = 1 << 7,  // 非 DMA32 区域
    RKNPU_MEM_TRY_ALLOC_SRAM  = 1 << 8,  // 尝试 SRAM 分配
};

1.3 闭源库的内存分配策略

用途flags原因
Task 数组KERNEL_MAPPING内核驱动需要直接读取 task 字段
命令流(regcmd)0硬件通过 DMA 读取,不需要内核映射
权重KERNEL_MAPPING 或 0取决于是否需要内核态访问
输入/输出0 或 CACHEABLE用户频繁读写时用 CACHEABLE
内部中间缓冲0仅硬件读写

逆向证据bench_mark.c 中 tasks 用 RKNPU_MEM_KERNEL_MAPPING,其余用 0。


二、Cache 同步

2.1 同步模式

enum e_rknpu_mem_sync_mode {
    RKNPU_MEM_SYNC_TO_DEVICE   = 1 << 0,  // CPU → 设备(flush)
    RKNPU_MEM_SYNC_FROM_DEVICE = 1 << 1,  // 设备 → CPU(invalidate)
};

2.2 同步时机

CPU 写入输入数据
    ↓
ioctl(MEM_SYNC, { flags=SYNC_TO_DEVICE, obj_addr, offset, size })
    ↓ flush CPU cache
NPU 可以安全读取
    ↓
NPU 执行完毕,写入输出
    ↓
ioctl(MEM_SYNC, { flags=SYNC_FROM_DEVICE, obj_addr, offset, size })
    ↓ invalidate CPU cache
CPU 可以安全读取输出

2.3 闭源库的 cache 优化标志

标志效果
RKNN_FLAG_DISABLE_FLUSH_INPUT_MEM_CACHE跳过输入 flush(用户自行保证)
RKNN_FLAG_DISABLE_FLUSH_OUTPUT_MEM_CACHE跳过输出 invalidate(输出由 GPU/RGA 消费)

三、零拷贝机制

3.1 标准路径 vs 零拷贝路径

标准路径rknn_inputs_set + rknn_outputs_get):

用户缓冲 → [格式转换] → [类型转换] → memcpy → DMA 缓冲 → NPU
NPU → DMA 缓冲 → memcpy → [反量化] → [格式转换] → 用户缓冲

至少 2 次 memcpy + 可能的格式/类型转换。

零拷贝路径rknn_set_io_mem):

用户直接写入 DMA 缓冲(native layout)→ NPU
NPU → DMA 缓冲 → 用户直接读取

0 次 memcpy,但用户需要自行处理 native layout。

3.2 零拷贝 API

内部分配

rknn_tensor_mem* rknn_create_mem(rknn_context ctx, uint32_t size);

闭源库内部调用 ioctl(MEM_CREATE) + ioctl(MEM_MAP) + mmap()

带标志分配

rknn_tensor_mem* rknn_create_mem2(rknn_context ctx, uint64_t size, uint64_t alloc_flags);
alloc_flags说明
RKNN_FLAG_MEMORY_CACHEABLE可缓存内存
RKNN_FLAG_MEMORY_NON_CACHEABLE不可缓存内存
RKNN_FLAG_MEMORY_TRY_ALLOC_SRAM尝试 SRAM

从外部 fd 创建

rknn_tensor_mem* rknn_create_mem_from_fd(rknn_context ctx,
    int32_t fd, void* virt_addr, uint32_t size, int32_t offset);

用于导入其他设备(如 camera、GPU)分配的 DMA buffer。

从物理地址创建

rknn_tensor_mem* rknn_create_mem_from_phys(rknn_context ctx,
    uint64_t phys_addr, void* virt_addr, uint32_t size);

从 mb_blk 创建

rknn_tensor_mem* rknn_create_mem_from_mb_blk(rknn_context ctx,
    void* mb_blk, int32_t offset);

用于 Rockchip 多媒体框架的内存块。

销毁

int rknn_destroy_mem(rknn_context ctx, rknn_tensor_mem* mem);

逆向推断:内部根据 mem->flags 判断是否需要 munmap()ioctl(MEM_DESTROY)

  • RKNN_TENSOR_MEMORY_FLAGS_ALLOC_INSIDE → 完整释放
  • RKNN_TENSOR_MEMORY_FLAGS_FROM_FD → 仅释放包装结构
  • RKNN_TENSOR_MEMORY_FLAGS_FROM_PHYS → 仅释放包装结构

Cache 同步

int rknn_mem_sync(rknn_context context, rknn_tensor_mem* mem, rknn_mem_sync_mode mode);
mode说明
RKNN_MEMORY_SYNC_TO_DEVICECPU 写完后调用
RKNN_MEMORY_SYNC_FROM_DEVICE读取设备输出前调用
RKNN_MEMORY_SYNC_BIDIRECTIONAL双向同步

逆向推断:内部调用 ioctl(DRM_IOCTL_RKNPU_MEM_SYNC, { flags, obj_addr, offset, size })

3.3 IO 内存绑定

int rknn_set_io_mem(rknn_context ctx, rknn_tensor_mem* mem, rknn_tensor_attr* attr);

将用户分配的内存绑定为模型的输入或输出。attr->index 指定输入/输出索引。

int rknn_set_weight_mem(rknn_context ctx, rknn_tensor_mem* mem);
int rknn_set_internal_mem(rknn_context ctx, rknn_tensor_mem* mem);

绑定权重和内部内存。需配合 RKNN_FLAG_MEM_ALLOC_OUTSIDE 使用。


四、SRAM 管理

RK3588 NPU 有片上 SRAM,可用于减少 DDR 带宽:

// 查询 SRAM 大小
rknn_mem_size mem_size;
rknn_query(ctx, RKNN_QUERY_MEM_SIZE, &mem_size, sizeof(mem_size));
// mem_size.total_sram_size — 总 SRAM 大小
// mem_size.free_sram_size  — 空闲 SRAM 大小

// 也可通过 ioctl 查询
struct rknpu_action action = { .flags = RKNPU_GET_TOTAL_SRAM_SIZE };
ioctl(fd, DRM_IOCTL_RKNPU_ACTION, &action);

启用 SRAM 分配:

  • 初始化时设置 RKNN_FLAG_ENABLE_SRAM
  • 内存分配时使用 RKNPU_MEM_TRY_ALLOC_SRAMRKNN_FLAG_MEMORY_TRY_ALLOC_SRAM

五、npu_llama 的 Buffer 池化策略

llama0.c 展示了一种用户态 buffer 池化方案,避免频繁的 DMA 内存分配/释放:

#define MAX_BUFFER_POOL_SIZE 8

typedef struct {
    void*    data;
    uint64_t dma;
    uint64_t obj;
    uint32_t handle;
    size_t   size;
    int      in_use;
} NPUBuffer;

// 预分配不同大小的 buffer
size_t sizes[] = { 512KB, 1MB, 2MB, 512KB, 256KB, 256KB, 128KB, 128KB };
for (int i = 0; i < 8; i++) {
    pool[i].data = mem_allocate(fd, sizes[i], &pool[i].dma, ...);
}

// 运行时从池中取用
NPUBuffer* buf = get_buffer_from_pool(t, required_size);
// ... 使用 buf->data / buf->dma ...
release_buffer_to_pool(buf);

闭源库内部很可能也使用类似的池化策略来管理内部中间缓冲。

闭源库内部机制

命令流生成、Task 构造、多核切分、数据格式转换等闭源库内部逻辑的逆向分析。

逆向来源:开源 demo npu_matmul.cbench_mark.cllama0.c、内核驱动 rknpu_job.c、StarryOS Rust 驱动。


一、命令流生成

1.1 命令流格式

每条命令是一个 64-bit 值,编码为:

 63        48 47        16 15         0
┌────────────┬─────────────┬───────────┐
│  模块 ID   │  寄存器值    │ 寄存器偏移 │
│  (16-bit)  │  (32-bit)   │ (16-bit)  │
└────────────┴─────────────┴───────────┘
#define NPUOP(op, value, reg) \
    (((uint64_t)(op & 0xffff)) << 48) | \
    (((uint64_t)(value & 0xffffffff)) << 16) | \
    (uint64_t)(reg & 0xffff)

1.2 模块 ID 编码

#define BLOCK_PC       0x0100
#define BLOCK_CNA      0x0200
#define BLOCK_CORE     0x0800
#define BLOCK_DPU      0x1000
#define BLOCK_DPU_RDMA 0x2000
#define BLOCK_PPU      0x4000
#define BLOCK_PPU_RDMA 0x8000

#define PC_OP_01     0x01    // 寄存器写入标志
#define PC_OP_40     0x40    // 未知用途
#define PC_OP_ENABLE 0x80    // 使能标志

#define OP_REG_CNA  (BLOCK_CNA  | PC_OP_01)  // 0x0201
#define OP_REG_CORE (BLOCK_CORE | PC_OP_01)  // 0x0801
#define OP_REG_DPU  (BLOCK_DPU  | PC_OP_01)  // 0x1001
#define OP_ENABLE   (PC_OP_ENABLE | PC_OP_01) // 0x0081

1.3 命令流结构(以 matmul 为例)

一个典型的 CNA→CORE→DPU 流水线命令流包含约 108 条指令:

ops[0]     DPU_S_POINTER = 0xE          ← DPU 寄存器组指针
ops[1~28]  CNA 寄存器(卷积参数)         ← 28 条
ops[29~40] CNA 权重解压缩寄存器          ← 12 条
ops[41~42] CNA 补充寄存器                ← 2 条
ops[43]    CNA_S_POINTER = 0xE          ← CNA 寄存器组指针
ops[44~49] CORE 寄存器                   ← 6 条
ops[50]    CORE_S_POINTER = 0xE         ← CORE 寄存器组指针
ops[51~106] DPU 寄存器(后处理参数)      ← 56 条
ops[107]   PC_OPERATION_ENABLE           ← 全局使能

最后一条指令触发硬件开始执行:

ops[107] = NPUOP(OP_ENABLE,
    PC_ENABLE_DPU | PC_ENABLE_CNA | PC_ENABLE,  // 0x0d
    PC_OPERATION_ENABLE);                         // 0x0008

1.4 S_POINTER 寄存器

每个模块有一个 S_POINTER 寄存器(偏移 0x_004),用于切换寄存器组(乒乓机制):

ops[0]  = NPUOP(OP_REG_DPU,  0xE, DPU_S_POINTER);   // 0x4004
ops[43] = NPUOP(OP_REG_CNA,  0xE, CNA_S_POINTER);   // 0x1004
ops[50] = NPUOP(OP_REG_CORE, 0xE, CORE_S_POINTER);  // 0x3004

0xE 的含义尚未完全逆向,推测与寄存器组选择和同步有关。


二、CNA 参数填充

CNA(Convolution Neural-network Accelerator)负责卷积计算的数据加载和 MAC 阵列控制。

2.1 CNA 描述符

typedef struct npu_cna_desc {
    uint8_t  conv_mode;          // 卷积模式(0=direct)
    uint8_t  in_precision;       // 输入精度(0=INT8, 2=FP16)
    uint8_t  proc_precision;     // 处理精度
    uint8_t  kernel_groups;      // 权重分组数
    uint16_t feature_grains;     // 特征粒度
    uint8_t  conv_x_stride;      // X 方向步长
    uint8_t  conv_y_stride;      // Y 方向步长
    uint16_t datain_width;       // 输入宽度
    uint16_t datain_height;      // 输入高度
    uint16_t datain_channel;     // 输入通道数
    uint16_t dataout_width;      // 输出宽度
    uint32_t dataout_atomics;    // 输出原子数
    uint32_t weight_bytes;       // 权重总字节数
    uint32_t weight_bytes_per_kernel; // 每个 kernel 的权重字节数
    uint8_t  weight_width;       // 权重宽度
    uint8_t  weight_height;      // 权重高度
    uint16_t weight_kernels;     // 权重 kernel 数
    uint8_t  weight_bank;        // 权重 CBUF bank 数
    uint8_t  data_bank;          // 数据 CBUF bank 数
    uint16_t data_entries;       // 数据 CBUF 条目数
    uint32_t feature_base_addr;  // 特征数据 DMA 基地址
    uint32_t line_stride;        // 行步长
    int32_t  surf_stride;        // 面步长
    // ... 更多字段
} npu_cna_desc;

2.2 CBUF Bank 分配

CBUF 是 CNA 内部的片上缓冲,共 12 个 bank,每个 32KB:

#define NPU_CBUF_BANK_SIZE 32768   // 32KB
#define NPU_CBUF_BANKS     12

// 分配策略(从 gen_matmul_fp16 逆向):
int weight_banks = ceil(weight_bytes / NPU_CBUF_BANK_SIZE);
int data_banks = NPU_CBUF_BANKS - weight_banks;
// 确保 data_banks >= 1

闭源库需要为每层计算最优的 bank 分配,平衡权重和数据的缓存需求。

2.3 Matmul → 卷积映射

NPU 没有专用的 matmul 单元,矩阵乘法通过卷积实现:

矩阵乘法 C[M×N] = A[M×K] × B[K×N]
    ↓ 映射为
1×1 卷积:
    输入特征: A 重排为 [M, K, 1, 1](M 个样本,K 通道)
    权重:     B 重排为 [N, K, 1, 1](N 个 1×1 kernel,K 通道)
    输出:     C 为 [M, N, 1, 1]

逆向证据gen_matmul_fp16() 中设置 conv_mode = direct_convolutionweight_width = weight_height = 1conv_x_stride = conv_y_stride = 1


三、DPU 参数填充

DPU(Data Processing Unit)负责后处理:BS(Bias/Scale)、BN(Batch Norm)、EW(Element-wise)、LUT(激活函数)、输出转换。

3.1 DPU 描述符

typedef struct npu_dpu_desc {
    uint8_t  flying_mode;       // 0=on-flying(从 CORE 直接接收)
    uint8_t  output_mode;       // 输出模式
    uint8_t  conv_mode;         // 卷积模式
    uint8_t  out_precision;     // 输出精度
    uint8_t  in_precision;      // 输入精度
    uint8_t  proc_precision;    // 处理精度
    uint32_t dst_base_addr;     // 输出 DMA 基地址
    uint32_t dst_surf_stride;   // 输出面步长
    uint16_t width, height;     // 输出尺寸
    uint16_t channel;           // 输出通道
    // BS 旁路控制
    uint8_t  bs_bypass;         // 1=旁路 BS
    uint8_t  bs_alu_bypass;     // 1=旁路 BS ALU
    uint8_t  bs_mul_bypass;     // 1=旁路 BS MUL
    uint8_t  bs_relu_bypass;    // 1=旁路 BS ReLU
    // BN 旁路控制
    uint8_t  bn_bypass;
    uint8_t  bn_alu_bypass;
    uint8_t  bn_mul_bypass;
    uint8_t  bn_relu_bypass;
    // EW 旁路控制
    uint8_t  ew_bypass;
    uint8_t  ew_op_bypass;
    uint8_t  ew_lut_bypass;
    uint8_t  ew_op_cvt_bypass;
    uint8_t  ew_relu_bypass;
    // 输出转换
    uint8_t  fp32tofp16_en;     // FP32→FP16 使能
    uint16_t out_cvt_scale;     // 输出缩放因子
    uint32_t surf_add;          // 面地址增量
} npu_dpu_desc;

3.2 旁路模式

对于简单的 matmul,DPU 大部分功能被旁路:

// gen_matmul_task() 中的典型设置:
dpu_desc.bs_bypass = 1;      // 无 bias
dpu_desc.bs_alu_bypass = 1;
dpu_desc.bs_mul_bypass = 1;
dpu_desc.bs_relu_bypass = 1;
dpu_desc.bn_bypass = 1;      // 无 batch norm
dpu_desc.bn_alu_bypass = 1;
dpu_desc.bn_mul_bypass = 1;
dpu_desc.bn_relu_bypass = 1;
dpu_desc.ew_bypass = 1;      // 无 element-wise
dpu_desc.ew_op_bypass = 1;
dpu_desc.ew_lut_bypass = 1;
dpu_desc.ew_op_cvt_bypass = 1;
dpu_desc.ew_relu_bypass = 1;

闭源库在编译复杂模型时,会根据每层的算子类型选择性启用这些功能。


四、Task 构造

4.1 Task 结构

struct rknpu_task {
    uint32_t flags;           // 任务标志
    uint32_t op_idx;          // 算子索引
    uint32_t enable_mask;     // 模块使能掩码
    uint32_t int_mask;        // 期望的中断掩码
    uint32_t int_clear;       // 中断清除值
    uint32_t int_status;      // [内核写回] 实际中断状态
    uint32_t regcfg_amount;   // 命令流中的指令数量
    uint32_t regcfg_offset;   // 命令流偏移(字节)
    uint64_t regcmd_addr;     // 命令流 DMA 地址
};

4.2 关键字段计算

enable_mask(偏移 0xF008):

#define PC_ENABLE      0x01   // 全局使能
#define PC_ENABLE_CNA  0x04   // CNA 中断使能
#define PC_ENABLE_DPU  0x08   // DPU 中断使能
#define PC_ENABLE_PPU  0x10   // PPU 中断使能

// CNA + CORE + DPU 流水线:
enable_mask = PC_ENABLE | PC_ENABLE_CNA | PC_ENABLE_DPU;  // 0x0d

int_mask(偏移 0x0020):

// 中断位定义(从内核驱动 rknpu_ioctl.h 推断):
// bit[8]  = DPU group 0 完成
// bit[9]  = DPU group 1 完成
// 对于单 task matmul:
int_mask = 0x300;  // 等待 DPU group 0 和 group 1 完成

int_clear

int_clear = 0x1ffff;  // 清除所有 17 位中断

regcfg_amount

// 命令流指令数 - 额外保留量 - 尾部保留
regcfg_amount = total_ops - RKNPU_PC_DATA_EXTRA_AMOUNT - 4;
// RKNPU_PC_DATA_EXTRA_AMOUNT = 4(内核驱动会额外加回)

逆向证据:内核驱动 rknpu_job_subcore_commit_pc() 中:

amount = task->regcfg_amount + rknpu->config->pc_data_extra_amount;

4.3 多 Task 场景

对于多层网络,闭源库生成多个 task,每个 task 对应一层或一组层:

Task[0]: 第 1 层卷积(CNA+CORE+DPU)
    regcmd_addr → 命令流偏移 0
    regcfg_amount = 108
Task[1]: 第 2 层卷积
    regcmd_addr → 命令流偏移 108*8
    regcfg_amount = 108
...
Task[N-1]: 最后一层

五、多核切分

5.1 Submit 结构

struct rknpu_submit {
    uint32_t flags;
    uint32_t timeout;
    uint32_t task_start;
    uint32_t task_number;        // 总 task 数
    uint32_t core_mask;          // 使用的核心掩码
    struct rknpu_subcore_task subcore_task[5]; // 每核心的 task 范围
};

struct rknpu_subcore_task {
    uint32_t task_start;         // 起始 task 索引
    uint32_t task_number;        // task 数量
};

5.2 单核模式

submit.core_mask = 0x1;  // 仅核心 0
submit.subcore_task[0] = { .task_start = 0, .task_number = N };
submit.subcore_task[1] = { .task_start = N, .task_number = 0 };  // 哨兵
submit.subcore_task[2] = { .task_start = N, .task_number = 0 };

逆向证据bench_mark.cllama0.c 均使用此模式。

5.3 多核模式(闭源库独有)

闭源库将 task 数组切分到多个核心:

假设 12 个 task,3 核心模式:
submit.core_mask = 0x7;  // 核心 0+1+2
submit.subcore_task[0] = { 0, 4 };   // 核心 0: task 0~3
submit.subcore_task[1] = { 4, 4 };   // 核心 1: task 4~7
submit.subcore_task[2] = { 8, 4 };   // 核心 2: task 8~11

切分策略是闭源库的核心竞争力之一,涉及:

  • 层间数据依赖分析
  • 计算量均衡
  • 内存带宽分配
  • CBUF 冲突避免

5.4 乒乓模式

RKNPU_JOB_PINGPONG 标志启用硬件乒乓机制:

Task[0] 在 group 0 执行
    ↓ 完成,触发 group 0 中断
Task[1] 在 group 1 执行(与 Task[0] 的 DPU 输出重叠)
    ↓ 完成,触发 group 1 中断
Task[2] 在 group 0 执行
    ...

这允许流水线执行:当 Task[N] 在 DPU 阶段输出时,Task[N+1] 已经在 CNA 阶段加载数据。


六、数据格式转换

6.1 特征数据排列(feature_data

npu_matmul.h 导出的函数,将行主序坐标转换为 NPU native layout 索引:

int feature_data(int C, int H, int W, int C2, int c, int h, int w);
参数说明
C通道总数
H高度
W宽度
C2通道分组大小(FP16=8, INT8=16, FP32=4)
c, h, w1-indexed 坐标

逆向推断的公式

native_index = (c-1)/C2 * (H*W*C2) + (h-1)*W*C2 + (w-1)*C2 + (c-1)%C2

这对应 NC1HWC2 格式:(N, ceil(C/C2), H, W, C2)

6.2 权重排列

FP16 权重(weight_fp16

int weight_fp16(int C, int k, int c);
参数说明
C输入通道数(K 维度)
kkernel 索引(1-indexed)
c通道索引(1-indexed)

对应 native layout (N/16, K/32, 16, 32) 的 FP16 变体。

INT8 权重(weight_int8

int weight_int8(int C, int k, int c);

对应 native layout (N/32, K/32, 32, 32)

6.3 闭源库的格式转换链

用户输入(NHWC, UINT8)
    ↓ rknn_inputs_set()
    ├── NHWC → NCHW(如果模型需要)
    ├── UINT8 → INT8(减去 zp)
    └── NCHW → NC1HWC2(NPU native)
    ↓
NPU 执行
    ↓
NPU 输出(NC1HWC2, INT8/FP16)
    ↓ rknn_outputs_get()
    ├── NC1HWC2 → NCHW/NHWC
    ├── INT8 → FP32((val - zp) * scale)
    └── 拷贝到用户缓冲

零拷贝路径跳过所有转换,用户直接操作 native layout。


七、闭源库 vs 裸 ioctl 对照表

闭源库内部操作裸 ioctl demo 对应文件
模型解析无(用户手动定义参数)
CNA 描述符填充gen_matmul_fp16()npu_matmul.c
DPU 描述符填充gen_matmul_task()npu_matmul.c
NPUOP 编码NPUOP()npu_hw.h
Task 构造手动填充 tasks[0]bench_mark.c
Submit 构造手动填充 submitbench_mark.c
权重转换weight_fp16() / weight_int8()npu_matmul.c
特征排列feature_data()npu_matmul.c
内存分配mem_allocate()npu_interface.c
内存释放mem_destroy()npu_interface.c
设备打开npu_open()npu_interface.c
设备复位npu_reset()npu_interface.c
Buffer 池化NPUBufferllama0.c
权重缓存NPUWeightCachellama0.c