[TencentOS tiny source depth analysis] (2) - scheduler

Tips: This article does not describe the content associated with the floating-point registers, learn self-inspection (after all, I do not know)

The basic concept of the scheduler

TencentOS tinyIt provided the task scheduler is priority-based preemptive scheduling full, the system is running, when there are more ready than the current task priority of the task, the current task immediately 切出, high-priority task 抢占processor is running.

TencentOS tinyThe kernel also allows you to create the same priority task. Priority tasks using the same round-robin scheduling mode (which is often said time-sharing scheduling), a round-robin scheduling is current systems no higher-priority ready task to be effective in the case.

In order to ensure real-time system, the system maximum extent possible to ensure that high-priority task to run. Principle task scheduling is once the task status is changed, and the currently running task priority is less than the priority tasks in the queue the highest priority, immediately task switching (unless the current system is in state interrupt handler or disable task switch) .

The scheduler is the operating system 核心, its main function is 实现任务的切换that the ready list from which 找到the highest priority task, then go to 执行the task.

Start Scheduler

The scheduler is initiated by cpu_sched_startto complete the function, it will be tos_knl_startinvoked function that mainly do two things, first by readyqueue_highest_ready_task_getacquiring the current system function at the highest priority ready task and assign it to point to the current task control block pointer k_curr_task, then set the state of the system is about the operating mode KNL_STATE_RUNNING.

Of course, the most important thing is to call functions written in assembly code cpu_sched_startto start the scheduler, the function in the source arch\arm\arm-v7mdirectory port_s.Sunder assembly files, TencentOS tinysupport for multiple core chip, as M3/M4/M7other different ways to achieve different chips that function, port_s.Sbut also TencentOS tinyas a software connected with the CPU hardware 桥梁. M4 is to cpu_sched_startgive an example:

__API__ k_err_t tos_knl_start(void)
{
    if (tos_knl_is_running()) {
        return K_ERR_KNL_RUNNING;
    }

    k_next_task = readyqueue_highest_ready_task_get();
    k_curr_task = k_next_task;
    k_knl_state = KNL_STATE_RUNNING;
    cpu_sched_start();

    return K_ERR_NONE;
}
port_sched_start
    CPSID   I   

    ; set pendsv priority lowest
    ; otherwise trigger pendsv in port_irq_context_switch will cause a context swich in irq
    ; that would be a disaster
    MOV32   R0, NVIC_SYSPRI14
    MOV32   R1, NVIC_PENDSV_PRI
    STRB    R1, [R0]

    LDR     R0, =SCB_VTOR
    LDR     R0, [R0]
    LDR     R0, [R0]
    MSR     MSP, R0

    ; k_curr_task = k_next_task
    MOV32   R0, k_curr_task
    MOV32   R1, k_next_task
    LDR     R2, [R1]
    STR     R2, [R0]

    ; sp = k_next_task->sp
    LDR     R0, [R2]
    ; PSP = sp
    MSR     PSP, R0

    ; using PSP
    MRS     R0, CONTROL
    ORR     R0, R0, #2
    MSR     CONTROL, R0

    ISB

    ; restore r4-11 from new process stack
    LDMFD   SP!, {R4 - R11}

    IF {FPU} != "SoftVFP"
    ; ignore EXC_RETURN the first switch
    LDMFD   SP!, {R0}
    ENDIF

    ; restore r0, r3
    LDMFD    SP!, {R0 - R3}
    ; load R12 and LR
    LDMFD    SP!, {R12, LR}
    ; load PC and discard xPSR
    LDMFD    SP!, {R1, R2}

    CPSIE    I
    BX       R1

Cortex-M core off interrupt instruction

From the above assembly code, I want to introduce the Cortex-Mkernel interrupt disable instruction, alas ~ feel a little trouble!
To quickly switch interrupt, Cortex-M specifically set up a core CPS 指令for operating the PRIMASKregister with the FAULTMASKregister, the two registers are maskable interrupt associated with, in addition to Cortex-Mthe core there is BASEPRIa register associated with the interrupt it is also incidentally tell us about it.

CPSID I     ;PRIMASK=1     ;关中断
CPSIE I     ;PRIMASK=0     ;开中断
CPSID F     ;FAULTMASK=1   ;关异常
CPSIE F     ;FAULTMASK=0   ;开异常
register Features
PRIMASK 1 after it has been set, turn off all maskable exception, only in response NMI and HardFault FAULT
FAULTMASK When it is set to 1, only NMI can, all the other exceptions are unable to respond (including HardFault FAULT)
BASEPRI This register up to nine (the number of bits expressing priority decision). It defines a priority masked threshold. When it is set to a value, all numbers greater than or equal to this priority interrupts are turned value (the greater the priority number, the lower the priority). However, if is set to 0, no interrupt is not turned off

More detailed description see my previous article: RTOS critical sections of knowledge: https: //blog.csdn.net/jiejiemcu/article/details/82534974

Return to the topic

In the kernel scheduler to start the process you need to configure PendSVthe interrupt priority level is the lowest, that is, to NVIC_SYSPRI14(0xE000ED22)write the address NVIC_PENDSV_PRI(0xFF). Because PendSVwill involve system scheduling, system scheduling priority to 低于other hardware system interrupt priority, that priority response system of external hardware interrupts, so PendSV interrupt priority To configure the lowest, or is likely in interrupt context is generated task scheduling.

PendSVAbnormal context switch will automatically delay the request until the other ISRhave completed treatment after release. To achieve this mechanism, we need to PendSVbe programmed to the lowest priority exception. If OSdetected a ISRbeing active, it hanging from an PendSVexception, in order to perform a context switch suspended. In other words, as long as the PendSVpriority to the lowest, systick even interrupted IRQ, it will not be a context switch immediately, but wait until ISRexecuted, PendSVservice routine began to perform, and perform a context switch on the inside . As shown in FIG procedure:

then you obtain MSPthe address of the primary stack pointer, in the Cortex-Mmiddle, 0xE000ED08a SCB_VTORregister address, which is stored the start address of the vector table.

Load k_next_taskpointed to the task control block R2, the first known member of the task control block is the stack pointer from the previous article, so in this case R2equal to the stack pointer.

ps: When the scheduler starts, k_next_taskand k_curr_taskis the same as ( k_curr_task = k_next_task)

Loaded R2into R0, then the stack pointer R0updates to pspthe stack pointer is used when the task execution is psp.

ps: spPointer two, respectively pspand msp. (Can be simply understood as: use in mission contexts psp, the use of the interrupt context msp, not necessarily correct, this is my personal understanding)

In R0the base address, the stack grows upwards in the 8loading of the contents of CPU registers words R4~R11, while R0will follow the self-energizing

Then you need to load R0 ~ R3、R12以及LR、 PC、xPSRinto the CPU register sets, PC is a pointer to the thread is about to run, while the LR register exit points to the task. 因为这是第一次启动任务,要全部手动把任务栈上的寄存器弹到硬件里,才能进入第一个任务的上下文,因为一开始并没有第一个任务运行的上下文环境,而在进入PendSV的时候需要上文保存,所以需要手动创造任务上下文环境(将这些寄存器加载到CPU寄存器组中)First assembler when the entry function, sp is the top of the stack of the task points to a selected ( k_curr_task).

Take a look at the task stack initialization

From the above understanding, look at the stack initialization tasks, there may be a little deeper impression. Mainly to understand the following points:

  • Gets the stack pointer stk_base[stk_size]high address, Cortex-Mthe kernel stack is 向下增长in.
  • R0、R1、R2、R3、R12、R14、R15和xPSR的位24CPU will be 自动loaded and saved.
  • xPSR of bit24必须置1that 0x01000000.
  • entry is the entry address the task, namelyPC
  • R14 ( LR) is the exit address the task, so the task is generally an infinite loop withoutreturn
  • R0: arg parameter is the main task
  • Initializing the stack pointer sp is decremented
__KERNEL__ k_stack_t *cpu_task_stk_init(void *entry,
                                              void *arg,
                                              void *exit,
                                              k_stack_t *stk_base,
                                              size_t stk_size)
{
    cpu_data_t *sp;

    sp = (cpu_data_t *)&stk_base[stk_size];
    sp = (cpu_data_t *)((cpu_addr_t)(sp) & 0xFFFFFFF8);

    /* auto-saved on exception(pendSV) by hardware */
    *--sp = (cpu_data_t)0x01000000u;    /* xPSR     */
    *--sp = (cpu_data_t)entry;          /* entry    */
    *--sp = (cpu_data_t)exit;           /* R14 (LR) */
    *--sp = (cpu_data_t)0x12121212u;    /* R12      */
    *--sp = (cpu_data_t)0x03030303u;    /* R3       */
    *--sp = (cpu_data_t)0x02020202u;    /* R2       */
    *--sp = (cpu_data_t)0x01010101u;    /* R1       */
    *--sp = (cpu_data_t)arg;            /* R0: arg  */

    /* Remaining registers saved on process stack */
    /* EXC_RETURN = 0xFFFFFFFDL
       Initial state: Thread mode +  non-floating-point state + PSP
       31 - 28 : EXC_RETURN flag, 0xF
       27 -  5 : reserved, 0xFFFFFE
       4       : 1, basic stack frame; 0, extended stack frame
       3       : 1, return to Thread mode; 0, return to Handler mode
       2       : 1, return to PSP; 0, return to MSP
       1       : reserved, 0
       0       : reserved, 1
     */
#if defined (TOS_CFG_CPU_ARM_FPU_EN) && (TOS_CFG_CPU_ARM_FPU_EN == 1U)
    *--sp = (cpu_data_t)0xFFFFFFFDL;
#endif

    *--sp = (cpu_data_t)0x11111111u;    /* R11      */
    *--sp = (cpu_data_t)0x10101010u;    /* R10      */
    *--sp = (cpu_data_t)0x09090909u;    /* R9       */
    *--sp = (cpu_data_t)0x08080808u;    /* R8       */
    *--sp = (cpu_data_t)0x07070707u;    /* R7       */
    *--sp = (cpu_data_t)0x06060606u;    /* R6       */
    *--sp = (cpu_data_t)0x05050505u;    /* R5       */
    *--sp = (cpu_data_t)0x04040404u;    /* R4       */

    return (k_stack_t *)sp;
}

Find the highest priority task

If only one operating system has a high-priority task can 立即get processors and with the characteristics of the implementation, then it's still not a real-time operating system. Because of this finds the highest priority task scheduling process determines whether the time is deterministic, it can simply be used 时间复杂度to describe it, if the system finds the highest priority task of the time O(N), so this time vary with the number of tasks increases increases, which is not desirable, TencentOS tinythe time complexity is O(1)that it provides two ways to find the highest priority task, by TOS_CFG_CPU_LEAD_ZEROS_ASM_PRESENTdefinition determines the macro.

  1. The first common method is to use, according to the ready list k_rdyq.prio_mask[]is determined whether the corresponding bit is set variable.
  2. The second method is a special method, using the calculated leading zero instructions CLZdirectly in k_rdyq.prio_mask[]the 32drawn position of the highest priority bits in which the variables directly, this method is more efficient than the conventional method, 但受限于平台(leading zero instructions requires hardware, in STM32, we can use this method).

Implementation process follows, it is recommended to see readyqueue_prio_highest_getthe function, his realization is still very delicate ~

__STATIC__ k_prio_t readyqueue_prio_highest_get(void)
{
    uint32_t *tbl;
    k_prio_t prio;

    prio    = 0;
    tbl     = &k_rdyq.prio_mask[0];

    while (*tbl == 0) {
        prio += K_PRIO_TBL_SLOT_SIZE;
        ++tbl;
    }
    prio += tos_cpu_clz(*tbl);
    return prio;
}
__API__ uint32_t tos_cpu_clz(uint32_t val)
{
#if defined(TOS_CFG_CPU_LEAD_ZEROS_ASM_PRESENT) && (TOS_CFG_CPU_LEAD_ZEROS_ASM_PRESENT == 0u)
    uint32_t nbr_lead_zeros = 0;

    if (!(val & 0XFFFF0000)) {
        val <<= 16;
        nbr_lead_zeros += 16;
    }

    if (!(val & 0XFF000000)) {
        val <<= 8;
        nbr_lead_zeros += 8;
    }

    if (!(val & 0XF0000000)) {
        val <<= 4;
        nbr_lead_zeros += 4;
    }

    if (!(val & 0XC0000000)) {
        val <<= 2;
        nbr_lead_zeros += 2;
    }

    if (!(val & 0X80000000)) {
        nbr_lead_zeros += 1;
    }

    if (!val) {
        nbr_lead_zeros += 1;
    }

    return (nbr_lead_zeros);
#else
    return port_clz(val);
#endif
}

Realization of task switching

We also know from the front, task switching is PendSVcarried out in the interrupt, the interrupt is implemented as a summary of the content essence of the word is above save, switch below , direct look at the source code:

PendSV_Handler
    CPSID   I
    MRS     R0, PSP

_context_save
    ; R0-R3, R12, LR, PC, xPSR is saved automatically here
    IF {FPU} != "SoftVFP"
    ; is it extended frame?
    TST     LR, #0x10
    IT      EQ
    VSTMDBEQ  R0!, {S16 - S31}
    ; S0 - S16, FPSCR saved automatically here

    ; save EXC_RETURN
    STMFD   R0!, {LR}
    ENDIF

    ; save remaining regs r4-11 on process stack
    STMFD   R0!, {R4 - R11}

    ; k_curr_task->sp = PSP
    MOV32   R5, k_curr_task
    LDR     R6, [R5]
    ; R0 is SP of process being switched out
    STR     R0, [R6]

_context_restore
    ; k_curr_task = k_next_task
    MOV32   R1, k_next_task
    LDR     R2, [R1]
    STR     R2, [R5]

    ; R0 = k_next_task->sp
    LDR     R0, [R2]

    ; restore R4 - R11
    LDMFD   R0!, {R4 - R11}

    IF {FPU} != "SoftVFP"
    ; restore EXC_RETURN
    LDMFD   R0!, {LR}
    ; is it extended frame?
    TST     LR, #0x10
    IT      EQ
    VLDMIAEQ    R0!, {S16 - S31}
    ENDIF

    ; Load PSP with new process SP
    MSR     PSP, R0
    CPSIE   I
    ; R0-R3, R12, LR, PC, xPSR restored automatically here
    ; S0 - S16, FPSCR restored automatically here if FPCA = 1
    BX      LR

    ALIGN
    END

The PSPvalue stored R0. When entering PendSVC_Handler, the operating environment on a task that is: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0the CPU registers will be 自动stored to the job stack, in which case the pointer has been updated automatically psp. While the rest r4~r11need 手动to save, and this is why you want to PendSVC_Handlersave the above ( _context_save) of the main reasons for not automatically load the saved CPU registers, which was pressed into the task stack.

Then find a task to be run next k_next_task, it is loaded into the stack of the task R0, the new task and then manually stack contents (herein means R4~R11) is loaded into the CPUregister set, this is the context switch, of course, there are other not method content is automatically saved manually loaded into the CPUregister bank. After loading manual, this time R0has been updated, the updated value of the psp, exit PendSVC_Handlerinterrupt, will be pspused as the base address, the task stack the rest of the content ( xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0) is automatically loaded into the CPU registers.

In fact, when an exception occurs, R14 saved abnormal return flag, including the return after entering the task mode or processor mode, use the PSP stack pointer or stack pointer MSP. In this case equals r14 0xFFFFFFFD, a rear most task back into the abnormal mode (after PendSVC_Handlerthe priority is the lowest, will return to the task), SP PSP as a stack pointer to the stack, the stack after completion PSPpoints to the top of the stack task. When the call instruction BX R14, the system PSPas SPa stack pointer, stack task to the new task to run next is loaded into the remaining contents of CPU registers: R0、R1、R2、R3、R12、R14(LR)、R15(PC)和xPSRto switch to a new task.

SysTick

SysTick initialization

systick is based system, and it is the core clock, as long as M0/M3/M4/M7the core it will present systicka clock, and it is programmed to be configured, which provides great convenience for the operating system migration.
TencentOS tinyWill cpu_initin the function systickis initialized, i.e., it calls cpu_systick_initthe function, this way there is no need to write user- systickinitialization related code.

__KERNEL__ void cpu_init(void)
{
    k_cpu_cycle_per_tick = TOS_CFG_CPU_CLOCK / k_cpu_tick_per_second;
    cpu_systick_init(k_cpu_cycle_per_tick);

#if (TOS_CFG_CPU_HRTIMER_EN > 0)
    tos_cpu_hrtimer_init();
#endif
}
__KERNEL__ void cpu_systick_init(k_cycle_t cycle_per_tick)
{
    port_systick_priority_set(TOS_CFG_CPU_SYSTICK_PRIO);
    port_systick_config(cycle_per_tick);
}

SysTick interrupt

SysTickInterrupt service function is the need to write our own, to be called inside look at TencentOS tinythe function-related, update the system to run the base, the drive system of the SysTick_Handlertransplant function is as follows:

void SysTick_Handler(void)
{
  HAL_IncTick();
  if (tos_knl_is_running())
  {
    tos_knl_irq_enter();
    
    tos_tick_handler();
    
    tos_knl_irq_leave();
  }
}

Mainly the need to call tos_tick_handlera function of the base system updates, specific see:

__API__ void tos_tick_handler(void)
{
    if (unlikely(!tos_knl_is_running())) {
        return;
    }

    tick_update((k_tick_t)1u);

#if TOS_CFG_TIMER_EN > 0u && TOS_CFG_TIMER_AS_PROC > 0u
    timer_update();
#endif

#if TOS_CFG_ROUND_ROBIN_EN > 0u
    robin_sched(k_curr_task->prio);
#endif
}

Have to say that TencentOS tinythe source code implementation is very simple 我非常喜欢, in tos_tick_handler, first determine what the system has been running, if not running will return directly If you have run, then call tick_updatea function to update the base system, if enabled TOS_CFG_TIMER_ENmacros representation use software timer, you need to update the appropriate treatment, for the time being not to mention here. If you enable the TOS_CFG_ROUND_ROBIN_ENmacros, also need to update the relevant time slice variable, explain later.

__KERNEL__ void tick_update(k_tick_t tick)
{
    TOS_CPU_CPSR_ALLOC();
    k_task_t *first, *task;
    k_list_t *curr, *next;

    TOS_CPU_INT_DISABLE();
    k_tick_count += tick;

    if (tos_list_empty(&k_tick_list)) {
        TOS_CPU_INT_ENABLE();
        return;
    }

    first = TOS_LIST_FIRST_ENTRY(&k_tick_list, k_task_t, tick_list);
    if (first->tick_expires <= tick) {
        first->tick_expires = (k_tick_t)0u;
    } else {
        first->tick_expires -= tick;
        TOS_CPU_INT_ENABLE();
        return;
    }

    TOS_LIST_FOR_EACH_SAFE(curr, next, &k_tick_list) {
        task = TOS_LIST_ENTRY(curr, k_task_t, tick_list);
        if (task->tick_expires > (k_tick_t)0u) {
            break;
        }

        // we are pending on something, but tick's up, no longer waitting
        pend_task_wakeup(task, PEND_STATE_TIMEOUT);
    }

    TOS_CPU_INT_ENABLE();
}

tick_updateThe main function is to function k_tick_count +1, and judge for themselves when the base list k_tick_listwhether the task (it can also be a delay list) timeout, if a timeout then wake up the task, otherwise direct exit. Time schedule on the sheet is very simple, the remaining time slice task variables timesliceminus one, and then reduced to zero when the variable, the variable weight loading timeslice_reload, and switching tasks knl_sched(), which process is as follows:

__KERNEL__ void robin_sched(k_prio_t prio)
{
    TOS_CPU_CPSR_ALLOC();
    k_task_t *task;

    if (k_robin_state != TOS_ROBIN_STATE_ENABLED) {
        return;
    }

    TOS_CPU_INT_DISABLE();

    task = readyqueue_first_task_get(prio);
    if (!task || knl_is_idle(task)) {
        TOS_CPU_INT_ENABLE();
        return;
    }

    if (readyqueue_is_prio_onlyone(prio)) {
        TOS_CPU_INT_ENABLE();
        return;
    }

    if (knl_is_sched_locked()) {
        TOS_CPU_INT_ENABLE();
        return;
    }

    if (task->timeslice > (k_timeslice_t)0u) {
        --task->timeslice;
    }

    if (task->timeslice > (k_timeslice_t)0u) {
        TOS_CPU_INT_ENABLE();
        return;
    }

    readyqueue_move_head_to_tail(k_curr_task->prio);

    task = readyqueue_first_task_get(prio);
    if (task->timeslice_reload == (k_timeslice_t)0u) {
        task->timeslice = k_robin_default_timeslice;
    } else {
        task->timeslice = task->timeslice_reload;
    }

    TOS_CPU_INT_ENABLE();
    knl_sched();
}

I like to focus on it!

I welcome public attention No.

The relevant code number can be acquired in public background.
For more information please concern "Things IoT development," the public number!

Guess you like

Origin www.cnblogs.com/iot-dev/p/11688906.html