如何以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")