我在阅读ucos源码过程遇到的一写小问题
1.OSTaskDel()任务删除函数
在任务函数中,调用次函数传入空指针,将会删除本任务(一直以为要传入任务控制块才行,原来还可以传入空指针)
2.钩子函数 以空闲任务的钩子函数来说, 在OSIdleTaskHook() (在os_cpu_c.c)中调用 最终调用的是函数App_OS_IdleTaskHook(),也就是说如果我们想要在空闲任务的钩子函数中做其他处理就需要将代码写在App_OS_IdleTaskHook()
如下:空闲任务钩子函数相应的处理应写在这个函数里面
使能钩子函数:
App_0S_SetAllHooks():所有的钩子函数都在这个设置函数里面
3.栈大小的设置 栈是单片机 RAM 里面一段连续的内存空间,栈的大小由启动文件里面的代码配置 在多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组。这些一个个的任务栈也是存在于 RAM 中,能够使用的最大的栈也是由启动文件中Stack_Size 决定。只是多任务系统中任务的栈就是在统一的一个栈空间里面分配好一个个独立的房间,每个任务只能使用各自的房间,而裸机系统中需要使用栈的时候则可以天马行空,随便在栈里面找个空闲的空间使用 堆也是一样的:
注: 在 μC/OS-III 中,空闲任务的栈最小应该大于 128
4.任务是怎么进行切换的?
a.首先用的OSTaskCreate()创建了任务,其实在OSTaskCreate()里又先调用了OSTaskStkInit()函数初始化。
1.获取任务栈的栈顶地址, M4处理器的栈是由高地址向低地址生长的。所以初始化栈之前,要获取到栈顶地址,然后栈地址逐一递减即可
2.**任务第一次运行的时候,加载到 CPU 寄存器的环境参数我们要 预先初始化好。初始化的顺序固定,首先是异常发生时自动保存的 8 个寄存器,**即 xPSR、R15、 R14、 R12、 R3、 R2、 R1 和 R0。其中 xPSR 寄存器的位 24 必须是 1, R15PC 指针必须存的是任务的入口地址, R0 必须是任务形参,剩下的 R14、 R12、 R3、 R2 和 R1 为了调试方便,填入与寄存器号相对应的 16 进制数。
扩展:Cortex_M3/M4通用寄存器 具体的可以看我的另一篇文章: 添加链接描述
3.返回栈指针 p_stk,这个时候 p_stk 指向剩余栈的栈顶。将剩余栈的栈顶指针 p_sp 保存到任务控制块 TCB 的成员StkPtr 中
b.然后后面又调用了OSStart(): 1.系统用一个全局变量 OSRunning 来指示系统的运行状态,刚开始 系统初始化的时候,默认为停止状态,即 OS_STATE_OS_STOPPED
2.找到最高优先级并返回给OSPrioHighRdy
3.OSTCBHighRdyPtr 用于指向就绪任务中优先级最高的任 务的 TCB,指向第一个要运行的任务,在任务切换的时候用得到 OSTCBCurPtr 是系统用于指向当前正在运行的任务的 TCB 指针,在任务切换的时候用得到
4.OSStartHighRdy() 用于启动任务切换,即配置 PendSV 的优先级 为最低,然后触发 PendSV 异常,在 PendSV 异常服务函数中进行任务切换。
注:uCOS操作系统里(其实实时操作系统都一样)各类异常/中断的优先级关系:SYSTICK异常>中断>PendSv异常 PendSV 异常会自动延迟上下文切换的请求,直到其它的 ISR 都完成了处理后才放行。这样保证了中断的快速响应性又保证了操作系统的正常轮转。
OSStartHighRdy()
开始第一次上下文切换
1、配置 PendSV 异常的优先级为最低
2、在开始第一次上下文切换之前,设置 psp=0
3、触发 PendSV 异常,开始上下文切换
6 ;*******************************************************************
OSStartHighRdy
8 LDR R0, = NVIC_SYSPRI14 ; 设置 PendSV 异常优先级为最低(1)
9 LDR R1, = NVIC_PENDSV_PRI
10 STRB R1, [R0]
11
12 MOVS R0, #0 ; 设置 psp 的值为 0,开始第一次上下文切换 (2)
13 MSR PSP, R0
14
15 LDR R0, =NVIC_INT_CTRL ; 触发 PendSV 异常(3)
16 LDR R1, =NVIC_PENDSVSET
17 STR R1, [R0]
18
19 CPSIE I ; 启用总中断, NMI 和 HardFault 除外(4)
20
21 OSStartHang
22 B OSStartHang ; 程序应永远不会运行到这里
常量定义:
NVIC_INT_CTRL EQU 0xE000ED04 ; 中断控制及状态寄存器 SCB_ICSR。
NVIC_SYSPRI14 EQU 0xE000ED22 ; 系统优先级寄存器 SCB_SHPR3:
; bit16~23
NVIC_PENDSV_PRI EQU 0xFF ; PendSV 优先级的值(最低)。
NVIC_PENDSVSET EQU 0x10000000 ; 触发 PendSV 异常的值 Bit28: PENDSVSET
1)配置 PendSV 的优先级为 0XFF,即最低。在 μC/OS-III 中,上下 文切换是在 PendSV 异常服务程序中执行的,配置 PendSV 的优先级为最低,从而消灭了在中断服务程序中执行上下文切换的可能。
2)设置 PSP 的值为 0,开始第一个任务切换。在任务中,使用的栈 指针都是 PSP,后面如果判断出 PSP 为 0,则表示第一次任务切换
3)触发 PendSV 异常,如果中断启用且有编写 PendSV 异常服务函数 的话,则内核会响应 PendSV 异常,去执行 PendSV 异常服务函数。
4)开中断,因为有些用户在 main() 函数开始会先关掉中断,等全部 初始化完成后,在启动 OS 的时候才开中断。
CPSID I ;PRIMASK=1 ; 关中断
CPSIE I ;PRIMASK=0 ; 开中断
CPSID F ;FAULTMASK=1 ; 关异常
CPSIE F ;FAULTMASK=0 ; 开异常
c. **OSStartHighRdy 是由 OSStart()调用,OSStart()用来开启多任务的,如果多任务开启正常则进入OSStartHighRdy 函数触发PendSv异常并马上进行任务切换;**如果多任务开启失败的话就会进 入 OSStartHang函数
当调用 OSStartHighRdy() 函数,触发 PendSV 异常后,就需要编写 PendSV 异常服务函数,然后在里面进行任务切换。
PendSV_Handler
PendSV_Handler
CPSID I ;关中断 ; Prevent interruption during context switch
MRS R0, PSP ; 将 psp 的值加载到 R0 ; PSP is process stack pointer
; CBZ判0转移,判断 R0如果值为 0 ,则跳转到 OS_CPU_PendSVHandler_nosave
; 进行第一次任务切换的时候,R0 肯定为 0
CBZ R0, PendSVHandler_nosave ; Skip register save the first time
;判读是否使用FPU
;Is the task using the FPU context? If so, push high vfp registers.
TST R14, #0X10
IT EQ
VSTMDBEQ R0!,{S16-S31}
SUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stack
;————————————保存上下文————————————————————
; 手动存储 CPU 寄存器 R4-R11 的值到当前任务的堆栈
STM R0, {R4-R11}
; 加载 OSTCBCurPtr 指针的地址到 R1
LDR R1, =OSTCBCurPtr ; OSTCBCurPtr->OSTCBStkPtr = SP;
LDR R1, [R1]
STR R0, [R1] ; R0 is SP of process being switched out
;—————————切换上下文—————————————
; 实现 OSTCBCurPtr = OSTCBHighRdyPtr
; 把下一个要运行的任务的堆栈OSPrioHighRdy加载到 CPU 寄存器中 ; At this point, entire context of process has been saved
PendSVHandler_nosave
PUSH {R14} ; Save LR exc_return value
LDR R0, =OSTaskSwHook ; OSTaskSwHook();
BLX R0
POP {R14}
LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy;
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
LDR R0, =OSTCBCurPtr ; OSTCBCurPtr = OSTCBHighRdyPtr;
LDR R1, =OSTCBHighRdyPtr
LDR R2, [R1]
STR R2, [R0]
; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr;
LDM R0, {R4-R11} ; Restore r4-11 from new process stack
ADDS R0, R0, #0x20
;Is the task using the FPU context? If so, push high vfp registers.
TST R14, #0x10
IT EQ
VLDMIAEQ R0!, {S16-S31}
MSR PSP, R0 ; Load PSP with new process SP
ORR LR, LR, #0x04 ; Ensure exception return uses process stack
CPSIE I
BX LR ; Exception return will restore remaining context
END
PendSV 异常服务要完成两个工作:1、保存上文;2、切换下文 即保存当前正在运行的任务的环境参数;二是切换下文,即把下一个需要运行的任务的环境参数从任务栈中加载到CPU 寄存器,从而实现任务的切换。
任务之间的切换就是发生在PendSV 异常服务函数里面。 可以理解为——触发了PendSV 异常->函数自动跳转到PendSV 异常服务函数->执行异常服务函数->实现任务的切换。
OSStartHighRdy :用得很少,只是在任务开启时在OSStart()函数里调用 OSCtxSw :实现任务级的任务切换 OSIntCtxSw :实现中断级的任务切换
知道了任务切换的大致过程那么什么会发送任务切换(任务调度)呢?
1.使用延时函数OSTimeDly()或者OSTimeDlyHMSM(); 2.OSStart()启动任务调度器 3.删除任务; 4.任务通过调用OSTaskSuspend()将自身挂起; 5.任务解挂某个挂起的任务; 6.用户调用OSSched(); 7.释放信号量或者发送消息,也可通过配置相应的参数不发生任务调度; 8.任务等待的事情还没发生(等待信号量,消息队列等); 9.任务取消等待; 10.删除一个内核对象; 11.任务改变自身的优先级或者其他任务的优先级; 12.退出所有的嵌套中断; 13.通过OSSchedUnlock()给调度器解锁; 14.任务调用OSSchedRoundRobinYield()放弃其执行时间片。
任务调度器有2种:任务级调度器和中断级调度器。
任务级调度器为函数OSSched(); 中断级调度器为函数OSIntExit(),当退出外部中断服务函数的时候使用中断级任务调度。
OSSched()
void OSSched (void)
{
CPU_SR_ALLOC();
if (OSIntNestingCtr > (OS_NESTING_CTR)0) { /* 检查是否在中断服务函数中调用 */
return; /* 任务型调度函数,不能在中断中使用 */
}
if (OSSchedLockNestingCtr > (OS_NESTING_CTR)0) { /* 检查调度器是否加锁 */
return;
}
CPU_INT_DIS();
OSPrioHighRdy = OS_PrioGetHighest(); /* 获取任务就绪表中就绪了的最高优先级任务 */
OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;
if (OSTCBHighRdyPtr == OSTCBCurPtr) { /* Current task is still highest priority task? */
CPU_INT_EN(); /* Yes ... no need to context switch */
return;
}
#if OS_CFG_TASK_PROFILE_EN > 0u
OSTCBHighRdyPtr->CtxSwCtr++; /* Inc. # of context switches to this task */
#endif
OSTaskCtxSwCtr++; /* Increment context switch counter */
#if defined(OS_CFG_TLS_TBL_SIZE) && (OS_CFG_TLS_TBL_SIZE > 0u)
OS_TLS_TaskSw();
#endif
OS_TASK_SW(); /* 执行任务切换 */
CPU_INT_EN();
}
在OSSched()中真正执行任务切换的是宏OS_TASK_SW()(在os_cpu.h中定义),**宏OS_TASK_SW()就是函数OSCtxSW(),OSCtxSW()是os_cpu_a.asm中用汇编写的一段代码,**OSCtxSW()要做的就是将当前任务的CPU寄存器的值保存在任务堆栈中,也就是保存现场,保存完当前任务的现场后将新任务的OS_TCB中保存的任务堆栈指针的值加载到CPU的堆栈指针寄存器中,最后还要从新任务的堆栈中恢复CPU寄存器的值。
中断级调度器函数OSIntExit()
void OSIntExit (void)
{
CPU_SR_ALLOC();
if (OSRunning != OS_STATE_OS_RUNNING) { /* 判断UCOSIII是否运行 */
return;
}
CPU_INT_DIS();
if (OSIntNestingCtr == (OS_NESTING_CTR)0) { /* 中断嵌套计数器 */
CPU_INT_EN();
return;
}
OSIntNestingCtr--;
if (OSIntNestingCtr > (OS_NESTING_CTR)0) { /* 还有中断发生,跳回到中断服务程序中,不做任务切换 */
CPU_INT_EN();
return;
}
if (OSSchedLockNestingCtr > (OS_NESTING_CTR)0) { /* 检查调度器是否加锁 */
CPU_INT_EN();
return;
}
OSPrioHighRdy = OS_PrioGetHighest(); /* Find highest priority */
OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr; /* Get highest priority task ready-to-run */
if (OSTCBHighRdyPtr == OSTCBCurPtr) { /* Current task still the highest priority? */
CPU_INT_EN(); /* Yes */
return;
}
#if OS_CFG_TASK_PROFILE_EN > 0u
OSTCBHighRdyPtr->CtxSwCtr++; /* Inc. # of context switches for this new task */
#endif
OSTaskCtxSwCtr++; /* Keep track of the total number of ctx switches */
#if defined(OS_CFG_TLS_TBL_SIZE) && (OS_CFG_TLS_TBL_SIZE > 0u)
OS_TLS_TaskSw();
#endif
OSIntCtxSw(); /*执行任务切换 */
CPU_INT_EN();
}
在中断级调度器中真正完成任务切换的就是中断级任务切换函数OSIntCtxSW(),与任务级切换函数OSCtxSW()不同的是,由于进入中断的时候现场已经保存过了,所以OSIntCtxSW()不需要像OSCtxSW()一样先保存当前任务现场,只需要做OSCtxSW()的后半部分工作,也就是从将要执行的任务堆栈中恢复CPU寄存器的值。
其中:OSIntNestingCtr为中断嵌套计数器,进入中断服务函数后我们要调用OSIntEnter()函数,在这个函数中会将OSIntNestingCtr加1,用来记录中断嵌套的次数。而OSIntExit()是在退出中断服务函数时调用的,因此中断嵌套计数器要减1。如果减1之后,OSIntNestingCtr还大于0,说明还有其他的中断发生,那么就跳回到中断服务程序中,不需要做任务切换。
小结: 任务级调度器函数OSSched()中真正执行任务切换的是函数OSCtxSW(); 中断级调度器函数OSIntExit()中真正完成任务切换的是函数OSIntCtxSW()。
其实后面这两个函数就是UCOSIII的任务切换函数。任务切换分为两种:任务级切换和中断级切换。然后就触发PendSV. 任务级切换函数为:OSCtxSw(); 中断级切换函数为:OSIntCtxSw()。
OSCtxSw
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
OSIntCtxSw
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
5.空闲任务 堆栈的定义
创建空闲任务
空闲任务函数(什么也没有做) 空闲任务初始化在OSInit()
空闲任务的初始化在 OSInit() 在完成,意味着在系统还没有启动之前空闲任务就已经创建好,必须创建空闲任务(不需要我们手动创建),因为CPU不能停下来
6.阻塞延时
阻塞延时的阻塞是指任务调用该延时函数后,任务会被剥离 CPU 使用权,然后进入阻塞状态,直到延时结束,任务重新获取 CPU 使用权才可以继续运行。在任务阻塞的这段时间, CPU 可以去执行其他的任务,如果其他的任务也在延时状态,那么 CPU 就将运行空闲任务
有两个阻塞延时函数OSTimeDly和OSTimeDlyHMSM 两个函数都有调用 OSSched(); 任务调用 OSTimeDly() 函数之后,任务就处于阻塞态,需要将任务从就绪列表中移除
OSTimeDly()部分代码:
void OSTimeDly (OS_TICK dly,
OS_OPT opt,
OS_ERR *p_err)
{
CPU_SR_ALLOC();
OS_CRITICAL_ENTER();
OSTCBCurPtr->TaskState = OS_TASK_STATE_DLY;
OS_TickListInsert(OSTCBCurPtr,
dly,
opt,
p_err);
if (*p_err != OS_ERR_NONE) {
OS_CRITICAL_EXIT_NO_SCHED();
return;
}
//移除当前任务控制块
OS_RdyListRemove(OSTCBCurPtr); /* Remove current task from ready list */
OS_CRITICAL_EXIT_NO_SCHED();
OSSched(); /* Find next task to run! 进行任务调度 */
*p_err = OS_ERR_NONE;
}
OSTimeDlyHMSM ()部分代码
void OSTimeDlyHMSM (CPU_INT16U hours,
CPU_INT16U minutes,
CPU_INT16U seconds,
CPU_INT32U milli,
OS_OPT opt,
OS_ERR *p_err)
{
CPU_SR_ALLOC();
if (ticks > (OS_TICK)0u) {
OS_CRITICAL_ENTER();
OSTCBCurPtr->TaskState = OS_TASK_STATE_DLY;
OS_TickListInsert(OSTCBCurPtr,
ticks,
opt_time,
p_err);
if (*p_err != OS_ERR_NONE) {
OS_CRITICAL_EXIT_NO_SCHED();
return;
}
//移除当前任务 控制块
OS_RdyListRemove(OSTCBCurPtr); /* Remove current task from ready list */
OS_CRITICAL_EXIT_NO_SCHED();
OSSched(); /* Find next task to run! 任务调度 */
*p_err = OS_ERR_NONE;
} else {
*p_err = OS_ERR_TIME_ZERO_DLY;
}
}
7.时间戳
如果要测量一段代码 A 的时间,那么可以在代码段 A 运行前记录一个时 间点 TimeStart,在代码段 A 运行完记录一个时间点 TimeEnd,那么代码段 A 的运行时间 TimeUse就等于 TimeEnd 减去 TimeStart。这里面的两个时间点 TimeEnd 和 TimeStart,就叫作时间戳,时间戳实际上就是一个时间点
在 ARM Cortex-M 系列内核中,有一个 DWT 的外设,该外设有一个 32 位的寄存器叫 CYCCNT,它是一个向上的计数器,记录的是内核时钟 HCLK 运行的个数,当 CYCCNT 溢出之后,会清零重新开始向上计数。该计数器在 μC/OS-III 中正好被用来实现时间戳的功能。
CPU_Init() 函数在 cpu_core.c主要做三件事: 1、初始化时间戳, 2、初始化中断禁用时间测量, 3、初始化 CPU 名字。
CPU_TS_Init() 是时间戳初始化函数 CPU_TS_TmrFreq_Hz 是一个在 cpu_core.h 中定义的全局变量,表示 CPU 的系统时钟,具体大小跟硬件相关,
时间戳定时器初始化函数 CPU_TS_TmrInit() 在 cpu_core.c 实现 在里面启用了外设DWT
BSP_CPU_ClkFreq() 是一个用于获取 CPU 的 HCLK 时钟的 BSP 函 数,具体跟硬件相关,
8.CPU_SR_ALLOC() 该宏定义的作用:
#define CPU_SR_ALLOC() CPU_SR cpu_sr = (CPU_SR)0
定义一个局部变量,用来存 CPU 关中断前的中断状态。
9.任务优先级 在任务创建的时候OSTaskCreate()函数就已经将任务的优先级加入就绪表中了 将任务插入就绪列表,这里需要分成两步来实现:
1、根据优先级置位优先级表中的相应位置; 2、将任务 TCB 放到 OSRdyList[优先级] 中,如果同一个优先级有多个任务,那么这些任务的 TCB 就会被放到 OSRdyList[优先级] 串成一个双向链表
10.OSStart() 具体哪一个任务最先运行,由优先级决定,运行所建的任务 (1): 调取 OS_PrioGetHighest() 函数从全局变量优先级表 OSPrioTbl[] 获取最高的优先级,放到 OSPrioHighRdy 这个全局变量中,然后把 OSPrioHighRdy 的值再赋给当前优先级 OSPrioCur 这个全局变量。在任务切换的时候需要用到 OSPrioHighRdy 和OSPrioCur 这两个全局变量
(2): 根据 **OSPrioHighRdy 的值,**作为全局变量 **OSRdyList[] 的下标索引找到最高优先级任务的 TCB,**传给全局变量 OSTCBHighRdyPtr,然后再将 OSTCBHighRdyPtr赋值给 OSTCBCurPtr。在任务切换的时候需要使用到 OSTCBHighRdyPtr 和 OSTCBCurPtr 这两个全局变量
四个全局变量:
OSPrioCur 当前优先级
OSPrioHighRdy 最高优先级
OSTCBCurPtr 当前正在运行的任务控制块
OSTCBHighRdyPtr 最高优先级任务控制块
11.在使用延迟函数后,怎么判断任务的延时时间是否到期,并且重新加入到就绪列表中的? 时基列表和就绪列表的操作都是在延迟函数中
时基列表是跟时间相关的,处于延时的任务和等待事件有超时限制的任务都会从就绪列表中移除,然后插入时基列表,时基列表在 OSTimeTick 中更新,如果任务的延时时间结束或者超时到期,就会让任务就绪,从时基列表移除,插入就绪列表。在 OS 中只实现了两个列表,另一个是就绪列表。
时基列表在代码层面上由全局数组 OSCfg_TickWheel[] 和全局变量 OSTickCtr 构成
OSTickCtr 为 SysTick 周期计数器,记录系统启动到现在或者 从上一次复位到现在经过了多少个 SysTick 周期
struct os_tick_spoke {
OS_TCB *FirstPtr;//1 Pointer to list of tasks in tick spoke
OS_OBJ_QTY NbrEntries;//2 Current number of entries in the tick spoke
OS_OBJ_QTY NbrEntriesMax;//3 Peak number of entries in the tick spoke
};
OS_TICK_SPOKE OSCfg_TickWheel [OS_CFG_TICK_WHEEL_SIZE];
1.时基列表 OSCfg_TickWheel[] 的每个成员都包含一条单向链表,被插入该条链表的 TCB 会按照延时时间做升序排列。 FirstPtr 用于指向这条单向链表的第一个节点。
2.时基列表 OSCfg_TickWheel[] 的每个成员都包含一条单向链表NbrEntries 表示该条单向链表当前有多少个节点
3.时基列表 OSCfg_TickWheel[] 的每个成员都包含一条单向链表, NbrEntriesMax 记录该条单向链表最多的时候有多少个节点,在增加节点的时候会刷新,在删除节点的时候不刷新
时基列表参数:
OS_TCB *TickNextPtr;
OS_TCB *TickPrevPtr;
OS_TICK_SPOKE *TickSpokePtr; /* Pointer to tick spoke if task is in the tick list */
OS_TICK TickCtrPrev; /* Previous time when task was ready */
OS_TICK TickCtrMatch; /* Absolute time when task is going to be ready */
OS_TICK TickRemain; /* Number of ticks remaining for a match (updated at ... */
/* ... run-time by OS_StatTask() */
时基列表 OSCfg_TickWheel[] 的每个成员都包含一条单向链表,被插入该条链表的 TCB 会按照延时时间做升序排列,为了 TCB 能按照延时时间从小到大串接在一起,
图时基列表中有两个 TCB 是在时基列表 OSCfg_TickWheel[] 索引 11 这条链表里面插入了两个 TCB,一个需要延时 1 个时钟周期,另外一个需要延时 13 个时钟周期。
TickNextPtr 用于指向链表中的下一个 TCB 节点
TickPrevPtr 用于指向链表中的上一个 TCB 节点
TickRemain 用于设置任务还需要等待多少个时钟周期,每到来一个时钟周期,该值会递减
TickCtrMatch 的值等于时基计数器 OSTickCtr 的值加上 TickRemain 的值,(TickCtrMatch是以OSTickCtr 为基础加上去的) 当 TickCtrMatch 的值等于 OSTickCtr 的值的时候,表示等待到期, TCB 会从链表中删除。
TickSpokePtr 每个被插入链表的 TCB 都包含一个字段 TickSpokePtr,用于回指到链表的根部。
OS_TickListInit() 函数 OS_TickListInit() 函数用于初始化时基列表,即将全局变量OSCfg_TickWheel[] 的数据域全部初始化为 0,一个初始化为 0 的的时基列表见图时基列表的数据域全部被初始化为 0 函数:
OS_TickListInsert() 函数 OS_TickListInsert() 函数用于往时基列表中插入一个任务 TCB, 将一个任务插入时基列表,根据延时时间的大小升序排列
/* 插入 OSCfg_TickWheel[spoke] 的第一个节点 */
if (p_spoke->NbrEntries == (OS_OBJ_QTY)0u) { /* First entry in the spoke */
p_tcb->TickNextPtr = (OS_TCB *)0;
p_tcb->TickPrevPtr = (OS_TCB *)0;
p_spoke->FirstPtr = p_tcb;
p_spoke->NbrEntries = (OS_OBJ_QTY)1u;
} else {
p_tcb1 = p_spoke->FirstPtr; /* Point to current first TCB in the list */
while (p_tcb1 != (OS_TCB *)0) {
p_tcb1->TickRemain = p_tcb1->TickCtrMatch /* Compute time remaining of current TCB in list */
- OSTickCtr;
if (p_tcb->TickRemain > p_tcb1->TickRemain) { /* Do we need to insert AFTER current TCB in list? */
if (p_tcb1->TickNextPtr != (OS_TCB *)0) { /* Yes, are we pointing at the last TCB in the list? */
p_tcb1 = p_tcb1->TickNextPtr; /* No, Point to next TCB in the list */
} else {
p_tcb->TickNextPtr = (OS_TCB *)0;
p_tcb->TickPrevPtr = p_tcb1;
p_tcb1->TickNextPtr = p_tcb; /* Yes, TCB to add is now new last entry in the list */
p_tcb1 = (OS_TCB *)0; /* Break loop */
}
} else { /* Insert before the current TCB */
if (p_tcb1->TickPrevPtr == (OS_TCB *)0) { /* Are we inserting before the first TCB? */
p_tcb->TickPrevPtr = (OS_TCB *)0;
p_tcb->TickNextPtr = p_tcb1;
p_tcb1->TickPrevPtr = p_tcb;
p_spoke->FirstPtr = p_tcb;
} else { /* Insert in between 2 TCBs already in the list */
p_tcb0 = p_tcb1->TickPrevPtr;
p_tcb->TickPrevPtr = p_tcb0;
p_tcb->TickNextPtr = p_tcb1;
p_tcb0->TickNextPtr = p_tcb;
p_tcb1->TickPrevPtr = p_tcb;
}
p_tcb1 = (OS_TCB *)0; /* Break loop */
}
}
//结点成功插入
p_spoke->NbrEntries++;
}
if (p_spoke->NbrEntriesMax NbrEntries) { /* Keep track of maximum # of entries in each spoke */
p_spoke->NbrEntriesMax = p_spoke->NbrEntries;
}
p_tcb->TickSpokePtr = p_spoke; /* Link back to tick spoke */
*p_err = OS_ERR_NONE;
OS_TickListRemove() 函数
OS_TickListRemove() 用于从时基列表删除一个指定的 TCB 节点,节点删除成功,链表中的节点计数器 NbrEntries 减一。
OS_TickListUpdate() 函数
OS_TickListUpdate() 在每个 SysTick 周期到来时在 OSTimeTick() 被调用,用于更新时基计数器OSTickCtr,扫描时基列表中的任务延时是否到期,
OSTickCtr++; /* Keep track of the number of ticks */
spoke = (OS_TICK_SPOKE_IX)(OSTickCtr % OSCfg_TickWheelSize);
p_spoke = &OSCfg_TickWheel[spoke];
计算要扫描的时基列表的索引,每次只扫描一条链表。时基列表里面有可能有多条链表,为啥只扫描其中一条链表就可以?
因为任务在插入时基列表的时候,插入的索引值 spoke_insert 是通过 TickCtrMatch 对 OSCfg_TickWheelSize 求余得出,现在需要扫描的索引值 spoke_update 是通过 OSTickCtr 对 OSCfg_TickWheelSize 求余得出, TickCtrMatch 的值等于 OSTickCt 加上 TickRemain,只有在经过TickRemain 个时钟周期后,spoke_update 的值才有可能等于 spoke_insert。如果算出的 spoke_update 小于 spoke_insert,且 OSCfg_TickWheel[spoke_update] 下的链表的任务没有到期,那后面的定都没有到期,不用继续扫描。
OSTimeDlyHMSM()函数中时基列表的操作
OS_TickListUpdate();函数的调用是在OS_TickTask()时钟节拍任务(必须创建的任务)
12.时间片 OS 支持同一个优先级下可以有多个任务的功能,这些任务可以分配不同的时间片,当任务时间片用完的时候,任务会从链表的头部移动到尾部,让下一个任务共享时间片,以此循环。
/* 时间片相关字段 */
OS_TICK TimeQuanta;(1)
OS_TICK TimeQuantaCtr;(2)
(1): TimeQuanta 表示任务需要多少个时间片,单位为系统时钟周 期 Tick
2): TimeQuantaCtr 表示任务还剩下多少个时间片,每到来一个系 统时钟周期, TimeQuantaCtr 会减一,当 TimeQuantaCtr 等于零的时候,表示时间片用完,任务的 TCB 会从就绪列表链表的头部移动到尾部,好让下一个任务共享时间片
OS_SchedRoundRobin() 函数 时间片调度函数 OS_SchedRoundRobin() 在 os_core.c 中实现,在 OSTimeTick() 调用, 只有在同一优先级并且处于就绪状态任务才会以共享时间片的形式运行。 不同优先级以可剥夺方式运行。