Week 2 Day 3: PWM 출력 - LED 밝기 조절

학습 목표

  • PWM(Pulse Width Modulation)의 동작 원리를 이해한다
  • 타이머를 이용한 PWM 신호 생성 방법을 익힌다
  • Duty Cycle을 조절하여 LED 밝기를 제어한다
  • PWM 주파수와 해상도의 관계를 이해한다
  • 페이드 인/아웃, 호흡 효과 등 다양한 LED 효과를 구현한다
  • 여러 채널의 PWM을 동시에 제어하는 방법을 습득한다

1. PWM 기초

PWM은 디지털 출력으로 아날로그 효과를 만들어내는 핵심 기술입니다. LED 밝기 조절, 모터 속도 제어, 서보 모터 제어, 오디오 출력 등 다양한 분야에서 사용됩니다.

1.1 PWM이란?

PWM은 전압의 높낮이를 조절하는 것이 아닌, 전기를 켜는 시간(On)과 끄는 시간(Off)의 비율을 아주 빠르게 조절하는 방식으로, 한 주기에 스위치가 얼마나 오래 켜져있는지에 따라 출력의 세기가 결정되는 방법입니다. 이 때 관여하는 인자들은 다음과 같습니다.

주기(Period): 신호가 한 번 ON/OFF를 반복하는 전체 시간입니다.
펄스 폭(Pulse Width): 한 주기 내에서 신호가 'ON' 상태로 유지되는 시간입니다.
듀티 사이클(Duty Cycle): 주기 대비 펄스 폭의 비율을 퍼센트(%)로 나타낸 것입니다.

PWM의 기본 원리

HIGH (100% 밝기)  ████████████████████████████
                  
PWM 75%          ██████████████████░░░░░░░░░░
                  
PWM 50%          ████████████░░░░░░░░░░░░░░░░
                  
PWM 25%          ██████░░░░░░░░░░░░░░░░░░░░░░
                  
LOW (0% 밝기)     ░░░░░░░░░░░░░░░░░░░░░░░░░░░░
                  
                 ◀────── 1 Period ──────▶

PWM 파라미터

주기 (Period):        신호가 반복되는 시간
주파수 (Frequency):    1 / Period (Hz)
Duty Cycle:          HIGH 상태 비율 (%)
펄스 폭 (Pulse Width): HIGH 상태 시간

Duty Cycle 계산

Duty Cycle (%) = (Pulse Width / Period) × 100

예: Period = 1ms, Pulse Width = 0.5ms
    Duty Cycle = (0.5 / 1.0) × 100 = 50%

1.2 PWM으로 LED 밝기 조절하는 원리

사람의 눈은 빠른 깜박임을 평균 밝기로 인식합니다. 이를 활용하여 디지털 신호로 아날로그 효과를 만듭니다.

시간 평균 전압

PWM 100%:  평균 전압 = 3.3V  →  최대 밝기
PWM 75%:   평균 전압 = 2.48V →  75% 밝기
PWM 50%:   평균 전압 = 1.65V →  50% 밝기
PWM 25%:   평균 전압 = 0.83V →  25% 밝기
PWM 0%:    평균 전압 = 0V    →  꺼짐

깜박임 없는 조건

주파수 > 100Hz (Period < 10ms)
- 100Hz: 사람 눈의 한계 (깜박임 감지 가능)
- 1kHz: 부드러운 밝기 조절 (권장)
- 10kHz 이상: 모터 제어에 적합

1.3 하드웨어 PWM vs 소프트웨어 PWM

항목하드웨어 PWM소프트웨어 PWM
구현타이머 하드웨어인터럽트/루프
정확도매우 높음낮음 (지터 발생)
CPU 부하거의 없음높음
채널 수제한적 (타이머 채널)무제한 (이론상)
주파수매우 높음 가능낮음 (~1kHz)
사용 예LED, 모터, 서보간단한 LED

2. STM32 타이머 PWM 동작 원리

STM32의 타이머를 이용해 앞서 배운 타임아웃/인터럽트 제어 뿐만 아니라 PWM 출력 기능도 제어할 수 있습니다. STM32에서 PWM을 만들 때는 CCR(Capture/Compare Register)이라는 레지스터를 사용하며 다음과 같은 로직으로 작동합니다.

ARR: 전체 주기 (예: 1000까지 세기)
CCR: 켜져 있을 시간 (예: 500으로 설정)
동작: CNT가 0부터 올라가다가 **CCR(500)**보다 작으면 HIGH, 크면 LOW를 출력합니다. 이렇게 하면 정확히 50% Duty Cycle이 만들어집니다.

2.1 타이머 PWM 모드

PWM 생성 과정

Timer Counter: 0 → 1 → 2 → ... → CCRx → ... → ARR
               ↑                   ↑              ↑
            시작(LOW)          HIGH→LOW     다시 시작(ARR+1)

출력:         ████████████████░░░░░░░░░░░░
              0              CCRx          ARR
              
Duty Cycle = CCRx / ARR × 100%

PWM Mode 1 vs Mode 2

PWM Mode 1 (일반적):
  Counter < CCRx:  출력 = HIGH
  Counter ≥ CCRx:  출력 = LOW
  
PWM Mode 2 (반대):
  Counter < CCRx:  출력 = LOW
  Counter ≥ CCRx:  출력 = HIGH

2.2 타이머 채널과 GPIO 매핑

STM32F407 타이머 채널

TIM1:  CH1-CH4 (고급 타이머)
TIM2:  CH1-CH4 (32비트)
TIM3:  CH1-CH4 (범용)
TIM4:  CH1-CH4 (범용)
TIM5:  CH1-CH4 (32비트)
...

GPIO 핀 매핑 (예: TIM4)

//TIM4의 멀티 채널을 이용해 각기 다른 LED 제어
TIM4_CH1: PD12 (LED4 - Green)
TIM4_CH2: PD13 (LED3 - Orange)
TIM4_CH3: PD14 (LED5 - Red)
TIM4_CH4: PD15 (LED6 - Blue)

2.3 PWM 주파수와 해상도

주파수 vs 해상도 트레이드오프
우선순위별 PWM 설정 전략

우선순위용도 (예시)특징
고주파 우선모터 제어, 스위칭 파워(SMPS)가청 주파수 소음(20Hz~20kHz)을 피하기 위해 보통 20kHz 이상의 높은 주파수를 사용합니다. 주기가 짧아지는 만큼 카운팅할 수 있는 단계(해상도)는 상대적으로 줄어듭니다.
고해상도 우선LED 밝기 조절 (Dimming), 오디오 DAC사람이 인지하기 힘든 수준의 아주 미세한 밝기 변화나 부드러운 아날로그 파형을 구현하는 것이 목적입니다. 주파수를 낮추는 대신 한 주기 내의 제어 단계(Resolution)를 최대한 늘려 정밀도를 확보합니다.
Timer Clock = 84MHz
ARR = 999  → 주파수 = 84kHz, 해상도 = 1000 steps
ARR = 99   → 주파수 = 840kHz, 해상도 = 100 steps
ARR = 9    → 주파수 = 8.4MHz, 해상도 = 10 steps

일반적인 설정 (LED):
  주파수 = 1kHz ~ 10kHz
  해상도 = 256 ~ 1000 steps

계산 공식

PWM_Frequency = Timer_Clock / ((PSC + 1) × (ARR + 1))
Resolution = ARR + 1

예: 1kHz, 1000 steps
  84MHz / ((0 + 1) × (83999 + 1)) = 1kHz
  Resolution = 84000 steps
  
  또는
  84MHz / ((83 + 1) × (999 + 1)) = 1kHz
  Resolution = 1000 steps (더 실용적)

3. PWM 설정 및 기본 사용

STM32CubeMX에서 PWM을 설정하고 기본적인 밝기 제어를 구현합니다.

3.1 STM32CubeMX 설정

Step 1: GPIO 및 타이머 설정

Pinout & Configuration:

1. GPIO 확인
   PD12: TIM4_CH1 (LED4)
   PD13: TIM4_CH2 (LED3)
   PD14: TIM4_CH3 (LED5)
   PD15: TIM4_CH4 (LED6)

2. Timers → TIM4 선택
   
3. Channel 설정
   Channel1: PWM Generation CH1
   Channel2: PWM Generation CH2
   Channel3: PWM Generation CH3
   Channel4: PWM Generation CH4

Step 2: 타이머 파라미터 설정 (1kHz, 1000 steps)

Configuration → TIM4

Parameter Settings:
  Prescaler (PSC):        83
  Counter Mode:           Up
  Counter Period (ARR):   999
  
  Internal Clock Division: No Division
  Auto-reload preload:    Enable

계산 확인:
  Timer Clock = 84MHz
  Counter Clock = 84MHz / (83 + 1) = 1MHz
  PWM Frequency = 1MHz / (999 + 1) = 1kHz ✓
  Resolution = 1000 steps ✓

Step 3: PWM 채널 설정

PWM Generation Channel 1-4:
  Mode:           PWM mode 1
  Pulse (CCR):    0 (초기값)
  Output compare preload: Enable
  Fast Mode:      Disable
  CH Polarity:    High

Step 4: 코드 생성

Project Manager → Generate Code

3.2 생성된 초기화 코드 확인

/* TIM4 init function */
void MX_TIM4_Init(void)
{
  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};
  TIM_OC_InitTypeDef sConfigOC = {0};

  htim4.Instance = TIM4;
  htim4.Init.Prescaler = 83;
  htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim4.Init.Period = 999;
  htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
  
  if (HAL_TIM_Base_Init(&htim4) != HAL_OK)
  {
    Error_Handler();
  }
  
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim4, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  
  if (HAL_TIM_PWM_Init(&htim4) != HAL_OK)
  {
    Error_Handler();
  }
  
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  
  if (HAL_TIMEx_MasterConfigSynchronization(&htim4, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  
  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 0;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  
  // Channel 1-4 설정
  if (HAL_TIM_PWM_ConfigChannel(&htim4, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_TIM_PWM_ConfigChannel(&htim4, &sConfigOC, TIM_CHANNEL_2) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_TIM_PWM_ConfigChannel(&htim4, &sConfigOC, TIM_CHANNEL_3) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_TIM_PWM_ConfigChannel(&htim4, &sConfigOC, TIM_CHANNEL_4) != HAL_OK)
  {
    Error_Handler();
  }
}

3.3 PWM 시작 및 Duty Cycle 설정

PWM 시작

/* USER CODE BEGIN 2 */
// 모든 채널 PWM 시작
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_3);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_4);

printf("PWM started on all channels\r\n");
/* USER CODE END 2 */

Duty Cycle 설정 방법

/* USER CODE BEGIN 0 */
// 방법 1: 직접 CCR 레지스터 설정 -> 성능 및 정밀도 위주
void set_pwm_duty(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t duty)
{
  // duty: 0 ~ 999 (0% ~ 100%)
  __HAL_TIM_SET_COMPARE(htim, channel, duty);
}

// 방법 2: 백분율로 설정 -> 사용자 편의 위주
void set_pwm_percent(TIM_HandleTypeDef *htim, uint32_t channel, uint8_t percent)
{
  // percent: 0 ~ 100
  if (percent > 100) percent = 100;
  
  uint16_t duty = (uint16_t)((percent * (htim->Init.Period + 1)) / 100);
  __HAL_TIM_SET_COMPARE(htim, channel, duty);
}

// 방법 3: 8비트 값으로 설정 (0-255) -> 프로토콜 위주
void set_pwm_8bit(TIM_HandleTypeDef *htim, uint32_t channel, uint8_t value)
{
  // value: 0 ~ 255
  uint16_t duty = (uint16_t)((value * (htim->Init.Period + 1)) / 256);
  __HAL_TIM_SET_COMPARE(htim, channel, duty);
}
/* USER CODE END 0 */

기본 사용 예제

/* USER CODE BEGIN 3 */
while (1)
{
  // LED1: 25% 밝기
  set_pwm_percent(&htim4, TIM_CHANNEL_1, 25);
  
  // LED2: 50% 밝기
  set_pwm_percent(&htim4, TIM_CHANNEL_2, 50);
  
  // LED3: 75% 밝기
  set_pwm_percent(&htim4, TIM_CHANNEL_3, 75);
  
  // LED4: 100% 밝기
  set_pwm_percent(&htim4, TIM_CHANNEL_4, 100);
  
  HAL_Delay(2000);
  
  // 모두 끄기
  set_pwm_percent(&htim4, TIM_CHANNEL_1, 0);
  set_pwm_percent(&htim4, TIM_CHANNEL_2, 0);
  set_pwm_percent(&htim4, TIM_CHANNEL_3, 0);
  set_pwm_percent(&htim4, TIM_CHANNEL_4, 0);
  
  HAL_Delay(1000);
}
/* USER CODE END 3 */

4. LED 효과 구현

4.1 페이드 인/아웃 (Fade In/Out)

선형 페이드

/* USER CODE BEGIN 0 */
void fade_in(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t duration_ms)
{
  uint16_t arr = htim->Init.Period; //ARR
  uint16_t steps = arr + 1; 
  uint16_t delay_per_step = duration_ms / steps;
  
  for (uint16_t i = 0; i <= arr; i++) //CCR
  {
    __HAL_TIM_SET_COMPARE(htim, channel, i);
    HAL_Delay(delay_per_step);
  }
}

void fade_out(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t duration_ms)
{
  uint16_t arr = htim->Init.Period;
  uint16_t steps = arr + 1;
  uint16_t delay_per_step = duration_ms / steps;
  
  for (int16_t i = arr; i >= 0; i--)
  {
    __HAL_TIM_SET_COMPARE(htim, channel, i);
    HAL_Delay(delay_per_step);
  }
}
/* USER CODE END 0 */

/* USER CODE BEGIN 3 */
while (1)
{
  fade_in(&htim4, TIM_CHANNEL_1, 1000);   // 1초 동안 켜짐
  HAL_Delay(500);
  fade_out(&htim4, TIM_CHANNEL_1, 1000);  // 1초 동안 꺼짐
  HAL_Delay(500);
}
/* USER CODE END 3 */

부드러운 페이드 (지수 곡선)

/* USER CODE BEGIN 0 */
#include <math.h>

void fade_in_smooth(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t duration_ms)
{
  uint16_t arr = htim->Init.Period;
  uint8_t steps = 100;
  uint16_t delay_per_step = duration_ms / steps;
  
  for (uint8_t i = 0; i <= steps; i++)
  {
    // 지수 곡선 (사람 눈의 비선형 특성 반영)
    float normalized = (float)i / steps;
    float exponential = normalized * normalized;  // x^2
    uint16_t duty = (uint16_t)(exponential * arr);
    
    __HAL_TIM_SET_COMPARE(htim, channel, duty);
    HAL_Delay(delay_per_step);
  }
}

void fade_out_smooth(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t duration_ms)
{
  uint16_t arr = htim->Init.Period;
  uint8_t steps = 100;
  uint16_t delay_per_step = duration_ms / steps;
  
  for (int8_t i = steps; i >= 0; i--)
  {
    float normalized = (float)i / steps;
    float exponential = normalized * normalized;
    uint16_t duty = (uint16_t)(exponential * arr);
    
    __HAL_TIM_SET_COMPARE(htim, channel, duty);
    HAL_Delay(delay_per_step);
  }
}
/* USER CODE END 0 */

4.2 호흡 효과 (Breathing)

사인파 호흡

/* USER CODE BEGIN 0 */
#include <math.h>

#define PI 3.14159265f

void breathing_effect(TIM_HandleTypeDef *htim, uint32_t channel, 
                     uint16_t period_ms, uint8_t min_brightness, uint8_t max_brightness)
{
  uint16_t arr = htim->Init.Period;
  uint16_t steps = 100;
  uint16_t delay_per_step = period_ms / steps;
  
  for (uint16_t i = 0; i < steps; i++)
  {
    // 사인파: 0 → 1 → 0
    float angle = (float)i * 2.0f * PI / steps;
    float sine_value = (sinf(angle) + 1.0f) / 2.0f;  // 0.0 ~ 1.0
    
    // 최소/최대 밝기 범위 적용
    uint8_t brightness = min_brightness + 
                        (uint8_t)(sine_value * (max_brightness - min_brightness));
    
    uint16_t duty = (uint16_t)((brightness * arr) / 100);
    __HAL_TIM_SET_COMPARE(htim, channel, duty);
    
    HAL_Delay(delay_per_step);
  }
}
/* USER CODE END 0 */

/* USER CODE BEGIN 3 */
while (1)
{
  // 2초 주기로 10% ~ 100% 사이에서 호흡
  breathing_effect(&htim4, TIM_CHANNEL_1, 2000, 10, 100);
}
/* USER CODE END 3 */

다중 채널 호흡 (위상차)

/* USER CODE BEGIN 0 */
void multi_breathing(TIM_HandleTypeDef *htim, uint16_t period_ms)
{
  uint16_t arr = htim->Init.Period;
  uint16_t steps = 100;
  uint16_t delay_per_step = period_ms / steps;
  
  for (uint16_t i = 0; i < steps; i++)
  {
    // CH1: 0도
    float angle1 = (float)i * 2.0f * PI / steps;
    float brightness1 = (sinf(angle1) + 1.0f) / 2.0f;
    
    // CH2: 90도 위상차
    float angle2 = angle1 + PI / 2.0f;
    float brightness2 = (sinf(angle2) + 1.0f) / 2.0f;
    
    // CH3: 180도 위상차
    float angle3 = angle1 + PI;
    float brightness3 = (sinf(angle3) + 1.0f) / 2.0f;
    
    // CH4: 270도 위상차
    float angle4 = angle1 + 3.0f * PI / 2.0f;
    float brightness4 = (sinf(angle4) + 1.0f) / 2.0f;
    
    __HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_1, (uint16_t)(brightness1 * arr));
    __HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_2, (uint16_t)(brightness2 * arr));
    __HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_3, (uint16_t)(brightness3 * arr));
    __HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_4, (uint16_t)(brightness4 * arr));
    
    HAL_Delay(delay_per_step);
  }
}
/* USER CODE END 0 */

4.3 펄스 효과 (Pulse)

빠른 플래시

/* USER CODE BEGIN 0 */
void pulse_effect(TIM_HandleTypeDef *htim, uint32_t channel, 
                 uint8_t pulses, uint16_t pulse_duration_ms)
{
  uint16_t arr = htim->Init.Period;
  
  for (uint8_t i = 0; i < pulses; i++)
  {
    // 즉시 켜기
    __HAL_TIM_SET_COMPARE(htim, channel, arr);
    HAL_Delay(pulse_duration_ms);
    
    // 즉시 끄기
    __HAL_TIM_SET_COMPARE(htim, channel, 0);
    HAL_Delay(pulse_duration_ms);
  }
}
/* USER CODE END 0 */

/* USER CODE BEGIN 3 */
while (1)
{
  pulse_effect(&htim4, TIM_CHANNEL_1, 3, 100);  // 3번 깜박임
  HAL_Delay(1000);
}
/* USER CODE END 3 */

하트비트 효과

/* USER CODE BEGIN 0 */
void heartbeat_effect(TIM_HandleTypeDef *htim, uint32_t channel)
{
  uint16_t arr = htim->Init.Period;
  
  // 첫 번째 비트
  for (uint16_t i = 0; i <= arr; i += 50)
  {
    __HAL_TIM_SET_COMPARE(htim, channel, i);
    HAL_Delay(5);
  }
  for (uint16_t i = arr; i > 0; i -= 50)
  {
    __HAL_TIM_SET_COMPARE(htim, channel, i);
    HAL_Delay(5);
  }
  
  HAL_Delay(100);
  
  // 두 번째 비트
  for (uint16_t i = 0; i <= arr; i += 50)
  {
    __HAL_TIM_SET_COMPARE(htim, channel, i);
    HAL_Delay(5);
  }
  for (uint16_t i = arr; i > 0; i -= 50)
  {
    __HAL_TIM_SET_COMPARE(htim, channel, i);
    HAL_Delay(5);
  }
  
  HAL_Delay(800);  // 긴 휴지기
}
/* USER CODE END 0 */

4.4 무지개 효과 (RGB LED용)

RGB LED 색상 전환

/* USER CODE BEGIN 0 */
typedef struct {
  uint8_t r;
  uint8_t g;
  uint8_t b;
} RGB_t;

void set_rgb(TIM_HandleTypeDef *htim, RGB_t color)
{
  uint16_t arr = htim->Init.Period;
  
  // R: CH1, G: CH2, B: CH3
  __HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_1, (color.r * arr) / 255);
  __HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_2, (color.g * arr) / 255);
  __HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_3, (color.b * arr) / 255);
}

void rainbow_effect(TIM_HandleTypeDef *htim, uint16_t duration_ms)
{
  RGB_t colors[] = {
    {255, 0, 0},      // 빨강
    {255, 127, 0},    // 주황
    {255, 255, 0},    // 노랑
    {0, 255, 0},      // 초록
    {0, 0, 255},      // 파랑
    {75, 0, 130},     // 남색
    {148, 0, 211}     // 보라
  };
  
  uint8_t num_colors = sizeof(colors) / sizeof(RGB_t);
  
  for (uint8_t i = 0; i < num_colors; i++)
  {
    set_rgb(htim, colors[i]);
    HAL_Delay(duration_ms);
  }
}

void smooth_rainbow(TIM_HandleTypeDef *htim, uint16_t cycle_duration_ms)
{
  uint16_t steps = 360;  // 색상환 360도
  uint16_t delay_per_step = cycle_duration_ms / steps;
  
  for (uint16_t i = 0; i < steps; i++)
  {
    RGB_t color = hsv_to_rgb(i, 100, 100);  // HSV → RGB 변환
    set_rgb(htim, color);
    HAL_Delay(delay_per_step);
  }
}

// HSV to RGB 변환 함수
RGB_t hsv_to_rgb(uint16_t h, uint8_t s, uint8_t v)
{
  RGB_t rgb;
  
  uint8_t region = h / 43;
  uint8_t remainder = (h - (region * 43)) * 6;
  
  uint8_t p = (v * (255 - s)) >> 8;
  uint8_t q = (v * (255 - ((s * remainder) >> 8))) >> 8;
  uint8_t t = (v * (255 - ((s * (255 - remainder)) >> 8))) >> 8;
  
  switch (region)
  {
    case 0:  rgb.r = v; rgb.g = t; rgb.b = p; break;
    case 1:  rgb.r = q; rgb.g = v; rgb.b = p; break;
    case 2:  rgb.r = p; rgb.g = v; rgb.b = t; break;
    case 3:  rgb.r = p; rgb.g = q; rgb.b = v; break;
    case 4:  rgb.r = t; rgb.g = p; rgb.b = v; break;
    default: rgb.r = v; rgb.g = p; rgb.b = q; break;
  }
  
  return rgb;
}
/* USER CODE END 0 */

5. 타이머 인터럽트와 PWM 결합

타이머 인터럽트를 사용하여 논블로킹 LED 효과를 구현합니다.

5.1 논블로킹 페이드 효과

타이머 인터럽트 기반 페이드

/* USER CODE BEGIN 0 */
typedef struct {
  uint16_t current_duty;
  uint16_t target_duty;
  int16_t step;
  uint8_t active;
} PWM_Fade_t;

PWM_Fade_t fade_ch1 = {0};

void start_fade(PWM_Fade_t *fade, uint16_t from, uint16_t to, uint16_t duration_ms)
{
  fade->current_duty = from;
  fade->target_duty = to;
  
  // 1ms마다 호출되므로 duration_ms 스텝
  int32_t total_change = (int32_t)to - (int32_t)from;
  fade->step = total_change / (int16_t)duration_ms;
  
  fade->active = 1;
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)  // 1ms 타이머
  {
    if (fade_ch1.active)
    {
      fade_ch1.current_duty += fade_ch1.step;
      
      // 목표 도달 확인
      if ((fade_ch1.step > 0 && fade_ch1.current_duty >= fade_ch1.target_duty) ||
          (fade_ch1.step < 0 && fade_ch1.current_duty <= fade_ch1.target_duty))
      {
        fade_ch1.current_duty = fade_ch1.target_duty;
        fade_ch1.active = 0;
      }
      
      __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, fade_ch1.current_duty);
    }
  }
}
/* USER CODE END 0 */

/* USER CODE BEGIN 2 */
// TIM6: 1ms 인터럽트 타이머
HAL_TIM_Base_Start_IT(&htim6);

// TIM4: PWM 출력
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);
/* USER CODE END 2 */

/* USER CODE BEGIN 3 */
while (1)
{
  // 페이드 인 시작 (1초 동안)
  start_fade(&fade_ch1, 0, 999, 1000);
  
  // 페이드 진행 중 다른 작업 가능
  while (fade_ch1.active)
  {
    printf("Fading... Duty: %d\r\n", fade_ch1.current_duty);
    HAL_Delay(100);
  }
  
  HAL_Delay(500);
  
  // 페이드 아웃
  start_fade(&fade_ch1, 999, 0, 1000);
  
  while (fade_ch1.active)
  {
    HAL_Delay(100);
  }
  
  HAL_Delay(500);
}
/* USER CODE END 3 */

5.2 다중 채널 독립 제어

4개 채널 동시 페이드

/* USER CODE BEGIN 0 */
#define NUM_PWM_CHANNELS 4

PWM_Fade_t fades[NUM_PWM_CHANNELS] = {0};
uint32_t pwm_channels[NUM_PWM_CHANNELS] = {
  TIM_CHANNEL_1, TIM_CHANNEL_2, TIM_CHANNEL_3, TIM_CHANNEL_4
};

void start_fade_multi(uint8_t channel, uint16_t from, uint16_t to, uint16_t duration_ms)
{
  if (channel < NUM_PWM_CHANNELS)
  {
    start_fade(&fades[channel], from, to, duration_ms);
  }
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)
  {
    for (uint8_t i = 0; i < NUM_PWM_CHANNELS; i++)
    {
      if (fades[i].active)
      {
        fades[i].current_duty += fades[i].step;
        
        if ((fades[i].step > 0 && fades[i].current_duty >= fades[i].target_duty) ||
            (fades[i].step < 0 && fades[i].current_duty <= fades[i].target_duty))
        {
          fades[i].current_duty = fades[i].target_duty;
          fades[i].active = 0;
        }
        
        __HAL_TIM_SET_COMPARE(&htim4, pwm_channels[i], fades[i].current_duty);
      }
    }
  }
}
/* USER CODE END 0 */

/* USER CODE BEGIN 3 */
while (1)
{
  // 각 채널 순차적으로 페이드 인
  start_fade_multi(0, 0, 999, 500);
  HAL_Delay(250);
  start_fade_multi(1, 0, 999, 500);
  HAL_Delay(250);
  start_fade_multi(2, 0, 999, 500);
  HAL_Delay(250);
  start_fade_multi(3, 0, 999, 500);
  
  HAL_Delay(2000);
  
  // 동시에 페이드 아웃
  for (uint8_t i = 0; i < NUM_PWM_CHANNELS; i++)
  {
    start_fade_multi(i, 999, 0, 1000);
  }
  
  HAL_Delay(2000);
}
/* USER CODE END 3 */

5.3 상태 머신 기반 LED 시퀀스

/* USER CODE BEGIN 0 */
typedef enum {
  LED_SEQ_IDLE,
  LED_SEQ_FADE_IN,
  LED_SEQ_HOLD_HIGH,
  LED_SEQ_FADE_OUT,
  LED_SEQ_HOLD_LOW
} LED_Sequence_State_t;

typedef struct {
  LED_Sequence_State_t state;
  uint16_t counter;
  uint16_t current_brightness;
} LED_Sequence_t;

LED_Sequence_t led_seq = {LED_SEQ_IDLE, 0, 0};

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)
  {
    switch (led_seq.state)
    {
      case LED_SEQ_IDLE:
        // 아무것도 안 함
        break;
        
      case LED_SEQ_FADE_IN:
        led_seq.current_brightness++;
        __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, led_seq.current_brightness);
        
        if (led_seq.current_brightness >= 999)
        {
          led_seq.state = LED_SEQ_HOLD_HIGH;
          led_seq.counter = 0;
        }
        break;
        
      case LED_SEQ_HOLD_HIGH:
        led_seq.counter++;
        if (led_seq.counter >= 1000)  // 1초 유지
        {
          led_seq.state = LED_SEQ_FADE_OUT;
        }
        break;
        
      case LED_SEQ_FADE_OUT:
        led_seq.current_brightness--;
        __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, led_seq.current_brightness);
        
        if (led_seq.current_brightness == 0)
        {
          led_seq.state = LED_SEQ_HOLD_LOW;
          led_seq.counter = 0;
        }
        break;
        
      case LED_SEQ_HOLD_LOW:
        led_seq.counter++;
        if (led_seq.counter >= 500)  // 0.5초 유지
        {
          led_seq.state = LED_SEQ_FADE_IN;  // 다시 시작
        }
        break;
    }
  }
}

void start_led_sequence(void)
{
  led_seq.state = LED_SEQ_FADE_IN;
  led_seq.counter = 0;
  led_seq.current_brightness = 0;
}

void stop_led_sequence(void)
{
  led_seq.state = LED_SEQ_IDLE;
  __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 0);
}
/* USER CODE END 0 */

6. 고급 PWM 기법

6.1 감마 보정 (Gamma Correction)

사람의 눈은 밝기를 비선형적으로 인식합니다. 감마 보정을 통해 선형 값을 시각적으로 균일한 밝기로 변환합니다.

/* USER CODE BEGIN 0 */
// 감마 보정 룩업 테이블 (8bit → 10bit)
const uint16_t gamma_table[256] = {
    0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   1,   1,   1,   1,   1,
    2,   2,   2,   3,   3,   4,   4,   5,   5,   6,   7,   8,   8,   9,  10,  11,
   12,  13,  15,  16,  17,  18,  20,  21,  23,  24,  26,  27,  29,  31,  33,  35,
   37,  39,  41,  43,  45,  48,  50,  52,  55,  57,  60,  62,  65,  68,  71,  74,
   77,  80,  83,  86,  90,  93,  97, 100, 104, 108, 112, 116, 120, 124, 128, 132,
  137, 141, 146, 151, 155, 160, 165, 170, 175, 181, 186, 191, 197, 202, 208, 214,
  220, 225, 231, 237, 244, 250, 256, 263, 269, 276, 283, 289, 296, 303, 310, 318,
  325, 332, 340, 347, 355, 363, 371, 379, 387, 395, 403, 411, 420, 428, 437, 446,
  454, 463, 472, 481, 490, 499, 508, 518, 527, 537, 546, 556, 566, 576, 586, 596,
  606, 616, 627, 637, 648, 658, 669, 680, 691, 702, 713, 724, 735, 747, 758, 770,
  782, 793, 805, 817, 829, 842, 854, 866, 879, 891, 904, 917, 930, 943, 956, 969,
  982, 996,1009,1023,1036,1050,1064,1078,1092,1106,1121,1135,1150,1164,1179,1194,
 1209,1224,1239,1254,1270,1285,1301,1316,1332,1348,1364,1380,1396,1413,1429,1446,
 1462,1479,1496,1513,1530,1547,1565,1582,1599,1617,1635,1652,1670,1688,1706,1724,
 1743,1761,1780,1798,1817,1836,1855,1874,1893,1912,1932,1951,1971,1991,2010,2030,
 2051,2071,2091,2112,2132,2153,2174,2195,2216,2237,2258,2280,2301,2323,2345,2366
};

void set_led_brightness_gamma(TIM_HandleTypeDef *htim, uint32_t channel, uint8_t brightness)
{
  // brightness: 0 ~ 255
  uint16_t corrected = gamma_table[brightness];
  
  // ARR=999일 때 2366을 999로 스케일링
  uint16_t duty = (corrected * 999) / 2366;
  
  __HAL_TIM_SET_COMPARE(htim, channel, duty);
}
/* USER CODE END 0 */

/* USER CODE BEGIN 3 */
while (1)
{
  // 감마 보정 없이
  printf("Linear fade:\r\n");
  for (uint16_t i = 0; i <= 255; i++)
  {
    __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, (i * 999) / 255);
    HAL_Delay(10);
  }
  HAL_Delay(1000);
  
  // 감마 보정 적용
  printf("Gamma corrected fade:\r\n");
  for (uint16_t i = 0; i <= 255; i++)
  {
    set_led_brightness_gamma(&htim4, TIM_CHANNEL_2, i);
    HAL_Delay(10);
  }
  HAL_Delay(1000);
}
/* USER CODE END 3 */

6.2 PWM 주파수 동적 변경

/* USER CODE BEGIN 0 */
void change_pwm_frequency(TIM_HandleTypeDef *htim, uint32_t frequency_hz)
{
  // 타이머 중지
  HAL_TIM_PWM_Stop(htim, TIM_CHANNEL_ALL);
  
  // 새로운 주파수 계산
  uint32_t timer_clock = 84000000;  // 84MHz
  uint32_t prescaler = 0;
  uint32_t period;
  
  // 적절한 prescaler 찾기
  for (prescaler = 0; prescaler < 65536; prescaler++)
  {
    period = (timer_clock / ((prescaler + 1) * frequency_hz)) - 1;
    
    if (period < 65536)  // 16비트 범위 내
    {
      break;
    }
  }
  
  // 파라미터 업데이트
  __HAL_TIM_SET_PRESCALER(htim, prescaler);
  __HAL_TIM_SET_AUTORELOAD(htim, period);
  
  // Duty cycle 재계산 (50% 유지)
  __HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_1, period / 2);
  
  // 타이머 재시작
  HAL_TIM_PWM_Start(htim, TIM_CHANNEL_1);
  
  printf("PWM Frequency changed to %lu Hz\r\n", frequency_hz);
  printf("  PSC: %lu, ARR: %lu\r\n", prescaler, period);
}
/* USER CODE END 0 */

6.3 PWM을 이용한 간단한 멜로디 출력

부저로 음계 출력

/* USER CODE BEGIN 0 */
// 음계 주파수 (Hz)
#define NOTE_C4  262
#define NOTE_D4  294
#define NOTE_E4  330
#define NOTE_F4  349
#define NOTE_G4  392
#define NOTE_A4  440
#define NOTE_B4  494
#define NOTE_C5  523

typedef struct {
  uint16_t frequency;
  uint16_t duration_ms;
} Note_t;

void play_tone(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t frequency, uint16_t duration_ms)
{
  if (frequency == 0)
  {
    // 쉼표
    HAL_TIM_PWM_Stop(htim, channel);
  }
  else
  {
    // 주파수 설정
    uint32_t timer_clock = 84000000;
    uint32_t period = (timer_clock / frequency) - 1;
    
    __HAL_TIM_SET_AUTORELOAD(htim, period);
    __HAL_TIM_SET_COMPARE(htim, channel, period / 2);  // 50% duty
    
    HAL_TIM_PWM_Start(htim, channel);
  }
  
  HAL_Delay(duration_ms);
  HAL_TIM_PWM_Stop(htim, channel);
}

void play_melody(TIM_HandleTypeDef *htim, uint32_t channel, Note_t *notes, uint8_t length)
{
  for (uint8_t i = 0; i < length; i++)
  {
    play_tone(htim, channel, notes[i].frequency, notes[i].duration_ms);
    HAL_Delay(50);  // 음 사이 간격
  }
}
/* USER CODE END 0 */

/* USER CODE BEGIN 3 */
// 도레미파솔라시도
Note_t scale[] = {
  {NOTE_C4, 500},
  {NOTE_D4, 500},
  {NOTE_E4, 500},
  {NOTE_F4, 500},
  {NOTE_G4, 500},
  {NOTE_A4, 500},
  {NOTE_B4, 500},
  {NOTE_C5, 500}
};

while (1)
{
  play_melody(&htim4, TIM_CHANNEL_1, scale, 8);
  HAL_Delay(2000);
}
/* USER CODE END 3 */

7. 실습 과제

과제 1: 4단계 밝기 조절

버튼을 누를 때마다 LED 밝기가 0% → 33% → 66% → 100% → 0% 순서로 변경되도록 구현하세요.

요구사항

  • 버튼 누를 때마다 밝기 변경
  • 부드러운 전환 효과 (페이드)
  • UART로 현재 밝기 출력

힌트

typedef enum {
  BRIGHTNESS_OFF = 0,
  BRIGHTNESS_LOW = 33,
  BRIGHTNESS_MID = 66,
  BRIGHTNESS_HIGH = 100
} Brightness_Level_t;

Brightness_Level_t current_level = BRIGHTNESS_OFF;

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  if (GPIO_Pin == BUTTON_PIN)
  {
    // 다음 밝기 레벨로 전환
    switch (current_level)
    {
      case BRIGHTNESS_OFF:  current_level = BRIGHTNESS_LOW; break;
      case BRIGHTNESS_LOW:  current_level = BRIGHTNESS_MID; break;
      case BRIGHTNESS_MID:  current_level = BRIGHTNESS_HIGH; break;
      case BRIGHTNESS_HIGH: current_level = BRIGHTNESS_OFF; break;
    }
    
    // 페이드 시작
    start_fade(&fade_ch1, fade_ch1.current_duty, 
               (current_level * 999) / 100, 500);
  }
}

과제 2: RGB LED 색상 믹싱

3개의 PWM 채널(R, G, B)을 사용하여 다양한 색상을 만드세요.

요구사항

  • 프리셋 색상 7가지 (빨강, 초록, 파랑, 노랑, 청록, 자홍, 흰색)
  • 버튼으로 색상 전환
  • 부드러운 색상 전환 효과
  • 무지개 모드 (자동 순환)

힌트

RGB_t colors[] = {
  {255, 0, 0},      // 빨강
  {0, 255, 0},      // 초록
  {0, 0, 255},      // 파랑
  {255, 255, 0},    // 노랑
  {0, 255, 255},    // 청록
  {255, 0, 255},    // 자홍
  {255, 255, 255}   // 흰색
};

void transition_color(RGB_t from, RGB_t to, uint16_t duration_ms)
{
  uint16_t steps = 100;
  uint16_t delay_per_step = duration_ms / steps;
  
  for (uint16_t i = 0; i <= steps; i++)
  {
    RGB_t current;
    current.r = from.r + ((to.r - from.r) * i) / steps;
    current.g = from.g + ((to.g - from.g) * i) / steps;
    current.b = from.b + ((to.b - from.b) * i) / steps;
    
    set_rgb(&htim4, current);
    HAL_Delay(delay_per_step);
  }
}

과제 3: PWM 기반 디지털 시계 디스플레이

4개의 LED를 사용하여 시간을 표시하세요.

요구사항

  • LED1: 시 (0-23, 밝기로 표현)
  • LED2: 분 (0-59, 밝기로 표현)
  • LED3: 초 (0-59, 밝기로 표현)
  • LED4: 깜박임 (1Hz)
  • 밝기는 시간 값에 비례

힌트

void update_clock_display(uint8_t hours, uint8_t minutes, uint8_t seconds)
{
  // 시: 0-23을 0-100%로 매핑
  uint8_t hour_brightness = (hours * 100) / 23;
  set_pwm_percent(&htim4, TIM_CHANNEL_1, hour_brightness);
  
  // 분: 0-59를 0-100%로 매핑
  uint8_t minute_brightness = (minutes * 100) / 59;
  set_pwm_percent(&htim4, TIM_CHANNEL_2, minute_brightness);
  
  // 초: 0-59를 0-100%로 매핑
  uint8_t second_brightness = (seconds * 100) / 59;
  set_pwm_percent(&htim4, TIM_CHANNEL_3, second_brightness);
  
  // 초 표시 LED는 1Hz로 깜박임
  if (seconds % 2 == 0)
    set_pwm_percent(&htim4, TIM_CHANNEL_4, 100);
  else
    set_pwm_percent(&htim4, TIM_CHANNEL_4, 0);
}

8. 트러블슈팅

문제 1: PWM 출력이 안 됨

증상

LED가 전혀 켜지지 않음

원인 및 해결

// 1. PWM 시작 확인
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);  // 호출했는지 확인

// 2. GPIO 설정 확인 (CubeMX)
// Pin Configuration:
//   PD12: TIM4_CH1 (Alternate Function)

// 3. Duty Cycle 확인
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 500);  // 0이 아닌 값

// 4. 타이머 클럭 활성화
__HAL_RCC_TIM4_CLK_ENABLE();

// 5. GPIO 클럭 활성화
__HAL_RCC_GPIOD_CLK_ENABLE();

문제 2: LED가 깜박임 (깜빡거림)

증상

부드럽게 켜지지 않고 깜박거림

원인

PWM 주파수가 너무 낮음

해결

// 주파수를 100Hz 이상으로 설정
// 현재 설정 확인
uint32_t freq = 84000000 / ((PSC + 1) * (ARR + 1));
printf("PWM Frequency: %lu Hz\r\n", freq);

// 1kHz로 변경 (권장)
// PSC = 83, ARR = 999
// Freq = 84MHz / (84 * 1000) = 1kHz

문제 3: 밝기 조절이 선형적이지 않음

증상

50% duty cycle인데 눈으로 보면 80%처럼 밝음

원인

사람의 눈은 밝기를 비선형적으로 인식

해결

// 감마 보정 적용
set_led_brightness_gamma(&htim4, TIM_CHANNEL_1, 128);  // 50%

// 또는 제곱 함수 사용
uint8_t linear = 50;  // 0-100
uint16_t corrected = (linear * linear * 999) / 10000;
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, corrected);

문제 4: 여러 채널 사용 시 간섭

증상

한 채널을 변경하면 다른 채널도 영향받음

원인

모든 채널이 같은 ARR 공유

해결

// 같은 타이머의 모든 채널은 같은 주파수
// 각 채널의 CCR만 독립적으로 조절 가능

__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 250);   // 25%
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_2, 500);   // 50%
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_3, 750);   // 75%
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, 999);   // 100%

// 다른 주파수가 필요하면 다른 타이머 사용

9. 학습 정리

오늘 배운 내용

  • PWM의 기본 원리와 동작 방식
  • STM32 타이머를 이용한 하드웨어 PWM 생성
  • Duty Cycle 조절을 통한 LED 밝기 제어
  • PWM 주파수와 해상도의 관계
  • 다양한 LED 효과 구현 (페이드, 호흡, 펄스, 무지개)
  • 타이머 인터럽트와 PWM 결합 (논블로킹 제어)
  • 감마 보정 등 고급 기법

핵심 개념

1. PWM 파라미터

Frequency = Timer_Clock / ((PSC + 1) × (ARR + 1))
Duty Cycle (%) = (CCR / ARR) × 100
Resolution = ARR + 1

예: LED 제어
  Frequency = 1kHz (깜박임 없음)
  Resolution = 1000 steps (0.1% 단위 조절)
  PSC = 83, ARR = 999

2. PWM 설정 가이드

용도주파수해상도설정 예
LED 밝기1kHz1000PSC=83, ARR=999
서보 모터50Hz1000PSC=83, ARR=19999
DC 모터20kHz420PSC=0, ARR=4199
부저 (음계)가변-동적 계산

3. 밝기 제어 방법 비교

방법장점단점
직접 CCR 설정빠름코드 가독성 낮음
백분율직관적계산 필요
8비트 값다른 시스템 호환정밀도 제한
감마 보정시각적으로 선형룩업 테이블 필요

10. 참고 자료

STM32 타이머 레지스터

TIMx_CR1:   제어 레지스터
TIMx_PSC:   Prescaler
TIMx_ARR:   Auto-Reload Register (Period)
TIMx_CCRx:  Capture/Compare Register (Duty Cycle)
TIMx_CCMR:  Capture/Compare Mode Register
TIMx_CCER:  Capture/Compare Enable Register

HAL 함수 요약

// PWM 시작/정지
HAL_TIM_PWM_Start(&htim, channel);
HAL_TIM_PWM_Stop(&htim, channel);

// Duty Cycle 설정
__HAL_TIM_SET_COMPARE(&htim, channel, value);

// 주파수 변경
__HAL_TIM_SET_PRESCALER(&htim, psc);
__HAL_TIM_SET_AUTORELOAD(&htim, arr);

// 현재 값 읽기
uint32_t current = __HAL_TIM_GET_COMPARE(&htim, channel);

유용한 매크로

// Duty를 퍼센트로 변환
#define DUTY_TO_PERCENT(duty, arr) ((duty * 100) / (arr))

// 퍼센트를 Duty로 변환
#define PERCENT_TO_DUTY(percent, arr) ((percent * (arr)) / 100)

// 8비트를 Duty로 변환
#define BYTE_TO_DUTY(value, arr) ((value * (arr)) / 255)

profile
당신의 코딩 메이트

0개의 댓글