【原创】IgH EtherCAT主站详解(十一)--拓扑扫描流程

拓扑扫描流程

4.2 — EtherCAT 总线拓扑发现、从站识别、拓扑树构建与 PDO 映射


概览

什么是拓扑扫描?

拓扑扫描是 EtherCAT 主站自动识别总线上所有从站设备并构建拓扑树的过程。整个流程分为三个阶段:

  1. 总线探测:Master FSM 通过 BRD 广播读取寄存器 0x0130,获取在线从站数量
  2. 逐站扫描:为每个从站分配递增站地址,读取基础信息、EEPROM/SII 数据、构建 PDO 映射
  3. 拓扑构建:根据每个从站各端口的 DC 接收时间差,递归计算出父子连接关系,形成完整的拓扑树

扫描触发时机

自动触发: IDLE 线程中 Master FSM 周期性 BRD 广播,检测到从站数量变化时自动扫描
手动触发: ethercat rescan 命令强制重新扫描
热插拔: 从站上线/离线时,WKC 变化触发重新扫描

地址递增机制

EtherCAT 使用自动递增物理寻址 (Auto-increment Physical) 来逐个发现从站。帧经过每个 ESC 时,地址字段自动减 1,只有地址减到 0 的从站才会响应。因此第一个从站的 ring_position = 0,站地址被分配为 ring_position + 1
4.2 — EtherCAT 总线拓扑扫描、从站识别与 PDO 构建

拓扑扫描地址递增示意图


技术详情

2.1 Master FSM 扫描触发流程

Master FSM 在 start 状态下发起 BRD 广播,工作流程如下:

flowchart TD A["Master FSM: start 状态<br/>BRD 读取 0x0130"] --> B{"从站数量变化?<br/>WKC ≠ 上次值"} B -->|否| C["检查下一个设备<br/>(主/备)"] C --> A B -->|是| D["清除旧从站列表<br/>分配 ec_slave_t 数组"] D --> E["dc_read_old_times<br/>APRD 读取所有从站<br/>0x0900 端口旧接收时间"] E --> F["clear_addresses<br/>BWR 广播写入 0x0010 = 0<br/>清除所有站地址"] F --> G["dc_measure_delays<br/>BWR 广播写入 0x0900 = 0<br/>触发从站锁存端口接收时间"] G --> H["scan_slave<br/>触发所有从站并行扫描"] H --> I["等待所有从站扫描完成"] I --> J["ec_master_calc_dc()<br/>计算拓扑树 + 传输延迟"] J --> K["attach_slave_configs<br/>匹配应用层配置"] K --> L["回到 start 监控"] style A fill:#c6f6d5,stroke:#68d391 style J fill:#bee3f8,stroke:#4299e1 style L fill:#c6f6d5,stroke:#68d391

关键步骤说明

阶段 Datagram 类型 操作 目的
broadcast BRD 读 0x0130 获取在线从站数量 (WKC = 从站数)
dc_read_old_times APRD 读 0x0900 (16 bytes) 记录旧端口接收时间,用于后续检测端口变化
clear_addresses BWR 写 0x0010 = 0x0000 清除所有从站的站地址,回到初始状态
dc_measure_delays BWR 写 0x0900 = 0 触发从站锁存 DC 接收时间,用于计算传播延迟
scan_slave 并行启动所有从站扫描 每个从站由独立的 Slave FSM 异步扫描

2.2 拓扑数据结构

扫描完成后,内核中的 ec_slave_t 结构体存储了完整的拓扑信息。核心字段如下:

字段 类型 含义 赋值时机
ring_position uint16_t 环位置索引(0 起始),表示从站在总线上的物理顺序 Master FSM 分配从站数组时
station_address uint16_t 配置站地址 = ring_position + 1 Slave Scan FSM state_start 阶段 APWR 写入
device_index ec_device_index_t 从站响应的设备索引(主/备网卡) Master FSM 分配从站数组时
upstream_port uint8_t 面向主站的端口索引(0-3) ec_master_calc_topology() 时计算
ports[4] ec_slave_port_t 4 个端口信息(描述、链路、下一从站指针、DC 时间) 扫描 + 拓扑计算阶段
ports[i].next_slave ec_slave_t * 该端口连接的下游从站指针(拓扑树父子关系) ec_master_calc_topology_rec() 递归赋值
ports[i].desc ec_slave_port_desc_t 端口类型:NOT_IMPLEMENTED / NOT_CONFIGURED / EBUS / MII Slave Scan FSM state_base 读寄存器 0x0007
ports[i].receive_time uint32_t DC 端口接收时间(ns),用于延迟计算和拓扑判断 dc_measure_delays 后从站锁存
ports[i].delay_to_next_dc uint32_t 到下一个支持 DC 的从站的延迟 (ns) ec_slave_calc_transmission_delays_rec() 计算

拓扑树构建原理

拓扑树通过 ports[i].next_slave 指针形成。每个从站的 upstream_port 指向主站方向,其余端口指向下游从站。端口遍历顺序为 next_table[4] = {3, 2, 0, 1},从 upstream_port 的下一个端口开始循环。

2.3 拓扑树递归构建算法

扫描完成后,ec_master_calc_dc() 调用拓扑计算,核心算法如下:

flowchart TD A["ec_master_calc_dc()"] --> B["ec_master_find_dc_ref_clock()<br/>找到第一个支持 DC 系统时间的从站"] B --> C["ec_master_calc_topology()"] C --> D["遍历所有从站<br/>ec_slave_calc_upstream_port()"] D --> E["upstream_port 选择规则:<br/>接收时间最小的开放端口"] E --> F["ec_master_calc_topology_rec()<br/>递归构建拓扑树"] F --> G["ec_master_calc_transmission_delays()<br/>计算传输延迟"] G --> H["完成"] style A fill:#c6f6d5,stroke:#68d391 style H fill:#c6f6d5,stroke:#68d391

upstream_port 判定规则

每个从站需确定哪个端口面向主站(upstream)。判定依据是 DC 端口接收时间

  1. 默认假设 upstream_port = 0(正常线型拓扑)
  2. 若端口 0 链路关闭或被旁路,则寻找替代
  3. 遍历端口 1-3,找到接收时间最小的开放端口作为 upstream
  4. 不支持 DC 的从站只能假设 upstream_port = 0

拓扑递归算法

ec_master_calc_topology_rec(master, upstream_slave, slave_position) 是核心递归函数:

  1. 取当前从站 = master->slaves[slave_position]
  2. 设置当前从站的 upstream 端口的 next_slave = upstream_slave(指向父节点)
  3. next_table = {3, 2, 0, 1} 顺序遍历非 upstream 端口
  4. 对每个链路开放的端口:分配下一个 slave_position,设置 ports[i].next_slave = 下一个从站
  5. 递归调用自身处理下游从站

关键洞察: 拓扑树是基于物理位置顺序(ring_position)和端口接收时间递归构建的。从站数组按总线遍历顺序排列,递归过程中依次消耗数组元素,形成深度优先的拓扑树。

2.4 Slave Scan FSM 完整流程

每个从站由独立的 Slave FSM 进行扫描,包含以下步骤:

flowchart TD A["start: APWR 分配站地址<br/>ring_position → 0x0010"] --> B["address: 验证地址写入"] B --> C["state: FPRD 读 AL 状态<br/>0x0130"] C --> D["base: FPRD 读基础寄存器<br/>0x0000 (12 bytes)"] D --> E{"base_dc_supported?"} E -->|是| F["dc_cap: FPRD 读系统时间<br/>0x0910 (4/8 bytes)"] E -->|否| G["datalink"] F --> H["dc_times: FPRD 读端口接收时间<br/>0x0900 (16 bytes)"] H --> G["datalink: FPRD 读 DL 状态<br/>0x0110"] G --> I{"编译选项"} I -->|EC_SII_ASSIGN| J["assign_sii: FPWR<br/>分配 SII 给 EtherCAT"] I -->|EC_SII_CACHE| K["sii_identity: 读 EEPROM<br/>alias/serial/vendor/product"] I -->|默认| L["attach_sii: 创建/匹配 SII 镜像"] J --> K K --> L L --> M{"已有完整 SII 缓存?"} M -->|是| N["跳过 EEPROM 读取"] M -->|否| O["sii_size: 遍历 Category<br/>确定 EEPROM 大小"] O --> P["sii_data: 读取完整 EEPROM<br/>每 2 words 循环"] P --> Q["sii_parse: 解析 Category 数据<br/>strings/general/syncs/PDOs"] N --> R{"支持 CoE 邮箱?"} Q --> R R -->|是| S["preop: 请求进入 Pre-Op<br/>(使用 Slave Config FSM)"] R -->|否| T["end: 扫描完成"] S --> U["sync: 读取邮箱 SM 配置<br/>0x0800 (2 × SM 页)"] U --> V["mailbox_cleared: 清空邮箱"] V --> W{"EC_SII_CACHE<br/>且已有 PDO 数据?"} W -->|是| X["end"] W -->|否| Y["pdos: 通过 CoE 读取<br/>PDO 分配和映射"] Y --> X["end: 扫描完成"] style A fill:#c6f6d5,stroke:#68d391 style X fill:#c6f6d5,stroke:#68d391 style T fill:#c6f6d5,stroke:#68d391

2.5 扫描获取的从站信息

阶段 读取内容 寄存器 / 方法 存储位置
start 分配站地址 APWR → 0x0010 slave->station_address
state AL 状态 FPRD → 0x0130 slave->current_state
base 类型、版本、端口数、FMMU/SM 数、DC 能力 FPRD → 0x0000-0x000B slave->base_*
base (端口) 4 个端口类型描述 0x0007 byte → 2-bit/port slave->ports[i].desc
dc_cap 是否有系统时间寄存器 FPRD → 0x0910, 检查 WKC slave->has_dc_system_time
dc_times 4 端口 DC 接收时间 FPRD → 0x0900 (16 bytes) slave->ports[i].receive_time
datalink 各端口链路状态 FPRD → 0x0110 slave->ports[i].link
sii_identity Alias, Vendor ID, Product Code, Serial EEPROM words 0x04-0x0E slave->sii_image->sii.*
sii_parse Strings (0x0A), General (0x1E), Syncs (0x29), TxPDO (0x32), RxPDO (0x33) EEPROM Category 遍历 slave->sii_image->sii.*
sync 已配置的邮箱 SM 偏移/大小 FPRD → 0x0800 slave->configured_{rx,tx}_mailbox_*
pdos CoE PDO 分配和映射(若支持) CoE SDO 读 slave->sii_image->sii.pdos

2.6 EEPROM (SII) Category 解析

sii_parse 阶段,Slave Scan FSM 解析 EEPROM 中的 Category 数据:

Category 类型 内容 解析函数
0x000A 字符串表 ec_slave_fetch_sii_strings()
0x001E General(设备名称、分组、物理层、CoE 详情) ec_slave_fetch_sii_general()
0x0028 FMMU 描述(跳过)
0x0029 Sync Manager 描述 ec_slave_fetch_sii_syncs()
0x0032 TxPDO(从站→主站)映射 ec_slave_fetch_sii_pdos(EC_DIR_INPUT)
0x0033 RxPDO(主站→从站)映射 ec_slave_fetch_sii_pdos(EC_DIR_OUTPUT)
0xFFFF 结束标记

2.7 ethercat graph 命令的拓扑信息来源

tool/CommandGraph.cpp 通过 ioctl 从内核获取已扫描的从站拓扑信息,转换为 DOT 图形语言输出。

数据获取路径

sequenceDiagram participant CG as CommandGraph<br/>(用户空间) participant MD as MasterDevice participant DEV as /dev/EtherCAT0 participant KRN as 内核 Master<br/>(ioctl 处理) participant SLV as ec_slave_t 数组 CG->>MD: getMaster(&master) MD->>DEV: ioctl(EC_IOCTL_MASTER) DEV->>KRN: ec_ioctl_master() KRN->>KRN: 读取 master->slave_count KRN-->>CG: slave_count = N loop i = 0 到 N-1 CG->>MD: getSlave(&slave, i) MD->>DEV: ioctl(EC_IOCTL_SLAVE, {position=i}) DEV->>KRN: ec_ioctl_slave() KRN->>SLV: ec_master_find_slave_const(master, 0, i) KRN->>KRN: 复制 ports[i].desc, .link, .receive_time KRN->>KRN: ports[i].next_slave → ring_position<br/>(指针转为 ring_position 索引) KRN->>KRN: 复制 dc_supported, dc_range,<br/>transmission_delay 等 KRN-->>CG: ec_ioctl_slave_t 结构体 end Note over CG: 根据 ports[i].next_slave<br/>构建 DOT 边 CG->>CG: 输出 DOT 格式拓扑图

ioctl 接口中的拓扑信息转换

关键转换发生在 master/ioctl.cec_ioctl_slave() 函数中:

内核数据结构 ioctl 输出字段 转换说明
slave->ports[i].next_slave
(ec_slave_t * 指针)
data.ports[i].next_slave
(uint16_t)
指针 → ring_position 索引
NULL → 0xFFFF(无连接)
slave->ports[i].desc data.ports[i].desc 直接复制端口类型枚举
slave->ports[i].link data.ports[i].link 直接复制链路状态
slave->ports[i].receive_time data.ports[i].receive_time 直接复制 DC 接收时间
slave->ports[i].delay_to_next_dc data.ports[i].delay_to_next_dc 直接复制延迟值

CommandGraph DOT 输出逻辑

CommandGraph::execute() 的 DOT 生成逻辑:

  1. 创建 "master" 节点,连接到第一个从站 (slave0) 的端口 0
  2. 遍历所有从站,创建节点,标签包含 position / alias:pos / order / DC 信息
  3. 遍历每个从站的端口 1-3(跳过端口 0,它是 upstream):
    • next_slave != 0xFFFF,创建边 slaveN -- slaveM
    • 边标签包含:端口索引、DC 延迟、CRC 错误(可选)
    • 端口媒体类型(MII/EBUS)作为边标签

重要说明

ethercat graph 本身不进行任何扫描或拓扑计算。它只是从内核读取已经扫描完成的拓扑数据并格式化输出。所有拓扑计算(upstream_port 判定、next_slave 指针赋值、延迟计算)都在内核的扫描阶段完成。

2.8 PDO 映射构建

PDO 映射有两个来源:

  • EEPROM SII:在 sii_parse 阶段从 EEPROM Category 0x32/0x33 解析
  • CoE 在线读取:在 pdos 阶段通过 CoE SDO 从已进入 Pre-Op 的从站读取 PDO 分配和映射

CoE 在线读取会覆盖 EEPROM 中的 PDO 数据(如果从站支持 CoE),因为 CoE 能获取运行时动态配置的 PDO 映射。


源码分析

Slave Scan FSM 状态函数清单

源文件: master/fsm_slave_scan.c, master/fsm_slave_scan.h

状态函数 行号 Datagram 职责 下一状态
state_start 211 APWR APWR 分配站地址到 0x0010 state_address
state_address 229 验证地址写入 (WKC=1) state_state
state_state 268 FPRD 0x0130 读取 AL 状态 state_base
state_base 314 FPRD 0x0000 (12B) 读取基础信息:type/revision/ports/FMMU/SM/DC state_dc_cap 或 enter_datalink
state_dc_cap 390 FPRD 0x0910 检测是否有系统时间寄存器 (WKC 判断) state_dc_times
state_dc_times 437 FPRD 0x0900 (16B) 读取 4 端口接收时间,检测 bypassed enter_datalink
state_datalink 680 FPRD 0x0110 读取数据链路状态 enter_assign_sii / enter_sii_identity / enter_attach_sii
state_assign_sii 726 FPWR 0x0500 [EC_SII_ASSIGN] 分配 SII 访问给 EtherCAT enter_sii_identity / enter_attach_sii
state_sii_identity 768 SII FSM [EC_SII_CACHE] 读 EEPROM alias/serial/vendor/product enter_attach_sii
state_sii_device 850 SII FSM [EC_SII_OVERRIDE] 读 EEPROM 前 16 words 获取设备 ID enter_sii_request
state_sii_request 984 异步 [EC_SII_OVERRIDE] 请求固件覆盖 SII state_sii_size / state_sii_parse
state_sii_size 1042 SII FSM 遍历 Category 直到 0xFFFF,确定 EEPROM 大小 state_sii_data
state_sii_data 1138 SII FSM 逐 word 读取完整 EEPROM 内容 state_sii_parse
state_sii_parse 1192 解析 Category:strings/general/syncs/PDOs enter_regalias / enter_preop / state_end
state_mailbox_cleared 1562 清空邮箱旧数据,分配邮箱缓冲区 enter_pdos / state_end
state_regalias 1376 FPRD 0x0012 [EC_REGALIAS] 从寄存器读 alias enter_preop / state_end
state_preop 1463 Slave Config FSM 请求进入 Pre-Op(委托给 fsm_slave_config) enter_clear_mailbox
state_sync 1483 FPRD 0x0800 读取已配置的邮箱 SM 偏移和大小 enter_clear_mailbox
state_pdos 1634 PDO FSM 通过 CoE SDO 读取 PDO 分配和映射 state_end
state_retry 1656 扫描失败重试入口 state_retry_wait
state_retry_wait 1673 等待 SCAN_RETRY_TIME (100ms) 后重试 state_start
state_end 1704 扫描完成(终态)
state_error 1693 扫描失败(终态)

Master FSM 扫描关键代码

源文件: master/fsm_master.c

从站数组分配 (ec_fsm_master_state_broadcast, line 263)

当 BRD 检测到从站数量变化时:

  1. ec_master_clear_slaves(master) — 清除旧从站列表
  2. kmalloc(sizeof(ec_slave_t) * count) — 分配从站数组
  3. 遍历初始化每个 ec_slave_t:设置 ring_position, station_address = i + 1, device_index
  4. 进入 DC 旧时间读取 → 地址清除 → 延迟测量 → 扫描

并行扫描启动 (ec_fsm_master_state_dc_measure_delays, line 993)

DC 延迟测量完成后,进入并行扫描阶段:

  1. 对每个从站调用 ec_fsm_slave_set_ready(&slave->fsm),标记为可扫描
  2. 每个从站的 Slave FSM 在各自的 fsm_slave 线程上下文中独立执行扫描
  3. Master FSM 进入 state_scan_slave 轮询等待所有从站完成

扫描完成后 (ec_fsm_master_state_scan_slave, line 1048)

所有从站扫描完成后:

  1. ec_master_attach_slave_configs(master) — 将应用层配置匹配到从站
  2. ec_master_calc_dc(master) — 计算拓扑树和传输延迟
  3. ec_master_eoe_start(master) — 启动 EoE 处理

拓扑计算源码分析

ec_slave_calc_upstream_port() (slave.c:1085)

根据端口接收时间判定 upstream 端口:

  • 初始 upstream_port = 0
  • 遍历端口 1-3,找接收时间最小的开放端口
  • 若端口 0 已关闭/bypassed,则任何开放端口都优于它
  • 不支持 DC 的从站默认端口 0

ec_master_calc_topology_rec() (master.c:2634)

递归构建拓扑树。关键代码逻辑:

// master/master.c : 2634
int ec_master_calc_topology_rec(
        ec_master_t *master, ec_slave_t *upstream_slave,
        unsigned int *slave_position)
{
    ec_slave_t *slave = master->slaves + *slave_position;
    // 端口遍历顺序:跳过 upstream,按 next_table 顺序
    static const unsigned int next_table[EC_MAX_PORTS] = {3, 2, 0, 1};

    slave->ports[slave->upstream_port].next_slave = upstream_slave;

    port_index = next_table[slave->upstream_port];
    while (port_index != slave->upstream_port) {
        if (!slave->ports[port_index].link.loop_closed) {
            // 链路开放的端口 → 有下游从站
            *slave_position = *slave_position + 1;
            slave->ports[port_index].next_slave =
                master->slaves + *slave_position;
            ec_master_calc_topology_rec(master, slave, slave_position);
        }
        port_index = next_table[port_index];
    }
}

ioctl 拓扑数据传递

源文件: master/ioctl.c

ec_ioctl_slave() (ioctl.c:216)

将内核 ec_slave_t 结构体转换为用户空间 ec_ioctl_slave_t

// master/ioctl.c : 297
for (i = 0; i < EC_MAX_PORTS; i++) {
    data.ports[i].desc = slave->ports[i].desc;
    data.ports[i].link = slave->ports[i].link;
    data.ports[i].receive_time = slave->ports[i].receive_time;
    // 关键:将内核指针转为 ring_position 索引
    if (slave->ports[i].next_slave) {
        data.ports[i].next_slave =
            slave->ports[i].next_slave->ring_position;
    } else {
        data.ports[i].next_slave = 0xffff; // 无连接
    }
    data.ports[i].delay_to_next_dc =
        slave->ports[i].delay_to_next_dc;
}

CommandGraph 源码分析

源文件: tool/CommandGraph.cpp

核心 execute() 方法逻辑 (line 92)

  1. 解析参数(DC / CRC 选项)
  2. m.getMaster(&master)ioctl(EC_IOCTL_MASTER) 获取 slave_count
  3. 循环 m.getSlave(&slave, i)ioctl(EC_IOCTL_SLAVE) 获取每个从站信息
  4. [CRC 模式] 通过 m.readReg(&io)ioctl(EC_IOCTL_SLAVE_REG_READ) 读 0x0300 寄存器获取 CRC 错误
  5. 生成 DOT 格式输出

DOT 边生成逻辑 (line 229)

// tool/CommandGraph.cpp : 229
// 遍历端口 1-3(跳过端口 0 = upstream)
for (int port = 1; port < EC_MAX_PORTS; port++) {
    uint16_t next_pos = si->ports[port].next_slave;
    if (next_pos == 0xffff) continue; // 无连接

    // 创建边:当前从站 → 下游从站
    cout << "slave" << si->position << " -- "
         << "slave" << next_pos
         << " [taillabel=\"" << port    // 当前端口索引
         << "\",headlabel=\"0"           // 对端始终是端口 0
         << "\",label=\"" << media      // MII/EBUS
         << "\"]";
}

DOT 边方向说明: CommandGraph 只遍历端口 1-3,因为端口 0 是 upstream(指向主站方向的父节点)。每条边表示一个从站的非 upstream 端口连接到下一个从站的端口 0(即下游从站的 upstream 端口)。0xFFFF 表示端口无连接(线型拓扑末端或星型拓扑未使用的端口)。

完整拓扑扫描时序图

sequenceDiagram participant IDLE as IDLE 线程 participant MFSM as Master FSM participant SSFSM as Slave Scan FSM<br/>(每个从站独立) participant SII as SII FSM participant BUS as EtherCAT 总线 loop 每个 IDLE 循环 IDLE->>MFSM: ec_fsm_master_exec() MFSM->>BUS: BRD 读取 0x0130 (从站数量) BUS-->>MFSM: WKC = N (slaves_responding) end Note over MFSM: 检测到从站数量变化 MFSM->>MFSM: kmalloc ec_slave_t[N] MFSM->>MFSM: 初始化 ring_position, station_address loop 所有从站 MFSM->>BUS: APRD 0x0900 (旧 DC 接收时间) end MFSM->>BUS: BWR 0x0010 = 0 (清除所有站地址) MFSM->>BUS: BWR 0x0900 = 0 (DC 延迟测量) Note over MFSM: 设置所有从站 fsm_ready<br/>触发并行扫描 par 从站 0 扫描 SSFSM->>BUS: APWR 0x0010 = 1 (分配地址) SSFSM->>BUS: FPRD 0x0000 (基础信息) SSFSM->>BUS: FPRD 0x0900 (DC 时间) SSFSM->>BUS: FPRD 0x0110 (DL 状态) SSFSM->>SII: 读 EEPROM (SII FSM) SII->>BUS: FPRD 读 EEPROM SSFSM->>SSFSM: 解析 Category SSFSM->>SSFSM: [CoE] 读取 PDO 映射 and 从站 1 扫描 (并行) SSFSM->>BUS: APWR 0x0010 = 2 SSFSM->>BUS: FPRD 0x0000 SSFSM->>SII: 读 EEPROM SSFSM->>SSFSM: 解析 Category and 从站 N-1 扫描 (并行) SSFSM->>BUS: APWR 0x0010 = N SSFSM->>BUS: FPRD 0x0000 SSFSM->>SII: 读 EEPROM end MFSM->>MFSM: 所有从站扫描完成 MFSM->>MFSM: ec_master_calc_dc() MFSM->>MFSM: calc_upstream_port() × N MFSM->>MFSM: calc_topology_rec() 递归建树 MFSM->>MFSM: calc_transmission_delays() MFSM->>MFSM: attach_slave_configs()
posted @ 2026-04-14 21:49  沐多  阅读(294)  评论(0)    收藏  举报