⏱️ Lekcja 13: Timery — opóźnienia bez _delay_ms()

Timer0/Timer1, preskalery, porównanie (CTC), odmierzanie czasu. To jest lekcja, w której przechodzisz z „programów szkolnych” na podejście firmware.

1) Cel lekcji

Do tej pory korzystałeś z _delay_ms(), które jest proste, ale blokuje program. W embedded to problem, bo gdy CPU „śpi” w opóźnieniu, nie reaguje szybko na przyciski, czujniki i zdarzenia.

  • poznasz zasadę działania timerów (sprzętowe liczniki),
  • nauczysz się ustawiać preskaler i tryb CTC,
  • zrobisz dokładne opóźnienia bez blokowania pętli głównej,
  • zrobisz „system tick” (np. 1 ms), który jest podstawą większości projektów.
Po tej lekcji: potrafisz zbudować czasomierz i wykonywać akcje „co X ms” bez użycia _delay_ms().

2) Co to jest timer w AVR?

Timer to sprzętowy licznik, który zlicza impulsy zegara. Gdy osiągnie wartość graniczną, może:

  • ustawić flagę (TOV/OCF),
  • wywołać przerwanie (ISR),
  • wyzerować się i liczyć od nowa.
ATmega328P — najważniejsze timery
  • Timer0 — 8-bit (0..255),
  • Timer1 — 16-bit (0..65535),
  • Timer2 — 8-bit (często do PWM/audio).
Typowe zastosowania
  • odmierzanie czasu (tick),
  • PWM (silniki, LED),
  • pomiar czasu (input capture),
  • generowanie częstotliwości.

3) Preskaler — po co i jak działa?

Timer liczy „takt” z zegara systemowego, ale możesz go spowolnić preskalerem. Przykładowo preskaler 64 oznacza, że timer dostaje 1 impuls na 64 impulsy CPU.

Wzór:
f_timer = f_cpu / prescaler
czas_1_tiku = 1 / f_timer

Jeśli masz F_CPU = 8 MHz i preskaler 64:

  • f_timer = 8 000 000 / 64 = 125 000 Hz
  • 1 tik = 1 / 125 000 = 8 µs
Uwaga: w kursie możesz mieć 8 MHz (RC) lub 16 MHz (kwarc). Timery będą działać zawsze, ale wartości OCR trzeba dobierać do F_CPU.

4) Tryb CTC — najwygodniejszy do „odmierzania”

CTC (Clear Timer on Compare Match) działa tak:

  • timer liczy od 0 do wartości w rejestrze OCR,
  • gdy osiągnie OCR → ustawia flagę porównania, może wywołać przerwanie,
  • automatycznie wraca do 0 i liczy dalej.
CTC = idealny do „co X ms” Możesz ustawić timer, aby dawał zdarzenie dokładnie co 1 ms, 10 ms, 100 ms itd.

5) Timer0 w CTC: zdarzenie co 1 ms (bez przerwań — polling)

Najpierw wersja najprostsza: nie używamy ISR, tylko sprawdzamy flagę porównania w pętli. To już usuwa _delay_ms() jako blokadę.

Założenia do obliczeń

  • F_CPU = 8 MHz (jeśli masz inaczej — podam też wariant dla 16 MHz)
  • Preskaler = 64
  • 1 tik = 8 µs
  • 1 ms = 1000 µs → potrzebujesz 125 tików
  • OCR0A = 124 (bo liczenie od 0)
#include <avr/io.h>

#define LED_DDR   DDRB
#define LED_PORT  PORTB
#define LED_PIN   PB0

static void timer0_ctc_1ms_init(void)
{
    // CTC: WGM01=1, WGM00=0
    TCCR0A = (1 << WGM01);

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

    // Compare Match co 1ms dla F_CPU=8MHz i preskaler=64
    OCR0A = 124;

    // wyczyść flagę porównania
    TIFR0 = (1 << OCF0A);
}

static inline uint8_t timer0_1ms_elapsed(void)
{
    if (TIFR0 & (1 << OCF0A))
    {
        TIFR0 = (1 << OCF0A); // skasuj flagę (piszesz 1!)
        return 1;
    }
    return 0;
}

int main(void)
{
    LED_DDR |= (1 << LED_PIN);
    timer0_ctc_1ms_init();

    uint16_t ms = 0;

    while (1)
    {
        if (timer0_1ms_elapsed())
        {
            ms++;

            // co 500 ms przełącz LED
            if (ms % 500 == 0)
            {
                LED_PORT ^= (1 << LED_PIN);
            }
        }

        // TU CPU jest wolny — możesz robić inne rzeczy (przycisk, UART, itp.)
    }
}
Check: LED na PB0 miga co 500 ms, ale program nie jest blokowany opóźnieniami.

6) Wariant dla F_CPU = 16 MHz (jeśli masz kwarc 16 MHz)

Dla 16 MHz i preskalera 64:

  • f_timer = 16 000 000 / 64 = 250 000 Hz
  • 1 tik = 4 µs
  • 1 ms = 250 tików
  • OCR0A = 249
Jeśli masz 16 MHz: ustaw OCR0A = 249.

7) System tick i harmonogram „co X ms”

Mając tick 1 ms możesz tworzyć proste „zadania” w pętli:

// przykładowe interwały
// LED1: co 200 ms
// LED2: co 500 ms
// BUZZ: co 1000 ms (pik)

uint16_t t200 = 0, t500 = 0, t1000 = 0;

if (timer0_1ms_elapsed())
{
    t200++;
    t500++;
    t1000++;

    if (t200 >= 200) { t200 = 0; /* akcja 200ms */ }
    if (t500 >= 500) { t500 = 0; /* akcja 500ms */ }
    if (t1000 >= 1000) { t1000 = 0; /* akcja 1000ms */ }
}
To jest mini-scheduler.
Tak działa mnóstwo prostych urządzeń: nie RTOS, tylko tick + liczniki.

8) Timer1: kiedy go używać?

Timer0 jest 8-bit i często bywa zajęty (np. pod PWM). Timer1 jest 16-bit i daje większy zakres, dokładność i wygodę w dłuższych czasach.

Praktyczna zasada:
Timer0 — tick/scheduler, Timer1 — dokładniejsze pomiary i dłuższe okresy, Timer2 — PWM/audio.

9) Zadania obowiązkowe

  1. Uruchom Timer0 CTC 1ms i zrób miganie LED co 250 ms.
  2. Dodaj drugi licznik i zrób „heartbeat”: krótkie mignięcie co 1000 ms.
  3. Połącz z lekcją 10: odczyt przycisku ma działać „od razu” nawet podczas migania.

10) Zadania dodatkowe (dla lepszych)

  1. Zrób 3 tryby migania LED (200/500/1000 ms) przełączane przyciskiem — bez _delay_ms().
  2. Zrób „debounce timerowy”: przycisk uznaj za stabilny, jeśli jest wciśnięty przez 20 ms (z ticka).
  3. Zrób wzorzec z lekcji 9 (4 LED) sterowany schedulerem.

11) Zadania PRO (prawie firmware)

  1. Zrób strukturę „tasków”: funkcje task_1ms(), task_10ms(), task_100ms().
  2. Zrób licznik czasu uptime_ms i wypisz go przez UART (jeśli masz w kursie) albo sygnalizuj LED.
  3. Dodaj timeout: jeśli przez 5 s nie było kliknięcia przycisku, LED przechodzi w tryb oszczędny (np. wolne miganie).
Zapowiedź:
Następny krok to przerwania (ISR) i tick w ISR — wtedy pętla główna jest jeszcze czystsza.