Here we detail on how CMRX RTOS is started up. While CMRX expects to use external HAL the description of CMRX kernel startup is closely related to integration of kernel into HAL / SDK.
CMRX is built on top of standard embedded C runtime environment. It is assumed that a main() function exists that gets executed by C runtime, which will zero-initialize the BSS region, initialize stack and call constructors. As with bare-metal firmware, it is expected that integrator provides their own main() function implementation which performs static initialization of hardware. It is also expected that C runtime and all code prior main() is provided preferably by MCU vendor, or an independent HAL vendor. CMRX doesn’t provide any startup code nor expects it to exist.
CMRX is based on CMake build system and expects the project using it too. While it is technically possible to integrate CMRX into non-CMake project, this is not supported at the moment.
The easiest way of integrating CMRX into project is using CMRX platform support by placing following snippet of code anywhere before the point of creation of firmware binary:
set(CMRX_DEVICE <MCU_device_name>)
include(cmrx/cmake/CMRX.cmake)
add_subdirectory(cmrx)
The code snippet assumes that CMRX source code is available in cmrx subdirectory of ${CMAKE_SOURCE_DIR}.
Variable CMRX_DEVICE shall be set to any value the HAL used in the project recognizes as microcontroller part number. CMRX will try to find platform support code which would configure CMRX automatically and execute it. Platform support code will detect HAL presence and configure CMRX to use it.
Only additional step required to finish CMRX integration is changing call to add_executable() in CMakeLists.txt to read as add_firmware(). This add_firmware() function is CMRX-provided wrapper around add_executable() that will perform some additional pre- and post-build actions to ensure fully automatic handling of memory protection.
For sake of speed and ease hardware initialization is performed in main() function using privileged mode CPUs usually start in , without kernel intervening before it is started up. We decided for this design based on the fact that initialization code is rarely processing user-supplied data and thus cybersecurity risk is low in this phase of execution. The code that runs in main() before kernel is started should only perform static initialization, such as configuring GPIO pins into correct operation mode, enabling peripherals, starting and routing clocks and setting-up static peripheral properties.
In this phase of execution, it is not possible to call any kernel services. Timing provider should have its interrupt disabled so it is not possible that prolonged hardware bringup will cause timing provider to execute scheduler callback before scheduler was initialized. Not all hardware initialization / setup is feasible for this stage. If hardware can be enabled / disabled on-demand, it is perfectly fine to move this part of driver to userland. Threads nor processes don’t exist at this time and memory protection boundaries are not enforced yet.
Once all the hardware is initialized and timing provider is configured, CMRX kernel is ready to be started. This is performed by calling function os_start(). This function will never return. Even in case of CMRX kernel shutdown, a special callback is executed instead.
When os_start() is called, it will initialize internal kernel structures based on compile-time configuration and tables generated during the build process. Most important are: table containing list of existing processes and thread autostart table. Kernel spawns any auto-started thread during its boot process. Additionally, kernel spawns one additional thread - idle thread, which is executed if no other thread is runnable. This thread calls CPU-specific variant of “sleep” instruction in tight loop to shut the CPU down until some event arrives. It never calls any CMRK kernel calls and never quits.
In fact, the initialization is a two-stage process. First a system-wide initialization is done, which is ever only performed once, even if machine is multi-core one. Then, core-local initialization is performed. CMRX kernel is inhernetly multi-core capable yet as there is very little amount of hardware that would allow SMP execution on microcontroller-class HW, this support is highly experimental.
When os_start() is done initializing kernel, it finds the task with highest priority and starts executing it. When a task is first executed, it already happens with memory isolation enabled. Memory protection hardware is initialized as a part of kernel initialization during os_start() function execution.
CMRX allocates its own thread stacks. The stack configured in HAL and used by main() function will be used by kernel system call routines and/or ISR handlers. Userspace threads will exclusively use stacks allocated by the kernel.
All userspace code must be organized into processes. To create a process, add_application() function shall be used in CMakeLists.txt file:
add_application(app_name source1.c source2.c)
target_link_libraries(app_name stdlib ....)
target_add_applications(firmware_target app_name)
Above snippet of CMake will create a process (a.k.a application) called app_name. This function is a wrapper around add_library() function, thus all CMRX processes are C static libraries. Following arguments to add_application() name source files this process is composed of. Call to target_link_libraries will link CMRX standard library to the application so system calls and some standard library functions (such as mutexes) can be used. If a process is driver, it is free to link HAL libraries. Last command will link an application to firmware. This function is in turn a wrapper around target_link_libraries.
Inside the library code, it is necessary to declare, that we are in fact creating an application. This is done by following piece of code:
#include <cmrx/application.h>
/* ..... */
OS_APPLICATION_MMIO_RANGE(app_name, 0, 0);
OS_APPLICATION(app_name);
This block of code will create necessary structures so that CMRX is able to recognize the task and manage memory protection unit automatically. The name of application used in C source file must match the name used in CMake functions, otherwise linking phase of application build will fail.
There are two options to start threads in CMRX:
OS_THREAD_CREATE macro to auto-start threads on CMRX startupos_thread_create system call to start threads on-demandIn both cases, thread can only be created in current process. In case of auto-start, current process is the process in whose C file the directive is placed. In latter case, current process is the process which owns the thread.
OS_THREAD_CREATE can be put into any source file belonging to a process, yet to keep code readable it is suggested to place them into same source file which contains OS_APPLICATION macro.
CMRX thread needs an entry point - a function that is executed when this new thread is created and started. This function must have following prototype:
int thread_main(void * data);
It is then possible to create thread using OS_THREAD_CREATE like this:
struct {
/* .... */
} ThreadData thread_data = { /* .... */ };
OS_THREAD_CREATE(app_name, thread_main, &thread_data, 32);
Where thread_data is an argument which will be passed to thread_main when it is entered. This value isn’t interpreted by CMRX kernel and usually it is typed as pointer to structure of data. The value 32 denotes scheduling priority of this thread. The lower the number is, the higher the scheduling priority.
If the function thread_main returns, the thread is terminated and its stack is freed-up.
Function thread_create works the same as OS_THREAD_CREATE macro with only one difference - it allows a thread to be created after CMRX kernel was started. It doesn’t take owning process as an argument - it is the same as the owning process of thread which spawns the next one. It is not possible to spawn a thread in foreign process.