[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 -> 主站
0
0

订阅

发表回复 0

Your email address will not be published. Required fields are marked *

captcha
Enter the characters shown in the image:
Reload

This CAPTCHA helps ensure that you are human. Please enter the requested characters.