214 lines
6.2 KiB
Markdown
214 lines
6.2 KiB
Markdown
# 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;
|
||
```
|