文章目录
序言
UCOSIII为我们提供了一个十分强大的工具:软件定时器。有了它,我们便有了原生STM32定时器的替代品。下面我将会简要介绍一下这个定时器的使用方法以及实现机理,同时会有一个小实验用来验证我们的理解。实验代码来自正点原子。
基本工作原理:定时器递减其计数值,当计数值为0时,就是定时期满的时候,通过调用回调函数执行相应的操作。回调函数是用户定义的。但是,不要在回调函数中使用能导致该定时器任务被阻塞或者被删除的函数,如OSTimeDly(),OSTimeDlyHMSM()等。
定时器的分辨率:定时器的分辨率取决于系统的时基频率(也就是滴答定时器的频率),也就是变量OS_CFG_TMR_TASK_RATE_HZ的值,它是以HZ为单位的。如果时基任务的频率设置为10HZ,所有定时器的分辨率为十分之一秒。事实上,这是定时器的推荐值。定时器用于不精确时间尺度的任务。
注:关于什么是回调函数,知乎上有个说法:当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。
相关函数介绍
函数名 | 功能 |
---|---|
OSTmrCreate() | 创建和设置定时器 |
OSTmrDel() | 删除一个定时器 |
OSTmrRemainGet() | 获得定时器的剩余期限值 |
OSTmrStart() | 开始定时器运行 |
OSTmrStateGet() | 获得定时器当前的状态 |
OSTmrStop() | 暂停定时器 |
OSTmrCreate()
定时器在被使用前必须被创建。通过调用OSTmrCreate(),并设置这个函数的相关参数。一旦定时器的操作模式被设置,就不能再改动,函数原型如下所示:
void OSTmrCreate (OS_TMR *p_tmr,
CPU_CHAR *p_name,
OS_TICK dly,
OS_TICK period,
OS_OPT opt,
OS_TMR_CALLBACK_PTR p_callback,
void *p_callback_arg,
OS_ERR *p_err)
- *p_tmr:一个定时器控制块的指针
- *p_name:定时器的名字
- *dly:初始延迟
- period:延时周期
- opt:定时模式
- p_callback:回调函数
- *p_callback_arg:回调函数参数
- *p_err:错误码指针
剩下的几个函数都很简单,可以自己看一看,
重要的是OSTmrCreate()、OSTmrDel()、OSTmrStart()、OSTmrStop()这四个函数。
几种定时模式介绍
从上面的OSTmrCreate()函数介绍中,我们知道UCOSIII的软件定时器有好几种定时模式,下面我们就介绍一下这几种定时模式。
一次性定时模式
正如其名字所表达,定时器会递减被设置初始的定时值,当该值 为 0 时就会调用回调函数并停止定时器。初始的定时值通过调用OSTmrSrart()设置,延时期满时,回调函数被调用(假定回调函数在定时器创建的时候被提供)。完成之后,定时器不做任务事情直到调用OSTmrStart()被重新开启。如下图所示:
无初始定时周期模式
定时器被设置为无初始定时周期模式。当定时器期满时,回调函数被调用,定时值被定时周期值重载,如此周期性地重复。如下图所示:
有初始定时周期模式
定时器可以被设置为有初始定延周期模式。第一 周期的递减值由OSTmrCreate()中的参数"dly"设置,以后的重载值由 "period"值确定。调用OSTmrStart()重新开始。如下图所示:
核心数据结构介绍
定时器的核心数据结构是OS_TMR,它在os.h中定义,下面我们来看看里面有些什么东西
struct os_tmr {
OS_OBJ_TYPE Type;
CPU_CHAR *NamePtr; /* Name to give the timer */
OS_TMR_CALLBACK_PTR CallbackPtr; /* Function to call when timer expires */
void *CallbackPtrArg; /* Argument to pass to function when timer expires */
OS_TMR *NextPtr; /* Double link list pointers */
OS_TMR *PrevPtr;
OS_TICK Match; /* Timer expires when OSTmrTickCtr matches this value */
OS_TICK Remain; /* Amount of time remaining before timer expires */
OS_TICK Dly; /* Delay before start of repeat */
OS_TICK Period; /* Period to repeat timer */
OS_OPT Opt; /* Options (see OS_OPT_TMR_xxx) */
OS_STATE State;
#if OS_CFG_DBG_EN > 0u
OS_TMR *DbgPrevPtr;
OS_TMR *DbgNextPtr;
#endif
};
emmm,注释里面说得很详细了,我这里就不说了
一个小实验
下面我们来介绍一下具体如何使用UCOSIII中的定时器,一共分为如下四个步骤:定义定时器控制块与声明定时器回调函数、编写回调函数、利用OSTmrCreate()创建定时器、利用OSTmrStart()开启定时器。整个实验的思路是这样的:main函数和前面一样,初始化一些外设,创建一个开始任务用来创建其它任务;开始任务中创建一个按键扫描任务用来扫描按键的状态,创建两个定时器任务,通过按键状态来决定启动还是关闭定时器。
注:定时器任务在程序结构上可以单独出来考虑,因为它不是普通的任务,所以这个程序的主框架就是一个按键扫描与指定状态执行程序
定义定时器控制块与声明回调函数
在main函数外面定义定定时器控制块,我们定义两个,注意:回调函数的参数列表格式最好不要乱动
OS_TMR tmr1; //定时器1
OS_TMR tmr2; //定时器2
void tmr1_callback(void *p_tmr, void *p_arg); //定时器1回调函数
void tmr2_callback(void *p_tmr, void *p_arg); //定时器2回调函数
编写回调函数
//定时器1的回调函数
void tmr1_callback(void *p_tmr, void *p_arg)
{
static u8 tmr1_num=0;
LCD_ShowxNum(62,111,tmr1_num,3,16,0x80); //显示定时器1的执行次数
LCD_Fill(6,131,114,313,lcd_discolor[tmr1_num%14]);//填充区域
tmr1_num++; //定时器1执行次数加1
}
//定时器2的回调函数
void tmr2_callback(void *p_tmr,void *p_arg)
{
static u8 tmr2_num = 0;
tmr2_num++; //定时器2执行次数加1
LCD_ShowxNum(182,111,tmr2_num,3,16,0x80); //显示定时器1执行次数
LCD_Fill(126,131,233,313,lcd_discolor[tmr2_num%14]); //填充区域
LED1 = ~LED1;
printf("定时器2运行结束\r\n");
}
我们看到,定时器1的回调函数的作用是显示定时器1的执行次数并根据执行次数来改变LCD填充颜色。定时器2的回调函数的作用是显示定时器2的执行次数并且根据执行次数来改变LCD填充颜色,除此之外还会让LED1翻转并向串口打印结束信息(因为我们后面打算设置定时器2为一次性定时模式)。
利用OSTmrCreate()创建定时器
我们把这个定时器的创建放在开始任务函数中,除了创建定时器之外,我们还创建了一个普通任务,这个任务的作用是扫描按键来决定启动还是关闭定时器
//开始任务函数
void start_task(void *p_arg)
{
OS_ERR err;
CPU_SR_ALLOC();
p_arg = p_arg;
CPU_Init();
#if OS_CFG_STAT_TASK_EN > 0u
OSStatTaskCPUUsageInit(&err); //统计任务
#endif
#ifdef CPU_CFG_INT_DIS_MEAS_EN //如果使能了测量中断关闭时间
CPU_IntDisMeasMaxCurReset();
#endif
#if OS_CFG_SCHED_ROUND_ROBIN_EN //当使用时间片轮转的时候
//使能时间片轮转调度功能,时间片长度为1个系统时钟节拍,既1*5=5ms
OSSchedRoundRobinCfg(DEF_ENABLED,1,&err);
#endif
//创建定时器1
OSTmrCreate((OS_TMR *)&tmr1, //定时器1
(CPU_CHAR *)"tmr1", //定时器名字
(OS_TICK )20, //20*10=200ms
(OS_TICK )100, //100*10=1000ms
(OS_OPT )OS_OPT_TMR_PERIODIC, //周期模式
(OS_TMR_CALLBACK_PTR)tmr1_callback,//定时器1回调函数
(void *)0, //参数为0
(OS_ERR *)&err); //返回的错误码
//创建定时器2
OSTmrCreate((OS_TMR *)&tmr2,
(CPU_CHAR *)"tmr2",
(OS_TICK )20, //200*10=2000ms
(OS_TICK )100,
(OS_OPT )OS_OPT_TMR_PERIODIC, //单次定时器
(OS_TMR_CALLBACK_PTR)tmr2_callback, //定时器2回调函数
(void *)0,
(OS_ERR *)&err);
OS_CRITICAL_ENTER(); //进入临界区
//创建TASK1任务
OSTaskCreate((OS_TCB * )&Task1_TaskTCB,
(CPU_CHAR * )"Task1 task",
(OS_TASK_PTR )task1_task,
(void * )0,
(OS_PRIO )TASK1_TASK_PRIO,
(CPU_STK * )&TASK1_TASK_STK[0],
(CPU_STK_SIZE)TASK1_STK_SIZE/10,
(CPU_STK_SIZE)TASK1_STK_SIZE,
(OS_MSG_QTY )0,
(OS_TICK )0,
(void * )0,
(OS_OPT )OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR,
(OS_ERR * )&err);
OS_CRITICAL_EXIT(); //退出临界区
OSTaskDel((OS_TCB*)0,&err); //删除start_task任务自身
}
利用OSTmrStart()开启定时器
//任务1的任务函数
void task1_task(void *p_arg)
{
u8 key,num;
OS_ERR err;
while(1)
{
key = KEY_Scan(0);
switch(key)
{
case WKUP_PRES: //当key_up按下的话打开定时器1
OSTmrStart(&tmr1,&err); //开启定时器1
printf("开启定时器1\r\n");
break;
case KEY0_PRES: //当key0按下的话打开定时器2
OSTmrStart(&tmr2,&err); //开启定时器2
printf("开启定时器2\r\n");
break;
case KEY1_PRES: //当key1按下话就关闭定时器
OSTmrStop(&tmr1,OS_OPT_TMR_NONE,0,&err); //关闭定时器1
OSTmrStop(&tmr2,OS_OPT_TMR_NONE,0,&err); //关闭定时器2
printf("关闭定时器1和2\r\n");
break;
}
num++;
if(num==50) //每500msLED0闪烁一次
{
num = 0;
//LED0 = ~LED0;
}
OSTimeDlyHMSM(0,0,0,10,OS_OPT_TIME_PERIODIC,&err); //延时10ms
}
}
总结
相比于STM32的原生通用定时器,UCOSIII的定时器使用起来更方便,但是它的限制就是只适合于对定时精度要求不高的场合,还有就是回调函数的概念要好好理解一下。
如果你是UCOSIII小白,一定要自己把代码写一下,好好思考一下,不要以为你真的懂了,那是自欺欺人,API本身不难,关键要花时间去熟悉每一个数据结构,变量,API之间的关系,为以后移植UCOSIII和深入学习核心算法打好基础。