369 lines
14 KiB
Markdown
369 lines
14 KiB
Markdown
# 05 — NPU 推理结果与 CAN 协议栈整合
|
||
|
||
## 一、为什么整合?
|
||
|
||
单独的故障识别模型输出只是"有故障了",真正产生保护动作价值的是**把故障信息实时推送到整个变电站自动化系统**。CAN 总线是间隔层装置之间最直接的通路。
|
||
|
||
## 二、故障信号编码映射
|
||
|
||
### 2.1 在 XML 配置中新增故障类型遥信
|
||
|
||
在 `config/protection_relay.xml` 的 `<signals type="yx">` 中添加:
|
||
|
||
```xml
|
||
<!-- ===== 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)。
|
||
|
||
## 三、主程序整合流程
|
||
|
||
```c
|
||
/**
|
||
* 保护装置主程序 — 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 检测到严重故障时,除了突发送遥信,还可以触发录波:
|
||
|
||
```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 → 自动上传
|
||
}
|
||
```
|