🌗 Lekcja 14: PWM — regulacja jasności LED

Tryby PWM, wypełnienie (duty cycle), płynne ściemnianie/rozjaśnianie. To jest moment, w którym LED przestaje być „ON/OFF”.

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).
Po tej lekcji: zrobisz płynne rozjaśnianie i ściemnianie LED sprzętowym PWM (bez ręcznego „migania” w pętli).

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ść.

Wypełnienie (duty cycle)
  • 0% → LED zgaszona
  • 50% → około połowy jasności
  • 100% → pełna jasność
Co się zmienia?
  • częstotliwość PWM zwykle stała
  • zmieniasz tylko „czas ON”
Uwaga:
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)
W tej lekcji użyjemy: PD6 (OC0A) — wygodny start i łatwe testy.

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).
Na początek wybieramy Fast PWM.
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.
Ważne sprzętowo:
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
    }
}
Check: jeśli ustawisz OCR0A na 255 → LED powinna świecić maksymalnie.

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);
        }
    }
}
Check: LED powinna płynnie jaśnieć i przygasać.

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.
W praktyce stosuje się korekcję gamma (tablica wartości), ale na tym etapie wystarczy wiedzieć, że „liniowe OCR ≠ liniowe odczucie”.

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];
        }
    }
}
Check: klik przełącza jasność: OFF → słabo → średnio → max → OFF.

10) Zadania obowiązkowe

  1. Uruchom PWM na PD6 i ustaw 3 stałe poziomy jasności (np. 30/120/255) zmieniane co 1 s.
  2. Zrób „fade” (rozjaśnianie i ściemnianie).
  3. Połącz z przyciskiem: klik zmienia poziom jasności (minimum 4 poziomy).

11) Zadania dodatkowe (dla lepszych)

  1. Zrób 10 poziomów jasności (0..255) i przewijaj je przyciskiem.
  2. Zrób tryby: MODE_FADE, MODE_STATIC, MODE_PULSE (z lekcji 11 maszyna stanów).
  3. Dodaj drugi kanał PWM (np. PD5 OC0B) i steruj dwiema diodami niezależnie.

12) Zadania PRO (projektowe)

  1. Zrób „dimmer”: długie przytrzymanie zwiększa jasność, puszczenie zatrzymuje, kolejne przytrzymanie zmniejsza.
  2. Zrób korekcję „pseudo-gamma”: użyj tablicy 16 wartości, które rosną nieliniowo.
  3. Połącz z brzęczykiem: zmiana poziomu jasności potwierdzana sygnałem (np. krótkie beep).
Zapowiedź:
W kolejnych lekcjach PWM wykorzystamy do sterowania silnikiem i do generowania dźwięku timerem (bez pętli).