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) { // 等到与上一次不同时才输出log
int 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或ADC2
uint8_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。