1. 简介
ESP32带有2个12位分辨率的ADC,配置有5个控制器进行控制;其中2个支持高性能多通道扫描、2个支持深度睡眠模式下的低功耗运行,另外1个专门用于PWDET / PKDET(功率检测和峰值监测)。
PWDET/PKDET控制器仅供Wi-Fi内部使用。如果Wi-Fi正在使用SAR ADC2,则用户无法使用 SAR ADC2 测量管脚的模拟信号。
1.1 RTC ADC控制器
该控制器主要在低功耗模式下使用,通过该控制器可以使设备在深度睡眠模式下进行ADC信号的采集;同时ULP可以访问该控制器,在低功耗模式下实现更高效的ADC数据采集。
1.2 DIG ADC控制器
该控制器的功能相比上面会强大很多,具有更快的时钟频率,并且支持DMA进行数据传输。除了支持常见的单次转换外,还支持连续扫描功能;连续扫描功能支持3种通道模式——单通道扫描、双通道扫描和双通道交替扫描。
如果使用连续扫描模式,那么是默认开启DMA传输的,DMA帧的格式有两种。
ch_sel[15:12] | data[11:0] |
---|---|
ADC通道号 | ADC数据(最高12位) |
sar_sel[15] | ch_sel[14:11] | data[10:0] |
---|---|---|
ADC号 | ADC通道号 | ADC数据(最高11位) |
1.3 滤波器
从上面的框图中可以知道ADC1中带有一个硬件滤波器,但官方文档中并没有介绍该滤波器的使用方法,固件库中倒是有对应的代码实现。观察后发现这个是IIR滤波器,然后配置项只有一个系数,因为不知道这个IIR滤波器是I型、II型还是并联型,所以不好计算系数。
如果后面官方更新了文档,我也会更新这部分内容。
2. 例程
例程中会配置ADC实现连续扫描转换DAC的输出管脚,DAC每500毫秒改变一次管脚电平。
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "esp_adc/adc_continuous.h"
#include "esp_adc/adc_cali.h"
#include "driver/dac_oneshot.h"
#include "esp_adc/adc_cali_scheme.h"
#include "soc/soc_caps.h"#include <string.h>#define TAG "app"adc_continuous_handle_t adc_handle = NULL;
adc_cali_handle_t adc_cali_handle = NULL;
dac_oneshot_handle_t dac_handle = NULL;
SemaphoreHandle_t data_ready_noti = NULL;static bool IRAM_ATTR adc_conv_done_cb(adc_continuous_handle_t handle, const adc_continuous_evt_data_t *edata, void *user_data)
{BaseType_t higher_task_woken = pdFALSE;xSemaphoreGiveFromISR(data_ready_noti, &higher_task_woken);return higher_task_woken == pdTRUE;
}static void adc_read_task(void *args)
{uint8_t *buf = (uint8_t *) malloc(1024);uint32_t len = 0;uint16_t last_raw = 0;while (1) {if (pdTRUE == xSemaphoreTake(data_ready_noti, portMAX_DELAY)) {memset(buf, 0, 1024);ESP_ERROR_CHECK(adc_continuous_read(adc_handle, buf, 1024, &len, 1000));for (int i = 0; i < len; i += SOC_ADC_DIGI_RESULT_BYTES) {adc_digi_output_data_t *p = (adc_digi_output_data_t*)&buf[i];if (abs(last_raw - p->type1.data) > 100) { // 等到与上一次不同时才输出logint voltage = 0;ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc_cali_handle, p->type1.data, &voltage));ESP_LOGI(TAG, "channel %d get raw %d voltage %d mV", p->type1.channel, p->type1.data, voltage);last_raw = p->type1.data;}}}vTaskDelay(10 / portTICK_PERIOD_MS); // 因为ADC的采集速度高于任务调度的速度,所以要定时放弃CPU占用}
}int app_main()
{/* 初始化DAC */dac_oneshot_config_t dac_cfg = {.chan_id = DAC_CHAN_0,};ESP_ERROR_CHECK(dac_oneshot_new_channel(&dac_cfg, &dac_handle));/* 初始化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, &adc_handle));/* 初始化ADC连续转换 */adc_digi_pattern_config_t adc_pattern[1] = {{.atten = ADC_ATTEN_DB_12, // 衰减12dB.channel = ADC_CHANNEL_0, // 通道0.unit = ADC_UNIT_1, // ADC1.bit_width = ADC_BITWIDTH_12, // 12位分辨率}};adc_continuous_config_t dig_cfg = {.sample_freq_hz = SOC_ADC_SAMPLE_FREQ_THRES_LOW, // 20kHz.conv_mode = ADC_CONV_SINGLE_UNIT_1, // 单通道,ADC1.format = ADC_DIGI_OUTPUT_FORMAT_TYPE1, // 1型ADC DMA数据.pattern_num = 1, // 1个ADC通道使用.adc_pattern = adc_pattern,};ESP_ERROR_CHECK(adc_continuous_config(adc_handle, &dig_cfg));/* 注册ADC校准 */adc_cali_line_fitting_efuse_val_t cali_val;ESP_ERROR_CHECK(adc_cali_scheme_line_fitting_check_efuse(&cali_val));adc_cali_line_fitting_config_t cali_cfg = {.unit_id = ADC_UNIT_1,.bitwidth = ADC_BITWIDTH_12,.atten = ADC_ATTEN_DB_12,.default_vref = cali_val == ADC_CALI_LINE_FITTING_EFUSE_VAL_DEFAULT_VREF ? 1100 : 0,};ESP_ERROR_CHECK(adc_cali_create_scheme_line_fitting(&cali_cfg, &adc_cali_handle));/* 注册回调 */data_ready_noti = xSemaphoreCreateBinary();xSemaphoreTake(data_ready_noti, 0);adc_continuous_evt_cbs_t cbs = {.on_conv_done = adc_conv_done_cb,};ESP_ERROR_CHECK(adc_continuous_register_event_callbacks(adc_handle, &cbs, NULL));/* 创建任务 */xTaskCreate(adc_read_task, "adc_read_task", 2048, NULL, 5, NULL);/* 启动ADC */ESP_ERROR_CHECK(adc_continuous_start(adc_handle));uint8_t val = 0;while (1) {ESP_ERROR_CHECK(dac_oneshot_output_voltage(dac_handle, val));vTaskDelay(500 / portTICK_PERIOD_MS);val += 10;}
}
1. 初始化DAC
例程中使用DAC生成不同的电压信号,这里使用的是DAC的单次模式,配置结构体如下。
typedef struct {dac_channel_t chan_id;
} dac_oneshot_config_t;
- chan_id:DAC通道号(0-1)。
2. 初始化ADC句柄
初始化结构体如下。
typedef struct {uint32_t max_store_buf_size;uint32_t conv_frame_size;struct {uint32_t flush_pool: 1;} flags;
} adc_continuous_handle_cfg_t;
- max_store_buf_size:缓冲区大小;
- conv_frame_size:转换帧大小,必须是SOC_ADC_DIGI_DATA_BYTES_PER_CONV的整数倍;
- flush_pool:缓冲区满时,是否清空。
3. 初始化ADC转换通道
初始化结构体如下。
typedef struct {uint32_t pattern_num;adc_digi_pattern_config_t *adc_pattern;uint32_t sample_freq_hz;adc_digi_convert_mode_t conv_mode;adc_digi_output_format_t format;
} adc_continuous_config_t;
- pattern_num:需要配置的通道数;
- adc_pattern:每个通道的配置;
typedef struct {uint8_t atten; // 信号衰减uint8_t channel; // 通道uint8_t unit; // ADC1或ADC2uint8_t bit_width; // 分辨率
} adc_digi_pattern_config_t;
- sample_freq_hz:采样率(20kHz-2MHz),单位HZ;
- conv_mode:转换模式;
- format:DMA数据格式。
4. 注册回调函数和数据处理任务
每次ADC成功转换一个转换帧会产生中断,通过注册回调函数可以在中断时提示对应的任务处理数据。数据处理任务中会读取ADC的每一个转换结果,如果结果与上一次的输出不同,就打印一次log。
这里有一个处理上的细节,因为ADC的处理速度太快了,以至于可能刚处理完,信号量又来临,导致空闲任务一直得不到执行,然后看门狗超时;所以一处理完数据就延时几毫秒,让空闲任务能运行。另外就是对比前后两次ADC数据时会有一定的余量,因为ADC的数据是不稳定的。
5. 注册ADC校准
这个功能主要用于原始值和电压值的转换,里面同时包含了校准操作。初始化结构体中的default_verf填写的是ADC的参考电压值,这个一般是由eFuse里面的特定位决定的,但如果eFuse里面没有配置才要手动配置,默认的参考值是1.1V。
6. 启动ADC转换
最后使能ADC转换。
DAC的通道0对应的是IO25,ADC1的通道0对应的是IO36,把两个管脚接一起。下面是程序运行的log。