驱动 |
Linux系统支持多种设备,这些设备的驱动程序之间有一些共同的特点:
* 内核代码:设备驱动程序是系统内核的一部分,所以如果驱动程序出现错误的话,将可能严重地破坏整个系统。
* 内核接口:设备驱动程序必须为系统内核或者它们的子系统提供一个标准的接口。例如,一个终端驱动程序必须为Linux内核提供一个文件I/O接口;一个SCSI设备驱动程序应该为SCSI子系统提供一个SCSI设备接口,同时SCSI子系统也应为系统内核提供文件I/O和缓冲区。
* 内核机制和服务:设备驱动程序利用一些标准的内核服务,例如内存分配等。
* 可装入:大多数的Linux设备驱动程序都可以在需要时装入内核,在不需要时卸载。
* 可设置:Linux系统设备驱动程序可以集成为系统内核的一部分,至于哪一部分需要集成到内核中,可以在系统编译时设置。
申明:这份文档是按照自由软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。
几乎每一种外设都是通过读写设备上的寄存器来进行的。外设寄存器也称为“I/O端口”,通常包括:控制寄存器、状态寄存器和数据寄存器三大类,而且一个外设的寄存器通常被连续地编址。CPU对外设IO端口物理地址的编址方式有两种:一种是I/O映射方式(I/O-mapped),另一种是内存映射方式(Memory-mapped)。而具体采用哪一种则取决于CPU的体系结构。
有些体系结构的CPU(如,PowerPC、m68k等)通常只实现一个物理地址空间(RAM)。在这种情况下,外设I/O端口的物理地址就被映射到CPU的单一物理地址空间中,而成为内存的一部分。此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。这就是所谓的“内存映射方式”(Memory-mapped)。
而另外一些体系结构的CPU(典型地如X86)则为外设专门实现了一个单独地地址空间,称为“I/O地址空间”或者“I/O端口空间”。这是一个与CPU地RAM物理地址空间不同的地址空间,所有外设的I/O端口均在这一空间中进行编址。CPU通过设立专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元(也即I/O端口)。这就是所谓的“I/O映射方式”(I/O-mapped)。与RAM物理地址空间相比,I/O地址空间通常都比较小,如x86 CPU的I/O空间就只有64KB(0-0xffff)。这是“I/O映射方式”的一个主要缺点。
Linux将基于I/O映射方式的或内存映射方式的I/O端口通称为“I/O区域”(I/O region)。在讨论对I/O区域的管理之前,我们首先来分析一下Linux是如何实现“I/O资源”这一抽象概念的。
3.1 Linux对I/O资源的描述
Linux设计了一个通用的数据结构resource来描述各种I/O资源(如:I/O端口、外设内存、DMA和IRQ等)。该结构定义在include/linux/ioport.h头文件中:
struct resource {
const char *name;
unsigned long start, end;
unsigned long flags;
struct resource *parent, *sibling, *child;
};
各成员的含义如下:
1. name指针:指向此资源的名称。
2. start和end:表示资源的起始物理地址和终止物理地址。它们确定了资源的范围,也即是一个闭区间[start,end]。
3. flags:描述此资源属性的标志(见下面)。
4. 指针parent、sibling和child:分别为指向父亲、兄弟和子资源的指针。
属性flags是一个unsigned long类型的32位标志值,用以描述资源的属性。比如:资源的类型、是否只读、是否可缓存,以及是否已被占用等。下面是一部分常用属性标志位的定义(ioport.h):
/*
* IO resources have these defined flags.
*/
#define IORESOURCE_BITS 0x000000ff /* Bus-specific bits */
#define IORESOURCE_IO 0x00000100 /* Resource type */
#define IORESOURCE_MEM 0x00000200
#define IORESOURCE_IRQ 0x00000400
#define IORESOURCE_DMA 0x00000800
#define IORESOURCE_PREFETCH 0x00001000 /* No side effects */
#define IORESOURCE_READONLY 0x00002000
#define IORESOURCE_CACHEABLE 0x00004000
#define IORESOURCE_RANGELENGTH 0x00008000
#define IORESOURCE_SHADOWABLE 0x00010000
#define IORESOURCE_BUS_HAS_VGA 0x00080000
#define IORESOURCE_UNSET 0x20000000
#define IORESOURCE_AUTO 0x40000000
#define IORESOURCE_BUSY 0x80000000
/* Driver has marked this resource busy */
指针parent、sibling和child的设置是为了以一种树的形式来管理各种I/O资源。
3.2 Linux对I/O资源的管理
Linux是以一种倒置的树形结构来管理每一类I/O资源(如:I/O端口、外设内存、DMA和IRQ)的。每一类I/O资源都对应有一颗倒置的资源树,树中的每一个节点都是一个resource结构,而树的根结点root则描述了该类资源的整个资源空间。
基于上述这个思想,Linux在kernel/Resource.c文件中实现了对资源的申请、释放及查找等操作。
3.2.1 I/O资源的申请
假设某类资源有如下这样一颗资源树:
节点root、r1、r2和r3实际上都是一个resource结构类型。子资源r1、r2和r3通过sibling指针链接成一条单向非循环链表,其表头由root节点中的child指针定义,因此也称为父资源的子资源链表。r1、r2和r3的parent指针均指向他们的父资源节点,在这里也就是图中的root节点。
假设想在root节点中分配一段I/O资源(由图中的阴影区域表示)。函数request_resource()实现这一功能。它有两个参数:①root指针,表示要在哪个资源根节点中进行分配;②new指针,指向描述所要分配的资源(即图中的阴影区域)的resource结构。该函数的源代码如下(kernel/resource.c):
int request_resource(struct resource *root, struct resource *new)
{
struct resource *conflict;
write_lock(&resource_lock);
conflict = __request_resource(root, new);
write_unlock(&resource_lock);
return conflict ? -EBUSY : 0;
}
对上述函数的NOTE如下:
①资源锁resource_lock对所有资源树进行读写保护,任何代码段在访问某一颗资源树之前都必须先持有该锁。其定义如下(kernel/Resource.c):
static rwlock_t resource_lock = RW_LOCK_UNLOCKED;
②可以看出,函数实际上是通过调用内部静态函数__request_resource()来完成实际的资源分配工作。如果该函数返回非空指针,则表示有资源冲突;否则,返回NULL就表示分配成功。
③最后,如果conflict指针为NULL,则request_resource()函数返回返回值0,表示成功;否则返回-EBUSY表示想要分配的资源已被占用。
函数__request_resource()完成实际的资源分配工作。如果参数new所描述的资源中的一部分或全部已经被其它节点所占用,则函数返回与new相冲突的resource结构的指针。否则就返回NULL。该函数的源代码如下
(kernel/Resource.c):
/* Return the conflict entry if you can't request it */
static struct resource * __request_resource
(struct resource *root, struct resource *new)
{
unsigned long start = new->start;
unsigned long end = new->end;
struct resource *tmp, **p;
if (end < start)
return root;
if (start < root->start)
return root;
if (end > root->end)
return root;
p = &root->child;
for (;;) {
tmp = *p;
if (!tmp || tmp->start > end) {
new->sibling = tmp;
*p = new;
new->parent = root;
return NULL;
}
p = &tmp->sibling;
if (tmp->end < start)
continue;
return tmp;
}
}
对函数的NOTE:
①前三个if语句判断new所描述的资源范围是否被包含在root内,以及是否是一段有效的资源(因为end必须大于start)。否则就返回root指针,表示与根结点相冲突。
②接下来用一个for循环遍历根节点root的child链表,以便检查是否有资源冲突,并将new插入到child链表中的合适位置(child链表是以I/O资源物理地址从低到高的顺序排列的)。为此,它用tmp指针指向当前正被扫描的resource结构,用指针p指向前一个resource结构的sibling指针成员变量,p的初始值为指向root->sibling。For循环体的执行步骤如下:
l 让tmp指向当前正被扫描的resource结构(tmp=*p)。
l 判断tmp指针是否为空(tmp指针为空说明已经遍历完整个child链表),或者当前被扫描节点的起始位置start是否比new的结束位置end还要大。只要这两个条件之一成立的话,就说明没有资源冲突,于是就可以把new链入child链表中:①设置new的sibling指针指向当前正被扫描的节点tmp(new->sibling=tmp);②当前节点tmp的前一个兄弟节点的sibling指针被修改为指向new这个节点(*p=new);③将new的parent指针设置为指向root。然后函数就可以返回了(返回值NULL表示没有资源冲突)。
l 如果上述两个条件都不成立,这说明当前被扫描节点的资源域有可能与new相冲突(实际上就是两个闭区间有交集),因此需要进一步判断。为此它首先修改指针p,让它指向tmp->sibling,以便于继续扫描child链表。然后,判断tmp->end是否小于new->start,如果小于,则说明当前节点tmp和new没有资源冲突,因此执行continue语句,继续向下扫描child链表。否则,如果tmp->end大于或等于new->start,则说明tmp->[start,end]和new->[start,end]之间有交集。所以返回当前节点的指针tmp,表示发生资源冲突。
3.2.2 资源的释放
函数release_resource()用于实现I/O资源的释放。该函数只有一个参数——即指针old,它指向所要释放的资源。起源代码如下:
int release_resource(struct resource *old)
{
int retval;
write_lock(&resource_lock);
retval = __release_resource(old);
write_unlock(&resource_lock);
return retval;
}
可以看出,它实际上通过调用__release_resource()这个内部静态函数来完成实际的资源释放工作。函数__release_resource()的主要任务就是将资源区域old(如果已经存在的话)从其父资源的child链表重摘除,它的源代码如下:
static int __release_resource(struct resource *old)
{
struct resource *tmp, **p;
p = &old->parent->child;
for (;;) {
tmp = *p;
if (!tmp)
break;
if (tmp == old) {
*p = tmp->sibling;
old->parent = NULL;
return 0;
}
p = &tmp->sibling;
}
return -EINVAL;
}
对上述函数代码的NOTE如下:
同函数__request_resource()相类似,该函数也是通过一个for循环来遍历父资源的child链表。为此,它让tmp指针指向当前被扫描的资源,而指针p则指向当前节点的前一个节点的sibling成员(p的初始值为指向父资源的child指针)。循环体的步骤如下:
①首先,让tmp指针指向当前被扫描的节点(tmp=*p)。
②如果tmp指针为空,说明已经遍历完整个child链表,因此执行break语句推出for循环。由于在遍历过程中没有在child链表中找到参数old所指定的资源节点,因此最后返回错误值-EINVAL,表示参数old是一个无效的值。
③接下来,判断当前被扫描节点是否就是参数old所指定的资源节点。如果是,那就将old从child链表中去除,也即让当前结点tmp的前一个兄弟节点的sibling指针指向tmp的下一个节点,然后将old->parent指针设置为NULL。最后返回0值表示执行成功。
④如果当前被扫描节点不是资源old,那就继续扫描child链表中的下一个元素。因此将指针p指向tmp->sibling成员。
3.2.3 检查资源是否已被占用,
函数check_resource()用于实现检查某一段I/O资源是否已被占用。其源代码如下:
int check_resource(struct resource *root, unsigned long start, unsigned long len)
{
struct resource *conflict, tmp;
tmp.start = start;
tmp.end = start + len - 1;
write_lock(&resource_lock);
conflict = __request_resource(root, &tmp);
if (!conflict)
__release_resource(&tmp);
write_unlock(&resource_lock);
return conflict ? -EBUSY : 0;
}
对该函数的NOTE如下:
①构造一个临时资源tmp,表示所要检查的资源[start,start+end-1]。
②调用__request_resource()函数在根节点root申请tmp所表示的资源。如果tmp所描述的资源还被人使用,则该函数返回NULL,否则返回非空指针。因此接下来在conflict为NULL的情况下,调用__release_resource()将刚刚申请的资源释放掉。
③最后根据conflict是否为NULL,返回-EBUSY或0值。
3.2.4 寻找可用资源
函数find_resource()用于在一颗资源树中寻找未被使用的、且满足给定条件的(也即资源长度大小为size,且在[min,max]区间内)的资源。其函数源代码如下:
/*
* Find empty slot in the resource tree given range and alignment.
*/
static int find_resource(struct resource *root, struct resource *new,
unsigned long size,
unsigned long min, unsigned long max,
unsigned long align,
void (*alignf)(void *, struct resource *, unsigned long),
void *alignf_data)
{
struct resource *this = root->child;
new->start = root->start;
for(;;) {
if (this)
new->end = this->start;
else
new->end = root->end;
if (new->start < min)
new->start = min;
if (new->end > max)
new->end = max;
new->start = (new->start + align - 1) & ~(align - 1);
if (alignf)
alignf(alignf_data, new, size);
if (new->start < new->end && new->end - new->start + 1 >= size)
{
new->end = new->start + size - 1;
return 0;
}
if (!this)
break;
new->start = this->end + 1;
this = this->sibling;
}
return -EBUSY;
}
对该函数的NOTE如下:
同样,该函数也要遍历root的child链表,以寻找未被使用的资源空洞。为此,它让this指针表示当前正被扫描的子资源节点,其初始值等于root->child,即指向child链表中的第一个节点,并让new->start的初始值等于root->start,然后用一个for循环开始扫描child链表,对于每一个被扫描的节点,循环体执行如下操作:
①首先,判断this指针是否为NULL。如果不为空,就让new->end等于this->start,也即让资源new表示当前资源节点this前面那一段未使用的资源区间。
②如果this指针为空,那就让new->end等于root->end。这有两层意思:第一种情况就是根结点的child指针为NULL(即根节点没有任何子资源)。因此此时先暂时将new->end放到最大。第二种情况就是已经遍历完整个child链表,所以此时就让new表示最后一个子资源后面那一段未使用的资源区间。
③根据参数min和max修正new->[start,end]的值,以使资源new被包含在[min,max]区域内。
④接下来进行对齐操作。
⑤然后,判断经过上述这些步骤所形成的资源区域new是否是一段有效的资源(end必须大于或等于start),而且资源区域的长度满足size参数的要求(end-start+1>=size)。如果这两个条件均满足,则说明我们已经找到了一段满足条件的资源空洞。因此在对new->end的值进行修正后,然后就可以返回了(返回值0表示成功)。
⑥如果上述两条件不能同时满足,则说明还没有找到,因此要继续扫描链表。在继续扫描之前,我们还是要判断一下this指针是否为空。如果为空,说明已经扫描完整个child链表,因此就可以推出for循环了。否则就将new->start的值修改为this->end+1,并让this指向下一个兄弟资源节点,从而继续扫描链表中的下一个子资源节点。
3.2.5 分配接口allocate_resource()
在find_resource()函数的基础上,函数allocate_resource()实现:在一颗资源树中分配一条指定大小的、且包含在指定区域[min,max]中的、未使用资源区域。其源代码如下:
/*
* Allocate empty slot in the resource tree given range and alignment.
*/
int allocate_resource(struct resource *root, struct resource *new,
unsigned long size,
unsigned long min, unsigned long max,
unsigned long align,
void (*alignf)(void *, struct resource *, unsigned long),
void *alignf_data)
{
int err;
write_lock(&resource_lock);
err = find_resource(root, new, size, min, max, align, alignf, alignf_data);
if (err >= 0 && __request_resource(root, new))
err = -EBUSY;
write_unlock(&resource_lock);
return err;
}
3.2.6 获取资源的名称列表
函数get_resource_list()用于获取根节点root的子资源名字列表。该函数主要用来支持/proc/文件系统(比如实现proc/ioports文件和/proc/iomem文件)。其源代码如下:
int get_resource_list(struct resource *root, char *buf, int size)
{
char *fmt;
int retval;
fmt = " %08lx-%08lx : %s
";
if (root->end < 0x10000)
fmt = " %04lx-%04lx : %s
";
read_lock(&resource_lock);
retval = do_resource_list(root->child, fmt, 8, buf, buf + size) - buf;
read_unlock(&resource_lock);
return retval;
}
可以看出,该函数主要通过调用内部静态函数do_resource_list()来实现其功能,其源代码如下:
/*
* This generates reports for /proc/ioports and /proc/iomem
*/
static char * do_resource_list(struct resource *entry, const char *fmt,
int offset, char *buf, char *end)
{
if (offset < 0)
offset = 0;
while (entry) {
const char *name = entry->name;
unsigned long from, to;
if ((int) (end-buf) < 80)
return buf;
from = entry->start;
to = entry->end;
if (!name)
name = "";
buf += sprintf(buf, fmt + offset, from, to, name);
if (entry->child)
buf = do_resource_list(entry->child, fmt, offset-2, buf, end);
entry = entry->sibling;
}
return buf;
}
函数do_resource_list()主要通过一个while{}循环以及递归嵌套调用来实现,较为简单,这里就不在详细解释了。
3.3 管理I/O Region资源
Linux将基于I/O映射方式的I/O端口和基于内存映射方式的I/O端口资源统称为“I/O区域”(I/O Region)。I/O Region仍然是一种I/O资源,因此它仍然可以用resource结构类型来描述。下面我们就来看看Linux是如何管理I/O Region的。
3.3.1 I/O Region的分配
在函数__request_resource()的基础上,Linux实现了用于分配I/O区域的函数__request_region(),如下:
struct resource * __request_region(struct resource *parent,
unsigned long start, unsigned long n, const char *name)
{
struct resource *res = kmalloc(sizeof(*res), GFP_KERNEL);
if (res) {
memset(res, 0, sizeof(*res));
res->name = name;
res->start = start;
res->end = start + n - 1;
res->flags = IORESOURCE_BUSY;
write_lock(&resource_lock);
for (;;) {
struct resource *conflict;
conflict = __request_resource(parent, res);
if (!conflict)
break;
if (conflict != parent) {
parent = conflict;
if (!(conflict->flags & IORESOURCE_BUSY))
continue;
}
/* Uhhuh, that didn't work out.. */
kfree(res);
res = NULL;
break;
}
write_unlock(&resource_lock);
}
return res;
}
NOTE:
①首先,调用kmalloc()函数在SLAB分配器缓存中分配一个resource结构。
②然后,相应的根据参数值初始化所分配的resource结构。注意!flags成员被初始化为IORESOURCE_BUSY。
③接下来,用一个for循环开始进行资源分配,循环体的步骤如下:
l 首先,调用__request_resource()函数进行资源分配。如果返回NULL,说明分配成功,因此就执行break语句推出for循环,返回所分配的resource结构的指针,函数成功地结束。
l 如果__request_resource()函数分配不成功,则进一步判断所返回的冲突资源节点是否就是父资源节点parent。如果不是,则将分配行为下降一个层次,即试图在当前冲突的资源节点中进行分配(只有在冲突的资源节点没有设置IORESOURCE_BUSY的情况下才可以),于是让parent指针等于conflict,并在conflict->flags&IORESOURCE_BUSY为0的情况下执行continue语句继续for循环。
l 否则如果相冲突的资源节点就是父节点parent,或者相冲突资源节点设置了IORESOURCE_BUSY标志位,则宣告分配失败。于是调用kfree()函数释放所分配的resource结构,并将res指针置为NULL,最后用break语句推出for循环。
④最后,返回所分配的resource结构的指针。
3.3.2 I/O Region的释放
函数__release_region()实现在一个父资源节点parent中释放给定范围的I/O Region。实际上该函数的实现思想与__release_resource()相类似。其源代码如下:
void __release_region(struct resource *parent,
unsigned long start, unsigned long n)
{
struct resource **p;
unsigned long end;
p = &parent->child;
end = start + n - 1;
for (;;) {
struct resource *res = *p;
if (!res)
break;
if (res->start <= start && res->end >= end) {
if (!(res->flags & IORESOURCE_BUSY)) {
p = &res->child;
continue;
}
if (res->start != start' 'res->end != end)
break;
*p = res->sibling;
kfree(res);
return;
}
p = &res->sibling;
}
printk("Trying to free nonexistent resource <%08lx-%08lx>
", start, end);
}
类似地,该函数也是通过一个for循环来遍历父资源parent的child链表。为此,它让指针res指向当前正被扫描的子资源节点,指针p指向前一个子资源节点的sibling成员变量,p的初始值为指向parent->child。For循环体的步骤如下:
①让res指针指向当前被扫描的子资源节点(res=*p)。
②如果res指针为NULL,说明已经扫描完整个child链表,所以退出for循环。
③如果res指针不为NULL,则继续看看所指定的I/O区域范围是否完全包含在当前资源节点中,也即看看[start,start+n-1]是否包含在res->[start,end]中。如果不属于,则让p指向当前资源节点的sibling成员,然后继续for循环。如果属于,则执行下列步骤:
l 先看看当前资源节点是否设置了IORESOURCE_BUSY标志位。如果没有设置该标志位,则说明该资源节点下面可能还会有子节点,因此将扫描过程下降一个层次,于是修改p指针,使它指向res->child,然后执行continue语句继续for循环。
l 如果设置了IORESOURCE_BUSY标志位。则一定要确保当前资源节点就是所指定的I/O区域,然后将当前资源节点从其父资源的child链表中去除。这可以通过让前一个兄弟资源节点的sibling指针指向当前资源节点的下一个兄弟资源节点来实现(即让*p=res->sibling),最后调用kfree()函数释放当前资源节点的resource结构。然后函数就可以成功返回了。
3.3.3 检查指定的I/O Region是否已被占用
函数__check_region()检查指定的I/O Region是否已被占用。其源代码如下:
int __check_region(struct resource *parent, unsigned long start, unsigned long n)
{
struct resource * res;
res = __request_region(parent, start, n, "check-region");
if (!res)
return -EBUSY;
release_resource(res);
kfree(res);
return 0;
}
该函数的实现与__check_resource()的实现思想类似。首先,它通过调用__request_region()函数试图在父资源parent中分配指定的I/O Region。如果分配不成功,将返回NULL,因此此时函数返回错误值-EBUSY表示所指定的I/O Region已被占用。如果res指针不为空则说明所指定的I/O Region没有被占用。于是调用__release_resource()函数将刚刚分配的资源释放掉(实际上是将res结构从parent的child链表去除),然后调用kfree()函数释放res结构所占用的内存。最后,返回0值表示指定的I/O Region没有被占用。
3.4 管理I/O端口资源
我们都知道,采用I/O映射方式的X86处理器为外设实现了一个单独的地址空间,也即“I/O空间”(I/O Space)或称为“I/O端口空间”,其大小是64KB(0x0000-0xffff)。Linux在其所支持的所有平台上都实现了“I/O端口空间”这一概念。
由于I/O空间非常小,因此即使外设总线有一个单独的I/O端口空间,却也不是所有的外设都将其I/O端口(指寄存器)映射到“I/O端口空间”中。比如,大多数PCI卡都通过内存映射方式来将其I/O端口或外设内存映射到CPU的RAM物理地址空间中。而老式的ISA卡通常将其I/O端口映射到I/O端口空间中。
Linux是基于“I/O Region”这一概念来实现对I/O端口资源(I/O-mapped 或 Memory-mapped)的管理的。
3.4.1 资源根节点的定义
Linux在kernel/Resource.c文件中定义了全局变量ioport_resource和iomem_resource,来分别描述基于I/O映射方式的整个I/O端口空间和基于内存映射方式的I/O内存资源空间(包括I/O端口和外设内存)。其定义如下:
struct resource ioport_resource =
{ "PCI IO", 0x0000, IO_SPACE_LIMIT, IORESOURCE_IO };
struct resource iomem_resource =
{ "PCI mem", 0x00000000, 0xffffffff, IORESOURCE_MEM };
其中,宏IO_SPACE_LIMIT表示整个I/O空间的大小,对于X86平台而言,它是0xffff(定义在include/asm-i386/io.h头文件中)。显然,I/O内存空间的大小是4GB。
3.4.2 对I/O端口空间的操作
基于I/O Region的操作函数__XXX_region(),Linux在头文件include/linux/ioport.h中定义了三个对I/O端口空间进行操作的宏:①request_region()宏,请求在I/O端口空间中分配指定范围的I/O端口资源。②check_region()宏,检查I/O端口空间中的指定I/O端口资源是否已被占用。③release_region()宏,释放I/O端口空间中的指定I/O端口资源。这三个宏的定义如下:
#define request_region(start,n,name)
__request_region(&ioport_resource, (start), (n), (name))
#define check_region(start,n)
__check_region(&ioport_resource, (start), (n))
#define release_region(start,n)
__release_region(&ioport_resource, (start), (n))
其中,宏参数start指定I/O端口资源的起始物理地址(是I/O端口空间中的物理地址),宏参数n指定I/O端口资源的大小。
3.4.3 对I/O内存资源的操作
基于I/O Region的操作函数__XXX_region(),Linux在头文件include/linux/ioport.h中定义了三个对I/O内存资源进行操作的宏:①request_mem_region()宏,请求分配指定的I/O内存资源。②check_ mem_region()宏,检查指定的I/O内存资源是否已被占用。③release_ mem_region()宏,释放指定的I/O内存资源。这三个宏的定义如下:
#define request_mem_region(start,n,name)
__request_region(&iomem_resource, (start), (n), (name))
#define check_mem_region(start,n)
__check_region(&iomem_resource, (start), (n))
#define release_mem_region(start,n)
__release_region(&iomem_resource, (start), (n))
其中,参数start是I/O内存资源的起始物理地址(是CPU的RAM物理地址空间中的物理地址),参数n指定I/O内存资源的大小。
3.4.4 对/proc/ioports和/proc/iomem的支持
Linux在ioport.h头文件中定义了两个宏:
get_ioport_list()和get_iomem_list(),分别用来实现/proc/ioports文件和/proc/iomem文件。其定义如下:
#define get_ioport_list(buf) get_resource_list(&ioport_resource, buf, PAGE_SIZE)
#define get_mem_list(buf) get_resource_list(&iomem_resource, buf, PAGE_SIZE)
3.5 访问I/O端口空间
在驱动程序请求了I/O端口空间中的端口资源后,它就可以通过CPU的IO指定来读写这些I/O端口了。在读写I/O端口时要注意的一点就是,大多数平台都区分8位、16位和32位的端口,也即要注意I/O端口的宽度。
Linux在include/asm/io.h头文件(对于i386平台就是include/asm-i386/io.h)中定义了一系列读写不同宽度I/O端口的宏函数。如下所示:
⑴读写8位宽的I/O端口
unsigned char inb(unsigned port);
void outb(unsigned char value,unsigned port);
其中,port参数指定I/O端口空间中的端口地址。在大多数平台上(如x86)它都是unsigned short类型的,其它的一些平台上则是unsigned int类型的。显然,端口地址的类型是由I/O端口空间的大小来决定的。
⑵读写16位宽的I/O端口
unsigned short inw(unsigned port);
void outw(unsigned short value,unsigned port);
⑶读写32位宽的I/O端口
unsigned int inl(unsigned port);
void outl(unsigned int value,unsigned port);
3.5.1 对I/O端口的字符串操作
除了上述这些“单发”(single-shot)的I/O操作外,某些CPU也支持对某个I/O端口进行连续的读写操作,也即对单个I/O端口读或写一系列字节、字或32位整数,这就是所谓的“字符串I/O指令”(String Instruction)。这种指令在速度上显然要比用循环来实现同样的功能要快得多。
Linux同样在io.h文件中定义了字符串I/O读写函数:
⑴8位宽的字符串I/O操作
void insb(unsigned port,void * addr,unsigned long count);
void outsb(unsigned port ,void * addr,unsigned long count);
⑵16位宽的字符串I/O操作
void insw(unsigned port,void * addr,unsigned long count);
void outsw(unsigned port ,void * addr,unsigned long count);
⑶32位宽的字符串I/O操作
void insl(unsigned port,void * addr,unsigned long count);
void outsl(unsigned port ,void * addr,unsigned long count);
3.5.2 Pausing I/O
在一些平台上(典型地如X86),对于老式总线(如ISA)上的慢速外设来说,如果CPU读写其I/O端口的速度太快,那就可能会发生丢失数据的现象。对于这个问题的解决方法就是在两次连续的I/O操作之间插入一段微小的时延,以便等待慢速外设。这就是所谓的“Pausing I/O”。
对于Pausing I/O,Linux也在io.h头文件中定义了它的I/O读写函数,而且都以XXX_p命名,比如:inb_p()、outb_p()等等。下面我们就以out_p()为例进行分析。
将io.h中的宏定义__OUT(b,”b”char)展开后可得如下定义:
extern inline void outb(unsigned char value, unsigned short port) {
__asm__ __volatile__ ("outb %" "b " "0,%" "w" "1"
: : "a" (value), "Nd" (port));
}
extern inline void outb_p(unsigned char value, unsigned short port) {
__asm__ __volatile__ ("outb %" "b " "0,%" "w" "1"
__FULL_SLOW_DOWN_IO
: : "a" (value), "Nd" (port));
}
可以看出,outb_p()函数的实现中被插入了宏__FULL_SLOWN_DOWN_IO,以实现微小的延时。宏__FULL_SLOWN_DOWN_IO在头文件io.h中一开始就被定义:
#ifdef SLOW_IO_BY_JUMPING
#define __SLOW_DOWN_IO "
jmp 1f
1: jmp 1f
1:"
#else
#define __SLOW_DOWN_IO "
outb %%al,$0x80"
#endif
#ifdef REALLY_SLOW_IO
#define __FULL_SLOW_DOWN_IO __SLOW_DOWN_IO
__SLOW_DOWN_IO __SLOW_DOWN_IO __SLOW_DOWN_IO
#else
#define __FULL_SLOW_DOWN_IO __SLOW_DOWN_IO
#endif
显然,__FULL_SLOW_DOWN_IO就是一个或四个__SLOW_DOWN_IO(根据是否定义了宏REALLY_SLOW_IO来决定),而宏__SLOW_DOWN_IO则被定义成毫无意义的跳转语句或写端口0x80的操作(根据是否定义了宏SLOW_IO_BY_JUMPING来决定)。
3.6 访问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内存资源。
3.6.1 映射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文件中。具体实现可参考《情景分析》一书。
3.6.2 读写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内存资源,而不应该通过指向核心虚地址的指针来访问。
字符设备接口支持面向字符的I/O操作,它不经过系统的快速缓存,所以它们负责管理自己的缓冲区结构。字符设备接口只支持顺序存取的功能,一般不能进行任意长度的I/O请求,而是限制I/O请求的长度必须是设备要求的基本块长的倍数。显然,本程序所驱动的串行卡只能提供顺序存取的功能,属于是字符设备,因此后面的讨论在两种设备有所区别时都只涉及字符型设备接口。设备由一个主设备号和一个次设备号标识。主设备号唯一标识了设备类型,即设备驱动程序类型,它是块设备表或字符设备表中设备表项的索引。次设备号仅由设备驱动程序解释,一般用于识别在若干可能的硬件设备中,I/O请求所涉及到的那个设备。
设备驱动程序可以分为三个主要组成部分:
(1) 自动配置和初始化子程序,负责检测所要驱动的硬件设备是否存在和是否能正常工作。如果该设备正常,则对这个设备及其相关的、设备驱动程序需要的软件状态进行初始化。这部分驱动程序仅在初始化的时候被调用一次。
(2) 服务于I/O请求的子程序,又称为驱动程序的上半部分。调用这部分是由于系统调用的结果。这部分程序在执行的时候,系统仍认为是和进行调用的进程属于同一个进程,只是由用户态变成了核心态,具有进行此系统调用的用户程序的运行环境,因此可以在其中调用sleep()等与进程运行环境有关的函数。
(3) 中断服务子程序,又称为驱动程序的下半部分。在UNIX系统中,并不是直接从中断向量表中调用设备驱动程序的中断服务子程序,而是由UNIX系统来接收硬件中断,再由系统调用中断服务子程序。中断可以产生在任何一个进程运行的时候,因此在中断服务程序被调用的时候,不能依赖于任何进程的状态,也就不能调用任何与进程运行环境有关的函数。因为设备驱动程序一般支持同一类型的若干设备,所以一般在系统调用中断服务子程序的时候,都带有一个或多个参数,以唯一标识请求服务的设备。
在系统内部,I/O设备的存取通过一组固定的入口点来进行,这组入口点是由每个设备的设备驱动程序提供的。一般来说,字符型设备驱动程序能够提供如下几个入口点:
(1) open入口点。打开设备准备I/O操作。对字符特别设备文件进行打开操作,都会调用设备的open入口点。open子程序必须对将要进行的I/O操作做好必要的准备工作,如清除缓冲区等。如果设备是独占的,即同一时刻只能有一个程序访问此设备,则open子程序必须设置一些标志以表示设备处于忙状态。
(2) close入口点。关闭一个设备。当最后一次使用设备终结后,调用close子程序。独占设备必须标记设备可再次使用。
(3) read入口点。从设备上读数据。对于有缓冲区的I/O操作,一般是从缓冲区里读数据。对字符特别设备文件进行读操作将调用read子程序。
(4) write入口点。往设备上写数据。对于有缓冲区的I/O操作,一般是把数据写入缓冲区里。对字符特别设备文件进行写操作将调用write子程序。
(5) ioctl入口点。执行读、写之外的操作。
(6) select入口点。检查设备,看数据是否可读或设备是否可用于写数据。select系统调用在检查与设备特别文件相关的文件描述符时使用select入口点。如果设备驱动程序没有提供上述入口点中的某一个,系统会用缺省的子程序来代替。对于不同的系统,也还有一些其它的入口点。
#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操作。
设备驱动程序所提供的入口点,在设备驱动程序初始化的时候向系统进行登记,以便系统在适当的时候调用。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里。
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 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 *)
这个函数会检查是否需要设置标志位,如果需要,则会将“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总线的支持用到较多的函数,并且要小心处理中断。
作为回报,不需要你的介入,系统把一切都配置好了。
if (atomic_read(&skb->users)!=1){
/*判断有几个人用skb. 是会有多出用skb,例如一边运行一边sniff.有些时候会修改skb, 这就要clone,如果这/个skb也被其他人用了.. */
struct sk_buff * skb2 = skb;
skb=skb_clone(skb,GFP_ATOMIC);
if(skb==NULL){
kfree_skb(skb2);
return 0;/*这里系统内存不足,为什么不报错?因为对kernel来说,mem 不够不是错,是会出现的实际情况,. 在这里的处理方式就是把这个包drop调.不loopback了. */
}
kfree_skb(skb2);
}
else
skb_orphan(skb);/*查
skb_orphan ---- orphan a buffer
@skb: buffer to orphan
If a buffer currently has an owner then we
call the owner's destructor function and
make the @skb unowned.The buffer continues
to exist but is no longer charged to its
former owner
那么skb_orphan以后,原来skb所指向的sk_buff
结构如何使用呢?skb是否成了一个空指针?
skb_orphan和kfree_skb有什么本质的区别?
其实这里应该不是free调的.还是可以用的.但是取消
原来的owner的引用而已. */
.
.
.
}
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 给网络卡
sis900_start_xmit(struct sk_buff *skb, struct net_device *net_dev);
这个函数是用来将一个由 skb 描述的网络资料缓冲区送进传送缓冲区中准备传送。其中最重要的程序码为
sis_priv->tx_ring[entry].bufptr = virt_to_bus(skb->data);
sis_priv->tx_ring[entry].cmdsts = (OWN | skb->len);
outl(TxENA, ioaddr + cr);
SIS900 会使用 DMA 由缓冲区中取得封包的资料。由于缓冲区的数目有限,我们必须在缓冲区用完的时后告诉上层的网络协定不要再往下送资料了。在这里我们用下面的程序来做这件事。
if (++sis_priv->cur_tx - sis_priv->dirty_tx < NUM_TX_DESC) {
netif_start_queue(net_dev);
} else {
sis_priv->tx_full = 1;
netif_stop_queue(net_dev);
}
netif_start_queue 用来告诉上层网络协定这个驱动程序还有空的缓冲区可用,请把下一个封包送进来。 netif_stop_queue 则是用来告诉上层网络协定所有的封包都用完了,请不要再送。
接收一个或多个封包
int sis900_rx(struct net_device *net_dev);
这个函式在会在有封包进入系统时被呼叫,因为可能有多于一个的封包在缓冲区之中。这个函数会逐一检查所有的缓冲区,直到遇到一个空的缓冲区为止。当我们发现一个有资料的缓冲区时,我们需要做二件事。首先是告知上层网络协定有一个新的封包进入系统,这件事由下面的程序完成
skb = sis_priv->rx_skbuff[entry];
skb_put(skb, rx_size);
skb->protocol = eth_type_trans(skb, net_dev);
netif_rx(skb);
前三行根据封包的内容更新 skbuff 中的档头。最后一行则是正式通知上层处理封包。
请注意 Linux 为了增加处理效能,在 netif_rx 并不会真的做完整接收封包的动作,而只是将这个封包记下来。真实的动作是在 bottom half 中才去处理。因为如此,原先储存封包的缓冲区暂时不能再被使用,我们必须重新分配一个新的缓冲区供下一个封包使用。下面的程序码是用来取得一个新的缓冲区。
if ((skb = dev_alloc_skb(RX_BUF_SIZE)) == NULL) {
sis_priv->rx_skbuff[entry] = NULL;
sis_priv->rx_ring[entry].cmdsts = 0;
sis_priv->rx_ring[entry].bufptr = 0;
sis_priv->stats.rx_dropped++;
break;
}
skb->dev = net_dev;
sis_priv->rx_skbuff[entry] = skb;
sis_priv->rx_ring[entry].cmdsts = RX_BUF_SIZE;
sis_priv->rx_ring[entry].bufptr = virt_to_bus(skb->tail);
sis_priv->dirty_rx++;
这个函数其馀的部份其实只是用来记录一些统计资料而己。
传送下一个封包
void sis900_finish_xmit (struct net_device *net_dev);
这个函数用来处理传送中断。在收到一个 TX 中断,表示有一个或多数缓冲区中的资料己经传送完成。我们可以把原先的缓冲区释出来供其它的封包使用,并且用下面的程序告诉上层协定可以送新的封包下来了。
if (sis_priv->tx_full && netif_queue_stopped(net_dev) &&
sis_priv->cur_tx - sis_priv->dirty_tx < NUM_TX_DESC - 4) {
sis_priv->tx_full = 0;
netif_wake_queue (net_dev);
}
netif_wake_queue() 会使得上层协定开始传送新的资料下来。
改变装置的设定
int sis900_set_config(struct net_device *dev, struct ifmap *map);
处理由 ifconfig 送来的命令,在驱动程序中我们通常只处理 media type的改变。这个函数会根据 ifconfig 送来的值改变 MII 控制器的 media tyep ,你可以使用
# ifconfig eth0 media 10basT
将目前的输出入界面强迫改到 10basT 。对于某些自动媒体检测做的有问题的switch 而言这可能是必要的设定,但一般而言默认的 auto 是最好的设定。硬件会自动决定要使用那一个界面,使用者完全不必担心,当实体层的设定改变 ( 例如将网络线插到不同的地方 ) ,硬件会自动侦测并改变设定。
void set_rx_mode(struct net_device *net_dev);
改变目前封包过滤器的模式。当你使用
# ifconfig eth0 promisc
# ifconfig eth0 multicast
等命令时会被呼叫。一般而言,驱动程序的默认值是只接受目的位址和网络卡的 MAC address 相同的封包。你可以透过 ifconfig 命令控制驱动程序接受其它种类的封包。结语好了 ! 我己经解析完整个网络卡的驱动程序了。当你了解这个驱动程序后,再去了解其它的驱动程序变成一件很简单的事情。大部份网络驱动程序的架构其实都很类似。事实上, Linux 早期的网络卡驱动程序几乎是由同一个人完成的。而后来的驱动程序也几乎
都以这些驱动程序为蓝本,所以看起来都很类似。你要不要也试著再去读另一个网络驱动程序的源代码呢 ? 也许 你会开始抱怨怎么写驱动程序这么神秘的东西怎么变得如此简单了 !
多馀的一节
这一节多馀的,你不想看就算了 :-) 为了证明网络驱动程序之间有多类似我再简略的trace Intel eepro100 的驱程程序给大家看。不罗唆,马上开始。
初始化
static struct pci_device_id eepro100_pci_tbl[] __devinitdata = {
{ PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82557,
PCI_ANY_ID, PCI_ANY_ID, },
{ PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82559ER,
PCI_ANY_ID, PCI_ANY_ID, },
{ PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_ID1029,
PCI_ANY_ID, PCI_ANY_ID, },
{ PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_ID1030,
PCI_ANY_ID, PCI_ANY_ID, },
{ PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82820FW_4,
PCI_ANY_ID, PCI_ANY_ID, },
{ 0,}
};
MODULE_DEVICE_TABLE(pci, eepro100_pci_tbl);
tatic struct pci_driver eepro100_driver = {
name: "eepro100",
id_table: eepro100_pci_tbl,
probe: eepro100_init_one,
remove: eepro100_remove_one,
#ifdef CONFIG_EEPRO100_PM
suspend: eepro100_suspend,
resume: eepro100_resume,
#endif
};
return pci_module_init(&eepro100_driver);
嗯 ! 一切都不出意类之外,是吧 !
初始化装置
eepro100_init_one()
这个看起来比 SIS900 的复杂多了。不过几个关鉴的函数还是一样,只是它的程序码看起比较乱。 BSD 的人喜欢说 Linux 的程序码太乱 ! 嗯,好像不承认不行 :-) 不过我说它乱的很可爱,行了吧 !
传送封包
speedo_start_xmit(struct sk_buff *skb, struct net_device *dev)
这个函数相似到我不必做任何讲解,也不必有任何文件你就可以知道它在做些什么事了 ! 程序码几乎到了一行对一行的程度 ( 夸张了一点 ! 不过很接近事实。我信相 SIS900 的 driver 是很整个程序 copy 过去再修改的 )
中断处理
void speedo_interrupt(int irq, void *dev_instance, struct pt_regs *regs);
这个函数,我再喜欢 Linux 也不得不抱怨一下了。 Donald Becker 先生,能麻烦程序写的好看一点好吗 ?
基本上,它把 sis900_rx 的内容直接放在中断处理函数之中。不过我想分开还是会清楚一些。
speedo_tx_buffer_gc 基本上就是 sis900_finish_xmit 。下面的程序是不是很眼熟呢 ?
dirty_tx = sp->dirty_tx;
while ((int)(sp->cur_tx - dirty_tx) > 0) {
int entry = dirty_tx % TX_RING_SIZE;
int status = le32_to_cpu(sp->tx_ring[entry].status);
}
连变数名字都很像呢 !
不过 eepro100 的驱动程序没有实作 set_config 的界面,所以你不能用ifconfig 来改变 media type 。不过 eepro100 提供了由模块命令列选项改变的功 能,当然它是不及 set_config 来的方便就是了。
还要再来一个吗 ? 你自己去做吧 !
(By 詹荣开,NUDT dep3)
Copyright ? 2002 by 詹荣开
E-mail:[email protected]
Linux-2.4.0 Version 1.0.0,2002-10-16
关键词:Linux、I/O、ISA总线、设备驱动程序
申明:这份文档是按照自由软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。
你应该已经和文档一起收到一份GNU通用公共许可证(GPL)的副本。如果还没有,写信给:The Free Software Foundation, Inc., 675 Mass Ave, Cambridge,MA02139, USA
欢迎各位指出文档中的错误与疑问。
----------------------------------------------------
DMA是一种无需CPU的参与就可以让外设与系统RAM之间进行双向(to device 或 from device)数据传输的硬件机制。使用DMA可以使系统CPU从实际的I/O数据传输过程中摆脱出来,从而大大提高系统的吞吐率(throughput)。
由于DMA是一种硬件机制,因此它通常与硬件体系结构是相关的,尤其是依赖于外设的总线技术。比如:ISA卡的DMA机制就与PCI卡的DMA机制有区别。本站主要讨论ISA总线的DMA技术。
1.DMA概述
DMA是外设与主存之间的一种数据传输机制。一般来说,外设与主存之间存在两种数据传输方法:(1)Pragrammed I/O(PIO)方法,也即由CPU通过内存读写指令或I/O指令来持续地读写外设的内存单元(8位、16位或32位),直到整个数据传输过程完成。(2)DMA,即由DMA控制器(DMA Controller,简称DMAC)来完成整个数据传输过程。在此期间,CPU可以并发地执行其他任务,当DMA结束后,DMAC通过中断通知CPU数据传输已经结束,然后由CPU执行相应的ISR进行后处理。
DMA技术产生时正是ISA总线在PC中流行的时侯。因此,ISA卡的DMA数据传输是通过ISA总线控制芯片组中的两个级联8237 DMAC来实现的。这种DMA机制也称为“标准DMA”(standard DMA)。标准DMA有时也称为“第三方DMA”(third-party DMA),这是因为:系统DMAC完成实际的传输过程,所以它相对于传输过程的“前两方”(传输的发送者和接收者)来说是“第三方”。
标准DMA技术主要有两个缺点:(1)8237 DMAC的数据传输速度太慢,不能与更高速的总线(如PCI)配合使用。(2)两个8237 DMAC一起只提供了8个DMA通道,这也成为了限制系统I/O吞吐率提升的瓶颈。
鉴于上述两个原因,PCI总线体系结构设计一种成为“第一方DMA”(first-party DMA)的DMA机制,也称为“Bus Mastering”(总线主控)。在这种情况下,进行传输的PCI卡必须取得系统总线的主控权后才能进行数据传输。实际的传输也不借助慢速的ISA DMAC来进行,而是由内嵌在PCI卡中的DMA电路(比传统的ISA DMAC要快)来完成。Bus Mastering方式的DMA可以让PCI外设得到它们想要的传输带宽,因此它比标准DMA功能满足现代高性能外设的要求。
随着计算机外设技术的不断发展,现代能提供更快传输速率的Ultra DMA(UDMA)也已经被广泛使用了。本为随后的篇幅只讨论ISA总线的标准DMA技术在Linux中的实现。记住:ISA卡几乎不使用Bus Mastering模式的DMA;而PCI卡只使用Bus Mastering模式的DMA,它从不使用标准DMA。
2.Intel 8237 DMAC
最初的IBM PC/XT中只有一个8237 DMAC,它提供了4个8位的DMA通道(DMA channel 0-3)。从IBM AT开始,又增加了一个8237 DMAC(提供4个16位的DMA通道,DMA channel 4-7)。两个8237 DMAC一起为系统提供8个DMA通道。与中断控制器8259的级联方式相反,第一个DMAC被级联到第二个DMAC上,通道4被用于DMAC级联,因此它对外设来说是不可用的。第一个DMAC也称为“slave DAMC”,第二个DMAC也称为“Master DMAC”。
下面我们来详细叙述一下Intel 8237这个DMAC的结构。
每个8237 DMAC都提供4个DMA通道,每个DMA通道都有各自的寄存器,而8237本身也有一组控制寄存器,用以控制它所提供的所有DMA通道。
2.1 DMA通道的寄存器
8237 DMAC中的每个DMA通道都有5个寄存器,分别是:当前地址寄存器、当前计数寄存器、地址寄存器(也称为偏移寄存器)、计数寄存器和页寄存器。其中,前两个是8237的内部寄存器,对外部是不可见的。
(1)当前地址寄存器(Current Address Register):每个DMA通道都有一个16位的当前地址寄存器,表示一个DMA传输事务(Transfer Transaction)期间当前DMA传输操作的DMA物理内存地址。在每个DMA传输开始前,8237都会自动地用该通道的Address Register中的值来初始化这个寄存器;在传输事务期间的每次DMA传输操作之后该寄存器的值都会被自动地增加或减小。
(2)当前计数寄存器(Current Count Register):每个每个DMA通道都有一个16位的当前计数寄存器,表示当前DMA传输事务还剩下多少未传输的数据。在每个DMA传输事务开始之前,8237都会自动地用该通道的Count Register中的值来初始化这个寄存器。在传输事务期间的每次DMA传输操作之后该寄存器的值都会被自动地增加或减小(步长为1)。
(3)地址寄存器(Address Register)或偏移寄存器(Offset Register):每个DMA通道都有一个16位的地址寄存器,表示系统RAM中的DMA缓冲区的起始位置在页内的偏移。
(4)计数寄存器(Count Register):每个DMA通道都有一个16位的计数寄存器,表示DMA缓冲区的大小。
(5)页寄存器(Page Register):该寄存器定义了DMA缓冲区的起始位置所在物理页的基地址,即页号。页寄存器有点类似于PC中的段基址寄存器。
2.2 8237 DAMC的控制寄存器
(1)命令寄存器(Command Register)
这个8位的寄存器用来控制8237芯片的操作。其各位的定义如下图所示:
(2)模式寄存器(Mode Register)
用于控制各DMA通道的传输模式,如下所示:
(3)请求寄存器(Request Register)
用于向各DMA通道发出DMA请求。各位的定义如下:
(4)屏蔽寄存器(Mask Register)
用来屏蔽某个DMA通道。当一个DMA通道被屏蔽后,它就不能在服务于DMA请求,直到通道的屏蔽码被清除。各位的定义如下:
上述屏蔽寄存器也称为“单通道屏蔽寄存器”(Single Channel Mask Register),因为它一次只能屏蔽一个通道。此外含有一个屏蔽寄存器,可以实现一次屏蔽所有4个DMA通道,如下:
(5)状态寄存器(Status Register)
一个只读的8位寄存器,表示各DMA通道的当前状态。比如:DMA通道是否正服务于一个DMA请求,或者某个DMA通道上的DMA传输事务已经完成。各位的定义如下:
2.3 8237 DMAC的I/O端口地址
主、从8237 DMAC的各个寄存器都是编址在I/O端口空间的。而且其中有些I/O端口地址对于I/O读、写操作有不同的表示含义。如下表示所示:
Slave DMAC’s I/O port Master DMAC’sI/O port read write
0x000 0x0c0 Channel 0/4 的Address Register
0x001 0x0c1 Channel 0/4的Count Register
0x002 0x0c2 Channel 1/5 的Address Register
0x003 0x0c3 Channel 1/5的Count Register
0x004 0x0c4 Channel 2/6的Address Register
0x005 0x0c5 Channel 2/6的Count Register
0x006 0x0c6 Channel 3/7的Address Register
0x007 0x0c7 Channel 3/7的Count Register
0x008 0x0d0 Status Register Command Register
0x009 0x0d2 Request Register
0x00a 0x0d4 Single Channel Mask Register
0x00b 0x0d6 Mode Register
0x00c 0x0d8 Clear Flip-Flop Register
0x00d 0x0da Temporary Register Reset DMA controller
0x00e 0x0dc Reset all channel masks
0x00f 0x0de all-channels Mask Register
各DMA通道的Page Register在I/O端口空间中的地址如下:
DMA channel Page Register’sI/O port address
0 0x087
1 0x083
2 0x081
3 0x082
4 0x08f
5 0x08b
6 0x089
7 0x08a
注意两点:
1. 各DMA通道的Address Register是一个16位的寄存器,但其对应的I/O端口是8位宽,因此对这个寄存器的读写就需要两次连续的I/O端口读写操作,低8位首先被发送,然后紧接着发送高8位。
2. 各DMA通道的Count Register:这也是一个16位宽的寄存器(无论对于8位DMA还是16位DMA),但相对应的I/O端口也是8位宽,因此读写这个寄存器同样需要两次连续的I/O端口读写操作,而且同样是先发送低8位,再发送高8位。往这个寄存器中写入的值应该是实际要传输的数据长度减1后的值。在DMA传输事务期间,这个寄存器中的值在每次DMA传输操作后都会被减1,因此读取这个寄存器所得到的值将是当前DMA事务所剩余的未传输数据长度减1后的值。当DMA传输事务结束时,该寄存器中的值应该被置为0。
2.4 DMA通道的典型使用
在一个典型的PC机中,某些DMA通道通常被固定地用于一些PC机中的标准外设,如下所示:
Channel Size Usage
0 8-bit Memory Refresh
1 8-bit Free
2 8-bit Floppy Disk Controller
3 8-bit Free
4 16-bit Cascading
5 16-bit Free
6 16-bit Free
7 16-bit Free
2.5 启动一个DMA传输事务的步骤
要启动一个DMA传输事务必须对8237进行编程,其典型步骤如下:
1.通过CLI指令关闭中断。
2.Disable那个将被用于此次DMA传输事务的DMA通道。
3.向Flip-Flop寄存器中写入0值,以重置它。
4.设置Mode Register。
5.设置Page Register。
6.设置Address Register。
7.设置Count Register。
8.Enable那个将被用于此次DMA传输事务的DMA通道。
9.用STI指令开中断。
3 Linux对读写操作8237 DMAC的实现
由于DMAC的各寄存器是在I/O端口空间中编址的,因此读写8237 DMAC是平台相关的。对于x86平台来说,Linux在include/asm-i386/Dma.h头文件中实现了对两个8237 DMAC的读写操作。
3.1 端口地址和寄存器值的宏定义
Linux用宏MAX_DMA_CHANNELS来表示系统当前的DMA通道个数,如下:
#define MAX_DMA_CHANNELS 8
然后,用宏IO_DMA1_BASE和IO_DMA2_BASE来分别表示两个DMAC在I/O端口空间的端口基地址:
#define IO_DMA1_BASE 0x00
/* 8 bit slave DMA, channels 0..3 */
#define IO_DMA2_BASE 0xC0
/* 16 bit master DMA, ch 4(=slave input)..7 */
接下来,Linux定义了DMAC各控制寄存器的端口地址。其中,slave SMAC的各控制寄存器的端口地址定义如下:
#define DMA1_CMD_REG 0x08 /* command register (w) */
#define DMA1_STAT_REG 0x08 /* status register (r) */
#define DMA1_REQ_REG 0x09 /* request register (w) */
#define DMA1_MASK_REG 0x0A /* single-channel mask (w) */
#define DMA1_MODE_REG 0x0B /* mode register (w) */
#define DMA1_CLEAR_FF_REG 0x0C /* clear pointer flip-flop (w) */
#define DMA1_TEMP_REG 0x0D /* Temporary Register (r) */
#define DMA1_RESET_REG 0x0D /* Master Clear (w) */
#define DMA1_CLR_MASK_REG 0x0E /* Clear Mask */
#define DMA1_MASK_ALL_REG 0x0F /* all-channels mask (w) */
Master DMAC的各控制寄存器的端口地址定义如下:
#define DMA2_CMD_REG 0xD0 /* command register (w) */
#define DMA2_STAT_REG 0xD0 /* status register (r) */
#define DMA2_REQ_REG 0xD2 /* request register (w) */
#define DMA2_MASK_REG 0xD4 /* single-channel mask (w) */
#define DMA2_MODE_REG 0xD6 /* mode register (w) */
#define DMA2_CLEAR_FF_REG 0xD8 /* clear pointer flip-flop (w) */
#define DMA2_TEMP_REG 0xDA /* Temporary Register (r) */
#define DMA2_RESET_REG 0xDA /* Master Clear (w) */
#define DMA2_CLR_MASK_REG 0xDC /* Clear Mask */
#define DMA2_MASK_ALL_REG 0xDE /* all-channels mask (w) */
8个DMA通道的Address Register的端口地址定义如下:
#define DMA_ADDR_0 0x00 /* DMA address registers */
#define DMA_ADDR_1 0x02
#define DMA_ADDR_2 0x04
#define DMA_ADDR_3 0x06
#define DMA_ADDR_4 0xC0
#define DMA_ADDR_5 0xC4
#define DMA_ADDR_6 0xC8
#define DMA_ADDR_7 0xCC
8个DMA通道的Count Register的端口地址定义如下:
#define DMA_CNT_0 0x01 /* DMA count registers */
#define DMA_CNT_1 0x03
#define DMA_CNT_2 0x05
#define DMA_CNT_3 0x07
#define DMA_CNT_4 0xC2
#define DMA_CNT_5 0xC6
#define DMA_CNT_6 0xCA
#define DMA_CNT_7 0xCE
8个DMA通道的Page Register的端口地址定义如下:
#define DMA_PAGE_0 0x87 /* DMA page registers */
#define DMA_PAGE_1 0x83
#define DMA_PAGE_2 0x81
#define DMA_PAGE_3 0x82
#define DMA_PAGE_5 0x8B
#define DMA_PAGE_6 0x89
#define DMA_PAGE_7 0x8A
Mode Register的几个常用值的定义如下:
#define DMA_MODE_READ 0x44
/* I/O to memory, no autoinit, increment, single mode */
#define DMA_MODE_WRITE 0x48
/* memory to I/O, no autoinit, increment, single mode */
#define DMA_MODE_CASCADE 0xC0
/* pass thru DREQ->HRQ, DACK<-HLDA only */
#define DMA_AUTOINIT 0x10
3.2 读写DMAC的高层接口函数
(1)使能/禁止一个特定的DMA通道
Single Channel Mask Register中的bit[2]为0表示使能一个DMA通道,为1表示禁止一个DMA通道;而该寄存器中的bit[1:0]则用于表示使能或禁止哪一个DMA通道。
函数enable_dma()实现使能某个特定的DMA通道,传输dmanr指定DMA通道号,其取值范围是0~DMA_MAX_CHANNELS-1。如下:
static __inline__ void enable_dma(unsigned int dmanr)
{
if (dmanr<=3)
dma_outb(dmanr, DMA1_MASK_REG);
else
dma_outb(dmanr & 3, DMA2_MASK_REG);
}
宏dma_outb和dma_inb实际上就是outb(或outb_p)和inb函数。注意,当dmanr取值大于3时,对应的是Master DMAC上的DMA通道0~3,因此在写DMA2_MASK_REG之前,要将dmanr与值3进行与操作,以得到它在master DMAC上的局部通道编号。
函数disable_dma()禁止一个特定的DMA通道,其源码如下:
static __inline__ void disable_dma(unsigned int dmanr)
{
if (dmanr<=3)
dma_outb(dmanr | 4, DMA1_MASK_REG);
else
dma_outb((dmanr & 3) | 4, DMA2_MASK_REG);
}
为禁止某个DMA通道,Single Channel Mask Register中的bit[2]应被置为1。
(2)清除Flip-Flop寄存器
函数Clear_dma_ff()实现对slave/Master DMAC的Flip-Flop寄存器进行清零操作。如下:
static __inline__ void clear_dma_ff(unsigned int dmanr)
{
if (dmanr<=3)
dma_outb(0, DMA1_CLEAR_FF_REG);
else
dma_outb(0, DMA2_CLEAR_FF_REG);
}
(3)设置某个特定DMA通道的工作模式
函数set_dma_mode()实现设置一个特定DMA通道的工作模式。如下:
static __inline__ void set_dma_mode(unsigned int dmanr, char mode)
{
if (dmanr<=3)
dma_outb(mode | dmanr, DMA1_MODE_REG);
else
dma_outb(mode | (dmanr&3), DMA2_MODE_REG);
}
DMAC 的Mode Register中的bit[1:0]指定对该DMAC上的哪一个DMA通道进行模式设置。
(4)为DMA通道设置DMA缓冲区的起始物理地址和大小
由于8237中的DMA通道是通过一个8位的Page Register和一个16位的Address Register来寻址位于系统RAM中的DMA缓冲区,因此8237 DMAC最大只能寻址系统RAM中物理地址在0x000000~0xffffff范围内的DMA缓冲区,也即只能寻址物理内存的低16MB(24位物理地址)。反过来讲,Slave/Master 8237 DMAC又是如何寻址低16MB中的物理内存单元的呢?
首先来看Slave 8237 DMAC(即第一个8237 DMAC)。由于Slave 8237 DMAC是一个8位的DMAC,因此DMA通道0~3在一次DMA传输操作(一个DMA传输事务又多次DMA传输操作组成)中只能传输8位数据,即一个字节。Slave 8237 DMAC将低16MB物理内存分成256个64K大小的页(Page),然后用Page Register来表示内存单元物理地址的高8位(bit[23:16]),也即页号;用Address Register来表示内存单元物理地址在一个Page(64KB大小)内的页内偏移量,也即24位物理地址中的低16位(bit[15:0])。由于这种寻址机制,因此DMA通道0~3的DMA缓冲区必须在一个Page之内,也即DMA缓冲区不能跨越64KB页边界。
再来看看Master 8237 DMAC(即第二个8237 DMAC)。这是一个16位宽的DMAC,因此DMA通道5~7在一次DMA传输操作时可以传输16位数据,也即一个字word。此时DMA通道的Count Register(16位宽)表示以字计的待传输数据块大小,因此数据块最大可达128KB(64K个字),也即系统RAM中的DMA缓冲区最大可达128KB。由于一次可传输一个字,因此Master 8237 DMAC所寻址的内存单元的物理地址肯定是偶数,也即物理地址的bit[0]肯定为0。此时物理内存的低16MB被化分成128个128KB大小的page,Page Register中的bit[7:1]用来表示页号,也即对应内存单元物理地址的bit[23:17],而Page Register的bit[0]总是被设置为0。Address Register用来表示内存单元在128KB大小的Page中的页内偏移,也即对应内存单元物理地址的bit[16:1](由于此时物理地址的bit[0]总是为0,因此不需要表示)。由于Master 8237 DMAC的这种寻址机制,因此DMA通道5~7的DMA缓冲区不能跨越128KB的页边界。
下面我们来看看Linux是如何实现为各DMA通道设置其Page寄存器的。NOTE!DMA通道5~7的Page Register中的bit[0]总是为0。如下所示:
static __inline__ void set_dma_page(unsigned int dmanr, char pagenr)
{
switch(dmanr) {
case 0:
dma_outb(pagenr, DMA_PAGE_0);
break;
case 1:
dma_outb(pagenr, DMA_PAGE_1);
break;
case 2:
dma_outb(pagenr, DMA_PAGE_2);
break;
case 3:
dma_outb(pagenr, DMA_PAGE_3);
break;
case 5:
dma_outb(pagenr & 0xfe, DMA_PAGE_5);
break;
case 6:
dma_outb(pagenr & 0xfe, DMA_PAGE_6);
break;
case 7:
dma_outb(pagenr & 0xfe, DMA_PAGE_7);
break;
}
}
在上述函数的基础上,函数set_dma_addr()用来为特定DMA通道设置DMA缓冲区的基地址,传输dmanr指定DMA通道号,传输a指定位于系统RAM中的DMA缓冲区起始位置的物理地址。如下:
/* Set transfer address & page bits for specific DMA channel.
* Assumes dma flipflop is clear.
*/
static __inline__ void set_dma_addr(unsigned int dmanr, unsigned int a)
{
set_dma_page(dmanr, a>>16);
if (dmanr <= 3) {
dma_outb( a & 0xff, ((dmanr&3)<<1) + IO_DMA1_BASE );
dma_outb( (a>>8) & 0xff, ((dmanr&3)<<1) + IO_DMA1_BASE );
} else {
dma_outb( (a>>1) & 0xff, ((dmanr&3)<<2) + IO_DMA2_BASE );
dma_outb( (a>>9) & 0xff, ((dmanr&3)<<2) + IO_DMA2_BASE );
}
}
函数set_dma_count()为特定DMA通道设置其Count Register的值。传输dmanr指定DMA通道,传输count指定待传输的数据块大小(以字节计),实际写到Count Register中的值应该是count-1。如下所示:
static __inline__ void set_dma_count(unsigned int dmanr, unsigned int count)
{
count--;
if (dmanr <= 3) {
dma_outb( count & 0xff, ((dmanr&3)<<1) + 1 + IO_DMA1_BASE );
dma_outb( (count>>8) & 0xff, ((dmanr&3)<<1) + 1 + IO_DMA1_BASE );
} else {
dma_outb( (count>>1) & 0xff, ((dmanr&3)<<2) + 2 + IO_DMA2_BASE );
dma_outb( (count>>9) & 0xff, ((dmanr&3)<<2) + 2 + IO_DMA2_BASE );
}
}
函数get_dma_residue()获取某个DMA通道上当前DMA传输事务的未传输剩余数据块的大小(以字节计)。DMA通道的Count Register的值在当前DMA传输事务进行期间会不断地自动将减小,直到当前DMA传输事务完成,Count Register的值减小为0。如下:
static __inline__ int get_dma_residue(unsigned int dmanr)
{
unsigned int io_port = (dmanr<=3)? ((dmanr&3)<<1) + 1 + IO_DMA1_BASE
: ((dmanr&3)<<2) + 2 + IO_DMA2_BASE;
/* using short to get 16-bit wrap around */
unsigned short count;
count = 1 + dma_inb(io_port);
count += dma_inb(io_port) << 8;
return (dmanr<=3)? count : (count<<1);
}
3.3 对DMAC的保护
DMAC是一种全局的共享资源,为了保证设备驱动程序对它的独占访问,Linux在kernel/dma.c文件中定义了自旋锁dma_spin_lock来保护它(实际上是保护DMAC的I/O端口资源)。任何想要访问DMAC的设备驱动程序都首先必须先持有自旋锁dma_spin_lock。如下:
static __inline__ unsigned long claim_dma_lock(void)
{
unsigned long flags;
spin_lock_irqsave(&dma_spin_lock, flags); /* 关中断,加锁*/
return flags;
}
static __inline__ void release_dma_lock(unsigned long flags)
{
spin_unlock_irqrestore(&dma_spin_lock, flags);/* 开中断,开锁*/
}
4 Linux对ISA DMA通道资源的管理
DMA通道是一种系统全局资源。任何ISA外设想要进行DMA传输,首先都必须取得某个DMA通道资源的使用权,并在传输结束后释放所使用DMA通道资源。从这个角度看,DMA通道资源是一种共享的独占型资源。
Linux在kernel/Dma.c文件中实现了对DMA通道资源的管理。
4.1 对DMA通道资源的描述
Linux在kernel/Dma.c文件中定义了数据结构dma_chan来描述DMA通道资源。该结构类型的定义如下:
struct dma_chan {
int lock;
const char *device_id;
};
其中,如果成员lock!=0则表示DMA通道正被某个设备所使用;否则该DMA通道就处于free状态。而成员device_id就指向使用该DMA通道的设备名字字符串。
基于上述结构类型dma_chan,Linux定义了全局数组dma_chan_busy[],以分别描述8个DMA通道资源各自的使用状态。如下:
static struct dma_chan dma_chan_busy[MAX_DMA_CHANNELS] = {
{ 0, 0 },
{ 0, 0 },
{ 0, 0 },
{ 0, 0 },
{ 1, "cascade" },
{ 0, 0 },
{ 0, 0 },
{ 0, 0 }
};
显然,在初始状态时除了DMA通道4外,其余DMA通道皆处于free状态。
4.2 DMA通道资源的申请
任何ISA卡在使用某个DMA通道进行DMA传输之前,其设备驱动程序都必须向内核提出DMA通道资源的申请。只有申请获得成功后才能使用相应的DMA通道。否则就会发生资源冲突。
函数request_dma()实现DMA通道资源的申请。其源码如下:
int request_dma(unsigned int dmanr, const char * device_id)
{
if (dmanr >= MAX_DMA_CHANNELS)
return -EINVAL;
if (xchg(&dma_chan_busy[dmanr].lock, 1) != 0)
return -EBUSY;
dma_chan_busy[dmanr].device_id = device_id;
/* old flag was 0, now contains 1 to indicate busy */
return 0;
}
上述函数的核心实现就是用原子操作xchg()让成员变量dma_chan_busy[dmanr].lock和值1进行交换操作,xchg()将返回lock成员在交换操作之前的值。因此:如果xchg()返回非0值,这说明dmanr所指定的DMA通道已被其他设备所占用,所以request_dma()函数返回错误值-EBUSY表示指定DMA通道正忙;否则,如果xchg()返回0值,说明dmanr所指定的DMA通道正处于free状态,于是xchg()将其lock成员设置为1,取得资源的使用权。
4.3 释放DMA通道资源
DMA传输事务完成后,设备驱动程序一定要记得释放所占用的DMA通道资源。否则别的外设将一直无法使用该DMA通道。
函数free_dma()释放指定的DMA通道资源。如下:
void free_dma(unsigned int dmanr)
{
if (dmanr >= MAX_DMA_CHANNELS) {
printk("Trying to free DMA%d
", dmanr);
return;
}
if (xchg(&dma_chan_busy[dmanr].lock, 0) == 0) {
printk("Trying to free free DMA%d
", dmanr);
return;
}
} /* free_dma */
显然,上述函数的核心实现就是用原子操作xchg()将lock成员清零。
4.4 对/proc/dma文件的实现
文件/proc/dma将列出当前8个DMA通道的使用状况。Linux在kernel/Dma.c文件中实现了函数个get_dma_list()函数来至此/proc/dma文件的实现。函数get_dma_list()的实现比较简单。主要就是遍历数组dma_chan_busy[],并将那些lock成员为非零值的数组元素输出到列表中即可。如下:
int get_dma_list(char *buf)
{
int i, len = 0;
for (i = 0 ; i < MAX_DMA_CHANNELS ; i++) {
if (dma_chan_busy[i].lock) {
len += sprintf(buf+len, "%2d: %s
",
i,
dma_chan_busy[i].device_id);
}
}
return len;
} /* get_dma_list */
5 使用DMA的ISA设备驱动程序
DMA虽然是一种硬件机制,但它离不开软件(尤其是设备驱动程序)的配合。任何使用DMA进行数据传输的ISA设备驱动程序都必须遵循一定的框架。
5.1 DMA通道资源的申请与释放
同I/O端口资源类似,设备驱动程序必须在一开始就调用request_dma()函数来向内核申请DMA通道资源的使用权。而且,最好在设备驱动程序的open()方法中完成这个操作,而不是在模块的初始化例程中调用这个函数。因为这在一定程度上可以让多个设备共享DMA通道资源(只要多个设备不同时使用一个DMA通道)。这种共享有点类似于进程对CPU的分时共享:-)
设备使用完DMA通道后,其驱动程序应该记得调用free_dma()函数来释放所占用的DMA通道资源。通常,最好再驱动程序的release()方法中调用该函数,而不是在模块的卸载例程中进行调用。
还需要注意的一个问题是:资源的申请顺序。为了避免死锁(deadlock),驱动程序一定要在申请了中断号资源后才申请DMA通道资源。释放时则要先释放DMA通道,然后再释放中断号资源。
使用DMA的ISA设备驱动程序的open()方法的如下:
int xxx_open(struct inode * inode, struct file * filp)
{
┆
if((err = request_irq(irq,xxx_ISR,SA_INTERRUPT,”YourDeviceName”,NULL))
return err;
if((err = request_dma(dmanr, “YourDeviceName”)){
free_irq(irq, NULL);
return err;
}
┆
return 0;
}
release()方法的范例代码如下:
void xxx_release(struct inode * inode, struct file * filp)
{
┆
free_dma(dmanr);
free_irq(irq,NULL);
┆
}
5.2 申请DMA缓冲区
由于8237 DMAC只能寻址系统RAM中低16MB物理内存,因此:ISA设备驱动程序在申请DMA缓冲区时,一定要以GFP_DMA标志来调用kmalloc()函数或get_free_pages()函数,以便在系统内存的DMA区中分配物理内存。
5.3 编程DMAC
设备驱动程序可以在他的read()方法、write()方法或ISR中对DMAC进行编程,以便准备启动一个DMA传输事务。一个DMA传输事务有两种典型的过程:(1)用户请求设备进行DMA传输;(2)硬件异步地将外部数据写道系统中。
用户通过I/O请求触发设备进行DMA传输的步骤如下:
1.用户进程通过系统调用read()/write()来调用设备驱动程序的read()方法或write()方法,然后由设备驱动程序read/write方法负责申请DMA缓冲区,对DMAC进行编程,以准备启动一个DMA传输事务,最后正确地设置设备(setup device),并将用户进程投入睡眠。
2.DMAC负责在DMA缓冲区和I/O外设之间进行数据传输,并在结束后触发一个中断。
3.设备的ISR检查DMA传输事务是否成功地结束,并将数据从DMA缓冲区中拷贝到驱动程序的其他内核缓冲区中(对于I/O device to memory的情况)。然后唤醒睡眠的用户进程。
硬件异步地将外部数据写到系统中的步骤如下:
1.外设触发一个中断通知系统有新数据到达。
2.ISR申请一个DMA缓冲区,并对DMAC进行编程,以准备启动一个DMA传输事务,最后正确地设置好外设。
3.硬件将外部数据写到DMA缓冲区中,DMA传输事务结束后,触发一个中断。
4. ISR检查DMA传输事务是否成功地结束,然后将DMA缓冲区中的数据拷贝驱动程序的其他内核缓冲区中,最后唤醒相关的等待进程。
网卡就是上述过程的一个典型例子。
为准备一个DMA传输事务而对DMAC进行编程的典型代码段如下:
unsigned long flags;
flags = claim_dma_lock();
disable_dma(dmanr);
clear_dma_ff(dmanr);
set_dma_mode(dmanr,mode);
set_dma_addr(dmanr, virt_to_bus(buf));
set_dma_count(dmanr, count);
enable_dma(dmanr);
release_dma_lock(flags);
检查一个DMA传输事务是否成功地结束的代码段如下:
int residue;
unsigned long flags = claim_dma_lock();
residue = get_dma_residue(dmanr);
release_dma_lock(flags);
ASSERT(residue == 0);
注:本节大部分内容来自于ldd2。 [目录]