# 02 - SOE 与扰动数据回调详解 > 源码: [DCU_HOST/applications/data/iec_itfc/YxItfc.c](../../../../../../DCU_HOST/applications/data/iec_itfc/YxItfc.c) --- ## 1. SOE 回调注册 ```c CS10x_S_SOEHandler(pstCs10x, GetSOE, GetSOESendNum, UpdateSoePtrOut); ``` 三个回调函数分工: | 回调 | 作用 | 库调用时机 | |------|------|-----------| | `GetSOESendNum(pvArg)` | 返回待发送 SOE 条数 | 调度时检查 | | `GetSOE(pstInfo, pvParam, uiPos)` | 填充第 uiPos 条 SOE 数据 | 有 SOE 待发时 | | `UpdateSoePtrOut(pvParam, uiPos)` | 主站确认 n 条后更新指针 | 收到确认帧后 | --- ## 2. GetSOESendNum — 获取待发送数量 ```c s32 GetSOESendNum(void *pvArg) { u8 ucYxInforNum = GetSOEOutNum(pvArg); // ★ 获取 SOE 数据出口序号 if(ucYxInforNum >= ALL_INFOR_CHANNEL_NUM) return 0; return UserGetSoeSendNum(ucYxInforNum); // 从数据池查询待发送 SOE 数量 } ``` **`GetSOEOutNum` 的作用**: DCU_HOST 中有多条通信通道 (4G/以太网/蓝牙等),每个 CS10x 实例绑定了不同通道。`GetSOEOutNum` 通过 `pvArg` (即 `uiItfs` 接口号) 查找对应的 SOE 数据出口序号,确保每个通道的 SOE 互不干扰。 ### RTU 适配 ```c s32 GetSOESendNum(void *pvArg) { stru_iec_60870 *p_iec = iec60870_get_by_interface(*(uint32_t*)pvArg); if(!p_iec) return 0; return p_iec->soe_queue.count; // 或 dc_get_soe_pending_count() } ``` --- ## 3. GetSOE — 填充单条 SOE 数据 ```c s32 GetSOE(SOE_Info *pstInfo, void *pvParam, u32 uiPos) { u8 ucYxInforNum = GetSOEOutNum(pvParam); return UserGetSoe(ucYxInforNum, pstInfo, uiPos, pvParam); } // UserGetSoe 的真正实现 s32 UserGetSoe(u8 ucCh, SOE_Info *pstInfo, u32 uiPos, void *pvArg) { u16 usTabIdx; // 1. 从数据池获取第 uiPos 条 SOE 原始数据 stYX_CHANGE_INFO stYxInfo; UserGetSoeEvent(ucCh, &stYxInfo, (u16)uiPos); // 2. 填充 SOE_Info 结构体 pstInfo->uiInfoAddr = stYxInfo.usYxPot; // 点号 pstInfo->ucStatus = stYxInfo.ucState; // 状态值 pstInfo->ucStateType = stYxInfo.ucType; // 类型 // 3. 双点遥信特殊处理 stLIB60870ENTITY *pstLibEntity = GetEntityNum(*(u32*)pvArg); if(pstLibEntity->stCs10x.usYxType) { pstInfo->ucStateType = 1; pstInfo->ucStatus += 1; // 单点 0/1 → 双点 1/2 } else { pstInfo->ucStateType = 1; } // 4. ★ 时间戳转换: Unix时间戳 → CP56Time2a Stamp2Cp56(&pstInfo->stCTime, stYxInfo.uiSeconds, stYxInfo.usMs); return 1; } ``` ### 关键:CP56Time2a 格式转换 ```c // 参考实现: Unix time (秒+毫秒) → CP56Time2a (7字节) void Stamp2Cp56(__CP56Time2a *pCp56, uint32_t uiSeconds, uint16_t usMs) { struct tm *ptm = gmtime((time_t*)&uiSeconds); uint16_t uiTotalMs = ptm->tm_sec * 1000 + usMs; pCp56->ucLMs = uiTotalMs & 0xFF; // 毫秒低字节 pCp56->ucHMs = (uiTotalMs >> 8) & 0xFF; // 毫秒高字节 pCp56->ucMin = ptm->tm_min; pCp56->ucHour = ptm->tm_hour; pCp56->ucDay = ptm->tm_mday; pCp56->ucMonth = ptm->tm_mon + 1; pCp56->ucYear = ptm->tm_year - 100; // 年 (0-99) } ``` **CP56Time2a 关键点**: - `ucHMs` 第 7 位是 IV 标志 (0=有效, 1=无效) - `ucHour` 第 7 位是 SU 标志 (夏令时) - 毫秒范围 0-59999 (分钟*1000 + 秒*1000 + 毫秒) --- ## 4. UpdateSoePtrOut — 更新已确认指针 ```c s32 UpdateSoePtrOut(void *pvParam, u32 uiPos) { u8 ucYxInforNum = GetSOEOutNum(pvParam); return UserUpdateSoeOut(ucYxInforNum, uiPos); // → 数据池中将 SOE 队列的读指针向后移动 uiPos 条 } ``` **调用时机**: 当主站对 SOE 帧回复确认 (COT=7) 后,CBB 库自动调用此回调告知应用层"前 uiPos 条 SOE 已被主站确认,可以移除了"。 --- ## 5. SOE 数据流全景 ```mermaid sequenceDiagram participant HW as 硬件采集板 participant YX as 遥信数据池 participant CBB as lib60870 participant Master as 主站 HW->>YX: 遥信变位事件 (点号+状态+时标) YX->>YX: 写入 SOE 队列 loop 10ms 定时调度 CBB->>YX: GetSOESendNum() → N 条待发 alt N > 0 CBB->>YX: GetSOE(uiPos=0) YX-->>CBB: SOE_Info {点号, 状态, CP56Time} CBB->>YX: GetSOE(uiPos=1...) CBB->>Master: M_SP_TB / M_DP_TB (带时标 SOE 帧) end end Master->>CBB: 确认帧 (COT=7) CBB->>YX: UpdateSoePtrOut(已确认条数) YX->>YX: 释放已确认 SOE 记录 ``` --- ## 6. 扰动遥测回调 ```c CS10x_S_DiturbYcHandler(pstCs10x, GetDiturbYc, GetDiturbYcSendNum, UpdateDiturbYcPtrOut); ``` 扰动遥测的交互模式与 SOE **完全一致**: - `GetDiturbYcSendNum` → 待发送扰动遥测数量 - `GetDiturbYc(Yc_Info *pstInfo, void *pvParam, u32 uiPos)` → 填充遥测值 - `UpdateDiturbYcPtrOut` → 更新已发送指针 ```c s32 GetDiturbYc(Yc_Info *pstInfo, void *pvParam, u32 uiPos) { stYC_RECORD stYcInfo; UserGetDisturbYc(ucInforNum, &stYcInfo, (u16)uiPos); pstInfo->uiInfoAddr = stYcInfo.usIndex; pstInfo->fVal = stYcInfo.fVal; return 1; } ``` --- ## 7. RTU 适配建议 ### SOE 数据来源 RTU 中 SOE 来自 datacenter 的事件队列,无需像 DCU_HOST 那样管理多通道数据出口: ```c // 简化版: 直接从 datacenter 获取 s32 GetSOESendNum(void *pvArg) { return dc_get_event_count(EVENT_SOE); } s32 GetSOE(SOE_Info *pstInfo, void *pvParam, u32 uiPos) { dc_event *evt = dc_get_event(EVENT_SOE, uiPos); if(!evt) return 0; pstInfo->uiInfoAddr = evt->info_addr; pstInfo->ucStatus = evt->value.u8; pstInfo->ucStateType = 0; // 单点 // 时间转换: struct timespec → CP56Time2a dc_timespec_to_cp56(&evt->timestamp, &pstInfo->stCTime); return 1; } s32 UpdateSoePtrOut(void *pvParam, u32 uiPos) { dc_ack_events(EVENT_SOE, uiPos); return 1; } ``` ### 品质位设置 SOE 帧的品质位 (`SIQ/DIQ`) 由 CBB 库内部的 ASDU 编码逻辑自动设置,**应用层通常无需手动处理品质位**。仅在需要标记无效数据时: ```c // 如果数据无效,可在 ucStatus 中设置 IV 位 pstInfo->ucStatus = (evt->valid ? 0x00 : 0x80) | actual_value; ```