Linux系统下基于setitimer的高精度嵌入式软件定时器实现方案

文章概要

〇、背景

  基于上一篇博文:Linux环境下的setitimer的延时/定时器的使用,我们已经了解定时器 setitimer setitimer 在同一个进程中只能使用一个的缺点,作为一个嵌入式的开发人员总觉得有点说不出的苦恼,那么本博文将基于这种力不从心的苦恼,编写一个满足基本要求的软件定时器。

《Linux系统下基于setitimer的高精度嵌入式软件定时器实现方案》

一、基本思路

1.1 定时器的结构定义

  作为定时器,一般情况下需要几个属性。

  • 定时器的定时时间:也就是定时的周期,多久触发一次
  • 定时器的状态:一般用来标识定时器当前初始使能不使能、运行或者停止、超时或者未超时
  • 定时器的模式:一般情况下,比较常见的是单次运行或者周期运行
  • 定时器到达之后的处理:也就是定时器定时时间到达之后的处理的事项,如果要作为通用定时的话可以将此处理的 事项作为一个函数指针,用于代码指定特定的回调函数
  • 既然为回调函数,那么回调函数是否需要传递参数等,可以进行额外的考虑

1.2 定时器的状态变换

  在实际的使用过程中,有些情况下需要定时器自启动开始直至程序终结都要运行,同样有些情况下需要定时器在某一时刻启动之后运行若干个周期或者完成某项功能后即退出,同样还有些情况下需要在某些特定的时刻触发一次。。。需求总是五花八门多种多样的,所以定时器的核心功能就是定时器的状态切换。

  参考于C#的定时器类,那么可以定义定时器的状态为 运行中、已停止、定时到达。

  1. 如果定时器当前状态为运行中,那么定时器应该要时刻判断当前的定时的时间是否到达,如果时间为到达,则继续运行,如果定时的时间到达,则将状态切换为定时到达的状态。
  2. 如果定时器当前状态为已停止,那么定时器将不进行任何操作。
  3. 如果定时器当前状态为定时到达(也就是超时),那么就首先执行回调函数,完成指定的任务。然后需要判断当前定时的模式。如果定时器的模式为单次触发,那么本次结束后将状态切换为已停止,如果定时器的模式为周期触发,那么就更新本次的到期的时间,然后继续运行。

1.3 定时器的时钟更新

  作为和时间相关的功能,那么运行中都需要一个基准,这个基准就是 时钟,时钟的粒度与精确度直接决定了定时器的 粒度精确度,在单片机开发中,我们可以使用单片机中的定时器中断来保证时间的精确度,单片机的晶振决定了时间的粒度。但是在基于操作系统的开发中,我们不能方便的拿到时钟频率,也不能保证通用性,同时还需要兼顾CPU消费与精度等等。

  在基于Linux系统的嵌入式开发中,前面已经说明了函数 setitimer 的相关功能,详细说明参考博文 Linux环境下的setitimer的延时/定时器的使用。所以采用此函数作为定时器的时钟功能。

二、代码实现

2.1 定时器的结构定义

  依据于前面得描述,本博文中设计的定时器的结构体如下所示。

/* 软件定时器的结构定义 */
typedef struct _softTimer_struct
{ 
    uint8_t state;                              /* 状态 */
    uint8_t mode;                               /* 模式 */
    uint32_t timeout;                           /* 到期时间 */
    uint32_t interval;                          /* 定时周期 */
    void (*handler)(uint16_t argc, void *argv); /* 回调函数指针 */
    uint16_t argc;                              /* 参数的个数 */
    void *argv;                                 /* 参数指针 */
} softTimer_st;

  另外,同一时刻定时器只能处于某一种状态,所以定时器的状态定义为共用体来表示,主要存在三种状态:停止运行超时。本设计中定时器的状态定义如下所示。

/* 定时器的状态定义 */
typedef enum _timerState_struct
{ 
    SOFTTIMER_STATE_STOPPED = 0xAA, /* 停止 */
    SOFTTIMER_STATE_RUNNING = 0x55, /* 运行 */
    SOFTTIMER_STATE_TIMEOUT = 0xCC  /* 超时 */
} timerState;

  同时根据一般的需求,定时器的工作模式定义为两种:单次运行模式周期运行模式。本设计中定时器的状态定义如下所示。

/* 定时的工作模式定时 */
typedef enum _timerMode_Struct
{ 
    SOFTTIMER_MODE_ONE_SHOT = 0x55, /* 单次运行模式 */
    SOFTTIMER_MODE_PERIODIC = 0xAA, /* 周期运行模式 */
    SOFTTIMER_MODE_DEFAULT = 0xFF,  /* 默认模式,无效 */
} timerMode;

2.2 定时器的状态更新

  设计中需要兼顾多个定时器的情况,每个定时都会有三种状态进行更新,为了增加适用性,将所有的定时器用一个指针的形式传递进来,同时将有多少个定时器也传递进来,然后根据定时器的数量一一进行状态更新。

  依据于前面的描述,本设计的定时器的状态更新实现代码如下所示。

/************************************************************** * 函数名称:softTimer_Update * 函数功能:定时的状态更新 * 函数参数: * timer :定时器的指针,不能为NULL * timernum:定时器的数量,不能超过定义的数量 * 函数返回: 无 * 函数错误: * 如果参数无效(timer指针为NULL,或者num大于实际定义的定时 * 的数量,本函数将导致进程奔溃) **************************************************************/
void softTimer_Update(softTimer_st *timer, uint16_t timernum)
{ 
    uint16_t i;

    for (i = 0; i < timernum; i++)
    { 
        switch (timer[i].state)
        { 
        case SOFTTIMER_STATE_STOPPED:
            break;

        case SOFTTIMER_STATE_RUNNING:
            /* 比较超时值与当前值的关系 */
            if (timer[i].timeout <= timerTicks_Get())
            { 
                timer[i].state = SOFTTIMER_STATE_TIMEOUT;
                /* 执行回调函数 */
                timer[i].handler(timer[i].argc, timer[i].argv);
            }
            break;

        case SOFTTIMER_STATE_TIMEOUT:
            /* 如果为单次触发,则超时之后设置为停止状态 */
            if (timer[i].mode == SOFTTIMER_MODE_ONE_SHOT)
            { 
                timer[i].state = SOFTTIMER_STATE_STOPPED;
            }
            else
            { 
                /* 比较超时值与当前值的关系 */
                timer[i].timeout = timerTicks_Get() + timer[i].interval;
                timer[i].state = SOFTTIMER_STATE_RUNNING;
            }
            break;

        default:
            break;
        }
    }
}

  按照常理设计来说,应该需要判断传入参数的合理性,但是作为一个数量的参数不好判断是否有效的(从参数的类型已经保证必须大于0),所以需要调用者来保证参数的有效性。

  这样做同样存在一个缺点,就是如果传入的参数无效,比如定时器的指针为NULL,或者定时器的数量过大(指的是超过实际定义的数量)等情况,那么将会导致进程异常奔溃。

2.3 定时器的启停控制

  在启动定时器的时候,需要告诉定时器当前的系统时钟值,便于定时器在运行过程中监控已经经历的时间,当前计数值加上定时的时间间隔即可得到定时器的超时的时间,所以启动定时器的功能函数可以如下编写。

/************************************************************************ * 函数名称:softTimer_Start * 函数功能:启动定时,需要设置定时的相关属性 * 函数参数: * timer :定时器的指针,不能为NULL * id :定时器的ID * mode :定时器的模式 * interval :定时器的定时时间 * callback :定时器到达后的服务函数指针 * argc :服务函数的参数个数 * argv :服务函数的参数指针 * 函数返回: * 启动成功:0 * 启动失败:非0 * 函数错误: * 如果参数无效(主要指的是id过大或者interval过大),则可能会导致进程奔溃 ************************************************************************/
int softTimer_Start(softTimer_st *timer, uint16_t id, timerMode mode,
                    uint32_t interval, void *callback, uint16_t argc, void *argv)
{ 
    /* 指针类型判断,不得为NULL */
    if (timer == NULL || callback == NULL)
    { 
        return -1;
    }
    /* 模式必须为有效的模式 */
    if (mode != SOFTTIMER_MODE_ONE_SHOT && mode != SOFTTIMER_MODE_PERIODIC)
    { 
        return -2;
    }

    timer[id - 1].state = SOFTTIMER_STATE_RUNNING;       /* 状态为运行状态 */
    timer[id - 1].mode = mode;                           /* 模式 */
    timer[id - 1].interval = interval;                   /* 运行的时间间隔 */
    timer[id - 1].timeout = timerTicks_Get() + interval; /* 运行到期的时间 */
    timer[id - 1].handler = callback;                    /* 服务函数 */
    timer[id - 1].argc = argc;                           /* 服务函数参数的个数 */
    timer[id - 1].argv = argv;                           /* 服务函数参数逇指针 */

    /* 返回成功 */
    return 0;
}

  不光要启动定时器,而且还要在需要的时候能够停止定时器,有些异常的场合则需要在定时器运行的过程中停止定时器来保护上下文,所以,定时器停止功能即修改定时器的状态即可。代码实现如下所示。

/************************************************************************ * 函数名称:softTimer_Stop * 函数功能:停止定时 * 函数参数: * timer :定时器的指针,不能为NULL * id :要停止的定时器的ID * 函数返回:无 * 函数错误: * 如果参数无效(主要指的是id过大),则可能会导致进程奔溃 ************************************************************************/
void softTimer_Stop(softTimer_st *timer, int16_t id)
{ 
    timer[id].state = SOFTTIMER_STATE_STOPPED;
}

2.4 定时器的状态获取

  在定时器中的运行中已经在实时的更新定时器的状态,并且在一些情况下可能会停止定时器,那么同样的在一些时候需要判断定时器的状态从而进行下一步的操作。所以定时器的状态函数可以直接返回其状态。

/************************************************************************ * 函数名称:softTimer_GetState * 函数功能:获取定时的状态 * 函数参数: * timer :定时器的指针,不能为NULL * id :要停止的定时器的ID * 函数返回: * 定时器的状态,在参数有效的情况下此函数永远返回正确 * 函数错误: * 如果参数无效(主要指的是id过大),则可能会导致进程奔溃 ************************************************************************/
uint8_t softTimer_GetState(softTimer_st *timer, uint16_t id)
{ 
    return timer[id].state;
}

2.5 定时器的时钟系统

  根据上面的描述,定时器的时钟系统无非就是注册 signal 处理,启动 setitimer 计时而已。所以实现代码如下所示。

/************************************************************** * 函数名称:softTimer_Timeticks_Start * 函数功能:定时的时钟启动 * 函数参数: * tv : 时钟的粒度 * 函数返回: * 启动成功,返回true * 启动失败,返回false * 函数错误:无 **************************************************************/
static bool softTimer_Timeticks_Start(uint32_t tv)
{ 
    bool return_val = true;

    /* 设置信号的处理方式 */
    signal(SIGALRM, softTime_Running_Timeticks);
    memset(&g_Running_Timeticks, 0, sizeof(g_Running_Timeticks));

    /* Timeout to run first time */
    g_Running_Timeticks.it_value.tv_sec = 0;
    g_Running_Timeticks.it_value.tv_usec = tv;

    /* After first, the Interval time for clock */
    g_Running_Timeticks.it_interval.tv_sec = 0;
    g_Running_Timeticks.it_interval.tv_usec = tv;

    /* 以系统真实的时间来计算,送出SIGALRM信号 */
    if (setitimer(ITIMER_REAL, &g_Running_Timeticks, NULL) < 0)
    { 
        printf("Fatal error %s\n", strerror(errno));
        return_val = false;
    }

    return return_val;
}

  但是需要注意的地方就是,上面函数的调用时机与参数的传递,下面一一说明。

调用时机:
  按理来说定时器的时钟应该需要在定时启动之前启动且只能启动一次,根据目前方案中采用的方式,所以时钟的启动和初始化应该放在定时器初始化函数中比较合理。当然也可以生成一个外部函数经由调用者去专门初始化,但是那样的定时器的封装将分离,所以还是推荐隐式调用。

参数设置:
  参数的大小决定了定时器的时钟的粒度,从而决定了定时器的粒度。从Linux系统来说,此值设置的过小将会失去一部分精度,设置过大则无意义,所以需要根据自己实际使用的场景去设置合适的值。

2.6 定时器的初始化

  系统要运行,首先躲不过一个初始化,不初始化则可能会产生一些莫名其妙的问题,而且本设计中初始化部分需要启动定时器的时钟,所以初始化非常重要!!!

/************************************************************** * 函数名称:softTimer_Init * 函数功能:定时器初始化,设置默认项 * 函数参数: * timer :定时器的指针,不能为NULL * timernum:定时器的数量,不能超过定义的数量 * 函数返回: 无 * 函数错误: * 如果参数无效(timer指针为NULL,或者num大于实际定义的定时 * 的数量,本函数将导致进程奔溃) **************************************************************/
void softTimer_Init(softTimer_st *timer, uint16_t timernum)
{ 
    uint16_t i = 0;
    
    /* 设置定时器时钟的粒度值,测试效果设置为10ms */
    (void)softTimer_Timeticks_Start(10000);

    /* 初始化每个定时器 */
    for (i = 0; i < timernum; i++)
    { 
        timer[i].state = SOFTTIMER_STATE_STOPPED;
        timer[i].mode = SOFTTIMER_MODE_DEFAULT;
        timer[i].timeout = 0;
        timer[i].interval = 0;
        timer[i].handler = NULL;
        timer[i].argv = NULL;
        timer[i].argc = 0;
    }
}

三、定时器功能测试

3.1 测试源码

  由上而下,定时器的基本功能的代码基本已经实现,那么应该还需要来测试一下,有没有破绽,有没有什么意想不到的问题等,所以本设计中设计了如下所示的测试代码。

#include "softtime.h"

/* 定义三个定时器 */
softTimer_st timer[3];

/* 定时器1的回掉函数,输出字符串 */
void stringPrintFunc(uint16_t argc, void *argv)
{ 
    struct timeval wtm;
    gettimeofday(&wtm, NULL);
    /* 设置输出到终端的颜色 */
    printf("\e[1;31margc = %d, argv = %s, stringPrint:%ld.%03ld\e[0m\n",
           argc, (char *)argv, wtm.tv_sec, wtm.tv_usec / 1000);
}

/* 定时器2的回掉函数,参数为空测试 */
void paraNullTest(uint16_t argc, void *argv)
{ 
    struct timeval wtm;
    gettimeofday(&wtm, NULL);
    /* 设置输出到终端的颜色 */
    printf("\e[1;32mparaNullTest:%ld.%03ld\e[0m\n", wtm.tv_sec, wtm.tv_usec / 1000);
}

/* 定时器3的回掉函数,单次触发测试 */
void nopnopnopnop(uint16_t argc, void *argv)
{ 
    struct timeval wtm;
    gettimeofday(&wtm, NULL);
    /* 设置输出到终端的颜色 */
    printf("\e[1;34margc = %d, argv = %d, nopnopnopnop:%ld.%03ld\e[0m\n",
           argc, *(int *)argv, wtm.tv_sec, wtm.tv_usec / 1000);
}

int main(int argc, const char *argv[])
{ 
    struct timeval tv;
    char *string = "I just grabbed a spoon.";
    int data = 0x3FF3U;

    /* 定义定时的ID的别名 */
    uint16_t TMR_STRING = 1;
    uint16_t TMR_PARANULL = 2;
    uint16_t TMR_DELAY_ON = 3;

    /* 定时器初始化 */
    softTimer_Init(timer, 3);

    /* 启动定时器,周期运行、间隔100ms,输出字符串 */
    softTimer_Start(timer, TMR_STRING, SOFTTIMER_MODE_PERIODIC, 10, stringPrintFunc, 5, string);
    /* 启动定时器,周期运行、间隔30ms,参数为空 */
    softTimer_Start(timer, TMR_PARANULL, SOFTTIMER_MODE_PERIODIC, 3, paraNullTest, 0, NULL);
    /* 启动定时器,单次运行、间隔100ms,输出字符串 */
    softTimer_Start(timer, TMR_DELAY_ON, SOFTTIMER_MODE_ONE_SHOT, 10, nopnopnopnop, 0, &data);

    /* 无限循环 */
    while (1)
    { 
        /* 定时器状态更新 */
        softTimer_Update(timer, 3);

        /* 适当延时,减少CPU占用 */
        tv.tv_sec = 0;
        tv.tv_usec = 1;
        select(0, NULL, NULL, NULL, &tv);
    }

    return 0;
}

3.2 测试效果

  代码已经就绪了,现在就来看看到底是好用还是不好用,在linux环境下直接在终端下进行编译然后直接运行,程序运行的效果如下图3.1所示。

《Linux系统下基于setitimer的高精度嵌入式软件定时器实现方案》
图3.1 程序运行效果图

  从上面运行效果图中可以看出来,三个定时均达到了设计的效果,并且每个定时器的定时的时间基本上保持时间稳定(偶尔会出现误差1ms,下一周期立即追回),所以综合来说,设计基本符合设想,基本满足设计的要求。

四、源码下载

  本设计中源码已经上传到CSDN下载模块,有需要的同学可以自信下载测试,下载链接为:

  https://download.csdn.net/download/zhemingbuhao/85419167

  好啦,废话不多说,总结写作不易,如果对本文的内容有任何疑问或者建议,请随时联系并欢迎讨论,由于本人学识与精力有限,难免会有一些错误,欢迎大佬指出并斧正。如果你觉得此文章对你有帮助,烦请帮忙点个赞鼓励一下,你的肯定将是我继续努力坚持的动力。

《Linux系统下基于setitimer的高精度嵌入式软件定时器实现方案》
《Linux系统下基于setitimer的高精度嵌入式软件定时器实现方案》

    原文作者:青椒*^_^*凤爪爪
    原文地址: https://blog.csdn.net/zhemingbuhao/article/details/124799672
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞