STM32F103 마이크로컨트롤러 PWM 출력 구현

이 글에서는 STM32F103 시리즈에서 PWM 신호를 생성하고 출력하는 방법에 대해详细介绍한다. 앞서 Timer 설정에 대해 설명한 바와 같이, PWM은 타이머의 출력 비교 기능을 활용하여 구현할 수 있다.

1. PWM 관련 레지스터

타이머로 PWM 신호를 생성하기 위해서는 다음과 같은 레지스터들이 필요하다. 기본적인 타이머 설정에 사용되는 레지스터之外에 추가적으로 필요한 레지스터들이다.

1.1 캡처/비교 모드 레지스터 (TIMx_CCMR1/2)

TIMx_CCMR 레지스터는 총 2개가 존재한다:

  • TIMx_CCMR1: 채널 1과 2의 동작 모드 설정
  • TIMx_CCMR2: 채널 3과 4의 동작 모드 설정

각 레지스터는 입력 캡처 모드와 출력 비교 모드를 모두 지원하며, PWM 출력 구현 시에는 출력 비교 기능만을 활용한다.

1.2 캡처/비교 활성화 레지스터 (TIMx_CCER)

TIMx_CCER 레지스터는 각 채널(4비트씩)마다 출력 활성화 여부와 극성을 설정한다:

  • CCxE: 출력 활성화 비트로 PWM 신호 출력에 필수
  • CCxP: 출력 신호의 극성 설정 (활성_HIGH 또는 활성_LOW)

1.3 캡처/비교 레지스터 (TIMx_CCR1~4)

총 4개의 CCR 레지스터가 존재하며, 각각의 채널에 대응된다. 이 레지스터에 저장된 값과 카운터 값을 비교하여 PWM의_HIGH 구간을 결정한다.

1.4 브레이크 및 데드타임 레지스터 (TIMx_BDTR)

고급 타이머(TIM1, TIM8)에서만 사용되며, 메인 출력 활성화(MOE) 비트가 Bit 15에 위치한다. 이 비트를 설정해야 PWM 출력이 가능하다.

2. PWM 생성 구현

2.1 초기화 절차

PWM 신호 생성을 위한 설정 단계는 다음과 같다:

(1) 타이머 클록 활성화: RCC_APB1ENR 또는 RCC_APB2ENR 레지스터를 통해 해당 타이머의 클록을 활성화한다.

(2) GPIO 복합 기능 설정: 해당 채널의 GPIO 핀을 복합 기능 push-pull 출력 모드로 설정한다.

(3) 타이머 기본 단위 설정:

  • TIMx_ARR 레지스터에 자동 재로드 값 설정
  • TIMx_PSC 레지스터에 프리스케일러 값 설정

(4) PWM 관련 레지스터 설정:

  • TIMx_CCMRx: 출력 비교 모드 선택 및 PWM 모드1 또는 모드2 설정
  • TIMx_CCER: 출력 활성화
  • TIMx_BDTR (고급 타이머만): MOE 비트 활성화

(5) 타이머 작동 허용: TIMx_CR1 레지스터의 Bit 0을 설정한다.

(6) Duty Cycle 조절: TIMx_CCRx 레지스터의 값을 변경하여 PWM의_HIGH 구간 길이를 조절한다.

2.2 소스 코드 구현

2.2.1 PWM 초기화 함수

다음은 PWM 초기화를 수행하는 함수 구현이다:

/**
 * 설명: STM32F103 PWM 초기화 함수
 * 매개변수:
 *   tmr_id - 타이머 번호 (0=TIM1, 1=TIM2, ..., 7=TIM8)
 *   freq_khz - 원하는 PWM 주파수 (kHz 단위)
 *   ch - PWM 채널 (PWM_CH1, PWM_CH2, PWM_CH3, PWM_CH4)
 */
void PWM_Initialize(uint8_t tmr_id, uint16_t freq_khz, enumPwmChannel ch)
{
    uint16_t reload_val;           // 자동 재로드 값 저장
    uint8_t timer_clk_mhz = 0;     // 타이머 클록 주파수 저장
    uint16_t prescaler = 1;        // 프리스케일러 기본값
    uint8_t remap_cfg = 0x00;      // 리맵핑 설정
    
    // 타이머 클록 결정 (APB 버스별 주파수 분석)
    if (tmr_id == 0 || tmr_id == 7)    // TIM1 또는 TIM8 (APB2 버스)
    {
        timer_clk_mhz = 72;            // APB2 1배주파수
    }
    else                                // 일반 타이머 (APB1 버스)
    {
        timer_clk_mhz = 72;            // APB1 2배주파수 (36MHz * 2)
    }
    
    // 자동 재로드 값 계산
    reload_val = timer_clk_mhz * 1000 / (prescaler + 1) / freq_khz - 1;
    
    // 타이머별 GPIO 및 레지스터 설정
    if (tmr_id == 0)                        // TIM1 (고급 타이머)
    {
        RCC->APB2ENR |= 1 << 11;            // TIM1 클록 활성화
        TIM1->BDTR |= 1 << 15;              // 메인 출력 활성화 (필수)
        
        // 채널별 GPIO 설정
        switch (ch)
        {
            case PWM_CH1:
                GPIO_Config(PA8, ALT_PP_50MHz);   // 주 출력
                GPIO_Config(PB13, ALT_PP_50MHz);  // 보완 출력
                break;
            case PWM_CH2:
                GPIO_Config(PA9, ALT_PP_50MHz);
                GPIO_Config(PB14, ALT_PP_50MHz);
                break;
            case PWM_CH3:
                GPIO_Config(PA10, ALT_PP_50MHz);
                GPIO_Config(PB15, ALT_PP_50MHz);
                break;
            case PWM_CH4:
                GPIO_Config(PA11, ALT_PP_50MHz);
                break;
        }
    }
    else if (tmr_id == 1)                   // TIM2
    {
        RCC->APB1ENR |= 1 << 0;             // TIM2 클록 활성화
        
        switch (ch)
        {
            case PWM_CH1: GPIO_Config(PA0, ALT_PP_50MHz); break;
            case PWM_CH2: GPIO_Config(PA1, ALT_PP_50MHz); break;
            case PWM_CH3: GPIO_Config(PA2, ALT_PP_50MHz); break;
            case PWM_CH4: GPIO_Config(PA3, ALT_PP_50MHz); break;
        }
    }
    else if (tmr_id == 2)                   // TIM3
    {
        RCC->APB1ENR |= 1 << 1;             // TIM3 클록 활성화
        
        switch (ch)
        {
            case PWM_CH1: GPIO_Config(PA6, ALT_PP_50MHz); break;
            case PWM_CH2: GPIO_Config(PA7, ALT_PP_50MHz); break;
            case PWM_CH3: GPIO_Config(PB0, ALT_PP_50MHz); break;
            case PWM_CH4: GPIO_Config(PB1, ALT_PP_50MHz); break;
        }
    }
    else if (tmr_id == 3)                   // TIM4
    {
        RCC->APB1ENR |= 1 << 2;             // TIM4 클록 활성화
        
        switch (ch)
        {
            case PWM_CH1: GPIO_Config(PB6, ALT_PP_50MHz); break;
            case PWM_CH2: GPIO_Config(PB7, ALT_PP_50MHz); break;
            case PWM_CH3: GPIO_Config(PB8, ALT_PP_50MHz); break;
            case PWM_CH4: GPIO_Config(PB9, ALT_PP_50MHz); break;
        }
    }
    else if (tmr_id == 4)                   // TIM5
    {
        RCC->APB1ENR |= 1 << 3;             // TIM5 클록 활성화
        
        switch (ch)
        {
            case PWM_CH1: GPIO_Config(PA0, ALT_PP_50MHz); break;
            case PWM_CH2: GPIO_Config(PA1, ALT_PP_50MHz); break;
            case PWM_CH3: GPIO_Config(PA2, ALT_PP_50MHz); break;
            case PWM_CH4: GPIO_Config(PA3, ALT_PP_50MHz); break;
        }
    }
    else if (tmr_id == 7)                   // TIM8 (고급 타이머)
    {
        RCC->APB2ENR |= 1 << 13;            // TIM8 클록 활성화
        TIM8->BDTR |= 1 << 15;              // 메인 출력 활성화
        
        switch (ch)
        {
            case PWM_CH1:
                GPIO_Config(PC6, ALT_PP_50MHz);
                GPIO_Config(PA7, ALT_PP_50MHz);
                break;
            case PWM_CH2:
                GPIO_Config(PC7, ALT_PP_50MHz);
                GPIO_Config(PB0, ALT_PP_50MHz);
                break;
            case PWM_CH3:
                GPIO_Config(PC8, ALT_PP_50MHz);
                GPIO_Config(PB1, ALT_PP_50MHz);
                break;
            case PWM_CH4:
                GPIO_Config(PC9, ALT_PP_50MHz);
                break;
        }
    }
    
    // 타이머 기본 설정 적용
    Timer_Register[tmr_id]->PSC = prescaler;
    Timer_Register[tmr_id]->ARR = reload_val;
    
    // PWM 모드 설정
    switch (ch)
    {
        case PWM_CH1:
            Timer_Register[tmr_id]->CCMR1 |= 0x07 << 4;     // PWM 모드2 설정
            Timer_Register[tmr_id]->CCER |= 0x0F;           // 출력 활성화
            Timer_Register[tmr_id]->CCR1 = 0;               // 초기 duty 0%
            break;
        case PWM_CH2:
            Timer_Register[tmr_id]->CCMR1 |= 0x07 << 12;
            Timer_Register[tmr_id]->CCER |= 0x0F << 4;
            Timer_Register[tmr_id]->CCR2 = 0;
            break;
        case PWM_CH3:
            Timer_Register[tmr_id]->CCMR2 |= 0x07 << 4;
            Timer_Register[tmr_id]->CCER |= 0x0F << 8;
            Timer_Register[tmr_id]->CCR3 = 0;
            break;
        case PWM_CH4:
            Timer_Register[tmr_id]->CCMR2 |= 0x07 << 12;
            Timer_Register[tmr_id]->CCER |= 0x03 << 12;
            Timer_Register[tmr_id]->CCR4 = 0;
            break;
    }
    
    // 타이머 가동 시작
    Timer_Register[tmr_id]->CR1 |= 1 << 0;
}
2.2.2 Duty Cycle 조절 함수

PWM 신호의 duty ratio를 조절하는 함수이다:

/**
 * 설명: PWM Duty Cycle 설정
 * 매개변수:
 *   tmr_id - 타이머 번호
 *   duty_percent -期望 duty 비율 (0~100)
 *   ch - PWM 채널
 */
void PWM_SetDutyCycle(uint8_t tmr_id, uint8_t duty_percent, enumPwmChannel ch)
{
    uint16_t arr_value;
    uint16_t compare_val;
    
    // 현재 자동 재로드 값 읽기
    arr_value = Timer_Register[tmr_id]->ARR;
    
    // Duty 비율에 따른 비교 값 계산
    compare_val = (uint16_t)((uint32_t)duty_percent * arr_value / 100);
    
    // 해당 채널의 CCR 레지스터에 값 쓰기
    switch (ch)
    {
        case PWM_CH1:
            Timer_Register[tmr_id]->CCR1 = compare_val;
            break;
        case PWM_CH2:
            Timer_Register[tmr_id]->CCR2 = compare_val;
            break;
        case PWM_CH3:
            Timer_Register[tmr_id]->CCR3 = compare_val;
            break;
        case PWM_CH4:
            Timer_Register[tmr_id]->CCR4 = compare_val;
            break;
    }
}

2.3 응용 예제

LED 제어를 통해 PWM 기능을 테스트할 수 있다. PA8 핀에 연결된 LED1의 밝기를 변화시키는 예제이다.

2.3.1 메인 함수 구현
int main(void)
{
    uint8_t brightness = 100;
    
    // 시스템 클록 초기화
    SystemClock_Config(9);
    
    // UART 시리얼 통신 초기화 (디버깅용)
    USART_Initialize(USART1, 115200);
    
    // LED GPIO 초기화
    GPIO_Config(PA8, OUTPUT_PP_50MHz);
    
    // PWM 초기화: 주파수 100kHz, 채널 1 사용
    PWM_Initialize(TIM1, 100, PWM_CH1);
    
    // 초기 duty 50%로 설정
    PWM_SetDutyCycle(TIM1, 50, PWM_CH1);
    
    while (1)
    {
        // 밝기를 100에서 0까지 감소 (LED가 점점 밝아짐)
        for (brightness = 100; brightness > 0; brightness--)
        {
            PWM_SetDutyCycle(TIM1, brightness, PWM_CH1);
            
            if (brightness % 10 == 0)
            {
                Debug_Print("Duty: %d%%\n", brightness);
            }
            
            Delay_Milliseconds(15);
        }
    }
}
2.3.2 테스트 방법

컴파일 후 마이크로컨트롤러에 프로그램을 다운로드한다. 시리얼 통신을 통해 현재 설정된 duty 비율을 확인할 수 있다.

오실로스코프나_logic analyzer가 있다면 PA8 핀의 출력波形을 직접 관찰할 수 있다. 측정 결과, 주기가 10μs로 나타나며 이는 설정한 10kHz 주파수와 일치한다.

3. 결론

STM32F103의 타이머 출력 비교 기능을 활용하면 정밀한 PWM 신호 생성이 가능하다. CCR 레지스터의 값만 변경하면 실시간으로 duty 비율을 조절할 수 있어 LED 디밍, 모터 속도 제어, 서보 모터 구동 등 다양한 응용에 활용할 수 있다.

태그: STM32F103 PWM TIM 마이크로컨트롤러 임베디드

8월 9일 09:44에 게시됨