我在 "Linux字元設備驅動框架" 一文中簡單介紹了Linux字元設備編程模型,在那個模型中,只要應用程式 open() 了相應的設備文件,就可以使用ioctl通過驅動程式來控制我們的硬體,這種模型直觀,但是從軟體設計的角度看,卻是一種十分糟糕的方式,它有一個致命的問題,就是設備信息和驅動代碼冗餘 ...
我在Linux字元設備驅動框架一文中簡單介紹了Linux字元設備編程模型,在那個模型中,只要應用程式open()了相應的設備文件,就可以使用ioctl通過驅動程式來控制我們的硬體,這種模型直觀,但是從軟體設計的角度看,卻是一種十分糟糕的方式,它有一個致命的問題,就是設備信息和驅動代碼冗餘在一起,一旦硬體信息發生改變甚至設備已經不在了,就必須要修改驅動源碼,非常的麻煩,為瞭解決這種驅動代碼和設備信息耦合的問題,Linux提出了platform bus(平臺匯流排)的概念,即使用虛擬匯流排將設備信息和驅動程式進行分離,設備樹的提出就是進一步深化這種思想,將設備信息進行更好的整理。平臺匯流排會維護兩條鏈表,分別管理設備和驅動,當一個設備被註冊到匯流排上的時候,匯流排會根據其名字搜索對應的驅動,如果找到就將設備信息導入驅動程式並執行驅動;當一個驅動被註冊到平臺匯流排的時候,匯流排也會搜索設備。總之,平臺匯流排負責將設備信息和驅動代碼匹配,這樣就可以做到驅動和設備信息的分離。
在設備樹出現之前,設備信息只能使用C語言的方式進行編寫,在3.0之後,設備信息就開始同時支持兩種編寫方式——設備樹、C語言,如果用設備樹,手動將設備信息寫到設備樹中之後,內核就可以自動從設備樹中提取相應的設備信息並將其封裝成相應的platform_device對象,i2c_device對象並註冊到相應的匯流排中,如果使用設備樹,我們就不需要對設備信息再進行編碼。如果使用C語言,顯然,我們需要將使用內核提供的結構將設備信息進行手動封裝,這種封裝又分為兩種形式,一種是使用平臺文件(靜態),將整個板子的所有設備都寫在一個文件中並編譯進內核。另一種是使用模塊(動態),將我們需要的設備信息編譯成模塊在insmod進內核。對於ARM平臺,使用設備樹封裝設備信息是將來的趨勢,但是由於歷史原因,當下的內核中這三種方式並存。封裝好後再創建相應的xxx_device實例最後註冊到匯流排中。針對平臺匯流排的設備信息,我在Linux設備樹語法詳解一文中已經討論了設備樹的寫法,所以,本文主要討論4個問題:
- 如何使用C語言封裝設備信息?
- 設備樹的設備信息和C語言的設備信息如何轉換?
- 如何將C語言設備信息封裝到platform_device結構中?
- 如何將封裝好的platform_device結構註冊到平臺匯流排中?
何為資源?
所謂的設備信息,主要分為兩種:硬體信息、軟體信息,硬體信息主要包括xxx控制器在xxx地址上,xxx設備占用了xxx中斷號,即地址資源,中斷資源等。內核提供了struct resource來對這些資源進行封裝。軟體信息的種類就比較多樣,比如網卡設備中的MAC地址等等,這些信息需要我們以私有數據的形式封裝的設備對象(內核使用面向對象的思想編寫,一個設備的設備信息是一個對象,即一個結構體實例,一個設備的驅動方法也是一個對象)中,這部分信息就需要我們自定義結構進行封裝。
struct resource那點事
這個結構用來描述一個地址資源或中斷資源,除了這個結構,內核還提供了一些巨集來幫助我們快速的創建一個resource對象。
//include/linux/ioport.h
18 struct resource {
19 resource_size_t start;
20 resource_size_t end;
21 const char *name;
22 unsigned long flags;
23 unsigned long desc;
24 struct resource *parent, *sibling, *child;
25 };
struct resource
--19--> start表示資源開始的位置,如果是IO地址資源,就是起始物理地址,如果是中斷資源,就是中斷號;
--20--> end表示資源結束的位置,如果是IO地址地址,就是映射的最後一個物理地址,如果是中斷資源,就不用填;
--21--> name就是這個資源的名字。
--22--> flags表示資源類型,提取函數在尋找資源的時候會對比自己傳入的參數和這個成員,理論上只要和可以隨便寫,但是合格的工程師應該使用內核提供的巨集,這些巨集也在"ioport.h"中進行了定義,比如IORESOURCE_MEM表示這個資源是地址資源,IORESOURCE_IRQ表示這個資源是中斷資源...。
//include/linux/ioport.h
33 #define IORESOURCE_BITS 0x000000ff /* Bus-specific bits */
35 #define IORESOURCE_TYPE_BITS 0x00001f00 /* Resource type */
36 #define IORESOURCE_IO 0x00000100 /* PCI/ISA I/O ports */
37 #define IORESOURCE_MEM 0x00000200
38 #define IORESOURCE_REG 0x00000300 /* Register offsets */
39 #define IORESOURCE_IRQ 0x00000400
40 #define IORESOURCE_DMA 0x00000800
41 #define IORESOURCE_BUS 0x00001000
...
147 #define DEFINE_RES_IO(_start, _size)
152 #define DEFINE_RES_MEM(_start, _size)
157 #define DEFINE_RES_IRQ(_irq)
162 #define DEFINE_RES_DMA(_dma)
有了這幾個屬性,就可以完整的描述一個資源,但如果每個資源都需要單獨管理而不是組成某種數據結構,顯然是一種非常愚蠢的做法,所以內核的resource結構還提供了三個指針:parent,sibling,child(24),分別用來表示資源的父資源,兄弟資源,子資源,這樣內核就可以使用樹結構來高效的管理大量的系統資源,linux內核有兩種樹結構:iomem_resource,ioport_resource,進行板級開發的時候,通常將主板上的ROM資源放入iomem_resource樹的一個節點,而將系統固有的I/O資源掛到ioport_resource樹上。
下麵是一個小例子,分別用兩種寫法表示了地址資源和中斷資源,強烈推薦使用DEFINE_RES_XXX的版本。
//IO地址資源,自己填充resource結構體+flags巨集
struct resource res= {
.start = 0x10000000,
.end = 0x20000000-1,
.flags = IORESOURCE_MEM
};
//IO地址資源,使用內核提供的定義巨集
struct resource res = DEFINE_RES_MEM(0x20000000, 1024);
//中斷資源,自己填充resource結構體+flags巨集
struct resource res = {
.start = 10,
.flags = IORESOURCE_IRQ,
};
//中斷資源,使用內核提供的定義巨集
struct resource res = DEFINE_RES_IRQ(11);
下麵是一個資源數組的實例,多個資源的時候就寫成數組,這裡我同時使用了上面兩種寫法。
struct resource res[] = {
[0] = {
.start = 0x10000000,
.end = 0x20000000-1,
.flags = IORESOURCE_MEM
},
[1] = DEFINE_RES_MEM(0x20000000, 1024),
[2] = {
.start = 10, //中斷號
.flags = IORESOURCE_IRQ|IRQF_TRIGGER_RISING //include/linux/interrupt.h
},
[3] = DEFINE_RES_IRQ(11),
};
resource VS dts
至此,我們已經討論了使用設備樹和resource結構兩種方式寫設備信息,顯然,這兩種方式最終是殊途同歸的,這裡我們簡單的討論一個二者之間的轉換問題。下圖是我在Linux設備樹語法詳解一文中用到的dm9000網卡的節點
將它的地址資源寫成resource就是這個樣子,清晰起見,這裡也是兩種寫法:
struct resource res[] = {
[0] = {
.start = 0x05000000,
.end = 0x05000000+0x2-1,
.flags = IORESOURCE_MEM,
},
[1] = DEFINE_RES_MEM(0x05000004,2),
};
platform_device對象
這個對象就是我們最終要註冊到平臺匯流排上的設備信息對象,對設備信息進行編碼,其實就是創建一個platform_device對象,可以看出,platform_device和其他設備一樣,都是device的子類
//include/linux/platform_device.h
22 struct platform_device {
23 const char *name;
24 int id;
25 bool id_auto;
26 struct device dev;
27 u32 num_resources;
28 struct resource *resource;
29
30 const struct platform_device_id *id_entry;
31
32 /* MFD cell pointer */
33 struct mfd_cell *mfd_cell;
34
35 /* arch specific additions */
36 struct pdev_archdata archdata;
37 };
在這個對象中,我們主要關心以下幾個成員
struct platform_device
--23-->name就是設備的名字,註意, 模塊名(lsmod)!=設備名(/proc/devices)!=設備文件名(/dev),這個名字就是驅動方法和設備信息匹配的橋梁
--24-->表示這個platform_device對象表徵了幾個設備,當多個設備有共用資源的時候(MFD),裡面填充相應的設備數量,如果只是一個,填-1
--26-->父類對象(include/linux/device.h +722),我們通常關心裡面的platform_data和release,前者是用來存儲私有設備信息的,後者是供當這個設備的最後引用被刪除時被內核回調,註意和rmmod沒關係。
--27-->資源的數量,即resource數組中元素的個數,我們用ARRAY_SIZE()巨集來確定數組的大小(include/linux/kernel.h +54 #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]) )
--28-->資源指針,如果是多個資源就是struct resource[]數組名,
static struct platform_device demoobj = {
//2. init obj
.name = "demo0",
.id = 0,
.dev = {
.platform_data = &priv,
.release = dev_release,
},
.num_resources = ARRAY_SIZE(res),
.resource = res,
};
設備對象的註冊與註銷
準備好了platform_device對象,接下來就可以將其註冊進內核,顯然內核已經為我們準備好了相關的函數
/**
*註冊:把指定設備添加到內核中平臺匯流排的設備列表,等待匹配,匹配成功則回調驅動中probe;
*/
int platform_device_register(struct platform_device *);
/**
*註銷:把指定設備從設備列表中刪除,如果驅動已匹配則回調驅動方法和設備信息中的release;
*/
void platform_device_unregister(struct platform_device *);
通常,我們會將platform_device_register寫在模塊載入的函數中,將platform_device_unregister寫在模塊卸載函數中。