文章目录
- [一、IgH 驱动适配框架](#一、IgH 驱动适配框架)
-
- 概览
- 技术详情
-
- [标准驱动 vs IgH 适配驱动对比](#标准驱动 vs IgH 适配驱动对比)
- [IgH 设备抽象层 API](#IgH 设备抽象层 API)
- [适配方法论:5 步流程](#适配方法论:5 步流程)
- [Generic 驱动方案](#Generic 驱动方案)
- [ec_device 与 net_device 的关系](#ec_device 与 net_device 的关系)
- [设备 MAC 匹配机制](#设备 MAC 匹配机制)
- 深入源码
-
- [ecdev_offer() 实现](#ecdev_offer() 实现)
- [ec_device_attach() 实现](#ec_device_attach() 实现)
- [ec_device_poll() 实现](#ec_device_poll() 实现)
- [ecdev_receive() 实现](#ecdev_receive() 实现)
- [Generic 驱动核心函数](#Generic 驱动核心函数)
- [二、r8169 驱动深入分析](#二、r8169 驱动深入分析)
-
- 概览
-
- [r8169 驱动与 EtherCAT](#r8169 驱动与 EtherCAT)
- 修改点总览
- 技术详情
-
- 钩子位置总览图
- 私有数据结构新增字段
- [核心辅助函数 get_ecdev()](#核心辅助函数 get_ecdev())
- [中断/轮询到 Datagram 的完整调用链](#中断/轮询到 Datagram 的完整调用链)
- 设备生命周期模式切换
- 深入源码
-
- [1. 私有数据结构扩展](#1. 私有数据结构扩展)
- [2. ec_poll() --- 核心轮询函数](#2. ec_poll() — 核心轮询函数)
- [3. ec_kick_watchdog() --- PHY 链路变化处理](#3. ec_kick_watchdog() — PHY 链路变化处理)
- [4. rtl_rx() --- RX 路径关键修改](#4. rtl_rx() — RX 路径关键修改)
- [5. rtl8169_start_xmit() --- TX 发送修改](#5. rtl8169_start_xmit() — TX 发送修改)
- [6. rtl_tx() --- TX 完成处理修改](#6. rtl_tx() — TX 完成处理修改)
- [7. rtl_irq_enable() --- 中断禁用](#7. rtl_irq_enable() — 中断禁用)
- [8. rtl_open() --- 设备打开修改](#8. rtl_open() — 设备打开修改)
- [9. rtl_init_one() --- 设备探测修改](#9. rtl_init_one() — 设备探测修改)
- [10. 电源管理阻断](#10. 电源管理阻断)
- 修改点完整清单
- 三、添加新驱动指南
-
- 概览
- 技术详情
-
- [适配 Checklist 流程图](#适配 Checklist 流程图)
- [Step 0: 准备工作](#Step 0: 准备工作)
- [Step 1: 添加 EtherCAT 字段到私有数据结构](#Step 1: 添加 EtherCAT 字段到私有数据结构)
- [Step 2: 实现 ec_poll() 函数](#Step 2: 实现 ec_poll() 函数)
- [Step 3: 修改设备初始化和清理](#Step 3: 修改设备初始化和清理)
- [Step 4: 修改中断控制](#Step 4: 修改中断控制)
- [Step 5: 修改 TX 路径](#Step 5: 修改 TX 路径)
- [Step 6: 修改 RX 路径(最关键)](#Step 6: 修改 RX 路径(最关键))
- [Step 7: 阻断电源管理](#Step 7: 阻断电源管理)
- [Step 8: 编译和集成](#Step 8: 编译和集成)
- 深入源码
一、IgH 驱动适配框架
5.1 --- 网卡驱动适配方法论 --- devices/
概览
为什么需要适配网卡驱动?
EtherCAT 主站需要精确控制网卡的数据收发时机。标准 Linux 网卡驱动使用中断驱动模型,其响应时间受内核调度器影响,无法满足 EtherCAT 周期性通信(通常 1ms 甚至更短)的确定性要求。
IgH EtherCAT Master 通过适配网卡驱动 解决这个问题:将网卡从中断驱动模式切换到轮询 (Poll) 模式,由主站的实时线程主动调用 poll 函数来收发数据。
核心思想
IgH 并不重新实现网卡驱动,而是在现有 Linux 内核驱动基础上 添加 EtherCAT 钩子 (hook)。驱动在运行时通过 MAC 地址匹配决定是工作在普通网络模式还是** EtherCAT 轮询模式**。两种模式互斥,同一时刻只能处于其中一种。
两种驱动方案
| 方案 | 实现 | 适用场景 | 性能 |
|---|---|---|---|
| Generic 驱动 | devices/generic.c |
快速验证,无需修改驱动源码 | 一般(通过 socket 收发,有额外拷贝) |
| 原生适配驱动 | devices/r8169/ 等 |
生产环境,需要最佳实时性能 | 优秀(直接 DMA 零拷贝) |
技术详情
标准驱动 vs IgH 适配驱动对比

IgH 设备抽象层 API
IgH 为网卡驱动提供一组简洁的 API(定义在 devices/ecdev.h),驱动只需调用这几个函数即可完成适配:
| API 函数 | 签名 | 作用 | 调用时机 |
|---|---|---|---|
ecdev_offer() |
ec_device_t *ecdev_offer(struct net_device *, ec_pollfunc_t, struct module *) |
将网卡设备"提交"给 EtherCAT 主站系统。若 MAC 匹配某个主站配置,返回非 NULL 的 ec_device_t* |
驱动 probe(rtl_init_one) |
ecdev_open() |
int ecdev_open(ec_device_t *) |
打开设备,调用 ndo_open,将主站切换到 IDLE 状态 |
probe 成功后立即调用 |
ecdev_close() |
void ecdev_close(ec_device_t *) |
关闭设备,调用 ndo_stop,主站离开 IDLE |
驱动移除(rtl_remove_one) |
ecdev_withdraw() |
void ecdev_withdraw(ec_device_t *) |
从主站撤回设备,释放资源 | 驱动移除或 open 失败时 |
ecdev_receive() |
void ecdev_receive(ec_device_t *, const void *, size_t) |
将接收到的原始帧数据传递给主站解析 | RX 路径(替代 napi_gro_receive) |
ecdev_set_link() |
void ecdev_set_link(ec_device_t *, uint8_t) |
通知主站链路状态变化 | 链路变化 / 看门狗超时 |
适配方法论:5 步流程
- 选择基准内核驱动版本
- 添加 ecdev 提交逻辑
- 实现 ec_poll 轮询函数
- 修改中断/设备管理
- 修改 TX/RX 数据路径
复制原始驱动源码
重命名为 *-ethercat.c
probe 时调用 ecdev_offer()
若匹配则跳过 register_netdev()
读取中断状态寄存器
调用原始 TX/RX 处理
处理链路变化事件
禁用 request_irq/free_irq
禁用 NAPI enable/disable
阻断电源管理 suspend/resume
RX: ecdev_receive 替代 napi_gro_receive
TX: 跳过 SKB 生命周期管理
跳过 netdev 队列操作
Generic 驱动方案
Generic 驱动(devices/generic.c)是一种无需修改驱动源码 的适配方案。它创建一个虚拟 net_device,通过内核 socket 收发原始以太网帧:
| 组件 | Generic 驱动 | 原生适配驱动 |
|---|---|---|
| 收发方式 | kernel_sendmsg / kernel_recvmsg |
直接 DMA buffer 操作 |
| SKB 管理 | 每次收发分配/释放 | 预分配环形缓冲 |
| 数据拷贝 | 至少一次额外拷贝 | 零拷贝(RX 直接传递 DMA 地址) |
| 适用场景 | 开发调试、快速验证 | 生产部署、实时性要求高 |
| 适配工作量 | 零(无需改驱动) | 中等(需修改驱动源码) |
⚠ 注意
Generic 驱动不适用于 Xenomai 内核空间模式。在 RTDM 模式下,必须使用原生适配驱动。此外,Generic 驱动的性能和延迟特性不如原生适配驱动。
ec_device 与 net_device 的关系
IgH 主站
网卡驱动
注册到
poll 指针
dev 指针
master 指针
ndo_start_xmit
ecdev_offer 时绑定
net_device
net_device_ops
ec_poll 函数
ec_device
ec_master
设备 MAC 匹配机制
ecdev_offer() 内部遍历所有已配置的主站实例,比较网卡的 MAC 地址与主站的 main_mac / backup_mac 配置(来自 /etc/sysconfig/ethercat 或模块参数)。匹配成功后:
- 调用
ec_device_attach()绑定设备和 poll 函数 - 将网卡名称改为
eXaM格式(如e0a0) - 返回非 NULL 的
ec_device_t*指针
若没有任何主站匹配该 MAC 地址,则返回 NULL,设备以普通网卡模式注册到内核网络栈。
深入源码
ecdev_offer() 实现
位置 : master/module.c:487--532
master/module.c : 487
c
ec_device_t *ecdev_offer(struct net_device *net_dev,
ec_pollfunc_t poll, struct module *module)
{
ec_master_t *master;
ec_device_t *device;
// 遍历所有主站实例
list_for_each_entry(master, &masters;, list) {
// 检查主设备和备设备槽位
for (dev_idx = EC_DEVICE_MAIN;
dev_idx < ec_master_num_devices(master);
dev_idx++) {
device = &master-;>devices[dev_idx];
// 如果槽位未被占用且 MAC 匹配
if (!device->dev &&
(is_broadcast_ether_addr(device->mac) ||
ether_addr_equal(device->mac, net_dev->dev_addr))) {
// 绑定设备
ec_device_attach(device, net_dev, poll, module);
// 修改网卡名称
snprintf(net_dev->name, IFNAMSIZ, "e%ua%u",
device->master->index, dev_idx);
return device; // 返回匹配的 ec_device
}
}
}
return NULL; // 无匹配,设备不归 EtherCAT 管理
}
ec_device_attach() 实现
位置 : master/device.c:223--248
将 net_device 和 poll 函数指针保存到 ec_device 结构体中。同时为预分配的 TX SKB 设置源 MAC 地址。
master/device.c : 223
c
void ec_device_attach(ec_device_t *device, struct net_device *net_dev,
ec_pollfunc_t poll, struct module *module)
{
unsigned int i;
device->dev = net_dev;
device->poll = poll;
device->module = module;
// 设置 TX SKB 的源 MAC 地址
for (i = 0; i < EC_TX_RING_SIZE; i++) {
device->tx_skb[i]->dev = net_dev;
// 填充以太网头:目的=广播, EtherType=0x88A4
}
}
ec_device_poll() 实现
位置 : master/device.c:563--578
主站线程在每个周期调用此函数,记录时间戳后调用驱动的 poll 回调。
master/device.c : 563
c
void ec_device_poll(ec_device_t *device)
{
// 记录 poll 时间(用于统计和超时检测)
device->jiffies_poll = jiffies;
// 调用驱动注册的 poll 函数
if (device->poll) {
device->poll(device->dev);
}
}
ecdev_receive() 实现
位置 : master/device.c:724--758
master/device.c : 724
c
void ecdev_receive(ec_device_t *device, const void *data, size_t size)
{
const uint8_t *ec_data;
// 跳过以太网头(14 字节)
ec_data = ((const uint8_t *) data) + ETH_HLEN;
size -= ETH_HLEN;
// 更新接收统计
device->rx_count++;
device->rx_bytes += size;
// 分发到主站 Datagram 处理
ec_master_receive_datagrams(device->master, ec_data, size);
}
Generic 驱动核心函数
位置 : devices/generic.c
ec_gen_device_poll()
Generic 驱动的 poll 实现:通过 kernel_recvmsg() 从原始 socket 读取帧数据,调用 ecdev_receive() 传递给主站。使用 budget=10 的循环处理积压帧。
devices/generic.c : 330
c
void ec_gen_device_poll(ec_gen_device_t *dev)
{
struct msghdr msg;
struct kvec iov;
int ret, budget = 10;
ecdev_set_link(dev->ecdev, netif_carrier_ok(dev->used_netdev));
do {
iov.iov_base = dev->rx_buf;
iov.iov_len = EC_GEN_RX_BUF_SIZE;
memset(&msg;, 0, sizeof(msg));
ret = kernel_recvmsg(dev->socket, &msg;, &iov;, 1,
iov.iov_len, MSG_DONTWAIT);
if (ret > 0) {
ecdev_receive(dev->ecdev, dev->rx_buf, ret);
} else if (ret < 0) {
break;
}
budget--;
} while (budget);
}
二、r8169 驱动深入分析
5.2 --- devices/r8169/r8169_main-6.1-ethercat.c --- 逐函数 IgH 适配分析
概览
r8169 驱动与 EtherCAT
r8169 是 Realtek RTL8169/8168/8101 系列 Gigabit 以太网控制器的 Linux 内核驱动。IgH EtherCAT Master 对其进行了适配,使其能够在 EtherCAT 轮询模式下工作。
适配策略的核心是一个运行时模式开关 :在设备探测 (probe) 时,驱动调用 ecdev_offer() 尝试将设备提交给 EtherCAT 主站。如果设备的 MAC 地址与某个主站配置匹配,驱动进入 EtherCAT 模式;否则,设备以普通网卡模式注册到内核网络栈。两种模式互斥。
适用版本
本分析基于 Linux Kernel 6.1 版本的 r8169 驱动。源文件:
r8169_main-6.1-ethercat.c(~5400 行),对比文件:r8169_main-6.1-orig.c。
修改点总览
| 修改类别 | 修改数量 | 关键函数 |
|---|---|---|
| 私有数据结构扩展 | 4 个新字段 | struct rtl8169_private |
| 新增函数 | 2 个 | ec_poll(), ec_kick_watchdog() |
| 设备初始化/清理 | 4 个函数修改 | rtl_init_one, rtl_remove_one, rtl_open, rtl8169_close |
| 中断控制 | 3 个函数修改 | rtl_irq_enable, rtl_schedule_task |
| TX 数据路径 | 4 个函数修改 | rtl8169_start_xmit, rtl_tx, rtl8169_tx_clear_range, rtl8169_tx_clear |
| RX 数据路径 | 1 个函数修改 | rtl_rx(最关键修改) |
| 电源管理 | 3 个函数修改 | suspend, resume, runtime_suspend |
技术详情
钩子位置总览图

私有数据结构新增字段
| 字段 | 类型 | 说明 |
|---|---|---|
ecdev_ |
ec_device_t * |
EtherCAT 设备指针。非 NULL 表示设备处于 EtherCAT 模式 |
ec_watchdog_jiffies |
unsigned long |
最后一次接收帧的 jiffies 时间戳,用于链路看门狗 |
ec_watchdog_kicker |
struct irq_work |
延迟中断工作,用于在安全上下文触发 PHY 链路变化处理 |
ecdev_initialized |
bool |
保护标志,防止 get_ecdev() 在 ecdev_ 赋值前被调用 |
核心辅助函数 get_ecdev()
所有修改点都通过 get_ecdev(tp) 判断当前是否处于 EtherCAT 模式。该函数返回 tp->ecdev_ 指针:
- 返回 非 NULL → EtherCAT 模式:执行 EtherCAT 分支逻辑
- 返回 NULL → 普通模式:执行原始 Linux 网络栈逻辑
中断/轮询到 Datagram 的完整调用链
device->poll(dev)
读取中断状态
有 TX 完成事件
有 RX 到达事件
LinkChg 事件
get_ecdev(tp) != NULL
跳过 ETH_HLEN
get_ecdev(tp) != NULL
irq_work 中断上下文
phy_mac_interrupt()
rtl_ack_events(tp, status)
主站线程周期触发
ec_device_poll()
ec_poll()
rtl_get_events(tp)
rtl_tx(dev, tp, 100)
rtl_rx(dev, tp, 100)
irq_work_queue(ec_watchdog_kicker)
ecdev_receive(ecdev, rx_buf, pkt_size)
ec_master_receive_datagrams()
Datagram 分发完成
跳过 napi_consume_skb
跳过 queue 管理
TX 完成
ec_kick_watchdog()
PHY 状态机处理
清除中断状态
设备生命周期模式切换
返回非 NULL
返回 NULL
跳过
成功
失败
非 NULL
NULL
rtl_init_one() --- 设备探测
ecdev_offer(dev, ec_poll, THIS_MODULE)
EtherCAT 模式
普通网络模式
跳过 register_netdev()
ecdev_open(ecdev)
设备就绪
ec_device → master
ecdev_withdraw(ecdev)
register_netdev(dev)
普通网卡就绪
rtl_remove_one() --- 设备移除
get_ecdev(tp)?
ecdev_close(ecdev)
irq_work_sync()
ecdev_withdraw(ecdev)
unregister_netdev(dev)
深入源码
1. 私有数据结构扩展
位置 : r8169_main-6.1-ethercat.c:632--644
r8169_main-6.1-ethercat.c : 632
c
struct rtl8169_private {
/* ... 原有字段 ... */
u32 ocp_base;
/* === IgH EtherCAT 新增字段 === */
ec_device_t *ecdev_; // EtherCAT 设备指针
unsigned long ec_watchdog_jiffies; // RX 看门狗时间戳
struct irq_work ec_watchdog_kicker; // PHY 链路变化延迟工作
bool ecdev_initialized; // 安全保护标志
};
static inline ec_device_t *get_ecdev(struct rtl8169_private *adapter)
{
#ifdef EC_ENABLE_DRIVER_RESOURCE_VERIFYING
WARN_ON(!adapter->ecdev_initialized);
#endif
return adapter->ecdev_;
}
2. ec_poll() --- 核心轮询函数
位置 : r8169_main-6.1-ethercat.c:5212--5233
这是注册给 IgH 主站的 poll 回调函数,替代中断处理程序。主站线程每个周期调用一次。
r8169_main-6.1-ethercat.c : 5212
c
static void ec_poll(struct net_device *dev)
{
struct rtl8169_private *tp = netdev_priv(dev);
u16 status = rtl_get_events(tp);
// 链路看门狗:超过 2 秒未收到帧则更新链路状态
if (jiffies - tp->ec_watchdog_jiffies >= 2 * HZ) {
ecdev_set_link(get_ecdev(tp), netif_carrier_ok(dev));
tp->ec_watchdog_jiffies = jiffies;
}
// 无事件则直接返回
if ((status & 0xffff) == 0xffff || !(status & tp->irq_mask))
return;
// 处理 TX 完成和 RX 到达
rtl_tx(dev, tp, 100);
rtl_rx(dev, tp, 100);
// 链路变化事件 → 通过 irq_work 延迟到安全上下文处理
if (status & LinkChg)
irq_work_queue(&tp-;>ec_watchdog_kicker);
// 清除已处理的中断状态
rtl_ack_events(tp, status);
}
3. ec_kick_watchdog() --- PHY 链路变化处理
位置 : r8169_main-6.1-ethercat.c:5204--5210
r8169_main-6.1-ethercat.c : 5204
c
static void ec_kick_watchdog(struct irq_work *work)
{
struct rtl8169_private *tp =
container_of(work, struct rtl8169_private, ec_watchdog_kicker);
phy_mac_interrupt(tp->phydev);
}
因为 ec_poll() 运行在实时线程或原子上下文中,不能直接调用可能休眠的 PHY 状态机函数。irq_work 机制将 phy_mac_interrupt() 延迟到硬件中断上下文执行,这是安全的。
4. rtl_rx() --- RX 路径关键修改
位置 : r8169_main-6.1-ethercat.c:4484--4525
这是数据路径中最关键的修改:在 EtherCAT 模式下跳过 SKB 分配和协议栈处理,直接将 DMA 缓冲区数据传递给主站。
r8169_main-6.1-ethercat.c : 4484
c
/* === 原始代码 vs EtherCAT 修改 === */
/* 原始: 分配 SKB */
// skb = napi_alloc_skb(&tp-;>napi, pkt_size);
/* EtherCAT: 跳过 SKB 分配 */
if (!get_ecdev(tp)) {
skb = napi_alloc_skb(&tp-;>napi, pkt_size);
if (unlikely(!skb)) {
dev->stats.rx_dropped++;
goto release_descriptor;
}
} else {
skb = NULL; // EtherCAT 模式不分配 SKB
}
addr = le64_to_cpu(desc->addr);
rx_buf = page_address(tp->Rx_databuff[entry]);
dma_sync_single_for_cpu(d, addr, pkt_size, DMA_FROM_DEVICE);
prefetch(rx_buf);
/* 关键分叉: 零拷贝传递 */
if (get_ecdev(tp)) {
// 直接将 DMA 缓冲区传给 EtherCAT 主站(零拷贝)
ecdev_receive(get_ecdev(tp), rx_buf, pkt_size);
tp->ec_watchdog_jiffies = jiffies;
} else {
// 标准路径: 拷贝到 SKB
skb_copy_to_linear_data(skb, rx_buf, pkt_size);
skb->tail += pkt_size;
skb->len = pkt_size;
}
dma_sync_single_for_device(d, addr, pkt_size, DMA_FROM_DEVICE);
/* EtherCAT 模式跳过所有协议栈处理 */
if (!get_ecdev(tp)) {
rtl8169_rx_csum(skb, status);
skb->protocol = eth_type_trans(skb, dev);
rtl8169_rx_vlan_tag(desc, skb);
if (skb->pkt_type == PACKET_MULTICAST)
dev->stats.multicast++;
napi_gro_receive(&tp-;>napi, skb);
dev_sw_netstats_rx_add(dev, pkt_size);
}
5. rtl8169_start_xmit() --- TX 发送修改
位置 : r8169_main-6.1-ethercat.c:4233--4277
r8169_main-6.1-ethercat.c : 4233
c
/* Door bell: EtherCAT 模式始终触发 */
door_bell = get_ecdev(tp) || __netdev_sent_queue(dev, skb->len, netdev_xmit_more());
/* Queue stop: EtherCAT 模式永不停止队列 */
stop_queue = !get_ecdev(tp) && !rtl_tx_slots_avail(tp);
/* ... TX descriptor 填充和 DMA 映射 ... */
/* 错误处理: EtherCAT 模式跳过 SKB 释放 */
err_dma_0:
if (!get_ecdev(tp))
dev_kfree_skb_any(skb);
err_stop_0:
if (!get_ecdev(tp))
netif_stop_queue(dev);
6. rtl_tx() --- TX 完成处理修改
位置 : r8169_main-6.1-ethercat.c:4382--4402
r8169_main-6.1-ethercat.c : 4382
c
/* 跳过 NAPI SKB 释放 */
if (!get_ecdev(tp))
napi_consume_skb(skb, budget);
/* 跳过 netdev 队列统计 */
if (!get_ecdev(tp)) {
netdev_completed_queue(dev, pkts_compl, bytes_compl);
dev_sw_netstats_tx_add(dev, pkts_compl, bytes_compl);
}
/* 跳过队列唤醒 */
if (!get_ecdev(tp) && netif_queue_stopped(dev) && rtl_tx_slots_avail(tp))
netif_wake_queue(dev);
7. rtl_irq_enable() --- 中断禁用
位置 : r8169_main-6.1-ethercat.c:1281
r8169_main-6.1-ethercat.c : 1281
c
static void rtl_irq_enable(struct rtl8169_private *tp)
{
if (get_ecdev(tp))
return; // EtherCAT: 永不启用硬件中断
if (rtl_is_8125(tp))
RTL_W32(tp, IntrMask_8125, tp->irq_mask);
else
RTL_W16(tp, IntrMask, tp->irq_mask);
}
8. rtl_open() --- 设备打开修改
位置 : r8169_main-6.1-ethercat.c:4737--4753
r8169_main-6.1-ethercat.c : 4737
c
/* 跳过中断注册 */
if (!get_ecdev(tp)) {
retval = request_irq(tp->irq, rtl8169_interrupt, irqflags,
dev->name, tp);
if (retval < 0)
goto err_release_fw_2;
}
/* 跳过 netdev 队列启动,改为设置 EtherCAT 链路状态 */
if (!get_ecdev(tp))
netif_start_queue(dev);
else
ecdev_set_link(get_ecdev(tp), netif_carrier_ok(dev));
9. rtl_init_one() --- 设备探测修改
位置 : r8169_main-6.1-ethercat.c:5414--5447
r8169_main-6.1-ethercat.c : 5414
c
tp->ecdev_initialized = false;
/* ... 硬件初始化 ... */
/* 提交设备给 EtherCAT 主站 */
tp->ecdev_ = ecdev_offer(dev, ec_poll, THIS_MODULE);
tp->ecdev_initialized = true;
tp->ec_watchdog_jiffies = jiffies;
if (!get_ecdev(tp)) {
/* 普通模式: 注册到内核网络栈 */
rc = register_netdev(dev);
if (rc)
return rc;
}
/* ... 电源管理设置 ... */
if (get_ecdev(tp)) {
/* EtherCAT 模式: 打开设备并初始化 irq_work */
rc = ecdev_open(get_ecdev(tp));
init_irq_work(&tp-;>ec_watchdog_kicker, ec_kick_watchdog);
if (rc) {
ecdev_withdraw(get_ecdev(tp));
return rc;
}
}
10. 电源管理阻断
位置 : r8169_main-6.1-ethercat.c:4837--4870
EtherCAT 设备绝不能被挂起或进入低功耗状态:
r8169_main-6.1-ethercat.c : 4837
c
static int __maybe_unused rtl8169_suspend(struct device *device)
{
struct rtl8169_private *tp = dev_get_drvdata(device);
if (get_ecdev(tp)) {
return -EBUSY; // EtherCAT 设备禁止挂起
}
/* ... 原始 suspend 逻辑 ... */
}
/* resume() 和 runtime_suspend() 同样添加此守护检查 */
修改点完整清单
| # | 函数 | 行号 | 修改内容 | 模式判断 |
|---|---|---|---|---|
| 1 | struct rtl8169_private |
632--635 | 新增 4 个 EtherCAT 字段 | --- |
| 2 | get_ecdev() |
638--644 | 新增辅助函数 | --- |
| 3 | rtl_irq_enable() |
1281 | 中断使能 → 直接返回 | if (get_ecdev(tp)) return |
| 4 | rtl_schedule_task() |
2171 | 工作调度 → 跳过 | if (!get_ecdev(tp)) |
| 5 | rtl8169_tx_clear_range() |
3883 | TX 清理 → 跳过 SKB 释放 | if (!get_ecdev(tp) && skb) |
| 6 | rtl8169_tx_clear() |
3892 | TX 清理 → 跳过队列重置 | if (!get_ecdev(tp)) |
| 7 | rtl8169_cleanup() |
3898 | 清理 → 跳过 NAPI disable | if (!get_ecdev(tp)) |
| 8 | rtl_reset_work() |
3938, 3946 | 重置 → 跳过队列停止和 NAPI | if (!get_ecdev(tp)) |
| 9 | rtl8169_start_xmit() |
4233, 4242, 4270 | TX 发送 → 始终响铃/不停队列/不释放 SKB | `get_ecdev(tp) |
| 10 | rtl_tx() |
4382, 4389, 4402 | TX 完成 → 跳过 SKB 释放和队列管理 | if (!get_ecdev(tp)) |
| 11 | rtl_rx() |
4484--4525 | RX 接收 → 零拷贝 ecdev_receive | if (get_ecdev(tp)) |
| 12 | rtl8169_up() |
4659 | 启动 → 跳过 NAPI enable | if (!get_ecdev(tp)) |
| 13 | rtl8169_close() |
4674, 4681 | 关闭 → 跳过队列停止和 IRQ 释放 | if (!get_ecdev(tp)) |
| 14 | rtl_open() |
4737, 4750 | 打开 → 跳过 request_irq + ecdev_set_link | if (!get_ecdev(tp)) |
| 15 | suspend/resume/runtime_suspend |
4837--4870 | 电源管理 → 返回 -EBUSY | if (get_ecdev(tp)) return -EBUSY |
| 16 | rtl_remove_one() |
4931 | 移除 → ecdev_close + ecdev_withdraw | if (get_ecdev(tp)) |
| 17 | ec_kick_watchdog() |
5204 | 新增: PHY 链路变化 irq_work 回调 | --- |
| 18 | ec_poll() |
5212 | 新增: IgH 轮询函数 | --- |
| 19 | rtl_init_one() |
5414--5447 | 探测 → ecdev_offer + ecdev_open | if (!get_ecdev(tp)) |
三、添加新驱动指南
5.3 --- 如何为 IgH EtherCAT Master 适配新的网卡驱动
概览
适配新网卡驱动的核心思路
为 IgH EtherCAT Master 适配新网卡驱动的本质是:在现有 Linux 内核驱动的基础上添加条件分支,让驱动在 EtherCAT 模式下使用轮询替代中断、直接操作 DMA 缓冲区替代 SKB 分配。
适配工作不需要重写驱动,而是在关键路径上插入判断钩子 ,所有修改都通过 get_ecdev(tp) 函数的返回值来决定执行哪条分支。
预估工作量
对于熟悉 Linux 网卡驱动和 NAPI 框架的开发者,适配一个新驱动通常需要 ~20 个修改点 ,核心工作包括:实现
ec_poll()函数、修改 RX/TX 路径、阻断中断和电源管理。可以参考r8169适配作为模板。
技术详情
适配 Checklist 流程图
是
否
是
否
Step 0: 准备工作
Step 1: 添加 EtherCAT 字段
Step 2: 实现 ec_poll()
Step 3: 修改设备初始化
Step 4: 修改中断控制
Step 5: 修改 TX 路径
Step 6: 修改 RX 路径
Step 7: 阻断电源管理
Step 8: 编译测试
通过?
Step 9: 集成验证
调试修复
主站识别设备?
Step 10: 性能验证
检查 MAC 配置
/etc/sysconfig/ethercat
完成
Step 0: 准备工作
- 复制原始驱动文件 :将内核源码中的驱动文件复制到
devices/<driver_name>/目录,重命名添加-ethercat后缀 - 添加 include :在头文件 include 区域添加
#include "../ecdev.h" - 修改头文件引用 :将本地头文件引用改为
-ethercat版本
Step 1: 添加 EtherCAT 字段到私有数据结构
模板代码 --- 私有数据结构扩展
c
/* 在驱动的私有数据结构末尾添加 */
struct driver_private {
/* ... 原有字段 ... */
/* === IgH EtherCAT 字段 === */
ec_device_t *ecdev_; // EtherCAT 设备指针
unsigned long ec_watchdog_jiffies; // RX 看门狗时间戳
bool ecdev_initialized; // 安全保护标志
};
/* 辅助函数:判断当前是否 EtherCAT 模式 */
static inline ec_device_t *get_ecdev(struct driver_private *tp)
{
#ifdef EC_ENABLE_DRIVER_RESOURCE_VERIFYING
WARN_ON(!tp->ecdev_initialized);
#endif
return tp->ecdev_;
}
⚠ 注意
如果驱动使用 NAPI 以外的中断处理方式(如 tasklet),可能还需要添加
struct irq_work用于延迟处理 PHY 链路变化事件。
Step 2: 实现 ec_poll() 函数
这是适配的核心。ec_poll() 替代中断处理程序,由主站线程周期性调用。
模板代码 --- ec_poll() 实现
c
static void ec_poll(struct net_device *dev)
{
struct driver_private *tp = netdev_priv(dev);
/* 1. 链路看门狗: 超时未收到帧则更新链路状态 */
if (jiffies - tp->ec_watchdog_jiffies >= 2 * HZ) {
ecdev_set_link(get_ecdev(tp), netif_carrier_ok(dev));
tp->ec_watchdog_jiffies = jiffies;
}
/* 2. 读取中断状态寄存器 */
u32 status = read_interrupt_status(tp);
if (!status)
return;
/* 3. 处理 TX 完成 */
driver_tx_cleanup(dev, tp, budget);
/* 4. 处理 RX 到达 */
driver_rx(dev, tp, budget);
/* 5. 处理链路变化(如需要,通过 irq_work 延迟) */
if (status & LINK_CHANGE_BIT)
handle_link_change(tp);
/* 6. 清除中断状态 */
clear_interrupt_status(tp, status);
}
Step 3: 修改设备初始化和清理
| 函数 | 修改内容 | 模板代码 |
|---|---|---|
probe() |
调用 ecdev_offer() 替代 register_netdev() | tp->ecdev_ = ecdev_offer(dev, ec_poll, THIS_MODULE); |
probe() 后半段 |
若 EtherCAT 模式则调用 ecdev_open() | if (get_ecdev(tp)) ecdev_open(get_ecdev(tp)); |
remove() |
调用 ecdev_close() + ecdev_withdraw() 替代 unregister_netdev() | if (get_ecdev(tp)) { ecdev_close(); ecdev_withdraw(); } |
ndo_open() |
跳过 request_irq() + 设置 ecdev_set_link() | if (!get_ecdev(tp)) request_irq(...); |
ndo_stop() |
跳过 free_irq() + 跳过 netif_stop_queue() | if (!get_ecdev(tp)) free_irq(...); |
Step 4: 修改中断控制
| 修改点 | 模板代码 |
|---|---|
| 中断使能函数 | if (get_ecdev(tp)) return; |
| 延迟工作调度 | if (!get_ecdev(tp)) schedule_work(...); |
| NAPI enable/disable | if (!get_ecdev(tp)) napi_enable/disable(...); |
Step 5: 修改 TX 路径
| 修改点 | 原因 | 模板代码 |
|---|---|---|
| TX 发送入口 | 主站管理 SKB 生命周期,驱动不应干预 | Door bell 始终触发:`door_bell = get_ecdev(tp) |
| 队列停止 | 主站控制流量,不使用 netdev 队列 | stop = !get_ecdev(tp) && !tx_slots_avail(); |
| TX 完成清理 | 跳过 SKB 释放和队列管理 | if (!get_ecdev(tp)) napi_consume_skb(skb, budget); |
| 错误处理 | 不释放主站的 SKB | if (!get_ecdev(tp)) dev_kfree_skb_any(skb); |
Step 6: 修改 RX 路径(最关键)
模板代码 --- RX 路径核心修改
c
/* === RX 处理中的 EtherCAT 分支 === */
/* 跳过 SKB 分配 */
if (!get_ecdev(tp)) {
skb = napi_alloc_skb(&tp-;>napi, pkt_size);
if (!skb) {
dev->stats.rx_dropped++;
goto release;
}
} else {
skb = NULL;
}
/* 获取 DMA 缓冲区数据 */
rx_buf = get_rx_buffer(tp, entry);
dma_sync_single_for_cpu(dma_dev, addr, pkt_size, DMA_FROM_DEVICE);
/* 关键分叉 */
if (get_ecdev(tp)) {
/* EtherCAT: 零拷贝直接传递给主站 */
ecdev_receive(get_ecdev(tp), rx_buf, pkt_size);
tp->ec_watchdog_jiffies = jiffies;
} else {
/* 标准: 拷贝到 SKB */
skb_copy_to_linear_data(skb, rx_buf, pkt_size);
skb_put(skb, pkt_size);
}
dma_sync_single_for_device(dma_dev, addr, pkt_size, DMA_FROM_DEVICE);
/* 跳过所有协议栈处理 */
if (!get_ecdev(tp)) {
skb->protocol = eth_type_trans(skb, dev);
napi_gro_receive(&tp-;>napi, skb);
dev_sw_netstats_rx_add(dev, pkt_size);
}
Step 7: 阻断电源管理
模板代码 --- 电源管理阻断
c
static int driver_suspend(struct device *dev)
{
struct driver_private *tp = dev_get_drvdata(dev);
if (get_ecdev(tp))
return -EBUSY; // EtherCAT 设备禁止挂起
/* ... 原始 suspend ... */
}
/* resume() 和 runtime_suspend() 添加同样的守护检查 */
Step 8: 编译和集成
- 在
devices/Makefile.am中添加新驱动目录 - 在
configure.ac中添加新驱动的编译选项 - 编译:
./configure --enable-<driver> && make - 配置 MAC 地址:
/etc/sysconfig/ethercat或模块参数master0_main_mac=xx:xx:xx:xx:xx:xx - 加载:
insmod ec_master.ko然后insmod <driver>.ko
深入源码
常见陷阱和注意事项
❌ 陷阱 1: 在 ec_poll() 中调用可能休眠的函数
ec_poll()运行在实时线程上下文中,绝不能 调用可能休眠的函数(如msleep()、mutex_lock()、kmalloc(GFP_KERNEL))。PHY 状态机操作(phy_mac_interrupt())在某些内核版本中可能休眠,需要通过irq_work延迟执行。
❌ 陷阱 2: 忘记跳过 NAPI 操作在 EtherCAT 模式下 NAPI 从未被启用,调用
napi_disable()会导致死锁或 WARN。确保所有napi_enable/disable调用都被get_ecdev()保护。
❌ 陷阱 3: SKB 生命周期冲突在 TX 完成处理中释放 SKB(
dev_kfree_skb_any/napi_consume_skb)会导致主站的 TX 环形缓冲损坏。EtherCAT 模式下主站管理 SKB 生命周期,驱动不应释放。同理,TX 错误路径也不能释放 SKB。
❌ 陷阱 4: DMA 缓冲区同步遗漏在 RX 路径中调用
ecdev_receive()之前必须确保dma_sync_single_for_cpu()已经完成,否则 CPU 可能读到陈旧数据。调用之后必须dma_sync_single_for_device()将缓冲区还给 DMA 引擎。
⚠ 陷阱 5: 中断状态寄存器 0xFFFF 问题某些硬件在中断禁用后读取状态寄存器返回
0xFFFF,这不是有效状态。在ec_poll()中应添加过滤:if ((status & 0xffff) == 0xffff) return;。
⚠ 陷阱 6: 链路状态看门狗必须实现 RX 看门狗机制。如果超过一定时间(通常 2 秒)没有收到帧,应通过
ecdev_set_link()通知主站链路可能已断开。否则拔掉网线后主站会持续等待超时。
调试方法
1. 编译时调试
Shell
bash
# 启用调试编译
./configure --enable-debug --enable-r8169
make
# 检查模块是否包含 ecdev 符号
nm -D ec_r8169.ko | grep ecdev
2. 加载时调试
Shell
bash
# 先加载主站模块
insmod ec_master.ko main_devices=XX:XX:XX:XX:XX:XX
# 再加载驱动模块(观察 dmesg 输出)
insmod ec_r8169.ko
dmesg | tail -20
# 期望看到类似输出:
# r8169 0000:01:00.0: offering device to EtherCAT master
# EtherCAT: Accepted device 01:02:03:04:05:06 as main device for master 0
# 检查设备状态
ethercat master
3. 运行时调试
Shell
bash
# 查看主站设备统计
ethercat master
# 查看从站扫描结果
ethercat slaves
# 检查过程数据交换
ethercat domains
# 如果设备未被识别,检查 MAC 配置
cat /etc/sysconfig/ethercat | grep DEVICE_MODULES
cat /etc/sysconfig/ethercat | grep MASTER0_DEVICE
4. 常见问题排查决策树
是
否
否
是
否
是
否
是
驱动加载后 ethercat master 无设备
dmesg 显示 ecdev_offer?
返回了 ec_device_t*?
检查: 驱动是否调用 ecdev_offer()
检查: 头文件 include 路径
MAC 地址不匹配
检查 /etc/sysconfig/ethercat
MASTER0_DEVICE 配置
ethercat slaves 有从站?
检查物理连接
检查网线/从站电源
设备识别成功
从站能进入 OP?
检查 ec_poll() 调用频率
检查 RX 路径 ecdev_receive
检查 DMA 缓冲区同步
适配完成
从 r8169 适配提取的通用 diff 模式
几乎所有适配都可以归结为以下几种 diff 模式:
| 模式 | 原始代码 | 修改后代码 | 应用场景 |
|---|---|---|---|
| Guard-Return | func() { do_A; do_B; } |
func() { if (ecdev) return; do_A; do_B; } |
中断使能、电源管理 |
| Guard-Skip | func() { do_A; do_B; do_C; } |
func() { if (!ecdev) do_A; if (!ecdev) do_B; do_C; } |
SKB 释放、NAPI 操作、队列管理 |
| Branch | func() { normal_path; } |
func() { if (ecdev) { ec_path; } else { normal_path; } } |
RX 处理(最关键) |
| Override | val = compute(); |
val = ecdev ? FORCE_VAL : compute(); |
TX door bell、queue stop |
Makefile 和 configure 集成
新驱动需要集成到 IgH 的构建系统中:
devices/Makefile.am 添加条目
make
## 新驱动
if ENABLE_NEW_DRIVER
obj-m += ec_new_driver.o
ec_new_driver-objs := new_driver/new_driver_main-$(KERNEL_VER)-ethercat.o
endif
configure.ac 添加选项
m4
AC_ARG_ENABLE([newdriver],
AS_HELP_STRING([--enable-newdriver],
[Enable NewDriver EtherCAT driver]),
[
case "${enableval}" in
yes) enable_newdriver=yes ;;
no) enable_newdriver=no ;;
*) AC_MSG_ERROR([Invalid value for --enable-newdriver]) ;;
esac
],
[enable_newdriver=no]
)
AM_CONDITIONAL(ENABLE_NEW_DRIVER, test "x$enable_newdriver" = "xyes")