从0到1教你写UCOS-III 第九部分:临界段

版权声明: https://blog.csdn.net/qq_38351824/article/details/89765578

9.1 临界段简介:


       临界段代码,也称作临界域,是一段不可分割的代码。 uCOS 中包含了很多临界段代码。如果临界段可能被中断,那么就需要关中断以保护临界段。如果临界段可能被任务级代码打断,那么需要锁调度器保护临界段。
       临界段用一句话概括就是一段在执行的时候不能被中断的代码段。在 uCOS 里面,这个临界段最常出现的就是对全局变量的操作,全局变量就好像是一个枪把子,谁都可以对他开枪,但是我开枪的时候,你就不能开枪,否则就不知道是谁命中了靶子。可能有人会说我可以在子弹上面做个标记,我说你能不能不要瞎扯淡。
       那么什么情况下临界段会被打断?一个是系统调度,还有一个就是外部中断。在uCOS 的系统调度,最终也是产生 PendSV 中断,在 PendSV Handler 里面实现任务的切换,所以还是可以归结为中断。既然这样, uCOS 对临界段的保护最终还是回到对中断的开和关的控制。
       uCOS 中定义了一个进入临界段的宏和两个出临界段的宏,用户可以通过这些宏定义进入临界段和退出临界段。
OS_CRITICAL_ENTER()
OS_CRITICAL_EXIT()
OS_CRITICAL_EXIT_NO_SCHED()
此外还有一个开中断但是锁定调度器的宏定义 OS_CRITICAL_ENTER_CPU_EXIT()


9.2 Cortex-M 内核快速关中断指令:


        为了快速地开关中断, Cortex-M 内核专门设置了一条 CPS 指令,有 4 种用法,具体见代码清单 5-26。
代码清单 9-1 CPS 指令用法

CPSID I ;PRIMASK=1 ;关中断
CPSIE I ;PRIMASK=0 ;开中断
CPSID F ;FAULTMASK=1 ;关异常
CPSIE F ;FAULTMASK=0 ;开异常

       代码清单 5-26 中 PRIMASK 和 FAULTMAST 是 Cortex-M 内核 里面三个中断屏蔽寄存器中的两个,还有一个是 BASEPRI,有关这三个寄存器的详细用法见表格 5-2。
表格 9-1 Cortex-M 内核中断屏蔽寄存器组描述

       但是,在 uCOS 中,对中断的开和关是通过操作 PRIMASK 寄存器来实现的, 使用CPSID I 指令就能立即关闭中断。很是方便。


9.3 关中断:


        uCOS 中关中断的函数在 cpu_a.asm 中定义,无论上层的宏定义是怎么实现的 ,底层操作关中断的函数还是 CPU_SR_Save(),具体实现见代码清单 9-2。
代码清单 9-2 关中断

CPU_SR_Save
        MRS R0, PRIMASK   (1)
        CPSID I           (2)
        BX LR             (3)

       代码清单 9-2(1):通过 MRS 指令将特殊寄存器 PRIMASK 寄存器的值存储到通用寄存器 r0。当在 C 中调用汇编的子程序返回时,会将 r0 作为函数的返回值。所以在 C 中调用 CPU_SR_Save()的时候, 需要事先声明一个变量用来存储 CPU_SR_Save()的返回值, 即r0 寄存器的值,也就是 PRIMASK 的值。
       代码清单 9-2(2):关闭中断,即使用 CPS 指令将 PRIMASK 寄存器的值置 1。在这里,我敢肯定,一定会有人有这样一个疑问:关中断,不就是直接使用 CPSID I 指令就行了嘛,为什么还要第一步,即在执行 CPSID I 指令前,要先把 PRIMASK 的值保存起来?这个疑问接下来在“临界段代码的应用”这个小结揭晓。
      代码清单 9-2(3):子程序返回


9.4 开中断:


        开中断要与关中断配合使用, uCOS 中开中断的函数在 cpu_a.asm 中定义,无论上层的宏定义是怎么实现的 ,底层操作关中断的函数还是 CPU_SR_Restore(),具体实现见代码清单 9-3。
代码清单 9-3 开中断

CPU_SR_Restore
        MSR PRIMASK, R0 (1)
        BX LR (2)

      代码清单 9-2(1): 通过 MSR 指令将通用寄存器 r0 的值存储到特殊寄存器 PRIMASK。当在 C 中调用汇编的子程序返回时,会将第一个形参传入到通用寄存器 r0。所以在 C 中调用 CPU_SR_Restore()的时候,需要传入一个形参,该形参是进入临界段之前保存的PRIMASK 的值。这个时候又有人会问,开中断,不就是使用 CPSIE I 指令就行了嘛,为啥跟我等凡人想的不一样?其中奥妙将在接下来“临界段代码的应用”这个小结揭晓。
       代码清单 9-2(2):子程序返回


9.5 临界段代码的应用:


       在进入临界段之前,我们会先把中断关闭,退出临界段时再把中断打开。而且 CortexM 内核设置了快速关中断的 CPS 指令,那么按照我们的第一思维,开关中断的函数的实现和临界段代码的保护应该是像代码清单 9-4 那样的。
代码清单 9-4 开关中断的函数的实现和临界段代码的保护

;//开关中断函数的实现
;/*
; * void CPU_SR_Save();
; */
CPU_SR_Save
CPSID I                                             (1)
BX LR
;/*
; * void CPU_SR_Restore(void);
; */
CPU_SR_Restore
CPSIE I                                             (2)
BX LR
PRIMASK = 0; /* PRIMASK 初始值为 0,表示没有关中断 */  (3)
/* 临界段代码保护 */
{
/* 临界段开始 */
CPU_SR_Save(); /* 关中断,PRIMASK = 1 */              (4)
{
/* 执行临界段代码,不可中断 */                        (5)
}
/* 临界段结束 */
CPU_SR_Restore(); /* 开中断,PRIMASK = 0 */           (6)
}

        代码清单 9-4(1):关中断直接使用了 CPSID I,没有跟代码清单 9-2 一样事先将PRIMASK 的值保存在 r0 中。
        代码清单 9-4(2):开中断直接使用了 CPSIE I,而不是像代码清单 9-3 那样从传进来的形参来恢复 PRIMASK 的值。
        代码清单 9-4(4):假设 PRIMASK 初始值为 0, 表示没有关中断。
        代码清单 9-4(4):临界段开始,调用关中断函数 CPU_SR_Save(), 此时 PRIMASK 的值等于 1,确实中断已经关闭。
        代码清单 9-4(5):执行临界段代码, 不可中断。
        代码清单 9-4(5):临界段结束,调用开中断函数 CPU_SR_Restore(), 此时 PRIMASK的值等于 0,确实中断已经开启。
        乍一看, 代码清单 9-4 的这种实现开关中断的方法确实有效, 没有什么错误, 但是我们忽略了一种情况, 就是当临界段是出现嵌套的时候, 这种开关中断的方法就不行了, 具体怎么不行具体见代码清单 9-5。

代码清单 9-5 开关中断函数的实现和嵌套临界段代码的保护(有错误,只为讲解)

;//开关中断函数的实现
;/*
; * void CPU_SR_Save();
; */
CPU_SR_Save
CPSID I
BX LR
;/*
; * void CPU_SR_Restore(void);
; */
CPU_SR_Restore
CPSIE I
BX LR
PRIMASK = 0; /* PRIMASK 初始值为 0,表示没有关中断 */
/* 临界段代码 */
{
/* 临界段 1 开始 */
CPU_SR_Save(); /* 关中断,PRIMASK = 1 */
{
/* 临界段 2 */
CPU_SR_Save(); /* 关中断,PRIMASK = 1 */
{
}
CPU_SR_Restore(); /* 开中断,PRIMASK = 0 */ (注意)
}
/* 临界段 1 结束 */
CPU_SR_Restore(); /* 开中断,PRIMASK = 0 */
}

        代码清单 9-5(注意):当临界段出现嵌套的时候,这里以一重嵌套为例。临界段 1 开始和结束的时候 PRIMASK 分别等于 1 和 0,表示关闭中断和开启中断,这是没有问题的。临界段 2 开始的时候, PRIMASK 等于 1,表示关闭中断,这是没有问题的,问题出现在临界段 2 结束的时候, PRIMASK 的值等于 0,如果单纯对于临界段 2 来说,这也是没有问题的,因为临界段 2 已经结束,可是临界段 2 是嵌套在临界段 1 中,虽然临界段 2 已经结束,但是临界段 1 还没有结束,中断是不能开启的,如果此时有外部中断来临,那么临界段 1就会被中断,违背了我们的初衷,那应该怎么办?正确的做法具体见代码清单 9-6。
代码清单 9-6 开关中断的函数的实现和嵌套临界段代码的保护(正确

;//开关中断函数的实现
;/*
; * void CPU_SR_Save();
; */
CPU_SR_Save
MRS R0, PRIMASK
CPSID I
BX LR
;/*
; * void CPU_SR_Restore(void);
; */
CPU_SR_Restore
MSR PRIMASK, R0
BX LR
PRIMASK = 0; /* PRIMASK 初始值为 0,表示没有关中断 */ (1)
CPU_SR cpu_sr1 = (CPU_SR)0
CPU_SR cpu_sr2 = (CPU_SR)0 (2)
/* 临界段代码 */
{
/* 临界段 1 开始 */
cpu_sr1 = CPU_SR_Save(); /* 关中断,cpu_sr1=0,PRIMASK=1 */ (3)
{
/* 临界段 2 */
cpu_sr2 = CPU_SR_Save();/* 关中断,cpu_sr2=1,PRIMASK=1 */ (4)
{
}
CPU_SR_Restore(cpu_sr2); /* 开中断,cpu_sr2=1,PRIMASK=1 */ (5)
}
/* 临界段 1 结束 */
CPU_SR_Restore(cpu_sr1); /* 开中断,cpu_sr1=0,PRIMASK=0 */(6)
}

        代码清单 9-6 (1):假设 PRIMASK 初始值为 0,表示没有关中断。
        代码清单 9-6 (2):定义两个变量,留着后面用。
        代码清单 9-6 (3):临界段 1 开始,调用关中断函数 CPU_SR_Save(), CPU_SR_Save()函数先将 PRIMASK 的值存储在通用寄存器 r0,一开始我们假设 PRIMASK 的值等于 0,所以此时 r0 的值即为 0。然后执行汇编指令 CPSID I 关闭中断,即设置 PRIMASK 等于 1,在返回的时候 r0 当做函数的返回值存储在 cpu_sr1,所以 cpu_sr1 等于 r0 等于 0。

        代码清单 9-6 (4):临界段 2 开始,调用关中断函数 CPU_SR_Save(), CPU_SR_Save()函数先将 PRIMASK 的值存储在通用寄存器 r0,临界段 1 开始的时候我们关闭了中断,即设置 PRIMASK 等于 1,所以此时 r0 的值等于 1。然后执行汇编指令 CPSID I 关闭中断,即设置 PRIMASK 等于 1,在返回的时候 r0 当做函数的返回值存储在 cpu_sr2,所以cpu_sr2 等于 r0 等于 1。
        代码清单 9-6 (5):临界段 2 结束,调用开中断函数 CPU_SR_Restore(cpu_sr2),cpu_sr2 作为函数的形参传入到通用寄存器 r0,然后执行汇编指令 MSR r0, PRIMASK 恢复PRIMASK 的值。此时 PRIAMSK = r0 = cpu_sr2 = 1。关键点来了,为什么临界段 2 结束了,PRIMASK 还是等于 1,按道理应该是等于 0。因为此时临界段 2 是嵌套在临界段 1 中的,还是没有完全离开临界段的范畴,所以不能把中断打开,如果临界段是没有嵌套的,使用当前的开关中断的方法的话,那么 PRIMASK 确实是等于 1,具体举例见代码清单 9-7。
代码清单 9-7 开关中断的函数的实现和一重临界段代码的保护(正确)

;//开关中断函数的实现
;/*
; * void CPU_SR_Save();
; */
CPU_SR_Save
MRS R0, PRIMASK
CPSID I
BX LR
;/*
; * void CPU_SR_Restore(void);
; */
CPU_SR_Restore
MSR PRIMASK, R0
BX LR
PRIMASK = 0; /* PRIMASK 初始值为 0,表示没有关中断 */
CPU_SR cpu_sr1 = (CPU_SR)0
/* 临界段代码 */
{
/* 临界段开始 */
cpu_sr1 = CPU_SR_Save();/* 关中断,cpu_sr1=0,PRIMASK=1 */
{
}
/* 临界段结束 */
CPU_SR_Restore(cpu_sr1); /* 开中断,cpu_sr1=0,PRIMASK=0 */ (注意点)
}

        代码清单 9-6 (6):临界段 1 结束, PRIMASK 等于 0,开启中断,与进入临界段 1 遥相呼应。


9.6 测量关中断时间:

        uCOS 提 供 了 测 量 关 中 断 时 间 的 功 能 , 通 过 设 置 cpu_cfg.h 中 的 宏 定 义CPU_CFG_INT_DIS_MEAS_EN 为 1 就表示启用该功能。

        系统会在每次关中断前开始测量,开中断后结束测量, 测量功能保存了 2 个方面的测量值, 总的关中断时间与最近一次关中断的时间。因此,用户可以根据得到的关中断时间对其加以优化。时间戳的速率决定于 CPU 的速率。 例如, 如果 CPU 速率为 72MHz, 时间戳的速率就为 72MHz, 那么时间戳的分辨率为 1/72M 微秒, 大约为 13.8 纳秒(ns) 。显然, 系统测出的关中断时间还包括了测量时消耗的额外时间, 那么测量得到的时间减掉测量时所耗时间就是实际上的关中断时间关中断时间跟处理器的指令、速度、 内存访问速度有很大的关系。

9.6.1 测量关中断时间初始化:

        关中断之前要用函数 CPU_IntDisMeasInit()函数进行初始化,可以直接调用函数CPU_Init()函数进行初始化,具体见代码清单 9-8。
代码清单 9-8 CPU_IntDisMeasInit()源码

#ifdef CPU_CFG_INT_DIS_MEAS_EN
static void CPU_IntDisMeasInit (void)
{
CPU_TS_TMR time_meas_tot_cnts;
CPU_INT16U i;
CPU_SR_ALLOC();
CPU_IntDisMeasCtr = 0u;
CPU_IntDisNestCtr = 0u;
CPU_IntDisMeasStart_cnts = 0u;
CPU_IntDisMeasStop_cnts = 0u;
CPU_IntDisMeasMaxCur_cnts = 0u;
CPU_IntDisMeasMax_cnts = 0u;
CPU_IntDisMeasOvrhd_cnts = 0u;
time_meas_tot_cnts = 0u;
CPU_INT_DIS(); /* 关中断 */
for (i = 0u; i < CPU_CFG_INT_DIS_MEAS_OVRHD_NBR; i++)
{
CPU_IntDisMeasMaxCur_cnts = 0u;
CPU_IntDisMeasStart(); /* 执行多个连续的开始/停止时间测量 */
CPU_IntDisMeasStop();
time_meas_tot_cnts += CPU_IntDisMeasMaxCur_cnts; /* 计算总的时间 */
}
CPU_IntDisMeasOvrhd_cnts = (time_meas_tot_cnts +
(CPU_CFG_INT_DIS_MEAS_OVRHD_NBR / 2u))/CPU_CFG_INT_DIS_MEAS_OVRHD_NBR;
/*得到平均值,就是每一次测量额外消耗的时间 */
CPU_IntDisMeasMaxCur_cnts = 0u;
CPU_IntDisMeasMax_cnts = 0u;
CPU_INT_EN();
}
#endif

        因为关中断测量本身也会耗费一定的时间,这些时间实际是加入到我们测量到的最大关中断时间里面,如果能够计算出这段时间,后面计算的时候将其减去可以得到更加准确的结果。 这段代码的核心思想很简单, 就是重复多次开始测量与停止测量,然后多次之后,取得平均值,那么这个值就可以看做是一次开始测量与停止测量的时间,保存在CPU_IntDisMeasOvrhd_cnts 变量中

9.6.2 测量最大关中断时间:

        如果用户使能了 CPU_CFG_INT_DIS_MEAS_EN 这个宏定义, 那么系统在关中断的时候会调用了开始测量关中断最大时间的函数 CPU_IntDisMeasStart()开中断的时候调用停止测量关中断最大时间的函数 CPU_IntDisMeasStop()。从代码中我们能看到, 只要在关中断且嵌套层数 OSSchedLockNestingCtr 为 0 的时候保存下时间戳, 如果嵌套层数不为 0 ,肯定不是刚刚进入中断,退出中断且嵌套层数为 0 的时候,这个时候才算是真正的退出中断,把测得的时间戳减去一次测量额外消耗的时间,便得到这次关中断的时间,再将这个时间跟历史保存下的最大的关中断的时间对比,刷新最大的关中断时间, 源码具体见代码清单 9-9。
代码清单 9-9 开始/停止测量关中断时间

/* 开始测量关中断时间 */
#ifdef CPU_CFG_INT_DIS_MEAS_EN
void CPU_IntDisMeasStart (void)
{
CPU_IntDisMeasCtr++;
if (CPU_IntDisNestCtr == 0u) /* 嵌套层数为 0 */
{
CPU_IntDisMeasStart_cnts = CPU_TS_TmrRd(); /* 保存时间戳 */
}
CPU_IntDisNestCtr++;
}
#endif
/* 停止测量关中断时间 */
#ifdef CPU_CFG_INT_DIS_MEAS_EN
void CPU_IntDisMeasStop (void)
{
CPU_TS_TMR time_ints_disd_cnts;
CPU_IntDisNestCtr--;
if (CPU_IntDisNestCtr == 0u) /* 嵌套层数为 0*/
{
CPU_IntDisMeasStop_cnts = CPU_TS_TmrRd(); /* 保存时间戳 */
time_ints_disd_cnts = CPU_IntDisMeasStop_cnts -
CPU_IntDisMeasStart_cnts;/* 得到关中断时间 */
/* 更新最大关中断时间 */
if (CPU_IntDisMeasMaxCur_cnts < time_ints_disd_cnts)
{
CPU_IntDisMeasMaxCur_cnts = time_ints_disd_cnts;
}
if (CPU_IntDisMeasMax_cnts < time_ints_disd_cnts)
{
CPU_IntDisMeasMax_cnts = time_ints_disd_cnts;
}
}
}
#endif

9.6.3 获取最大关中断时间:

        现在得到了关中断时间,那么 uCOS 也提供了三个与获取关中断时间有关的函数,分别是:
         CPU_IntDisMeasMaxCurReset()
         CPU_IntDisMeasMaxCurGet()
         CPU_IntDisMeasMaxGet()

        如果想直接获取整个程序运行过程中最大的关中断时间的话, 直接调用函数CPU_IntDisMeasMaxGet()获取即可。
        如果想要测量某段程序执行的最大关中断时间, 那么在这段程序的前面调用CPU_IntDisMeasMaxCurReset()函数将 CPU_IntDisMeasMaxCur_cnts 变量清 0在这段程序结束的时候调用函数 CPU_IntDisMeasMaxCurGet()即可。
        这些函数的源码很简单, 具体见代码清单 9-10。
代码清单 9-10 获取最大关中断时间相关源码

#ifdef CPU_CFG_INT_DIS_MEAS_EN //如果使能了关中断时间测量
CPU_TS_TMR CPU_IntDisMeasMaxCurGet (void) //获取测量的程序段的最大关中断时间
{
CPU_TS_TMR time_tot_cnts;
CPU_TS_TMR time_max_cnts;
CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必需该宏,该宏声明和
//定义一个局部变量,用于保存关中断前的 CPU 状态寄存器
// SR(临界段关中断只需保存 SR),开中断时将该值还原。
CPU_INT_DIS(); //关中断
time_tot_cnts = CPU_IntDisMeasMaxCur_cnts;
//获取未处理的程序段最大关中断时间
CPU_INT_EN(); //开中断
time_max_cnts = CPU_IntDisMeasMaxCalc(time_tot_cnts);
//获取减去测量时间后的最大关中断时间
return (time_max_cnts); //返回程序段的最大关中断时间
}
#endif
#ifdef CPU_CFG_INT_DIS_MEAS_EN //如果使能了关中断时间测量
CPU_TS_TMR CPU_IntDisMeasMaxGet (void)
//获取整个程序目前最大的关中断时间
{
CPU_TS_TMR time_tot_cnts;
CPU_TS_TMR time_max_cnts;
CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必需该宏,该宏声明和
//定义一个局部变量,用于保存关中断前的 CPU 状态寄存器
// SR(临界段关中断只需保存 SR),开中断时将该值还原。
CPU_INT_DIS(); //关中断
time_tot_cnts = CPU_IntDisMeasMax_cnts;
//获取尚未处理的最大关中断时间
CPU_INT_EN(); //开中断
time_max_cnts = CPU_IntDisMeasMaxCalc(time_tot_cnts);
//获取减去测量时间后的最大关中断时间
return (time_max_cnts); //返回目前最大关中断时间
}
#endif
#ifdef CPU_CFG_INT_DIS_MEAS_EN //如果使能了关中断时间测量
CPU_TS_TMR CPU_IntDisMeasMaxCurReset (void)
//初始化(复位)测量程序段的最大关中断时间
{
CPU_TS_TMR time_max_cnts;
CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必需该宏,该宏声明和
//定义一个局部变量,用于保存关中断前的 CPU 状态寄存器
// SR(临界段关中断只需保存 SR),开中断时将该值还原。
time_max_cnts=CPU_IntDisMeasMaxCurGet();//获取复位前的程序段最大关中断时间
CPU_INT_DIS(); //关中断
CPU_IntDisMeasMaxCur_cnts = 0u; //清零程序段的最大关中断时间
CPU_INT_EN(); //开中断
return (time_max_cnts); //返回复位前的程序段最大关中断时间
}
#endif

9.7 main 函数:

        本章 main 函数没有添加新的测试代码,只需理解章节内容即可。

9.8 实验现象:

        本章没有实验,只需理解章节内容即可。

猜你喜欢

转载自blog.csdn.net/qq_38351824/article/details/89765578