uCOS-III 学习记录(4)——时间戳
参考内容:《[野火]uCOS-III内核实现与应用开发实战指南——基于STM32》第 9 章。
1 时间戳
在 uCOS 中,如果要测量一段代码 A 的时间,那么可以在代码段 A 运行前记录一个时间点 TimeStart,在代码段 A 运行完记录一个时间点 TimeEnd,那么代码段 A 的运行时间 TimeUse 就等于 TimeEnd 减去 TimeStart。这里面的两个时间点 TimeEnd 和 TimeStart,就叫作时间戳(Timestamp,简称ts),时间戳实际上就是一个时间点。
比如,在下面的代码中,在需要开始测量的地方调用OS_TS_GET()
,在需要结束测量的地方再次调用OS_TS_GET()
,则两次调用之间的代码即为被测量的时间长度。
TimeStart = OS_TS_GET();
OSTimeDly (20);
TimeEnd = OS_TS_GET();
TimeUse = TimeEnd - TimeStart;
如果 Tick = 10ms,那么 TimeUse 测量到的是 200ms。
现在的问题是:如何实现时间戳呢?DWT 要出场了。
2 DWT 外设
2.1 DWT 外设简介
在 uCOS 中,我们已经使用了 SysTick 作为系统的时间片,所以不能再使用 SysTick 来实现时间戳了。在 Cortex-M3 中有一个调试组件,其中有一个组件是跟踪组件,叫数据观察点与跟踪(Data Watchpoint and Trace,DWT)外设,该外设有一个 32 位寄存器 CYCCNT,它是一个向上的计数器,记录的是内核时钟 HCLK 运行的个数,当 CYCCNT 溢出之后,会清零重新开始向上计数。该计数器在 uCOS 中正好被用来实现时间戳的功能。
在 STM32F103 系列的单片机中,HCLK 时钟最高为 72M,单个时钟的周期为 1/72us = 0.0139us = 14ns,CYCCNT 总共能记录的时间为 2^32 * 14 = 60s。在 uCOS 中,要测量的时间都是很短的,都是 ms 级别,根本不需要考虑定时器溢出的问题。如果内核代码执行的时间超过 s 的级别,那就背离了实时操作系统实时的设计初衷了,没有意义。
2.2 初始化 DWT 的步骤
- 使能 DWT 外设:由内核调试寄存器 DEMCR(地址:0xE000EDFC)的位 24 控制,写入 1 表示开启 DWT 外设。
- 初始化 DWT_CYCCNT 寄存器:将 DWT_CYCCNT(地址:0xE0001004)寄存器清零。
- 启用 DWT_CYCCNT 寄存器:由 DWT_CTRL(地址:0xE0001000)寄存器的位 0(CYCCNTENA)控制,写入 1 表示开启 DWT_CYCCNT 寄存器。
2.3 DWT 外设的宏定义(cpu_core.c)
为了提高代码的可读性和易修改性,将与 DWT 外设有关的地址和掩码定义为宏定义,如下所示:
/*
*********************************************************************************************************
* 寄存器定义
*********************************************************************************************************
*/
#define BSP_REG_DEM_CR (*(CPU_REG32 *)0xE000EDFC) // DEMCR的地址
#define BSP_REG_DWT_CR (*(CPU_REG32 *)0xE0001000) // DWT_CTRL的地址
#define BSP_REG_DWT_CYCCNT (*(CPU_REG32 *)0xE0001004) // DWT_CYCCNT的地址
#define BSP_REG_DBGMCU_CR (*(CPU_REG32 *)0xE0042004) // 未用到,先忽略
/*
*********************************************************************************************************
* 寄存器位定义
*********************************************************************************************************
*/
#define BSP_DBGMCU_CR_TRACE_IOEN_MASK 0x10 // 这些暂时用不到,可先忽略
#define BSP_DBGMCU_CR_TRACE_MODE_ASYNC 0x00
#define BSP_DBGMCU_CR_TRACE_MODE_SYNC_01 0x40
#define BSP_DBGMCU_CR_TRACE_MODE_SYNC_02 0x80
#define BSP_DBGMCU_CR_TRACE_MODE_SYNC_04 0xC0
#define BSP_DBGMCU_CR_TRACE_MODE_MASK 0xC0
#define BSP_BIT_DEM_CR_TRCENA (1<<24) // DEMCR的位24置1
#define BSP_BIT_DWT_CR_CYCCNTENA (1<<0) // DWT_CTRL的位0置1
3 CPU 和时间戳的初始化
如何开启时间戳?这个问题的本质就是如何开启 DWT 外设和 CYCCNT 寄存器。基本思路是:首先,按照上述步骤开启 DWT 和 CYCCNT,然后
3.1 时间戳的相关定义(cpu_core.h)
3.1.1 通过宏定义开启/关闭时间戳功能
uCOS 实现了很多功能,但很多时候,有些功能用不到,我们并不需要那么长的代码。于是,我们可以在 H 文件中通过宏定义开启或关闭某些功能,这样就能达到裁剪的目的。
在 cpu_core.h 中定义了使能时间戳的宏定义,如下所示(其中 CPU_CFG_INT_DIS_MEAS_EN 与测量中断时间有关,可先忽略):
/**********************************开启/关闭***************************************************/
/* 是否开启时间戳? */
#if ((CPU_CFG_TS_32_EN == DEF_ENABLED) || (CPU_CFG_TS_64_EN == DEF_ENABLED))
#define CPU_CFG_TS_EN DEF_ENABLED
#else
#define CPU_CFG_TS_EN DEF_DISABLED
#endif
/* 是否开启时间戳定时器? */
#if ((CPU_CFG_TS_EN == DEF_ENABLED) || defined(CPU_CFG_INT_DIS_MEAS_EN))
#define CPU_CFG_TS_TMR_EN DEF_ENABLED
#else
#define CPU_CFG_TS_TMR_EN DEF_DISABLED
#endif
这些宏定义怎么用呢?待会可以参考下面几节的代码,你就明白了。
3.1.2 时间戳的数据类型定义
typedef CPU_INT32U CPU_TS32;
typedef CPU_INT32U CPU_TS_TMR_FREQ;
typedef CPU_TS32 CPU_TS;
typedef CPU_INT32U CPU_TS_TMR;
3.1.3 时间戳的全局变量定义——CPU_TS_TmrFreq_Hz
CPU_TS_TmrFreq_Hz
是一个 32 位的全局变量,用来记录 CPU 的系统时钟频率。
#if (CPU_CFG_TS_TMR_EN == DEF_ENABLED)
CPU_CORE_EXT CPU_TS_TMR_FREQ CPU_TS_TmrFreq_Hz;
#endif
3.2 时间戳的初始化
3.2.1 CPU 初始化函数 CPU_Init()(cpu_core.c)
该函数实现的功能:
- 初始化时间戳。
- 初始化中断禁用时间测量。(目前未实现)
- 初始化 CPU 名字。(目前未实现)
/* CPU 初始化函数 */
void CPU_Init (void)
{
#if ((CPU_CFG_TS_EN == DEF_ENABLED) || (CPU_CFG_TS_TMR_EN == DEF_ENABLED))
CPU_TS_Init(); /* 时间戳初始化函数 */
#endif
}
发现时间戳初始化函数被包在了条件编译中,当我们没有定义 CPU_CFG_TS_EN 为 DEF_ENABLED 时,这段代码将不会出现在执行中,这样就能实现代码的裁剪了。
3.2.2 时间戳初始化函数 CPU_TS_Init()(cpu_core.c)
该函数实现的功能是:
- 清零 CPU_TS_TmrFreq_Hz。
- 初始化时间戳定时器。
/* 时间戳初始化函数 */
#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.2.3 时间戳定时器初始化函数 CPU_TS_TmrInit()(cpu_core.c)
这个函数的功能是:
- 开启 CYCCNT 计数器开始计数。
- 获取时钟源
BSP_CPU_ClkFreq()
,并将它作为系统 OS 的时钟源CPU_TS_TmrFreqSet()
。这部分是下一节的内容。
/* 时间戳定时器初始化函数 */
#if (CPU_CFG_TS_TMR_EN == DEF_ENABLED)
void CPU_TS_TmrInit (void)
{
CPU_INT32U fclk_freq;
fclk_freq = BSP_CPU_ClkFreq();
BSP_REG_DEM_CR |= BSP_BIT_DEM_CR_TRCENA; /* 启用 DWT 外设 */
BSP_REG_DWT_CYCCNT = (CPU_INT32U) 0u; /* DWT CYCCNT 寄存器计数清零 */
BSP_REG_DBGMCU_CR |= BSP_BIT_DWT_CR_CYCCNTENA; /* 启用 DWT CYCCNT 计数器 */
CPU_TS_TmrFreqSet ((CPU_TS_TMR_FREQ) fclk_freq);
}
#endif
3.3 初始化系统时钟
3.3.1 获取 CPU 的 HCLK 时钟 BSP_CPU_ClkFreq()(cpu_core.c)
这个函数的功能是:
- 获得芯片或 CPU 的时钟源,并返回。当然,因为我们使用的是软件仿真,所以直接返回自己设定的时钟频率。
/* 获取 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;
CPU_HCLK = 25000000;
return CPU_HCLK;
#endif
}
获取 CPU 的时钟源后,需要将其作为系统的时钟。
3.3.2 获取系统时钟 CPU_TS_TmrFreqSet()(cpu_core.c)
该函数用于初始化系统 OS 的时钟,具体做法是将时钟源频率freq_hz
赋值给全局变量CPU_TS_TmrFreq_Hz
。
/* 初始化 OS 系统时钟 */
#if (CPU_CFG_TS_TMR_EN == DEF_ENABLED)
void CPU_TS_TmrFreqSet (CPU_TS_TMR_FREQ freq_hz)
{
CPU_TS_TmrFreq_Hz = freq_hz;
}
#endif
3.4 获取 CYCCNT 计数器 CPU_TS_TmrRd()(cpu_core.c)
该函数用于返回 CYCCNT 计数器的值。
/* 获得 DWT 的 CYCCNT */
#if (CPU_CFG_TS_TMR_EN == DEF_ENABLED)
CPU_TS_TMR CPU_TS_TmrRd (void)
{
CPU_TS_TMR ts_tmr_cnts;
ts_tmr_cnts = (CPU_TS_TMR)BSP_REG_DWT_CYCCNT;
return ts_tmr_cnts;
}
#endif
现在,所有功能都已实现完毕,万事俱备只欠东风。
4 时间戳的使用
4.1 使能时间戳(os_cfg.h 和 os_cpu.h)
在 os_cfg.h 中使能时间戳:
/* 使能时间戳 */
#define OS_CFG_TS_EN 1u
一旦OS_CFG_TS_EN
定义为 1u,那么在 os_cpu.h 里,OS_TS_GET
会被定义:
#if (OS_CFG_TS_EN == 1u)
#define OS_TS_GET() (CPU_TS)CPU_TS_TmrRd()
#else
#define OS_TS_GET() (CPU_TS)0u
#endif
OS_TS_GET
将 CPU 底层的函数CPU_TS_TmrRd()
重新取个名字封装,供内核和用户函数使用。
4.2 主函数(app.c)
在 app.c 中,在 main 函数中加入 CPU 初始化函数,在 Task1 中加入时间戳的测量。
#include "ARMCM3.h"
#include "os.h"
#define TASK1_STK_SIZE 20
#define TASK2_STK_SIZE 20
static CPU_STK Task1Stk[TASK1_STK_SIZE];
static CPU_STK Task2Stk[TASK2_STK_SIZE];
static OS_TCB Task1TCB;
static OS_TCB Task2TCB;
uint32_t flag1;
uint32_t flag2;
uint32_t TimeStart;
uint32_t TimeEnd;
uint32_t TimeUse;
void Task1 (void *p_arg);
void Task2 (void *p_arg);
void delay(uint32_t count);
int main (void)
{
OS_ERR err;
/* 初始化相关的全局变量,创建空闲任务 */
OSInit(&err);
/* <----- CPU 初始化:初始化时间戳 */
CPU_Init();
/* 关中断,因为此时 OS 未启动,若开启中断,那么 SysTick 将会引发中断 */
CPU_IntDis();
/* 初始化 SysTick,配置 SysTick 为 10ms 中断一次,Tick = 10ms */
OS_CPU_SysTickInit(10);
/* 创建任务 */
OSTaskCreate ((OS_TCB*) &Task1TCB,
(OS_TASK_PTR) Task1,
(void *) 0,
(CPU_STK*) &Task1Stk[0],
(CPU_STK_SIZE) TASK1_STK_SIZE,
(OS_ERR *) &err);
OSTaskCreate ((OS_TCB*) &Task2TCB,
(OS_TASK_PTR) Task2,
(void *) 0,
(CPU_STK*) &Task2Stk[0],
(CPU_STK_SIZE) TASK2_STK_SIZE,
(OS_ERR *) &err);
/* 将任务加入到就绪列表 */
OSRdyList[0].HeadPtr = &Task1TCB;
OSRdyList[1].HeadPtr = &Task2TCB;
/* 启动OS,将不再返回 */
OSStart(&err);
}
void Task1 (void *p_arg)
{
for (;;)
{
flag1 = 1;
TimeStart = OS_TS_GET(); /* <----- */
OSTimeDly (20);
TimeEnd = OS_TS_GET(); /* <----- */
TimeUse = TimeEnd - TimeStart; /* <----- */
flag1 = 0;
OSTimeDly (5);
}
}
void Task2 (void *p_arg)
{
for (;;)
{
flag2 = 1;
OSTimeDly (5);
flag2 = 0;
OSTimeDly (5);
}
}
让我们来看看初始化的运行流程:
- CPU 进行初始化:运行
CPU_Init()
,然后运行时间戳初始化函数CPU_TS_Init()
。 - 时间戳进行初始化:先将记录系统时钟的全局变量
CPU_TS_TmrFreq_Hz
清零,然后初始化时间戳定时器(即计数器)CPU_TS_TmrInit()
。 - 初始化计数器:启用 DWT 和 CYCCNT,并将其清零,然后获取 CPU(芯片)的时钟源,将其赋值给
CPU_TS_TmrFreq_Hz
作为系统时钟源。 - 运行
BSP_CPU_ClkFreq
,获取时钟源。 - 此时,CPU 已经初始化完毕,CYCCNT 开始计数工作。
在任务 Task1 中:
- 调用
CPU_TS_TmrRd
,获得第一个时间戳,赋值给 TimeStart。 - 执行 A 代码。
- 再次调用
CPU_TS_TmrRd
,获得第二个时间戳,赋值给 TimeEnd。 - 两值相减,再乘以系统时钟源频率(
CPU_TS_TmrFreq_Hz
),得到的就是代码 A 的真实运行时间。
(完)