Напоминание: В этой статье не описывается содержимое регистров, связанных с плавающей запятой, если вам нужно об этом знать, проверьте сами (ведь я сам в этом не разбираюсь)
Основные понятия планировщиков
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
будет использоваться процессором自动
загружается и сохраняется. - xPSR
bit24必须置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
Определение макроса решает.
- Первый - использовать обычный метод, согласно готовому списку
k_rdyq.prio_mask[]
Переменная определяет, установлен ли соответствующий бит в 1. - Второй метод — это специальный метод, использующий инструкцию для вычисления начальных нулей.
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»