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

14 KiB
Raw Blame History

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_dataDIRECT 时写入 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 控制流程

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 脏信号队列

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     → 运行时值存储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 元数据:

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处理在锁外

设计原则: 所有锁持有时间极短,仅在操作容器时加锁,回调执行和数据处理在锁外完成。