[임베디드 OS 개발 프로젝트] 7. 타이머

Sijin·2025년 6월 22일

타이머

  • 임베디드 시스템에서 타이머는 목표 카운트 레지스터측정 카운트 레지스터를 조합해서 만든다
    • 목표 카운트 레지스터에 목표하는 값을 지정한다
    • 측정 카운트 레지스터의 값은 증가 혹은 감소한다
    • 두 레지스터 값이 같아지면 인터럽트를 발생시킨다
  • RealViewPB에서는 SP804라는 타이머 하드웨어를 가지고 있다

타이머 하드웨어 초기화

  • 타이머 하드웨어 레지스터를 추상화해보자
typedef struct Timer_t
 {
     uint32_t        timerxload;     // 0x00
     uint32_t        timerxvalue;    // 0x04
     TimerXControl_t timerxcontrol;  // 0x08
     uint32_t        timerxintclr;   // 0x0C
     TimerXRIS_t     timerxris;      // 0x10
     TimerXMIS_t     timerxmis;      // 0x14
     uint32_t        timerxbgload;   // 0x18
 } Timer_t;
  • Timer_t 타입 구조체에는 7개의 레지스터가 정의되어있다
    • timerxload는 목표 카운트 레지스터이다
    • timerxvalue는 측정 카운트 레지스터이다
      • 타이머가 켜지면, timerxvalue에 timerxload 값을 복사하고, timerxvalue가 감소한다
      • timerxvalue가 0이 되면 인터럽트가 발생한다
    • timerxcontrol은 하드웨어 속성을 설정하는 레지스터이다
    • timerxintclr은 인터럽트 처리 완료를 나타낸다
typedef union TimerXControl_t
 {
     uint32_t all;
     struct {
         uint32_t OneShot:1;     //0
         uint32_t TimerSize:1;   //1
         uint32_t TimerPre:2;    //3:2
         uint32_t Reserved0:1;   //4
         uint32_t IntEnable:1;   //5
         uint32_t TimerMode:1;   //6
         uint32_t TimerEn:1;     //7
         uint32_t Reserved1:24;  //31:8
     } bits;
 } TimerXControl_t;
  • TimerXControl_t는 타이머 하드웨어 속성을 설정한다
    • Oneshot이 1이면, 타이머 인터럽트 한 번 발생 후 타이머가 바로 꺼진다
    • TimerSize는 timerxload, timerxvalue의 크기를 설정한다
      • 0이면 16bit / 1이면 32bit를 모두 사용하게 된다
    • TimerPre는 1클럭/16클럭/256클럭마다 카운터를 줄일지 설정한다
    • IntEnable은 타이머의 하드웨어 인터럽트를 켠다
    • TimerMode는 free-running/periodic모드 중 선택할 수 있게 한다
      • free-running 모드에서는 timerxload를 사용하지 않고, 0xFFFF(32bit인 경우 0xFFFFFFFF)부터 0까지 카운트가 내려가야 인터럽트가 발생한다
      • periodic 모드에서는 timerxload에 지정한 값부터 0이 되면 인터럽트가 발생한다
      • 우리는 delay 함수를 구현할 것이므로 periodic 모드를 사용한다
    • timerEn은 타이머 하드웨어 전체를 켜고 끄는 레지스터이다
#define TIMER_CPU_BASE  0x10011000
#define TIMER_INTERRUPT 36
#define TIMER_1MZ_INTERVAL     (1024 * 1024)
  • TIMER_CPU_BASE는 타이머 하드웨어 레지스터가 할당된 메모리 주소이다
  • TIMER_INTERRUPT는 GIC에 전달되는 타이머 인터럽트 번호이다
  • TIMER_1MZ_INTERVAL는 1MHz 클럭에서 1초간 발생하는 클럭 횟수이다
volatile Timer_t* Timer = (Timer_t*)TIMER_CPU_BASE;
  • TIMER_CPU_BASE 주소값을 이용해서 Timer 객체를 선언하자
void Hal_timer_init()
{
    // reset
    Timer->timerxcontrol.bits.TimerEn = 0;
    Timer->timerxcontrol.bits.TimerMode = 0;
    Timer->timerxcontrol.bits.OneShot = 0;
    Timer->timerxcontrol.bits.TimerSize = 0;
    Timer->timerxcontrol.bits.TimerPre = 0;
    Timer->timerxcontrol.bits.IntEnable = 1;
    Timer->timerxload = 0;
    Timer->timerxvalue = 0xFFFFFFFF;

    // periodic mode
    Timer->timerxcontrol.bits.TimerMode = TIMER_PERIOIC;
    Timer->timerxcontrol.bits.TimerSize = TIMER_32BIT_COUNTER;
    Timer->timerxcontrol.bits.OneShot = 0;
    Timer->timerxcontrol.bits.TimerPre = 0;
    Timer->timerxcontrol.bits.IntEnable = 1;

    uint32_t interval_1ms = TIMER_10HZ_INTERVAL / 1000;

    Timer->timerxload = interval_1ms;
    Timer->timerxcontrol.bits.TimerEn = 1;

    internal_1ms_counter = 0;
    
    // register interrupt
    Hal_interrupt_enable(TIMER_INTERRUPT);
    Hal_interrupt_register_handler(interrupt_handler, TIMER_INTERRUPT);
}
  • Timer init 함수는 위와 같다
    • 우선 Timer의 control bits들을 초기화한다
    • 이후 Timer를 periodic 모드로 변경한다
    • timerxload에 1ms에 해당하는 값을 넣고 Timer를 enable 시킨다
      • RealViewPB의 타이머는 클럭으로 1MHz를 사용, 즉 1초에 1024 * 1024번의 clock이 발생한다
      • 1ms = 1/1000초이므로, TIMER_10HZ_INTERVAL / 1000interval_1ms로 사용한다
    • 1ms가 몇번 지났는지 체크하는 internal_1ms_counter를 0으로 초기화 한다
    • 타이머 관련 interrupt를 등록한다
static uint32_t internal_1ms_counter;

static void interrupt_handler(void)
{
    internal_1ms_counter++;

    Timer->timerxintclr = 1;
}
  • interrupt_handler에서는 인터럽트가 발생하였을 때 internal_1ms_counter를 증가시킨다
  • 이후 timerxintclr 레지스터를 통해 인터럽트 처리 완료를 타이머에 전달한다

delay 함수

타이머 카운트 overflow

  • 1ms마다 타이머 인터럽트가 발생하면 internal_1ms_counter에 1씩 값이 증가한다
  • internal_1ms_counter은 32bit 변수로, 최대 크기까지 변수가 증가하면 오버플로우가 발생한다
    • 0xffffffff / 1000 = 4294967초 = 1193시간 = 약 50일 이후에는 오버플로우가 발생할 것 이다
  • 몇 달, 몇 년 이상 전원이 켜져있는 시스템이 있을 수 있으므로 타이머 카운터의 오버플로우에도 정상 동작이 필요하다

delay 함수

  • delay 함수는 특정 시간 만큼 아무일도 하지 않고 시간이 지날 때 까지 기다리는 함수이다
  • delay 함수와 같은 유틸리티 함수는 stdlib.h에 정의한다
# include/stdlib.h

#ifndef LIB_STDLIB_H_
#define LIB_STDLIB_H_

void delay(uint32_t ms);

#endif /* LIB_STDLIB_H_ */
  • delay 함수는 타이머 카운터 internal_1ms_counter 값을 사용해야 한다
    • internal_1ms_counter는 로컬 전역 변수로 선언되어 있다
    • 이 값을 읽기 위한 인터페이스가 필요하다
  • HalTimer에 타이머 카운터 값을 읽기 위한 인터페이스를 선언 및 구현하자
uint32_t Hal_timer_get_1ms_counter()
{
    return internal_1ms_counter;
}
  • 만든 함수를 통해 delay 함수를 아래와 같이 구현할 수 있다
# lib/stdlib.c

void delay(uint32_t ms)
{
    uint32_t goal = Hal_timer_get_1ms_counter() + ms;
    while(goal != Hal_timer_get_1ms_counter());
}
  • 타이머 카운터 값이 goal에 도달했는지 확인하는 방법은 <!= 두가지 연산자를 활용하는 방법 두가지가 있다
    • 보통의 경우에는 <를 사용하는게 더 안전하다
    • 그러나 < 방식은 goal 변수에 overflow가 일어나는 경우 잘못 동작할 수 있다
    • 따라서, 위 코드처럼 != 방식을 사용해야 overflow에도 의도대록 동작하게 된다

동작 확인

# Main.c

static void Hw_init(void)
{
    ...
    Hal_timer_init();
}

static void Timer_test(void)
{
    while(true)
    {
        debug_printf("cur count: %u\n", Hal_timer_get_1ms_counter());
        delay(100);
    }
}

void main(void)
{
    Hw_init();
    ...
    Timer_test();
}
  • Main 코드에서 delay 함수와 internal_1ms_counter 값이 의도대로 증가하는지 확인해보면 아래와 같은 결과를 확인할 수 있다

0개의 댓글