Hello all, This is a follow-up email regarding Mynewt system initialization. I took all of your feedback (very helpful, thanks!) and did some more thinking. As always, all comments, criticisms, and suggestions are appreciated.
Thanks, Chris ### Recap Proposal: 1. Move system initialization out of apps and into BSPs and auto-generated C files. 2. Specify system settings in config files. Motivations: 1. Simplify process of creating an app (no more boilerplate code). 2. Simplify system configuration (don't specify config within C code). 3. Better code organization; cputime, msys, etc. "feel" more like system-level modules than application-level, so put initialization code where it belongs. Examples of "system modules" are: * mbufs / msys. * Network stacks (e.g., NimBLE host, controller, and HCI transport). * newtmgr * console * logging * drivers The biggest challenge is: ensuring the developer maintains precise control over the system configuration. The current Mynewt scheme gives control to the developer by pushing configuration up to the application level where he is forced to deal with it. ### Changes from previous proposal * Settings go in pkg.yml files; elimination of syscfg.yml files. * Initialization is done by generated C files; init functions specified in pkg.yml files. * Removal of newt "features" (pkg.features). ### Summary System initialization happens in two stages: 1. BSP init (bsp_init()) 2. sysinit (sysinit()) (1) BSP init sets up everything that is BSP specific: * UARTs * Flash map * Other hardware drivers (2) Sysinit sets up all the libraries which aren't BSP or hardware dependent (os, msys, network stacks, etc). The initialization code is implemented in the appropriate package. Newt generates C code which executes each package's initialization function. ### Syscfg header file generation The system knows what to initialize and how to initialize it based on the contents of the following header file: <target-path>/include/syscfg/syscfg.h For example: targets/bleprph-nrf52dk/include/syscfg/syscfg.h This header file is generated by newt during the build process. Newt arranges for all packages to have access to this header file. The syscfg.h header file consists of three sections: * Settings * Indication of which packages are present * Indication of which APIs are available Here is an abbreviated syscfg.h: /*** Settings */ #ifndef MYNEWT_VAL_CLOCK_FREQ #define MYNEWT_VAL_CLOCK_FREQ (1000000) #endif #ifndef MYNEWT_VAL_MSYS_1_BLOCK_COUNT #define MYNEWT_VAL_MSYS_1_BLOCK_COUNT (15) #endif #ifndef MYNEWT_VAL_MSYS_1_BLOCK_SIZE #define MYNEWT_VAL_MSYS_1_BLOCK_SIZE (260) #endif /*** Packages */ #ifndef MYNEWT_PKG_LIBS_NEWTMGR #define MYNEWT_PKG_LIBS_NEWTMGR (1) #endif #ifndef MYNEWT_PKG_LIBS_NEWTMGR_TRANSPORT_BLE #define MYNEWT_PKG_LIBS_NEWTMGR_TRANSPORT_BLE (1) #endif #ifndef MYNEWT_PKG_SYS_CONFIG #define MYNEWT_PKG_SYS_CONFIG (1) #endif /*** APIs */ #ifndef MYNEWT_API_BLE_DRIVER #define MYNEWT_API_BLE_DRIVER (1) #endif #ifndef MYNEWT_API_BLE_TRANSPORT #define MYNEWT_API_BLE_TRANSPORT (1) #endif #ifndef MYNEWT_API_CONSOLE #define MYNEWT_API_CONSOLE (1) #endif Newt generates the syscfg.h file from information read from pkg.yml files. This proposal adds two items to the pkg.yml structure: 1. pkg.syscfg_defs 2. pkg.syscfg_vals Both of these items are optional. (1) pkg.syscfg_defs is an associative array of setting definitions. The setting name is indicated by the element key. A setting definition has the following fields: * description ("description") * default value ("value") * requirements (TBD) Here is an example from net/nimble/host/pkg.yml: pkg.syscfg_defs: BLE_SM: description: 'Security manager legacy pairing.' value: 1 BLE_SM_SC: description: 'Security manager secure connections (4.2).' value: 0 A setting can only be defined once. If a setting with the same name is defined more than once, in any number of pkg.yml files, newt complains and aborts the build. (2) pkg.syscfg_vals is also an associative array keyed by setting name. However, it only contains setting values. This array is used to override default setting values. Here is a hypothetical example: pkg.syscfg_vals: MSYS_1_BLOCK_COUNT: 12 MSYS_1_BLOCK_SIZE: 260 CLOCK_FREQ: 1000000 If a package attempts to override an undefined setting, newt complains and the build is aborted. If several packages override the same setting, the tie is broken according to package priority. Packages are prioritized as follows (lowest priority first, highest last): 1. Library 2. BSP 3. App 4. Target The expectation is that libraries would define sensible defaults in their pkg.yml files, and BSPs, apps, and targets would override those defaults. A library is not allowed to override settings; it can only define them. Within the syscfg.h file: * Each setting macro is named as follows: MYNEWT_VAL_<escaped-setting-name> * Each package-presence macro is named as follows: MYNEWT_PKG_<escaped-package-name> * Each API-presence macro is named as follows: MYNEWT_API_<escaped-API-name> A name is escaped by converting it to upper case and replacing all non-alphanumeric characters with underscores. To prevent unnecessary rebuilds, newt only overwrites the syscfg.h file if there are any configuration changes since the previous build. ### Task priorities Settings that specify task priority should not have plain numeric values. Instead, the value should be one of the following forms: (1) PRIORITY_<number> (2) PRIORITY_ANY Newt converts these strings to numeric values as follows: 1. All explicit priorities (form 1) are assigned as specified (i.e., the "PRIORITY_" prefix is removed) 2. All remaining priority settings are iterated alphabetically by name; each setting is assigned a value that is one greater than the current greatest priority. Newt raises an error and aborts if it runs out of priority values (increases beyond 255). Newt raises an error if multiple packages are assigned the same priority. ### newt target config command Newt would have a new command: newt target config <target-name>. This command shows the following information about each setting for the specified target: * Setting name * Setting description * Actual value and package which specifies it. * Default value and package which defines it. * Corresponding C macro name. ### sysinit pkg.yml items The following two optional items are added to the pkg.yml structure: 1. pkg.init_function 2. pkg.init_stage (1) pkg.init_function specifies the name of a C function which initializes the package. (2) pkg.init_stage is a numeric value indicating when the package should get initialized, relative to other packages. Initialization stages are executed in ascending order. Initialization of packages within a stage are ordered alphabetically by package name. ### sysinit C code At build time, newt generates the following target-specific C file: <target-path>/src/sysinit.c This file initializes each package whose pkg.yml file specifies a pkg.init_function item. This file does not depend on any other packages, and it does not include any headers. Here is an abbreviated example of how this file might look: void bootutil_pkg_init(void); void console_pkg_init(void); void log_init(void); void log_reboot_pkg_init(void); void nffs_pkg_init(void); void os_pkg_init(void); void shell_pkg_init(void); void stats_module_init(void); void sysinit(void) { /*** Stage 0 */ /* 0.0: libs/os */ os_pkg_init(); /* 0.1: sys/stats */ stats_pkg_init(); /*** Stage 1 */ /* 1.0: sys/log */ log_pkg_init(); /*** Stage 2 */ /* 2.0: fs/nffs */ nffs_pkg_init(); /* 2.1: sys/reboot */ log_reboot_pkg_init(); /*** Stage 5 */ /* 5.0: libs/bootutil */ bootutil_pkg_init(); /* 5.1: libs/console/full */ console_pkg_init(); /* 5.2: libs/shell */ shell_pkg_init(); } An init function takes no parameters and returns void. The parameter list is empty because the function is expected to read its configuration from the syscfg.h file. The void return type is a bit harder to justify, but here is my thinking. If init functions all returned error codes, the generated sysinit code would always handle a failure return code in the same manner: abort the initialization sequence and perform some emergency behavior (reboot, enter rescure mode, whatever). This repeated checking of return codes would result in extra code (text) with no real benefit. It would be better for initialization to call a panic function on error. The panic function could either assert(0), or use longjmp to break out of the init sequence. Mynewt would need to allow the application to assign semantics to the panic function. ### Removal of features First, here is a summary of how the Mynewt features mechanism works. A feature is a system-wide boolean setting defined by a package. A package's configuration can be dependent on which features the system enables. For example, a feature being enabled by cause a package to specify additional dependencies, cflags, or even other features. I believe features are currently used to solve the following problems: 1. Allow package A to depend on package B, but only if package B is already part of the system. I.e., use it if it's already there, but don't pull it into the build if it isn't. Here is an example of this usage from fs/fs/pkg.yml: pkg.deps.SHELL: - libs/shell I.e., the file system package depends on shell only if the SHELL feature is enabled (presumably by the shell package). The reason for the conditional dependency is the existence of file system shell commands (e.g., ls). 2. Allow package A's C code to know if package B is part of the build. This has a similar use case to (1) above. The fs/fs package does this as follows: pkg.cflags.SHELL: -DSHELL_PRESENT The fs C code registers its shell commands if the SHELL_PRESENT macro is defined. 3. Allow the BSP download and debug scripts to know if a boot loader is being uploaded to the board. The scripts need to know this information so that they know the correct flash address to write the binary. Newt conveys this information to the scripts by passing the list of enabled features as command line arguments to the scripts. If a script detects the "bootloader" argument, it knows it is dealing with a boot loader. I believe the sysinit mechanism handles all of these cases at least as well as the features mechanism does. To simplify usage, I think it is best to remove features entirely. Below I will explain how I expect sysinit to handle the above three cases. (1) Conditional dependencies The contitional "deps" syntax remains the same, except a setting name is appended to "deps." rather than a feature name. If the named setting is defined an non-zero, the dependency is processed. I think this is better than the feature-based method, because usually the simple presence of a package should not determine whether another package depends on it. For example, it is not hard to imagine an application which uses both the fs and shell packages, but which should not contain the fs shell commands (e.g., on a space constrained platform). Currently it is not possible to arrange for this. With syscfg, fs would define a setting indicating whether the shell commands should be included. Example (fs/fs/pkg.yml): pkg.syscfg_defs: FS_CLI: description: 'Enable file system shell commands' value: 1 pkg.deps.FS_CLI: libs/shell (2) Allow package A's C code to know if package B is part of the build. This is trivially handled via inclusion of the syscfg.h header. (3) Allow the BSP download and debug scripts to know if a boot loader is being uploaded to the board. I think the right answer here is to use a separate BSP for the bootloader. Then, the scripts don't have to know what settings are defined because the boot loader BSP scripts are hardcoded to write to address 0. Alternatively, newt could just pass the name of every non-zero setting to the scripts, and the boot app could define a "bootloader" setting. There is one thing that features can do that syscfg cannot: conditionally override other settings. In other words, you can't do something like this: pkg.syscfg_vals: FS_CLI: 1 FS_CLI.I_DONT_WANT_CLI: 0 I don't view this as an issue - I think the appropriate package will know the exact value to assign to a setting. Removal of features would also mean removal of the pkg.feature_blacklist and pkg.feature_whitelist configuration items. While I understand why these are necessary today, I think they have the potential to cause a lot of confusion when trying to nail down exactly which settings your build is configured with. ### Questions / issues 1. Do we need second-stage initialization? This would occur after the OS has been started but before the scheduler is running. This might be tricky to arrange; it isn't clear which stack this code should run in since not all packages have a dedicated stack. 2. Setting profiles. It might be useful for NimBLE to have a peripheral-only configuration, for example. In the system I outlined above, the user would need to override a bunch of BLE settings in his app or target to achieve this. 3. Should libraries be able to override another libraries settings? In this proposal, a package's settings can only be overridden by something above it in the hierarchy (i.e., bsp, app, or target). I couldn't think of a compelling reason to allow this, and it could make builds harder to understand and debug.