2.2 系统模块

本小节介绍Apache Mynewt操作系统内核意外的核心系统功能。包括如何计算统计数据,日志数据以及管理运行Mynewt系统的设备的配置。

2.2.1 配置

配置子系统用于包中每个设备配置和运行时状态。

配置项以键值对的形式保存,而键和值都应该是字符串。键被划分为组件元素,其中包使用第一个元素注册它的子树。如键id/serial包含了两个组件,id以及serial。包sys/id在id下注册了配置元素的子树(sys/id/…)。

有一些方便的操作方式用于进行value的字符串来回转换。

处理程序(Handlers)

配置处理程序为子树实现了一组handler函数。他们通过调用conf_register()函数注册。

  • ch_get,当调用conf_get_value()从配置中获取元素值时,调用此接口
  • ch_set,调用conf_set_value()函数设置值,或从持续存储中用conf_load()加载配置时,调用此接口
  • ch_commit,在配置被完整加载后调用。有时不希望在没配置一个value就立即生效,比如多个相互依赖的设置。
  • ch_export,到处当前所有配置时调用。发生在使用conf_save()试图保存设置,或使用CLI将当前系统配置导出到控制台。
  1. /**
  2. * Configuration handler, used to register a config item/subtree.
  3. */
  4. struct conf_handler {
  5. SLIST_ENTRY(conf_handler) ch_list;
  6. /**
  7. * The name of the conifguration item/subtree
  8. */
  9. char *ch_name;
  10. /** Get configuration value */
  11. conf_get_handler_t ch_get;
  12. /** Set configuration value */
  13. conf_set_handler_t ch_set;
  14. /** Commit configuration value */
  15. conf_commit_handler_t ch_commit;
  16. /** Export configuration value */
  17. conf_export_handler_t ch_export;
  18. };

持久化

配置的后端存储可以在FCB,文件系统,或二者都使用。

可以为配置声明多个源,当调用conf_load时,所有设置都将重置。

写配置的目标只能有一个,这个就是调用conf_save()或conf_save_one()保存数据的地方。

FCB读目标使用conf_fcb_src()函数,写目标通过conf_fcb_dst()。conf_fcb_src()函数附带初始化FCB区域,因此在调用conf_fcb_dst()时也需要调用此函数。文件读取目标使用conf_file_src(),写目标使用conf_file_dst()。

为了便于一个配置区域的初始化,可以通过设置syscfg变量:CONFIG_FCB或CONFIG_NFFS。它们使用其他syscfg变量来确定BSP的哪个flash_map入口定义了flash区域,或使用哪个文件。检查sys/config包中的syscfg.yml文件可以获得更多的描述。

CLI

当通过将syscfg变量中的CONFIG_CLI设置1,将使能shell包。

通过CLI,你可以设置配置变量,检查他们的值,并打印已保存的配置和正在运行的配置。

  • config dump,导出当前正在运行的配置。
  • config dump saved,导出已经保存的撇合作,将按照恢复的顺序打印。
  • config ,打印key对应的值。
  • config ,设置变量key的value。

2.2.2 日志

Mynewt的日志包支持在Mynewt应用程序中记录日志信息。它允许包用单独的名称定义自己的日志流。它还允许应用程序控制日志的输出目的地。

描述

在Mynewt操作系统中,log包有两个版本:

  • sys/log/full包,实现完整的日志功能和API
  • sys/log/stub包,为API实现存根(stub)

两个包都到处了log应用程序接口,任何使用log应用程序接口的包必须在pkg.yml文件的依赖中声明log:

  1. pkg.req_apis:
  2. - log

应用程序的pkg.yml文件指定了log包的版本。若一个项目需要使用完整日志功能,则需要在pgk.yml文件的依赖中声明sys/log/full包:

  1. pkg.deps:
  2. - "@apache-mynewt-core/sys/log/full"

如果在应用程序中不需要使用日志记录,可以使用sys/log/stub来减小代码大小。

Syscfg设置

系统设置LOG_LEVEL允许你在应用程序中启用日志等级。这样只有日志等级等于或高于LOG_LEVEL的日志才允许打印。应用程序中包含的日志记录数量都会影响应用程序的尺寸。LOG_LEVEL_DEBUG为0,包含所有的日志,将LOG_LEVEL设置为255将禁止所有的日志。日志级别在sys/log/full/include/log/log.h文件中通过#define指定,如下设置将日志等级设置为LOG_LEVEL_ERROR。

  1. syscfg.vals:
  2. LOG_LEVEL: 3

日志等级:LOG_LEVEL_DEBUG—0,INFO—1,WARN—2,ERROR—3,CRITICAL—4。

每个日志流都需要一个log结构体来定义日志的属性。

日志处理程序

要使用日志,需要一个日志处理程序完成日志的I/O操作。log包提供了三种预先构建的日志处理程序:

  • console,控制台,日志流事件直接记录到控制台端口,但不支持walking和读。
  • cbmem,将日志事件读/写到环形缓冲器,支持通过newtmgr和shell命令行walking和读。
  • fcb,读/写日志事件到一个flash循环缓冲区,支持通过newtmgr和shell命令行walking和读。

要使用日志记录,通常不需要创建自己的日志处理程序,可以使用预先构建的。一个包或应用程序必须定义struct log的变量,并使用日志包注册一个日志处理程序。必须调用log_register()来指定日志处理程序:

  1. log_register(char *name, struct log *log, const struct log_handler *lh, void *arg, uint8_t level)

参数:

  • name,日志流的名称
  • log,注册的日志实例
  • lh,日志处理程序的指针,可以指定任意一个预先构建好的日志处理程序:

    • &log_console_hander
    • &log_cbm_hander
    • &log_fcb_handler
  • arg,日志处理程序使用的参数,根据指定的日志处理接口不同而不同

    • log_console_handler使用NULL。
    • log_cbm_handler则传递一个指向已经初始化的cbmem结构体指针。
    • log_fcb_handler则传递一个指向已经初始化的fcb_log结构体指针。

通常,一个使用日志记录的包将定义一个struct log类型的全局变量,如my_package_log。然后使用默认值调用log_register函数,通常应用程序会覆盖日志记录属性和日志记录的位置。包可以通过两种方式来允许应用程序覆盖这些值:

  • 定义应用程序可以设置的系统配置设置,然后包可以使用配置值来调用log_register函数
  • 将my_package_log变量设置为外部变量extern,并让应用程序调用log_register来指定特定的日志处理程序。

2.2.3 统计模块

统计模块允许应用、库或驱动程序记录可以通过Newtmgr工具和控制台显示的统计信息。

通过统计模块,Mynewt系统的故障诊断,维护和使用监视的统计数据更加易于集成。

通过创建和注册统计信息,它们将会自动包含在Newtmgr shell和控制台API中。

实现细节

统计数据是一个可以通过代码设置的无符号整数。在构建统计模块时,实现人员根据统计的评论和计数器所需要的分辨率来选择统计数据的大小。

通常,统计数据是随着代码事件的增加而增加的;然后,却没有明确这样的限定。

统计数据被分为几个段,每个段的统计数据都有自己的名称,可以通过独立的API单独查询。每个段的统计数据也有自己的统计大小,允许用户将大型(64-bit)的统计数据与小型(16-bit)的统计数据分开。

注意:目前不可能将不同大小的统计数据分组到同一个部分。因此需要注意每个段有相同的尺寸。

统计段当前被存储在一个单独的全局统计组中。

统计数据存储在一个简单的结构体中,该结构体包含一个小的stats头,紧接着为stats列表。统计头包含:

  1. struct stats_hdr {
  2. char *s_name;
  3. uint8_t s_size;
  4. uint8_t s_cnt;
  5. uint16_t s_pad1;
  6. #if MYNEWT_VAL(STATS_NAMES)
  7. const struct stats_name_map *s_map;
  8. int s_map_cnt;
  9. #endif
  10. STAILQ_ENTRY(stats_hdr) s_next;
  11. };

MYNEWT_VAL块下的字段只有STATS_NAMES在syscfg配置中设置为1时才使能,允许使用统计名称。

启用统计名称

默认情况下,统计数据是按数字查询的。当syscfg中的STATS_NAMES设置使能统计名称后,可以按照名称查看结果。使能统计名称可以在报告的统计信息中提供更好的描述,但需要在镜像的代码空间中存储更多的字符串信息。

为了使能统计名称,在syscfg.ym文件中将STATS_NAMES值设置为1,或使用newt target set命令设置syscfg中的配置值。

  1. # Package: apps/myapp
  2. syscfg.vals:
  3. STATS_NAMES: 1
  1. $newt target set myapp syscfg=STATS_NAMES=1

注意:newt target set命令只设置了STATS_NAMES设置的syscfg变量为示例,实际应用时应该添加其他设置值。

添加统计

创建新的统计列表需要按以下步骤:

  • 包含stats头文件

    将stats库添加到包或应用的pkg.yml文件中:

    1. pkg.deps:
    2. - "@apache-mynewt-core/sys/stats"

    直接在代码中添加stats库头文件包含:

    1. #include <stats/stats.h>
  • 定义stats段

    使用stats.h中的宏定义来定义stats表,一个stats段的定义:

    1. STATS_SECT_START(my_stat_section)
    2. STATS_SECT_ENTRY(attempt_stat)
    3. STATS_SECT_ENTRY(error_stat)
    4. STATS_SECT_END

    stats.h支持3种不同统计大小的宏定义:

    • STATS_SIZE_16——stats统计大小是16位
    • STATS_SIZE_32——stats统计大小为32位
    • STATS_SIZE_64——stats统计大小为64位

    当编译/预处理时,它会产生这样的结构定义:

    1. struct stats_my_stat_section {
    2. struct stats_hdr s_hdr;
    3. uint32_t sattempt_stat;
    4. uint32_t serror_stat;
    5. };

    可以看到定义的结构包含了一个stats头结构以及两个我们定义的状态。

    若定义的stats在多个模块中使用,需要在头文件中包含定义。

  • 声明该段的实例

    声明全局变量保存统计信息。由于可能使用同一段的多个副本,stats段的变量名必须是唯一的。

    1. STATS_SECT_DECL(my_stat_section) g_mystat;

    如果定义的stats段在多个C文件中使用,需要在其中一个C文件中添加如上的全局声明,在使用文件的头文件中添加extern声明。

    1. extern STATS_SECT_DECL(my_stat_section) g_mystat;
  • 定义stats段的名称表

    不论是否使能STATS_NAMES,都需要定义个stats名称表。如果STATS_NAMES没有使能,将不会占用任何代码空间或镜像。

    1. /* define a few stats for querying */
    2. STATS_NAME_START(my_stat_section)
    3. STATS_NAME(my_stat_section, attempt_stat)
    4. STATS_NAME(my_stat_section, error_stat)
    5. STATS_NAME_END(my_stat_section)

    当预处理器编译时将会产生如下一个结构:

    1. struct stats_name_map g_stats_map_my_stat_section[] = {
    2. { __builtin_offsetof (struct stats_my_stat_section, sattempt_stat), "attempt_stat" },
    3. { __builtin_offsetof (struct stats_my_stat_section, serror_stat), "error_stat" },
    4. };

    利用这个表将允许UI组件为stats找到一个字符串名称。

  • 在代码中实现stat

    在代码中,可以通过使用STATS_INC或STATS_INCN来增加统计数据。

    1. STATS_INC(g_mystat, attempt_stat);
    2. rc = do_task();
    3. if(rc == ERR) {
    4. STATS_INC(g_mystat, error_stat);
    5. }
  • 初始化stats

    要是统计库,需要初始化统计数据。初始化将告诉系统每个统计数据有多大,以及统计数据的数量。如果启用了统计名称,也将初始化统计数据的名称信息。

    1. rc = stats_init(
    2. STATS_HDR(g_mystat),
    3. STATS_SIZE_INIT_PARMS(g_mystat, STATS_SIZE_32),
    4. STATS_NAME_INIT_PARMS(my_stat_section));
    5. assert(rc == 0);
  • 注册stats

    要想系统知道设定的统计信息,需要注册它们。

    1. rc = stats_register("my_stats", STATS_HDR(g_mystat));
    2. assert(rc == 0);

    此外也可以使用stats_init_and_reg函数同时执行初始化和注册。

检索统计数据

统计数据可以通过控制台或newtmgr进行检索。如果在项目中启用了控制台,则可以通过定义的串口查看状态。

使能了统计名称的统计情况如下:

  1. >stat my_stats
  2. >12274:attempt_stat: 3
  3. >12275:error_stat: 0

若未使能统计名称,则统计将不会显示统计名称的字符串:

  1. >stat my_stats
  2. >29149:s0: 3
  3. >29150:s1: 0

2.2.4 控制台

控制台是一个操作系统窗口,用户可以通过控制台与操作系统子系统或控制台应用交互。用户通常通过键盘输入文本并在计算机显示器上查看系统输出文本。文本以用户和操作系统中间的字符序列发送。

可以将控制台配置为通过UART或SEGGER实时终端(RTT)。syscfg设置中的CONSOLE_UART允许使用UART通信,并在默认情况下使能。CONSOLE_RTT设置则允许通过RTT通信,默认情况下未使能。当CONSOLE_UART设置启用时,需要进行以下设置:

  • CONSOLE_UART_DEV,指定UART设备,默认为uart0;
  • CONSOLE_UART_BAUD,指定UART设置的波特率,默认为115200;
  • CONSOLE_UART_FLOW_CONTROL,指定UART的数据流控制,默认为UART_FLOW_CONTROL_NONE;
  • CONSOLE_UART_TX_BUF_SIZE,指定发送缓存区的大小,必须为2的幂次方,默认为32。

CONSOLE_TICKS设置,允许控制台在每一行输出中打印当前操作系统的滴答数。

注意:

  • 在Mynewt 1.0包中,SEGGER RTT暂时没有支持;
  • 控制台包在系统初始化(sysinit)期间初始化,因此不需要初始化控制台。但是如果使用Mynewt 1.0控制台API从控制台读取数据,则需要调用console_init函数来支持向后兼容。

描述

在Mynewt操作系统中,控制台库包含了三个版本:

  • sys/console/full,实现了完整的控制台功能和API;
  • sys/console/stub,实现了控制台API的存根(没有功能);
  • sys/console/minimal,实现了最小的控制台功能,完成控制台读写。实现了console_read()console_write()函数,以及其他控制台函数的stub。

所有这些包都将导出console的API,同时需要在pkg.yml文件中添加依赖:

  1. pkg.name: sys/shell
  2. pkg.deps:
  3. - "@apache-mynewt-core/kernel/os"
  4. - "@apache-mynewt-core/encoding/base64"
  5. - "@apache-mynewt-core/time/datetime"
  6. - "@apache-mynewt-core/util/crc"
  7. pkg.req_apis:
  8. - console

项目pkg.yml文件还需要指定要使用的控制台的版本。

完整控制台

要使用完整控制台功能,必须在pkg.yml文件中列出sys/console/full包作为依赖项:

  1. pkg.name: apps/slinky
  2. pkg.deps:
  3. - "@apache-mynewt-core/test/flash_test"
  4. - "@apache-mynewt-core/mgmt/imgmgr"
  5. - "@apache-mynewt-core/mgmt/newtmgr"
  6. - "@apache-mynewt-core/mgmt/newtmgr/transport/nmgr_shell"
  7. - "@apache-mynewt-core/kernel/os"
  8. - "@apache-mynewt-core/boot/bootutil"
  9. - "@apache-mynewt-core/sys/shell"
  10. - "@apache-mynewt-core/sys/console/full"
  11. ...
  12. - "@apache-mynewt-core/sys/id"

Stub控制台包

需要在项目的pk.yml中添加sys/console/stub包。

使用stub控制台的项目可能有以下几个原因:

  • 项目可能没有物理控制台(如UART接口),可能依赖于具有控制台功能的包;

  • bootloader项目希望保持镜像文件尽可能的小。它包含lib/os包,用于控制台打印消息(如硬件故障),以及lib/util包,使能完整的控制台(仅在SHELL提供CLI的情况下)。然而,但是我们不希望在bootloader中使用任何控制台IO,从而可以保证bootloader尽可能的小。因此我们只使用控制台存根,项目的pkg.yml文件如下:

    1. pkg.name: apps/boot
    2. pkg.deps:
    3. - "@apache-mynewt-core/boot/bootutil"
    4. - "@apache-mynewt-core/kernel/os"
    5. - "@apache-mynewt-core/sys/console/stub"

最小控制台包

有一些项目,依然需要通过串行连接来读写数据,但是又不需要完整的控制台功能。比如只需要支持串口进行镜像升级但又不需要完整的newtmgr功能,此时可以使用最小控制台包,配置pkg.yml文件如下:

  1. pkg.name: apps/boot
  2. pkg.type: app
  3. pkg.description: Boot loader application.
  4. pkg.author: "Apache Mynewt <dev@mynewt.apache.org>"
  5. pkg.homepage: "http://mynewt.apache.org/"
  6. pkg.keywords:
  7. - loader
  8. pkg.deps:
  9. - "@apache-mynewt-core/boot/bootutil"
  10. - "@apache-mynewt-core/kernel/os"
  11. - "@apache-mynewt-core/sys/console/stub"
  12. pkg.deps.BOOT_SERIAL.OVERWRITE:
  13. - "@apache-mynewt-core/sys/console/minimal"
  14. - "@apache-mynewt-core/boot/boot_serial"

控制台输出

可以使用console_write()函数在写原始输出,console_printf()来编写C格式的格式化字符串到控制台。

控制台输入

在syscfg设置中通过以下配置来控制控制台的输入:

  • CONSOLE_INPUT,使能控制台输入,默认开启;
  • CONSOLE_ECHO,允许将控制台接收的数据回显。回显也是默认开启的。尤其是终端程序更希望这样做,这样用户才知道控制台连接和响应。还可以通过使用console_echo()函数来设置回显的打开或关闭;
  • CONSOLE_MAX_INPUT_LEN,指定输入的最大行数。

Mynewt 1.1控制台包添加了一个新的API,用于从控制台读取输入数据。当然这个包也是支持Mynewt 1.0控制台API的向下兼容。

2.2.5 Shell

Shell是运行在控制台程序之上,提供了以下功能:

  • 处理控制台输入
  • 通过串口传输实现newtmgr协议

shell使用系统默认事件队列处理shell事件,且在主任务的上下文环境中运行。应用程序也可以指定一个专门的事件队列供shell使用。

sys/shell包实现了shell程序,要使用shell程序需要完成以下配置:

  • 包含sys/shell包;
  • 设置syscfg配置中的SHELL_TASK为1,使能shell。

注意:

只有在SHELL_TASK设置时,shell相关的函数才会编译和链接到应用程序中。当开发一个希望支持shell命令的包,需要在包中定义:

  1. syscfg配置允许包处理shell命令,即启用SHELL_TASK;
  2. 在包中添加sys/shell包,并解决相关依赖关系。
  1. # sys/log/full syscfg.yml
  2. LOG_CLI:
  3. description: 'Expose "log" command in shell.'
  4. value: 0
  5. restrictions:
  6. - SHELL_TASK
  7. # sys/log/full pkg.yml
  8. pkg.deps.LOG_CLI:
  9. - "@apache-mynewt-core/sys/shell"

以上示例在syscfg.yml文件中定义了LOG_CLI,设置和使能shell中的日志命令,pkg.yml文件中添加了sys/shell包支持,解决LOG_CLI的依赖。

描述

shell的一个工作是将传入的命令定向到其他子系统。它将解析输入的字符串解析为令牌,使用第一个令牌来确定调用哪个子系统命令处理程序。当shell调用命令处理程序,它将其他令牌作为参数传递到处理程序。

注册命令处理程序

实现shell命令的包必须注册一个命令处理程序来处理任务。

在Mynewt 1.1以后,shell支持模块的概念,允许包在命名空间下对shell命令分组。在shell中运行命令,需要输入模块名以及命令名。当然,也可以使用select命令来设置默认模块,这样就只需要输入命令名就可以运行命令。

在Mynewt 1.1中有两种方式可以注册命令处理程序:

  • 为模块定义和注册一组命令。这种方法允许将shell命令分组到命名空间中。一个包调用shell_register()函数定义一个模块,并为模块注册命令处理程序。

    注意:syscfg设置中的SHELL_MAX_MODULES参数,限定了最大的模块数量,若应用程序及包需要的注册值较多,可以修改此值。

  • 在不定义模块的情况下注册一个命令处理程序。调用Mynewt 1.0中定义的shell_cmd_register()函数来注册一个命令处理程序。当使用这种方法注册shell命令时,命令将自动添加到compat模块下。compat模块支持向后兼容所有使用shell_cmd_register函数注册的命令处理程序。

使能Shell命令的帮助信息

shell支持命令帮助。一个包在将命令注册到shell之前,用命令的帮助文本初始化shell_cmd数据结构。syscfg配置中的SHELL_CMD_HELP使能或禁止所有shell命令的帮助支持,默认情况下启用该特性。

启用os和prompt模块

shell实现os和prompt模块。这两个模块支持shell命令来查看操作系统资源。

os模块实现了列出任务和内存池使用信息的命令,并查看和修改时间。syscfg配置中的SHELL_OS_MODULE使能和禁止此模块,默认使能。

prompt模块是实现了ticks命令,决定是否在提示符中打印当前操作系统tick。syscfg配置中的SHELL_PROMPT_MODULE使能和禁止此模块,默认禁止。

串行协议处理Newtmgr协议

shell的第二个工作是处理通过控制台发送的newtmgr协议消息的帧,编码以及解码。newtmgr串行传输包(mgmg/newtmgr/transport/newtmgr_shell)调用shell_nlip_input_regitster()函数来注册shell收到的newtmgr请求消息的处理程序。

syscfg配置中的SHELL_NEWTMGR决定在shell上是否启用newtmgr,默认启用。

  1. struct shell_cmd {
  2. const char *sc_cmd;
  3. shell_cmd_func_t sc_cmd_func;
  4. const struct shell_cmd_help *help;
  5. };
  1. struct shell_module {
  2. const char *name;
  3. const struct shell_cmd *commands;
  4. };
  1. struct shell_cmd_help {
  2. const char *summary;
  3. const char *usage;
  4. const struct shell_param *params;
  5. };

参考资料:

http://mynewt.apache.org/latest/os/modules/system_modules.html