517 lines
15 KiB
Markdown
517 lines
15 KiB
Markdown
# 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` — 跑通后集成到保护程序:
|
|
|
|
```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
|
|
|
|
```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 运行
|
|
|
|
```bash
|
|
# 编译
|
|
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 资源已释放
|
|
```
|
|
|
|
## 四、集成到保护程序的关键修改
|
|
|
|
```c
|
|
// === 在你的保护程序主循环中 ===
|
|
|
|
// 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 零拷贝模式(生产环境)
|
|
|
|
```c
|
|
/* 使用 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 等待) |
|