Skip to content

Latest commit

 

History

History
564 lines (410 loc) · 34.5 KB

File metadata and controls

564 lines (410 loc) · 34.5 KB

ESP32外设-ADC入门

Note

对应视频教程:暂无
对应示例代码:暂无

本文档主要转载自Augtons正(单片机)的ESP32系列教程(关于ADC部分的梳理甚至比官方教程都清晰)。

一、介绍

ESP32(经典版)集成了两个 12位SAR(逐次逼近寄存器)adc,共支持18个测量通道。

  • 采用 2 个 SAR ADC,可支持同时采样与转换
  • 采用 5 个专用 ADC 控制器,可支持不同应用场景(比如,高性能、低功耗,或功率检测和峰值检测)
  • 支持 18 个模拟输入管脚
  • 可配置 12 位、11 位、10 位、9 位多种分辨率
  • 支持 DMA(1 个控制器支持)
  • 支持多通道扫描模式(2 个控制器支持)
  • 支持 Deep-sleep 模式运行(1 个控制器支持)
  • 支持 ULP 协处理器控制(2 个控制器支持)

1.1 ADC通道概览

ADC1:

  • 支持 8 个通道,包括:GPIO32 - GPIO39(并非按顺序) ADC2:

  • 支持 10 个通道,包括:GPIO0, GPIO2, GPIO4, GPIO12 - GPIO15, GOIO25 - GPIO27(并非按顺序)

ESP32 内置霍尔传感器,采用 ADC1 的通道0和3(GPIO36 和 GPIO39)。

注意

  1. ADC2模块也被Wi-Fi使用,当它们一起使用时,只有一个会被抢占,这意味着adc2_get_raw()可能会被阻塞,直到Wi-Fi停止,反之亦然。换言之,ADC2 不能与 WIFI 共用!
  2. 从一个没有连接到任何信号的引脚读取到的 ADC 值是 随机 的。

ADC通道对应关系如下(经典模组):

ADC1_CHANNEL_0	GPIO 36
ADC1_CHANNEL_1	GPIO 37
ADC1_CHANNEL_2	GPIO 38
ADC1_CHANNEL_3	GPIO 39
ADC1_CHANNEL_4	GPIO 32
ADC1_CHANNEL_5	GPIO 33
ADC1_CHANNEL_6	GPIO 34
ADC1_CHANNEL_7	GPIO 35

ADC2_CHANNEL_0	ESP32:GPIO 4	
ADC2_CHANNEL_1	ESP32:GPIO 0	
ADC2_CHANNEL_2	ESP32:GPIO 2	
ADC2_CHANNEL_3	ESP32:GPIO 15	
ADC2_CHANNEL_4	ESP32:GPIO 13	
ADC2_CHANNEL_5	ESP32:GPIO 12	
ADC2_CHANNEL_6	ESP32:GPIO 14	
ADC2_CHANNEL_7	ESP32:GPIO 27	
ADC2_CHANNEL_8	ESP32:GPIO 25	
ADC2_CHANNEL_9	ESP32:GPIO 26	

1.2 ADC 衰减

ADC模块 能读取电压的范围(量程)有限,因此我们一般给某个 ADC 通道配置一定的衰减,使其读取更大的电压。但更大的量程会导致更小的精度。因此根据 ADC 的应用场景,选择适当的衰减级别十分必要。

ESP32 的每一个通道都有提供了4个级别的衰减等级,不同的衰减等级对于的量程在下表列出:注意,下表中的 “推荐范围” 并不是量程 ,而是在某衰减等级下测量最精确的推荐测量范围

1.2 ADC校准与减小测量误差

减少噪声

ESP32 ADC 对噪声敏感,可能导致 ADC 读数出现较大偏差。根据不同使用场景,要减少噪声影响,你可能需要将旁路电容(如 100 nF 陶瓷电容)连接到 ADC 使用的输入管脚。此外,也可以通过多次采样,进一步减轻噪声的影响。

图中展示了连接电容以及 64 次采样对噪声的抑制效果。其中纵轴(ADC Reading)代表不同方式读取到的ADC值,横轴(Sample Number)是采样次数。

ADC 校准

关于ADC校准的库为esp_adc_cal.h

#include "esp_adc/adc_cali.h"

个库提供 API 函数用于校正基准ADC 参考电压,ADC参考电压为 1100 m V。 对于不同的参考电压,ADC 值与输入电压值(待测电压)的关系不同。关系如下图: ​

上图列出了参考电压分别在:V r e f = =1070mV(蓝色) 和 V r e f = =1160mV(橙色) 下的 ADC 值和待测电压 Voltage 的关系。

我们把这条拟合曲线称为 ADC 模块(在某参考电压下)的 ADC-Voltage 特征曲线

在实际应用中,我们调用esp_adc_cal.h库提供的 API 函数去求得指定参考电压下的 ADC-Voltage 特征曲线,并利用这一条曲线去将 ADC 测量值转换为欲测量的电压Voltage。开发者可以选择自定义参考电压值,也可以利用ESP32 内部 eFuse(一次性可编程存储器)中储存的出厂参考电压校准值去获取这个曲线。

使用下面的命令:

%IDF_PATH%/components/esptool_py/esptool/espefuse.py --port COMx adc_info

效果:

除此之外,ESP32 内部的参考电压也可以手动测量,方法是将此电压输出到(路由到)某个GPIO口上,然后手动测量此GPIO口和GND接口之间的电压,就是eFuse内部的参考电压。将参考电压路由到GPIO的方法是调用API函数adc_vref_to_gpio(),参数是想要输出电压的GPIO口编号。必须是ADC2的通道IO口之一,因为ESP32只支持将参考电压路由到 ADC2 上

/* 将参考电压路由到GPIO25上 */ 
adc_vref_to_gpio(ADC_UNIT_2, GPIO_NUM_25);

二、使用

单次转换模式使用

1、 资源分配 :获取 ADC 句柄,以及回收资源

需要配置结构体 adc_oneshot_unit_init_cfg_t

参数为:

完成 ADC 初始配置后,使用已设置的初始配置结构体 adc_oneshot_unit_init_cfg_t 调用 adc_oneshot_new_unit()。如果分配成功,该函数将返回 ADC 单元实例句柄。

该函数可能因参数无效、内存不足等原因返回错误代码。比如,当要分配的 ADC 实例已经注册时,该函数会返回 ESP_ERR_NOT_FOUND 错误。可用 ADC 数量可通过 SOC_ADC_PERIPH_NUM 查看

如果不再需要先前创建的 ADC 单元实例,请调用 adc_oneshot_del_unit() 回收该实例,相关的硬件和软件资源也会回收。

创建 ADC 单元实例句柄示例:

adc_oneshot_unit_handle_t adc1_handle;
adc_oneshot_unit_init_cfg_t init_config1 = {
    .unit_id = ADC_UNIT_1,
    .ulp_mode = ADC_ULP_MODE_DISABLE,
};
ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config1, &adc1_handle));

回收 ADC 单元实例示例

ESP_ERROR_CHECK(adc_oneshot_del_unit(adc1_handle));

设置 adc_oneshot_chan_cfg_t 配置 ADC IO 以测量模拟信号

参数:

使用上述配置结构体调用 adc_oneshot_config_channel(),并指定要配置的 ADC 通道。函数 adc_oneshot_config_channel() 支持多次调用,以配置不同的 ADC 通道。驱动程序将在内部保存每个通道的配置。

示例:配置两个 ADC 通道

adc_oneshot_chan_cfg_t config = {
    .bitwidth = ADC_BITWIDTH_DEFAULT,
    .atten = ADC_ATTEN_DB_12,
};
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, EXAMPLE_ADC1_CHAN0, &config));
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, EXAMPLE_ADC1_CHAN1, &config));

调用 adc_oneshot_read() 可以获取 ADC 通道的原始转换结果。

通过该函数获取的 ADC 转换结果为原始数据。可以使用以下公式,根据 ADC 原始结果计算电压:

Vout = Dout * Vmax / Dmax       
Vout 数字输出结果,代表电压。
Dout ADC 原始数字读取结果。
Vmax 可测量的最大模拟输入电压,与 ADC 衰减相关,请参考 技术参考手册 > 片上传感器与模拟信号处理
Dmax 输出 ADC 原始数字读取结果的最大值,即 2^位宽,位宽即之前配置的 adc_digi_pattern_config_t::bit_width
读取原始结果示例:
ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, EXAMPLE_ADC1_CHAN0, &adc_raw[0][0]));
ESP_LOGI(TAG, "ADC%d Channel[%d] Raw Data: %d", ADC_UNIT_1 + 1, EXAMPLE_ADC1_CHAN0, adc_raw[0][0]);

ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, EXAMPLE_ADC1_CHAN1, &adc_raw[0][1]));
ESP_LOGI(TAG, "ADC%d Channel[%d] Raw Data: %d", ADC_UNIT_1 + 1, EXAMPLE_ADC1_CHAN1, adc_raw[0][1]);

注:

  • 随机数生成器 (RNG) 以 ADC 为输入源。使用 ADC 单次转换模式驱动从 RNG 生成随机数时,随机性会减弱。
  • 一个 ADC 单元每次只能在一种操作模式下运行,可以是连续模式或单次模式。adc_oneshot_start() 提供了保护措施。
  • Wi-Fi 也使用 ADC2,adc_oneshot_read() 提供了 Wi-Fi 驱动与 ADC 单次转换模式驱动间的保护。
  • ESP32-DevKitC:GPIO0 已用于自动烧录功能,不能用于 ADC 单次转换模式。
  • ESP-WROVER-KIT:GPIO0、GPIO2、GPIO4 和 GPIO15 已有其他用途,不能用于 ADC 单次转换模式。

连续转换模式使用

ADC 连续转换模式驱动由多个转换帧组成。

1、 资源分配

首先设置配置结构体 adc_continuous_handle_cfg_t,创建 ADC 连续转换模式驱动的句柄,参数如下:

完成以上 ADC 配置后,使用已设置的配置结构体 adc_continuous_handle_cfg_t 调用 adc_continuous_new_handle()来分配资源。

如果不再使用 ADC 连续转换模式驱动,请调用 adc_continuous_deinit() 将驱动去初始化。

配置示例:

adc_continuous_handle_t handle = NULL;
adc_continuous_handle_cfg_t adc_config = {
    .max_store_buf_size = 1024,
    .conv_frame_size = 100,
};
ESP_ERROR_CHECK(adc_continuous_new_handle(&adc_config));
2、 配置 ADC

初始化 ADC 连续转换模式驱动后,设置 adc_continuous_config_t 配置 ADC IO,测量模拟信号:

设置 adc_digi_pattern_config_t

最后,使用上述配置结构体,调用 adc_continuous_config()

配置示例:

    //  ADC IO
    adc_continuous_config_t dig_cfg = {
        .sample_freq_hz = 20 * 1000,         // 采样频率
        .conv_mode = ADC_CONV_SINGLE_UNIT_1,  // 转换模式
        .format = ADC_DIGI_OUTPUT_FORMAT_TYPE1,    // 输出格式
    };

    adc_digi_pattern_config_t adc_pattern[SOC_ADC_PATT_LEN_MAX] = {0};
    dig_cfg.pattern_num = channel_num;   // 通道数量
    for (int i = 0; i < channel_num; i++) {
        adc_pattern[i].atten = ADC_ATTEN_DB_0;  // ADC 衰减
        adc_pattern[i].channel = channel[i] & 0x7;  // 通道
        adc_pattern[i].unit = ADC_UNIT_1;			// ADC单元
        adc_pattern[i].bit_width = SOC_ADC_DIGI_MAX_BITWIDTH; // 位宽

		// 打印配置信息
        ESP_LOGI(TAG, "adc_pattern[%d].atten is :%"PRIx8, i, adc_pattern[i].atten);
        ESP_LOGI(TAG, "adc_pattern[%d].channel is :%"PRIx8, i, adc_pattern[i].channel);
        ESP_LOGI(TAG, "adc_pattern[%d].unit is :%"PRIx8, i, adc_pattern[i].unit);
    }

	// 要使用的 ADC 通道的配置列表
    dig_cfg.adc_pattern = adc_pattern;
3、 ADC 控制

调用 adc_continuous_start(),将使 ADC 开始从配置好的 ADC 通道测量模拟信号,并生成转换结果。

调用 adc_continuous_stop() 则会停止 ADC 转换。

调用 adc_continuous_register_event_callbacks(),可以将自己的函数链接到驱动程序的 ISR 中。通过 adc_continuous_evt_cbs_t 可查看所有支持的事件回调。

转换完成事件

当驱动程序完成一次转换后,会触发 adc_continuous_evt_cbs_t::on_conv_done 事件,并填充事件数据。事件数据包含一个指向转换帧缓冲区的指针,以及转换帧缓冲区大小。要了解事件数据结构,请参阅 adc_continuous_evt_data_t

调用 adc_continuous_start() 启动 ADC 连续转换,调用 adc_continuous_read() 可以获取 ADC 通道的转换结果。注意提供缓冲区,获取原始结果。

函数 adc_continuous_read() 每次都会尝试以期望长度读取转换结果。 参数:

  • handle -- [in] ADC连续模式驱动句柄
  • buf -- **[out]**要从ADC读取的转换结果缓冲区。
  • length_max -- [in] 从ADC读取的转换结果的预期长度(以字节为单位)。
  • out_length -- [out] 通过此API从ADC读取的转换结果的实际长度(以字节为单位)。
  • timeout_ms -- [in] 通过此API等待数据的时间(以毫秒为单位)。

六、ADC 示例

单次转换示例程序:

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_adc/adc_oneshot.h"

#define EXAMPLE_ADC1_CHAN0 ADC_CHANNEL_6  // 根据您的硬件配置选择合适的通道
#define EXAMPLE_ADC1_CHAN1 ADC_CHANNEL_7  // 根据您的硬件配置选择合适的通道

static const char* TAG = "ADC_ONESHOT_EXAMPLE";

void app_main(void)
{
    adc_oneshot_unit_handle_t adc1_handle;
    adc_oneshot_unit_init_cfg_t init_config1 = {
        .unit_id = ADC_UNIT_1,
        .ulp_mode = ADC_ULP_MODE_DISABLE,
    };
    ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config1, &adc1_handle));

    adc_oneshot_chan_cfg_t config = {
        .bitwidth = ADC_BITWIDTH_DEFAULT,
        .atten = ADC_ATTEN_DB_11,  // 设置适当的衰减以匹配您的输入电压范围
    };

    // 配置两个通道
    ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, EXAMPLE_ADC1_CHAN0, &config));
    ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, EXAMPLE_ADC1_CHAN1, &config));

    int adc_raw[2];

    // 对每个通道进行单次转换并读取结果
    for (int i = 0; i < 10; i++) {
        ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, EXAMPLE_ADC1_CHAN0, &adc_raw[0]));
        ESP_LOGI(TAG, "ADC1 Channel[%d] Raw Data: %d", EXAMPLE_ADC1_CHAN0, adc_raw[0]);

        ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, EXAMPLE_ADC1_CHAN1, &adc_raw[1]));
        ESP_LOGI(TAG, "ADC1 Channel[%d] Raw Data: %d", EXAMPLE_ADC1_CHAN1, adc_raw[1]);

        vTaskDelay(pdMS_TO_TICKS(1000));  // 等待1秒再次读取
    }

    // 回收资源
    ESP_ERROR_CHECK(adc_oneshot_del_unit(adc1_handle));
}

为通道6接上1V电压:

串口输出结果:

连续转换示例程序:

参考:https://github.com/espressif/esp-idf/blob/fdb7a43752633560c73ee079d512c0c13808456f/examples/peripherals/adc/continuous_read/main/continuous_read_main.c

#include <string.h>
#include <stdio.h>
#include "sdkconfig.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_adc/adc_continuous.h"

#define _EXAMPLE_ADC_UNIT_STR(unit)         #unit   
// C预处理器的字符串化操作符 #,它可以将宏参数转换为字符串常量。如果传递 ADC_UNIT 给 _EXAMPLE_ADC_UNIT_STR,它会生成字符串 "ADC_UNIT"。
#define EXAMPLE_ADC_UNIT_STR(unit)          _EXAMPLE_ADC_UNIT_STR(unit)
// 宏嵌套

// 用于从 adc_digi_output_data_t 结构体中提取通道号和数据值。
#define EXAMPLE_ADC_GET_CHANNEL(p_data)     ((p_data)->type1.channel)
#define EXAMPLE_ADC_GET_DATA(p_data)        ((p_data)->type1.data)


#define ADC_UNIT                    ADC_UNIT_1


// ADC通道
//static adc_channel_t channel[2] = {ADC_CHANNEL_6, ADC_CHANNEL_7};
static adc_channel_t channel[1] = {ADC_CHANNEL_6};


static TaskHandle_t s_task_handle;
static const char *TAG = "ADC_CONTINUOUS";

// ADC连续模式的事件回调(一个转换帧完成时)
static bool IRAM_ATTR s_conv_done_cb(adc_continuous_handle_t handle, const adc_continuous_evt_data_t *edata, void *user_data)
{
    BaseType_t mustYield = pdFALSE;
    //Notify that ADC continuous driver has done enough number of conversions
	//vTaskNotifyGiveFromISR 是 FreeRTOS 提供的一个函数,它允许从中断服务例程(ISR)安全地向任务发送通知
    vTaskNotifyGiveFromISR(s_task_handle, &mustYield);

    return (mustYield == pdTRUE);
}

// adc初始化
static void continuous_adc_init(adc_channel_t *channel, uint8_t channel_num, adc_continuous_handle_t *out_handle)
{
	// 创建一个ADC连续模式的句柄
    adc_continuous_handle_t handle = NULL;

	// 配置ADC连续模式的参数
    adc_continuous_handle_cfg_t adc_config = {
        .max_store_buf_size = 1024,  			// 最大存储缓冲区大小
        .conv_frame_size = 256,	// 转换帧大小
    };
    ESP_ERROR_CHECK(adc_continuous_new_handle(&adc_config, &handle));

    //  ADC IO
    adc_continuous_config_t dig_cfg = {
        .sample_freq_hz = 20 * 1000,         // 采样频率
        .conv_mode = ADC_CONV_SINGLE_UNIT_1,  // 转换模式
        .format = ADC_DIGI_OUTPUT_FORMAT_TYPE1,    // 输出格式
    };

    adc_digi_pattern_config_t adc_pattern[SOC_ADC_PATT_LEN_MAX] = {0};
    dig_cfg.pattern_num = channel_num;   // 通道数量
    for (int i = 0; i < channel_num; i++) {
        adc_pattern[i].atten = ADC_ATTEN_DB_11;  // ADC 衰减
        adc_pattern[i].channel = channel[i] & 0x7;  // 通道
        adc_pattern[i].unit = ADC_UNIT;			// ADC单元
        adc_pattern[i].bit_width = SOC_ADC_DIGI_MAX_BITWIDTH; // 位宽

		// 打印配置信息
		// - PRIx8 是一个预处理宏,定义在 C 语言的标准库头文件 <inttypes.h> 中。它用于以可移植的方式格式化输出 uint8_t 类型的数据为十六进制形式。
        ESP_LOGI(TAG, "adc_pattern[%d].atten is :%"PRIx8, i, adc_pattern[i].atten);
        ESP_LOGI(TAG, "adc_pattern[%d].channel is :%"PRIx8, i, adc_pattern[i].channel);
        ESP_LOGI(TAG, "adc_pattern[%d].unit is :%"PRIx8, i, adc_pattern[i].unit);
    }

	// 要使用的 ADC 通道的配置列表
    dig_cfg.adc_pattern = adc_pattern;
    ESP_ERROR_CHECK(adc_continuous_config(handle, &dig_cfg));

    *out_handle = handle;
}

void app_main(void)
{
    esp_err_t ret; // 返回状态
    uint32_t ret_num = 0;  // 转换完成的数据数量
	// 定义接收数组
    uint8_t result[256] = {0};
	// 初始化数组,填充为0xcc
    memset(result, 0xcc, 256);

	//获取app_mian任务的句柄。
    s_task_handle = xTaskGetCurrentTaskHandle();

	// 初始化ADC
    adc_continuous_handle_t handle = NULL;
    continuous_adc_init(channel, sizeof(channel) / sizeof(adc_channel_t), &handle);

	// 事件回调
    adc_continuous_evt_cbs_t cbs = {
		// 当一个转换帧完成时,触发此事件:s_conv_done_cb
        .on_conv_done = s_conv_done_cb,
    };

	// 注册事件回调
    ESP_ERROR_CHECK(adc_continuous_register_event_callbacks(handle, &cbs, NULL));
	// 启动ADC连续模式
    ESP_ERROR_CHECK(adc_continuous_start(handle));

    while (1) {

        /**
         * This is to show you the way to use the ADC continuous mode driver event callback.
         * This `ulTaskNotifyTake` will block when the data processing in the task is fast.
         * However in this example, the data processing (print) is slow, so you barely block here.
         *
         * Without using this event callback (to notify this task), you can still just call
         * `adc_continuous_read()` here in a loop, with/without a certain block timeout.
         */
		//  ulTaskNotifyTake() 等待通知
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

		//	生成字符串
        char unit[] = EXAMPLE_ADC_UNIT_STR(ADC_UNIT);

        while (1) {
			// 读取ADC数据
            ret = adc_continuous_read(handle, result, 256, &ret_num, 0);
			// 读取成功
            if (ret == ESP_OK) {
				// 显示读取操作的返回状态和实际读取到的数据字节数
                ESP_LOGI("TASK", "ret is %x, ret_num is %"PRIu32" bytes", ret, ret_num);
				// 循环遍历读取到的数据,解析每个ADC数据项,并打印出来
				// - 循环以 SOC_ADC_DIGI_RESULT_BYTES 为步长迭代,这个常量定义了每个ADC数据项的字节大小。
				// - adc_digi_output_data_t 是一个结构体类型,用于解析ADC数据项。
				// - EXAMPLE_ADC_GET_CHANNEL(p) 和 EXAMPLE_ADC_GET_DATA(p) 是宏,用于从 adc_digi_output_data_t 结构体中提取通道号和数据值。
                for (int i = 0; i < ret_num; i += SOC_ADC_DIGI_RESULT_BYTES) {
                    adc_digi_output_data_t *p = (adc_digi_output_data_t*)&result[i];
                    uint32_t chan_num = EXAMPLE_ADC_GET_CHANNEL(p);
                    uint32_t data = EXAMPLE_ADC_GET_DATA(p);
                    /*检查通道编号验证,如果通道编号超过最大通道,则数据无效 */
					// - PRIu32 是 C 语言标准库中的宏,它用于以可移植的方式格式化输出 uint32_t 类型的数据。
                    if (chan_num < SOC_ADC_CHANNEL_NUM(ADC_UNIT)) {
                        ESP_LOGI(TAG, "Unit: %s, Channel: %"PRIu32", Value: %"PRIu32, unit, chan_num, data);
                    } else {
                        ESP_LOGW(TAG, "Invalid data [%s_%"PRIu32"_%"PRIu32"]", unit, chan_num, data);
                    }
                }
                /**
                 * Because printing is slow, so every time you call `ulTaskNotifyTake`, it will immediately return.
                 * To avoid a task watchdog timeout, add a delay here. When you replace the way you process the data,
                 * usually you don't need this delay (as this task will block for a while).
                 */
                vTaskDelay(1);
            } else if (ret == ESP_ERR_TIMEOUT) {
                //We try to read `EXAMPLE_READ_LEN` until API returns timeout, which means there's no available data
                break;
            }
        }
    }
	// 停止ADC连续模式
    ESP_ERROR_CHECK(adc_continuous_stop(handle));
    ESP_ERROR_CHECK(adc_continuous_deinit(handle));
}

接线:

效果:

参考链接

  1. https://blog.csdn.net/m0_50064262/article/details/118817032
  2. https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/peripherals/adc_oneshot.html#_CPPv427adc_oneshot_unit_init_cfg_t
  3. extension://bfdogplmndidlpjfhoijckpakkdjkkil/pdf/viewer.html?file=https%3A%2F%2Fwww.espressif.com%2Fsites%2Fdefault%2Ffiles%2Fdocumentation%2Fesp32_technical_reference_manual_cn.pdf
  4. https://github.com/espressif/esp-idf/tree/fdb7a43/examples/peripherals/adc/oneshot_read