单片机软件定时器的实现方法

描述

1.1 背景

目前市面上的单片机基本都带有硬件定时器功能,单片机应用程序开发中也经常会用到定时器进行一些和时间相关的开发,比如延时或者周期性地执行一些操作。单片机的硬件定时器个数一般都是固定的,而且一些低端单片机的定时器个数一般都比较少,在一些有多个周期性操作的应用场合就无法满足要求。这时,就可以基于硬件定时器派生出软件定时器,来满足这种多种周期性或多个单次延时操作的需求。软件定时器的优点就是个数可以根据实际需求进行灵活配置,而且可以实现多种不同的定时周期。

1.2 测试平台

这里使用的开发环境和相关硬件如下。

  • 操作系统:Ubuntu 20.04.2 LTS x86_64(使用uname -a命令查看)
  • 集成开发环境(IDE):Eclipse IDE for Embedded C/C++ Developers,Version: 2021-06 (4.20.0)
  • 硬件开发板:STM32F429I-DISCO
  • 本文对应的例程代码链接如下。

https://download.csdn.net/download/goodrenze/85106391

1.3 软件定时器实现方法

这里就结合开发板STM32F429I-DISCO上的STM32F429ZI的单片机来演示软件定时器的实现方法。

一般定时器的计数方式有2种:一种是单次定时,即定时时间到了之后,自动停止定时;另一种是周期定时,定时时间到了之后,自动按照之前的定时周期重新定时。对于周期定时,可以手动进行定时器的启动、关闭和删除。

下面讲解软件定时器的实现步骤。

1)由于软件定时器是基于硬件定时器的,所以需要先初始化一个硬件定时器,并启动硬件定时器。这里使用STM32F429ZI的硬件定时器7,定时器的定时周期为10ms,即每10ms产生一次定时器中断。初始化代码如下。

TIM_HandleTypeDef    Tim7Handle;
uint8_t InitTim7(uint32_t period_ms)
{
  uint16_t uwPrescalerValue;


  if(0 == period_ms)
  {
    return 1;
  }


  __HAL_RCC_TIM7_CLK_ENABLE();


  HAL_NVIC_SetPriority(TIM7_IRQn, 2, 0);


  HAL_NVIC_EnableIRQ(TIM7_IRQn);


  /* Compute the prescaler value to have TIM7 counter clock equal to 10 KHz */
  uwPrescalerValue = (uint32_t) ((SystemCoreClock /2) / 10000) - 1;


  /* Set TIM7 instance */
  Tim7Handle.Instance = TIM7;
  Tim7Handle.Init.Period = period_ms * 10 - 1;
  Tim7Handle.Init.Prescaler = uwPrescalerValue;
  Tim7Handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  Tim7Handle.Init.CounterMode = TIM_COUNTERMODE_UP;
  if(HAL_TIM_Base_Init(&Tim7Handle) != HAL_OK)
  {
    return 1;
  }


  /* Start the TIM Base generation in interrupt mode */
  if(HAL_TIM_Base_Start_IT(&Tim7Handle) != HAL_OK)
  {
    return 1;
  }


  return 0;
}

2)硬件定时器定时时间到了之后,会产生中断,所以需要实现定时器中断处理函数。这里基于STM32的HAL进行开发,所以在定时器的中断入口函数中直接调用HAL_TIM_IRQHandler()函数,然后实现实际的中断处理回调函数HAL_TIM_PeriodElapsedCallback()。对应的代码如下。其中调用的软件定时器更新函数会在后面介绍。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if(htim == &Tim7Handle)
  {
    SwTimerUpdateCount();
  }
}

3)设计软件定时器对应的结构体。按照软件定时器的实际使用特点,必须要包含定时器计数值和周期定时器的重装载值,另外还要有定时器时间到之后需要执行的回调函数。对应的软件定时器结构体如下所示。当结构体中的TimerCount和TimerReload都为0时,说明软件定时器处于空闲状态,可以分配使用;如果TimerCount非0,而TimerReload为0,说明软件定时器是单次定时器;如果TimerCount和TimerReload都非0,说明软件定时器是周期定时器。

typedef void (*TimerCallbackFunc)(void);


typedef struct _SwTimer_t
{
  uint32_t TimerCount;
  uint32_t TimerReload;
  TimerCallbackFunc TimerCallback;
}SwTimer_t;

4)这里将软件定时器设置成10个,可以通过宏定义来设置软件定时器个数。使用软件定时器结构体定义一个具有10个定时器的数组。如下代码所示。

#define SW_TIMER_NUM    10
SwTimer_t SwTimer[SW_TIMER_NUM];

5)软件定时器复位函数,用于实现所有软件定时器的重置操作,重置后,所有软件定时器都处于空闲状态,可供分配使用。函数代码如下。

void SwTimerReset(void)
{
  uint8_t i;


  for(i = 0; i < SW_TIMER_NUM; i++)
  {
    SwTimer[i].TimerCount = 0;
    SwTimer[i].TimerReload = 0;
    SwTimer[i].TimerCallback = 0;
  }
}

6)启动单次定时器函数,用于实现单次定时,定时时间到了之后,执行对应回调函数,并停止定时和释放定时器资源。函数代码如下。如果启动成功,函数返回定时器的索引号(该值小于定时器个数值);启动失败,返回的定时器索引号等于定时器的个数。

uint8_t SwTimerStartSingleTimer(uint32_t single_ms, TimerCallbackFunc TimerCallback)
{
  uint8_t i;


  single_ms /= MINI_PERIOD_MS;
  if(0 == single_ms)
  {
    single_ms = 1;
  }
  for(i = 0; i < SW_TIMER_NUM; i++)
  {
    if((SwTimer[i].TimerCount == 0) && (SwTimer[i].TimerReload == 0))
    {
      SwTimer[i].TimerCount = single_ms;
      SwTimer[i].TimerCallback = TimerCallback;
      break;
    }
  }


  return i;
}

7)添加周期定时器函数,用于添加一个新的周期定时器但不启动定时,需要手动启动定时器。函数代码如下。如果添加成功,函数返回定时器的索引号(该值小于定时器个数值);添加失败,返回的定时器索引号等于定时器的个数。

uint8_t SwTimerAddPeriodTimer(uint32_t period_ms, TimerCallbackFunc TimerCallback)
{
  uint8_t i;


  period_ms /= MINI_PERIOD_MS;
  if(0 == period_ms)
  {
    period_ms = 1;
  }
  for(i = 0; i < SW_TIMER_NUM; i++)
  {
    if((SwTimer[i].TimerCount == 0) && (SwTimer[i].TimerReload == 0))
    {
      SwTimer[i].TimerReload = period_ms;
      SwTimer[i].TimerCallback = TimerCallback;
      break;
    }
  }


  return i;
}

8)启动周期定时器函数,用于启动指定定时器索引号的周期定时器开始定时。函数代码如下。如果启动成功,函数返回定时器的索引号(该值小于定时器个数值);启动失败,返回的定时器索引号等于定时器的个数。

uint8_t SwTimerStartPeroidTimer(uint8_t timer_no)
{
  if(SW_TIMER_NUM <= timer_no)
  {
    return SW_TIMER_NUM;
  }
  else if((SwTimer[timer_no].TimerCount == 0) && (SwTimer[timer_no].TimerReload == 0))
  {
    return SW_TIMER_NUM;
  }
  else
  {
    SwTimer[timer_no].TimerCount = SwTimer[timer_no].TimerReload;
    return timer_no;
  }
}

9)停止定时器函数,用于结束指定定时器索引号的定时器的定时,可用于停止单次或周期定时器。函数代码如下。如果停止成功,函数返回定时器的索引号(该值小于定时器个数值);停止失败,返回的定时器索引号等于定时器的个数。

uint8_t SwTimerStopTimer(uint8_t timer_no)
{
  if(SW_TIMER_NUM <= timer_no)
  {
    return SW_TIMER_NUM;
  }
  else
  {
    SwTimer[timer_no].TimerCount = 0;
    return timer_no;
  }
}

10)删除周期定时器函数,用于结束指定定时器索引号的周期定时器的定时,并释放定时器资源。函数代码如下。如果删除成功,函数返回定时器的索引号(该值小于定时器个数值);删除失败,返回的定时器索引号等于定时器的个数。

uint8_t SwTimerDeletePeroidTimer(uint8_t timer_no)
{
  if(SW_TIMER_NUM <= timer_no)
  {
    return SW_TIMER_NUM;
  }
  else
  {
    SwTimer[timer_no].TimerCount = 0;
    SwTimer[timer_no].TimerReload = 0;
    return timer_no;
  }
}

11)软件定时器计数值更新函数,用于更新每个已经启动定时的软件定时器的计数值,该函数必须在硬件定时器的中断处理函数中调用。函数的实现思路是:遍历所有的软件定时器,如果遍历到的定时器的计数值非0,则进行减1操作。如果减1后计数值为0,如果定时器的重装载值非0,说明是周期定时器,需要将计数值更新成对应的重装载值以便重新定时,同时执行对应的回调函数;如果定时器的重装载值是0,说明是单次定时器,执行完回调函数后自动停止定时并释放定时器资源。如果减1后计数值不为0,继续遍历更新后续的定时器,直到所有定时器都遍历完毕。函数流程图和对应代码如下。

函数

图1 软件定时器计数值更新函数

void SwTimerUpdateCount(void)
{
  uint8_t i;


  for(i = 0; i < SW_TIMER_NUM; i++)
  {
    if(SwTimer[i].TimerCount != 0)
    {
      SwTimer[i].TimerCount -= 1;
      if(SwTimer[i].TimerCount == 0)
      {
        if(SwTimer[i].TimerReload != 0)
        {
          SwTimer[i].TimerCount = SwTimer[i].TimerReload;
        }
        if(SwTimer[i].TimerCallback != 0)
        {
          SwTimer[i].TimerCallback();
        }
      }
    }
  }
}

12)软件定时器的实际使用示例。代码如下。

int main(void)
{
  uint8_t no;


  HAL_Init();


  /* Configure the system clock to 168 MHz */
  SystemClock_Config();


  BSP_LED_Init(LED3);
  BSP_LED_Init(LED4);
  InitTim7(MINI_PERIOD_MS);
  SwTimerReset();
  no = SwTimerAddPeriodTimer(500, ToggleLed3);
  if(no < SW_TIMER_NUM)
  {
    SwTimerStartPeroidTimer(no);
  }
#if 1
  no = SwTimerAddPeriodTimer(1000, ToggleLed4);
  if(no < SW_TIMER_NUM)
  {
    SwTimerStartPeroidTimer(no);
  }
#else
  no = SwTimerStartSingleTimer(5000, ToggleLed4);
#endif


  while (1)
  {
  }
}
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分