一文搞懂网卡驱动的原理与移植方法

1、网卡设备驱动原理

1.1 层次结构

Linux系统对网络设备驱动定义了4个层次, 这4个层次有到下分为:

  • 1、网络协议接口层:实现统一的数据包收发的协议。该层主要负责调用dev_queue_xmit()函数发送数据, netif_rx()函数接收数据。当上层 ARP 或 IP 协议需要发送数据包时,它将调用网络协议接口层的 dev_queue_xmit()函数发送该数据包,同时需传递给该函数一个指向 struct sk_buff数据 结构的指针。dev_queue_xmit()函数的原型为:
dev_queue_xmit (struct sk_buff * skb ); 

同样地,上层对数据包的接收也通过向 netif_rx()函数传递一个 struct sk_buff数据 结构的指针来完成。netif_rx()函数的原型为:

 int netif_rx(struct sk_buff *skb); 
  • 2、网络设备接口层:通过net_device结构体来描述一个具体的网络设备的信息,实现不同的硬件的统一
  • 3、设备驱动功能层:用来负责驱动网络设备硬件来完成各个功能, 它通过hard_start_xmit() 函数启动发送操作, 并通过网络设备上的中断触发接收操作。
  • 4、网络设备与媒介层:用来负责完成数据包发送和接收的物理实体, 设备驱动功能层的函数都在这物理上驱动的

层次结构如下图所示:

image-20210809230524324

设计具体的网络设备驱动程序时,我们需要完成的主要工作是编写设备驱动功 能层的相关函数以填充 net_device 数据结构的内容并将 net_device 注册入内核。

1.2 网卡驱动的初始化

网卡驱动程序,只需要编写网络设备接口层,填充net_device数据结构的内容并将net_device注册入内核,设置硬件相关操作,使能中断处理等。

1.2.1 初始化网卡步骤

  • 1)使用alloc_netdev()来分配一个net_device结构体
  • 2)设置网卡硬件相关的寄存器
  • 3)设置net_device结构体的成员
  • 4)使用register_netdev()注册net_device结构体

1.2.2 net_device结构体

网络设备接口层的主要功能是为千变万化的网络设备定义了统一、抽象的数据结 构 net_device 结构体,以不变应万变,实现多种硬件在软件层次上的统一。 net_device 结构体在内核中指代一个网络设备,网络设备驱动程序只需通过填充 net_device 的具体成员并注册 net_device 即可实现硬件操作函数与内核的挂接。 net_device 本身是一个巨型结构体,包含网络设备的属性描述和操作接口。当我 们编写网络设备驱动程序时,只需要了解其中的一部分。

struct net_device
{
       char               name[IFNAMSIZ];      //网卡设备名称
       unsigned long      mem_end;             //该设备的内存结束地址
       unsigned long      mem_start;            //该设备的内存起始地址
       unsigned long      base_addr;            //该设备的内存I/O基地址
       unsigned int       irq;                  //该设备的中断号

       unsigned char      if_port;              //该字段仅针对多端口设备,用于指定使用的端口类型
    unsigned char      dma;                  //该设备的DMA通道
       unsigned long      state;                //网络设备和网络适配器的状态信息

       struct net_device_stats* (*get_stats)(struct net_device *dev); //获取流量的统计信息,运行ifconfig便会调用该成员函数,并返回一个net_device_stats结构体获取信息

      struct net_device_stats  stats;      //用来保存统计信息的net_device_stats结构体

 
       unsigned long         features;        //接口特征,     
       unsigned int          flags; //flags指网络接口标志,以IFF_开头,包括:IFF_UP( 当设备被激活并可以开始发送数据包时, 内核设置该标志)、 IFF_AUTOMEDIA(设置设备可在多种媒介间切换)、IFF_BROADCAST( 允许广播)、IFF_DEBUG( 调试模式, 可用于控制printk调用的详细程度) 、 IFF_LOOPBACK( 回环)、IFF_MULTICAST( 允许组播) 、 IFF_NOARP( 接口不能执行ARP,点对点接口就不需要运行 ARP) 和IFF_POINTOPOINT( 接口连接到点到点链路) 等。

 
       unsigned        mtu;        //最大传输单元,也叫最大数据包

       unsigned short  type;     //接口的硬件类型

       unsigned short   hard_header_len;     //硬件帧头长度,在以太网设备的初始化函数中一般被赋为ETH_HLEN,即14
 
    unsigned char    *dev_addr;   //存放设备的MAC地址,需由驱动程序从硬件上读出
      unsigned char    broadcast[MAX_ADDR_LEN];    //存放设备的广播地址,对于以太网而言,地址长度为6个0XFF

       unsigned long    last_rx;    //接收数据包的时间戳,调用netif_rx()后赋上jiffies即可

       unsigned long    trans_start;   //发送数据包的时间戳,当要发送的时候赋上jiffies即可

       int (*hard_start_xmit) (struct sk_buff *skb, struct net_device *dev);//数据包发送函数, 以使得驱动程序能获取从上层传递下来的数据包。
                                   
    void  (*tx_timeout) (struct net_device *dev); //发包超时处理函数,需采取重新启动数据包发送过程或重新启动硬件等策略来恢复网络设备到正常状态
    ... ...
}

1.2.3 net_device_stats结构体

net_device_stats 结构体定义在内核的 include/linux/netdevice.h 文件中,其中重要成员如下所示:

struct net_device_stats
{
    unsigned long   rx_packets;     /*收到的数据包数*/
    unsigned long   tx_packets;     /*发送的数据包数    */
    unsigned long   rx_bytes;       /*收到的字节数,可以通过sk_buff结构体的成员len来获取*/       unsigned long   tx_bytes;       /*发送的字节数,可以通过sk_buff结构体的成员len来获取*/
    unsigned long   rx_errors;      /*收到的错误数据包数*/
    unsigned long   tx_errors;      /*发送的错误数据包数*/
    ... ...
}

net_device_stats 结构体适宜包含在设备的私有信息结构体中,而其中统计信息的 修改则应该在设备驱动的与发送和接收相关的具体函数中完成,这些函数包括中断处 理程序、数据包发送函数、数据包发送超时函数和数据包接收相关函数等。我们应该 在这些函数中添加相应的代码:

/* 发送超时函数 */ 
void xxx_tx_timeout(struct net_device *dev) 
{ 
    struct xxx_priv *priv = netdev_priv(dev); 
    ... 
    priv->stats.tx_errors++; /* 发送错误包数加 1 */ 
    ... 
} 

/* 中断处理函数 */ 
static void xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs) 
{ 
    switch (status &ISQ_EVENT_MASK) 
    { 
        ... 
        case ISQ_TRANSMITTER_EVENT: / 
            priv->stats.tx_packets++; /* 数据包发送成功,tx_packets 信息加1 */ 
            netif_wake_queue(dev); /* 通知上层协议 */ 
            if ((status &(TX_OK | TX_LOST_CRS | TX_SQE_ERROR | 
                TX_LATE_COL | TX_16_COL)) != TX_OK) /*读取硬件上的出错标志*/ 
            { 
                /* 根据错误的不同情况,对 net_device_stats 的不同成员加 1 */ 
                if ((status &TX_OK) == 0) 
                    priv->stats.tx_errors++; 
                if (status &TX_LOST_CRS) 
                    priv->stats.tx_carrier_errors++; 
                if (status &TX_SQE_ERROR)
                    priv->stats.tx_heartbeat_errors++; 
                if (status &TX_LATE_COL) 
                    priv->stats.tx_window_errors++; 
                if (status &TX_16_COL) 
                    priv->stats.tx_aborted_errors++; 
            } 
            break; 
        case ISQ_RX_MISS_EVENT: 
            priv->stats.rx_missed_errors += (status >> 6); 
            break; 
        case ISQ_TX_COL_EVENT: 
            priv->stats.collisions += (status >> 6); 
            break; 
    } 
}

1.2.4 sk_buff结构体

sk_buff 结构体非常重要,它的含义为“套接字缓冲区”,用于在 Linux 网络子系 统中的各层之间传递数据,是 Linux 网络子系统数据传递的“中枢神经”。当发送数据包时,Linux 内核的网络处理模块必须建立一个包含要传输的数据包 的 sk_buff,然后将 sk_buff 递交给下层,各层在 sk_buff 中添加不同的协议头直至交 给网络设备发送。同样地,当网络设备从网络媒介上接收到数据包后,它必须将接收 到的数据转换为 sk_buff 数据结构并传递给上层,各层剥去相应的协议头直至交给用 户。

参看 linux/skbuff.h 中的源代码,sk_buff 结构体包含的主要成员如下:

/* /include/linux/skbuff.h */
struct sk_buff {
       /* These two members must be first. */
       struct sk_buff        *next;      //指向下一个sk_buff结构体
       struct sk_buff        *prev;     //指向前一个sk_buff结构体
    ktime_t             tstamp;
      struct sock       *sk;
      struct net_device *dev;
    /*
       * This is the control buffer. It is free to use for every
       * layer. Please put your private variables there. If you
       * want to keep them across layers you have to do a skb_clone()
       * first. This is owned by whoever has the skb queued ATM.
       */
       ......
       unsigned int         len,         //数据包的总长度,包括线性数据和非线性数据
                            data_len;        //非线性的数据长度
       __u16                mac_len,         //mac包头长度
                            hdr_len;
      .....
    __u32      priority;          //该sk_buff结构体的优先级
      ......
    __be16    protocol;           //存放上层的协议类型,可以通过eth_type_trans()来获取
       ... ...

      sk_buff_data_t              transport_header;    //传输层头部的偏移值
      sk_buff_data_t              network_header;     //网络层头部的偏移值
      sk_buff_data_t              mac_header;          //MAC数据链路层头部的偏移值
      /* These elements must be at the end, see alloc_skb() for details.  */
    sk_buff_data_t              tail;                    //指向缓冲区的数据包末尾
      sk_buff_data_t              end;                     //指向缓冲区的末尾
      unsigned char            *head,                   //指向缓冲区的协议头开始位置
                                  *data;                   //指向缓冲区的数据包开始位置
      unsigned int                truesize;
      atomic_t                    users;
}
  1. 数据缓冲区指针 head、data、tail 和 end。

head 指针指向内存中已分配的用于承载网络数据的缓冲区的起始地址;

data 指针则指向对应当前协议层有效数据的起始地址。各层的有效数据信息包含的内容都不一样:对于传输层而言,用户数据和传输层协议头属于有效数据。 l 对于网络层而言,用户数据、传输层协议头和网络层协议头是其有效数据。 l 对于数据链路层而言,用户数据、传输层协议头、网络层协议头和链路层头 部都属于有效数据。 因此,data 指针的值需随着当前拥有 sk_buff 的协议层的变化进行相应的移动。

tail 指针则指向对应当前协议层有效数据负载的结尾地址,与 data 指针对应。

end 指针指向内存中分配的数据缓冲区的结尾,与 head 指针对应。

其实,end 指针所指地 址 数 据 缓 冲 区 的 末 尾 还 包 括 一 个 skb_shared_info结构体的空间,这个结构体 存放分隔存储的数据片段,意味着可以将数 据包的有效数据分成几片存储在不同的内存空间中。 每一个分片frags的长度上限是一页。

  • sk_buff结构体的空间
  • image-20210809231316947
  1. 长度信息 len、data_len、truesize。

len是指数据包有 效数据的长度,包括协议头和负载;

data_len 这个成员,它记录分片的 数据长度;

truesize 表示缓存区的整体长度: sizeof(struct sk_buff) + “传入 alloc_skb()或dev_alloc_skb()的长度“(但不包括结构体 skb_shared_info 的长度)。

  • sk_buff-> data数据包格式
image-20210809231423958
  • 套接字缓冲区操作

    • (1)分配

      • struct sk_buff *alloc_skb(unsigned int len,int priority);alloc_skb()函数分配一个套接字缓冲区和一个数据缓冲区,参数 len 为数据缓冲区 的空间大小,以 16 字节对齐,参数 priority 为内存分配的优先级
      • struct sk_buff *dev_alloc_skb(unsigned int len);dev_alloc_skb()函数只是以 GFP_ATOMIC 优先级(代表分配过程不能被中断)调 用上面的 alloc_skb()函数,并保存 skb->head 和 skb->data 之间的 16 个字节。
      • 分配成功之后,因为还没有存放具体的网络数据包,所以 sk_buff 的 data、tail 指 针都指向存储空间的起始地址 head,而 len 的大小则为 0。
    • (2)释放

      • void kfree_skb(struct sk_buff *skb);  //在内核内部使用,而网络设备 驱动程序中则必须使用下面3个其一
        void dev_kfree_skb(struct sk_buff *skb); //用于非中断上下文
        void dev_kfree_skb_irq(struct sk_buff *skb); //用于中断上下文
        void dev_kfree_skb_any(struct sk_buff *skb); //在中断和非中断上下文中皆可采用
        
  • (3)指针移动

    套接字缓冲区中的数据缓冲区指针移动操作包括 put(放置)、push(推)、 pull(拉)、reserve(保留)等。

    • put 操作(用于在缓冲区尾部添加数据):

      • unsigned char *skb_put(struct sk_buff *skb, unsigned int len); //会检测放入缓冲区的数据
        unsigned char *__skb_put(struct sk_buff *skb, unsigned int len); //不会检测放入缓冲区的数据
        

        上述函数将 tail 指针下移,增加 sk_buff 的 len 值,并返回 skb->tail 的当前值。

    • push 操作(用于在数据包发送时添加头部

      • unsigned char *skb_push(struct sk_buff *skb, unsigned int len); //会检测放入缓冲区的数据
        unsigned char *_ _skb_push(struct sk_buff *skb, unsigned int len); //不会检测放入缓冲区的数据
        

        push 操作在存储空间的头部增加一段可以存储网络数据包的空间。

    • pull 操作(用于下层协议向上层协议移交数据包,使 data 指针指向上一层协议的协议头)

      • unsigned char * skb_pull(struct sk_buff *skb, unsigned int len);
        

        skb_pull()函数将 data 指针下移,并减小 skb 的 len 值。

    • reserve 操作(用于在存储空间 的头部预留 len 长度的空隙)

      • void skb_reserve(struct sk_buff *skb, unsigned int len);
        

        skb_reserve()函数将 data 指针和 tail 指针同时下移

1.3 网卡驱动发包过程

在内核中,当上层要发送一个数据包时, 就会调用网络设备层里net_device数据结构的成员hard_start_xmit()将数据包发送出去。

hard_start_xmit()发包函数需要我们自己构建,该函数原型如下所示:

int    (*hard_start_xmit) (struct sk_buff *skb, struct net_device *dev);

这个函数中需要涉及到sk_buff结构体,含义为(socket buffer)套接字缓冲区,用来网络各个层次之间传递数据.

发包函数处理步骤:

  • 1、把数据包发出去之前,需要使用netif_stop_queue()来停止上层传下来的数据包;

  • 2.1、设置寄存器,通过网络设备硬件来发送数据

  • 2.2、当数据包发出去后, 再调用dev_kfree_skb()函数来释放sk_buff,该函数原型如下:void dev_kfree_skb(struct sk_buff *skb);

  • 3、当数据包发出成功,就会进入TX接收中断函数,然后更新统计信息,调用netif_wake_queue()来唤醒,启动上层继续发包下来;

  • 4、若数据包发出去超时,一直进不到TX中断函数,就会调用net_device结构体的*tx_timeout超时成员函数,在该函数中更新统计信息,并调用netif_wake_queue()来唤醒。

1.4 网卡驱动收包过程

接收数据包主要是通过中断函数处理,来判断中断类型,如果等于ISQ_RECEIVER_EVENT表示为接收中断,然后进入接收数据函数,通过netif_rx()将数据上交给上层。例如,下图内核中自带的网卡驱动:/drivers/net/cs89x0.c

image-20210809232214875

通过获取的status标志来判断是什么中断,如果是接收中断,就进入net_rx()。

  • 收包函数处理步骤
    • 1、使用dev_alloc_skb()来构造一个新的sk_buff;
    • 2、使用skb_reserve(rx_skb, 2) 将sk_buff缓冲区里的数据包先向后位移2字节,腾出sk_buff缓冲区里的头部空间;
    • 3、读取网络设备硬件上接收到的数据;
    • 4、使用memcpy()将数据复制到新的sk_buff里的data成员指向的地址处,可以使用skb_put()来动态扩大sk_buff结构体里中的数据区;
    • 5、使用eth_type_trans()来获取上层协议,将返回值赋给sk_buff的protocol成员里;
    • 6、然后更新统计信息,最后使用netif_rx( )来将sk_fuffer传递给上层协议中。
  • skb_put()函数
    • 原型:static inline unsigned char *skb_put(struct sk_buff *skb, unsigned int len);
    • 作用:将数据区向下扩大len字节
    • sk_buff缓冲区变化图:
    • image-20210809232643374

1.5 网卡驱动的注册与注销

网络设备驱动的注册与注销使用成对出现的register_netdev()unregister_netdev()函数完成,这两个函数的原型为:

int register_netdev(struct net_device *dev); 
void unregister_netdev(struct net_device *dev); 

这两个函数都接收一个 net_device 结构体指针为参数, net_device 的生成和成员的赋值并非一定要由工程师逐个亲自动手完成,可以利 用下面的函数帮助我们填充:

struct net_device *alloc_netdev(int sizeof_priv, const char *name,  
                                void(*setup)(struct net_device*));  

struct net_device *alloc_etherdev(int sizeof_priv)
{ 
    /* 以 ether_setup 为 alloc_netdev 的 setup 参数 */ 
    return alloc_netdev(sizeof_priv, "eth%d", ether_setup); 
} 

alloc_netdev()函数生成一个 net_device 结构体,对其成员赋值并返回该结构体的指针。第一个参数为设备私有成员的大小,第二个参数为设备名,第三个参数为 net_device 的 setup()函数指针。setup()函数接收的参数也为 net_device 指针,用 于预置 net_device 成员的值。

alloc_etherdev()是 alloc_netdev()针对以太网的“快捷”函数,其中的ether_setup()是由 Linux 内核提供的一个对以太网设备 net_device 结构体中公有成员 快速赋值的函数

释放 net_device 结构体 的函数为:

void free_netdev(struct net_device *dev);

2、编写虚拟网卡驱动

虚拟网卡驱动,也就是说不需要硬件相关操作,所以就没有中断函数,我们通过linux的ping命令来实现发包,然后在发包函数中伪造一个收的ping包函数,实现能ping通任何ip地址。

2.1 编写步骤

  • init初始函数

    • 1)使用alloc_netdev()来分配一个net_device结构体
    • 2)设置net_device结构体的成员
    • 3)使用register_netdev()来注册net_device结构体
  • 发包函数

    • 1)使用netif_stop_queue()来阻止上层向网络设备驱动层发送数据包
    • 2)调用收包函数,并代入发送的sk_buff缓冲区, 里面来伪造一个收的ping包函数
    • 3)使用dev_kfree_skb()函数来释放发送的sk_buff缓存区
    • 4)更新发送的统计信息
    • 5)使用netif_wake_queue()来唤醒被阻塞的上层
  • 收包函数:修改发送的sk_buff里数据包的数据,使它变为一个接收的sk_buff。

    • 1)需要对调上图的ethhdr结构体 ”源/目的”MAC地址

    • 2)需要对调上图的iphdr结构体”源/目的” IP地址

    • 3)使用ip_fast_csum()来重新获取iphdr结构体的校验码

    • 4)设置上图数据包的数据类型,之前是发送ping包0x08,需要改为0x00,表示接收ping包

    • 5)使用dev_alloc_skb()来构造一个新的sk_buff

    • 6)使用skb_reserve(rx_skb, 2);将sk_buff缓冲区里的数据包先后位移2字节,来腾出sk_buff缓冲区里的头部空间

    • 7)使用memcpy()将之前修改好的sk_buff->data复制到新的sk_buff里的data成员指向的地址处:

    • 8)设置新的sk_buff 其它成员

    • 9)使用eth_type_trans()来获取上层协议,将返回值赋给sk_buff的protocol成员里

    • 10)然后更新接收统计信息,最后使用netif_rx( )来将sk_fuffer传递给上层协议中

  • 具体代码

/*
 * 参考 drivers\net\cs89x0.c
 */

#include <linux/module.h>
#include <linux/errno.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/interrupt.h>
#include <linux/ioport.h>
#include <linux/in.h>
#include <linux/skbuff.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/string.h>
#include <linux/init.h>
#include <linux/bitops.h>
#include <linux/delay.h>
#include <linux/ip.h>

#include <asm/system.h>
#include <asm/io.h>
#include <asm/irq.h>

static struct net_device *vnet_dev;

static void emulator_rx_packet(struct sk_buff *skb, struct net_device *dev)
{
    /* 参考LDD3 */
    unsigned char *type;
    struct iphdr *ih;
    __be32 *saddr, *daddr, tmp;
    unsigned char   tmp_dev_addr[ETH_ALEN];
    struct ethhdr *ethhdr;
    
    struct sk_buff *rx_skb;
        
    /* 1.对调ethhdr结构体的"源/目的"的mac地址 */
    ethhdr = (struct ethhdr *)skb->data;
    memcpy(tmp_dev_addr, ethhdr->h_dest, ETH_ALEN);
    memcpy(ethhdr->h_dest, ethhdr->h_source, ETH_ALEN);
    memcpy(ethhdr->h_source, tmp_dev_addr, ETH_ALEN);

    /* 2.对调iphdr结构体的"源/目的"的ip地址 */    
    ih = (struct iphdr *)(skb->data + sizeof(struct ethhdr));
    saddr = &ih->saddr;
    daddr = &ih->daddr;

    tmp = *saddr;
    *saddr = *daddr;
    *daddr = tmp;
    
    /* 3.使用ip_fast_csum()来重新获取iphdr结构体的校验码*/
    ih->check = 0;         /* and rebuild the checksum (ip needs it) */
    ih->check = ip_fast_csum((unsigned char *)ih,ih->ihl);
    
    /* 4.设置数据类型*/
    type = skb->data + sizeof(struct ethhdr) + sizeof(struct iphdr);
    *type = 0; /*原来0x8表示发送ping包,现在0表示接收ping包 */
    
    /* 5.构造一个新的sk_buff */
    rx_skb = dev_alloc_skb(skb->len + 2);
    
    /* 6.使用skb_reserve腾出2字节头部空间*/
    skb_reserve(rx_skb, 2); /* align IP on 16B boundary */  
    
    /* 7.将之前修改好的sk_buff->data复制到新的sk_buff里 */
    memcpy(skb_put(rx_skb, skb->len), skb->data, skb->len); //用skb_put()扩大sk_buff的数据区,避免溢出

    /* 8.设置新sk_buff的其它成员*/
    rx_skb->dev = dev;
    rx_skb->ip_summed = CHECKSUM_UNNECESSARY; /* don't check it */
    
    /* 9.使用eth_type_trans()来获取上层协议 */
    rx_skb->protocol = eth_type_trans(rx_skb, dev);
    
    /* 10.更新接收统计信息,并向上层传递sk_fuffer收包 */
    dev->stats.rx_packets++;
    dev->stats.rx_bytes += skb->len;
    dev->last_rx = jiffies;     //收包时间戳

    // 提交sk_buff
    netif_rx(rx_skb);
}

static int virt_net_send_packet(struct sk_buff *skb, struct net_device *dev)
{
    static int cnt = 0;
    printk("virt_net_send_packet cnt = %d\n", ++cnt);

    /* 1.停止该网卡的队列,阻止上层向驱动层继续发送数据包 */
    netif_stop_queue(dev); 
    
    /* 2.真实驱动要把skb的数据写入网卡 ,但在此先通过emulator_rx_packet模拟 */
    emulator_rx_packet(skb, dev);   /* 构造一个假的sk_buff,上报 */

    /* 3.释放发送的sk_buff缓存区*/
    dev_kfree_skb (skb);
    
    /* 4.更新统计信息 */
    dev->stats.tx_packets++;
    dev->stats.tx_bytes += skb->len;
    dev->trans_start = jiffies; //发送时间戳
    
    /* 5.数据全部发送出去后,唤醒网卡的队列 (真实网卡应在中断函数里唤醒)*/
    netif_wake_queue(dev); 
    
    return 0;
}


static const struct net_device_ops vnetdev_ops = {
    .ndo_start_xmit     = virt_net_send_packet,
};

static int virt_net_init(void)
{
    /* 1. 分配一个net_device结构体 */
    vnet_dev = alloc_netdev(0, "vnet%d", ether_setup);;  /* alloc_ether_dev */

    /* 2. 设置 */
    //vnet_dev->hard_start_xmit = virt_net_send_packet;
    vnet_dev->netdev_ops    = &vnetdev_ops;

    /* 设置MAC地址 */
    vnet_dev->dev_addr[0] = 0x08;
    vnet_dev->dev_addr[1] = 0x89;
    vnet_dev->dev_addr[2] = 0x66;
    vnet_dev->dev_addr[3] = 0x77;
    vnet_dev->dev_addr[4] = 0x88;
    vnet_dev->dev_addr[5] = 0x99;

    /* 设置下面两项才能ping通 */
    vnet_dev->flags           |= IFF_NOARP;
    vnet_dev->features        |= NETIF_F_IP_CSUM;

    /* 3. 注册 */
    //register_netdevice(vnet_dev);     //编译会出错!
    register_netdev(vnet_dev);
    
    return 0;
}

static void virt_net_exit(void)
{
    unregister_netdev(vnet_dev);
    free_netdev(vnet_dev);
}

module_init(virt_net_init);
module_exit(virt_net_exit);

MODULE_AUTHOR("liangzc1124@163.com");
MODULE_LICENSE("GPL");

2.2 测试

  • 挂载驱动,可以看到net类下就有了这个网卡设备,并尝试ping自己
  • image-20210809233459142

上图的ping之所以成功,并是因为我们在发包函数中伪造了一个来收包,而是因为linux中,ping自己的时候并不调用(不需要)发送函数向网络设备发送sk_buff数据包。

  • ping同网段其它地址
  • image-20210809233646706

ping其它ip地址之所以成功,是因为我们在发包函数中利用emulator_rx_packet函数伪造了一个接收数据包,并通过netif_rx()来将收包上传给上层。

3、移植内核自带的网卡驱动程序

在移植之前,首先我们来看一下mini2440(对应的机器ID为:set machid 7CF)中,是如何支持dm9000网卡的。
进入到入口函数,找到结构体:

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),
};

一般是通过.name这个成员进行匹配的,搜索字符串“dm9000”,找到如下结构体(在平台文件中:arch\arm\mach-s3c24xx\Mach-mini2440.c):

static struct platform_device mini2440_device_eth = {
    .name       = "dm9000",
    .id     = -1,
    .num_resources  = ARRAY_SIZE(mini2440_dm9k_resource),
    .resource   = mini2440_dm9k_resource,
    .dev        = {
        .platform_data  = &mini2440_dm9k_pdata,
    },
};

然后搜索结构体mini2440_device_eth,找到:

static struct platform_device *mini2440_devices[] __initdata = {
    &s3c_device_ohci,
    &s3c_device_wdt,
    &s3c_device_i2c0,
    &s3c_device_rtc,
    &s3c_device_usbgadget,
    &mini2440_device_eth,  //在这里
    &mini2440_led1,
    &mini2440_led2,
    &mini2440_led3,
    &mini2440_led4,
    &mini2440_button_device,
    &s3c_device_nand,
    &s3c_device_sdi,
    &s3c_device_iis,
    &uda1340_codec,
    &mini2440_audio,
    &samsung_asoc_dma,
};

然后再搜索:mini2440_devices,找到:

platform_add_devices(mini2440_devices, ARRAY_SIZE(mini2440_devices));

这就是把结构体mini2440_devices添加到内核,里面的关于网卡的结构在里面,最终匹配驱动程序,就可以使用驱动程序了。
(这就是所谓的平台设备平台驱动的东西了,把可变的东西抽象出来放到平台相关的文件中定义,而我们的驱动程序,基本上是不需要改变的,它是稳定的内容,我们移植的时候,只需要把平台层可变的相关结构体加上,需要修改的资源,进行修改就可以了)。

而我们用的是smdk2440(对应的机器ID为:set machid 16a),然后我在Mach-smdk2440.c中添加以下函数:

/* 以下为liangzc1124@163.com 添加
 * The DM9000 has no eeprom, and it's MAC address is set by
 * the bootloader before starting the kernel.
 */


/* DM9000AEP 10/100 ethernet controller */

#define MACH_SMDK2440_DM9K_BASE (S3C2410_CS4 + 0x300)


static struct resource smdk2440_dm9k_resource[] = {
    [0] = {
        .start = MACH_SMDK2440_DM9K_BASE,
        .end   = MACH_SMDK2440_DM9K_BASE + 3,
        .flags = IORESOURCE_MEM
    },
    [1] = {
        .start = MACH_SMDK2440_DM9K_BASE + 4,
        .end   = MACH_SMDK2440_DM9K_BASE + 7,
        .flags = IORESOURCE_MEM
    },
    [2] = {
        .start = IRQ_EINT7,
        .end   = IRQ_EINT7,
        .flags = IORESOURCE_IRQ | IORESOURCE_IRQ_HIGHEDGE,
    }
};


static struct dm9000_plat_data smdk2440_dm9k_pdata = {
    .flags      = (DM9000_PLATF_16BITONLY | DM9000_PLATF_NO_EEPROM),
};

static struct platform_device smdk2440_device_eth = {
    .name       = "dm9000",
    .id     = -1,
    .num_resources  = ARRAY_SIZE(smdk2440_dm9k_resource),
    .resource   = smdk2440_dm9k_resource,
    .dev        = {
        .platform_data  = &smdk2440_dm9k_pdata,
    },
};

/* 以下为liangzc1124@163.com 添加 */

在结构体smdk2440_devices中添加网卡成员:

static struct platform_device *smdk2440_devices[] __initdata = {
    &s3c_device_ohci,
    &s3c_device_lcd,
    &s3c_device_wdt,
    &s3c_device_i2c0,
    &s3c_device_iis,
    &smdk2440_device_eth, /* lyy:添加 */
};

添加头文件:

#include <linux/dm9000.h>  /* 以下为liangzc1124@163.com 添加*/

然后重新编译内核。成功。烧写新内核:

S3C2440A # nfs 30000000 192.168.1.101:/home/leon/nfs_root/first_fs/uImage;

S3C2440A # bootm 30000000

然后挂载网络文件系统:
mount -t nfs -o nolock 192.168.1.101:/home/leon/nfs_root/first_fs /mnt

成功挂载网络文件系统。

4、自己编写网卡驱动程序

有时候,内核自带的网卡驱动程序比较老,而我们的硬件有可能比较新,那么我们就不能使用内核的网卡驱动程序了,就需要去移植最新的网卡驱动程序,那么这种类型的,又该如何移植呢?

4.1 网络设备驱动程序的模块加载和卸载函数

int xxx_init_module(void) 
{ 
    ... 
    /* 分配 net_device 结构体并对其成员赋值 */ 
    xxx_dev = alloc_netdev(sizeof(struct xxx_priv), "sn%d", xxx_init); 
    if (xxx_dev == NULL) 
    ... /* 分配 net_device 失败 */ 

    /* 注册 net_device 结构体 */ 
    if ((result = register_netdev(xxx_dev))) 
    ... 
} 

void xxx_cleanup(void) 
{ 
    ... 
    /* 注销 net_device 结构体 */ 
    unregister_netdev(xxx_dev); 
    /* 释放 net_device 结构体 */ 
    free_netdev(xxx_dev); 
} 

4.2 网络设备的初始化

网络设备的初始化主要需要完成如下几个方面的工作:

  1. 进行硬件上的准备工作,检查网络设备是否存在,如果存在,则检测设备使用的硬件资源;
  2. 进行软件接口上的准备工作,分配 net_device 结构体并对其数据和函数指针 成员赋值;
  3. 获得设备的私有信息指针并初始化其各成员的值。如果私有信息中包括自旋 锁或信号量等并发或同步机制,则需对其进行初始化。

个网络设备驱动初始化函数的模板如下所示:

void xxx_init(struct net_device *dev) 
{ 
    /*设备的私有信息结构体*/ 
    struct xxx_priv *priv; 
 
    /* 检查设备是否存在、具体硬件配置和设置设备所使用的硬件资源 */ 
    xxx_hw_init(); 
 
    /* 初始化以太网设备的公用成员 */ 
    ether_setup(dev); 
 
    /*设置设备的成员函数指针*/ 
    dev->open = xxx_open; 
    dev->stop = xxx_release; 
    dev->set_config = xxx_config; 
    dev->hard_start_xmit = xxx_tx; 
    dev->do_ioctl = xxx_ioctl; 
    dev->get_stats = xxx_stats; 
    dev->change_mtu = xxx_change_mtu; 
    dev->rebuild_header = xxx_rebuild_header; 
    dev->hard_header = xxx_header; 
    dev->tx_timeout = xxx_tx_timeout; 
    dev->watchdog_timeo = timeout; 
 
    /*如果使用 NAPI,设置 pool 函数*/ 
    if (use_napi) 
    { 
        dev->poll = xxx_poll; 
    } 

    /* 取得私有信息,并初始化它*/ 
    priv = netdev_priv(dev);
    ... /* 初始化设备私有数据区 */ 
} 

4.3 网络设备的打开与释放

网络设备的打开函数需要完成如下工作。

1.  使能设备使用的硬件资源,申请 I/O 区域、中断和 DMA 通道等。

2.  调用 Linux 内核提供的 netif_start_queue()函数,激活设备发送队列。 网络设备的关闭函数需要完成如下工作。

3.  调用 Linux 内核提供的 netif_stop_queue()函数,停止设备传输包。

4. 释放设备所使用的 I/O 区域、中断和 DMA 资源。

Linux 内核提供的 netif_start_queue()和 netif_stop_queue()两个函数的原型为:

void netif_start_queue(struct net_device *dev);  

void netif_stop_queue (struct net_device *dev);

根据以上分析,可得出网络设备打开和释放函数的模板:

int xxx_open(struct net_device *dev) 
{ 
    /* 申请端口、IRQ 等,类似于 fops->open */ 
    ret = request_irq(dev->irq, &xxx_interrupt, 0, dev->name, dev); 
    ... 
    netif_start_queue(dev); 
    ... 
} 
 
int xxx_release(struct net_device *dev) 
{ 
    /* 释放端口、IRQ 等,类似于 fops->close */ 
    free_irq(dev->irq, dev); 
    ... 
    netif_stop_queue(dev); /* can't transmit any more */ 
    ... 
} 

4.3 数据发送流程

(1)网络设备驱动程序从上层协议传递过来的 sk_buff 参数获得数据包的有效数 据和长度,将有效数据放入临时缓冲区。

(2)对于以太网,如果有效数据的长度小于以太网冲突检测所要求数据帧的最小 长度 ETH_ZLEN,则给临时缓冲区的末尾填充 0。

(3)设置硬件的寄存器,驱使网络设备进行数据发送操作。

int xxx_tx(struct sk_buff *skb, struct net_device *dev) 
{ 
    int len; 
    char *data, shortpkt[ETH_ZLEN]; 
    /* 获得有效数据指针和长度 */ 
    data = skb->data; 
    len = skb->len; 
    if (len < ETH_ZLEN) 
    { 
        /* 如果帧长小于以太网帧最小长度,补 0 */ 
    memset(shortpkt, 0, ETH_ZLEN); 
    memcpy(shortpkt, skb->data, skb->len); 
    len = ETH_ZLEN; 
    data = shortpkt; 
    } 
 
    dev->trans_start = jiffies; /* 记录发送时间戳 */ 
 
    /* 设置硬件寄存器让硬件把数据包发送出去 */ 
    xxx_hw_tx(data, len, dev); 
    ... 
} 

当数据传输超时时,意味着当前的发送操作失败,此时,数据包发送超时处理函 数 xxx_tx_ timeout()将被调用。这个函数需要调用 Linux 内核提供的 netif_wake_queue()函数重新启动设备发送队列:

void xxx_tx_timeout(struct net_device *dev) 
{ 
    ... 
    netif_wake_queue(dev); /* 重新启动设备发送队列 */ 
}

4.3 数据接收流程

网络设备接收数据的主要方法是由中断引发设备的中断处理函数,中断处理函数 判断中断类型,如果为接收中断,则读取接收到的数据,分配 sk_buffer 数据结构和数 据缓冲区,将接收到的数据复制到数据缓冲区,并调用 netif_rx()函数将 sk_buffer 传 递给上层协议。完成这一过程的函数模板:

static void xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs) 
{ 
    ... 
    switch (status &ISQ_EVENT_MASK) 
    { 
        case ISQ_RECEIVER_EVENT: /* 获取数据包 */
            xxx_rx(dev); 
            break; 
            /* 其他类型的中断 */ 
    } 
} 

static void xxx_rx(struct xxx_device *dev) 
{ 
    ... 
    length = get_rev_len (...); 
    /* 分配新的套接字缓冲区 */ 
    skb = dev_alloc_skb(length + 2); 
    skb_reserve(skb, 2); /* 对齐 */ 
    skb->dev = dev; 

    /* 读取硬件上接收到的数据 */ 
    insw(ioaddr + RX_FRAME_PORT, skb_put(skb, length), length >> 1); 
    if (length &1) 
        skb->data[length - 1] = inw(ioaddr + RX_FRAME_PORT); 
 
    /* 获取上层协议类型 */ 
    skb->protocol = eth_type_trans(skb, dev); 
 
    /* 把数据包交给上层 */ 
    netif_rx(skb); 
 
    /* 记录接收时间戳 */ 
    dev->last_rx = jiffies; 
    ... 
} 

如果是 NAPI 兼容的设备驱动,则可以通过 poll 方式接收数据包。这种情况下, 我们需要为该设备驱动提供 xxx_poll()函数:

static int xxx_poll(struct net_device *dev, int *budget) 
{ 
    //dev->quota 是当前 CPU 能够从所有接口中接收数据包的最大数目,budget 是在初始化阶段分配给接口的 weight 值
    int npackets = 0, quota = min(dev->quota, *budget); 
    struct sk_buff *skb; 
    struct xxx_priv *priv = netdev_priv(dev); 
    struct xxx_packet *pkt; 

    while (npackets < quota && priv->rx_queue) 
    { 
        /*从队列中取出数据包*/ 
        pkt = xxx_dequeue_buf(dev); 
 
        /*接下来的处理,和中断触发的数据包接收一致*/ 
        skb = dev_alloc_skb(pkt->datalen + 2); 
        if (!skb) 
        { 
            ... 
            continue; 
        } 
        skb_reserve(skb, 2); 
        memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen); 
        skb->dev = dev; 
        skb->protocol = eth_type_trans(skb, dev); 
        /*调用 netif_receive_skb 而不是 net_rx 将数据包交给上层协议
          这里体现出了中断处理机制和轮询机制之间的差别。
         */ 
        netif_receive_skb(skb); 

        /*更改统计数据 */ 
        priv->stats.rx_packets++; 
        priv->stats.rx_bytes += pkt->datalen; 
        xxx_release_buffer(pkt); 
    } 
    /* 网络设备接收缓冲区中的数据包都被读取完了*/ 
    *budget -= npackets; 
    dev->quota -= npackets; 

    if (!priv->rx_queue) 
    { 
        netif_rx_complete(dev); //把当前指定的设备从 poll 队列中清除
        xxx_enable_rx_int (…); /* 再次使能网络设备的接收中断 */ 
        return 0; 
    } 

    return 1; 
} 

虽然 NAPI 兼容的设备驱动以 poll 方式接收数据包,但是仍然需要首次数据包接 收中断来触发 poll 过程。与数据包的中断接收方式不同的是,以轮询方式接收数据包 时,当第一次中断发生后,中断处理程序要禁止设备的数据包接收中断。poll 中断处理函数如下:

static void xxx_poll_interrupt(int irq, void *dev_id, struct pt_regs *regs) 
{ 
    switch (status &ISQ_EVENT_MASK) 
    { 
    case ISQ_RECEIVER_EVENT:
        .../* 获取数据包 */ 
        xxx_disable_rx_int(...); /* 禁止接收中断 */ 
        netif_rx_schedule(dev); 
        break;
    .../* 其他类型的中断 */ 
    } 
} 

上述代码的 netif_rx_schedule()函数被轮询方式驱动的中断程序调用,将设 备的 poll 方法添加到网络层的 poll 处理队列中,排队并且准备接收数据包,最终触发 一个 NET_RX_SOFTIRQ 软中断,通知网络层接收数据包。下图为 NAPI 驱动 程序各部分的调用关系:

image-20220113131412114

4.4 网络连接状态

网络设备驱动程序中往往设置一个定时器来对链路状态进行周期性地检查。当定 时器到期之后,在定时器处理函数中读取物理设备的相关寄存器获得载波状态,从而 更新设备的连接状态。

网络设备驱动可以通过 netif_carrier_on()netif_carrier_off()函数改变或通知内核网络设备的连接状态。此外,函数 netif_carrier_ok() 可用于向调用者返回链路上的载波信号是否存在。

void netif_carrier_on(struct net_device *dev); 
void netif_carrier_off(struct net_device *dev); 
int netif_carrier_ok(struct net_device *dev); 

以下代码显示了网络设备驱动用定时器周期检查链路状态:

static void xxx_timer(unsigned long data) 
{ 
    struct net_device *dev = (struct net_device*)data; 
    u16 link; 
    ...
    if (!(dev->flags &IFF_UP)) 
    { 
        goto set_timer; 
    }
    /* 获得物理上的连接状态 */ 
    if (link = xxx_chk_link(dev)) //读取网络适配器硬件的相关寄存器以获得链路连接状态
    { 
        if (!(dev->flags &IFF_RUNNING)) 
        { 
            netif_carrier_on(dev); 
            dev->flags |= IFF_RUNNING; 
            printk(KERN_DEBUG "%s: link up\n", dev->name); 
        } 
    } 
    else    
    { 
        if (dev->flags &IFF_RUNNING) 
        { 
            netif_carrier_off(dev); 
            dev->flags &= ~IFF_RUNNING; 
            printk(KERN_DEBUG "%s: link down\n", dev->name); 
        } 
    } 
 
    set_timer: 
    priv->timer.expires = jiffies + 1 * HZ; 
    priv->timer.data = (unsigned long)dev; 
    priv->timer.function = &xxx_timer; /* timer handler */ 
    add_timer(&priv->timer); 
} 

从上述源代码还可以看出,定时器处理函数会不停地利用第 31~35 行代 码启动新的定时器以实现周期检测的目的。那么最初启动定时器的地方在哪里呢?很 显然,它最适合在设备的打开函数中完成:

 static int xxx_open(struct net_device *dev) 
{ 
    struct xxx_priv *priv = (struct xxx_priv*)dev->priv; 
 
    ... 
    priv->timer.expires = jiffies + 3 * HZ; 
    priv->timer.data = (unsigned long)dev; 
    priv->timer.function = &xxx_timer; /* timer handler */ 
    add_timer(&priv->timer); 
    ...
}

5. CS8900 网卡设备驱动实例分析

当 CS8900 处于 I/O 模式下时(这里所说的 CS8900 处于 I/O 模式并非意味着它一定位于 CPU 的 I/O 空间,实 际上,CS8900 I/O 模式下的寄存器仍然映射 ARM 处理器的内存空间。因此, 我们直接通过读/写寄存器地址ioremap()之后的虚拟地址即可),可以通过以 下几个 PacketPage 空间内的寄存器来控制 CS8900 的行为(括号内给出的是寄存器地 址相对于 PacketPage 基地址的偏移):

寄存器 作用
LINECTL(0112H) 决定 CS8900 的基本配置和物理接口,可选择使用 10BASE-T 接口、AUI 接口或者自动选择。
RXCTL(0104H) 控制 CS8900 接收特定数据包,控制是否接收多播、广播和单播包。
RXCFG(0102H) RXCFG 控制 CS8900 接收到特定数据包后引发接收中断,并控制是否使用接收 DMA 和 CRC 校验。
BUSCT(0116H) BUSCT 可控制芯片的工作模式、DMA 方式、是否使能外部中断引脚。
BUSST(0138H) 标志网络设备的发送状态,如设备是否准备好发送。
ISQ(0120H) 网卡芯片的中断状态寄存器

在 I/O 模式下,CS8900 发送数据包的步骤如下:

(1)向控制寄存器 TXCMD 寄存器写入发送命令write_reg(TXCMD, send_cmd);

(2)将发送数据长度写入 TXLENG 寄存器write_reg(TXLENG, send_len)

(3)读取 PacketPage 空间内的 BUSST 寄存器,确定其第 8 位被设置为 Rdy4TxNOW,即设备处于准备发送状态reg(BusST)&0x100

(4)将要发送的数据循环写入 PORT0 寄存器write_reg(PORT0, data)

(5)将数据组织为以太网帧并添加填充位和 CRC 校验信息,然后将数据转化为比特流传送到网络媒介。

在 I/O 模式下,CS8900 接收数据包的方法如下:

(1)接收到网络适配器产生的中断,查询相对于 I/O 基地址偏移 0008H 中断状态 队列端口,判断中断类型为接收中断。

(2)读 PORT0 寄存器依次获得接收状态 rxStatus、接收数据长度 rxLength。

(3)循环继续对 PORT0 寄存器读取 rxLength 次,获得整个数据包。

(4)驱动程序进行数据包处理并传递给上层。

对于一种网络设备的驱动而言,工程师主要需完成设备驱动功能层的设计。在 16.2~ 16.8节已经给出了设备驱动功能层主要数据结构和函数的设计模板,因此,在编写CS8900 的这些数据结构和函数时,实际要完成的工作就是用具体的针对 CS8900 的操作来填充模 板,具体包括以下工作:

  1. 填充 CS8900 的私有信息结构体,把 CS8900 特定的数据放入这个私有结构体中。在 CS8900 驱动程序中,这个数据结构为 struct net_local

    在 CS8900 的设备驱动程序中,核心数据结构 net_device 以全局变量的方式定义, 其数个成员的初始值都被置为空,私有信息结构体为 net_local:

    static struct net_device dev_cs89x0 = 
    { 
     "", 
         0, 0, 0, 0, 
         0, 0, 
         0, 0, 0, NULL, NULL 
    };
    
    struct net_local 
    { 
     struct net_device_stats stats; /* 网络设备状态结构体 */ 
     int chip_type; /* 区分芯片类型:CS89x0 */ 
     char chip_revision; /* 芯片版本字母,如"A" */ 
     int send_cmd; /* 发送命令: TX_NOW, TX_AFTER_381 或 TX_AFTER_ALL */ 
     ... 
        spinlock_t lock; /* 并发控制自旋锁 */ 
    }; 
    

    当芯片的版本字母不同时,net_local 结构体中记录的 send_cmd 命令将不同。例如, 同样是 CS8900 网卡,若芯片版本字母为大于等于“F”,则发送命令为 TX_NOW,而 对于 CS8900A,发送命令为 TX_AFTER_ALL。

  2. 填充设备初始化模板,初始化 net_device 结构体,将其注册入内核。net_device 的注册与注销在模块加载与注销函数中完成。在 CS8900 驱动程序中,与此相关的函数有:

    struct net_device * _ _init cs89x0_probe(int unit);
    
    int cs89x0_probe1(struct net_device *dev, int ioaddr, int modular); 
    
    int init_module(void);  
    
    void cleanup_module(void); 
    

    设备的初始化由 net_device 结构体中的 init()函数完成,这个函数将在 net_device 被注册时自动被调用。init()函数在 CS8900 网卡的驱动程序中对应 于 cs89x0_probe()函数:

    int __init cs89x0_probe(struct net_device *dev) 
    { 
     int i; 
     
     SET_MODULE_OWNER(dev); 
     DPRINTK(1, "cs89x0:cs89x0_probe(0x%x)\n", base_addr); 
    
     BWSCON = (BWSCON & ~(BWSCON_ST3 | BWSCON_WS3 | BWSCON_DW3)) | 
                 (BWSCON_ST3 | BWSCON_WS3 | BWSCON_DW(3, BWSCON_DW_16)); 
     BANKCON3= BANKCON_Tacs0 | BANKCON_Tcos4 | BANKCON_Tacc14 | 
                 BANKCON_Toch1 | BANKCON_Tcah4 | BANKCON_Tacp6 | BANKCON_PMC1; 
     
     set_external_irq(IRQ_CS8900, EXT_RISING_EDGE, GPIO_PULLUP_DIS); 
    
         for (i = 0; netcard_portlist[i]; i++) 
        { 
             if (cs89x0_probe1(dev, netcard_portlist[i]) == 0) //验证网卡的存在,并获取 CS8900所使用的硬件资源
             return 0; 
     } 
     printk(KERN_WARNING "cs89x0: no cs8900 or cs8920 detected." 
           "Be sure to disable PnP with SETUP\n"); 
     return -ENODEV; 
    }
    
    
    static unsigned int netcard_portlist[] __initdata = 
    { 
        vCS8900_BASE + 0x300,    //假设硬件平台中网卡的基地址为 vCS8900_BASE + 0x300
        0
    }; 
    
    /*
     *上述 cs89x0_probe1()函数的流程如下。
     *(1)第 8~20 行分配设备的私有信息结构体内存并初始化,若分配失败,则直接跳入第 78 行的代码返回。
     *(2)第 24~26 行从寄存器中读取芯片的具体类型。
     *(3)第 27~32 行判断芯片类型,若不是 CS8900 则直接跳入第 77 行的代码,释放私有信息结构体并返回。
     *(4)当芯片类型为 CS8900 时,第 34~69 行完成 net_device 设备结构体的初始化,赋值其属性和函数指针。
     */
    
    static int __init cs89x0_probe1(struct net_device *dev, int ioaddr)
    { 
     struct net_local *lp; 
     unsigned rev_type = 0; 
     int ret; 
     
         /* 初始化设备结构体私有信息 */ 
         if (dev->priv == NULL) 
     { 
             dev->priv = kmalloc(sizeof(struct net_local), GFP_KERNEL); 
         if (dev->priv == 0) 
         { 
             ret = - ENOMEM; 
             goto before_kmalloc; 
         } 
     lp = (struct net_local*)dev->priv; 
     memset(lp, 0, sizeof(*lp)); 
     spin_lock_init(&lp->lock); 
     } 
     lp = (struct net_local*)dev->priv; 
    
     dev->base_addr = ioaddr; 
     /* 读取芯片类型 */ 
     rev_type = readreg(dev, PRODUCT_ID_ADD); 
     lp->chip_type = rev_type &~REVISON_BITS;
         lp->chip_revision = ((rev_type &REVISON_BITS) >> 8) + 'A';30 
     if (lp->chip_type != CS8900) 
     { 
         printk(_ _FILE_ _ ": wrong device driver!\n"); 
         ret = - ENODEV; 
         goto after_kmalloc;
     } 
     /* 根据芯片类型和版本确定正确的发送命令 */ 
     lp->send_cmd = TX_AFTER_ALL; 
     if (lp->chip_type == CS8900 && lp->chip_revision >= 'F') 
         lp->send_cmd = TX_NOW; 
    
     reset_chip(dev); 
    
     lp->adapter_cnf = A_CNF_10B_T | A_CNF_MEDIA_10B_T; 
     lp->auto_neg_cnf = EE_AUTO_NEG_ENABLE;
     printk(KERN_INFO "cs89x0 media %s%s", (lp->adapter_cnf &A_CNF_10B_T) ? "RJ-45": "", (lp->adapter_cnf &A_CNF_AUI) ? "AUI" : ""); 
    
         /* 设置 CS8900 的 MAC 地址 */ 
     dev->dev_addr[0] = 0x00; 
     dev->dev_addr[1] = 0x00; 
     dev->dev_addr[2] = 0xc0; 
     dev->dev_addr[3] = 0xff; 
     dev->dev_addr[4] = 0xee; 
     dev->dev_addr[5] = 0x08; 
         set_mac_address(dev, dev->dev_addr); 
     
     /* 设置设备中断号 */ 
     dev->irq = IRQ_LAN; 
     printk(", IRQ %d", dev->irq); 
     
     /* 填充设备结构体的成员函数指针 */ 
     dev->open = net_open; 
     dev->stop = net_close; 
     dev->tx_timeout = net_timeout; 
     dev->watchdog_timeo = 3 * HZ; 
     dev->hard_start_xmit = net_send_packet; 
     dev->get_stats = net_get_stats; 
     dev->set_multicast_list = set_multicast_list; 
     dev->set_mac_address = set_mac_address; 
     
     /* 填充以太网公用数据和函数指针 */ 
     ether_setup(dev); 
    
     printk("\n"); 
     DPRINTK(1, "cs89x0_probe1() successful\n"); 
     return 0;
         after_kmalloc: kfree(dev->priv); 
     before_kmalloc: return ret; 
    } 
    
    
    static int __init init_cs8900a_s3c2410(void) 
    { 
     struct net_local *lp; 
     int ret = 0; 
     
     dev_cs89x0.irq = irq; 
     dev_cs89x0.base_addr = io; 
     dev_cs89x0.init = cs89x0_probe; //在使用 register_netdev()函数注net_device 设备结构体时,cs89x0_probe()函数会被自动调用以完成 net_device 结构体的初始化。
     dev_cs89x0.priv = kmalloc(sizeof(struct net_local), GFP_KERNEL); 
     if (dev_cs89x0.priv == 0) 
     { 
         printk(KERN_ERR "cs89x0.c: Out of memory.\n"); 
         return - ENOMEM; 
     } 
     memset(dev_cs89x0.priv, 0, sizeof(struct net_local)); 
     lp = (struct net_local*)dev_cs89x0.priv; 
        
         //为 CS8900 网卡申请了 NETCARD_IO_EXTENT大小的I/O 地址区域
     request_region(dev_cs89x0.base_addr, NETCARD_IO_EXTENT, "cs8900a");
        spin_lock_init(&lp->lock); 
     
     /* 设置物理接口的正确类型*/ 
     if (!strcmp(media, "rj45")) 
     lp->adapter_cnf = A_CNF_MEDIA_10B_T | A_CNF_10B_T; 
     else if (!strcmp(media, "aui")) 
         lp->adapter_cnf = A_CNF_MEDIA_AUI | A_CNF_AUI; 
     else if (!strcmp(media, "bnc")) 
         lp->adapter_cnf = A_CNF_MEDIA_10B_2 | A_CNF_10B_2; 
     else 
         lp->adapter_cnf = A_CNF_MEDIA_10B_T | A_CNF_10B_T; 
     
     if (duplex == - 1) 
         lp->auto_neg_cnf = AUTO_NEG_ENABLE; 
     
     if (io == 0) 
     { 
         printk(KERN_ERR "cs89x0.c: Module autoprobing not allowed.\n"); 
         printk(KERN_ERR "cs89x0.c: Append io=0xNNN\n"); 
         ret = - EPERM; 
         goto out; 
     } 
         //net_device 设备结构体的注册
     if (register_netdev(&dev_cs89x0) != 0) 
     { 
         printk(KERN_ERR "cs89x0.c: No card found at 0x%x\n", io); 
         ret = - ENXIO; 
         goto out; 
     } 
    out: if (ret) 
         kfree(dev_cs89x0.priv); 
     return ret; 
    } 
    
    
    static void _ _exit cleanup_cs8900a_s3c2410(void) 
    { 
     if (dev_cs89x0.priv != NULL) 
     { 
         /* 释放私有信息结构体 */ 
         unregister_netdev(&dev_cs89x0); 
         outw(PP_ChipID, dev_cs89x0.base_addr + ADD_PORT); 
         kfree(dev_cs89x0.priv); 
         dev_cs89x0.priv = NULL; 
         /* 释放 CS8900 申请的 I/O 地址区域 */ 
            release_region(dev_cs89x0.base_addr, NETCARD_IO_EXTENT); 
     } 
    } 
    

    上述函数第 8~11 行设置 S3C2410A ARM 处理器的片选,第 13行设置 ARM 与 CS8900 网卡对应的中断,第 15~18 行循环检索 netcard_portlist[ ]数组中定义的基地址处 是否存在 CS8900 网卡

  3. 填充设备发送数据包函数模板,把真实的数据包发送硬件操作填充入 xxx_tx() 函数,并填充发送超时函数 xxx_tx_timeout()。当发送数据包超时时,CS8900 驱动程序的数据包发送超时函数将被调用,它重 新启动设备发送队列。在初始化函数中,CS8900 的数据包发送函数指针 hard_ start_xmit被赋值为 CS8900 驱动程序中的 net_send_packet(),这个函数完成硬件发送序列。具体代码如下:

    static int net_send_packet(struct sk_buff *skb, struct net_device  *dev)
    {
        struct net_local *lp = (struct net_local*)dev->priv; 
     
        writereg(dev, PP_BusCTL, 0x0); 
        writereg(dev, PP_BusCTL, readreg(dev, PP_BusCTL) | ENABLE_IRQ); 
     
        spin_lock_irq(&lp->lock);/* 使用自旋锁阻止多个数据包被同时写入硬件*/ 
        netif_stop_queue(dev); 
     
        /* 初始化硬件发送序列 */ 
        writeword(dev, TX_CMD_PORT, lp->send_cmd); 
        writeword(dev, TX_LEN_PORT, skb->len); 
     
        /* 检测硬件是否处于发送 READY 状态 */ 
        if ((readreg(dev, PP_BusST) &READY_FOR_TX_NOW) == 0) 
        { 
            spin_unlock_irq(&lp->lock); 
            DPRINTK(1, "cs89x0: Tx buffer not free!\n"); 
            return 1; 
        } 
     
        writeblock(dev, skb->data, skb->len);   /* 将数据写入硬件 */ 
     
        spin_unlock_irq(&lp->lock);     /* 解锁自旋锁 */ 
        dev->trans_start = jiffies;     /* 记录发送开始的时间戳 */ 
        dev_kfree_skb(skb);             /* 释放 sk_buff 和数据缓冲区 */ 
     
        return 0; 
    }
    
    static void net_timeout(struct net_device *dev)
    { 
        DPRINTK(1, "%s: transmit timed out, %s?\n", dev->name,
            tx_done(dev) ? "IRQ conflict ?" : "network cable problem"); 
     
        net_close(dev); //停止网卡
        writereg(dev, PP_SelfCTL, readreg(dev, PP_SelfCTL) | POWER_ON_RESET); //网卡硬复位
        net_open(dev); //再次启动网卡
    } 
    
  1. 填充设备驱动程序的中断处理程序 xxx_interrupt()和具体的数据包接收函数 xxx_rx(),填入真实的硬件操作。在 CS8900 驱动程序中,与此相关的函数有:

    irqreturn_t net_interrupt(int irq, void *dev_id, struct pt_regs *regs)
    { 
     struct net_device *dev = dev_id; 
     struct net_local *lp; 
     int ioaddr, status; 
     
     ioaddr = dev->base_addr; 
     lp = (struct net_local*)dev->priv; 
     
     /* 读取中断事件类型 */ 
     while ((status = readword(dev, ISQ_PORT))) 
     { 
         DPRINTK(4, "%s: event=%04x\n", dev->name, status); 
         switch (status &ISQ_EVENT_MASK) 
         { 
             case ISQ_RECEIVER_EVENT: 
                 /* 获得数据包 */ 
                 net_rx(dev); 
                 break; 
             ... /* 其他类型中断 */ 
         } 
     } 
    } 
    
    
    static void net_rx(struct net_device *dev)
    {
        struct net_local *lp = (struct net_local*)dev->priv; 
     struct sk_buff *skb; 
     int status, length; 
     
     int ioaddr = dev->base_addr; 
     
     status = inw(ioaddr + RX_FRAME_PORT); 
     if ((status &RX_OK) == 0) 
     { 
         count_rx_errors(status, lp); 
         return ; 
     } 
     
     length = inw(ioaddr + RX_FRAME_PORT);/* 读取接收数据包的长度 */ 
     
     /* 分配新的套接字缓冲区和数据缓冲区 */ 
     skb = dev_alloc_skb(length + 2); 
     if (skb == NULL) 
     { 
         lp->stats.rx_dropped++; /* 分配失败,统计被丢弃的包数 */ 
         return ; 
     } 
     skb_reserve(skb, 2); 
     skb->len = length; 
     skb->dev = dev;
     readblock(dev, skb->data, skb->len); /* 从硬件中读取数据包放入数据缓冲区 */
        skb->protocol = eth_type_trans(skb, dev);/* 解析收到数据包的网络层协议类型 */ 
     
     netif_rx(skb); /* 传递给上层协议 */ 
     
     dev->last_rx = jiffies; /* 记录最后收到数据包的时间戳 */ 
     /* 统计接收数据包数和接收字节数 */ 
     lp->stats.rx_packets++; 
     lp->stats.rx_bytes += length; 
    } 
    
  1. 填充设备打开 xxx_open()与释放 xxx_release()函数代码。在 CS8900 驱动程序 中,与此相关的函数有:

    int net_open(struct net_device *dev);  
    
    int net_close(struct net_device *dev);
    
  1. 填充设备配置与数据统计的具体代码,填充返回设备冲突的 xxx_stats()函数。

6. 网卡驱动移植一般步骤

拿到一块新的网卡,一般厂家会有自带的驱动程序给你,你所要做的就是以下几个事情:

  1. 根据网卡与开发板的连接方式确定网卡的内存映射地址iobase,也即确定网卡的片选信号所连接的CPU内存的哪一个bank(nGCS?),然后根据网卡内存的大小,在网卡驱动的初始化函数中调用ioremap()进行地址重映射;
  2. 根据网卡与开发板的硬件连接图确定中断号,并在初始化函数中利于request_irq()函数,向内核申请中断(确定中断触发方式、中断处理函数等);
  3. 根据网卡datasheet查看网卡的读写时序和位宽参数,设置开发板相应的内存控制寄存器BWSCON和BANKCON*。
  4. 将它拷贝到内核源代码的相关目录并修改该目录下的Makefile文件以添加修改后的网卡驱动目标文件。假设我们已经改好的网卡驱动程序为:dm9dev9000c.c,编译也没有错误。
cp dm9dev9000c.c /home/leon/linux-3.4.2/drivers/net/ethernet/davicom/

修改该目录Makefile文件:

#
# Makefile for the Davicom device drivers.
#

#obj-$(CONFIG_DM9000) += dm9000.o
obj-$(CONFIG_DM9000) += dm9dev9000c.o
  1. 重新编译内核,烧写新的uImage文件到开发板中,看看是否可以挂载网络根文件系统或者可以设置IP地址及ping通网络。如果可以成功挂载网络根文件系统,所以网卡移植是成功的。

    nfs 30000000 192.168.1.101:/work/nfs_root/uImage_net_new; 
    
    bootm 30000000
        
    mount -t nfs -o nolock,vers=2 192.168.1.101:/work/nfs_root/fs_mini_mdev_new /mnt
    
  • 我们也可以设置开机直接挂载网络根文件系统,这样就可以直接开机启动网络根文件系统了。

    • uboot中设置:

      set bootargs console=ttySAC0,115200 root=/dev/nfs nfsroot=192.168.1.101:/home/leon/nfs_root/first_fs ip=192.168.1.50:192.168.1.101:192.168.1.1:255.255.255.0::eth0:off
          
      save
      
      tftp 30000000 uImage
      
      bootm 30000000
      

ip=192.168.1.50:为单板ip,

192.168.1.101:为服务器ip,

192.168.1.1为网关,

255.255.255.0为子网掩码

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容