1) Cel lekcji
W tej lekcji poznasz PWM (Pulse Width Modulation), czyli modulację szerokości impulsu. To podstawowa technika sterowania:
- jasnością LED,
- prędkością silników DC,
- mocą grzałek,
- głośnością (w pewnych zastosowaniach).
2) Idea PWM — co oznacza wypełnienie?
PWM to szybkie przełączanie stanu pinu między 0 i 1. Jeśli przełączasz bardzo szybko, oko widzi „średnią” jasność.
- 0% → LED zgaszona
- 50% → około połowy jasności
- 100% → pełna jasność
- częstotliwość PWM zwykle stała
- zmieniasz tylko „czas ON”
PWM „software’owy” da się zrobić w pętli, ale jest niestabilny i blokuje CPU. Prawidłowo robi się PWM sprzętowym timerem.
3) Który pin ma PWM w ATmega328P?
ATmega328P ma wyjścia PWM związane z kanałami timerów. Najpopularniejsze:
- PD6 = OC0A (Timer0)
- PD5 = OC0B (Timer0)
- PB1 = OC1A (Timer1)
- PB2 = OC1B (Timer1)
- PB3 = OC2A (Timer2)
- PD3 = OC2B (Timer2)
4) Tryby PWM w skrócie: Fast PWM vs Phase Correct
Na start potrzebujesz dwóch pojęć:
- Fast PWM — timer liczy 0→TOP i wraca do 0 (często używany, prosty).
- Phase Correct PWM — timer liczy 0→TOP→0 (bardziej symetryczny).
Jest prosty i w praktyce bardzo często wystarczający do LED.
5) Konfiguracja Timer0: Fast PWM na OC0A (PD6)
Konfigurujemy Timer0 tak, aby generował PWM na pinie OC0A. W AVR trzeba ustawić:
- tryb pracy timera (WGM),
- tryb wyjścia OC0A (COM0A),
- preskaler (CS),
- wypełnienie w OCR0A.
Pin OC0A (PD6) musi być ustawiony jako wyjście w DDRD.
#include <avr/io.h>
#define PWM_DDR DDRD
#define PWM_PIN PD6 // OC0A
static void pwm0_init(void)
{
// PD6 jako wyjście
PWM_DDR |= (1 << PWM_PIN);
// Fast PWM (TOP=0xFF): WGM01=1, WGM00=1
TCCR0A = (1 << WGM01) | (1 << WGM00);
// Non-inverting mode na OC0A: COM0A1=1, COM0A0=0
// (im większe OCR0A, tym jaśniej)
TCCR0A |= (1 << COM0A1);
// Preskaler: np. 64 (częstotliwość PWM będzie ok do LED)
TCCR0B = (1 << CS01) | (1 << CS00);
// startowo: 0% jasności
OCR0A = 0;
}
int main(void)
{
pwm0_init();
while (1)
{
// PWM działa sprzętowo, CPU może robić inne rzeczy
}
}
6) Regulacja jasności: OCR0A jako „gałka”
W Fast PWM (8-bit) masz zakres 0..255:
- OCR0A = 0 → 0% (OFF)
- OCR0A = 128 → ~50%
- OCR0A = 255 → ~100%
Test poziomów jasności (manualnie)
OCR0A = 20; // bardzo ciemno
OCR0A = 80; // ciemno
OCR0A = 160; // jasno
OCR0A = 255; // max
7) Płynne rozjaśnianie i ściemnianie (fade)
Zrobimy efekt „oddechu” LED: zwiększamy OCR, potem zmniejszamy. PWM jest sprzętowe, ale zmiana OCR robiona w pętli daje płynność.
#include <avr/io.h>
#include <util/delay.h>
#define PWM_DDR DDRD
#define PWM_PIN PD6
static void pwm0_init(void)
{
PWM_DDR |= (1 << PWM_PIN);
TCCR0A = (1 << WGM01) | (1 << WGM00) | (1 << COM0A1);
TCCR0B = (1 << CS01) | (1 << CS00); // /64
OCR0A = 0;
}
int main(void)
{
pwm0_init();
while (1)
{
// rozjaśnianie
for (uint16_t v = 0; v <= 255; v++)
{
OCR0A = (uint8_t)v;
_delay_ms(5);
}
// ściemnianie
for (int16_t v = 255; v >= 0; v--)
{
OCR0A = (uint8_t)v;
_delay_ms(5);
}
}
}
8) PWM a „nieliniowość” jasności (pro tip)
Oko ludzkie nie odbiera jasności liniowo. Dlatego:
- zmiana 0→20 wydaje się duża,
- zmiana 200→220 prawie niewidoczna.
9) PWM + przycisk: 4 poziomy jasności
Łączymy lekcję 10/11 z PWM. Każdy klik zmienia poziom jasności.
#include <avr/io.h>
#include <util/delay.h>
#define PWM_DDR DDRD
#define PWM_PIN PD6
#define BTN_DDR DDRD
#define BTN_PORT PORTD
#define BTN_PINREG PIND
#define BTN_PIN PD2
static inline uint8_t button_is_pressed(void)
{
return ((BTN_PINREG & (1 << BTN_PIN)) == 0);
}
static inline uint8_t button_click(void)
{
if (button_is_pressed())
{
_delay_ms(20);
if (button_is_pressed())
{
while (button_is_pressed()) { }
_delay_ms(20);
return 1;
}
}
return 0;
}
static void pwm0_init(void)
{
PWM_DDR |= (1 << PWM_PIN);
TCCR0A = (1 << WGM01) | (1 << WGM00) | (1 << COM0A1);
TCCR0B = (1 << CS01) | (1 << CS00); // /64
OCR0A = 0;
}
int main(void)
{
pwm0_init();
BTN_DDR &= ~(1 << BTN_PIN);
BTN_PORT |= (1 << BTN_PIN); // pull-up
uint8_t level = 0;
uint8_t levels[] = { 0, 40, 120, 255 };
while (1)
{
if (button_click())
{
level++;
if (level >= 4) level = 0;
OCR0A = levels[level];
}
}
}
10) Zadania obowiązkowe
- Uruchom PWM na PD6 i ustaw 3 stałe poziomy jasności (np. 30/120/255) zmieniane co 1 s.
- Zrób „fade” (rozjaśnianie i ściemnianie).
- Połącz z przyciskiem: klik zmienia poziom jasności (minimum 4 poziomy).
11) Zadania dodatkowe (dla lepszych)
- Zrób 10 poziomów jasności (0..255) i przewijaj je przyciskiem.
- Zrób tryby: MODE_FADE, MODE_STATIC, MODE_PULSE (z lekcji 11 maszyna stanów).
- Dodaj drugi kanał PWM (np. PD5 OC0B) i steruj dwiema diodami niezależnie.
12) Zadania PRO (projektowe)
- Zrób „dimmer”: długie przytrzymanie zwiększa jasność, puszczenie zatrzymuje, kolejne przytrzymanie zmniejsza.
- Zrób korekcję „pseudo-gamma”: użyj tablicy 16 wartości, które rosną nieliniowo.
- Połącz z brzęczykiem: zmiana poziomu jasności potwierdzana sygnałem (np. krótkie beep).
W kolejnych lekcjach PWM wykorzystamy do sterowania silnikiem i do generowania dźwięku timerem (bez pętli).