Linux設備驅動之——PCI 總線

PCI總線概述               

           隨着通用處理器和嵌入式技術的迅猛發展,越來越多的電子設備需要由處理器控制。目前大多數CPU和外部設備都會提供PCI總線的接口,PCI總線已成爲計算機系統中一種應用廣泛、通用的總線標準[1]。Linux因其開放源代碼以及穩定的性能,越來越受到廣大用戶青睞。同時,基於Linux內核的嵌入式操作系統應用勢頭強勁,開發基於Linux的設備驅動程序,具有很強的實用性和可移植性。

   PCI總線概述

  PCI(Peripheral ComponentInterconnect)總線,即外部設備互連,是現在流行的一種連接PC和外圍設備的總線結構。PCI提供了一組完整的總線接口規範,可以在33MHz時鐘頻率、32 bit數據總線寬度的條件下達到峯值132Mb/s的傳輸速率;它能支持一種稱爲線性突發的數據傳輸模式,可確保總線不斷滿載數據;採用總線主控與同步操作,顯著改善PCI的性能;PCI獨立於處理器的結構,用戶可隨意增添外圍設備,以擴展電腦系統而不必擔心在不同時鐘頻率下會導致性能下降。

        

  從圖中我們可以看出PCI 總線架構主要被分成三部分:
           1.PCI  設備。符合 PCI 總線標準的設備就被稱爲 PCI 設備,PCI  總線架構中可以包含多個 PCI 設備。圖中的 Audio 、LAN 都是一個 PCI 設備。PCI  設備同時也分爲主設備和目標設備兩種,主設備是一次訪問操作的發起者,而目標設備則是被訪問者。
           2.PCI  總線。PCI  總線在系統中可以有多條,類似於樹狀結構進行擴展,每條 PCI 總線都可以連接多個 PCI  設備/ 橋。上圖中有兩條 PCI 總線。
           3.PCI  橋。當一條 PCI 總線的承載量不夠時,可以用新的 PCI 總線進行擴展,而 PCI 橋則是連接 PCI 總線之間的紐帶。圖中的 PCI 橋有兩個,一個橋用來連接處理器、內存以及 PCI 總線,而另外一條則用來連接另一條 PCI 總線。


PCI設備與驅動

           1. PCI相關結構體

            Linux  提供了三類數據結構用以描述 PCI 控制器、PCI  設備以及 PCI 總線。

              PCI  控制器:

    struct pci_controller {
    struct pci_controller *next;               //該屬性指向下一個 PCI 控制器
    struct pci_bus *bus;                         // 該屬性標誌了當前 PCI 控制器所連接的 PCI 總線

    struct pci_ops *pci_ops;                 //該屬性標誌了當前 PCI 控制器所對應的 PCI 配製空間讀寫操作函數
    struct resource *mem_resource;   //該屬性標誌了當前 PCI 控制器所支持的 Memory 地址區間
    unsigned long mem_offset;             //地址偏移量
    struct resource *io_resource;         //該屬性標誌了當前 PCI 控制器所支持的 IO  地址空間。
    unsigned long io_offset;                 //偏移量
    unsigned long io_map_base;        //PCI 設備的 IO map 基地址

    unsigned int index;                       //該屬性標誌 PCI 控制器的編號。
    /* For compatibility with current (as of July 2003) pciutils
       and XFree86. Eventually will be removed. */
    unsigned int need_domain_info; //域信息

    int iommu; //MMU

    /* Optional access methods for reading/writing the bus number
       of the PCI controller */
    int (*get_busno)(void);
    void (*set_busno)(int busno);
};

          PCI  總線:

struct pci_bus {
    struct list_head node;        /* node in list of buses */
    struct pci_bus    *parent;    /* parent bus this bridge is on */
    struct list_head children;    //總線連接的所有 PCI 子總線鏈表。
    struct list_head devices;    //總線連接的所有 PCI 設備鏈表
    struct pci_dev    *self;        //該屬性標誌了連接的上行 PCI 橋
    struct list_head slots;        /* list of slots on this bus */
    struct resource    *resource[PCI_BUS_NUM_RESOURCES];
                    //該屬性標誌了 Memory/IO 地址空間。

    struct pci_ops    *ops;        //該屬性標誌了總線上所有 PCI 設備的配製空間讀寫操作函數。
    void        *sysdata;    //指向系統特定的擴展數據
    struct proc_dir_entry *procdir;    /* directory entry in /proc/bus/pci */

    unsigned char    number;        /* bus number */
    unsigned char    primary;    /* number of primary bridge */
    unsigned char    secondary;    /* number of secondary bridge */
    unsigned char    subordinate;    /* max number of subordinate buses */

    char        name[48];

    unsigned short  bridge_ctl;    /* manage NO_ISA/FBB/et al behaviors */
    pci_bus_flags_t bus_flags;    /* Inherited by child busses */
    struct device        *bridge;
    struct device        dev;
    struct bin_attribute    *legacy_io; /* legacy I/O for this bus */
    struct bin_attribute    *legacy_mem; /* legacy mem */
    unsigned int        is_added:1;
};

       PCI  設備:

   每種類的PCI設備都可以用結構類型pci_dev來描述。更爲準確地說,應該是每一個PCI功能,即PCI邏輯設備都唯一地對應有一個pci_dev設備描述符。該數據結構的定義部分屬性如下(include/linux/pci.h):

struct pci_dev {

struct list_head bus_list;    /* node in per-bus list */
    struct pci_bus    *bus;        /* bus this device is on */
    struct pci_bus    *subordinate;    /* bus this device bridges to */

    void        *sysdata;    /* hook for sys-specific extension */
    struct proc_dir_entry *procent;    /* device entry in /proc/bus/pci */
    struct pci_slot    *slot;        /* Physical slot this device is in */

    unsigned int    devfn;          //標誌了設備編號和功能編號。
    unsigned short    vendor;  //屬性標誌了供應商編號
    unsigned short    device;   //標誌 設備編號
    unsigned short    subsystem_vendor;  //這是一個16無符號整數,表示PCI設備的子系統廠商ID
    unsigned short    subsystem_device;

    struct pci_driver *driver;    //指向這個PCI設備所對應的驅動程序定義的pci_driver結構

    u8        revision;    /* PCI revision, low byte of class word */
    u8        hdr_type;    /* PCI header type (`multi' flag masked out) */
    u8        pcie_type;    /* PCI-E device/port type */
    u8        rom_base_reg;    /* which config register controls the ROM */
    u8        pin;          /* which interrupt pin this device uses */

}

         總線設備鏈表元素bus_list:每一個pci_dev結構除了鏈接到全局設備鏈表中外,還會通過這個成員連接到其所屬PCI總線的設備鏈表中。每一條PCI總線都維護一條它自己的設備鏈表視圖,以便描述所有連接在該PCI總線上的設備,其表頭由PCI總線的pci_bus結構中的 devices成員所描述。

          hdr_type:8位符號整數,表示PCI配置空間頭部的類型。其中,bit[7]=1表示這是一個多功能設備,bit[7]=0表示這是一個單功能設備。Bit[6:0]則表示PCI配置空間頭部的佈局類型,值00h表示這是一個一般PCI設備的配置空間頭部,值01h表示這是一個PCI-to-PCI橋的配置空間頭部,值02h表示CardBus橋的配置空間頭部

         rom_base_reg:8位無符號整數,表示PCI配置空間中的ROM基地址寄存器在PCI配置空間中的位置。ROM基地址寄存器在不同類型的PCI配置空間頭部的位置是不一樣的,對於type 0的配置空間佈局,ROM基地址寄存器的起始位置是30h,而對於PCI-to-PCI橋所用的type 1配置空間佈局,ROM基地址寄存器的起始位置是38h

      2.PCI設備與驅動關係

        PCI設備通常由一組參數唯一地標識,它們被vendorID,deviceID和class nodes所標識,即設備廠商,型號等,這些參數保存在 pci_device_id結構中。每個PCI設備都會被分配一個pci_dev變量,內核就用這個數據結構來表示一個PCI設備。
        所有的PCI驅動程序都必須定義一個pci_driver結構變量,在該變量中包含了這個PCI驅動程序所提供的不同功能的函數,同時,在這個結構中也包含了一個device_driver結構,這個結構定義了PCI子系統與PCI設備之間的接口。在註冊PCI驅動程序時,這個結構將被初始化,同時這個 pci_driver變量會被鏈接到pci_bus_type中的驅動鏈上去。
         在pci_driver中有一個成員struct pci_device_id *id_table,它列出了這個設備驅動程序所能夠處理的所有PCI設備的ID值。

 

     3.PCI設備與驅動的綁定過程
        下面描述一下對於PCI設備與驅動綁定的過程。首先在系統啓動的時候,PCI總線會去掃描連接到這個總線上的設備,同時爲每一個設備建立一個pci_dev結構,在這個結構中有一個device成員,並將這些pci_dev結構鏈接到PCI總線描述符上的devices鏈。如下圖所示:

                               http://life.chinaunix.net/bbsfile/forum/linux/month_0812/20081215_5ce7ae92b4120b99fb33RNGu5zFL2PNl.jpg

        第二步是當PCI驅動被加載時,pci_driver結構體將被初始化,這一過程在函數pci_register_driver中:
        drv->driver.bus = &pci_bus_type;
        drv->driver.probe = pci_device_probe;
        最後會調用driver_register(&drv->driver)將這個PCI驅動掛載到總線描述符的驅動鏈上。同時在註冊的過程中,會根據pci_driver中的id_table中的ID值去查看該驅動支持哪些設備,將這些設備掛載到pci_driver中的devices鏈中來。如下圖所示:

                    http://life.chinaunix.net/bbsfile/forum/linux/month_0812/20081215_1ef87134e9fa07e8cb33zsXr2CsdSNdS.jpg

               

               對於不同的設備,可能驅動程序也不一樣,因此,對於上圖中的Dev3,可能就需要另外一個驅動程序來對其進行驅動。所以當加載了Dev3的驅動程序時,其示意圖如下圖所示:

                        http://life.chinaunix.net/bbsfile/forum/linux/month_0812/20081215_d07192500e2e0e79abfbl518V8P8M8N2.jpg


PCI設備驅動程序

      1.基本框架

        在用模塊方式實現PCI設備驅動程序時,通常至少要實現以下幾個部分:初始化設備模塊、設備打開模塊、數據讀寫和控制模塊、中斷處理模塊、設備釋放模塊、設備卸載模塊。下面給出一個典型的PCI設備驅動程序的基本框架,從中不難體會到這幾個關鍵模塊是如何組織起來的。


上面這段代碼給出了一個典型的PCI設備驅動程序的框架,是一種相對固定的模式。需要注意的是,同加載和卸載模塊相關的函數或數據結構都要在前面加上__init、__exit等標誌符,以使同普通函數區分開來。構造出這樣一個框架之後,接下去的工作就是如何完成框架內的各個功能模塊了。

/* 指明該驅動程序適用於哪一些PCI設備 */
static struct pci_device_id xxx_pci_tbl [] __initdata = {
    {PCI_VENDOR_ID_DEMO, PCI_DEVICE_ID_DEMO,
     PCI_ANY_ID, PCI_ANY_ID, 0, 0, DEMO},
    {0,}
};
/* 對特定PCI設備進行描述的數據結構 */
struct xxx_card {
    unsigned int magic;
    /* 使用鏈表保存所有同類的PCI設備 */
    struct xxx_card *next;
    
    /* ... */
}
/* 中斷處理模塊 */
static void xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    /* ... */
}
/* 設備文件操作接口 */
static struct file_operations demo_fops = {
    owner:      THIS_MODULE,   /* demo_fops所屬的設備模塊 */
    read:       xxx_read,    /* 讀設備操作*/
    write:      xxx_write,    /* 寫設備操作*/
    ioctl:      xxx_ioctl,    /* 控制設備操作*/
    mmap:       xxx_mmap,    /* 內存重映射操作*/
    open:       xxx_open,    /* 打開設備操作*/
    release:    xxx_release    /* 釋放設備操作*/
    /* ... */
};
/* 設備模塊信息 */
static struct pci_driver demo_pci_driver = {
    name:       xxx_MODULE_NAME,    /* 設備模塊名稱 */
    id_table:   xxx_pci_tbl,    /* 能夠驅動的設備列表 */
    probe:      xxx_probe,    /* 查找並初始化設備 */
    remove:     xxx_remove    /* 卸載設備模塊 */
    /* ... */
};
static int __init xxx_init_module (void)
{
    /* ... */
}
static void __exit xxx_cleanup_module (void)
{
    pci_unregister_driver(&xxx_pci_driver);
}
/* 加載驅動程序模塊入口 */
module_init(xxx_init_module);
/* 卸載驅動程序模塊入口 */
module_exit(xxx_cleanup_module);

     2.初始化

在Linux系統下,想要完成對一個PCI設備的初始化,需要完成以下工作:

  • 檢查PCI總線是否被Linux內核支持;
  • 檢查設備是否插在總線插槽上,如果在的話則保存它所佔用的插槽的位置等信息。
  • 讀出配置頭中的信息提供給驅動程序使用。

當Linux內核啓動並完成對所有PCI設備進行掃描、登錄和分配資源等初始化操作的同時,會建立起系統中所有PCI設備的拓撲結構,此後當PCI驅動程序需要對設備進行初始化時,一般都會調用如下的代碼:

static int __init xxx_init_module (void)
{
    /* 檢查系統是否支持PCI總線 */
    if (!pci_present())
        return -ENODEV;
    /* 註冊硬件驅動程序 */
    if (!pci_register_driver(&demo_pci_driver)) {
        pci_unregister_driver(&demo_pci_driver);
                return -ENODEV;
    }
    /* ... */
   
    return 0;
}

       驅動程序首先調用函數pci_present( )檢查PCI總線是否已經被Linux內核支持,如果系統支持PCI總線結構,這個函數的返回值爲0,如果驅動程序在調用這個函數時得到了一個非0的返回值,那麼驅動程序就必須得中止自己的任務了。在2.4以前的內核中,需要手工調用pci_find_device( )函數來查找PCI設備,但在2.4以後更好的辦法是調用pci_register_driver( )函數來註冊PCI設備的驅動程序,此時需要提供一個pci_driver結構,在該結構中給出的probe探測例程將負責完成對硬件的檢測工作。

static int __init xxx_probe(struct pci_dev *pci_dev, const struct pci_device_id *pci_id)
{
    struct xxx_card *card;
    /* 啓動PCI設備 */
    if (pci_enable_device(pci_dev))
        return -EIO;
    /* 設備DMA標識 */
    if (pci_set_dma_mask(pci_dev, DEMO_DMA_MASK)) {
        return -ENODEV;
    }
    /* 在內核空間中動態申請內存 */
    if ((card = kmalloc(sizeof(struct demo_card), GFP_KERNEL)) == NULL) {
        printk(KERN_ERR "pci_demo: out of memory\n");
        return -ENOMEM;
    }
    memset(card, 0, sizeof(*card));
    /* 讀取PCI配置信息 */
    card->iobase = pci_resource_start (pci_dev, 1);
    card->pci_dev = pci_dev;
    card->pci_id = pci_id->device;
    card->irq = pci_dev->irq;
    card->next = devs;
    card->magic = DEMO_CARD_MAGIC;
    /* 設置成總線主DMA模式 */    
    pci_set_master(pci_dev);
    /* 申請I/O資源 */
    request_region(card->iobase, 64, card_names[pci_id->driver_data]);
    return 0;
}

    3.OPEN

     在這個模塊裏主要實現申請中斷、檢查讀寫模式以及申請對設備的控制權等。在申請控制權的時候,非阻塞方式遇忙返回,否則進程主動接受調度,進入睡眠狀態,等待其它進程釋放對設備的控制權。

static int xxx_open(struct inode *inode, struct file *file)
{
    /* 申請中斷,註冊中斷處理程序 */
    request_irq(card->irq, &xxx_interrupt, SA_SHIRQ,
        card_names[pci_id->driver_data], card)) {
    /* 檢查讀寫模式 */
    if(file->f_mode & FMODE_READ) {
        /* ... */
    }
    if(file->f_mode & FMODE_WRITE) {
       /* ... */
    }
    
    /* 申請對設備的控制權 */
    down(&card->open_sem);
    while(card->open_mode & file->f_mode) {
        if (file->f_flags & O_NONBLOCK) {
            /* NONBLOCK模式,返回-EBUSY */
            up(&card->open_sem);
            return -EBUSY;
        } else {
            /* 等待調度,獲得控制權 */
            card->open_mode |= f_mode & (FMODE_READ | FMODE_WRITE);
            up(&card->open_sem);
            /* 設備打開計數增1 */
            MOD_INC_USE_COUNT;
            /* ... */
        }
    }
}

      4.數據的讀寫和控制

      PCI設備驅動程序可以通過demo_fops 結構中的函數demo_ioctl( ),嚮應用程序提供對硬件進行控制的接口。例如,通過它可以從I/O寄存器裏讀取一個數據,並傳送到用戶空間裏:

static int xxx_ioctl(struct inode *inode, struct file *file,
      unsigned int cmd, unsigned long arg)
{
    /* ... */
    
    switch(cmd) {
        case DEMO_RDATA:
            /* 從I/O端口讀取4字節的數據 */
            val = inl(card->iobae + 0x10);
            
/* 將讀取的數據傳輸到用戶空間 */
            return 0;
    }
    
    /* ... */
}

事實上,在demo_fops裏還可以實現諸如demo_read( )、demo_mmap( )等操作,Linux內核源碼中的driver目錄裏提供了許多設備驅動程序的源代碼,找那裏可以找到類似的例子。在對資源的訪問方式上,除了有I/O指令以外,還有對外設I/O內存的訪問。對這些內存的操作一方面可以通過把I/O內存重新映射後作爲普通內存進行操作,另一方面也可以通過總線主DMA(Bus Master DMA)的方式讓設備把數據通過DMA傳送到系統內存中。

       中斷處理

       5.中斷處理

      PC的中斷資源比較有限,只有0~15的中斷號,因此大部分外部設備都是以共享的形式申請中斷號的。當中斷髮生的時候,中斷處理程序首先負責對中斷進行識別,然後再做進一步的處理。

static void xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    struct xxx_card *card = (struct xxx_card *)dev_id;
    u32 status;
    spin_lock(&card->lock);
    /* 識別中斷 */
    status = inl(card->iobase + GLOB_STA);
    if(!(status & INT_MASK)) 
    {
        spin_unlock(&card->lock);
        return;  /* not for us */
    }
    /* 告訴設備已經收到中斷 */
    outl(status & INT_MASK, card->iobase + GLOB_STA);
    spin_unlock(&card->lock);
    
    /* 其它進一步的處理,如更新DMA緩衝區指針等 */
}

     6.釋放和卸載

      釋放設備模塊主要負責釋放對設備的控制權,釋放佔用的內存和中斷等,所做的事情正好與打開設備模塊相反:

static int xxx_release(struct inode *inode, struct file *file)
{
    /* ... */
    
    /* 釋放對設備的控制權 */
    card->open_mode &= (FMODE_READ | FMODE_WRITE);
    
    /* 喚醒其它等待獲取控制權的進程 */
    wake_up(&card->open_wait);
    up(&card->open_sem);
    
    /* 釋放中斷 */
    free_irq(card->irq, card);
    
    /* 設備打開計數增1 */
    MOD_DEC_USE_COUNT;
    
    /* ... */  
}

    卸載設備模塊與初始化設備模塊是相對應的,實現起來相對比較簡單,主要是調用函數pci_unregister_driver( )從Linux內核中註銷設備驅動程序:

static void __exit xxx_cleanup_module (void)
{
    pci_unregister_driver(&xxx_pci_driver);
}

參考文章:http://www.ibm.com/developerworks/cn/linux/l-pci/index.html

                   http://www.ibm.com/developerworks/cn/linux/l-cn-pci/index.html

                         http://bbs.chinaunix.net/thread-2010492-1-1.html




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