65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Qemu 的 Linux 以太网驱动程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2016 年 3 月 25 日

GPL3

11分钟阅读

viewsIcon

34436

downloadIcon

727

本文详细介绍了如何在Qemu平台上为伪以太网设备编写Linux设备驱动程序。

引言

Qemu是一个非常强大的硬件/软件协同设计和开发平台。在本文中,我们将开发一个基于ARM的系统,该系统包含一个连接到AXI总线上的ARM-A9处理器的伪以太网设备。然后,我们将在该虚拟化机器上运行Linux操作系统。我们从头开始开发一个模拟的以太网设备以及相应的以太网Linux驱动程序,以展示使用Qemu进行完整系统仿真的便捷性。

搭建基础系统

本文使用了开源机器模拟器Qemu。由于我们的目的是展示Linux中以太网设备及其相关设备驱动程序的仿真,我们将使用一个广泛使用的平台“ARM Versatile Express”作为基础,并为此平台构建Linux Kernel 3.2。

下载ARM版Linaro工具链

由于我们使用ARM Versatile Express作为基础平台,我们需要使用arm-gcc编译器交叉编译Linux源文件和busybox实用程序。我们使用Linaro arm工具链进行交叉编译。如果您使用的是Ubuntu机器,以下命令将安装该工具链。

sudo apt-get install gcc-arm-linux-gnueabi

下载并构建Qemu源代码

从http://wiki.qemu.org/Download下载Qemu源代码。由于我们将通过添加模拟的以太网设备来增强ARM Vexpress平台,因此我们需要使用源代码包。对于这个项目,我使用了qemu-2.5.0。任何其他版本可能需要移植Qemu伪以太网仿真代码。

将qemu-2.5.0源代码下载到~/dwnld_dir,创建一个工作目录并执行以下命令。这里我们正在为Arm架构构建Qemu。构建时不需要提供平台名称。我们需要在调用Qemu模拟器时提供平台名称。

mkdir ~/work
cd work
wget http://wiki.qemu-project.org/download/qemu-2.5.0.tar.bz2 
tar -xjvf qemu-2.5.0.tar.bz2
cd qemu-2.5.0/
./configure --target-list=arm-softmmu
make
ls arm-softmmu/qemu-system-arm

下载并构建Linux Kernel源代码

从https://linuxkernel.org.cn下载Linux内核源代码,并为其ARM Vexpress板构建。确保压缩的Linux映像zImage位于arch/arm/boot/目录中。

cd ~/work
wget https://linuxkernel.org.cn/pub/linux/kernel/v3.0/linux-3.2.tar.bz2
tar -xjvf linux-3.2.tar.bz2 
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabi-
cd linux-3.2/
make vexpress_defconfig
make all
ls arch/arm/boot/zImage

下载并构建Busybox源代码

我们将需要一个根文件系统。我们可以使用为此平台预先构建的根文件系统,也可以自己构建一个。我们将使用Busybox构建一个骨架文件系统。以下是创建骨架根文件系统的说明。请使用menuconfig选项选择“Busybox Settings” --->“Build Options” ---> “Build Busybox as a static library”。文件系统构建完成后,我们将它复制到将用于调用Qemu模拟器的目录中。

注意:使用此busybox版本会遇到链接错误。请在menuconfig中取消选择“Networking Utilities” --->“inetd” ---> “Support RPC Services”选项。

cd ~/work
wget http://www.busybox.net/downloads/busybox-1.24.1.tar.bz2
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- 
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- install
cd _install/
cp ../rcS etc/init.d
mkdir proc sys dev etc etc/init.d
chmod +x etc/init.d/rcS 
find . | cpio -o --format=newc > ../rootfs.img
cp ../rootfs.img ~/work

在这里,我们在根文件系统中使用了一个rcS文件,内容如下。rcS挂载了proc和sysfs。

#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
/sbin/mdev -s

运行基础系统

现在我们的系统已准备就绪。我们将使用以下命令运行它。

./qemu-2.5.0/arm-softmmu/qemu-system-arm  -M vexpress-a9 -m 256M -kernel ./linux-3.2/arch/arm/boot/zImage -initrd rootfs.img -append "root=/dev/ram rdinit=/sbin/init ip=dhcp console=ttyAMA0" -net user,hostfwd=tcp::2222-:22 -net nic,model=lan9118 -nographic
这里我们以无图形模式运行Qemu。系统将启动,您将看到一个提示符。Qemu使用虚拟COM端口,因此如果您想关闭会话,需要像使用minicom一样按下“Ctrl+A”,然后是x。
Sending DHCP requests ., OK
IP-Config: Got DHCP answer from 10.0.2.2, my address is 10.0.2.15
IP-Config: Complete:
     device=eth0, addr=10.0.2.15, mask=255.255.255.0, gw=10.0.2.2,
     host=10.0.2.15, domain=, nis-domain=(none),
     bootserver=10.0.2.2, rootserver=10.0.2.2, rootpath=
Freeing init memory: 172K
input: AT Raw Set 2 keyboard as /devices/mb:kmi0/serio0/input/input0

Please press Enter to activate this console. input: ImExPS/2 Generic Explorer Mouse as /devices/mb:kmi1/serio1/input/input1

/ # ls
bin      etc      lib      proc     sbin     sys
dev      include  linuxrc  root     share    usr
/ # 
该系统附带默认的nic模型lan9118。由于Qemu中的网络模型使用SLIRP,我们无法使用ICMP消息。因此,ping将不起作用。我们可以使用一个简单的套接字服务器/客户端程序来测试Qemu模拟器与主机PC之间的网络通信是否正常。我们甚至可以使用为该平台编译的Iperf(只有客户端模式可以从Qemu设备工作)。

运行简单的套接字应用程序

我们现在将运行一个简单的tcp_client应用程序。交叉编译tcp_client.c应用程序并将其放入骨架根文件系统中。

arm-linux-gnueabi-gcc -g -Wall -g -o tcp_client.out  tcp_client.c -static
cp tcp_client.out ~/work/busybox-1.24.1/_install/bin/
cd ~/work/busybox-1.24.1/_install
find . | cpio -o --format=newc > ../rootfs.img
cp ../rootfs.img ~/work

在您的主机PC上打开一个TCP服务器示例应用程序。启动Qemu,然后运行/bin/tcp_client.out。您将看到TCP客户端与TCP服务器建立连接并发送数据包。

伪以太网设备

现在我们将建模一个伪以太网设备。以太网设备通过MII和MDIO接口连接到一个外部PHY。以太网数据包由描述符表示,这些描述符包含有关数据包大小和数据包数据所在的精确内存位置的信息。有4种类型的描述符,传输端有两种类型,接收端有两种类型。这些描述符维护在它们各自固定大小的队列中。驱动程序必须为所有描述符和队列分配内存。当驱动程序编程这些队列的基地址时,硬件会收到这些队列存在的通知。

Tx描述符队列

此队列包含表示必须由硬件传输的出站以太网帧的描述符。队列包含以太网设备寄存器中的两个指针:由设备驱动程序维护的Head指针,以及由以太网设备维护的Tail指针。当设备驱动程序想要传输一个帧时,它会构造一个传输描述符,并将其放在Head指针寄存器指向的位置,然后将Head指针寄存器加一。以太网硬件的传输端会持续检查Head指针寄存器是否不等于Tail指针寄存器,以指示是否有任何数据包需要传输。然后,它会检索Tail指针指向的描述符,从Tx描述符中检索数据包,并将其发送到网络。

Tx完成描述符队列

此队列包含表示已由硬件发送的以太网帧的描述符。队列包含以太网设备寄存器中的两个指针:由以太网设备维护的Head指针,以及由设备驱动程序维护的Tail指针。当Tx描述符队列中的传输描述符表示的数据包成功发送到网络后,硬件会构造一个Tx完成描述符,并将其放在Tx完成描述符队列中,放在Head指针指向的位置。然后,它会触发一个传输完成中断。设备驱动程序的ISR通过Tail指针逐个查看Tx完成描述符,并释放这些数据包使用的套接字缓冲区。

Rx空描述符队列

此队列包含表示硬件可以将接收到的以太网数据放入其中的空以太网缓冲区的描述符。队列包含以太网设备寄存器中的两个指针。Head指针由设备驱动程序维护,Tail指针由以太网设备维护。在以太网设备初始化期间,设备驱动程序会排队足够的空数据包缓冲区以填满此队列。当以太网设备的接收端在网络上检测到Rx帧时,它会从该队列的Tail指针指向的位置取出一个空缓冲区,将数据复制到其中,并前进Tail指针。驱动程序会定期检查此队列中是否有足够的空缓冲区,并在低于某个阈值时补充缓冲区。

Rx完成描述符队列

此队列包含表示已接收以太网数据包的描述符。队列包含以太网设备寄存器中的两个指针。Head指针由硬件维护,Tail指针由设备驱动程序维护。每当硬件接收到一个帧时,它就会从Rx空描述符的Tail指针指向的位置取出一个空缓冲区,用接收到的数据填充该缓冲区,构造一个指向已接收以太网数据包的Rx完成描述符,并将其放在Rx完成描述符队列中,放在Head指向的位置。然后,它会增加Head的位置。它会触发一个Rx完成中断。设备驱动程序的ISR会检测到新接收到的帧。它会从Rx完成描述符的Tail指针指向的位置获取以太网数据包,并将其发送到TCP/IP堆栈。它会增加Tail指针。它会重复上述步骤,直到所有数据包都发送完毕。

----- +                     +------------------------+
      |                     |         MAC            |
      |    Tx Desc Queue    |    +--------------+    |
      |     =============   |    |              |    | 
      | ===> XXXXXXXXXX ==> |    |              |    |       +-----------+
      |     =============   |    |              |    |       |           |
      |                     |    |   Transmit   |    |       |           |
      |  Tx Done Desc Queue |    |              |    |       |           |
      |     =============   |    |              |    |       |           |
      | <=== XXXXXXXXXX <== |    |              |    |       |           |
      |     =============   |    +--------------+    |  MDIO |   Phy     |
 C    |                     |                        | <===> |           |
 P    |                     |                        |  MII  |           |
 U    | Rx Empty Desc Queue |    +--------------+    | <===> |           |
      |     =============   |    |              |    |       |           |
      | ===> XXXXXXXXXX <== |    |              |    |       |           |
      |     =============   |    |              |    |       |           |
      |                     |    |   Receive    |    |       |           |
      |  Rx Done Desc Queue |    |              |    |       |           |
      |     =============   |    |              |    |       |           |
      | <=== XXXXXXXXXX <== |    |              |    |       +-----------+
      |     =============   |    +--------------+    |
      |                     |                        |
------+                     +------------------------+

Qemu中的伪以太网设备模型

在本节中,我将简要介绍Qemu中伪NIC的建模方式。

模型以函数skel_eth_device_register_types()开始。该函数注册一个类型为skel_eth_device_info的新设备。

static const TypeInfo skel_eth_device_info = {
    .name          = TYPE_SKEL_ETH_DEV,
    .parent        = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(skel_eth_device_state),
    .class_init    = skel_eth_device_class_init,
};

static void skel_eth_device_register_types(void)
{
    type_register_static(&skel_eth_device_info);
}

在Qemu Vexpress仿真主文件中的vexpress_common_init()函数中,我们调用skel_eth_device_init()函数,该函数创建此NIC模型,将其寄存器空间映射到指定的基地址,最后连接到所需的IRQ引脚。

void skel_eth_device_init(NICInfo *nd, uint32_t base, qemu_irq irq)
{
    DeviceState *dev;
    SysBusDevice *s;
    qemu_check_nic_model(nd, TYPE_SKEL_ETH_DEV);
    dev = qdev_create(NULL, TYPE_SKEL_ETH_DEV);
    qdev_set_nic_properties(dev, nd);
    qdev_init_nofail(dev);
    s = SYS_BUS_DEVICE(dev);
    sysbus_mmio_map(s, 0, base);
    sysbus_connect_irq(s, 0, irq);
}

Qemu核心仿真层会调用我们在class_init()中注册的设备的类特定初始化函数skel_eth_device_init1()。skel_eth_device_init1()将读/写钩子函数映射到处理CPU到其寄存器地址空间的寄存器读/写。

static const MemoryRegionOps skel_eth_device_mem_ops = {
    .read = skel_eth_device_readl,
    .write = skel_eth_device_writel,
    .endianness = DEVICE_NATIVE_ENDIAN,
};

static int skel_eth_device_init1(SysBusDevice *sbd)
{
    const MemoryRegionOps *mem_ops = &skel_eth_device_mem_ops;
    memory_region_init_io(&s->mmio, OBJECT(dev), mem_ops, s, "skel_eth_device-mmio", 0x100);
    return 0;
}

其余逻辑相当简单。skel_eth_device_readl()函数包含一个大的switch->case语句来处理寄存器读取。

static uint64_t skel_eth_device_readl(void *opaque, hwaddr offset,
                              unsigned size)
{

    skel_eth_device_state *s = (skel_eth_device_state *)opaque;
    switch (offset) {
        case ETH_CTRL:
            return s->eth_ctrl;
        case MAC_ADDR_HIGH:
            return s->mac_addr_high;
        case MAC_ADDR_LOW:
            return s->mac_addr_low;
        case MGMT_FRM:
            //Read/Write PHY register here
            return s->mac_mii_data ;
...
}

skel_eth_device_writel()函数也是如此。

static void skel_eth_device_writel(void *opaque, hwaddr offset,
                           uint64_t val, unsigned size)
{
    skel_eth_device_state *s = (skel_eth_device_state *)opaque;
    offset &= 0xff;
    switch (offset) {
        case ETH_CTRL:
            if (((s->eth_ctrl & 1) != (val&1)) && ((val&1) == 1))
            {
                skel_eth_device_reset(s);
            }
            s->eth_ctrl=val;
            break;

        case MAC_ADDR_HIGH:
            s->mac_addr_high=val;
            s->conf.macaddr.a[4] = val & 0xff;
            s->conf.macaddr.a[5] = (val >> 8) & 0xff;
            skel_eth_device_mac_changed(s);
            break;

        case MAC_ADDR_LOW:
            s->mac_addr_low=val;
            s->conf.macaddr.a[0] = val & 0xff;
            s->conf.macaddr.a[1] = (val >> 8) & 0xff;
            s->conf.macaddr.a[2] = (val >> 16) & 0xff;
            s->conf.macaddr.a[3] = (val >> 24) & 0xff;
            skel_eth_device_mac_changed(s);
            break;
...
}

传输数据包

当设备驱动程序在Tx描述符队列中写入新的传输描述符时,将触发数据包的传输。writel函数检查队列是否非空,并调用do_tx_packet()函数。

static void skel_eth_device_writel(void *opaque, hwaddr offset,
                           uint64_t val, unsigned size)
{
    skel_eth_device_state *s = (skel_eth_device_state *)opaque;
    offset &= 0xff;
    switch (offset) {
        case TX_DESC_FIFO_HEAD:
            s->tx_desc_fifo_head=val;
            if(s->tx_desc_fifo_head != s->tx_desc_fifo_tail)
            {
                do_tx_packet(s);
            }

下面的代码列出了do_tx_packet()。它获取当前的Tx描述符,将数据包数据复制到一个临时保存缓冲区,然后调用qemu_send_packet()将其发送到网络。之后,它会构造一个Tx完成描述符并将其放入Tx完成描述符队列。它会推进Tx描述符Tail指针和Tx完成描述符Head指针。最后,它断言tx_complete中断。

static void do_tx_packet(skel_eth_device_state *s)
{
    while(s->tx_desc_fifo_head != s->tx_desc_fifo_tail)
    {
        // Get the current tx_desc
        tx_desc = s->tx_desc_base_low + (s->tx_desc_fifo_tail * sizeof(tx_desc_t));
        cpu_physical_memory_read(tx_desc, &tx_desc_local, sizeof(tx_desc_t));
        cpu_physical_memory_read(tx_desc_local.u.tx_buf_address, buf,
                                        tx_desc_local.u.buffer_length);
        qemu_send_packet(qemu_get_queue(s->nic), (const uint8*)(buf),
                                        tx_desc_local.u.buffer_length);
        // Move this descriptor to 
        memset(&tx_done_desc_local, 0, sizeof(tx_done_desc_t));
        tx_done_desc_local.u.send_done=1;
        tx_done_desc_local.u.index = s->tx_desc_fifo_tail; 

        // Get the pointer to current tx_done_desc
        tx_done_desc = s->tx_done_desc_base_low +
                        (s->tx_done_desc_fifo_head * sizeof(tx_done_desc_t));

        // Copy the updated tx_done_desc to the actual location
        cpu_physical_memory_write(tx_done_desc, &tx_done_desc_local, sizeof(tx_done_desc_t));

        // Advance the tx_desc_fifo_tail & tx_done_desc_fifo_head
        ...
    }
    // Assert Transmit complete interrupt
    s->int_sts1 |= 1;
    skel_eth_device_update(s);
}

接收数据包

接收数据包的路径略有不同。我们在创建NIC并向Qemu模拟器注册时,将接收函数skel_eth_device_receive()注册为NIC模型的一部分。每当系统收到以太网帧时,都会调用此注册函数,并将接收到的数据包作为参数传递。

static ssize_t skel_eth_device_receive(NetClientState *nc, const uint8_t *buf,
                               size_t size)
{
    // Get the current rx_empty_desc
    rx_empty_desc = s->rx_empty_desc_base_low +
                        (s->rx_empty_desc_fifo_tail * sizeof(rx_empty_desc_t));
    cpu_physical_memory_read(rx_empty_desc, &rx_empty_desc_local, sizeof(rx_empty_desc_t));

    // Copy the payload to the buffer pointed to by the rx descriptor
    cpu_physical_memory_write(rx_empty_desc_local.u.rx_buf_address, buf, size);

    // Update rx_done_desc descriptor
    memset(&rx_done_desc_local, 0, sizeof(rx_done_desc_t));
    rx_done_desc_local.u.receive_done=1;
    rx_done_desc_local.u.index = s->rx_empty_desc_fifo_tail; 
    rx_done_desc_local.u.cur_buf_length = size;

    // Get the pointer to current rx_done_desc
    rx_done_desc = s->rx_done_desc_base_low +
                (s->rx_done_desc_fifo_head * sizeof(rx_done_desc_t));
    cpu_physical_memory_write(rx_done_desc, &rx_done_desc_local, sizeof(rx_done_desc_t));

    // Advance the rx_done_desc_fifo_head and tx_desc_fifo_tail
    ...

    // Assert interrupt complete interrupt
    s->int_sts1 |= 2; //Rx-Queue Complete interrupt
    skel_eth_device_update(s);
    return size;
}

以太网驱动程序

在本节中,我将简要解释如何编写伪以太网设备的设备驱动程序。我将驱动程序保持在最低限度,只实现最重要的函数。因此,该驱动程序远非完整。

修改Linux Kernel Makefile以包含我们的驱动程序

ARM Vexpress平台附带smsc911驱动程序。我们将简单地删除此驱动程序,并将我们的骨架以太网驱动程序替换它。(正确的方法是更改配置文件为骨架以太网驱动程序添加条目并选中它。由于我们的重点是解释以太网驱动程序模型和相关驱动程序,我们在这里采取捷径)

gvim linux-3.2/drivers/net/ethernet/smsc/Makefile

#obj-$(CONFIG_SMSC911X) += smsc911x.o <--commented this line and added the following line
obj-$(CONFIG_SMSC911X) += skel_eth_drv.o

我们还更改了V2M Core BSP文件,以使用骨架设备而不是smsc911x设备。

gvim linux-3.2/arch/arm/mach-vexpress/v2m.c

static struct platform_device v2m_eth_device = {
    //.name        = "smsc911x", <-- Commented this line and added the following line
    .name        = "skel_eth_dev",
    .id        = -1,
    .resource    = v2m_eth_resources,
    .num_resources    = ARRAY_SIZE(v2m_eth_resources),
    .dev.platform_data = &v2m_eth_config,
};

编写以太网设备驱动程序

在本节中,我们将详细介绍编写伪以太网设备的驱动程序。

驱动程序从platform_driver_register()开始,该函数从我们的init_module函数调用。

static struct platform_driver skel_eth_driver = {
    .probe = skel_eth_drv_probe,
    .remove = skel_eth_drv_remove,
    .driver = {
        .name    = SKEL_ETH_NAME,
        .owner    = THIS_MODULE,
        .pm    = SKEL_ETH_PM_OPS,
        .of_match_table = of_match_ptr(skel_eth_dt_ids),
    },
};

/* Entry point for loading the module */
static int __init skel_eth_init_module(void) {
    return platform_driver_register(&skel_eth_driver);
}

由于我们已经在BSP文件v2m.c中注册了此设备,因此内核会识别出该驱动程序与已注册的设备匹配,并调用相应的probe函数。

下面是驱动程序的probe函数。probe函数分配驱动程序结构,为各种描述符队列分配内存,使用这些队列的基地址对硬件进行编程,初始化硬件以发送和接收以太网数据包,最后,它通过调用register_netdev()向操作系统注册其以太网设备操作。

static const struct net_device_ops skel_eth_netdev_ops = {
    .ndo_open        = skel_eth_open,
    .ndo_stop        = skel_eth_stop,
    .ndo_start_xmit  = skel_eth_hard_start_xmit,
    .ndo_do_ioctl    = skel_eth_do_ioctl,
};

/* Initializing private device structures, only called from probe */
static int skel_eth_init(struct net_device *dev)
{
    struct skel_eth_data *pdata = netdev_priv(dev);
    unsigned int eth_ctrl;
    unsigned int to = 100;
    rx_empty_desc_t *rx_empty_desc;

    int retval;

    // Allocate memory for Tx, TxDone, RxEmpty and RxDone Descriptor queues.
    ...

    // Program the hardware base address for Tx, TxDone, RxEmpty and RxDone Descriptor queues.
    ...

    // Enable ethernet peripheral. Set up speed and mode
    ...

    // Fill the rx ring with RX EB descriptors
    skel_eth_rx_desc_refill(pdata, NUM_RX_DESC);


    ether_setup(dev);
    dev->flags |= IFF_MULTICAST;
    netif_napi_add(dev, &pdata->napi, skel_eth_poll, SKEL_ETH_DEV_NAPI_WEIGHT);
    dev->netdev_ops = &skel_eth_netdev_ops;

    pr_info(" %s DONE!!!\n", __FUNCTION__);
    return 0;
}
static int skel_eth_drv_probe(struct platform_device *pdev)
{
    struct net_device *dev;
    struct skel_eth_data *pdata;
    struct resource *res, *irq_res;
    int res_size, irq_flags;
    int retval;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);    
    request_mem_region(res->start, res_size, SKEL_ETH_NAME);

    irq_res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);

    dev = alloc_etherdev(sizeof(struct skel_eth_data));
    SET_NETDEV_DEV(dev, &pdev->dev);

    pdata = netdev_priv(dev);
    dev->irq = irq_res->start;
    irq_flags = irq_res->flags & IRQF_TRIGGER_MASK;
    pdata->ioaddr = ioremap_nocache(res->start, res_size);

    pdata->dev = dev;
    platform_set_drvdata(pdev, dev);

    pdata->ops = &standard_skel_eth_ops;

    retval = skel_eth_init(dev);

    retval = request_irq(dev->irq, skel_eth_irqhandler, irq_flags | IRQF_SHARED, dev->name, dev);

    retval = register_netdev(dev);

    retval = skel_eth_mii_init(pdev, dev);

    spin_lock_irq(&pdata->mac_lock);
    /* Check if mac address has been specified when bringing interface up */
    if (is_valid_ether_addr(dev->dev_addr)) {
        skel_eth_set_hw_mac_address(pdata, dev->dev_addr);
    } else {
        /* eeprom values are invalid, generate random MAC */
        dev_hw_addr_random(dev, dev->dev_addr);
        skel_eth_set_hw_mac_address(pdata, dev->dev_addr);
    }
    spin_unlock_irq(&pdata->mac_lock);
    return 0;
}

传输数据包

每当需要发送以太网数据包时,网络堆栈会调用注册的_start_xmit函数。下面是_start_xmit函数的列表。它简单地使用缓冲区地址和大小更新Tx描述符队列head指针指向的Tx描述符。然后,它将head指针加一,告知硬件一个新的数据包已排队。

static int skel_eth_hard_start_xmit(struct sk_buff *skb, struct net_device *dev)
{
    struct skel_eth_data *pdata = netdev_priv(dev);
    dma_addr_t bus_addr;
    struct netdev_queue *txq;
    unsigned int nr_frags;
    tx_desc_t *tx_desc;
    u32 tx_desc_fifo_tail = 0;

    // Read the TX EB head and tail pointeres from the hardware
    tx_desc_fifo_head = skel_eth_reg_read(pdata, TX_DESC_FIFO_HEAD);

    /* total number of fragments in the SKB */
    nr_frags = skb_shinfo(skb)->nr_frags;

    // Get the current tx_desc
    tx_desc = pdata->tx_desc_base + (tx_desc_fifo_head * sizeof(tx_desc_t));

    // Update tx_desc parameters
    len = skb_headlen(skb);
    bus_addr = dma_map_single(&dev->dev, skb->data, len, DMA_TO_DEVICE);

    tx_desc->u.tx_buf_address = cpu_to_le32(bus_addr & 0xFFFFFFFF);
    tx_desc->u.buffer_length = len;

    // Increment the head pointer with nr_frags+1
    tx_desc_fifo_head = (tx_desc_fifo_head + nr_frags+1) & TX_INDEX_MASK;
    skel_eth_reg_write(pdata, TX_DESC_FIFO_HEAD, tx_desc_fifo_head);

    return NETDEV_TX_OK;
}

接收数据包

接收数据包始于硬件接收到完整的以太网数据包并触发Rx完成中断。在irqhandler函数中,我们简单地确认Rx中断并禁用它。然后,我们调用napi_schedule()函数指示操作系统调用我们的napi poll函数。

static irqreturn_t skel_eth_irqhandler(int irq, void *dev_id)
{
    int serviced = IRQ_NONE;
    struct net_device *dev = dev_id;
    struct skel_eth_data *pdata = netdev_priv(dev);
    u32 intsts;

    intsts = skel_eth_reg_read(pdata, INT_STS1);
    skel_eth_reg_write(pdata, INT_CLR1, intsts);

    if(intsts & (1<<INT_STS1_RX_QUEUE_SHIFT)) {
        if (likely(napi_schedule_prep(&pdata->napi))) {

            /* Disable Rx interrupts */
            ...

            /* Schedule a NAPI poll */
            __napi_schedule(&pdata->napi);
        } 
        serviced = IRQ_HANDLED;
    }
    return serviced;
}

下面是napi_poll函数的列表。我们遍历rx_done描述符队列,从相应的s/w环中提取所需的skb信息,然后调用netif_receive_skb()将skb发送到网络堆栈。
如果我们已经接收了NAPI预算中足够数量的数据包,或者我们已经接收了所有可用的数据包,我们将告诉napi停止调用poll函数,然后启用rx中断。

static int skel_eth_poll(struct napi_struct *napi, int budget)
{
    struct skel_eth_data *pdata = container_of(napi, struct skel_eth_data, napi);
    struct net_device *dev = pdata->dev;
    int npackets = 0;
    u32 rx_done_desc_fifo_tail = 0;
    u32 rx_done_desc_fifo_head = 0;
    rx_done_desc_t* rx_done_desc;
    dma_addr_t bus_addr;

    rx_done_desc_fifo_tail = skel_eth_reg_read(pdata, RX_DONE_DESC_FIFO_TAIL);
    rx_done_desc_fifo_head = skel_eth_reg_read(pdata, RX_DONE_DESC_FIFO_HEAD);

    // Work till we have processed all the completed Rx packets or budget
    while ((npackets < budget) && (rx_done_desc_fifo_head != rx_done_desc_fifo_tail)) {
        struct sk_buff *skb;

        //Get the current rx_done_desc corresponding to this rx_done_desc_fifo_tail index
        rx_done_desc = pdata->rx_done_desc_base + (rx_done_desc_fifo_tail * sizeof(rx_done_desc_t));

        // Get the skb information from the sw ring
        skb = pdata->sw_rx_desc_info_ring[rx_done_desc->u.index].skb;
        bus_addr = pdata->sw_rx_desc_info_ring[rx_done_desc->u.index].dma_addr;
        pdata->sw_rx_desc_info_ring[rx_done_desc->u.index].skb = NULL;

        dma_unmap_single(&pdata->dev->dev, bus_addr, rx_done_desc->u.cur_buf_length, DMA_FROM_DEVICE);

        skb_put(skb, rx_done_desc->u.cur_buf_length);
        skb->protocol = eth_type_trans(skb, dev);

        // Send this skb to tcp/ip layer
        netif_receive_skb(skb);

        npackets++;

        // increment the rx_done_desc_fifo_tail and write it to h/w
        ...

        // refill the rx_desc with descriptors.
        skel_eth_rx_desc_refill(pdata, 1);
    }

    if(rx_done_desc_fifo_head == rx_done_desc_fifo_tail)
    {
        /* We processed all packets available. Tell NAPI to stop polling & re-enable rx interrupts */
        if(npackets != SKEL_ETH_DEV_NAPI_WEIGHT) {
            napi_complete(napi);
        }
       
        // Renabled the Rx interrupt
        ...
    }
    return npackets;
}

运行我们的新系统

现在,我们的新模拟系统已准备就绪。我们将使用以下命令运行它。

./qemu-2.5.0/arm-softmmu/qemu-system-arm  -M vexpress-a9 -m 256M -kernel linux-3.2/arch/arm/boot/zImage -initrd rootfs.img -append "root=/dev/ram rdinit=/sbin/init ip=dhcp console=ttyAMA0" -net user,hostfwd=tcp::2222-:22 -net nic,model=skel_eth_dev -nographic

这里我们使用了新开发的以太网模型skel_eth_dev。我们可以在模拟系统中运行相同的tcp_client应用程序,并在主机PC上运行相应的tcp_server,以验证我们的新以太网模型是否正常工作。

 

 

 

© . All rights reserved.