ESP32-S3学习笔记<6>:ADC的应用
1. 头文件包含
#include "hal/adc_types.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
2. ADC的配置
ADC配置,主要分为三个步骤。首先配置ADC外设,然后配置ADC通道,最后初始化校准并获得通道校准句柄。
2.1 配置ADC外设
使用如下函数配置ADC外设:
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 的结构定义如下:
typedef struct {
adc_unit_t unit_id; ///< ADC unit
adc_oneshot_clk_src_t clk_src; ///< Clock source
adc_ulp_mode_t ulp_mode; ///< ADC controlled by ULP, see `adc_ulp_mode_t`
} adc_oneshot_unit_init_cfg_t;
第一个成员 unit_id 用于指定要配置哪个ADC。ESP32-S3有2个ADC外设可用。合法的选项有:
typedef enum {
ADC_UNIT_1, ///< SAR ADC 1
ADC_UNIT_2, ///< SAR ADC 2
} adc_unit_t;
第二个成员 clk_src 用于指定时钟源。实际上只有一个选项:
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;
第三个成员 ulp_mode 用于指定是否使用低功耗控制。不需要系统睡眠时ADC运行的话,使用第一个选项即可:
typedef enum {
ADC_ULP_MODE_DISABLE = 0, ///< ADC ULP mode is disabled
ADC_ULP_MODE_FSM = 1, ///< ADC is controlled by ULP FSM
ADC_ULP_MODE_RISCV = 2, ///< ADC is controlled by ULP RISCV
} adc_ulp_mode_t;
函数的第二个参数为出参数,创建成功时返回ADC外设单元的句柄。
2.2 配置ADC通道
ESP32-S3有2个ADC外设,每个ADC外设又支持10个通道输入。因此需要配置ADC量化哪个通道。一个ADC外设在某个时间点,是肯定只能转化一个通道的。但是驱动都将这些做好的。一个ADC可以添加多个通道。
使用如下的函数来指定ADC的通道。
esp_err_t adc_oneshot_config_channel(adc_oneshot_unit_handle_t handle, adc_channel_t channel, const adc_oneshot_chan_cfg_t *config);
第一个参数 handle ,是上一步配置ADC外设时返回的句柄。
第二个参数 channel,指定要将哪个通道加入到ADC外设上。可用的值如下面所示。注意硬件上,GPIO和ADC通道的对应关系。要查数据手册,明确硬件设计的需要ADC的GPIO对应的哪个ADC外设,以及哪个ADC通道。
typedef enum {
ADC_CHANNEL_0, ///< ADC channel
ADC_CHANNEL_1, ///< ADC channel
ADC_CHANNEL_2, ///< ADC channel
ADC_CHANNEL_3, ///< ADC channel
ADC_CHANNEL_4, ///< ADC channel
ADC_CHANNEL_5, ///< ADC channel
ADC_CHANNEL_6, ///< ADC channel
ADC_CHANNEL_7, ///< ADC channel
ADC_CHANNEL_8, ///< ADC channel
ADC_CHANNEL_9, ///< ADC channel
} adc_channel_t;
第三个参数 config ,为通道的转换参数。其定义如下:
typedef struct {
adc_atten_t atten; ///< ADC attenuation
adc_bitwidth_t bitwidth; ///< ADC conversion result bits
} adc_oneshot_chan_cfg_t;
第一个成员 atten 用于指定衰减。
首先,ESP32-S3的IO是3.3V容限的,因此输入电压不能超过3.3V。此外,由于内部参考电压是1.1V的,如果GPIO输入超过这个电压,则读取结果为满量程(对于12bit分辨率,为4095)。所以,一方面如果待测信号超过3.3V,要使用合适的外部电路衰减到3.3V以内(实际上还应该更低一些,有文档说实际上ADC输入只能测到3.1V左右)。另一方面,根据输入到GPIO的信号的最大幅值,设定合适的片内衰减。
atten 的选项有:
typedef enum {
ADC_ATTEN_DB_0 = 0, ///<No input attenuation, ADC can measure up to approx.
ADC_ATTEN_DB_2_5 = 1, ///<The input voltage of ADC will be attenuated extending the range of measurement by about 2.5 dB
ADC_ATTEN_DB_6 = 2, ///<The input voltage of ADC will be attenuated extending the range of measurement by about 6 dB
ADC_ATTEN_DB_12 = 3, ///<The input voltage of ADC will be attenuated extending the range of measurement by about 12 dB
ADC_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 用于指定输出位宽。可用的选项有:
typedef enum {
ADC_BITWIDTH_DEFAULT = 0, ///< Default ADC output bits, max supported width will be selected
ADC_BITWIDTH_9 = 9, ///< ADC output width is 9Bit
ADC_BITWIDTH_10 = 10, ///< ADC output width is 10Bit
ADC_BITWIDTH_11 = 11, ///< ADC output width is 11Bit
ADC_BITWIDTH_12 = 12, ///< ADC output width is 12Bit
ADC_BITWIDTH_13 = 13, ///< ADC output width is 13Bit
} adc_bitwidth_t;
2.3 配置校准
ESP32-S3内部有一个1.1V的参考源。GPIO上输入的待测信号,在ADC内,外设根据这个参考源生成一个模拟信号,并将该模拟信号和待测信号相比较。反复调节这个内部模拟信号,直到和输入待测信号相同。这时读取内部生成的模拟信号所用的数字量(相当于DAC的输入),得到的就是转换值。这就是SARADC的工作原理。由此可见,参考源的精度直接决定了ADC量化的精度或误差。1.1V的参考源本身未必准确,它的偏差会带来测量转换结果的偏差。因此厂家一般会在芯片出厂前内部测试这个参考源的真实电压或偏差,并存入到芯片的一个只读存储区。用户可以获得该芯片的真实参考电压。
ESP32-S3的驱动已经考虑了这一点。因此很简单地就可以得到考虑了参考源误差之后的ADC转换值。在初始化阶段先为ADC外设和某个通道创建校准算法并得到句柄。后续读取数据后,可以根据该句柄获取到校准后的输入电压值。
使用如下函数创建校准句柄:
esp_err_t adc_cali_create_scheme_curve_fitting(const adc_cali_curve_fitting_config_t *config, adc_cali_handle_t *ret_handle);
第一个参数 config 为配置参数,其定义为:
typedef struct {
adc_unit_t unit_id; ///< ADC unit
adc_channel_t chan; ///< ADC channel, for chips with SOC_ADC_CALIB_CHAN_COMPENS_SUPPORTED, calibration can be per channel
adc_atten_t atten; ///< ADC attenuation
adc_bitwidth_t bitwidth; ///< ADC raw output bitwidth
} adc_cali_curve_fitting_config_t;
- unit_id:指定哪个ADC外设。
- chan:指定通道。理论上参考电压和通道是无关的。这里还是规范填上。
- atten:输入衰减,和前面通道配置保持一致。
- bitwidth:量化位宽,和前面通道配置保持一致。
第二个参数 ret_handle ,为校准返回的句柄,需要保存下来,后续要用到。
3. ADC的读取
使用如下函数来读取某个通道的转换值:
esp_err_t adc_oneshot_read(adc_oneshot_unit_handle_t handle, adc_channel_t chan, int *out_raw);
第一个参数 handle ,是配置ADC外设时返回的句柄。
第二个参数 chan ,指定转换的通道。
第三个参数 out_raw,保存转换值。
得到转换值后,为了转换为GPIO的输入电压,并且考虑校准的因素在内,使用如下函数进行转换:
esp_err_t adc_cali_raw_to_voltage(adc_cali_handle_t handle, int raw, int *voltage);
第一个参数 handle,是创建校准时返回的句柄。
第二个参数 raw,是刚刚读取到的量化数据。
第三个参数 voltage,返回转换好的电压。注意电压是以毫伏(mV)为单位的。
4. 示例
以下程序创建一个ADC外设并配置一个通道。然后将输入电压打印出来。
test_adc.h文件:
#include "hal/adc_types.h"
#define TEST_ADC_ADC_INPUT_CHANNEL (ADC_CHANNEL_7)
void TEST_ADC_ADCConfig(void) ;
void TEST_ADC_ADCGet(void) ;
test_adc.c文件:
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#include "esp_err.h"
#include "test_adc.h"
adc_oneshot_unit_handle_t g_pstADC1Handle ;
adc_cali_handle_t g_pstADC1CaliHandle ;
void TEST_ADC_ADCConfig(void)
{
adc_oneshot_unit_init_cfg_t stADCConfig ;
adc_oneshot_chan_cfg_t stChannelConfig ;
adc_cali_curve_fitting_config_t stCaliConfig ;
adc_cali_scheme_ver_t enCaliScheme ;
esp_err_t iRetVal ;
/* config adc peripheral */
stADCConfig.unit_id = ADC_UNIT_1 ;
stADCConfig.clk_src = ADC_RTC_CLK_SRC_DEFAULT ;
stADCConfig.ulp_mode = ADC_ULP_MODE_DISABLE ;
iRetVal = adc_oneshot_new_unit(&stADCConfig, &g_pstADC1Handle) ;
if(ESP_OK != iRetVal)
{
printf("adc_oneshot_new_unit error. Return value is %d\n", iRetVal) ;
}
/* config adc channel */
stChannelConfig.atten = ADC_ATTEN_DB_12 ;
stChannelConfig.bitwidth = ADC_BITWIDTH_12 ;
iRetVal = adc_oneshot_config_channel(g_pstADC1Handle, TEST_ADC_ADC_INPUT_CHANNEL, &stChannelConfig) ;
if(ESP_OK != iRetVal)
{
printf("adc_oneshot_config_channel error. Return value is %d\n", iRetVal) ;
}
/* check calibration scheme */
iRetVal = adc_cali_check_scheme(&enCaliScheme) ;
if(ESP_OK != iRetVal)
{
printf("adc_cali_check_scheme error. Return value is %d\n", iRetVal) ;
}
if(ADC_CALI_SCHEME_VER_LINE_FITTING == enCaliScheme)
{
printf("Chip support line fitting.\n") ;
}
else if(ADC_CALI_SCHEME_VER_CURVE_FITTING == enCaliScheme)
{
printf("Chip support curve fitting.\n") ;
}
else
{
printf("No fitting supported by chip.\n") ;
}
/* create calibration */
stCaliConfig.unit_id = ADC_UNIT_1 ;
stCaliConfig.chan = TEST_ADC_ADC_INPUT_CHANNEL ;
stCaliConfig.atten = ADC_ATTEN_DB_12 ;
stCaliConfig.bitwidth = ADC_BITWIDTH_12 ;
iRetVal = adc_cali_create_scheme_curve_fitting(&stCaliConfig, &g_pstADC1CaliHandle) ;
if(ESP_OK != iRetVal)
{
printf("adc_cali_create_scheme_curve_fitting error. Return value is %d\n", iRetVal) ;
}
return ;
}
void TEST_ADC_ADCGet(void)
{
esp_err_t iRetVal ;
int iADCRaw ;
int iVolPreCali ;
int iVolPostCali ;
/* 读取转换数据 */
iRetVal = adc_oneshot_read(g_pstADC1Handle, TEST_ADC_ADC_INPUT_CHANNEL, &iADCRaw) ;
if(ESP_OK != iRetVal)
{
printf("adc_oneshot_read error. Return value is %d\n", iRetVal) ;
}
else if(0 <= iADCRaw)
{
/* 1100.0f : internal reference voltage
* 3.9824f : 12db gain
*/
iVolPreCali = (int)((((float)iADCRaw / 4095.0f) * 1100.0f) * 3.9824f) ;
adc_cali_raw_to_voltage(g_pstADC1CaliHandle, iADCRaw, &iVolPostCali) ;
printf("ADC voltage : pre calib : %dmV, post calib : %dmV.\n", iVolPreCali, iVolPostCali) ;
}
}
main.c文件:
#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "test_adc.h"
void app_main(void)
{
TEST_ADC_ADCConfig() ;
while (true)
{
TEST_ADC_ADCGet() ;
vTaskDelay(1000) ;
}
}