Board logo

标题: linux驱动的一些知识(IO 具体实现等)(续) [打印本页]

作者: linuxarm    时间: 2006-8-30 19:09     标题: linux驱动的一些知识(IO 具体实现等)(续)

访问I/O内存资源:


  尽管I/O端口空间曾一度在x86平台上被广泛使用,但是由于它非常小,因此大多数现代总线的设备都以内存映射方式(Memory-mapped)来映射它的I/O端口(指I/O寄存器)和外设内存。基于内存映射方式的I/O端口(指I/O寄存器)和外设内存可以通称为“I/O内存”资源(I/O Memory)。因为这两者在硬件实现上的差异对于软件来说是完全透明的,所以驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是“I/O内存”资源。


  从前几节的阐述我们知道,I/O内存资源是在CPU的单一内存物理地址空间内进行编址的,也即它和系统RAM同处在一个物理地址空间内。因此通过CPU的访内指令就可以访问I/O内存资源。


  一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,这可以通过系统固件(如BIOS)在启动时分配得到,或者通过设备的硬连线(hardwired)得到。比如,PCI卡的I/O内存资源的物理地址就是在系统启动时由PCI BIOS分配并写到PCI卡的配置空间中的BAR中的。而ISA卡的I/O内存资源的物理地址则是通过设备硬连线映射到640KB-1MB范围之内的。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,因为它们是在系统启动后才已知的(某种意义上讲是动态的),所以驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。
映射I/O内存资源:


  Linux在io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中,如下:



void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
void iounmap(void * addr);



  函数用于取消ioremap()所做的映射,参数addr是指向核心虚地址的指针。这两个函数都是实现在mm/ioremap.c文件中。具体实现可参考《情景分析》一书。


读写I/O内存资源:


  在将I/O内存资源的物理地址映射成核心虚地址后,理论上讲我们就可以象读写RAM那样直接读写I/O内存资源了。但是,由于在某些平台上,对I/O内存和系统内存有不同的访问处理,因此为了确保跨平台的兼容性,Linux实现了一系列读写I/O内存资源的函数,这些函数在不同的平台上有不同的实现。但在x86平台上,读写I/O内存与读写RAM无任何差别。如下所示(include/asm-i386/io.h):



#define readb(addr) (*(volatile unsigned char *) __io_virt(addr))
#define readw(addr) (*(volatile unsigned short *) __io_virt(addr))
#define readl(addr) (*(volatile unsigned int *) __io_virt(addr))


#define writeb(b,addr) (*(volatile unsigned char *) __io_virt(addr) = (b))
#define writew(b,addr) (*(volatile unsigned short *) __io_virt(addr) = (b))
#define writel(b,addr) (*(volatile unsigned int *) __io_virt(addr) = (b))


#define memset_io(a,b,c)        memset(__io_virt(a),(b),(c))
#define memcpy_fromio(a,b,c) memcpy((a),__io_virt(b),(c))
#define memcpy_toio(a,b,c)        memcpy(__io_virt(a),(b),(c))


  上述定义中的宏__io_virt()仅仅检查虚地址addr是否是核心空间中的虚地址。该宏在内核2.4.0中的实现是临时性的。具体的实现函数在arch/i386/lib/Iodebug.c文件。


  显然,在x86平台上访问I/O内存资源与访问系统主存RAM是无差别的。但是为了保证驱动程序的跨平台的可移植性,我们应该使用上面的函数来访问I/O内存资源,而不应该通过指向核心虚地址的指针来访问。


作者: linuxarm    时间: 2006-8-30 19:11

驱动程序

2.LINUX系统下的设备驱动程序
具体到LINUX系统里,设备驱动程序所提供的这组入口点由一个结构来向系统进行说明,此结构定义为:
#include
struct file_operations {
int (*lseek)(struct inode *inode,struct file *filp,
off_t off,int pos);
int (*read)(struct inode *inode,struct file *filp,
char *buf, int count);
int (*write)(struct inode *inode,struct file *filp,
char *buf,int count);
int (*readdir)(struct inode *inode,struct file *filp,
struct dirent *dirent,int count);
int (*select)(struct inode *inode,struct file *filp,
int sel_type,select_table *wait);
int (*ioctl) (struct inode *inode,struct file *filp,
unsigned int cmd,unsigned int arg);
int (*mmap) (void);

int (*open) (struct inode *inode, struct file *filp);
void (*release) (struct inode *inode, struct file *filp);
int (*fsync) (struct inode *inode, struct file *filp);
};

其中,struct inode提供了关于特别设备文件/dev/driver(假设此设备名为driver)的信息,它的定义为:

#include
struct inode {
dev_t i_dev;
unsigned long i_ino; /* Inode number */
umode_t i_mode; /* Mode of the file */
nlink_t i_nlink;
uid_t i_uid;
gid_t i_gid;
dev_t i_rdev; /* Device major and minor numbers*/
off_t i_size;
time_t i_atime;
time_t i_mtime;
time_t i_ctime;
unsigned long i_blksize;
unsigned long i_blocks;
struct inode_operations * i_op;
struct super_block * i_sb;
struct wait_queue * i_wait;
struct file_lock * i_flock;
struct vm_area_struct * i_mmap;
struct inode * i_next, * i_prev;
struct inode * i_hash_next, * i_hash_prev;
struct inode * i_bound_to, * i_bound_by;
unsigned short i_count;
unsigned short i_flags; /* Mount flags (see fs.h) */
unsigned char i_lock;
unsigned char i_dirt;
unsigned char i_pipe;
unsigned char i_mount;
unsigned char i_seek;
unsigned char i_update;
union {
struct pipe_inode_info pipe_i;
struct minix_inode_info minix_i;
struct ext_inode_info ext_i;
struct msdos_inode_info msdos_i;
struct iso_inode_info isofs_i;
struct nfs_inode_info nfs_i;
} u;
};

struct file主要用于与文件系统对应的设备驱动程序使用。当然,其它设备驱动程序也可以使用它。它提供关于被打开的文件的信息,定义为:#include
struct file {
mode_t f_mode;
dev_t f_rdev; /* needed for /dev/tty */
off_t f_pos; /* Curr. posn in file */
unsigned short f_flags; /* The flags arg passed to open */
unsigned short f_count; /* Number of opens on this file */
unsigned short f_reada;
struct inode *f_inode; /* pointer to the inode struct */
struct file_operations *f_op;/* pointer to the fops struct*/
};

在结构file_operations里,指出了设备驱动程序所提供的入口点位置,分别是
(1) lseek,移动文件指针的位置,显然只能用于可以随机存取的设备。
(2) read,进行读操作,参数buf为存放读取结果的缓冲区,count为所要读取的数据长度。返回值为负表示读取操作发生错误,否则返回实际读取的字节数。对于字符型,要求读取的字节数和返回的实际读取字节数都必须是inode->i_blksize的的倍数。
(3) write,进行写操作,与read类似。
(4) readdir,取得下一个目录入口点,只有与文件系统相关的设备驱动程序才使用。
(5) selec,进行选择操作,如果驱动程序没有提供select入口,select操作将会认为设备已经准备好进行任何的I/O操作。
(6) ioctl,进行读、写以外的其它操作,参数cmd为自定义的的命令。
(7) mmap,用于把设备的内容映射到地址空间,一般只有块设备驱动程序使用。
(8) open,打开设备准备进行I/O操作。返回0表示打开成功,返回负数表示失败。如果驱动程序没有提供open入口,则只要/dev/driver文件存在就认为打开成功。
(9) release,即close操作。
作者: linuxarm    时间: 2006-8-30 19:12

设备驱动程序所提供的入口点,在设备驱动程序初始化的时候向系统进行登记,以便系统在适当的时候调用。LINUX系统里,通过调用register_chrdev向系统注册字符型设备驱动程序。register_chrdev定义为:

#include
#include
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

其中,major是为设备驱动程序向系统申请的主设备号,如果为0则系统为此驱动程序动态地分配一个主设备号。name是设备名。fops就是前面所说的对各个调用的入口点的说明。此函数返回0表示成功。返回-EINVAL表示申请的主设备号非法,一般来说是主设备号大于系统所允许的最大设备号。返回-EBUSY表示所申请的主设备号正在被其它设备驱动程序使用。如果是动态分配主设备号成功,此函数将返回所分配的主设备号。如果register_chrdev操作成功,设备名就会出现在/proc/devices文件里。
初始化部分一般还负责给设备驱动程序申请系统资源,包括内存、中断、时钟、I/O端口等,这些资源也可以在open子程序或别的地方申请。在这些资源不用的时候,应该释放它们,以利于资源的共享。在UNIX系统里,对中断的处理是属于系统核心的部分,因此如果设备与系统之间以中断方式进行数据交换的话,就必须把该设备的驱动程序作为系统核心的一部分。设备驱动程序通过调用request_irq函数来申请中断,通过free_irq来释放中断。它们的定义为:

#include
int request_irq(unsigned int irq,
void (*handler)(int irq,void dev_id,struct pt_regs *regs),
unsigned long flags,
const char *device,
void *dev_id);
void free_irq(unsigned int irq, void *dev_id);

参数irq表示所要申请的硬件中断号。handler为向系统登记的中断处理子程序,中断产生时由系统来调用,调用时所带参数irq为中断号,dev_id为申请时告诉系统的设备标识,regs为中断发生时寄存器内容。device为设备名,将会出现在/proc/interrupts文件里。flag是申请时的选项,它决定中断处理程序的一些特性,其中最重要的是中断处理程序是快速处理程序(flag里设置了SA_INTERRUPT)还是慢速处理程序(不设置SA_INTERRUPT),快速处理程序运行时,所有中断都被屏蔽,而慢速处理程序运行时,除了正在处理的中断外,其它中断都没有被屏蔽。

在LINUX系统中,中断可以被不同的中断处理程序共享,这要求每一个共享此中断的处理程序在申请中断时在flags里设置SA_SHIRQ,这些处理程序之间以dev_id来区分。如果中断由某个处理程序独占,则dev_id可以为NULL。request_irq返回0表示成功,返回-INVAL表示irq>15或handler==NULL,返回-EBUSY表示中断已经被占用且不能共享。作为系统核心的一部分,设备驱动程序在申请和释放内存时不是调用malloc和free,而代之以调用kmalloc和kfree,它们被定义为:

#include
void * kmalloc(unsigned int len, int priority);
void kfree(void * obj);

参数len为希望申请的字节数,obj为要释放的内存指针。priority为分配内存操作的优先级,即在没有足够空闲内存时如何操作,一般用GFP_KERNEL。与中断和内存不同,使用一个没有申请的I/O端口不会使CPU产生异常,也就不会导致诸如“segmentation fault"一类的错误发生。任何进程都可以访问任何一个I/O端口。此时系统无法保证对I/O端口的操作不会发生冲突,甚至会因此而使系统崩溃。因此,在使用I/O端口前,也应该检查此I/O端口是否已有别的程序在使用,若没有,再把此端口标记为正在使用,在使用完以后释放它。这样需要用到如下几个函数:

int check_region(unsigned int from, unsigned int extent);
void request_region(unsigned int from, unsigned int extent, const char *name);
void release_region(unsigned int from, unsigned int extent);

调用这些函数时的参数为:from表示所申请的I/O端口的起始地址;extent为所要申请的从from开始的端口数;name为设备名,将会出现在/proc/ioports文件里。check_region返回0表示I/O端口空闲,否则为正在被使用。
在申请了I/O端口之后,就可以如下几个函数来访问I/O端口:

#include
inline unsigned int inb(unsigned short port);
inline unsigned int inb_p(unsigned short port);
inline void outb(char value, unsigned short port);
inline void outb_p(char value, unsigned short port);

其中inb_p和outb_p插入了一定的延时以适应某些慢的I/O端口。在设备驱动程序里,一般都需要用到计时机制。在LINUX系统中,时钟是由系统接管,设备驱动程序可以向系统申请时钟。与时钟有关的系统调用有:

#include
#include
void add_timer(struct timer_list * timer);
int del_timer(struct timer_list * timer);
inline void init_timer(struct timer_list * timer);

struct timer_list的定义为:

struct timer_list {
struct timer_list *next;
struct timer_list *prev;
unsigned long expires;
unsigned long data;
void (*function)(unsigned long d);
};

其中expires是要执行function的时间。系统核心有一个全局变量JIFFIES表示当前时间,一般在调用add_timer时jiffies=JIFFIES+num,表示在num个系统最小时间间隔后执行function。系统最小时间间隔与所用的硬件平台有关,在核心里定义了常数HZ表示一秒内最小时间间隔的数目,则num*HZ表示num秒。系统计时到预定时间就调用function,并把此子程序从定时队列里删除,因此如果想要每隔一定时间间隔执行一次的话,就必须在function里再一次调用add_timer。function的参数d即为timer里面的data项。在设备驱动程序里,还可能会用到如下的一些系统函数:

#include
#define cli() __asm__ __volatile__ ("cli":
#define sti() __asm__ __volatile__ ("sti":

这两个函数负责打开和关闭中断允许。

#include
void memcpy_fromfs(void * to,const void * from,unsigned long n);
void memcpy_tofs(void * to,const void * from,unsigned long n);

在用户程序调用read 、write时,因为进程的运行状态由用户态变为核心态,地址空间也变为核心地址空间。而read、write中参数buf是指向用户程序的私有地址空间的,所以不能直接访问,必须通过上述两个系统函数来访问用户程序的私有地址空间。memcpy_fromfs由用户程序地址空间往核心地址空间复制,memcpy_tofs则反之。参数to为复制的目的指针,from为源指针,n为要复制的字节数。在设备驱动程序里,可以调用printk来打印一些调试信息,用法与printf类似。printk打印的信息不仅出现在屏幕上,同时还记录在文件syslog里。

作者: linuxarm    时间: 2006-8-30 19:13


具体实现

3.LINUX系统下的具体实现
在LINUX里,除了直接修改系统核心的源代码,把设备驱动程序加进核心里以外,还可以把设备驱动程序作为可加载的模块,由系统管理员动态地加载它,使之成为核心地一部分。也可以由系统管理员把已加载地模块动态地卸载下来。
LINUX中,模块可以用C语言编写,用gcc编译成目标文件(不进行链接,作为*.o文件存在),为此需要在gcc命令行里加上-c的参数。在编译时,还应该在gcc的命令行里加上这样的参数:-D__KERNEL__ -DMODULE。由于在不链接时,gcc只允许一个输入文件,因此一个模块的所有部分都必须在一个文件里实现。编译好的模块*.o放在/lib/modules/xxxx/misc下(xxxx表示核心版本,如在核心版本为2.0.30时应该为/lib/modules/2.0.30/misc),然后用depmod -a使此模块成为可加载模块。模块用insmod命令加载,用rmmod命令来卸载,并可以用lsmod命令来查看所有已加载的模块的状态。

编写模块程序的时候,必须提供两个函数,一个是int init_module(void),供insmod在加载此模块的时候自动调用,负责进行设备驱动程序的初始化工作。init_module返回0以表示初始化成功,返回负数表示失败。另一个函数是voidcleanup_module (void),在模块被卸载时调用,负责进行设备驱动程序的清除工作。

在成功的向系统注册了设备驱动程序后(调用register_chrdev成功后),就可以用mknod命令来把设备映射为一个特别文件,其它程序使用这个设备的时候,只要对此特别文件进行操作就行了。



PCI

PCI是一种广泛采用的总线标准,它提供了优于其他总线标准(比如EISA)的特性。在大多数奔腾主板上,PCI是高速、高带宽(32-bit和64-bit)、处理器无关的总线。对PCI的支持第一次加入Linux中时,其内核接口是PCI BIOS32函数的堆砌。这样做有几个问题:
* PCI BIOS仅存在于PC上;
* PCI BIOS只代表特定的结构,非PC类机器的某些PCI设置不能用PCI BIOS来描述;
* 个别机子的PCI BIOS函数不象预期的那样工作。

Linux 2.2提供了一个通用的PCI接口。Linux x86内核实际上努力直接驱动硬件,只有当它发现某些东西不能理解时,它才会调用PCI BIOS32函数。
驱动程序可以继续使用老的PCI接口,但是为了兼容将来的内核,可能需要更新。
如果驱动程序将要跨平台工作,那就更加需要更新了。多数新、老函数有简单的对应关系。PCI BIOS基于总线号/设备号/功能号的思想,而新的代码使用pci_bus和pci_dev结构。第一个新PCI函数是:

pci_present()

这个函数检查机器是否存在一条或更多的PCI总线。老内核有一个pcibios_present()函数,它们的用法完全相同。

确认PCI存在之后,你可以扫描PCI总线来查找设备。PCI设备通过几个配置寄存器来标识,主要是供应商ID和设备ID。每个供应商被分配了一个唯一的标识(ID),并且假设供应商给他们的设备(板子、芯片等)分配唯一的设备ID。PCI的一个好处是它提供了版本和编程接口信息,因此可以发现板子的变化。

在Linux 2.2中,扫描PCI总线一般用pci_find_device()函数。范例如下:

struct pci_dev *pdev = NULL;
while ((pdev = pci_find_device(PCI_MY_VENDOR,
PCI_MY_DEVICE, pdev)) != NULL)
{
/* Found a device */
setup_device(pdev);
}

pci_find_device()有3个参数:第一个是供应商ID,第二个是设备ID,第三个是函数的返回值,NULL表示你想从头开始查找。在这个例子中,对找到的设备调用setup_device()来进行设置。

另一个值得高兴的事情,是PCI为你处理了所有资源配置工作。一般来说PCI BIOS具体做这些工作,但是在其他平台上,这项工作由固件或者体系结构相关的Linux代码来做。到你的驱动程序查找PCI卡的时候,它已经被分配了系统资源。

Linux在pci_dev结构中提供了PCI相关的核心信息。同时还允许读写每个卡的PCI配置空间。当你可以直接查找资源数据时应该小心,对许多系统来说,卡上配置的数据与内核提供的数据并不相符。因为许多非PC机器有多条PCI总线,PCI总线以设备卡不知道的方式映射到系统中。

Linux直接提供了IRQ和PCI BARs(基址寄存器)。为了避免代码在非PC平台上出现意外,你应该总是使用内核提供的数据。下面代码列出了setup_device()例程:

Listing One: The setup_device () Function
void setup_device(struct pci_dev *dev)
{
int io_addr = dev->base_address[0] & PCI_BASE_ADDRESS_IO_MASK;
int irq = dev->irq;
u8 rev;

pci_read_config_byte(dev, PCI_REVISION_ID, &rev);

if (rev<64)
printk("Found a WonderWidget 500 at I/O 0x%04X, IRQ %d.\n",
io_addr, irq);
else
printk("Found a WonderWidget 600 at I/O 0x%04X, IRQ %d.\n",
io_addr, irq);

/* Check for a common BIOS problem - if you
* expect an IRQ you might not get it */
if (irq==0)
{
printk(KERN_ERR "BIOS has not assigned the WonderWidget"
" an interrupt.\n");
return;
}

/* Now do the board initialization knowing the resources */
init_device(io_addr, irq, rev<64 ? 0 : 1);

pci_set_master(dev);
}

当你的卡被BIOS配置后,某些特性可能会被屏蔽掉。比如,多数BIOS都会清掉“master”位,这导致板卡不能随意向主存中拷贝数据。Linux 2.2提供了一个辅助函数:

pci_set_master(struct pci_dev *)
作者: linuxarm    时间: 2006-8-30 19:14

这个函数会检查是否需要设置标志位,如果需要,则会将“master”位置位。例子函数setup_device还使用了pci_read_config_byte来读取配置空间数据。内核提供了一整套与配置空间相关的函数:

pci_read_config_byte,
pci_read_config_word,
和pci_read_config_dword

分别从配置空间获取8,16和32位数据;

pci_write_config_byte,
pci_write_config_word,
和pci_write_config_dword

分别向配置空间写入8,16和32位数据。PCI配置空间独立于I/O和内存空间,只能通过这些函数访问。

最后一组有用的PCI函数以不同的方式扫描PCI总线。pci_find_class查找符合给定类别(class)的设备。PCI规范把设备分为不同的类别,你可以根据类别查找设备。例如,为了查找一个USB控制器,可以用

struct pci_dev *pdev = NULL;
while((pdev=pci_find_class
(PCI_CLASS_SERIAL_USB <<8, pdev))!=NULL)
{
u8 type;
pci_read_config_byte(dev,
PCI_CLASS_PROG, &type);
if(type!=0)
continue;
/* FOUND IT */
}

另一个例子是I2O。这时,供应商ID只用来确定板卡的实际类型(type),偶尔用来对付特定板卡的bug。

扫描PCI设备的最后一种途径是pci_find_slot,使你按照特定的顺序扫描PCI插槽和功能。它很少使用,但是,如果你要控制查找某一类型设备时扫描PCI总线的顺序,你可以用它。这种情况通常出现在你需要遵照主板BIOS报告设备的顺序时,或者你想使Linux和非Linux驱动程序以相同的顺序报告设备时。传递给pci_find_slot()的是总线号slot和设备-功能号function(slot<<3 | function)。

PCI中断和其他注意事项

PCI总线一个重要的概念是共享中断处理,这在ISA总线设备中一般是看不到的。PCI总线中断也是电平触发的(level-triggered),也就是说,中断一直在那里,直到设备去清除它。这些特性给驱动程序处理中断加上了一些重要的限制。

驱动程序注册PCI中断时,总是应该带上SA_SHIRQ标志,用来指明中断线是可以共享的。如果不这样做,那么系统中的其他设备有可能不能正常工作,用户也可能遇到麻烦。

由于中断是共享的,PCI设备驱动程序和内核都需要与每个中断处理例程进行沟通的方法。你必须用一个非空(non-NULL)的dev_id来注册共享中断,否则,当你需要用free_irq来释放一个中断时,内核不能区分不同的中断处理例程。dev_id被送到中断处理例程,因此它非常重要。例如,你可以这样:

if (request_irq(dev->irq, dev_interrupt,
SA_SHIRQ, "wonderwidget",
dev))
return -EAGAIN;

结束时,用下面的语句来正确释放中断:

free_irq(dev->irq, dev)

中断处理例程被调用时收到dev参数,这使事情很简单了。你不必搜寻使用该中断的设备,通常可以这样做:

Listing Two: Using the dev_id
static void dev_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
struct wonderwidget *dev = dev_id;
u32 status;

/* It is important to exit interrupt handlers
* that are not for us as fast as possible */

if((status=inl(dev->port))==0) /* Not our interrupt */
return;

if(status&1)
handle_rx_intr(dev);
....
}

你必须总是小心处理中断。永远不要在安装中断处理例程之前产生中断。因为PCI中断是电平触发的,如果你产生了中断而又不能处理它,可能会导致死机。这意味着写初始化代码时必须特别小心,你必须在打开设备的中断之前注册中断处理例程。同样,关闭时必须在注销中断处理例程之前屏蔽设备的中断。与ISA总线相比,Linux对PCI总线的支持用到较多的函数,并且要小心处理中断。
作为回报,不需要你的介入,系统把一切都配置好了。

作者: linuxarm    时间: 2006-8-30 19:16

Sis 900

SIS 900 是一个可以用来实作 10/100 网络卡的控制芯片。它提供了对 PCI mastermode , MII, 802.3x 流量控制等各种标准的支援。这篇文章将告诉大家,如何写一个 Linux 的网络驱动程序,它将比大家想像中简单很多。这篇文章将以 Linux 2.4 版为对象, 2.2 版提供的界面略有不同,但差别并不太大,读完本文后再读 2.2 版的程序码应该不会有太大困难才是。 本文所参考的驱动程序是在 2.4.3 版中 drivers/net/sis900.c 这个档案。你可以在 http://xxx.xxx.xxx.xxx/linux-2.4.3/drivers/net/sis900.c 找到它。如果你能有一份硬件的 databook 在手边,读起驱动程序的码可能会更简单。 SIS900的 databook 可以直接在http://www.sis.com.tw/ftp/Databook/900/sis900.exe下载。
PCI 驱动程序
对一个 PCI 驱动程序而言, Linux 提供了很完整的支援,大部份的 PCI 资讯都由内建的程序读出。对个别的驱动程序而言直接使用就可以了。所以在这个部份,唯一要做的事只是告知 PCI 子系统一个新的驱动程序己经被加入系统之中了。在档案的最末端,你会看到下面的程序,

static struct pci_driver sis900_pci_driver = {
name: SIS900_MODULE_NAME,
id_table: sis900_pci_tbl,
probe: sis900_probe,
remove: sis900_remove,
};
static int __init sis900_init_module(void)
{
printk(KERN_INFO "%s", version);
return pci_module_init(&sis900_pci_driver);
}
static void __exit sis900_cleanup_module(void)
{
pci_unregister_driver(&sis900_pci_driver);
}

pci_module_init 是用来向 PCI 子系统注册一个 PCI 驱动程序。根据 id_table 中所提供的资料, PCI 子系统会在发现符合驱动程序要求的装置时使用它。那 PCI 子系统如何做到这件事呢 ? 我们先看一下 id_table 的内容就很清楚了。

static struct pci_device_id sis900_pci_tbl [] __devinitdata = {
{PCI_VENDOR_ID_SI, PCI_DEVICE_ID_SI_900,
PCI_ANY_ID, PCI_ANY_ID, 0, 0, SIS_900},
{PCI_VENDOR_ID_SI, PCI_DEVICE_ID_SI_7016,
PCI_ANY_ID, PCI_ANY_ID, 0, 0, SIS_7016},
{0,}
};
MODULE_DEVICE_TABLE (pci, sis900_pci_tbl);

看懂了吗 ? 嗯,我想你懂了。不过我还是解释一下。前面四个分别是

vendor id : PCI_VENDOR_ID_SI
device id : PCI_DEVICE_ID_SI_900
sub vendor id : PCI_ANY_ID
sub device id : PCI_ANY_ID

意思是说这个驱动程序支援 SIS 出的 SIS900 系列所有的硬件,我们不介意 subvendor id 和 sub device id 。你可以加入任何你想要的项目。对于不同的网络卡制造商,它们可能会有不同的 sub vendor id 和 sub device id 。但只要它们用SIS900 这个芯片,那这个驱动程序就可能适用。我们可以说这是一个『公版』的驱动程序。初始化好了,那其它的部份呢 ? 还记意 sis900_pci_driver 中其它的二个项目 probe 和remove 吗 ? 它们是用来初始化和移除一个驱动程序的呼叫。你可以把它们想成驱动程序物件的 constructor 和 destructor 。在 probe 中,你应该由硬件中把一些将来可能会用到的资讯准备好。由于这是一个 PCI 驱动程序,你不必特意去检查装置是否真的存在。但如果你的驱动程序只支援某些特定的硬件,或是你想要检查系统中是否有一些特别的硬件存在,你可以在这里做。例如在这个驱动程序中,对不同版本的硬件,我们用不
同的方法去读它的 MAC 位址。
pci_read_config_byte(pci_dev, PCI_CLASS_REVISION, &revision);
if (revision == SIS630E_900_REV || revision == SIS630EA1_900_REV)
ret = sis630e_get_mac_addr(pci_dev, net_dev);
else if (revision == SIS630S_900_REV)
ret = sis630e_get_mac_addr(pci_dev, net_dev);
else
ret = sis900_get_mac_addr(pci_dev, net_dev);
对于 SIS630E SIS630EA1 和 SIS630S 这些整合式芯片而言,其 MAC 位址被储存在 APC CMOS RAM 之中。但对其它独立的芯片而言则是存在网络卡的 EEPROM 之上。为了不要让这篇文章像流水帐一般,我不仔细的说明 probe 的过程。大家自己揣摸一下吧 !

在 probe 中还有一段比较和后文有关的程序码
net_dev->open = &sis900_open;
net_dev->hard_start_xmit = &sis900_start_xmit;
net_dev->stop = &sis900_close;
net_dev->get_stats = &sis900_get_stats;
net_dev->set_config = &sis900_set_config;
net_dev->set_multicast_list = &set_rx_mode;
net_dev->do_ioctl = &mii_ioctl;
net_dev->tx_timeout = sis900_tx_timeout;
net_dev->watchdog_timeo = TX_TIMEOUT;
我想这很清楚,我们透过 net_dev 这个结构告诉 Linux 网络子系统如何来操作这个装置。当你使用 ifconfig 这个 R 令时,系统会使用 sis900_open 打开这个驱动程序,并使用 set_config 来说定装置的参数,如 IP address 。当有资料需要被传送时, sis900_start_xmit 被用来将资料送入装置之中。接下来,我们就一一的检视这些函数。

初始化装置
sis900_open(struct net_device *net_dev);

这个函数会在我们使用 ifconfig 将一网络装置激活时被呼叫。当驱动程序被插入系统之后,通常并不会马上开始接收或传送封包。一般来说,在 probe 的阶段,我们只是单纯的判断装置是否存在。实际激活硬件的动作在这里才会被实际执行。以 SIS900 为例,在其硬件中只有一个大约 2K 的缓冲区。也就是说在装置上只有一个
封包的缓冲区。当一个封包被传送后,装置必须产生一个中断要求操作系统将下一个封包传入。如果由中断到中断驱动程序被执行需要 5ms 的时间,那一秒至多我们可以送出 200 个封包。也就是说网络传送是不可能大于 400K/s ,这对于一般的情况下是不太可能接受的事。SIS900 虽然在装置上只有很小的缓冲区,但它可以透过 PCI master 模式直接控制主机板上的记忆体。事实上,它使用下面的方式来传送资料。你必须在记忆体中分配一组串接成环状串列的缓冲区,然后将 TXDP 指向缓冲区的第一个位址。 SIS900 会在第一个缓冲区传送完后自动的由第二个缓冲区取资料,并更新记忆中的资料将己传送完缓冲区的 OWN 位元清除。当 CPU 将缓冲区串列设定完成后,这个动作可以在完全没有 CPU 的介入下完成。所以硬件不必等待作业系统将新的资料送入,而可以连续的送出多个封包。操作系统只要能来的及让环状串列不会进入空的状态就可以了。

同样的,我们也需要一个接收缓冲区,使用进来的封包不至因操作系统来不及处理而遗失。在 sis900_open 中, sis900_init_rx_ring 和 sis900_init_tx_ring 就是用来负处初始化这二个串列。
在初始化串列之后,我们便可以要求 SIS900 开始接收封包。下面二行程序码便是用来做这件事。

outl((RxSOVR|RxORN|RxERR|RxOK|TxURN|TxERR|TxIDLE), ioaddr + imr);
outl(RxENA, ioaddr + cr);
outl(IE, ioaddr + ier);

第一行设定硬件在下列情况发出一个系统中断,
接收失败时
接收成功 时
传送失败时
所有缓冲区中的资料都传送完时
第二行则告诉硬件操作系统己经准备好要接收资料了。第三行则时硬件实际开始送出中断。
在这个函数的最后,我们安装一个每秒执行五次的 timer 。在它的处理函数 sis900_timer 中,我们会检查目前的连结状态,这包括了连结的种类 (10/100)和连接的状态 ( 网络卡是否直的被接到网络上去 ) 。
如果各位用过 Window 2000 ,另人印象最深刻的是当你将网络线拔出时, GUI 会自动警言网络己经中断。其实 Linux 也可以做到这件事,只是你需要一个比较好的图形界面就是了。
传送一个封包的 descriptor 给网络卡




欢迎光临 电子技术论坛_中国专业的电子工程师学习交流社区-中电网技术论坛 (http://bbs.eccn.com/) Powered by Discuz! 7.0.0