前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux驱动之网卡驱动剖析

Linux驱动之网卡驱动剖析

作者头像
菜菜cc
发布2022-11-15 21:39:30
56.1K1
发布2022-11-15 21:39:30
举报
文章被收录于专栏:菜菜的技术博客

网络设备不同于字符设备和块设备,并不对应于/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%s\n", 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 resources\n");
       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 area\n");
       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 reg\n");
       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 area\n");
       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 reg\n");
       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 16bit\n");
	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 device\n");

	/* 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 0x%08x\n", id_val);
}

if (id_val != DM9000_ID) {
    dev_err(db->dev, "wrong id: 0x%08x\n", 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 0x%02x  , io_mode %02x \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 %02x => defaulting to DM9000E\n", 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 ifconfig\n", 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 %s\n", 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 %s\n", __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 %02x\n", 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: %d\n", 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 %02x, length %04x\n",
				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:%x\n", 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 error\n");
				dev->stats.rx_fifo_errors++;
			}
			if (rxhdr.RxStatus & RSR_CE) {
				if (netif_msg_rx_err(db))
					dev_dbg(db->dev, "crc error\n");
				dev->stats.rx_crc_errors++;
				GoodPacket = false;
			}
			if (rxhdr.RxStatus & RSR_RF) {
				if (netif_msg_rx_err(db))
					dev_dbg(db->dev, "length error\n");
				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 %04x %04x %04x %04x\n",
						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 国际许可协议 进行许可。转载请注明出处!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-10-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Linux 网络设备驱动架构
    • 协议接口层
      • 设备接口层
        • 设备驱动功能层
        • 源码分析
          • platform 框架分析
            • dm9000 open 过程分析
              • DM9000 发送过程分析
                • DM9000 接收过程分析
                  • NAPI 方式接收介绍
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档