Embedded System/STM32

[Cortex-M3] 타이머(TIMER) 사용하기

임베지수 2017. 4. 2. 14:52

STM32F100RB 칩 안에는 AVR처럼 여러개의 타이머가 있다. 각 타이머는 16bit로 동작하는것 같다.
먼저 타이머의 기능을 살펴보면 여러가지 모드가 있는데 가장 기본적인 Counter Mode(Upcounting & Downcounting)가 있고 아래의 기능도 제공한다.

 

  - Input Capture
  - Output Compare
  - PWM generation
  - One-pulse mode output

 

여기서는 가장 기본적인 Counter Mode와 PWM generation을 다루어 보겠다.
먼저 Counter Mode - Upcounting의 소스를 보면..

 


#include "stm32f10x.h"

volatile unsigned int Timer2_Counter=0;

void init_port()
{    
    GPIO_InitTypeDef PORTA;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    PORTA.GPIO_Pin = GPIO_Pin_0;
    PORTA.GPIO_Mode = GPIO_Mode_Out_PP;
    PORTA.GPIO_Speed = GPIO_Speed_10MHz;
    GPIO_Init(GPIOA, &PORTA);
}

void TIM2_IRQHandler(void)
{
    if(TIM_GetITStatus(TIM2,TIM_IT_Update) != RESET)
    {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // Clear the interrupt flag
        Timer2_Counter++;
    }
}

void init_Timer2()
{
    NVIC_InitTypeDef NVIC_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    
    /* TIM2 Clock Enable */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    
    /* Enable TIM2 Global Interrupt */
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    
    /* TIM2 Initialize */    
    TIM_TimeBaseStructure.TIM_Period=1000-1; // 1kHz
    TIM_TimeBaseStructure.TIM_Prescaler=24-1; // 1MHz
    TIM_TimeBaseStructure.TIM_ClockDivision=0;
    TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure);
    
    /* TIM2 Enale */
    TIM_Cmd(TIM2,ENABLE);
    TIM_ITConfig(TIM2,TIM_IT_Update, ENABLE); // interrupt enable
}

void delay_ms(unsigned int del)
{
    Timer2_Counter=0;
    while(Timer2_Counter < del);
}

int main()
{
    SystemInit();
    init_port();
    init_Timer2();
    
    while(1)
    {
        GPIOA->BRR = GPIO_Pin_0;  // PA0 OFF
        delay_ms(500);
        GPIOA->BSRR = GPIO_Pin_0; // PA0 ON
        delay_ms(500);
    }
}

 

init_port()는 저번에 했으니까 넘어가고 init_Timer2() 랑 TIM2_IRQHandler()를 살펴보자.

NVIC_InitTypeDef 이라는 구조체가 있는데 인터럽트 관련 구조체이다.
TIM_TimeBaseInitTypeDef 이건 타이머와 관련된 구조체이다.
먼저 NVIC이라는 걸 살펴보면 Nested vectored interrupt controller의 약자이다.
데이터시트에 나와있는 특징을 살펴보면... 


  • 60 maskable interrupt channels in high-density value line devices and 56 in low and medium-density value line devices (not including the sixteen Cortex™-M3 interrupt lines)
  • programmable priority levels (4 bits of interrupt priority are used)
  • Low-latency exception and interrupt handling
  • Power management control
  • Implementation of System Control Registers

뭐... 그렇다는군... 넘어가고... 나는 TIM2를 사용했으니까 TIM2의 인터럽트를 확인해보면 28번에 물려있는걸 알 수 있다.

이거에 대해선 나중에 보고...

 

 

GPIO랑 마찬가지로 TIM2를 사용하려면 클럭을 인가해줘야하는데 블록도를 보면 APB1 클럭버스에 물려있는걸 알 수 있다.
따라서 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE) 이렇게 클럭신호를 넣어준다.

 

이제 NVIC관련해서 값을 넣어보자.. 먼저 NVIC_InitStructure.NVIC_IRQChannel에 TIM2_IRQn이라는 걸 넣어주는데 define된 값을 보면 28번으로 정의되어있다!! 아까 28번이 여기에 쓰인다.

 

 

 

 

나머지는 우선순위랑 NVIC Enable시켜주는거니까 대충 넣어주고...(나도 정확히는 모름 하하하...)

NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

다음으로 TIM_TimeBaseInitTypeDef 관련해서 값을 집어넣기 전에 타이머 값을 계산하는 방법을 살펴보자.

 

 

 

AVR이랑 크게 다르지 않다. 프리스케일러로 클럭을 나누어주고 주기를 곱해주면 끝,,, 내가 사용하는 STM32F100RB의 경우 최대 클럭이 24MHz이므로 f_CLK가 24MHz이다. STM32F103의 경우 최대 클럭이 72MHz일거다... 자세한건 데이터시트 ㄱㄱ

 

 

위에 그림처럼 넣어주면 되는데 보면 대입하는 값에 1씩 빼준다. 이유는 아래 타이밍도를 보자.

 

 

위 타이밍도는 프리스케일 값이 3일때를 나타내고 있는데 맨 밑에 그림에서 보면 0부터 시작하는 걸 알 수 있다. 그래서 3을 넣으면 4개가 한 세트가 되는거다. 따라서 대입하는 값에 1씩 빼주는거다.


그리고 TIM2를 동작시키기 위해서 TIM_Cmd()를 하고 인터럽트 Enable해주는 TIM_ITConfig()함수로 Update인터럽트를 사용가능하게 해준다. AVR에서의 오버플로우 인터럽트 같은 놈이다. 타이머가 가득차면 인터럽트 발생...


각 구조체에 값을 넣어주고나서 NVIC_Init(), TIM_TimeBaseInit()함수를 사용해서 레지스터에 값을 꼭 써주자... GPIO 사용하는 법이랑 크게 다르지 않다. GPIO도 그렇고 TIMER도 그렇고 전부 구조체를 사용해서 접근한다.(물론 레지스터에 직접 접근해도 된다.)


그럼 이제 인터럽트가 걸렸을때 실행되는 함수를 살펴보자... 함수 이름은 TIM2_IRQHandler() 이라고 되어있는데 이건 미리 정의되어 있다. startup_stm32f10x_md_vl.s 파일을 까보면 아래처럼 핸들러 함수가 정의되어있다.

 

 

핸들러 함수에서 TIM_GetITStatus(TIM2,TIM_IT_Update)를 통해서 업데이트 인터럽트 플래그가 set되어있는지 확인하고 인터럽트 플래그를 반드시 clear해주어야한다. 그런다음 내가 만든 cnt변수를 증가시켜준다. 이렇게 하면 1ms마다 인터럽트가 걸리고 cnt변수가 1씩 증가하는 것이다. 입맛에 맞게 delay_ms()함수를 만들어서 확인해주자.

 

 

 

 

 

이제 마지막으로 PWM 신호를 만들어보자. STM32VLDISCOVERY보드의 데이터시트를 보면 PA0이 부가기능으로 TIM2_CH1인 것을 확인할 수 있다.

 

여기서 소스를 먼저 보면...

 

#include "stm32f10x.h"

volatile unsigned int Timer2_Counter=0;

void init_port()
{    
    GPIO_InitTypeDef PORTA;
    GPIO_InitTypeDef PORTB;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE);
    
    PORTA.GPIO_Pin = GPIO_Pin_0;
    PORTA.GPIO_Mode = GPIO_Mode_AF_PP;
    PORTA.GPIO_Speed = GPIO_Speed_10MHz;
    GPIO_Init(GPIOA, &PORTA);
    
    PORTB.GPIO_Pin = GPIO_Pin_0;
    PORTB.GPIO_Mode = GPIO_Mode_Out_PP;
    PORTB.GPIO_Speed = GPIO_Speed_10MHz;
    GPIO_Init(GPIOB, &PORTB);
}

void TIM2_IRQHandler(void)
{
    if(TIM_GetITStatus(TIM2,TIM_IT_Update) != RESET)
    {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // Clear the interrupt flag
        Timer2_Counter++;
        GPIOB->BRR = GPIO_Pin_0;  // PB0 OFF
    }

    if(TIM_GetITStatus(TIM2,TIM_IT_CC1) != RESET)
    {
        TIM_ClearITPendingBit(TIM2,TIM_IT_CC1);
        GPIOB->BSRR = GPIO_Pin_0;  // PB0 ON
    }
}

void init_Timer2()
{
    NVIC_InitTypeDef NVIC_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_OCInitTypeDef OutputChannel;    
    
    /* TIM2 Clock Enable */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    
    /* Enable TIM2 Global Interrupt */
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    
    /* TIM2 Initialize */    
    TIM_TimeBaseStructure.TIM_Period=100-1; // 100kHz
    TIM_TimeBaseStructure.TIM_Prescaler=24-1;
    TIM_TimeBaseStructure.TIM_ClockDivision=0;
    TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure);
    
    /* TIM2 PWM Initialize */
    OutputChannel.TIM_OCMode = TIM_OCMode_PWM1;
    OutputChannel.TIM_OutputState=TIM_OutputState_Enable;
    OutputChannel.TIM_OutputNState=TIM_OutputNState_Enable;
    OutputChannel.TIM_Pulse=50-1; // 50% duty ratio
    OutputChannel.TIM_OCPolarity=TIM_OCPolarity_Low;
    OutputChannel.TIM_OCNPolarity=TIM_OCNPolarity_High;
    OutputChannel.TIM_OCIdleState=TIM_OCIdleState_Set;
    OutputChannel.TIM_OCNIdleState=TIM_OCIdleState_Reset;
    TIM_OC1Init(TIM2,&OutputChannel);
    
    /* TIM2 Enale */
    TIM_Cmd(TIM2,ENABLE);
    TIM_ITConfig(TIM2,TIM_IT_Update | TIM_IT_CC1 ,ENABLE); // interrupt enable
}

void make_pwm(u16 val)
{
    TIM_OCInitTypeDef OutputChannel;
    
    OutputChannel.TIM_OCMode = TIM_OCMode_PWM1;
    OutputChannel.TIM_OutputState=TIM_OutputState_Enable;
    OutputChannel.TIM_OutputNState=TIM_OutputNState_Enable;
    OutputChannel.TIM_Pulse=val;
    OutputChannel.TIM_OCPolarity=TIM_OCPolarity_Low;
    OutputChannel.TIM_OCNPolarity=TIM_OCNPolarity_High;
    OutputChannel.TIM_OCIdleState=TIM_OCIdleState_Set;
    OutputChannel.TIM_OCNIdleState=TIM_OCIdleState_Reset;
    TIM_OC1Init(TIM2,&OutputChannel);
}

void delay(unsigned int del)
{
    Timer2_Counter=0;
    while(Timer2_Counter < del);
}

int main()
{
    u16 i;
    SystemInit();
    init_port();
    init_Timer2();
    
    while(1)
    {        
        for(i=0; i<100; i++)
        {
            TIM2->CCR1 = i;
            delay(100);
        }
        
        for(i=98; i>0; i--)
        {
            TIM2->CCR1 = i;
            delay(100);
        }
    }
}

 

 

GPIO 초기화에서 PORTA.GPIO_Mode = GPIO_Mode_AF_PP 이렇게 Alternate function으로 동작하게 해준다.

그런 다음 init_Timer2()함수에서 TIM_OCInitTypeDef 구조체를 추가해준다. TIM_OCMode는 TIM_OCMode_PWM1 으로 해주는데 PWM1이 있고 PWM2이 있다. 이 둘의 차이는 아래 그림을 참고하자.

 

위상이 반대이다. 글고 듀티비는 TIM_CCR / TIM_ARR으로 결정된다. TIM_TimeBaseStructure.TIM_Period에 쓰는 값이 TIM_ARR로 들어간다. OutputChannel.TIM_Pulse에 쓰는 값은 TIM_CCRx으로 들어가게 된다.

나머지 State, NState, Polarity NPolarity 등등은 잘 모르겠다 ㅋㅋㅋㅋ 시키는대로 넣어야지...

역시 마찬가지로 TIM_OC1Init()함수를 통해 레지스터에 값을 넣어주고...

TIM_ITConfig(TIM2,TIM_IT_Update | TIM_IT_CC1 ,ENABLE) 여기에 Capture/Compare인터럽트를 사용한다고 추가해준다. 핸들러 함수안에 if(TIM_GetITStatus(TIM2,TIM_IT_CC1) != RESET)를 추가하여 CC인터럽트가 발생하면 동작시키걸 추가해준다.

그리고 main의 while문 안에서 일정시간마다 펄스폭을 조절해준다... 그럼 PWM도 끝.