# 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 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 vec_p_data; // 数据指针列表(多条目,如多定值区) void *p_last_data; // 上次数据指针(用于变更检测) std::vector vec_p_default_data; // 默认值指针列表 std::vector link_saddrs; // 关联的 in 信号 saddr std::unordered_set link_set; // O(1) 查重 uint8_t ctrl_type; // 控制类型(SBO/Direct) stru_signal_param param; // 参数元数据(min/max/step/unit) std::vector> out_change_cb_list; // out 变更回调 std::vector> 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 g_dirty_out_signals; // 脏队列 static std::mutex g_dirty_out_mutex; static void dc_mark_signal_dirty(stru_signal *p_signal) { std::lock_guard 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 dirty; { std::lock_guard 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,处理在锁外 | **设计原则**: 所有锁持有时间极短,仅在操作容器时加锁,回调执行和数据处理在锁外完成。