当前位置: 首页 > 科技观察

中国移动Oneos框架基础及其组件分析

时间:2023-03-16 22:13:04 科技观察

1、oneos系统1.1开发手册OneOS是中国移动面向物联网领域推出的一款轻量级操作系统。具有可裁剪、跨平台、低功耗、高安全性等特点,支持ARMCortex-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=1567994787777822721.3软件框架OneOS整体架构采用分层设计,主体由驱动、内核、组件和安全框架,使用轻量级内核和多个系统组件。与只包含内核的freeRTOS相比,oneos支持常用组件或第三方库,特别方便与移动平台对接,遵循Apache2.0许可开源协议,可随意使用。1.4内核Oneos内核提供常用的RTOS功能,如任务管理和调度、任务之间的同步和通信、定时器和内存管理。与freeRTOS等常规RTOS相比,只有几个小区别:1.任务需要额外调用后启动,不同于freeRTOS统一创建所有任务并开始调度,所有任务开始执行。如果每个任务都是独立启动的,需要注意顺序关系。task1启动后不能向task2的队列发送消息。这时候可能队列为NULL,发送前没有判断,会导致重启。2、消息队列,它的动态创建和发送接口类似,但是接收队列消息需要传入的参数不同,需要传入期望的字节大小。巧的是,使用的消息队列项目都是相同的结构。如果不一致,需要特殊处理。3、工作队列在一定程度上封装了任务。无需创建新任务来处理某个事件。交给系统预先创建的任务统一执行。执行完成后触发回调函数,这样多个长时间运行但不频繁的触发就可以交给工作队列处理了。UIS8910系统自带此功能。4.Oneos特有的Mailbox,可以理解为消息队列的简化版。如果对freeRTOS不熟悉,可以参考介绍文章1.5组件。前面说到如何使用oneos,移动官网有详细的说明。本文仅介绍oneos的基本开发流程,分析其部分功能的实现,然后使用其设备框架,SHELL和单元测试主要由三部分组成。2.系统移植2.1基于oneos的开发过程与以往有所不同。首先将原项目编译生成库,全部复制到oneos项目中,然后基于oneos系统开发业务逻辑。开发环境与原项目开发环境无关。Oneos工程编译生成的bin文件下载到设备中。完整版支持microPython,可以导入python文件直接运行。有些功能可能比较特殊,使用原库文件无法实现,比如获取系统的某个参数。在原来的项目开发中,可以直接将自定义的代码插入到某个接口中进行拦截,基于oneos开发时尽量避免,但这是唯一的办法。这样做之后,原来的工程编译成功,但是链接一定失败了,不过不影响结果,只要输出lib库即可。2.2操作系统适配如果没有原版SDK,要运行oneos,只需将原版库,如原版STM32HAL库,拷贝到oneos/thirdparty即可;但如果有基础SDK,而SDK是基于其他RTOS开发的,其库需要在oneos上运行,需要适配转换,有两种方式。以支持cat1网络的两种流行芯片平台为例。紫光展锐UIS8910平台使用的是freeRTOS,而且基本是开源的,所以可以将UIS8910项目中的freeRTOS系统界面替换成oneos界面来实现其功能内容。ASR1603平台使用threadX,库已关闭。所以在oneos项目中,oneos系统内核接口函数的内容是使用ASR1603提供的库实现的。<公众号:EmbeddedSystem>之前的UIS8910是oneos直接在底层替换freeRTOS,相当于只跑一个比较干净的oneos;后者完全匹配两套RTOS的接口,中间不是一对一的替换。2.3风险与局限原项目开发功能直接调用。引入oneos框架后,内核适配和驱动框架增加了代码量,运行效率也有一定损失。对于网络调制解调器相关的操作,oneos采用AT通信,其阻塞方式对原有应用逻辑影响较大,不如原有API方便。3.系统组成3.1编译器关键字重点介绍section关键字,后续章节均与之相关。section的主要作用是将函数或变量放在指定的section中,函数可以在指定的地方执行。//main.c//sectiondemo#include"stdio.h"int__attribute__((section("my_fun")))test1(inta,intb){return(a+b);}inttest(intb){return2*b;}int__attribute__((section("my_fun")))test0(inta,intb){return(a*b);}int__attribute__((section("my_val")))chengi;int__attribute__((section("my_val")))chengj;intmain(void){intsum,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);return0;}编译生成map文件:gcc-omain.exemain.c-Wl,-Map,my_test.mapmy_test.map文件片段如下:.text0x004014600xa0C:\Users\think\ccmGLaeH.o0x00401460test0x0040146amain.text0x004015000x0c:/mingw/bin/../libmingw32.a(CRTglob.o)......my_fun0x0040x400[PRO___start_my_fun,)my_fun0x00404000000x1cc:\uders\think\ccmglaeh.o0x004040004000test10x0040400400dtest0[!supply]提供(________________________my_mmy_fun_fun,!provide]PROVIDE(___start_my_val,.)my_val0x004060000x8C:\Users\think\ccdMcTrl.o0x00406000chengi0x00406004chengj[!provide]PROVIDE(___stop_my_val,.).rdata0x004070000x400在连续变量的分析中是可见的,所以通过地址Store修改的函数根据变量的地址可以得到同段变量的地址,为后续的自动初始化等功能提供依据。之后,系统会自动获取段中的函数指针,并一一执行。上层应用无需主动调用,系统自动初始化。考虑到硬件初始化和应用函数初始化的顺序,可以分配段名,映射文件按照段名排序。自动初始化的对象是OS_INIT_EXPORT宏。typedefos_err_t(*os_init_fn_t)(void);#defineOS_INIT_EXPORT(fn,level)\constos_init_fn_t__os_call_##fnOS_SECTION(".init_call."level)=fn#defineOS_BOARD_INIT(fn)OS_INIT_EXPORT(fn,"1")#PRINIT_OS(_(fn"2")#defineOS_DEVICE_INIT(fn)OS_INIT_EXPORT(fn,"3")#defineOS_CMPOENT_INIT(fn)OS_INIT_EXPORT(fn,"4")#defineOS_ENV_INIT(fn)OS_INIT_EXPORT(fn,"5")#defineOS_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"section*/constos_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");最终生成的地图文件如下://系统底层适时调用下面两个函数自动执行指定段内的所有函数//系统底层适时调用下面两个函数自动执行指定段内的所有函数voidos_board_auto_init(void){constos_init_fn_t*fn_ptr_board_init_start;constos_init_fn_t*fn_ptr_board_init_end;constos_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)();}返回;}staticvoidos_other_auto_init(void){constos_init_fn_t*fn_ptr_other_init_start;constos_init_fn_t*fn_ptr_other_init_end;constos_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.3Deviceframework3.3.1Devicemodel一般HAL包括GPIO、UART、ADC等,各个设备节点的类型和控制接口、参数个数和含义完全不同。即使都是GPIO,不同厂家提供的接口也是不一样的。相同的。设备框架是将底层原有的API进行封装,然后统一注册到设备节点表中,使用时获取节点及其对应的操作接口,使得应用层的代码在风格上比较统一.当应用层需要对设备进行操作时,根据名称查找设备,然后使用提供的API进行操作,无需关注设备的具体端口、状态等详细信息;它的风格接近linux驱动程序的风格。3.3.2设备注册以I2C设备为例:#defineOS_DEVICE_INFOstaticOS_SECTION("device_table")constos_device_info_tOS_DEVICE_INFOasr1603_i2c1_device={.name="i2c1",.driver="ASR1603_I2C_DRIVER",.info=OS_NULL,};OS_DEVICE_INFOasr1603_i2c2_device={.name="i2c2",.driver="ASR1603_I2C_DRIVER",.info=OS_NULL,};所有的设备信息都存在于device_table段中,只分配了设备驱动类型和名称。OS_DRIVER_INFOasr1603_i2c_driver={.name="ASR1603_I2C_DRIVER",.probe=asr1603_i2c_probe,//I2C设备初始化和注册};OS_DRIVER_DEFINE(asr1603_i2c_driver,"2");#defineOS_DRIVER_DEFINE(_driver_,sequence)\staticos_err_t__driver_##_driver_##_init(void)\{\returndriver_match_devices(&_driver_);\}\OS_INIT_EXPORT(__driver_##_driver_##_init,sequence)//OS_INIT_EXPORT就是上面说的自启动定义宏,开机后自动执行_asr1603_i2c_driver_driver__init,即自动设置device_table自动执行段设备对应的驱动asr1603_i2c_probe,实现所有设备的初始化,staticintasr1603_i2c_probe(constos_driver_info_t*drv,constos_device_info_t*dev){...//所有I2C设备(有多个)都初始化if(!strcmp(dev->name,"i2c1")){g_i2c1.id=ASR1603_DEV_I2C1;i2c_p=&g_i2c1;}elseif(!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));returnret;}os_i2c_bus_device_register将I2C设备注册到系统设备列表os_device_list中,包括其对外接口i2c_opsstructos_device_ops{os_err_t(*init)(os_device_t*dev);os_)openr_t((os_device_t*dev,os_uint16_toflag);os_err_t(*close)(os_device_t*dev);os_size_t(*read)(os_device_t*dev,os_off_tpos,void*buffer,os_size_tsize);os_size_t(*write)(os_device_t*dev,os_off_tpos,constvoid*buffer,os_size_tsize);os_err_t(*control)(os_device_t*dev,os_int32_tcmd,void*args);};所有设备提供的接口都差不多,不支持的部分为NULL,风格与linux设备驱动,这些接口是封装前面的i2c_bus_ops提供的硬件专用驱动,完成I2C设备框架和硬件驱动的绑定和自动初始化3.3.3框架应用应用层使用I2C设备:os_device_find("i2c1");之后获取成功,正常流程是使用i2c_ops提供的接口操作设备,实际调用也是基于i2c_bus_ops封装的接口。可见oneos不是很标准;最佳操作可以参考串口的使用。3.4ModulelinkkitMolink(Modulelinkkit),设备通过AT与网络模块交互的接口,内置基带使用虚拟AT通道。Molink很适合MCU加模组的方案,但是对于内置基带的芯片反而影响效率,因为它的AT是采用阻塞的方式实现的,比如扫描周围的wifi热点会导致电流任务阻塞几秒,这个处理只是为了统一API接口,实现MCU+模组和内置基带两种硬件方案应用代码的无缝迁移。名字高大上,其实就是在启动时初始化一个大数组。在module_asr1603_create()中,对不同功能的AT进行了分类,封装了AT的收发和解析接口。使用mo-link首先获取数组中的对应项,使用其支持的API操作AT指令,以阻塞方式运行。ASR1603内置基带,socket没有使用AT方式,而是LWIP接口,效率高。3.5Shell工具类似于linux中的shell。它使用命令行来触发函数运行。在shell控制端口,默认是OS_CONSOLE_DEVICE_NAME来输入命令。shell任务会分析并自动扫描内部函数表,执行函数后输出响应,并将结果显示在上级控制终端上。Shell对软件调试非常方便。例如调试I2C接口,只需要定义:SH_CMD_EXPORT(test,test_i2c,"testi2capi");开机后通过串口输入测试字符串,设备会运行test_i2c()函数。原理如下:#defineSH_CMD_EXPORT(cmd,func,desc)SH_FUNCTION_EXPORT_CMD(func,__cmd_##cmd,desc)#defineSH_FUNCTION_EXPORT_CMD(func,cmd,desc)\constchar__fsym_##cmd##_name[]=#cmd;\constchar__fsym_##cmd##_desc[]=desc;\OS_USEDconstsh_syscall_t__fsym_##cmdOS_SECTION("FSymTab")=\{\__fsym_##cmd##_name,\__fsym_##cmd##_desc,\(syscall_func_t)func\};SH_CMD_EXPORT宏将提供前面的i2c参数进行转换,在FsymTab段创建一个名为__fsym___cmd_test的结构体,它的三个成员分别是角色名、描述和函数体。OS_APP_INIT(sh_system_init);开机后启动sh_system_init,创建gs_shell_task任务,接收shell控制端口的字符数据,满足一定条件后进入sh_exec,搜索FsymTab段的变量名,sh_get_cmd_func找到对应的函数然后执行。shell工具调试方便,调试复杂函数时注意堆栈空间;但在数据安全上存在很大隐患,占用独立任务和串口,浪费硬件资源,正式发布的软件必须关闭。3.6unittest的作用类似于assert。当判断条件为假时,触发异常。单元测试与之类似,统计判断结果出报告。OneOS开发的单元测试框架atest(andtest)和网上开源的类似。#defineATEST_TC_EXPORT(name,testcase,init,cleanup,priority)\OS_USEDstaticconstatest_tc_entry_tgs_atest##testcaseOS_SECTION("AtestTcTab")=\{\#name,\init,\testcase,\cleanup,\priority\};其原理是软件自动执行某段代码,将运行结果与期望值进行比较统计,对软件质量的检测效果取决于单元测试用例的设计水平。该特性与平台无关,适用于新平台首次使用时的API测试。4、Python开发是基于前面shell的原理,绑定的函数可以根据输入的字符串执行。如果对字符串定义了一定的规则,并且支持自动解析执行,则函数可以根据提供的文本执行。这套文本规则就是python语法,解析器就是MicroPython内核,这样就可以在嵌入式平台上实现python开发。MicroPython软件具有天然的分层性,严格区分驱动层和应用层,实现了应用软件的跨平台移植。Oneos集成了开源的MicroPython,其源码下载地址为:https://github.com/micropython/micropythonOneOS-MicroPython开发环境:VsCode+NODE+Pymakr,其中.mpy文件混淆加密工具在MicroPython中源码mpy-cross自己编译。短期内Python不会成为主流的嵌入式开发语言,但是掌握它的基础也是大有裨益的。