PCI驱动编程
一、字符設備和塊設備
Linux抽象了對硬件的處理,所有的硬件設備都可以像普通文件一樣來看待:它們可以使用和操作文件相同的、標準的系統調用接口來完成打開、關閉、讀寫和I/O控制操作,而驅動程序的主要任務也就是要實現這些系統調用函數。Linux系統中的所有硬件設備都使用一個特殊的設備文件來表示,例如,系統中的第一個IDE硬盤使用/dev/hda表示。每個設備文件對應有兩個設備號:一個是主設備號,標識該設備的種類,也標識了該設備所使用的驅動程序;另一個是次設備號,標識使用同一設備驅動程序的不同硬件設備。設備文件的主設備號必須與設備驅動程序在登錄該設備時申請的主設備號一致,否則用戶進程將無法訪問到設備驅動程序。
在Linux操作系統下有兩類主要的設備文件:一類是字符設備,另一類則是塊設備。字符設備是以字節為單位逐個進行I/O操作的設備,在對字符設備發出讀寫請求時,實際的硬件I/O緊接著就發生了,一般來說字符設備中的緩存是可有可無的,而且也不支持隨機訪問。塊設備則是利用一塊系統內存作為緩沖區,當用戶進程對設備進行讀寫請求時,驅動程序先查看緩沖區中的內容,如果緩沖區中的數據能滿足用戶的要求就返回相應的數據,否則就調用相應的請求函數來進行實際的I/O操作。塊設備主要是針對磁盤等慢速設備設計的,其目的是避免耗費過多的CPU時間來等待操作的完成。一般說來,PCI卡通常都屬于字符設備。
所有已經注冊(即已經加載了驅動程序)的硬件設備的主設備號可以從/proc/devices文件中得到。使用mknod命令可以創建指定類型的設備文件,同時為其分配相應的主設備號和次設備號。例如,下面的命令:
[root@gary root]# mknod /dev/lp0 c 6 0
將建立一個主設備號為6,次設備號為0的字符設備文件/dev/lp0。當應用程序對某個設備文件進行系統調用時,Linux內核會根據該設備文件的設備類型和主設備號調用相應的驅動程序,并從用戶態進入到核心態,再由驅動程序判斷該設備的次設備號,最終完成對相應硬件的操作。
二、設備驅動程序接口
Linux中的I/O子系統向內核中的其他部分提供了一個統一的標準設備接口,這是通過include/linux/fs.h中的數據結構file_operations來完成的:
當應用程序對設備文件進行諸如open、close、read、write等操作時,Linux內核將通過file_operations結構訪問驅動程 序提供的函數。例如,當應用程序對設備文件執行讀操作時,內核將調用file_operations結構中的read函數。
三、設備驅動程序模塊
Linux下的設備驅動程序可以按照兩種方式進行編譯,一種是直接靜態編譯成內核的一部分,另一種則是編譯成可以動態加載的模塊。如果編譯進內核的話,會增加內核的大小,還要改動內核的源文件,而且不能動態地卸載,不利于調試,所有推薦使用模塊方式。
從本質上來講,模塊也是內核的一部分,它不同于普通的應用程序,不能調用位于用戶態下的C或者C++庫函數,而只能調用Linux內核提供的函數,在/proc/ksyms中可以查看到內核提供的所有函數。
在以模塊方式編寫驅動程序時,要實現兩個必不可少的函數init_module( )和cleanup_module( ),而且至少要包含和兩 個頭文件。一般使用LDD3 例程中使用的makefile 作為基本的版本,稍作改變之后用來編譯驅動,編譯生成的模塊(一般為.ko文件)可以使用命令insmod載入Linux內核,從而成為內核的一個組成部分,此時內核會調用 模塊中的函數init_module( )。當不需要該模塊時,可以使用rmmod命令進行卸載,此進內核會調用模塊中的函數cleanup_module( )。任何時候都可以使用命令來lsmod查看目前已經加載的模塊以及正在使用該模塊的用戶數。
四、設備驅動程序結構
了解設備驅動程序的基本結構(或者稱為框架),對開發人員而言是非常重要的,Linux的設備驅動程序大致可以分為如下幾個部分:驅動程序的注冊與注銷、設備的打開與釋放、設備的讀寫操作、設備的控制操作、設備的中斷和輪詢處理。
1.驅動程序的注冊與注銷
向系統增加一個驅動程序意味著要賦予它一個主設備號,這可以通過在驅動程序的初始化過程中調用alloc_chrdev_region( )或者register_chrdev_region( )來完成。而在關閉字符設備時,則需要通過調用unregister_chrdev_region( )從內核中注銷設備,同時釋放占用的主設備號。
2.設備的打開與釋放
打開設備是通過調用file_operations結構中的函數open( )來完成的,它是驅動程序用來為今后的操作完成初始化準備工作的。在大部分驅動程序中,open( )通常需要完成下列工作:
a. 檢查設備相關錯誤,如設備尚未準備好等。
b. 如果是第一次打開,則初始化硬件設備。
c. 識別次設備號,如果有必要則更新讀寫操作的當前位置指針f_ops。
d. 分配和填寫要放在file->private_data里的數據結構。
e. 使用計數增1。
釋放設備是通過調用file_operations結構中的函數release( )來完成的,這個設備方法有時也被稱為close( ),它的作用正好與open( )相反,通常要完成下列工作:
a. 使用計數減1。
b. 釋放在file->private_data中分配的內存。
c. 如果使用計算為0,則關閉設備。
3.設備的讀寫操作
字符設備的讀寫操作相對比較簡單,直接使用函數read( )和write( )就可以了。但如果是塊設備的話,則需要調用函數block_read( )和block_write( )來進行數據讀寫,這兩個函數將向設備請求表中增加讀寫請求,以便Linux內核可以對請求順序進行優化。由于是對內存緩沖區而不是直接對設備進行操作的,因此能很大程度上加快讀寫速度。如果內存緩沖區中沒有所要讀入的數據,或者需要執行寫操作將數據寫入設備,那么就要執行真正的數據傳輸,這是通過調用數據結構blk_dev_struct中的函數request_fn( )來完成的。
4.設備的控制操作
除了讀寫操作外,應用程序有時還需要對設備進行控制,這可以通過設備驅動程序中的函數ioctl( )來完成,ioctl 系統調用有下面的原型: int ioctl(int fd, unsigned long cmd, …),第一個參數是文件描述符,第二個參數是具體的命令,一般使用宏定義來確定,第三個參數一般是傳遞給驅動中處理設備控制操作函數的參數。ioctl( )的用法與具體設備密切關聯,因此需要根據設備的實際情況進行具體分析。
5.設備的中斷和輪詢處理
對于不支持中斷的硬件設備,讀寫時需要輪流查詢設備狀態,以便決定是否繼續進行數據傳輸。如果設備支持中斷,則可以按中斷方式進行操作。
五、PCI驅動程序框架
1.關鍵數據結構
PCI設備上有三種地址空間:PCI的I/O空間、PCI的存儲空間和PCI的配置空間。CPU可以訪問PCI設備上的所有地址空間,其中I/O空間和存儲空間提供給設備驅動程序使用,而配置空間則由Linux內核中的PCI初始化代碼使用。內核在啟動時負責對所有PCI設備進行初始化,配置好所有的PCI設備,包括中斷號以及I/O基址,并在文件/proc/pci中列出所有找到的PCI設備,以及這些設備的參數和屬性。
Linux驅動程序通常使用結構(struct)來表示一種設備,而結構體中的變量則代表某一具體設備,該變量存放了與該設備相關的所有信息。好的驅動程序都應該能驅動多個同種設備,每個設備之間用次設備號進行區分,如果采用結構數據來代表所有能由該驅動程序驅動的設備,那么就可以簡單地使用數組下標來表示次設備號。
在PCI驅動程序中,下面幾個關鍵數據結構起著非常核心的作用:
a. pci_driver
這個數據結構在文件include/linux/pci.h里,其中最主要的是用于識別設備的id_table結構,以及用于檢測設備的函數probe( )和卸載設備的函數remove( ):
b. pci_dev
這個數據結構也在文件include/linux/pci.h里,它詳細描述了一個PCI設備幾乎所有的硬件信息,包括廠商ID、設備ID、各種資源等
2.基本框架
在用模塊方式實現PCI設備驅動程序時,通常至少要實現以下幾個部分:初始化設備模塊、設備打開模塊、數據讀寫和控制模塊、中斷處理模塊、設備釋放模塊、設備卸載模塊。下面給出一個典型的PCI設備驅動程序的基本框架,從中不難體會到這幾個關鍵模塊是如何組織起來的。
上面這段代碼給出了一個典型的PCI設備驅動程序的框架,是一種相對固定的模式。需要注意的是,同加載和卸載模塊相關的函數或數據結構都要在前面加上 __init、__exit等標志符,以使同普通函數區分開來。構造出這樣一個框架之后,接下去的工作就是如何完成框架內的各個功能模塊了。
六、框架的具體實現之模塊操作
1.struct pci_device_id
PCI驅動程序向PCI子系統注冊其支持的廠家ID,設備ID和設備類編碼。使用這個數據庫,插入的卡通過配置空間被識別后,PCI子系統把插入的卡和對應的驅動程序綁定。
PCI設備列表
pci_device_id被用在struct pci_device 中。在示例中,創建了一個結構體數組,每一個結構表明使用該結構體數組的驅動支持的設備,數組的最后一個值是全部設置為0的空結構體,也就是{0,}。這個結構體需要被導出到用戶空間,使熱插拔和模塊裝載系統知道什么模塊對應什么硬件設備,宏MODULE_DEVICE_TABLE完成這個工作。例如:
MODULE_DEVICE_TABLE(pci, demo_pci_tbl);2.初始化設備模塊
在Linux系統下,想要完成對一個PCI設備的初始化,需要完成以下工作:
a. 檢查PCI總線是否被Linux內核支持;
b. 檢查設備是否插在總線插槽上,如果在的話則保存它所占用的插槽的位置等信息。
c. 讀出配置頭中的信息提供給驅動程序使用。
當Linux內核啟動并完成對所有PCI設備進行掃描、登錄和分配資源等初始化操作的同時,會建立起系統中所有PCI設備的拓撲結構。系統加載模塊是調用pci_init_module函數,在這個函數中我們通過pci_register_driver 把new_pci_driver注冊到系統中。在調用pci_register_driver時,需要提供一個pci_driver結構。這個函數首先檢測id_table中定義的PCI信息是否和系統中的PCI信息有匹配,如果有則返回0,匹配成功后調用probe函數對PCI設備進行進一步的操作。
static int __init demo_init_module (void) { /* allocate (several) major number */ret = alloc_chrdev_region(&devno, 0, MAX_DEVICE, "buffer"); ret= pci_register_driver(&demo_pci_driver); }probe函數的作用就是啟動pci設備,讀取配置空間信息,進行相應的初始化。
static int __init demo_probe(struct pci_dev *pci_dev,const struct pci_device_id *pci_id) {int result;printk("probe function is running\n");struct device_privdata *privdata;privdata->pci_dev = pci_dev; //把設備指針地址放入PCI設備中的設備指針中,便于后面調用pci_get_drvdatapci_set_drvdata(pci_dev, privdata); /* 啟動PCI設備 */if(pci_enable_device(pci_dev)){printk(KERN_ERR "%s:cannot enable device\n", pci_name(pci_dev)); return -ENODEV;} /*動態申請設備號,把fops傳進去*/privdata->cdev = cdev_alloc();privdata->cdev->ops=&jlas_fops;privdata->cdev->owner = THIS_MODULE;cdev_add(privdata->cdev,devno,1);/*動態創建設備節點*/privdata->cdev_class = class_create(THIS_MODULE,DEV_NAME);device_create(privdata->cdev_class,NULL, devno, pci_dev, DEV_NAME);privdata->irq=pci_dev->irq;privdata->iobase=pci_resource_start(privdata->pci_dev, BAR_IO);/*判斷IO資源是否可用*/if((pci_resource_flags(pci_dev, BAR_IO) & IORESOURCE_IO) != IORESOURCE_IO)goto err_out;/* 對PCI區進行標記 ,標記該區域已經分配出去*/ ret= pci_request_regions(pci_dev, DEVICE_NAME);if(ret) goto err_out;/*初始化tasklet*/tasklet_init(&(privdata->my_tasklet),jlas_1780_do_tasklet,(unsigned long )&jlas_pci_cdev); /* 初始化自旋鎖 */ spin_lock_init(&private->lock); /*初始化等待隊列*/init_waitqueue_head(&(privdata->read_queue)); /* 設置成總線主DMA模式 */ pci_set_master(pci_dev); /*申請內存*/privdata->mem = (u32 *) __get_free_pages(GFP_KERNEL|__GFP_DMA | __GFP_ZERO, memorder);if (!privdata->mem) { goto err_out;} /*DMA映射*/privdata->dma_addrp = pci_map_single(pdev, privdata->mem,PAGE_SIZE * (1 << memorder), PCI_DMA_FROMDEVICE);if (pci_dma_mapping_error(pdev, privdata->dma_addrp)) {goto err_out;} /*對硬件進行初始化設置,往寄存器中寫一些值,復位硬件等*/device_init(xx_device);return 0;err_out:printk("error process\n");resource_cleanup_dev(FCswitch); //如果出現任何問題,釋放已經分配了的資源return ret; }3.卸載設備模塊
卸載設備模塊與初始化設備模塊是相對應的,實現起來相對比較簡單,主要是調用函數pci_unregister_driver( )從Linux內核中注銷設備驅動程序:
在卸載模塊時調用pci_cleanup_module,這個函數中通過pci_unregister_driver對new_pci_driver進行注銷,這個會調用到remove函數。remove函數的職責就是釋放一切分配過的資源,根據自己代碼的需要進行具體的操作。
static void __devexit my_pci_remove(struct pci_dev *pci_dev) {struct device_private *private;private= (struct device_private*)pci_get_drvdata(pci_dev); /*對硬件進行操作,如硬件復位*/Device_close(xx_device);pci_unmap_single(pdev, privdata->dma_mem,PAGE_SIZE * (1 << memorder), PCI_DMA_FROMDEVICE); // 釋放分配的內存空間free_pages ((unsigned long) privdata->mem, memorder);pci_clear_master(pdev); /* Nobody seems to do this */tasklet_kill(&(privdata->my_tasklet));pci_release_regions(pci_dev); // 移除動態創建的設備號和設備device_destroy(device_class, device->my_dev);class_destroy(device_class);if(privdata->pci_dev!=NULL)cdev_del(privdata->cdev); privdata->pci_dev=NULL;pci_disable_device(pci_dev);pci_set_drvdata(pci_dev,NULL); }4.中斷處理
中斷處理,主要就是讀取中斷寄存器,然后調用中斷處理函數來處理中斷的下半部分,一般通過tasklet或者workqueue來實現。
注意:由于使用request_irq 獲得的中斷是共享中斷,因此在中斷處理函數的上半部需要區分是不是該設備發出的中斷,這就需要讀取中斷狀態寄存器的值來判斷,如果不是該設備發起的中斷則 返回 IRQ_NONE
七、框架的具體實現之設備文件操作
1.設備文件操作接口
當應用程序對設備文件進行諸如open、close、read、write等操作時,Linux內核將通過file_operations結構訪問驅動程序提供的函數。例如,當應用程序對設備文件執行讀操作時,內核將調用file_operations結構中的read函數。
2.打開設備
open 方法提供給驅動來做任何的初始化來準備后續的操作.在這個模塊里主要實現申請中斷、檢查讀寫模式以及申請對設備的控制權等。在申請控制權的時候,非阻塞方式遇忙返回,否則進程主動接受調度,進入睡眠狀態,等待其它進程釋放對設備的控制權。 open 方法的原型是:
inode 參數有我們需要的信息,以它的 i_cdev 成員的形式, 里面包含我們之前建立的cdev 結構. 唯一的問題是通常我們不想要 cdev 結構本身, 我們需要的是包含 cdev 結構的 device_private 結構.
static int demo_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++; /*申請中斷*/ret = request_irq(privdata->irq,interrupt_handler, IRQF_SHARED, DEV_NAME,privdata);if(ret)return -EINVAL; ...try_module_get(THIS_MODULE);return 0; }3.釋放設備
release 方法的角色是 open 的反面,設備方法應當進行下面的任務:
a. 釋放 open 分配在 filp->private_data 中的任何東西
b. 在最后的 close 關閉設備
4.設備數據讀寫和ioctl
PCI設備驅動程序可以通過device_fops 結構中的函數device_ioctl( ),向應用程序提供對硬件進行控制的接口。例如,通過它可以從I/O寄存器里讀取一個數據,并傳送到用戶空間里。
5.內存映射
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()函數的原型
該函數的功能是創建頁表。其中參數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把相應的內存設置為保留后就可以。
八、附錄
1.PCI設備私有數據結構
2.PCI配置寄存器
所有的PCI設備都有至少256字節的地址空間,前64字節是標準化的,而其余的是設備相關的。圖1顯示了設備無關的配置空間的布局。
在Linux系統上,可以通過cat /proc/pci 等命令查看系統中所有PCI設備的類別、型號以及廠商等等信息,那就是從這些寄存器來的。下面是用lspci -x命令截取的部分信息(lspci命令也是使用/proc文件作為其信息來源)(PCI寄存器是小端字節序格式的):
00:00.0 Host bridge: Intel Corp. 440BX/ZX/DX - 82443BX/ZX/DX Host bridge (rev 01) 00: 86 80 90 71 06 00 00 02 01 00 00 06 00 00 00 00 10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 20: 00 00 00 00 00 00 00 00 00 00 00 00 ad 15 76 19 30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00那么根據下面的PCI配置寄存器組的結構,這個Host bridge的Vendor ID就是0x8086。
圖1.標準化的PCI配置寄存器
參考資料:
1. LINUX設備驅動程序(第三版)
2. Linux下PCI設備驅動程序開發
http://www.ibm.com/developerworks/cn/linux/l-pci/index.html
3. Linux PCI 設備驅動基本框架(一)
http://www.cnblogs.com/zhuyp1015/archive/2012/06/30/2571400.html
4. Linux PCI 設備驅動基本框架(二)
http://www.cnblogs.com/zhuyp1015/archive/2012/06/30/2571408.html
5. 淺談Linux PCI設備驅動(一)
http://blog.csdn.net/linuxdrivers/article/details/5849698
總結
- 上一篇: 前端的长度单位有哪些
- 下一篇: 流程是什么