0-1write MC/OS __Basics1

2021-07-16 22:23:32 Friday

1, Bare metal system and multitasking system

Bare metal system

  • polling systems
    During bare metal programming, first initialize the relevant hardware, and then let the main program cycle continuously in a dead cycle to do all kinds of things sequentially, with poor real-time performance.
  • Front and rear console system
    Interrupt is added to the polling system. The response of external events is completed in the interrupt, and the event processing is still completed in the polling system. If the event to be processed is very short, it can be processed in the interrupt service program.
    The interrupt is called the foreground, and the infinite loop in the main function is called the background.
    The event response and processing are separated, but the event processing is executed sequentially in the background. Compared with the polling system, the front and rear systems ensure that the events will not be lost. In addition, the interrupt has the function of nesting, which greatly improves the real-time response ability of the program.

Multitasking system
Compared with the front and back systems, the event response of multi task system is also completed in interrupt, but the event processing is completed in task. In a multitasking system, tasks, like interrupts, also have priority, and tasks with high priority will be executed first.
When an emergency event is marked with an interrupt, if the task priority corresponding to the event is high enough, it will be responded immediately. Compared with the front and rear systems, the real-time performance of multi task system is improved.

What is a mission?
Compared with the program subjects executed in the background in the front and back system, in the multi task system, the program subjects are divided into independent, infinite loop and non returnable small programs according to the function of the program. This small program is called task.
Tasks are scheduled and managed by the operating system.

Differences among polling, front and back stations and multitasking systems

2021-07-20 01:01:16 Tuesday

2, Task definition and implementation of task switching

Learn how to create tasks and how to switch tasks (task switching is completed by assembly code)

1. Create task

1.1. Define task stack

When the system is running, how are global variables, local variables when sub functions are called, function return addresses when interrupts occur and other environmental parameters stored?

In the bare metal system, it is placed in the stack (the stack is a continuous memory space in the MCU RAM). The size of the stack is configured by the code in the startup file, and finally by the C library function_ main to initialize.
When the bare metal system needs to use the stack, you can find a free space in the stack, but the multitasking system can't.

In a multitasking system, each task is independent and does not interfere with each other, so an independent stack space should be allocated for each task. (stack space is usually a predefined global array) the largest stack that can be used is Stack_Size decision.
In a multitasking system, the task stack is to allocate independent rooms in a unified stack space, and each task can only use its own room.

The task stack is actually a predefined global data, and the data type is CPU_STK

static   CPU_STK   Task1Stk[TASK1_STK_SIZE];
static   CPU_STK   Task2Stk[TASK2_STK_SIZE];

Supplement:

volatile keyword

Volatile is meant to be "Volatile". Because accessing registers is much faster than accessing memory units, the compiler will generally optimize to reduce memory access, but it may read dirty data. When a variable value is required to be declared using volatile, the system always reads data from its memory again, even if its previous instruction has just read data from there. Specifically, when a variable declared by this keyword is encountered, the compiler will no longer optimize the code accessing the variable, so as to provide stable access to special addresses; If valatile is not used, the compiler optimizes the declared statement. (to put it succinctly, volatile keyword affects the compiler compilation results. The variable declared by volatile indicates that the variable may change at any time. For the operations related to the variable, do not compile and optimize to avoid errors)

1.2. Define task function

Task is an independent function. The function body loops indefinitely and cannot return.

1.3. Define task control block TCB

As mentioned earlier, the execution of tasks in a multitasking system is scheduled by the system. In order to schedule tasks smoothly, the system defines an additional Task control block (TCB) for each Task, which is equivalent to the ID card of the Task. It contains all the information of the Task, such as Task stack, Task name, Task parameters, etc.

All system operations on tasks can be realized through this TCB, which is a new data type.

/* Task control block data type declaration */
struct os_tcb {
    CPU_STK         *StkPtr;//Stack pointer
    CPU_STK_SIZE    StkSize;//Stack size
};
//Task TCB * * definition**
static OS_TCB Task1TCB;
static OS_TCB Task2TCB;

1.4. Implement task creation function

The task stack, the function entity of the task, and the TCB of the task finally need to be linked to be uniformly scheduled by the system.
This connection is implemented by the task creation function OSTaskCreate.

void OSTaskCreate ( OS_TCB        *p_tcb,//Task control block pointer
                    OS_TASK_PTR   p_task,//Task function name
                    void          *p_arg,//Task parameters, used to pass task parameters
                    CPU_STK       *p_stk_base, //Point to the starting address of the task stack
                    CPU_STK_SIZE  stk_size, //Indicates the size of the task stack
                    OS_ERR        *p_err) //Used to store error codes.
{
    CPU_STK       *p_sp;
//Task stack initialization function. When the task runs for the first time, the parameters loaded into the CPU register are placed in the task stack. When the task is created, the stack is initialized in advance.
    p_sp = OSTaskStkInit (p_task,//Task name, indicating the entry address of the task. Load to PC register R15 during task switching
                          p_arg,//Formal parameters of the task. Load to register R0 during task switching
                          p_stk_base,//Start address of task stack
                          stk_size);//Size of task stack
    p_tcb->StkPtr = p_sp;
    p_tcb->StkSize = stk_size;

    *p_err = OS_ERR_NONE;
}

After the task is created, it should be added to the ready list, indicating that the task is ready and the system can schedule it at any time.

typedef struct  os_rdy_list         OS_RDY_LIST;

struct os_rdy_list {
    OS_TCB        *HeadPtr;
    OS_TCB        *TailPtr;
};

OS_EXT OS_RDY_LIST OSRdyList[OS_CFG_PRIO_MAX];
/* Add task to ready list */
OSRdyList[0].HeadPtr = &Task1TCB;
OSRdyList[1].HeadPtr = &Task2TCB;

2. OS system initialization

OS system initialization is generally done after hardware initialization, as long as the defined global variables are initialized.

void OSInit (OS_ERR *p_err)
{
    OSRunning =  OS_STATE_OS_STOPPED;//Operation status of the system

    OSTCBCurPtr = (OS_TCB *)0;//The system pointer to the currently running TCB.
    OSTCBHighRdyPtr = (OS_TCB *)0;//Point to the TCB of the task with the highest priority in the ready task

    OS_RdyListInit();//Initialize the global variable OSRdyList [], that is, initialize the sequence table.

    *p_err = OS_ERR_NONE;//Running the code here means there are no errors.
}
void OS_RdyListInit(void)
{
    OS_PRIO i;
    OS_RDY_LIST *p_rdy_list;

    for ( i=0u; i<OS_CFG_PRIO_MAX; i++ )
    {
        p_rdy_list = &OSRdyList[i];
        p_rdy_list->HeadPtr = (OS_TCB *)0;
        p_rdy_list->TailPtr = (OS_TCB *)0;
    }
}

3. Start the system

After the task is created and the system is initialized, start the system.

void OSStart (OS_ERR *p_err)
{
    if ( OSRunning == OS_STATE_OS_STOPPED ) {
        /* Manually configure task 1 to run first */
        OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;

        /* Start task switching and will not return. It is written in assembly language*/
        OSStartHighRdy();

        /* It will not run here. Running here indicates that a fatal error has occurred */
        *p_err = OS_ERR_FATAL_RETURN;
    }
    else
    {
        *p_err = OS_STATE_OS_RUNNING;
    }
}

4. Task switching

After calling the OSStartHighRdy() function to trigger the PendSV exception, you need to write the PendSV exception service function, and then switch tasks in it.

PendSV exception service mainly completes two tasks: one is to save the above, that is, to save the environment parameters of the currently running task; The second is to switch the following, that is, load the environment parameters of the next task to be run from the task stack to the CPU register, so as to realize the task switching.

5. main() function

int main(void)
{
    OS_ERR err;

    /* Initialize related global variables */
    OSInit(&err);

    /* Create task */
    OSTaskCreate ((OS_TCB*)      &Task1TCB,
                (OS_TASK_PTR ) Task1,
                (void *)       0,
                (CPU_STK*)     &Task1Stk[0],
                (CPU_STK_SIZE) TASK1_STK_SIZE,
                (OS_ERR *)     &err);

    OSTaskCreate ((OS_TCB*)      &Task2TCB,
                (OS_TASK_PTR ) Task2,
                (void *)       0,
                (CPU_STK*)     &Task2Stk[0],
                (CPU_STK_SIZE) TASK2_STK_SIZE,
                (OS_ERR *)     &err);

    /* Add task to ready list */
    OSRdyList[0].HeadPtr = &Task1TCB;
    OSRdyList[1].HeadPtr = &Task2TCB;

    /* Start the OS and will not return */
    OSStart(&err);
}

Task functions Task1 and Task2 do not really switch automatically. Instead, the OSSched() function is added to their respective function bodies to realize manual switching.

/* Task switching actually triggers a PendSV exception, and then performs context switching in the PendSV exception */
void OSSched (void)
{
    if ( OSTCBCurPtr == OSRdyList[0].HeadPtr )
    {
        OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
    }
    else
    {
        OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
    }

    OS_TASK_SW();
}

The scheduling algorithm of the OSSched() function is very simple, that is, if the current task is task 1, the next task is task 2. If the current task is task 2, the next task is task 1.
Then call the OS_TASK_SW() function triggers PendSV exception, and then implements task switching in PendSV exception.

Tuesday, July 20, 2021, 19:56:48

3, Task time slice run

On the basis of the previous code example, SysTick interrupt is added to switch tasks in the SysTick interrupt service function, so as to realize the time slice operation of dual tasks, that is, the running time of each task is the same.

1. What is SysTick?

RTOS needs a time base to drive, and the frequency of system task scheduling is equal to that of the time base. Usually, the time base is provided by a timer and can also be obtained from other periodic signal sources.

There is a system timer SysTick in Cortex-M kernel, which is embedded in NVIC. It is a 24 bit decreasing counter. The time of each count is 1/SYSCLK.
When the value of the reload value register decreases to 0, the system timer generates an interrupt to cycle back and forth.

SysTick is the most suitable timer to provide time base for the operating system and maintain the system heartbeat.

2. How to use SysTick?

Only one initialization function is required.

void  OS_CPU_SysTickInit (CPU_INT32U  ms)
{
    /* Sets the value of the reload register */
    SysTick->LOAD  = ms * SystemCoreClock / 1000 - 1;

    /* Configure interrupt priority as the lowest */
    NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);

    /* Resets the value of the current counter */
    SysTick->VAL   = 0;

    /* Select clock source, enable interrupt, enable counter */
    SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |
                    SysTick_CTRL_TICKINT_Msk   |
                    SysTick_CTRL_ENABLE_Msk;
}

3. Write SysTick interrupt service function

void SysTick_Handler(void)
{
    OSTimeTick();
}
void  OSTimeTick (void)
{
    /* Task scheduling, like the previous code example, does not need to be modified */
    OSSched();
}

4. main() function

Compared with the previous main(), only SysTick related content is added.

int main(void)
{
    OS_ERR err;

    /* Close interrupt */
    CPU_IntDis();//(1) Why turn off interrupts?

    /* Configure systick to interrupt once every 10ms */
    OS_CPU_SysTickInit (10);//(2) The task scheduling is completed in the interrupt service function of SysTick

    /* Initialize related global variables */
    OSInit(&err);

    /* Create task */
    OSTaskCreate ((OS_TCB*)      &Task1TCB,
                (OS_TASK_PTR ) Task1,
                (void *)       0,
                (CPU_STK*)     &Task1Stk[0],
                (CPU_STK_SIZE) TASK1_STK_SIZE,
                (OS_ERR *)     &err);

    OSTaskCreate ((OS_TCB*)      &Task2TCB,
                (OS_TASK_PTR ) Task2,
                (void *)       0,
                (CPU_STK*)     &Task2Stk[0],
                (CPU_STK_SIZE) TASK2_STK_SIZE,
                (OS_ERR *)     &err);

    /* Add task to ready list */
    OSRdyList[0].HeadPtr = &Task1TCB;
    OSRdyList[1].HeadPtr = &Task2TCB;

    /* Start the OS and will not return */
    OSStart(&err);
}

(1) Before OS system initialization, SysTick timer is enabled to generate 10ms interrupt, which triggers task scheduling,
If we don't turn off the interrupt at the beginning, we will enter SysTick interrupt before the OS is started, and then task scheduling will occur,
Since the OS has not been started, scheduling is not allowed, so close the interrupt first.
After the system starts, the interrupt is restarted by OSStartHighRdy() in the OSStart() function.

In this code example, the task scheduling will no longer be implemented in their respective tasks, but will be put into the SysTick interrupt service function,
Thus, each task can run the same time slice, occupy the CPU in turn and enjoy the CPU equally.
In the previous code, the two tasks also occupy the CPU in turn and enjoy the same time slice, which is the time of a single task run.
The difference is that in this code, the time slice of the task is equal to the time base of the SysTick timer, which is the synthesis of the single running time of many tasks,
That is, the task runs many times in this time slice.

Wednesday, July 21, 2021, 10:17:05

4, Blocking delay and idle tasks

In the previous example, the software delay is used for the delay in the task body, that is, the CPU is left idle to achieve the delay effect. In order to drain the performance of the CPU and try not to let it idle, blocking delay is introduced here.

What is blocking delay?

When the task needs to be delayed, the task will give up the right to use the CPU, and the CPU can do other things. When the task delay time expires, the CPU right will be regained, and the task will continue to run, making full use of the CPU resources.
When the task needs to be delayed and enters the blocking state, if the CPU has no other tasks to run, the system will create an idle task for the CPU. At this time, the CPU will run the idle task.

What are idle tasks?

Idle task is the task with the lowest priority created by the system during initialization. The main body of idle task is very simple, just counting a global variable.
In practical application, when the system enters the idle task, the single chip microcomputer can enter sleep or low power consumption in the idle task.

1. Implement idle tasks

1.1. Define idle task stack

CPU_STK OSCfg_IdleTaskStk[OS_CFG_IDLE_TASK_STK_SIZE];

/* Starting address of idle task stack */
CPU_STK      * const  OSCfg_IdleTaskStkBasePtr   = (CPU_STK    *)&OSCfg_IdleTaskStk[0];

/* Idle task stack size */
CPU_STK_SIZE   const  OSCfg_IdleTaskStkSize      = (CPU_STK_SIZE)OS_CFG_IDLE_TASK_STK_SIZE;
//The starting address and size of the stack of idle tasks are defined as a constant and cannot be modified.

1.2. Define idle task TCB

/* Idle task TCB */
OS_EXT    OS_TCB         OSIdleTaskTCB;

1.3. Define idle task function

/* Idle task  */
void  OS_IdleTask (void  *p_arg)
{
    p_arg = p_arg;

/* Idle tasks do nothing but operate on the global variable OSIdleTaskCtr + + */
for (;;) {
        OSIdleTaskCtr++;//Idle task count variable
    }
}

1.4. Idle task initialization

Idle tasks are completed during system initialization, which means that idle tasks have been created before the system is started.

void OSInit (OS_ERR *p_err)
{
    /* Configure the initial status of the OS to stop */
    OSRunning =  OS_STATE_OS_STOPPED;

    /* Initialize two global TCB S, which are used for task switching */
    OSTCBCurPtr = (OS_TCB *)0;
    OSTCBHighRdyPtr = (OS_TCB *)0;

    /* Initialize ready list */
    OS_RdyListInit();

    /* Initialize idle tasks */
    OS_IdleTaskInit(p_err);(1)
    if (*p_err != OS_ERR_NONE) {
    return;
    }
}

/* Idle task initialization */
void  OS_IdleTaskInit(OS_ERR  *p_err)
{
    /* Initialize idle task counters */
    OSIdleTaskCtr = (OS_IDLE_CTR)0;(2)

    /* Create idle task */
    OSTaskCreate( (OS_TCB     *)&OSIdleTaskTCB,(3)
                (OS_TASK_PTR )OS_IdleTask,
                (void       *)0,
                (CPU_STK    *)OSCfg_IdleTaskStkBasePtr,
                (CPU_STK_SIZE)OSCfg_IdleTaskStkSize,
                (OS_ERR     *)p_err );
}

2. Implement blocking delay

Blocking of blocking delay means that after the task calls the delay function, the task will be stripped of CPU usage, and then enter the blocking state. The task can not continue to run until the delay ends and the task regains CPU usage. During the period when the task is blocked, the CPU can execute other tasks. If other tasks are also in the delayed state, the CPU will run idle tasks.

/* Blocking delay */
void  OSTimeDly(OS_TICK dly)
{
    /* Set delay time */
    OSTCBCurPtr->TaskDelayTicks = dly;//TaskDelayTicks is a member of the task control block. It is used to record the time that the task needs to be delayed. The unit is the interrupt cycle of SysTick.

    /* Task scheduling */
    OSSched();
}
struct os_tcb {
    CPU_STK         *StkPtr;
    CPU_STK_SIZE    StkSize;

    /* Number of task delay cycles */
    OS_TICK         TaskDelayTicks;
};
void OSSched(void)
{
/* If the current task is an idle task, try to execute task 1 or task 2,
See if their delay time ends. If the delay time of the task does not expire,
Then go back and continue the idle task */
    if ( OSTCBCurPtr == &OSIdleTaskTCB ) 
    {
        if (OSRdyList[0].HeadPtr->TaskDelayTicks == 0)
        {
            OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
        }
        else if (OSRdyList[1].HeadPtr->TaskDelayTicks == 0)
        {
            OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
        }
        else
        {
            /* If the task delay has not expired, return to continue to execute the idle task */
            return;
        }
    }
    else 
    {
/*If it is task1 or task2, check another task,
If another task is not in delay, switch to the task
 Otherwise, judge whether the current task should enter the delay state,
If so, switch to idle tasks. Otherwise, no switching will be performed */
        if (OSTCBCurPtr == OSRdyList[0].HeadPtr)
        {
            if (OSRdyList[1].HeadPtr->TaskDelayTicks == 0)
            {
                OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
            }
            else if (OSTCBCurPtr->TaskDelayTicks != 0)
            {
                OSTCBHighRdyPtr = &OSIdleTaskTCB;
            }
            else
            {
            /* Return and do not switch because both tasks are in delay */
            return;
            }
        }
        else if (OSTCBCurPtr == OSRdyList[1].HeadPtr)
        {
            if (OSRdyList[0].HeadPtr->TaskDelayTicks == 0)
            {
                OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
            }
            else if (OSTCBCurPtr->TaskDelayTicks != 0)
            {
                OSTCBHighRdyPtr = &OSIdleTaskTCB;
            }
            else
            {
                /* Return and do not switch because both tasks are in delay */
                return;
            }
        }
    }

    /* Task switching triggers PendSV exception.*/
    OS_TASK_SW();
}

3. main() function

There is little change from the previous experimental code.

  • The initialization function of the idle task is called in OSInint, and the idle task is created before the system starts.
  • The delay function in the task is replaced by blocking delay. The delay time is two SysTick interrupt cycles, i.e. 20ms.

Posted by manianprasanna on Tue, 30 Nov 2021 04:59:33 -0800