Kurs esp32/esp8266 | #6 – ADC – przetwornik analogowo-cyfrowy

ADC – przetwornik analogowo cyfrowy | esp-idf

Przetwornik analogowo-cyfrowy to układ, który sygnał analogowy zamienia na sygnał cyfrowy, robi to porównując napięcie podane na jego wyprowadznie do napięcia odniesienia. W większości mikrokontrolerów znajduje się kilka takich wbudowanych przetworników, w esp32 mamy dwa 12-bitowe przetworniki, które mają 8 i 10 kanałów. Natomiast w esp8266 mamy zaledwie jeden 1-kanałowy 10-bitowy przetwornik.

Bitowość można określić jako dokładność. Maksymalna osiągalna liczba 12-bitowa to 4095, i w zakresie właśnie 0-4095 będzie podawany nam wynik z porównania napięcia podanego, do napięcia odniesienia. Przykładowo jeśli mielibyśmy przetwornik 16-bitowy to jego zakres wynosiłby 0-65535. Także widać tutaj różnicę, większa bitowość = większa dokładność, mocno to widać chociażby rejestrując dźwięk z mikrofonu elektrolitycznego.

Do czego może nam się przydać ADC? Na przykład do wyżej wymienionego, rejestrowania dźwięku za pomocą mikrofonu elektrolitycznego, ale także do mierzenia napięcia – przykładowo baterii, by sprawdzać jej stan naładowania. Czy, żeby podłączyć fotorezystor i mierzyć natężenie światła otoczenia.


Obsługa adc w środowiskach esp

Na płytkach prototypowych często znajduje się wbudowany dzielnik napięcia, który pozwala podłączyć napięcie do 3.3V.

Warto na wstępie zaznaczyć, że układy ADC znajdujące się w mikrokontrolerach nie są najlepszym rozwiązaniem jeśli zależy nam na jak najlepszej precyzji. Na rynku bez problemu znaleźć można zewnętrzne przetworniki analogowo-cyfrowe oferujące większą stabilność oraz zakres.


esp32

Jak wspomniałem esp32 ma dwa przetworniki – ADC1 i ADC2, z czego pierwszy posiada 8 kanałów znajdujących się na pinach GPIO32-GPIO39, natomiast drugi 10 kanałów na pinach: GPIO0, GPIO2, GPIO4, GPIO12 – GPIO15, GPIO25 – GPIO27.

Dokumentacja podaje jednak, że ADC2 jest także używany przez moduł wi-fi, więc nie możemy z niego korzystać jeżeli w projekcie będziemy używali wi-fi.

Domyślne napięcie referencyjne (odniesienia) w esp to 1.1-1.2 V. Jeżeli na pin adc podamy większe napięcie niż napięcie odniesienia, to przetwornik zwróci nam maksymalną możliwą liczbę (4095). Ważne by nie podać na pin ADC napięcia większego niż napięcie 2.5V – możemy wtedy uszkodzić układ ADC.

Poniżej przedstawię prosty program, który będzie odczytywał napięcie z dzielnika rezystorowego. Napięciem źródłowym będzie 3.3V, natomiast napięcie wyjściowe tego dzielnika mierzone multimetrem wynosi około 730mV. Nie wspomniałem, że biblioteka obsługująca ADC oferuje nam możliwość automatycznego obliczenia podanego napięcia – to właśnie postaramy się wykorzystać.

Na początek importujemy bibliotekę:

#include "driver/adc.h"

W funkcji app_main wywołujemy funkcje:

adc1_config_width( ADC_WIDTH_BIT_12 );

Jak można wywnioskować korzystamy z przetwornika ADC1, a powyższą funkcją ustawiamy z jaką „bitowością” zbieramy odczyt. Możemy wybrać z ADC_WIDTH_BIT_9 – ADC_WIDTH_BIT_12.

Teraz zapiszemy program, który co 3 sekundy będzie wyświetlał wartość odczytu w zakresie 0-4095.

while (1)
{
    vTaskDelay( 3000 / portTICK_RATE_MS);
    uint16_t value = adc1_get_raw(ADC1_CHANNEL_5); // badam wartość na pinie ADC5
    printf(„odczyt: %d\n”, value);
}

W moim przypadku na terminalu wyświetla się liczba około 3440, z dość sporymi wachaniami. Jak podaje producent, by zniwelować wachania możemu podłączyć kondnsator 0.1uF do nóżki adc oraz do GND. Ponadto powinniśmy wykonać więcej pomiarów, a następnie je uśrednić. Zrobimy to modyfikując kod w następujący sposób:

while (1)
{
    uint32_t odczyt = 0;
    vTaskDelay( 3000 / portTICK_RATE_MS);
    for (uint8_t i=0; i<64; i++)
    {
        uint16_t value = adc1_get_raw(ADC1_CHANNEL_5); // badam wartość na pinie ADC5
        odczyt += value;	
    }
    odczyt = odczyt/64;
    printf(„odczyt: %d\n”, odczyt);
}

Po tych operacjach odczytywane dane są znacznie bardziej zbliżone do siebie więc zamieńmy je na wartości w mV.
Do tego musimy zaimportować plik nagłówkowy odpowiadający za kalibrację ADC:

#include "esp_adc_cal.h"

A przed pętlą główną dopisujemy dwa polecenia:

esp_adc_cal_characteristics_t adc_cal;
esp_adc_cal_characterize( ADC_UNIT_1, ADC_ATTEN_DB_0, ADC_WIDTH_BIT_12, 1100, &adc_cal);

Pierwszym poleceniem zadeklarowaliśmy strukturę typu „esp_adc_cal_characteristics_t”, a kolejnym ją wypełniliśmy. Przekazaliśmy funkcji kolejno, makrodefinicję ADC1, „osłabienie” badanego sygnału, szerokość w bitach, napięcie odniesienia (mV), oraz wskaźnik struktury.

Do pętli dodamy tylko dwa polecenia – jedno odpowiedzialne za zamianę wartości z zakresu 0-4095 na mV i polecenie printf by to wyświetlać.

while (1)
{
    uint32_t odczyt = 0;
    vTaskDelay( 3000 / portTICK_RATE_MS);
    for (uint8_t i=0; i<64; i++)
    {
        uint16_t value = adc1_get_raw(ADC1_CHANNEL_5); // badam wartość na pinie ADC5
        odczyt += value;	
    }
    odczyt = odczyt/64;
    uint16_t voltage = esp_adc_calc_raw_to_voltage(odczyt, &adc_cal);
    printf(„odczyt: %d\n”, odczyt);
    printf(„mv: %d\n”, voltage);
}

Coś jest jednak nie tak, ponieważ wartość voltage w moim przypadku to około 900mV, a powinno być ~730. Spróbujmy więc zmienić wartość zmiennej „attenuation”, która osłabia wprowadzany sygnał by móc mierzyć większe napięcia. W dokumentacji znajdziemy taką rozpiskę:

+----------+-------------+-----------------+
|          | attenuation | suggested range |
|    SoC   |     (dB)    |      (mV)       |
+==========+=============+=================+
|          |       0     |    100 ~  950   |
|          +-------------+-----------------+
|          |       2.5   |    100 ~ 1250   |
|   ESP32  +-------------+-----------------+
|          |       6     |    150 ~ 1750   |
|          +-------------+-----------------+
|          |      11     |    150 ~ 2450   |
+----------+-------------+-----------------+
|          |       0     |      0 ~  750   |
|          +-------------+-----------------+
|          |       2.5   |      0 ~ 1050   |
| ESP32-S2 +-------------+-----------------+
|          |       6     |      0 ~ 1300   |
|          +-------------+-----------------+
|          |      11     |      0 ~ 2500   |
+----------+-------------+-----------------+

Domyślnie ustawiona jest wartość 0 i 730mV znajduję się przecież w przedziale 100 – 950, jednak niestety nie wiem czemu wynik wyszedł zakłamany.

Zmieńmy więc wartość attenuation na 2.5 takim poleceniem:

adc1_config_channel_atten( ADC1_CHANNEL_5, ADC_ATTEN_DB_2_5);

Trzeba tę wartość także podmienić we wcześniejszej funkcji „esp_adc_cal_characterize”.

Po tej operacji odczyt jest prawie idealny. Dla jeszcze lepszej dokładności moglibyśmy pomanewrować wartością napięcia odniesienia w funkcji „ esp_adc_cal_characterize”, gdyż ta może się mieścić w przedziale 1000 – 1200 mV.

esp32 hall sensor – czujnik Halla

Wspomnę tutaj także o czujniku Halla, który jest wbudowany w mikrokontroler esp32. Po zaimportowaniu biblioteki „adc.h” możemy z niego w prosty sposób korzystać za pomocą polecenia „hall_sensor_read”, który zwraca wartość w zakresie 0-4095.

uint16_t val = hall_sensor_read();
printf(„%d”, val);

esp8266

Zajmniemy się teraz obsługą przetwornika ADC na esp8266. Skoro ten, ma tylko jeden 1-kanałowy taki układ, nie jest to wielce skomplikowane. Załączmy więc bibliotekę…

#include "driver/adc.h"

Następnie powołajmy strukturę typu „adc_config_t”, dzięki której skonfigurujemy układ adc.

adc_config_t adc_conf;
adc_conf.mode = ADC_READ_TOUT_MODE;
adc_conf.clk_div = 8;

Zdefiniowałem tutaj tylko dwie zmienne odpowidzialne za tryb pracy, oraz dzielnik zegara taktującego procesor – domyślnie to 80MHz.

TOUT, oznacza po prostu wyjście ADC0, zamiast tego moglibyśmy zastosować makro ADC_READ_VDD_MODE by mierzyć napięcie zasilania.

Wystarczy tylko przekazać wskaźnik do tej struktury funkcji „adc_init” by rozpocząć rozpocząć odczyt.

adc_init( &adc_conf );

Możemy mierzyć napięcie między 0 a 1V. Napięcie odniesienia wynosi więc 1V. Chociaż wiele płytek prototypowych, jak choćby nodeMCU posiada na sobie dzielnik napięcia, który pozwala podłączyć pod pin ADC0 napięcie do 3.3V – wtedy właśnie 3.3V uznamy za napięcie odniesienia.

Na wyjściu rezystorowego dzielnika napięcia otrzymałem napięcie o wartości ~780mV. Podepnę je pod pin A0 i załaduję następujący program.

void app_main(void)
{
    adc_config_t adc_conf;
    adc_conf.mode = ADC_READ_TOUT_MODE;
    adc_conf.clk_div = 8;
    
    adc_init(&adc_conf);
    uint16_t pomiar;
    
    while (1) 
    {
       vTaskDelay( 2000 / portTICK_RATE_MS );
       uint16_t odczyt=0;
       for (uint8_t i=0; i<32; i++)
       {
           adc_read(&pomiar);
           odczyt += pomiar;
           pomiar = 0;
       }
       ets_printf("odczyt: %d\n", odczyt/32);
       
    }
}

Funkcja „adc_read” służy do odczytu, przekazujemy jej adres zmiennej typu uint16_t – w niej zapisze wynik. Oczywiście skorzystałem z multisampling’u w celu uśrednienia wyniku i na wyjściu otrzymałem wartość 291.

Korzystam z płytki NodeMCU z wbudowanym dzielnikiem napięcia więc napięcie odniesienia u mnie wynosi 3.3V. Prosta kalkulacja:

Jak więc widać, pomiar nie jest zbyt dokłady i raczej niewiele z tym zrobimy – nie mamy tutaj możliwości kalibracji ADC jak w przypadku esp32. Być może byłby odrobinę lepiej gdyby skorzystać z gołego modułu esp8266 z napięciem referencyjnym 1V. Musimy więc pamiętać, że układ ADC nie jest najlepszą stroną tego mikrokontrolera. Do prostych operacji powinien jednak wystarczyć.


Kolejna część: #7 – Touch sensor

Kurs esp32 i esp8266 – spis treści