IgH EtherCAT主站详解(十六)--Igh主站实时网卡驱动开发指南(以r8169为例)

文章目录

  • [一、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 驱动深入分析)
    • 概览
    • 技术详情
    • 深入源码
      • [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: 编译和集成)
    • 深入源码
      • 常见陷阱和注意事项
      • 调试方法
        • [1. 编译时调试](#1. 编译时调试)
        • [2. 加载时调试](#2. 加载时调试)
        • [3. 运行时调试](#3. 运行时调试)
        • [4. 常见问题排查决策树](#4. 常见问题排查决策树)
      • [从 r8169 适配提取的通用 diff 模式](#从 r8169 适配提取的通用 diff 模式)
      • [Makefile 和 configure 集成](#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 步流程

  1. 选择基准内核驱动版本
  2. 添加 ecdev 提交逻辑
  3. 实现 ec_poll 轮询函数
  4. 修改中断/设备管理
  5. 修改 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 或模块参数)。匹配成功后:

  1. 调用 ec_device_attach() 绑定设备和 poll 函数
  2. 将网卡名称改为 eXaM 格式(如 e0a0
  3. 返回非 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_devicepoll 函数指针保存到 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: 准备工作

  1. 复制原始驱动文件 :将内核源码中的驱动文件复制到 devices/<driver_name>/ 目录,重命名添加 -ethercat 后缀
  2. 添加 include :在头文件 include 区域添加 #include "../ecdev.h"
  3. 修改头文件引用 :将本地头文件引用改为 -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: 编译和集成

  1. devices/Makefile.am 中添加新驱动目录
  2. configure.ac 中添加新驱动的编译选项
  3. 编译:./configure --enable-<driver> && make
  4. 配置 MAC 地址:/etc/sysconfig/ethercat 或模块参数 master0_main_mac=xx:xx:xx:xx:xx:xx
  5. 加载: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")
相关推荐
织_网1 天前
SDD规范驱动开发全解析:核心理念、工作流、落地层级+多AI协同实战
人工智能·驱动开发
发发就是发1 天前
触摸屏驱动调试手记:从I2C鬼点到坐标漂移的实战录
linux·服务器·驱动开发·单片机·嵌入式硬件
发发就是发1 天前
I2C适配器与算法:从一次诡异的时序问题说起
服务器·驱动开发·单片机·嵌入式硬件·算法·fpga开发
不怕犯错,就怕不做1 天前
(Hisilicon)笔试题:嵌入式Linux C语言GPIO中断与按键消抖(转载)
linux·驱动开发·嵌入式硬件
GS8FG2 天前
Busybox生成根文件系统,并移植e2fsprogs:RK3568
linux·驱动开发
嵌入式×边缘AI:打怪升级日志2 天前
Linux GPIO子系统与中断驱动开发:从入门到实战(完整版)
linux·运维·驱动开发
小明的IT世界2 天前
企业内部落地AI编程实践分析
驱动开发·ai编程
somi72 天前
ARM-驱动-09-LCD FrameBuffer
arm开发·驱动开发·算法·自用
SuperEugene2 天前
Vue3 配置文件管理:按模块拆分配置,提升配置可维护性|配置驱动开发实战篇
前端·javascript·vue.js·驱动开发