本章介绍使用 ESP32-S3模数转换器(ADC),通过三种方式实现进行通道的电压采集。通过本章将学习到单通道 ADC 的使用。
本章分为如下几个小节:
24.1 ADC 简介
24.2 硬件设计
24.3 程序设计
24.4 下载验证

24.1.1 ADC 简介

        生活中接触到的大多数信息是随着时间连续变化的物理量,如声音、温度、压力等。表达这些信息的电信号,称为模拟信号(Analog Signal)。为了方便存储、处理,在计算机系统中,都是数字 0 和 1 信号,将模拟信号(连续信号)转换为数字信号(离散信号)的器件就叫模数转换器(Analog-to-Digital Convert, ADC)。
        ADC 转换器可分为: 并行比较型 A/D 转换器(FLASH ADC)、逐次比较型 A/D 转换器(SAR ADC)和双积分式 A/D 转换器(Double Integral ADC)。
        A/D 转换过程通常为 4 步:采样、保持、量化和编码,如下图所示。

图 24.1.1.1 A/D 转换过程图

        采样:把时间连续变化的信号变换为时间离散的信号。
        保持:保持采样信号,使有充分时间转换为数字信号。
        量化:把采样保持电路的输出信号用单位量化电压的整数倍表示。
        编码:把量化的结果用二进制代码表示。
        采样和保持通常是在采样-保持电路中完成,量化和编码通常在 ADC 数字编码电路中完成。

24.1.2 ESP32-S3 ADC 介绍

        ESP32-S3 集成了两个 12 位 SAR ADC, ADC1 和 ADC2,支持 20 个模拟通道输入。这 20 个模拟通道输入对应着具体的 IO,并不是随意的 IO 都有模拟输入功能,具备模拟输入功能的引脚如下表所示。

表 24.1.2.1 具备模拟输入功能的引脚

        ESP32-S3 的 ADC 模块的分辨率为 12 位,所以 AD转换后的值范围为 0~4095。由于 ESP32-S3 的工作电压为 3.3V,所以当 AD 值为 4095 时,对应的电压为 3.3V;当 AD 值为 0 时,对应的电压为 0V。对于 AD 值和电压值,这里就会有一个简单的关系,如下所示。

重要注意事项:

(1)ADC2 与 Wi-Fi 的冲突​​:
        ​​严禁​​在开启 Wi-Fi 或蓝牙时使用 ​​ADC2​​ 的任何函数(如 adc2_get_raw),因为 Wi-Fi 射频模块与 ADC2 共享硬件资源,同时使用会导致系统崩溃或读数失败。​​如需使用无线功能,请务必只使用 ADC1​​。
(2)精度、噪声与滤波​​:

  • ESP32 片内 ADC 易受电源噪声影响,读数可能存在波动;
  • 软件滤波​​:最简单的提高稳定性方法是​​多次采样取平均值​​;
#define SAMPLE_TIMES 64 // 采样次数
uint32_t adc_reading = 0;
for (int i = 0; i < SAMPLE_TIMES; i++)
{
    adc_reading += adc1_get_raw(ADC1_CHANNEL_6);
}
adc_reading /= SAMPLE_TIMES; // 计算平均值
  • 硬件滤波​​:在模拟输入引脚和 GND 之间并联一个 100nF 的电容有助于滤除高频噪声;
  • 非线性误差​​:ESP32 的 ADC 在电压范围的两端可能存在非线性。对精度要求高的应用,建议进行​​校准​​或使用外部 ADC 芯片。

(3)输入电压范围​​:
        绝对禁止​​输入超过 GPIO 引脚绝对最大额定值(通常为 3.6V)的电压,即使使用了衰减器,持续输入超过 3.3V 的电压也可能损坏芯片​。

24.2 硬件设计

24.2.1 例程功能
(1)本章实验功能简介:使用 ADC 采集通道 7(IO8)上面的电压,在 SPILCD 模块上面显示ADC 转换值以及换算成电压后的电压值。在 P3 端口中,使用 ,使得 IO8 连接到电位器上,然后将 ADC 采集到的数据和转换后的电压值在 SPILCD 屏中显示。用户可以通过调节电位器的旋钮改变电压值。 LED 闪烁,提示程序运行。
(2)LED 闪烁,指示程序正在运行。

24.2.2 硬件资源

        1. LED
                LED - IO1
        2. XL9555
                IIC_SDA-IO41
                IIC_SCL-IO42
        3. SPILCD
                CS-IO21
                SCK-IO12
                SDA-IO11
                DC-IO40(在 P5 端口,使用跳线帽将 IO_SET 和 LCD_DC 相连)
                PWR- IO1_3(XL9555)
                RST- IO1_2(XL9555)
        4. ADC
                ADC1_CHANNEL_7

24.2.3 原理图

        ADC 属于 ESP32-S3 内部资源,实际上我们只需要软件设置就可以正常工作,另外还需要将待测量的电压源连接到 ADC 通道上,以便 ADC 测量。本实验,我们通过 ADC1 的通道 7(IO8)来采集外部电压值,开发板有一个电位器,可调节的电压范围是: 0~3.3V。我们可以通过跳线帽将 IO8 与电位器连接,如下图所示:

图 24.2.3.1 IO8(对应 AIN 排针)与电位器引脚示意图

        使用杜邦线将 ADC 和 RV1 排针连接好后,并下载程序后,就可以用螺丝刀调节电位器变换多种电压值进行测量。
        如果想测量其它地方的电压值,只需要 1 根杜邦线,一端接到 AIN 排针上,另外一端就接要测试的电压点。一定要保证测试点的电压在 0~3.3V 的电压范围,否则可能烧坏我们的 ADC,甚至是整个主控芯片。

24.3 程序设计

24.3.1 程序流程图
        本实验的程序流程图:

图 24.3.1.1 ADC 实验程序流程图

24.3.2 ADC 函数解析
        ESP-IDF 提供了一套 API 来配置 ADC。要使用此功能,需要导入必要的头文件:

#include " esp_adc_cal.h"
#include " driver/adc"

        接下来,作者将介绍一些常用的 ESP32-S3 中的 ADC 函数,这些函数的描述及其作用如下:
(1)配置 ADC
        该函数用于配置 ADC 各项参数,其函数原型如下所示:

esp_err_t adc_digi_controller_configure(const adc_digi_configuration_t *config);

        该函数的形参描述,如下表所示:

形参 描述
config 指向 ADC 配置结构体的指针需自行定义,并根据 ADC 的配置参数填充结构体中的成员变量

表 24.3.2.1 函数 adc_digi_controller_configure()形参描述

        返回值: ESP_OK 表示配置成功。其他表示配置失败。
        该函数使用 adc_digi_pattern_config_t 以及 adc_digi_configuration_t 类型的结构体变量传入,该结构体的定义如下所示:

结构体 成员变量 可选参数
adc_digi_pattern_config_t atten:
配置 ADC 衰减程度, adc_atten_t 结构体为索引
ADC_ATTEN_DB_0,输入电压 0-1.1V
ADC_ATTEN_DB_2_5,输入电压 0-1.5V
ADC_ATTEN_DB_6,输入电压 0-2.2V
ADC_ATTEN_DB_11,输入电压 0-3.3V
channel:
配置 ADC 通道,
adc1_channel_t 结构
体为索引

ADC1_CHANNEL_0 ,
ADC1_CHANNEL_1,
ADC1_CHANNEL_2,
ADC1_CHANNEL_3,
ADC1_CHANNEL_4,

ADC1_CHANNEL_5,
ADC1_CHANNEL_6,
ADC1_CHANNEL_7,

unit:
配置 ADC 单元,adc_unit_t 结构体为索引
ADC_UNIT_1
ADC_UNIT_2
bit_width:
配置 ADC 位宽,adc_bitwidth_t 结构体为索引
ADC_BITWIDTH_DEFAULT
ADC_BITWIDTH_9
ADC_BITWIDTH_10
ADC_BITWIDTH_11
ADC_BITWIDTH_12
ADC_BITWIDTH_13

表 24.3.2.2 adc_digi_pattern_config_t 结构体参数值描述

结构体 成员变量 可选参数
adc_digi_configuration_t adc_pattern:
配置将要使用的每个
ADC 参数
无,该参数为结构体指针,当
adc_digi_pattern_config_t 结构体成员配
置完成后,以指针的方式将参数传递
至 adc_pattern 这一成员中

表 24.3.2.3 adc_digi_configuration_t 结构体参数值描述

        完成上述结构体参数配置之后,可以将结构传递给 adc_digi_controller_configure () 函数,用以实例化 ADC。
(2)读取 ADC 原始数据
        该函数用于读取 ADC 原始数据,其函数原型如下所示:

int adc1_get_raw(adc1_channel_t channel);

        该函数的形参描述,如下表所示:

形参 描述
channel ADC 通道

表 24.3.2.4 函数 adc1_get_raw()形参描述

        返回值:其他值表示 ADC 原始数值, -1 表示读取失败。

 需要注意:

        该函数对指定的 ADC1 通道执行一次​​阻塞式​​采样,并返回其原始数字转换值。意味着函数执行期间,CPU 会等待 ADC 转换完成后才返回结果。对于高速采样场景,建议使用 ADC 连续模式 (DMA)。

(3)校准 ADC(模数转换器)

        该函数用于​​校准 ADC(模数转换器)特性​​的核心函数,它能显著提高 ESP32 系列芯片 ADC 模块的测量精度。其函数原型如下所示:

esp_adc_cal_value_t esp_adc_cal_characterize(adc_unit_t adc_num, 
                                             adc_atten_t atten, 
                                             adc_bits_width_t bit_width, 
                                             uint32_t default_vref, 
                                             esp_adc_cal_characteristics_t *chars)

        该函数会根据您提供的参数以及芯片 eFuse 中存储的出厂校准值(如果有),来​​表征或“校准”指定 ADC 单元和衰减配置的特性​​。它会填充一个 esp_adc_cal_characteristics_t结构体,该结构体包含了将原始 ADC 读数转换为精确电压值所需的所有参数和公式(通常是线性关系:y = coeff_a * x + coeff_b)。
        ESP32 的片内 ADC 存在一定的​​非线性误差​​和​​基准电压(Vref)偏差​​(不同芯片个体的 Vref 可能在 1000mV ~ 1200mV 之间波动,而非常理想的 1100mV)。直接使用原始读数计算电压会导致显著误差。此函数通过校准过程,能有效补偿这些误差,获得更精确的电压值。

        该函数的形参描述,如下表所示:

参数

类型

说明

​​adc_num​​

adc_unit_t

​ADC 单元编号​​。可选 ADC_UNIT_1或 ADC_UNIT_2。​​特别注意:ADC2 与 Wi-Fi 射频硬件共享资源,启用 Wi-Fi 后无法使用 ADC2​

​​atten​​

adc_atten_t

​衰减配置​​。决定 ADC 的可测量电压范围。可选:
ADC_ATTEN_DB_0(约 0~950 mV)
ADC_ATTEN_DB_2_5(约 0~1250 mV)
ADC_ATTEN_DB_6(约 0~1750 mV)
ADC_ATTEN_DB_11(约 0~3100 mV,最常用)​

​​bit_width​​

adc_bits_width_t

​ADC 位宽(分辨率)​​。可选:
ADC_WIDTH_BIT_9
ADC_WIDTH_BIT_10
ADC_WIDTH_BIT_11
ADC_WIDTH_BIT_12​ (0-4095)

​​default_vref​​

uint32_t

​默认参考电压值(单位: mV)​​。当芯片 eFuse 中​​没有​​存储出厂校准值时,将使用此值作为回退。通常填入 1100

​​*chars​​

esp_adc_cal_characteristics_t*

​输出参数​​。指向一个 esp_adc_cal_characteristics_t结构体的指针,函数将填充此结构体。​​此内存需由用户在调用前预先分配好​。

表 24.3.2.5 函数 esp_adc_cal_characterize()形参描述

        返回值:esp_adc_cal_value_t枚举值,​​指示本次校准所使用的校准值类型​​,对于调试和了解校准精度来源非常有用:

  • ​​ESP_ADC_CAL_VAL_EFUSE_VREF​​:​​最佳情况​​。成功使用了芯片 eFuse 中存储的​​出厂校准 Vref 值​​。这是精度最高的方式;
  • ESP_ADC_CAL_VAL_EFUSE_TP​​:使用了 eFuse 中存储的​​两点校准值​​(仅用于线性模式),精度也较高;
  • ​​ESP_ADC_CAL_VAL_DEFAULT_VREF​​:eFuse 中未找到校准值,​​使用了您提供的default_vref参数​​。精度取决于您提供的默认值是否接近芯片的实际 Vref。

典型使用流程
        分配特性结构体​​:为 esp_adc_cal_characteristics_t分配内存(通常动态分配);
        配置 ADC​​:使用 adc1_config_width()和 adc1_config_channel_atten()等函数配置 ADC 的位宽和衰减;
        调用校准函数​​:调用 esp_adc_cal_characterize()来填充特性结构体;
        读取和转换​​:在循环中读取原始值 (adc1_get_raw()),并使用esp_adc_cal_raw_to_voltage()和之前得到的特性结构体将原始值转换为精确的电压值(单位: mV)。

(4)检查 eFuse 中 ADC 校准数据是否可用

        该函数用于检查 eFuse 中是否烧录了特定类型的 ADC 校准数据。需要包含头文件​​:#include "esp_adc_cal.h",函数原型如下:

esp_err_t esp_adc_cal_check_efuse(esp_adc_cal_value_t value_type)

        该函数的形参描述,如下表所示:

参数​

​类型​

​说明​

value_type

esp_adc_cal_value_t

要检查的 eFuse 校准值类型。

ESP_ADC_CAL_VAL_EFUSE_VREF​: 检查 eFuse 中是否烧录了 ​​Vref(参考电压)​​ 校准值;

ESP_ADC_CAL_VAL_EFUSE_TP​: 检查 eFuse 中是否烧录了 ​​Two Point(两点)​​ 校准值。这是 ESP32-S2、ESP32-C3、ESP32-S3、ESP32-C6 和 ESP32-H2 等型号上唯一支持的校准方案

表 24.3.2.6 函数 esp_adc_cal_check_efuse()形参描述

        返回值​​:

  • ESP_OK: 指定的校准类型在 eFuse 中可用;
  • ESP_ERR_NOT_SUPPORTED: 请求的校准类型不支持;
  • ESP_ERR_INVALID_VERSION: eFuse 中的编码版本不支持;
  • 其他错误码表示检查过程中遇到问题。

使用流程:

(1)检查 eFuse 支持情况​​:在调用 esp_adc_cal_characterize()之前,先使用此函数确认芯片是否支持 eFuse 校准。
(2)根据检查结果选择策略​​:
        如果返回 ESP_OK,则后续可以使用 eFuse 中的校准值进行高精度表征;
        如果返回错误(如 ESP_ERR_INVALID_VERSION),则表明 eFuse 中没有可用的校准数据,后续需要依赖用户提供的默认 Vref (default_vref) 进行表征,精度会有所降低。

代码示例:

#include "esp_adc_cal.h"

// 检查 eFuse 中的 Two Point 校准值是否可用
esp_err_t ret = esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_TP);
if (ret == ESP_OK) {
    printf("eFuse Two Point 校准值可用,精度较高\n");
} else if (ret == ESP_ERR_INVALID_VERSION) {
    printf("eFuse 未烧录校准数据,将使用默认 Vref\n");
} else if (ret == ESP_ERR_NOT_SUPPORTED) {
    printf("请求的校准类型不支持\n");
} else {
    printf("检查 eFuse 时发生错误: 0x%x\n", ret);
}

24.3.3 ADC 驱动解析

        在 IDF版14_adc 例程中,14_adc \components\BSP 路径下新增了一个 ADC 文件夹,分别用于存放 adc.c、adc.h 这两个文件。其中, adc.h 文件负责声明 ADC 相关的函数和变量,而adc.c 文件则实现了 ADC 的驱动代码。下面,我们将详细解析这两个文件的实现内容。
(1) adc.h 文件

#define ADC_ADCX_CHY    ADC1_CHANNEL_7 
#define DEFAULT_VREF    1100   // 默认参考电压 1100mV
#define ADC_WIDTH       ADC_WIDTH_BIT_12
#define ADC_ATTEN       ADC_ATTEN_DB_11

/* 函数声明 */
void adc_init(void);                                            /* 初始化ADC */
uint32_t adc_get_voltage_value(uint32_t ch, uint16_t times);   /* 获取ADC转换且进行校准后的结果 */

(2)adc.c 文件

/**
 * @brief       初始化ADC
 * @param       无
 * @retval      无
 */
void adc_init(void)
{
    adc_digi_pattern_config_t adc1_digi_pattern_config;         /* ADC1配置句柄 */
    adc_digi_configuration_t adc1_init_config;                  /* ADC1初始化句柄 */
    
    /* 配置ADC1 */
    adc1_digi_pattern_config.atten = ADC_ATTEN_DB_11;           /* 配置ADC衰减程度 */
    adc1_digi_pattern_config.channel = ADC_ADCX_CHY;            /* 配置ADC通道 */
    adc1_digi_pattern_config.unit = ADC_UNIT_1;                 /* 配置ADC单元 */
    adc1_digi_pattern_config.bit_width = ADC_BITWIDTH_12;       /* 配置ADC位宽 */
    adc1_init_config.adc_pattern = &adc1_digi_pattern_config;   /* 配置将要使用的每个ADC参数 */
    adc_digi_controller_configure(&adc1_init_config);           /* 配置ADC1 */
}

        从上面的代码中可以看出, ADC 的初始化函数中,配置了 ADC1 通道 7。

使用三种方法实现adc_get_result_average函数:

  • 获取原始值方法:

        ADC 驱动中,获取 ADC 转换结果的函数,如下所示:

/**
 * @brief       获取ADC转换且进行均值滤波后的结果
 * @param       ch      : 通道号, 0~9
 * @param       times   : 获取次数
 * @retval      通道ch的times次转换结果平均值
 */
uint32_t adc_get_result_average(uint32_t ch, uint32_t times)
{
    uint32_t temp_val = 0;
    uint8_t t;

    for (t = 0; t < times; t++) /* 获取times次数据 */
    {
        temp_val += adc1_get_raw(ch);
        vTaskDelay(5);
    }

    return temp_val / times;    /* 返回平均值 */
}

        以上函数用于获取 ADC 转换结果的函数,函数 adc_get_result_averagr()则是多次调用adc1_get_raw()获取多次 ADC 的转换结果,然后进行均值滤波。

  • 使用校准代码实现
esp_adc_cal_characteristics_t *adc_chars = NULL; // 特性结构体指针

/**
 * @brief       获取ADC转换且进行均值滤波后的结果
 * @param       ch      : 通道号, 0~9
 * @param       times   : 获取次数
 * @retval      通道ch的times次转换结果平均值
 */
uint32_t adc_get_result_average(uint32_t ch, uint16_t times)
{
	uint32_t temp_val = 0;
	if (0 == times) return 0;
	// 检查 eFuse 中的 Two Point 校准值是否可用
	esp_err_t ret = esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_TP_FIT);
	if (ret == ESP_OK) 
	{
		printf("eFuse Two Point 校准值可用,精度较高\n");
	} else if (ret == ESP_ERR_INVALID_VERSION) 
	{
		printf("eFuse 未烧录校准数据,将使用默认 Vref\n");
	} else if (ret == ESP_ERR_NOT_SUPPORTED) 
	{
		printf("请求的校准类型不支持\n");
	} else {
		printf("检查 eFuse 时发生错误: 0x%x\n", ret);
	}

	// 分配内存
	adc_chars = calloc(1, sizeof(esp_adc_cal_characteristics_t));
	// 配置ADC硬件
    adc1_config_width(ADC_WIDTH);
    adc1_config_channel_atten(ADC_ADCX_CHY, ADC_ATTEN);
	// 表征/校准ADC
    esp_adc_cal_value_t val_type = esp_adc_cal_characterize(ADC_UNIT_1, 
                                                            ADC_ATTEN, 
                                                            ADC_WIDTH, 
                                                            DEFAULT_VREF, 
                                                            adc_chars);
    // (可选)检查校准类型
    if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) 
	{
        printf("使用 eFuse Vref 校准值,精度最高\n");
    } 
	else if (val_type == ESP_ADC_CAL_VAL_DEFAULT_VREF) 
	{
        printf("使用默认 Vref 值\n");
    }
    
	for (uint16_t t = 0; t < times; t++)
	{
		// 读取和转换(在循环中)
		uint32_t raw_value = adc1_get_raw(ch);
		uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(raw_value, adc_chars); // 使用校准!
		printf("Raw: %d\tVoltage: %dmV\n", raw_value, voltage_mv);
		temp_val += voltage_mv;
		vTaskDelay(5);
	}
    free(adc_chars); // 释放内存

	return temp_val/times;
}

        对应main,c修改:

/**
 * @brief       程序入口
 * @param       无
 * @retval      无
 */
void app_main(void)
{
    uint16_t adcdata;
    esp_err_t ret;
    float voltage;
    
    ret = nvs_flash_init();                                     /* 初始化NVS */

    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }

    led_init();                                                 /* 初始化LED */
    i2c0_master = iic_init(I2C_NUM_0);                          /* 初始化IIC0 */
    spi2_init();                                                /* 初始化SPI2 */
    xl9555_init(i2c0_master);                                   /* 初始化XL9555 */
    lcd_init();                                                 /* 初始化LCD */
    adc_init();                                                 /* 初始化ADC */

    lcd_show_string(10, 50, 200, 16, 16, "ESP32", RED);
    lcd_show_string(10, 70, 200, 16, 16, "ADC TEST", RED);
    lcd_show_string(10, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    
    // lcd_show_string(10, 110, 200, 16, 16, "ADC1_CH7_VAL:0", BLUE);
    lcd_show_string(10, 130, 200, 16, 16, "ADC1_CH7_VOL:0.000V", BLUE);
    
    while(1)
    {
        adcdata = adc_get_result_average(ADC_ADCX_CHY, 10);
		voltage = ((float)adcdata/1000);
		adcdata = voltage;
        lcd_show_xnum(114, 130, adcdata, 1, 16, 0, BLUE);       /* 显示电压值的整数部分 */
        voltage -= adcdata;/* 把已经显示的整数部分去掉,留下小数部分 */
		voltage *= 1000; /* 小数部分乘以 1000 */
        lcd_show_xnum(130, 130, voltage, 3, 16, 0x80, BLUE);    /* 显示小数部分 */
    
        LED_TOGGLE();                                           /* LED翻转函数 */
        vTaskDelay(100);
    }
}
  • DMA (直接内存访问) 模式读取 ADC

        配置DMA模式读取ADC通常遵循以下步骤:

(1)初始化配置​​:创建并配置连续模式句柄 (adc_continuous_handle_cfg_t),指定DMA缓冲区大小和转换帧大小;
(2)​​设置转换参数​​:配置采样率、转换模式、数据格式等 (adc_continuous_config_t);
(3)设置通道规则​​:定义要扫描的每个通道的参数(衰减、位宽、所属单元等)(adc_digi_pattern_config_t);
(4)启动转换​​:调用 adc_continuous_start()开始连续采样;
(5)读取数据​​:在循环中调用 adc_digi_read_bytes()从DMA缓冲区获取数据;
(6)解析数据​​:根据设定的数据格式解析读取到的原始数据;
(7)停止与清理​​:完成采样后,停止转换并释放资源。

具体代码实现如下,对应main,c未修改:

/**
 * @brief       初始化ADC
 * @param       无
 * @retval      无
 */
void adc_init(void)
{
	adc_digi_pattern_config_t adc1_digi_pattern_config; /* ADC1配置句柄 */
	adc_digi_configuration_t adc1_init_config;			/* ADC1初始化句柄 */

	adc_digi_init_config_t adc_digi_config = {
		.max_store_buf_size = 102400,
		.conv_num_each_intr = 256,
		.adc1_chan_mask = BIT(ADC_ADCX_CHY), // 只启用目标通道
	};
	ESP_ERROR_CHECK(adc_digi_initialize(&adc_digi_config));

	/* 配置ADC1 */
	adc1_digi_pattern_config.atten = ADC_ATTEN_DB_11;		  /* 配置ADC衰减程度 */
	adc1_digi_pattern_config.channel = ADC_ADCX_CHY;		  /* 配置ADC通道 */
	adc1_digi_pattern_config.unit = ADC_UNIT_1;				  /* 配置ADC单元 */
	adc1_digi_pattern_config.bit_width = ADC_BITWIDTH_12;	  /* 配置ADC位宽 */
	adc1_init_config.pattern_num = 1;
	adc1_init_config.adc_pattern = &adc1_digi_pattern_config; /* 配置将要使用的每个ADC参数 */
	adc1_init_config.format = ADC_DIGI_OUTPUT_FORMAT_TYPE2;
	adc1_init_config.sample_freq_hz = 20000;
	adc1_init_config.conv_limit_en = 0;
	adc1_init_config.conv_mode = ADC_CONV_SINGLE_UNIT_1;
	ESP_ERROR_CHECK(adc_digi_controller_configure(&adc1_init_config)); /* 配置ADC1 */

	// 启动连续转换
	ESP_ERROR_CHECK(adc_digi_start());
}

/**
 * @brief       获取ADC转换且进行均值滤波后的结果
 * @param       ch      : 通道号, 0~9
 * @param       times   : 获取次数
 * @retval      通道ch的times次转换结果平均值
 */

#define READ_LENGTH 256
uint32_t adc_get_result_average(uint32_t ch, uint32_t times)
{
	// 循环读取并解析数据
	uint32_t raw_value = 0;
	uint32_t valid_samples = 0;
	uint8_t buffer[READ_LENGTH] = {0};
	uint32_t read_byte_num = 0;

	// 循环直到收集足够的样本
	while (valid_samples < times)
	{
		esp_err_t ret = adc_digi_read_bytes(buffer, READ_LENGTH, &read_byte_num, 100);
		printf("ret: 0x%x, read_byte_num: %d\n", ret, read_byte_num);

		if (ret == ESP_OK && read_byte_num > 0)
		{
			// 解析数据,只处理目标通道
			for (uint32_t i = 0; i < read_byte_num; i += SOC_ADC_DIGI_RESULT_BYTES)
			{
				adc_digi_output_data_t *p = (adc_digi_output_data_t *)&buffer[i];

				// 检查是否为目标通道
				if (p->type2.unit == 0 && p->type2.channel == ch)
				{
					uint16_t sample = p->type2.data & 0xFFF;  //数据没有校准
					raw_value += sample;
					valid_samples++;

					if (valid_samples >= times)
						break;
				}
			}
		}
		else if (ret == ESP_ERR_TIMEOUT)
		{
			// 短暂延迟后重试
			vTaskDelay(pdMS_TO_TICKS(1));
		}
		else
		{
			printf("ADC读取错误: 0x%x\n", ret);
			break;
		}
	}
	printf("valid_samples: %d\n", valid_samples);

	return (valid_samples > 0) ? (raw_value / valid_samples) : 0;
}

  24.3.4 CMakeLists.txt 文件
        打开本实验 BSP 下的 CMakeLists.txt 文件,其内容如下所示:

set(src_dirs
            ADC
            IIC
            LCD
            LED
            SPI
            XL9555)

set(include_dirs
            ADC
            IIC
            LCD
            LED
            SPI
            XL9555)

set(requires
            driver
            esp_adc)

idf_component_register(SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})

component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)

24.3.5 实验应用代码

        打开 main/main.c 文件,该文件定义了工程入口函数,名为 app_main。该函数代码如下。

i2c_obj_t i2c0_master;

/**
 * @brief       程序入口
 * @param       无
 * @retval      无
 */
void app_main(void)
{
    uint16_t adcdata;
    float voltage;
    esp_err_t ret;
    
    
    ret = nvs_flash_init();                                     /* 初始化NVS */

    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }

    led_init();                                                 /* 初始化LED */
    i2c0_master = iic_init(I2C_NUM_0);                          /* 初始化IIC0 */
    spi2_init();                                                /* 初始化SPI2 */
    xl9555_init(i2c0_master);                                   /* 初始化XL9555 */
    lcd_init();                                                 /* 初始化LCD */
    adc_init();                                                 /* 初始化ADC */

    lcd_show_string(10, 50, 200, 16, 16, "ESP32", RED);
    lcd_show_string(10, 70, 200, 16, 16, "ADC TEST", RED);
    lcd_show_string(10, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    
    lcd_show_string(10, 110, 200, 16, 16, "ADC1_CH7_VAL:0", BLUE);
    lcd_show_string(10, 130, 200, 16, 16, "ADC1_CH7_VOL:0.000V", BLUE);
    
    while(1)
    {
        adcdata = adc_get_result_average(ADC_ADCX_CHY, 20);
        lcd_show_xnum(114, 110, adcdata, 5, 16, 0, BLUE);       /* 显示ADC采样后的原始值 */

        voltage = (float)adcdata * (3.3 / 4095);                /* 获取计算后的带小数的实际电压值 */
        adcdata = voltage;                                      /* 赋值整数部分给adcx变量 */
        lcd_show_xnum(114, 130, adcdata, 1, 16, 0, BLUE);       /* 显示电压值的整数部分 */
        
        voltage -= adcdata;                                     /* 把已经显示的整数部分去掉,留下小数部分 */
        voltage *= 1000;                                        /* 小数部分乘以1000 */
        lcd_show_xnum(130, 130, voltage, 3, 16, 0x80, BLUE);    /* 显示小数部分 */
    
        LED_TOGGLE();                                           /* LED翻转函数 */
        vTaskDelay(100);
    }
}

        从上面的代码中可以看出,在进行完包括 ADC 的所有初始化工作后,便不断地获取 ADC1通道 7 进行 10 次转换后经过均值滤波后的结果,并将该原始值显示在 LCD 上,同时还通过该电压的原始值计算出了电压的模拟量,并在 LCD 上进行显示。

24.4 下载验证

        在完成编译和烧录操作后,可以看到 LCD 上实时刷新显示着 ADC1 通道 7(IO8 引脚)采集到电压的数字量和模拟量,此时可以通过杜邦线给 IO8 引脚接入不同的电压值(注意共地,且输入电压不能超过 3.3V,否则可能损坏开发板),可以看到 LCD 上显示的电压数字量和模拟量也随之改变。

图 24.4.1 SPI LCD 显示效果图
 

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐