Kernel porting
After studying the kernel chapter, everyone has a good understanding of RT-Thread, but many people may not be familiar with how to port the RT-Thread kernel to different hardware platforms. Kernel porting refers to running the RT-Thread kernel on different chip architectures and different boards, and can have functions such as thread management and scheduling, memory management, inter-thread synchronization and communication, and timer management. Porting can be divided into two parts: CPU architecture porting and BSP (Board support package) porting.
This chapter will introduce the CPU architecture transplantation and BSP transplantation. The CPU architecture transplantation part will be introduced in conjunction with the Cortex-M CPU architecture, so it is necessary to review the "Cortex-M CPU Architecture Basics" introduced in the previous chapter "Interrupt Management" . At the end of this chapter, the complete process of RT-Thread kernel transplantation is demonstrated with an example of actual transplantation to a development board. After reading this chapter, we will understand how to complete the kernel transplantation of RT-Thread.
There are many different CPU architectures in the embedded field, such as Cortex-M, ARM920T, MIPS32, RISC-V, etc. In order to enable RT-Thread to run on chips with different CPU architectures, RT-Thread provides a libcpu abstraction layer to adapt to different CPU architectures. The libcpu layer provides a unified interface to the kernel, including the switch of global interrupts, initialization of thread stacks, context switching, etc.
RT-Thread's libcpu abstraction layer provides a unified set of CPU architecture porting interfaces, which include global interrupt switch functions, thread context switch functions, clock beat configuration and interrupt functions, Cache, etc. The following table lists the interfaces and variables that need to be implemented for CPU architecture porting.
libcpu porting related API
Functions and variables
describe
rt_base_t rt_hw_interrupt_disable(void);
Disable global interrupts
void rt_hw_interrupt_enable(rt_base_t level);
Enable global interrupts
rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter, rt_uint8_t *stack_addr, void *texit);
Initialization of the thread stack. The kernel calls this function during thread creation and thread initialization.
void rt_hw_context_switch_to(rt_uint32_t to);
There is no context switch of the source thread. It is called when the scheduler starts the first thread and in the signal.
void rt_hw_context_switch(rt_uint32_t from, rt_uint32_t to);
Switch from the from thread to the to thread, used to switch between threads
void rt_hw_context_switch_interrupt(rt_uint32_t from, rt_uint32_t to);
Switch from the from thread to the to thread, used when switching in the interrupt
rt_uint32_t rt_thread_switch_interrupt_flag;
A flag indicating that a switch is required in an interrupt
rt_uint32_t rt_interrupt_from_thread, rt_interrupt_to_thread;
When the thread context switches, it is used to save the from and to threads.
Whether in kernel code or user code, there may be some variables that need to be used in multiple threads or interrupts. If there is no corresponding protection mechanism, it may lead to critical section problems. In order to solve this problem, RT-Thread provides a series of inter-thread synchronization and communication mechanisms. However, these mechanisms all need to use the global interrupt switch function provided by libcpu. They are:
The following describes how to implement these two functions on the Cortex-M architecture. As mentioned earlier, Cortex-M implements the CPS instruction for fast switching interrupts, which can be used here.
The functions that need to be completed in the rt_hw_interrupt_disable() function are:
1). Save the current global interrupt status and use the status as the return value of the function.
2). Disable global interrupts.
Based on MDK, the global interrupt is disabled on the Cortex-M core, as shown in the following code:
Disable global interrupts
The above code first uses the MRS instruction to save the value of the PRIMASK register to the r0 register, then uses the "CPSID I" instruction to disable global interrupts, and finally uses the BX instruction to return. The data stored in r0 is the return value of the function. An interrupt can occur between the "MRS r0, PRIMASK" instruction and "CPSID I", which will not cause confusion in the global interrupt status.
Different CPU architectures have different conventions on how registers are managed during function calls and in interrupt handlers. A more detailed introduction to register usage conventions for Cortex-M can be found in the ARM official manual "Procedure Call Standard for the ARM ® Architecture".
In rt_hw_interrupt_enable(rt_base_t level), the variable level is used as the state to be restored, overwriting the global interrupt state of the chip.
Based on MDK, the implementation on the Cortex-M core enables global interrupts, as shown in the following code:
Enable global interrupts
The above code first uses the MSR instruction to write the value register r0 to the PRIMASK register, thereby restoring the previous interrupt status.
When dynamically creating and initializing threads, the internal thread initialization function _rt_thread_init() is used. The _rt_thread_init() function calls the stack initialization function rt_hw_stack_init(). In the stack initialization function, a context content is manually constructed. This context content will be used as the initial value of each thread's first execution. The layout of the context in the stack is shown in the following figure:
The following code is the code for stack initialization:
Building context in the stack
In different CPU architectures, the context switch between threads and the context switch from interrupt to thread may have different or the same context registers. In Cortex-M, context switching is done uniformly using the PendSV exception, and the switching part is the same. However, in order to adapt to different CPU architectures, the libcpu abstraction layer of RT-Thread still needs to implement three thread switching related functions:
1) rt_hw_context_switch_to(): There is no source thread, switch to the target thread, and is called when the scheduler starts the first thread.
2) rt_hw_context_switch(): In a thread environment, switches from the current thread to the target thread.
3) rt_hw_context_switch_interrupt (): In an interrupt environment, switch from the current thread to the target thread.
There is a difference between switching in a thread environment and switching in an interrupt environment. In a thread environment, if the rt_hw_context_switch() function is called, the context switch can be performed immediately; in an interrupt environment, the context switch can only be performed after the interrupt handling function is completed.
Due to this difference, the implementation of rt_hw_context_switch() and rt_hw_context_switch_interrupt() is different on platforms such as ARM9. If the thread scheduling is triggered in the interrupt handler, the scheduling function will call rt_hw_context_switch_interrupt() to trigger the context switch. After the interrupt transaction is processed in the interrupt handler, before the interrupt exits, the rt_thread_switch_interrupt_flag variable is checked. If the value of the variable is 1, the thread context switch is completed according to the rt_interrupt_from_thread variable and the rt_interrupt_to_thread variable.
In the Cortex-M processor architecture, context switching can be implemented more concisely based on the features of automatic partial stack push and PendSV.
The context switching between threads is shown in the following figure:
Before entering the PendSV interrupt, the hardware automatically saves the PSR, PC, LR, R12, R3-R0 registers of the from thread, then saves the R11~R4 registers of the from thread in PendSV, and restores the R4~R11 registers of the to thread. Finally, after exiting the PendSV interrupt, the hardware automatically restores the R0~R3, R12, LR, PC, and PSR registers of the to thread.
The context switch from interrupt to thread can be represented by the following figure:
The hardware automatically saves the PSR, PC, LR, R12, R3-R0 registers of the from thread before entering the interrupt, and then triggers the PendSV exception. In the PendSV exception handling function, the R11~R4 registers of the from thread are saved, and the R4~R11 registers of the to thread are restored. Finally, after exiting the PendSV interrupt, the hardware automatically restores the R0~R3, R12, PSR, PC, and LR registers of the to thread.
Obviously, in the Cortex-M kernel, rt_hw_context_switch() and rt_hw_context_switch_interrupt() have the same function, both of which complete the saving and restoring of the remaining context in PendSV. So we only need to implement one code to simplify the porting work.
Implementing rt_hw_context_switch_to()
rt_hw_context_switch_to() has only the target thread, but no source thread. This function implements the function of switching to the specified thread. The following figure is the flow chart:
The implementation of rt_hw_context_switch_to() on the Cortex-M3 core (based on MDK) is shown in the following code:
MDK version of rt_hw_context_switch_to() implementation
实现 rt_hw_context_switch()/ rt_hw_context_switch_interrupt()
Function rt_hw_context_switch() and function rt_hw_context_switch_interrupt() both have two parameters, from thread and to thread. They implement the function of switching from from thread to to thread. The following figure is the specific flow chart:
The implementation of rt_hw_context_switch() and rt_hw_context_switch_interrupt() on the Cortex-M3 core (based on MDK) is shown in the following code:
rt_hw_context_switch()/rt_hw_context_switch_interrupt() 实现
Implementing the PendSV interrupt
In Cortex-M3, the PendSV interrupt handler is PendSV_Handler(). The actual work of thread switching is completed in PendSV_Handler(). The following figure is a specific flow chart:
The following code is the implementation of PendSV_Handler:
With the foundation of global interrupt and context switching, RTOS can create, run, and schedule threads. With clock beat support, RT-Thread can schedule threads of the same priority using time slice rotation, implement timer functions, implement rt_thread_delay() delay function, and so on.
The work that needs to be done in the porting of libcpu is to ensure that the rt_tick_increase() function will be called periodically in the interruption of the clock beat. The calling period depends on the value of the macro RT_TICK_PER_SECOND in rtconfig.h.
In Cortex M, the clock beat function can be realized by implementing the SysTick interrupt processing function.
In actual projects, different boards may use the same CPU architecture, but carry different peripheral resources to complete different products, so we also need to adapt the boards. RT-Thread provides a BSP abstraction layer to adapt to common boards. If you want to use the RT-Thread kernel on a board, in addition to the corresponding chip architecture porting, you also need to port it to the board, that is, to implement a basic BSP. The main task is to establish a basic environment for the operating system to run. The main tasks that need to be completed are:
1) Initialize CPU internal registers and set RAM working timing.
2) Implement clock drive and interrupt controller drive, and improve interrupt management.
3) Implement serial port and GPIO drivers.
4) Initialize the dynamic memory heap and implement dynamic heap memory management.
Last updated