PlatformIO版ESP-IDF V6に移行する – ADC

カテゴリー: ESP32  タグ:

ESP32-S3の開発にはPlatformIO(以降PIO)版ESP-IDFV5.1.1で行っていましたが、V6.1.0まで使えるようになったので今後のことも考えて移行することにします。

PIO版ESP-IDF V6公式版ESP-IDF V5に相当し、公式版ではV4→V5で大幅に変更がありました。migrationの一覧はここ、詳細はここで。バージョンの値が近いのでややこしいです。

以前公式版とPIO版のVersionを混同してPCNTをレガシー表記で使った記事を書きましたが、今回の変更でそれも新表記に修正することになりました。

今回はADCについての覚書。キャリブレーションの効果も簡易的に測定しました。

ADCはOneshot、Continuousの2つ、ESP32-S3でCalibrationが可能になった。

ESP32-S3でのADCキャリブレーションはレガシー表記ではesp_adc_cal_check_efuse()関数がESP_ERR_NOT_SUPPORTEDを返すので、うまくいきませんでした。ハードウェアキャリブレーション機能は使えないようです。新表記ではキャリブレーション可能になりました(ソフトウェア的キャリブレーションの匂いがしますが・・・後述)。

コード記述はExampleを見るのが手っ取り早いです。二種類あります。

oneshot_read トリガをかけて単発変換。キャリブレーション使用例も。

continuous_read マルチチャンネルの連続変換モード。連続といっても獲得メモリサイズの範囲で有限。

Oneshotモード

初期化はadc_oneshot_new_unit()関数で使用チャンネル毎にadc_oneshot_config_channel()関数でセットする。変換クロックやアクイジョンタイムなど一般MCUで見られる細かい設定項目はざっとみたけど見当たらない。

adc_oneshot_read()関数で一発変換&リード。単純明快です。Exampleはポートを自分の環境にあわせるだけで動作するので、全コードは割愛。

このExampleの中で、キャリブレーション後に読み出しコードは補正しmV換算しているので、次の「continuous」に応用してみる。

Continuousモード

初期設定でREAD_LENで確保したフレーム領域に連続して変換データを格納するモードです。一回の変換命令で有限の連続データを配列に格納する動作です。「単発変換」がoneshotなら、continuousは「マルチチャンネル有限複数サンプリング」ということでしょうか。

ESP32-S3では1データサイズは4byteなので、READ_LEN=256とすると、連続データは64個ということになります。領域確保する4×256で自動的に算出してくれても良さそうですが、バイト数は別途.max_store_buf_sizeでセットしないといけないようです。

また、ExampleではChennel2/3の2個を使うので、格納データは下記。

初期化

大部分Exampleと同じです。今回はChennel7しか使いませんが、あえて配列サイズは2のまま、両方Chennel7にして同一チャンネルにしています。

#define ADC_UNIT									ADC_UNIT_1
#define _ADC_UNIT_STR(unit)				#unit
#define ADC_UNIT_STR(unit)				_ADC_UNIT_STR(unit)
#define ADC_CONV_MODE					ADC_CONV_SINGLE_UNIT_1
#define ADC_ATTEN								ADC_ATTEN_DB_0
#define ADC_BIT_WIDTH						SOC_ADC_DIGI_MAX_BITWIDTH
#define ADC_OUTPUT_TYPE				ADC_DIGI_OUTPUT_FORMAT_TYPE2
#define ADC_GET_CHANNEL(p_data)	((p_data)->type2.channel)
#define ADC_GET_DATA(p_data)			((p_data)->type2.data)

static adc_channel_t channel[2] = {ADC_CHANNEL_7, ADC_CHANNEL_7};	//同じチャンネル

static void continuous_adc_init(adc_channel_t *channel, uint8_t channel_num, adc_continuous_handle_t *out_handle) {
	adc_continuous_handle_t handle = NULL;
	adc_continuous_handle_cfg_t adc_config = {
		.max_store_buf_size = 1024,			// max 1024byte
		.conv_frame_size = READ_LEN,	// frame data byte 256(/4)
	};
	ESP_ERROR_CHECK(adc_continuous_new_handle(&adc_config, &handle));
	adc_continuous_config_t dig_cfg = {
		.sample_freq_hz = 20 * 1000,			// 20kHz
		.conv_mode = ADC_CONV_MODE,
		.format = ADC_OUTPUT_TYPE,
	};
	adc_digi_pattern_config_t adc_pattern[SOC_ADC_PATT_LEN_MAX] = {0};
	dig_cfg.pattern_num = channel_num;
	for (int i = 0; i < channel_num; i++) {
		adc_pattern[i].atten = ADC_ATTEN;
		adc_pattern[i].channel = channel[i] & 0x7;
		adc_pattern[i].unit = ADC_UNIT;
		adc_pattern[i].bit_width = ADC_BIT_WIDTH;
	}
	dig_cfg.adc_pattern = adc_pattern;
	ESP_ERROR_CHECK(adc_continuous_config(handle, &dig_cfg));
	*out_handle = handle;
}

コールバック関数を記述

これはExampleをそのまま

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;
	//Notify that ADC continuous driver has done enough number of conversions
	vTaskNotifyGiveFromISR(s_task_handle, &mustYield);
	return (mustYield == pdTRUE);
}

キャリブレーションはソフトウェア補正?

oneshotのExampleに入っていたキャリブレーションをcontinuousに応用。この機能は、読み出しコードが直接補正されるわけではなく、コードからmV換算するときに関数を呼んで補正するので、どうやらADCのハードウェア機能ではなく、ソフトウェア補正に近い。

補正方法は直線補正と曲線補正が選べるが、ESP32-S3ではカーブフィッティング(曲線補正)が使える。

キャリブレーションの初期化は下記、初期化関数を読み出せばOK。

	adc_cali_handle_t adc1_cali_handle = NULL;	// ADC1 Calibration Init
	bool do_calibration1 = adc_calibration_init(ADC_UNIT_1, ADC_ATTEN, &adc1_cali_handle);

キャリブレーション初期化関数は以下。oneshotのExampleそのままです。

static bool adc_calibration_init(adc_unit_t unit, adc_atten_t atten, adc_cali_handle_t *out_handle) {
	adc_cali_handle_t handle = NULL;
	esp_err_t ret = ESP_FAIL;
	bool calibrated = false;
	if (!calibrated) {
		ESP_LOGI(TAG, "calibration scheme version is %s", "Curve Fitting");
		adc_cali_curve_fitting_config_t cali_config = {
			.unit_id = unit,
			.atten = atten,
			.bitwidth = ADC_BITWIDTH_DEFAULT,
		};
		ret = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
		if (ret == ESP_OK) {
			calibrated = true;
		}
	}
	*out_handle = handle;
	if (ret == ESP_OK) {
		ESP_LOGI(TAG, "Calibration Success");
	} else if (ret == ESP_ERR_NOT_SUPPORTED || !calibrated) {
		ESP_LOGW(TAG, "eFuse not burnt, skip software calibration");
	} else {
		ESP_LOGE(TAG, "Invalid arg or no memory");
	}
	return calibrated;
}

初期化した後は読み出しコードを変換関数でmVに換算します。

	adc_cali_raw_to_voltage(adc1_cali_handle, 入力変数, &出力変数);

測定ループ。20kHzサンプルで64データ≒3.2msで一回のサンプルが終わる。

測定ループはタスクにしてみました。

サンプリング20kHz。一回のサンプル数64。変換関数adc_continuous_read()は3.2ms。「連続」というよりは「指定回数サンプリング」です。

下記のコードはソフトウェアによる、一種の64個データ平均のオーバーサンプリングフィルタを構成しました。算出された値に対しキャリブレーション変換関数でmVに換算しprint。continuousのExample抜粋+oneshotのExampleからキャリブレーションするコードです。

void adcTestTask(void *pvParameters) {
	esp_err_t ret;
	uint32_t ret_num = 0;
	uint8_t result[READ_LEN] = {0};
	memset(result, 0xcc, READ_LEN);
	int voltage;
	int total=0;
	s_task_handle = xTaskGetCurrentTaskHandle();
	adc_continuous_handle_t handle = NULL;
	continuous_adc_init(channel, sizeof(channel) / sizeof(adc_channel_t), &handle);
	adc_continuous_evt_cbs_t cbs = {
		.on_conv_done = s_conv_done_cb,
	};
	adc_cali_handle_t adc1_cali_handle = NULL;	// ADC1 Calibration Init
	bool do_calibration1 = adc_calibration_init(ADC_UNIT_1, ADC_ATTEN, &adc1_cali_handle);
	ESP_ERROR_CHECK(adc_continuous_register_event_callbacks(handle, &cbs, NULL));
	ESP_ERROR_CHECK(adc_continuous_start(handle));
	while(1) {
		ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
		while (1) {
			ret = adc_continuous_read(handle, result, READ_LEN, &ret_num, 0);
			if (ret == ESP_OK) {
				if (do_calibration1) {
					total = 0;
					for (int i = 0; i < ret_num; i += SOC_ADC_DIGI_RESULT_BYTES) {
						adc_digi_output_data_t *p = (void*)&result[i];
						uint32_t data = ADC_GET_DATA(p);
						total += data;
					}
					total /= (ret_num / SOC_ADC_DIGI_RESULT_BYTES);
					adc_cali_raw_to_voltage(adc1_cali_handle, total, &voltage);
//					voltage = total*1000/4096;	// キャリブレーション無し出力の場合
					printf("TOTAL:%d volt:%dmV\r\n",(int)total,(int)voltage);
				}
				vTaskDelay(500);
			} else if (ret == ESP_ERR_TIMEOUT) { 
				break;
			}
		}
	}
}

タスクで回しておけば、常にフィルタリングされた新しい値を得られます。

キャリブレーションの効果。0~0.9Vの間で効果がある。

キャリブレーション有り/無しでデータを比較してみました。電圧発生器SS7012で0~1.1Vを0.1V刻みで印加し測定、その結果です。

この個体はオフセットはマイナス。直線性は0.9Vまで良好。その上は急激に悪化。

キャリブレーション後はスケールのみ改善。オフセット変わらず。下のグラフはオレンジが無しでが有りです。小さいグラフのように0.9V以上は使い物になりませんが、それ以下ではもともと直線性はR2がかなり良いので曲線補正の効果は不明。

オフセットはキャリブレーションで取れないのでアプリで対処する

測定個体一個でしかも間引きサンプルの結果だけで結論づけるのは危ないですが、スケールの傾きは6.3%が0.28%まで改善したので、オフセットのみアプリケーション側で調整すればちょっとしたアナログ出力のセンサ読み取りには使えそうです。

お気軽にコメントをどうぞ。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)