如何以r8169为例开发IgH EtherCAT主站实时网卡驱动?
摘要:EtherCAT 主站需要精确控制网卡的数据收发时机。标准 Linux 网卡驱动使用中断驱动模型,其响应时间受内核调度器影响,无法满足 EtherCAT 周期性通信(通常 1ms 甚至更短)的确定性要求。
目录一、IgH 驱动适配框架概览为什么需要适配网卡驱动?两种驱动方案技术详情标准驱动 vs IgH 适配驱动对比IgH 设备抽象层 API适配方法论:5 步流程Generic 驱动方案ec_device 与 net_device 的关系设备 MAC 匹配机制深入源码ecdev_offer() 实现ec_device_attach() 实现ec_device_poll() 实现ecdev_receive() 实现Generic 驱动核心函数ec_gen_device_poll()二、r8169 驱动深入分析概览r8169 驱动与 EtherCAT修改点总览技术详情钩子位置总览图私有数据结构新增字段核心辅助函数 get_ecdev()中断/轮询到 Datagram 的完整调用链设备生命周期模式切换深入源码1. 私有数据结构扩展2. ec_poll() — 核心轮询函数3. ec_kick_watchdog() — PHY 链路变化处理4. rtl_rx() — RX 路径关键修改5. rtl8169_start_xmit() — TX 发送修改6. rtl_tx() — TX 完成处理修改7. rtl_irq_enable() — 中断禁用8. rtl_open() — 设备打开修改9. rtl_init_one() — 设备探测修改10. 电源管理阻断修改点完整清单三、添加新驱动指南概览适配新网卡驱动的核心思路技术详情适配 Checklist 流程图Step 0: 准备工作Step 1: 添加 EtherCAT 字段到私有数据结构Step 2: 实现 ec_poll() 函数Step 3: 修改设备初始化和清理Step 4: 修改中断控制Step 5: 修改 TX 路径Step 6: 修改 RX 路径(最关键)Step 7: 阻断电源管理Step 8: 编译和集成深入源码常见陷阱和注意事项调试方法1. 编译时调试2. 加载时调试3. 运行时调试4. 常见问题排查决策树从 r8169 适配提取的通用 diff 模式Makefile 和 configure 集成
一、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 步流程
flowchart TD
A["1. 选择基准内核驱动版本"] --> B["2. 添加 ecdev 提交逻辑"]
B --> C["3. 实现 ec_poll 轮询函数"]
C --> D["4. 修改中断/设备管理"]
D --> E["5. 修改 TX/RX 数据路径"]
A -.- A1["复制原始驱动源码\n重命名为 *-ethercat.c"]
B -.- B1["probe 时调用 ecdev_offer()\n若匹配则跳过 register_netdev()"]
C -.- C1["读取中断状态寄存器\n调用原始 TX/RX 处理\n处理链路变化事件"]
D -.- D1["禁用 request_irq/free_irq\n禁用 NAPI enable/disable\n阻断电源管理 suspend/resume"]
E -.- E1["RX: ecdev_receive 替代 napi_gro_receive\nTX: 跳过 SKB 生命周期管理\n跳过 netdev 队列操作"]
style A fill:#ebf8ff,stroke:#3182ce
style B fill:#ebf8ff,stroke:#3182ce
style C fill:#ebf8ff,stroke:#3182ce
style D fill:#ebf8ff,stroke:#3182ce
style E fill:#ebf8ff,stroke:#3182ce
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 的关系
flowchart LR
subgraph 网卡驱动
ND[net_device]
OPS[net_device_ops]
POLL[ec_poll 函数]
end
subgraph IgH 主站
ED[ec_device]
EM[ec_master]
end
ND --> OPS
ND --> POLL
POLL -->|注册到| ED
ED -->|poll 指针| POLL
ED -->|dev 指针| ND
ED -->|master 指针| EM
OPS -->|ndo_start_xmit| ED
ED -.->|ecdev_offer 时绑定| ND
设备 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
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
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
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
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
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 的完整调用链
flowchart TD
START["主站线程周期触发"] --> POLL["ec_device_poll()"]
POLL -->|"device->poll(dev)"| ECPOLL["ec_poll()"]
ECPOLL -->|"读取中断状态"| STATUS["rtl_get_events(tp)"]
STATUS -->|"有 TX 完成事件"| TX["rtl_tx(dev, tp, 100)"]
STATUS -->|"有 RX 到达事件"| RX["rtl_rx(dev, tp, 100)"]
STATUS -->|"LinkChg 事件"| LINK["irq_work_queue(ec_watchdog_kicker)"]
RX -->|"get_ecdev(tp) != NULL"| DIRECT["ecdev_receive(ecdev, rx_buf, pkt_size)"]
DIRECT -->|"跳过 ETH_HLEN"| RECV["ec_master_receive_datagrams()"]
RECV --> END1["Datagram 分发完成"]
TX -->|"get_ecdev(tp) != NULL"| TXDONE["跳过 napi_consume_skb\n跳过 queue 管理"]
TXDONE --> END2["TX 完成"]
LINK -->|irq_work 中断上下文| KICK["ec_kick_watchdog()"]
KICK -->|"phy_mac_interrupt()"| PHY["PHY 状态机处理"]
ECPOLL -->|"rtl_ack_events(tp, status)"| ACK["清除中断状态"]
style ECPOLL fill:#fefcbf,stroke:#ecc94b,stroke-width:2
style DIRECT fill:#ebf8ff,stroke:#3182ce,stroke-width:2
设备生命周期模式切换
flowchart TD
PROBE["rtl_init_one() — 设备探测"] --> OFFER["ecdev_offer(dev, ec_poll, THIS_MODULE)"]
OFFER -->|"返回非 NULL"| EC_MODE["EtherCAT 模式"]
OFFER -->|"返回 NULL"| NORMAL_MODE["普通网络模式"]
EC_MODE -->|"跳过"| NO_REG["跳过 register_netdev()"]
NO_REG --> EC_OPEN["ecdev_open(ecdev)"]
EC_OPEN -->|"成功"| EC_READY["设备就绪\nec_device → master"]
EC_OPEN -->|"失败"| EC_WITHDRAW["ecdev_withdraw(ecdev)"]
NORMAL_MODE --> REG["register_netdev(dev)"]
REG --> NORMAL_READY["普通网卡就绪"]
REMOVE["rtl_remove_one() — 设备移除"] --> CHECK{"get_ecdev(tp)?"}
CHECK -->|"非 NULL"| EC_CLEAN["ecdev_close(ecdev)\nirq_work_sync()\necdev_withdraw(ecdev)"]
CHECK -->|"NULL"| NORMAL_CLEAN["unregister_netdev(dev)"]
style EC_MODE fill:#ebf8ff,stroke:#3182ce,stroke-width:2
style NORMAL_MODE fill:#f0fff4,stroke:#68d391,stroke-width:2
深入源码
1. 私有数据结构扩展
位置: r8169_main-6.1-ethercat.c:632–644
r8169_main-6.1-ethercat.c : 632
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
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
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
/* === 原始代码 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
/* 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
/* 跳过 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
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
/* 跳过中断注册 */
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
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
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 流程图
flowchart TD
A["Step 0: 准备工作"] --> B["Step 1: 添加 EtherCAT 字段"]
B --> C["Step 2: 实现 ec_poll()"]
C --> D["Step 3: 修改设备初始化"]
D --> E["Step 4: 修改中断控制"]
E --> F["Step 5: 修改 TX 路径"]
F --> G["Step 6: 修改 RX 路径"]
G --> H["Step 7: 阻断电源管理"]
H --> I["Step 8: 编译测试"]
I --> J{通过?}
J -->|是| K["Step 9: 集成验证"]
J -->|否| L["调试修复"]
L --> I
K --> M{主站识别设备?}
M -->|是| N["Step 10: 性能验证"]
M -->|否| O["检查 MAC 配置\n/etc/sysconfig/ethercat"]
O --> K
N --> P["完成"]
style A fill:#ebf8ff,stroke:#3182ce,stroke-width:2
style C fill:#fefcbf,stroke:#ecc94b,stroke-width:2
style G fill:#fefcbf,stroke:#ecc94b,stroke-width:2
style P fill:#f0fff4,stroke:#68d391,stroke-width:2
Step 0: 准备工作
复制原始驱动文件:将内核源码中的驱动文件复制到 devices/<driver_name>/ 目录,重命名添加 -ethercat 后缀
添加 include:在头文件 include 区域添加 #include "../ecdev.h"
修改头文件引用:将本地头文件引用改为 -ethercat 版本
Step 1: 添加 EtherCAT 字段到私有数据结构
模板代码 — 私有数据结构扩展
/* 在驱动的私有数据结构末尾添加 */
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() 实现
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 路径核心修改
/* === 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: 阻断电源管理
模板代码 — 电源管理阻断
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
# 启用调试编译
./configure --enable-debug --enable-r8169
make
# 检查模块是否包含 ecdev 符号
nm -D ec_r8169.ko | grep ecdev
2. 加载时调试
Shell
# 先加载主站模块
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
# 查看主站设备统计
ethercat master
# 查看从站扫描结果
ethercat slaves
# 检查过程数据交换
ethercat domains
# 如果设备未被识别,检查 MAC 配置
cat /etc/sysconfig/ethercat | grep DEVICE_MODULES
cat /etc/sysconfig/ethercat | grep MASTER0_DEVICE
4. 常见问题排查决策树
flowchart TD
START["驱动加载后 ethercat master 无设备"] --> A{"dmesg 显示 ecdev_offer?"}
A -->|"是"| B{"返回了 ec_device_t*?"}
A -->|"否"| C["检查: 驱动是否调用 ecdev_offer()\n检查: 头文件 include 路径"]
B -->|"否"| D["MAC 地址不匹配\n检查 /etc/sysconfig/ethercat\nMASTER0_DEVICE 配置"]
B -->|"是"| E{"ethercat slaves 有从站?"}
E -->|"否"| F["检查物理连接\n检查网线/从站电源"]
E -->|"是"| G["设备识别成功"]
G --> H{"从站能进入 OP?"}
H -->|"否"| I["检查 ec_poll() 调用频率\n检查 RX 路径 ecdev_receive\n检查 DMA 缓冲区同步"]
H -->|"是"| J["适配完成"]
style C fill:#fed7d7,stroke:#fc8181
style D fill:#fed7d7,stroke:#fc8181
style I fill:#fefcbf,stroke:#ecc94b
style J fill:#f0fff4,stroke:#68d391
从 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 添加条目
## 新驱动
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 添加选项
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")
