14 KiB
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. 五张信号表
static stru_datacenter g_datacenter = {
.signal_out, // 输出信号(遥测/遥信)
.signal_in, // 输入信号(链接到 out)
.signal_yk, // 遥控信号
.signal_ao, // AO 信号(SP 定值、定值区号)
.signal_param, // 参数信号(多定值区)
};
每张表的结构:
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 字符串精确匹配:
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 自定义哈希/相等函子
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. 核心信号结构
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)
// 简单注册
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)
dc_signal_in(saddr, desc, link_saddr, &p_data)
// link_saddr 指向一个已注册的 out 信号
// in 信号直接共享 out 信号的数据指针,不上报变更回调
5.3 遥控信号(yk)
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 信号
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 信号(多定值区)
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 脏信号队列
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 执行)
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 回调注册
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 元数据:
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)→ 类型 IDdc_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,处理在锁外 |
设计原则: 所有锁持有时间极短,仅在操作容器时加锁,回调执行和数据处理在锁外完成。