Timer 만들기

지금 말씀드리고자 하는 Timer는 MCU의 인터럽트 타이머를 이야기하는 것은 아닙니다. 우리가 PC프로그램에서 익히 알고 있는 Software Timer입니다. 

 

이 Timer는 알람처럼 해당 주기가 되면 자동적으로 callback 함수를 실행시켜 줍니다. 물론 Timer 인터럽트에서 Tick을 증가 시킬수 있도록 도와주어야 합니다. 

 

우선 자주 사용하는 주기를 파악해야 합니다. 최소단위를 10msec으로 보고 10msec 인터럽트에서 등록된 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_ */

 

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

 

다음은 10msec 타이머 인터럽트에서 직접 콜 해줘야 할 코드입니다. 실제로 10msec 인터럽트에 넣어야 합니다. 인터럽트 내에서 실행하여야 하기 때문에 정말 간단해야 합니다. 등록된 Timer 들의 주기를 향해 tick들을 증가시킵니다만, 해당 Timer의 핸들러가 동작중일때는 증가시키지 않습니다.

/**
  * @brief process_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 등록 및 삭제, 실행 코드들 입니다. 별다른 내용은 없고 배열에 넣었다 빼었다 하는 내용입니다. 현재 13개까지 등록이 가능하도록 했는데, 알아서 갯수를 조절해서 사용하면 됩니다. typedef timer_t을 살펴볼까요?


id고유번호,
busy는 해당 타이머 핸들러가 실행중인지를 표시하는 플래그라고 보시면 됩니다.
tick은 해당 타이머의 count tick 그리고
period는 해당 타이머 핸들러의 실행 주기입니다.

handler는 타이머의 시간이 되었을 때 호출될 함수 포인터입니다.

mode는 타이머의 종류로 한번만 실행되는 ONE_SHOT, 주기적으로 실행되는 PERIODIC입니다.

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는 모든 타이머들을 초기화 하는 함수입니다. 실제 타이머를 실행하는 함수를 살펴보겠습니다. 아래 함수는 메인의 무한루프에서 호출합니다. 각각의 타이머의 tick이 period를 넘어가는 순간 핸들러를 실행시키고 busy 플래그를 set해줍니다. 실행이 끝나면 다시 reset합니다. 코드를 보니 감이 오시나요? 

10msec이기 때문에 하나의 타이머 핸들러가 실행하는 시간이 10msec 보다 길면 안됩니다.  그러한 코드가 많으면 RTOS를 사용하시던가 프로세스를 쪼개서 main 무한루프에서 실행 시켜야 합니다.

/**
  * @brief process_timer
  */
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은 10msec에 한번씩 호출되고 process2는 100msec에 한번씩 호출 될 것입니다. 

int main(void)
{
  register_timer(TIMER_100ms, process1, PERIODIC);
  register_timer(TIMER_1s, process2, PERIODIC);
  register_timer(TIMER_3s, process3, ONE_SHOT);
  
  while (1)
  {
    process_timer(0, 0);
  }
}

static void process1(int8_t timer_id, uint32_t param1)
{
  printf("process1 \r\n");
}

static void process2(int8_t timer_id, uint32_t param1)
{
  printf("process2 \r\n");
}

static void process3(int8_t timer_id, uint32_t param1)
{

  printf("one shot process3 !!! \r\n");
}

 

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

int main(void)
{
  register_timer(TIMER_100ms, process1, PERIODIC);
  register_timer(TIMER_1s, process2, PERIODIC);
  register_timer(TIMER_3s, process3, ONE_SHOT);
  
  register_schedule(process_timer);
  
  while (1)
  {
    process_timer(0, 0);
  }
}

 

이렇게 하면 타이머를 실행시키는 함수도 스케쥴에 들어가 알아서 다른 것들과 함께 순차적으로 실행 될 것 같습니다. 만약 한치의 오차도 없이 실행되어야 하는 함수는 인터럽트에서 실행되도록 코드를 변경해야 할 것 같습니다. 예를 들면 LED 깜빡이는 것과 같은 거나, PWM on/off는 조금만 타이밍이 틀어져도 눈에 혹은 귀에 띄니깐요.

 

감사합니다.

'▶ 이전글 > C Pattern' 카테고리의 다른 글

facade 패턴  (0) 2018.01.11
Message 기반 만들기  (0) 2018.01.11
Scheduler 구현하기  (0) 2018.01.11
C 언어에서 this 구현  (0) 2015.01.20
Iterator 패턴 - Queue  (0) 2015.01.20