12. ADC

一、ADC简介

  生活中接触到的大多数信息是醉着时间连续变化的物理量,如声音、温度、压力等。表达这些信息的电信号,称为 模拟信号(Analog Signal)。为了方便存储、处理,在计算机系统中,都是数字 0 和 1 信号,将模拟信号(连续信号)转换为数字信号(离散信号)的器件就叫模数转换器(Analog-to-Digital Convert,ADC)。

  ADC 转换器可分为:并行比较型 A/D 转换器(FLASH ADC)、逐次比较型 A/D 转换器(SARADC)和 双积分式 A/D 转换(Double Integral ADC)。

  A/D 转换过程通常为 4 步:采样保持量化编码,如下图所示。

AD转换过程图

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

采样和保持通常是在采样-保持电路中完成,量化和编码通常在 ADC 数字编码电路中完成。

二、ESP32的ADC外设

  在 ESP32 S3 中,模数转换器(ADC)比较输入的模拟电压和参考电压,以确定每一位数字输出结果。ESP32 S3 设计的 ADC 参考电压为 1100 mV。然而,不同芯片的真实参考电压可能会略有变化,范围在 1000 mV 到 1200 mV 之间。

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

具备模拟输入功能的引脚

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

\[V_{引脚电压} = \frac{引脚的 AD 值}{2^{12}} * 3.3V \]

三、ADC常用函数

  ESP-IDF 提供了一套 API 来配置 ADC。要使用此功能,我们需要在 CMakeLists.txt 文件中导入 esp_timer 依赖库,然后还需要导入必要的头文件:

# 注册组件到构建系统的函数
idf_component_register(
    # 依赖库的路径
    REQUIRES esp_adc
)

  如果要使用 ADC 的单次转换,我们需要导入以下头文件。

#include "esp_adc/adc_oneshot.h"

3.1、配置ADC单次转换

  我们可以使用 adc_oneshot_new_unit() 函数用于 配置 ADC 单次转换,其函数原型如下所示:

/**
 * @brief 配置ADC单次转换
 * 
 * @param init_config ADC单次转换配置结构体指针
 * @param ret_unit 保存ADC句柄
 * @return esp_err_t ESP_OK 表示ADC单元创建成功
 *                   ESP_ERR_INVALID_ARG 表示错误参数
 *                   ESP_ERR_NO_MEM 表示内存不足
 *                   ESP_ERR_NOT_FOUND 表示ADC外设在使用
 *                   ESP_FAIL 表示时钟源初始化不正确
 */
esp_err_t adc_oneshot_new_unit(const adc_oneshot_unit_init_cfg_t *init_config, adc_oneshot_unit_handle_t *ret_unit);

  形参 init_config指向 ADC 单次转换配置结构体的指针,它的定义如下:

typedef struct 
{
    adc_unit_t unit_id;             // 使用的ADC单元
    adc_oneshot_clk_src_t clk_src;  // 时钟源
    adc_ulp_mode_t ulp_mode;        // 是否支持ADC在ULP模式下工作
} adc_oneshot_unit_init_cfg_t;

  成员 unit_id使用的 ADC 单元,它的可选值如下:

typedef enum 
{
    ADC_UNIT_1,        // SAR ADC 1
    ADC_UNIT_2,        // SAR ADC 2
} adc_unit_t;

  成员 clk_srcADC 的时钟源,它的可选值如下:

typedef enum 
{
    ADC_RTC_CLK_SRC_RC_FAST = SOC_MOD_CLK_RC_FAST,          // Select RC_FAST as the source clock
    ADC_RTC_CLK_SRC_DEFAULT = SOC_MOD_CLK_RC_FAST,          // Select RC_FAST as the default clock choice
} soc_periph_adc_rtc_clk_src_t;

typedef soc_periph_adc_rtc_clk_src_t adc_oneshot_clk_src_t;

3.2、配置ADC单次转换的通道

  我们可以使用 adc_oneshot_config_channel() 函数 配置 ADC 单次转换的通道,它的原型如下:

/**
 * @brief 配置ADC单次转换的通道
 * 
 * @param handle ADC句柄
 * @param channel 使用的ADC通道
 * @param config ADC单次转换通道配置的结构体指针
 * @return esp_err_t ESP_OK 表示配置成功
 *                   ESP_ERR_INVALID_ARG 表示错误参数
 */
esp_err_t adc_oneshot_config_channel(adc_oneshot_unit_handle_t handle, adc_channel_t channel, const adc_oneshot_chan_cfg_t *config);

  形参 channel使用的 ADC 通道,它的可选值如下:

typedef enum 
{
    ADC_CHANNEL_0,      // ADC channel 0
    ADC_CHANNEL_1,      // ADC channel 1
    ADC_CHANNEL_2,      // ADC channel 2
    ADC_CHANNEL_3,      // ADC channel 3
    ADC_CHANNEL_4,      // ADC channel 4
    ADC_CHANNEL_5,      // ADC channel 5
    ADC_CHANNEL_6,      // ADC channel 6
    ADC_CHANNEL_7,      // ADC channel 7
    ADC_CHANNEL_8,      // ADC channel 8
    ADC_CHANNEL_9,      // ADC channel 9
} adc_channel_t;

  形参 configADC 单次转换通道配置的结构体指针,它的定义如下:

typedef struct 
{
    adc_atten_t atten;              // ADC的衰减系数
    adc_bitwidth_t bitwidth;        // ADC的转换结果位
} adc_oneshot_chan_cfg_t;

  形参 attenADC 的衰减系数,ESP32 的 ADC 采样电压为 1100mV,只能测量 0 ~ 1100mV 之间的电压,如果要测量更大范围的电压,必须设置衰减系数。

typedef enum 
{
    ADC_ATTEN_DB_0   = 0,   // 不衰减,可测量范围0~1.2V
    ADC_ATTEN_DB_2_5 = 1,   // 衰减2.5db,可测量范围0~1.5V
    ADC_ATTEN_DB_6   = 2,   // 衰减6db,可测量范围0~2.0V
    ADC_ATTEN_DB_12  = 3,   // 衰减12db,可测量范围0~3.3V
    ADC_ATTEN_DB_11 __attribute__((deprecated)) = ADC_ATTEN_DB_12,
} adc_atten_t;

  形参 bitwidthADC 的转换结果位,它的可选值如下:

typedef enum 
{
    ADC_BITWIDTH_DEFAULT = 0,   // ADC位宽为默认
    ADC_BITWIDTH_9  = 9,        // ADC位宽为 9 Bit
    ADC_BITWIDTH_10 = 10,       // ADC位宽为 10 Bit
    ADC_BITWIDTH_11 = 11,       // ADC位宽为 11 Bit
    ADC_BITWIDTH_12 = 12,       // ADC位宽为 12 Bit
    ADC_BITWIDTH_13 = 13,       // ADC位宽为 13 Bit
} adc_bitwidth_t;

3.3、读取单次ADC采样的原始数据

  我们可以使用 adc_oneshot_read() 函数 读取单次 ADC 采样的原始数据,该函数的原型如下:

/**
 * @brief 读取单次ADC采样的原始数据
 * 
 * @param handle ADC句柄
 * @param chan 使用的ADC通道
 * @param out_raw 转换后的原始数据
 * @return esp_err_t ESP_OK 表示读取数据成功
 *                   ESP_ERR_INVALID_ARG 表示参数有误
 *                   ESP_ERR_TIMEOUT 表示操作超时
 */
esp_err_t adc_oneshot_read(adc_oneshot_unit_handle_t handle, adc_channel_t chan, int *out_raw);

3.4、获得ADC校准结果

  我们可以使用 adc_oneshot_get_calibrated_result() 函数 获得 ADC 校准结果,该函数的原型如下:

/**
 * @brief 获得ADC校准结果
 * 
 * @param handle ADC句柄
 * @param cali_handle ADC校准句柄
 * @param chan 使用的ADC通道
 * @param cali_result 校准后的结构
 * @return esp_err_t 
 */
esp_err_t adc_oneshot_get_calibrated_result(adc_oneshot_unit_handle_t handle, adc_cali_handle_t cali_handle, adc_channel_t chan, int *cali_result);

  形参 cali_handleADC 校准句柄,它的定义如下:

struct adc_cali_scheme_t
{
    esp_err_t (*raw_to_voltage)(void *arg, int raw, int *voltage);  // 将ADC原始数据转换为校准电压函数指针
    void *ctx;                                                      // 用户上下文
};

typedef struct adc_cali_scheme_t *adc_cali_handle_t;

  这里用户需要实现将 ADC 读取的原始数据转换成校准后电压的函数,它的声明如下:

esp_err_t raw_to_voltage(void *arg, int raw, int *voltage);

四、实验例程

  这里,我们在【components】文件夹下的【peripheral】文件夹下的【inc】文件夹(用来存放头文件)新建一个 bsp_adc.h 文件,在【components】文件夹下的【peripheral】文件夹下的【src】文件夹(用来存放源文件)新建一个 bsp_adc.c 文件。

#ifndef __BSP_ADC_H__
#define __BSP_ADC_H__

#include "esp_adc/adc_oneshot.h"

extern adc_oneshot_unit_handle_t g_adc_oneshot_unit_handle;

void bsp_adc_oneshot_init(adc_oneshot_unit_handle_t *handle, adc_unit_t unit, adc_channel_t channel);
int bsp_adc_oneshot_read(adc_oneshot_unit_handle_t handle, adc_channel_t adc_channel);

#endif // !__BSP_ADC_H__
#include "bsp_adc.h"

adc_oneshot_unit_handle_t g_adc_oneshot_unit_handle;

/**
 * @brief ADC单次转换初始化函数
 * 
 * @param handle ADC句柄
 * @param unit 使用的ADC单元
 * @param channel 使用的ADC通道
 */
void bsp_adc_oneshot_init(adc_oneshot_unit_handle_t *handle, adc_unit_t unit, adc_channel_t channel)
{
    adc_oneshot_unit_init_cfg_t adc_oneshot_unit_init_config = {0};
    adc_oneshot_chan_cfg_t adc_oneshot_channel_config = {0};

    adc_oneshot_unit_init_config.unit_id = unit;                        // 使用的ADC单元
    adc_oneshot_unit_init_config.clk_src = ADC_RTC_CLK_SRC_DEFAULT;     // ADC的时钟源
    adc_oneshot_new_unit(&adc_oneshot_unit_init_config, handle);

    adc_oneshot_channel_config.atten = ADC_ATTEN_DB_12;                 // 衰减系数
    adc_oneshot_channel_config.bitwidth = ADC_BITWIDTH_12;              // ADC的位数
    adc_oneshot_config_channel(*handle, channel, &adc_oneshot_channel_config);
}

/**
 * @brief ADC单次读取原始数据
 * 
 * @param handle ADC句柄
 * @param adc_channel 使用的ADC通道
 * @return int 读取的原始数据
 */
int bsp_adc_oneshot_read(adc_oneshot_unit_handle_t handle, adc_channel_t adc_channel)
{
    int raw_data = 0;
    esp_err_t result = ESP_OK;
    result = adc_oneshot_read(handle, adc_channel, &raw_data);
  
    return result == ESP_OK ? raw_data : -1;
}

  然后,我们修改【components】文件夹下【peripheral】文件夹下的 CMakeLists.txt 文件。

# 源文件路径
set(src_dirs src)

# 头文件路径
set(include_dirs inc)

# 设置依赖库
set(requires 
    driver
    esp_adc
)

# 注册组件到构建系统的函数
idf_component_register(
    # 源文件路径
    SRC_DIRS ${src_dirs}
    # 自定义头文件的路径
    INCLUDE_DIRS ${include_dirs}
    # 依赖库的路径
    REQUIRES ${requires}
)

# 设置特定组件编译选项的函数
# -ffast-math: 允许编译器进行某些可能减少数学运算精度的优化,以提高性能。
# -O3: 这是一个优化级别选项,指示编译器尽可能地进行高级优化以生成更高效的代码。
# -Wno-error=format: 这将编译器关于格式字符串不匹配的警告从错误降级为警告。
# -Wno-format: 这将完全禁用关于格式字符串的警告。
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)

  修改【main】文件夹下的 main.c 文件。

#include <stdio.h>

#include "freertos/FreeRTOS.h"

#include "bsp_adc.h"

// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{
    bsp_adc_oneshot_init(&g_adc_oneshot_unit_handle, ADC_UNIT_1, ADC_CHANNEL_9);
  
    while (1)
    {
        int raw_data = bsp_adc_oneshot_read(g_adc_oneshot_unit_handle, ADC_CHANNEL_9);
        int voltage = 3300 * raw_data / 4096;;
        printf("voltage = %dmV\r\n", voltage);

        // 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

一定要保证测试点的电压在 0 ~ 3.3V 的电压范围,否则可能烧坏 ADC,甚至是整个主控芯片。

posted @ 2025-03-20 22:58  星光映梦  阅读(230)  评论(0)    收藏  举报