2011年3月3日 星期四

如何撰寫ARM CPU的Linux PCI Host 驅動程式

簡介

PCI是一種BUS,它可以讓你擴充週邊元件的一種BUS,在PC上使用非常普遍,因為它是一個共同的標準介面,所以它可以讓很多的週邊元件附加在這個電腦上,而且可以很容易地附加進來,雖然現在的嵌入式系統已經把很多的週邊已SoCCPU裏面,可是有時候在成本的考量,由PCI來擴充週邊,就不會有必須要的現象產生,因為他可以選擇,這樣就不會造成SoCCPU成本過高,另外把過多的週邊元件SoC會造成CPU用電過高,更進而造成過熱現象產生,造成必需使用其它的煽熱元件,如鋁片或者風扇等,這會造成產品體積變大,成本增加,等問題產生。

PCI的另一個好處是它是一個PNPBUS,對資源的分配及取得是較有彈性,如此一來對軟體撰寫者也是一大好處,因為這樣一來軟體較容易移植至各個不同的CPU平台,而不需要因為不同的CPU而撰寫不同的程式。


ARMx86PCI有何不同

以硬體來說是没有不同,因為它本來就是一種標準,那在軟體上有何不同?因為在x86上只有一種CPU,而且它的PCI HostIC都一樣,並不會因為不同的生產廠商而有所不同,也因此在PCI Host的驅動程式都是OS廠商所撰寫好了,但是每家的ARM CPU廠商所生產的PCI Host的控制器都不同,所以軟體人員必須各別撰寫,以應付不同的CPU廠商,也是為什麼我們必須要知道如何撰寫PCI Host驅動程式。

另外PCI它是一種主從式架構,host端是由主機板廠商所負責,x86平台它通常會留下幾個標準的slot讓使用者可以購買其它的client的擴充卡來增加週邊元件,在ARM base的嵌入系統中,通常PCI Host會是SoCCPU內,對於client的元件,原則上不會是做成slot再插卡,通常是直接拉PCI Bus的訊號線從PCI HostPCI client,如此一來就可節省空間及成本。


PCI Bus開機順序

PCI Bus在開機程序中,開機時PCI Host會先自我控制器做初始化程序,之後會將可分配及使用的資源和系統登錄,之後將掃描是否有PCI client的週邊元件接上,若有則依照它們所要求的資源分配給它們,分配完之後則檢查是否有相對應的client端登錄驅動程式,若有則呼叫其所登錄的call back function,而這個call back function則是PCI client的驅動程式所登錄,在這個call back function中通常所完成的事是初始化其元件,PCI host將會逐一已插上的PCI client重覆前面的動作,一直到所有已插上的週邊元件都完成分配資源。


PCI Configuration Table

PCI hostPCI client之間有一個表叫做configuration table,該表有256字元,它是做為hostclient之間溝通資源分配及其資訊交換的表格,其內容如下:
















以上所看到是我們較會用到的部分,這份表是存放在PCI client上的,其中的幾個重要欄位說明如下:

  • Vendor ID & Device ID是唯讀的,PCI host將會把它存起來,PCI client也是用這兩個ID來向host做註冊,host也會拿它來做比對,當前面所提要呼叫client端時才知道要呼叫那一個驅動程式。
  • command則是host有些動作需要client端來做時所下的指令,而這些指令在PCI的標準有定義。
  • status是目前client現狀的表示,也是PCI標準中有所定義。
  • Base Address Registers是非常重要的項目,它是clienthost端資源描述的地方,而且它是一個雙向溝通的資料,對32位元的設備來說則總共最多有6個資源可以取得,若是64位元的設備,則最多只有3個資源可以取得,其實做法很簡單,就是將兩個32位元合成一個64位元。在此我們只說明32位元的設備,因為目前64位元的嵌入式並不多見,在說明之前我們先了解一件事,就是當CPU和週邊做溝通有兩個路徑,一個是I/O存取,一個記憶體存取,這在硬體上有所差異,所以在這欄位使用最低的兩個位元來表示需求何種資源,以二進位來看,00代表是需要記憶,01代表是需要 IO,所耍的大小從位元2開始,若不需要則填零,不然則填其所需的大小,當PCI Host掃描之後會將該client週邊所分配到的起始位置填回,如此完成資源的分配。

PCI Host實做
在了解了PCI Bus的基本定義之後我們開始要來談實做的部分,在PCI Host部分,因為像前面所提的部分標準和硬硬體無關,或者另外的說法是硬體工作內容是一樣,因為這部分已由GNU所撰寫好,我們並不需要做任何更動,這部分的原始碼在kernel sourcedrivers/pci目錄之下,我們所要寫的是HAL部分,你必須在arch/arm/mach-xxxx的目錄之下(這個目錄是你在porting一個SoC時會建立的目錄,其中的xxxx是你自行定義的名稱),撰寫一個PCI Host的一個C程式,而在這程式有幾個部分需要撰寫,首先需要宣告一個資料結構如下:



struct hw_pci ixdp425_pci __initdata = {

.nr_controllers = 1,

.preinit = ixdp425_pci_preinit,

.swizzle = pci_std_swizzle,

.setup = ixp4xx_setup,

.scan = ixp4xx_scan_bus,

.map_irq = ixdp425_map_irq,

};



先在此說明一下這個資料結構的定義,其資料結構原型為struct hw_pci,其定義在arch/arm/mach/pci.h (這是kernel版本2.6.31之後,ixdp425_pci 是使用者自行取的名字,__initdata定義為這個資料在kernel初始化之後會給予free收回使用),其各個成員說明如下:

.nr_controllers

是指示有多少個PCI host控制器,通常都是只有一個。

.preinit

這是在初始化該PCI host chipsetcall back function,這也是和各個SoC有所不同,所要做的初始化內容也不盡相同, 當註冊一個PCI Host時第一個被呼叫的副程式,若有需要中斷的設定,必需要在此完成和系統註冊。

.setup

它是一個call back function pointer,在這個副程式原型為int ixp4xx_setup(int nr, struct pci_sys_data *sys),所以系統會將第幾個PCI host controller - nr以及一個指struct pci_sys_data *sys,在呼它時傳送給它,這個指標將會是需要填入這PCI Host controller可以使用的資源,以下是一個範例:

struct resource *res;

res = kzalloc(sizeof(*res) * 2, GFP_KERNEL);

res[0].name = "PCI I/O Space";

res[0].start = 0x00000000;

res[0].end = 0x0000ffff;

res[0].flags = IORESOURCE_IO;

res[1].name = "PCI Memory Space";

res[1].start = 0x4b000000;

res[1].end = 0x4bffffff;

res[1].flags = IORESOURCE_MEM

request_resource(&ioport_resource, &res[0]);

request_resource(&iomem_resource, &res[1]);

sys->resource[0] = &res[0];
sys->resource[1] = &res[1];

sys->resource[2] = NULL;

從以上的註冊系統將會保留所要的資源,而在之前所提的標準的PCI driver會協助分配資源給PCI client,所以之後如何分配及管理這些資源,你就不用費心了。

.scan

這是系統要開始掃描在PCI Bus有插上那些PCI client時會呼叫的副程式,通常都會直接再展轉呼叫系統的掃描程式,以下是其範例程式:

return pci_scan_bus(sys->busnr, &ixp4xx_ops, sys);


在以上的sys變數為系統傳過來的,ixp4xx_ops則是我們所要準備的一些有關PCI Bus設定的一些HAL call back function,其內容為如下:
struct pci_ops ixp4xx_ops = {
.read = ixp4xx_pci_read_config,
.write = ixp4xx_pci_write_config,
};

上面其的read/write為在讀寫前面所談的configuration table的方式,這和每一個晶片不同,其控制的位置也不同,所以根據每一顆SoC來寫,而這兩個的函數原型如下:

int ixp4xx_pci_read_config(struct pci_bus *bus, unsigned int devfn, int where, int size, u32 *value)

int ixp4xx_pci_write_config(struct pci_bus *bus, unsigned int devfn, int where, int size, u32 value)

在系統呼叫進來的bus的資料結構中成員bus->number是這個系統中第幾個bus,在控制晶片時可能會用到,devfn則是slot numberfunction number的結合,你可以使用巨集指令PCI_SLOT()PCI_FUNC()分別取得其資訊,size則是表示1 or 2 or 4 bytes的存取,where則是需要特別注意,它是在存取1 or 2 bytes會用到,在256 bytesconfiguration table的相對位置,大部分PCI host 控制器會有boundary存取的問題,這邊需要特別注意做轉換,value則是較簡單,是讀取或寫入所放的值。


.map_irq

則是當PCI Host在掃描時在要指定IRQ時會呼叫這個副程式,而這個副程式則會回傳要指定的中斷號碼。

當以上的資料準備好之後,你需要撰寫一個初始化的進入點,讓系統開機會呼叫這個初始化副程式,以下為其範例:


int __init ixdp425_pci_init(void)

{

return pci_common_init(&ixdp425_pci);

}

subsys_initcall(ixdp425_pci_init);

由以上的程式碼可以看到,該程式會直接呼叫系統的pci_common_init()而且給它前面所提的資料結構,當你呼叫該系統程式時,系統會馬上呼叫你所給的資料結構中的preinit call back function,之後會呼叫setup call back function,再接著就是呼叫scan call back function,如此而完成一個PCI host的載入動作,以下用流程圖來表示如下:





最後記得用巨集指令subsys_initcall()將你所完成的副程式註冊為一個開機會被呼叫的副程式。


PCI Host需要注意的地方

原則上在以上所說明的應該就完成了PCI Host的移植,因為我們在移植時只要完成這部分會和硬體有關的 HAL 層即可,但是還是有一些地方必需要注意,第一個就是在x86的平台上是有所謂的I/O存取,我是指CPU本身就有這個支援,CPU對週邊的元件的存取方式,本來就有兩種,一種是記憶體存取,一種是I/O存取,而在PCI bus也有這兩種的存取方式,所以這兩種不同的存取方法,被視為兩種不同的資源,但是在ARM CPU中是没有I/O 存取的,它只有記憶體存取,而我們在寫驅動程式時若是I/O存取則會使用inb(), oub(), inw(), outw(), inl(), outl()等副程式,若是記憶體存取則會使用readb(), writeb(), readw(), writew(), readl(), writel()等副程式,因為ARM CPU沒有I/O 存取所以在HAL層會將inx(), outx()等副程式內容改寫,改成使用記憶體存取,如此一來應是沒有任何問題才對,問題在於對於I/O存取的驅動程式,對於所使的資源(也就所分配到的I/O 位置)不會將其轉換為虛擬位址,可是如此一來將會產生非法存取記憶體位置,因為kernel一啓動所有的程式碼都需要使用虛擬位址,對於這個問題有幾個解決方法,第一個是修改PCI client的驅動程式碼,可是這會有一個缺點,那就是這個原始碼必需只能使用在你的ARM平台,其它如x86的平台將無法使用,另外你也必需修改你的驅動程式,所以個人比較不喜歡這個方式,另一個方式則是修改我的PCI host的資源分配,很簡單的做法,就是前面有提到在初始化時需要將可分配的資源向系統註冊,註冊前先將其轉換為虛擬位址,再向系統註冊就可以了,如此的修改不多,又沒有前一個方法的問題,並且前面已說明我們本來就是需要修改PCI host的程式碼。

同樣是存取方式的問題,我們先看以下的表示圖,再來討論這個問題:



因為ARM CPU只有記憶體存取的方式,而PCI bus卻有兩種(記憶體和I/O的存取方式),另外PCI host的晶片有兩種,一種是它本身對PCI bus的兩種不同存取方式會做自動的轉換,意思對於PCI client是使用I/O存取方式的週邊,它會自動將ARM CPU過來的記憶體存取方式,自動轉換為I/O存取方式,所以對於前面所提的第二種I/O存取方式的修改將可以達成,而對於ARM CPU的記憶體存取方式,則可以直接轉換至PCI bus即可,對於這種PCI Host的晶片可以省卻我們很多工夫。但是有另外一種PCI host晶片是不會如此做的,對於這種晶片則是較麻煩的,對於這種晶片你必須控其中的一些暫存器在PCI bus上做出I/O 存取的訊號,以下則是其寫法說明(這是在2.6.31之後的寫法):

首先你必須在arch/arm/mach-xxxx的目錄之下,建立一個檔案include/mach/io.h,請注意其路徑及檔名不可更改,因為在整個kernel 原始碼中的Makefile及編譯時要尋找C語言的include file路徑都已寫好,你若更改將會造成編譯錯誤,在這個檔案之中將可以改寫ARM CPU已內定改好的inx()outx()的副程式,若你在這個檔案之中宣告如下:

#define __io(v) __typesafe_io(v)

表示使用ARM CPU中己改好的inx()outx()副程式,這是使用PCI host會自動轉換I/O 存取至PCI bus的晶片,若不是你所需要的修改如下:

#define outb(p, v) __ixp4xx_outb(p, v)

#define outw(p, v) __ixp4xx_outw(p, v)

#define outl(p, v) __ixp4xx_outl(p, v)

#define outsb(p, v, l) __ixp4xx_outsb(p, v, l)

#define outsw(p, v, l) __ixp4xx_outsw(p, v, l)

#define outsl(p, v, l) __ixp4xx_outsl(p, v, l)

#define inb(p) __ixp4xx_inb(p)

#define inw(p) __ixp4xx_inw(p)

#define inl(p) __ixp4xx_inl(p)

#define insb(p, v, l) __ixp4xx_insb(p, v, l)

#define insw(p, v, l) __ixp4xx_insw(p, v, l)

#define insl(p, v, l) __ixp4xx_insl(p, v, l)

你必需針對每一個副給予重新定義至另一個你改寫的內容,而這些改寫的內容則是針對你所使用的PCI host晶片做控制,以下為其範例:

static inline void

__ixp4xx_outb(u8 value, u32 addr)

{

u32 n, byte_enables, data;


n = addr % 4;

byte_enables = (0xf & ~BIT(n)) << IXP4XX_PCI_NP_CBE_BESL;

..........................

return data >> (8*n);

}

.................................


另外幾個和I/O有關的副程式你也必需重新改寫如下:

#define ioread8(p) __ixp4xx_ioread8(p)

#define ioread16(p) __ixp4xx_ioread16(p)

#define ioread32(p) __ixp4xx_ioread32(p)

#define ioread8_rep(p, v, c) __ixp4xx_ioread8_rep(p, v, c)

#define ioread16_rep(p, v, c) __ixp4xx_ioread16_rep(p, v, c)

#define ioread32_rep(p, v, c) __ixp4xx_ioread32_rep(p, v, c)

#define iowrite8(v,p) __ixp4xx_iowrite8(v,p)

#define iowrite16(v,p) __ixp4xx_iowrite16(v,p)

#define iowrite32(v,p) __ixp4xx_iowrite32(v,p)

#define iowrite8_rep(p, v, c) __ixp4xx_iowrite8_rep(p, v, c)

#define iowrite16_rep(p, v, c) __ixp4xx_iowrite16_rep(p, v, c)

#define iowrite32_rep(p, v, c) __ixp4xx_iowrite32_rep(p, v, c)

#define ioport_map(port, nr) ((void __iomem*)(port + PIO_OFFSET))

#define ioport_unmap(addr)

#define PIO_OFFSET 0x10000UL

#define PIO_MASK 0x0ffffUL

#define __is_io_address(p) (((unsigned long)p >= PIO_OFFSET) && \

((unsigned long)p <= (PIO_MASK + PIO_OFFSET)))

static inline unsigned int

__ixp4xx_ioread8(const void __iomem *addr)

{

unsigned long port = (unsigned long __force)addr;

if (__is_io_address(port))

return (unsigned int)__ixp4xx_inb(port & PIO_MASK);

else

return (unsigned int)__ixp4xx_readb(addr);

}

..........................................

static inline void

__ixp4xx_iowrite8(u8 value, void __iomem *addr)

{

unsigned long port = (unsigned long __force)addr;

if (__is_io_address(port))

__ixp4xx_outb(value, port & PIO_MASK);

else

__ixp4xx_writeb(value, addr);

}

....................................

除此之外有些PCI host的晶片對於ARM CPU的記憶體存取方式也不會直接自動轉換至PCI bus,和I/O存取一樣必須控制PCI host晶片來產生一個PCI bus上的記憶體存取,其改寫內容和I/O改寫很像,一樣的你若不需要改寫,使用ARM CPU內定的程式碼則必需有一個以下的宣告:


#define __mem_pci(a) (a)

若要改寫以下為其範例:


#define writeb(v, p) __ixp4xx_writeb(v, p)

#define writew(v, p) __ixp4xx_writew(v, p)

#define writel(v, p) __ixp4xx_writel(v, p)

#define writesb(p, v, l) __ixp4xx_writesb(p, v, l)

#define writesw(p, v, l) __ixp4xx_writesw(p, v, l)

#define writesl(p, v, l) __ixp4xx_writesl(p, v, l)

#define readb(p) __ixp4xx_readb(p)

#define readw(p) __ixp4xx_readw(p)

#define readl(p) __ixp4xx_readl(p)

#define readsb(p, v, l) __ixp4xx_readsb(p, v, l)

#define readsw(p, v, l) __ixp4xx_readsw(p, v, l)

#define readsl(p, v, l) __ixp4xx_readsl(p, v, l)


static inline void

__ixp4xx_writeb(u8 value, volatile void __iomem *p)

{

....................................

if (addr >= VMALLOC_START) {

__raw_writeb(value, addr);

return;

}
.............................

}

....................................


/*

* We can use the built-in functions b/c they end up calling writeb/readb

*/

#define memset_io(c,v,l) _memset_io((c),(v),(l))

#define memcpy_fromio(a,c,l) _memcpy_fromio((a),(c),(l))

#define memcpy_toio(c,a,l) _memcpy_toio((c),(a),(l))


最後有一個問題需要注意的是,通常對於使用記憶體存取的使用之前,做一個實體記憶體轉換為虛擬位址,若你的PCI host是前面的方式需要使用PCI host來產生一個存取動作,那麼你可以也需要改寫ioremap() & iounmap()的副程式,其範例程式如下:


static inline void __iomem *

__ixp4xx_ioremap(unsigned long addr, size_t size, unsigned int mtype)

{

if((addr <> 0x4fffffff))

return __arm_ioremap(addr, size, mtype);

return (void __iomem *)addr;

}


static inline void

__ixp4xx_iounmap(void __iomem *addr)

{

if ((__force u32)addr >= VMALLOC_START)

__iounmap(addr);

}

#define __arch_ioremap(a, s, f) __ixp4xx_ioremap(a, s, f)

#define __arch_iounmap(a) __ixp4xx_iounmap(a)

至此你應可以完成你的PCI Host的程式,而且可以讓你的PCI client的程式更有相容性,而可以使用在多種CPU之下,你也許會有疑問為何要如此的相容性?因為你的PCI client的驅動程式,可能由晶片廠商所提供,但是他們只有在x86的平台有測試過,為了可以無痛的使用這些原始碼,所以你必需了解以上所提的事。