溫馨提示:本文不描述與浮點相關的暫存器的內容,如需了解自行查閱(畢竟我自己也不懂)
調度器的基本概念
TencentOS tiny中提供的任務調度器是基于優先級的全搶占式調度,在系統運行程序中,當有比當前任務優先級更高的任務就緒時,當前任務將立刻被切出,高優先級任務搶占處理器運行,
TencentOS tiny內核中也允許創建相同優先級的任務,相同優先級的任務采用時間片輪轉方式進行調度(也就是通常說的分時調度器),時間片輪轉調度僅在當前系統中無更高優先級就緒任務的情況下才有效,
為了保證系統的實時性,系統盡最大可能地保證高優先級的任務得以運行,任務調度的原則是一旦任務狀態發生了改變,并且當前運行的任務優先級小于優先級佇列中任務最高優先級時,立刻進行任務切換(除非當前系統處于中斷處理程式中或禁止任務切換的狀態),
調度器是作業系統的核心,其主要功能就是實作任務的切換,即從就緒串列里面找到優先級最高的任務,然后去執行該任務,
啟動調度器
調度器的啟動由cpu_sched_start函式來完成,它會被tos_knl_start函式呼叫,這個函式中主要做兩件事,首先通過readyqueue_highest_ready_task_get函式獲取當前系統中處于最高優先級的就緒任務,并且將它賦值給指向當前任務控制塊的指標k_curr_task ,然后設定一下系統的狀態為運行態KNL_STATE_RUNNING,
當然最重要的是呼叫匯編代碼寫的函式cpu_sched_start啟動調度器,該函式在原始碼的arch\arm\arm-v7m目錄下的port_s.S匯編檔案下,TencentOS tiny支持多種內核的芯片,如M3/M4/M7等,不同的芯片該函式的實作方式不同,port_s.S也是TencentOS tiny作為軟體與CPU硬體連接的橋梁,以M4的cpu_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,
ps:
sp指標有兩個,分別為psp和msp,(可以簡單理解為:在任務背景關系環境中使用psp,在中斷背景關系環境使用msp,也不一定是正確的,這是我個人的理解)
以R0為基地址,將堆疊中向上增長的8個字的內容加載到CPU暫存器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是會被CPU自動加載與保存的,- 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這些CPU暫存器的值會自動存盤到任務的堆疊中,此時psp指標已經被自動更新,而剩下的r4~r11需要手動保存,這也是為啥要在PendSVC_Handler中保存上文(_context_save)的原因,主要是加載CPU中不能自動保存的暫存器,將其壓入任務堆疊中,
接著找到下一個要運行的任務k_next_task,將它的任務堆疊頂加載到R0,然后手動將新任務堆疊中的內容(此處是指R4~R11)加載到CPU暫存器組中,這就是下文切換,當然還有一些其他沒法自動保存的內容也是需要手動加載到CPU暫存器組的,手動加載完后,此時R0已經被更新了,更新psp的值,在退出PendSVC_Handler中斷時,會以psp作為基地址,將任務堆疊中剩下的內容(xPSR,PC(任務入口地址),R14,R12,R3,R2,R1,R0)自動加載到CPU暫存器,
其實在例外發生時,R14中保存例外回傳標志,包括回傳后進入任務模式還是處理器模式、使用PSP堆疊指標還是MSP堆疊指標,此時的r14等于0xfffffffd,最表示例外回傳后進入任務模式(畢竟PendSVC_Handler優先級是最低的,會回傳到任務中),SP以PSP作為堆疊指標出堆疊,出堆疊完畢后PSP指向任務堆疊的堆疊頂,當呼叫 BX R14指令后,系統以PSP作為SP指標出堆疊,把接下來要運行的新任務的任務堆疊中剩下的內容加載到CPU暫存器: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減一,然后當變數減到0時,將該變數進行重裝載timeslice_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開發”公眾號!
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/30531.html
標籤:嵌入式
