Implementing a Simple FreeRTOS: Core Code Overview

Introduction:

In embedded system development, Real-Time Operating Systems (RTOS) play a crucial role in effectively managing the execution of multiple tasks, enhancing system concurrency and response speed. FreeRTOS, as a widely used open-source RTOS, has core design concepts and implementation mechanisms that are worth in-depth study and learning. To better understand how an RTOS works, I decided to mimic the core code of FreeRTOS and implement a simplified version of an RTOS. The main differences between this simplified RTOS and the standard FreeRTOS are:

  1. No priority/suspension: All tasks have the same priority and use a simple round-robin scheduling strategy.

  2. Only supports time-slice round-robin scheduling: Does not support other complex scheduling algorithms, such as priority preemptive scheduling.

This article will detail the implementation process of this simplified RTOS, from task creation, scheduler design to exception handling, gradually analyzing its core code and design ideas. The next article (the second half) will cover some driver functions for the peripherals of this operating system.

Next, I will write out a simple FreeRTOS step by step.Table of Contents:

1. Task Breakdown

  1. OS Initialization and Interrupt Management

  2. Task Creation and Startup

  3. Task Round-Robin Scheduling

  4. Exception Handling

2. Detailed Analysis of the Implementation Ideas of the Simple OS

(1) OS Initialization and Interrupt Management
  1. Vector Table Initialization (start.s)

  • Reset Handler (Reset_Handler)

  • Exception Handling Interrupt Design

  • (2) Task Creation and Startup
    1. Task Creation (task.c)

    2. Task Startup (start.s)

    (3) Task Round-Robin Scheduling
    1. SysTick Interrupt Service Function (SysTick_Handler_asm)

    2. Scheduler Implementation (systick.c)

    (4) Exception Handling
    1. HardFault_Handler

    2. UsageFault_Handler

    3. SVC_Handler

    4. UsageFaultInit

    3. Implementation of the OS Main Function

    1. Stack Space Definition and Alignment (stack_a-d)

    2. Task Function Definition

  • Task Creation and Startup

  • 4. Summary of the ArticleThis article implements a simplified version of an RTOS based on time-slice round-robin scheduling, completing hardware initialization and interrupt vector table setup through start.s, initializing task stacks with create_task and simulating register states, and using SysTick interrupts to trigger task switching, achieving concurrent execution of multiple tasks.1. Task BreakdownBased on the previous articles and the two differences mentioned above, we can plan out several steps to achieve our goal:1. OS Initialization and Interrupt Management2. Task Creation and Startup3. Task Round-Robin Scheduling4. Exception HandlingThese four tasks are interlinked and implementing them will help us achieve our goal. Below, I will analyze these four steps in detail with my code.2. Detailed Analysis of the Implementation Ideas of the Simple OS1. OS Initialization and Interrupt ManagementI set up a start.s file as theprocessor startup file, written in assembly language, responsible forinitializing the hardware environment & setting the interrupt vector table andbooting the system into the main C program (such as <span><span>main()</span></span>).(1) Vector Table Initialization (start.s)

    ; Vector Table Mapped to Address 0 at Reset                AREA    RESET, DATA, READONLY                EXPORT  __Vectors                EXPORT StartTask_asm                IMPORT HardFault_Handler                IMPORT UsageFault_Handler                IMPORT SVC_Handler                IMPORT SysTick_Handler__Vectors       DCD     0                                  DCD     Reset_Handler              ; Reset Handler                DCD     0                ; NMI Handler                DCD     HardFault_Handler          ; Hard Fault Handler                DCD     0          ; MPU Fault Handler                DCD     0           ; Bus Fault Handler                DCD     UsageFault_Handler_asm         ; Usage Fault Handler                DCD     0                          ; Reserved                DCD     0                          ; Reserved                DCD     0                          ; Reserved                DCD     0                          ; Reserved                DCD     SVC_Handler                ; SVCall Handler                DCD     0           ; Debug Monitor Handler                DCD     0                          ; Reserved                DCD     0             ; PendSV Handler                DCD     SysTick_Handler_asm            ; SysTick Handler
    • Defines the vector table mapped to address 0 at reset

    • Each DCD entry corresponds to the address of an exception handler

    • Includes important interrupt vectors such as reset, hard fault, system call (SVC), SysTick, etc.

    In the last two lines, we can see the previously mentioned PendSV and SysTick interrupts. In this task, since only time-slice round-robin scheduling is supported, there is no need for a separate PendSV. We directly implement task scheduling inSysTick.

    (2) Reset Handler (start.s)

    Reset_Handler PROC     LDR SP, =(0x20000000+0x10000)  ; Initialize stack pointer to the end of RAM     BL SystemInit                  ; Initialize system clock, etc.     BL uart_init                   ; Initialize UART     BL UsageFaultInit              ; Initialize usage fault handling     BL SysTickInit                 ; Initialize system timer     BL LedInit                     ; Initialize LED     BLX mymain                     ; Jump to main program ENDP

    This interrupt is triggered at system startup to initialize some hardware requirements

    • It is the first code executed after system startup

    • Initializes the stack pointer, hardware peripherals, and system components

    • Finally jumps to the C language main function

    Exception Handling Interrupt Design (start.s)

    UsageFault_Handler_asm PROCMOV R0, SPB UsageFault_HandlerENDP

    This interrupt is called during system exceptions (usage faults, not hardware), and it will pass the stack pointer to the handling function

    • Passes the current stack pointer to the C language handling function

    • Used for debugging and error diagnosis

    Its handling function (in the exception.c exception handling module)

    void UsageFault_Handler(unsigned int *stack){    SCB_Type * SCB = (SCB_Type *)SCB_BASE_ADDR;puts("UsageFault_Handler\n\r");put_s_hex("R0  = ", stack[0]);put_s_hex("R1  = ", stack[1]);put_s_hex("R2  = ", stack[2]);put_s_hex("R3  = ", stack[3]);put_s_hex("R12 = ", stack[4]);put_s_hex("LR  = ", stack[5]);put_s_hex("PC  = ", stack[6]);put_s_hex("xPSR= ", stack[7]);/* 1. clear usage fault */    SCB->CFSR = SCB->CFSR;/* 2. set return address in sp, point to next instruction */    stack[6] += 4;}

    This function will find the location of the error stack, output the values of all registers for user judgment, and move the stack pointer to execute the next instruction.2. Task Creation and StartupThis step mainly relies on a large number of task operation functions in the task module and the StartTask_asm task startup function in start.s.(1) Task Creation (task.c)

    void create_task(task_function f, void *param, char *stack, int stack_len){int* top=(int*)(stack+stack_len);    top-=16;//The following are R4-11 (software part)    top[0]=0;    top[1]=0;    top[2]=0;    top[3]=0;    top[4]=0;    top[5]=0;    top[6]=0;    top[7]=0;    top[9]=0;//R0 saves entry parameters    top[8]=(int) param;//R1,2,3,12    top[10]=0;    top[11]=0;    top[12]=0;//lr currently has no content    top[13]=0;    top[14]=(int)f;//14 is the pointer to the execution function, executed with r0 parameters    top[15]=(1<<24);//Instruction set setting    Task_stack_addr[task_count++] = (int)top;//Address of the stack}

    Fakes the stack saved by the task, allocates a block of memory from the stack (according to user requirements malloc, at least 16), fills in the values of 16 registers in order, so that it can be restored to the CPU and run normally.

    This table explains the role of each register, the order of saving, and the saving method

    Register Save List (Stack Offset Order)

    Stack Offset (<span>top[N]</span>)

    Register

    Saving Method

    Usage Description

    <span>top[0]</span>

    R4

    Manually initialized (=0)

    Caller-saved register, meaningless when the task runs for the first time, initialized to 0.

    <span>top[1]</span>

    R5

    Manually initialized (=0)

    Same as above.

    <span>top[2]</span>

    R6

    Manually initialized (=0)

    Same as above.

    <span>top[3]</span>

    R7

    Manually initialized (=0)

    Same as above.

    <span>top[4]</span>

    R8

    Manually initialized (=0)

    Same as above.

    <span>top[5]</span>

    R9

    Manually initialized (=0)

    Same as above.

    <span>top[6]</span>

    R10

    Manually initialized (=0)

    Same as above.

    <span>top[7]</span>

    R11

    Manually initialized (=0)

    Same as above.

    <span>top[8]</span>

    R0

    Set to <span>param</span>

    Task parameter passing stores <span>void *param</span> for use by the task function.

    <span>top[9]</span>

    R12

    Manually initialized (=0)

    Temporary register, meaningless when the task runs for the first time.

    <span>top[10]</span>

    R1

    Manually initialized (=0)

    Temporary register, initialized to 0.

    <span>top[11]</span>

    R2

    Manually initialized (=0)

    Temporary register, initialized to 0.

    <span>top[12]</span>

    R3

    Manually initialized (=0)

    Temporary register, initialized to 0.

    <span>top[13]</span>

    LR

    Manually initialized (=0)

    Link Register, the task does not return, so set to 0.

    <span>top[14]</span>

    PC

    Set to <span>f</span> (task entry)

    Program Counter points to the task function <span>task_function f</span>, determining the first instruction executed after the task starts.

    <span>top[15]</span>

    xPSR

    Set to <span>(1 << 24)</span>

    Program Status Register, Bit 24=1 indicates Thumb mode (required for Cortex-M).

    (2) Start the Created Task (start.c)

    StartTask_asm PROC; Task starter: gives the stack position and return method to start the task in the stack    ;r0: stack address of task    ;r1: lr (method of executing start/return task)    LDMIA R0!, {R4-R11}    ;other r's are stored by hardware    MSR MSP, R0    ;Msp is the point of task stack    BX R1    ;return to this task(lr: return way ;msp: return place)    ENDP
    • Restores registers R4-11 from the provided stack address (others are automatically restored)

    • Finds the stack pointer of this task and starts the task code contained in this stack

    Each task startup method is quite similar.3. Task Round-Robin SchedulingMy operating system can only perform task switching based on time-slice round-robin, so the switching function is closely tied to theSysTick interrupt.

    static int Task_stack_addr[32];

    I defined an array containing the positions of all task stacks, corresponding to the task list in the previous source code, to facilitate quick calls to each task.(1) SysTick Interrupt Service Function (triggered on time-slice overflow)

    SysTick_Handler_asm PROC    STMDB sp!, {r4-r11}    STMDB sp!, {lr}    ;save rg+lr of this task    MOV   R0,LR    ADD R1, SP, #4 ; stack top (r1+4 because we saved lr)    BL SysTick_Handler ;c function to change    LDMIA sp!, { r0 }    LDMIA sp!, { r4 - r11 }    ;return rg+lr of this task    BX R0 ;run by lr(in r0) and sp    ENDP    END

    Save the context

        STMDB sp!, {r4-r11}    STMDB sp!, {lr}

    Saves registers R4-R11 to the stack, while others are automatically saved.

    Saves lr (return method) for preparation to return.

    Calls the switching function with two parameters, lr and sp

    MOV   R0,LR    ADD R1, SP, #4 ; stack top (r1+4 because we saved lr)    BL SysTick_Handler ;c function to change

    Switching function (systick.c)

    void SysTick_Handler(int lr_bak,int old_sp){int stack;int old_task;int new_task;    SCB_Type * SCB = (SCB_Type *)SCB_BASE_ADDR;    SCB->ICSR |= SCB_ICSR_PENDSTCLR_Msk; //clear interruptif(!is_task_running())//if no task created    {return; //return will directly return to source task}if(cur_task==-1)//whether tasks have been started (there are tasks that have not been started)    {        cur_task=0;        stack=get_stack(cur_task);//get task addressStartTask_asm(stack, lr_bak);//start taskreturn;}else    {        old_task=cur_task;        new_task=get_next_task();if(old_task!=new_task)        {//first save old task (we need to save to the return table, note: the address in the return table needs to be updated continuously)set_task_stack(old_task, old_sp);//switch task            stack = get_stack(new_task);            cur_task=new_task;//update task pointerStartTask_asm(stack,lr_bak);        }    }}

    First clear the interrupt flag, then store the old stack pointer in the linked list, extract the next stack pointer from the linked list, and execute the next task based on lr.

    In addition to these core functions, there are many other task handling functions

    int cur_task=-1;//Currently running program numberint old_task = -1;   // Initialize to invalid valueint task_count=0;// Program countint task_running = 0;//running stateint is_task_running(void)//get running state{return task_running;}int get_stack(int cur_task)//get task address{return Task_stack_addr[cur_task];}int get_next_task(void)//increment to process the next task{int index = cur_task;    index++;if (index >= task_count)    index = 0;return index;}void set_task_stack(int task, int sp)//save for{    Task_stack_addr[task] = sp;}void start_task(void){    task_running = 1;while (1);}

    These functions assist the core switching function above. After initializing a task, we just need to use task_running to free up the timer. SysTick will allow all tasks to run.

    Implementing a Simple FreeRTOS: Core Code Overview

    This image illustrates how tasks begin to run, with subsequent tasks being scheduled by the SysTick scheduler.

    4. Exception Handling (exception.c)

    #include "uart.h"#include "exception.h"void HardFault_Handler(void){puts("HardFault_Handler\n\r");while (1);}void UsageFault_Handler(unsigned int *stack){    SCB_Type * SCB = (SCB_Type *)SCB_BASE_ADDR;puts("UsageFault_Handler\n\r");put_s_hex("R0  = ", stack[0]);put_s_hex("R1  = ", stack[1]);put_s_hex("R2  = ", stack[2]);put_s_hex("R3  = ", stack[3]);put_s_hex("R12 = ", stack[4]);put_s_hex("LR  = ", stack[5]);put_s_hex("PC  = ", stack[6]);put_s_hex("xPSR= ", stack[7]);/* 1. clear usage fault */    SCB->CFSR = SCB->CFSR;/* 2. set return address in sp, point to next instruction */    stack[6] += 4;}void SVC_Handler(void){puts("SVC_Handler\n\r");}void UsageFaultInit(void){    SCB_Type * SCB = (SCB_Type *)SCB_BASE_ADDR;    SCB->SHCSR |= (SCB_SHCSR_USGFAULTENA_Msk);}

    This program is for ARM Cortex-M processors exception handling code, mainly handling HardFault (hard error), UsageFault (usage error) and SVC (system call) three types of exceptions.

    (1) HardFault_Handler(void)

    Triggered when a hardware error occurs, including illegal memory access (such as accessing unmapped addresses), stack overflow (SP pointing to illegal areas), and other serious hardware errors, prints error information, and enters an infinite loop (requires external reset).

    (2) UsageFault_Handler

    This usage fault was mentioned earlier, generally triggered by unaligned memory access (such as non-4-byte aligned <span><span>int</span></span> read/write), executing undefined instructions (such as illegal opcodes), and division by zero errors (requires enabling the <span><span>DIV_0_TRP</span></span> bit):

    1. Print register status:

      Accesses the registers pushed onto the stack during the exception (<span><span>R0-R3, R12, LR, PC, xPSR</span></span>).

    2. Clear error flags:

      Writes to <span><span>SCB->CFSR</span></span> (Configurable Fault Status Register) to clear the error status.

    3. Skip erroneous instructions:

      Modifies the <span><span>PC</span></span> in the stack (<span><span>stack[6]</span></span>), pointing it to the next instruction (<span><span>+4</span></span> applies to most Thumb instructions).

    (3) SVC_Handler

    void SVC_Handler (void) {     puts("SVC_Handler\n\r"); }

    Only prints debugging information; in actual use, the SVC number needs to be parsed and the corresponding operation executed.(4) UsageFaultInitEnables the UsageFault exception (<span>SCB->SHCSR</span> (System Handler Control and State Register) USGFAULTENA bit)The above four parts are the core code of the simple operating system. Now, we only need a main function to create and run tasks.3. OS Main Function

    #include "uart.h"#include "string.h"#include "task.h"static char stack_a[1024] __attribute__ ((aligned (4)));;static char stack_b[1024] __attribute__ ((aligned (4)));;static char stack_c[1024] __attribute__ ((aligned (4)));;static char stack_d[1024] __attribute__ ((aligned (4)));;void task_a(void *param){char c = 'e';while (1)    {putchar(c);}}void task_b(void *param){char c=(char)param;while(1)    {putchar(c);}}void task_c(void *param){char c=(char)param;while(1)    {putchar(c);}}void task_d(void *param){int i;int sum = 0;for (i = 0; i <= 100; i++)        sum += i;while (1)    {put_s_hex("sum = ", sum);}}int mymain(){create_task(task_a, 'b', stack_a, 1024);create_task(task_b, 'a', stack_b, 1024);create_task(task_c, 'c', stack_c, 1024);create_task(task_d, '0', stack_d, 1024);start_task();return 0;}

    (1) Request 1024 size stack space aligned to four bytes

    staticchar stack_a[1024] __attribute__ ((aligned (4)));;staticchar stack_b[1024] __attribute__ ((aligned (4)));;staticchar stack_c[1024] __attribute__ ((aligned (4)));;staticchar stack_d[1024] __attribute__ ((aligned (4)));;

    (2) Create task processing functions

    voidtask_a(void *param){char c = 'e';while (1){putchar(c);}}

    (3) Create tasks for the stack and task processing functions

    create_task(task_a, 'b', stack_a, 1024);

    (4) Hand over time to the scheduler

    start_task();

    In summary, the main function, as the entry point of the program, is responsible for initializing the multi-task environment and starting the task scheduler. The core logic is:

    1. Create 4 tasks: Allocate stack space and set task entry functions.

    2. Start the scheduler: Trigger task switching through <span><span>start_task()</span></span> to enable concurrent execution of tasks.

    4. Summary of the Article

    This article provides a detailed overview of the implementation process of a simplified RTOS, from OS initialization, task creation and startup, task round-robin scheduling to exception handling, gradually analyzing the design ideas of the core code. Although this simplified RTOS has basic functionality, it covers the core mechanisms of an RTOS, such as task scheduling, context switching, and exception handling. The second half of this article will initialize some external drivers corresponding to this operating system.

    Leave a Comment