⑤重启网卡设备
重启网卡设备是初始化网卡设备的一个重要部分,它的原理就是向寄存器中写入命令就可以了(注意这里写寄存器,而不是配置空间,因为跟PCI没有什么关系),代码如下:
writeb ((readb(ioaddr+ChipCmd) & ChipCmdClear) | CmdReset,ioaddr+ChipCmd);
是我们看到第二参数ioaddr+ChipCmd,ChipCmd是一个位移,使地址刚好对应的就是ChipCmd哪个寄存器,读者可以查阅官方 datasheet得到这个位移量,我们在程序中定义的这个值为:ChipCmd = 0x37;与datasheet是吻合的。我们把这个命令寄存器中相应位(RESET)置1就可以完成操作。
⑥获得MAC地址,并把它存储到net_device中。
for(i = 0; i < 6; i++) { /* Hardware Address */
dev->dev_addr[i] = readb(ioaddr+i);
dev->broadcast[i] = 0xff;
}
我们可以看到读的地址是ioaddr+0到ioaddr+5,读者查看官方datasheet会发现寄存器地址空间的开头6个字节正好存的是这个网卡设备的MAC地址,MAC地址是网络中标识网卡的物理地址,这个地址在今后的收发数据包时会用的上。
⑦向net_device中登记一些主要的函数
dev->open = rtl8139_open;
dev->hard_start_xmit = rtl8139_start_xmit;
dev->stop = rtl8139_close;
由于dev(net_device)代表着设备,把这些函数注册完后,rtl8139_open就是用于打开这个设备, rtl8139_start_xmit就是当应用程序要通过这个设备往外面发数据时被调用,具体的其实这个函数是在网络协议层中调用的,这就涉及到 Linux网络协议栈的内容,不再我们讨论之列,我们只是负责实现它。rtl8139_close用来关掉这个设备。
好了,到此我们把 rtl8139_init_one函数介绍完了,初始化个设备完了之后呢,我们通过ifconfig eth0 up命令来把我们的设备激活。这个命令直接导致了我们刚刚注册的rtl8139_open的调用。这个函数激活了设备。这个函数主要做了三件事。
①注册这个设备的中断处理函数。当网卡发送数据完成或者接收到数据时,是用中断的形式来告知的,比如有数据从网线传来,中断也通知了我们,那么必须要有一个处理这个中断的函数来完成数据的接收。关于Linux的中断机制不是我们详细讲解的范畴,有兴趣的可以参考《Linux内核源代码情景分析》,但是有个非常重要的资源我们必须注意,那就是中断号的分配,和内存地址映射一样,中断号也是BIOS在初始化阶段分配并写入设备的配置空间的,然后Linux在建立pci_dev时从配置空间读出这个中断号然后写入pci_dev的irq成员中,所以我们注册中断程序需要中断号就是直接从pci_dev里取就可以了。
retval = request_irq (dev->irq, rtl8139_interrupt, SA_SHIRQ, dev->name, dev);
if (retval) {
return retval;
}
我们注册的中断处理函数是rtl8139_interrupt,也就是说当网卡发生中断(如数据到达)时,中断控制器8259A把中断号发给CPU, CPU根据这个中断号找到处理程序,这里就是rtl8139_interrupt,然后执行。rtl8139_interrupt也是在我们的程序中定义好了的,这是驱动程序的一个重要的义务,也是一个基本的功能。request_irq 的代码在arch/i386/kernel/irq.c中。
②分配发送和接收的缓存空间
根据官方文档,发送一个数据包的过程是这样的:先从应用程序中把数据包拷贝到一段连续的内存中(这段内存就是我们这里要分配的缓存),然后把这段内存的地址写进网卡的数据发送地址寄存器(TSAD)中,这个寄存器的偏移量是TxAddr0 = 0x20。在把这个数据包的长度写进另一个寄存器(TSD)中,它的偏移量是TxStatus0 = 0x10。然后就把这段内存的数据发送到网卡内部的发送缓冲中(FIFO),最后由这个发送缓冲区把数据发送到网线上。
好了现在创建这么一个发送和接收缓冲内存的目的已经很显然了。
tp->tx_bufs = pci_alloc_consistent(tp->pci_dev, TX_BUF_TOT_LEN,
&tp->tx_bufs_dma);
tp->rx_ring = pci_alloc_consistent(tp->pci_dev, RX_BUF_TOT_LEN,
&tp->rx_ring_dma);
tp 是net_device的priv的指针,tx_bufs是发送缓冲内存的首地址,rx_ring是接收缓存内存的首地址,他们都是虚拟地址,而最后一个参数tx_bufs_dma和rx_ring_dma均是这一段内存的物理地址。为什么同一个事物,既用虚拟地址来表示它还要用物理地址呢,是这样的, CPU执行程序用到这个地址时,用虚拟地址,而网卡设备向这些内存中存取数据时用的是物理地址(因为网卡相对CPU属于头脑比较简单型的)。 pci_alloc_consistent的代码在Linux/arch/i386/kernel/pci-dma.c中。
③发送和接收缓冲区初始化和网卡开始工作的操作
RTL8139有4个发送描述符(包括4个发送缓冲区的基地址寄存器(TSAD0-TSAD3)和4个发送状态寄存器(TSD0-TSD3)。也就是说我们分配的缓冲区要分成四个等分并把这四个空间的地址都写到相关寄存器里去,下面这段代码完成了这个操作。
for (i = 0; i < NUM_TX_DESC; i++)
((struct rtl8139_private*)dev->priv)->tx_buf[i] =
&((struct rtl8139_private*)dev->priv)->tx_bufs[i * TX_BUF_SIZE];
上面这段代码负责把发送缓冲区虚拟空间进行了分割。
for (i = 0; i < NUM_TX_DESC; i++)
{
writel(tp->tx_bufs_dma+(tp->tx_buf[i]tp->tx_bufs),ioaddr+TxAddr0+(i*4));
readl(ioaddr+TxAddr0+(i * 4));
}
上面这段代码负责把发送缓冲区物理空间进行了分割,并把它写到了相关寄存器中,这样在网卡开始工作后就能够迅速定位和找到这些内存并存取他们的数据。
writel(tp->rx_ring_dma,ioaddr+RxBuf);
上面这行代码是把接收缓冲区的物理地址写到了相关寄存器中,这样网卡接收到数据后就能准确的把数据从网卡中搬运到这些内存空间中,等待CPU来领走他们。
writeb((readb(ioaddr+ChipCmd) & ChipCmdClear) |
CmdRxEnb | CmdTxEnb,ioaddr+ChipCmd);
重新RESET设备后,我们要激活设备的发送和接收的功能,上面这行代码就是向相关寄存器中写入相应值,激活了设备的这些功能。
writel ((TX_DMA_BURST << TxDMAShift),ioaddr+TxConfig);
上面这行代码是向网卡的TxConfig (位移是0x44)寄存器中写入TX_DMA_BURST << TxDMAShift这个值,翻译过来就是6<<8,就是把第8到第10这三位置成110,查阅管法文档发现6就是110代表着一次DMA的数据量为1024字节。
另外在这个阶段设置了接收数据的模式,和开启中断等等,限于篇幅由读者自行研究。
下面进入数据收发阶段:
当一个网络应用程序要向网络发送数据时,它要利用Linux的网络协议栈来解决一系列问题,找到网卡设备的代表net_device,由这个结构来找到并控制这个网卡设备来完成数据包的发送,具体是调用net_device的hard_start_xmit成员函数,这是一个函数指针,在我们的驱动程序里它指向的是 rtl8139_start_xmit,正是由它来完成我们的发送工作的,下面我们就来剖析这个函数。它一共做了四件事。
①检查这个要发送的数据包的长度,如果它达不到以太网帧的长度,必须采取措施进行填充。
if( skb->len < ETH_ZLEN ){//if data_len < 60
if( (skb->data + ETH_ZLEN) <= skb->end ){
memset( skb->data + skb->len, 0x20, (ETH_ZLEN - skb->len) );
skb->len = (skb->len >= ETH_ZLEN) ? skb->len : ETH_ZLEN;}
else{
printk("%s:(skb->data+ETH_ZLEN) > skb->end\n",__FUNCTION__);
}
}
skb->data和skb->end就决定了这个包的内容,如果这个包本身总共的长度(skb->end- skb->data)都达不到要求,那么想填也没地方填,就出错返回了,否则的话就填上。
②把包的数据拷贝到我们已经建立好的发送缓存中。
memcpy (tp->tx_buf[entry], skb->data, skb->len);
其中skb->data就是数据包数据的地址,而tp->tx_buf[entry]就是我们的发送缓存地址,这样就完成了拷贝,忘记了这些内容的回头看看前面的介绍。
③光有了地址和数据还不行,我们要让网卡知道这个包的长度,才能保证数据不多不少精确的从缓存中截取出来搬运到网卡中去,这是靠写发送状态寄存器(TSD)来完成的。
writel(tp->tx_flag | (skb->len >= ETH_ZLEN ? skb->len : ETH_ZLEN),ioaddr+TxStatus0+(entry * 4));
我们把这个包的长度和一些控制信息一起写进了状态寄存器,使网卡的工作有了依据。
