[TencentOS tiny] Углубленный анализ исходного кода (2) — планировщик

Интернет вещей

Напоминание: В этой статье не описывается содержимое регистров, связанных с плавающей запятой, если вам нужно об этом знать, проверьте сами (ведь я сам в этом не разбираюсь)

Основные понятия планировщиков

TencentOS tinyПредусмотренный планировщик задач представляет собой полное упреждающее планирование на основе приоритета.Во время работы системы, когда задача с более высоким приоритетом, чем текущая задача, будет готова, текущая задача будет немедленно отправлена.切出, первоочередные задачи抢占Процессор работает.

TencentOS tinyВ ядре также разрешено создание задач с таким же приоритетом. Задачи с одинаковым приоритетом планируются циклическим методом с разделением времени (то есть планировщиком с разделением времени), а циклическое планирование с разделением времени доступно только в текущей системе.Нет готовой задачи с более высоким приоритетомдействует только в случае .

Чтобы обеспечить производительность системы в режиме реального времени, система обеспечивает выполнение высокоприоритетных задач в максимально возможной степени. Принцип планирования задач заключается в том, что как только состояние задачи изменится, а приоритет текущей задачи станет ниже наивысшего приоритета задачи в очереди приоритетов, переключение задачи будет выполнено немедленно (если только текущая система не находится в очереди). обработчик прерываний или переключение задач отключены).

Планировщик — это программа операционной системы.核心, его основная функция实现任务的切换, то есть из готового списка找到задача с наивысшим приоритетом, затем перейдите к执行задание.

запустить планировщик

Планировщик запускаетсяcpu_sched_startфункция для завершения, это будетtos_knl_startВызов функции, эта функция в основном делает две вещи, сначала черезreadyqueue_highest_ready_task_getФункция получает готовую задачу с наивысшим приоритетом в текущей системе и присваивает ей указатель на блок управления текущей задачейk_curr_task , а затем установите состояние системы в рабочее состояниеKNL_STATE_RUNNING.

Конечно, самое главное — вызвать функцию, написанную на ассемблере.cpu_sched_startЗапустите планировщик, эта функция есть в исходникахarcharmarm-v7mв каталогеport_s.SВ файле сборкиTencentOS tinyЧипы, поддерживающие несколько ядер, такие какM3/M4/M7и т.д. В разных чипах эта функция реализована по-разному.port_s.SСлишкомTencentOS tinyКак программное обеспечение и аппаратное соединение ЦП桥梁. Возьми М4cpu_sched_startНапример:

__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

Из приведенного выше ассемблерного кода я хотел бы снова представитьCortex-MЯдро отключает инструкцию прерывания, увы~ Я все еще чувствую себя немного хлопотно! Для быстрого переключения прерываний ядро ​​Cortex-M специально устанавливаетCPS 指令, для работыPRIMASKзарегистрироваться, а затемFAULTMASKРегистр, эти два регистра относятся к маскированию прерываний, в дополнение кCortex-Mядро все еще существуетBASEPRIРегистры также связаны с прерываниями, так что давайте, между прочим, представим их.

CPSID I     ;PRIMASK=1     ;关中断
CPSIE I     ;PRIMASK=0     ;开中断
CPSID F     ;FAULTMASK=1   ;关异常
CPSIE F     ;FAULTMASK=0   ;开异常
регистр Функции
PRIMASK
После того, как он установлен в 1, все маскируемые исключения отключаются, оставляя реагировать только NMI и HardFault FAULT.
FAULTMASK
Когда установлено значение 1, только NMI может отвечать, все остальные исключения не могут отвечать (включая HardFault FAULT).
BASEPRI
Этот регистр имеет максимум 9 бит (определяется количеством битов, выражающих приоритет). Он определяет порог приоритета блокировки. Когда он установлен на определенное значение, все прерывания с номером приоритета больше или равным этому значению отключаются (чем выше номер приоритета, тем ниже приоритет). Но если установлено значение 0, прерывания не отключаются.

Более подробное описание смотрите в моих предыдущих статьях:Знание критического раздела RTOS: https://blog.csdn.net/jiejiemcu/article/details/82534974

Вернуться к сути

Его необходимо настроить в процессе запуска планировщика ядра.PendSVПриоритет прерывания самый низкий, т.NVIC_SYSPRI14(0xE000ED22)адрес написатьNVIC_PENDSV_PRI(0xFF). так какPendSVЭто будет включать системное планирование, и приоритет системного планирования должен быть低于Приоритет других аппаратных прерываний в системе, то есть приоритет реагирования на внешние аппаратные прерывания в системе, поэтому приоритет прерывания PendSV должен быть настроен на самый низкий, иначе он, вероятно, сгенерирует планирование задач в контексте прерывания. .

PendSVИсключения автоматически задерживают запросы на переключение контекста до тех пор, пока другиеISRОн будет выпущен после завершения всей обработки. Для реализации этого механизма необходимоPendSVИсключение запрограммировано на самый низкий приоритет. еслиOSобнаружилISRактивен, он приостановитPendSVИсключение, чтобы отложить выполнение переключения контекста. То есть до тех пор, покаPendSVПриоритет установлен на самый низкий, даже если systick прервет IRQ, он не переключит контекст немедленно, а подождет, покаISRзаконченный,PendSVСлужебная подпрограмма только начинает выполняться, и внутри нее происходит переключение контекста. Процесс показан на рисунке:затем получитьMSPадрес указателя основного стека, вCortex-Mсередина,0xE000ED08даSCB_VTORАдрес регистра, в котором хранится начальный адрес векторной таблицы.

нагрузкаk_next_taskуказывает на блок управления задачамиR2, из предыдущей статьи мы знаем, что первым членом блока управления задачами является указатель на вершину стека, поэтому на данный моментR2Равен вершине указателя стека.

ps : Когда планировщик запускается,k_next_taskиk_curr_taskэто то же самое(k_curr_task = k_next_task)

нагрузкаR2прибытьR0, то вершина указателя стекаR0Обновить доpsp, указатель стека, используемый при выполнении задачи,psp.

пс:spЕсть два указателя соответственноpspиmsp. (можно просто понять как: использовать в контексте задачиpsp, используется в контексте прерыванияmsp, это не обязательно правильно, это моё личное понимание)

отR0это базовый адрес, который будет расти в стеке8Содержимое слов загружается в регистры процессора.R4~R11,в то же времяR0также увеличится

Затем нужно загрузитьR0 ~ R3、R12以及LR、 PC、xPSRВ группе регистров CPU указатель PC указывает на поток, который вот-вот запустится, а регистр LR указывает на выход из задачи.因为这是第一次启动任务,要全部手动把任务栈上的寄存器弹到硬件里,才能进入第一个任务的上下文,因为一开始并没有第一个任务运行的上下文环境,而在进入PendSV的时候需要上文保存,所以需要手动创造任务上下文环境(将这些寄存器加载到CPU寄存器组中), в первый раз, когда эта функция ввода сборки, sp указывает на вершину стека выбранной задачи (k_curr_task).

Посмотрите на инициализацию стека задач

Исходя из вышеизложенного, давайте взглянем на инициализацию стека задач, которая может произвести более глубокое впечатление. В основном, чтобы знать следующие моменты:

  • Получите указатель верхнего стека какstk_base[stk_size]высокий адрес,Cortex-MСтек ядра向下增长из.
  • R0、R1、R2、R3、R12、R14、R15和xPSR的位24будет использоваться процессором自动загружается и сохраняется.
  • xPSRbit24必须置1, то есть 0x01000000.
  • entry — адрес входа задачи, т.е.PC
  • R14 (LR) — это адрес выхода задачи, поэтому задача, как правило, представляет собой бесконечный цикл безreturn
  • R0: arg — формальный параметр тела задачи.
  • Указатель sp уменьшается при инициализации стека
__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;
}

Найдите задачу с наивысшим приоритетом

Если в операционной системе есть только высокоприоритетные задачи, она может立即Получить процессор и получить характеристики исполнения, тогда это все же не операционная система реального времени. Поскольку этот процесс поиска задачи с наивысшим приоритетом определяет, является ли время планирования детерминированным, вы можете просто использовать时间复杂度Чтобы описать это, если время, необходимое системе для поиска задачи с наивысшим приоритетом, равноO(N), то это время будет увеличиваться по мере увеличения количества задач, что нежелательно.TencentOS tinyВременная сложностьO(1), который предоставляет два способа найти задачу с наивысшим приоритетом:TOS_CFG_CPU_LEAD_ZEROS_ASM_PRESENTОпределение макроса решает.

  1. Первый - использовать обычный метод, согласно готовому спискуk_rdyq.prio_mask[]Переменная определяет, установлен ли соответствующий бит в 1.
  2. Второй метод — это специальный метод, использующий инструкцию для вычисления начальных нулей.CLZ, прямо вk_rdyq.prio_mask[]это32Позиция с наивысшим приоритетом получается напрямую из переменной бита, что быстрее, чем обычный метод.但受限于平台(Требуются аппаратные начальные нулевые инструкции, в STM32 мы можем использовать этот метод).

Процесс реализации выглядит следующим образом, рекомендуется ознакомитьсяreadyqueue_prio_highest_getФункция, его реализация все еще очень деликатная~

__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
}

Реализация переключения задач

Мы также заранее знаем, что переключение между задачамиPendSVВ прерывании содержание, реализованное в этом прерывании, можно резюмировать в одном типичном предложении:Сохранить выше, переключиться ниже, посмотрите прямо на исходный код:

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

будетPSPЗначение хранится вR0. при входеPendSVC_Handler, среда, в которой выполняется предыдущая задача:xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0Значения этих регистров ЦП будут自动Хранится в стеке задачи, после чего указатель psp автоматически обновляется. и остальныеr4~r11необходимость手动беречь, поэтомуPendSVC_HandlerСохраните вышеуказанное в (_context_save), основная причина — загрузить регистры, которые не могут быть автоматически сохранены в ЦП, и поместить их в стек задач.

Затем найдите следующую задачу для запускаk_next_task, который загружает верхнюю часть своего стека задач вR0, а затем вручную вставьте содержимое нового стека задач (здесь имеется в видуR4~R11) загружается вCPUВ группе регистров это переключение контекста.Конечно, есть и другое содержимое, которое не может быть сохранено автоматически, которое нужно вручную загрузить вCPUнабор регистр. После загрузки вручную, на этот разR0обновлено, обновите значение psp, выйдитеPendSVC_HandlerПри прерывании будетpspВ качестве базового адреса остальная часть стека задач (xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0) автоматически загружаются в регистры ЦП.

На самом деле, когда возникает исключение, флаг возврата исключения сохраняется в R14, в том числе, следует ли переходить в режим задачи или режим процессора после возврата, а также следует ли использовать указатель стека PSP или указатель стека MSP. В это время r14 равен 0xffffffffd, что означает переход в режим задачи после аварийного возврата (ведьPendSVC_HandlerПриоритет является самым низким и будет возвращен задаче), SP использует PSP в качестве указателя стека для извлечения из стека после завершения всплывающего окна.PSPУказывает на вершину стека задач. При вызове инструкции BX R14 системаPSPв видеSPУказатель извлекается из стека, а остальная часть стека новой задачи, которая должна быть запущена, загружается в регистры ЦП:R0、R1、R2、R3、R12、R14(LR)、R15(PC)和xPSR, тем самым переключаясь на новую задачу.

SysTick

Инициализация SysTick

systick — это база времени системы и часы ядра, если ониM0/M3/M4/M7ядро будет существоватьsystickЧасы, причем их можно программировать и настраивать, что обеспечивает большое удобство при портировании операционной системы.TencentOS tinyБудет вcpu_initгенерал-лейтенантsystickинициализировать, то есть вызватьcpu_systick_initфункция, так что пользователям не нужно писать свои собственныеsystickИнициализировать связанный код.

__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

SysTickФункцию обслуживания прерывания нужно написать самим, и нам нужно вызвать ее в нейTencentOS tinyСвязанные функции, обновление базы системного времени для управления работой системы,SysTick_HandlerПортирование функции выглядит следующим образом:

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

В основном нужно позвонитьtos_tick_handlerФункция обновляет базу системного времени, см.:

__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
}

должен сказатьTencentOS tinyРеализация исходного кода очень проста,我非常喜欢,существуетtos_tick_handler, сначала оцените, запустилась ли система, если не запущена, то вернется напрямую, если запущена, то вызовитеtick_updateФункция обновляет базу системного времени, если она включенаTOS_CFG_TIMER_ENОпределение макроса указывает на то, что используется программный таймер, и необходимо обновить соответствующую обработку, которая здесь не упоминается. если включеноTOS_CFG_ROUND_ROBIN_EN Определение макроса также должно обновлять переменные, связанные с временным интервалом, что будет объяснено позже.

__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_updateОсновная функция функции состоит в том, чтобыk_tick_count +1, и оцените список временной базыk_tick_list(Это также может быть список задержек) Если время ожидания задачи истекло, если время ожидания истекло, разбудите задачу, в противном случае просто выйдите напрямую. Планирование временных отрезков также очень просто.Остальные переменные временных отрезков задачиtimesliceУменьшите на единицу, затем перезагрузите переменную, когда переменная уменьшится до 0timeslice_reload, затем поменяйте задачиknl_sched(), процесс реализации выглядит следующим образом:

__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();
}

Следуй за мной, если хочешь!

欢迎关注我公众号

Соответствующий код можно получить в фоне официального аккаунта. Добро пожаловать в публичный аккаунт «Развития Интернета вещей IoT»