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