中國移動 Oneos 框架基礎及其組件解析
1、oneos系統
1.1 開發手冊
OneOS是中國移動針對物聯網領域推出的輕量級操作系統,具有可裁剪、跨平臺、低功耗、高安全等特點,支持ARM Cortex-M、MIPS、RISC-V等主流芯片架構,兼容POSIX、CMSIS等標準接口,支持MicroPython語言開發,提供圖形化開發工具,能夠有效提升開發效率并降低開發成本,集成公共組件,適用于安全易用的物聯網產品。
移動官網提供完善的oneos開發文檔 https://os.iot.10086.cn/v2/doc/homePage
也可參考RT-Thread的資料,https://www.rt-thread.org/document/site/#/
1.2 開發工具
OneOS開發環境是基于命令行的OneOS-Cube,在對應工程目錄下,執行menuconfig配置系統,scons編譯構造。具體操作說明:https://os.iot.10086.cn/v2/doc/detailPage/documentHtml?idss=157071776529260544&proId=156799478777782272
1.3 軟件框架
OneOS總體架構采用分層設計,主體由驅動、內核、組件、安全框架組成,采用一個輕量級內核加多個系統組件的模式。
與freeRTOS只包括內核相比,oneos支持通用組件或第三方庫,尤其對接移動平臺方便,遵循 Apache 2.0 license 開源協議,任意使用。
1.4 內核
Oneos內核提供任務管理調度、任務間同步與通信、定時器、內存管理等常用RTOS功能,與常規的RTOS如freeRTOS相比,僅有幾點小區別:
1、任務,創建task后需要額外調用才啟動,不同于freeRTOS創建所有的task后統一啟動調度,全部task都開始執行。這種各task獨立啟動的,需要注意先后關系,不能task1啟動后就向task2的隊列發消息,此時隊列可能為NULL且發送前沒有判斷,會導致重啟。
2、消息隊列,其動態創建和發送接口類似,但接收隊列消息需要傳入的參數不同,需要傳入期望接收的字節大小。巧合的是項目使用的消息隊列都是同一個結構體,如果不一致,需要特殊處理。
3、工作隊列,將task進行了一定封裝,不需要為處理某個事件而新建task,交給系統提前創建的task統一執行,執行結束后觸發回調函數,這樣多個長時間運行但不經常觸發的,可以交給工作隊列處理,該功能在UIS8910中系統自帶。
4、郵箱,Oneos特有的,可理解為簡化版的消息隊列。
如果對freeRTOS不熟悉的,可以參考入門文章
1.5 組件
正如前面提到,如何使用oneos,移動官網有詳細的說明,本文只是介紹oneos的基本開發流程,分析其部分功能的實現方式,后續以其設備框架、SHELL和單元測試三部分為主。
2、系統移植
2.1 開發流程
基于oneos的開發方式和以往不同,先將原始工程編譯生成庫,全部復制到oneos工程,再基于oneos系統開發業務邏輯,其開發環境和原始工程開發環境無關。Oneos工程編譯生成的bin文件下載到設備,完整版本支持microPython,可以導入python文件直接運行。
可能部分功能比較特殊,使用原始庫文件無法實現,例如獲取系統某個參數。在原始工程開發,可以直接將客制化代碼插入某個接口攔截,基于oneos開發時盡量避免,但是實在不行也只能這樣。這樣操作后,原始工程編譯成功,但鏈接肯定失敗,但不影響結果,只要輸出lib庫即可。
2.2 操作系統適配
如果沒有原始SDK,要運行oneos,直接將原始庫,例如STM32原廠HAL庫復制到oneos/thirdparty即可;但是有基礎SDK,且SDK是基于其它RTOS開發,則其庫要在oneos運行,需要進行適配轉換,存在兩種方式。
以目前支持cat1網絡比較火的兩個芯片平臺為例。紫光展銳UIS8910平臺使用freeRTOS,且基本開源,因此可以將UIS8910工程中的freeRTOS系統接口,其函數內容替換使用oneos的接口實現。
翱捷ASR1603平臺使用threadX,且已封庫,因此是在oneos工程,將oneos的系統內核接口函數內容使用ASR1603提供的庫實現。
<公眾號:嵌入式系統>
前者UIS8910是oneos直接在底層替換了freeRTOS,相當于只運行了一套較為干凈的oneos;后者完全是將兩套RTOS的接口互相匹配,且中間并不是一對一替換。
2.3 風險與限制
原始工程開發函數是直接調用,引入oneos框架后,內核適配、驅動框架增加了代碼量,運行效率也存在一定損失。對網絡modem相關的操作,oneos使用AT通信,其阻塞方式對原有應用邏輯存在較大影響,不如原始API便捷。
3、系統組件
3.1 編譯器關鍵字
重點介紹section關鍵字,后續章節都與其有關,section主要作用是將函數或者變量放在指定段中,可在指定的地方取函數執行。
- //main.c
- //section demo
- #include "stdio.h"
- int __attribute__((section("my_fun"))) test1(int a,int b)
- {
- return (a+b);
- }
- int test(int b)
- {
- return 2*b;
- }
- int __attribute__((section("my_fun"))) test0(int a,int b)
- {
- return (a*b);
- }
- int __attribute__((section("my_val"))) chengi;
- int __attribute__((section("my_val"))) chengj;
- int main(void)
- {
- int sum,c,j;
- chengi=1,chengj=2;
- sum=test1(chengi,chengj);
- c=test(100);
- j=test0(chengi,chengj);
- printf("sum=%d,c=%d,j=%d\r\n",sum,c,j);
- return 0;
- }
編譯生成map文件:
- gcc -o main.exe main.c -Wl,-Map,my_test.map
my_test.map 文件片段如下:
- .text 0x00401460 0xa0 C:\Users\think\ccmGLaeH.o0x00401460 test0x0040146a main
- .text 0x00401500 0x0 c:/mingw/bin/../libmingw32.a(CRTglob.o)
- ...... my_fun 0x00404000 0x200 [!provide] PROVIDE (___start_my_fun, .)
- my_fun 0x00404000 0x1c C:\Users\think\ccmGLaeH.o
- 0x00404000 test10x0040400d test0
- [!provide] PROVIDE (___stop_my_fun, .)
- .data 0x00405000 0x200
- 0x00405000 _data_start_ = . ...... *(.data_cygwin_nocopy) my_val 0x00406000 0x200
- [!provide] PROVIDE (___start_my_val, .)
- my_val 0x00406000 0x8 C:\Users\think\ccdMcTrl.o0x00406000 chengi0x00406004 chengj
- [!provide] PROVIDE (___stop_my_val, .)
- .rdata 0x00407000 0x400
分析可見,使用section修飾的函數和變量在自定義的片段,而且是連續存放,這樣可根據變量的地址得出與其同段變量的地址,為后續自動初始化等功能提供了基礎。
3.2 自動初始化
基于前面section的作用,可以將同類函數指針全部使用同一個段名修飾,然后開機后系統自動檢索段內函數指針,逐個執行,對上層應用就是無需主動調用,系統自動初始化??紤]到硬件初始化與應用功能初始化的先后順序,可以對段名進行分配,map文件按段名排序。自動初始化主體是OS_INIT_EXPORT宏。
- typedef os_err_t (*os_init_fn_t)(void);
- #define OS_INIT_EXPORT(fn, level) \
- const os_init_fn_t __os_call_##fn OS_SECTION(".init_call."level) = fn
- #define OS_BOARD_INIT(fn) OS_INIT_EXPORT(fn, "1")
- #define OS_PREV_INIT(fn) OS_INIT_EXPORT(fn, "2")
- #define OS_DEVICE_INIT(fn) OS_INIT_EXPORT(fn, "3")
- #define OS_CMPOENT_INIT(fn) OS_INIT_EXPORT(fn, "4")
- #define OS_ENV_INIT(fn) OS_INIT_EXPORT(fn, "5")
- #define OS_APP_INIT(fn) OS_INIT_EXPORT(fn, "6")
例如shell初始化函數,定義如下:
- OS_APP_INIT(sh_system_init);
將宏定義展開
- /* 含義是函數指針 __os_call_sh_system_init
- * 其指向sh_system_init函數,且該指針編譯后放在".init_call.6"段
- */
- const os_init_fn_t __os_call_sh_system_init
- __attribute__((section((".init_call.6")))) = sh_system_init
系統自身也有自定義函數,用來標記起止點函數
- OS_INIT_EXPORT(os_init_start, "0");
- OS_INIT_EXPORT(os_board_init_start, "0.end");
- OS_INIT_EXPORT(os_board_init_end, "1.end");
- OS_INIT_EXPORT(os_init_end, "6.end");
最終生成的map文件如下圖:
//系統底層在合適的時機調用如下兩函數,將指定段區間內的所有函數自動執行
- //系統底層在合適的時機調用如下兩函數,將指定段區間內的所有函數自動執行
- void os_board_auto_init(void)
- {
- const os_init_fn_t *fn_ptr_board_init_start;
- const os_init_fn_t *fn_ptr_board_init_end;
- const os_init_fn_t *fn_ptr;
- fn_ptr_board_init_start = &__os_call_os_board_init_start + 1;
- fn_ptr_board_init_end = &__os_call_os_board_init_end - 1;
- for (fn_ptr = fn_ptr_board_init_start; fn_ptr <= fn_ptr_board_init_end; fn_ptr++)
- {
- (void)(*fn_ptr)();
- }
- return;
- }
- static void os_other_auto_init(void)
- {
- const os_init_fn_t *fn_ptr_other_init_start;
- const os_init_fn_t *fn_ptr_other_init_end;
- const os_init_fn_t *fn_ptr;
- fn_ptr_other_init_start = &__os_call_os_board_init_end + 1;
- fn_ptr_other_init_end = &__os_call_os_init_end - 1;
- for (fn_ptr = fn_ptr_other_init_start; fn_ptr <= fn_ptr_other_init_end; fn_ptr++)
- {
- (void)(*fn_ptr)();
- }
- return;
- }
系統執行os_other_auto_init時實現了sh_system_init的自動執行,即使應用層沒有顯示的去調用它。使用符號段的方式實現初始化函數自動執行,應用層修改軟件,增加功能啟動或者裁剪,對底層代碼無需任何改動。
3.3 設備框架
3.3.1 設備模型
一般HAL包括GPIO、UART、ADC等,每個設備節點的類型和控制接口、參數個數及含義完全不同,即使都是GPIO,不同原廠提供的接口也各不相同。設備框架就是在底層封裝原始API,然后統一注冊到設備節點表,使用時獲取節點及其對應的操作接口,這樣應用層的代碼在風格上比較統一。
應用層需要操作設備時,根據名稱查找設備,再使用該提供的API進行操作,無需關注該設備具體對應的端口、狀態等細節信息;其風格與linux驅動接近。
3.3.2 設備注冊
以I2C設備為例:
- #define OS_DEVICE_INFO static OS_SECTION("device_table") const os_device_info_t
- OS_DEVICE_INFO asr1603_i2c1_device = {
- .name = "i2c1",
- .driver = "ASR1603_I2C_DRIVER",
- .info = OS_NULL,
- };
- OS_DEVICE_INFO asr1603_i2c2_device = {
- .name = "i2c2",
- .driver = "ASR1603_I2C_DRIVER",
- .info = OS_NULL,
- };
所有的設備信息存在device_table段,只是分配設備驅動類型和名稱。
- OS_DRIVER_INFO asr1603_i2c_driver = {
- .name = "ASR1603_I2C_DRIVER",
- .probe = asr1603_i2c_probe, //I2C設備初始化和注冊
- };
- OS_DRIVER_DEFINE(asr1603_i2c_driver, "2");
- #define OS_DRIVER_DEFINE(_driver_, sequence) \
- static os_err_t __driver_##_driver_##_init(void) \
- { \
- return driver_match_devices(&_driver_); \
- } \
- OS_INIT_EXPORT(__driver_##_driver_##_init, sequence)
- //OS_INIT_EXPORT即為前面提到的開機自啟動定義宏
開機后自動執行_asr1603_i2c_driver_driver__init,也就是自動將device_table段設備對應的驅動程序asr1603_i2c_probe自動執行,實現了所有設備的初始化,
- static int asr1603_i2c_probe(const os_driver_info_t *drv, const os_device_in
- fo_t *dev)
- {
- ...
- //所有的 I2C 設備(一種設備有多個)進行初始化
- if(!strcmp(dev->name, "i2c1"))
- {
- g_i2c1.id = ASR1603_DEV_I2C1;
- i2c_p = &g_i2c1;
- }
- else if(!strcmp(dev->name, "i2c2"))
- {
- g_i2c2.id = ASR1603_DEV_I2C2;
- i2c_p = &g_i2c2;
- }
- ....
- asr1603_wrap_i2c_init(i2c_p->id);
- i2c_p->i2c_bus.ops = &i2c_bus_ops; //底層操作 I2C 的接口,與實際硬件綁定
- i2c_p->i2c_bus.priv = i2c_p;
- ret = os_i2c_bus_device_register(&(i2c_p->i2c_bus), dev->name, OS_DEVICE
- _FLAG_RDWR, &(i2c_p->i2c_bus));
- return ret;
- }
os_i2c_bus_device_register將I2C設備注冊到系統設備列表os_device_list,包括其對外接口i2c_ops。
- struct os_device_ops
- {
- os_err_t (*init) (os_device_t *dev);
- os_err_t (*open) (os_device_t *dev, os_uint16_t oflag);
- os_err_t (*close) (os_device_t *dev);
- os_size_t (*read) (os_device_t *dev, os_off_t pos, void *buffer, os_size_t size);
- os_size_t (*write) (os_device_t *dev, os_off_t pos, const void *buffer, os_size_t size);
- os_err_t (*control)(os_device_t *dev, os_int32_t cmd, void *args);
- };
所有設備對外提供接口都類似,部分不支持的為NULL,風格和linux設備驅動一致,這些接口是封裝前面i2c_bus_ops提供的硬件特有驅動,這樣完成了I2C設備框架與硬件驅動綁定以及自動初始化。
3.3.3 框架應用
應用層使用I2C設備:
- os_device_find("i2c1");
獲取成功后,正常流程是使用i2c_ops提供的接口操作設備,實際調用也基于i2c_bus_ops封裝的接口,可見oneos也不太標準;最佳操作可以參考UART的用法。
3.4 模組連接套件
模組連接套件 Molink (Module link kit),設備通過AT與網絡模組交互的接口,內置基帶的使用虛擬AT通道。
Molink對單片機加模組的方案非常合適,對內置基帶的芯片,反而影響效率,因為其AT是阻塞方式實現,例如掃描周圍wifi熱點,會導致當前task阻塞幾秒鐘,這樣處理只是為統一API接口,實現MCU+模組和內置基帶兩種硬件方案的應用代碼無縫遷移。
名稱高大上,其實就是開機初始化一個大數組,在module_asr1603_create(),不同作用的AT分類,封裝AT收發、解析接口。
使用mo-link先獲取數組中對應項,使用其支持的API操作AT指令,以阻塞方式運行。對于ASR1603內置基帶的,socket沒有使用AT方式,而是LWIP接口,這種效率高。
3.5 Shell工具
和linux中shell類似,以命令行觸發函數運行,在shell控制口,默認是OS_CONSOLE_DEVICE_NAME輸入命令,shell task會解析并自動掃描內部函數表,執行函數后輸出回應,將結果顯示在控制終端上。
Shell對軟件調試非常方便,例如調試I2C接口,只需定義:
- SH_CMD_EXPORT(test, test_i2c, "test i2c api");
開機后串口輸入test字符串,設備即運行test_i2c()函數,其原理如下:
- #define SH_CMD_EXPORT(cmd, func, desc) SH_FUNCTION_EXPORT_CMD(func, __cmd_##cmd, desc)
- #define SH_FUNCTION_EXPORT_CMD(func, cmd, desc) \
- const char __fsym_##cmd##_name[] = #cmd; \
- const char __fsym_##cmd##_desc[] = desc; \
- OS_USED const sh_syscall_t __fsym_##cmd OS_SECTION("FSymTab") = \
- { \
- __fsym_##cmd##_name, \
- __fsym_##cmd##_desc, \
- (syscall_func_t)func \
- };
SH_CMD_EXPORT宏將前面i2c提供的參數轉換,在FsymTab段創建一個名為__fsym___cmd_test的結構體,其3個成員分別是字符名,描述和函數體。
- OS_APP_INIT(sh_system_init);
開機自啟動sh_system_init,創建gs_shell_task任務,接收shell控制口的字符數據,滿足一定條件后進入sh_exec,搜索FsymTab段區間變量名,sh_get_cmd_func找到對應函數再執行。
shell工具便于調試,調試復雜功能注意??臻g;但其在數據安全方面存在較大隱患,且占用獨立的task和串口,浪費硬件資源,正式發布的軟件務必關閉。
3.6 單元測試
類比assert的作用,判斷條件為假時觸發異常,單元測試與其類似,統計判斷結果導致報告。OneOS 開發的單元測試框架atest(and test),和網上開源的差不多。
- #define ATEST_TC_EXPORT(name, testcase, init, cleanup, priority) \
- OS_USED static const atest_tc_entry_t gs_atest##testcase OS_SECTION("AtestTcTab") = \
- { \
- #name, \
- init, \
- testcase, \
- cleanup, \
- priority \
- };
其原理就是軟件自動執行某一段代碼,將運行結果和期望值進行比較并統計,對軟件質量的檢測效果,取決于單元測試用例的設計水平。該功能與平臺無關,適用于新平臺首次使用時測試API。
4、 Python開發
基于前面shell的原理,可以按輸入的字符串執行與之綁定的函數,如果對字符串進行一定的規則定義,支持自動解析執行,即可實現函數按提供的文本執行。這套文本規則就是python語法,解析器就是MicroPython內核,這樣就能實現在嵌入式平臺使用python開發。
MicroPython對軟件進行天然的分層,嚴格區分驅動層和應用層,實現應用軟件的跨平臺移植。Oneos集成的就是開源的MicroPython,其源碼下載地址是:https://github.com/micropython/micropython
OneOS-MicroPython開發環境:VsCode+NODE+Pymakr ,其中.mpy文件混淆加密的工具在MicroPython源碼mpy-cross中自行編譯。
短期內Python不會成為嵌入式的主流開發語言,但掌握其基礎也有大有裨益。