보충 3) PWM 수학과 하드웨어 완전 정복 – 계산부터 구현까지

안녕하세요! 7편에서 갑자기 등장한 복잡한 PWM 계산 공식삼각함수, HSV 색공간 때문에 어려움을 느끼셨을 수도 있습니다. (이걸 진짜 많은 레벨의 사람들이 본다고 생각하고 썼어야 했는데…ㅠㅠ 그러질 못했습니다.) PWM은 단순해 보이지만 실제로는 수학적 이해가 바탕이 되어야 제대로 활용할 수 있습니다. 이번에는 PWM의 모든 수학적 배경을 완전히 정복해보겠습니다! 도중에 삼각함수 나오니 뭐하니 해도 도망갈 필요 없습니다.

이 글의 목표

  • PWM 주파수 계산을 단계별로 완전 분해
  • 타이머 레지스터 설정의 물리적 의미 파악
  • 삼각함수를 이용한 자연스러운 효과 구현 원리
  • HSV 색공간과 RGB 변환의 수학적 배경
  • 복잡한 공식을 직관적으로 이해하는 방법

PWM 주파수 계산 – 단계별 완전 분해

기본 개념: 클럭부터 PWM까지의 여정

16MHz 시스템 클럭
      ↓
   분주기 (Prescaler)
      ↓  
   타이머 카운터
      ↓
   PWM 출력 핀

1단계: 시스템 클럭 이해하기

ATmega328P의 심장박동: 16MHz

16,000,000 Hz = 16,000,000번/초 = 1틱당 62.5나노초

1초 = 1,000,000,000 나노초
1틱 = 1,000,000,000 ÷ 16,000,000 = 62.5 나노초

직관적 이해:

  • 1초 동안 1600만 번 “틱틱틱” 소리
  • 매우 빨라서 우리가 직접 사용하기엔 너무 세밀함
  • 그래서 “분주기”로 속도를 줄여서 사용

2단계: 분주기(Prescaler)의 역할

c

TCCR1B |= (1 << CS11) | (1 << CS10);  // 64 분주

분주비 64의 의미:

원래 주파수: 16,000,000 Hz
분주 후:     16,000,000 ÷ 64 = 250,000 Hz
1틱당 시간:  1 ÷ 250,000 = 4 마이크로초

분주비별 비교:

분주비주파수1틱 시간용도
116MHz62.5ns정밀한 타이밍
82MHz500ns빠른 PWM
64250kHz4µs일반적 PWM
25662.5kHz16µs느린 PWM
102415.6kHz64µs매우 느린 PWM

3단계: PWM 주파수 계산 공식 유도

Fast PWM 모드의 경우

8비트 Fast PWM (0~255 카운트):

PWM 주파수 = 타이머 주파수 ÷ 256

예시: 64 분주 사용 시
타이머 주파수 = 16,000,000 ÷ 64 = 250,000 Hz  
PWM 주파수 = 250,000 ÷ 256 = 976.56 Hz

일반 공식:

PWM 주파수 = F_CPU ÷ (분주비 × (TOP + 1))

여기서:
- F_CPU = 16,000,000 Hz (시스템 클럭)
- TOP = 255 (8비트 모드) 또는 다른 값

Phase Correct PWM 모드의 경우

PWM 주파수 = F_CPU ÷ (분주비 × 2 × TOP)

왜 2를 곱하나?
- 카운터가 0→TOP→0으로 오르락내리락 (2배 시간 소요)

4단계: 톤(소리) 생성 주파수 계산

CTC 모드에서 정확한 주파수 생성:

c

// 440Hz (라 음) 생성하기
uint16_t frequency = 440;  // 목표 주파수
uint8_t prescaler = 8;     // 분주비

OCR1A = (F_CPU / (2 * prescaler * frequency)) - 1;

단계별 계산:

1단계: 분주 후 주파수 계산
       16,000,000 ÷ 8 = 2,000,000 Hz

2단계: Toggle 모드 고려 (2로 나누기)
       2,000,000 ÷ 2 = 1,000,000 Hz

3단계: 목표 주파수로 나누기  
       1,000,000 ÷ 440 = 2,272.7

4단계: 레지스터 값 (1 빼기)
       2,272 - 1 = 2,271

왜 1을 빼나?

  • 카운터가 0부터 시작하므로 (0, 1, 2, …, N = N+1개)
  • OCR1A = N이면 실제로는 N+1번 카운트
  • 따라서 원하는 카운트에서 1을 빼야 함

삼각함수를 이용한 자연스러운 효과

왜 삼각함수를 사용할까?

선형 변화 vs 사인파 변화

c

// 선형 변화 - 부자연스러움
for (uint8_t brightness = 0; brightness < 255; brightness++) {
    set_led_brightness(brightness);
    delay(10);
}
// → 기계적이고 딱딱한 느낌

// 사인파 변화 - 자연스러움  
for (uint16_t angle = 0; angle < 360; angle++) {
    float radians = angle * PI / 180.0;
    uint8_t brightness = (sin(radians) + 1.0) * 127.5;
    set_led_brightness(brightness);
    delay(10);
}  
// → 부드럽고 자연스러운 느낌

삼각함수 변환 과정 상세 분석

1단계: 각도를 라디안으로 변환

c

float radians = angle * 3.14159 / 180.0;

왜 라디안을 사용할까?

  • sin() 함수는 라디안 단위를 입력받음
  • 1라디안 = 180°/π ≈ 57.3°
  • 360° = 2π 라디안

변환 과정:

  0° → 0 라디안        → sin(0) = 0
 90° → π/2 라디안      → sin(π/2) = 1  
180° → π 라디안        → sin(π) = 0
270° → 3π/2 라디안     → sin(3π/2) = -1
360° → 2π 라디안       → sin(2π) = 0

2단계: 사인 값을 0~255 범위로 변환

c

uint8_t brightness = (sin(radians) + 1.0) * 127.5;

변환 과정 세부 분석:

sin() 함수 출력: -1.0 ~ +1.0 범위

1단계: +1.0 더하기
       -1.0 + 1.0 = 0.0
       +1.0 + 1.0 = 2.0
       → 0.0 ~ 2.0 범위

2단계: 127.5 곱하기 (255÷2)
       0.0 × 127.5 = 0
       2.0 × 127.5 = 255  
       → 0 ~ 255 범위

왜 127.5일까?

  • PWM은 0~255 범위 (256단계)
  • 사인파는 -1~+1 범위 (2.0 폭)
  • 255 ÷ 2 = 127.5
  • 이렇게 하면 완벽히 0~255 범위로 매핑됨

위상차를 이용한 파도 효과

c

for (uint8_t i = 0; i < 8; i++) {
    float phase = (time + i * 45) * PI / 180.0;
    float brightness = (sin(phase) + 1.0) / 2.0;
    
    if (brightness > 0.5) {
        pattern |= (1 << i);
    }
}

위상차의 효과:

LED 0: phase = time + 0×45° = time
LED 1: phase = time + 45°   = time + 45°
LED 2: phase = time + 90°   = time + 90°
...
LED 7: phase = time + 315°  = time + 315°

시간에 따른 변화:

시간 0°:  LED0=max, LED2=0,   LED4=min, LED6=0
시간 45°: LED1=max, LED3=0,   LED5=min, LED7=0
시간 90°: LED2=max, LED4=0,   LED6=min, LED0=0
→ 파도가 오른쪽으로 이동하는 효과!

HSV 색공간과 RGB 변환

HSV vs RGB – 인간 친화적 색상 표현

RGB의 한계

c

// RGB로 빨간색에서 파란색으로 변화시키려면?
for (int i = 0; i < 256; i++) {
    red = 255 - i;     // 빨강 감소
    green = 0;         // 초록 유지  
    blue = i;          // 파랑 증가
}
// → 중간에 자주색/보라색 거쳐감 (부자연스러움)

HSV의 장점

c

// HSV로 색상환 순환
for (int hue = 0; hue < 360; hue++) {
    // Hue(색상): 0~360° (빨→주→노→초→청→보→빨)
    // Saturation(채도): 100% (선명함)  
    // Value(명도): 50% (적당한 밝기)
    hsv_to_rgb(hue, 100, 50);
}
// → 자연스러운 무지개 색상 변화!

HSV→RGB 변환 알고리즘 완전 분해

1단계: HSV 정규화

c

float h = hue / 60.0;        // 0~360° → 0~6 범위
float s = saturation / 100.0; // 0~100% → 0~1 범위  
float v = value / 100.0;      // 0~100% → 0~1 범위

2단계: 색상환 섹터 결정

c

int sector = (int)h;  // 0, 1, 2, 3, 4, 5
float f = h - sector; // 소수점 부분

섹터별 색상:

섹터 0 (0°~60°):   빨강 → 노랑
섹터 1 (60°~120°): 노랑 → 초록  
섹터 2 (120°~180°): 초록 → 청록
섹터 3 (180°~240°): 청록 → 파랑
섹터 4 (240°~300°): 파랑 → 자주  
섹터 5 (300°~360°): 자주 → 빨강

3단계: 중간값 계산

c

float p = v * (1 - s);           // 최소값
float q = v * (1 - s * f);       // 감소하는 값  
float t = v * (1 - s * (1 - f)); // 증가하는 값

각 값의 의미:

  • v: 최대값 (해당 섹터의 주 색상)
  • p: 최소값 (완전히 꺼진 색상)
  • t: 증가하는 값 (페이드 인)
  • q: 감소하는 값 (페이드 아웃)

4단계: 섹터별 RGB 할당

c

switch(sector) {
    case 0: r=v; g=t; b=p; break;  // 빨강→노랑: R고정, G증가
    case 1: r=q; g=v; b=p; break;  // 노랑→초록: G고정, R감소
    case 2: r=p; g=v; b=t; break;  // 초록→청록: G고정, B증가  
    case 3: r=p; g=q; b=v; break;  // 청록→파랑: B고정, G감소
    case 4: r=t; g=p; b=v; break;  // 파랑→자주: B고정, R증가
    case 5: r=v; g=p; b=q; break;  // 자주→빨강: R고정, B감소
}

5단계: 0~255 범위로 변환

c

red_pwm = (uint8_t)(r * 255);
green_pwm = (uint8_t)(g * 255);  
blue_pwm = (uint8_t)(b * 255);

간단한 HSV→RGB 변환 함수

c

typedef struct {
    uint8_t r, g, b;
} rgb_t;

rgb_t hsv_to_rgb(uint16_t hue, uint8_t sat, uint8_t val) {
    rgb_t rgb = {0, 0, 0};
    
    // 입력값 검증
    hue = hue % 360;
    if (sat > 100) sat = 100;
    if (val > 100) val = 100;
    
    // 특별한 경우들
    if (sat == 0) {
        // 무채색 (회색조)
        rgb.r = rgb.g = rgb.b = (val * 255) / 100;
        return rgb;
    }
    
    if (val == 0) {
        // 완전히 어두움 (검정)
        return rgb;  // 이미 {0,0,0}
    }
    
    // 일반적인 경우
    float h = hue / 60.0f;
    float s = sat / 100.0f;
    float v = val / 100.0f;
    
    int sector = (int)h;
    float f = h - sector;
    
    float p = v * (1 - s);
    float q = v * (1 - s * f);  
    float t = v * (1 - s * (1 - f));
    
    switch(sector) {
        case 0: case 6: rgb.r = v*255; rgb.g = t*255; rgb.b = p*255; break;
        case 1:         rgb.r = q*255; rgb.g = v*255; rgb.b = p*255; break;
        case 2:         rgb.r = p*255; rgb.g = v*255; rgb.b = t*255; break;
        case 3:         rgb.r = p*255; rgb.g = q*255; rgb.b = v*255; break;
        case 4:         rgb.r = t*255; rgb.g = p*255; rgb.b = v*255; break;
        case 5:         rgb.r = v*255; rgb.g = p*255; rgb.b = q*255; break;
    }
    
    return rgb;
}

타이머 레지스터 설정 완전 해부

Timer1 레지스터의 물리적 의미

TCCR1A (Timer/Counter Control Register 1A)

c

TCCR1A |= (1 << WGM10) | (1 << COM1A1);

비트별 분석:

TCCR1A: 7  6  5  4  3  2  1  0
        COM1A1 COM1A0 COM1B1 COM1B0 - - WGM11 WGM10

WGM10 = 1: Waveform Generation Mode의 하위 비트
COM1A1 = 1: Compare Output Mode for Channel A

COM1A 비트 조합의 의미:

COM1A1COM1A0모드동작
00NormalOC1A 핀 연결 해제
01Toggle매치 시 OC1A 토글
10Clear매치 시 LOW, TOP에서 HIGH
11Set매치 시 HIGH, TOP에서 LOW

TCCR1B (Timer/Counter Control Register 1B)

c

TCCR1B |= (1 << WGM12) | (1 << CS11) | (1 << CS10);

비트별 분석:

TCCR1B: 7  6  5  4  3  2  1  0
        ICNC1 ICES1 - WGM13 WGM12 CS12 CS11 CS10

WGM12 = 1: Waveform Generation Mode의 상위 비트
CS11 = 1, CS10 = 1: Clock Select (64 분주)

WGM 비트 조합과 PWM 모드:

WGM13WGM12WGM11WGM10모드TOPPWM 주파수
00018비트 Fast PWM0xFFF_CPU/(분주×256)
00109비트 Fast PWM0x1FFF_CPU/(분주×512)
001110비트 Fast PWM0x3FFF_CPU/(분주×1024)
01018비트 Phase Correct0xFFF_CPU/(분주×512)

PWM 듀티 사이클 계산

c

OCR1A = duty_cycle;  // 0~255 (8비트 모드)

듀티 사이클 계산:

듀티 사이클(%) = (OCR1A / 255) × 100

예시:
OCR1A = 0   → 듀티 사이클 0%   (완전 OFF)
OCR1A = 64  → 듀티 사이클 25%  (1/4 ON)  
OCR1A = 128 → 듀티 사이클 50%  (1/2 ON)
OCR1A = 192 → 듀티 사이클 75%  (3/4 ON)
OCR1A = 255 → 듀티 사이클 100% (완전 ON)

PWM 신호 분석과 측정

오실로스코프 없이 PWM 확인하기

방법 1: LED 밝기로 확인

c

// 다양한 듀티 사이클로 테스트
uint8_t test_values[] = {0, 64, 128, 192, 255};

for (int i = 0; i < 5; i++) {
    OCR1A = test_values[i];
    _delay_ms(2000);  // 2초씩 유지
}
// 육안으로 밝기 변화 확인

방법 2: 다른 핀으로 주파수 측정

c

volatile uint32_t pwm_edges = 0;

// 외부 인터럽트로 PWM 엣지 카운트
ISR(INT0_vect) {
    pwm_edges++;
}

void measure_pwm_frequency() {
    pwm_edges = 0;
    _delay_ms(1000);  // 1초 측정
    
    uint32_t frequency = pwm_edges / 2;  // 상승/하강 엣지 → 주파수
    printf("PWM Frequency: %lu Hz\n", frequency);
}

PWM 품질 개선 기법

1. 고주파 PWM 사용

c

// 너무 낮은 주파수 (976Hz) - 깜빡임 보임
TCCR1B |= (1 << CS11) | (1 << CS10);  // 64 분주

// 높은 주파수 (31.25kHz) - 깜빡임 안보임  
TCCR1B |= (1 << CS10);  // 분주 없음

2. Phase Correct PWM 사용

c

// Fast PWM은 비대칭적 (노이즈 발생 가능)
TCCR1A |= (1 << WGM10);  // Fast PWM

// Phase Correct PWM은 대칭적 (더 깨끗한 신호)
TCCR1A |= (1 << WGM10);
TCCR1B |= (1 << WGM12);  // Phase Correct PWM

3. 필터 회로 추가

PWM 출력 → RC 필터 → 부드러운 아날로그 전압
          (R=1kΩ, C=100nF)

PWM 수학 마스터 체크리스트

다음 항목들을 모두 이해했다면 PWM 수학을 마스터한 것입니다:

기본 계산

  • 시스템 클럭에서 PWM 주파수까지의 계산 과정을 설명할 수 있다
  • 분주비가 PWM 주파수에 미치는 영향을 계산할 수 있다
  • 원하는 주파수에 맞는 레지스터 값을 계산할 수 있다

삼각함수 활용

  • 각도를 라디안으로 변환하는 이유를 안다
  • 사인파를 PWM 범위로 변환하는 과정을 이해한다
  • 위상차를 이용한 파도 효과를 구현할 수 있다

색공간 변환

  • HSV와 RGB의 차이점을 설명할 수 있다
  • HSV→RGB 변환 알고리즘을 이해한다
  • 무지개 색상 변화를 프로그래밍할 수 있다

하드웨어 제어

  • 타이머 레지스터의 각 비트 의미를 안다
  • 다양한 PWM 모드를 상황에 맞게 선택할 수 있다
  • PWM 품질 개선 기법을 적용할 수 있다

실전 응용 프로젝트

PWM 수학을 마스터했다면 다음 프로젝트들에 도전해보세요:

프로젝트 1: 정밀한 주파수 생성기

  • 1Hz~20kHz 범위의 정확한 주파수 출력
  • 소수점 단위 정밀도 (예: 440.5Hz)
  • 주파수 안정도 측정

프로젝트 2: 음악 신시사이저

  • 12음계 정확한 주파수 생성
  • 화음(코드) 생성 (여러 주파수 동시 출력)
  • ADSR 엔벨로프 적용

프로젝트 3: RGB LED 무드등

  • HSV 기반 색상 제어
  • 자연스러운 색상 전환
  • 음악에 반응하는 색상 변화

다음 단계

PWM 수학을 마스터했다면:

  1. 7편의 모든 코드가 완전히 이해될 겁니다
  2. 고급 신호 처리 기법에 도전할 수 있습니다
  3. 실제 제품 수준의 PWM 제어가 가능합니다

PWM은 단순한 디지털 출력에서 정교한 아날로그 제어로 한 단계 올라서는 핵심 기술입니다!


수학이 어려워 보여도 실제로는 논리적이고 아름다운 패턴들입니다. PWM 수학을 마스터하면 디지털과 아날로그 세상을 자유자재로 오갈 수 있어요! 사실 오실로스코프 사용이나 그런 것까지 더하면 되겠지만, 7편의 보충이기도 하고, 기본적인 내용으로만 정리해봅니다!

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

이 사이트는 Akismet을 사용하여 스팸을 줄입니다. 댓글 데이터가 어떻게 처리되는지 알아보세요.