# 05 — NPU 推理结果与 CAN 协议栈整合 ## 一、为什么整合? 单独的故障识别模型输出只是"有故障了",真正产生保护动作价值的是**把故障信息实时推送到整个变电站自动化系统**。CAN 总线是间隔层装置之间最直接的通路。 ## 二、故障信号编码映射 ### 2.1 在 XML 配置中新增故障类型遥信 在 `config/protection_relay.xml` 的 `` 中添加: ```xml ``` `period_ms="0"` 表示不周期上送(按需突发),`cos_en="1"` 表示变位主动上送。 ### 2.2 故障类型 → DPS 编码 | NPU 输出类别 | DPS 编码 | 含义 | |:---:|:---:|------| | 0 (Normal) | 01 | 正常运行时状态 (分=正常, 合=故障) | | 1 (A-G) | 10 | A相接地故障 | | 2 (BC) | 10 | BC相间短路故障 | | 3 (AB-G) | 10 | AB两相接地故障 | | 4 (ABC) | 10 | 三相短路故障 | 当 NPU 检测到故障时,对应的遥信点变为 10(合=DPS ON),同时"正常"变为 01(分=DPS OFF)。 ## 三、主程序整合流程 ```c /** * 保护装置主程序 — NPU + CAN 协议栈 整合示例 * * 整合了: * - learn/CAN/ 的 CAN 协议栈 (v2.0) * - learn/NPU/ 的波形故障识别 * * 数据流: * 采样 → 滑动窗口 → NPU 推理 → CAN 突发上送 → 数据中心 */ #include #include #include #include #include #include #include #include "can_protocol.h" /* CAN 协议栈 v2.0 */ #include "rknn_api.h" /* NPU 推理 API */ /* ================================================================ * 配置 * ================================================================ */ #define MODEL_PATH "/opt/protect/fault_model.rknn" #define XML_CONFIG "/opt/protect/protection_relay.xml" #define CAN_IF "can0" #define SEQ_LEN 128 /* 波形窗口长度 */ #define SLIDE_STRIDE 64 /* 滑动步长 (50% 重叠) */ #define NUM_CLASSES 5 #define RING_BUF_SIZE (1024 * 1024) /* 环形缓冲区 (约1秒录波) */ /* 故障遥信地址映射 (与 XML 配置对齐) */ #define YX_ADDR_NORMAL 0x0100 /* 正常 */ #define YX_ADDR_BASE 0x0101 /* A相接地 (后续+类别-1) */ #define YX_ADDR_CONF 0x0105 /* 置信度 */ #define YX_ADDR_LATENCY 0x0106 /* 推理耗时 */ /* ================================================================ * 全局状态 * ================================================================ */ static volatile int g_running = 1; /* 环形采样缓冲区 */ typedef struct { float *data; int capacity; int write_pos; int ready_count; /* 可读取的采样点数 */ pthread_mutex_t lock; } ring_buf_t; static ring_buf_t g_ring; /* NPU 上下文 */ typedef struct { /* 见 04-板端推理部署.md 中的 npu_context_t */ } npu_context_t; static npu_context_t g_npu; /* ================================================================ * 信号处理 * ================================================================ */ static void sig_handler(int sig) { printf("\n收到信号 %d, 退出...\n", sig); g_running = 0; can_protocol_stop(); } /* ================================================================ * NPU 推理线程 * ================================================================ */ static void *npu_thread(void *arg) { (void)arg; float window[SEQ_LEN]; float probs[NUM_CLASSES]; int predicted; int last_fault = 0; /* 上一次的故障类型 (用于变位检测) */ printf("[NPU] 推理线程启动\n"); while (g_running) { /* 等待累积足够的采样点 */ pthread_mutex_lock(&g_ring.lock); if (g_ring.ready_count < SEQ_LEN) { pthread_mutex_unlock(&g_ring.lock); usleep(500); /* 500μs = 2kHz 采样, 等够 128 点约需 64ms */ continue; } /* 复制窗口数据 */ int start = (g_ring.write_pos - SEQ_LEN + g_ring.capacity) % g_ring.capacity; for (int i = 0; i < SEQ_LEN; i++) { int idx = (start + i) % g_ring.capacity; window[i] = g_ring.data[idx]; } g_ring.ready_count -= SLIDE_STRIDE; /* 消费了 64 点 */ pthread_mutex_unlock(&g_ring.lock); /* Z-score 归一化 */ float sum = 0, sq_sum = 0; for (int i = 0; i < SEQ_LEN; i++) { sum += window[i]; } float mean = sum / SEQ_LEN; for (int i = 0; i < SEQ_LEN; i++) { float diff = window[i] - mean; sq_sum += diff * diff; } float std = sqrtf(sq_sum / SEQ_LEN); if (std < 1e-6f) std = 1.0f; /* 防止除零 */ for (int i = 0; i < SEQ_LEN; i++) { window[i] = (window[i] - mean) / std; } /* NPU 推理 */ float elapsed = npu_inference(&g_npu, window, probs, &predicted); /* 故障变位检测 */ if (predicted != last_fault) { printf("[NPU] 故障状态变化: %d → %d (置信度: %.2f%%, 耗时: %.2fms)\n", last_fault, predicted, (double)(probs[predicted] * 100.0f), (double)elapsed); uint32_t ts = (uint32_t)(time(NULL) * 1000); /* ms 时标 */ /* ================================================ * 关键: 通过 CAN 协议栈突发上送故障信息 * ================================================ */ /* 1. 推送故障类型遥信 (当前故障种类) */ if (predicted > 0) { /* 新的故障 */ can_protocol_push_yx_burst( YX_ADDR_BASE + (predicted - 1), /* 故障地址 */ YX_DPS_ON, /* 合=故障 */ ts ); /* 清除"正常"状态 */ can_protocol_push_yx_burst( YX_ADDR_NORMAL, YX_DPS_OFF, ts ); } else { /* 恢复正常 */ can_protocol_push_yx_burst( YX_ADDR_NORMAL, YX_DPS_ON, ts ); /* 清除上一个故障状态 */ if (last_fault > 0) { can_protocol_push_yx_burst( YX_ADDR_BASE + (last_fault - 1), YX_DPS_OFF, ts ); } } /* 2. 推送置信度遥信 (作为辅助信息) */ /* 置信度 × 100 取整, 通过遥测突发上送 */ can_protocol_push_yc_burst( YX_ADDR_CONF, /* 置信度地址 */ probs[predicted], /* float 值 */ ts ); /* 3. 推送推理耗时 (用于监测 NPU 状态) */ can_protocol_push_yc_burst( YX_ADDR_LATENCY, elapsed, /* float ms */ ts ); last_fault = predicted; } } printf("[NPU] 推理线程退出\n"); return NULL; } /* ================================================================ * CAN 协议栈回调 (与 learn/CAN/app/test_protocol.c 相同) * ================================================================ */ static int yx_read_cb(uint16_t addr, uint8_t *bitmap, int max_bytes, void *arg) { (void)arg; (void)addr; memset(bitmap, 0, max_bytes); /* 占位 */ return 0; } static int yc_read_cb(uint16_t addr, float *value, void *arg) { (void)arg; (void)addr; *value = 0.0f; /* 占位 */ return 0; } static int yk_cb(uint8_t point, uint8_t op, void *arg) { (void)arg; printf("[CAN] 遥控请求: 点=%u 操作=%s\n", point, op == 0 ? "选择" : op == 1 ? "执行" : "撤销"); return 0; } static int dz_cb(uint16_t addr, float value, void *arg) { (void)arg; printf("[CAN] 定值请求: 地址=0x%04X 值=%.3f\n", addr, (double)value); return 0; } /* ================================================================ * main * ================================================================ */ int main(int argc, char *argv[]) { (void)argc; (void)argv; /* ---- 1. 初始化 CAN 协议栈 ---- */ printf("===== 保护装置启动 (NPU + CAN) =====\n"); if (can_protocol_init(XML_CONFIG) < 0) { fprintf(stderr, "CAN 协议栈初始化失败!\n"); return 1; } can_protocol_register_yx_read_callback(yx_read_cb, NULL); can_protocol_register_yc_read_callback(yc_read_cb, NULL); can_protocol_register_yk_callback(yk_cb, NULL); can_protocol_register_dz_callback(dz_cb, NULL); if (can_protocol_bind(CAN_IF) < 0) { fprintf(stderr, "CAN 绑定失败\n"); can_protocol_deinit(); return 1; } /* ---- 2. 初始化 NPU ---- */ if (npu_init(&g_npu, MODEL_PATH) < 0) { fprintf(stderr, "NPU 初始化失败\n"); can_protocol_deinit(); return 1; } /* ---- 3. 初始化环形缓冲区 ---- */ g_ring.capacity = RING_BUF_SIZE; g_ring.data = calloc(RING_BUF_SIZE, sizeof(float)); g_ring.write_pos = 0; g_ring.ready_count = 0; pthread_mutex_init(&g_ring.lock, NULL); /* ---- 4. 启动 NPU 推理线程 ---- */ pthread_t npu_tid; pthread_create(&npu_tid, NULL, npu_thread, NULL); /* ---- 5. 信号处理 ---- */ signal(SIGINT, sig_handler); signal(SIGTERM, sig_handler); /* ---- 6. CAN 协议栈主循环 (接管主线程) ---- */ printf("进入主循环... (Ctrl-C 退出)\n"); can_protocol_run(); /* ---- 7. 清理 ---- */ g_running = 0; pthread_join(npu_tid, NULL); pthread_mutex_destroy(&g_ring.lock); free(g_ring.data); npu_deinit(&g_npu); can_protocol_deinit(); printf("保护装置正常退出\n"); return 0; } ``` ## 四、整合后的数据流全景 ``` ┌──────────┐ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │ ADC 采样 │ ──►│ 环形缓冲区 │ ──►│ NPU 推理线程 │ ──►│ CAN 协议栈 │ │ (2kHz) │ │ (1M 浮点) │ │ (每64点推理) │ │ (突发队列) │ └──────────┘ └─────────────┘ └──────┬───────┘ └───────┬───────┘ │ │ ┌──────────────┘ │ ▼ ▼ ┌──────────────────┐ ┌────────────────────┐ │ 故障分类: │ │ 紧急: MSG_URG_PROTECT│ │ • A相接地 → 0x0101│ │ 突发: MSG_PD_YX_BURST│ │ • BC相间 → 0x0102│ │ │ │ • 置信度 → 0x0105│ │ PRIO_BURST(001) │ │ • 推理耗时→ 0x0106│ │ → CAN FD 64字节 │ └──────────────────┘ │ → 9个遥信点/帧 │ └─────────┬──────────┘ │ CAN 总线 ─▼──► 数据中心 ``` ## 五、时标一致性 NPU 推理结果的时标来源选择: | 来源 | 精度 | 适用场景 | |------|:---:|------| | `time(NULL)` | ±1 秒 | 只关心"什么时候发生的" | | `clock_gettime(CLOCK_MONOTONIC)` | ±1 μs | 精准 SOE 分析 | | 采样硬件打时戳 | ±1 μs | **最佳**: 时标 = 窗口第一个采样点的时间 | > 推荐:在环形缓冲区中同时存储"采样点 + 时标",NPU 推理时取窗口第一个点的时标作为故障发生时间。这样定位最准。 ## 六、录波文件召唤联动 当 NPU 检测到严重故障时,除了突发送遥信,还可以触发录波: ```c if (predicted >= 2) { // 相间短路或更严重 // 1. 从环形缓冲区 dump 故障前后各 5 个周波的录波数据 save_comtrade("rec/wave_%u.dat", timestamp); // 2. 主站可通过文件召唤获取录波文件 // 主站: can_protocol_request_file(dst, file_id, "rec/wave_*.dat") // 从站: handle_ft_frame() → MSG_FT_READ_REQ → 自动上传 } ```