【原创】IgH EtherCAT主站详解(十一)--拓扑扫描流程
拓扑扫描流程
4.2 — EtherCAT 总线拓扑发现、从站识别、拓扑树构建与 PDO 映射
概览
什么是拓扑扫描?
拓扑扫描是 EtherCAT 主站自动识别总线上所有从站设备并构建拓扑树的过程。整个流程分为三个阶段:
- 总线探测:Master FSM 通过 BRD 广播读取寄存器 0x0130,获取在线从站数量
- 逐站扫描:为每个从站分配递增站地址,读取基础信息、EEPROM/SII 数据、构建 PDO 映射
- 拓扑构建:根据每个从站各端口的 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 广播,工作流程如下:
关键步骤说明
| 阶段 | 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() 调用拓扑计算,核心算法如下:
upstream_port 判定规则
每个从站需确定哪个端口面向主站(upstream)。判定依据是 DC 端口接收时间:
- 默认假设
upstream_port = 0(正常线型拓扑) - 若端口 0 链路关闭或被旁路,则寻找替代
- 遍历端口 1-3,找到接收时间最小的开放端口作为 upstream
- 不支持 DC 的从站只能假设 upstream_port = 0
拓扑递归算法
ec_master_calc_topology_rec(master, upstream_slave, slave_position) 是核心递归函数:
- 取当前从站 =
master->slaves[slave_position] - 设置当前从站的 upstream 端口的
next_slave = upstream_slave(指向父节点) - 按
next_table = {3, 2, 0, 1}顺序遍历非 upstream 端口 - 对每个链路开放的端口:分配下一个
slave_position,设置ports[i].next_slave = 下一个从站 - 递归调用自身处理下游从站
关键洞察: 拓扑树是基于物理位置顺序(ring_position)和端口接收时间递归构建的。从站数组按总线遍历顺序排列,递归过程中依次消耗数组元素,形成深度优先的拓扑树。
2.4 Slave Scan FSM 完整流程
每个从站由独立的 Slave FSM 进行扫描,包含以下步骤:
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 图形语言输出。
数据获取路径
ioctl 接口中的拓扑信息转换
关键转换发生在 master/ioctl.c 的 ec_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 生成逻辑:
- 创建 "master" 节点,连接到第一个从站 (slave0) 的端口 0
- 遍历所有从站,创建节点,标签包含 position / alias:pos / order / DC 信息
- 遍历每个从站的端口 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 检测到从站数量变化时:
ec_master_clear_slaves(master)— 清除旧从站列表kmalloc(sizeof(ec_slave_t) * count)— 分配从站数组- 遍历初始化每个
ec_slave_t:设置ring_position,station_address = i + 1,device_index - 进入 DC 旧时间读取 → 地址清除 → 延迟测量 → 扫描
并行扫描启动 (ec_fsm_master_state_dc_measure_delays, line 993)
DC 延迟测量完成后,进入并行扫描阶段:
- 对每个从站调用
ec_fsm_slave_set_ready(&slave->fsm),标记为可扫描 - 每个从站的 Slave FSM 在各自的
fsm_slave线程上下文中独立执行扫描 - Master FSM 进入
state_scan_slave轮询等待所有从站完成
扫描完成后 (ec_fsm_master_state_scan_slave, line 1048)
所有从站扫描完成后:
ec_master_attach_slave_configs(master)— 将应用层配置匹配到从站ec_master_calc_dc(master)— 计算拓扑树和传输延迟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)
- 解析参数(DC / CRC 选项)
m.getMaster(&master)→ioctl(EC_IOCTL_MASTER)获取slave_count- 循环
m.getSlave(&slave, i)→ioctl(EC_IOCTL_SLAVE)获取每个从站信息 - [CRC 模式] 通过
m.readReg(&io)→ioctl(EC_IOCTL_SLAVE_REG_READ)读 0x0300 寄存器获取 CRC 错误 - 生成 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 表示端口无连接(线型拓扑末端或星型拓扑未使用的端口)。

EtherCAT 总线拓扑扫描、从站识别与 PDO 构建流程。
浙公网安备 33010602011771号