[STM32] TIMER

CS·2025년 3월 4일

STM32

목록 보기
5/8

Overview

지금까지, 기본적인 MCU 내용들에 대해서 다루었다.
이제는 중요한 주변장치들에 대해 학습할 차례다.

우리가 Multi-tasking을 수행하기 위해 멀티 프로세스, 멀티 쓰레드 등의 기법으로 프로그램을 설계하고 작성하게 된다. 이때 OS의 스케줄링에 따라 문맥 교환이 발생하며(자원 관리도 같이함), 여러 가지 일이 동시에 실행되는 것처럼 보이게 동작한다.

선점형 스케줄링의 경우 정해진 Time Slice가 존재하며, MCU에서도 같은 동작을 하기 위해 RTOS를 사용할 수 있다.

Cortex-M 계열 MCU의 15번째 Interrupt인 SysTick은 기본 제공되는 System Timer로, OS 사용 시 일정한 주기로 Tick Interrupt를 생성하는 데 사용된다. 이를 통해 RTOS는 태스크 전환을 수행할 수 있다.

또한, 일정한 간격으로 타이머 인터럽트를 발생시키는 기능을 활용하여 주기적인 작업을 수행할 수 있으며, SW적으로 PWM 신호를 생성하는 데도 응용할 수 있다. 물론 SysTick대신 TIM1, TIM8같은 Advanced Timer를 사용해 PWM 신호를 생성하는게 일반적이다.

Timer

STM32 MCU 종류와 상관없이 모두 동일하게 적용되는 내용들이다.

1️⃣ Basic Timer : TIM6 / TIM 7
동작 중 조정 가능한 prescaler에 의해 구동되는 16bits auto-reload counter(CNT)로 구성
I/O pin을 갖지 않음. APB1 bus에 연결되어 있고, DAC에 연결되어 있다.

사용 :
일반적인 시간 간격 설정, 다른 TIM의 Master로 사용,
DAC를 위한 time 구간 설정(내부적으로 DAC에 연결되어 있어 trigger 출력을 통해 구동 가능함)

2️⃣ GP Timer : TIM2 / TIM3 / TIM4 / TIM5 / TIMx
동작 중 조정 가능한 prescaler에 의해 구동되는 16bits auto-reload counter(CNT)로 구성
상위 버전의 MCU는 32bits auto-reload counter 또한 내장하고 있다.
APB1 Bus에 연결되어 있다.

사용 :
Basic Timer 기능들을 모두 수행
최대 4개의 PWM channel 사용 가능
Advanced control timer와 함께 동기화 가능

Update Interrupt : counter 초기화 / counter over-under flow 발생
Trigger Interrupt : counter start / stop / Init / 내-외부 trigger에 의한 count
입력 capture와 출력을 비교

3️⃣ Advanced control Timer : TIM1 / TIM8 / TIMy
동작 중 조정 가능한 prescaler에 의해 구동되는 16bits auto-reload counter(CNT)로 구성

사용 :
GP Timer의 모든 기능 수행
timer 출력 신호를 reset or 알려진 상태로 설정하는 break 입력 기능
3상 교류 전동기 제어, 전력 변환 관련 기능들 제공

  • Clock Tree를 보면 APB1/2 timer clock을 통해 CK_INT(Internal clock = Timer clock)을 확인할 수 있다.

PSC(prescaler), CNT(counter), ARR값들은 counter 동작시에도 읽고, 설정이 가능하다.
HAL 라이브러리의 TIM_TypeDef Struct에 PSC / CNT / ARR 멤버변수로 선언되어 있어

TIMx -> PSC

위와 같은 방법으로 직접 접근이 가능하다.


예를 들어, 제공 받은 timer clock이 있다고 하자.
이들은 CK_PSC이며, PSC를 거쳐 다음과 같은 수식에 따라 CK_CNT를 출력한다.

CK_CNT = CK_PSC / (PSC+1)

즉 CubeMX에서 8000-1이라고 PSC를 설정하고 CK_PSC가 40[MHz]라고 가정할 때,

CK_CNT = 40000000 / 8000 = 5000[Hz]

가 될 것이다.

여기에 ARR에 10-1을 지정해주면, CNT counter는 0~9까지(up) 10번 count후(비교) Update Interrupt(UI)를 발생시키게 된다. 즉 500[Hz]마다 UI를 발생시킨다는 것이다.

주파수와 주기의 역수 관계에 따라

Frequency = 1/T then, 1/500 = 0.002[s] 즉, 2[ms]마다 UI가 발생하게 된다.

이때, 당연히 TIM Update Interrupt를 enable시켜주어야 한다.

추가적으로 RCR(Repetition Counter Register)가 존재하는데
이는 CNT counter가 ARR을 RCR 지정값 +1 만큼 읽으면 UI를 발생시키게끔 하는
Counting 반복을 설정해주는 레지스터이다.
즉, RCR이 1이라면 0~9까지 2번 세고 UI를 발생시킨다는 얘기이다.

이는 PWM 주기마다 duty cycle을 변경하는 것이 아니라, 지정한 배수마다 duty cycle 변경시 사용하게 된다.


📌 정리 :
timer의 시간 간격은 16bits PSC prescaler를 이용해 CK_PSC clock을 낮추고
-> CK_CNT clock을 생성한다. 이를 ARR과 RCR에 지정한 값들을 통해 한 주기를 만들 수 있다.

첫번째 수식은 위에서 든 예시를 그대로 대입한 것이고,
두번째 수식은 첫번째 수식을 변형한 것이다.

예를 들어, 500[Hz] 즉, 2[ms]마다 UI를 발생시키고 싶다면?
CK_PSC는 40[MHz]로 설정하며 이미 정해진 값이다.

즉,

40000000/500 = (PSC + 1) * (ARR + 1) = 80000

이 된다. 이를 만족하는 PSC와 ARR을 선택하여 사용하게 되면 된다.
PSC 기반으로 ARR을 계산하면 된다.
단, PSC를 작게잡고 ARR을 크게 잡아주면 좀 더 정확한 시간 간격을 만들 수 있다는 것을 기억하자.

또한, 밑의 함수를 반드시 while loop 이전에 호출하여야 timer를 동작 시킬 수가 있다.

HAL_TIM_BASE_START_IT() / HAL_TIME_BASE_START()

IT()은 InTerrupt version이고, 없으면 Interrupt없이 사용할 수 있는 version이다.

이는 PWM 신호를 생성하기 위해 작성한 코드인데, Interrupt를 사용하지 않아
TIM1, TIM8에 HAL_TIM_Base_Start(&htimx);를 사용하였다.
또한 PWM channel을 사용하기 위해서도 HAL_TIM_PWM_Start(&htimx, TIM_CHANNEL_x);를 사용해야 한다.


참고) 특정 순간에 UI를 발생 / disable 하는 방법

  1. 발생 : 원하는 타이밍에 EGR.UG=1로 설정. UI 발생후, RCR 값을 포함해 counting 작업을 Reset한다.
    TIMx->EGR=1; 과 같이 사용하면 된다.

  2. disable : 원하는 타이밍에 CR1.UDIS=1로 설정. 혹은 ARR을 0으로 지정하여 CNT counter가 counting을 하지 않도록 해 UI 자체가 발생하지 않게 함. 다른 timer의 UI callback 함수에서
    TIMx->ARR=9;와 같이 지정하면 다시 counting을 하게 만들 수가 있음.

Timer 설정

1️⃣ TIM1 :
APB2 clock(36MHz) / UI 1[ms] / UP mode
ex) 전체 Application의 기준 시간으로 사용

2️⃣ TIM2 :
APB1(18MHz) / UI 2[ms] / Center aligned mode
ex) 특정 함수의 기준 시간으로 사용

3️⃣ TIM3 :
APB1(18MHz) / UI 100[ms]
ex) 500[ms] 주기로 PC에 Data 전송

라고 가정하자.
수식에 따라,

TIM1 : 36000000 / 1000 = (ARR+1) * (PSC+1) = 36000

PSC에 6000-1을 지정하고, ARR에 6-1을 지정(6까지 세고싶음).
이렇게 되면, clock의 edge마다 0~6까지 세고 Counter Overflow가 발생해 counter가 다시 0이 되며
Overflow 발생시 UI가 발생한다.
주기는 0~6이니까 7 * Clock 주기가 되겠다.

TIM2의 경우도 동일하게 진행해주면 되고(대신 down)

TIM3의 경우 "Center aligned mode"가 설정되어 있다. 이 경우에는 ARR-1값을 지정해주면 안된다.
원하는 ARR값에 *2값인 12를 지정해주면, (0~6~1)까지 UP->DOWN으로 세다가 0이 되면 UI가 발생한다. 즉 ARR을 12로 설정해줘야 한다.

이때, 주기는 똑같이 (1+ARR) * clock period 이다.


만약에 Interrupt로 수행할 시에, NVIC setting에서 해당 TIM들의 Interrupt들을 enadble 해주어야 한다.

그러면 CubeMX에 의해 다음과 같은 함수들이 생성된다.


//in stm32f4xx_it.c
void TIM1_UP_TIM16_IRQHandler(void){
	//...
    HAL_TIM_IRQHandler(&htim1);
    //...
}

//in stm32f4xx_hal_tim.c
HAL_TIM_IRQHandler(TIM_HandleTypeDef* htim){
	// Capture compare 1 event to 4 처리
    // check한 TIM Update event를 
    // HAL_TIM_PeriodElapsedCallback로 처리
    // HAL_TIM_PeriodElapsedCallback는 __weak선언이므로, 재정의해서 사용
    
 }


//in main.c
void HAL_TIM_PeriodElapsedCallback(TIM HandleTypeDef *htim) {
	if(htim->instance == TIM1) {
    	//TODO..
    }
    if(htim->instance == TIM2) {
    //TODO..
    }
    if(htim->instance == TIM3) {
    //TODO..
    }
}

즉, TIM1의 UI 발생시, main.c에서 재정의 한 callback 함수가 호출될 것이고, htim의 instance를 통해 어떤 TIM에서 발생한 UI인지 거를 수가 있다.

단, HAL_TIM_BASE_Start_IT(&htim1); 을 반드시 main전에 USER CODE에서 작성해주어야 한다.
주로 초기화가 이루어진 후인 USER CODE BEGIN2 ~ END2 사이에 작성한다.
IT을 빼면 Polling방식으로 Interrupt가 발생하지 않는다.

Watchdog Timer

거의 모든 processor에서 지원하는 기능으로,

IWDG는 설정한 시간 내에 소프트웨어가 정해진 방식으로 Refresh(갱신)하지 않으면 시스템을 강제로 리셋하는 역할을 합니다.
Refresh는 IWDG_KR 레지스터에 특정 키(0xAAAA)를 써서 수행합니다.

System 고장 방지를 위한 system 감시 timer로 전원 인가시 enable 상태로되며, 주기적으로 system reset을 발생시킨다. 그러므로 while loop 내에서 제일 처음에 watchdog timer가 reset을 발생시키지 않도록 counter를 reload해주어야 한다.

만약에, MCU가 제공하는 임의 I/O 주변 장치를 통해 "계속해서" data가 잘못 들어오거나
잘못되어 반복문에서 빠져나오지 못할 때 System을 Reset시켜 처음부터 다시 실행하도록 한다.

  1. MCU와 외부 소자를 연결하는 임의 I/O 주변장치를 통해 원하는 Data가 제대로 I/O 하는지 판단하는 높은 우선순위의 ISR을 내부에 넣었는데, 그 Data가 제대로 I/O를 하지않아 계속 기다린다면
    Core를 계속해서 해당 반복-판단문이 붙잡고 있어 System이 진행을 하지 못하고 전체가 hang되어버린다.

  2. 또는, 어떤 함수가 event를 반복문을 통해 기다리는데 event가 미발생시 여기서도 hang되어버린다.

이런 경우에 설정한 시간 구간까지 timer를 clear하지 않으면, (time out 발생)
전체 System을 Reset하도록 만드는 특수 목적의 timer이다.

📌 정리 :
timer로 지정한 시간을 넘어서도 응답을 하지 않을 경우 System Reset을 하도록 만드는 것
main()함수 실행중 PC(Program Counter)가 어딘가에서 빠져나오지 못하는 경우에 사용된다.
즉, 예상(지정) 시간내로 loop을 순환해야하는데 어디서 움직이지 않으면, 외부에선 SYS가 멈춘 것이나 다름 없다. 이에 대한

  1. MCU main clock과 상관없이 동작하는 IWDG(Independent Watch Dog)
  2. APB clock으로 동작하는 WWDG(Window Watch Dog)

두가지를 가진다.


IWDG의 경우 MCU main clock source가 아닌 내부 32[kHz] 전용 RC clock을 사용함.
역시나 내부 RC clock을 사용하므로 온도에 따라 특성이 달라 오차가 클 수 있단 얘기이다.

위와 같이 활성화 해주면 된다. PSC와 downcounter value가 보인다.

WIN[11:0] register(Refresh 가능한 허용 구간)에 설정한 값보다 작은 구간에서 counting을 reload하면 IWDG Reset이 발생하지 않고, 클때 refresh(reload)가 발생한다면 IWDG Reset이 발생한다.

다만, RL[11:0] 레지스터(ReLoad)와 WIN의 default값이 0x0FFF로 동일하다. 그렇다면 두 시작점이 같아져 Refresh not allowed 구간이 제거된다.(클때 발생하니까) 즉 Refresh allowed 구간만 존재해 RL값을 어디서 Refresh해도 IWDG Reset이 발생하지 않는다. 그러니까 어디에 잘못 들어도 reset을 못시키는 상황이 오게 됨. Reset이 발생하지 않을때 Refresh하면 정상 동작을 하기때문

즉 CNT가 RL에서 시작해 0이되면 Reset(Time-OUT)
Refresh Not allowed구간에서 Refresh시 IWDG Reset이 발생함.
근데 이 Not allowed구간은

  • CNT > WIN일때의 구간

그래서 WIN = RL이 되면 CNT가 WIN보다 큰 순간이 존재하지가 않음.
어디서 Refresh를 걸어도 Not allowed 구간이 아니라 Refresh해도 IWDG Reset이 안됨

📌 정리 :
임의 구간에서 RL값을 refresh해도 IWDG Reset이 발생하지않는다.

현재 counter값이 WIN[11:0] register값보다 더 작을 때 RL을 refresh하면 reset이 발생하지 않는다는 얘기다.

refresh는 HAL_IWDG_Refresh() 함수를 사용한다.


tms[ms]동안 구간을 설정해 이 구간 안에 RL 값을 refresh하지 않으면 IWDG reset이 걸리도록 하고 싶다면, RL[11:0]에 다음 수식으로 값을 설정하면 된다.

WIN[11:0] = 4095, LSI RC= 32[kHZ], 2^PR[2:0]+2 = PR로 가정하자.
tms = (1/32000) PR (RL[11:0]+1) [ms]가 성립한다.
이를 RL[11:0] 중점으로 다시 풀어주면,

RL[11:0] = (tms X 32000) / (PR X 1000) - 1과 같은 식이된다.
분모에 1000을 곱한 것은 tms단위를 [ms]로 변경하기 위해서다.

예를 들어, main() 내에서 MX_IWDG_Init()을 call해 IWDG를 Enable시키면
그 이후로는 Disable되지 않는다.
10[ms] 구간 동안 RL이 refresh되지 않으면 자동으로 IWDG Reset이 걸리도록
RL[11:0] 값을 계산하면, (PR은 default로 4 선택)

RL[11:0] = (10 X 32000) / (4 X 1000) - 1 = 320/4 -1 = 79가 된다.

//main 내부

MX_IWDG_INIT();

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, 1);
while(1){
	HAL_Delay(9); //9ms
    HAL_IWDG_Refresh(&hiwdg);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, 0);

자 여기서 10[ms] 내로 RL Refresh를 수행하지만, 확인해보면 timeout으로 잘못 판단하고 IWDG Reset이 반복적으로 걸린다.

RL값을 83으로 변경해 10.5[ms]를 갖게하면 timeout으로 판단하지 않는다는 것을 확인 가능하다.

해결

이러한 오차 때문에, MCU GPIO pin에 전용 watchdog 소자를 사용하는 경우가 많다.
주로 MCU 전원 관련 모니터링을 수행하기 위해 사용한다.
지정 시간 내에 toggling이 안된다면, 전용 watchdog 소자가 reset핀을 active low로 구동해 MCU를 reset시켜준다. (NRST는 Pull-up)

ADM8613라는 소자가 있다.

Timer활용하여 마이크로 세컨드 Delay 함수 만들기

HAL 라이브러리에는 HAL_Delay(); 라는 [ms]단위의 Core 실행 지연 함수가 있다.
즉, 어떤 시간 마다 업무를 수행하도록 Interrupt를 걸어줄 필요가 없다.
물론, 마이크로 세컨드 단위로 인터럽트를 발생하는 것은 MCU한테 좋지 않다.
GPIO는 pulse 생성시 setup/hold time으로 50[MHz] 이상 pulse를 만드는 것은 어렵기 때문이다.

여하튼, APB2 Timer clock을 80[MHz]로 set하고, PSC = 80-1 을 하면,
80000000/80 = 1000000[Hz] = 1[MHz]를 얻어낼 수 있다.

주기와 주파수의 관계로 "1[us]"를 얻어낸 것이다.
즉 '1[us]'마다 TIM1의 counter 값을 counting 할 수가 있다. (CK_CNT)
ARR을 0xFFFF-1인 MAX값으로 설정시, 안정적인 1마이크로 세컨드 시간을 얻을 수 있다.

이 값들을 CubeMX에서 적어주면 된다.

void NonHAL_Delay(uint16_t us){
	__HAL_TIM_SET_COUNTER(&htim1, 0); // TIM1을 0으로 SET
    while(__HAL_TIM_SET_COUNTER(&htim1) < us); // 해당 us 도달까지 loop
 }
 
 
 -----------------------------------------------------------------
 
 HAL_TIM_Base_Start(&htim1); // NO Interrupt
 
 
 while(1){
 	NonHAL_Delay(10);
    HAL_GPIO_Togglepin(GPIOA, GPIO_PIN_8);
    
  
  }

이렇게 하면 CK_CNT에 따라서 1[us]마다 Counter 값을 Counting 해준다.
그 상태에서 10을 전달해 호출하면 NonHAL_Delay()에 10[us]만큼 머무르고 while을 벗어난 후, main의 while로 다시 돌아와 PA8의 pin을 토글한다.

Interrupt를 사용하지 않기 때문에 HAL_TIM_Base_Start(&htim1);을 사용했음을 알 수 있다.
따라서 callback함수나, Interrupt enable도 없다.

주의: 동일 TIM에 HAL_TIM_Base_Start_IT()과, HAL_TIM_Base_Start()를 동시에 적용하면 안됨.
다른 장치도 마찬가지.

RTOS등을 사용할 때, 이런 방식들을 응용하여 스위칭을 해주면 되겠다.

profile
학습

0개의 댓글