🌈 Lekcja 16: Dioda RGB

Mieszanie kolorów, PWM na kanałach, sceny świetlne. W tej lekcji budujesz „mini-oświetlenie” RGB jak w prawdziwych urządzeniach.

1) Cel lekcji

Dioda RGB to trzy diody w jednej obudowie: Red, Green, Blue. Sterując jasnością każdego kanału osobno (najlepiej PWM), możesz uzyskać praktycznie dowolny kolor.

  • rozróżnisz RGB wspólna anoda vs wspólna katoda,
  • uruchomisz PWM na 3 kanałach,
  • zrobisz funkcję rgb_set(r,g,b),
  • zrobisz paletę kolorów i sceny świetlne,
  • połączysz RGB z przyciskiem (tryby).
Po tej lekcji: masz gotowy moduł RGB do dalszych projektów (wskaźniki, statusy, efekty).

2) Rodzaje diody RGB: wspólna katoda vs wspólna anoda

Wspólna katoda (Common Cathode)
  • wspólna nóżka do GND
  • kanały R/G/B sterujesz stanem HIGH/PWM (1 = świeci)
  • logika „normalna” (non-inverting)
Wspólna anoda (Common Anode)
  • wspólna nóżka do VCC
  • kanały R/G/B świecą przy stanie LOW (0 = świeci)
  • logika „odwrócona” (inverting)
Na start przyjmujemy wspólną katodę.
Jeśli masz wspólną anodę: w kodzie odwrócisz wypełnienie (255 - wartość).

3) Podłączenie RGB do pinów PWM

Potrzebujesz 3 wyjść PWM. Najprostsza konfiguracja na ATmega328P:

  • PD6 = OC0A → kanał R
  • PD5 = OC0B → kanał G
  • PB1 = OC1A → kanał B

Każdy kanał musi mieć osobny rezystor (np. 220–330 Ω). Wspólna katoda do GND.

Kluczowe:
Nie podłączaj RGB bez rezystorów — spalisz diody lub przeciążysz GPIO.

4) PWM na 3 kanałach — koncepcja

Timer0 ma 2 kanały PWM (OC0A, OC0B), Timer1 ma kanał OC1A. Ustawimy:

  • Timer0: Fast PWM dla R i G
  • Timer1: Fast PWM dla B (TOP=0x00FF, czyli 8-bit) — żeby zakres był spójny 0..255
Dlaczego 8-bit na Timer1?
Żeby każdy kanał miał identyczną skalę 0..255 i proste mieszanie kolorów.

5) Kod: inicjalizacja PWM dla R (PD6) i G (PD5) — Timer0

#include <avr/io.h>

#define R_DDR   DDRD
#define R_PIN   PD6   // OC0A
#define G_DDR   DDRD
#define G_PIN   PD5   // OC0B

static void pwm_timer0_init(void)
{
    // PD6 i PD5 jako wyjścia
    R_DDR |= (1 << R_PIN);
    G_DDR |= (1 << G_PIN);

    // Fast PWM (TOP=0xFF): WGM00=1, WGM01=1
    // Non-inverting: COM0A1=1, COM0B1=1
    TCCR0A = (1 << WGM01) | (1 << WGM00)
           | (1 << COM0A1) | (1 << COM0B1);

    // Preskaler /64 (stabilnie dla LED)
    TCCR0B = (1 << CS01) | (1 << CS00);

    // Start: OFF
    OCR0A = 0; // R
    OCR0B = 0; // G
}

6) Kod: PWM dla B (PB1) — Timer1 w 8-bit Fast PWM

Ustawiamy Fast PWM 8-bit na Timer1 (WGM10=1, WGM12=1).

#define B_DDR   DDRB
#define B_PIN   PB1   // OC1A

static void pwm_timer1_8bit_init(void)
{
    B_DDR |= (1 << B_PIN);

    // Fast PWM 8-bit: WGM10=1, WGM11=0, WGM12=1, WGM13=0
    // Non-inverting OC1A: COM1A1=1
    TCCR1A = (1 << WGM10) | (1 << COM1A1);
    TCCR1B = (1 << WGM12);

    // Preskaler /64
    TCCR1B |= (1 << CS11) | (1 << CS10);

    OCR1A = 0; // B
}
W praktyce:
Timer0 i Timer1 mogą mieć różne częstotliwości PWM. Dla LED to nie problem, byle nie było migotania.

7) Funkcja rgb_set(r,g,b)

Robimy interfejs jak w bibliotekach: podajesz 0..255 na każdy kanał.

static void rgb_set(uint8_t r, uint8_t g, uint8_t b)
{
    // wspólna katoda: wprost
    OCR0A = r; // R
    OCR0B = g; // G
    OCR1A = b; // B
}

Jeśli masz wspólną anodę

static void rgb_set_common_anode(uint8_t r, uint8_t g, uint8_t b)
{
    // wspólna anoda: odwróć
    OCR0A = 255 - r;
    OCR0B = 255 - g;
    OCR1A = 255 - b;
}

8) Test kolorów — paleta podstawowa

#include <util/delay.h>

static void demo_basic_colors(void)
{
    rgb_set(255, 0, 0);   _delay_ms(800); // czerwony
    rgb_set(0, 255, 0);   _delay_ms(800); // zielony
    rgb_set(0, 0, 255);   _delay_ms(800); // niebieski

    rgb_set(255, 255, 0); _delay_ms(800); // żółty
    rgb_set(0, 255, 255); _delay_ms(800); // cyan
    rgb_set(255, 0, 255); _delay_ms(800); // magenta

    rgb_set(255, 255, 255); _delay_ms(800); // biały (jeśli dioda pozwala)
    rgb_set(0, 0, 0);       _delay_ms(600); // OFF
}
Check: widzisz 7 kolorów sekwencyjnie. Jeśli „biały” jest różowy/niebieski — kanały mają różną jasność (to normalne).

9) Sceny świetlne: fade pojedynczego kanału i mieszanie

Zrobimy dwa efekty:

  • fade czerwonego kanału,
  • płynne przejście kolorów (prosty „color wheel”).

Fade czerwieni

static void fade_red(void)
{
    for (uint16_t v = 0; v <= 255; v++) { rgb_set(v, 0, 0); _delay_ms(4); }
    for (int16_t v = 255; v >= 0; v--) { rgb_set(v, 0, 0); _delay_ms(4); }
}

Prosty „color wheel” (3 fazy)

static void color_wheel_simple(void)
{
    // R -> G
    for (uint16_t i = 0; i <= 255; i++) { rgb_set(255 - i, i, 0); _delay_ms(4); }
    // G -> B
    for (uint16_t i = 0; i <= 255; i++) { rgb_set(0, 255 - i, i); _delay_ms(4); }
    // B -> R
    for (uint16_t i = 0; i <= 255; i++) { rgb_set(i, 0, 255 - i); _delay_ms(4); }
}
Wada:
Używamy _delay_ms w pętlach. W następnych krokach połączysz to z timerami (lekcja 13) dla płynności bez blokowania.

10) Sterowanie trybami RGB przyciskiem (maszyna stanów)

Łączymy z lekcją 11: klik zmienia tryb sceny.

typedef enum {
    SCENE_OFF = 0,
    SCENE_BASIC,
    SCENE_FADE_RED,
    SCENE_WHEEL,
    SCENE_MAX
} scene_t;

W pętli:

scene_t scene = SCENE_OFF;

if (button_click())
{
    scene++;
    if (scene >= SCENE_MAX) scene = SCENE_OFF;
}

I obsługa:

switch (scene)
{
    case SCENE_OFF:
        rgb_set(0,0,0);
        _delay_ms(20);
        break;

    case SCENE_BASIC:
        demo_basic_colors();
        break;

    case SCENE_FADE_RED:
        fade_red();
        break;

    case SCENE_WHEEL:
        color_wheel_simple();
        break;

    default:
        scene = SCENE_OFF;
        break;
}

11) Pełny program lekcji (gotowy do wgrania)

#include <avr/io.h>
#include <util/delay.h>

#define R_PIN   PD6   // OC0A
#define G_PIN   PD5   // OC0B
#define B_PIN   PB1   // OC1A (8-bit PWM)

#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 pwm_timer0_init(void)
{
    DDRD |= (1 << R_PIN) | (1 << G_PIN);

    TCCR0A = (1 << WGM01) | (1 << WGM00)
           | (1 << COM0A1) | (1 << COM0B1);

    TCCR0B = (1 << CS01) | (1 << CS00); // /64

    OCR0A = 0;
    OCR0B = 0;
}

static void pwm_timer1_8bit_init(void)
{
    DDRB |= (1 << B_PIN);

    TCCR1A = (1 << WGM10) | (1 << COM1A1);
    TCCR1B = (1 << WGM12);

    TCCR1B |= (1 << CS11) | (1 << CS10); // /64

    OCR1A = 0;
}

static void rgb_set(uint8_t r, uint8_t g, uint8_t b)
{
    OCR0A = r;
    OCR0B = g;
    OCR1A = b;
}

static void demo_basic_colors(void)
{
    rgb_set(255, 0, 0);     _delay_ms(600);
    rgb_set(0, 255, 0);     _delay_ms(600);
    rgb_set(0, 0, 255);     _delay_ms(600);
    rgb_set(255, 255, 0);   _delay_ms(600);
    rgb_set(0, 255, 255);   _delay_ms(600);
    rgb_set(255, 0, 255);   _delay_ms(600);
    rgb_set(255, 255, 255); _delay_ms(600);
    rgb_set(0, 0, 0);       _delay_ms(400);
}

static void fade_red(void)
{
    for (uint16_t v = 0; v <= 255; v++) { rgb_set(v, 0, 0); _delay_ms(4); }
    for (int16_t v = 255; v >= 0; v--) { rgb_set(v, 0, 0); _delay_ms(4); }
}

static void color_wheel_simple(void)
{
    for (uint16_t i = 0; i <= 255; i++) { rgb_set(255 - i, i, 0); _delay_ms(4); }
    for (uint16_t i = 0; i <= 255; i++) { rgb_set(0, 255 - i, i); _delay_ms(4); }
    for (uint16_t i = 0; i <= 255; i++) { rgb_set(i, 0, 255 - i); _delay_ms(4); }
}

typedef enum {
    SCENE_OFF = 0,
    SCENE_BASIC,
    SCENE_FADE_RED,
    SCENE_WHEEL,
    SCENE_MAX
} scene_t;

int main(void)
{
    pwm_timer0_init();
    pwm_timer1_8bit_init();

    BTN_DDR &= ~(1 << BTN_PIN);
    BTN_PORT |= (1 << BTN_PIN);

    scene_t scene = SCENE_OFF;

    while (1)
    {
        if (button_click())
        {
            scene++;
            if (scene >= SCENE_MAX) scene = SCENE_OFF;
        }

        switch (scene)
        {
            case SCENE_OFF:
                rgb_set(0,0,0);
                _delay_ms(20);
                break;

            case SCENE_BASIC:
                demo_basic_colors();
                break;

            case SCENE_FADE_RED:
                fade_red();
                break;

            case SCENE_WHEEL:
                color_wheel_simple();
                break;

            default:
                scene = SCENE_OFF;
                break;
        }
    }
}

12) Zadania obowiązkowe

  1. Podłącz RGB do pinów PWM i uruchom paletę 7 kolorów.
  2. Zrób funkcję rgb_set(r,g,b) i użyj jej we wszystkich efektach.
  3. Zrób 3 sceny i przełączaj je przyciskiem (OFF / BASIC / WHEEL).

13) Zadania dodatkowe (dla lepszych)

  1. Zrób 10 „zapisanych” kolorów w tablicy i przewijaj je klikami.
  2. Zrób „oddech” białego światła (RGB razem).
  3. Zrób „sygnalizację stanu”: zielony=OK, żółty=warning, czerwony=error (i zmieniaj przyciskiem).

14) Zadania PRO (projektowe)

  1. Zrób efekt bez blokowania: użyj tick 1ms z lekcji 13 i zmieniaj kolory „co X ms”.
  2. Zrób korekcję jasności (pseudo-gamma) dla każdego kanału (tablica 16/32 wartości).
  3. Dodaj tryb „audio-reactive”: zmiana koloru zależna od tonu z lekcji 15 (manualnie, bez ADC na razie).
Gotowe:
Masz RGB jako moduł, który można wkleić do każdego projektu (status LED, efekty, UI).