[EtherCAT]以CIA402例程为例介绍MDP设备的OD处理
接上文《EtherCAT|Module/Slot概念与OD分配》,本文以HPM ECAT_CIA402例程为例, 从ESI XML文件和从站软件代码两部分解析MDP设备是如何处理模块的OD。
一、ESI内容解析
一份 MDP 设备的 ESI XML 分两个独立区域描述 OD:
<EtherCATInfo>
<Descriptions>
<Devices>
<Device> ← 描述设备本体(始终存在的全局 OD)
<Profile>
<Dictionary>
<Objects> ← 全局 OD:0x1000、0x1018、0x1C12、0x1C13、0xF000...
</Objects>
</Dictionary>
<Profile>
<Slots SlotPdoIncrement="16" SlotIndexIncrement="#x800">
<Slot MinInstances="1" MaxInstances="1"> ← Slot 0:Axis 0
<ModuleIdent>#x119800</ModuleIdent> ← 允许的 ModuleIdent
<ModuleIdent>#x219800</ModuleIdent>
</Slot>
<Slot MinInstances="0" MaxInstances="1"> ← Slot 1:Axis 1
<ModuleIdent>#x119800</ModuleIdent>
</Slot>
</Slots>
</Device>
</Devices>
<Modules> ← Module 库
<Module ModuleIdent="#x119800"> ← Module 定义
<RxPdo>
<Index DependOnSlot="true">#x1600</Index>
<Entry>
<Index DependOnSlot="true">#x6040</Index> ← 控制字 DependOnSlot="true"
...
</Entry>
</RxPdo>
<TxPdo>
<Index DependOnSlot="true">#x1a00</Index>
<Entry>
<Index DependOnSlot="true">#x6041</Index> ← 状态字 DependOnSlot="true"
...
</Entry>
</TxPdo>
<Profile>
<Dictionary>
<Objects> ← Module 自身的 OD 对象
<Object>
<Index DependOnSlot="true">#x6040</Index>
<Name>Control Word</Name>
...
</Object>
</Objects>
</Dictionary>
</Profile>
</Module>
</Modules>
</Descriptions>
</EtherCATInfo>
核心要点:
- Device 区的
Objects描述的是"设备本体"的对象——不管装什么 Module 都存在的全局对象(如 0x1000 Device Type、0x1018 Identity、0x1C12/0x1C13 PDO Assign、0xF000 MDP Info、0xF030 Configured Module Ident List 等)。 - Modules 区的每个 Module 都有 独立的 Objects 列表,描述"当这个 Module 被装入某个 Slot 时,OD 里会出现哪些对象"。
DependOnSlot="true"是关键属性:它告诉配置工具和代码生成器"这个对象的索引不是固定的,要随它被放入的 Slot 实例号而偏移"。
1.1 Slots 节点规定了什么
<Slots SlotPdoIncrement="16" SlotIndexIncrement="#x800">
这一行直接告诉工具:
- SlotIndexIncrement = 0x800:每多一个 Slot 实例,CiA402 对象索引加 0x800;
- SlotPdoIncrement = 16 (0x10):每多一个 Slot 实例,PDO 映射索引加 0x10。
1.2 Slot 内的 ModuleIdent 列表规定了什么
每个 Slot 内的 ModuleIdent 列出"这个位置允许装哪些 Module"。如默认的CIA402双轴:
- Slot 0(Axis 0):允许装 0x119800(CSP/CSV 动态切换)、0x219800(纯 CSP)、0x319800(纯 CSV),默认 0x319800;
- Slot 1(Axis 1):同上,但
Default="0"表示默认不装(MinInstances=0)。
配置工具的职责就是让用户在这些允许范围内为每个 Slot 选一个 ModuleIdent。
1.3. DependOnSlot 机制
当一个 ModuleIdent 为 0x319800 的 Module 被装入 Slot i 时,它的所有 DependOnSlot 对象索引按以下公式换算:
实际 OD 索引 = 模板索引 + i × @SlotIndexIncrement
实际 PDO 映射索引 = 模板 PDO 映射索引 + i × @SlotPdoIncrement
以双轴伺服为例(SlotIndexIncrement=0x800, SlotPdoIncrement=0x10):
| 对象 | 模板索引 | Slot 0 (Axis 0) | Slot 1 (Axis 1) |
|---|---|---|---|
| Control Word | 0x6040 | 0x6040 + 0×0x800 = 0x6040 | 0x6040 + 1×0x800 = 0x6840 |
| Status Word | 0x6041 | 0x6041 | 0x6841 |
| Target Position | 0x607A | 0x607A | 0x687A |
| Target Velocity | 0x60FF | 0x60FF | 0x68FF |
| RxPDO Mapping | 0x1602 | 0x1602 | 0x1602 + 1×0x10 = 0x1612 |
| TxPDO Mapping | 0x1A02 | 0x1A02 | 0x1A02 + 1×0x10 = 0x1A12 |
DependOnSlot 把 Module 的"模板 OD"按 Slot 实例分配到整个设备的OD空间,保证互相不冲突。
二、从站软件如何构建OD表
2.1 两个核心数据结构
a) 全局对象字典(设备本体)
// cia402appl.h, ApplicationObjDic[]
// 始终存在的设备级对象
TOBJECT ApplicationObjDic[] = {
{ 0x1C12, ... }, // RxPDO Assign
{ 0x1C13, ... }, // TxPDO Assign
{ 0xF000, ... }, // MDP Info (index distance=0x800, max modules=2)
{ 0xF010, ... }, // Module Profile List
{ 0xF030, ... }, // Configured Module Ident List
{ 0xF050, ... }, // Detected Module Ident List
...
};
b) 轴模板对象字典(每个 Module 一份)
// cia402appl.h, DefCiA402AxisObjDic[]
// 单轴的"模板 OD"——索引是基准值(0x6040、0x6060 等),未偏移
TOBJECT DefCiA402AxisObjDic[] = {
{ 0x1600, ... }, // RxPDO Mapping (csv/csp)
{ 0x1601, ... }, // RxPDO Mapping (csp)
{ 0x1602, ... }, // RxPDO Mapping (csv)
{ 0x1A00, ... }, // TxPDO Mapping (csv/csp)
{ 0x1A01, ... }, // TxPDO Mapping (csp)
{ 0x1A02, ... }, // TxPDO Mapping (csv)
{ 0x603F, ... }, // Error Code
{ 0x6040, ... }, // Control Word
{ 0x6041, ... }, // Status Word
{ 0x6060, ... }, // Modes of Operation
{ 0x6061, ... }, // Mode Display
{ 0x6064, ... }, // Position Actual Value
{ 0x606C, ... }, // Velocity Actual Value
{ 0x607A, ... }, // Target Position
{ 0x607D, ... }, // Software Position Limit
{ 0x60FF, ... }, // Target Velocity
{ 0x6502, ... }, // Supported Drive Modes
...
};
每轴还有一个独立的数据存储结构:
// cia402appl.h
typedef struct {
UINT16 objControlWord; // 0x6040 的实际数据
UINT16 objStatusWord; // 0x6041 的实际数据
INT32 objTargetPosition; // 0x607A
INT32 objPositionActualValue; // 0x6064
INT32 objTargetVelocity; // 0x60FF
INT32 objVelocityActualValue; // 0x606C
INT16 objModesOfOperation; // 0x6060
...
TOBJ1600 sRxPDOMap0; // 0x1600 PDO 映射条目
TOBJ1A00 sTxPDOMap0; // 0x1A00 PDO 映射条目
...
} CiA402Objects;
typedef struct {
BOOL bAxisIsActive;
...
UINT16 i16State;
CiA402Objects Objects; // 本轴的数据
TOBJECT *ObjDic; // 本轴的 OD 条目数组(动态分配)
} TCiA402Axis;
TCiA402Axis LocalAxes[MAX_AXES]; // MAX_AXES = 2
2.2 CiA402_Init() — 初始化
调用时机:设备上电时调用。
-
代码执行流程
CiA402_Init() │ ├─ 遍历 AxisCnt = 0 ~ MAX_AXES-1 (共2个轴) │ │ │ ├─ 1. 清零轴数据缓冲 │ │ HMEMSET(&LocalAxes[AxisCnt], 0, sizeof(TCiA402Axis)) │ │ │ ├─ 2. 设置轴的初始硬件状态标志 │ │ bAxisIsActive = FALSE ← 轴尚未激活 │ │ bBrakeApplied = TRUE ← 制动器已施加 │ │ bLowLevelPowerApplied = TRUE ← 低压已上电 │ │ bHighLevelPowerApplied = FALSE ← 高压未上电 │ │ bAxisFunctionEnabled = FALSE ← 轴功能未使能 │ │ bConfigurationAllowed = TRUE ← 允许配置 │ │ │ ├─ 3. 设置 CiA402 状态机初始状态 │ │ i16State = STATE_NOT_READY_TO_SWITCH_ON (0x0001) │ │ │ ├─ 4. 拷贝默认对象值 │ │ HMEMCPY(&LocalAxes[AxisCnt].Objects, &DefCiA402ObjectValues, ...) │ │ ← 所有轴共享同一份默认值模板 │ │ │ ├─ 5. 调整 PDO 映射条目中的对象索引 (加上轴偏移), 数值编码:[Index:16][SubIndex:8][BitSize:8] │ │ ├─ sRxPDOMap0.aEntries[j] += AxisCnt * (0x800 << 16) │ │ ├─ sRxPDOMap1.aEntries[j] += AxisCnt * (0x800 << 16) │ │ ├─ sRxPDOMap2.aEntries[j] += AxisCnt * (0x800 << 16) │ │ ├─ sTxPDOMap0.aEntries[j] += AxisCnt * (0x800 << 16) │ │ ├─ sTxPDOMap1.aEntries[j] += AxisCnt * (0x800 << 16) │ │ └─ sTxPDOMap2.aEntries[j] += AxisCnt * (0x800 << 16) │ │ │ ├─ 6. 分配并拷贝轴对象字典 │ │ ALLOCMEM(sizeof(DefCiA402AxisObjDic)) → LocalAxes[AxisCnt].ObjDic │ │ HMEMCPY(ObjDic, DefCiA402AxisObjDic, ...) │ │ │ └─ 7. 遍历字典条目,修正每个对象的索引和变量指针 │ ├─ switch(Index) 为每个对象绑定 pVarPtr → LocalAxes[AxisCnt].Objects.xxx │ │ 例: 0x1600 → sRxPDOMap0 │ │ 0x6040 → objControlWord │ │ 0x6041 → objStatusWord │ │ ... │ │ │ └─ 偏移索引: │ if (0x1400 <= Index <= 0x1BFF) → Index += AxisCnt * 0x10 │ else → Index += AxisCnt * 0x800 │ └─ return 0 (成功) -
此函数不修改 MDP 对象(0xF000/0xF010/0xF030/0xF050),它们在
cia402appl.h中以静态初始化赋值 -
此函数不调用
COE_AddObjectToDic(),轴对象字典只是分配和准备好了,尚未注册到 CoE 对象字典中 -
轴对象字典的注册推迟到
APPL_GenerateMapping()中,根据 PDO Assign 动态进行
2.3 APPL_GenerateMapping() — PDO 映射计算与轴激活
调用时机:状态从 INIT -> PREOP 转换时调用。
- 执行流程
APPL_GenerateMapping(pInputSize, pOutputSize) │ ├─ 1. 安全检查 │ if (sRxPDOassign.u16SubIndex0 > MAX_AXES) │ return ALSTATUSCODE_NOVALIDOUTPUTS │ ├─ 2.【第一轮遍历】根据 0x1C12 (RxPDO Assign) 激活/去激活轴 │ for PDOAssignEntryCnt = 0 ~ sRxPDOassign.u16SubIndex0-1 │ │ │ ├─ 提取 AxisIndex = (sRxPDOassign.aEntries[...] & 0xF0) >> 4 │ │ 例: 0x1602 -> (0x02 & 0xF0)>>4 = 0 -> Axis 0 │ │ 0x1612 -> (0x12 & 0xF0)>>4 = 1 -> Axis 1 │ │ │ ├─ if (AxisIndex == PDOAssignEntryCnt) <- 轴按序排列,需要激活 │ │ ├─ if (!bAxisIsActive) │ │ │ ├─ 遍历 ObjDic,逐条调用 COE_AddObjectToDic() 注册到 CoE OD │ │ │ └─ bAxisIsActive = TRUE │ │ └─ (已激活的轴跳过) │ │ │ └─ else (AxisIndex != PDOAssignEntryCnt) <- 轴未映射,需要去激活 │ ├─ if (bAxisIsActive) │ │ ├─ 遍历 ObjDic,逐条调用 COE_RemoveDicEntry(Index) │ │ └─ bAxisIsActive = FALSE │ └─ (已去激活的轴跳过) │ ├─ 3.【第二轮遍历】扫描 0x1C12 计算 RxPDO 总位宽 (OutputSize) │ for PDOAssignEntryCnt = 0 ~ sRxPDOassign.u16SubIndex0-1 │ │ │ ├─ 取低4位判断模式: sRxPDOassign.aEntries[...] & 0x000F │ │ │ ├─ case 0: CSV/CSP 模式 (0x1600) │ │ ├─ objSupportedDriveModes = 0x180 (bit7=CSP, bit8=CSV) │ │ └─ 累加 sRxPDOMap0 所有条目的 BitSize │ │ │ ├─ case 1: CSP 模式 (0x1601) │ │ ├─ objSupportedDriveModes = 0x80 (bit7=CSP) │ │ └─ 累加 sRxPDOMap1 所有条目的 BitSize │ │ │ └─ case 2: CSV 模式 (0x1602) │ ├─ objSupportedDriveModes = 0x100 (bit8=CSV) │ └─ 累加 sRxPDOMap2 所有条目的 BitSize │ │ OutputSize >>= 3 (bit -> byte) │ ├─ 4.【第三轮遍历】扫描 0x1C13 计算 TxPDO 总位宽 (InputSize) │ for PDOAssignEntryCnt = 0 ~ sTxPDOassign.u16SubIndex0-1 │ │ │ ├─ case 0: 累加 sTxPDOMap0 (CSV/CSP TxPDO) │ ├─ case 1: 累加 sTxPDOMap1 (CSP TxPDO) │ └─ case 2: 累加 sTxPDOMap2 (CSV TxPDO) │ │ InputSize >>= 3 (bit -> byte) │ └─ *pInputSize = InputSize *pOutputSize = OutputSize return 0
2.4 APPL_InputMapping() — TxPDO 输入映射
调用时机:每个 EtherCAT 周期,由 SSC 框架在发送过程数据前调用,将本地对象数据拷贝到 ESC 输出缓冲区(从 EtherCAT 主站视角为"输入")。
-
执行流程
APPL_InputMapping(pData) <- pData 指向 ESC 输入过程数据缓冲区 │ ├─ pTmpData = (UINT8*)pData <- 字节级游标 │ ├─ for j = 0 ~ sTxPDOassign.u16SubIndex0-1 │ │ │ ├─ AxisIndex = (sTxPDOassign.aEntries[j] & 0xF0) >> 4 │ │ │ ├─ switch (sTxPDOassign.aEntries[j] & 0x000F) │ │ │ ├─ case 0: CSV/CSP 模式 (TCiA402PDO1A00, 共12字节) │ │ { │ │ ObjStatusWord <- SWAPWORD(objStatusWord) │ │ ObjPositionActualValue <- SWAPDWORD(objPositionActualValue) │ │ ObjVelocityActualValue <- SWAPDWORD(objVelocityActualValue) │ │ ObjModesOfOperationDisplay <- SWAPWORD(objModesOfOperationDisplay & 0xFF) │ │ } │ │ pTmpData += sizeof(TCiA402PDO1A00) // 12 bytes │ │ │ ├─ case 1: CSP 模式 (TCiA402PDO1A01, 共8字节) │ │ { │ │ ObjStatusWord <- SWAPWORD(objStatusWord) │ │ ObjPositionActualValue <- SWAPDWORD(objPositionActualValue) │ │ Padding16Bit <- (未显式赋值) │ │ } │ │ pTmpData += sizeof(TCiA402PDO1A01) // 8 bytes │ │ │ └─ case 2: CSV 模式 (TCiA402PDO1A02, 共8字节) │ { │ ObjStatusWord <- SWAPWORD(objStatusWord) │ ObjPositionActualValue <- SWAPDWORD(objPositionActualValue) │ Padding16Bit <- (未显式赋值) │ } │ pTmpData += sizeof(TCiA402PDO1A02) // 8 bytes │ └─ (函数结束,ESC 缓冲区已填充) -
内存布局示例(2轴全 CSV 模式)
ESC Input Process Data Buffer (TxPDO) +------------------------------+ byte 0 | Axis0 StatusWord (2B) | | Axis0 PositionActual (4B) | | Axis0 Padding (2B) | <- TCiA402PDO1A02 = 8 bytes +------------------------------+ byte 8 | Axis1 StatusWord (2B) | | Axis1 PositionActual (4B) | | Axis1 Padding (2B) | +------------------------------+ byte 16 总输入大小 = 8 x 2 = 16 字节
2.5 APPL_OutputMapping() — RxPDO 输出映射
调用时机:每个 EtherCAT 周期,由 SSC 框架在接收到主站过程数据后调用,将 ESC 输入缓冲区中的数据解析到本地对象变量。
-
执行流程
APPL_OutputMapping(pData) <- pData 指向 ESC 输出过程数据缓冲区 │ ├─ pTmpData = (UINT8*)pData <- 字节级游标 │ ├─ for j = 0 ~ sRxPDOassign.u16SubIndex0-1 │ │ │ ├─ AxisIndex = (sRxPDOassign.aEntries[j] & 0xF0) >> 4 │ │ │ ├─ switch (sRxPDOassign.aEntries[j] & 0x000F) │ │ │ ├─ case 0: CSV/CSP 模式 (TCiA402PDO1600, 共12字节) │ │ { │ │ objControlWord <- SWAPWORD(pOutputs->ObjControlWord) │ │ objTargetPosition <- SWAPDWORD(pOutputs->ObjTargetPosition) │ │ objTargetVelocity <- SWAPDWORD(pOutputs->ObjTargetVelocity) │ │ objModesOfOperation<- SWAPWORD(pOutputs->ObjModesOfOperation & 0xFF) │ │ } │ │ pTmpData += sizeof(TCiA402PDO1600) // 12 bytes │ │ │ ├─ case 1: CSP 模式 (TCiA402PDO1601, 共8字节) │ │ { │ │ objControlWord <- SWAPWORD(pOutputs->ObjControlWord) │ │ objTargetPosition <- SWAPDWORD(pOutputs->ObjTargetPosition) │ │ } │ │ pTmpData += sizeof(TCiA402PDO1601) // 8 bytes │ │ │ └─ case 2: CSV 模式 (TCiA402PDO1602, 共8字节) │ { │ objControlWord <- SWAPWORD(pOutputs->ObjControlWord) │ objTargetVelocity <- SWAPDWORD(pOutputs->ObjTargetVelocity) │ } │ pTmpData += sizeof(TCiA402PDO1602) // 8 bytes │ └─ (函数结束,本地对象已更新) -
内存布局示例(2轴全 CSV 模式)
ESC Input Process Data Buffer (RxPDO) +------------------------------+ byte 0 | Axis0 ControlWord (2B) | | Axis0 TargetVelocity (4B) | | Axis0 Padding (2B) | <- TCiA402PDO1602 = 8 bytes +------------------------------+ byte 8 | Axis1 ControlWord (2B) | | Axis1 TargetVelocity (4B) | | Axis1 Padding (2B) | +------------------------------+ byte 16 总输入大小 = 8 x 2 = 16 字节
三、完整生命周期时序
上述代码的执行时序。
设备上电
|
v
INIT 状态
+-- CiA402_Init()
| +-- 拷贝默认对象值到每轴
| +-- 修正 PDO 映射条目的索引偏移
| +-- 为每轴分配对象字典
| +-- 绑定变量指针 + 偏移索引
|
v (主站配置 PDO Assign 0x1C12/0x1C13)
INIT -> PREOP 转换
+-- APPL_GenerateMapping()
| +-- 根据PDO Assign 激活/去激活轴 (添加/移除OD条目)
| +-- 计算 RxPDO 总字节数 -> OutputSize
| +-- 计算 TxPDO 总字节数 -> InputSize
|
v
PREOP -> SAFEOP 转换
+-- APPL_StartInputHandler()
|
v
SAFEOP -> OP 转换
+-- APPL_StartOutputHandler()
|
v
OP 状态 — 周期性循环
+-- 每个周期:
| +-- APPL_OutputMapping() <- 解析主站 RxPDO -> LocalAxes
| +-- APPL_Application()
| +-- APPL_InputMapping() <- LocalAxes -> 打包 TxPDO -> 主站