🧠 Lekcja 5: Podstawy języka C dla AVR (szczegółowo)

Składnia C, struktura programu embedded, typy danych, operatory, instrukcje sterujące i dobre praktyki pod mikrokontrolery.

1) Jak wygląda program w C na mikrokontroler?

Program w C dla AVR to zwykły plik .c, który kompilujemy do kodu maszynowego i wgrywamy do pamięci FLASH. W odróżnieniu od aplikacji na PC, program zwykle działa w nieskończonej pętli i steruje sprzętem przez rejestry.

Model działania embedded Urządzenie ma działać cały czas: po starcie wykonuje inicjalizację, a potem bez końca reaguje na wejścia i steruje wyjściami.

Minimalny „szkielet” programu

#include <avr/io.h>

int main(void)
{
    // 1) Inicjalizacja (ustawienia portów, timerów, itp.)

    while (1)
    {
        // 2) Pętla główna (logika programu)
    }
}

W kolejnych lekcjach dojdą biblioteki np. <util/delay.h>, a w lekcjach timerów – przerwania.

2) Składnia podstawowa: instrukcje, bloki, komentarze

  • Instrukcje w C kończą się średnikiem: ;
  • Blok kodu jest w klamrach: { ... }
  • Komentarz jednoliniowy: // ...
  • Komentarz wieloliniowy: /* ... */
// To jest komentarz jednoliniowy

/*
   To jest komentarz wieloliniowy
*/

int a = 5;          // instrukcja
a = a + 1;          // instrukcja
if (a > 5) {        // blok
    a = 0;
}

3) Zmienne i typy danych (pod AVR)

Mikrokontroler ma ograniczoną pamięć SRAM, więc typy danych dobiera się świadomie. W embedded częściej używa się typów o znanym rozmiarze (np. 8-bit, 16-bit).

Typ Typowy rozmiar na AVR Zastosowanie
uint8_t 8 bit (0–255) bity, porty, małe liczniki
int8_t 8 bit (-128–127) małe wartości ze znakiem
uint16_t 16 bit (0–65535) liczniki czasu, ADC, większe zakresy
int zwykle 16 bit ogólne obliczenia (ale uważaj na zakres)
float 32 bit (wolne) raczej unikać na start, kosztowne obliczeniowo
Ważne w embedded Jeśli możesz, używaj uint8_t/uint16_t zamiast int. To ułatwia kontrolę rozmiaru i zużycia pamięci.

Niezbędny nagłówek do typów stałej szerokości

#include <stdint.h>

uint8_t  a = 10;
uint16_t b = 1000;

4) Stałe: #define vs const

#define (makro preprocesora)

#define LED_PIN PB0
#define DELAY_MS 500

const (stała w C)

const uint16_t delay_ms = 500;
Praktyka kursowa Do numerów pinów i masek bitowych często stosuje się #define. Do wartości „logiczych” (np. czas) możesz użyć const.

5) Operatory – szczególnie bitowe (kluczowe w AVR)

Operatory arytmetyczne

  • +, -, *, /, %

Operatory porównania

  • ==, !=, >, <, >=, <=

Operatory logiczne

  • && (AND), || (OR), ! (NOT)

Operatory bitowe (najważniejsze)

  • & AND bitowy
  • | OR bitowy
  • ^ XOR bitowy
  • ~ negacja bitowa
  • <<, >> przesunięcia
Dlaczego bitowe są kluczowe? Rejestry sprzętowe to bity. Ustawiasz lub kasujesz konkretne bity, aby włączyć/wyłączyć funkcje.

Przykład: tworzenie maski bitu

// (1 << n) tworzy maskę: tylko bit n ustawiony na 1
uint8_t maska0 = (1 << 0); // 0000 0001
uint8_t maska3 = (1 << 3); // 0000 1000

Ustawianie bitu (SET)

PORTB |= (1 << PB0); // ustaw bit PB0 na 1

Kasowanie bitu (CLEAR)

PORTB &= ~(1 << PB0); // wyzeruj bit PB0

Sprawdzanie bitu (READ)

if (PINB & (1 << PB0)) {
    // bit PB0 jest 1
}

Przełączanie bitu (TOGGLE)

PORTB ^= (1 << PB0); // zmień stan PB0 na przeciwny

6) Instrukcje sterujące: if/else, switch, pętle

if / else

if (warunek) {
    // jeśli prawda
} else {
    // jeśli fałsz
}

switch (gdy jest wiele przypadków)

switch (tryb) {
  case 0:
    // tryb 0
    break;
  case 1:
    // tryb 1
    break;
  default:
    // inne
    break;
}

while (najczęściej w embedded)

while (1) {
    // pętla nieskończona
}

for (gdy powtarzasz N razy)

for (uint8_t i = 0; i < 10; i++) {
    // 10 powtórzeń
}
Uwaga Na AVR unikaj długich pętli „na czekanie” – docelowo przechodzimy na timery i przerwania.

7) Funkcje – porządek w kodzie

Funkcje pozwalają dzielić program na logiczne części: inicjalizacja, obsługa LED, obsługa przycisku itd.

void init(void)
{
    // konfiguracja sprzętu
}

void led_on(void)
{
    PORTB |= (1 << PB0);
}

int main(void)
{
    init();

    while (1) {
        led_on();
    }
}

Dlaczego warto?

  • kod jest czytelniejszy,
  • łatwiej go rozwijać,
  • mniej błędów przy rozbudowie projektu.

8) volatile – kiedy jest potrzebne?

Słowo kluczowe volatile mówi kompilatorowi: „ta zmienna może zmieniać się poza normalnym kodem (np. w przerwaniu) – nie optymalizuj jej”.

volatile uint8_t flaga = 0;
Kiedy użyjesz volatile? Gdy zaczniesz używać przerwań (ISR). Na razie zapamiętaj pojęcie – wrócimy do niego.

9) Pełny przykład: „szkielet projektu” pod AVR

To jest wzorcowy układ kodu, który będziesz powtarzać w praktycznych lekcjach. Na razie to przykład struktury (bez zależności od konkretnych pinów).

#include <avr/io.h>
#include <stdint.h>

static void init_io(void)
{
    // Tu w przyszłości ustawisz kierunki pinów (DDRx) i stany startowe (PORTx)
}

static void app_loop(void)
{
    // Tu będzie główna logika aplikacji
}

int main(void)
{
    init_io();

    while (1)
    {
        app_loop();
    }
}
Co umiesz po tej lekcji? Rozumiesz, jak buduje się program w C pod AVR, znasz podstawowe typy danych, instrukcje sterujące oraz operacje bitowe potrzebne do pracy z rejestrami.

10) Szybkie sprawdzenie (pytania)

  1. Dlaczego w AVR używa się operatorów bitowych?
  2. Jak ustawić bit PB0 w rejestrze PORTB?
  3. Co robi PORTB &= ~(1 << PB0);?
  4. Czym się różni && od &?
  5. Dlaczego program embedded zwykle ma while(1)?