본문 바로가기

stm32 레지스터 직접 접근 (6. Timer/Counter [함수 설명 Part 02])

반응형

관련글


stm32 레지스터 직접 접근 (6. Timer/Counter [코드 Part 01]) (tistory.com)

stm32 레지스터 직접 접근 (6. Timer/Counter [Registers Part 03]) (tistory.com)

 


 

 

더보기

 

 


 

 

서론


저번 시간에는 먼저 Timer/Counter 관련된 함수를 만들었습니다. 이번 시간에는 각 함수들이 어떤 역할을 수행하는지, 그리고 어떤 원리로 동작되는지에 대해 알아 볼 예정입니다. (Part로 끝나는 시리즈는 연관된 글이니, 꼭 순서에 맞게 따라오셔야합니다.)

함수의 자세한 기능와 레지스터에 대한 간략한 기능을 알아보는 포스트이며, 레지스터에 대해 좀 더 알고 싶다면 Part 03로 넘어가시면 됩니다.


Timer/Counter 원리

  Interrupt

initRCC

initTIM2

enableTIM2

TIM2_IRQHandler


참고 자료


 

STM32F103ZET6 Reference Manual

STM32F10xxx/20xxx/21xxx/L1xxxx Cortex®-M3 programming manual

 

Timer/Counter 원리


stm32에 있는 TC는 일정 시간이 지나면 Interrupt를 발생시킵니다. 그러면 여기서 중요한 것은 어떻게 그 일정 시간을 지정할 수 있는가?에 관한 것이겠죠. 이것에 대해 알려면 TC가 Interrupt를 부르는 원리를 간략하게라도 알아야합니다.

TC에는 두 가지 중요한 요소가 있습니다. 바로 카운터(CNT)기준값(정확히는 Auto reload value)입니다.

 

어떤 사람이 5초마다 마우스를 클릭하는 상황이 있습니다.

그 사람은 5...4...3...2...1 마우스 클릭, 다시 5...4...3...2...1 마우스 클릭 -> 5...이것을 반복할 것입니다.

마음속으로 5에서부터 숫자를 카운트하여 1이 되는 순간 마우스를 클릭하겠죠.

 

반대로 1...2...3...4...5 마우스 클릭, 다시 1...2...3...4...5 마우스 클릭 -> 1... 이렇게 숫자를 증가시키면서 5가 되면 마우스를 클릭하는 사람도 있을 것입니다.

 

첫 번째 상황은 5 -> 1로 숫자가 작아진다는 의미에서 Down count라고 부르고 두 번째 상황은 Up count라고 부릅니다.

MCU가 일정 주기마다 Interrupt를 수행하는 것도 마찬가지입니다. 내부에 카운터(CNT)가 0 -> 4까지 숫자를 세고 4가 되는 순간 Interrupt를 발생시킵니다. 또는 (CNT)가 4 -> 0까지 숫자를 세고 0이 되는 순간 Interrupt를 발생시킵니다. 여기서 CNT는 하드웨어적으로 자동으로 증가합니다.

여기서도 마찬가지로 0 -> 4까지 숫자를 세는 TC를 Up count TC라고 부르고 두 번째는 Down count TC라고 부릅니다. 그리고 두 상황 모두에서 4라는 숫자는 기준값 (정확히는 Auto Reload Value)라고 부릅니다. 즉, Up count TC라고 가정하고, CNT가 일정 주기마다 증가하다 ARV값을 만나면 CNT는 0으로 다시 변하고 Interrupt가 발생되는 원리입니다.

 

stm32의 TC는 Up count TC입니다. 만약에 0.5초마다 Interrupt를 발생시키고 싶다! 라고 한다면 여러 상황으로 이를 구현할 수 있습니다. 또한 stm32에서 실제 ARV 값은 우리가 설정한 레지스터의 ARV 값 + 1로 계산됩니다. 따라서 다음과 같이 계산합니다.

1. CNT를 0.1초 주기마다 증가하게 합니다. ARV를 4로 설정합니다. 그러면

 0 -> (0.1초 지남) -> 1 -> (0.1초 지남) -> 2 -> (0.1초 지남) -> 3 -> (0.1초 지남) -> 4 -> (0.1초 지남, Interrupt 발생) -> 5, 0 -> ...

로 0.5초마다 Interrupt가 발생될 것입니다.

 

또는 CNT의 주기를 바꿀 수도 있습니다.

2. CNT를 0.25초 주기마다 증가하게 합니다. ARV를 1로 설정합니다. 그러면

0 -> (0.25초 지남) -> 1 -> (0.25초 지남, Interrupt 발생) -> 2, 0 -> ...

로 동일하게 0.5초마다 Interrupt가 발생될 것입니다.

 

 

따라서 우리는 TC의 주기를 ARV와 CNT가 증가하는 주기로 설정할 수 있습니다.

TC의 주기 = (CNT가 증가하는 주기) / (ARV + 1)

 

stm32에서는 CNT가 증가하는 주기를 PSC 레지스터를 이용하여 설정할 수 있습니다.

TIM2는 APB1에 연결되어 있어 APB1에 공급되는 클럭으로 동작합니다. (자세한 건 initRCC 내용 참고) 예를 들어 APB1에 공급되는 클럭이 36 MHz면 이 클럭 값은 (PSC + 1)으로 나눠져서 TC2에 공급됩니다. 가령 PSC 값이 36,000 - 1이라면, TC2에는 (36,000,000 / (36,000 - 1 + 1)) = 1,000 Hz가 TC2(TIM2)에 공급되는 거죠.

 

주기 설정은 여기까지입니다. 그러면 Interrupt는 정확히 어떻게 발생되는지 그리고 어떻게 동작하는지 알아보겠습니다.

 

Interrupt

Up count TC에서 TC 주기마다 CNT는 0으로 초기화됩니다. 0 -> 1 -> 2 ..... -> (n - 1) -> n, 0 -> 1 ...식으로 말이죠. 이때 CNT가 0으로 초기화 되면 SR 레지스터의 UIF 비트가 1로 변합니다. Update Interrupt Flag의 약자로 CNT가 0으로 초기화(또는 재초기화) 되었을 때를 알려줍니다. 하드웨어는 자동으로 UIF 값을 읽고 Interrupt를 발생시키는 것입니다. 

UIF 비트가 1이면 Interrupt를 발생시켜라!가 되는 것입니다. 반대로 말하면 Interrupt가 발생 되었다면 UIF를 0으로 초기화해줘야 Interrupt가 연속적으로 발생하지 않는다는 의미입니다. 즉, TIM2(TC2) 인터럽트가 발생되면 바로 즉시 SR 레지스터의 UIF 비트를 0으로 만들어줘야한다는 거죠.

따라서 밑에서 볼 TIM2_IRQHandler()의 첫 줄에 TIM2 -> SR = 0;으로 하여 UIF를 0으로 초기화한 것입니다. (UIF 뿐만 아니라 다른 모든 Flag도 0으로 초기화)

 

 

다만 SR 레지스터에 1을 쓰는 건 불가능합니다. 임의로 TC Interrupt를 부르는 행위를 방지하기 위해서죠. 따라서 0으로 초기화만 가능한 신기한 레지스터입니다.

 

initRCC


initRCC는 TIM2가 연결된 APB1이라는 회로(Bus)에 적절한 클럭을 공급하고, TIM2(TC2)를 활성화하는 함수입니다.

void initRCC(void) {
/*

	TIM2를 사용할 것이기 때문에 TIM2가 연결 되어 있는 APB1의 주파수를 설정합니다.
    
    여기서는 36 MHz로 설정했습니다.

*/
  RCC -> CFGR = (1 << PLLXTPRE) | (1 << PLLSRC) | (7 << PLLMUL) | (2 << SW) | (4 << HPRE);      // APB1에 18 MHz를 공급합니다.
  RCC -> CR = (1 << PLLON) | (1 << HSEON) | (16 << HSITRIM);            // PLL, HSE를 ON합니다.
  RCC -> APB1ENR = 0x00000001;  // TIM2 활성화
}

CFGR에서 HSE클럭을 PLL클럭에 연결하고, PLL클럭을 SYSCLK에 연결합니다. 여기서 PLL클럭은 HSE(8 MHz) / 2 * 9 = 36 MHz로 설정되었으며, PLL을 그대로 SYSCLK에 연결했기 때문에 SYSCLK도 36 MHz가 되었습니다.

APB1은 SYSCLK -> APB1 prescaler -> APB1으로 회로가 연결되어 있고,

APB1 prescaler는 SYSCLK의 높은 클럭률을 낮춰주는 역할을 수행합니다. 가령 /1, /2, /4등으로 말이죠

하지만 이번에는 APB1 prescaler 값을 /1로 설정(정확히는 기본 설정이기 때문에 아무 것도 안 건들여도 됩니다. 위에서도 따로 설정하지는 않았습니다.) 했습니다.

 

 

CR로 HSE 클럭을 키고, PLL 클럭을 on합니다. (HSE는 보드에 달린 크리스털 또는 오실레이터에서 발생된 클럭입니다.)

 

여기까지 보면 HSE(8 MHz) / 2 * 9 -> PLL (36 MHz) -> SYSCLK (36 MHz) -> APB1 (36 MHz)가 되었네요.

참고로 APB1이 감당할 수 있는 최대 클럭률은 36 MHz이고, 이보다 크면 HardFault interrupt가 발생됩니다.

 

 

클럭 설정이 끝났으니 마지막으로 TIM2에 APB1 클럭을 공급하기 위해 APB1ENR을 설정합니다.

 

 

initTIM2


클럭 설정이 끝났으니 TIM2에 대한 설정을 본격적으로 합니다.

void initTIM2(void) {
/*
   TIM2를 0.5s 주기로 설정합니다.

  0.5s마다 TIM2_Handler를 발생시키는 것과 동일한 의미입니다.
*/
  TIM2 -> PSC = 36000 - 1;      // 36000000 / (36000 - 1 + 1) -> 1000  Hz
  TIM2 -> ARR = 500 - 1;        // (500 - 1 + 1) / 1000 -> 0.5 sec
  TIM2 -> CNT = 0;              // TIM2 초기값을 초기화합니다.
  TIM2 -> SR = 0;               // TIM2 관련 모든 상태를 초기화합니다.
}

위에서 설명한 거처럼 우리는 APB1에 공급되는 클럭을 36 MHz로 설정했습니다.

여기서 PSC를 36,000 - 1로 설정하여 TIM2에 공급되는 클럭률 (CNT의 주기)를 1,000 Hz로 변경했습니다.

ARR은 위에서 설명한 ARV 값이 들어 있는 레지스터입니다. Auto Reload Register라고 부르죠. 이 값에 500 - 1 값을 넣어 위에서 말한 공식 TC의 주기 = (CNT가 증가하는 주기) / (ARV + 1)에 대입하면 (1,000) / (500 - 1 + 1) = 2 Hz가 나옵니다. 

이를 시간으로 표현하면 0.5s 마다 Interrupt가 발생한다는 뜻이죠.

 

그 다음은 TIM2의 CNT 값을 0으로 바꿔줍니다. 정상적인 동작을 위해 CNT값을 0으로 초기화하는 것입니다.

 

마지막으로 SR은 위에서 설명한 것처럼 Interrupt를 실행시킬지 아닐지를 알려줍니다. 지금은 초기 설정 단계이기 때문에 0을 넣은 것입니다.

 

enableTIM2


TIM2 설정을 끝냈으니, 이제 활성화할 시간입니다.

void enableTIM2(void) {
  TIM2 -> DIER = (1 << 0);      //  TIM2  interrupt를 활성화합니다.
  NVIC_ISER0 = (1 << 28);       // NVIC에 TIM2를 연결합니다.
  
  TIM2 -> EGR = (1 << 0);       // 레지스터가 update 되었다는 것을 알려줍니다. (자세한 것은 더 알아봐야합니다...)
  TIM2 -> CR1 = (1 << 0);       // TIM2 시작
}

DIER은 TIM2를 활성화하는데 관여하는 레지스터입니다. (CNT를 동작하게 하는 활성화와는 다릅니다.)

TIM2에는 CNT가 0으로 될 때 발생되는 Interrupt 외에, 다양한 상황에서 Interrupt를 발생시킬 수 있기 때문에 DIER에서 어떤 상황에서 Interrupt를 발생시킬지는 지정해 줍니다.

DIER의 0번째 비트는 UIE입니다. Update Interrupt Enable의 약자로 CNT가 0으로 Update될 때(또는 재초기화) Interrupt를 발생시킨다는 의미입니다. 

더보기

그 외에도 Compare, Capture, Trigger 등에서도 발생시킬 수 있는 CC1DE ~ CC4DE, TIE 비트 등이 있지만 여기서는 다루지 않을 예정입니다. 

 

TIM2 Interrupt의 고유 번호는 28번이기에 NVIC에 이를 연결합니다.
stm32 레지스터 직접 접근 (5. Exception / Interrupt [설명 & 코드 Part 01]) (tistory.com)

 

이제 대부분의 설정이 끝났으니, 레지스터를 update해야합니다. (이것에 관해서는 잘 모르겠습니다...)

 

마지막으로 CR1의 0번째 비트를 1로 만들어 TIM2의 CNT가 동작하게 만듭니다.

CR1의 0번째 비트는 CEN: Counter enable로 CNT를 확성화한다는 의미이죠.

 

 

TIM2_IRQHandler


타이머는 동작을 시작했습니다.

이제 일정 주기마다 실행할 코드를 작성해야겠죠?

void TIM2_IRQHandler(void) {    // TIM2에 대한 Handler입니다.
  TIM2 -> SR = 0;       // TIM2가 SR의 0번째 비트를 기준으로 1이면 인터럽트를 발생시키기 때문에 다시 0으로 초기화합니다.(수동으로 초기화 해야함)
  GPIOF -> ODR ^= (1 << 6);     // PF6를 Toggle합니다.
}

TIM2_IRQHandler는 CNT가 0으로 초기화되도 불러지는 Handler입니다.

위에서 설명한 것처럼 하드웨어사 SR 레지스터의 UIF 비트를 보고 실행되는 거죠.

Handler가 한 번 불러졌기 때문에 SR 레지스터의 UIF를 0으로 다시 초기화해야합니다.

더보기

TIM2_IRQHandler는 Update 말고도 Trigger, Capture/Compare에서도 발생됩니다. 어떤 상황에서 발생되었는지는 SR 레지스터의 UIF, TIF, CC1IF ~ CC4IF 비트를 보고 판단할 수 있습니다.


Continue

 


 

반응형