Linux驱动之网卡驱动剖析

2022-11-15 21:39:30 浏览数 (1)

网络设备不同于字符设备和块设备,并不对应于/dev目录下的文件,应用程序通过 socket 完成与网络设备的交互,在网络设备上并不体现”一切皆文件”的设计思想。

Linux 网络设备驱动架构

驱动架构自上而下分为4层:

  • 协议接口层
  • 设备接口层
  • 设备驱动功能层
  • 网络设备与媒介层

协议接口层

协议接口层主要功能是给上层协议提供接收和发送的接口。当内核协议栈需要发送数据时,会通过调用 dev_queue_xmit 函数来发送数据。同样内核协议栈接收数据也是通过协议接口层的 netif_rx 函数来进行的。传递的数据被描述为套接字缓冲区,用struct sk_buff结构描述,该结构体定义位于include/linux/skbuff.h中,用于在Linux网络子系统中的各层之间传输数据,该结构在整个网络收发过程中贯穿始终。

sk buffer 结构可以分为两部分,一部分是存储真正的数据包,在图中为 Packetdata,另一部分是一组指针组成。

  • head 指向内核缓冲区(Packetdata)的头部(headroom)
  • data 指向的是实际数据包的头部
  • tail 指向的是实际数据包的尾部
  • end 指向内核缓冲区的尾部

设备接口层

网络设备接口层用于抽象各种不同的网络设备,用 struct net_device来表示网络设备,该结构地位等同于字符设备的抽象描述struct cdev

设备驱动功能层

类似于字符设备,struct net_device结构体也提供了一个操作函数集struct net_device_ops来描述对网卡的各种操作。

源码分析

笔者基于的是 S5PV210 的 DM9000 驱动,会大体上对 DM9000 的驱动源码进行分析, 分析源码位于DM9000 源码

platform 框架分析

DM9000 的驱动是基于 platform 架构实现,首先从 platform 框架入手。

代码语言:javascript复制
static struct platform_driver dm9000_driver = {
    .driver    = {
        .name    = "dm9000",
        .owner     = THIS_MODULE,
        .pm     = &dm9000_drv_pm_ops,
    },
    .probe   = dm9000_probe,
    .remove  = __devexit_p(dm9000_drv_remove),
};

static int __init dm9000_init(void)
{
    /* disable buzzer */
    s3c_gpio_setpull(S5PV210_GPD0(2), S3C_GPIO_PULL_UP);
    s3c_gpio_cfgpin(S5PV210_GPD0(2), S3C_GPIO_SFN(1));
    gpio_set_value(S5PV210_GPD0(2), 0);

    dm9000_power_int();
    printk(KERN_INFO "%s Ethernet Driver, V%sn", CARDNAME, DRV_VERSION);

    return platform_driver_register(&dm9000_driver);
}

该函数调用了 platform_driver_register 函数注册了一个平台总线驱动,对应的平台设备的注册定义位于 xxx_machine_init中,在笔者基于的s5pv210 kernel 上位于arch/arm/mach-s5pv210/mach-x210.c中的smdkc110_machine_init中,具体的分析过程省略,笔者直接列出对应的平台总线设备。

代码语言:javascript复制
/* DM9000 registrations */
#ifdef CONFIG_DM9000
static struct resource s5p_dm9000_resources[] = {
    [0] = {
        .start = S5P_PA_DM9000,
        .end   = S5P_PA_DM9000   3,
        .flags = IORESOURCE_MEM,    // 内存资源 (DM900 地址端口)
    },
    [1] = {
        .start = S5P_PA_DM9000   4,
        .end   = S5P_PA_DM9000   7,
        .flags = IORESOURCE_MEM,      // 内存资源  (DM900 数据端口)
    },
    [2] = {
        .start = IRQ_EINT10,
        .end   = IRQ_EINT10,
        .flags = IORESOURCE_IRQ | IORESOURCE_IRQ_HIGHLEVEL, // 中断资源 (高电平触发)
    }
};

static struct dm9000_plat_data s5p_dm9000_platdata = {
    .flags = DM9000_PLATF_16BITONLY | DM9000_PLATF_NO_EEPROM,
    .dev_addr = {0x00,0x09,0xc0,0xff,0xec,0x48},
};

struct platform_device s5p_device_dm9000 = {
    .name      = "dm9000",
    .id        =  0,
    .num_resources    = ARRAY_SIZE(s5p_dm9000_resources),
    .resource   = s5p_dm9000_resources,
    .dev        = {
        .platform_data = &s5p_dm9000_platdata,
    }
};

根据平台总线的原理,驱动和设备匹配上后,会调用驱动的 probe 函数 dm9000_probe,分段进行分析

代码语言:javascript复制
struct dm9000_plat_data *pdata = pdev->dev.platform_data;
struct board_info *db;    /* Point a board information structure */
struct net_device *ndev;   /* struct net_device 为网络设备的抽象 */
const unsigned char *mac_src;
int ret = 0;
int iosize;
int i;
u32 id_val;

/* Init network device */
ndev = alloc_etherdev(sizeof(struct board_info)); /* 同时为 ndev 和 db 申请内存, db 内存位于 ndev 后面 */
if (!ndev) {
    dev_err(&pdev->dev, "could not allocate device.n");
    return -ENOMEM;
}

SET_NETDEV_DEV(ndev, &pdev->dev);

dev_dbg(&pdev->dev, "dm9000_probe()n");dm9000_opendm9000_open

/* setup board info structure */
db = netdev_priv(ndev);

db->dev = &pdev->dev;
db->ndev = ndev;

spin_lock_init(&db->lock);
mutex_init(&db->addr_lock);

INIT_DELAYED_WORK(&db->phy_poll, dm9000_poll_work);

该部分为 struct net_devicestruct board_info 结构体申请内存,struct board_info定义在 DM9000 的驱动文件中,表示设备的私有数据,随后对各个指针做了挂接,并初始化了一部分 struct board_info 中的成员。

代码语言:javascript复制
   db->addr_res = platform_get_resource(pdev, IORESOURCE_MEM, 0); /* dm9000 地址端口 */
   db->data_res = platform_get_resource(pdev, IORESOURCE_MEM, 1); /* dm9000 数据端口 */
   db->irq_res  = platform_get_resource(pdev, IORESOURCE_IRQ, 0); /* dm9000 irq 号 */

   if (db->addr_res == NULL || db->data_res == NULL ||
       db->irq_res == NULL) {
       dev_err(db->dev, "insufficient resourcesn");
       ret = -ENOENT;
       goto out;
   }

/*
 * 第二个参数为 1 表示获取的是第二个中断资源。
 * 由于只定义了一个中断, 所以返回 -ENXIO
 */
   db->irq_wake = platform_get_irq(pdev, 1);
   if (db->irq_wake >= 0) {
   	/* 这一段代码并不会执行, 省略 */
       // ...
   }

   iosize = resource_size(db->addr_res); // res->end - res->start   1 = 4
   /* 申请地址端口内存 */
   db->addr_req = request_mem_region(db->addr_res->start, iosize,
                     pdev->name);

   if (db->addr_req == NULL) {
       dev_err(db->dev, "cannot claim address reg arean");
       ret = -EIO;
       goto out;
   }

/* 映射地址端口虚拟地址 */
   db->io_addr = ioremap(db->addr_res->start, iosize);

   if (db->io_addr == NULL) {
       dev_err(db->dev, "failed to ioremap address regn");
       ret = -EINVAL;
       goto out;
   }


   iosize = resource_size(db->data_res);
   /* 申请数据端口内存 */
   db->data_req = request_mem_region(db->data_res->start, iosize,
                     pdev->name);

   if (db->data_req == NULL) {
       dev_err(db->dev, "cannot claim data reg arean");
       ret = -EIO;
       goto out;
   }

  /* 映射数据端口虚拟地址 */
   db->io_data = ioremap(db->data_res->start, iosize);

   if (db->io_data == NULL) {
       dev_err(db->dev, "failed to ioremap data regn");
       ret = -EINVAL;
       goto out;
   }

   /* fill in parameters for net-dev structure */
   ndev->base_addr = (unsigned long)db->io_addr;
   ndev->irq       = db->irq_res->start;

以上代码从platform_device中获取 DM9000 资源: 地址端口、数据端口地址和中断号, 并为端口地址 ioremap

代码语言:javascript复制
    /* ensure at least we have a default set of IO routines */
    dm9000_set_io(db, iosize); /* 在下面 if 判断中还会设置一次, 所以这里设置无效 */

    /* check to see if anything is being over-ridden */
    if (pdata != NULL) {
        /* check to see if the driver wants to over-ride the
         * default IO width */

        if (pdata->flags & DM9000_PLATF_8BITONLY)
            dm9000_set_io(db, 1);

        if (pdata->flags & DM9000_PLATF_16BITONLY)  /* 只有这个 if 成立 */
            dm9000_set_io(db, 2);  /* 设置 board_info 的读写函数 */

        if (pdata->flags & DM9000_PLATF_32BITONLY)
            dm9000_set_io(db, 4);

        /* check to see if there are any IO routine
         * over-rides */

        if (pdata->inblk != NULL)
            db->inblk = pdata->inblk;

        if (pdata->outblk != NULL)
            db->outblk = pdata->outblk;

        if (pdata->dumpblk != NULL)
            db->dumpblk = pdata->dumpblk;

        db->flags = pdata->flags;
    }

#ifdef CONFIG_DM9000_FORCE_SIMPLE_PHY_POLL
    db->flags |= DM9000_PLATF_SIMPLE_PHY;
#endif

    dm9000_reset(db);    /* 重启 dm9000 */

根据平台设备的平台数据,DM9000 配置在了 16bit 的模式下,所以这一部分设置只有dm9000_set_io(db, 2);是成功的。 dm9000_set_io 函数用于设置 DM9000 的读写函数。

代码语言:javascript复制
static void dm9000_set_io(struct board_info *db, int byte_width)
{
	/* use the size of the data resource to work out what IO
	 * routines we want to use
	 */

	switch (byte_width) {
	case 1:
		db->dumpblk = dm9000_dumpblk_8bit;
		db->outblk  = dm9000_outblk_8bit;
		db->inblk   = dm9000_inblk_8bit;
		break;


	case 3:
		dev_dbg(db->dev, ": 3 byte IO, falling back to 16bitn");
	case 2:
		db->dumpblk = dm9000_dumpblk_16bit;
		db->outblk  = dm9000_outblk_16bit;
		db->inblk   = dm9000_inblk_16bit;
		break;

	case 4:
	default:
		db->dumpblk = dm9000_dumpblk_32bit;
		db->outblk  = dm9000_outblk_32bit;
		db->inblk   = dm9000_inblk_32bit;
		break;
	}
}

设置完读写函数后,软件重启 DM9000。

代码语言:javascript复制
static void dm9000_reset(board_info_t * db)
{
	dev_dbg(db->dev, "resetting devicen");

	/* RESET device */
	writeb(DM9000_NCR, db->io_addr); //  DM9000_NCR: 0x00
	udelay(200);
	writeb(NCR_RST, db->io_data);    // NCR_RST: 1 << 0
	udelay(200);
}

DM9000 通过端口来操作寄存器, 先将寄存器的偏移值或命令码写入地址端口, 再将值写入数据端口。重启 DM900 只需往地址为 0 的端口写入 1。

重启完 DM9000 后,开始读取 DM9000 的寄存器

代码语言:javascript复制
/* try multiple times, DM9000 sometimes gets the read wrong */
for (i = 0; i < 8; i  ) {
    id_val  = ior(db, DM9000_VIDL);             /* DM9000_VIDL:0x28, 读取 vendor id */
    id_val |= (u32)ior(db, DM9000_VIDH) << 8;   /* DM9000_VIDH: 0x29 */
    id_val |= (u32)ior(db, DM9000_PIDL) << 16;  /* DM9000_PIDL: 0x2A, 读取 product id */
    id_val |= (u32)ior(db, DM9000_PIDH) << 24;  /* DM9000_PIDH: 0x2B */

    if (id_val == DM9000_ID)   /* 验证是否是 DM900 */
        break;
    dev_err(db->dev, "read wrong id 0xxn", id_val);
}

if (id_val != DM9000_ID) {
    dev_err(db->dev, "wrong id: 0xxn", id_val);
    ret = -ENODEV;
    goto out;
}

/* Identify what type of DM9000 we are working on */

/* I/O mode */
db->io_mode = ior(db, DM9000_ISR) >> 6;    /* ISR bit7:6 keeps I/O mode */ // 读取 I/O mode
id_val = ior(db, DM9000_CHIPR);  /* DM9000_CHIPR: 0x2C, 读取 chip revision */
dev_dbg(db->dev, "dm9000 revision 0xx  , io_mode x n", id_val, db->io_mode);

switch (id_val) {
case CHIPR_DM9000A:
    db->type = TYPE_DM9000A;
    break;
case 0x1a:
    db->type = TYPE_DM9000C;
    break;
default:
    dev_dbg(db->dev, "ID x => defaulting to DM9000En", id_val);
    db->type = TYPE_DM9000E;
}

读取 vendor id 和 product id 验证是否是 DM9000。再读取 I/O mode 和 chip revision, 并根据不同 revision 对db->type进行赋值。

代码语言:javascript复制
/* driver system function */
ether_setup(ndev);

ndev->netdev_ops    = &dm9000_netdev_ops;     // net device 的 ops
ndev->watchdog_timeo    = msecs_to_jiffies(watchdog);
ndev->ethtool_ops    = &dm9000_ethtool_ops;   // ethtool 的 ops, 用于支持应用层的 ethtool 命令

db->msg_enable       = NETIF_MSG_LINK;
db->mii.phy_id_mask  = 0x1f;
db->mii.reg_num_mask = 0x1f;
db->mii.force_media  = 0;
db->mii.full_duplex  = 0;
db->mii.dev         = ndev;
db->mii.mdio_read    = dm9000_phy_read;
db->mii.mdio_write   = dm9000_phy_write;

mac_src = "eeprom";

/* try reading the node address from the attached EEPROM */
/* platdata 设置了 DM9000_PLATF_NO_EEPROM flag, 所以这个读取无效 */
for (i = 0; i < 6; i  = 2)
    dm9000_read_eeprom(db, i / 2, ndev->dev_addr i);

if (!is_valid_ether_addr(ndev->dev_addr) && pdata != NULL) {
    mac_src = "platform data";
    //memcpy(ndev->dev_addr, pdata->dev_addr, 6);
    /* mac from bootloader */
    memcpy(ndev->dev_addr, mac, 6);  /* 这是真正的设置 mac 地址, 其他设置均无效 */
}

if (!is_valid_ether_addr(ndev->dev_addr)) {
    /* try reading from mac */

    mac_src = "chip";
    for (i = 0; i < 6; i  )
        ndev->dev_addr[i] = ior(db, i DM9000_PAR);
}

if (!is_valid_ether_addr(ndev->dev_addr))
    dev_warn(db->dev, "%s: Invalid ethernet MAC address. Please "
         "set using ifconfign", ndev->name);

platform_set_drvdata(pdev, ndev);
ret = register_netdev(ndev);   // 注册网络设备

if (ret == 0)
    printk(KERN_INFO "%s: dm9000%c at %p,%p IRQ %d MAC: %pM (%s)n",
           ndev->name, dm9000_type_to_char(db->type),
           db->io_addr, db->io_data, ndev->irq,
           ndev->dev_addr, mac_src);
return 0;

调用ether_setup函数对ndev成员进行初始化。

代码语言:javascript复制
void ether_setup(struct net_device *dev)
{
    dev->header_ops   = &eth_header_ops; /* 硬件头部操作函数集,主要完成创建硬件头和从 sk_buf 分析硬件头等操作 */
    dev->type         = ARPHRD_ETHER;    // 设置以太网协议
    dev->hard_header_len     = ETH_HLEN; // 以太网头部大小   14B
    dev->mtu          = ETH_DATA_LEN;    // 设置以太网 MTU  1500B
    dev->addr_len     = ETH_ALEN;        // mac 地址长度    6B
    dev->tx_queue_len = 1000;    /* Ethernet wants good queues */
    dev->flags        = IFF_BROADCAST|IFF_MULTICAST;

    memset(dev->broadcast, 0xFF, ETH_ALEN);
}

初始化完ndev后,设置了netdev_ops 和 mac 地址,最后调用register_netdev函数注册了网络设备。至此,probe 函数分析完毕,紧接着把关注点放在netdev_ops上。

代码语言:javascript复制
static const struct net_device_ops dm9000_netdev_ops = {
	.ndo_open		= dm9000_open,              /* ifconfig eth0 up */
	.ndo_stop		= dm9000_stop,              /* ifconfig eth0 down */
	.ndo_start_xmit		= dm9000_start_xmit,    /* 数据包发送时由网络协议栈调用 */
	.ndo_tx_timeout		= dm9000_timeout,       /* 数据包发送超时后会被调用 */
	.ndo_set_multicast_list	= dm9000_hash_table,
	.ndo_do_ioctl		= dm9000_ioctl,
	.ndo_change_mtu		= eth_change_mtu,
	.ndo_validate_addr	= eth_validate_addr,
	.ndo_set_mac_address	= eth_mac_addr,
#ifdef CONFIG_NET_POLL_CONTROLLER
	.ndo_poll_controller	= dm9000_poll_controller,
#endif
};

dm9000 open 过程分析

当用户执行命令ifconfig eth0 up后会调用网卡驱动的 open 函数

代码语言:javascript复制
/*
 *  Open the interface.
 *  The interface is opened whenever "ifconfig" actives it.
 */
static int dm9000_open(struct net_device *dev)
{
	board_info_t *db = netdev_priv(dev);
	unsigned long irqflags = db->irq_res->flags & IRQF_TRIGGER_MASK;

	if (netif_msg_ifup(db))
		dev_dbg(db->dev, "enabling %sn", dev->name);

	/* If there is no IRQ type specified, default to something that
	 * may work, and tell the user that this is a problem */

	if (irqflags == IRQF_TRIGGER_NONE)
		dev_warn(db->dev, "WARNING: no IRQ resource flags set.n");

	irqflags |= IRQF_SHARED;

	/* 申请收发中断 */
	if (request_irq(dev->irq, dm9000_interrupt, irqflags, dev->name, dev))
		return -EAGAIN;

	/* Initialize DM9000 board */
//	dm9000_reset(db);
	dm9000_init_dm9000(dev);   /* 初始化 DM9000 */

	/* Init driver variable */
	db->dbug_cnt = 0;

	mii_check_media(&db->mii, netif_msg_link(db), 1);
	netif_start_queue(dev);   /* 激活设备发送队列,允许上层调用 xxx_xmit 函数 */

	dm9000_schedule_poll(db);

	return 0;
}

open 函数主要做了申请收发中断、初始化 DM9000、激活设备发送队列。其中 DM900 的初始化全是对硬件寄存器的操作,在此省略。

DM9000 发送过程分析

应用程序调用send函数去发送数据,内核协议栈会将数据构造成struct sk_buff后放入等待队列,调用start_xmit通知网卡发送数据。

代码语言:javascript复制
static int dm9000_start_xmit(struct sk_buff *skb, struct net_device *dev)
{
	unsigned long flags;
	board_info_t *db = netdev_priv(dev);

	dm9000_dbg(db, 3, "%s:n", __func__);

	if ((db->tx_pkt_cnt > 0) && !netif_carrier_ok(dev))
		return NETDEV_TX_BUSY;

	spin_lock_irqsave(&db->lock, flags);

	netif_stop_queue(dev);  /* 关闭发送队列,通知协议接口层停止向下递交数据包 */

	db->tx_pkt_cnt  ;
	dev->stats.tx_packets  ;
	dev->stats.tx_bytes  = skb->len;

	/* Set TX length to DM9000 */       // 设置数据包总长度
	iow(db, DM9000_TXPLL, skb->len);    // DM9000_TXPLL: 0xFC
	iow(db, DM9000_TXPLH, skb->len >> 8); // DM9000_TXPLH: 0xFD

	/* Move data to DM9000 TX RAM */   /* 将数据包放入 TX SRAM 中 */
	writeb(DM9000_MWCMD, db->io_addr);   // DM9000_MWCMD: 0xF8
	(db->outblk)(db->io_data, skb->data, skb->len);

	/* Issue TX polling command */  /* 开始将 TX SRAM 中的数据发送出去, 发送完毕会通过中断告知 */
	iow(db, DM9000_TCR, TCR_TXREQ);	/* Cleared after TX complete */ // DM9000_TCR: 0x02, TCR_TXREQ: 1 << 0
	dev->trans_start = jiffies;

	spin_unlock_irqrestore(&db->lock, flags);

	/* free this SKB */
	dev_kfree_skb(skb);

	return NETDEV_TX_OK;
}

由以上代码可知,先关闭发送队列,通知协议接口层停止向下递交数据包, 然后设置数据包的总长度后将数据包拷贝进 DM9000 的 TX SRAM 中,再然后置位 TCR 寄存器后网卡开始发送数据,该标志位会在发送完毕后硬件自动清 0, 最后由中断通知 CPU 数据发送完毕

在 open 函数中申请过 DM9000 的硬件中断,该中断在发送和接收完毕都会触发,在这先只关注中断处理函数的发送完毕过程

代码语言:javascript复制
static irqreturn_t dm9000_interrupt(int irq, void *dev_id)
{
	struct net_device *dev = dev_id;
	board_info_t *db = netdev_priv(dev);
	int int_status;
	unsigned long flags;
	u8 reg_save;

	dm9000_dbg(db, 3, "entering %sn", __func__);

	/* A real interrupt coming */

	/* holders of db->lock must always block IRQs */
	spin_lock_irqsave(&db->lock, flags);

	/* Save previous register address */
	reg_save = readb(db->io_addr);

	/* Disable all interrupts */
	iow(db, DM9000_IMR, IMR_PAR);   // 先 disable 掉所有中断

	/* Got DM9000 interrupt status */
	int_status = ior(db, DM9000_ISR);	/* Got ISR */  /* 获取中断状态, 是接收中断还是发送中断 */
	iow(db, DM9000_ISR, int_status);	/* Clear ISR status */  /* 清中断 */

	if (netif_msg_intr(db))
		dev_dbg(db->dev, "interrupt status xn", int_status);

	/* Received the coming packet */
	if (int_status & ISR_PRS)   /* ISR_PRS: 1 << 0, 接收中断 */
		dm9000_rx(dev);

	/* Got DM9000 interrupt status */
	int_status |= ior(db, DM9000_ISR);	/* Got ISR */

	/* Trnasmit Interrupt check */
	if (int_status & ISR_PTS)   /* ISR_PTS: 1 << 1, 发送中断 */
	{
		iow(db, DM9000_ISR, ISR_PTS);	/* Clear ISR status */
		dm9000_tx_done(dev, db);
	}

	if (db->type != TYPE_DM9000E) {
		if (int_status & ISR_LNKCHNG) {
			/* fire a link-change request */
			schedule_delayed_work(&db->phy_poll, 1);
		}
	}

	/* Re-enable interrupt mask */
	iow(db, DM9000_IMR, db->imr_all);

	/* Restore previous register address */
	writeb(reg_save, db->io_addr);

	spin_unlock_irqrestore(&db->lock, flags);

	return IRQ_HANDLED;
}

先禁用所有中断,然后通过读取 ISR 寄存器获取中断状态

由 bit 0 和 1 可判断是接收中断还是发送中断, 如果是发送中断,则清中断后调用dm9000_tx_done函数

代码语言:javascript复制
static void dm9000_tx_done(struct net_device *dev, board_info_t *db)
{
	int tx_status = ior(db, DM9000_TCR);	/* Got TX status */

	if (tx_status & TCR_TXREQ) {
		dev->stats.tx_fifo_errors  ;
	} else {
		if (db->tx_pkt_cnt && !db->wait_reset) {
			/* One packet sent complete */
			db->tx_pkt_cnt = 0;
			dev->trans_start = 0;
			netif_wake_queue(dev);  /* 唤醒发送队列,协议接口层可以继续向下递交数据了 */
		}
	}
}

再次读取寄存器状态,如果发送中断未置位,则唤醒发送队列,表示协议接口层可以继续向下递交数据了。由于在dm9000_start_xmit函数中将发送队列关闭了并且调用dm9000_tx_done前清了中断,此时如果中断仍置位,表示出错了,所以dev->stats.tx_fifo_errors ;

以 UDP 为例,下图说明 DM9000 发送数据包的流程

DM9000 接收过程分析

由发送过程分析可知,接收也是由中断通知的。而且与发送过程共用同一个中断处理函数,当中断是接收中断时会调用dm9000_rx函数来处理接收过程。

RX SRAM 中一个完整数据包包含 4 字节的头部,其中第一个字节固定为 0x01, 第二个字节为数据包状态,最后两个字节表示有效数据的长度。驱动代码中用这样一个结构体来表示头部,头部之后的数据才为真正有效数据

代码语言:javascript复制
struct dm9000_rxhdr {
	u8	RxPktReady;    // 固定为 0x01
	u8	RxStatus;
	__le16	RxLen;
} __attribute__((__packed__));

dm9000_rx函数比较长,关键部分都在代码中注释说明

代码语言:javascript复制
static void dm9000_rx(struct net_device *dev)
{
	board_info_t *db = netdev_priv(dev);
	struct dm9000_rxhdr rxhdr;   /* RX SRAM 存储的数据的四字节头部, 去除头部后才是数据包 */
	struct sk_buff *skb;
	u8 rxbyte, *rdptr;
	bool GoodPacket;
	int RxLen;
	int save_mrr, calc_mrr, check_mrr;

	/* Check packet ready or not */
	do {
		ior(db, DM9000_MRCMDX);	/* Dummy read */
		save_mrr = (ior(db, 0xf5) << 8) | ior(db, 0xf4);
		/* Get most updated data */
		rxbyte = ior(db, DM9000_MRCMDX); /* 读取 RX SRAM 的数据, 地址不会自增 */

		if(rxbyte != DM9000_PKT_RDY)  /* DM9000_PKT_RDY: 0x01, RX sram存储的数据的四字节头部第一字节固定为 0x01 */
		{
			/* Status check: this byte must be 0 or 1 */
			if (rxbyte > DM9000_PKT_RDY) {
				dev_warn(db->dev, "status check fail: %dn", rxbyte);
				iow(db, DM9000_RCR, 0x00);	/* Stop Device */
				iow(db, DM9000_IMR, IMR_PAR);	/* Stop INT request */

				db->wait_reset = 1;
				dev->trans_start = 1;
			}

			return;
		}

		/* A packet ready now  & Get status/length */
		GoodPacket = true;
		writeb(DM9000_MRCMD, db->io_addr);  /* 读取 RX SRAM 的数据, 并且地址自增 */
		(db->inblk)(db->io_data, &rxhdr, sizeof(rxhdr));

		RxLen = le16_to_cpu(rxhdr.RxLen);  // 数据包的总长度

		calc_mrr = save_mrr   4   RxLen;
		if(0x00 == db->io_mode)  //16 bit only
		{
			if(RxLen & 0x01) calc_mrr  ;
		}
		if(calc_mrr > 0x3fff) calc_mrr -= 0x3400;

		if (netif_msg_rx_status(db))
			dev_dbg(db->dev, "RX: status x, length xn",
				rxhdr.RxStatus, RxLen);

		/* Packet Status check */
		/* 64 < 以太网帧长度 <= 1536 */
		if (RxLen < 0x40) {
			GoodPacket = false;
			if (netif_msg_rx_err(db))
				dev_dbg(db->dev, "RX: Bad Packet (runt)n");
		}

		if (RxLen > DM9000_PKT_MAX) {
			dev_dbg(db->dev, "RST: RX Len:%xn", RxLen);
		}

        // 校验头部的状态值,判断是否是一个正常的数据包
		/* rxhdr.RxStatus is identical to RSR register. */
		if (rxhdr.RxStatus & (RSR_FOE | RSR_CE | RSR_AE |
				      RSR_PLE | RSR_RWTO |
				      RSR_LCS | RSR_RF)) {
			if (rxhdr.RxStatus & RSR_FOE) {
				if (netif_msg_rx_err(db))
					dev_dbg(db->dev, "fifo errorn");
				dev->stats.rx_fifo_errors  ;
			}
			if (rxhdr.RxStatus & RSR_CE) {
				if (netif_msg_rx_err(db))
					dev_dbg(db->dev, "crc errorn");
				dev->stats.rx_crc_errors  ;
				GoodPacket = false;
			}
			if (rxhdr.RxStatus & RSR_RF) {
				if (netif_msg_rx_err(db))
					dev_dbg(db->dev, "length errorn");
				dev->stats.rx_length_errors  ;
				GoodPacket = false;
			}
		}

		/* Move data from DM9000 */
		if (GoodPacket &&
		    ((skb = dev_alloc_skb(RxLen   4)) != NULL)) {   // 如果是正常数据包,就申请 sk buffer
			skb_reserve(skb, 2);
			rdptr = (u8 *) skb_put(skb, RxLen - 4);

			/* Read received packet from RX SRAM */

			(db->inblk)(db->io_data, rdptr, RxLen);  // 将 RX SRAM 中的有效数据拷贝到 sk buffer 中
			dev->stats.rx_bytes  = RxLen;

			/* Pass to upper layer */
			skb->protocol = eth_type_trans(skb, dev);

			netif_rx(skb);           /* 将 skb uffer 向上递交给协议接口层 */
			dev->stats.rx_packets  ;

			check_mrr = (ior(db, 0xf5) << 8) | ior(db, 0xf4);
			if(calc_mrr != check_mrr)
			{

				if (netif_msg_rx_err(db))
					dev_dbg(db->dev, "rx point error x x x xn",
						save_mrr, RxLen, calc_mrr, check_mrr);

				iow(db, 0xf5, (calc_mrr >> 8) & 0xff);
				iow(db, 0xf4, calc_mrr & 0xff);
			}

		} else {
			/* need to dump the packet's data */
			iow(db, 0xf5, (calc_mrr >> 8) & 0xff);
			iow(db, 0xf4, calc_mrr & 0xff);
		}

	} while (rxbyte & DM9000_PKT_RDY);
}

大体逻辑可以归为以下流程:

1.先读取 RX SRAM 中 4 字节头部到struct dm9000_rxhdr rxhdr

2.判断第一字节是否为 0x01, 判断数据包总长度是否符合以太网规范,最后根据头部中的状态值是否是一个正常的封包

3.经过 2 判断是正常封包后,读取有效数据

4.创建分配 sk buffer,并将有效数据拷贝到 sk buffer 中

5.调用netif_rx, 将 sk buffer 向上递交给协议接口层

以 UDP 为例,下图说明 DM9000 接收数据包的流程

NAPI 方式接收介绍

通常情况下,网络驱动以中断方式接收数据,但是当数据量大的时候会频繁产生中断,CPU 要频繁去处理中断导致效率低下而不如纯轮询模式。在 kernel 2.5 之后引入了新的处理方式,叫 NAPI,综合了中断方式和轮询方式。NAPI 这个名字取得不知所云,据说由于当时未找到合适的名字,就叫 NAPI (New API),目前已经公认为专有名词了。

NAPI 接收数据的流程:接收中断来临 -> 关闭接收中断 -> 轮询方式接收所有数据包直到为空 -> 开启接收中断 -> 接收中断来临 -> …

笔者在 DM9000 中加入了 NAPI 的支持 git commit。

主要修改如下:

1.在driver/net/Kconfig中加入配置

代码语言:javascript复制
config DM9000_NAPI
    bool "DM9000 NAPI"
    depends on DM9000
    default n
    help
        Support DM9000 driver run NAPI mode

2.在struct board_info添加成员

代码语言:javascript复制
#ifdef CONFIG_DM9000_NAPI
    struct napi_struct napi;
#endif

3.在 probe 函数中调用netif_napi_add注册 NAPI 要调度执行的轮询函数

代码语言:javascript复制
#define DM9000_NAPI_WEIGHT 64

#ifdef CONFIG_DM9000_NAPI
    netif_napi_add(ndev, &db->napi, dm9000_napi_poll, DM9000_NAPI_WEIGHT);
#endif

dm9000_napi_poll函数如下

代码语言:javascript复制
#ifdef CONFIG_DM9000_NAPI
static int dm9000_napi_poll(struct napi_struct *napi, int budget)
{
    board_info_t *db = container_of(napi, board_info_t, napi);
    unsigned long flags;
    u8 reg_save;

    spin_lock_irqsave(&db->lock, flags);

    reg_save = readb(db->io_addr);

    dm9000_rx(db->ndev, budget);    // 轮询处理收包

    napi_complete(napi);

    iow(db, DM9000_IMR, db->imr_all);

    writeb(reg_save, db->io_addr);

    spin_unlock_irqrestore(&db->lock, flags);

    return 0;
}
#endif

dm9000_rx轮询处理完收包后,需要调用napi_complete表示轮询完毕。

4.在 open 函数中调用napi_enable使能 NAPI 调度

代码语言:javascript复制
#ifdef CONFIG_DM9000_NAPI
    napi_enable(&db->napi);
#endif

同样在 stop 函数中禁止 NAPI 调度

代码语言:javascript复制
#ifdef CONFIG_DM9000_NAPI
    napi_disable(&db->napi);
#endif

本文作者: Ifan Tsai  (菜菜)

本文链接: https://cloud.tencent.com/developer/article/2164607

版权声明: 本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!

0 人点赞