基于Zephyr RTOS的嵌入式軟件開發(fā)實(shí)踐
掃描二維碼
隨時(shí)隨地手機(jī)看文章
Zephyr是由Linux基金會(huì)管理的開源實(shí)時(shí)操作系統(tǒng)(RTOS) [1],其前身為用于數(shù)字信號(hào)處理的Virtuoso操作系統(tǒng),后被風(fēng)河(Wind River)收購(gòu),更名為Rocket RTOS。2016年它成為了Linux基金會(huì)的項(xiàng)目,更名為Zephyr。
Zephyr得到了多家半導(dǎo)體企業(yè)的支持,包括恩智浦、意法半導(dǎo)體、瑞薩、北歐半導(dǎo)體(Nordic)、英特爾和德州儀器等,并已經(jīng)被應(yīng)用到了眾多設(shè)備中,覆蓋了消費(fèi)電子、能源、醫(yī)療、工業(yè)、農(nóng)業(yè)等領(lǐng)域[2]。Zephyr的Apache 2.0開源協(xié)議授權(quán)讓它在非商用和商用解決方案中都可免費(fèi)使用[3]。
近年來Zephyr的熱度逐漸上升,在嵌入式開發(fā)中的采用度逐步增加。Eclipse基金會(huì)的《2024年物聯(lián)網(wǎng)和嵌入式開發(fā)者調(diào)查報(bào)告》表明,在資源受限設(shè)備上使用Zephyr的開發(fā)者從2022年的8%增長(zhǎng)到了2024年的21%,這已經(jīng)和裸機(jī)直接編程的比例相當(dāng),也非常接近第二位的FreeRTOS (29%) [4]。
相比FreeRTOS等小型RTOS而言,教育生態(tài)不夠成熟的Zephyr系統(tǒng)規(guī)模更大,結(jié)構(gòu)更復(fù)雜,這提高了開發(fā)者入門和精通的門檻。本文對(duì)Zephyr硬件抽象層和設(shè)備驅(qū)動(dòng)的架構(gòu)與實(shí)現(xiàn)進(jìn)行系統(tǒng)性分析,重點(diǎn)闡述了設(shè)備驅(qū)動(dòng)模型和設(shè)備樹的作用。為了展示基于Zephyr的嵌入式軟件開發(fā),本文在BBC micro:bit V2開源硬件上構(gòu)建樣例Zephyr設(shè)備驅(qū)動(dòng)和應(yīng)用程序,并做解釋和驗(yàn)證。
Zephyr有著完善的設(shè)備驅(qū)動(dòng)支持,而且高度可配置。作為L(zhǎng)inux基金會(huì)的項(xiàng)目,它用到了和Linux內(nèi)核類似的工具,特別是設(shè)備樹(Device Tree)和Kconfig配置語(yǔ)言。本章將對(duì)與開發(fā)息息相關(guān)的硬件抽象化和配置進(jìn)行概述。
2.1. 設(shè)備驅(qū)動(dòng)模型
Zephyr的設(shè)備驅(qū)動(dòng)模型負(fù)責(zé)初始化系統(tǒng)中所有的驅(qū)動(dòng)程序,為系統(tǒng)中的所有設(shè)備驅(qū)動(dòng)提供了統(tǒng)一的配置方法[5]。如圖1所示的是設(shè)備驅(qū)動(dòng)模型的概覽。
Zephyr中每一種子系統(tǒng)驅(qū)動(dòng)(UART、I2C等)都有著泛用類型(Generic Type,非設(shè)備特定)的接口,具體的驅(qū)動(dòng)實(shí)現(xiàn)會(huì)提供實(shí)現(xiàn)這些驅(qū)動(dòng)接口函數(shù)的指針。在圖1中可以看到,在子系統(tǒng)2中有兩種設(shè)備驅(qū)動(dòng)的實(shí)例,但是兩種驅(qū)動(dòng)都會(huì)提供泛用API 1到3的實(shí)現(xiàn)。應(yīng)用程序代碼可以在兼容的設(shè)備上直接使用泛用API,具體驅(qū)動(dòng)的實(shí)現(xiàn)代碼會(huì)被調(diào)用。如子系統(tǒng)1中所示,同一種驅(qū)動(dòng)可以在系統(tǒng)中多次實(shí)例化,比如多個(gè)UART接口。
設(shè)備驅(qū)動(dòng)代碼在初始化時(shí)也會(huì)為每個(gè)設(shè)備提供驅(qū)動(dòng)特定的配置,即圖1中的struct config。在實(shí)際代碼中這可能是通過Kconfig配置的參數(shù),比如顯示器的刷新頻率。驅(qū)動(dòng)代碼還可以為每個(gè)驅(qū)動(dòng)指定一個(gè)結(jié)構(gòu)用于存儲(chǔ)相關(guān)的數(shù)據(jù)。
Figure 1. An overview of the device driver model (source: zephyrproject.org)
圖1. 設(shè)備驅(qū)動(dòng)模型概覽(來源:zephyrproject.org)
一個(gè)驅(qū)動(dòng)的泛用接口定義會(huì)出現(xiàn)在驅(qū)動(dòng)的頭文件中,圖2中定義了subsystem子系統(tǒng)的泛用接口subsystem_do_this和subsystem_do_that函數(shù)。圖3中的my_driver驅(qū)動(dòng)實(shí)現(xiàn)了自己的do_this和do_that函數(shù),并將它們的指針填入了驅(qū)動(dòng)API結(jié)構(gòu)(do_this和do_that成員)。注意應(yīng)用程序代碼應(yīng)該直接使用subsystem_do_this/that函數(shù),這兩個(gè)函數(shù)會(huì)通過DEVICE_API_GET宏進(jìn)入正確的驅(qū)動(dòng)接口實(shí)現(xiàn),即my_driver_do_this/that函數(shù)。在實(shí)際的驅(qū)動(dòng)中,subsystem會(huì)被替代為能夠代表設(shè)備的名稱,例如在通用的顯示驅(qū)動(dòng)接口(include/zephyr/drivers/display.h)中,subsystem被替代為了display。
Figure 2. A sample driver interface definition (source: zephyrproject.org)
圖2. 樣例驅(qū)動(dòng)接口定義(來源:zephyrproject.org)
Figure 3. A sample driver implementation (source: zephyrproject.org)
圖3. 樣例驅(qū)動(dòng)實(shí)現(xiàn)(來源:zephyrproject.org)
在進(jìn)行具體子系統(tǒng)驅(qū)動(dòng)的實(shí)例化時(shí),驅(qū)動(dòng)代碼還會(huì)提供初始化代碼和初始化的優(yōu)先級(jí)。
2.2. 設(shè)備樹
設(shè)備樹(Device Tree)是用于描述硬件的層級(jí)化數(shù)據(jù)結(jié)構(gòu)。設(shè)備樹規(guī)范[6]描述了設(shè)備樹的概念、用途、結(jié)構(gòu)、設(shè)備樹綁定(binding)和設(shè)備樹語(yǔ)言。
2.2.1. 設(shè)備樹的作用
Zephyr和Linux同樣使用設(shè)備樹,Zephyr為了減少運(yùn)行時(shí)的數(shù)據(jù)和代碼,會(huì)使用設(shè)備樹的數(shù)據(jù)產(chǎn)生C語(yǔ)言頭文件[7]。Zephyr中定義了一整套宏,用于訪問設(shè)備樹節(jié)點(diǎn)和取得設(shè)備樹節(jié)點(diǎn)的屬性。
Zephyr中設(shè)備樹有兩項(xiàng)主要作用:
-
在設(shè)備驅(qū)動(dòng)模型中描述硬件。
-
提供硬件的初始配置。
設(shè)備樹和Kconfig在Zephyr中都起到了配置語(yǔ)言的作用,設(shè)備樹用于描述硬件和啟動(dòng)時(shí)的配置,Kconfig則主要用于配置軟件。
設(shè)備樹有兩種輸入文件:設(shè)備樹源文件和設(shè)備樹綁定[8]。源文件描述了設(shè)備樹本身,綁定則用于描述設(shè)備樹的內(nèi)容,特別是數(shù)據(jù)類型和結(jié)構(gòu)。Zephyr在構(gòu)建時(shí)使用這兩種文件生成C頭文件,devicetree.h頭文件提供通用的宏訪問設(shè)備樹(以“DT_”打頭)。
2.2.2. 設(shè)備樹的語(yǔ)法
圖4所示的是一個(gè)最小的樣例設(shè)備樹源文件[9]:
Figure 4. A minimum device tree file (source: zephyrproject.org)
圖4. 設(shè)備樹最小樣例(來源:zephyrproject.org)
圖中“/”代表根節(jié)點(diǎn),a-node是根節(jié)點(diǎn)的子節(jié)點(diǎn),a-sub-node是a-node的子節(jié)點(diǎn),a-sub-node還有一個(gè)label (標(biāo)簽) subnode_nodelabel。標(biāo)簽是可選的,在設(shè)備樹中每個(gè)標(biāo)簽只能出現(xiàn)一次,代碼可以通過標(biāo)簽直接訪問節(jié)點(diǎn)。每個(gè)節(jié)點(diǎn)都有自己的路徑,和Linux文件路徑相似,例如a-sub-node的全路徑為:/a-node/a-sub-node。
圖5所示的是一個(gè)較為貼近實(shí)際硬件的設(shè)備樹樣例:
Figure 5. A complete device tree example (source: zephyrproject.org)
圖5. 一個(gè)完整的設(shè)備樹樣例(來源:zephyrproject.org)
在圖5中可以看到節(jié)點(diǎn)名的命名方法為“總線類型或設(shè)備名@地址”,這樣的慣例不僅有助于區(qū)分類似的節(jié)點(diǎn),還能夠幫助快速確定節(jié)點(diǎn)指向的設(shè)備和總線類型。地址的慣例根據(jù)設(shè)備類型有所不同:
-
在內(nèi)存中映射的外設(shè):使用寄存器映射的基地址,例如i2c@40003000表示I2C映射的寄存器基地址為0x40003000。
-
I2C外設(shè):使用外設(shè)在I2C總線上的地址,例如apds9960的I2C地址為0x39。
-
SPI外設(shè):使用外設(shè)的片選線序號(hào),如果沒有則使用0。
-
內(nèi)存:使用物理內(nèi)存的起始地址,例如memory@2000000表示從0x2000000物理地址開始的RAM。
-
在內(nèi)存中映射的閃存:和RAM類似使用物理起始地址,例如flash@8000000。
-
固定的閃存分區(qū):使用分區(qū)的偏移量,例如在flash@8000000設(shè)備中可以有一個(gè)partitions節(jié)點(diǎn)代表分區(qū)表,其中有partition@0和partition@20000兩個(gè)節(jié)點(diǎn),分別意味著起始地址0x8000000和0x8020000的兩個(gè)分區(qū)。
設(shè)備樹節(jié)點(diǎn)中每個(gè)屬性有一個(gè)名稱和一個(gè)值,屬性的值可以是字符串、整型數(shù)、布爾值、8位整型數(shù)組、字符串?dāng)?shù)組、混合類型數(shù)組、指向節(jié)點(diǎn)的phandle (類似C語(yǔ)言中的指針)、復(fù)數(shù)的phandle或是phandle數(shù)組。
設(shè)備樹節(jié)點(diǎn)中幾個(gè)重要的屬性如下:
-
compatible:表示節(jié)點(diǎn)所代表的硬件設(shè)備,本文翻譯為兼容名。兼容名屬性在構(gòu)建過程中十分重要,驅(qū)動(dòng)程序通過兼容名的值查找可以適配的硬件。兼容名的值可以是字符串?dāng)?shù)組,將數(shù)個(gè)驅(qū)動(dòng)程序從最特定到最泛用進(jìn)行排列,首個(gè)匹配的驅(qū)動(dòng)程序會(huì)被加載。
-
reg:用于設(shè)備尋址,其格式為16進(jìn)制的<地址,長(zhǎng)度>。
-
status:用于表示節(jié)點(diǎn)是否啟用。Zephyr支持“okay”和“disabled”,分別表示啟用和禁用。節(jié)點(diǎn)必須啟用,Zephyr的驅(qū)動(dòng)模型才會(huì)應(yīng)用到節(jié)點(diǎn)上。
除了標(biāo)簽,設(shè)備樹源文件中還可以定義chosen (選擇)和alias (別名)來幫助應(yīng)用代碼或驅(qū)動(dòng)尋找特定的節(jié)點(diǎn),如圖6所示。
Figure 6. Use chosen and aliases nodes in a device tree file (source: zephyrproject.org)
圖6. 在設(shè)備樹中使用chosen和aliases節(jié)點(diǎn)(來源:zephyrproject.org)
圖中/alias和/chosen節(jié)點(diǎn)都不指向?qū)嶋H的硬件設(shè)備,它們被用來指定設(shè)備樹中的其他節(jié)點(diǎn):my-uart是/soc/serial@12340000路徑的別名(uart0標(biāo)簽名),uart0標(biāo)簽還被選為“zephyr, console”。選擇和別名可以幫助抽象化不同的開發(fā)板,例如閃燈樣例(samples/basic/blinky/src/main.c)中使用led0別稱節(jié)點(diǎn)達(dá)到支持多種開發(fā)板的目的,只要開發(fā)板的設(shè)備樹文件中有別稱為led0的節(jié)點(diǎn),樣例即可運(yùn)行。
Zephyr中每個(gè)支持的開發(fā)板都有自己的主設(shè)備樹文件,micro:bit V2的文件位于路徑boards/bbc/microbit_v2/bbc_microbit_v2.dts,其中可以看到GPIO按鈕、LED顯示矩陣、I2C總線和I2C總線上的傳感器等硬件。應(yīng)用也可以提供專門針對(duì)開發(fā)板的設(shè)備樹覆蓋文件,路徑為“<應(yīng)用或模塊路徑>/boards/<開發(fā)板名>.overlay”。覆蓋文件中可以增加新的選擇/別名節(jié)點(diǎn),也可以配合新的設(shè)備樹綁定文件(見下節(jié))增加節(jié)點(diǎn)。
2.2.3. 設(shè)備樹綁定
設(shè)備樹自身的結(jié)構(gòu)相對(duì)自由,需要有設(shè)備樹綁定才能夠正確、完整地描述硬件[10]。設(shè)備樹綁定中包含對(duì)設(shè)備樹節(jié)點(diǎn)格式和內(nèi)容的要求。Zephyr使用YAML文件存儲(chǔ)設(shè)備樹綁定。
Figure 7. A sample device tree binding file (source: Martin Lampacher’s code on GitHub)
圖7. 一個(gè)樣例設(shè)備樹綁定文件(來源:Martin Lampacher在GitHub上的代碼)
-
description (描述):描述綁定文件適配的硬件的字符串。
-
compatible (兼容名):和設(shè)備樹中的兼容名對(duì)應(yīng),一個(gè)綁定文件的兼容名如果和一個(gè)設(shè)備樹節(jié)點(diǎn)一致,則該設(shè)備樹節(jié)點(diǎn)的格式應(yīng)當(dāng)符合綁定文件的內(nèi)容。
-
properties (屬性):描述了符合綁定的節(jié)點(diǎn)中的屬性與格式。
圖8所示的是設(shè)備樹節(jié)點(diǎn)符合圖7中的定義:
Figure 8. A device tree node that is compatible with the binding (source: Martin Lampacher’s code on GitHub)
圖8. 符合綁定文件的設(shè)備樹節(jié)點(diǎn)(來源:Martin Lampacher在GitHub上的代碼)
從圖8中可以看到:
-
節(jié)點(diǎn)的兼容名和綁定的一致。
-
每一個(gè)屬性都有按照綁定中type的類型賦值。
Zephyr中默認(rèn)包括的綁定文件位于dts/bindings子目錄下,按照類型進(jìn)行分類,以兼容名的名稱進(jìn)行命名。
除非向Zephyr中添加新的硬件支持,一般開發(fā)中不添加新的綁定文件。需要時(shí)應(yīng)用可以增加新的綁定文件(<應(yīng)用或模塊路徑>/dts/bindings/<兼容名>.yaml),并在設(shè)備樹覆蓋文件中添加符合綁定定義的節(jié)點(diǎn)。
2.2.4. 在程序中訪問設(shè)備樹節(jié)點(diǎn)和屬性
從C/C++應(yīng)用代碼中可以用多種方式訪問設(shè)備樹節(jié)點(diǎn)。
Figure 9. Methods to access a device tree node (source: zephyrproject.org)
圖9. 訪問設(shè)備樹節(jié)點(diǎn)的方法(來源:zephyrproject.org)
以圖9為例,多種宏都可以得到i2c@40002000節(jié)點(diǎn)(注意:將所有不是字母數(shù)字的字符替換為下劃線):
-
DT_PATH(soc, i2c_40002000):將全路徑以逗號(hào)隔開,省略所有“/”。
-
DT_NODELABEL(i2c1):使用標(biāo)簽名。
-
DT_ALIAS(sensor_controller):使用別名。
-
DT_INST(x, vnd_soc_i2c):尋找第x個(gè)兼容名為“vnd,soc-i2c”的節(jié)點(diǎn)。在本例中因?yàn)橹挥幸粋€(gè)節(jié)點(diǎn),x應(yīng)為0。在多“vnd, soc-i2c”節(jié)點(diǎn)的情況下,x和設(shè)備樹中節(jié)點(diǎn)的對(duì)應(yīng)關(guān)系不能保證。
對(duì)于chosen節(jié)點(diǎn)(圖9中不包括),使用DT_CHOSEN指定節(jié)點(diǎn),例如針對(duì)圖6中的設(shè)備樹可以使用“DT_CHOSEN(zephyr_console)”。
注意:上述宏不能用于變量,只能用于宏定義。
DT_NODE_HAS_PROP宏可以用于檢測(cè)節(jié)點(diǎn)是否有特定屬性,例如 “DT_NODE_HAS_PROP(DT_NODELABEL(i2c1), clock_frequency)”的值為1。訪問節(jié)點(diǎn)的屬性時(shí)使用DT_PROP宏,例如“DT_PROP(DT_PATH(soc, i2c_40002000), clock_frequency)”的值為100000。DT_PROP的值可以用于變量初始化或是靜態(tài)定義。
Zephyr定義了眾多與設(shè)備樹相關(guān)的宏,在官方文檔中有分類總結(jié)。在開發(fā)中請(qǐng)根據(jù)需要查閱文檔,并參考Zephyr豐富的開發(fā)板/傳感器樣例庫(kù)。
2.3. Kconfig配置工具
Kconfig是在構(gòu)建時(shí)配置Zephyr內(nèi)核和子系統(tǒng)的主要方式,Kconfig也是Linux內(nèi)核的配置系統(tǒng)。Zephyr中的Kconfig配置選項(xiàng)按照文件夾的層級(jí)結(jié)構(gòu)分布,從Zephyr代碼庫(kù)根目錄的Kconfig.zephyr文件開始。根Kconfig文件用包含(include)語(yǔ)句包括了子系統(tǒng)(例如內(nèi)核、驅(qū)動(dòng)和代碼庫(kù))的Kconfig文件,子系統(tǒng)還可以進(jìn)一步深入定義更深層的Kconfig結(jié)構(gòu)和選項(xiàng)。
開發(fā)板和應(yīng)用可以指定需要啟用的配置。BBC micro:bit V2板的默認(rèn)選項(xiàng)位于文件boards/bbc/microbit_v2/bbc_microbit_v2_defconfig中,包括系統(tǒng)時(shí)鐘、串口和控制臺(tái)等選項(xiàng)。每個(gè)應(yīng)用中的prj.conf則包含了應(yīng)用所需的選項(xiàng)。
與Linux類似,在Zephyr中可以通過命令行界面進(jìn)行Kconfig選項(xiàng)配置[13]。針對(duì)應(yīng)用構(gòu)建后產(chǎn)生的build文件夾運(yùn)行命令“west build --build-dir ./build -t menuconfig”即可進(jìn)入命令行界面(見圖10)。
Figure 10. Kconfig menuconfig interface
圖10. Kconfig配置命令行界面
在界面中可以通過方向鍵和ESC/空格鍵進(jìn)行導(dǎo)航,在選項(xiàng)上通過空格鍵進(jìn)行選擇。修改選項(xiàng)后D鍵保存最小配置到文件,也就是當(dāng)前界面中定義的Kconfig選項(xiàng)和Zephyr定義的開發(fā)板默認(rèn)選項(xiàng)的區(qū)別。圖11所示的是micro:bit V2 LED矩陣顯示樣例的輸出結(jié)果(第3.1節(jié)會(huì)使用這一樣例):
Figure 11. Kconfig minimum config output
圖11. Kconfig最小配置輸出
對(duì)比前面提到的開發(fā)板默認(rèn)Kconfig選項(xiàng)和應(yīng)用添加的選項(xiàng)(samples/boards/bbc/microbit/display/prj.conf),可以看到只有“CONFIG_NRFX_GPIOTE_NUM_OF_EVT_HANDLERS”選項(xiàng)是上述兩個(gè)文件中沒有包括的,這是因?yàn)楸睔W半導(dǎo)體的HAL層自動(dòng)定義了這一選項(xiàng)(modules/hal_nordic/nrfx/nrfx_kconfig.h)。除了這樣的例外情況,一般在命令行界面中選中了新的選項(xiàng),用最小選項(xiàng)輸出就可以幫助確定新的選項(xiàng)名,之后就可以將其加入到prj.conf文件中,從而在編譯過程中包括這一選項(xiàng)。
Kconfig選項(xiàng)除了用于開啟子系統(tǒng)功能之外,也用于配置驅(qū)動(dòng)、應(yīng)用代碼,以及下一章將要講解的日志系統(tǒng)。在代碼中可以用“CONFIG_
本章中,我們將結(jié)合樣例在Zephyr上實(shí)踐嵌入式應(yīng)用開發(fā),幫助理解上一章中的理論。
3.1. 環(huán)境配置和運(yùn)行第一個(gè)程序
首先,跟隨Zephyr項(xiàng)目入門指南完成環(huán)境配置、Zephyr和Zephyr SDK的安裝??傮w來說,Zephyr在Linux中的安裝和配置步驟最為簡(jiǎn)潔,推薦在Ubuntu Linux上進(jìn)行Zephyr的實(shí)驗(yàn)和開發(fā)。本章提及的命令和環(huán)境細(xì)節(jié)均以在Ubuntu 24.04版本上使用Zephyr 4.0.99開發(fā)版本為準(zhǔn),運(yùn)行時(shí)使用BBC micro:bit V2開發(fā)板(見圖12)。
Figure 12. micro:bit V2 board (source: microbit.org [14])
圖12. micro:bit V2板(來源:microbit.org [14])
Zephyr的樣例庫(kù)中包括眾多開發(fā)板和傳感器的樣例,不過指南中提到的閃燈樣例(Blinky,路徑samples/basic/blinky)并不能直接套用在micro:bit V2上。此處我們采用micro:bit V2的LED矩陣顯示樣例(路徑samples/boards/bbc/microbit/display)。連接開發(fā)板到Ubuntu系統(tǒng)上,運(yùn)行圖13中的命令進(jìn)行編譯和燒錄。命令中的“-p”選項(xiàng)意味著進(jìn)行全新編譯,當(dāng)對(duì)工程進(jìn)行重復(fù)編譯時(shí)使用“-p auto”選項(xiàng)允許west工具只對(duì)更改的部分進(jìn)行重新編譯,這適合在開發(fā)迭代時(shí)節(jié)約時(shí)間。
Figure 13. Commands to compile and flash the sample onto the micro:bit V2 board
圖13. 針對(duì)micro:bit V2板編譯和燒錄的命令
成功后開發(fā)板會(huì)自動(dòng)啟動(dòng)Zephyr,開發(fā)板背后(見圖12,有BBC micro:bit v2字樣的面為正面) 5乘5的LED矩陣會(huì)顯示數(shù)字倒計(jì)時(shí)9到0,然后是LED的逐行逐列“行軍”,最后開始持續(xù)滾動(dòng)顯示“Hello Zephyr!”的字樣。
該實(shí)例展示了較為復(fù)雜的單組件運(yùn)作,從主函數(shù)(samples/boards/bbc/microbit/display/src/main.c)可以看到樣例通過一個(gè)針對(duì)micro:bit板專用的中間層(drivers/display/mb_display.c)對(duì)泛用的顯示驅(qū)動(dòng)(頭文件zephyr/drivers/display.h)進(jìn)行擴(kuò)充,實(shí)現(xiàn)了大多數(shù)的功能,例如初始化、打印數(shù)字或字母,以及按照0/1矩陣點(diǎn)亮LED等。
3.2. 閃燈樣例和設(shè)備樹問題
上一節(jié)提到,Zephyr的閃燈樣例在micro:bit V2上不能運(yùn)行,本節(jié)讓我們了解其背后的理由和如何修復(fù)與設(shè)備樹相關(guān)的問題。
運(yùn)行圖14所示的命令嘗試編譯閃燈樣例:
Figure 14. Commands to compile the blinky sample
圖14. 編譯閃燈樣例的命令
運(yùn)行的結(jié)果是圖15所示的編譯錯(cuò)誤:
Figure 15. Compile error of the blinky sample
圖15. 閃燈樣例的編譯錯(cuò)誤
第2.2.4節(jié)中提到,Zephyr提供一整套設(shè)備樹宏,本例中GPIO代碼使用的DT_ALIAS宏不能完全展開。Zephyr中設(shè)備樹宏錯(cuò)誤的原因一般都與編譯錯(cuò)誤中提到的頭文件無關(guān),而是設(shè)備樹有格式/內(nèi)容的錯(cuò)誤,或者訪問設(shè)備樹的方式有誤。幾種常見的錯(cuò)誤如下:
-
混淆了選擇、別名、標(biāo)簽名和節(jié)點(diǎn)名,或者輸入了錯(cuò)誤的字符串(例如沒有將非字母數(shù)字的字符轉(zhuǎn)換為下劃線)。
-
在硬件特定的宏中(例如圖15的GPIO_DT_SPEC_GET需要指向一個(gè)GPIO phandle節(jié)點(diǎn))使用了不同硬件的節(jié)點(diǎn)。
-
設(shè)備樹的節(jié)點(diǎn)和綁定的格式要求不一致,導(dǎo)致節(jié)點(diǎn)未能生成正確的頭文件,因此應(yīng)用或者驅(qū)動(dòng)中的宏無法展開。注意:這和簡(jiǎn)單的設(shè)備樹語(yǔ)法錯(cuò)誤不同,語(yǔ)法問題在編譯設(shè)備樹時(shí)就會(huì)導(dǎo)致編譯失敗,內(nèi)容的問題則可能導(dǎo)致在應(yīng)用代碼中無法使用特定屬性或宏。
-
使用了錯(cuò)誤的宏組合或者宏的參數(shù)錯(cuò)誤,特別是For-Each循環(huán)宏和硬件特定的宏。
打開micro:bit V2的設(shè)備樹文件(boards/bbc/microbit_v2/bbc_microbit_v2.dts),可以看到aliases節(jié)點(diǎn)下沒有l(wèi)ed0,缺少led0別名導(dǎo)致了編譯的失敗[15]。
micro:bit V2的LED矩陣由十個(gè)GPIO輸出控制,個(gè)別改變一個(gè)控制引腳(pin)并不能點(diǎn)亮LED。紅色的電源指示燈和黃色的USB指示燈也并沒有連接到GPIO上,因此只是依靠開發(fā)板本身,我們并不能通過擴(kuò)展設(shè)備樹簡(jiǎn)單地修改好閃燈樣例。不過,micro:bit V2可以外接LED,將外接LED的GPIO添加到設(shè)備樹中就可以修復(fù)閃燈樣例。
添加設(shè)備樹覆蓋文件samples/basic/blinky/boards/bbc_microbit_v2.overlay修復(fù)編譯錯(cuò)誤[16],見圖16:
Figure 16. The device tree overlay file to fix the compilation error
圖16. 修復(fù)編譯錯(cuò)誤的設(shè)備樹覆蓋文件
可以看到文件增加了一個(gè)兼容名為gpio-leds的節(jié)點(diǎn)leds,然后為含有GPIO信息的led_0子節(jié)點(diǎn)增加別名led0。gpio-leds的驅(qū)動(dòng)(drivers/led/led_gpio.c)提供了開關(guān)和設(shè)定亮度的接口,不過在閃燈樣例中,代碼(samples/basic/blinky/src/main.c)只是通過GPIO_DT_SPEC_GET宏從設(shè)備樹取得了GPIO引腳的信息,然后直接使用gpio_pin_toggle_dt切換GPIO輸出狀態(tài)。
對(duì)比主設(shè)備樹文件的edge_connector (邊緣連接器)節(jié)點(diǎn)和開發(fā)板的引腳圖[17]可以看到,圖16中g(shù)pio0接入點(diǎn)引腳4對(duì)應(yīng)P2引腳(開發(fā)板下側(cè)標(biāo)記2的金手指),運(yùn)行時(shí)如果有連接外接LED,閃燈樣例就能夠運(yùn)行。
類似的設(shè)備樹覆蓋文件方法,只要正確地修改GPIO接入點(diǎn)和引腳號(hào),也可以讓沒有l(wèi)ed0別名的開發(fā)板支持閃燈樣例。
3.3. 樣例應(yīng)用和詳解
本節(jié)將使用基于官方樣例[18]改編的樣例應(yīng)用。除了主程序代碼還包括:
-
一個(gè)簡(jiǎn)單的自定義代碼庫(kù)(accel):從3-軸加速度傳感器取得加速度數(shù)值,該庫(kù)可以通過Kconfig啟用或禁用。
-
一個(gè)簡(jiǎn)單的自定義LED矩陣驅(qū)動(dòng)層(ledmatrix):不使用Zephyr的顯示驅(qū)動(dòng),手動(dòng)通過GPIO點(diǎn)亮單個(gè)行或列的LED,該驅(qū)動(dòng)層可以通過Kconfig啟用/禁用和配置。
-
設(shè)備樹覆蓋文件:用于輔助自定義代碼庫(kù)和LED矩陣驅(qū)動(dòng)層,并展示簡(jiǎn)單的設(shè)備樹功能。
驅(qū)動(dòng)、代碼庫(kù)和主函數(shù)各自配置了日志模塊,可以通過Kconfig配置日志級(jí)別。
3.3.1. 3-軸加速度傳感器的代碼調(diào)用
從主設(shè)備樹文件上可以看到,micro:bit V2上內(nèi)建了ST的lsm303agr 3-軸加速度傳感器(見圖17)。在樣例應(yīng)用中,custom-module/lib/accel/accel.c源代碼和custom-module/include/app/lib/accel.h頭文件將尋找傳感器設(shè)備和從傳感器設(shè)備取得3-軸加速度值的功能包裝到了一個(gè)簡(jiǎn)單的自定義庫(kù)accel中。
Figure 17. micro:bit V2 device tree file snippet (source: Zephyr on GitHub)
圖17. micro:bit V2設(shè)備樹文件片段(來源:Zephyr GitHub代碼庫(kù))
accel庫(kù)代碼中,尋找傳感器設(shè)備的get_accel_device函數(shù)通過別名accel尋找設(shè)備樹中的加速度傳感器設(shè)備,這一別名在micro:bit V2的主設(shè)備樹文件中并不存在(其中只有accel0),而是由樣例應(yīng)用設(shè)備樹覆蓋文件(app/boards/bbc_microbit_v2.overlay)提供的。其中增加了accel別名,指向標(biāo)簽為lsm303agr_accel的節(jié)點(diǎn)。
設(shè)備樹覆蓋文件能在開發(fā)板的主設(shè)備樹文件上進(jìn)行增添和修改,它的幾項(xiàng)用途如下[19]:
-
增加別名(本例的accel)或者選擇。
-
覆寫已有節(jié)點(diǎn)的屬性值,例如更改串口的數(shù)據(jù)速率。
-
刪除節(jié)點(diǎn)的一個(gè)屬性。
-
增加子節(jié)點(diǎn),例如總線上新的子設(shè)備。
回到accel.c代碼中,get_accel_values函數(shù)用于獲取3-軸加速度值,其中sensor_sample_fetch和sensor_channel_get函數(shù)調(diào)用完成了樣本刷新和取樣本值的功能。了解它們是如何針對(duì)特定的傳感器完成代碼調(diào)用的,能夠幫助我們更加深入地理解Zephyr的設(shè)備驅(qū)動(dòng)模型(第2.1節(jié))。
sensor_sample_fetch和sensor_channel_get函數(shù)均為泛用傳感器驅(qū)動(dòng)API,從Zephyr代碼庫(kù)頭文件include/zephyr/drivers/sensor.h可以看到兩個(gè)函數(shù)會(huì)分別調(diào)用設(shè)備驅(qū)動(dòng)API sample_fetch和channel_get函數(shù)。設(shè)備樹中設(shè)備的兼容名決定了適配的驅(qū)動(dòng)程序。在設(shè)備樹文件中,傳感器的兼容名有兩個(gè):“st,lis2dh”和“st,lsm303agr-accel”。驅(qū)動(dòng)的適配順序是先查找第一個(gè)兼容名,在Zephyr代碼中搜索st_lis2dh (非字母數(shù)字的字符替代為下劃線),可以找到drivers/sensor/st/lis2dh/lis2dh.c文件包含定義驅(qū)動(dòng)的語(yǔ)句“#define DT_DRV_COMPAT st_lis2dh”。圖18所示的是該驅(qū)動(dòng)的驅(qū)動(dòng)API結(jié)構(gòu)定義:
Figure 18. lis2dh device driver API definition (source: Zephyr on GitHub)
圖18. lis2dh設(shè)備驅(qū)動(dòng)的API定義(來源:Zephyr GitHub代碼庫(kù))
可以看到該驅(qū)動(dòng)將lis2dh_sample_fetch和list2dh_channel_get函數(shù)的指針指定為設(shè)備sample_fetch和channel_get API的實(shí)現(xiàn)。lis2dh驅(qū)動(dòng)支持I2C和SPI總線,在主設(shè)備樹文件中可以看到,micro:bit V2中的傳感器是在i2c總線上的。圖19所示的是lis2dh驅(qū)動(dòng)的部分初始化代碼:
Figure 19. lis2dh device driver initialization code (source: Zephyr on GitHub)
圖19. lis2dh設(shè)備驅(qū)動(dòng)初始化代碼(來源:Zephyr GitHub代碼庫(kù))
代碼通過DT_INST_FOREACH_STATUS_OKAY宏,對(duì)每一個(gè)狀態(tài)為okay的兼容設(shè)備擴(kuò)展LIS2DH_DEFINE宏,后者會(huì)通過DT_INST_ON_BUS判斷設(shè)備是否在spi總線上,如果是,就進(jìn)一步擴(kuò)展LIS2DH_DEFINE_SPI初始化驅(qū)動(dòng),否則會(huì)擴(kuò)展LIS2DH_DEFINE_I2C宏(micro:bit V2的情況)。那么,設(shè)備樹是如何讓DT_INST_ON_BUS能夠進(jìn)行判定的呢?
micro:bit V2設(shè)備樹中傳感器所在的i2c節(jié)點(diǎn)兼容名為“nordic,nrf-twim”,從其綁定文件dts/bindings/i2c/nordic,nrf-twim.yaml中可以看到,文件包含(include)了nordic,nrf-twi-common.yaml (同文件夾下),然后該文件又進(jìn)一步包含了i2c-controller.yaml,在這一文件中終于看到了“bus: i2c”的信息。也就是說,從設(shè)備樹綁定可以得知傳感器從屬于使用i2c總線的控制器。
由于lis2dh驅(qū)動(dòng)能夠被正確地配置,系統(tǒng)不會(huì)查找兼容“st,lsm303agr-accel”的驅(qū)動(dòng)。在運(yùn)行時(shí),accel代碼庫(kù)中的sensor_sample_fetch和sensor_channel_get函數(shù)會(huì)調(diào)用st_lis2dh驅(qū)動(dòng)的函數(shù)。
在Zephyr的在線文檔中,通過兼容名可以找到設(shè)備樹綁定的參考頁(yè)面,例如本例中的驅(qū)動(dòng)文檔標(biāo)題為“st,lis2dh (on i2c bus)”。
3.3.2. 設(shè)備樹綁定和自定義驅(qū)動(dòng)
在樣例應(yīng)用中,自定義的ledmatrix驅(qū)動(dòng)(custom-module/drivers/ledmatrix/ledmatrix.c)使用GPIO在LED矩陣上實(shí)現(xiàn)了簡(jiǎn)單點(diǎn)亮矩陣邊緣一排或一行5枚LED的功能。在前一節(jié)中提到,驅(qū)動(dòng)需要匹配到設(shè)備樹的設(shè)備節(jié)點(diǎn)上。本例中我們創(chuàng)建了自定義的“custom-ledmatrix”兼容名和其綁定,以及l(fā)edmatrix驅(qū)動(dòng)實(shí)現(xiàn)。
圖20和圖21所示的分別是custom-ledmatrix設(shè)備樹綁定文件(custom-module/dts/bindings/custom-ledmatrix.yaml)和micro:bit V2設(shè)備樹覆蓋文件中的對(duì)應(yīng)節(jié)點(diǎn):
Figure 20. The device tree binding file for custom-ledmatrix
圖20. custom-ledmatrix設(shè)備樹綁定文件
Figure 21. The custom-ledmatrix device tree node
圖21. cutstom-ledmatrix設(shè)備樹節(jié)點(diǎn)
從圖20中可以看到,custom-ledmatrix綁定中有兩個(gè)GPIO引腳phandle數(shù)組,分別代表LED矩陣的行GPIO引腳(推挽)和列GPIO引腳(開漏) [20]。在圖21中,注意到GPIO接入點(diǎn)、引腳號(hào)和邏輯電平模式與開發(fā)板主設(shè)備樹文件中“l(fā)ed_matrix”節(jié)點(diǎn)(兼容名“nordic,nrf-led-matrix”)是一致的[21]。樣例中我們使用GPIO在不使用動(dòng)態(tài)刷新的情況下進(jìn)行亮、滅燈,所以不需要其他的屬性。
需要特別注意的是,為了表示phandle每個(gè)說明符(specifier)成員的長(zhǎng)度(例如GPIO除了接入點(diǎn)之外需要提供兩個(gè)數(shù)據(jù)成員),在綁定中一般應(yīng)提供名稱為“#*-cells”的屬性。不過由于GPIO類phandle十分常見,只要屬性的命名以“-gpios”結(jié)尾,如本例中的led-row-gpios和led-col-gpios,就不需要提供這一屬性。關(guān)于“#*-cells”屬性的細(xì)節(jié)詳見官方文檔。
從圖21中還可以看到設(shè)定設(shè)備狀態(tài)就緒的語(yǔ)句(status為“okay”),節(jié)點(diǎn)能夠使用該屬性是因?yàn)榻壎ㄎ募薭ase.yaml。上一節(jié)中提到,標(biāo)記設(shè)備就緒對(duì)于驅(qū)動(dòng)的初始化是必須的,例如圖19中用到的DT_INST_FOREACH_STATUS_OKAY宏。
自定義驅(qū)動(dòng)的頭文件定義見custom-module/include/app/drivers/ledmatrix.h,可以看到驅(qū)動(dòng)API由5個(gè)函數(shù)組成(見ledmatrix_driver_api結(jié)構(gòu)定義),分別負(fù)責(zé)點(diǎn)亮LED矩陣最邊緣的行或是列(共4個(gè)API)和關(guān)閉LED顯示(第5個(gè)API)。在驅(qū)動(dòng)的實(shí)現(xiàn)(custom-module/drivers/ledmatrix/ledmatrix.c)中,這5個(gè)函數(shù)會(huì)被實(shí)現(xiàn)(見driver_api結(jié)構(gòu)) [22]。現(xiàn)在讀者應(yīng)該能夠理解ledmatrix驅(qū)動(dòng)的基本結(jié)構(gòu)。最后,圖22所示的是驅(qū)動(dòng)的初始化宏:
Figure 22. The initialization of the ledmatrix driver
圖22. ledmatrix驅(qū)動(dòng)的初始化
LEDMATRIX_DEFINE中使用GPIO_DT_SPEC_GET_BY_IDX配合DT_INST_FOREACH_PROP_ELEM_SEP,從設(shè)備樹循環(huán)提取GPIO引腳phandle數(shù)組中的成員,從而靜態(tài)組成gpio_dt_spec數(shù)組[23],用于在設(shè)備驅(qū)動(dòng)配置結(jié)構(gòu)(見圖23)中存儲(chǔ)行和列GPIO引腳屬性[24]。
Figure 23. The configuration structure of the ledmatrix driver
圖23. ledmatrix驅(qū)動(dòng)的配置結(jié)構(gòu)
和上一節(jié)提到的傳感器驅(qū)動(dòng)類似,DT_INST_FOREACH_STATUS_OKAY針對(duì)每個(gè)狀態(tài)為就緒的、兼容名為“custom-ledmatrix”的設(shè)備進(jìn)行驅(qū)動(dòng)初始化。
通過ledmatrix驅(qū)動(dòng)層,樣例應(yīng)用的主函數(shù)就可以很容易地直接進(jìn)行LED行或是列的點(diǎn)亮操作。配合加速度傳感器的數(shù)據(jù),樣例應(yīng)用實(shí)現(xiàn)了根據(jù)重力方向點(diǎn)亮LED矩陣對(duì)應(yīng)邊緣行或列的效果。
3.3.3. 日志系統(tǒng)
Zephyr提供了日志系統(tǒng)的支持,應(yīng)用代碼、驅(qū)動(dòng)、代碼庫(kù)可以注冊(cè)各自的日志模塊,并通過Kconfig配置模塊的日志級(jí)別。日志的可能級(jí)別從低到高分別為:DBG (調(diào)試)、INF (信息)、WRN (警告)和ERR (錯(cuò)誤)。代碼中通過調(diào)用LOG_X (X為級(jí)別)宏就可以使用與printk類似的語(yǔ)法寫日志。以樣例應(yīng)用中的accel代碼庫(kù)為例,custom-module/lib/accel/accel.c中包含了zephyr/logging/log.h頭文件,然后使用LOG_MODULE_REGISTER宏定義了日志模塊accel,其日志級(jí)別為CONFIG_ACCELLIB_LOG_LEVEL。
在Kconfig中,CONFIG_LOG配置用于在全局啟用日志,然后通過添加CONFIG_<模塊>_LOG_LEVEL_X (X為級(jí)別)配置設(shè)定個(gè)別模塊的級(jí)別。本例中應(yīng)用的配置文件app/prj.conf通過CONFIG_LOG=y在全局開啟了日志功能,然后通過CONFIG_ACCELLIB_LOG_LEVEL_INF=y選項(xiàng)將accel模塊的日志級(jí)別定義為INF(信息)級(jí)別。ledmatrix驅(qū)動(dòng)和應(yīng)用主代碼各自也有日志模塊的配置。
使用日志系統(tǒng)相比使用printk更加可配置,例如只有調(diào)試時(shí)才需要的日志可以通過默認(rèn)日志級(jí)別進(jìn)行過濾,發(fā)布應(yīng)用時(shí)也可以很容易地禁用日志輸出。
4. 運(yùn)行和調(diào)試Zephyr應(yīng)用
4.1. 運(yùn)行樣例應(yīng)用
編譯和部署樣例應(yīng)用的命令如圖24所示:
Figure 24. Commands to download and deploy the example application
圖24. 下載和部署樣例應(yīng)用的命令
樣例應(yīng)用開始運(yùn)行時(shí),將開發(fā)板平放于臺(tái)面上,此時(shí)LED矩陣不會(huì)點(diǎn)亮,如果將開發(fā)板拿起,一側(cè)垂直朝向地面時(shí),檢測(cè)到重力一側(cè)的一排或一列5個(gè)LED會(huì)點(diǎn)亮。例如,當(dāng)開發(fā)板垂直于臺(tái)面正面并面向讀者時(shí),LED矩陣最下一行會(huì)點(diǎn)亮(見圖25)。
Figure 25. Illumination of the bottom row LEDs when the board is upright
圖25. 開發(fā)板垂直擺放時(shí),最下一排的LED點(diǎn)亮
4.2. Zephyr應(yīng)用的調(diào)試
在Zephyr應(yīng)用開發(fā)中,最簡(jiǎn)單的調(diào)試方法就是輸出日志。micro:bit V2運(yùn)行樣例應(yīng)用時(shí)會(huì)將日志輸出到串口,可以通過任何串口工具連接串口,例如使用minicom的命令:“minicom -D /dev/ttyACM0 -b 115200”。
樣例應(yīng)用的默認(rèn)日志級(jí)別為INF,編譯時(shí)可以通過包括debug.conf的選項(xiàng)將日志級(jí)別降低為DBG,程序就會(huì)輸出傳感器數(shù)據(jù)和GPIO操作細(xì)節(jié),命令為:“west build -b bbc_microbit_v2 app -p --extra-conf debug.conf”。
Zephyr支持在micro:bit V2上使用GDB進(jìn)行遠(yuǎn)程調(diào)試,應(yīng)用編譯和燒錄(“west build”和“west flash”)后,運(yùn)行“west debug”就會(huì)啟動(dòng)GDB。GDB簡(jiǎn)單的用法例如:設(shè)置斷點(diǎn)(“b main.c:<行數(shù)>”或“b <函數(shù)名>”)、逐行執(zhí)行(n)、繼續(xù)執(zhí)行(c)和打印變量(“p <變量名>”)?!皐est debug”命令還可以指定GDB以外的調(diào)試接口[25],例如jlink和openocd。
Zephyr在系統(tǒng)設(shè)計(jì)上借鑒了Linux等大型開源軟件的設(shè)計(jì)理念,引入了Linux和桌面系統(tǒng)開發(fā)者熟悉的概念和開發(fā)過程,但相對(duì)常見的RTOS,復(fù)雜度增加了數(shù)個(gè)級(jí)別。通過將硬件進(jìn)行抽象化,以及提供幫助簡(jiǎn)化開發(fā)過程的工具和框架(例如west工具和twister測(cè)試框架),Zephyr希望能吸引不同領(lǐng)域的開發(fā)者和企業(yè)用戶。但是,開發(fā)和調(diào)試難度的上升也讓不少開發(fā)者望而卻步,特別是熟悉面向硬件直接編程或是使用小型RTOS的嵌入式開發(fā)者。
希望在閱讀本文后,讀者對(duì)在Zephyr上進(jìn)行嵌入式軟件開發(fā)有了初步的了解。本文中的實(shí)例并不涉及過于具體的硬件細(xì)節(jié)或是復(fù)雜的應(yīng)用需求,Zephyr的官方文檔、實(shí)例,以及北歐半導(dǎo)體等硬件廠商的樣例項(xiàng)目都十分有參考價(jià)值。雖然官方文檔的中文化有所欠缺,但國(guó)內(nèi)開發(fā)者在各類平臺(tái)上發(fā)布的學(xué)習(xí)筆記一直在增加,線上討論也十分熱烈。
Zephyr近年來勁頭強(qiáng)勢(shì),硬件廠商、開發(fā)者和開源社區(qū)的熱情正盛,項(xiàng)目的開發(fā)活躍程度遠(yuǎn)超其他RTOS。期待Zephyr項(xiàng)目在未來能夠簡(jiǎn)化復(fù)雜的系統(tǒng)架構(gòu),改善學(xué)習(xí)難度高和代碼調(diào)試?yán)щy等問題,并覆蓋更多的硬件和應(yīng)用,成為一個(gè)全方位的主流物聯(lián)網(wǎng)操作系統(tǒng)。