本文已参与「新人创作礼」活动,一起开启掘金创作之路。
前言
在开发嵌入式软件时,定时器是必不可少的,或用于实时系统的心跳、延时,或用于裸机系统中的周期性任务等。因为硬件定时器资源有限,并且还有可能用于PWM,输入捕获等功能,所以更多的人会基于一个硬件定时器实现多个软件定时器,用于对时间精度要求没那么高的场合,如发送周期性CAN报文,定时采集传感器信号等。本文基于S32K144平台介绍笔者常用的两种软件定时器,希望对读者有用。
1.网红软件定时器MultiTimer
1.1 MultiTimer 简介
MultiTimer是最近比较火的一个开源项目,作者为0x1abin,github地址为:GitHub - 0x1abin/MultiTimer at master,遵循MIT开源许可协议,目前已收获502 stars。
MultiTimer是一个软件定时器扩展模块,可无限扩展你所需的定时器任务,取代传统的标志位判断方式, 更优雅更便捷地管理程序的时间触发时序。
1.2 准备工作
- S32K144EVB-Q100
- S32DS For ARM 2.2
- PC端的串口调试部助手
1.3 MultiTimer 使用
MultiTimer的README文档对于如何使用MultiTimer介绍的非常详细。本文基于S32K144平台并参考README文档介绍如何使用MultiTimer。
1.3.1 新建工程
- 选择从例程中新建工程,如下图:
- 选择lpit_periodic_interrupt_s32k144的例程,并重命名为lpit_multitimer_s32k144,如下图,点击Finish:
- 配置lpit组件,设置定时器周期为1ms,并使能中断,如下图:
- 将multi_timer.c和multi_timer.h复制到工程所在的文件夹,并按F5刷新一下,就能在工程树看到了,如下图所示(串口打印相关的文件后面再介绍):
- 确认multi_timer.h中设置的tick值,如下图:
/*
It means 1 tick for 1ms.
Your can configurate for your tick time such as 5ms/10ms and so on.
*/
#define CFG_TIMER_1_TICK_N_MS 1
源代码此处的注释有误,如果想要tick为5ms,宏定义改为5是不行的,需要将硬件定时器的中断周期改为5ms。如果此处改为5,会导致软件定时器的超时时间变为原来的1/5。
1.3.2 修改主函数
- 首先在main.c中包含multi_timer.h,并创建两个软件定时器,如下:
#include "multi_timer.h"
/* 创建两个软件定时器 */
struct Timer timer1;
struct Timer timer2;
- 然后对两个软件定时器进行初始化并启动,如下:
/* 软件定时器初始化*/
timer_init(&timer1, timer1_callback, 1000, 2000, NULL); //1s loop
timer_start(&timer1);
timer_init(&timer2, timer2_callback, 50, 0, NULL); //50ms delay
timer_start(&timer2);
printf("timer start!\r\n");
timer_init函数的定义如下,其中第三个函数表示第一次的超时时间,第四个参数表示第2次到第N次的超时时间。第4个参数如果为0,那么该软件定时器只发生一次超时,对应的回调函数也只执行一次。
void timer_init(struct Timer* handle, void (*timeout_cb)(void *arg), uint32_t timeout, uint32_t repeat, void *arg)
- 接着在1ms的硬件定时器中断里调用timer_ticks(),如下:
/*!
* @brief: LPIT interrupt handler.
* When an interrupt occurs clear channel flag and toggle LED0
*/
void LPIT_ISR(void)
{
/* Clear LPIT channel flag */
LPIT_DRV_ClearInterruptFlagTimerChannels(INST_LPIT1, (1 << LPIT_CHANNEL));
/* 软件定时器的ticks,此处为1ms*/
timer_ticks();
}
- 最后实现两个软件定时器的回调函数,注意不能有耗时过长的操作,如下:
/* 每个软件定时器的回调函数,不能在回调函数做耗时操作,否则会导致其他定时器无法正常超时 */
void timer1_callback(void *arg)
{
/* Toggle LED0 */
PINS_DRV_TogglePins(LED_GPIO_PORT, (1 << LED0_PIN_INDEX));
printf("timer1 timeout! arg: %p\r\n", arg);
}
void timer2_callback(void *arg)
{
printf("timer2 timeout! arg: %p\r\n", arg);
}
1.3.3 增加串口打印功能
- 首先在工程中增加lpuart组件,在组件库中找到lpuart,双击即可添加,如下图:
- 然后点击添加到工程的lpuart组件,选择lpuart1,波特率设为115200,如下:
- 配置下lpuart对应的pin脚,RX选择PTC6,TX选择PTC7,如下:
- 工程中需要增加四个文件,如下所示:
其中,printf.c,uart_console_io.c,uart.h在S32DS For ARM 2.2安装目录下,uart.c需要自己编写。
- printf.c地址如下:
- uart_console_io.c地址如下:
- uart.h地址如下:
- uart.c需要自己实现如下四个函数
#include "uart.h"
#include "lpuart1.h"
UARTError InitializeUART(UARTBaudRate baudRate)
{
//lpuart1_InitConfig0.baudRate = baudRate;
/* Initialize LPUART instance */
LPUART_DRV_Init(INST_LPUART1, &lpuart1_State, &lpuart1_InitConfig0);
return kUARTNoError;
}
UARTError ReadUARTPoll(char* c)
{
UARTError err = kUARTNoError;
err = LPUART_DRV_ReceiveDataBlocking(INST_LPUART1, (uint8_t *)c, 1, 1000);
return err;
}
UARTError ReadUARTN(void* bytes, unsigned long length)
{
UARTError err = kUARTNoError;
err = LPUART_DRV_ReceiveDataBlocking(INST_LPUART1, (uint8_t *)bytes, length, 1000);
return err;
}
UARTError WriteUARTN(const void* bytes, unsigned long length)
{
UARTError err = kUARTNoError;
uint32_t bytesRemaining;
err = LPUART_DRV_SendData(INST_LPUART1, (const uint8_t *const)bytes, length);
/* Wait for transfer to be completed */
while(LPUART_DRV_GetTransmitStatus(INST_LPUART1, &bytesRemaining) != STATUS_SUCCESS);
return err;
}
- 工程的属性中的库需要更改为ewl_c no I/O。
- 工程属性中的linker的库需要全部删除,否则编译时会发生错误
- 在main.c中包含stdio.h之后,就可以调用printf函数在串口打印信息了。
1.4 MultiTimer功能测试
将程序下载到S32K144EVB中,在串口助手打印的信息如下:
从打印信号可以看出,各个定时器的运行符合预期。
1.5 MultiTimer借鉴
MultiTimer使用了单链表操作,且代码量不大,可以借鉴该项目学习单链表。网络上对该项目的解读文章非常多,笔者推荐阅读如下这个:
2.个人常用软件定时器
除了网红定时器MultiTimer,笔者再推荐一个自己经常使用的软件定时器。
2.1 个人常用软件定时器介绍
- 需要将上一章节的工程中的multi_timer.c和multi_timer.h换成自己编写的soft_timer.c和soft_timer.h,工程树如下:
- soft_timer.h主要定义定时器相关的枚举和结构体,如下所示:
typedef enum
{
TIMER1,
TIMER2,
VTIMER_NUM,
} VtimerName_t;
typedef struct
{
unsigned char enable;
unsigned int msec;
} Vtimer_t,*PVtimer;
- soft_timer.c主要实现定时器的初始化,启动,判断以及心跳函数等,如下所示:
Vtimer_t sVtimer[VTIMER_NUM];
void vtimer_Init()
{
unsigned char i;
for (i=0; i<VTIMER_NUM; i++)
{
sVtimer[i].msec = 0;
sVtimer[i].enable = false;
}
vtimer_SetTimer(TIMER1, TIMER1_PERIOD);
vtimer_SetTimer(TIMER2, TIMER2_PERIOD);
}
void vtimer_SetTimer(VtimerName_t name, unsigned int msec)
{
if(false == sVtimer[name].enable)
{
sVtimer[name].msec = msec;
sVtimer[name].enable = true;
}
}
bool vtimer_TimerElapsed(VtimerName_t name)
{
if((sVtimer[name].msec == 0) && (sVtimer[name].enable == true))
{
sVtimer[name].enable = false;
return true;
}
else
{
return false;
}
}
void vtimer_UpdateHandler(void)
{
/*Enter each 1ms*/
unsigned char i;
for (i=0; i<VTIMER_NUM; i++)
{
if (sVtimer[i].msec && (sVtimer[i].enable == true))
{
sVtimer[i].msec--;
}
else
{
sVtimer[i].msec = 0;
}
}
}
- main.c中在硬件定时器中断中调用心跳函数,如下所示:
/*!
* @brief: LPIT interrupt handler.
* When an interrupt occurs clear channel flag and toggle LED0
*/
void LPIT_ISR(void)
{
/* Clear LPIT channel flag */
LPIT_DRV_ClearInterruptFlagTimerChannels(INST_LPIT1, (1 << LPIT_CHANNEL));
/* 软件定时器的ticks,此处为1ms*/
vtimer_UpdateHandler();
}
- 在main函数中调用初始化函数和判断函数,如下所示:
/* 软件定时器初始化*/
vtimer_Init();
printf("timer start!\r\n");
while(1)
{
if(vtimer_TimerElapsed(TIMER1) == true)
{
vtimer_SetTimer(TIMER1, TIMER1_PERIOD);
/* Toggle LED0 */
PINS_DRV_TogglePins(LED_GPIO_PORT, (1 << LED0_PIN_INDEX));
printf("timer1 timeout!\r\n");
}
if(vtimer_TimerElapsed(TIMER2) == true)
{
vtimer_SetTimer(TIMER2, TIMER2_PERIOD);
printf("timer2 timeout!\r\n");
}
}
2.2 功能测试
将程序下载到S32K144EVB,串口助手打印信息如下:
打印信息符合预期。
3.例程分享
本文使用的两个例程已上传百度网盘,分享如下:
- 链接:pan.baidu.com/s/1u6ja9roG…
- 提取码:yrlw
如果觉得本文对你有用,不妨给个一键三连!!!