15 KiB
15 KiB
04 — 板端推理部署详解
一、整体架构
┌─────────────────────────────────────────────────┐
│ 保护装置应用层主线程 │
│ │
│ ┌─────────────────┐ ┌──────────────────────┐ │
│ │ 录波/采样模块 │ │ CAN 协议栈 (PRIO_BURST)│ │
│ │ (ADC 驱动) │ │ can_protocol_push_*() │ │
│ └───────┬─────────┘ └──────────▲───────────┘ │
│ │ 滑动窗口 buffer │ 故障分类结果 │
│ ▼ │ │
│ ┌──────────────────────────────┐ │ │
│ │ NPU 推理线程 (SCHED_FIFO) │─┘ │
│ │ • rknn_run() │ │
│ │ • 1~3ms 延迟 │ │
│ │ • 双缓冲, 零拷贝 │ │
│ └──────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐│
│ │ librknnrt.so (RKNN Runtime) ││
│ │ └─ /dev/rknpu 或 npu_transfer_proxy ││
│ └──────────────────────────────────────────────┘│
└──────────────────────────────────────────────────┘
二、最简推理 Demo
code/inference_demo.c — 跑通后集成到保护程序:
/**
* 电力波形故障识别 — RK3568 NPU 板端推理 Demo
*
* 编译:
* ${CROSS_COMPILE}gcc -o fault_infer inference_demo.c \
* -lrknnrt -lpthread -lm
*
* 运行:
* ./fault_infer fault_model.rknn test_waveform.npy
*
* 依赖:
* - librknnrt.so (NPU 运行时, 板端已预装)
* - rknn_api.h (从 SDK 的 include/ 路径获取)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include <math.h>
#include "rknn_api.h"
/* ================================================================
* 配置 (需与训练/转换时保持一致)
* ================================================================ */
#define MODEL_INPUT_CH 1
#define MODEL_SEQ_LEN 128
#define MODEL_NUM_CLASSES 5
/* 归一化参数 (从训练集统计) */
#define NORM_MEAN 0.0f
#define NORM_STD 1.0f
/* 故障类型名称 */
static const char *fault_names[] = {
"正常",
"A相单相接地",
"BC相间短路",
"AB两相接地",
"三相短路",
};
/* ================================================================
* 1. 模型加载
* ================================================================ */
typedef struct {
rknn_context ctx;
rknn_input inputs[1];
rknn_output outputs[1];
rknn_tensor_attr input_attr;
rknn_tensor_attr output_attr;
uint8_t *model_data;
uint32_t model_size;
} nnd_context_t;
int npu_init(npu_context_t *npu, const char *model_path)
{
int ret;
memset(npu, 0, sizeof(*npu));
/* 1. 读取 .rknn 模型文件到内存 */
FILE *fp = fopen(model_path, "rb");
if (!fp) {
perror("fopen model");
return -1;
}
fseek(fp, 0, SEEK_END);
npu->model_size = (uint32_t)ftell(fp);
fseek(fp, 0, SEEK_SET);
npu->model_data = (uint8_t *)malloc(npu->model_size);
if (!npu->model_data) {
fclose(fp);
return -1;
}
size_t n = fread(npu->model_data, 1, npu->model_size, fp);
fclose(fp);
if (n != npu->model_size) {
fprintf(stderr, "模型文件读取不完整\n");
return -1;
}
printf("模型文件加载: %s (%u bytes)\n", model_path, npu->model_size);
/* 2. 初始化 RKNN 上下文 */
ret = rknn_init(&npu->ctx, npu->model_data, npu->model_size,
0, NULL);
if (ret < 0) {
fprintf(stderr, "rknn_init 失败: %d\n", ret);
return -1;
}
printf("RKNN 初始化成功\n");
/* 3. 查询输入属性 */
npu->input_attr.index = 0;
ret = rknn_query(npu->ctx, RKNN_QUERY_INPUT_ATTR,
&npu->input_attr, sizeof(npu->input_attr));
if (ret < 0) {
fprintf(stderr, "查询输入属性失败: %d\n", ret);
return -1;
}
printf("输入: fmt=%d, n_dims=%d, dims=[%d,%d,%d,%d]\n",
npu->input_attr.fmt,
npu->input_attr.n_dims,
npu->input_attr.dims[0],
npu->input_attr.dims[1],
npu->input_attr.dims[2],
npu->input_attr.dims[3]);
/* 4. 查询输出属性 */
npu->output_attr.index = 0;
ret = rknn_query(npu->ctx, RKNN_QUERY_OUTPUT_ATTR,
&npu->output_attr, sizeof(npu->output_attr));
if (ret < 0) {
fprintf(stderr, "查询输出属性失败: %d\n", ret);
return -1;
}
printf("输出: fmt=%d, n_dims=%d, size=%d\n",
npu->output_attr.fmt,
npu->output_attr.n_dims,
npu->output_attr.size);
/* 5. 预设输入结构 (减少重复赋值) */
npu->inputs[0].index = 0;
npu->inputs[0].type = RKNN_TENSOR_FLOAT32;
npu->inputs[0].size = MODEL_INPUT_CH * MODEL_SEQ_LEN * sizeof(float);
npu->inputs[0].fmt = RKNN_TENSOR_NCHW;
npu->inputs[0].pass_through = 0; /* 不跳过预处理 */
return 0;
}
/* ================================================================
* 2. 预处理: 滑动窗口 + 归一化
* ================================================================ */
/**
* 从采样环形缓冲区提取一个窗口并归一化
*
* @param raw_samples 原始采样值 (环形缓冲区)
* @param offset 窗口起始偏移
* @param window_buf 输出: 归一化后的窗口 [1][1][128] (NCHW 格式)
*/
void preprocess_window(const float *raw_samples, int offset,
float *window_buf)
{
/* Z-score 归一化 */
for (int i = 0; i < MODEL_SEQ_LEN; i++) {
float val = raw_samples[offset + i];
window_buf[i] = (val - NORM_MEAN) / NORM_STD;
}
/* NCHW 格式已通过一维数组表达: [1][1][128] = window_buf[0..127] */
}
/* ================================================================
* 3. 推理
* ================================================================ */
/**
* 执行一次 NPU 推理
*
* @param npu NPU 上下文
* @param window_buf 预处理后的波形窗口 [128] float
* @param probs 输出: 各类别的概率 [NUM_CLASSES]
* @param predicted 输出: 预测类别
* @return 推理耗时 (ms), <0 表示错误
*/
float npu_inference(npu_context_t *npu,
const float *window_buf,
float *probs,
int *predicted)
{
int ret;
struct timespec t0, t1;
/* 设置输入 */
npu->inputs[0].buf = (void *)window_buf;
ret = rknn_inputs_set(npu->ctx, 1, npu->inputs);
if (ret < 0) {
fprintf(stderr, "rknn_inputs_set 失败: %d\n", ret);
return -1.0f;
}
/* 推理 */
clock_gettime(CLOCK_MONOTONIC, &t0);
ret = rknn_run(npu->ctx, NULL);
clock_gettime(CLOCK_MONOTONIC, &t1);
if (ret < 0) {
fprintf(stderr, "rknn_run 失败: %d\n", ret);
return -1.0f;
}
/* 获取输出 */
npu->outputs[0].want_float = 1; /* 要求输出 float 格式 */
npu->outputs[0].buf = probs;
ret = rknn_outputs_get(npu->ctx, 1, npu->outputs, NULL);
if (ret < 0) {
fprintf(stderr, "rknn_outputs_get 失败: %d\n", ret);
return -1.0f;
}
/* 计算耗时 */
float elapsed_ms = (t1.tv_sec - t0.tv_sec) * 1000.0f +
(t1.tv_nsec - t0.tv_nsec) / 1.0e6f;
/* 释放输出 (必须调用,否则内存泄漏) */
rknn_outputs_release(npu->ctx, 1, npu->outputs);
/* Softmax + argmax */
float max_prob = probs[0];
int max_idx = 0;
float sum = 0.0f;
for (int i = 0; i < MODEL_NUM_CLASSES; i++) {
/* 数值稳定的 softmax */
float exp_val = expf(probs[i] - max_prob);
probs[i] = exp_val;
sum += exp_val;
}
for (int i = 0; i < MODEL_NUM_CLASSES; i++) {
probs[i] /= sum;
if (probs[i] > max_prob) {
max_prob = probs[i];
max_idx = i;
}
}
*predicted = max_idx;
return elapsed_ms;
}
/* ================================================================
* 4. 清理
* ================================================================ */
void npu_deinit(npu_context_t *npu)
{
if (npu->ctx) {
rknn_destroy(npu->ctx);
npu->ctx = 0;
}
free(npu->model_data);
npu->model_data = NULL;
printf("NPU 资源已释放\n");
}
/* ================================================================
* 5. main — 演示连续推理流程
* ================================================================ */
int main(int argc, char *argv[])
{
if (argc < 2) {
printf("用法: %s <model.rknn> [test_waveform.npy]\n", argv[0]);
return 1;
}
const char *model_path = argv[1];
/* ---- 初始化 NPU ---- */
npu_context_t npu;
if (npu_init(&npu, model_path) < 0) {
return 1;
}
/* ---- 准备测试数据 ---- */
float window_buf[MODEL_SEQ_LEN];
float probs[MODEL_NUM_CLASSES];
int predicted;
if (argc >= 3) {
/* 从 .npy 文件加载 (跳过 128 字节 NPY 头) */
printf("加载测试波形: %s\n", argv[2]);
/* 简化: 直接读取非 NPY 格式的 float 数组 */
FILE *fp = fopen(argv[2], "rb");
if (fp) {
fread(window_buf, sizeof(float), MODEL_SEQ_LEN, fp);
fclose(fp);
/* 归一化 */
for (int i = 0; i < MODEL_SEQ_LEN; i++) {
window_buf[i] = (window_buf[i] - NORM_MEAN) / NORM_STD;
}
} else {
printf("无法打开文件, 使用随机数据演示\n");
goto random_test;
}
} else {
random_test:
/* 生成模拟波形数据 (方波 + 噪声, 用于演示) */
srand(time(NULL));
for (int i = 0; i < MODEL_SEQ_LEN; i++) {
window_buf[i] = (i > 30 && i < 70) ? 2.0f : 0.0f; /* 模拟突变 */
window_buf[i] += (rand() / (float)RAND_MAX - 0.5f) * 0.1f;
}
}
/* ---- 连续推理 ---- */
printf("\n开始推理...\n");
printf("%-10s %-6s %-12s %s\n", "次数", "类别", "置信度", "耗时");
for (int run = 0; run < 20; run++) {
float elapsed = npu_inference(&npu, window_buf, probs, &predicted);
if (elapsed < 0) break;
printf("%-10d %-6d %-12s %-6.2f ms\n",
run + 1,
predicted,
fault_names[predicted],
(double)elapsed);
/* 模拟滑动窗口: 偏移半个窗口 (64点) */
float shifted[MODEL_SEQ_LEN];
memcpy(shifted, window_buf + 64, (MODEL_SEQ_LEN - 64) * sizeof(float));
for (int i = 0; i < 64; i++)
shifted[MODEL_SEQ_LEN - 64 + i] =
(rand() / (float)RAND_MAX - 0.5f) * 0.1f;
memcpy(window_buf, shifted, sizeof(window_buf));
}
/* ---- 清理 ---- */
npu_deinit(&npu);
return 0;
}
三、编译与运行
3.1 Makefile
# NPU 推理 Demo — Makefile
# 在 RK3568 板端编译: make CROSS_COMPILE=aarch64-buildroot-linux-gnu-
CROSS_COMPILE ?=
CC = $(CROSS_COMPILE)gcc
CFLAGS = -Wall -O2 -std=gnu99
LDFLAGS = -lrknnrt -lpthread -lm
TARGET = fault_infer
SRCS = inference_demo.c
all: $(TARGET)
$(TARGET): $(SRCS)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
clean:
rm -f $(TARGET)
test: $(TARGET)
./$(TARGET) deploy/fault_model.rknn
3.2 运行
# 编译
make
# 运行 (需要 root 权限访问 NPU)
sudo ./fault_infer deploy/fault_model.rknn
# 输出示例:
# 模型文件加载: deploy/fault_model.rknn (29184 bytes)
# RKNN 初始化成功
# 输入: fmt=2, n_dims=4, dims=[1,1,128,1]
# 输出: fmt=2, n_dims=2, size=20
#
# 开始推理...
# 次数 类别 置信度 耗时
# 1 2 BC相间短路 1.82 ms
# 2 2 BC相间短路 1.65 ms
# 3 5 Normal 1.71 ms
# ...
# NPU 资源已释放
四、集成到保护程序的关键修改
// === 在你的保护程序主循环中 ===
// 1. 初始化 NPU (程序启动时)
npu_context_t npu;
npu_init(&npu, "/opt/protect/fault_model.rknn");
// 2. 注册信号处理 (优雅退出时释放 NPU)
signal(SIGTERM, sig_handler);
// void sig_handler(int sig) { npu_deinit(&npu); exit(0); }
// 3. 主循环 (与采样和 CAN 协议栈协作)
float ring_buffer[1024 * 1024]; // 环形缓冲区
int ring_pos = 0;
while (running) {
// 等待新采样数据...
// ring_buffer[ring_pos] = new_sample;
// ring_pos = (ring_pos + 1) % BUF_SIZE;
if (ring_pos >= MODEL_SEQ_LEN && ring_pos % 64 == 0) {
// 每 64 点跑一次推理 (滑动窗口 50% 重叠)
float window[MODEL_SEQ_LEN];
preprocess_window(ring_buffer, ring_pos - MODEL_SEQ_LEN, window);
float probs[MODEL_NUM_CLASSES];
int predicted;
float elapsed = npu_inference(&npu, window, probs, &predicted);
if (predicted != 0) { // 非 Normal
// 通过 CAN 协议栈突发上送故障信息
can_protocol_push_yx_burst(
FAULT_ADDR, // 故障信号地址
(uint8_t)predicted, // 故障类型编码
get_time_ms() // 时标
);
// 如果是严重故障 (相间短路/三相短路)
if (predicted >= 2) {
// 可以触发录波、保护动作等
trigger_wave_recording();
}
}
}
}
// 4. 清理
npu_deinit(&npu);
五、性能优化
5.1 零拷贝模式(生产环境)
/* 使用 rknn_set_io_mem 替代 rknn_inputs_set */
// 需要先分配 DMA 物理连续内存
#include <dma-buf.h>
int dma_fd = dma_buf_alloc(MODEL_SEQ_LEN * sizeof(float), O_RDWR);
void *dma_buf = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED, dma_fd, 0);
rknn_tensor_mem mem;
mem.virt_addr = dma_buf;
mem.fd = dma_fd;
mem.size = MODEL_SEQ_LEN * sizeof(float);
// 绑定到 NPU 输入
rknn_set_io_mem(npu.ctx, RKNN_TENSOR_INPUT, &npu.input_attr, &mem);
// 之后直接往 dma_buf 写数据, 调用 rknn_run 即可
// 不需要每次 rknn_inputs_set, 省去一次 memcpy
5.2 双缓冲流水线
Buffer A (预处理) ──→ NPU 推理 ──→ 结果处理
│
Buffer B (预处理) ──→ (等待) ──→ NPU 推理 ──→ ...
将预处理和 NPU 推理时间重叠,吞吐量翻倍。
5.3 预期性能
| 指标 | 预期值 |
|---|---|
| 单次推理延迟 | 1~3 ms |
| 连续推理吞吐 | 300~500 fps |
| 内存占用 (模型) | 30~100 KB |
| 内存占用 (运行时) | ~2 MB |
| CPU 占用 (推理期间) | < 5% (NPU 计算, CPU 等待) |