一、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 步:采样、保持、量化 和 编码,如下图所示。
- 采样:把时间连续变化的信号变换为时间离散的信号。
- 保持:保持采样信号,使有充分时间转换为数字信号。
- 量化:把采样保持电路的输出信号用单位量化电压的整数倍表示。
- 编码:把量化的结果用二进制代码表示。
采样和保持通常是在采样-保持电路中完成,量化和编码通常在 ADC 数字编码电路中完成。
二、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 值和电压值,这里就会有一个简单的关系,如下所示。
二、ADC常用函数
ESP-IDF 提供了一套 API 来配置 ADC。要使用此功能,我们需要在 CMakeLists.txt 文件中导入 esp_timer
依赖库,然后还需要导入必要的头文件:
# 注册组件到构建系统的函数
idf_component_register(# 依赖库的路径REQUIRES esp_adc
)
如果要使用 ADC 的单次转换,我们需要导入以下头文件。
#include "esp_adc/adc_oneshot.h"
2.1、配置ADC单次转换
我们可以使用 adc_oneshot_new_unit()
函数用于 配置 ADC 单次转换,其函数原型如下所示:
/*** @brief 配置ADC单次转换* * @param init_config ADC单次转换配置结构体指针* @param ret_unit 保存ADC句柄* @return esp_err_t ESP_OK表示配置成功,其它表示配置失败*/
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 controlled by ULP, see `adc_ulp_mode_t`
} adc_oneshot_unit_init_cfg_t;
成员 unit_id
是 使用的 ADC 单元,它的可选值如下:
typedef enum
{ADC_UNIT_1, ///< SAR ADC 1ADC_UNIT_2, ///< SAR ADC 2
} adc_unit_t;
成员 clk_src
是 ADC 的时钟源,它的可选值如下:
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;
2.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_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 channelADC_CHANNEL_1, ///< ADC channelADC_CHANNEL_2, ///< ADC channelADC_CHANNEL_3, ///< ADC channelADC_CHANNEL_4, ///< ADC channelADC_CHANNEL_5, ///< ADC channelADC_CHANNEL_6, ///< ADC channelADC_CHANNEL_7, ///< ADC channelADC_CHANNEL_8, ///< ADC channelADC_CHANNEL_9, ///< ADC channel
} adc_channel_t;
形参 config
是 ADC 单次转换通道配置的结构体指针,它的定义如下:
typedef struct
{adc_atten_t atten; // ADC的衰减系数adc_bitwidth_t bitwidth; // ADC的转换结果位
} adc_oneshot_chan_cfg_t;
形参 atten
是 ADC 的衰减系数,ESP32 的 ADC 采样电压为 1100mV,只能测量 0 ~ 1100mV 之间的电压,如果要测量更大范围的电压,必须设置衰减系数。
typedef enum
{ADC_ATTEN_DB_0 = 0, // 不衰减,可测量范围0~1.2VADC_ATTEN_DB_2_5 = 1, // 衰减2.5db,可测量范围0~1.5VADC_ATTEN_DB_6 = 2, // 衰减6db,可测量范围0~2.0VADC_ATTEN_DB_12 = 3, // 衰减12db,可测量范围0~3.3VADC_ATTEN_DB_11 __attribute__((deprecated)) = ADC_ATTEN_DB_12, ///<This is deprecated, it behaves the same as `ADC_ATTEN_DB_12`
} adc_atten_t;
形参 bitwidth
是 ADC 的转换结果位,它的可选值如下:
typedef enum
{ADC_BITWIDTH_DEFAULT = 0, ///< Default ADC output bits, max supported width will be selectedADC_BITWIDTH_9 = 9, ///< ADC output width is 9BitADC_BITWIDTH_10 = 10, ///< ADC output width is 10BitADC_BITWIDTH_11 = 11, ///< ADC output width is 11BitADC_BITWIDTH_12 = 12, ///< ADC output width is 12BitADC_BITWIDTH_13 = 13, ///< ADC output width is 13Bit
} adc_bitwidth_t;
2.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_t adc_oneshot_read(adc_oneshot_unit_handle_t handle, adc_channel_t chan, int *out_raw);
2.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_handle
是 ADC 校准句柄,它的定义如下:
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 driveresp_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));}
}