В предыдущей статье я слегка затронул тему использования АЦП в ESP микроконтроллерах. При тестировании использовался Devkit:
- MH-ET Live ESP32 Devkit 38 pins — приобретенный на Aliexpress.
В ESP32 используется два 12-bit SAR (Successive Approximation Register) ЦАП-а, поддерживающих суммарно 18 каналов аналоговых измерений.
ЦАП драйвера поддерживает ADC1 (8 каналов — GPIOs 32 — 39), и ADC2 (10 каналов — GPIOs 0, 2, 4, 12 — 15 и 25 — 27). Однако ADC2 имеет определенные ограничения по использованию:
- ADC2 используется Wi-Fi драйвером, поэтому приложение может использовать ADC2 только когда он не используется.
- Некоторые пины ADC2 используются также для других целей (GPIO 0, 2, 4, 15) поэтому есть ограничения по их использованию. Подробнее в документации для плат DevKit-ов:
- ESP32 DevKitC: GPIO 0.
- ESP-WROVER-KIT: GPIO 0, 2, 4 и 15.
Конфигурирование АЦП и чтение данных
Перед началом использования АЦП ESP 32 микроконтроллера необходимо выполнить конфигурирование:
- Для ADC1 нужно выставить желаемую точность, а также величину ослабления входного сигнала, вызвав функции
adc1_config_width()
иadc1_config_channel_atten()
. - Для ADC2, необходимо задать величину ослабления сигнала, вызвав
adc2_config_channel_atten()
. Точность замеров для ADC2 должна выполняться каждый раз при чтении данных.
Величина ослабления сигнала задается для каждого канала АЦП. См. adc1_channel_t
и adc2_channel_t
, которые выступают в качестве параметра вышеуказанных функций.
После конфигурирования АЦП ESP32 для чтения данных вызываются функции adc1_get_raw()
и adc2_get_raw()
. Разрядность (точность) ADC2 передается в качестве параметра при вызове функции adc2_get_raw()
Поскольку ADC2 используется для работы WiFi, операция с которым имеет максимальный приоритет, вызов функцииadc2_get_raw()
приведет к появлению ошибки, если он произведен между esp_wifi_start()
и esp_wifi_stop()
. Нужно использовать код ошибки, чтобы определить, что чтение было данных произведено успешно.
В ESP32 есть внутренний датчик Холла, данные с которого можно вычитать ADC1, вызвав функцию hall_sensor_read()
. Хотя датчик Холла внуренний в ESP32, чтение данных с него задействует каналы 0 и 3 ADC1 (GPIO 36 и 39). Не нужно использовать эти контакты и изменять их конфигурацию впротивном случае это может сказаться на измерении сигналов с низким уровнем напряжения, получаемых с датчика.
API обеспечивает удобный способ конфигурирования ADC1 для чтения в режиме ULP, когда данные могут приходить с АЦП, I2C и датчика температуры, даже когда процессор находится в режиме глубокого сна (deep sleep mode). Чтобы задействовать этот режим нужно вызвать функцию adc1_ulp_enable()
и установить точность и ослабление уровня сигнала, как говорилось ранее.
В ESP32 есть ещё одна специфическая функция adc2_vref_to_gpio()
Она используется для перенаправления внуреннего опорного напряжения (internal reference voltage) на выбранный GPIO вход. Функция удобна при калибровке АЦП и будет обсуждаться далее в разделе Минимизация уровня шума.
Пример конфигурирования и чтения данных с АЦП ESP32
Для конфигурирования используется код:
#include <driver/adc.h> void setup() { Serial.begin(9600); adc1_config_width(ADC_WIDTH_BIT_12); adc1_config_channel_atten(ADC1_CHANNEL_0,ADC_ATTEN_DB_11); }
Он без проблем компилируется в Arduino IDE. В примере использовалось максимальное напряжение около 3 V (ослабление 11 dB). Величины ослабления сигнала описаны adc_atten_t
. Разрядность АЦП задана максимальная 12 бит (ADC_WIDTH_BIT_12). Пример использования ADC драйвера с использованием калибрования: peripherals/adc
Чтобы прочитать данные с определенного канала АЦП ADC1:
void loop() { int val = adc1_get_raw(ADC1_CHANNEL_0); int adcr = analogRead(A0); //Использовать в аргументах ADC1_CHANNEL_0 нельзя!!! Serial.print("Read ADC pin [" + String(ADC1_CHANNEL_0_GPIO_NUM) + "]: " + String(val) + " [" + String(adcr)+"]"); }
При чтении данных с помощью функции из библиотеки ESP32 и штатной Arduino analogRead возвращается немного отличающиеся результаты:
Read ADC pin [39]: 1382 [1381] Read ADC pin [39]: 1380 [1365] Read ADC pin [39]: 1371 [1343] Read ADC pin [39]: 1377 [1383] Read ADC pin [39]: 1376 [1355] Read ADC pin [39]: 1387 [1379] Read ADC pin [39]: 1383 [1382] Read ADC pin [39]: 1377 [1355] Read ADC pin [39]: 1386 [1376] Read ADC pin [39]: 1371 [1381]
Почему так проиходит я не знаю, привел информацию на всякий случай.
Чтобы прочитать данные с канала 7 ADC2 (GPIO 27):
#include <driver/adc.h> ... int read_raw; adc2_config_channel_atten( ADC2_CHANNEL_7, ADC_ATTEN_0db ); esp_err_t r = adc2_get_raw( ADC2_CHANNEL_7, ADC_WIDTH_12Bit, &read_raw); if ( r == ESP_OK ) { printf("%d\n", read_raw ); } else if ( r == ESP_ERR_TIMEOUT ) { printf("ADC2 used by Wi-Fi.\n"); }
При работе Wi-Fi чтение данных может привести к появлению ошибки из-за коллизий с Wi-Fi. Пример использования ADC2 драйвера для чтения выхода ЦАП: peripherals/adc2
Для чтения внутренного датчика Холла используется следующий код:
#include <driver/adc.h> ... adc1_config_width(ADC_WIDTH_BIT_12); int val = hall_sensor_read();
Снижение уровня шума
АЦП ESP32 довольно чувствительный к шуму. Это приводит к разбросу в показаниях АЦП. Чтобы свести к минимуму шум, на вход АЦП можно подключить конденсатор 0,1 мкФ.
Для защиты АЦП от перегрузок по току или неправильной полярности при подключении при использовании с датчиками 4..20 мА можно использовать специализированный чип MAX14626ETTT.
На графике видно, что добавление емкости существенно уменьшает разброс значений на входе АЦП.
Помимо аппаратных способов можно использовать программные. Например, усреднение дает неплохой вариант. Пример кода для усреднения данных считываемых с датчика тока 4..20 мА:
#include <driver/adc.h> #define channel ADC1_CHANNEL_3 //SVN input #define pin ADC1_CHANNEL_3_GPIO_NUM //SVN input void setup() { Serial.begin(9600); adc1_config_width(ADC_WIDTH_BIT_12); adc1_config_channel_atten(channel, ADC_ATTEN_DB_11); } int lastMillis = 0; int lastMeasurementsMillis = 0; float Average = 0; int MeasurementsToAverage = 0; void loop() { int currentMillis = millis(); if (currentMillis - lastMeasurementsMillis > 50) { float adcr = adc1_get_raw(channel); Average += adcr; MeasurementsToAverage++; lastMeasurementsMillis = currentMillis; } if (currentMillis - lastMillis > 1000) { float adcr = adc1_get_raw(channel); Average /= MeasurementsToAverage; float val = adcr*4/1375;//3850; Serial.print("Read ADC pin [" + String(pin) + "]: " + String(adcr) + "\tAverage: " + String(Average) +"\t"); Serial.println("ReadADC, mA: \t" + String(val)); lastMillis = currentMillis; Average = 0; MeasurementsToAverage = 0; } }
Другие варианты фильтров для сглаживания показаний на входе АЦП в конце статьи.
Калибровка АЦП
В esp_adc_cal/include/esp_adc_cal.h API есть функции для корректировки разброса в измерениях напряжения из-за различия опорных (reference voltages (Vref) напряжений в чипах ESP32. Опорное напряжение АЦП должно составлять 1100mV. Реальные значения «гуляют» от 1000mV до 1200mV от чипа к чипу ESP32s.
Из-за изменения опорного напряжения количество отсчетов при считывании функцией adc1_get_raw() будет разным от чипа к чипу.
При калибровке производится серия замеров для получения функции
y = coeff_a * x + coeff_b при выбранном параметре ослабления. По этой кривой можно определить каким образом входное напряжение на АЦП будет преобразовано в отсчеты на выходе. Параметры полученные при калибровке могут быть сохранены в eFuse или использоватся каждый раз при вызове функций преобразования.
#include <driver/adc.h> #include "esp_adc_cal.h" static esp_adc_cal_characteristics_t *adc_chars; static const adc_atten_t atten = ADC_ATTEN_DB_11; static const adc_unit_t unit = ADC_UNIT_1; void check_efuse() { //Check TP is burned into eFuse if (esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_TP) == ESP_OK) { Serial.println("eFuse Two Point: Supported"); } else { Serial.println("eFuse Two Point: NOT supported"); } //Check Vref is burned into eFuse if (esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_VREF) == ESP_OK) { Serial.println("eFuse Vref: Supported"); } else { Serial.println("eFuse Vref: NOT supported"); } } void print_char_val_type(esp_adc_cal_value_t val_type) { if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP) { Serial.println("Characterized using Two Point Value\n"); } else if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { Serial.println("Characterized using eFuse Vref"); } else { Serial.println("Characterized using Default Vref"); } } String NumToAtten(int atten) { switch (atten) { case 0: return "ADC_ATTEN_DB_0. No chages for the input voltage"; case 1: return "ADC_ATTEN_DB_2_5. The input voltage will be reduce to about 1/1.34."; case 2: return "ADC_ATTEN_DB_6. The input voltage will be reduced to about 1/2"; case 3: return "ADC_ATTEN_DB_11. The input voltage will be reduced to about 1/3.6"; } return "Unknown attenuation."; } String NumToWidth(int width) { switch (width) { case 0: return "ADC_WIDTH_BIT_9. ADC capture width is 9Bit"; case 1: return "ADC_WIDTH_BIT_10. ADC capture width is 10Bit"; case 2: return "ADC_WIDTH_BIT_11. ADC capture width is 11Bit"; case 3: return "ADC_WIDTH_BIT_12. ADC capture width is 12Bit"; } return "Unknown width."; } void setup() { Serial.begin(9600); adc1_config_width(ADC_WIDTH_BIT_12); adc1_config_channel_atten(channel, ADC_ATTEN_DB_11); check_efuse(); //Characterize ADC at particular atten adc_chars = (esp_adc_cal_characteristics_t *)calloc(1, sizeof(esp_adc_cal_characteristics_t)); esp_adc_cal_value_t val_type = esp_adc_cal_characterize(unit, atten, ADC_WIDTH_BIT_12, DEFAULT_VREF, adc_chars); Serial.println("ADC number:\t" + String(adc_chars->adc_num)); Serial.println("ADC attenuation:\t" + NumToAtten(adc_chars->atten)); Serial.println("ADC bit width:\t" + NumToWidth(adc_chars->bit_width)); Serial.println("ADC coeff_a:\t" + String(adc_chars->coeff_a)); Serial.println("ADC coeff_b:\t" + String(adc_chars->coeff_b)); Serial.println("ADC VRef:\t" + String(adc_chars->vref)); //Check type of calibration value used to characterize ADC print_char_val_type(val_type); }
Результат выполнения скетча:
14:17:25.180 -> eFuse Two Point: NOT supported 14:17:25.215 -> eFuse Vref: Supported 14:17:25.249 -> ADC number: 1 14:17:25.249 -> ADC attenuation: ADC_ATTEN_DB_11. The input voltage will be reduced to about 1/3.6 14:17:25.354 -> ADC bit width: ADC_WIDTH_BIT_12. ADC capture width is 12Bit 14:17:25.422 -> ADC coeff_a: 52798 14:17:25.422 -> ADC coeff_b: 142 14:17:25.456 -> ADC VRef: 1100 14:17:25.456 -> ESP_ADC_CAL_VAL_EFUSE_VREF. Characterization based on reference voltage stored in eFuse.
Не смотря на то, что тест показывает использование eFuse Vref (reference voltage stored in eFuse), значение ADC Vref = 1100 mV — т.е. дефолтное, что маловероятно, если бы на заводе делали замер опорного напряжения. Проверка ADC Vref с помощью утилиты espefuse.py под Windows дает тот-же результат.
И на другом ESP32 чипе:
При перенаправлении выхода генератора опорного напряжения на GPIO 26:
esp_err_t status = adc2_vref_to_gpio(GPIO_NUM_26); if (status == ESP_OK) { printf("v_ref routed to GPIO\n"); } else { printf("failed to route v_ref\n"); }
и замере его высокоточным мультметром напряжение получается очень близким к 1.1 V. На одном чипе 1134 mV, на втором — 1105 mV. Скорее всего заводское оборудование более точное, так что опорное напряжение действительно близко отображаемым 1100 mV.
Диапазоны измерений АЦП ESP32
Если посмотреть техническую документацию на ESP32, то в ней указано, что при напряжении VDD_A = 3.3V:
- 0dB ослабление (ADC_ATTEN_DB_0) дает диапазон до 1.1V.
- 2.5dB ослабление (ADC_ATTEN_DB_2_5) дает диапазон до 1.5V.
- 6dB ослабление (ADC_ATTEN_DB_6) дает диапазон до 2.2V.
- 11dB ослабление (ADC_ATTEN_DB_11) дает диапазон до 3.9V.
По факту, если при ADC_ATTEN_DB_11 замерять напряжение, то уже при величине порядка 2.6 V на 12-ти битах АЦП начинает выдавать предельные 4095.
Из-за характеристики АЦП наиболее точные результаты измерений получаются в более узком диапазоне, чем заявлено в документации. На начальном и конечном участке более или менее линейная характеристика АЦП изгибается и становится более пологой, поэтому измерения очень неточные. В документации указаны рекомендуемые диапазоны измерений:
- 0dB ослабление(ADC_ATTEN_DB_0) — от 100 до 950mV.
- 2.5dB ослабление (ADC_ATTEN_DB_2_5) от 100 до 1250mV.
- 6dB ослабление (ADC_ATTEN_DB_6) от 150 до 1750mV.
- 11dB ослабление (ADC_ATTEN_DB_11) от 150 до 2450mV.
В документации есть примечание, что при ослаблении в 11dB максимальное напряжение ограничено VDD_A, а не указанным ранее 3.9 V.
Медианный фильтр
/* median_filter.ino 2014-03-25: started by David Cary */ int median_of_3( int a, int b, int c ){ int the_max = max( max( a, b ), c ); int the_min = min( min( a, b ), c ); // unnecessarily clever code int the_median = the_max ^ the_min ^ a ^ b ^ c; return( the_median ); } int newest = 0; int recent = 0; int oldest = 0; void setup() { Serial.begin(9600); // read first value, initialize with it. oldest = random(200); recent = oldest; newest = recent; Serial.println("median filter example: "); } void loop() { // drop oldest value and shift in latest value oldest = recent; recent = newest; newest = random(200); Serial.print("new value: "); Serial.print(newest, DEC); int median = median_of_3( oldest, recent, newest ); Serial.print("smoothed value: "); Serial.print(median, DEC); Serial.println(""); delay(5000); }
Полезные ссылки
- Микросхемы MAXIM для защиты сигнальных линий и линий питания. MAX14626ETTT для защиты АЦП при подключении датчиков 4..20 мА.
- https://randomnerdtutorials.com/esp32-adc-analog-read-arduino-ide/
- THREE METHODS TO FILTER NOISY ARDUINO MEASUREMENTS
- Smoothing. Скользящее среднее.
- SmoothADC.
- Arduino Analog Smooth.
- MicroSmooth (Simple Moving Average, Cumulative Moving Average, Exponential Moving Average, Savitzky Golay Filter, Ramer Douglas Peucker Algorithm, Kolmogorov Zurbenko Algorithm).
- ADC Input Noise: The Good, The Bad, and The Ugly. Is No Noise Good Noise?
- Paul Badger: smooth digital low-pass filter
- Paul Badger: digitalSmooth digital low-pass filter with outlier rejection
- David A. Mellis and Tom Igoe: Smoothing tutorial
- Majenki: Average Library
- jeroendoggen: Arduino-signal-filtering-library
- karlward: Arduino data filtering library
- sebnil: FIR-filter-Arduino-Library
- daPhoosa: MedianFilter
- arc12: A Collection of Digital Signal Filters (intended for use with Arduino)
- sebnil: Selfbalancing robot in Arduino. Implemented with PID controllers, FIR filters, complementary filter.