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