【RK3588S 嵌入式AI系列③】RKNN SDK快速上手:第一个NPU推理程序全流程
系列导读:前两篇搞定了硬件认知和开发环境。这一篇正式进入 RKNN 推理开发——用 MobileNetV2 图像分类任务,完整走一遍"PyTorch模型 → RKNN格式转换 → 板端C++推理"的全流程。跑通这篇,你就掌握了 RK3588S NPU 推理开发的核心骨架,后续所有任务都是在这个骨架上扩展。
一、RKNN推理开发的整体流程
在写任何代码之前,先建立全局认知。RKNN 推理开发分为两个阶段,运行在两台不同的机器上:
【PC 端(Ubuntu 开发机)】
PyTorch/ONNX 模型
↓ RKNN-Toolkit2(Python)
.rknn 模型文件(含量化参数)
↓ adb push / scp
【板端(RK3588S)】
.rknn 模型文件
↓ librknnrt.so(C API)
推理结果
PC 端:负责模型转换和量化,使用 Python API(rknn-toolkit2),只在开发阶段运行一次。
板端:负责实际推理,使用 C/C++ API(rknn_api.h + librknnrt.so),是最终产品运行的代码。
这两套 API 完全独立,初学者经常混淆——记住:Python API 是转换工具,C API 是推理引擎。
二、准备模型:获取 MobileNetV2
我们用 MobileNetV2 做图像分类,这是入门推理开发的经典选择:模型小(14MB)、推理快、结果直观。
2.1 导出 ONNX 格式
在 PC 开发机上执行:
# export_mobilenetv2.py
import torch
import torchvision.models as models
# 加载预训练模型
model = models.mobilenet_v2(pretrained=True)
model.eval()
# 创建示例输入(batch=1, RGB, 224×224)
dummy_input = torch.randn(1, 3, 224, 224)
# 导出 ONNX
torch.onnx.export(
model,
dummy_input,
"mobilenetv2.onnx",
opset_version=12, # RKNN-Toolkit2 推荐 opset 11-13
input_names=["input"],
output_names=["output"],
dynamic_axes=None # 边缘端固定 batch=1,不用动态 shape
)
print("导出成功:mobilenetv2.onnx")
source ~/rknn-env/bin/activate
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
python3 export_mobilenetv2.py
💡 也可以直接用 RKNN Model Zoo:
ls ~/rknn-toolkit2/rknn_model_zoo/models/ # 里面有预置的 mobilenet_v2.onnx,可以跳过导出步骤直接用
三、PC端:模型转换(ONNX → RKNN)
3.1 准备量化校准数据集
INT8 量化需要少量真实图片做校准,100张足够:
# 从 ImageNet 验证集随机取100张,或直接用网上的样例图
mkdir -p ~/rknn_demo/dataset
# 把100张 jpg 图片放入该目录,文件名任意
# 生成数据集列表文件(RKNN-Toolkit2 需要)
ls ~/rknn_demo/dataset/*.jpg > ~/rknn_demo/dataset.txt
3.2 转换脚本
# convert_mobilenetv2.py
from rknn.api import RKNN
ONNX_MODEL = "mobilenetv2.onnx"
RKNN_MODEL = "mobilenetv2.rknn"
DATASET = "./dataset.txt"
# ── 初始化 ──────────────────────────────────────
rknn = RKNN(verbose=False)
# ── 配置转换参数 ─────────────────────────────────
rknn.config(
mean_values=[[123.675, 116.28, 103.53]], # ImageNet 均值(RGB)
std_values=[[58.395, 57.12, 57.375]], # ImageNet 标准差
target_platform="rk3588", # 目标芯片
quantized_dtype="asymmetric_quantized-8", # INT8 非对称量化
quantized_algorithm="normal", # 普通校准,速度快
optimization_level=3 # 最高优化等级
)
# ── 加载 ONNX 模型 ───────────────────────────────
print(">>> 加载 ONNX 模型...")
ret = rknn.load_onnx(model=ONNX_MODEL)
assert ret == 0, "加载失败"
# ── 构建 RKNN 模型(含量化)────────────────────────
print(">>> 构建并量化模型...")
ret = rknn.build(do_quantization=True, dataset=DATASET)
assert ret == 0, "构建失败"
# ── 导出 .rknn 文件 ──────────────────────────────
print(">>> 导出 RKNN 模型...")
ret = rknn.export_rknn(RKNN_MODEL)
assert ret == 0, "导出失败"
print(f"✅ 转换完成:{RKNN_MODEL}")
rknn.release()
python3 convert_mobilenetv2.py
# 耗时约 2-5 分钟(取决于校准数据集大小)
3.3 PC端模拟推理验证(可选但推荐)
转换完之后先在 PC 上模拟推理,确认模型没问题,不用频繁推到板子:
# pc_infer_test.py
from rknn.api import RKNN
from PIL import Image
import numpy as np
rknn = RKNN()
rknn.load_rknn("mobilenetv2.rknn")
rknn.init_runtime() # PC 模拟运行,不需要连板子
# 准备输入:加载图片,归一化
img = Image.open("test.jpg").resize((224, 224))
img = np.array(img, dtype=np.float32)
img = np.expand_dims(img, axis=0) # 增加 batch 维度
# 推理
outputs = rknn.inference(inputs=[img])
top5 = np.argsort(outputs[0][0])[::-1][:5]
print("Top-5 类别索引:", top5)
rknn.release()
⚠️ 常见问题:PC 端模拟推理的精度与板端结果可能有细微差异(<1%),这是正常的,由量化误差引起,不影响实际使用。
四、板端:C++推理程序
这是本篇最核心的部分。我们用 C++ 调用 RKNN C API 写一个完整的推理程序。
4.1 RKNN C API 核心概念
在写代码之前,先理解 5 个核心 API:
| API | 作用 |
|---|---|
rknn_init() |
加载 .rknn 模型,初始化推理上下文 |
rknn_inputs_set() |
设置输入张量数据 |
rknn_run() |
执行一次推理 |
rknn_outputs_get() |
获取推理输出结果 |
rknn_destroy() |
释放资源 |
整个推理流程就是这 5 步,非常清晰。
4.2 完整推理程序
// main.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <algorithm>
#include <vector>
#include "rknn_api.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h" // 轻量图片加载库,无需 OpenCV
// ── 工具函数:加载文件到内存 ────────────────────────
static unsigned char* load_model(const char* path, int* size) {
FILE* fp = fopen(path, "rb");
if (!fp) { fprintf(stderr, "无法打开模型文件: %s\n", path); return nullptr; }
fseek(fp, 0, SEEK_END);
*size = ftell(fp);
fseek(fp, 0, SEEK_SET);
unsigned char* data = (unsigned char*)malloc(*size);
fread(data, 1, *size, fp);
fclose(fp);
return data;
}
// ── Top-K 结果 ──────────────────────────────────
void print_topk(float* probs, int num_classes, int k = 5) {
std::vector<std::pair<float, int>> scores;
for (int i = 0; i < num_classes; i++)
scores.push_back({probs[i], i});
std::sort(scores.begin(), scores.end(), std::greater<>());
printf("\n===== Top-%d 推理结果 =====\n", k);
for (int i = 0; i < k; i++)
printf(" [%d] 类别 %4d 置信度: %.4f\n", i+1, scores[i].second, scores[i].first);
}
int main(int argc, char* argv[]) {
if (argc != 3) {
printf("用法: %s <model.rknn> <image.jpg>\n", argv[0]);
return -1;
}
const char* model_path = argv[1];
const char* image_path = argv[2];
// ── 1. 加载模型 ──────────────────────────────
int model_size = 0;
unsigned char* model_data = load_model(model_path, &model_size);
rknn_context ctx;
int ret = rknn_init(&ctx, model_data, model_size, 0, NULL);
if (ret < 0) { fprintf(stderr, "rknn_init 失败: %d\n", ret); return -1; }
free(model_data); // 模型已加载到 NPU,原始数据可释放
// ── 2. 查询模型输入输出信息 ───────────────────
rknn_input_output_num io_num;
rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num));
printf("输入张量数: %d 输出张量数: %d\n", io_num.n_input, io_num.n_output);
rknn_tensor_attr input_attr = {0};
input_attr.index = 0;
rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, &input_attr, sizeof(input_attr));
printf("输入尺寸: %dx%dx%d\n", input_attr.dims[2], input_attr.dims[1], input_attr.dims[3]);
// ── 3. 加载并预处理图片 ──────────────────────
int img_w, img_h, img_c;
unsigned char* img_data = stbi_load(image_path, &img_w, &img_h, &img_c, 3);
if (!img_data) { fprintf(stderr, "图片加载失败: %s\n", image_path); return -1; }
// 简单 resize(生产环境用 RGA,见第8篇)
int in_h = input_attr.dims[1]; // 224
int in_w = input_attr.dims[2]; // 224
// 此处省略 resize 代码,假设输入图片已是 224×224
// ── 4. 设置输入 ──────────────────────────────
rknn_input inputs[1] = {0};
inputs[0].index = 0;
inputs[0].type = RKNN_TENSOR_UINT8; // 输入 uint8,SDK 内部做归一化
inputs[0].size = in_w * in_h * 3;
inputs[0].fmt = RKNN_TENSOR_NHWC;
inputs[0].buf = img_data;
inputs[0].pass_through = 0; // 0 = SDK 自动做均值/方差归一化
ret = rknn_inputs_set(ctx, 1, inputs);
if (ret < 0) { fprintf(stderr, "rknn_inputs_set 失败: %d\n", ret); return -1; }
stbi_image_free(img_data);
// ── 5. 执行推理 ──────────────────────────────
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
ret = rknn_run(ctx, NULL);
clock_gettime(CLOCK_MONOTONIC, &t1);
float elapsed_ms = (t1.tv_sec - t0.tv_sec) * 1000.0f +
(t1.tv_nsec - t0.tv_nsec) / 1e6f;
printf("推理耗时: %.2f ms\n", elapsed_ms);
if (ret < 0) { fprintf(stderr, "rknn_run 失败: %d\n", ret); return -1; }
// ── 6. 获取输出 ──────────────────────────────
rknn_output outputs[1] = {0};
outputs[0].index = 0;
outputs[0].want_float = 1; // 自动反量化为 float32
ret = rknn_outputs_get(ctx, 1, outputs, NULL);
float* probs = (float*)outputs[0].buf;
int num_classes = outputs[0].size / sizeof(float); // MobileNetV2 = 1000
print_topk(probs, num_classes, 5);
// ── 7. 释放资源 ──────────────────────────────
rknn_outputs_release(ctx, 1, outputs);
rknn_destroy(ctx);
return 0;
}
4.3 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(rknn_classify)
set(CMAKE_C_COMPILER aarch64-none-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-none-linux-gnu-g++)
set(CMAKE_CXX_STANDARD 14)
set(RKNN_API_PATH "$ENV{HOME}/rknn-toolkit2/rknpu2/runtime/Linux/librknn_api")
add_executable(rknn_classify main.cpp)
target_include_directories(rknn_classify PRIVATE
${RKNN_API_PATH}/include
)
target_link_directories(rknn_classify PRIVATE
${RKNN_API_PATH}/aarch64
)
target_link_libraries(rknn_classify
rknnrt pthread dl m
)
4.4 编译与部署
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
# 推到板子
adb push rknn_classify /tmp/
adb push mobilenetv2.rknn /tmp/
adb push test.jpg /tmp/
adb shell "chmod +x /tmp/rknn_classify"
# 在板子上运行
adb shell "/tmp/rknn_classify /tmp/mobilenetv2.rknn /tmp/test.jpg"
预期输出:
输入张量数: 1 输出张量数: 1
输入尺寸: 224x224x3
推理耗时: 3.87 ms
===== Top-5 推理结果 =====
[1] 类别 285 置信度: 0.8921
[2] 类别 281 置信度: 0.0634
[3] 类别 282 置信度: 0.0241
...
MobileNetV2 的推理耗时约 3-5ms,这就是 NPU 单核 2 TOPS 的实际表现。使用三核并发可降至约 2ms(后续第7篇详述)。
五、关键参数详解
5.1 pass_through 参数
这是初学者最容易踩的坑:
inputs[0].pass_through = 0; // SDK 自动用 mean/std 做归一化(推荐)
inputs[0].pass_through = 1; // 原始数据直接送入,你自己负责归一化
当 pass_through = 0 时,SDK 会用转换时配置的 mean_values 和 std_values 自动做归一化,输入 uint8 图像数据即可,最省事。
当 pass_through = 1 时,你需要自己把图片归一化到模型要求的数值范围,适合有特殊前处理需求的场景。
初学阶段统一用 pass_through = 0。
5.2 want_float 参数
outputs[0].want_float = 1; // 自动反量化,输出 float32(方便调试)
outputs[0].want_float = 0; // 输出原始 int8,后处理需要手动反量化(性能略好)
调试阶段用 want_float = 1,省去手动反量化的麻烦。量产代码可以改成 0 并自己处理,减少一次类型转换的开销。
5.3 NPU core mask 设置
// 默认:SDK 自动分配一个核心
rknn_init(&ctx, model_data, model_size, 0, NULL);
// 指定使用 Core 0
rknn_set_core_mask(ctx, RKNN_NPU_CORE_0);
// 单模型使用三核(适合大模型降延迟)
rknn_set_core_mask(ctx, RKNN_NPU_CORE_0_1_2);
MobileNetV2 这种小模型用单核就够,三核并发反而因调度开销导致延迟增大。
六、常见报错与解决方案
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
rknn_init error ret=-1 |
模型文件路径错误或文件损坏 | 检查路径,重新转换模型 |
librknnrt.so: cannot open shared object file |
运行时库未部署到板子 | adb push librknnrt.so /usr/lib/ && adb shell ldconfig |
rknn_run error ret=-9 |
NPU 驱动版本与 SDK 不匹配 | 确保板子内核驱动版本与 librknnrt.so 版本一致 |
| 推理结果全为 0 或乱码 | mean/std 配置与模型不匹配 |
检查转换时的 mean_values/std_values 是否正确 |
| 推理耗时异常高(>50ms) | NPU 核心没有激活 | 检查 /sys/kernel/debug/rknpu/load,确认 NPU 在工作 |
七、完整项目结构
至此,一个最小可用的 RKNN 推理项目结构如下:
rknn_classify/
├── CMakeLists.txt
├── main.cpp
├── stb_image.h # 单头文件图片库,从 GitHub 下载
├── mobilenetv2.rknn # 转换好的模型(由 PC 端生成)
└── build/
└── rknn_classify # 编译产物(aarch64 可执行文件)
这个骨架是后续所有推理项目的模板,换一个 .rknn 模型文件,修改输入输出处理逻辑,就能适配任意任务。
八、总结与下篇预告
本篇完成的工作:
- 理解了 PC 端(转换)和板端(推理)两套 API 的分工
- 用 RKNN-Toolkit2 完成了 ONNX → RKNN 的 INT8 量化转换
- 用 C++ RKNN API 写出了完整的板端推理程序
- 掌握了
pass_through、want_float、core_mask三个关键参数
下一篇(系列第 4 篇)深入讲模型转换全流程,覆盖 PyTorch / TFLite / PaddlePaddle 三种框架的转换方式,以及常见算子不支持时的解决策略。
本系列文章列表(持续更新)
- ✅ 第1篇:硬件全解析:NPU/CPU/GPU架构与芯片选型指南
- ✅ 第2篇:Linux开发环境从零搭建
- ✅ 第3篇:RKNN SDK快速上手(本文)
- 🔜 第4篇:模型转换全流程
- 🔜 第5篇:INT8量化实战
- … 共16篇
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)