电脑技术学习

freebsd网卡驱动程序详解 (1)

dn001
/* 注释:xIE_minix */
/*此处为BSD申明,略过... 最好是拷贝下来用C的开发工具来看比较好 :) */
/*
当网络上一台计算机准备发送数据时,他的网卡开始工作了,首先网卡的芯片侦听在网络上是否有数据在
流动,如果没有,他就把数据发送到网络上,在侦听和发送之间有一段极小的时间延迟,在这段时间内,也有
可能在网络上有其他的计算机也准备发送数据,也侦听到网络上没有数据在流动,这就可能两台甚至多台
的数据一起发送到网络上,产生数据的碰撞,发送数据的计算机的网卡芯片当然要在发送完成后再校验返回
的数据,如果发现和发送的数据不一致,那就是说产生了碰撞,所以在一个以太网络中的计算机数量不宜过多,
他不但会增加广播包在网络中的数量,也请也会增加数据包的碰撞次数.
我们的计算机的网卡芯片在接收到一完整的数据包后,芯片的一引脚通知8259中断控制器,中断控制器再
发出中断给CPU,由此,CPU随即调用该网卡的中断例程,如:
Dos是这样的
屏蔽所有中断(cli)
push any register
因为中断向量在段0
所以xor ax,ax
mov ds,ax
mul ax,中断号
那么在数据段的[ax]偏移处是该中断例程的指针了
call [ax]就到该中断例程了
...(DOS是比较遥远的事情了,我所描述的是他的原理,当然不会这么简单,如果那位网友有兴趣详细描述一下
上面的原理,纠正或替换掉我所写的就感激不尽了)

总之,在本例程中,CPU将调用elintr中断例程,并带有参数unit即该种网卡的第几块(因为在计算机中,你有可能
装了相同的网卡有几块),elintr的作用是把数据从网卡的数据存储器中读到我们在该网卡初始化时预先分配好
的数据缓冲区中,他调用的函数就只有elread,同样elread也只调用了elget一个函数.elread函数比较简单,就是
调用elget,elget则相对比较复杂一点,涉及到核心内存分配mbuf,mbuf是比较恐怖的东西,正如STEVEN所写的,为
了节约当时"巨大"的4M内存,牺牲了性能搞出了这个mbuf东东,mbuf是必须要弄懂的,虽然在设备驱动程序中调用
他的宏和函数不多,但在后面的IP协议,TCP协议中有不少涉及的地方.
关于数据发送方面和接收差不多,在上层协议放置好数据到mbuf链后,调用el_start函数,该函数把mbuf链中
的数据放置到本块网卡的发送队列缓冲el_pktbuf中,然后再调用el_xmit函数,此函数把发送队列缓冲el_pktbuf
中的数据有传递到网卡的数据存储器中.我认为,这中间的内存拷贝是多于的,应该在el_start函数中直接把mbuf
中的数据传递到网卡的数据存储器中,这样会使性能有较大幅度的提升,因为在驱动程序设计时,最好减少大量的
内存拷贝,他占用的时间太多了.
*/

/* FreeBSD的3COM以太网设备驱动程序 */
/*本段头文件是在编译核心时产生的*/



#include "el.h" /*此三文件为编译时产生的头文件,内容是定制核心的一些常量*/
#include "opt_inet.h"
#include "opt_ipx.h"

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/sockio.h>
#include <sys/mbuf.h>
#include <sys/socket.h>
#include <sys/syslog.h>

#include <net/ethernet.h>
#include <net/if.h>

#include <netinet/in.h>
#include <netinet/if_ether.h>

#include <net/bpf.h>

#include <Machine/clock.h>

#include <i386/isa/isa_device.h>
#include <i386/isa/if_elreg.h>/*此头文件是3COM卡的寄存器常量*/

/* 为了调试方便 */
#ifdef EL_DEBUG
#define dprintf(x) printf x /*如果定义了DEBUG调试,则打印到屏幕*/
#else
#define dprintf(x)
#endif

/* softc结构,每种网卡的该结构是不同的,主要是该第一个成员必须是一以太网的共用结构arpcom*/
static struct el_softc {
struct arpcom arpcom; /* 以太网公共部分 */
u_short el_base; /* 基本输入输出地址 */
char el_pktbuf[EL_BUFSIZ]; /* 帧缓冲大小2048 */
} el_softc[NEL]; /*NEL在el.h中定义,即编译时产生的头文件,意思为支持的网卡数*/
/*
看看arpcom结构吧

* 该结构是以太网设备驱动程序和ARP程序所共享.

struct arpcom {
/*
* ifnet 结构必须在此结构的第一个位置.
/
struct ifnet ac_if;
u_char ac_enaddr[6]; /* 以太网硬件地址/
int ac_multicnt; /* 多播地址列表数 /
void *ac_netgraph; /* netgraph 节点信息,即我们所说的PPPoE,也就是ADSL宽带所用到的 /
};


*/

/* 一些函数申明 */
static int el_attach(struct isa_device *);/*第二步:填充相关的数据结构*/
static void el_init(void *); /*不用说,是初始化,在probe,attach之后被调用*/
static int el_ioctl(struct ifnet *,u_long,CADdr_t);/*控制网卡的函树指针*/
static int el_probe(struct isa_device *);/*第一步:探测程序.查看是否卡存在和是否在正确的位置.*/
static void el_start(struct ifnet *);/*把数据包从硬件接口输出去*/
static void el_reset(void *);/* 该例程重设接口. 在el_watchdog()中调用*/
static void el_watchdog(struct ifnet *);/*一般该函数用于包在一定时间内没发送出去,就调用他,在
本驱动程序中并不支持该函数,在我的rtl8139说明中有*/
static void el_stop(void *);/*停止接口,在el_ioctl()和el_reset()中调用*/
static int el_xmit(struct el_softc *,int);/*把数据包放到芯片内,发送到以太网上*/
static ointhand2_t elintr;/*中断例程*/
static __inline void elread(struct el_softc *,caddr_t,int);/* 传递包到更高一级协议处理,即ether_input()例程.由elintr()调用 */
static struct mbuf *elget(caddr_t,int,struct ifnet *); /* 从网卡上下载数据包,len是数据的长度,本地以太网头部被分开*/
static __inline void el_hardreset(void *);/* 这是一个子程序,目的是重设硬件.*/

/* isa_driver结构 为 autoconf准备 */
/* isa_driver结构说明:
该结构来之于文件isa_device.h头文件
结构成员:
/*
* 通用的设备驱动程序结构.
*
* 没一设备驱动程序定义一组例程入口,由设置程序在启动时使用.

struct isa_driver {
int (*probe) __P((struct isa_device *idp));
/* 测试设备是否存在
int (*attach) __P((struct isa_device *idp));
/* 为设备设置驱动程序
char *name; /* 设备名称
int sensitive_hw; /* 探测发生有冲突时为真,ISA设备的老毛病
};
*/
struct isa_driver eldriver = {
el_probe, el_attach, "el"
};


/* 探测程序.查看是否卡存在和是否在正确的位置. */
static int
el_probe(struct isa_device *idev)
{
/*
isa_device 是设备的通用结构,该结构说明在isa_device.h头文件中,说明如下:
struct isa_device {
int id_id; /* 设备的 id
struct isa_driver *id_driver; 指向设备的驱动程序结构
int id_iobase; /* 基本IO地址
int id_iosize; /* 基本IO地址的长度
u_int id_irq; /* 中断
int id_drq; /* DMA
caddr_t id_maddr; /* 在总线中的物理IO内存地址(即便要)
int id_msize; /* IO内存的大小
union {
inthand2_t *id_i;
ointhand2_t *id_oi;中断例程指针
} id_iu; /* 中断接口例程
#define id_intr id_iu.id_i
#define id_ointr id_iu.id_oi
int id_unit; /* 在该类设备中是第几个
int id_flags; /* flags
int id_enabled; /* 设备激活了吗
struct isa_device *id_next; /* 在 userconfig()中用于isa_devlist
struct device *id_device;
};

*/
struct el_softc *sc;
u_short base; /* 仅仅为了方便 */
u_char station_addr[ETHER_ADDR_LEN];/*以太网的硬件地址*/
int i;

/* 搜集一些信息 */
sc = &el_softc[idev->id_unit];/*sc是softc结构,如果你有NEL块el卡的话就有NEL个softc
结构,当然也有可能同时还有其他的xx_softc结构*/
sc->el_base = idev->id_iobase;/*该块卡的基本I/O地址*/
base = sc->el_base;/*有一点多余,只是为了方便下面的引用*/

/* 第一次检查地址,看看基本地址是否在0X280到0X3F0之内 */
if((base < 0x280) || (base > 0x3f0)) {
printf("el%d: ioaddr must be between 0x280 and 0x3f0n",
idev->id_unit);
return(0);
}

/* 现在尝试从PROM中获取地址,看看是否包含了3COM供应商的标识代码.
*/
dprintf(("Probing 3c501 at 0x%x...n",base));/*在调试时会打印出*/

/* 重置板卡 */
dprintf(("Resetting board...n"));
outb(base+EL_AC,EL_AC_RESET);/*我们一般定义基地址为0X300,EL_AC=0E,是辅助命令寄存器*/
DELAY(5);/*延迟5毫秒*/
outb(base+EL_AC,0);
dprintf(("Reading station address...n"));
/* 读硬件地址,共六次 */
for(i=0;i<ETHER_ADDR_LEN;i++) {
outb(base+EL_GPBL,i);
station_addr[i] = inb(base+EL_EAW);/*EL_EAW是该卡的地址口,station_addr是函数内部变量,
下面判断了生产厂家后就没用的*/
}
dprintf(("Address is %6Dn",station_addr, ":"));

/* 如果厂商标识代码正确,那么返回1.
*/
if((station_addr[0] != 0x02) || (station_addr[1] != 0x60)
|| (station_addr[2] != 0x8c)) {
dprintf(("Bad vendor code.n"));/*3COM厂商此种卡的代码为02608C*/
return(0);
} else {
dprintf(("Vendor code ok.n"));
/* 把地址拷贝到arpcom结构中 */
bcopy(station_addr,sc->arpcom.ac_enaddr,ETHER_ADDR_LEN);
return(1);
}
}

/* 这是一个子程序,目的是重设硬件. 在el_init()中调用,在elintr()中调用,产生中断,有溢出发生时调用*/
static __inline void
el_hardreset(xsc)
void *xsc;
{
register struct el_softc *sc = xsc;/*记住在C中,寄存器变量只能有三个,可加快速度*/
register int base;
register int j;

base = sc->el_base;

/* 第一步,重设板卡,和el_probe中的一样(前面) */
outb(base+EL_AC,EL_AC_RESET);
DELAY(5);
outb(base+EL_AC,0);

/* 又把地址填回去,为什么?没有为什么,就是厂商规定的,一些端口填什么数据时会怎么样,只有厂商知道,我相信,
在同一厂商之间的网卡,交换机,路由器进行秘密通讯是非常可能的,他可以不返回到CPU层*/
for(j=0;j<ETHER_ADDR_LEN;j++)
outb(base+j,sc->arpcom.ac_enaddr[j]);
}

/* 连接该接口到核心数据结构.被调用时,我们已经知道该卡已经存在在给定的I/O
* 地址,我们还假定中断号是正确的.
*/
static int
el_attach(struct isa_device *idev)
{
struct el_softc *sc;
struct ifnet *ifp;/*该结构是一个巨大的结构,在STEVEN的书中有描述,我也写了一篇*/
u_short base;/*没用上,可以去掉*/

dprintf(("Attaching el%d...n",idev->id_unit));

/* 放置一些指针. */
idev->id_ointr = elintr;/*放置中断例程指针,中断例程在下面*/
sc = &el_softc[idev->id_unit];/*定位本设备的softc结构指针*/
ifp = &sc->arpcom.ac_if;/*定位ifnet结构*/
base = sc->el_base;/*从程序来看,这一句可以去掉,根本没用,因为在该函数中没用到base*/

/* 重设板卡 */
dprintf(("Resetting board...n"));
el_hardreset(sc);/*该程序在上面*/

/* 初始化ifnet结构,该结构的成员经常用来被ether网子程序,arp,bridge等调用 */
ifp->if_softc = sc;/*该网卡的IFP(通用接口结构)的专用结构指针(softc结构)*/
ifp->if_unit = idev->id_unit;/*第几块网卡*/
ifp->if_name = "el";/*网络卡的名称*/
ifp->if_mtu = ETHERMTU;/*1500*/
ifp->if_output = ether_output;/*以太网的输出子程序指针(不要搞错了,是向IP层
输出,按我们的理解是数据输入了,再转送到上一层协议)*/
ifp->if_start = el_start;/*把数据包从硬件接口输出去*/
ifp->if_ioctl = el_ioctl;/*控制网卡的函树指针*/
ifp->if_watchdog = el_watchdog;/*一般该函数用于包在一定时间内没发送出去,就调用他,在
本驱动程序中并不支持该函数,在我的rtl8139说明中有*/
ifp->if_init = el_init; /*不用说,是初始化,在probe,attach之后被调用*/
ifp->if_flags = (IFF_BROADCAST | IFF_SIMPLEX);/*支持广播和单播*/

/* 调用通用以太网初始化例程 */
dprintf(("Attaching interface...n"));
ether_ifattach(ifp, ETHER_BPF_SUPPORTED);
/*
在if_ethersubr.c中的ether_ifattach例程
void ether_ifattach(ifp, bpf) 调用时,ETHER_BPF_SUPPORTED是BSD的
包过滤器,如果在编译时设置文件没有
打开包过滤器,那么代表0,否则是1
register struct ifnet *ifp;
int bpf;
{
register struct ifaddr *ifa;
register struct sockaddr_dl *sdl;

if_attach(ifp); 此例程在if.c 中
ifp->if_type = IFT_ETHER;代表以太网
ifp->if_addrlen = 6;硬件地址长度是6
ifp->if_hdrlen = 14;包的头长度是6+6+2=14,其中2是协议类型
ifp->if_mtu = ETHERMTU; 为1500,多此一举,在前面你可看到,已
经填充了.
ifp->if_resolvemulti = ether_resolvemulti; 以太网解析多播例程指针
if (ifp->if_baudrate == 0) 波特率
ifp->if_baudrate = 10000000;
ifa = ifnet_addrs[ifp->if_index - 1];在ifnet_addrs[]数组中找到本地址指针
KASSERT(ifa != NULL, ("%s: no lladdr!n", __FUNCTION__));
sdl = (struct sockaddr_dl *)ifa->ifa_addr; ifa->ifa_addr在此时指向的是sockaddr_dl结构.
sdl->sdl_type = IFT_ETHER;
sdl->sdl_alen = ifp->if_addrlen;
bcopy((IFP2AC(ifp))->ac_enaddr, LLADDR(sdl), ifp->if_addrlen);把硬件地址拷贝到sdl结构中
if (bpf) bpf为真,即加入了BSD包过滤
bpfattach(ifp, DLT_EN10MB, sizeof(struct ether_header));
if (ng_ether_attach_p != NULL)
(*ng_ether_attach_p)(ifp);
}

*/
printf("el%d: 3c501 address %6Dn",idev->id_unit,
sc->arpcom.ac_enaddr, ":");

dprintf(("el_attach() finished.n"));
return(1);
}

/* 该例程重设接口. 在el_watchdog()中调用,因为watchdog不在本驱动程序中支持,所以从不被调用*/
static void
el_reset(xsc)/*上面的一个函数,重设硬件*/
void *xsc;
{
struct el_softc *sc = xsc;
int s;

dprintf(("elreset()n"));
s = splimp();/*关网络中断*/
el_stop(sc);/*下面的一个函数*/
el_init(sc);/*重新初始化卡*/
splx(s);/*开网络中断*/
}
/*停止接口,在el_ioctl()和el_reset()中调用*/
static void el_stop(xsc)
void *xsc;
{
struct el_softc *sc = xsc;

outb(sc->el_base+EL_AC,0);/*用0写辅助命令寄存器*/
}

/* 初始化接口. */
static void
el_init(xsc)
void *xsc;
{
struct el_softc *sc = xsc;
struct ifnet *ifp;
int s;
u_short base;

ifp = &sc->arpcom.ac_if;/*定位ifnet结构*/
base = sc->el_base;/*网卡基本I/O地址*/

/* 如果地址不知道,什么也不做. */
if(TAILQ_EMPTY(&ifp->if_addrhead)) /* 在if.c中的if_attach例程
中已经填充,由el_attach调用
ether_attach时再调用if_attach */
return;

s = splimp();/*关网络中断*/

/* 重设板卡. */
dprintf(("Resetting board...n"));
el_hardreset(sc);/*该函数在上面,重设硬件*/

/* 设置接收寄存器 rx */
dprintf(("Configuring rx...n"));
if(ifp->if_flags & IFF_PROMISC) /*是混杂模式?EL_RXC是0X6接收命令寄存器*/
outb(base+EL_RXC,(EL_RXC_PROMISC|EL_RXC_AGF|EL_RXC_DSHORT|EL_RXC_DDRIB|EL_RXC_DOFLOW));
else
outb(base+EL_RXC,(EL_RXC_ABROAD|EL_RXC_AGF|EL_RXC_DSHORT|EL_RXC_DDRIB|EL_RXC_DOFLOW));
outb(base+EL_RBC,0);/*接收缓冲寄存器清0*/

/* 设置传输寄存器 TX */
dprintf(("Configuring tx...n"));
outb(base+EL_TXC,0);

/* 开始接收 */
dprintf(("Starting reception...n"));
outb(base+EL_AC,(EL_AC_IRQE|EL_AC_RX));/*EL_AC_IRQE是IRQ enable(可用) EL_AC_RX为接收寄存器*/

/* 设置一些开始使用的标志 */
ifp->if_flags |= IFF_RUNNING;/*加上正在运行标志*/
ifp->if_flags &= ~IFF_OACTIVE;/*去掉正在传输标志*/

/* 调用输出. */
el_start(ifp);

splx(s);/*开网络中断*/
}

标签: