From 88b05f939164c432be6c7e2805e039759dc28418 Mon Sep 17 00:00:00 2001 From: Darian Leung Date: Mon, 7 Aug 2017 21:33:01 +0800 Subject: [PATCH] esp_adc_cal: Added ADC calibration component Added component containg API that is able to correct raw ADC readings into a voltage in mV. Also provided a helper function that combines the process of getting the raw ADC1 reading then converting it to a voltage in mV. In doing so, the adc1_get_voltage() function of the ADC driver has been deprecated. Instead there is now adc1_get_raw to obtain the raw ADC1 reading, and adc1_to_voltage() that gets the raw reading and converts all in one function. Functions using the deprecated adc1_get_voltage() have also been updated to use adc1_get_raw(). Conversion is based on ADC characteristics. The characteristics are based on the ADC's v_ref, herefore the appropriate structure and functions have been provided to obtain the ADC characteristics. The existing ADC driver has also been modified by adding a function to route the internal ADC reference voltage to a GPIO allowing users to measure it manually. Relevant documentation has also been updated --- components/driver/include/driver/adc.h | 32 +++- components/driver/rtc_module.c | 52 ++++++- components/esp_adc_cal/component.mk | 5 + components/esp_adc_cal/esp_adc_cal.c | 111 ++++++++++++++ .../esp_adc_cal/esp_adc_cal_lookup_tables.c | 96 ++++++++++++ components/esp_adc_cal/include/esp_adc_cal.h | 141 ++++++++++++++++++ docs/Doxyfile | 1 + docs/api-reference/peripherals/adc.rst | 61 +++++++- .../peripherals/adc/main/adc1_example_main.c | 51 +++++++ examples/peripherals/adc/main/adc1_test.c | 35 ----- examples/system/app_trace_to_host/README.md | 2 +- .../main/app_trace_to_host_test.c | 2 +- 12 files changed, 542 insertions(+), 47 deletions(-) create mode 100644 components/esp_adc_cal/component.mk create mode 100644 components/esp_adc_cal/esp_adc_cal.c create mode 100644 components/esp_adc_cal/esp_adc_cal_lookup_tables.c create mode 100644 components/esp_adc_cal/include/esp_adc_cal.h create mode 100644 examples/peripherals/adc/main/adc1_example_main.c delete mode 100644 examples/peripherals/adc/main/adc1_test.c diff --git a/components/driver/include/driver/adc.h b/components/driver/include/driver/adc.h index 19068a235..444f145eb 100644 --- a/components/driver/include/driver/adc.h +++ b/components/driver/include/driver/adc.h @@ -21,6 +21,7 @@ extern "C" { #include #include "esp_err.h" +#include "driver/gpio.h" typedef enum { ADC_ATTEN_0db = 0, /*!0dB signal attenuation for that ADC channel. @@ -107,13 +108,22 @@ esp_err_t adc1_config_channel_atten(adc1_channel_t channel, adc_atten_t atten); * - -1: Parameter error * - Other: ADC1 channel reading. */ -int adc1_get_voltage(adc1_channel_t channel); +int adc1_get_raw(adc1_channel_t channel); + +/** @cond */ //Doxygen command to hide deprecated function from API Reference +/* + * @deprecated This function returns an ADC1 reading but is deprecated due to + * a misleading name and has been changed to directly call the new function. + * Use the new function adc1_get_raw() instead + */ +int adc1_get_voltage(adc1_channel_t channel) __attribute__((deprecated)); +/** @endcond */ /** * @brief Configure ADC1 to be usable by the ULP * * This function reconfigures ADC1 to be controlled by the ULP. - * Effect of this function can be reverted using adc1_get_voltage function. + * Effect of this function can be reverted using adc1_get_raw function. * * Note that adc1_config_channel_atten, adc1_config_width functions need * to be called to configure ADC1 channels, before ADC1 is used by the ULP. @@ -136,6 +146,22 @@ void adc1_ulp_enable(); */ int hall_sensor_read(); +/** + * @brief Output ADC2 reference voltage to gpio 25 or 26 or 27 + * + * This function utilizes the testing mux exclusive to ADC 2 to route the + * reference voltage one of ADC2's channels. Supported gpios are gpios + * 25, 26, and 27. This refernce voltage can be manually read from the pin + * and used in the esp_adc_cal component. + * + * @param[in] gpio GPIO number (gpios 25,26,27 supported) + * + * @return + * - ESP_OK: v_ref successfully routed to selected gpio + * - ESP_ERR_INVALID_ARG: Unsupported gpio + */ +esp_err_t adc2_vref_to_gpio(gpio_num_t gpio); + #ifdef __cplusplus } #endif diff --git a/components/driver/rtc_module.c b/components/driver/rtc_module.c index eb89da60c..aecb0e92f 100644 --- a/components/driver/rtc_module.c +++ b/components/driver/rtc_module.c @@ -945,7 +945,7 @@ esp_err_t adc1_config_width(adc_bits_width_t width_bit) return ESP_OK; } -int adc1_get_voltage(adc1_channel_t channel) +int adc1_get_raw(adc1_channel_t channel) { uint16_t adc_value; @@ -975,6 +975,11 @@ int adc1_get_voltage(adc1_channel_t channel) return adc_value; } +int adc1_get_voltage(adc1_channel_t channel) //Deprecated. Use adc1_get_raw() instead +{ + return adc1_get_raw(channel); +} + void adc1_ulp_enable(void) { portENTER_CRITICAL(&rtc_spinlock); @@ -989,6 +994,43 @@ void adc1_ulp_enable(void) portEXIT_CRITICAL(&rtc_spinlock); } +esp_err_t adc2_vref_to_gpio(gpio_num_t gpio) +{ + int channel; + if(gpio == GPIO_NUM_25){ + channel = 8; //Channel 8 bit + }else if (gpio == GPIO_NUM_26){ + channel = 9; //Channel 9 bit + }else if (gpio == GPIO_NUM_27){ + channel = 7; //Channel 7 bit + }else{ + return ESP_ERR_INVALID_ARG; + } + + //Configure RTC gpio + rtc_gpio_init(gpio); + rtc_gpio_output_disable(gpio); + rtc_gpio_input_disable(gpio); + rtc_gpio_pullup_dis(gpio); + rtc_gpio_pulldown_dis(gpio); + + SET_PERI_REG_BITS(RTC_CNTL_BIAS_CONF_REG, RTC_CNTL_DBG_ATTEN, 0, RTC_CNTL_DBG_ATTEN_S); //Check DBG effect outside sleep mode + //set dtest (MUX_SEL : 0 -> RTC; 1-> vdd_sar2) + SET_PERI_REG_BITS(RTC_CNTL_TEST_MUX_REG, RTC_CNTL_DTEST_RTC, 1, RTC_CNTL_DTEST_RTC_S); //Config test mux to route v_ref to ADC2 Channels + //set ent + SET_PERI_REG_MASK(RTC_CNTL_TEST_MUX_REG, RTC_CNTL_ENT_RTC_M); + //set sar2_en_test + SET_PERI_REG_MASK(SENS_SAR_START_FORCE_REG, SENS_SAR2_EN_TEST_M); + //force fsm + SET_PERI_REG_BITS(SENS_SAR_MEAS_WAIT2_REG, SENS_FORCE_XPD_SAR, 3, SENS_FORCE_XPD_SAR_S); //Select power source of ADC + //set sar2 en force + SET_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_SAR2_EN_PAD_FORCE_M); //Pad bitmap controlled by SW + //set en_pad for channels 7,8,9 (bits 0x380) + SET_PERI_REG_BITS(SENS_SAR_MEAS_START2_REG, SENS_SAR2_EN_PAD, 1< +#include "driver/adc.h" + +#include "esp_adc_cal.h" + +static const esp_adc_cal_lookup_table_t *table_ptrs[4] = {&esp_adc_cal_table_atten_0, + &esp_adc_cal_table_atten_1, + &esp_adc_cal_table_atten_2, + &esp_adc_cal_table_atten_3}; + +uint32_t get_adc_vref_from_efuse() +{ + //TODO: Replaced with read to eFuse once ATE confirms location of 5 bits + return 0; +} + +void esp_adc_cal_get_characteristics(uint32_t v_ref, + adc_atten_t atten, + adc_bits_width_t bit_width, + esp_adc_cal_characteristics_t *chars) +{ + chars->v_ref = v_ref; + chars->table = table_ptrs[atten]; + chars->bit_width = bit_width; + if (v_ref >= ADC_CAL_LOW_V_REF) { + chars->gain = ((chars->v_ref - ADC_CAL_LOW_V_REF) + * chars->table->gain_m) + + chars->table->gain_c; + chars->offset = (((chars->v_ref - ADC_CAL_LOW_V_REF) + * chars->table->offset_m) + + chars->table->offset_c + + ((1 << ADC_CAL_OFFSET_SCALE) / 2)) + >> ADC_CAL_OFFSET_SCALE; //Bit shift to cancel 2^10 multiplier + chars->ideal_offset = (((ADC_CAL_IDEAL_V_REF - ADC_CAL_LOW_V_REF) + * chars->table->offset_m) + + chars->table->offset_c + + ((1 << ADC_CAL_OFFSET_SCALE) / 2)) //Rounding + >> ADC_CAL_OFFSET_SCALE; + } else { //For case where v_ref is smaller than low bound resulting in negative + chars->gain = chars->table->gain_c + - ((ADC_CAL_LOW_V_REF - chars->v_ref) + * chars->table->gain_m); + chars->offset = (chars->table->offset_c + - ((chars->v_ref - ADC_CAL_LOW_V_REF) + * chars->table->offset_m) + + ((1 << ADC_CAL_OFFSET_SCALE) / 2)) //Rounding + >> ADC_CAL_OFFSET_SCALE; //Bit shift to cancel 2^10 multiplier + chars->ideal_offset = (chars->table->offset_c + - ((ADC_CAL_IDEAL_V_REF - ADC_CAL_LOW_V_REF) + * chars->table->offset_m) + + ((1 << ADC_CAL_OFFSET_SCALE) / 2)) //Rounding + >> ADC_CAL_OFFSET_SCALE; + } +} + +static uint32_t esp_adc_cal_interpolate_round(uint32_t lower, uint32_t upper, + uint32_t step, uint32_t point) +{ + //Interpolate 'point' between 'lower' and 'upper' seperated by 'step' + return ((lower * step) - (lower * point) + (upper * point) + (step / 2)) / step; +} + +uint32_t esp_adc_cal_raw_to_voltage(uint32_t adc, + const esp_adc_cal_characteristics_t *chars) +{ + //Scale ADC to 12 bit width (0 to 4095) + adc <<= (ADC_WIDTH_12Bit - chars->bit_width); + uint32_t i = (adc >> chars->table->bit_shift); //find index for lut voltages + //Refernce LUT to obtain voltage using index + uint32_t voltage = esp_adc_cal_interpolate_round(chars->table->voltage[i], + chars->table->voltage[i + 1], + (1 << chars->table->bit_shift), + adc - (i << chars->table->bit_shift)); + /* + * Apply Gain, scaling(bit shift) and offset to interpolated voltage + * v_true = (((v_id - off_id)*gain)*scaling) + off_true + */ + if (voltage > chars->ideal_offset) { + voltage = (voltage - chars->ideal_offset) * chars->gain; + voltage += (1 << ADC_CAL_GAIN_SCALE) / 2; //For rounding when scaled + voltage >>= ADC_CAL_GAIN_SCALE; + voltage += chars->offset; + } else { //For case where voltage is less than ideal offset leading to negative value + voltage = ((chars->ideal_offset - voltage) * chars->gain); + voltage += (1 << ADC_CAL_GAIN_SCALE) / 2; //For rounding when scaled + voltage >>= ADC_CAL_GAIN_SCALE; + voltage = chars->offset - voltage; + } + + return voltage; +} + +uint32_t adc1_to_voltage(adc1_channel_t channel, const esp_adc_cal_characteristics_t *chars) +{ + return esp_adc_cal_raw_to_voltage((uint32_t)adc1_get_raw(channel), chars); +} + diff --git a/components/esp_adc_cal/esp_adc_cal_lookup_tables.c b/components/esp_adc_cal/esp_adc_cal_lookup_tables.c new file mode 100644 index 000000000..111f75c4b --- /dev/null +++ b/components/esp_adc_cal/esp_adc_cal_lookup_tables.c @@ -0,0 +1,96 @@ +// Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "esp_adc_cal.h" + +/** + * Mean error of 219 modules: 3.756418mV + * Max error of 219 modules: 26.314087mV + * Mean of max errors of 219 modules: 7.387282mV + */ +const esp_adc_cal_lookup_table_t esp_adc_cal_table_atten_0 = { + .gain_m = 56, + .gain_c = 59928, + .offset_m = 91, + .offset_c = 52798, + .bit_shift = 7, + .voltage = { + 54, 90, 120, 150, 180, 209, 241, 271, + 301, 330, 360, 391, 421, 450, 480, 511, + 541, 571, 601, 630, 660, 690, 720, 750, + 780, 809, 839, 870, 900, 929, 959, 988, + 1018 + } +}; + +/** + * Mean error of 219 modules: 4.952441mV + * Max error of 219 modules: 38.235321mV + * Mean of max errors of 219 modules: 9.718749mV + */ +const esp_adc_cal_lookup_table_t esp_adc_cal_table_atten_1 = { + .gain_m = 57, + .gain_c = 59834, + .offset_m = 108, + .offset_c = 54733, + .bit_shift = 7, + .voltage = { + 60, 102, 143, 184, 223, 262, 303, 343, + 383, 423, 463, 503, 543, 583, 623, 663, + 703, 742, 782, 823, 862, 901, 942, 981, + 1022, 1060, 1101, 1141, 1180, 1219, 1259, 1298, + 1338 + } +}; + +/** + * Mean error of 219 modules: 6.793558mV + * Max error of 219 modules: 51.435440mV + * Mean of max errors of 219 modules: 13.083121mV + */ +const esp_adc_cal_lookup_table_t esp_adc_cal_table_atten_2 = { + .gain_m = 56, + .gain_c = 59927, + .offset_m = 154, + .offset_c = 71995, + .bit_shift = 7, + .voltage = { + 82, 138, 194, 250, 305, 360, 417, 473, + 529, 584, 639, 696, 751, 806, 861, 917, + 971, 1026, 1081, 1136, 1192, 1246, 1301, 1356, + 1411, 1466, 1522, 1577, 1632, 1687, 1743, 1799, + 1855 + } +}; + +/** + * Mean error of 219 modules: 13.149460mV + * Max error of 219 modules: 97.102951mV + * Mean of max errors of 219 modules: 35.538924mV + */ +const esp_adc_cal_lookup_table_t esp_adc_cal_table_atten_3 = { + .gain_m = 33, + .gain_c = 62214, + .offset_m = 610, + .offset_c = 108422, + .bit_shift = 7, + .voltage = { + 110, 221, 325, 430, 534, 637, 741, 845, + 947, 1049, 1153, 1256, 1358, 1461, 1565, 1670, + 1774, 1878, 1983, 2088, 2192, 2293, 2393, 2490, + 2580, 2665, 2746, 2820, 2885, 2947, 3007, 3060, + 3107 + } +}; + diff --git a/components/esp_adc_cal/include/esp_adc_cal.h b/components/esp_adc_cal/include/esp_adc_cal.h new file mode 100644 index 000000000..2d561f5c0 --- /dev/null +++ b/components/esp_adc_cal/include/esp_adc_cal.h @@ -0,0 +1,141 @@ +// Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include "driver/adc.h" + +/** @cond */ +#define ADC_CAL_GAIN_SCALE 16 +#define ADC_CAL_OFFSET_SCALE 10 + +#define ADC_CAL_IDEAL_V_REF 1100 //In mV +#define ADC_CAL_LOW_V_REF 1000 +#define ADC_CAL_HIGH_V_REF 1200 +#define ADC_CAL_MIN 0 +#define ADC_CAL_MAX 4095 +/** @endcond */ + +/** + * @brief Structure storing Lookup Table + * + * The Lookup Tables (LUT) of a given attenuation contains 33 equally spaced + * points. The Gain and Offset curves are used to find the appopriate gain and + * offset factor given a reference voltage v_ref. + * + * @note A seperate LUT is provided for each attenuation and are defined in + * esp_adc_cal_lookup_tables.c + */ +typedef struct { + uint32_t gain_m; /** @@ -24,7 +26,7 @@ Reading voltage on ADC1 channel 0 (GPIO 36):: adc1_config_width(ADC_WIDTH_12Bit); adc1_config_channel_atten(ADC1_CHANNEL_0,ADC_ATTEN_0db); - int val = adc1_get_voltage(ADC1_CHANNEL_0); + int val = adc1_get_raw(ADC1_CHANNEL_0); Reading the internal hall effect sensor:: @@ -43,3 +45,58 @@ API Reference .. include:: /_build/inc/adc.inc +ADC Calibration +=============== + +Overview +-------- +The esp_adc_cal API provides functions to correct for differences in measured voltages caused by non-ideal ADC reference voltages in ESP32s. The ideal ADC reference voltage is 1100mV however the reference voltage of different ESP32s can range from 1000mV to 1200mV. + +Correcting the measured voltage using the esp_adc_cal API involves referencing a lookup table of voltages. The voltage obtained from the lookup table is the scaled and shifted by a gain and offset factor that is based on the ADC's reference voltage. + +The reference voltage of the ADCs can be routed to certain GPIOs and measured manually using the ADC driver’s adc2_vref_to_gpio() function. + +Application Example +------------------- + +Reading the ADC and obtaining a result in mV:: + + #include + #include + + ... + #define V_REF 1100 //ADC reference voltage + + //Config ADC and characteristics + adc1_config_width(ADC_WIDTH_12Bit); + adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_11db); + + //Calculate ADC characteristics i.e. gain and offset factors + esp_adc_cal_characteristics_t characteristics; + esp_adc_cal_get_characteristics(V_REF, ADC_ATTEN_11db, ADC_WIDTH_12Bit, &characteristics); + + //Read ADC and obtain result in mV + uint32_t voltage = adc1_to_voltage(ADC1_CHANNEL_6, &characteristics); + printf("%d mV\n",voltage); + + +Routing ADC reference voltage to GPIO:: + + #include + #include + #include + + ... + + esp_err_t status = adc2_vref_to_gpio(GPIO_NUM_25); + if (status == ESP_OK){ + printf("v_ref routed to GPIO\n"); + }else{ + printf("failed to route v_ref\n"); + } + + +API Reference +------------- + +.. include:: /_build/inc/esp_adc_cal.inc \ No newline at end of file diff --git a/examples/peripherals/adc/main/adc1_example_main.c b/examples/peripherals/adc/main/adc1_example_main.c new file mode 100644 index 000000000..42e78a2a6 --- /dev/null +++ b/examples/peripherals/adc/main/adc1_example_main.c @@ -0,0 +1,51 @@ +/* ADC1 Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "driver/gpio.h" +#include "driver/adc.h" +#include "esp_system.h" +#include "esp_adc_cal.h" + +/*Note: Different ESP32 modules may have different reference voltages varying from + * 1000mV to 1200mV. Use #define GET_VREF to route v_ref to a GPIO + */ +#define V_REF 1100 +#define ADC1_TEST_CHANNEL (ADC1_CHANNEL_6) //GPIO 34 +//#define V_REF_TO_GPIO //Remove comment on define to route v_ref to GPIO + +void app_main(void) +{ +#ifndef V_REF_TO_GPIO + //Init ADC and Characteristics + esp_adc_cal_characteristics_t characteristics; + adc1_config_width(ADC_WIDTH_12Bit); + adc1_config_channel_atten(ADC1_TEST_CHANNEL, ADC_ATTEN_0db); + esp_adc_cal_get_characteristics(V_REF, ADC_ATTEN_0db, ADC_WIDTH_12Bit, &characteristics); + uint32_t voltage; + while(1){ + voltage = adc1_to_voltage(ADC1_TEST_CHANNEL, &characteristics); + printf("%d mV\n",voltage); + vTaskDelay(pdMS_TO_TICKS(1000)); + } +#else + //Get v_ref + esp_err_t status; + status = adc2_vref_to_gpio(GPIO_NUM_25); + if (status == ESP_OK){ + printf("v_ref routed to GPIO\n"); + }else{ + printf("failed to route v_ref\n"); + } + fflush(stdout); +#endif +} diff --git a/examples/peripherals/adc/main/adc1_test.c b/examples/peripherals/adc/main/adc1_test.c deleted file mode 100644 index 269969365..000000000 --- a/examples/peripherals/adc/main/adc1_test.c +++ /dev/null @@ -1,35 +0,0 @@ -/* ADC1 Example - - This example code is in the Public Domain (or CC0 licensed, at your option.) - - Unless required by applicable law or agreed to in writing, this - software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. -*/ -#include -#include -#include -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "freertos/queue.h" -#include "driver/gpio.h" -#include "driver/adc.h" - -#define ADC1_TEST_CHANNEL (ADC1_CHANNEL_6) - -void adc1task(void* arg) -{ - // initialize ADC - adc1_config_width(ADC_WIDTH_12Bit); - adc1_config_channel_atten(ADC1_TEST_CHANNEL, ADC_ATTEN_11db); - while(1){ - printf("The adc1 value:%d\n",adc1_get_voltage(ADC1_TEST_CHANNEL)); - vTaskDelay(1000/portTICK_PERIOD_MS); - } -} - -void app_main() -{ - xTaskCreate(adc1task, "adc1task", 1024*3, NULL, 10, NULL); -} - diff --git a/examples/system/app_trace_to_host/README.md b/examples/system/app_trace_to_host/README.md index b15466dd8..4b2405aed 100644 --- a/examples/system/app_trace_to_host/README.md +++ b/examples/system/app_trace_to_host/README.md @@ -18,7 +18,7 @@ int sampling_period = 20; int i = 0; uint32_t sampling_start = esp_log_timestamp(); //this clock counts miliseconds do { - ESP_LOGI(TAG, "Sample:%d, Value:%d", ++i, adc1_get_voltage(ADC1_TEST_CHANNEL)); + ESP_LOGI(TAG, "Sample:%d, Value:%d", ++i, adc1_get_raw(ADC1_TEST_CHANNEL)); } while (esp_log_timestamp() - sampling_start < sampling_period); ``` diff --git a/examples/system/app_trace_to_host/main/app_trace_to_host_test.c b/examples/system/app_trace_to_host/main/app_trace_to_host_test.c index d0b8df8f4..65738cd26 100644 --- a/examples/system/app_trace_to_host/main/app_trace_to_host_test.c +++ b/examples/system/app_trace_to_host/main/app_trace_to_host_test.c @@ -87,7 +87,7 @@ int adc1_sample_and_show(int sampling_period) int i = 0; uint32_t sampling_start = esp_log_timestamp(); do { - ESP_LOGI(TAG, "Sample:%d, Value:%d", ++i, adc1_get_voltage(ADC1_TEST_CHANNEL)); + ESP_LOGI(TAG, "Sample:%d, Value:%d", ++i, adc1_get_raw(ADC1_TEST_CHANNEL)); } while (esp_log_timestamp() - sampling_start < sampling_period); return i; }