RT-Thread RTOS
An open source embedded real-time operating system
|
After learning the previous chapters, everyone has a better understanding of RT-Thread, but many people are not familiar with how to port the RT-Thread kernel to different hardware platforms. Kernel porting refers to the RT-Thread kernel running on different chip architectures and different boards. It 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 CPU architecture porting and BSP porting. The CPU architecture porting part will be introduced in conjunction with the Cortex-M CPU architecture. Therefore, it is necessary to review "Cortex-M CPU Architecture Foundation" in the previous chapter Interrupt Management. After reading this chapter, how to complete the RT-Thread kernel porting will be learned.
There are many different CPU architectures in the embedded world, for example, Cortex-M, ARM920T, MIPS32, RISC-V, etc. In order to enable RT-Thread to run on different CPU architecture chips, RT-Thread provides a libcpu abstraction layer to adapt to different CPU architectures. The libcpu layer provides unified interfaces to the kernel, including global interrupt switches, thread stack initialization, context switching, and more.
RT-Thread's libcpu abstraction layer provides a unified set of CPU architecture porting interfaces downwards. This part of the interface includes global interrupt switch functions, thread context switch functions, clock beat configuration and interrupt functions, Cache, and so on. The following table shows the interfaces and variables that the CPU architecture migration needs to implement.
libcpu porting related API
Functions and Variables | Description |
---|---|
rt_base_t rt_hw_interrupt_disable(void); | disable global interrupt |
void rt_hw_interrupt_enable(rt_base_t level); | enable global interrupt |
rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter, rt_uint8_t *stack_addr, void *texit); | The initialization of the thread stack, the kernel will call this function during thread creation and thread initialization. |
void rt_hw_context_switch_to(rt_uint32 to); | Context switch without source thread, which is called when the scheduler starts the first thread, and is called in the signal. |
void rt_hw_context_switch(rt_uint32 from, rt_uint32 to); | Switch from from thread to to thread, used for switch between threads. |
void rt_hw_context_switch_interrupt(rt_uint32 from, rt_uint32 to); | Switch from from thread to to thread, used for switch in interrupt. |
rt_uint32_t rt_thread_switch_interrupt_flag; | A flag indicating that a switch is needed in the interrupt. |
rt_uint32_t rt_interrupt_from_thread, rt_interrupt_to_thread; | Used to save from and to threads when the thread is context switching. |
Regardless of 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 mechanism. But these mechanisms require the global interrupt enable/disable function provided in libcpu. They are, respectively:
The following describes how to implement these two functions on the Cortex-M architecture. As mentioned earlier, the Cortex-M implements the CPS instruction in order to achieve fast switch interrupts, which can be used here.
The functions that need to be done in order 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 the global interrupt.
Based on MDK, the global interrupt disabled function on the Cortex-M core, is shown in the following code:
Disable global interrupt
The above code first uses the MRS instruction to save the value of the PRIMASK register to the r0 register, then disable the global interrupt with the "CPSID I" instruction, and finally returns with the BX instruction. The data stored by r0 is the return value of the function. Interrupts can occur between the “MRS r0, PRIMASK” instruction and “CPSID I”, which does not cause a global interrupt status disorder.
There are different conventions for different CPU architectures regarding how registers are managed during function calls and in interrupt handlers. A more detailed introduction to the use of registers for Cortex-M can be found in the official ARM 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, overriding the global interrupt status of the chip.
Based on MDK, implementation on the Cortex-M core enables a global interrupt, as shown in the following code:
Enable global interrupt
The above code first uses the MSR instruction to write the value register of r0 to the PRIMASK register, thus restoring the previous interrupt status.
When dynamically creating threads 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(), which manually constructs a context in the stack initialization function. The context will be used as the initial value for each thread's first execution. The layout of the context on the stack is shown below:
The following code is the stack initialization code:
Build a context on the stack
In different CPU architectures, context switches between threads and context switches from interrupts to context, the register portion of the context may be different or the same. In Cortex-M, context switching is done uniformly using PendSV exceptions, and there is no difference in the switching parts. However, in order to adapt to different CPU architectures, RT-Thread's libcpu abstraction layer still needs to implement three thread switching related functions:
1) rt_hw_context_switch_to(): no source thread, switching to the target thread, which is called when the scheduler starts the first thread.
2) rt_hw_context_switch():In a threaded environment, switch from the current thread to the target thread.
3) rt_hw_context_switch_interrupt ():In the interrupt environment, switch from the current thread to the target thread.
There are differences between switching in a threaded environment and switching in an interrupt environment. In the threaded environment, if the rt_hw_context_switch() function is called, the context switch can be performed immediately; in the interrupt environment, it needs to wait for the interrupt handler to complete processing the functions before switching.
Due to this difference, the implementation of rt_hw_context_switch() and rt_hw_context_switch_interrupt() is not the same on platforms such as ARM9. If the thread's schedule is triggered in the interrupt handler, rt_hw_context_switch_interrupt() is called in the dispatch function to trigger the context switch. After the interrupt handler has processed the interrupt, check the rt_thread_switch_interrupt_flag variable before the schedule exits. If the value of the variable is 1, the context switch of the thread 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 made more compact based on the features of automatic partial push and PendSV.
Context switching between threads, as shown in the following figure:
The hardware automatically saves the PSR, PC, LR, R12, R3-R0 registers of the from thread before entering the PendSV interrupt, then saves the R11~R4 registers of the from thread in PendSV, and restores the R4~R11 registers of the to thread, and finally the hardware automatically restores the R0~R3, R12, LR, PC, PSR registers of the to thread after exiting the PendSV interrupt.
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 a PendSV exception. R11~R4 registers of the from thread are saved and R4~R11 registers of the to thread are restored in the PendSV exception handler. Finally, the hardware automatically restores the R0~R3, R12, PSR, PC, LR registers of the to thread after exiting the PendSV interrupt.
Obviously, in the Cortex-M kernel, the rt_hw_context_switch() and rt_hw_context_switch_interrupt() have same functions, which is finishing saving and replying the remaining contexts in PendSV. So we just need to implement a piece of code to simplify the porting.
rt_hw_context_switch_to() has only the target thread and no source thread. This function implements the function of switching to the specified thread. The following figure is a flowchart:
The rt_hw_context_switch_to() implementation on the Cortex-M3 kernel (based on MDK), as shown in the following code:
MDK version rt_hw_context_switch_to() implementation
The function rt_hw_context_switch() and the function rt_hw_context_switch_interrupt() have two parameters, the from thread and the to thread. They implement the function to switch from the from thread to the to thread. The following figure is a specific flow chart:
The rt_hw_context_switch() and rt_hw_context_switch_interrupt() implementations on the Cortex-M3 kernel (based on MDK) are shown in the following code:
Implement rt_hw_context_switch()/rt_hw_context_switch_interrupt()
In Cortex-M3, the PendSV interrupt handler is PendSV_Handler(). The actual thread switching is done in PendSV_Handler(). The following figure is a specific flow chart:
The following code is a PendSV_Handler implementation:
With the basics of switching global interrupts and context switching, RTOS can perform functions such as creating, running, and scheduling threads. With OS Tick, RT-Thread can schedule time-to-roll rotations for threads of the same priority, implementing timer functions, implementing rt_thread_delay() delay functions, and so on.
In oder to finish the porting of libcpu, we need to ensure that the rt_tick_increase() function is called periodically during the clock tick interrupt. The call cycle depends on the value of the RT_TICK_PER_SECOND macro of rtconfig.h.
In Cortex M, the OS tick can be implemented by SysTick's interrupt handler.
In a practical project, for the same CPU architecture, different boards may use the same CPU architecture, carry different peripheral resources, and complete different products, so we also need to adapt to the board. RT-Thread provides a BSP abstraction layer to fit boards commonly seen. If you want to use the RT-Thread kernel on a board, in addition to the need to have the corresponding chip architecture porting, you need to port the corresponding board, that is, implement a basic BSP. The job is to establish a basic environment for the operating system to run. The main tasks that need to be completed are:
1) Initialize the CPU internal registers and set the RAM operation timing.
2) Implement clock driver and interrupt controller driver and interrupt management.
3) Implement UART and GPIO driver.
4) Initialize the dynamic memory heap to implement dynamic heap memory management.