14 KiB
14 KiB
05 — NPU 推理结果与 CAN 协议栈整合
一、为什么整合?
单独的故障识别模型输出只是"有故障了",真正产生保护动作价值的是把故障信息实时推送到整个变电站自动化系统。CAN 总线是间隔层装置之间最直接的通路。
二、故障信号编码映射
2.1 在 XML 配置中新增故障类型遥信
在 config/protection_relay.xml 的 <signals type="yx"> 中添加:
<!-- ===== NPU 故障识别遥信 (CAN 协议栈 v2.0) ===== -->
<signal addr="0x0100" name="NPU故障识别-正常" period_ms="0" cos_en="1" />
<signal addr="0x0101" name="NPU故障识别-A相接地" period_ms="0" cos_en="1" />
<signal addr="0x0102" name="NPU故障识别-BC相间短路" period_ms="0" cos_en="1" />
<signal addr="0x0103" name="NPU故障识别-AB两相接地" period_ms="0" cos_en="1" />
<signal addr="0x0104" name="NPU故障识别-三相短路" period_ms="0" cos_en="1" />
<signal addr="0x0105" name="NPU故障识别-置信度" period_ms="0" cos_en="1" />
<signal addr="0x0106" name="NPU故障识别-推理耗时ms" period_ms="0" cos_en="1" />
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)。
三、主程序整合流程
/**
* 保护装置主程序 — NPU + CAN 协议栈 整合示例
*
* 整合了:
* - learn/CAN/ 的 CAN 协议栈 (v2.0)
* - learn/NPU/ 的波形故障识别
*
* 数据流:
* 采样 → 滑动窗口 → NPU 推理 → CAN 突发上送 → 数据中心
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <pthread.h>
#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 检测到严重故障时,除了突发送遥信,还可以触发录波:
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 → 自动上传
}