409 lines
14 KiB
Markdown
409 lines
14 KiB
Markdown
# 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_data;DIRECT 时写入 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 控制流程
|
||
|
||
SBO(Select 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 → 运行时值存储(value),datacenter 自动生成
|
||
```
|
||
|
||
### 9.2 初始化流程(datacenter_init)
|
||
|
||
```
|
||
1. 解析 param.xml → 提取元数据 → g_param_metadata(saddr → {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,处理在锁外 |
|
||
|
||
**设计原则**: 所有锁持有时间极短,仅在操作容器时加锁,回调执行和数据处理在锁外完成。
|