临界段保护:
(1)什么是临界段:
临界段用一句话概括就是一段在执行的时候不能被中断的代码段。在 FreeRTOS 里面,这个临界段最常出现的就是对全局变量的操作,全局变量就好像是一个枪把子,谁都可以对他开枪,但是我开枪的时候,你就不能开枪,否则就不知道是谁命中了靶子。
**那么什么情况下临界段会被打断?一个是系统调度,还有一个就是外部中断。**在FreeRTOS,系统调度,最终也是产生 PendSV 中断,在 PendSV Handler 里面实现任务的切换,所以还是可以归结为中断。既然这样,FreeRTOS 对临界段的保护最终还是回到对中断的开和关的控制。
(2)Cortex-M 内核快速关中断指令
为了快速地开关中断, Cortex-M 内核专门设置了一条 CPS 指令,有 4 种用法,具体见代码清单
CPSID I ;PRIMASK=1 ;关中断
CPSIE I ;PRIMASK=0 ;开中断
CPSID F ;FAULTMASK=1 ;关异常
CPSIE F ;FAULTMASK=0 ;开异常
PRIMASK 和 FAULTMAST 是 Cortex-M内核 里面三个中断屏蔽寄存器中的两个,还有一个是 BASEPRI,有关这三个寄存器的详细用法见表格:

但是,在 FreeRTOS 中,对中断的开和关是通过操作 BASEPRI 寄存器来实现的,即大于等于 BASEPRI 的值的中断会被屏蔽,小于 BASEPRI 的值的中断则不会被屏蔽,不受FreeRTOS 管理。用户可以设置 BASEPRI 的值来选择性的给一些非常紧急的中断留一条后路。
(3)关中断
FreeRTOS 关中断的函数在 portmacro.h 中定义,分不带返回值和带返回值两种,具体实现见代码
/*———————————————————–*/
/* 不带返回值的关中断函数,不能嵌套,不能在中断里面使用 */
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
/* Set BASEPRI to the max syscall priority to effect a critical
section. */
msr basepri, ulNewBASEPRI
dsb
isb
}
}
/*———————————————————–*/
/*———————————————————–*/
/* 带返回值的关中断函数,可以嵌套,可以在中断里面使用 */
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
/* Set BASEPRI to the max syscall priority to effect a critical
section. */
mrs ulReturn, basepri
msr basepri, ulNewBASEPRI
dsb
isb
}
return ulReturn;
}
/*———————————————————–*/
(3.1)不带返回值的关中断函数
不带返回值的关中断函数,不能嵌套,不能在中断里面使用。不带返回值的意思是:在往 BASEPRI 写入新的值的时候,不用先将 BASEPRI 的值保存起来,即不用管当前的中断状态是怎么样的,既然不用管当前的中断状态,也就意味着这样的函数不能在中断里面调用。
在vPortRaiseBASEPRI()函数中:
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY: configMAX_SYSCALL_INTERRUPT_PRIORITY 是 一 个 在FreeRTOSConfig.h 中定义的宏,即要写入到 BASEPRI 寄存器的值。该宏默认定义为 191(1011 1111),高四位有效,即等于 0xb0或者是 11,即优先级大于等于 11 的中断都会被屏蔽,11 以内的中断则不受 FreeRTOS 管理。
msr basepri, ulNewBASEPRI: 将 configMAX_SYSCALL_INTERRUPT_PRIORITY 的值写入BASEPRI 寄存器,实现关中断(准确来说是关部分中断)。
(补充:像之前的MSP、SPS、还有现在的BASEPRI,这些特殊寄存器不在内存里,就在 CPU 内核内部,所以我们去读、写的时候是把内核内部的一个物理寄存器直接搬进通用寄存器,不走总线、不经过内存。
类比:外设寄存器是小区快递柜——有柜号(地址),得走一趟(总线)去取;内核寄存器是口袋里的东西——没有门牌号,伸手就掏(mrs/msr)。
这也是为什么 PendSV 里取任务栈顶必须用 ( mrs r0, psp: ) PSP压根没有地址,你想 ldr 都没地方填地址。而挂起 PendSV 却能写 0xE000ED04——因为 ICSR 是映射到内存空间的外设寄存器,有地址。)
(3.2)带返回值的关中断函数
带返回值的关中断函数,可以嵌套,可以在中断里面使用。带返回值的意思是:在往 BASEPRI 写入新的值的时候,先将 BASEPRI 的值保存起来,在更新完BASEPRI 的值的时候,将之前保存好的 BASEPRI 的值返回,返回的值作为形参传入开中断函数。
在ulPortRaiseBASEPRI()函数中:
uint32_t ulRetu在这里插入代码片rn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY: configMAX_SYSCALL_INTERRUPT_PRIORITY 是 一 个 在FreeRTOSConfig.h 中定义的宏,即要写入到 BASEPRI 寄存器的值。该宏默认定义为 191,高四位有效,即等于 0xb0,或者是 11,即优先级大于等于 11 的中断都会被屏蔽,11 以内的中断则不受 FreeRTOS 管理
mrs ulReturn, basepri: mrs 是读取 BASEPRI 的当前值到 ulReturn(保存旧值),记录当前哪些中断被关闭。
msr basepri, ulNewBASEPRI: 更新 BASEPRI 的值。
return ulReturn: 返回原来 BASEPRI 的值。
(4)开中断:
FreeRTOS 开中断的函数在 portmacro.h 中定义,具体实现见代码清单:
/* 不带中断保护的开中断函数 */
#define portENABLE_INTERRUPTS()vPortSetBASEPRI( 0 )
/* 带中断保护的开中断函数 */
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x)vPortSetBASEPRI(x)
/*———————————————————–*/
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
/* Barrier instructions are not used as this function is only used to
lower the BASEPRI value. */
msr basepri, ulBASEPRI
}
}
/*———————————————————–*/
vPortSetBASEPRI( 0 )函数: 不带中断保护的开中断函数,直接将 BASEPRI 的值设置为 0,与portDISABLE_INTERRUPTS()成对使用。
**vPortSetBASEPRI(x)函数:**带中断保护的开中断函数,将上一次关中断时保存的 BASEPRI 的值作为形参 ,与portSET_INTERRUPT_MASK_FROM_ISR()成对使用。
void vPortSetBASEPRI( uint32_t ulBASEPRI ): 开中断函数,具体是将传进来的形参更新到 BASEPRI 寄存器。根据传进来形参的不同,分为中断保护版本与非中断保护版本。
(5)进入/退出临界段的宏
进入和退出临界段的宏在 task.h 中定义,具体见代码清单:
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
进入和退出临界段的宏分中断保护版本和非中断版本,但最终都是通过开/关中断来实现。有关开/光中断的底层代码我们已经讲解,那么接下来的退出和进入临界段的代码配套注释来理解即可。
(5.1)进入临界段
1.不带中断保护版本,不能嵌套
/*
*************************************************************************
* 临界段相关函数
*************************************************************************
*/
void vPortEnterCritical( void )
{
portDISABLE_INTERRUPTS();
uxCriticalNesting++;
/* This is not the interrupt safe version of the enter critical function so
assert() if it is being called from an interrupt context. Only API
functions that end in "FromISR" can be used in an interrupt. Only assert if
the critical nesting count is 1 to protect against recursive calls if the
assert function also uses a critical section. */
if( uxCriticalNesting == 1 )
{
//configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
}
}
void vPortExitCritical( void )
{
//configASSERT( uxCriticalNesting );
uxCriticalNesting—;
if( uxCriticalNesting == 0 )
{
portENABLE_INTERRUPTS();
}
}
uxCriticalNesting: 是在 port.c 中定义的静态变量,表示临界段嵌套计数 器 , 默 认 初 始 化 为 0xaaaaaaaa , 在 调 度 器 启 动 时 会 被 重 新 初 始 化 为 0 :vTaskStartScheduler()->xPortStartScheduler()->uxCriticalNesting = 0。
if( uxCriticalNesting == 1 ): 如果 uxCriticalNesting 等于 1,即一层嵌套,要确保当前没有中断活跃,即内核外设 SCB 中的中断和控制寄存器 SCB_ICSR 的低 8 位要等于 0。
进入临界段,带中断保护版本且可以嵌套的代码实现具体见代码清单
2.带中断保护版本,可以嵌套
/* ==========进入临界段,带中断保护版本,可以嵌套=============== */
/* 在 task.h 中定义 */
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
/* 在 portmacro.h 中定义 */
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
/* 在 portmacro.h 中定义 */
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
mrs ulReturn, basepri
msr basepri, ulNewBASEPRI
dsb
isb
}
return ulReturn;
}
(5.2 )退出临界段
退出临界段,不带中断保护版本且不能嵌套的代码实现具体见代码清单
/* ==========退出临界段,不带中断保护版本,不能嵌套=============== */
/* 在 task.h 中定义 */
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
/* 在 portmacro.h 中定义 */
#define portEXIT_CRITICAL() vPortExitCritical()
/* 在 port.c 中定义 */
void vPortExitCritical( void )
{
configASSERT( uxCriticalNesting );
uxCriticalNesting—;
if ( uxCriticalNesting == 0 )
{
portENABLE_INTERRUPTS();
}
}
/* 在 portmacro.h 中定义 */
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
/* 在 portmacro.h 中定义 */
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
msr basepri, ulBASEPRI
}
}
/* ==========退出临界段,带中断保护版本,可以嵌套=============== */
/* 在 task.h 中定义 */
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
/* 在 portmacro.h 中定义 */
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
/* 在 portmacro.h 中定义 */
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
msr basepri, ulBASEPRI
}
}
(6)临界段代码的应用
在 FreeRTOS 中,对临界段的保护出现在两种场合,一种是在中断场合一种是在非中断场合,具体的应用见。
/* 在中断场合,临界段可以嵌套 */
{
uint32_t ulReturn;
/* 进入临界段,临界段可以嵌套 */
ulReturn = taskENTER_CRITICAL_FROM_ISR();
/* 临界段代码 */
/* 退出临界段 */
taskEXIT_CRITICAL_FROM_ISR( ulReturn );
}
/* 在非中断场合,临界段不能嵌套 */
{
/* 进入临界段 */
taskENTER_CRITICAL();
/* 临界段代码 */
/* 退出临界段*/
taskEXIT_CRITICAL();
}
(7)这样看起来很乱,下面总结一下就清楚了:
为什么定义这么多宏:三层分工
FreeRTOS 要跑在几十种内核上(M3、M4、RISC-V……),每种内核关中断的指令都不一样。所以分成三层:
taskENTER_CRITICAL() ← 用户层:你写程序只管拨这个"总机号码"
= portENTER_CRITICAL() ← 移植层:换内核时只改这一层
= vPortRaiseBASEPRI() ← 底层:msr basepri, 191(真正办事的汇编)
类比:总机号码 → 内部转接台 → 具体办事员。你永远拨同一个号码;公司搬到别的楼(换内核),只改转接表,你的号码不变。这就是"跳来跳去"的全部原因——你只跟第一层打交道,后面两层是移植者的事。
为什么分"中断版"和"非中断版"两套:一条规则
| 进入 | taskENTER_CRITICAL() | ulReturn = taskENTER_CRITICAL_FROM_ISR() |
| 退出 | taskEXIT_CRITICAL() | taskEXIT_CRITICAL_FROM_ISR(ulReturn) |
| 嵌套靠什么 | 计数器 uxCriticalNesting(最外层退出才真正开中断) | 保存旧值、退出时恢复旧值 |
| 退出动作 | 直接写 0(全开) | 恢复来时状态(不能写 0) |
为什么不能混用? 因为任务里进临界段之前,中断本来就是全开的——退出写 0 没毛病。但中断里进临界段之前,BASEPRI 可能已经有值(比如外层中断已经屏蔽了一批),退出时写 0 就把外层的中断状态也冲掉了。所以中断版必须"记住来时状态,走时恢复原样"。
一句话记:非中断版 = 退出全开;中断版 = 恢复原样。
(7.1) 四个宏一路展开到底
① 非中断版 · 进入:taskENTER_CRITICAL()
taskENTER_CRITICAL()
→ portENTER_CRITICAL()(关中断uxCriticalNesting++,开中断uxCriticalNesting–)
→ vportDISABLE_INTERRUPTS()
→ vPortRaiseBASEPRI()
最底层:
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; /* = 191 */
__asm
{
msr basepri, ulNewBASEPRI /* BASEPRI = 191:屏蔽优先级 ≥ 11 的中断 */
dsb
isb
}
}
② 非中断版 · 退出:taskEXIT_CRITICAL()
taskEXIT_CRITICAL()
→ portEXIT_CRITICAL()
→ vPortExitCritical()
└─ 嵌套计数减到 0 时 → portENABLE_INTERRUPTS() → vPortSetBASEPRI(0)
最底层:
void vPortExitCritical( void )
{
uxCriticalNesting—; /* 嵌套计数减 1 */
if( uxCriticalNesting == 0 ) /* 最外层退出才真正开中断 */
{
portENABLE_INTERRUPTS(); /* = vPortSetBASEPRI( 0 ) */
}
}
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
msr basepri, ulBASEPRI /* BASEPRI = 0:全部打开 */
}
}
③ 中断版 · 进入:taskENTER_CRITICAL_FROM_ISR()
taskENTER_CRITICAL_FROM_ISR()
→ portSET_INTERRUPT_MASK_FROM_ISR()
→ ulPortRaiseBASEPRI()
最底层:
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
mrs ulReturn, basepri /* 先保存旧值(来时状态) */
msr basepri, ulNewBASEPRI /* BASEPRI = 191:屏蔽 ≥ 11 的中断 */
dsb
isb
}
return ulReturn; /* 旧值交给调用者保管 */
}
④ 中断版 · 退出:taskEXIT_CRITICAL_FROM_ISR(x)
taskEXIT_CRITICAL_FROM_ISR( x )
→ portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
→ vPortSetBASEPRI( x )
最底层:
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI ) /* 形参 = 之前保存的旧值 */
{
__asm
{
msr basepri, ulBASEPRI /* 恢复来时状态,不是写 0 */
}
}
最终效果对照
| taskENTER_CRITICAL() | vPortRaiseBASEPRI() | BASEPRI ← 191(屏蔽 ≥11) |
| taskEXIT_CRITICAL() | vPortExitCritical() → vPortSetBASEPRI(0) | 计数减 1,到 0 时 BASEPRI ← 0(全开) |
| taskENTER_CRITICAL_FROM_ISR() | ulPortRaiseBASEPRI() | 存旧值 → BASEPRI ← 191 → 返回旧值 |
| taskEXIT_CRITICAL_FROM_ISR(x) | vPortSetBASEPRI(x) | BASEPRI ← 旧值(恢复原样) |
发现没:绕了那么多层宏,最终全是 msr basepri 一条指令的包装——要么写 191(关门),要么写 0 或旧值(开门)。
空闲任务与阻塞延时的实现
前言:
在上一章节中,任务体内的延时使用的是软件延时,即还是让 CPU 空等来达到延时的效果。使用 RTOS 的很大优势就是榨干 CPU 的性能,永远不能让它闲着,任务如果需要延时也就不能再让 CPU 空等来实现延时的效果。
RTOS 中的延时叫阻塞延时,即任务需要延时的时候,任务会放弃 CPU 的使用权,CPU 可以去干其它的事情,当任务延时时间到,重新获取 CPU 使用权,任务继续运行,这样就充分地利用了 CPU 的资源,而不是干等着。
当任务需要延时,进入阻塞状态,那 CPU 又去干什么事情了?如果没有其它任务可以运行,RTOS 都会为 CPU 创建一个空闲任务,这个时候 CPU 就运行空闲任务。
在 FreeRTOS 中,空闲任务是系统在【启动调度器】的时候创建的优先级最低的任务,空闲任务主体主要是做一些系统内存的清理工作(释放栈的空间,后面学习删除任务的时候可能会出现删除栈无法释放栈空间,这时空闲任务就可以帮忙释放了)。但是为了简单起见,我们本章实现的空闲任务只是对一个全局变量进行计数。
鉴于空闲任务的这种特性,在实际应用中,当系统进入空闲任务的时候,可在空闲任务中让单片机进入休眠或者低功耗等操作。
1.实现空闲任务
目前我们在创建任务时使用的栈和 TCB 都使用的是静态的内存,即需要预先定义好内存,空闲任务也不例外。有关空闲任务的栈和 TCB 需要用到的内存空间均在 main.c 中定义。
/*创建空闲任务*/
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
/*定义任务的TCB,用来描述该任务的信息*/
TCB_t IdleTaskTCB;
**configMINIMAL_STACK_SIZE:**空闲任务的栈是一个定义好的数组,大小由 FreeRTOSConfig.h 中定义的宏 configMINIMAL_STACK_SIZE 控制,默认为 128,单位为字,即 512 个字节。
**IdleTaskTCB:**任务控制块是每一个任务必须的,空闲任务的的任务控制块我们在 main.c 中定义,是一个全局变量
(1)创建空闲任务
当定义好空闲任务的栈,任务控制块后,就可以创建空闲任务。空闲任务在调度器启动函数 vTaskStartScheduler()中创建:
/*======================================创建空闲任务start==============================================*/
TCB_t *pxIdleTaskTCBBuffer = NULL; /* 用于指向空闲任务控制块 */
StackType_t *pxIdleTaskStackBuffer = NULL; /* 用于空闲任务栈起始地址 */
uint32_t ulIdleTaskStackSize;
/* 获取空闲任务的内存:任务栈和任务TCB */
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer,
&pxIdleTaskStackBuffer,
&ulIdleTaskStackSize );
xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任务入口 */
(char *)"IDLE", /* 任务名称,字符串形式 */
(uint32_t)ulIdleTaskStackSize , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(StackType_t *)pxIdleTaskStackBuffer, /* 任务栈起始地址 */
(TCB_t *)pxIdleTaskTCBBuffer ); /* 任务控制块 */
/* 将任务添加到就绪列表 */
vListInsertEnd( &( pxReadyTasksLists[0] ), &( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem ) );
/*======================================创建空闲任务end================================================*/
我们先前在main.c定义了空闲任务的栈、TCB这些信息,但创建空闲任务是在task.c创建的,所以获 取 空 闲 任 务 的 内 存 , 即 将 pxIdleTaskTCBBuffer 和pxIdleTaskStackBuffer 这两个接下来要作为形参传到 xTaskCreateStatic()函数的指针分别指向空闲任务的 TCB 和栈的起始地址,这个操作由二级指针函数 vApplicationGetIdleTaskMemory()来实现:
main.c
/*二级指针函数,参数是二级指针,指向一级指针*/
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize )
{
*ppxIdleTaskTCBBuffer=&IdleTaskTCB;
*ppxIdleTaskStackBuffer=IdleTaskStack;
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}
task.c:
/*创建空闲任务*/
extern StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
extern TCB_t IdleTaskTCB;
/*二级指针函数,参数是二级指针,指向一级指针*/
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize )
{
*ppxIdleTaskTCBBuffer=&IdleTaskTCB;
*ppxIdleTaskStackBuffer=IdleTaskStack;
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}
/*======================================创建空闲任务start==============================================*/
TCB_t *pxIdleTaskTCBBuffer = NULL; /* 用于指向空闲任务控制块 */
StackType_t *pxIdleTaskStackBuffer = NULL; /* 用于空闲任务栈起始地址 */
uint32_t ulIdleTaskStackSize;
/* 获取空闲任务的内存:任务栈和任务TCB */
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer,
&pxIdleTaskStackBuffer,
&ulIdleTaskStackSize );
xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任务入口 */
(char *)"IDLE", /* 任务名称,字符串形式 */
(uint32_t)ulIdleTaskStackSize , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(StackType_t *)pxIdleTaskStackBuffer, /* 任务栈起始地址 */
(TCB_t *)pxIdleTaskTCBBuffer ); /* 任务控制块 */
/* 将任务添加到就绪列表 */
vListInsertEnd( &( pxReadyTasksLists[0] ), &( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem ) );
/*======================================创建空闲任务end================================================*/
我们把整套机制类比成小区物业的钥匙领取窗口:
- 前两个信封是专门用来装 “钥匙” 的(对应二级指针:因为要返回的本身就是一个指针 / 地址);
- 第三个信封是用来装 “库房面积数字” 的(对应一级指针:返回普通数值)。
- 不管谁来、递过来什么样的空信封,窗口都会把固定的那三把钥匙 / 数字装进信封里;
- 所以所有人领回去的,指向的都是同一批库房,拿到的信息完全一致。
- *ppxIdleTaskTCBBuffer = &IdleTaskTCB; → 把 TCB 库房的钥匙,塞进内核递来的第一个信封里
- *ppxIdleTaskStackBuffer = IdleTaskStack; → 把栈库房的钥匙,塞进内核递来的第二个信封里
- *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE; → 把栈面积数字,塞进第三个信封里
一句话总结:窗口是固定的,库房是固定的,钥匙也是固定的——变的只是来领钥匙的人(内核)递过来的信封。 所以不管内核怎么调用,拿回去的永远是同一批内存的地址,这就是 vApplicationGetIdleTaskMemory() 用二级指针把「内存地址」装进「参数信封」返回给内核的全部秘密。
创建空闲任务入口:
#define portTASK_FUNCTION( vFunction, pvParameters ) void vFunction( void *pvParameters )
/*创建空闲任务*/
static portTASK_FUNCTION( prvIdleTask, pvParameters )
{
/* 防止编译器的警告 */
( void ) pvParameters;
for(;;)
{
/* 空闲任务暂时什么都不做 */
}
}
最后将空闲任务插入到就绪列表的开头。在下一章我们会支持优先级,空闲任务默认的优先级是最低的,即排在就绪列表的开头。
2.实现阻塞延时
(1)vTaskDelay ()函数
阻塞延时的阻塞是指任务调用该延时函数后,任务会被剥离 CPU 使用权,然后进入阻塞状态,直到延时结束,任务重新获取 CPU 使用权才可以继续运行。在任务阻塞的这段时间,CPU 可以去执行其它的任务,如果其它的任务也在延时状态,那么 CPU 就将运行空闲任务。阻塞延时函数在 task.c 中定义:
/*任务阻塞函数*/
void vTaskDelay( const TickType_t xTicksToDelay )
{
TCB_t *pxTCB = NULL;
/* 获取当前任务的TCB */
pxTCB = pxCurrentTCB;
/* 设置延时时间 */
pxTCB->xTicksToDelay = xTicksToDelay;
/* 任务切换 */
taskYIELD();
}
pxTCB = pxCurrentTCB: 取当前任务的任务控制块。pxCurrentTCB 是一个在 task.c 定义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块。
pxTCB->xTicksToDelay = xTicksToDelay: xTicksToDelay 是任务控制块的一个成员,用于记录任务需要延时的时间,单位为 SysTick 的中断周期。比如我们本书当中 SysTick 的中断周期为 10ms,调用 vTaskDelay( 2 )则完成 2*10ms 的延时。
之前的TCB结构体是没有xTicksToDelay这个成员的,所以我们要修改TCB结构体:
/*StackType_t就是uint32_t*/
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 栈顶 */
ListItem_t xStateListItem; /* 任务节点 */
StackType_t *pxStack; /* 任务栈起始地址 */
/* 任务名称,字符串形式 */
char pcTaskName[ configMAX_TASK_NAME_LEN ];
TickType_t xTicksToDelay; /* 用于延时 */
} tskTCB;
typedef tskTCB TCB_t;
taskYIELD(): 任务切换。调用 tashYIELD()会产生 PendSV中断,在 PendSV中断服务函数中会调用上下文切换函数 vTaskSwitchContext(),该函数的作用是寻找最高优先级的就绪任务,然后更新 pxCurrentTCB。上一章我们只有两个任务,则 pxCurrentTCB 不是指向任务 1 就是指向任务 2,本章节开始我们多增加了一个空闲任务,则需要让pxCurrentTCB 在这三个任务中切换,算法需要改变
(2)修改 vTaskSwitchContext()函数
#if 0
void vTaskSwitchContext( void )
{
if( pxCurrentTCB == &Task1TCB )
{
pxCurrentTCB = &Task2TCB;
}
else
{
pxCurrentTCB = &Task1TCB;
}
}
#else
void vTaskSwitchContext( void )
{
/* 如果当前线程是空闲线程,那么就去尝试执行线程1或者线程2,
看看他们的延时时间是否结束,如果线程的延时时间均没有到期,
那就返回继续执行空闲线程 */
if( pxCurrentTCB == &IdleTaskTCB )
{
if(Task1TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task1TCB;
}
else if(Task2TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task2TCB;
}
else
{
return;/* 线程延时均没有到期则返回,继续执行空闲线程 */
}
}
else
{
/*如果当前线程是线程1或者线程2的话,检查下另外一个线程,如果另外的线程不在延时中,就切换到该线程
否则,判断下当前线程是否应该进入延时状态,如果是的话,就切换到空闲线程。否则就不进行任何切换 */
if(pxCurrentTCB == &Task1TCB)
{
/*如果任务不处于阻塞状态就切换任务二*/
if(Task2TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task2TCB;
}
/*如果当前任务处于阻塞状态,就进入空闲任务*/
else if(pxCurrentTCB->xTicksToDelay != 0)
{
pxCurrentTCB = &IdleTaskTCB;
}
else
{
return;/* 返回,不进行切换,因为两个线程都处于延时中 */
}
}
else if(pxCurrentTCB == &Task2TCB)
{
if(Task1TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task1TCB;
}
else if(pxCurrentTCB->xTicksToDelay != 0)
{
pxCurrentTCB = &IdleTaskTCB;
}
else
{
return;/* 返回,不进行切换,因为两个线程都处于延时中 */
}
}
}
}
#endif
if( pxCurrentTCB == &IdleTaskTCB ): 如果当前任务是空闲任务,那么就去尝试执行任务 1 或者任务 2,看看他们的延时时间是否结束,如果任务的延时时间均没有到期,那就返回继续执行空闲任务。
else: 如果当前任务是任务 1 或者任务 2 的话,检查下另外一个任务,如果另外的任务不在延时中,就切换到该任务。否则,判断下当前任务是否应该进入延时状态,如果是的话,就切换到空闲任务,否则就不进行任何切换 。
(3)SysTick 中断服务函数
在任务上下文切换函数 vTaskSwitchContext ()中,会判断每个任务的任务控制块中的延时成员 xTicksToDelay 的值是否为 0,如果为 0就要将对应的任务就绪,如果不为 0 就继续延时。如果一个任务要延时,一开始 xTicksToDelay 肯定不为 0,当 xTicksToDelay 变为0 的时候表示延时结束,那么 xTicksToDelay 是以什么周期在递减?在哪里递减?在FreeRTOS 中,这个周期由 SysTick 中断提供,操作系统里面的最小的时间单位就是SysTick 的中断周期,我们称之为一个 tick,SysTick 中断服务函数在 port.c.c 中实现
/*
*************************************************************************
* SysTick中断服务函数
*************************************************************************
*/
void xPortSysTickHandler( void )
{
/* 关中断 */
vPortRaiseBASEPRI();
/* 更新系统时基 */
xTaskIncrementTick();
/* 开中断 */
vPortClearBASEPRIFromISR();
}
从前面的学习我们只SysTick中断是优先级最低的中断,所以为了防止SysTick中断执行到一半被其它优先级更高的中断打断,所以我们要在执行
更新系统时基之前关闭中断
(4)xTaskIncrementTick()函数:
static volatile TickType_t xTickCount = ( TickType_t ) 0U;
void xTaskIncrementTick( void )
{
TCB_t *pxTCB = NULL;
BaseType_t i = 0;
/* 更新系统时基计数器xTickCount,xTickCount是一个在task.c中定义的全局变量 */
const TickType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;
/* 扫描就绪列表中所有线程的xTicksToDelay,如果不为0,则减1 */
for(i=0; i<configMAX_PRIORITIES; i++)
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &pxReadyTasksLists[i] ) );
if(pxTCB->xTicksToDelay > 0)
{
pxTCB->xTicksToDelay —;
}
}
/* 任务切换 */
portYIELD();
}
(1)const TickType_t xConstTickCount = xTickCount + 1: 更新系统时基计数器 xTickCount,加一操作。xTickCount 是一个在
port.c 中定义的全局变量,在函数 vTaskStartScheduler()中调用 xPortStartScheduler()函数前初始化。如下:
/*开启任务调度*/
void vTaskStartScheduler( void )
{
/*======================================创建空闲任务start==============================================*/
TCB_t *pxIdleTaskTCBBuffer = NULL; /* 用于指向空闲任务控制块 */
StackType_t *pxIdleTaskStackBuffer = NULL; /* 用于空闲任务栈起始地址 */
uint32_t ulIdleTaskStackSize;
/* 获取空闲任务的内存:任务栈和任务TCB */
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer,
&pxIdleTaskStackBuffer,
&ulIdleTaskStackSize );
xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任务入口 */
(char *)"IDLE", /* 任务名称,字符串形式 */
(uint32_t)ulIdleTaskStackSize , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(StackType_t *)pxIdleTaskStackBuffer, /* 任务栈起始地址 */
(TCB_t *)pxIdleTaskTCBBuffer ); /* 任务控制块 */
/* 将任务添加到就绪列表 */
vListInsertEnd( &( pxReadyTasksLists[0] ), &( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem ) );
/*======================================创建空闲任务end================================================*/
/* 手动指定第一个运行的任务 */
pxCurrentTCB = &Task1TCB;
/* 初始化系统时基计数器 */
xTickCount = ( TickType_t ) 0U;
/* 启动调度器 */
if( xPortStartScheduler() != pdFALSE )
{
/* 调度器启动成功,则不会返回,即不会来到这里 */
}
}
(2)for(i=0; i<configMAX_PRIORITIES; i++): 扫描就绪列表中所有任务的 xTicksToDelay,如果不为 0,则减 1。该if()语句调用了:
listGET_OWNER_OF_HEAD_ENTRY,宏定义如下:
#define listGET_OWNER_OF_HEAD_ENTRY( pxList ) ( (&( ( pxList )->xListEnd ))->pxNext->pvOwner )
可以看到该宏定义是访问每个链表的第一个节点的pvOwner成员,也就是拿到第一个节点的任务TCB,接着就可以访问这个TCB的xTicksToDelay成员(可以看到这个是有缺陷的,只能查看第一个节点,所以处于阻塞状态的任务是不在就绪链表的,而且是在延时链表)
(3)portYIELD(): 执行一次任务切换。联合前面的vTaskSwitchContext()函数可以知道后面就不用在任务的后面调用portYIELD(),以后不用在任务代码后面手动调 portYIELD(),任务切换统一由 xTaskIncrementTick() 在每次 tick 时触发。
(5)SysTick 初始化函数
SysTick 的中断服务函数要想被顺利执行,则 SysTick 必须先初始化。SysTick 初始化函数在 port.c 中定义
/* SysTick 配置寄存器 */
#define portNVIC_SYSTICK_CTRL_REG( * ( ( volatile uint32_t * ) 0xe000e010 ) )
#define portNVIC_SYSTICK_LOAD_REG( * ( ( volatile uint32_t * ) 0xe000e014 ) )
#ifndef configSYSTICK_CLOCK_HZ
#define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ
/* 确保SysTick的时钟与内核时钟一致 */
#define portNVIC_SYSTICK_CLK_BIT( 1UL << 2UL )
#else
#define portNVIC_SYSTICK_CLK_BIT( 0 )
#endif
#define portNVIC_SYSTICK_INT_BIT( 1UL << 1UL )
#define portNVIC_SYSTICK_ENABLE_BIT( 1UL << 0UL )
/*
*************************************************************************
* 初始化SysTick
*************************************************************************
*/
void vPortSetupTimerInterrupt( void )
{
/* 设置重装载寄存器的值 */
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) – 1UL;
/* 设置系统定时器的时钟等于内核时钟
使能SysTick 定时器中断
使能SysTick 定时器 */
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT |
portNVIC_SYSTICK_INT_BIT |
portNVIC_SYSTICK_ENABLE_BIT );
}
**(1)SysTick 配置寄存器:**配置 SysTick 需要用到的寄存器和宏定义,在 port.c 中实现。
(2)portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) – 1UL: 设置重装载寄存器的值,决定 SysTick 的中断周期。从代码清单 9-10(1) 可 以 知 道 : 如 果 没 有 定 义 configSYSTICK_CLOCK_HZ 那 么configSYSTICK_CLOCK_HZ 就 等 于 configCPU_CLOCK_HZ ,configSYSTICK_CLOCK_HZ 确 实 没 有 定 义 , 则 configSYSTICK_CLOCK_HZ 由 在FreeRTOSConfig.h 中定义的 configCPU_CLOCK_HZ 决定,同时 configTICK_RATE_HZ 也在 FreeRTOSConfig.h 中定义
#define configCPU_CLOCK_HZ( ( unsigned long ) 25000000 )
#define configTICK_RATE_HZ( ( TickType_t ) 100 )
这步计算公式如下:
LOAD = 25M / 100 – 1 = 249999 → 计数器计 250000 个时钟
每个时钟周期 = 1/25M = 40ns
中断周期 = 250000 × 40ns = 10ms → 是【乘法】
portNVIC_SYSTICK_CTRL_REG: 设置系统定时器的时钟等于内核时钟,使能 SysTick 定时器中断,使能 SysTick 定时器。
最后该vPortSetupTimerInterrupt( )SysTick 初始化函数会在xPortStartScheduler()启动调度器函数被调用,如下:
BaseType_t xPortStartScheduler( void )
{
/* 配置PendSV 和 SysTick 的中断优先级为最低 */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 初始化SysTick */
vPortSetupTimerInterrupt();
/* 启动第一个任务,不再返回 */
prvStartFirstTask();
/* 不应该运行到这里 */
return 0;
}
总结:vPortSetupTimerInterrupt() 在启动调度器被调用后,就会使能 SysTick 中断,接着按照我们的配置每 10ms 触发一次 SysTick 中断,随后进入 xPortSysTickHandler() 中断服务函数。该函数先关中断(vPortRaiseBASEPRI()),再调用 xTaskIncrementTick() 函数,这个函数干三件事:
随后开中断(vPortClearBASEPRIFromISR()),SysTick 中断解除,PendSV 中断响应,完成一次任务切换。
最后mian函数实现:
#include "FreeRTOS.h"
#include "task.h"
portCHAR flag1;
portCHAR flag2;
portCHAR flag3;
/*定义任务的栈,StackType_t就是uint32_t*/
#define TASK1_STACK_SIZE 128
StackType_t Task1_Stack[TASK1_STACK_SIZE];
/*定义任务的TCB,用来描述该任务的信息*/
TCB_t Task1TCB;
#define TASK2_STACK_SIZE 128
StackType_t Task2_Stack[TASK2_STACK_SIZE];
/*定义任务的TCB,用来描述该任务的信息*/
TCB_t Task2TCB;
/*声明外部调用列表项*/
extern List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
/*声明这两个任务*/
void Task1_Entry(void* p_arg);
void Task2_Entry(void* p_arg);
int main(void)
{
/*初始化就绪列表*/
prvInitialiseTaskLists();
xTaskCreateStatic(Task1_Entry,"Task1",TASK1_STACK_SIZE,NULL,Task1_Stack,&Task1TCB);
/*将任务1的节点插入到就绪链表0中*/
vListInsertEnd(&pxReadyTasksLists[2],&Task1TCB.xStateListItem);
xTaskCreateStatic(Task2_Entry,"Task2",TASK1_STACK_SIZE,NULL,Task2_Stack,&Task2TCB);
/*将任务2的节点插入到就绪链表1中*/
vListInsertEnd(&pxReadyTasksLists[3],&Task2TCB.xStateListItem);
/*开启调度器*/
vTaskStartScheduler();
for(;;)
{
}
}
/* 软件延时 */
void delay (uint32_t count)
{
for(; count!=0; count—);
}
/*创建任务1,目前没有参数传进来,不过后面会用到先定义一个void*指针*/
void Task1_Entry(void* p_arg)
{
for(;;)
{
flag1=1;
flag2=0;
flag3=0;
vTaskDelay(1);
}
}
/*创建任务2*/
void Task2_Entry(void* p_arg)
{
for(;;)
{
flag1=0;
flag2=1;
flag3=0;
vTaskDelay(1);
}
}
/*创建空闲任务*/
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
/*定义任务的TCB,用来描述该任务的信息*/
TCB_t IdleTaskTCB;
/*二级指针函数,参数是二级指针,指向一级指针*/
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize )
{
*ppxIdleTaskTCBBuffer=&IdleTaskTCB;
*ppxIdleTaskStackBuffer=IdleTaskStack;
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}
空闲任务:
/*创建空闲任务*/
static portTASK_FUNCTION( prvIdleTask, pvParameters )
{
/* 防止编译器的警告 */
( void ) pvParameters;
for(;;)
{
flag1=0;
flag2=0;
flag3=1;
vTaskDelay(1);
/* 空闲任务暂时什么都不做 */
}
}
(6)实验现象如下:

可以看到任务一先执行,然后进入阻塞状态,任务二执行然后又进入阻塞状态,最后很长一段时间都是空闲任务执行,这是为什么呢?因为任务一和任务二只执行几微秒,而 vTaskDelay(1) 依靠的中断服务函数(SysTick 中断)来递减延时计数,而我们 tick 中断是 10ms 才触发一次,也就是在这将近 10ms 时间内,任务一和任务二的延时计数都不会被递减,所以它们都无法被调度执行,才会导致后面很长的时间都是空闲任务执行。那我们把 vTaskDelay(1) 删除试试呢?


可以看到空闲任务不被执行,而任务一和任务二轮流切换,时间间隔为10ms
网硕互联帮助中心


评论前必须登录!
注册