안녕하세요! 지금까지는 디지털 신호(HIGH/LOW)만 다뤘습니다. 하지만 실제 세상은 아날로그입니다. LED의 밝기를 조절하거나, 모터의 속도를 제어하려면 PWM(Pulse Width Modulation)이 필요합니다. 디지털 마이크로컨트롤러로 아날로그 효과를 만드는 마법을 배워보겠습니다! (드디어 아날로그의 세상에 오신 걸 환영합니다.)
이번 편의 목표
- PWM의 개념과 동작 원리 이해
- 소프트웨어 PWM vs 하드웨어 PWM 비교
- LED 밝기 조절과 페이드 효과 구현
- 서보모터와 DC모터 제어 기초
- 아날로그 출력 시뮬레이션
PWM이란 무엇인가?
PWM의 기본 개념
PWM(Pulse Width Modulation)은 펄스의 폭을 조절해서 평균 전압을 제어하는 기법입니다.
듀티 사이클 25%:  ██░░██░░██░░██░░  평균 전압: 1.25V
듀티 사이클 50%:  ████░░░░████░░░░  평균 전압: 2.5V  
듀티 사이클 75%:  ██████░░██████░░  평균 전압: 3.75V핵심 용어들
- 주기(Period): 하나의 완전한 PWM 사이클 시간
- 듀티 사이클(Duty Cycle): HIGH 상태인 시간의 비율 (%)
- 주파수(Frequency): 1초당 PWM 사이클 수 (Hz)
듀티 사이클 = (HIGH 시간 / 전체 주기) × 100%
주파수 = 1 / 주기왜 PWM을 사용할까?
장점:
- 디지털 신호로 아날로그 효과 구현
- 전력 효율성이 높음 (스위칭 손실 최소)
- 정밀한 제어 가능
- 노이즈에 강함
단점:
- 리플(ripple) 발생 가능
- EMI(전자기 간섭) 발생
- 필터링 회로 필요할 수 있음
소프트웨어 PWM 구현
1. 기본 소프트웨어 PWM
c
#include <avr/io.h>
#include <util/delay.h>
void software_pwm(uint8_t duty_cycle) {
    // duty_cycle: 0~100 (퍼센트)
    
    if (duty_cycle == 0) {
        PORTB &= ~(1 << PB5);  // 완전히 끄기
    } else if (duty_cycle >= 100) {
        PORTB |= (1 << PB5);   // 완전히 켜기
    } else {
        // PWM 구현
        PORTB |= (1 << PB5);           // HIGH
        _delay_us(duty_cycle * 10);    // HIGH 시간
        
        PORTB &= ~(1 << PB5);          // LOW  
        _delay_us((100 - duty_cycle) * 10); // LOW 시간
    }
}
int main(void) {
    DDRB |= (1 << PB5);  // LED 출력 설정
    
    while(1) {
        // 50% 듀티 사이클로 PWM 출력
        software_pwm(50);
    }
    
    return 0;
}2. 타이머 기반 소프트웨어 PWM
c
#include <avr/io.h>
#include <avr/interrupt.h>
volatile uint8_t pwm_duty = 50;    // 0~255 범위
volatile uint8_t pwm_counter = 0;
ISR(TIMER0_OVF_vect) {
    pwm_counter++;
    
    if (pwm_counter <= pwm_duty) {
        PORTB |= (1 << PB5);   // LED 켜기
    } else {
        PORTB &= ~(1 << PB5);  // LED 끄기
    }
    
    // 256에서 리셋 (8비트 카운터 특성상 자동)
}
int main(void) {
    DDRB |= (1 << PB5);
    
    // Timer0 설정 (빠른 PWM 주파수용)
    TCCR0B |= (1 << CS00);  // 분주 없음 (61kHz PWM)
    TIMSK0 |= (1 << TOIE0);
    
    sei();
    
    while(1) {
        // 여기서 pwm_duty 값을 변경하면 밝기 조절 가능
    }
    
    return 0;
}하드웨어 PWM 사용하기
ATmega328P는 Timer1을 이용해 진짜 하드웨어 PWM을 제공합니다.
Timer1 하드웨어 PWM 설정
c
#include <avr/io.h>
#include <util/delay.h>
void hardware_pwm_init(void) {
    // PB1(OC1A)을 출력으로 설정
    DDRB |= (1 << PB1);
    
    // Fast PWM, 8비트 모드
    TCCR1A |= (1 << WGM10);
    TCCR1B |= (1 << WGM12);
    
    // Non-inverting 모드
    TCCR1A |= (1 << COM1A1);
    
    // 분주비 64 (약 976Hz PWM 주파수)
    TCCR1B |= (1 << CS11) | (1 << CS10);
}
void set_pwm_duty(uint8_t duty) {
    // 0~255 범위
    OCR1A = duty;
}
int main(void) {
    hardware_pwm_init();
    
    while(1) {
        // 밝기를 점진적으로 증가
        for (uint8_t brightness = 0; brightness < 255; brightness++) {
            set_pwm_duty(brightness);
            _delay_ms(10);
        }
        
        // 밝기를 점진적으로 감소  
        for (uint8_t brightness = 255; brightness > 0; brightness--) {
            set_pwm_duty(brightness);
            _delay_ms(10);
        }
    }
    
    return 0;
}LED 페이드 효과 구현
1. 선형 페이드
c
#include <avr/io.h>
#include <util/delay.h>
void hardware_pwm_init(void) {
    DDRB |= (1 << PB1);
    TCCR1A |= (1 << WGM10) | (1 << COM1A1);
    TCCR1B |= (1 << WGM12) | (1 << CS11) | (1 << CS10);
}
int main(void) {
    hardware_pwm_init();
    
    uint8_t brightness = 0;
    int8_t direction = 1;  // 1: 밝아지는 중, -1: 어두워지는 중
    
    while(1) {
        OCR1A = brightness;
        
        brightness += direction;
        
        // 방향 전환
        if (brightness == 255) {
            direction = -1;
        } else if (brightness == 0) {
            direction = 1;
        }
        
        _delay_ms(5);  // 페이드 속도 조절
    }
    
    return 0;
}2. 사인파 페이드 (더 자연스러운 효과)
c
#include <avr/io.h>
#include <util/delay.h>
#include <math.h>
void hardware_pwm_init(void) {
    DDRB |= (1 << PB1);
    TCCR1A |= (1 << WGM10) | (1 << COM1A1);
    TCCR1B |= (1 << WGM12) | (1 << CS11) | (1 << CS10);
}
int main(void) {
    hardware_pwm_init();
    
    uint16_t angle = 0;
    
    while(1) {
        // 0~359도를 라디안으로 변환
        float radians = angle * 3.14159 / 180.0;
        
        // 사인값을 0~255 범위로 변환
        uint8_t brightness = (uint8_t)((sin(radians) + 1.0) * 127.5);
        
        OCR1A = brightness;
        
        angle = (angle + 2) % 360;  // 각도 증가
        _delay_ms(20);
    }
    
    return 0;
}3. 다중 LED RGB 효과
c
#include <avr/io.h>
#include <avr/interrupt.h>
#include <math.h>
// 소프트웨어 PWM으로 RGB LED 제어
volatile uint8_t red_duty = 0;
volatile uint8_t green_duty = 0;  
volatile uint8_t blue_duty = 0;
volatile uint8_t pwm_counter = 0;
ISR(TIMER0_OVF_vect) {
    pwm_counter++;
    
    // Red LED (PB0)
    if (pwm_counter <= red_duty) {
        PORTB |= (1 << PB0);
    } else {
        PORTB &= ~(1 << PB0);
    }
    
    // Green LED (PB1)  
    if (pwm_counter <= green_duty) {
        PORTB |= (1 << PB1);
    } else {
        PORTB &= ~(1 << PB1);
    }
    
    // Blue LED (PB2)
    if (pwm_counter <= blue_duty) {
        PORTB |= (1 << PB2);
    } else {
        PORTB &= ~(1 << PB2);
    }
}
int main(void) {
    // RGB LED 출력 설정
    DDRB |= (1 << PB0) | (1 << PB1) | (1 << PB2);
    
    // Timer0 설정 (고주파 PWM용)
    TCCR0B |= (1 << CS00);
    TIMSK0 |= (1 << TOIE0);
    
    sei();
    
    uint16_t hue = 0;  // 색상환 각도
    
    while(1) {
        // HSV에서 RGB로 변환 (간단한 버전)
        float h = hue * 6.0 / 360.0;  // 0~6 범위
        int sector = (int)h;
        float f = h - sector;
        
        uint8_t brightness = 128;  // 전체 밝기
        
        switch(sector % 6) {
            case 0: // Red to Yellow
                red_duty = brightness;
                green_duty = (uint8_t)(brightness * f);
                blue_duty = 0;
                break;
            case 1: // Yellow to Green  
                red_duty = (uint8_t)(brightness * (1.0 - f));
                green_duty = brightness;
                blue_duty = 0;
                break;
            case 2: // Green to Cyan
                red_duty = 0;
                green_duty = brightness;
                blue_duty = (uint8_t)(brightness * f);
                break;
            case 3: // Cyan to Blue
                red_duty = 0;
                green_duty = (uint8_t)(brightness * (1.0 - f));
                blue_duty = brightness;
                break;
            case 4: // Blue to Magenta
                red_duty = (uint8_t)(brightness * f);
                green_duty = 0;
                blue_duty = brightness;
                break;
            case 5: // Magenta to Red
                red_duty = brightness;
                green_duty = 0;
                blue_duty = (uint8_t)(brightness * (1.0 - f));
                break;
        }
        
        hue = (hue + 1) % 360;
        _delay_ms(50);
    }
    
    return 0;
}버튼으로 밝기 제어
업/다운 버튼으로 밝기 조절
c
#include <avr/io.h>
#include <util/delay.h>
void hardware_pwm_init(void) {
    DDRB |= (1 << PB1);
    TCCR1A |= (1 << WGM10) | (1 << COM1A1);
    TCCR1B |= (1 << WGM12) | (1 << CS11) | (1 << CS10);
}
uint8_t debounce_button(uint8_t pin) {
    if (!(PINC & (1 << pin))) {
        _delay_ms(20);
        if (!(PINC & (1 << pin))) {
            // 버튼이 떼질 때까지 대기
            while (!(PINC & (1 << pin)));
            _delay_ms(20);
            return 1;
        }
    }
    return 0;
}
int main(void) {
    hardware_pwm_init();
    
    // 버튼 입력 설정
    DDRC &= ~((1 << PC0) | (1 << PC1));  // UP, DOWN 버튼
    PORTC |= (1 << PC0) | (1 << PC1);    // 풀업 활성화
    
    uint8_t brightness = 128;  // 초기 밝기 50%
    
    while(1) {
        // UP 버튼 (밝기 증가)
        if (debounce_button(PC0)) {
            if (brightness < 250) {  // 오버플로우 방지
                brightness += 5;
            }
        }
        
        // DOWN 버튼 (밝기 감소)
        if (debounce_button(PC1)) {
            if (brightness > 5) {  // 언더플로우 방지
                brightness -= 5;
            }
        }
        
        OCR1A = brightness;
        _delay_ms(10);
    }
    
    return 0;
}톤(소리) 생성
PWM을 이용해서 피에조 부저로 소리를 만들 수 있습니다.
1. 기본 톤 생성
c
#include <avr/io.h>
#include <util/delay.h>
// 음계별 주파수 (Hz)
#define NOTE_C4  261
#define NOTE_D4  293
#define NOTE_E4  329
#define NOTE_F4  349
#define NOTE_G4  392
#define NOTE_A4  440
#define NOTE_B4  493
#define NOTE_C5  523
void tone_init(void) {
    DDRB |= (1 << PB1);  // 부저 연결 핀
    TCCR1A |= (1 << WGM10) | (1 << COM1A0);  // Toggle OC1A
    TCCR1B |= (1 << WGM12) | (1 << CS11);    // CTC 모드, 8 분주
}
void play_tone(uint16_t frequency, uint16_t duration_ms) {
    if (frequency == 0) {
        // 무음 (휴식)
        TCCR1A &= ~(1 << COM1A0);  // PWM 끄기
    } else {
        TCCR1A |= (1 << COM1A0);   // PWM 켜기
        
        // 주파수에 맞는 비교값 계산
        // F_CPU / (2 * prescaler * frequency) - 1
        OCR1A = (F_CPU / (2 * 8 * frequency)) - 1;
    }
    
    _delay_ms(duration_ms);
}
void stop_tone(void) {
    TCCR1A &= ~(1 << COM1A0);  // PWM 끄기
}
int main(void) {
    tone_init();
    
    // 간단한 멜로디
    uint16_t melody[] = {NOTE_C4, NOTE_D4, NOTE_E4, NOTE_F4, NOTE_G4, NOTE_A4, NOTE_B4, NOTE_C5};
    uint16_t durations[] = {500, 500, 500, 500, 500, 500, 500, 1000};
    uint8_t melody_length = sizeof(melody) / sizeof(melody[0]);
    
    while(1) {
        for (uint8_t i = 0; i < melody_length; i++) {
            play_tone(melody[i], durations[i]);
            _delay_ms(50);  // 음표 사이 짧은 쉼
        }
        
        stop_tone();
        _delay_ms(2000);  // 2초 쉬고 반복
    }
    
    return 0;
}2. 사이렌 효과
c
#include <avr/io.h>
#include <util/delay.h>
void tone_init(void) {
    DDRB |= (1 << PB1);
    TCCR1A |= (1 << WGM10) | (1 << COM1A0);
    TCCR1B |= (1 << WGM12) | (1 << CS11);
}
void set_frequency(uint16_t freq) {
    if (freq > 0) {
        OCR1A = (F_CPU / (2 * 8 * freq)) - 1;
        TCCR1A |= (1 << COM1A0);
    } else {
        TCCR1A &= ~(1 << COM1A0);
    }
}
int main(void) {
    tone_init();
    
    while(1) {
        // 경찰차 사이렌 (주파수 스위핑)
        for (uint16_t freq = 400; freq <= 800; freq += 10) {
            set_frequency(freq);
            _delay_ms(20);
        }
        
        for (uint16_t freq = 800; freq >= 400; freq -= 10) {
            set_frequency(freq);
            _delay_ms(20);
        }
    }
    
    return 0;
}DC모터 속도 제어 기초
1. 기본 모터 제어
c
#include <avr/io.h>
#include <util/delay.h>
void motor_pwm_init(void) {
    DDRB |= (1 << PB1);  // 모터 제어 핀
    TCCR1A |= (1 << WGM10) | (1 << COM1A1);  // Fast PWM
    TCCR1B |= (1 << WGM12) | (1 << CS11) | (1 << CS10);
}
void set_motor_speed(uint8_t speed) {
    // speed: 0 (정지) ~ 255 (최대속도)
    OCR1A = speed;
}
int main(void) {
    motor_pwm_init();
    
    while(1) {
        // 점진적 가속
        for (uint8_t speed = 0; speed < 255; speed += 5) {
            set_motor_speed(speed);
            _delay_ms(100);
        }
        
        _delay_ms(1000);  // 최대속도로 1초 유지
        
        // 점진적 감속
        for (uint8_t speed = 255; speed > 0; speed -= 5) {
            set_motor_speed(speed);
            _delay_ms(100);
        }
        
        _delay_ms(2000);  // 2초 정지
    }
    
    return 0;
}PWM 최적화와 고급 기법
1. 다중 채널 PWM
c
// Timer1의 채널 A, B 동시 사용
void dual_pwm_init(void) {
    DDRB |= (1 << PB1) | (1 << PB2);  // OC1A, OC1B
    
    TCCR1A |= (1 << WGM10);           // 8비트 Fast PWM
    TCCR1A |= (1 << COM1A1);          // 채널 A 활성화
    TCCR1A |= (1 << COM1B1);          // 채널 B 활성화
    
    TCCR1B |= (1 << WGM12) | (1 << CS11) | (1 << CS10);
}
void set_dual_pwm(uint8_t duty_a, uint8_t duty_b) {
    OCR1A = duty_a;  // 채널 A 듀티 사이클
    OCR1B = duty_b;  // 채널 B 듀티 사이클
}2. 16비트 정밀도 PWM
c
void precision_pwm_init(void) {
    DDRB |= (1 << PB1);
    
    // 16비트 Fast PWM, TOP = ICR1
    TCCR1A |= (1 << WGM11) | (1 << COM1A1);
    TCCR1B |= (1 << WGM13) | (1 << WGM12) | (1 << CS11);
    
    ICR1 = 1023;  // 10비트 분해능 (0~1023)
}
void set_precision_pwm(uint16_t duty) {
    // 0~1023 범위의 정밀한 제어
    OCR1A = duty;
}응용 과제
과제 1: 스마트 무드등
- 버튼으로 색상 변경
- 자동 색상 순환 모드
- 밝기 조절 기능
과제 2: 음악 플레이어
- 여러 곡 저장
- 템포 조절 기능
- 버튼으로 곡 선택
과제 3: 로봇 제어 기초
- 2개의 DC모터로 차동 조향
- PWM으로 속도 제어
- 간단한 라인 트레이싱
다음 편 예고
다음 편에서는 **ADC(Analog to Digital Converter)**를 배워보겠습니다:
- 아날로그 센서 값 읽기
- 가변저항으로 LED 밝기 제어
- 온도 센서와 광센서 활용
- 아날로그 신호 처리 기법
- ADC와 PWM 조합으로 완전한 아날로그 시스템 구축
PWM으로 출력을 마스터했다면, 이제 아날로그 입력을 정복할 시간입니다!
PWM은 디지털과 아날로그를 연결하는 다리입니다. 이제 마이크로컨트롤러로 진짜 아날로그 세상을 제어할 수 있어요!
(다시 한 번, 아닐로그의 세상에 오신 걸 환영합니다.)