PWM (펄스폭 변조)
그동안 통신 쪽(I2C, SPI, UART)이랑 ADC까지 정리했으니, 이번엔 방향을 좀 틀어서 PWM을 정리해본다. ADC가 바깥 세상의 아날로그 값을 마이컴 안으로 읽어들이는 거였다면, PWM은 반대로 마이컴이 아날로그처럼 바깥으로 내보내는 느낌이라 짝처럼 묶어서 보기 좋더라.
일단 이 궁금증부터
LED 밝기를 절반만 켜고 싶다. 모터를 천천히만 돌리고 싶다. 그런데 마이컴 핀이 내보낼 수 있는 건 0V(LOW) 아니면 5V(HIGH), 딱 두 가지뿐이다. 디지털이니까. 그럼 그 중간인 "반쯤"은 대체 어떻게 만들지? 나는 처음에 무슨 볼륨 노브처럼 전압을 슬슬 낮춰주는 뭔가가 따로 있는 줄 알았다 ㅋㅋ
답은 좀 얍삽하다(?). 아주 빠르게 껐다 켰다를 반복하면서, "켜져 있는 시간의 비율"로 평균 전압을 만드는 거다. 절반만 켜고 싶으면 절반의 시간만 HIGH로 두면 된다. 이렇게 켜짐/꺼짐의 폭을 조절해서 원하는 세기를 만드는 게 바로 PWM이다.
용어부터 풀어보자
PWM은 Pulse Width Modulation, 우리말로 펄스폭 변조다. 이름 그대로 "펄스(HIGH로 튀어나온 신호)의 폭(넓이)을 변조(조절)한다"는 뜻이다. "피-더블유-엠"이라고 그냥 읽으면 된다.
핵심 규칙이 하나 있다. 주기(한 번 껐다 켜지는 데 걸리는 시간)는 고정해두고, 그 한 주기 안에서 HIGH로 켜져 있는 시간만 늘렸다 줄였다 한다는 거다. 켜짐과 꺼짐이 세트로 한 번 도는 게 한 주기고, 그 주기가 1초에 몇 번 도느냐가 주파수다.
듀티비(duty cycle)가 진짜 주인공
여기서 제일 중요한 놈이 듀티비(duty cycle)다. 한 주기 중에 HIGH로 켜져 있는 시간의 비율(%)을 말한다. 앞 그림에 있던 그 공식이다.
듀티비 = (ON 시간 / 전체 주기) × 100%
이 듀티비가 결국 평균 전압을 결정한다. 5V 기준으로 계산하면 이렇게 된다.
| 듀티비 | 평균 전압 (5V 기준) | 체감 |
|---|---|---|
| 25% | ≈ 1.25V | LED 어둑, 모터 느릿 |
| 50% | ≈ 2.5V | 딱 절반 |
| 75% | ≈ 3.75V | LED 밝음, 모터 빠릿 |
| 100% | 5V | 계속 켜짐 (그냥 HIGH) |
계산도 간단하다. 평균 전압 = 전원 전압 × 듀티비. 듀티 25%면 5V × 0.25 = 1.25V다. 이 평균값이 LED한테는 밝기로, 모터한테는 속도로 느껴진다.
근데 이게 왜 되는 거지?
솔직히 처음엔 "껐다 켰다 하면 깜빡거리기만 하지 어떻게 중간 밝기가 되냐"가 이해가 안 됐다. 포인트는 속도다. 스위칭을 눈이나 모터가 도저히 못 따라올 만큼 빠르게(보통 수백 Hz에서 수십 kHz) 반복하면, 사람 눈이나 모터의 관성이 그 깜빡임을 하나하나 못 쫓아가고 그냥 평균값으로 뭉뚱그려 느끼게 된다.
형광등이 사실은 미친 듯이 깜빡이는데 우리 눈엔 그냥 계속 켜진 걸로 보이는 거랑 비슷하다. 모터도 마찬가지로, 전기가 워낙 빨리 끊겼다 이어지니까 회전 관성이 그걸 평균으로 받아서 "적당한 속도"로 돈다. 그래서 PWM에서 주파수를 충분히 높게 잡는 게 중요하다. 너무 낮으면 LED가 눈에 띄게 깜빡거리거나 모터가 덜덜거린다.
AVR에서는 어떻게 만드나 (타이머)
이걸 소프트웨어로 PORT 켰다 껐다 delay 주면서 직접 만들 수도 있긴 한데, 그러면 CPU가 그 짓만 하고 있어야 해서 낭비다. 그래서 AVR은 타이머(Timer)가 하드웨어로 알아서 PWM을 뽑아준다. 우리는 레지스터 몇 개만 세팅하면 끝이고, 그 뒤론 CPU가 다른 일 해도 핀에서 파형이 계속 나온다. 이게 진짜 편하다.
PWM에 관여하는 레지스터를 정리하면 이렇다. (Timer0 기준으로 설명한다.)
- WGM 비트 (
TCCR0안) : 타이머 동작 모드를 고른다. PWM을 쓰려면 Fast PWM이나 Phase-correct PWM 모드로 맞춘다. - COM 비트 (
TCCR0안) : 출력핀(OCn)을 어떻게 토글할지. 보통 "비교값 만나면 LOW로" 하는 non-inverting 모드를 쓴다. - CS 비트 (
TCCR0안) : 프리스케일러. 타이머 클럭을 몇으로 나눌지 정해서 PWM 주파수를 조절한다. - OCR0 레지스터 : 비교값. 이게 바로 듀티비를 정하는 값이다. 타이머가 0부터 세다가 이 값에 도달하면 핀을 내린다.
PWM 주파수는 이 공식으로 정해진다.
PWM 주파수 = F_CPU / (프리스케일러 × (TOP + 1))
8비트 타이머면 TOP이 255라서 (TOP+1)은 256이 된다. 여기까지가 세팅이고, 실제로 밝기를 바꾸는 건 오로지 OCR0 값 하나다.
코드로 보자 (Timer0, 8비트 Fast PWM)
말로 하면 복잡하니 코드가 마음 편하다. Timer0을 Fast PWM으로 세팅하는 예시다.
void PWM_Init(void)
{
// OC0 핀(PWM 출력)을 출력으로. (ATmega128 기준 PB4)
DDRB |= (1 << PB4);
// Fast PWM 모드 : WGM01, WGM00 둘 다 1
// non-inverting 출력 : COM01 = 1 (비교값 만나면 LOW)
// 프리스케일러 8 : CS01 = 1
TCCR0 = (1 << WGM00) | (1 << WGM01) | (1 << COM01) | (1 << CS01);
OCR0 = 64; // 듀티 ≈ 64/255 ≈ 25% → LED 어둑하게
}
여기서 WGM00+WGM01로 Fast PWM 모드가 되고, COM01로 "OCR0에 도달하면 출력핀을 LOW로 내리는" 동작이 켜진다. 즉 타이머가 0→255를 계속 반복하는데, 0부터 OCR0까지는 HIGH, OCR0 넘으면 255까지는 LOW가 된다. 그래서 OCR0가 클수록 HIGH 구간이 길어지고 = 듀티가 커지고 = 밝아진다.
핵심은 세팅은 한 번만 하고, 그 다음부턴 OCR0 값만 바꾸면 밝기/속도가 실시간으로 바뀐다는 거다.
void main(void)
{
PWM_Init();
while (1)
{
OCR0 = 64; // 듀티 25% (5V × 0.25 ≈ 1.25V) → 어둑
// OCR0 = 128; // 듀티 50% (≈ 2.5V) → 절반
// OCR0 = 191; // 듀티 75% (≈ 3.75V) → 밝음
}
}
이 OCR0 한 줄만 0부터 255 사이로 바꿔주면 밝기가 쭉 조절된다. 만약 가변저항 값을 ADC로 읽어서 그 값을 OCR0에 그대로 넣어주면? 노브를 돌리는 대로 LED 밝기가 따라오는 디머(dimmer)가 된다. ADC랑 PWM이 이렇게 만난다 ^^
어디에 쓰나
- LED 디밍 :
OCR값만 바꿔서 밝기 조절. 부드럽게 밝아졌다 어두워지는 숨쉬기 효과도 이걸로 만든다. - DC 모터 속도 제어 : 듀티비로 평균 전압 = 속도 조절. (실제론 모터 전류 때문에 트랜지스터/MOSFET를 하나 끼워서 구동한다.)
- 서보모터 : 얘는 좀 특이한데, 주기 20ms(50Hz)에 펄스폭 1~2ms를 실어서 그 폭으로 각도를 정한다. (밝기가 아니라 위치를 PWM으로 지정하는 케이스 — 이건 나중에 따로 다뤄야겠다.)
정리하면
- 마이컴 핀은 0V/5V만 내보내지만, 빠르게 껐다 켰다 하면 그 평균으로 중간값을 만들 수 있다. 그게 PWM.
- 주기는 고정, 한 주기 안의 HIGH 시간(펄스 폭)만 조절한다.
- 듀티비 = ON시간 / 주기. 평균 전압 = 전원 전압 × 듀티비. → LED 밝기, 모터 속도가 된다.
- AVR에선 타이머가 하드웨어로 만들어준다.
OCR값만 바꾸면 밝기/속도가 바뀐다.
정리하다 보니 결국 PWM은 타이머가 다 해주는 거였다. 주파수도 프리스케일러+TOP으로 정하고, 듀티도 비교값으로 정하고… 죄다 타이머 이야기다. 그래서 다음엔 타이머와 인터럽트를 제대로 파봐야겠다. PWM도 결국 타이머로 만드는 거니까, 순서상 그게 맞는 것 같다.