Computer Science
탄탄한 기반 실력을 위한
전공과 이론 지식 모음
Today I Learned!
배웠으면 기록을 해야지
TIL 사진
Flutter 사진
Flutter로 모바일까지
거꾸로캠퍼스 코딩랩 Flutter 앱개발 강사
스파르타코딩클럽 즉문즉답 튜터
카카오테크캠퍼스 3기 학습코치
프로필 사진
박성민
임베디드 세계에
발을 들인 박치기 공룡
임베디드 사진
EMBEDDED SYSTEM
임베디드 SW와 HW, 이론부터 실전까지
ALGORITHM
알고리즘 해결 전략 기록
🎓
중앙대학교 소프트웨어학부
텔레칩스 차량용 임베디드 스쿨 3기
애플 개발자 아카데미 1기
깃허브 사진
GitHub
프로젝트 모아보기
Instagram
인스타그램 사진

Embedded System/MCU

[MCU] Basic Timer

sm_amoled 2025. 11. 7. 19:20
STM32F429ZIT6 / NUCLEO-F429ZI 보드 기준

Basic Timer의 목적

Basic Timer로 분류되는 TIM6과 TIM7은 다음의 특징을 가진다.

  • 시간 측정만 수행하는 아주 단순한 타이머이다. (Generic Timer!)
  • 내부적으로 DAC와 연결되어, DAC의 출력 트리거로 사용할 수 있다.
  • Waveform 생성을 위해 사용할 수 있다. (신호 파형)

시간 측정만을 위해 사용하는 단순한 타이머인데 이게 어떻게 신호 파형을 만드는데에 사용될 수 있다는거지? → 라는 생각이 들어서 찾아봤는데, DAC의 출력 트리거로 사용될 수 있다는게 힌트였다. DAC(Digital2Analog)에서 신호의 트리거를 Basic Timer를 활용해 제어해서 Waveform을 만들어낼 수 있다!

TIM6랑 DAC로 파형 만들어서 로직애널라이저로 찍어보는 실습은 아래에서 확인할 수 있다!

Basic Timer의 동작 이해

Timer의 핵심 구성요소는 다음과 같다.

  • Counter 레지스터
  • Prescaler 레지스터
  • Auto-Reload 레지스터

Counter 의 동작 방식

잠깐! 용어정리부터!

  • Update Event (UEV) : 카운터가 끝에 다다랐을 때(오버플로우/언더플로우 발생) 소리지름.
  • Overflow / Underflow : 카운터가 값을 넘어서는 현상 (ARR 레지스터 값을 초과하거나 0 아래로 내려가는것)
  • Update Interrupt Flag (UIF) : 소리지른거(인터럽트) 해결 여부. 아직 해결 안했으면 SET

Auto Reload 레지스터의 값이 36이라고 하자. (=Counter의 목표값이 36!) 그러면 아래와 같은 프로세스를 따른다.

  1. Counter Register의 값이 Auto Reload 레지스터의 값과 동일해질 때 까지 일정한 주기마다 Counter 레지스터의 값을 키운다.
  2. 만약 동일해졌다면 Counter Overflow 이벤트가 발생한다.
  3. Counter Register는 다시 0으로 돌아가고, Interrupt Flag를 Set 한다. (타이머가 Auto Reload 레지스터의 값까지 도달했음 알리기)
  4. 위 과정을 반복한다.

그렇다면 어떻게 Counter의 값을 키우냐?

기본적인 Basic Timer의 Counter 레지스터의 값 증가 과정은 다음과 같다. 아래 그림은 prescaler가 1에서 4로 바뀌는 과정을 표현한 것이다. 우선은 오른쪽 부분만 먼저 보자.

  1. CK_PSC를 통해 CLK이 똑딱똑딱 들어온다.
  2. Prescaler Counter가 CLK_PSC의 Rise마다 값을 1 증가시킨다.
  3. Prescaler Counter의 값이 (Prescaler Buffer + 1) 값이 될 때 0으로 초기화한다.
  4. Prescaler Counter가 0으로 초기화됨과 동시에 CK_CNT가 Tick 되고, Counter 레지스터 값이 1씩 증가한다.
  5. 위 과정을 반복한다.

위 그림에서, Prescaler Control Register의 값을 도중에 바꾸었음에도 바로 해당 변경이 적용되어 Prescaler Buffer의 값이 변경되지 않고, Counter Register에 Overflow Event가 발생한 이후에 변경사항이 적용되고 있다. 타이머의 동작 도중에 Auto Reload Register 의 값을 변경할 수 있는데 (버그가 아니라 기능이예요), 이 값들은 따로 설정을 통해서 즉시 반영할지, 아니면 임의의 신호(Update Event)가 들어와야 반영할 지 결정해줄 수 있다. Prescaler Counter 레지스터의 값은 항상 Update Event에 의해 영향을 받는 듯

Prescaler Counter 도 Update Event 없이 바로 반영 가능한가?
→ Prescaler는 만약 즉시 반영이 되어버린다면 도중에 Counter를 올리는 클럭 자체가 변경되어버린다. 그러면 타이머에서 측정한 이번 주기에 대한 카운트가 의미가 없어지게 되기 때문에 이런 방식은 지원되지 않는 것으로 생각된다. ARR은 그나마 명확해서 (의미있으니깐) preload 없이 사용할 수 있는 것 같은데, PSC는 불필요한 CLK 낭비를 줄이기 위해 즉시 반영이 되지 않는 것 같다.

레지스터 파악

Basic Timer의 Presclaer와 Auto-Reload 레지스터는 둘 다 16bit의 범위를 가지고 있다. 즉, Counter 레지스터의 값을 올리는 것은 타이머의 주기를 최대 “65535 CLK 에 한 번”까지 늦출 수 있고, Counter 레지스터가 0으로 초기화되어 Overflow Event가 발생하게 되는 값은 최대 65535 까지만 설정할 수 있다.

들어오는 CLK에 대해서 최대 약 $2^{32}$ CLK 정도를 타이머로 카운트 할 수 있다. 이게 몇 초인지는 연결된 CLK의 주파수에 따라 달라진다.

그외에 Basic Timer가 가지고 있는 레지스터들

Basic Timer는 2가지 Control 레지스터를 가지고 있다.

  • ARPE Auto-Reload Preload Enable : ARR 레지스터에 값을 담았을 때 즉시 반영할 지 여부
  • OPM One-Pulse Mode : 카운터가 다음 Update Event 이후 중지 (CEN RESET)
  • URS Update Request Source : Update Event의 발생 원인을 결정
    • 0 : Overflow/Underflow + UG (Update Generate)의 SET + Slave Mode Controller 의 신호가 모두 인터럽트를 발생시킬 수 있도록 하겠다.
    • 1 : 단순히 카운터의 Overflow / Underflow만 Update Event 를 SET 하게 하겠다.
  • UDIS Update Disable : Update Event의 발생 여부를 결정
    • 0 : Update Event 발생 허용. UEV가 발생하면 preload된 ARR, PSC → 반영시킨다.
    • 1 : Update Event 비활성화. ARR과 PSC는 반영되지 않고 기다린다. 만약 UG를 직접 SET or Slave Mode에서 신호가 왔다면 반영 가능
  • CEN Counter Enable : 카운터 활성화

2번째 Control 레지스터는 Master Mode에 대한 제어를 담당한다.

  • UDE Update DMA request Enable : DMA의 접근을 허용
  • UIE Update Interrupt Enable : 인터럽트 발생 허용

  • UIF Update Interrupt Flag : Update Event가 발생하면 이 Bit 가 SET된다. UIF이 1이라면 인터럽트의 처리를 기다리고 있다는 뜻. 인터럽트를 처리했다면 이걸 직접 RESET 해줘야한다.

  • UG Update event Generation : UG bit 에다가 값을 1 쓰면 곧바로 Update Event를 호출해버린다. SR 레지스터의 UIF bit가 1로 SET 되고, Count 레지스터의 값은 초기값(0)으로 즉시 변경된다.

실습해보자

이번에도 UART를 이용해 PC쪽으로 매 0.5초마다 . 또는 #을 전송하는 것을 Polling과 Interrupt를 사용하는 방식으로 타이머를 사용해보자

Polling 방식

Polling 방식에서는 이전 SysTick과 거의 유사하다. 다른점은 Counter가 끝까지 도달했다는 신호(FLAG)를 코드를 통해서 수동으로 RESET 해줘야한다. 나머지는 유사하게 Prescaler를 설정해주고, ARR값을 지정해주고 카운터를 활성화해주면 된다.

(플래그를 자동으로 안꺼주는 건 버그가 아니라 기능인가? 왜인지 찾아볼 것)
→ SysTick은 RTOS의 Tick에 대한 인터럽트의 호출을 위해 있는 타이머라서 플래그의 상태를 체크할 이유가 없다. 최대한 오버헤드를 줄여서 OS의 스케줄링을 빠르게 수행하는 것이 목적이다.

일반적인 Timer 들은 Flag를 SW적으로 RESET 하도록 만들어서 요런 장점들을 챙길 수 있다.

  • 특정한 상황에서만 인터럽트를 처리하기
  • 인터럽트 도중에 발생한 인터럽트 (놓친 인터럽트) 확인
  • 하나의 Source(타이머) 에서 발생하는 여러 인터럽트에 대해서 구분 가능
  • 기타 둥둥

아래 코드에서는 0.5초 마다 출력을 하기 위해서 다음의 설정을 사용해줬다.

  • Prescaler : 16MHz 에서 CLK이 1ms 마다 Counter 레지스터의 값을 1만큼 증가시키도록 하기 위해 16000 (-1 보정) 으로 지정해주었다. 여기에서 1을 빼주는 이유는 Prescalser는 0을 지정하면 1배, 1을 지정하면 2배, 2를 지정하면 3배… 이렇게 증가하기 때문임 ( N +1 배)
  • Auto-Reload : 1ms 마다 증가하는 Counter가 500ms마다 다시 리셋되고 인터럽트 혹은 플래그를 표시하도록 하기위해 500 (-1 보정)으로 지정해주었다. 여기에서 1을 빼주는 이유는 0부터 카운트를 시작하기 때문이다.

  // TIM6 클럭 활성화
  RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;

  // TIM6 설정
  // 0.5초 = 500ms를 만들기 위한 설정
  // Prescaler 설정: 16MHz / (15999 + 1) = 1kHz
  TIM6->PSC = 16000 - 1;

  // Auto-reload 설정: 1kHz / (499 + 1) = 2Hz (0.5초 주기)
  TIM6->ARR = 500 - 1;

  // 카운터 초기화
  TIM6->CNT = 0;

  // 타이머 활성화
  TIM6->CR1 |= 0x1;

  while (1)
  {
      // TIM6의 Update Interrupt Flag 값이 1이라면
        if(TIM6->SR & 0x1) {
            // UIF 값만 0으로 초기화해주고
      TIM6->SR &= ~(0x1);

            while(((USART3->SR >> 7) & 0x1) == 0);
            USART3->DR = '.';
        }
  }

Interrupt 방식

인터럽트 방식에서는 Interrupt Enable Bit을 Set 해주어서 Counter에서 Overflow Event가 발생할 때 마다 인터럽트를 호출해주면 된다.

  // TIM6 클럭 활성화
  RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;

  // TIM6 설정
  // 0.5초 = 500ms를 만들기 위한 설정
  // Prescaler 설정: 16MHz / (15999 + 1) = 1kHz
  TIM6->PSC = 16000 - 1;

  // Auto-reload 설정: 1kHz / (499 + 1) = 2Hz (0.5초 주기)
  TIM6->ARR = 500 - 1;

  // 카운터 초기화
  TIM6->CR1 |= 0x1;

  // Update 인터럽트 활성화
  TIM6->DIER |= 0x1;

  // 타이머 활성화
  TIM6->CR1 |= 0x1;

  while (1)
  {

  }

인터럽트는 아래처럼 Interrupt 파일에서 TIM6 에서 적용할 수 있는 이름을 찾아서 UART로 #을 출력하는 함수를 작성해 재정의해주었다.

void TIM6_DAC_IRQHandler(void)
{
    // Update 인터럽트 플래그 확인
    if(TIM6->SR & 0x1)
    {
        // 인터럽트 플래그를 0으로 초기화
        TIM6->SR &= ~(0x1);

        while(((USART3->SR >> 7) & 0x1) == 0);
        USART3->DR = '#';
    }
}

그런데 여기까지만 하면 인터럽트가 동작하지 않는다! 이건 인터럽트가 발생하기는 하지만, 이 발생한 인터럽트에 대해서 CPU가 처리하기 위해서는 NVIC의 도움이 필요하기 때문이다.

문서에서 NVIC 파트를 찾아 살펴보면 아래처럼 CMSIS를 이용해 NVIC 레지스터를 제어하는 함수가 소개되어있다. 이 중에서 우리는 NVIC에서 Interrupt Request를 활성화 해야하므로, NVIC_EnableIRQ에 대해서 파악해야한다.

CubeIDE에서 이 NVIC_EnableIRQ 함수를 찾아서 타고 들어가보면 아래처럼 구성되어있다. ㄷ

여기에서 인자로 넣어주는 IRQn_Type 은 IRQ의 번호를 말한다. 문서에서 ISR_Vector에 있는 TIM6의 인터럽트 번호를 확인해보면 54번으로 되어있다.

함수를 살펴보면 결국 위 함수가 하는 일은 NVIC→ISER[idx] 에다가 특정한 위치에 값을 대입해주고있다. 여기는 무엇이냐? NVIC_ISER 레지스터에서 TIM6에 해당하는 bit 에다가 1을 넣어주는 작업을 하고 있다. 하나의 ISER 레지스터가 최대 32개의 IRQ만 받아들일 수 있기 때문에, 54번 IRQ를 Enable 하기 위해서 NVIC_ISER2 의 22번 bit 에다가 1을 대입해주면 된다.

  NVIC_EnableIRQ(54);
  // 또는
  // NVIC_EnableIRQ(TIM6_DAC_IRQn);

  // 타이머 활성화 이전에 위 코드 작성해주기
  TIM6->CR1 |= TIM_CR1_CEN;

NVIC에 대한 내용은 곧 정리해서 또 올라올 예정임.

Basic Timer와 DAC로 파형 만들기

제일 궁금했다. 사인파의 형태를 만들기 위해 우선 사인파의 값들을 아래처럼 만들어줬다. 클로드의 도움을 좀 받았다 ^,<. 0~4095 범위에서 사인파의 형태를 64개의 t로 쪼개어 값으로 넣어주었다.

$$
v= 2048 + 2047 \times \sin(\frac{t}{2\pi}))
$$

// 64 샘플 사인파 테이블 (12-bit, 0~4095)
// 진폭: 0V ~ 3.3V
const uint16_t sine_table[64] = {
    2048, 2248, 2447, 2642, 2831, 3013, 3185, 3347,
    3496, 3631, 3750, 3853, 3939, 4007, 4056, 4086,
    4095, 4086, 4056, 4007, 3939, 3853, 3750, 3631,
    3496, 3347, 3185, 3013, 2831, 2642, 2447, 2248,
    2048, 1847, 1648, 1453, 1264, 1082,  910,  748,
     599,  464,  345,  242,  156,   88,   39,    9,
       0,    9,   39,   88,  156,  242,  345,  464,
     599,  748,  910, 1082, 1264, 1453, 1648, 1847
};

타이머의 클럭을 보고, 신호의 주파수를 1kHz(1ms)로 맞추기 위해서 적당한 값으로 나눠 PSC와 ARR 값을 넣어주고, 타이머와 관련된 여러가지 설정들을 넣어준다.

void TIM6_Init(void)
{
    // TIM6 클럭 활성화 (APB1)
    RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;

    // APB1 타이머 클럭: 90MHz (STM32F429, APB1=45MHz, 타이머 클럭 x2)
    // 목표: 64kHz 인터럽트
    // 90MHz / (PSC+1) / (ARR+1) = 64kHz

    // PSC = 0: 90MHz 유지
    TIM6->PSC = 0;

    // ARR = 1405: 90MHz / 1406 ≈ 64kHz
    // 정확한 계산: 90,000,000 / 64,000 = 1406.25 ≈ 1406
    TIM6->ARR = 1405;

    // 카운터 초기화
    TIM6->CNT = 0;

    // Update 인터럽트 활성화
    TIM6->DIER |= TIM_DIER_UIE;

    // Update generation (설정값 즉시 적용)
    TIM6->EGR |= TIM_EGR_UG;

    // NVIC에서 TIM6 인터럽트 활성화
    NVIC_EnableIRQ(TIM6_DAC_IRQn);
    NVIC_SetPriority(TIM6_DAC_IRQn, 0);

    // 타이머 시작
    TIM6->CR1 |= TIM_CR1_CEN;
}

Digital to Analog

void DAC_GPIO_Init(void)
{
    // GPIOA 클럭 활성화
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

    // PA4를 Analog 모드로 설정
    GPIOA->MODER &= ~(0x3 << (4*2));    // 클리어
    GPIOA->MODER |= (0x3 << (4*2));     // 0b11: Analog mode

    // Pull-up/Pull-down 없음
    GPIOA->PUPDR &= ~(0x3 << (4*2));
}

void DAC1_Init(void)
{
    // DAC 클럭 활성화 (APB1)
    RCC->APB1ENR |= RCC_APB1ENR_DACEN;

    // DAC 채널1 설정
    // - EN1: 채널1 활성화
    // - BOFF1: 출력 버퍼 OFF (더 빠른 응답)
    // - TEN1: 트리거 비활성화 (소프트웨어 트리거)
    DAC->CR &= ~DAC_CR_EN1;             // 설정 중에는 비활성화

    DAC->CR &= ~DAC_CR_TSEL1;           // 트리거 소스 클리어
    DAC->CR &= ~DAC_CR_TEN1;            // 트리거 비활성화
    DAC->CR |= DAC_CR_BOFF1;            // 출력 버퍼 OFF

    // DAC 채널1 활성화
    DAC->CR |= DAC_CR_EN1;
}

이 신호를 매 타이머 주기마다 호출하는 것이 목표이므로, TIM6의 인터럽트 핸들러에서 TIM6로부터 인터럽트가 발생한 경우, 매 인터럽트마다 순서대로 sin 파의 출력을 샘플링하도록 해주었다.

// 현재 샘플 인덱스
volatile uint8_t sample_index = 0;

/**
 * @brief  TIM6 인터럽트 핸들러
 *         64kHz로 호출되어 DAC 값을 업데이트
 */
void TIM6_DAC_IRQHandler(void)
{
    // Update 인터럽트 플래그 확인
    if(TIM6->SR & 0x1)
    {
        // 플래그 클리어
        TIM6->SR &= ~(0x1);

        // DAC에 현재 샘플 값 출력
        DAC->DHR12R1 = sine_table[sample_index];

        // 다음 샘플로 이동
        sample_index++;
        if(sample_index >= 64)
        {
            sample_index = 0;
        }
    }
}

앞서 작성한 코드들을 main 에서 호출해주면 준비 완료이다.

// 1. GPIO 초기화 (PA4)
DAC_GPIO_Init();

// 2. DAC1 초기화
DAC1_Init();

// 3. TIM6 초기화 및 시작
TIM6_Init();

while(1) {

}

실행해보고 GPIO DAC와 연결된 PA4 핀으로부터 파형을 출력해보면, 아래처럼 신호가 주기적으로 출력되는 것을 볼 수 있다!

각 신호를 쭉 당겨서 확인해보면, 이렇게 HIGH/LOW가 삐뚤빼뚤하게 신호로 전달되는 것을 볼 수 있다. 각 신호의 간격은 대략 0.77~0.86ms 정도.

나는 이게 DAC의 출력 파형이라고 생각했다. 그런데, 분명 나는 아날로그로 출력했던 것 같은데 왜 이런 신호가…? 혹시 이걸 다른 장치에서 다시 아날로그로 바꾸는건가…? 그럴리가 없는데…? 라는 생각에 빠져있었다가 검색을 해봤는데, 내가 이 신호를 측정하기 위해서 사용한 장비가 오실로스코프가 아니라 로직 애널라이저이고, 로직 애널라이저는 Digital 신호만 측정할 수 있기 때문에, 아날로그 파형을 측정할 수가 없는게 정상이라고 한다… 위 파형은 그냥 재미로 보자 ^,^

유의할 점

CR1 레지스터의 UDIS 는 Interrupt 도 막아버린다.

알고싶지 않았다.

아래처럼 코드 환경을 구성해서, PSC, ARR 값이 진짜로 안바뀌는지 테스트를 해보고싶었는데, UDIS 에다가 1을 대입해서 Update Event의 생성을 막아버렸더니 인터럽트로 다시 들어오지 않는 문제가 발생해버렸다. 🤨🤨

void TIM6_DAC_IRQHandler(void)
{
    // Update 인터럽트 플래그 확인
    if(TIM6->SR & 0x1)
    {
        // 인터럽트 플래그를 0으로 초기화
        TIM6->SR &= ~(0x1);

        HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_7);

                // 이게 UDIS 비트를 1로 설정하는 코드!
                // UDIS가 활성화되면 PSC, ARR이 실제로 반영되지 않고 기다린다.
        TIM6->CR1 |= (0x1 << 1);

        TIM6->PSC = 1000;
        TIM6->ARR = 100;
    }
}

이게 그래서 PSC, ARR 값을 Shadow에다가 반영하지 않고 Preload로 가지고 있는건 맞는지 확인해봤는데, 이건 잘 되는 것 같다. (물론 Preload 기능을 Enable 해야한다. CR1 레지스터에서 설정해줄 수 있는 항목!) 그치만 인터럽트를 꺼버린다니…? 이건 내가 생각하지 못했던 사이드이펙트인데..?

혹시 이 상태에서 폴링은 동작하는지 확인해봐야겠다 싶어서 아래처럼 코드를 구성했다. 그런데 테스트를 해보니, SR 레지스터의 UIF 비트도 SET이 되지 않는 문제가 있었다.

  while (1)
  {
      // 상태 레지스터에서 UIF(Update Interrupt Flag)를 확인하고
      // SET 되어있다면 작업 수행 
      if(TIM6->SR & 0x1) {
      TIM6->SR &= ~(0x1);

          HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_7);
      }
  }

전혀 동작하지 않음.

아 그래서 이게 용도가 또 따로 있는건가? 혹시 UG 비트를 직접 SET해서 Update Event를 직접 Generate 하면 되는건가? 싶어서 코드를 작성해봤는데, 동일하게 동작하지 않았다. 엥엥엥???

while (1)
{
  if(TIM6->SR & 0x1) {
      HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_7);
  }

    // 유저 버튼이 눌렸을 때
  if(GPIOC->IDR >> 13 & 0x1) {
  // UG 비트를 직접 SET 해서 이벤트 만들기 
  // -> 이거 해도 인터럽트 안걸림
      TIM6->EGR |= 0x1;

    // 결국 UDIS를 다시 RESET 해줘야 다시 동작함
  // TIM6->CR1 &= ~0x2;
  }
}

사실상 UDIS랑 CLK Enable이 비슷하게 동작하는 현상을 보이고 있었다. 이게 단순히 값 업데이트를 중지하는 느낌의 레지스터 비트는 아닌 듯 하다.

그래서 내가 확인한 바에 따르면 UDIS를 SET하면 Update Event가 생성되지 않으며,

  • Interrupt도 발생하지 않음
  • UIF(Update Interrupt Flag)가 SET되지 않음
  • 그래서 폴링도 작동하지 않음
  • UG 비트로 수동 trigger해도 작동하지 않음

이것이 버그가 아니라 기능이라는 것.

UDIS는 PSC와 ARR의 값 반영을 막는 기능이라기보다는, 타이머 CLK은 멈추지 않으면서 “Counter의 값이 현재 무의미하다 → PSC와 ARR 반영하고 나서 의미있을 때부터 다시 이벤트를 발생시키겠따”는 기능이라고 봐야할 것 같다.

320x100

'Embedded System > MCU' 카테고리의 다른 글

[MCU] General Purpose Timer  (0) 2025.11.09
[MCU] STM32F429ZIT6의 타이머들  (0) 2025.11.07
[MCU] SysTick 타이머  (2) 2025.11.04
[MCU] Push-Pull과 Open-Drain의 장단점?  (0) 2025.11.02
[MCU] 풀업, 풀다운 저항은 MOSFET인가?  (0) 2025.11.02