# libmms_m 模块分析 **日期**: 2026-06-12 **基于源码**: `src/protocol/libmms_m/`(8个文件,约3000行) --- ## 1. 模块定位 `libmms_m` 是 RTU 的 IEC 61850 MMS 客户端库,位于协议层。它通过 libiec61850 库连接到远方 IED 装置,以事件驱动架构实现遥测/遥信读取(All-Call、GI)、遥控执行(SBO/Direct)、AO/参数读写、报告订阅等全部 MMS 客户端功能。 **与上下层关系**: - **下层**: 依赖 libiec61850 v1.5.x 提供的 IedConnection API - **上层**: 被 `iec61850m`(系统层封装)通过对外 API 调用 - **并发**: 每个 IED 连接一个独立线程 `mms_m_run_thread`,通过事件队列 + 信号量与其他线程通信 ## 2. 核心数据结构 ### 2.1 主对象 `stru_mms_m_obj` 每个 IED 连接创建一个 `stru_mms_m_obj` 实例,全局保存在 `g_mms_m_obj_map`(map)中,以 `obj_fd` 为句柄。 | 字段 | 用途 | |------|------| | `cfg_path` | 配置文件路径,用于错误日志和重复检测 | | `ied_name` | IED 名称,从配置中提取 | | `debug_print_flag` | 调试打印开关(`MMS_M_DEBUG_PRINT_ON = 1`) | | `connectionTimeout` | 连接超时(毫秒) | | `p_cfg` | 指向 `stru_cfg` 的点表配置(ST/MX/CO/AO/Param) | | `ldevs` | LDevice 树(LD→LN→DO→point_item 指针),用于 dataset member 匹配 | | `ld_datasets` | 运行时发现的 LD→Dataset→Member→RCB 完整结构 | | `rcb_numbers` | 可配置的 RCB 编号列表(如 `{"01","02"}`),空则默认 `"01"` | | `zone_saddr` | 绑定定值区信号的 saddr,用于检测定值区切换 | | `current_zone` | 当前定值区号 | | `obj_fd` | 对象句柄,对外 API 用此标识 | | `run` | 运行时状态(连接、定时器、事件队列、回调列表) | ### 2.2 运行时结构 `stru_mms_m_run` | 字段 | 用途 | |------|------| | `con` | `IedConnection` 句柄 | | `con_state` / `old_con_state` | 连接状态(前后帧对比检测上线/离线) | | `ip` / `port` | 远方 IED 的 IP/端口 | | `running_init` | ICD 初始化是否完成标志 | | `event_queue` | 环形事件队列(容量 `EVENT_QUEUE_SIZE`) | | `sem` | 信号量,保护事件队列的读写 | | `timer[4]` | 四个定时器 T0-T3 | | `pthread_task` | 工作线程句柄 | | `out_cb_lists` | 数据回调函数列表,向上层推送读取到的值 | | `out_status_cb` | 连接状态回调(上线/离线通知) | ### 2.3 事件类型 `_MMS_M_EVENT` ``` _MMS_M_EVENT_ALL_CALL // 全数据读取(ST+MX) _MMS_M_EVENT_GI_CALL // 总召 _MMS_M_EVENT_CO_SELECT // 遥控选择 _MMS_M_EVENT_CO_DIRECT // 遥控执行 _MMS_M_EVENT_CO_CANCEL // 遥控取消 _MMS_M_EVENT_AO_READ // 读取AO值 _MMS_M_EVENT_AO_WRITE // 写入AO值 _MMS_M_EVENT_PARAM_READ // 读取参数值 _MMS_M_EVENT_PARAM_WRITE // 写入参数值 ``` ### 2.4 定时器配置 | 定时器 | 间隔 | 触发动作 | |--------|------|---------| | T0 | 120s | 发送 All-Call → 全数据刷新 | | T1 | 60s | 发送 GI → 总召 | | T2 | 30s | 读取所有 AO + Param | | T3 | 20s | 预留(未使用) | 定时器实现为倒计数(`cnt--`),在每次 `mms_m_run` 中检查。到零时触发回调并重置计数。 ## 3. 核心流程 ### 3.1 初始化流程 ``` 上层调用 mms_m_out_init(p_cfg, debug_flag, timeout) → 检查配置文件是否重复 → new stru_mms_m_obj,初始化基础字段 → mms_m_ied_init(obj): - 提取 ied_name, ip, port - 遍历 ST/MX/CO/AO/Param 点表 - 调用 mms_m_add_point_to_ldevs 构建 LD→LN→DO→point 树形索引 - 用于后续 dataset member 匹配 → sem_init 信号量 → pthread_create → mms_m_run_thread 启动工作线程 → 返回 obj_fd ``` ### 3.2 连接管理(`mms_m_do_comm`) ``` mms_m_run_thread 主循环(每 MMS_M_THREAD_RUN_TM 毫秒): → mms_m_do_comm: 比较 con_state 与 old_con_state: - 非连接状态: IedConnection_connectAsync 异步重连 - 新上线 (old != CONNECTED, new == CONNECTED): mms_m_control_init() 重建 ControlObjectClient 回调 out_status_cb(ON_LINE) - 新离线 (old == CONNECTED, new != CONNECTED): running_init = false // RCB 需重新订阅 回调 out_status_cb(OFF_LINE) old_con_state = con_state → if CONNECTED: mms_m_run(obj) ``` ### 3.3 运行流程(`mms_m_run`) ``` mms_m_run(obj): → mms_m_run_init(obj): // 仅首次执行 mms_m_icd_init(obj): - 遍历 LD → LN(通过 IedConnection_getLogicalDeviceList/Directory) - 对每个 LN: mms_m_icd_dataset_init(获取所有 DataSet + Members) - 对每个 LN: mms_m_icd_report_init(URCB + BRCB,按 rcb_numbers 过滤) mms_m_ld_dataset_match_point_init(obj): - 遍历所有 dataset member - 解析 ref 得到 LD/LN/DO 名称 - 在 ldevs 树中查找匹配的 DO,建立 member.p_do_vec 指针关系 - 报告回调时通过此关系快速定位到具体信号点 mms_m_rcb_init(obj): - 获取 RCB 值(Resv→TrgOps→RptEna→GI) - 安装 mms_m_report_callback - 设置 TrgOps = dchg | qchg | GI → mms_m_do_send(obj): 处理事件队列中的操作 → mms_m_timer_running(obj): 检查四个定时器 ``` ### 3.4 All-Call 流程 ``` T0 定时器到期 → mms_m_do_call_all: → 推送 _MMS_M_EVENT_ALL_CALL 事件 → 重置 T0 mms_m_do_send → mms_m_send_call_all: 遍历 ST 和 MX 点表: IedConnection_readObject(p_con, point.reference, fc) → mms_m_get_mmsValue: MMS 类型转换 (BOOLEAN→uint8, INTEGER→int32, UNSIGNED→uint32, FLOAT→float, BIT_STRING→quality) → 构造 out_val { name, desc, reference, time, value, quality, reason } → mms_m_put_value: 遍历 out_cb_lists 执行每个回调 ``` ### 3.5 遥控流程(SBO/Direct) ``` 上层调用 mms_m_out_do_set_yk(event): → 按 app_fd 或 ied_name 定位 obj → mms_m_push_event mms_m_do_send → mms_m_send_co: switch ctrl_type: _MMS_M_EVENT_CO_SELECT: ControlObjectClient_select _MMS_M_EVENT_CO_DIRECT: ControlObjectClient_operate _MMS_M_EVENT_CO_CANCEL: ControlObjectClient_cancel → mms_m_send_set_callback: 设置操作完成回调 ``` ### 3.6 AO/Param 读写 ``` AO 读取:mms_m_send_read_ao → 按 saddr 或全表读取 → IedConnection_readObject → 类型转换 → 回调推送 Param 写入:mms_m_send_param_write → 检测定值区是否切换(对比 current_zone) → 定值区变化: 读取 SG 信息 → select EditSG → 写入 SE 值 → confirm CnfEdit → 定值区不变: 直接写入 SE 值 → confirm → 操作完成回调 ``` ### 3.7 报告回调(`mms_m_report_callback`) ``` IedConnection 收到 report → mms_m_report_callback: → 遍历 dataset members → 检查 ReasonForInclusion(非 NOT_INCLUDED 才处理) → mms_m_get_MmsValue 解析数据值和时标 → 通过 member.p_do_vec 找到所有绑定的信号点 → 构造 out_val → 遍历 out_cb_lists 推送 ``` ## 4. 对外 API | API | 说明 | |-----|------| | `mms_m_out_init(p_cfg, dbg, timeout)` | 创建 IED 连接,返回 obj_fd | | `mms_m_out_get_connect_status(fd, cb)` | 注册连接状态回调 | | `mms_m_out_debug_print_swicth(fd, flag)` | 开关调试打印 | | `mms_m_out_do_set_yk(event)` | 下达遥控命令(Select/Direct/Cancel) | | `mms_m_out_read_ao_or_params(fd, type, saddr)` | 读取 AO/Param 值 | | `mms_m_out_get_value(fd, cb)` | 注册数据回调 | | `mms_m_out_bind_param_zone_signal(fd, saddr)` | 绑定定值区信号 | | `mms_m_out_set_rcb_numbers(fd, numbers)` | 设置 RCB 订阅编号 | | `mms_m_create_data_ptr(type)` | 创建类型对应的数据指针 | | `mms_m_set_data_value(src, dst, type)` | 类型化数据赋值 | | `mms_m_get_data_value_str(data, type, str)` | 类型化数据转字符串 | | `mms_m_set_data_by_str(data, type, str)` | 字符串转类型化数据 | | `mms_m_out_reason_str(reason)` | 原因码转字符串 | | `mms_m_dbg_get(obj)` | 获取调试标志 | | `mms_m_get_obj(fd)` | 按句柄获取对象指针 | ## 5. 线程安全 - 事件队列 `push/pop` 受 `sem_wait/sem_post` 保护 - `out_cb_lists` 在初始化时注册,运行时只读 - `con_state/old_con_state` 仅在主线程(`mms_m_run_thread`)修改 - libiec61850 的 report callback 在其他线程触发,通过 `mms_m_push_event` 转入主线程处理 ## 6. 已知问题 1. **定时器倒计数精度**: 定时器基于循环次数×休眠时间,非高精度。取决于 `MMS_M_THREAD_RUN_TM` 大小 2. **事件队列溢出**: 容量 `EVENT_QUEUE_SIZE`,环形覆盖不报错 3. **RCB 编号过滤在 report_init 中执行**: 一旦初始化完成,变更 rcb_numbers 需重建连接 4. **离线时 running_init 重置**: 重连后整个 ICD 发现流程重新执行