参考内容:《[野火]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(),则两次调用之间的代码即为被测量的时间长度。
1 2 3 4
| 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 外设有关的地址和掩码定义为宏定义,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
#define BSP_REG_DEM_CR (*(CPU_REG32 *)0xE000EDFC) #define BSP_REG_DWT_CR (*(CPU_REG32 *)0xE0001000) #define BSP_REG_DWT_CYCCNT (*(CPU_REG32 *)0xE0001004) #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)
#define BSP_BIT_DWT_CR_CYCCNTENA (1<<0)
|
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 与测量中断时间有关,可先忽略):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
#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 时间戳的数据类型定义
1 2 3 4
| 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 的系统时钟频率。
1 2 3
| #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 名字。(目前未实现)
1 2 3 4 5 6 7
| 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)
1 2 3 4 5 6 7 8 9 10
| #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_TS_TmrInit(); #endif } #endif
|
3.2.3 时间戳定时器初始化函数 CPU_TS_TmrInit()(cpu_core.c)
这个函数的功能是:
- 开启 CYCCNT 计数器开始计数。
- 获取时钟源
BSP_CPU_ClkFreq(),并将它作为系统 OS 的时钟源CPU_TS_TmrFreqSet()。这部分是下一节的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #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; BSP_REG_DWT_CYCCNT = (CPU_INT32U) 0u; BSP_REG_DBGMCU_CR |= BSP_BIT_DWT_CR_CYCCNTENA; CPU_TS_TmrFreqSet ((CPU_TS_TMR_FREQ) fclk_freq); } #endif
|
3.3 初始化系统时钟
3.3.1 获取 CPU 的 HCLK 时钟 BSP_CPU_ClkFreq()(cpu_core.c)
这个函数的功能是:
- 获得芯片或 CPU 的时钟源,并返回。当然,因为我们使用的是软件仿真,所以直接返回自己设定的时钟频率。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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。
1 2 3 4 5 6 7
| #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)
该函数用于返回计数器的值。
1 2 3 4 5 6 7 8 9
| #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 中使能时间戳:
1 2
| #define OS_CFG_TS_EN 1u
|
一旦OS_CFG_TS_EN定义为 1u,那么在 os_cpu.h 里,OS_TS_GET会被定义:
1 2 3 4 5
| #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 中加入时间戳的测量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| #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_Init(); CPU_IntDis(); 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; 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 的真实运行时间。
(完)