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