2. PCI 總線族
下面的總線都屬于 PCI 族:
- PCI:32 bit 總線,33 或 66 MHz。
- MiniPCI:插槽更小,用于筆記本電腦。
- CardBus:外部卡槽,用于筆記本電腦。
- PIX Extended(PCI-X):比 PCI 插槽要寬,64 bit,但支持插入一個標準 PCI 卡。
- PCI Express(PCIe or PCI-E):PCI 的當前代,用串行接口取代并行接口。
- PCI Express Mini Card:應用于較新的筆記本,取代 MiniPCI。
- Express Card:應用于較新的筆記本,取代 CardBus。
這些技術都是互相兼容的,內核驅動可以使用同一套。內核不需要感知硬件到底用的是什么插槽和總線變種。
3. PCI 設備類型
PCI 總線上的設備類型主要有:
4. PCI 特性
主要是針對設備驅動開發者。
- boot 階段 BIOS 或 linux(如果配置了的話)會自動分配設備的資源(I/O 地址,IRQ 中斷線)。PCI/PCIe 的地址分配,參考本號《系統地址映射初始化:基于 PCI 的系統》、《系統地址映射初始化:基于 PCIe 的系統》。
- 設備驅動只需要讀取系統地址空間中的相應配置即可。
- 大小端:PCI 設備的配置信息是小端的。寫驅動的時候要注意(有些內核函數可以提供大小端轉化)。
5. 列舉 PCI 設備
- lspci:列舉所有 PCI 設備。
- lspci -tv:列舉 PCI 總線設備樹。
- PCI 設備樹與 /sys 中的數據結構對應
6. PCI 設備配置
- 每個 PCI 設備有一個 256 byte 地址空間長度的配置寄存器。
- 可以通過 lspci -x 查看設備的配置:
- 標準的 PCI 配置信息:
- 偏移 0:Vendor ID。
- 偏移 2:Device ID。
- 偏移 10:Class ID(網卡、顯卡、橋 ...)。
- 偏移 16 - 39:Base Address Registers(BAR)0 到 5。
- 偏移 44:SubVendor ID。
- 偏移 46:SubDevice ID。
- 偏移 64 及以上:設備制造商。
這些偏移的定義,在內核的 include/linux/pci_regs.h。
7. linux 驅動
7.1 注冊所支持的設備
drivers/net/ne2k-pci.c:
static struct pci_device_id ne2k_pci_tbl[] = {
{ 0x10ec, 0x8029, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_RealTek_RTL_8029 },
{ 0x1050, 0x0940, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_Winbond_89C940 },
{ 0x11f6, 0x1401, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_Compex_RL2000 },
{ 0x8e2e, 0x3000, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_KTI_ET32P2 },
{ 0x4a14, 0x5000, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_NetVin_NV5000SC },
{ 0x1106, 0x0926, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_Via_86C926 },
{ 0x10bd, 0x0e34, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_SureCom_NE34 },
{ 0x1050, 0x5a5a, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_Winbond_W89C940F },
{ 0x12c3, 0x0058, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_Holtek_HT80232 },
{ 0x12c3, 0x5598, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_Holtek_HT80229 },
{ 0x8c4a, 0x1980, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_Winbond_89C940_8c4a },
{ 0, }
};
MODULE_DEVICE_TABLE(pci, ne2k_pci_tbl);
7.2 注冊驅動
注冊驅動的操作函數,以及驅動所支持的設備表:
static struct pci_driver ne2k_driver = {
.name = DRV_NAME,
.probe = ne2k_pci_init_one,
.remove = __devexit_p(ne2k_pci_remove_one),
.id_table = ne2k_pci_tbl,
#ifdef CONFIG_PM
.suspend = ne2k_pci_suspend,
.resume = ne2k_pci_resume,
#endif /* CONFIG_PM */
};
static int __init ne2k_pci_init(void)
{
return pci_register_driver(&ne2k_driver);
}
static void __exit ne2k_pci_cleanup(void)
{
pci_unregister_driver (&ne2k_driver);
}
- 驅動的操作函數以及所支持的設備,會在模塊加載時被載入。
- 如果找到一個匹配的設備,PCI 框架代碼會調用驅動的 probe() 函數。
- 非常類似 USB 設備驅動!
7.3 驅動操作函數修飾
- __init:模塊初始化函數。這些代碼會在驅動初始化完之后被丟棄。
- __exit:模塊退出函數。如果是靜態編譯(編到內核中)的驅動,會忽略之。
- __devinit:probe 函數以及所有初始化函數。如果使能了 CONFIG_HOTPLUG 內核配置,則此類函數就是個正常的函數,否則等同 __init。
- __devinitconst:用于設備 ID 表。
- __devexit:移除時會調用的函數。同 __devinit。
- 所有引用 __devinit 函數地址的地方,都應該使用 __devexit_p(fun) 進行聲明修飾。如果代碼被丟棄的話,此修飾會將函數地址替換為 NULL。
示例:
static struct pci_driver ne2k_driver = {
.name = DRV_NAME,
.probe = ne2k_pci_init_one,
.remove = __devexit_p(ne2k_pci_remove_one),
.id_table = ne2k_pci_tbl,
...
};
7.4 設備初始化步驟
- 使能設備。
- 請求 I/O 端口以及 I/O 內存資源。
- 設置 DMA mask size(coherent 及 streaming DMA 皆需要)。
- 分配并初始化共享控制數據(pci_allocate_coherent())。
- 初始化設備寄存器(如果需要的話)。
- 注冊 IRQ 處理函數(request_irq())。
- 注冊進其他子系統(網絡、顯示、存儲,等等)。
- 使能 DMA 處理引擎。
7.5 設備使能
- 在訪問設備寄存器之前,驅動需要先執行 pci_enable_device(),這會導致:
- 如果設備在 suspend 狀態,則喚醒之。
- 分配設備的 I/O 和內存區域(如果 BIOS 沒有搞定的話)。
- 為設備分配一個 IRQ(如果 BIOS 沒有搞定的話)。
pci_enable_device() 可能會失敗,所以需要檢查其返回值!
pci_enable_device(drivers/net/ne2k-pci.c)示例:
static int __devinit ne2k_pci_init_one
(struct pci_dev *pdev, const struct pci_device_id *ent)
{
...
i = pci_enable_device (pdev);
if (i)
return i;
...
}
- 調用 pci_set_master() 使能 DMA,這會導致:
- 通過設置 PCI_COMMAND 寄存器中的 bus master bit 來使能 DMA。完成后設備便可在地址總線上扮演一個 master 的角色。
- 如果 BIOS 設置了偽造的值,則修復延遲定時器的值(Fix the latency timer value if it's set to something bogus by the BIOS. 這句話沒看明白)。
- 如果設備可以使用 PCI Memory-Write-Invalidate transaction(寫整個 cache lines),你還可以調用 pci_set_mwi():
- 此函數使能 Memory-Write-Invalidate 的 PCI_COMMAND bit。
- 此函數還確保對 cache line size 寄存器進行正確的設置。
7.6 訪問配置寄存器
訪問 I/O 內存和端口信息。
#include < linux/pci.h >
/* 讀接口 */
int pci_read_config_byte(struct pci_dev *dev, int where, u8 *val);
int pci_read_config_word(struct pci_dev *dev, int where, u16 *val);
int pci_read_config_dword(struct pci_dev *dev, int where, u32 *val);
/* 讀示例:drivers/net/cassini.c */
pci_read_config_word(cp- >pdev, PCI_STATUS, &cfg);
/* 寫接口 */
int pci_write_config_byte(struct pci_dev *dev, int where, u8 val);
int pci_write_config_word(struct pci_dev *dev, int where, u16 val);
int pci_write_config_dword(struct pci_dev *dev, int where, u32 val);
/* 寫示例:drivers/net/s2io.c */
/* Clear "detected parity error" bit
pci_write_config_word(sp- >pdev, PCI_STATUS, 0x8000);
- 每個 PCI 設備最多有 6 個 I/O 或內存區域,通過 BAR0 到 BAR5 來描述。
- 訪問 I/O 區域的基地址:
#include < linux/pci.h >
long iobase = pci_resource_start(pdev, bar);
- 訪問 I/O 區域的大小:
long iosize = pci_resource_len(pdev, bar);
- 預留 I/O 區域:
request_region(iobase, iosize, “my driver”);
或者簡單點:
pci_request_region(pdev, bar, “my driver”);
或者更簡單點:
pci_request_regions(pdev, “my driver”);
示例代碼(drivers/net/ne2k-pci.c):
ioaddr = pci_resource_start (pdev, 0);
irq = pdev- >irq;
if (!ioaddr || ((pci_resource_flags (pdev, 0) & IORESOURCE_IO) == 0))
{
dev_err(&pdev- >dev, "no I/O resource at PCI BAR #0\\n");
return -ENODEV;
}
if (request_region (ioaddr, NE_IO_EXTENT, DRV_NAME) == NULL) {
dev_err(&pdev- >dev, "I/O resource 0x%x @ 0x%lx busy\\n", NE_IO_EXTENT, ioaddr);
return -EBUSY;
}
7.7 設置 DMA mask size
- 對于擁有超過(或少于)(那不就是 whatever 么?) 32 bit 總線 master capability 的設備,對此設備調用 pci_dma_set_mask() 聲明之。
- 特別是對于使用 64 bit DMA 的 PCI-X 和 PCIe 兼容設備來說,驅動必須調用此函數
- 如果設備可以直接尋址系統 RAM 中高于 4G 物理地址的“緩存一致性內存”,則通過 pci_set_consistent_dma_mask() 注冊之。
示例(drivers/net/wireless/ipw2200.c):
err = pci_set_dma_mask(pdev, DMA_32BIT_MASK);
if (!err)
err = pci_set_consistent_dma_mask(pdev, DMA_32BIT_MASK);
if (err) {
printk(KERN_WARNING DRV_NAME ": No suitable DMA available.\\n");
goto out_pci_disable_device;
}
7.8 分配緩存一致性的 DMA buffers
至此,已完成 DMA mask size 的分配。
- 如果你準備使用緩存一致性的 buffers,則分配之。
- 詳情參考內核 Documentation/DMA-API.txt。
7.9 初始化設備寄存器
如果設備需要的話:
- 設置一些 "capability" 域。
- 進行一些 vendor specific 的初始化工作或復位。
示例:清除 pending 的中斷。
7.10 注冊中斷處理函數
- 調用 request_irq() 時需要傳入 IRQF_SHARED flag,因為 PCI 中斷線是可共享的。
- 中斷注冊同時會使能中斷,故在中斷注冊的時間點上,需要:
- 確保設備已完成全部初始化工作并準備好處理中斷。
- 確保設備在調用 request_irq() 之前沒有 pending 的中斷。
- 實際調用 request_irq() 的地方取決于設備類型,以及其所屬的子系統(比如網絡、顯示、存儲 ...)。
- 驅動隨后被注冊進其所屬的子系統。
7.11 PCI 設備關停
在 remove() 函數中,你通常需要對在設備初始化(probe() 函數)中所做工作進行逆操作:
- 禁能設備產生中斷:如果你不禁能設備中斷的話,系統可能會收到 spurious 中斷,并最終禁用掉整個中斷線。該中斷線上的其他設備就遭殃了!
- 釋放 IRQ。
- 停止所有 DMA 活動:需要在關閉 IRQs 之后再做(原 slides 這句話后面還有一個注:could start new DMAs,這句話沒看明白)。
- 釋放 DMA buffers:先釋放 streaming buffers,然后再釋放 consistent buffers。
- 從其他子系統中取消注冊。
- 通過 io_unmap() 取消映射 I/O 內存及端口。
- 通過 pci_disable_device() 關閉設備。
- 取消注冊 I/O 內存和端口:如果這一步不做的話,會無法重新加載驅動。
評論
查看更多