이 글에서는 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 디밍, 모터 속도 제어, 서보 모터 구동 등 다양한 응용에 활용할 수 있다.