Linux 內核驅動簡介

 驅動

 

目 錄

  1. 驅動
    1. I/O端口
    2. from smth
      1. 基本結構
      2. 驅動程序
      3. 具體實現
    3. PCI
    4. loopback
    5. Sis 900
    6. ISA總線DMA的實現


驅動

    Linux系統支持三種類型的硬件設備:字符設備、塊設
<script src="http://pagead2.googlesyndication.com/pagead/show_ads.js" type="text/javascript"></script> <iframe width="120" scrolling="no" height="60" frameborder="0" name="google_ads_frame" src="http://pagead2.googlesyndication.com/pagead/ads?client=ca-pub-2299987709779770&dt=1175945550538&lmt=1175945544&prev_fmts=160x600_as&format=120x60&output=html&url=http%3A%2F%2Fblog.csdn.net%2Ffengyv%2Farchive%2F2006%2F04%2F12%2F659922.aspx&ref=http%3A%2F%2Ftag.csdn.net%2FArticle%2F95473644-6e49-4bb4-871f-aef8cfa0fbb1.html&cc=30&u_h=768&u_w=1024&u_ah=738&u_aw=1024&u_cd=32&u_tz=330&u_his=1&u_java=true&u_nplug=16&u_nmime=59" marginwidth="0" marginheight="0" vspace="0" hspace="0" allowtransparency="true"></iframe>
備 和網絡設備。字符設備是直接讀取的,不必使用緩衝區。例如,系統的串行口/dev/cua0和/dev/cua1。塊設備每次只能讀取一定大小的塊的倍 數,通常一塊是512或者1024字節。塊設備通過緩衝區讀寫,並且可以隨機地讀寫。塊設備可以通過它們的設備文件存取,但通常是通過文件系統存取。只有 塊設備支持掛接的文件系統。網絡設備是通過BSD套接字界面存取的。

    Linux系統支持多種設備,這些設備的驅動程序之間有一些共同的特點:
    * 內核代碼:設備驅動程序是系統內核的一部分,所以如果驅動程序出現錯誤的話,將可能嚴重地破壞整個系統。
    * 內核接口:設備驅動程序必須爲系統內核或者它們的子系統提供一個標準的接口。例如,一個終端驅動程序必須爲Linux內核提供一個文件I/O接口;一個 SCSI設備驅動程序應該爲SCSI子系統提供一個SCSI設備接口,同時SCSI子系統也應爲系統內核提供文件I/O和緩衝區。
    * 內核機制和服務:設備驅動程序利用一些標準的內核服務,例如內存分配等。
    * 可裝入:大多數的Linux設備驅動程序都可以在需要時裝入內核,在不需要時卸載。
    * 可設置:Linux系統設備驅動程序可以集成爲系統內核的一部分,至於哪一部分需要集成到內核中,可以在系統編譯時設置。

 

[目錄]


I/O端口

  關鍵詞:設備管理、驅動程序、I/O端口、資源

   申明:這份文檔是按照自由軟件開放源代碼的精神發佈的,任何人可以免費獲得、使用和重新發布,但是你沒有限制別人重新發布你發佈內容的權利。發佈本文的 目的是希望它能對讀者有用,但沒有任何擔保,甚至沒有適合特定目的的隱含的擔保。更詳細的情況請參閱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內存資源,而不應該通過指向核心虛地址的指針來訪問。


[目錄]


from smth

[目錄]


基本結構

1.UNIX下設備驅動程序的基本結構
    在UNIX系統裏,對用戶程序而言,設備驅動程序隱藏了設備的具體細節,對各種不同設備提供了一致的接口,一般來說是把設備映射爲一個特殊的設備文件,用 戶程序可以象對其它文件一樣對此設備文件進行操作。UNIX對硬件設備支持兩個標準接口:塊特別設備文件和字符特別設備文件,通過塊(字符)特別 設備文件存取的設備稱爲塊(字符)設備或具有塊(字符)設備接口。 塊設備接口僅支持面向塊的I/O操作,所有I/O操作都通過在內核地址空間中的I/O緩衝區進行,它可以支持幾乎任意長度和任意位置上的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入 口點。如果設備驅動程序沒有提供上述入口點中的某一個,系統會用缺省的子程序來代替。對於不同的系統,也還有一些其它的入口點。

 


[目錄]


驅動程序

2.LINUX系統下的設備驅動程序
    具體到LINUX系統裏,設備驅動程序所提供的這組入口點由一個結構來向系統進行說明,此結構定義爲:

#include <linux/fs.h>
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 <linux/fs.h>
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 <linux/fs.h>
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 <linux/fs.h>
#include <linux/errno.h>
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 <linux/sched.h>
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 <linux/kernel.h>
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 <asm/io.h>
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 <asm/param.h>
#include <linux/timer.h>
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 <asm/system.h>
#define cli() __asm__ __volatile__ ("cli"::)
#define sti() __asm__ __volatile__ ("sti"::)

這兩個函數負責打開和關閉中斷允許。

#include <asm/segment.h>
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裏。

 


[目錄]


具體實現

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 *)

    這個函數會檢查是否需要設置標誌位,如果需要,則會將“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總線的支持用到較多的函數,並且要小心處理中斷。
    作爲回報,不需要你的介入,系統把一切都配置好了。

 

 

[目錄]


loopback

各位大俠,最近我看Linux源碼中的網絡驅動部分。
先從loopback.c入手的。
loopback.c中的loopback_xmit函數中有這麼一段:
static int loopback_xmit(struct sk_buff * skb,struct net_device * dev)
{
        struct net_device_stats * stats = (struct net_device_stats *)dev_priv;

        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);/*查<linux/skbuff.h>中定義:
                                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的引用而已. */
        .
        .
        .
}

 

 

 

[目錄]


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 給網絡卡

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 來的方便就是了。
還要再來一個嗎 ? 你自己去做吧 !

 


[目錄]


ISA總線DMA的實現

Linux對ISA總線DMA的實現

  (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);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章