RTU/mimo/工程/libdatacenter模块分析.md

409 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# libdatacenter 模块分析
**日期**: 2026-06-12
**基于源码**: `src/system/libdatacenter/`7个文件约3000行
---
## 1. 模块定位
`libdatacenter` 是 RTU 的核心数据中枢负责所有信号的存储、检索、变更检测、值校验和控制流程管理。它是整个系统的数据总线所有模块iec61850、self_ptl、web_server、com_channel都通过它读写信号。
**核心职责**:
- 五大类信号的统一注册和存储out/in/yk/ao/param
- XXH128 哈希索引的高效信号查找
- 输出信号增量变更检测(脏队列→去重→比对→回调)
- SBO选择-执行)控制流程管理
- 参数配置持久化param.xml / self_param.xml 双文件机制)
- 事件队列系统(扰动/SOE/故障)
- 18 种数据类型的统一处理
## 2. 五张信号表
```cpp
static stru_datacenter g_datacenter = {
.signal_out, // 输出信号(遥测/遥信)
.signal_in, // 输入信号(链接到 out
.signal_yk, // 遥控信号
.signal_ao, // AO 信号SP 定值、定值区号)
.signal_param, // 参数信号(多定值区)
};
```
每张表的结构:
```cpp
typedef struct {
std::mutex mtx; // 表级互斥锁
uint32_t signal_id; // 自增 ID 分配器
hash_signal_map map_signals; // XXH128 hash → signal 的多重映射
std::vector<stru_signal*> id_index; // ID → 指针 O(1) 索引
} stru_signal_map;
```
## 3. XXH128 哈希索引
### 3.1 动机
信号以 `saddr`(字符串,如 `"PROT/LLN0$SG$sg1$StrVal"`)作为唯一标识。直接字符串比较在大量信号时效率低,因此使用 XXH3-128bits 对 saddr 计算 128 位哈希值,以哈希为键建立 `unordered_multimap`
### 3.2 哈希碰撞处理
使用 `unordered_multimap`(而非 `unordered_map`),相同哈希值允许多个条目共存。查找时先通过哈希定位桶,再逐个比对 saddr 字符串精确匹配:
```cpp
LOCAL stru_signal* dc_find_signal(const std::string &saddr, stru_signal_map &dc_signal_map) {
XXH128_hash_t hash = XXH3_128bits(saddr.c_str(), saddr.length());
auto range = dc_signal_map.map_signals.equal_range(hash);
for (auto it = range.first; it != range.second; ++it) {
if (it->second.saddr == saddr) return &it->second;
}
return nullptr;
}
```
### 3.3 自定义哈希/相等函子
```cpp
struct XXH128Hash {
size_t operator()(const XXH128_hash_t& h) const noexcept {
// 低64位 << 17 + 高64位 >> 13再混合
const uint64_t mix = h.low64 ^ (h.high64<<17) + (h.high64>>13);
return mix ^ (mix >> 32);
}
};
struct XXH128Equal {
// 优先用库的 XXH128_isEqual否则手动比较 high64 和 low64
};
```
## 4. 核心信号结构
```cpp
typedef struct stru_signal {
uint32_t id; // 自增 ID
XXH128_hash_t hash; // saddr 的 128 位哈希
std::string saddr; // 短地址(信号唯一标识)
std::string desc; // 描述
uint8_t data_type; // 数据类型18 种之一)
std::vector<void*> vec_p_data; // 数据指针列表(多条目,如多定值区)
void *p_last_data; // 上次数据指针(用于变更检测)
std::vector<void*> vec_p_default_data; // 默认值指针列表
std::vector<std::string> link_saddrs; // 关联的 in 信号 saddr
std::unordered_set<std::string> link_set; // O(1) 查重
uint8_t ctrl_type; // 控制类型SBO/Direct
stru_signal_param param; // 参数元数据min/max/step/unit
std::vector<std::pair<std::string, out_signal_change_cb>> out_change_cb_list; // out 变更回调
std::vector<std::pair<std::string, signal_change_cb>> change_cb_list; // 通用变更回调
std::string last_caller_module; // 最后修改者模块(回路阻断)
} stru_signal;
```
## 5. 信号注册 API
### 5.1 输出信号out
```cpp
// 简单注册
dc_signal_out(saddr, desc, data_type, p_data)
// 注册 + 变更回调
dc_signal_out_with_callback(saddr, desc, data_type, p_data, cb, module_id)
// 后续绑定(已注册信号追加回调)
dc_signal_out_link_with_callback(saddr, &p_data, cb, module_id)
// 设置值(触发脏标记)
dc_set_out_signal_val(saddr, set_data, module_id)
```
### 5.2 输入信号in
```cpp
dc_signal_in(saddr, desc, link_saddr, &p_data)
// link_saddr 指向一个已注册的 out 信号
// in 信号直接共享 out 信号的数据指针,不上报变更回调
```
### 5.3 遥控信号yk
```cpp
dc_signal_yk(saddr, desc, data_type, ctrl_type, p_data, cb, module_id)
dc_signal_yk_link_with_callback(saddr, &p_data, cb, module_id)
dc_signal_yk_set_status(saddr, step, ctrl, p_data, module_id)
// ctrl_type: SIGNAL_CTRL_TYPE::SBO_NORMAL 或 DIRECT_NORMAL
// step: SELECT → check → DIRECT/CANCEL
```
### 5.4 AO 信号
```cpp
dc_signal_ao(saddr, desc, data_type, ctrl_type, p_data, p_default_data, cb, module_id)
dc_signal_ao_link_with_callback(saddr, &p_data, cb, module_id)
dc_signal_ao_set_val(saddr, step, ctrl, p_data, module_id)
// step SELECT 时存值到 ctrl.p_dataDIRECT 时写入 p_signal->vec_p_data[0]
```
### 5.5 Param 信号(多定值区)
```cpp
dc_signal_param(saddr, desc, data_type, ctrl_type, pp_data, pp_default_data, data_num, cb, module_id)
dc_signal_param_link_with_callback(saddr, pp_data, data_num, cb, module_id)
dc_signal_param_set_val(saddr, step, ctrl, setting_zone, p_data, module_id)
dc_signal_param_set_val_without_check(saddr, data_type, setting_zone, p_data, module_id)
// setting_zone: 定值区号0-based写入 vec_p_data[setting_zone]
```
## 6. SBO 控制流程
SBOSelect Before Operate是电力系统遥控的标准流程
```
状态机: READY → SELECT → DIRECT/CANCEL → READY
dc_check_ctrl_valid(p_signal, step, ctrl, p_data):
SELECT 步骤:
step == SELECT → 允许
更新 ctrl.p_data = p_data暂存选择值
ctrl.step → SELECT
DIRECT 步骤:
仅当 ctrl.step == SELECT 时允许
dc_check_ctrl_val_valid(ctrl, p_data):
校验 DIRECT 传入的值与 SELECT 暂存的值完全一致
如果不一致 → 拒绝执行
写入 p_signal->vec_p_data[0]
ctrl.step → READY
CANCEL 步骤:
仅当 ctrl.step == SELECT 时允许
ctrl.step → READY
DIRECT_NORMAL 模式(无 SBO:
step == DIRECT → 直接写入,无 SELECT 校验
```
值校验覆盖 18 种数据类型,包括 IP 的 4 字节 memcmp、MAC 的 6 字节 memcmp、字符串的 strcmp 等。
## 7. 增量推送机制
### 7.1 脏信号队列
```cpp
static std::vector<stru_signal*> g_dirty_out_signals; // 脏队列
static std::mutex g_dirty_out_mutex;
static void dc_mark_signal_dirty(stru_signal *p_signal) {
std::lock_guard<std::mutex> lock(g_dirty_out_mutex);
g_dirty_out_signals.push_back(p_signal); // 追加到队列
}
```
调用 `dc_set_out_signal_val` 时自动标记脏。`
### 7.2 变更检测(每 100ms 执行)
```cpp
void dc_signal_out_change_check() {
// 1. swap 取出脏队列(锁内操作极短)
std::vector<stru_signal*> dirty;
{ std::lock_guard<std::mutex> lock(g_dirty_out_mutex);
dirty.swap(g_dirty_out_signals); }
// 2. 去重sort + unique
std::sort(dirty.begin(), dirty.end());
auto last = std::unique(dirty.begin(), dirty.end());
// 3. 逐信号比对 + 回调
for (auto it = dirty.begin(); it != last; ++it) {
if (dc_data_compare(data_type, vec_p_data[0], p_last_data) != 0) {
// 数据确实变化了
for (auto &[module_id, cb] : out_change_cb_list) {
if (module_id == last_caller_module) continue; // 回路阻断
cb(saddr, data_type, vec_p_data[0], p_last_data);
}
// 更新 last_data 为当前值
dc_set_signal_val(p_last_data, data_type, vec_p_data[0]);
}
}
}
```
### 7.3 回路阻断
`last_caller_module` 记录最后一次修改该信号的模块 ID。变更检测时跳过该模块自己的回调防止回路触发
```
iec61850m 修改信号 → dc_set_out_signal_val → last_caller_module = "iec61850m"
→ 100ms 后 dc_signal_out_change_check:
遍历回调: 跳过 module_id == "iec61850m" 的
→ iec61850s 的回调正常触发
```
## 8. 事件队列系统
### 8.1 三种事件队列
| 队列 | 数据类型 | 用途 |
|------|---------|------|
| `g_disturb_dd_queue` | `stru_disturb_dd` | 扰动录波触发 |
| `g_soe_queue` | `stru_dc_soe` | SOE 事件记录 |
| `g_fault_queue` | `stru_dc_fault` | 故障记录(含 SOE 列表) |
### 8.2 Push/Pop 模型
```
生产者(任意线程):
dc_disturb_dd_queue_push(dd)
→ lock → push → unlock
消费者datacenter_run_100ms每 100ms:
dc_disturb_dd_queue_pop()
→ lock → local_queue.swap(g_disturb_dd_queue) → unlock
→ 遍历 local_queue → 调用所有注册的 pop_cb
```
- 使用 `swap` 替代逐个 pop减少锁持有时间
- 事件自动打时标(`gettimeofday`):如果传入 `sec=0, ms=0`,自动填入当前 Unix 时间
### 8.3 回调注册
```cpp
dc_disturb_dd_register_queue_pop(cb)
dc_event_register_queue_pop(cb)
dc_fault_register_queue_pop(cb)
```
各模块self_ptl、iec61850m 等)注册回调,接收弹出的事件。
## 9. 参数配置管理
### 9.1 双文件机制
```
param.xml → 参数模板min/max/step/unit/num/default不随运行修改
self_param.xml → 运行时值存储valuedatacenter 自动生成
```
### 9.2 初始化流程datacenter_init
```
1. 解析 param.xml → 提取元数据 → g_param_metadatasaddr → {min, max, step, unit}
2. 构造 self_param.xml 路径(同目录,文件名替换)
3. 加载 self_param.xml 中的运行时值 → self_values map
4. 注册 Ao 信号:
- 值优先级: self_param.xml > param.xml value > param.xml default
- dc_signal_ao(..., SBO_NORMAL, p_data, p_default_data)
5. 注册 Param 信号(多定值区):
- 遍历 Item 子元素,每个 Item 一个定值区
- dc_signal_param(..., pp_data, pp_default_data, num)
6. 标记 cfg_change = true触发 self_param.xml 生成
```
### 9.3 运行时持久化dc_param_cfg_check
每 1000ms 检查 `g_param_cfg_change` 标志:
```
dc_param_cfg_check(path):
→ 如果 cfg_change == false → 跳过
→ 遍历 signal_ao 表: 序列化所有 Ao 信号到 XML
→ 遍历 signal_param 表: 序列化所有 Param 信号(含各定值区 Item
→ 写入 self_param.xml
→ 设置 cfg_change = false
```
### 9.4 元数据注入
信号注册后自动从 `g_param_metadata` 查找并注入 `param` 元数据:
```cpp
LOCAL void dc_signal_apply_metadata(stru_signal &signal) {
stru_signal_param param;
if (dc_param_metadata_lookup(signal.saddr, param))
signal.param = param; // 注入 min/max/step/unit
}
```
## 10. 值校验dc_check_val_valid
对写入值进行范围和类型校验,覆盖 18 种数据类型:
| 类型 | 校验规则 |
|------|---------|
| `DATA_TYPE_B` | 必须为 0 或 1 |
| `DATA_TYPE_S8/U8/S16/U16/S32/U32` | 范围 `[param.min, param.max]` |
| `DATA_TYPE_L64/UL64` | 范围校验 |
| `DATA_TYPE_F32/D64` | 范围校验,直接比较(无 epsilon 容差) |
| `DATA_TYPE_IP/MAC/C8/C32/C64/C128/STR` | strcmp 比较是否变化 |
| `DATA_TYPE_C1` | 单字符直接比较 |
**设计要点**: 所有类型校验先检查"值是否变化",未变化时拒绝写入(避免无效的持久化和回调触发)。
## 11. 数据类型系统
支持 18 种数据类型:
| ID | 名称 | C 类型 | 字节 |
|----|------|--------|------|
| B | 布尔 | uint8_t | 1 |
| S8/U8 | 有/无符号 8 位 | int8_t/uint8_t | 1 |
| S16/U16 | 有/无符号 16 位 | int16_t/uint16_t | 2 |
| S32/U32 | 有/无符号 32 位 | int32_t/uint32_t | 4 |
| L64/UL64 | 有/无符号 64 位 | int64_t/uint64_t | 8 |
| F32/D64 | 浮点/双精度 | float/double | 4/8 |
| IP | IP 地址 | char[4] | 4 |
| MAC | MAC 地址 | char[6] | 6 |
| C1/C8/C32/C64/C128 | 定长字符 | char[N] | N |
| STR | 变长字符串 | char[128] | 128 |
工具函数:
- `dc_get_data_type_str_by_id(id)` → 类型名称字符串
- `dc_get_data_type_id_by_str(str)` → 类型 ID
- `dc_get_data_type_len(id)` → 类型字节长度
- `dc_create_data_ptr_by_type(id)` → 创建新数据指针new
- `dc_delete_signal_data(p_data, id)` → 删除数据指针delete
- `dc_get_signal_val(p_data, id)` → 格式化为字符串
- `dc_set_signal_val_from_str(p_data, id, str)` → 从字符串解析并设置
## 12. 定时调度
`datacenter` 的运行由 `app_sys` 线程驱动:
```
datacenter_run_100ms():
→ dc_signal_out_change_check() // 脏信号变更检测 + 回调
→ dc_disturb_dd_queue_pop() // 扰动事件消费
→ dc_event_queue_pop() // SOE 事件消费
→ dc_fault_queue_pop() // 故障事件消费
datacenter_run_1000ms():
→ dc_param_cfg_check(g_param_path) // 参数持久化(仅在变更时)
```
## 13. 调试命令
CLI 命令 `datacenter` 提供信号表查看功能:
```
datacenter out → 查看 output 信号
datacenter in → 查看 input 信号
datacenter yk → 查看遥控信号
datacenter ao → 查看 AO 信号
datacenter param → 查看 Param 信号
datacenter all → 查看全部五张表
```
支持 Tab 自动补全out/in/yk/ao/param/all
每张表的显示包含id、saddr、desc、type、当前值、关联的 in 信号列表。使用视觉宽度对齐(中文字符占 2 列)。
## 14. 线程安全
| 机制 | 保护对象 |
|------|---------|
| `stru_signal_map.mtx` | 每张信号表的读写 |
| `g_dirty_out_mutex` | 脏信号队列 `g_dirty_out_signals` |
| `g_param_cfg_change_mutex` | 参数变更标志 |
| 事件队列各 mutex | `g_disturb_dd_queue` 等 |
| 表级锁 + swap 模式 | pop 操作锁内仅 swap处理在锁外 |
**设计原则**: 所有锁持有时间极短,仅在操作容器时加锁,回调执行和数据处理在锁外完成。