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

Description of CMRX porting layer. More...

Data Structures

struct  Arch_State_t
 ARM-specific architecture state of a thread. More...
 

Functions

void os_ipi_sync_request ()
 Request Inter-processor synchronization.
 
void os_ipi_sync_release ()
 Release cores waiting in inter-processor synchronization loop.
 
void os_ipi_sync_probe ()
 Check if inter-processor synchronization is requested.
 
void os_request_context_switch (bool activate)
 Request context switch.
 
unsigned coreid ()
 Provide kernel with identification of current CPU core.
 
void os_smp_lock ()
 Lock the "big kernel lock".
 
void os_smp_unlock ()
 Unlock the "big kernel lock".
 
void os_core_lock ()
 Lock the current core This function ensures that no other code will interrupt the currently running core.
 
void os_core_unlock ()
 Unlock the current core Unlocks exclusive access to the current core.
 
void os_core_sleep ()
 Park current core This function is free to park current core in whatever way that will still allow the core to be woken up by external interrupts.
 
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.
 
void os_memory_protection_stop ()
 Disable memory protection.
 
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.
 
void os_thread_initialize_arch (struct OS_thread_t *thread)
 Finalize architecture-specific initialization of thread.
 
void os_init_arch (void)
 Perform architecture-specific initialization during kernel startup.
 
void os_init_core (unsigned core_id)
 Perform architecture-specific initialization of core during boot.
 
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.
 

Detailed Description

Description of CMRX porting layer.

CMRX offers primitives to perform unaddressed synchronization unlike kill() call, which allows thread to send a signal to another thread, API in this group uses memory location as a designator, rather than specific thread.

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 update the target os to include sources located in the architecture support directory. This way the platform-independent portion will be extended by the necessary platform support. If platform support requires linking of any additional libraries, such as HAL, then commands to let the os target link them should be present in CMakeLists.txt in this directory as well.

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.

Using this API, a programmer can write code that notifies unknown recipient. All that is needed for notifier and waiter for this mechanism to work is to agree upon memory location used as notification object.

Any object can be used as the object itself is not examined or used during the notification. It is just a convenient way on determining the notification ID.

It allows multiple object to wait for single object and thus can serve as a building block for more advanced primitives such as semaphores or queues.

Function Documentation

◆ coreid()

unsigned coreid ( )

Provide kernel with identification of current CPU core.

Kernel calls this function if it is configured for SMP. This function is part of the porting layer but does not need to be implemented in port. If the architecture does not provide unified way of determining which core is the one currently executing this code, then the implementation of this method may be left to the user.

Returns
ID of the currently running code

◆ 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
hosted_stateMPU state buffer for the current host process
parent_stateMPU state buffer for the parent process

◆ 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_core_lock()

void os_core_lock ( )

Lock the current core This function ensures that no other code will interrupt the currently running core.

It usually boils down to disabling interrupts.

Note
Calling this function does NOT prevent another core from running code which will manipulate kernel data structures. If you want this behavior, use os_smp_lock().

◆ os_core_sleep()

void os_core_sleep ( )

Park current core This function is free to park current core in whatever way that will still allow the core to be woken up by external interrupts.

◆ os_core_unlock()

void os_core_unlock ( )

Unlock the current core Unlocks exclusive access to the current core.

Usually it boils down to enabling interrupts.

◆ os_init_arch()

void os_init_arch ( void  )
extern

Perform architecture-specific initialization during kernel startup.

This can be used to perform system-wide initialization that has to be done once per whole system. Kernel guarantees that this function will be called exactly once

◆ os_init_core()

void os_init_core ( unsigned  core_id)
extern

Perform architecture-specific initialization of core during boot.

This can be used to perform architecture specific initialization of CPU core before kernel starts executing on the core. Note that you shall not use this function to initialize memory protection unless some very specific steps have to be taken as there is API to initialize MPU elsewhere in the porting layer. This function will be called once per core managed by the kernel before kernel starts initialization of the kernel structures for that core.

◆ os_ipi_sync_probe()

void os_ipi_sync_probe ( )

Check if inter-processor synchronization is requested.

Each core should call this function from time to time. If any other core calls for inter-processor synchronization, then core calling this function will be blocked in the call until the core which requested the synchronization doesn't release it.

◆ os_ipi_sync_release()

void os_ipi_sync_release ( )

Release cores waiting in inter-processor synchronization loop.

This function will release all cores which are waiting in os_ipi_sync_probe() function waiting for core which requested synchronization to release it. Failure to call this function after os_ipi_sync_request() was called will result in permanent halt of all other system cores.

◆ os_ipi_sync_request()

void os_ipi_sync_request ( )

Request Inter-processor synchronization.

This is minimalistic implementation for inter-processor synchronization request for Cortex-M processors. This implementation does not assume presence of any inter-processor interrupt mechanism. It assumes that each core checks for inter-processor sync every now and then. This function will block until all online cores call os_ipi_sync_probe(). By doing so all cores remain synchronized and it is guarranteed they will do nothing. Then this function returns.

◆ os_kernel_shutdown()

void os_kernel_shutdown ( )

Stop running the kernel.

Return to bare metal execution mode similar to one after CPU reset. This function should configure the CPU to continue execution in privileged mode not distinguishing between thread and kernel space. Once this mode is configured the function cmrx_shutdown_handler() should be executed.

This is a point of no return. Code here is free to destroy any previous context.

Stop running the kernel.

This is platform-specific way of how to shutdown the kernel. In this case an interrupt frame is forged on stack that will resemble a frame returning back to the cmrx_shutdown_handler function. The result of running this function will be that the processor leaves the handler mode, enters privileged thread mode and will be using MSP.

◆ 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_memory_protection_stop()

void os_memory_protection_stop ( )

Disable memory protection.

Disables memory protection unit so that no rules are enforced by the hardware. The CPU state after this call should resemble MPU state after reset.

◆ 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_request_context_switch()

void os_request_context_switch ( bool  activate)
inline

Request context switch.

This function is called by the platform independent part of the kernel when cpu_context structure is filled with valid data and context switch shall happen. The implementation of this function should configure the hardware in a way that context switch will happen as soon as kernel finishes its work and is ready to return the CPU back to the userspace code.

◆ os_reset_cpu()

void os_reset_cpu ( )

Reset the CPU.

This is architecture- (and possibly HAL-) specific way to reset the CPU. This function can be used before the kernel has been started or after it has been shut down. If there is no shutdown handler provided by the integrator then the default handler will call this function to reset the CPU automatically.

◆ 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_set_syscall_return_value()

int os_set_syscall_return_value ( Thread_t  thread_id,
int32_t  retval 
)

Set return value of syscall for given thread.

Parameters
thread_idId of the thread whose exception handler is being modified
retvalvalue to store on the stack

◆ os_smp_lock()

void os_smp_lock ( )

Lock the "big kernel lock".

This function starts the critical section within kernel. Actions that happen within critical section can ever only happen on one CPU core and other cores must not motify the same data. Big kernel lock is used to lock thread table, stack allocation table and sleepers table.

This function is part of porting layer but depends heavily on target CPU. It may be left to be implemented by the user. If this function is called, the code should make sure that if this function is called on any other core before os_smp_unlock() is called that another call won't proceed.

◆ os_smp_unlock()

void os_smp_unlock ( )

Unlock the "big kernel lock".

This function ends the critical setion within kernel.

This function is part of the porting layer but depends heavily on target CPU. It may be left the be implemented by the user. Once this function is called the code must make sure that any other core potentially being blocked by parallel call to os_smp_lock() will be unlocked and continue execution.

◆ os_thread_initialize_arch()

void os_thread_initialize_arch ( struct OS_thread_t thread)
extern

Finalize architecture-specific initialization of thread.

This routine is called when thread has been created. It has opportunity to perform architecture-specific default-initialization of the thread.

Note
To save some CPU cycles, you can define this function as an empty macro if it is not used. In that case the Arch_State_t should be an empty structure.

◆ 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.

◆ 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