【快速上手ESP32(ESP-IDF)】ADC数模转换(含单次转换和连续转换以及校准)

折途想要敲代码 2024-09-30 15:05:03 阅读 77

这篇为重置版。

因为准备录制视频了,然后回过头看看之前讲ADC的文章发现有不少错误的地方(但是代码是可以用的),而且讲的也不全面,因此决定写下这个重置版。

这边提供三种使用ADC的方法,第一种是老方法,我就直接把之前的文章给复制过来并进行部分修正。看过的小伙伴可以跳过,直接看单次转换模式和连续转换模式。

目录

老方法

ADC单次转换模式

ADC连续转换模式

校准

老方法

首先是包含头文件。

<code>#include "esp_adc_cal.h"

#include "driver/adc.h"

接下来进行两个配置。

第一个是配置adc1的精度,esp32中一共有俩adc,分别是adc1和adc2,按理说我们都可以用。但是我们最好就是用adc1,每个adc有十个通道,所以一个也是够用的。

至于为什么不用adc2,这是因为adc2和WiFi是冲突的,因此为了避免冲突,咱就是用adc1。

void adc1_config_width(adc_bits_width_t width);

可选的参数有9~13位精度的,但是根据之前的文档我们知道,ESP32中的ADC只有最高12位精度,因此13那个选项不能选(应该吧)。

下一个是衰减参数,衰减越大,可接受的电压越大。

<code>void adc1_config_channel_atten(adc1_channel_t channel, adc_atten_t atten);

第一个参数选择通道,我们知道每个ADC都有十个通道,因此可选的有十个。

第二个选择衰减参数。

这个根据自己手上的传感器的电气参数来选择,不清楚的话就先选11db的,这样就算电压范围不一样也不会导致板子烧坏。

接下来就可以读取ADC的值了。

<code>int adc1_get_raw(adc1_channel_t channel);

另外需要说的是ADC的通道对应的GPIO口是固定的。

可以到官方文档里去查询。

GPIO & RTC GPIO - ESP32-S3 - — ESP-IDF 编程指南 v5.2.2 文档 (espressif.com)

icon-default.png?t=N7T8

https://docs.espressif.com/projects/esp-idf/zh_CN/v5.2.2/esp32s3/api-reference/peripherals/gpio.html

千万要记得左侧选择型号要选对。之前的文章就出现失误了,原因就是我没有选型号,结果GPIO和ADC的通道对不上。

完整实操代码如下。

<code>#include <stdio.h>

#include "driver/gpio.h"

#include "driver/adc.h"

#include <unistd.h>

void app_main(void){

adc1_config_width(ADC_WIDTH_BIT_12);

adc1_config_channel_atten(ADC1_CHANNEL_0,ADC_ATTEN_DB_11);

while (1){

uint16_t adc_val = adc1_get_raw(ADC1_CHANNEL_0);

printf("%d\r\n",adc_val);

sleep(1);

}

}

(上图是之前文章的图,配的文字也错了,这边就懒得改了。。。。。。)

ADC单次转换模式

我们跟着官方文档一步步走,步骤看着多,实际上只需要前三步即可。

首先还是需要包含头文件,这个可以在官方文档中ADC单次转化模式章节中的API参考小节找到,但是这俩头文件在小节中是分开来的,因此我之前没找到。

<code>#include "hal/adc_types.h"

#include "esp_adc/adc_oneshot.h"

第一步资源分配,我们使用下面这个函数。

esp_err_t adc_oneshot_new_unit(const adc_oneshot_unit_init_cfg_t *init_config, adc_oneshot_unit_handle_t *ret_unit)

第一个参数是结构体变量的指针,我们通过配置这个结构体来对ADC单元进行配置,第二个参数是传出参数,传出的是句柄。

我们来看看参数一结构体是怎么样的。

一共是三个成员,第一个是指定ADC,我们一般用的是ADC1,因此填入的参数ADC_UNIT_1。第二个是时钟源,我们默认就行,选择ADC_RTC_CLK_SRC_DEFAULT。第三个是由ULP控制的模式,我们不给它控制,选择ADC_ULP_MODE_DISABLE

资源分配完之后我们进入下一步配置ADC单元实例。但其实我觉得这一步更像是配置ADC通道。

<code>esp_err_t adc_oneshot_config_channel(adc_oneshot_unit_handle_t handle, adc_channel_t channel, const adc_oneshot_chan_cfg_t *config)

参数一是上一步得到的ADC句柄,参数二指定通道,参数三是传入结构体变量的指针来配置。

通道可选的有下面十个,前面说的每个ADC都有十个通道。 

接着还是来看看参数三的结构体是什么样的。

一共俩成员,第一个是指定衰减,另一个是ADC转换结果的位数。

衰减可选的有下面几个,啥意思也都有注释。

但我估摸着看得懂的人不多(包括我),咱就参考下面这个,根据自己模块的电压范围选择,一般咱和MCU是连一起的,所以咱选最大的11dB衰减,起码烧不坏板子。

上面两个配置完之后我们就可以读取数据了。

<code>esp_err_t adc_oneshot_read(adc_oneshot_unit_handle_t handle, adc_channel_t chan, int *out_raw)

参数一给个ADC句柄,参数二指定我们要读取的通道,参数三是传出参数,传出的是读取的结果。

接下来我用一个完整实例来演示一下。

<code>#include <stdio.h>

#include "freertos/FreeRTOS.h"

#include "freertos/task.h"

#include "esp_adc/adc_oneshot.h"

#include "hal/adc_types.h"

adc_oneshot_unit_handle_t unit_handle;

void ADC_init(void) {

adc_oneshot_unit_init_cfg_t unit_initer = {

.clk_src = ADC_RTC_CLK_SRC_DEFAULT, // 默认时钟源

.ulp_mode = ADC_ULP_MODE_DISABLE, // 不启用ULP

.unit_id = ADC_UNIT_1 // 使用ADC1

};

adc_oneshot_new_unit(&unit_initer, &unit_handle);

adc_oneshot_chan_cfg_t channel_initer = {

.atten = ADC_ATTEN_DB_11, // 11dB衰减

.bitwidth = ADC_BITWIDTH_12 // 输出12bit

};

adc_oneshot_config_channel(unit_handle, ADC_CHANNEL_0, &channel_initer);

adc_oneshot_config_channel(unit_handle, ADC_CHANNEL_1, &channel_initer);

}

void app_main(void) {

ADC_init();

int xVal = 0, yVal = 0;

while (1) {

adc_oneshot_read(unit_handle, ADC_CHANNEL_0, &xVal);

adc_oneshot_read(unit_handle, ADC_CHANNEL_1, &yVal);

printf("xval is %d yval is %d\r\n", xVal, yVal);

vTaskDelay(1000 / portTICK_PERIOD_MS);

}

}

我这边用的是双轴摇杆模块,摇杆的x轴和y轴的改变实际上是电位器,这边测试过后是可以读出来的。

这边有些小问题。

第一个是我每次读取都需要手动去调用读取函数。

第二个是我每个通道都需要单独去读取一次,这样太麻烦了。

那么接下来就轮到我们的ADC连续转换模式登场了。

ADC连续转换模式

先包含头文件。

<code>#include "hal/adc_types.h"

#include "esp_adc/adc_continuous.h"

跟着步骤走,一共是五步。

第一步资源分配。

<code>esp_err_t adc_continuous_new_handle(const adc_continuous_handle_cfg_t *hdl_config, adc_continuous_handle_t *ret_handle)

和单次转换差不多,参数一是用来配置的结构体,参数二是传出参数,传出的是句柄。

参数一的结构体就俩成员。

第一个是最大的缓冲字节数,参数二是转换帧的大小。

我们先来解释一下什么叫转换帧,转换帧就是将我们配置的ADC通道的转换结果连接起来合成一帧,就是下面这样。

每个通道的转换结果都会在转换帧中占用4个byte,那怕我们输出的大小就是12bit,它也是占用4个byte,因此我们参数二填入(需要配置的通道数*4)。

因为我们是连续转换模式,因此ADC会不停地转换,给我们不停地生成转换帧,那么参数一就是我们存放转换帧的缓冲区的大小,这个无所谓,随便给个值就行,但是要大于一个转换帧的大小。

第二步配置ADC,这一步稍微复杂一点。

<code>esp_err_t adc_continuous_config(adc_continuous_handle_t handle, const adc_continuous_config_t *config)

参数一是上一步得到的句柄,参数二是配置用的结构体指针。

结构体的第一个成员是使用的ADC通道数,我们用几个就填几。第二个先空着,等等来。

成员三是ADC的采样频率,咱不懂填啥的话就参考官方的demo,官方填的是20 * 1000。

成员四是转换模式。因为我们只用了ADC1,所以选择ADC_CONV_SINGLE_UNIT_1

成员五是转换输出格式。其实我(ESP32S3)没得选,只能选类型二,ADC_DIGI_OUTPUT_FORMAT_TYPE2,如果是ESP32或者是ESP32S2的话就选类型一。

一开始试的时候我默认就是选择类型一,结果一直板子重启,然后我翻看日志才发现不让用类型一。看了官方demo才发现不同型号用的类型不一样。

接着回头看看成员二,需要传入的是结构体数组,数组里每个元素都配置一个ADC通道。

结构体如下。

其实成员我们都不陌生了,按照顺序就是衰减,通道,ADC资源,输出bit数,我们参考着单次转换模式那样填就行。

也可以参考一下我下面的写法。

<code> adc_digi_pattern_config_t adc_digi_arr[] = {

{

.atten = ADC_ATTEN_DB_11,

.bit_width = ADC_BITWIDTH_12,

.channel = ADC_CHANNEL_0,

.unit = ADC_UNIT_1

},{

.atten = ADC_ATTEN_DB_11,

.bit_width = ADC_BITWIDTH_12,

.channel = ADC_CHANNEL_1,

.unit = ADC_UNIT_1

}

};

adc_continuous_config_t conti_config = {

.adc_pattern = adc_digi_arr, // 配置的通道数组

.conv_mode = ADC_CONV_SINGLE_UNIT_1, // 只使用ADC1

.format = ADC_DIGI_OUTPUT_FORMAT_TYPE2, // 没得选,只能选type2

.pattern_num = 2, // 使用的通道数

.sample_freq_hz = 20000 // 采样频率,官方demo用的20 * 1000

};

adc_continuous_config(conti_handle, &conti_config);

第三步是ADC控制。

开启连续转换和停止连续转换用的是下面俩函数。

esp_err_t adc_continuous_start(adc_continuous_handle_t handle)

<code>esp_err_t adc_continuous_stop(adc_continuous_handle_t handle)

虽然在官方文档里这是第三步,但是我们需要在第四步完成之后再调用,也就是注册完回调函数之后再开启转换。

我们看看第四步,注册回调函数。

<code>esp_err_t adc_continuous_register_event_callbacks(adc_continuous_handle_t handle, const adc_continuous_evt_cbs_t *cbs, void *user_data)

参数一传入句柄,参数二给个结构体指针来配置回调函数,参数三是传给回调函数的参数,我们可以直接给个NULL。

来看看参数二的结构体类型。

一共是俩,第一个是转换完毕之后调用的回调函数,第二个是缓冲区满了之后调用的回调函数。

我们接着看看回调函数是有什么要求。

首先是返回值为bool类型,参数依次是句柄,事件给的参数,我们给的参数。

我们再接着看看事件给的参数是什么样的。

第一个是指向存放转换帧的缓冲区的指针,第二个是转换帧的大小。

也就是说我们可以通过回调函数的参数来获取到ADC转换的结果(多个转换帧)。

然后需要记住的是每个转换帧是 通道数*4,而我们只需要获取我们设置的输出bit即可,所以在取数据那一步需要额外注意一下。

接下来我贴一下完整的代码,可以参考着使用。

<code>#include <stdio.h>

#include "freertos/FreeRTOS.h"

#include "freertos/task.h"

#include "hal/adc_types.h"

#include "esp_adc/adc_continuous.h"

adc_continuous_handle_t conti_handle;

int adc_num; // 缓冲区大小

int xVal = 0, yVal = 0;

uint8_t* adc_val; // 缓冲区

bool adc_callback(adc_continuous_handle_t handle,const adc_continuous_evt_data_t* edata,void* user_data) {

adc_num = edata->size; // 获取缓冲区的大小

adc_val = edata->conv_frame_buffer; // 获取转换结果

if (adc_num == 8) { // 将转换结果(4个byte)合成一个int

xVal = (((uint16_t)adc_val[1] & 0x0F) << 8) | adc_val[0]; //因为我们设置的是输出12bit,因此需要把16bit的高4位去掉,所以需要&0x0F

yVal = (((uint16_t)adc_val[5] & 0x0F) << 8) | adc_val[4];

return true;

}

return false;

}

void nADC_init(void) {

adc_continuous_handle_cfg_t conti_initer = {

.conv_frame_size = 8, // 2 * 4 两个通道,每个4byte

.max_store_buf_size = 1024 // 随便填,比2*4大就行

};

adc_continuous_new_handle(&conti_initer, &conti_handle);

adc_digi_pattern_config_t adc_digi_arr[] = {

{

.atten = ADC_ATTEN_DB_11, // 11dB衰减

.bit_width = ADC_BITWIDTH_12, // 输出12bit

.channel = ADC_CHANNEL_0, // 通道0

.unit = ADC_UNIT_1 // ADC1

},{

.atten = ADC_ATTEN_DB_11,

.bit_width = ADC_BITWIDTH_12,

.channel = ADC_CHANNEL_1, // 通道1

.unit = ADC_UNIT_1

}

};

adc_continuous_config_t conti_config = {

.adc_pattern = adc_digi_arr, // 配置的通道数组

.conv_mode = ADC_CONV_SINGLE_UNIT_1, // 只使用ADC1

.format = ADC_DIGI_OUTPUT_FORMAT_TYPE2, // 没得选,只能选type2

.pattern_num = 2, // 使用的通道数

.sample_freq_hz = 20000 // 采样频率,官方demo用的20 * 1000

};

adc_continuous_config(conti_handle, &conti_config);

adc_continuous_evt_cbs_t conti_evt = {

.on_conv_done = adc_callback, // 绑定转换完毕后的回调函数

};

adc_continuous_register_event_callbacks(conti_handle,&conti_evt,NULL);

adc_continuous_start(conti_handle); // 开启连续转换

}

void app_main(void) {

nADC_init();

while (1) {

for(int i = 0; i < adc_num; ++i){ //打印看看缓冲区

printf("%d\t",adc_val[i]);

}

printf("xval is %d yval is %d\r\n", xVal, yVal);

vTaskDelay(1000 / portTICK_PERIOD_MS);

}

}

除了上面这种直接通过回调函数来获取转换结果的方法,还有一个函数可以读取转换结果。

<code>esp_err_t adc_continuous_read(adc_continuous_handle_t handle, uint8_t *buf, uint32_t length_max, uint32_t *out_length, uint32_t timeout_ms)

参数一是句柄。参数二是传出参数,也就是存放转换帧的缓冲区。参数三是我们要读取的长度,这边需要填通道数*4,我之前给的1024,结果它真的给我1024个数了。参数四是传出参数,给的是实际上读取的长度。参数五是等待时间。

这个函数我试过,就是它只有第一次读取的结果是准的,后面读出的结果是第一次结果的小幅度震荡,也就是说除了第一次是准的,后面读出的都是不准的。具体为什么我也不知道。

看了好久官方demo也找不出原因,所以就不贴出示例代码了,回调函数获取转换结果的方法能用,那就先用着。

校准

校准简单,创建完校准方案之后就可以直接用了。

先包含头文件。

<code>#include "esp_adc/adc_cali.h"

#include "esp_adc/adc_cali_scheme.h"

第一步创建校准方案。

esp_err_t adc_cali_create_scheme_curve_fitting(const adc_cali_curve_fitting_config_t *config, adc_cali_handle_t *ret_handle)

参数一传入配置用的结构体指针,参数二传出参数,传输句柄。

结构体成员的名字和上面的不一样,但是类型是一样的,估计写代码的不是同一批人。

结构体成员依次是ADC资源,通道,衰减,输出bit数。

获得了句柄之后我们就可以通过调用下面这个函数来获取校准后的电压了,单位是mV。

<code>esp_err_t adc_cali_raw_to_voltage(adc_cali_handle_t handle, int raw, int *voltage)

就俩步很简单。接下来直接贴上我的示例代码。

<code>#include <stdio.h>

#include "freertos/FreeRTOS.h"

#include "freertos/task.h"

#include "hal/adc_types.h"

#include "esp_adc/adc_continuous.h"

#include "esp_adc/adc_cali.h"

#include "esp_adc/adc_cali_scheme.h"

adc_cali_handle_t cali_handle;

adc_continuous_handle_t conti_handle;

int adc_num; // 缓冲区大小

int xVal = 0, yVal = 0;

uint8_t* adc_val; // 缓冲区

bool adc_callback(adc_continuous_handle_t handle,const adc_continuous_evt_data_t* edata,void* user_data) {

adc_num = edata->size; // 获取缓冲区的大小

adc_val = edata->conv_frame_buffer; // 获取转换结果

if (adc_num == 8) { // 将转换结果(4个byte)合成一个int

xVal = (((uint16_t)adc_val[1] & 0x0F) << 8) | adc_val[0]; //因为我们设置的是输出12bit,因此需要把16bit的高4位去掉,所以需要&0x0F

yVal = (((uint16_t)adc_val[5] & 0x0F) << 8) | adc_val[4];

return true;

}

return false;

}

void nADC_init(void) {

adc_continuous_handle_cfg_t conti_initer = {

.conv_frame_size = 8, // 2 * 4 两个通道,每个4byte

.max_store_buf_size = 1024 // 随便填,比2*4大就行

};

adc_continuous_new_handle(&conti_initer, &conti_handle);

adc_digi_pattern_config_t adc_digi_arr[] = {

{

.atten = ADC_ATTEN_DB_11, // 11dB衰减

.bit_width = ADC_BITWIDTH_12, // 输出12bit

.channel = ADC_CHANNEL_0, // 通道0

.unit = ADC_UNIT_1 // ADC1

},{

.atten = ADC_ATTEN_DB_11,

.bit_width = ADC_BITWIDTH_12,

.channel = ADC_CHANNEL_1, // 通道1

.unit = ADC_UNIT_1

}

};

adc_continuous_config_t conti_config = {

.adc_pattern = adc_digi_arr, // 配置的通道数组

.conv_mode = ADC_CONV_SINGLE_UNIT_1, // 只使用ADC1

.format = ADC_DIGI_OUTPUT_FORMAT_TYPE2, // 没得选,只能选type2

.pattern_num = 2, // 使用的通道数

.sample_freq_hz = 20000 // 采样频率,官方demo用的20 * 1000

};

adc_continuous_config(conti_handle, &conti_config);

adc_continuous_evt_cbs_t conti_evt = {

.on_conv_done = adc_callback, // 绑定转换完毕后的回调函数

};

adc_continuous_register_event_callbacks(conti_handle,&conti_evt,NULL);

adc_continuous_start(conti_handle); // 开启连续转换

}

void adc_cali_init(void){

adc_cali_curve_fitting_config_t cali_initer = {

.atten = ADC_ATTEN_DB_11,

.bitwidth = ADC_BITWIDTH_12,

.chan = ADC_CHANNEL_0,

.unit_id = ADC_UNIT_1

};

adc_cali_create_scheme_curve_fitting(&cali_initer, &cali_handle);

}

void app_main(void) {

nADC_init();

adc_cali_init();

int newX,newY;

while (1) {

for(int i = 0; i < adc_num; ++i){ //打印看看缓冲区

printf("%d\t",adc_val[i]);

}

adc_cali_raw_to_voltage(cali_handle,xVal,&newX);

adc_cali_raw_to_voltage(cali_handle,yVal,&newY);

printf("xval is %d yval is %d newX is %dmV newY is %dmV\r\n", xVal, yVal,newX,newY);

vTaskDelay(1000 / portTICK_PERIOD_MS);

}

}

前半部分和连续转换模式的代码一样,直接看下半部分就行。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。