🔟 Lekcja 21: ADC — pomiar napięcia

Konfiguracja ADC, kanały, referencja, odczyt i interpretacja. W tej lekcji mierzysz napięcie (np. z potencjometru) i zamieniasz wynik na wartości użyteczne w programie.

1) Cel lekcji

ADC (Analog-to-Digital Converter) w ATmega328P zamienia napięcie analogowe na liczbę. Dzięki temu możesz mierzyć: potencjometr, czujniki analogowe, baterię (po dzielniku), sygnały z układów audio.

  • poznasz podstawowe pojęcia: rozdzielczość 10-bit, Vref, kanał, preskaler,
  • ustawisz ADC w trybie pojedynczego pomiaru,
  • zrobisz funkcję adc_read(channel),
  • policzysz napięcie w voltach (lub mV),
  • wykorzystasz ADC w praktyce (np. regulacja jasności PWM, próg alarmu, mapa 0..9999 na 7-seg).
Po tej lekcji: potrafisz czytać ADC poprawnie i świadomie interpretować wynik.

2) Rozdzielczość i wzór na napięcie

ADC w ATmega328P jest 10-bitowy, czyli wynik jest w zakresie 0..1023.

Wzór:
V_in = (ADC / 1023) * V_ref

Praktycznie (w mV):
V_in_mV = (ADC * Vref_mV) / 1023
Uwaga:
Dokładność zależy od jakości Vref i zakłóceń (zasilanie, masa, filtracja).

3) Kanały ADC na ATmega328P (skrót)

ADC ma kanały analogowe (ADC0..ADC5) dostępne zwykle na pinach portu C:

Kanał ADCPin MCUTypowe oznaczenie na płytkach
ADC0PC0A0
ADC1PC1A1
ADC2PC2A2
ADC3PC3A3
ADC4PC4A4 (SDA)
ADC5PC5A5 (SCL)
Uwaga projektowa:
Jeśli PC0..PC3 używasz do wyboru cyfr 7-seg (CA1..CA4), to do ADC wybierz np. PC4/PC5 (ADC4/ADC5), albo w tym ćwiczeniu odłącz wyświetlacz i skup się na ADC. Najważniejsze jest zrozumienie konfiguracji i odczytu.

4) Rejestry ADC — minimum, które musisz znać

ADMUX
  • REFS1:0 — wybór Vref
  • ADLAR — wyrównanie wyniku
  • MUX[3:0] — wybór kanału
ADCSRA
  • ADEN — włączenie ADC
  • ADSC — start pomiaru
  • ADIF — flaga końca
  • ADPS[2:0] — preskaler
ADCL/ADCH — rejestry z wynikiem 10-bit.
Czytasz zawsze najpierw ADCL, potem ADCH.

5) Vref — co wybrać i dlaczego

Najczęściej masz trzy opcje (w zależności od sprzętu):

  • AVCC jako referencja (typowo 5V) — prosto, ale zależy od wahań zasilania
  • wewnętrzna 1.1V — lepsza stabilność dla małych napięć (wymaga dzielnika/podłączenia pod zakres)
  • AREF (zewnętrzna) — najlepsza, jeśli masz precyzyjne źródło odniesienia
W tej lekcji:
użyjemy AVCC jako Vref (najprościej). Jeśli zasilanie jest 5V, ADC 1023 ≈ 5V.

6) Preskaler ADC — żeby wynik był poprawny

ADC musi mieć zegar w rozsądnym zakresie (typowo ok. 50–200 kHz). Przy F_CPU=8 MHz dobry preskaler to 64 → 125 kHz.

Przykład (F_CPU=8MHz):
preskaler 64 → ADPS2=1, ADPS1=1, ADPS0=0

7) Kod: inicjalizacja ADC i odczyt kanału (single conversion)

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

static void adc_init_avcc(void)
{
    // Vref = AVCC, wynik wyrównany do prawej (ADLAR=0)
    ADMUX = (1<<REFS0);

    // włącz ADC, preskaler 64 (dla 8MHz daje ok. 125kHz)
    ADCSRA = (1<<ADEN) | (1<<ADPS2) | (1<<ADPS1);

    // opcjonalnie: "dummy read" po włączeniu
    ADCSRA |= (1<<ADSC);
    while (ADCSRA & (1<<ADSC)) { }
    (void)ADC;
}

static uint16_t adc_read(uint8_t channel)
{
    channel &= 0x07; // ADC0..ADC7 (my używamy 0..5)
    // zachowaj REFS, wyczyść MUX i ustaw nowy kanał
    ADMUX = (ADMUX & 0xF0) | channel;

    // start pomiaru
    ADCSRA |= (1<<ADSC);

    // czekaj aż skończy (ADSC spada do 0)
    while (ADCSRA & (1<<ADSC)) { }

    // odczyt 10-bit
    return (uint16_t)ADC;
}
Minimalny poprawny odczyt ADC:
ustaw Vref, preskaler, wybierz kanał, start, czekaj, odczytaj ADC.

8) Przeliczanie na napięcie (mV) i interpretacja

static uint16_t adc_to_mV(uint16_t adc_val, uint16_t vref_mV)
{
    // (adc * vref) / 1023
    // używamy uint32_t aby uniknąć przepełnienia
    uint32_t tmp = (uint32_t)adc_val * (uint32_t)vref_mV;
    return (uint16_t)(tmp / 1023UL);
}

Jeśli Vref=5000 mV:

  • adc=0 → ~0 mV
  • adc=512 → ~2500 mV
  • adc=1023 → ~5000 mV

9) Praktyka 1: ADC → PWM jasności LED (najbardziej czytelne ćwiczenie)

Odczytujesz potencjometr (np. ADC4) i ustawiasz PWM (np. na PD6/OC0A). Dzięki temu widać „analogowy efekt” od razu.

Uwaga:
Jeśli w Twoim projekcie Timer0 jest zajęty przez tick/multipleks, PWM na Timer0 będzie kolidować. Wtedy ustaw PWM na Timer1 (OC1A PB1) albo zrób test ADC bez PWM (np. mapuj na 7-seg).

Wariant A: PWM na Timer1 (bez konfliktu z Timer0 tick)

// PWM 8-bit na OC1A (PB1) — jak w lekcji 16
static void pwm_timer1_8bit_init_oc1a(void)
{
    DDRB |= (1<<PB1); // OC1A

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

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

    OCR1A = 0;
}

static uint8_t adc10_to_pwm8(uint16_t adc_val)
{
    // 0..1023 -> 0..255
    return (uint8_t)(adc_val >> 2);
}

Main: ADC4 steruje jasnością

#include <util/delay.h>

int main(void)
{
    adc_init_avcc();
    pwm_timer1_8bit_init_oc1a();

    while (1)
    {
        uint16_t a = adc_read(4);     // ADC4 = PC4 (A4)
        uint8_t  p = adc10_to_pwm8(a);

        OCR1A = p; // jasność LED na OC1A

        _delay_ms(10);
    }
}

10) Praktyka 2: ADC → liczba na 7-seg (0..9999)

To jest wariant „w Twoim stylu” z wcześniejszych lekcji: mierzysz ADC i wyświetlasz wynik jako liczba.

Skala:

  • 0..1023 → 0..9999 (proporcja)
static uint16_t map_adc_to_9999(uint16_t adc_val)
{
    // (adc * 9999) / 1023
    uint32_t tmp = (uint32_t)adc_val * 9999UL;
    return (uint16_t)(tmp / 1023UL);
}
W praktyce:
Jeśli masz już silnik multipleksu (lekcja 18/19), w main robisz: val = map_adc_to_9999(adc_read(ch)) i wrzucasz do bufora wyświetlacza.

11) Typowe błędy w ADC (i szybkie naprawy)

Wynik „skacze”
  • zakłócenia na wejściu
  • brak kondensatora na AREF/AVCC
  • długie przewody do potencjometru
Wynik stale 0 lub 1023
  • źle podłączony suwak potencjometru
  • pin ustawiony jako wyjście
  • brak masy wspólnej
Najprostszy filtr:
średnia z N próbek (np. 8) — bardzo poprawia stabilność.

Filtr: średnia z 8 próbek

static uint16_t adc_read_avg8(uint8_t channel)
{
    uint32_t sum = 0;
    for (uint8_t i = 0; i < 8; i++)
        sum += adc_read(channel);

    return (uint16_t)(sum / 8);
}

12) Zadania obowiązkowe

  1. Zrób odczyt ADC z kanału (np. ADC4) i mapuj 0..1023 na 0..9999. Wyświetl na 7-seg.
  2. Dodaj filtr średniej z 8 próbek i porównaj stabilność wskazań.
  3. Zamiast mapowania na 9999 przelicz na mV (Vref=5000 mV) i wyświetl „napięcie” w mV (np. 2500).

13) Zadania dodatkowe (dla lepszych)

  1. Zrób „bargraph”: wynik ADC dziel na 10 poziomów i pokazuj 0..9 na 7-seg jako „poziom”.
  2. Zrób próg alarmu: jeśli ADC > próg, zapal LED / włącz brzęczyk.
  3. Zrób dwie referencje: porównaj wyniki przy AVCC vs wewnętrznej 1.1V (dla małego napięcia).

14) Zadania PRO (projektowe)

  1. Zrób automatyczną regulację jasności wyświetlacza 7-seg (PWM lub czas świecenia) w zależności od ADC (np. fotorezystor).
  2. Zrób pomiar baterii przez dzielnik napięcia: przelicz na rzeczywiste Vbat (uwzględnij R1/R2).
  3. Przełączanie kanałów: co 200 ms czytaj inny kanał i wyświetlaj go w trybie „A0/A1/A2” (własne znaki).
Zapowiedź:
Następny krok to ADC w trybie auto-trigger / przerwania ADC oraz bardziej „ciągłe” próbkowanie (np. do audio/analizy sygnału).