0-1write MC/OS __Basics2

2021-07-22 11:04:30 星期四

五、时间戳

什么是时间戳?

时间戳实际上就是一个时间点。
在操作系统中,代码加入了时间测量的功能,比如任务关中断时间,关调度器时间等。
知道了代码的运行时间便可知道该代码的执行效率。

在代码运行前记录一个时间点start,在代码运行完记录一个时间点end,
这段代码的运行时间为end-start,这两个时间点便为时间戳。

时间戳怎么实现?

通常执行一条代码是需要多个时钟周期即ns级别的,而单片机的硬件定时器的精度都是us级别,远达不到测量几条代码运行时间的精度。
在ARM Cortex-M系列内核中,有一个DWT的外设,该外设有一个32位的寄存器CYCCNT,它是一个向上的计数器, 记录的是内核时钟HCLK运行的个数,当CYCCNT溢出之后,会清零重新开始向上计数。该计数器在μC/OS-III中正好被用来实现时间戳的功能。


1、CPU初始化函数CPU_Init()

/* CPU初始化函数 */
void  CPU_Init (void)
{
/* CPU初始化函数中总共做了三件事
    1、初始化时间戳
    2、初始化中断禁用时间测量
    3、初始化CPU名字
这里只讲时间戳功能, */

#if ((CPU_CFG_TS_EN     == DEF_ENABLED) || \
    (CPU_CFG_TS_TMR_EN == DEF_ENABLED))    //用于控制时间戳是32位还是64位,默认启用32位
    CPU_TS_Init();//函数如下,,
#endif

}

2、时间戳初始化函数CPU_TS_Init()

#if ((CPU_CFG_TS_EN     == DEF_ENABLED) || \
    (CPU_CFG_TS_TMR_EN == DEF_ENABLED))
static  void  CPU_TS_Init (void)
{

#if (CPU_CFG_TS_TMR_EN == DEF_ENABLED)
    CPU_TS_TmrFreq_Hz   = 0u;//全局变量,表示CPU的系统时钟,具体大小跟硬件相关。
    CPU_TS_TmrInit();//函数如下
#endif

}
#endif

3、时间戳定时器初始化函数CPU_TS_TmrInit()

/* 时间戳定时器初始化 */
#if (CPU_CFG_TS_TMR_EN == DEF_ENABLED)
void  CPU_TS_TmrInit (void)
{
    CPU_INT32U  fclk_freq;
    fclk_freq = BSP_CPU_ClkFreq();//函数如下,,
//初始化时间戳计数器CYCCNT,启用CYCCNT计数的操作步骤:
    /* 1、启用DWT外设 */
    BSP_REG_DEM_CR     |= (CPU_INT32U)BSP_BIT_DEM_CR_TRCENA;(1)
    /* 2、DWT CYCCNT寄存器计数清零 */
    BSP_REG_DWT_CYCCNT  = (CPU_INT32U)0u;
    /* 注意:当使用软件仿真全速运行的时候,会先停在这里,
    就好像在这里设置了一个断点一样,需要手动运行才能跳过,
    当使用硬件仿真的时候却不会 */
    /* 3、启用Cortex-M3 DWT CYCCNT寄存器 */
    BSP_REG_DWT_CR     |= (CPU_INT32U)BSP_BIT_DWT_CR_CYCCNTENA;

    CPU_TS_TmrFreqSet((CPU_TS_TMR_FREQ)fclk_freq);//把函数BSP_CPU_ClkFreq()获取到的CPU的HCLK时钟赋值给全局变量CPU_TS_TmrFreq_Hz...代码如下,,,
}
#endif

4、获取CPU的HCLK时钟的函数BSP_CPU_ClkFreq()

/* 获取CPU的HCLK时钟
这个是跟硬件相关的,目前我们是软件仿真,我们暂时把跟硬件相关的代码屏蔽掉,
直接手动设置CPU的HCLK时钟*/
CPU_INT32U  BSP_CPU_ClkFreq (void)
{
#if 0
    RCC_ClocksTypeDef  rcc_clocks;
    RCC_GetClocksFreq(&rcc_clocks);
return ((CPU_INT32U)rcc_clocks.HCLK_Frequency);
#else
    CPU_INT32U    CPU_HCLK;

    /* 目前软件仿真我们使用25M的系统时钟 */
    CPU_HCLK = 25000000;

    return CPU_HCLK;
#endif
}

5、CPU_TS_TmrFreqSet()函数

/* 初始化CPU_TS_TmrFreq_Hz,这个就是系统的时钟,单位为HZ */
#if (CPU_CFG_TS_TMR_EN == DEF_ENABLED)
void  CPU_TS_TmrFreqSet (CPU_TS_TMR_FREQ  freq_hz)
{
    CPU_TS_TmrFreq_Hz = freq_hz;//把函数BSP_CPU_ClkFreq()获取到的CPU的HCLK时钟赋值给全局变量CPU_TS_TmrFreq_Hz
}
#endif

6、获取CYCNNT计数器函数 CPU_TS_TmrRd()

#if (CPU_CFG_TS_TMR_EN == DEF_ENABLED)
CPU_TS_TMR  CPU_TS_TmrRd (void)//用于获取CYCNNT计数器的值
{
    CPU_TS_TMR  ts_tmr_cnts;
    ts_tmr_cnts = (CPU_TS_TMR)BSP_REG_DWT_CYCCNT;
    return (ts_tmr_cnts);
}
#endif

7、OS_TS_GET()函数

//用于获取CYCNNT计数器的值,实际上是一个宏定义,将CPU底层的函数CPU_TS_TmrRd()重新取个名字封装, 供内核和用户函数使用
#define OS_CFG_TS_EN                    1u

#if      OS_CFG_TS_EN == 1u
#define  OS_TS_GET()               (CPU_TS)CPU_TS_TmrRd()
#else
#define  OS_TS_GET()               (CPU_TS)0u
#endif

8、main()函数

与四的代码差别不大,在函数开头加入CPU_Init()函数,然后在任务1中对延时函数的执行时间进行测量。


六、临界段

什么是临界段?

临界段就是一段在执行的时候不能被中断的代码段。
临界段最常出现的就是对全局变量的操作。
什么情况下临界段会被打断?
系统调度或者外部中断都会打断临界段,需要关中断或者锁调度器保护临界段。
在ucos的系统调度,最终也是产生PendSV中断,在PendSV Handler里面实现任务的切换,所以还是可以归结为中断。
所以,对临界段的保护最终还是回到对中断的开和关的控制。


1、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
//在μC/OS中,对中断的开和关是通过操作PRIMASK寄存器来实现的,使用CPSID I指令就能立即关闭中断。很是方便。

2、关中断

底层操作关中断的函数CPU_SR_Save()

CPU_SR_Save
        MRSR0, PRIMASK              //(1)通过MRS指令将特殊寄存器PRIMASK寄存器的值存储到通用寄存器r0。当在C中调用汇编的子程序返回时, 会将r0作为函数CPU_SR_Save()的返回值。
        CPSID   I                   //(2)关中断
        BX      LR                  //(3)子函数返回

为什么关中断之前要执行MRS指令将PRIMASK的值保存起来?

3、开中断

底层操作开中断的函数是CPU_SR_Restore()

CPU_SR_Restore
        MSR     PRIMASK, R0      //(1)通过MSR指令将通用寄存器r0的值存储到特殊寄存器PRIMASK。当在C中调用汇编的子程序返回时.会将第一个形参传入到通用寄存器r0。
                                      所以在C中调用CPU_SR_Restore()的时候,需要传入一个形参, 该形参是进入临界段之前保存的PRIMASK的值。 
        BX      LR               //(2)子程序返回。

为什么开中断之前要执行MRS指令将通用寄存器r0的值存储到特殊寄存器PRIMASK?

4、临界段代码的应用

;//开关中断函数的实现
;/*
; * 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)
}

这种方法可以防止当临界段出现嵌套时,中断的开关有效。

5、测量关中断时间

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

6、测量关中断时间初始化

#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变量中。

7、测量最大关中断时间

/* 开始测量关中断时间  */
#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

把测得的时间戳减去一次测量额外消耗的时间, 便得到这次关中断的时间,再将这个时间跟历史保存下的最大的关中断的时间对比,刷新最大的关中断时间

8、获取最大关中断时间

现在得到了关中断时间,那么μC/OS也提供了三个与获取关中断时间有关的函数,分别是:

  • CPU_IntDisMeasMaxCurReset()

  • CPU_IntDisMeasMaxCurGet()

  • CPU_IntDisMeasMaxGet()

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

posted @ 2021-11-30 20:33  cswft  Views(92)  Comments(0Edit  收藏  举报