소프트웨어 Timer 만들기

오늘 포스팅할 내용은 소프트웨어 Timer입니다. 소프트웨어 Timer는 MCU의 페리페럴 중 하나인 하드웨어 타이머와 다르며 오히려 PC 소프트웨어의 Timer에 가깝습니다.

 

구현하고자하는 소프웨어 Timer는 설정한 주기가 되면 자동으로 등록한 Callback 함수를 실행시켜 줍니다. 소프트웨어 Timer이기 때문에 Tick을 증가시켜줄 MCU의 하드웨어 Timer가 하나 필요합니다.

 

먼저 자주 사용하는 주기를 파악 합니다. 최소단위를 10ms으로 정하고 10ms 주기의 하드웨어 Timer를 하나 만들어 인터럽트에서 소프트웨어 Timer의 Tick을 증가시켜 줍니다.

 

먼저 헤더파일로 전체 윤곽을 만듭니다. Timer 핸들러의 typedef도 선언되어 있습니다.

#ifndef SRC_TIMER_H_
#define SRC_TIMER_H_

#define TIMER_MAX_COUNT   (10)  /*the value of max*/
#define TIMER_TICK  (10)  /*10ms*/
#define TIMER_10ms  ((uint32_t)(  10/TIMER_TICK))
#define TIMER_50ms  ((uint32_t)(  50/TIMER_TICK))
#define TIMER_100ms ((uint32_t)( 100/TIMER_TICK))
#define TIMER_1s    ((uint32_t)(1000/TIMER_TICK))
#define TIMER_3s    ((uint32_t)(3000/TIMER_TICK))
#define TIMER_5s    ((uint32_t)(5000/TIMER_TICK))

typedef enum
{
  ONE_SHOT,
  PERIODIC,
}timer_mode;
typedef void (*timer_handler)(int8_t timer_id, uint32_t param1);

extern int8_t register_timer(uint32_t period, timer_handler handler, timer_mode mode);
extern int8_t unregister_timer(int8_t timer_id);
extern void   clear_timer(void);
extern void   tick_timer(void);
extern void   process_timer(uint32_t param1, uint32_t param2);


#endif /* SRC_TIMER_H_ */
 

1 Tick 을 10ms으로 계산해서 자주 사용하는 시간을 #define으로 미리 정의해 놓았습니다. 그리고 외부에서 호출하는 함수들은 extern으로 선언해 주었습니다.

 

다음은 소프트웨어 Timer의 Tick을 증가시키기 위한 10ms 하드웨어 Timer 인터럽트 코드입니다. 실제로 10ms 인터럽트에 tick_timer()를 호출해야 합니다. 예제는 TIM7을 10ms 인터럽트로 설정해 놓은 IRQ 핸들러 입니다.

void TIM7_IRQHandler(void)
{
  /* USER CODE BEGIN TIM7_IRQn 0 */
  if(LL_TIM_IsActiveFlag_UPDATE(TIM7) == 1)
  {
    tick_timer();
    LL_TIM_ClearFlag_UPDATE(TIM7);
  }
  /* USER CODE END TIM7_IRQn 0 */
  /* USER CODE BEGIN TIM7_IRQn 1 */

  /* USER CODE END TIM7_IRQn 1 */
}

 

인터럽트 내에서 실행하여야 하기 때문에 간단한 동작만 합니다. 등록된 Timer 들의 주기를 향해 tick들을 증가시킵니다만, 해당 Timer의 핸들러가 동작중일때는 증가시키지 않습니다.

void tick_timer(void)
{
  for(int8_t i=0; i<TIMER_MAX_COUNT; i++)
  {
    if(timer[i].handler == NULL)
    {
      continue;
    }

    if(timer[i].busy == 0)
    {
      timer[i].tick++;
    }
  }
}
 

다음 소스는 Timer 등록 및 삭제, 실행 코드들 입니다. 중요하지만 어려운 코드는 아닙니다. 예제는 10개까지 등록이 가능하도록 했는데 필요한 만큼 갯수를 조절하면 됩니다.

 

typedef timer_t은 아래와 같이 설명됩니다.

  • id : 고유번호
  • busy : 해당 타이머 핸들러가 실행중인지를 표시하는 플래그입니다.
  • tick : 해당 타이머의 tick counter
  • period : 해당 타이머 핸들러의 실행 주기입니다.
  • handler : 해당 타이머의 주기가 돌아왔을 때 호출되는 콜백 핸들러입니다.
  • mode : 해당 타이머가 한번만 실행되는지 계속 주기적으로 실행하는 모드인지 결정합니다.
typedef struct
{
  int8_t   id;
  uint8_t  busy;
  uint32_t tick;
  uint32_t period;
  timer_handler handler;
  timer_mode mode;
}timer_t;

static timer_t timer[TIMER_MAX_COUNT]={0};

/**
  * @brief register_timer
  */
int8_t register_timer(uint32_t period, timer_handler handler, timer_mode mode)
{
  for(int8_t i=0; i<TIMER_MAX_COUNT; i++)
  {
    if(timer[i].handler == NULL)
    {
      timer[i].tick = 0;
      timer[i].id = i;
      timer[i].busy = 0;
      timer[i].period = period;
      timer[i].handler = handler;
      timer[i].mode = mode;
      return i;
    }
  }

  return -1;
}

/**
  * @brief unregister_timer
  */
int8_t unregister_timer(int8_t timer_id)
{
  if((timer_id > 0) && (timer_id < TIMER_MAX_COUNT))
  {
    timer[timer_id].handler = NULL;
    return 0;
  }

  return -1;
}

/**
  * @brief clear_timer
  */
void clear_timer(void)
{
  for(int8_t i=0; i<TIMER_MAX_COUNT; i++)
  {
    timer[i].handler = NULL;
  }
}
 

clear_timer는 모든 타이머들을 초기화 하는 함수입니다.

 

실제 타이머를 실행하는 함수 process_timer 는 메인(main.c)의 무한루프에서 호출합니다. 각각의 타이머의 tick이 period를 넘어가는 순간 핸들러를 실행시키고 busy 플래그를 set해줍니다. 실행이 끝나면 다시 reset합니다.

 

10ms이기 때문에 하나의 타이머 핸들러가 실행하는 시간이 10ms 보다 길면 다음에 실행되어야 할 핸들러의 지연이 증가 할 수 있어 안됩니다. 그러한 코드는 RTOS를 사용하거나 혹은 프로세스를 쪼개서 분리해야 합니다. 만약 타이머 모드가 ONE_SHOT이면 한번 실행하고 타이머를 없앱니다.

void process_timer(uint32_t param1, uint32_t param2)
{
  for(int8_t i=0; i<TIMER_MAX_COUNT; i++)
  {
    if(timer[i].handler == NULL)
    {
      continue;
    }

    if(timer[i].tick >= timer[i].period)
    {
      timer[i].busy = 1;
      timer[i].tick = 0;
      timer[i].handler(timer[i].id, param1);
      timer[i].busy = 0;

      if(timer[i].mode == ONE_SHOT)
      {
        unregister_timer(i);
      }
    }
  }
}
 

그럼, 실제 main에서 타이머를 등록하고 실행하는 코드를 작성해 보도록 하겠습니다. 아래 예제의 process1은 10ms에 한번씩 호출되고 process2는 50msec에 한번씩 호출 될 것입니다.

/* main.c */
void main(void)
{
	register_timer(TIMER_10ms, process1);
	register_timer(TIMER_50ms, process2);
	register_timer(TIMER_100ms, process3);

	while(1)
	{
		process_timer(0, 0);
	}
}

void process1(int8_t timer_id, uint32_t param1)
{
	...
}

void process2(int8_t timer_id, uint32_t param1)
{
	...
}

void process3(int8_t timer_id, uint32_t param1)
{
	...
}
 

위의 코드를 보면 process_timer(0,0) 처럼 파라메타가 2개인 것을 볼 수 있을 텐데요, 이미 이전 스케줄러 코드를 보신 분들은 왜 그렇게 했는지 아실 것 같습니다. 맞습니다. 이전에 만든 "RTOS 없이 구현한 스케쥴러"과 연동하기 위해서 그렇게 했습니다. 아래 처럼 그 스케쥴러와 연동해 볼까요?

/* main.c */
void main(void)
{
	register_timer(TIMER_10ms, process1);
	register_timer(TIMER_50ms, process2);
	register_timer(TIMER_100ms, process3);

	register_schedule(process_timer)

	while(1)
	{
		execute_schedule(0, 0);
	}
}

void process1(int8_t timer_id, uint32_t param1)
{
	...
}

void process2(int8_t timer_id, uint32_t param1)
{
	...
}

void process3(int8_t timer_id, uint32_t param1)
{
	...
}
 

이렇게 하면 타이머를 실행시키는 함수도 스케쥴러에 들어가 다른 것들과 함께 순차적으로 실행될 것입니다. 만약 지연 오차를 허용하지 않는 함수가 있다면 인터럽트내에서 process_timer를 실행되도록 코드를 추가하면 될 것입니다.

 

감사합니다.

 

Full source : https://github.com/soloungos/stm32g431rb_sotfware_timer

 

 

 

 

'▶ C Application > 디자인 패턴' 카테고리의 다른 글

Adapter 패턴 - 1  (0) 2023.12.24
Factory method 패턴  (0) 2023.12.23
Template method 패턴  (0) 2023.12.17
Iterator 패턴 - Queue  (1) 2023.12.16
Singleton 패턴  (0) 2023.12.16