C Microkernel Realtime eXecutive
Realtime Operating System for Cortex-M based microcontrollers
 
Loading...
Searching...
No Matches

Description

Description of CMRX porting layer.

CMRX expects that the following functions are provided by architecture support layer. If creating new layer, these functions have to be implemented.

This list is by no means definitive. Each architecture will require its own mechanisms to be implemented so the kernel can actually work. Mechanisms CMRX requires to be present in the architecture and be implemented are:

  • service / system call mechanism that allows to call the kernel from usermode code
  • memory protection mechanism
  • mechanism allowing to schedule and then perform thread switching

Similarly, some of kernel syscalls are directly implemented by the architecture support layer as there is no way for kernel to know how platform will implement given mechanisms. This covers mostly the RPC call / return syscalls and signal delivery.

To create a port of CMRX to the new architecture a few steps are needed. This guide roughly describes them in general terms and outlines items that have to be provided which are not immediately obvious from the portin layer API.

In the following text, several terms will be used:

Architecture - refers to the CPU family which determines most of target CPU functionality. In case of CMRX, ports are mainly done to support certain CPU architecture rather than specific platform (see next). Examples of architectures are: ARM (Cortex-M), RISC-V (RV32/64-E).

Platform - refers to specific subfamily of CPU. This subfamily may further determine presence of absence of certain CPU features. CMRX mostly don't care about presence of features it does not directly support or require. Features, that are required by CMRX (especially memory protection) must be present, otherwise it is not possible to port CMRX to such architecture reliably. In case of ARM, such platforms might be Cortex-M0+, Cortex-M4 or Cortex-M4F.

Port - refers to specific vendor implementation of the platform. In case of CMRX, specifically for the ARM architecture, all ports are covered by generic port named "CMSIS". This ports expects that your vendor's SDK provides CMSIS-compatible headers. The CMRX build system supports creation of ports, but as long as there is technical solution available that does not require port creation, it should be avoided. In CMRX terminology port refers to the same thing as term "HAL" does.

Anatomy of CMRX port

CMRX kernel is divided into two parts which are interconnected and together form a full CMRX kernel. One part is the platform-independent code that mostly covers the CMRX API and common functionality, like scheduler, timer implementation, etc. Another part provides architecture- and platform-specific functionality needed for the former part to be able to execute.

Following text describes the latter, the porting layer. Porting layer basically consists of three (possibly four) parts:

CMake script to define actions specific to correctly support the target architecture C headers containing definitions both required by platform-independent part and required internally by the port itself. C sources containing implementation of the port. Optionally, there might be some scripts required to support architecture-specific features.

CMake script

Architecture support script is expected to be stored in <root_dir>/cmake/arch/<architecture>/<port>/CMRX.cmake file. This file will be included automatically, once include(CMRX) is hit in project's CMakeLists.txt.

This file is expected to define two functions: add_firmware() target_add_application()

C headers

All headers that provide support for some specific platform shall be stored in <root_dir>/include/cmrx/<architecture>/<port>/arch/ directory. This directory will automatically be added into include paths of CMRX kernel. The code can then refer to headers inside this directory as using #include <arch/header.h>.

This directory shall contain all the header files your port contains. There are no limits on what files you create and how you name them. CMRX platform-independent part will not include almost none of them.

The platform-independent part of CMRX expects just a few files to exist in this directory. They will be directly included by the platform-independent part so they shall only contain entities described below and shall not include any other headers, if possible.

corelocal.h

This file has to contain two entities. They might be implemented as macros, static inline functions or any other kind of function, as needed by the platform.

coreid() - provides ID of the currently running core. For uni-processor systems, this can be a macro hardcoded to return value of 0. OS_NUM_CORES - usually a macro, that provides information on amount of cores present in the system.

mpu.h

This file has to contain one type definition. Portable part of kernel expects its existence:

struct MPU_Registers - the internal structure of this type is not important for the portable part of CMRX kernel, but the size of this structure must be large enough so that the porting layer for the architecture will be able to store memory protection unit state of CPU when swapping threads in and out. The design of this structure is entirely up on the designed of the port. Portable part of CMRX doesn't use the data stored there.

sysenter.h

This file has to define two objects:

__SYSCALL - this macro shall expand to list of attributes that function serving as the syscall entrypoint has to have. It can be empty if there are none, but has to be defined.

__SVC() - this can be either macro or a function, depending on the architecture. It shall expand to or directly execute architecture-specific means of calling the system calls. It has to accept one argument, which is a system call ID.

Port implementation sources

Port has to implement certain functions that are expected to be provided by it. Sources of the port can be stored in directory <root_dir>/src/os/arch/<architecture>. CMRX build system will expect that CMakeLists.txt file exist there and will include it automatically. This CMakeLists.txt file shall define one static library named cmrx_arch. platform-independent part of CMRX will automatically link this library. It can link to cmrx library to obtain access to certain functions of CMRX kernel that might be usable for it.

These sources shall provide implementation of functions outlined in this section of the manual. If port fails to provide the implementation for any of them, build will most probably fail. Port has to accept the API and semantics of functions.

To provide full set of functionality, some architecture-specific mechanisms might need to be implemented by the port. CMRX porting layer does not directly prescribe, which these are as it is not possible to determine them in advance. It is up to the designer of the port to figure out what exact mechanisms are these.

For CMRX purposes, mechanisms, which are known to be needed are:

mechanism to handle "service call". The "service call" is a mechanism of transferring control from the user space into kernel space. This mechanism has to be able to provide numeric value of service which was called.

Optionally, additional mechanisms can be implemented in the port, such as:

mechanism to handle certain types of program faults. This handler might be useful in determining if the root cause of the fault was a memory protection violation and to send the signal to kill the failing thread.

mechanism to defer thread switch. CMRX is written in a way, that thread switch is expected to be executed after the system call / interrupt service handler or kernel callback has finished its run. If target architecture doesn't support this behavior, this mechanism does not need to be implemented.

Functions

void os_memory_protection_start ()
 Start memory protection.
 
int mpu_init_stack (int thread_id)
 Initialize MPU for stack of thread.
 
int mpu_restore (const MPU_State *hosted_state, const MPU_State *parent_state)
 Load MPU settings.
 
int os_rpc_call (uint32_t arg0, uint32_t arg1, uint32_t arg2, uint32_t arg3)
 Kernel implementation of rpc_call syscall.
 
int os_rpc_return (uint32_t arg0, uint32_t arg1, uint32_t arg2, uint32_t arg3)
 Kernel implementation of rpc_return syscall.
 
unsigned static_init_thread_count ()
 Provides count of statically initialized threads.
 
const struct OS_thread_create_tstatic_init_thread_table ()
 Provides address of statically initialized thread table.
 
unsigned static_init_process_count ()
 Provides count of statically initialized processes.
 
const struct OS_process_definition_tstatic_init_process_table ()
 Provides address of statically intialized process table.
 
uint32_t * os_thread_populate_stack (int stack_id, unsigned stack_size, entrypoint_t *entrypoint, void *data)
 Populate stack of new thread so it can be executed.
 
bool schedule_context_switch (uint32_t current_task, uint32_t next_task)
 Schedule context switch on next suitable moment.
 
int os_process_create (Process_t process_id, const struct OS_process_definition_t *definition)
 Create process using process definition.
 
void os_boot_thread (Thread_t boot_thread)
 Start executing thread.
 

Function Documentation

◆ mpu_init_stack()

int mpu_init_stack ( int  thread_id)

Initialize MPU for stack of thread.

Performs initialization of the MPU to enable the given thread to use the stack.

Parameters
thread_idThread stack has to be initialized for

◆ mpu_restore()

int mpu_restore ( const MPU_State *  hosted_state,
const MPU_State *  parent_state 
)

Load MPU settings.

Loads MPU settings for default amount of regions from off-CPU buffer. This is suitable for store-resume during task switching.

Parameters
stateMPU state buffer

◆ os_boot_thread()

void os_boot_thread ( Thread_t  boot_thread)

Start executing thread.

Used to actually start executing in thread mode just after the kernel has been initialized and is ready to start the first thread. This function has to perform CPU switch from privileged mode in which kernel runs into unprivileged mode in which threads are supposed to run. Thread passed to this function is in state ready to be executed by normal kernel thread switching mechanism on this platform.

Parameters
boot_threadID of thread that shall be started

◆ os_memory_protection_start()

void os_memory_protection_start ( )

Start memory protection.

Initialize hardware memory protection unit so that following conditions are met:

  • RAM is not executable
  • kernel can execute all the flash and read/write all the RAM
  • FLASH can optionally be executable from userspace, if hardware is not capable enough to allow for fine-grained execution access. Kernel must be able to continue execution past this point.

◆ os_process_create()

int os_process_create ( Process_t  process_id,
const struct OS_process_definition_t definition 
)

Create process using process definition.

Takes process definition and initializes MPU regions for process out of it.

Parameters
process_idID of process to be initialized
definitionprocess definition. This is constructed at compile time using OS_APPLICATION macros
Returns
E_OK if process was contructed properly, E_INVALID if process ID is already used or if process definition contains invalid section boundaries. E_OUT_OF_RANGE is returned if process ID requested is out of limits given by the size of process table.

◆ os_rpc_call()

int os_rpc_call ( uint32_t  arg0,
uint32_t  arg1,
uint32_t  arg2,
uint32_t  arg3 
)

Kernel implementation of rpc_call syscall.

This syscall has to validate the RPC service and method IDs, determine the address of RPC method and owning process. Then it has to transfer the control to RPC method in a manner that:

  • method called will be able to access the first four arguments given to the rpc_call() call.
  • when method returns, the os_rpc_return() is triggered and will transfer the control back
  • to the calling code and process.

◆ os_rpc_return()

int os_rpc_return ( uint32_t  arg0,
uint32_t  arg1,
uint32_t  arg2,
uint32_t  arg3 
)

Kernel implementation of rpc_return syscall.

This syscall has to return the control back to the code which called rpc_call. This has to be done in a way that the calling code will be able to access the return value of the RPC method.

◆ os_thread_populate_stack()

uint32_t * os_thread_populate_stack ( int  stack_id,
unsigned  stack_size,
entrypoint_t entrypoint,
void *  data 
)

Populate stack of new thread so it can be executed.

Populates stack of new thread so that it can be executed with no other actions required. Returns the address where SP shall point to.

Parameters
stack_idID of stack to be populated
stack_sizesize of stack in 32-bit quantities
entrypointaddress of thread entrypoint function
dataaddress of data passed to the thread as its 1st argument
Returns
Address to which the SP shall be set.

◆ schedule_context_switch()

bool schedule_context_switch ( uint32_t  current_task,
uint32_t  next_task 
)

Schedule context switch on next suitable moment.

This function will tell scheduler, that we want to switch running tasks. Switch itself will be performed on next suitable moment by asynchronous routine. This mechanism is used to avoid context switch in the middle of interrupt routine, or otherwise complicated situation.

Parameters
current_taskID of thread which is currently being executed
next_taskID of thread which should start being executed
Returns
true if context switch will happen, false otherwise

Schedule context switch on next suitable moment.

Calling this function will prepare task switch. It will set up some stuff needed and then schedule PendSV.

Parameters
current_taskID of task, which is currently running
next_taskID of task, which should be scheduled next
Note
API of this function is dumb.

◆ static_init_process_count()

unsigned static_init_process_count ( )

Provides count of statically initialized processes.

Returns
amount of processes that have to be statically initialized

◆ static_init_process_table()

const struct OS_process_definition_t * static_init_process_table ( )

Provides address of statically intialized process table.

Returns
address of table containins details of statically initialized processes

◆ static_init_thread_count()

unsigned static_init_thread_count ( )

Provides count of statically initialized threads.

Returns
amount of threads that have to be statically initialized

◆ static_init_thread_table()

const struct OS_thread_create_t * static_init_thread_table ( )

Provides address of statically initialized thread table.

Returns
address of table containing details of statically initialized threads