CofMilk 发表于 2023-7-26 16:13

一个在单片机上使用的软件定时器

# 单片机软件定时器
#### 介绍
本程序实现了一个向下计数自动重装载的定时器。时基通过一个硬件定时器确定。不含有任何与底层MCU相关代码,可以移植到任何嵌入式平台。定时器回调函数的运行可以在中断中,或者在主函数while循环中执行。可以使用提供的各种API函数,来修改定时器的计数值、重装载值,运行次数。可以开启或关闭单个软件定时器。
#### 数据结构介绍
这个是软件定时器的结构体,包含了一个软件定时器运行必须的变量。
```
/* 软件定时器结构体声明 */
typedef struct
{
                SWT_uint8_t id;            //软件定时器ID
                SWT_uint32_t count;      //计数值
                SWT_uint32_t expire;       //重装载值
                timeout_handler callback;//回调函数
                SWT_uint8_t isEnable;   //定时器使能 0:disable 1:enable
                SWT_int16_t repeat;      //定时器循环次数 -1:forever
                SWT_uint8_t run;         //回调函数运行标志 0:不运行,1:运行
} SoftwareTimerStruct;
```
这里使用了一个结构体数组来保存多个软件定时器。
```
/* 软件定时器结构体 */
SoftwareTimerStruct SoftwareTimerList;
```
#### 核心代码介绍
软件定时器最核心的代码就是经过一个时基的时间后,处理各种变量的函数SoftwareTimer_Tick()(以下简称tick函数)。
通过在硬件定时器中断服务函数中的tick函数,来实现定时器中计数值的变化,判断是否要运行各个软件定时器的回调函数,以及实现各种需求的功能。

在tick函数中,每次进入函数都轮询一遍软件定时器的结构体数组,根据是否使能对应编号的定时器来决定是否对结构体数组内的成员变量进行操作。如果某些设计好的软件定时器并未使能,则跳过此软件定时器,继续检查下一个定时器。

如果这个软件定时器已经使能了,那么继续判断计数值是否大于0,如果大于0,就让计数值减1;如果等于0,则计数值不能减1,否则会出现变量数值下溢,影响到后续的判断。

因为这里实现的是一个向下计数的定时器,所以这里需要判断计数值是否为0。如果为0,则说明需要触发软件定时器的回调函数了,但是这里不在定时器中断服务中运行软件定时器的调函数,而是将这个回调函数运行标志位置1。以便在其他地方轮询这个标志位,再运行回调函数。这样的处理方法可以解决在硬件定时器的中断服务函数中运行过多代码的问题。

与一般的硬件定时器不同,这里增加了一个循环运行次数的功能,使用一个变量来保存需要运行的次数,每运行一个回调函数这个变量就减1,当这个变量为0时,则失能这个软件定时器,若要再次运行软件定时器,则需要重新使能软件定时器,并设置循环次数。将变量设置为最大值时,则认为是无限循环。

那么直到把所有的软件定时器轮询一遍,那么这一个时基要做的事情就完成了。
```
void SoftwareTimer_Tick(void)
{
        SWT_uint8_t i = 0;
        //轮询所有的软件定时器
        for (i = 0; i < SOFTWARE_TIMER_NUM; i++)
        {
                //如果定时器未使能,则跳过。
                if (SoftwareTimerList.isEnable == 0)
                {
                        continue;
                }
                /**
               * @brief 为了解决初始化之后计数值为零。此时计数值不应该再减一。
               * @EditTime 2021年11月24日17:13:44
               *
               */
                if (SoftwareTimerList.count > 0)
                {
                        //软件定时器任务计数值减少
                        SoftwareTimerList.count--;
                }
                //当软件定时器计数值为0时
                if (SoftwareTimerList.count == 0)
                {
                        //软件定时器计数值重装载。
                        SoftwareTimerList.count = SoftwareTimerList.expire;
                        //软件定时器循环次数不为零
                        if (SoftwareTimerList.repeat > 0)
                        {
                                SoftwareTimerList.repeat--;
                                //将回调函数运行标志位置1
                                SoftwareTimerList.run = 1;
                        }
                        //软件定时器循环次数为零
                        else if (SoftwareTimerList.repeat == 0)
                        {
                                //不使能此软件定时器
                                SoftwareTimerList.isEnable = 0;
                        }
                        else
                        {
                                //设置为无限循环时
                                SoftwareTimerList.run = 1;
                        }
                }
        }
}
```
#### 使用说明
1.需要确定时基,一般由一个硬件定时器提供,将SoftwareTimer_Tick()函数放入定时器中断服务函数下。
下面的示例代码展示了使用一个1ms的硬件定时器给软件定时器生成一个1s的时基。
```
    //stm32平台1ms定时器生成1s的时基的中断服务函数
    void TIM1_UP_IRQHandler(void)
    {
      static uint16_t TimesCounter_ms = 0;
      if (TIM_GetITStatus(TIM1, TIM_IT_Update))
      {
            TimesCounter_ms++;
            TIM_ClearITPendingBit(TIM1, TIM_IT_Update);
            if (TimesCounter_ms >= 1000)
            {
                TimesCounter_ms = 0;
                SoftwareTimer_Tick();
            }
      }
    }
```
2.使用SoftwareTimer_Init()函数初始化一个软件定时器。
```
    //这里初始化了两个软件定时器,重装载2,计数值分别为0,1,循环无限次
    //可以实现SW1_callback、SW2_callback两个回调函数前后依次运行
    SoftwareTimer_Init(0, 0, 2, SW1_callback, FOREVER);
    SoftwareTimer_Init(1, 1, 2, SW2_callback, FOREVER);
```
3.将SoftwareTimer_Loop()函数放到想要回调函数运行的位置
```
    //这里把SoftwareTimer_Loop放到了主函数while循环中,回调函数会在这里被调用。
    int main(void)
    {
                ....
      while(1)
      {
                                ....
            SoftwareTimer_Loop();
                                ....
      }
    }
```

####使用注意
本定时器定时时长与选择硬件定时器周期、所使用的MCU平台相关(51类单片机、ARM类单片机)。在相同平台下,时基越大则定时时间越长;不同平台下计数值所使用的位数不同,如STC15单片机(51类)中long型数据类型长度为4个字节,int型为2个字节;而在STM32F103中(ARM类)中,int型、long型均为4个字节。本质上来说,此软件定时器是一个软件计数器计数的最大次数与两次计数的最长时间最终都是由底层MCU来决定。

要注意的是,单个软件定时器任务要按照编写中断服务函数的原则来编写,尽量不要使用过长的延时。如果一定要使用软件延时,可以将此延时通过状态机机制实现。单个定时器任务的运行时间最好不要超过定时器时基长度。

作者以在STC15W、STC8A的51单片机上运行了上述代码,在keil C51的环境下,对于函数指针的使用需要自行维护keil中的函数调用树,否则会出现各种各样的内存错误。实际上是因为keil C51的编译器中使用了一种叫做overlay功能造成的,且函数调用树本身就是为了这个overlay功能服务的。个人使用的时候由于使用的单片机的资源相较于实现的功能来说比较富裕,所以我直接关闭了这个overlay功能。在Arm类的单片机中(这里作者使用过的基本就是STM32F103和一众国产类STM32的单片机),由于没有不存在overlay功能,所以基本没有什么问题。至于其他的单片机(什么AVR、PIC、树莓派balabala)几乎没有用过,所以只能是理论上来说是可以用的。

#### 结尾
从这个项目发布到现在也使用了将近两年,运行基本稳定。解决了上面函数指针使用的问题后就基本不出问题了。剩下就是业务层面上的逻辑问题了。源代码在这里[单片机软件定时器](http:///gitee.com/milkli/mcu-software-timer/tree/master)。
如果觉得好用的话,点个星星吧,诶嘿。

CofMilk 发表于 2023-8-8 17:05

heimareed 发表于 2023-8-8 02:04
结构体数组换成链表可能有奇效。理论上就可以不限Soft timer的数量,遍历的时候也无需判断结构体中的标志位 ...

是的 考虑过用链表形式的 。但是从使用场景上来看。链表的优势在于增删,,也就是添加定时器任务和删除定时器任务,而数组来说优势在遍历。对于单片机来说,,整块的内存访问要比链表的访问速度也快,,所以最后我是选择了这种数组的形式 而不是链表。不过我有其他的同事他是用的链表,,算是各有优劣把。

还有就是 这个限制不限制数量,,其实就我这里目前使用来说,都是静态使用,也就是说再程序写完的时候,数量就是固定的,,所以也不存在限制不限制数量这个说法。

最后还是感谢驻足观看~

CofMilk 发表于 2023-9-11 09:52

DDDY 发表于 2023-9-9 11:25
应该会受扫描周期影响吧?

emmm 你说的对,,,只不过这时间相比于现代的单片机的运行速度来说,很小很小。实际上我也没有专门去测试过,,如果遇到需要严格定时的地方,我还是会用硬件定时器。但就目前为止我这里没有要求的太严格。总而言之,这点时间差距可以接受。

CofMilk 发表于 2023-7-26 16:20

啊,新人发帖,为保账号,如有不适,还望海涵。

Yr99 发表于 2023-7-26 17:20

感谢分享!

高苗苗 发表于 2023-7-26 18:11

mark一下以备不时之需

BG8HVH 发表于 2023-7-26 18:42

做个记号,以备不时之需

kantal 发表于 2023-7-26 19:07

mark一下以备不时之需

leeyolo 发表于 2023-7-26 19:20

做个记号&#128527;

renpeng009 发表于 2023-7-26 19:32

写得很不错,介绍也十分详细

LQ584521 发表于 2023-7-26 20:22

支持下楼主。感谢

heng179 发表于 2023-7-26 20:24

页: [1] 2 3
查看完整版本: 一个在单片机上使用的软件定时器