1. 程式人生 > 其它 >Linux PCI 裝置驅動基本框架

Linux PCI 裝置驅動基本框架

技術標籤:linuxlinux

Linux PCI 裝置驅動基本框架(一)

Linux將所有外部裝置看成是一類特殊檔案,稱之為“裝置檔案”,如果說系統呼叫是Linux核心和應用程式之間的介面,那麼裝置驅動程式則可以看成是 Linux核心與外部裝置之間的介面。裝置驅動程式嚮應用程式遮蔽了硬體在實現上的細節,使得應用程式可以像操作普通檔案一樣來操作外部裝置。

1. 字元裝置和塊裝置

Linux抽象了對硬體的處理,所有的硬體裝置都可以像普通檔案一樣來看待:它們可以使用和操作檔案相同的、標準的系統呼叫介面來完成開啟、關閉、讀寫和 I/O控制操作,而驅動程式的主要任務也就是要實現這些系統呼叫函式。Linux系統中的所有硬體裝置都使用一個特殊的裝置檔案來表示,例如,系統中的第 一個IDE硬碟使用/dev/hda表示。每個裝置檔案對應有兩個裝置號:一個是主裝置號,標識該裝置的種類,也標識了該裝置所使用的驅動程式;另一個是 次裝置號,標識使用同一裝置驅動程式的不同硬體裝置。裝置檔案的主裝置號必須與裝置驅動程式在登入該裝置時申請的主裝置號一致,否則使用者程序將無法訪問到 裝置驅動程式。


在Linux作業系統下有兩類主要的裝置檔案:一類是字元裝置,另一類則是塊裝置。字元裝置是以位元組為單位逐個進行I/O操作的裝置,在對字元裝置發出讀 寫請求時,實際的硬體I/O緊接著就發生了,一般來說字元裝置中的快取是可有可無的,而且也不支援隨機訪問。塊裝置則是利用一塊系統記憶體作為緩衝區,當用 戶程序對裝置進行讀寫請求時,驅動程式先檢視緩衝區中的內容,如果緩衝區中的資料能滿足使用者的要求就返回相應的資料,否則就呼叫相應的請求函式來進行實際 的I/O操作。塊裝置主要是針對磁碟等慢速裝置設計的,其目的是避免耗費過多的CPU時間來等待操作的完成。一般說來,PCI卡通常都屬於字元裝置。

2. 裝置驅動程式介面



Linux中的I/O子系統向核心中的其他部分提供了一個統一的標準裝置介面,這是通過include/linux/fs.h中的資料結構file_operations來完成的:

複製程式碼

struct file_operations {
     struct module *owner;
     loff_t (*llseek) (struct file *, loff_t, int);
     ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
     ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
     ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
     ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
     int (*readdir) (struct file *, void *, filldir_t);
     unsigned int (*poll) (struct file *, struct poll_table_struct *);
     long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
     long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
     int (*mmap) (struct file *, struct vm_area_struct *);
     int (*open) (struct inode *, struct file *);
     int (*flush) (struct file *, fl_owner_t id);
     int (*release) (struct inode *, struct file *);
     int (*fsync) (struct file *, loff_t, loff_t, int datasync);
     int (*aio_fsync) (struct kiocb *, int datasync);
     int (*fasync) (int, struct file *, int);
     int (*lock) (struct file *, int, struct file_lock *);
     ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
     unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
     int (*check_flags)(int);
     int (*flock) (struct file *, int, struct file_lock *);
     ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
     ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
     int (*setlease)(struct file *, long, struct file_lock **);
     long (*fallocate)(struct file *file, int mode, loff_t offset,
                 loff_t len);
};

複製程式碼

當應用程式對裝置檔案進行諸如open、close、read、write等操作時,Linux核心將通過file_operations結構訪問驅動程 序提供的函式。例如,當應用程式對裝置檔案執行讀操作時,核心將呼叫file_operations結構中的read函式。

3. 裝置驅動程式模組

Linux下的裝置驅動程式可以按照兩種方式進行編譯,一種是直接靜態編譯成核心的一部分,另一種則是編譯成可以動態載入的模組。如果編譯進核心的話,會增加核心的大小,還要改動核心的原始檔,而且不能動態地解除安裝,不利於除錯,所有推薦使用模組方式。

從本質上來講,模組也是核心的一部分,它不同於普通的應用程式,不能呼叫位於使用者態下的C或者C++庫函式,而只能呼叫Linux核心提供的函式,在/proc/ksyms中可以檢視到核心提供的所有函式。

在以模組方式編寫驅動程式時,要實現兩個必不可少的函式init_module( )和cleanup_module( ),而且至少要包含和兩 個頭檔案。一般使用LDD3 例程中使用的makefile 作為基本的版本,稍作改變之後用來編譯驅動,編譯生成的模組(一般為.ko檔案)可以使用命令insmod載入Linux核心,從而成為核心的一個組成部分,此時核心會呼叫 模組中的函式init_module( )。當不需要該模組時,可以使用rmmod命令進行解除安裝,此進核心會呼叫模組中的函式cleanup_module( )。任何時候都可以使用命令來lsmod檢視目前已經載入的模組以及正在使用該模組的使用者數。

4. 裝置驅動程式結構

瞭解裝置驅動程式的基本結構(或者稱為框架),對開發人員而言是非常重要的,Linux的裝置驅動程式大致可以分為如下幾個部分:驅動程式的註冊與登出、裝置的開啟與釋放、裝置的讀寫操作、裝置的控制操作、裝置的中斷和輪詢處理。

驅動程式的註冊與登出

向系統增加一個驅動程式意味著要賦予它一個主裝置號,這可以通過在驅動程式的初始化過程中呼叫register_chrdev( )或者register_blkdev( )來完成。而在關閉字元裝置或者塊裝置時,則需要通過呼叫unregister_chrdev( )或unregister_blkdev( )從核心中登出裝置,同時釋放佔用的主裝置號。但是現在

程式設計師都傾向於動態建立裝置號和裝置結點,動態建立裝置號和裝置結點需要幾個指定的函式,具體

可以參見“Linux字元驅動中動態分配裝置號與動態生成裝置節點”。


裝置的開啟與釋放

開啟裝置是通過呼叫file_operations結構中的函式open( )來完成的,它是驅動程式用來為今後的操作完成初始化準備工作的。在大部分驅動程式中,open( )通常需要完成下列工作:

1.檢查裝置相關錯誤,如裝置尚未準備好等。

2.如果是第一次開啟,則初始化硬體裝置。

3.識別次裝置號,如果有必要則更新讀寫操作的當前位置指標f_ops。

4.分配和填寫要放在file->private_data裡的資料結構。

5.使用計數增1。

釋放裝置是通過呼叫file_operations結構中的函式release( )來完成的,這個裝置方法有時也被稱為close( ),它的作用正好與open( )相反,通常要完成下列工作:

1.使用計數減1。

2.釋放在file->private_data中分配的記憶體。

3.如果使用計算為0,則關閉裝置。

裝置的讀寫操作

字元裝置的讀寫操作相對比較簡單,直接使用函式read( )和write( )就可以了。但如果是塊裝置的話,則需要呼叫函式block_read( )和block_write( )來進行資料讀寫,這兩個函式將向裝置請求表中增加讀寫請求,以便Linux核心可以對請求順序進行優化。由於是對記憶體緩衝區而不是直接對裝置進行操作 的,因此能很大程度上加快讀寫速度。如果記憶體緩衝區中沒有所要讀入的資料,或者需要執行寫操作將資料寫入裝置,那麼就要執行真正的資料傳輸,這是通過呼叫 資料結構blk_dev_struct中的函式request_fn( )來完成的。

裝置的控制操作

除了讀寫操作外,應用程式有時還需要對裝置進行控制,這可以通過裝置驅動程式中的函式ioctl( )來完成,ioctl 系統呼叫有下面的原型:int ioctl(int fd, unsigned long cmd, ...),第一個引數是檔案描述符,第二個引數是具體的命令,一般使用巨集定義來確定,第三個引數一般是傳遞給驅動中處理裝置控制操作函式的引數。ioctl( )的用法與具體裝置密切關聯,因此需要根據裝置的實際情況進行具體分析。


裝置的中斷和輪詢處理

對於不支援中斷的硬體裝置,讀寫時需要輪流查詢裝置狀態,以便決定是否繼續進行資料傳輸。如果裝置支援中斷,則可以按中斷方式進行操作。

基本框架

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

複製程式碼

/* 指明該驅動程式適用於哪一些PCI裝置 */
static struct pci_device_id my_pci_tbl [] __initdata = {
{PCI_VENDOR_ID, PCI_DEVICE_ID,PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0},
{0,}
};

/* 對特定PCI裝置進行描述的資料結構 */
struct device_private {
...
}

/* 中斷處理模組 */
static irqreturn_t device_interrupt(int irq, void *dev_id)
{
/* ... */
}

/* 裝置檔案操作介面 */
static struct file_operations device_fops = {
owner: THIS_MODULE, /* demo_fops所屬的裝置模組 */
read: device_read, /* 讀裝置操作*/
write: device_write, /* 寫裝置操作*/
ioctl: device_ioctl, /* 控制裝置操作*/
mmap: device_mmap, /* 記憶體重對映操作*/
open: device_open, /* 開啟裝置操作*/
release: device_release /* 釋放裝置操作*/
/* ... */
};

/* 裝置模組資訊 */
static struct pci_driver my_pci_driver = {
name: DEVICE_MODULE_NAME, /* 裝置模組名稱 */
id_table: device_pci_tbl, /* 能夠驅動的裝置列表 */
probe: device_probe, /* 查詢並初始化裝置 */
remove: device_remove /* 解除安裝裝置模組 */
/* ... */
};

static int __init init_module (void)
{
/* ... */
}

static void __exit cleanup_module (void)
{
     pci_unregister_driver(&my_pci_driver);
}

/* 載入驅動程式模組入口 */
module_init(init_module);

/* 解除安裝驅動程式模組入口 */
module_exit(cleanup_module);

複製程式碼

上面這段程式碼給出了一個典型的PCI

針對相應裝置定義描述該PCI裝置的資料結構:

複製程式碼

struct device_private
{

     /*註冊字元驅動和發現PCI裝置的時候使用*/
     struct pci_dev  *my_pdev;//
     struct cdev my_cdev;//

     dev_t my_dev;
     atomic_t created;


      /* 用於獲取PCI裝置配置空間的基本資訊 */
     unsigned long mmio_addr;
     unsigned long regs_len;
     int     irq;//中斷號
    
     /*用於儲存分配給PCI裝置的記憶體空間的資訊*/
     dma_addr_t rx_dma_addrp;
     dma_addr_t tx_dma_addrp;


     /*基本的同步手段*/

     spinlock_t lock_send;
     spinlock_t lock_rev;


     /*儲存記憶體空間轉換後的地址資訊*/
     void __iomem *ioaddr;
     unsigned long virts_addr;


      int open_flag // 裝置開啟標記

     .....
    
};

複製程式碼

初始化裝置模組:

複製程式碼

static struct pci_driver my_pci_driver = {
     name:     DRV_NAME,  // 驅動的名字,一般是一個巨集定義
     id_table:     my_pci_tbl, //包含了相關物理PCI裝置的基本資訊,vendorID,deviceID等
     probe:     pci_probe, //用於發現PCI裝置
     remove:     __devexit_p(pci_remove), //PCI裝置的移除
};

複製程式碼

// my_pci_tbl 其實是一個 struct pci_device 結構,該結構可以有很多項,每一項代表一個裝置

// 該結構可以包含很多項,每一項表明使用該結構的驅動支援的裝置

// 注意:需要以一個空的項結尾,也就是:{0,}

複製程式碼

static struct pci_device_id my_pci_tbl[] __initdata = {
     { vendor_id, device_id, PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0},
     { 0,}
};

 

static int __init init_module(void) 
{
     int result;

     printk(KERN_INFO "my_pci_driver built on %s, %s\n",__DATE__,__TIME__);

     result = pci_register_driver(&my_pci_driver ); //註冊裝置驅動
     if(result)
          return result;

     return 0;
}

複製程式碼

解除安裝裝置模組:

複製程式碼

static void __devexit my_pci_remove(struct pci_dev *pci_dev)
{
     struct device_private *private;
     private= (struct device_private*)pci_get_drvdata(pci_dev);
    
     printk("FCswitch->irq = %d\n",private->irq);
     

     // register_w32 是封裝的巨集,便於直接操作

     // #define register_w32 (reg, val32)     iowrite32 ((val32), device_private->ioaddr + (reg))

     // 這裡的作用是關中斷,硬體復位

     register_w32(IntrMask,0x00000001); 
     register_w32(Reg_reset,0x00000001);
    
     // 移除動態建立的裝置號和裝置
     device_destroy(device_class, device->my_dev);
     class_destroy(device_class);
     

     cdev_del(&private->my_cdev);
     unregister_chrdev_region(priv->my_dev,1);
    
     //清理用於對映到使用者空間的記憶體頁面
     for(private->virts_addr = (unsigned long)private->rx_buf_virts;private->virts_addr < (unsigned long)private->rx_buf_virts + BUF_SIZE;private->virts_addr += PAGE_SIZE)
     {
          ClearPageReserved(virt_to_page(FCswitch->virts_addr));
     }
     ...

     // 釋放分配的記憶體空間
     pci_free_consistent(private->my_pdev, BUF_SIZE, private->rx_buf_virts, private->rx_dma_addrp);
     ...    


     free_irq(private->irq, private);


     iounmap(private->ioaddr);
     pci_release_regions(pci_dev);
     kfree(private);
    
     pci_set_drvdata(pci_dev,NULL);
     pci_disable_device(pci_dev);
}

複製程式碼

// 總之模組解除安裝函式的職責就是釋放一切分配過的資源,根據自己程式碼的需要進行具體的操作

PCI裝置的探測(probe):

複製程式碼

static int __devinit pci_probe(struct pci_dev *pci_dev, const struct pci_device_id *pci_id)
{
     unsigned long mmio_start;
     unsigned long mmio_end;
     unsigned long mmio_flags;
     unsigned long mmio_len;
     void __iomem *ioaddr1=NULL;
     struct device_private *private;
     int result;
     printk("probe function is running\n");

     /* 啟動PCI裝置 */
     if(pci_enable_device(pci_dev))
     {
          printk(KERN_ERR "%s:cannot enable device\n",pci_name(pci_dev));
          return -ENODEV;
     }
     printk( "enable device\n");
     /* 在核心空間中動態申請記憶體 */
     if((private= kmalloc(sizeof(struct device_private), GFP_KERNEL)) == NULL)
     {
          printk(KERN_ERR "pci_demo: out of memory\n");
          return -ENOMEM;
     }
     memset(private, 0, sizeof(*private));

     private->my_pdev = pci_dev;

     mmio_start = pci_resource_start(pci_dev, 0);
     mmio_end = pci_resource_end(pci_dev, 0);
     mmio_flags = pci_resource_flags(pci_dev, 0);
     mmio_len = pci_resource_len(pci_dev, 0);
     printk("mmio_start is 0x%0x\n",(unsigned int)mmio_start);
     printk("mmio_len is 0x%0x\n",(unsigned int)mmio_len);
     if(!(mmio_flags & IORESOURCE_MEM))
     {
          printk(KERN_ERR "cannot find proper PCI device base address, aborting.\n");
          result = -ENODEV;
          goto err_out;
     }
    

     /* 對PCI區進行標記 ,標記該區域已經分配出去*/
     result = pci_request_regions(pci_dev, DEVICE_NAME);
     if(result)
          goto err_out;

    
     /* 設定成匯流排主DMA模式 */
     pci_set_master(pci_dev);
    
     /*ioremap 重對映一個實體地址範圍到處理器的虛擬地址空間, 使它對核心可用.*/

     ioaddr1 = ioremap(mmio_start, mmio_len);
     if(ioaddr1 == NULL)
     {
          printk(KERN_ERR "%s:cannot remap mmio, aborting\n",pci_name(pci_dev));
          result = -EIO;
          goto err_out;
     }
     printk("ioaddr1 =  0x%0x\n",(unsigned int)ioaddr1);

    

     private->ioaddr = ioaddr1;
     private->mmio_addr = mmio_start;
     private->regs_len = mmio_len;
     private->irq = pci_dev->irq;
     printk("irq is %d\n",pci_dev->irq);


     /* 初始化自旋鎖 */
     spin_lock_init(&private->lock_send);
     spin_lock_init(&private->lock_rev);
    

     if(my_register_chrdev(private)) //注:這裡的註冊字元裝置,類似於前面的文章中介紹過的動態建立裝置號和動態生成裝置結點
     {
          printk("chrdev register fail\n");
          goto err_out;
     }

     //下面這兩個函式根據具體的硬體來處理,主要就是記憶體分配、對硬體進行初始化設定等
     device_init_buf(xx_device);//這個函式主要進行記憶體分配,記憶體對映,獲取中斷
     device_hw_start(xx_device);//這個函式主要是往暫存器中寫一些值,復位硬體,開中斷,開啟DMA等
    

     //把裝置指標地址放入PCI裝置中的裝置指標中,便於後面呼叫pci_get_drvdata

     pci_set_drvdata(pci_dev, FCswitch);  
      return 0;
err_out:
     printk("error process\n");
      resource_cleanup_dev(FCswitch); //如果出現任何問題,釋放已經分配了的資源
     return result;
}

複製程式碼

// probe函式的作用就是啟動pci裝置,讀取配置空間資訊,進行相應的初始化

中斷處理:

//中斷處理,主要就是讀取中斷暫存器,然後呼叫中斷處理函式來處理中斷的下半部分,一般通過tasklet或者workqueue來實現

注意:由於使用request_irq 獲得的中斷是共享中斷,因此在中斷處理函式的上半部需要區分是不是該裝置發出的中斷,這就需要讀取中斷狀態暫存器的值來判斷,如果不是該裝置發起的中斷則 返回 IRQ_NONE

複製程式碼

static irqreturn_t device_interrupt(int irq, void *dev_id)

{

     ...

   if( READ(IntMask) == 0x00000001)
   {
      return IRQ_NONE;
   }
   WRITE(IntMask,0x00000001);
    tasklet_schedule(&my_tasklet);  // 需要先申明tasklet 並關聯處理函式

     ... 

     return IRQ_HANDLED;

}

// 宣告tasklet

static void my_tasklet_process(unsigned long unused);
DECLARE_TASKLET(my_tasklet, my_tasklet_process, (unsigned long)&private);//第三個引數為傳遞給my_tasklet_process 函式的引數

複製程式碼

裝置驅動的介面:

複製程式碼

 static struct file_operations device_fops = {

     owner:     THIS_MODULE,
     open:       device_open, //開啟裝置
     ioctl:        device_ioctl,  //裝置控制操作
     mmap:     device_mmap,//記憶體重對映操作
     release:    device_release,// 釋放裝置
};

複製程式碼

開啟裝置:

open 方法提供給驅動來做任何的初始化來準備後續的操作. open 方法的原型是:

int (*open)(struct inode *inode, struct file *filp);
inode 引數有我們需要的資訊,以它的 i_cdev 成員的形式, 裡面包含我們之前建立的cdev 結構. 唯一的問題是通常我們不想要 cdev 結構本身, 我們需要的是包含 cdev 結構的 device_private 結構.

複製程式碼

static int device_open(struct inode *inode, struct file *filp)
{
     struct device_private *private;
     private= container_of(inode->i_cdev, struct device_private, my_cdev);
     filp->private_data = private;

     private->open_flag++;
     try_module_get(THIS_MODULE);
     ...
     return 0;
}

複製程式碼

釋放裝置:

release 方法的角色是 open 的反面,裝置方法應當進行下面的任務:

• 釋放 open 分配在 filp->private_data 中的任何東西
• 在最後的 close 關閉裝置

複製程式碼

 static int FCswitch_release(struct inode *inode,struct file *filp)

{
     struct device_private *private= filp->private_data;
     private->open_flag--;
 
     module_put(THIS_MODULE);

     printk("pci device close success\n");

     return 0;
}

複製程式碼

裝置控制操作:

PCI裝置驅動程式可以通過device_fops結構中的函式device_ioctl( ),嚮應用程式提供對硬體進行控制的介面。例如,通過它可以從I/O暫存器裡讀取一個數據,並傳送到使用者空間裡。

複製程式碼

static int device_ioctl(struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg)
{
     int retval = 0;
     struct device_private *FCswitch = filp->private_data;
     
     switch (cmd)
     {

          case DMA_EN://DMA使能
               device_w32(Dma_wr_en, arg);
               break;

          ...

          default:
               retval = -EINVAL;
     }
     return retval;
}

複製程式碼

記憶體對映:

複製程式碼

static int device_mmap(struct file *filp, struct vm_area_struct *vma)
{
     int ret;
     struct device_private *private = filp->private_data;
     vma->vm_page_prot = PAGE_SHARED;//訪問許可權
     vma->vm_pgoff = virt_to_phys(FCswitch->rx_buf_virts) >> PAGE_SHIFT;//偏移(頁幀號)

     ret = remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, (unsigned long)(vma->vm_end-vma->vm_start), vma->vm_page_prot);
     if(ret!=0)
          return -EAGAIN;

     return 0;
}

複製程式碼

對 remap_pfn_range()函式的說明:

remap_pfn_range()函式的原型:
int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot);

該函式的功能是建立頁表。其中引數vma是核心根據使用者的請求自己填寫的,而引數addr表示記憶體對映開始處的虛擬地址,因此,該函式為addr~addr+size之間的虛擬地址構造頁表。

另外,pfn(Page Fram Number)是虛擬地址應該對映到的實體地址的頁面號,實際上就是實體地址右移PAGE_SHIFT位。如果PAGE_SHIFT為4kb,則 PAGE_SHIFT為12,因為PAGE_SHIFT等於1<<PAGE_SHIFT。最後一個引數prot是新頁所要求的保護屬性。
在驅動程式中,一般能使用remap_pfn_range()對映記憶體中的保留頁(如X86系統中的640KB~1MB區域)和裝置I/O記憶體。因此,如 果想把kmalloc()申請的記憶體對映到使用者空間,則可以通過SetPageReserved把相應的記憶體設定為保留後就可以。

make it simple, make it happen

一,初始化裝置模組

當Linux核心啟動並完成對所有PCI裝置進行掃描、登入和分配資源等初始化操作的同時,會建立起系統中所有PCI裝置的拓撲結構,此後當PCI驅動程式需要對裝置進行初始化時,一般都會呼叫如下的程式碼:

複製程式碼

 static int __init example_init_module (void)   
 {
    /* 註冊硬體驅動程式 */
         if(!pci_register_driver(&example_pci_driver)){
         pci_unregister_driver(&example_pci_driver);
         return-ENODEV;
         }
         /* ... */
        
         return 0;
 }

複製程式碼

從上面的省略號可以看出,這僅僅只是核心的一部分,其他的東西,就要看你具體的應用是在什麼地方。

僅僅使用上面的init函式是不夠的,因為此時你並不知道你的裝置是什麼樣的,是不是都已經準備好了,因此,還有更重要的一步---probe。探測完成對硬體的檢測工作。

我們先將相關程式碼進行列示:

複製程式碼

 static int __init example_probe(struct pci_dev *pci_dev,conststruct pci_device_id *pci_id)    
{
         struct example_pci *my_pci;
         /* 啟動PCI裝置 */
         if(pci_enable_device(pci_dev))
         return-EIO;
         /* 裝置DMA標識 */
         if(pci_set_dma_mask(pci_dev, EXAMPLE_DMA_MASK))
         return-ENODEV;
         /* 在核心空間中動態申請記憶體 */
         if((my_pci = kmalloc(sizeof(struct example_pci), GFP_KERNEL))== NULL){
         printk(KERN_ERR "example_pci: out of memory\n");
         return-ENOMEM;
         }
         memset(my_pci,0,sizeof(*my_pci));
         /* 讀取PCI配置資訊 */
         my_pci->iobase = pci_resource_start(pci_dev,1);
         my_pci->pci_dev = pci_dev;
         my_pci->pci_id = pci_id->device;
         my_pci->irq = pci_dev->irq;
         my_pci->next= devs;
         my_pci->magic = EXAMPLE_MAGIC;
         /* 設定成匯流排主DMA模式 */
         pci_set_master(pci_dev);
         /* 申請I/O資源 */
         request_region(my_pci->iobase,64,my_pci_names[pci_id->driver_data]);
         return 0;
}

複製程式碼

整個程式的思路很清晰,並不需要去太多的講解,只是對裡面的一些函式需要進行一下講解.

1.pci_enable_device

啟用PCI裝置,在驅動程式可以訪問PCI裝置的任何裝置資源之前(I/O區域或者中斷),驅動程式必須呼叫該函式:

   int pci_enable_device(struct pci_dev *dev);                       /*driver/pci/pci.c*/

該函式實際的啟用裝置。它把裝置喚醒,在某些情況下還指派它的中斷線和I/O區域。

2.訪問PCI地址空間

在驅動程式檢測到裝置之後,它通常需要讀取或寫入三個地址空間:記憶體,埠和配置。對驅動程式來說,對配置空間的訪問至關重要,因為這是它找到裝置對映到記憶體和I/O空間的什麼位置的唯一途徑。

因而,首先來看看配置空間的訪問:

Linux核心為我們想的很周到,在核心中就已經提供了訪問配置空間的標準介面,我們只要去直接呼叫就好了。

對於驅動程式而言,可通過8位,16位,32位的資料傳輸訪問配置空間。相關函式定義在<linux/pci.h>中:

    int pci_read_config_byte(conststruct pci_dev *dev,intwhere, u8 *val);/*8位,讀入一個位元組*/
     
    int pci_read_config_word(conststruct pci_dev *dev,intwhere, u16 *val);/*16位,讀入兩個位元組*/
     
    int pci_read_config_dword(conststruct pci_dev *dev,intwhere, u32 *val);/*32位,讀入四個位元組*/

const struct pci_dev *dev:由dev標識的裝置配置空間;

int where:從配置空間起始位置計算的位元組偏移量;

u8/u16/u32 *val:從配置空間獲得的值通過val指標返回;

函式本身返回的值是錯誤碼。

注意:word和dword函式會將讀取到的little-endian值轉換成處理器固有的位元組序。我們自己無需處理位元組序。

上面的是讀的情況,寫的情況也是類似的<linux/pci.h>:

    int pci_write_config_byte(conststruct pci_dev *dev,intwhere, u8 *val);/*8位,寫入一個位元組*/
     
    int pci_write_config_word(conststruct pci_dev *dev,intwhere, u16 *val);/*16位,寫入兩個位元組*/
     
    int pci_write_config_dword(conststruct pci_dev *dev,intwhere, u32 *val);/*32位,寫入四個位元組*/

因此,我們可以利用上面的函式讀取和修改裝置的資訊。

講完配置空間,接下來嘮叨一下I/O和記憶體空間。

一個PCI裝置可實現多達6個I/O地址區域,每個區域既可以使記憶體也可以是I/O地址。在核心中PCI裝置的I/O區域已經被整合到通用資源管理器。因此,我們無需訪問配置變數來了解裝置被對映到記憶體或者I/O空間的何處。獲得區域資訊的首選介面是下面的巨集定義:

  #define pci_resource_start(dev, bar)((dev)->resource[(bar)].start)

該巨集返回六個PCI I/O區域之一的首地址(記憶體地址或者I/O埠號).該區域由整數的bar(base address register,基地址暫存器)指定,bar取值為0到5。

    #define pci_resource_end(dev, bar)((dev)->resource[(bar)].end)

該巨集返回第bar個I/O區域的首地址。注意這是最後一個可用的地址,而不是該區域之後的第一個地址。

    #define pci_resource_flags(dev, bar)((dev)->resource[(bar)].flags)

該巨集返回和該資源相關聯的標誌。

資源標誌用來定義單個資源的特性,對與PCI I/O區域相關的PCI資源,該資訊從基地址暫存器中獲得,但對於和PCI無關的資源,它可能來自其他地方。所有資源標誌定義在<linux/ioport.h>。

二,開啟裝置模組

複製程式碼

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

複製程式碼

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

三,資料讀寫和資訊控制模組

複製程式碼

static int example_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)   
{
         /* ... */
         
         switch(cmd) {
         case EXAMPLE_RDATA:
             /* 從I/O埠讀取4位元組的資料 */
             val = inl(my_pci->iobae + 0x10);
             
            /* 將讀取的資料傳輸到使用者空間 */
             return 0;
         }
         
         /* ... */
}

複製程式碼

PCI裝置驅動程式可以通過example_fops結構中的函式example_ioctl( ),嚮應用程式提供對硬體進行控制的介面。例如,通過它可以從I/O暫存器裡讀取一個數據,並傳送到使用者空間裡。

四,中斷模組

複製程式碼

    static void example_interrupt(int irq, void *dev_id, struct pt_regs *regs)
    {
             struct example_pci *my_pci = (struct example_pci *)dev_id;
             u32 status;
             spin_lock(&my_pci->lock);
             /* 中斷 */
             status = inl(my_pci->iobase + GLOB_STA);
             if(!(status & INT_MASK)) 
             {
                 spin_unlock(&my_pci->lock);
                 return; /* not for us */
             }
             /* 告訴裝置已經收到中斷 */
             outl(status & INT_MASK, my_pci->iobase + GLOB_STA);
             spin_unlock(&my_pci->lock);
             
             /* 其它進一步的處理 */
    }

複製程式碼

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

五,釋放裝置模組

複製程式碼

static int example_release(struct inode *inode, struct file *file)    
{
     /* ... */
     
     /* 釋放對裝置的控制權 */
     my_pci->open_mode &= (FMODE_READ | FMODE_WRITE);
     
     /* 喚醒其它等待獲取控制權的程序 */
     wake_up(&my_pci->open_wait);
     up(&my_pci->open_sem);
     
     /* 釋放中斷 */
     free_irq(my_pci->irq, my_pci);
     
     /* 裝置開啟計數增1 */
     MOD_DEC_USE_COUNT;
     
     /* ... */

複製程式碼

釋放裝置模組主要負責釋放對裝置的控制權,釋放佔用的記憶體和中斷等,所做的事情正好與開啟裝置模組相反。

以上就是我對PCI驅動程式的現階段理解,比較初淺,若有任何不合適的地方,請大家指出。

裝置驅動程式的框架,是一種相對固定的模式。需要注意的是,同載入和解除安裝模組相關的函式或資料結構都要在前面加上 __init、__exit等標誌符,以使同普通函式區分開來。構造出這樣一個框架之後,接下去的工作就是如何完成框架內的各個功能模組了。