ESP-IDF版本:v5.4.3
乐鑫官方API网址:ESP32 - ESP-IDF编程指南 v5.4.3
有了前面几篇的基础,现在我们已经初步掌握了FreeRTOS的一些基本用法和概念。区别于Mcu裸机开发,使用了FreeRTOS之后,我们尽量为不同的功能模块创建单独的任务,任务之间的通信采用队列的方式。
这个写法是基于乐鑫官方的example:v5.4.3\esp-idf\examples\peripherals\adc\continuous_read这个工程,在此基础上,整合进了我的demo工程。
先贴全部代码:
c
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#define EXAMPLE_ADC_OUTPUT_TYPE ADC_DIGI_OUTPUT_FORMAT_TYPE1
#define EXAMPLE_ADC_GET_CHANNEL(p_data) ((p_data)->type1.channel)
#define EXAMPLE_ADC_GET_DATA(p_data) ((p_data)->type1.data)
adc_channel_t channel[2] = {ADC_CHANNEL_0, ADC_CHANNEL_3};
adc_continuous_handle_t adc_handle = NULL;
static const char *BSP_ADC_DEVICE = "bsp_adc_device";
static const char *TAG = "ADC_Task";
static TaskHandle_t s_adc_task_handle = NULL;
// ================= 回调函数 =================
// 当 DMA 缓冲区满时触发
static bool IRAM_ATTR s_conv_done_cb(adc_continuous_handle_t handle, const adc_continuous_evt_data_t *edata, void *user_data)
{
BaseType_t mustYield = pdFALSE;
// 通知任务:数据准备好了
vTaskNotifyGiveFromISR(s_adc_task_handle, &mustYield);
return (mustYield == pdTRUE);
}
// ================= 初始化函数 =================
static esp_err_t continuous_adc_init(adc_continuous_handle_t *out_handle)
{
adc_continuous_handle_t handle = NULL;
adc_continuous_handle_cfg_t adc_config = {
.max_store_buf_size = 1024,
.conv_frame_size = 256,
};
ESP_ERROR_CHECK(adc_continuous_new_handle(&adc_config, &handle));
/* adc_continuous_config_t结构体内成员说明
struct adc_continuous_config_t:
ADC continuous mode driver configurations.
Public Members:
uint32_t pattern_num:Number of ADC channels that will be used.
adc_digi_pattern_config_t *adc_pattern: List of configs for each ADC channel that will be used.
uint32_t sample_freq_hz: The expected ADC sampling frequency in Hz. Please refer to soc/soc_caps.h to know available sampling frequency range
adc_digi_convert_mode_t conv_modeL: ADC DMA conversion mode, see adc_digi_convert_mode_t.
adc_digi_output_format_t format:ADC DMA conversion output format, see adc_digi_output_format_t.
*/
adc_continuous_config_t dig_cfg = {
// 对于温度采集,物理变化很慢。
// 为了方便取平均值滤波,我们可以设置一个适中的频率,例如 1000 Hz (ESP-IDF 连续模式有最低频率限制,通常不能低于 20KHz 或由底层硬件决定)
// 实际上,ESP32 连续 ADC 的下限受时钟分频器限制。ESP-IDF 官方推荐最低 20kHz。
// 如果我们只需要慢速数据,维持 20kHz 没问题,但我们可以通过"软件降采样"来处理。
.sample_freq_hz = 20 * 1000,
.conv_mode = ADC_CONV_SINGLE_UNIT_1, // 只用ADC1进行转换,把
.format = ADC_DIGI_OUTPUT_FORMAT_TYPE1,
};
/* adc_digi_pattern_config_t结构体内成员说明
struct adc_digi_pattern_config_t:
ADC digital controller pattern configuration.
Public Members:
uint8_t atten:Attenuation of this ADC channel.
uint8_t channel:ADC channel.
uint8_t unit:ADC unit.
uint8_t bit_width:ADC output bit width.
*/
adc_digi_pattern_config_t adc_pattern[SOC_ADC_PATT_LEN_MAX] = {0};
dig_cfg.pattern_num = 2;
for (int i = 0; i < 2; i++)
{
adc_pattern[i].atten = ADC_ATTEN_DB_11;
// 取出数组中的通道号: 第1次循环是 ADC_CHANNEL_0, 第2次是 ADC_CHANNEL_3
adc_pattern[i].channel = channel[i] & 0x7;
// 强制指定为 UNIT_1
adc_pattern[i].unit = ADC_UNIT_1;
adc_pattern[i].bit_width = SOC_ADC_DIGI_MAX_BITWIDTH;
ESP_LOGI(BSP_ADC_DEVICE, "adc_pattern[%d].atten is :%"PRIx8, i, adc_pattern[i].atten);
ESP_LOGI(BSP_ADC_DEVICE, "adc_pattern[%d].channel is :%"PRIx8, i, adc_pattern[i].channel);
ESP_LOGI(BSP_ADC_DEVICE, "adc_pattern[%d].unit is :%"PRIx8, i, adc_pattern[i].unit);
}
dig_cfg.adc_pattern = adc_pattern; // 将数组首地址赋值给指针
ESP_ERROR_CHECK(adc_continuous_config(handle, &dig_cfg));
*out_handle = handle; // 将内部配置好的handle值传去实参
return ESP_OK;
}
void ntc_task(void *arg)
{
esp_err_t ret;
uint32_t ret_num = 0;
uint8_t result[256] = {0};
memset(result, 0x00, 256);
ESP_LOGI(TAG, "ADC Continuous Task Started");
// 1. 保存当前任务句柄,供回调使用
s_adc_task_handle = xTaskGetCurrentTaskHandle();
// 2. 初始化 ADC
if (continuous_adc_init(&adc_handle) != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize ADC");
vTaskDelete(NULL); // 初始化失败,删除任务
return;
}
adc_cali_handle_t cali_handle = NULL;
adc_cali_line_fitting_config_t cali_config = {
.unit_id = ADC_UNIT_1,
.atten = ADC_ATTEN_DB_11,
.bitwidth = ADC_BITWIDTH_12,
};
ESP_ERROR_CHECK(adc_cali_create_scheme_line_fitting(&cali_config, &cali_handle));
// 3. 注册回调
adc_continuous_evt_cbs_t cbs =
{
.on_conv_done = s_conv_done_cb,
};
ESP_ERROR_CHECK(adc_continuous_register_event_callbacks(adc_handle, &cbs, NULL));
// 4. 启动 ADC
ESP_ERROR_CHECK(adc_continuous_start(adc_handle));
ESP_LOGI(TAG, "ADC Sampling Started");
// 5. 主循环:等待通知 -> 读取数据
while (1)
{
// 阻塞等待,直到回调函数发出通知
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 批量读取数据,直到缓冲区为空
while (1)
{
ret = adc_continuous_read(adc_handle, result, 256, &ret_num, 0); // 0 tick timeout
if (ret == ESP_OK)
{
uint32_t sum_ch0 = 0, count_ch0 = 0;
uint32_t sum_ch3 = 0, count_ch3 = 0;
// 解析数据
for (int i = 0; i < ret_num; i += SOC_ADC_DIGI_RESULT_BYTES)
{
adc_digi_output_data_t *p = (adc_digi_output_data_t*)&result[i];
uint32_t chan_num = EXAMPLE_ADC_GET_CHANNEL(p);
uint32_t data = EXAMPLE_ADC_GET_DATA(p);
// 简单的有效性检查
if (chan_num < SOC_ADC_CHANNEL_NUM(ADC_UNIT_1))
{
if (chan_num == channel[0])
{
sum_ch0 += data;
count_ch0++;
}
else if (chan_num == channel[1])
{
sum_ch3 += data;
count_ch3++;
}
}
else
{
ESP_LOGW(TAG, "Invalid Channel: %"PRIu32, chan_num);
}
}
// 打印平均值 (相当于软件降采样滤波)
static uint32_t print_timer = 0;
if (xTaskGetTickCount() - print_timer > pdMS_TO_TICKS(1000))
{
int voltage_mv_ch0 = 0;
int voltage_mv_ch3 = 0;
if (count_ch0 > 0)
{
uint32_t avg_raw_ch0 = sum_ch0 / count_ch0;
// 将原始值转换为实际电压
ESP_ERROR_CHECK(adc_cali_raw_to_voltage(cali_handle, avg_raw_ch0, &voltage_mv_ch0));
ESP_LOGI(TAG, "Ch:%"PRIu32" Avg Raw:%"PRIu32" -> Vol:%d mV", (uint32_t)channel[0], avg_raw_ch0, voltage_mv_ch0);
}
if (count_ch3 > 0)
{
uint32_t avg_raw_ch3 = sum_ch3 / count_ch3;
// 将原始值转换为实际电压
ESP_ERROR_CHECK(adc_cali_raw_to_voltage(cali_handle, avg_raw_ch3, &voltage_mv_ch3));
ESP_LOGI(TAG, "Ch:%"PRIu32" Avg Raw:%"PRIu32" -> Vol:%d mV", (uint32_t)channel[1], avg_raw_ch3, voltage_mv_ch3);
}
print_timer = xTaskGetTickCount();
}
// 防止看门狗复位,稍微延时
vTaskDelay(pdMS_TO_TICKS(10));
}
else if (ret == ESP_ERR_TIMEOUT)
{
// 缓冲区数据读完了,跳出内层循环,继续等待下一次通知
break;
}
else
{
ESP_LOGE(TAG, "Read failed: %s", esp_err_to_name(ret));
break;
}
}
}
// 理论上不会运行到这里,除非任务被外部删除
adc_continuous_stop(adc_handle);
adc_continuous_deinit(adc_handle);
}
void adc_init(void)
{
xTaskCreatePinnedToCore(ntc_task,
"NTC_Task",
task_init[TASK_ADC_CHECK].task_stack, NULL,
task_init[TASK_ADC_CHECK].task_priority, NULL,
task_init[TASK_ADC_CHECK].task_core);
}
区别于以前常用的Mcu,ESP32的ADC转换之后,还需要调用一个官方的API函数adc_cali_raw_to_voltage,将ADC转换出来的原始值转换为电压值,得到电压值之后再去计算要采样的阻值是多大(我这里是NTC场景)。
简单介绍一下这个ESP32的ADC初始化步骤:
第一步:
首先新建两个句柄,第一个是任务类型(TaskHandle_t)的句柄,命名为s_adc_task_handle并初始化为NULL。第二个是Adc连续转换类型(adc_continuous_handle_t)的,命名为adc_handle也是初始化为NULL。
这两个句柄一个是用来初始化这个Adc采集和转化任务的。另一个是用来初始化Adc配置用到的。
第二步:回调函数
这个函数没什么能解释的,乐鑫官方提供的回调函数写法,至于为什么这样,我也没去钻研。。只要知道这个代码是DMA中断的回调函数就够了,一般不改动。
第三部:开始配置Adc初始化结构体参数(传入参数为全局建的那个Adc连续转换类型的句柄)
首先在函数内也新建一个adc_continuous_handle_t类型的句柄,目的是为了在函数内就给这个句柄配好相应的值,方便传出。
然后根据官方写法,新建一个adc_continuous_handle_cfg_t类型的结构体,并且初始化里面两个成员的值,一个是 Adc的最大存储缓冲区大小,另一个是每xxx个数据,才会调用一次回调函数。比如我这里面配了2路Adc,每次Adc转换会得到8个字节的数据(每路能转换一次能得到2个字节的数据),我这里配置了256,也就是256/8=32次,即32次的双通道转换后,Adc会触发一次回调函数的调用。
再根据官方写法,新建一个adc_continuous_config_t类型的结构体dig_cfg,用来存放即将要配的一些参数。包括采样频率、转换模式(这个比较重要,意思是你选Adc1来做转换还是Adc2来做转换,主要要看硬件接在哪组Adc上,通常选ADC1,ADC2与WIFI会有冲突,问AI说的)配置dig_cfg的参数就不作解释了,不懂,都是参考的乐鑫原写法,不过可以知道的是,如果Adc数目增多了,dig_cfg.pattern_num要增加,其他参数不太懂。
再根据官方写法,调用adc_continuous_config(handle, &dig_cfg),将刚刚配置的dig_cfg的参数,配置到函数内的结构体handle的各成员。
最后*out_handle = handle;意思是将这个函数内配置好的结构体,传出去外面那个实参(即adc_handle)。这样后续就可以这个句柄来操作这个Adc,且这个结构体内存的已经是刚刚配置好的参数了。
第四步:创建任务函数(Adc采集及转换任务)
有了刚刚那些铺垫,接下来是编写Adc采集及转换任务的任务函数。
这部分都是Ai写的,按照注释理解一下。
初始化结束之后,Adc就已经启动了,进入主循环后不断的等待回调事件,一旦Adc转换了足够多的数据触发回调之后,进入下方的while开始读数据,问了Ai这个地方为什么要用while包着,回复是生产数据的速度大于搬运走数据的速度,所以如果不用while包着,当读走了256字节数据后,可能缓冲区里面又进去了几十个字节的数据(读前256过程中,Adc没有暂停,产生的数据),如果不再这次回调事件里面把这些数据全部取尽,可能下次间隔非常短又会进入回调,这就会造成频繁中断,频繁上下文切换,影响RTOS的效率。
在ret == ESP_OK判断内,解析数据的方法也是乐鑫官方的,比较巧妙的是建立的结果数组为256大小,对应的就是前面配置的每次回调会携带多少个数据,这里面每2个字节是一组Adc的转换结果,然后是通道0、通道1、通道0、通道1这样排列的,乐鑫的做法是将这个数组强制转换为了adc_digi_output_data_t类型,这个类型根据用的芯片不同,宏会进行不同的处理,在ESP32这个平台上,这个类型的结构体里面就两个成员,一个是uint16_t data,另一个是uint16_t channel,所以把数组转换成这样格式的数据之后,就可以区分数组里不同通道的转换值,累加之后求平均就可以得到一次回调的Adc各通道的均值。
最后就是我人为设定的每1秒才读出一次Adc值,转换在后台不断产生,Adc的平均值也在不断刷新,但我规定了程序每1秒才根据这个平均值去转换出一次电压值,这属于业务逻辑,可以不用参考。这里面值得注意的就是调用乐鑫提供的API接口,将Adc各通道的平均值转换为电压值,在根据电压值和参考电压就可以计算出输入的电阻是多大。验证过乐鑫这个还是比较准的,误差只在0.0几v。
函数的最后就是安全起见做的冗余,假设真的运行到那了,就停止Adc,并且取消定义Adc。
根据上文就可以知道怎么完成一次基础的Adc转换了,更高级的功能没钻研。