PWM과 아날로그 출력) 디지털로 아날로그를 시뮬레이션하기

안녕하세요! 지금까지는 디지털 신호(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은 디지털과 아날로그를 연결하는 다리입니다. 이제 마이크로컨트롤러로 진짜 아날로그 세상을 제어할 수 있어요!

(다시 한 번, 아닐로그의 세상에 오신 걸 환영합니다.)

답글 남기기

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

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