注:由于內(nèi)核版本的演變,設(shè)備樹成了任何使用較高版本linux系統(tǒng)的設(shè)備平臺所必須文件,然國內(nèi)相關(guān)技術(shù)文檔嚴(yán)重不足,本文是國外技術(shù)專欄的翻譯.
本教程是針對Xilinx Zynq-7000設(shè)備寫的,但其中的概念適用于所有使用了設(shè)備樹的Linux內(nèi)核。本文使用Xillinux發(fā)行版為例,該發(fā)行版運(yùn)行于Zedboard硬件上。
?
設(shè)備樹有什么好處
設(shè)想一下:bootloader剛剛將Linux內(nèi)核復(fù)制到內(nèi)存中,然后跳到內(nèi)核的入口點(diǎn)開始執(zhí)行。此時內(nèi)核就像運(yùn)行在處理器上的一個裸機(jī)程序。需要配置處理器,設(shè)置虛擬內(nèi)存,向控制臺打印一些信息。但是這些事情如何完成?所有的這些操作都要通過寫寄存器來實現(xiàn),但Linux內(nèi)核如何知道這些寄存器的地址?如何知道當(dāng)前有多少個CPU核可以使用?有多少內(nèi)存可以訪問?
最直接的辦法就是在內(nèi)核代碼里為指定平臺寫好這些代碼,由內(nèi)核配置參數(shù)決定哪些平臺代碼將被啟用。當(dāng)一切都固定不變時這種方法還不錯,比如在x86處理器上內(nèi)部的寄存器,或是BIOS的訪問。但對于變化量來說, 比如PCI/PCIe外設(shè),就需要內(nèi)核明確了解這些變化的細(xì)節(jié)。
ARM架構(gòu)已經(jīng)變成了Linux社區(qū)的一個在麻煩:即使處理器使用相同的編譯器和函數(shù),但具體到某一種芯片,它就有自己的寄存器地址和不同的配置方式。不僅如此,每種板子都有自己的外設(shè)。結(jié)果造成內(nèi)核中有大量的頭文件、補(bǔ)丁和特殊的配置參數(shù),它們的一種組合就對應(yīng)于一款芯片的一種特殊板型??傊@造成了大量丑陋和不可維護(hù)的代碼。
另外,每個編譯出來的內(nèi)核bin文件都是為某一款芯片的某一種板子,有點(diǎn)像為市場上某一款PC主板編譯內(nèi)核。所以很希望為所有ARM處理器編譯內(nèi)核時,讓內(nèi)核能以某種方式識別硬件,然后使用正確的驅(qū)動,就像一臺PC一樣。
怎么實現(xiàn)呢?在PC上,寄存器初始化是硬編碼的,其他的信息由BIOS提供。所以當(dāng)有另一塊軟件提供這些信息時,硬件自動檢測也很容易。ARM處理器沒有BIOS,Linux內(nèi)核只能靠自己了。
解決方案是設(shè)備樹devicetree, 也稱作Open Firmware(OF)或FlattenedDevice Tree(FDT)。本質(zhì)上是一個字節(jié)碼格式的數(shù)據(jù)結(jié)構(gòu),其中包含信息在內(nèi)核啟動時非常有用。bootloader在跳到內(nèi)核入口點(diǎn)之前將這一塊數(shù)據(jù)復(fù)制到RAM中的已知地址。
設(shè)備樹的嚴(yán)格的規(guī)范,卻沒有規(guī)定哪些內(nèi)容可以放置其中以及放置的位置。內(nèi)核可以搜索設(shè)備樹中的任意路徑和參數(shù)。程序員來決定哪些配置作為參數(shù)放進(jìn)設(shè)備樹里,以及放置在什么地方。
采取標(biāo)準(zhǔn)的樹結(jié)構(gòu),則可用一套方便的API來操作。例如,約定好如何定義總線上的外設(shè),那么API可以獲取到驅(qū)動所需的基本信息:地址、中斷和自定義變量。后面會介紹更多。
對于我們大多數(shù)人來說,我們用設(shè)備樹來向內(nèi)核描述對硬件的添加或刪除操作,作為響應(yīng),內(nèi)核就可以加載或卸載相應(yīng)的驅(qū)動。硬件的特殊信息也可以通過設(shè)備樹來向內(nèi)核傳達(dá)。
編譯設(shè)備樹
設(shè)備樹有三種形式:
* 文本文件 (.dts) - 源
二進(jìn)制對象 (.dtb) - 目標(biāo)碼
Linux系統(tǒng)中/proc/device-tree目錄 - 調(diào)試和逆向信息
啟用/proc/device-tree目錄需要打開配置CONFIG_PROC_DEVICETREE:
Device Drivers --->
Device Tree and Open Firmware support --->
[*] Support for device tree in /proc
對于設(shè)備樹,我們一般的使用流程是:編輯DTS文件,然后用一個工具將其編譯成DTB文件,這個工具就在Linux內(nèi)核源碼scripts/dtc/目錄下。
設(shè)備樹編譯器也可以單獨(dú)下載并編譯:
$ git clone git://www.jdl.com/software/dtc.git dtc
$ cd dtc
$ make
但是下文的描述都使用內(nèi)核原碼中的dtc工具。
設(shè)備樹的語法在這里描述。注意這種語言并不作任何執(zhí)行操作,不像XML,這只是一種組織數(shù)據(jù)的語法。一些架構(gòu)有自動產(chǎn)生設(shè)備樹的工具,來自于XPS項目。但目前對于Zynq EPP平臺還沒有此工具。
DTS編譯為DTB:
$ scripts/dtc/dtc -I dts -O dtb -o /path/to/my-tree.dtb /path/to/my-tree.dts
這樣就創(chuàng)建了my-tree.dtb二進(jìn)制文件。dtc是主機(jī)上的一個程序。如果內(nèi)核沒有編譯過,則先需要編譯好DTS編譯器:配置內(nèi)核,也可以復(fù)制一份已有的配置文件到內(nèi)核根目錄下的.config。如下:
$ make ARCH=arm digilent_zed_defconfig
生成DTS編譯器:
$ make ARCH=arm scripts
dtc也可以從一個DTB文件或/proc/device-tree文件系統(tǒng)反編譯。例如從DTB反編譯:
$ scripts/dtc/dtc -I dtb -O dts -o /path/to/fromdtb.dts/path/to/booted_with_this.dtb
生成的dts文件仍然可以被用來生成dtb。但最好還是使用最初的DTS文件,因為一些參考標(biāo)簽在反編譯的DTS文件中顯示為數(shù)字。
從運(yùn)行中的內(nèi)核生成DTS文件:
# scripts/dtc/dtc -I fs -O dts -o ~/effective.dts /proc/device-tree/
設(shè)備樹結(jié)構(gòu)
Zynq的設(shè)備樹如下:
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
compatible = "xlnx,zynq-zed";
interrupt-parent = <&gic>;
model = "Xillinux for Zedboard";
aliases {
serial0 = &ps7_uart_1;
} ;
chosen {
bootargs = "consoleblank=0 root=/dev/mmcblk0p2 rw rootwaitearlyprintk";
linux,stdout-path = "/axi@0/uart@E0001000";
};
cpus {
[ ... CPU definitions ... ]
} ;
ps7_ddr_0: memory@0 {
device_type = "memory";
reg = < 0x0 0x20000000 >;
} ;
ps7_axi_interconnect_0: axi@0 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "xlnx,ps7-axi-interconnect-1.00.a","simple-bus";
ranges ;
[ ... Peripheral definitions... ]
} ;
} ;
這是Xillinux使用的設(shè)備樹,刪去了兩個部分:一個描述CPUs(比較無趣),另一個定義外設(shè)(太長了,后面再深入其中的信息)。
默認(rèn)使用的dts是/boot/devicetree-3.3.0-xillinux-1.0.dts。
在第一行版本描述之后,設(shè)備樹以一個斜杠/開始,表示這是樹的根, 然后是大括號。從DTS編譯器的角度來看,大括號包含大更深的層級(相當(dāng)于文件系統(tǒng)里的目錄結(jié)構(gòu))。內(nèi)核代碼會遍歷這顆樹,在某個路徑上抓取到想要的信息(就像在文件系統(tǒng)的某個路徑上讀文件)。
樹的結(jié)構(gòu)是以內(nèi)核期望為準(zhǔn)。其中的賦值對dtc來說沒有意義。事實上,樹中的許多賦值語句都會被內(nèi)核忽略掉,就像某個文件存在于文件系統(tǒng)中,但沒有程序去打開它。
從用戶空間訪問數(shù)據(jù)
與文件系統(tǒng)作類比并不是想當(dāng)然,內(nèi)核真的實現(xiàn)了這個文件系統(tǒng)/proc/device-tree:每個大括號表示一個目錄,目錄名是大括號前面的字符串。
如:
# hexdump -C '/proc/device-tree/#size-cells'
00000000 00 00 00 01 |....|
00000004
# hexdump -C '/proc/device-tree/axi@0/compatible'
00000000 78 6c 6e 78 2c 70 73 37 2d 61 78 69 2d 69 6e 74 |xlnx,ps7-axi-int|
00000010 65 72 63 6f 6e 6e 65 63 74 2d 31 2e 30 30 2e 61 |erconnect-1.00.a|
00000020 00 73 69 6d 70 6c 65 2d 62 75 73 00 |.simple-bus.|
0000002c
或直接:
# cat '/proc/device-tree/axi@0/compatible'
xlnx,ps7-axi-interconnect-1.00.asimple-bus
:
ps7_axi_interconnect_0:
冒號之前的是標(biāo)簽,只出現(xiàn)在DTS文件中,而不會出現(xiàn)在DTB文件里。而靠近大括號的字符串為目錄名。
如上示范,賦值操作在/proc里表示為一個文件,文件名為等號左邊的字符串,文件內(nèi)容為等號右邊的字符串。如果只沒有等號,則創(chuàng)建一個空文件。
上例顯示,設(shè)備樹即能方便地向用戶空間程序傳遞信息,也能向內(nèi)核傳遞信息, /proc/device-tree虛擬文件系統(tǒng)讓這些信息變得可訪問。毋須多言,內(nèi)核中有一套API可訪問設(shè)備樹結(jié)構(gòu)和數(shù)據(jù)。
你可能注意到了整型以大端形式表示,Zynq處理器是小端的,留意這點(diǎn)。
設(shè)備樹里的啟動參數(shù)
一般有三個地方可以放置內(nèi)核啟動命令:
. 內(nèi)核配置的CONFIG_CMDLINE參數(shù)
. 由bootloader傳遞給內(nèi)核
. 在設(shè)備樹的chosen/bootargs下描述
使用哪一個取決于內(nèi)核的配置。在Xillinux中,使用設(shè)備樹chosen/bootargs里描述的cmdline。
選擇哪個UART來輸出內(nèi)核啟動信息是在初始代碼里寫死的。這里即使刪掉ps7_uart_1: serial@e0001000這一行,啟動信息仍會從UART中輸出,只是不會再出現(xiàn)/dev/ttyPS0設(shè)備節(jié)點(diǎn)了。
"alias"和"linux,stdout-path"賦值語句是這個架構(gòu)歷史遺留的,在這里沒有意義。
定義外設(shè)
可能你讀本文是為了給你的設(shè)備寫一個Linux驅(qū)動,在這方面要推薦著名的《Linux Device Driver》。但是在寫一個設(shè)備驅(qū)動之前,允許我分享寫Linux驅(qū)動的第一誡:永遠(yuǎn)不要為Linux寫設(shè)備驅(qū)動。
更好的辦法是找一個維護(hù)狀態(tài)良好的類似功能的設(shè)備驅(qū)動,然后修改它。這不僅僅意味著更容易,更可能幫我們避免我們一些未意識到的問題。從其他驅(qū)動移植過來可以讓這份驅(qū)動更容易被理解,可移植,更可能被內(nèi)核樹接受。
所以現(xiàn)在的重點(diǎn)變?yōu)槔斫馄渌?qū)動,然后做一點(diǎn)調(diào)整。有疑問的地方就照著別人的做法做。創(chuàng)新和個人風(fēng)格在這里沒什么用。
現(xiàn)在,回到設(shè)備樹。讓我們來看看第二部分省略的內(nèi)容:
ps7_axi_interconnect_0: axi@0 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "xlnx,ps7-axi-interconnect-1.00.a","simple-bus";
ranges ;
gic: interrupt-controller@f8f01000 {
#interrupt-cells = < 3 >;
compatible = "arm,cortex-a9-gic";
interrupt-controller ;
reg = < 0xf8f01000 0x1000 >,< 0xf8f00100 0x100 >;
} ;
pl310: pl310-controller@f8f02000 {
arm,data-latency = < 3 2 2 >;
arm,tag-latency = < 2 2 2 >;
cache-level = < 2 >;
cache-unified ;
compatible = "arm,pl310-cache";
interrupts = < 0 34 4 >;
reg = < 0xf8f02000 0x1000 >;
} ;
[ ... more items ... ]
xillybus_0:xillybus@50000000 {
compatible = "xlnx,xillybus-1.00.a";
reg = < 0x50000000 0x1000 >;
interrupts = < 0 59 1 >;
interrupt-parent = <&gic>;
xlnx,max-burst-len = <0x10>;
xlnx,native-data-width = <0x20>;
xlnx,slv-awidth = <0x20>;
xlnx,slv-dwidth = <0x20>;
xlnx,use-wstrb = <0x1>;
} ;
} ;
這里只列出原始DTS文件中的兩個設(shè)備。
第一個條目:Zynq處理器的中斷控制器。這個條目確保中斷控制器被加載。注意它的標(biāo)簽是“gic"。這個標(biāo)簽被每個使用中斷的設(shè)備引用。
終于可以講述最有趣的部分了:以上說的這些如何與內(nèi)核代碼配合工作。
關(guān)于內(nèi)核驅(qū)動
設(shè)備驅(qū)動加載和卸載時有四件事情會發(fā)生:
. 硬件存在時(比如在設(shè)備樹中聲明),內(nèi)核代碼加載相應(yīng)驅(qū)動
. 驅(qū)動需要了解設(shè)備的物理地址
. 驅(qū)動需要了解設(shè)備觸發(fā)的中斷號,用來注冊中斷處理函數(shù)。
. 一些特殊信息需要被獲取
內(nèi)核中有直接訪問設(shè)備樹的API,但是設(shè)備驅(qū)動使用專用接口更方便,這些專用接口受PCI/PCIe驅(qū)動的API影響。來看下xillybus_0條目,這是一個掛載于AXI總線上的典型邏輯設(shè)備。
標(biāo)簽和節(jié)點(diǎn)名
首先,標(biāo)簽("xillybus")和條目名()。標(biāo)簽可以省略,條目節(jié)點(diǎn)名的格式為(),最后在/sys下產(chǎn)生一個標(biāo)準(zhǔn)的條目(/sys/devices/axi.0/50000000.xillybus/)。,不過內(nèi)核肯定不是從這里訪問設(shè)備樹的。
驅(qū)動自動加載
節(jié)點(diǎn)中的第一個賦值語句compatible = “xlnx,xillybus-1.00.a”是最重要的一句:它連接硬件和驅(qū)動。當(dāng)內(nèi)核在總線上掃描設(shè)備時(設(shè)備節(jié)點(diǎn)在設(shè)備樹里掛在一個總線節(jié)點(diǎn)下),內(nèi)核檢索"compatible"字段,然后將其字符串與一些已知的字符串比較。這個過程會在啟動時自動發(fā)生兩次:
. 內(nèi)核啟動時,編譯進(jìn)內(nèi)核的驅(qū)動與設(shè)備樹中某個"compatible"條目匹配
. 之后加載內(nèi)核模塊時,再觸發(fā)一次匹配操作
內(nèi)核驅(qū)動和"compatible"條目的連接由驅(qū)動代碼中的一小段完成:
static struct of_device_id xillybus_of_match[] __devinitdata = {
{ .compatible = "xlnx,xillybus-1.00.a", },
{}
};
MODULE_DEVICE_TABLE(of,xillybus_of_match);
這段代碼使得驅(qū)動與某一個"compatible"條目匹配。注意上面的id表中有一個空結(jié)構(gòu),用這個空意緒標(biāo)志id表的結(jié)束。
在上段代碼之后,一定有類似如下的一段代碼:
static struct platform_driver xillybus_platform_driver = {
.probe = xilly_drv_probe,
.remove = xilly_drv_remove,
.driver = {
.name = "xillybus",
.owner = THIS_MODULE,
.of_match_table = xillybus_of_match,
},
};
platform_driver_register(&xillybus_platform_driver)在模塊初始化里被調(diào)用。這個結(jié)構(gòu)告訴內(nèi)核,當(dāng)驅(qū)動與某個硬件匹配時,xilly_drv_probe 被調(diào)用。
對內(nèi)核來說,"compatible"字串需要與某個驅(qū)動名相同。”xlnx"前綴用于防止名字沖突。
另外,一個設(shè)備可以有多個"compatible"。因為一個設(shè)備可以有多個模塊對應(yīng)多個驅(qū)動。
可能會需要匹配硬件的名字和類型,但這不常用。
寫內(nèi)核模塊時需要特別注意,自動加載機(jī)制依賴于/lib/modules/{kernel version}/modules.ofmap文件中的"compatible"字串,其他定義文件也在這個目錄下。正確的方式是把*.ko文件復(fù)制到/lib/modules/{kernelversion}/kernel/drivers/下的相關(guān)目錄中,然后:
depmod -a
獲取資源信息
內(nèi)核模塊驅(qū)動加載之后,就開始把硬件資源管理起來,如讀寫寄存器、接收中斷。
來看看設(shè)備樹里的一條:
xillybus_0: xillybus@50000000 {
compatible = "xlnx,xillybus-1.00.a";
reg = < 0x50000000 0x1000 >;
interrupts = < 0 59 1 >;
interrupt-parent = <&gic>;
xlnx,max-burst-len =<0x10>;
xlnx,native-data-width = <0x20>;
xlnx,slv-awidth = <0x20>;
xlnx,slv-dwidth = <0x20>;
xlnx,use-wstrb = <0x1>;
} ;
驅(qū)動一般在探測函數(shù)里就取得了硬件內(nèi)存段的所有權(quán)(探測函數(shù)就是probe指針指向的函數(shù))。
來看看一個典型探測函數(shù)的框架:
static int __devinit xilly_drv_probe(struct platform_device *op)
{
const struct of_device_id *match;
match =of_match_device(xillybus_of_match, &op->dev);
if (!match)
return -EINVAL;
第一個操作就是檢查probe是否作用在相關(guān)硬件上。
訪問寄存器
下一步,分配一段內(nèi)存并映射到虛擬內(nèi)存中。
int rc = 0;
struct resource res;
void *registers;
rc = of_address_to_resource(&op->dev.of_node,0, &res);
if (rc) {
/* Fail */
}
if(!request_mem_region(res.start, resource_size(&res), "xillybus")){
/* Fail */
}
registers =of_iomap(op->dev.of_node, 0);
if (!registers) {
/* Fail */
}
of_address_to_resource() 在設(shè)備樹中找到第一個"reg",并將解析到的信息填充在"res"結(jié)構(gòu)體里。這個例子里"reg = <0x50000000 0x1000 >”, 指的是分配一塊起始物理地址是0x50000000,長度為0x1000字節(jié)的空間。of_address_to_resource()會設(shè)置res.start =0x50000000, res.end = 0x50000fff。
調(diào)用request_mem_region()是為了注冊特殊的內(nèi)存段。目的是避免兩個驅(qū)動訪問同一段寄存器空間而造成的沖突。resource_size()是個內(nèi)聯(lián)函數(shù),返回segment的大小(此處是0x1000)。
of_iomap()函數(shù)是of_address_to_resource()和ioremap()的組合,本質(zhì)上等效于ioremap(re.start, resource_size(&res)).確保物理段已經(jīng)映射到虛擬內(nèi)存中,函數(shù)返回內(nèi)存段的虛擬地址空間起始地址。
顯然,當(dāng)模塊卸載或某個錯誤發(fā)生時,這些操作都需要有恢復(fù)動作。
訪問硬件寄存器請使用iowrite32(),ioread32()以及其他的函數(shù)和宏,而不要直接使用上面的"register"指針。
中斷處理
這部分的驅(qū)動很簡單,類似如下:
irq = irq_of_parse_and_map(op->dev.of_node, 0);
rc = request_irq(irq,xillybus_isr, 0, "xillybus", op->dev);
irq_of_parse_and_map()在設(shè)備樹里查找中斷的描述項,然后返回中斷號,request_irq()將使用這個中斷號來注冊。第二個參數(shù)是0,表示使用設(shè)備樹中的第一個中斷。
設(shè)備樹里面描述是:
interrupts = < 0 59 1 >;
interrupt-parent = <&gic>;
那么使用了這三個數(shù)據(jù)中的哪一個呢?
第一個0是一個標(biāo)志,用于指示中斷是否是SPI(共享中斷,shared peripheral interrupt)。非0值表示它是SPI。事實上在Zynq硬件上,這些中斷都是共享的,這里是為了方便才寫0, 軟件上認(rèn)為它不共享。
第二個數(shù)據(jù)表示中斷號。
第三個數(shù)字是中斷類型,可以有如下值:
0 - 內(nèi)核不改變它,開機(jī)或uboot設(shè)置它是什么樣就什么樣。
1 - 上升沿觸發(fā)
4 - 電平觸發(fā),高電平表示來中斷。
不允許有其他值,下降沿觸發(fā)和低電平中斷目前不支持,因為硬件不支持那些模式。如果需要這樣的觸發(fā)方式,就得在硬件上加一個非門。
值得注意的是第三個數(shù)字在設(shè)備樹里通常都是0, 所以Linux內(nèi)核不去改變中斷模式。這通常意味著高電平觸發(fā)。這也讓驅(qū)動依賴于bootloader里的設(shè)置。
interrupt-parent 這一句,必須指向中斷控制器&gic。如果反編譯一個DTB文件,這里的&gic會被一個數(shù)字代替,通常是0x1。
Application-specific data
之前提過,設(shè)備樹中是一些特殊信息,這樣一個驅(qū)動可以管理數(shù)片類似的硬件。例如,一個LCD顯示驅(qū)動,分辨率信息和物理尺寸可能出現(xiàn)在設(shè)備樹中。串口信息要告訴驅(qū)動當(dāng)前的時鐘頻率。
最簡單的,最常用的形式,這個信息由一條賦值語句組成:
xlnx,slv-awidth = <0x20>;
"xlnx"前綴可以防止命名沖突。名字可以任意取,但最好能望文知意。這里的"xlnx"是使用軟件自動生成設(shè)備樹時加上的前綴。
為了抓取到這一條信息,代碼可以這樣寫:
void *ptr;
ptr = of_get_property(op->dev.of_node, "xlnx,slv-awidth", NULL);
if (!ptr) {
/* Couldn't find the entry */
}
第三個參數(shù)NULL,是一個長度指針,可以返回數(shù)據(jù)的長度。
這條語句的值是一個數(shù)字:
int value;
value = be32_to_cpup(ptr);
be32_to_cpup讀“ptr”指向的數(shù)據(jù),從大端轉(zhuǎn)到處理器的小端,然后就得到想要的數(shù)字了。
drivers/of/base.c中有大量讀取這些信息的API。
總結(jié)
為一個外置寫一個設(shè)備樹entry很簡單:
. 為"compatible"賦一個字符串"magicstring",自動生成工具的生成格式一般是:名字+版本。
. 在數(shù)據(jù)手冊里查看總線上設(shè)備的地址分配信息, 寫一條 "reg=" 語句。
. "interrupt-parent=<&gic>"
. 中斷號 "interrupt="
. 最后加上一些設(shè)備的自定義參數(shù)
評論