# 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 #include #include #include #include #include #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 [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 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 等待) |