ESP32のADコンバータを調整:ESP-IDFのドライバAPIでキャリブレーションしてみる

Arduino/ESP32ADコンバータ,eFuse,ESP-IDF,ESP32,Vref

前回、初めてADコンバータというものを使ってみました。

とはいってもESP32に内蔵されているものですが、、、

しかし困った事に、かなり実測値とかけ離れたデータが取れてしまいます。

Webで情報収集してみると、入力は3.3Vまでだが実際は3.9Vスケールだとか3.6Vスケールだとか色々出てくるわけですが、、、

確かに前回の数値を3.9Vスケールで計算すると、実測値に近づくは近づくのですが、、

なんだかそういうのは違う気がする。

一応、3.6V、3.9Vのスケールで計算してみました。

3.3V(外部)3.3V(ESP32)5V(外部)5V(ESP32)
analogRead1392143922052303
計算電圧2.80V2.90V4.45V4.65V
実測電圧3.22V3.274.77V4.91V
実測差0.42V0.38V0.32V0.26V
3.6Vスケール3.05V3.16V4.84V5.06V
3.9Vスケール3.31V3.42V5.25V5.48V

釈然としませんね、、、

そんな時はリファレンスを読めとじっちゃんが言ってた

リファレンスによると、チップによってADCが参照する電圧レンジに差があるから、内部基準電圧で補正するAPIを使えとか書いてあるンゴ

ついでにulp(省電力モード)でも使える様にする方法も少し書いてありますたが、今回はひとまずパス。

で、肝心のAPIですが、、

↑これはESP-IDFのAPIですが、Arduino用も恐らくあるだろうと探してみるとありますた。

多分これ。↓

GitHub espressif – arduino-esp32/tools/sdk/include/esp_adc_cal/esp_adc_cal.h

必要そうなメソッドはとりあえず移植されているっぽいので、よく判らないけど使ってみます。

ESP-IDFのexampleを見るに、ADC用の構造体を作って補正APIを紐づける感じなのかな。

これによると、、ADCドライバAPIを使うのに構造体に必要なのは以下の4点っぽいですん

ADCチャンネル番号

APIがサポートするGPIOの範囲は以下の通り

ADC1    GPIO 32-39
ADC2    GPIO 0, 2, 4, 12-15, 25-27

チャンネル番号で指定しなくてはいけないらしく、どのGPIOがどのチャンネルか調べる必要がありそう。

なんかGPIOとADCチャンネルの対応表みたいなのが何処かのヘッダファイルにきっとある

アッテネータの値

アッテネータってなんだ・・・?という感じですが、、

ざっくりと信号を波形はそのまま減衰させる装置が内臓されているらしいですぬ

4段階で使えるそうで、0db、2.5db、6db、11dbだそうな。

どんな感じかよく判らないけど、、

調べてる最中に見かけたものの中に、ADCは0~1.1Vまでの電圧を読み取るからアッテネーターで減衰して使うみたいなのがあった気がする。

と思ったら、公式ドキュメントにありました。

When VDD_A is 3.3V:
 ・0dB attenuaton (ADC_ATTEN_DB_0) gives full-scale voltage 1.1V
 ・2.5dB attenuation (ADC_ATTEN_DB_2_5) gives full-scale voltage 1.5V
 ・6dB attenuation (ADC_ATTEN_DB_6) gives full-scale voltage 2.2V
 ・11dB attenuation (ADC_ATTEN_DB_11) gives full-scale voltage 3.9V (see note below)

ESPRESSIF – ESP-IDF Programming Guide
https://docs.espressif.com/projects/esp-idf/en/stable/api-reference/peripherals/adc.html#_CPPv425adc1_config_channel_atten14adc1_channel_t11adc_atten_t

減衰を11dBにするとスケールは0~3.9Vで計算されるよ、と。

さらにこんな記述が。

 At 11dB attenuation the maximum voltage is limited by VDD_A, not the full scale voltage.
 ・0dB attenuaton (ADC_ATTEN_DB_0) between 100 and 950mV
 ・2.5dB attenuation (ADC_ATTEN_DB_2_5) between 100 and 1250mV
 ・6dB attenuation (ADC_ATTEN_DB_6) between 150 to 1750mV
 ・11dB attenuation (ADC_ATTEN_DB_11) between 150 to 2450mV
For maximum accuracy, use the ADC calibration APIs and measure voltages within these recommended ranges.

ESPRESSIF – ESP-IDF Programming Guide
https://docs.espressif.com/projects/esp-idf/en/stable/api-reference/peripherals/adc.html#_CPPv425adc1_config_channel_atten14adc1_channel_t11adc_atten_t

ちょっと何言ってるかわかんないですね

つまり11dBの減衰では3.9Vのスケールで計算されるけど、VDD_Aの値に制限される、と。

ちょっと何言ってるかわかんないですね

VDD_Aというのは電源電圧の事らしい。

減衰を11dBにすると、最大3.9Vのスケールでデータを取得できるが、電源電圧が大体3.35V位迄のため、VDD_Aを使って計算しろと、、?

あってます、、、?

そんな、、、

電圧計を作ってるのに、電源電圧で計算しろとか

ちょっと何言ってるか判らないですね・・・

進まないのでひとまず置いておきます。

解像度

9,10,11,12ビットが使えるらしい。

それぞれ入力電圧までのスケールをどのくらいの解像度で取得するか、という設定の様です。

ビット数の値によって、512,1024,2048,4096階調出ると。

電圧計用途のため速度も必要ありませんので、勿論12ビットしか選びませんが、、、

校正値(Vref)

これが多分キモです。

製造チップによって基準が違っているらしく、なんかAPIでこれを調べられるメソッドがある模様。

尚、値が既に入っているものはAPIを使えばわざわざ指定しなくても勝手に使ってくれるらしい。

コマンドラインツールで確認するのがお手軽っぽいのでちょっと打ってみました。

> espefuse.py.exe --port COM3 adc_info
espefuse.py v2.8
Connecting....
ADC VRef calibration: 1142mV

このチップでは1142mVで校正が行われていたという事らしいです。

このタイプのほか、2点校正(150mVと850mV)を行っているチップもあるらしいですが、ひとまず私のは一点校正だったという事で。

さて、、わりと調べるのがありますな、、、

もう疲れたよバドラッジュ

尚、ADCチャンネル番号と、アッテネータの値の定数は以下のヘッダにありました。

GPIOとADCチャンネルの対応を調べるのが面倒ですが、GPIOでチャンネルが判るマクロがある様です。

ざっくりとまとめると、こんな感じかしらん

・ESP-IDFでADコンバータのAPIを利用すれば校正値を利用できる
・校正値は製造チップで3パターンあり、eFuseに書かれている(2地点計測/1地点計測/校正情報なし)
・APIを利用すれば自動的に校正値が有効になる(無い場合はデフォルト値が使われる)

かなり面倒そうな感触ではありますが、ここまで来たらやるしかないンゴねぇ

さてじゃそろそろ使ってみる事にします。

ESP-IDFのAPIでADCを構成する

キャリブレーションを通してADCを利用する際には、ADC1を使うかADC2を使うかで若干の違いがある様でした。

基本的にADC2はwifiを使うとおかしくなる様ですので、ADC1のGPIOピンを利用する方が良いと思います。

全体の工程はこんな感じ

  • ADCで使うピン(GPIO)を決定
  • 設定値を決定(解像度、減衰値)し、ADCに渡す
  • 校正用の構造体インスタンスを作成し、キャリブレーションAPIに設定値とともに構造体を渡す
  • getメソッド実行

スケッチにするとそれほど量は多くないです。

下の方のモニタ用キャリブレーション値変更の箇所がやたら長いだけです、、

#include <Arduino.h>
/*
	ESP-IDF ADコンバータAPI キャリブレーション利用
	電圧測定
*/

// ADCドライバ
#include <driver/adc.h>
#include <esp_adc_cal.h>

#define DEFAULT_VREF 1100 // eFuse Vrefがチップに記録されていない場合に利用される(らしい)

// 校正計算用
adc_unit_t unit1 = ADC_UNIT_1;				   //	ADC_UNIT_1, ADC_UNIT_2, ADC_UNIT_BOTH, ADC_UNIT_ALTER (1-3,7) + ADC_UNIT_MAX
adc_unit_t unit2 = ADC_UNIT_2;				   //	ADC_UNIT_1, ADC_UNIT_2, ADC_UNIT_BOTH, ADC_UNIT_ALTER (1-3,7) + ADC_UNIT_MAX
adc_atten_t atten = ADC_ATTEN_DB_11;		   //	ADC_ATTEN_DB_0, ADC_ATTEN_DB_2_5, ADC_ATTEN_DB_6, ADC_ATTEN_DB_11 (0-3) + ADC_ATTEN_DB_MAX
adc_bits_width_t width_bit = ADC_WIDTH_BIT_12; //	ADC_WIDTH_BIT_9, ADC_WIDTH_BIT_10, DC_WIDTH_BIT_11, ADC_WIDTH_BIT_12 (0-3) + ADC_WIDTH_MAX
//adc1_channel_t channel = ADC1_CHANNEL_4;	   //	GPIO32 if ADC1
adc1_channel_t channel_1 = ADC1_GPIO32_CHANNEL; //	ADC1_GPIO32_CHANNEL マクロがあるので表を調べなくても大丈夫っぽい
adc2_channel_t channel_2 = ADC2_GPIO27_CHANNEL; //	ADC2_GPIO27_CHANNEL

// ADC補正用構造体へのポインタ
esp_adc_cal_characteristics_t *adc_chars_1;
esp_adc_cal_characteristics_t *adc_chars_2;

uint8_t adc1_pin = 32;
uint8_t adc2_pin = 27;
float_t R1 = 330; //	330Ω 金属皮膜抵抗 328
float_t R2 = 220; //	220Ω 金属皮膜抵抗 217

static void check_efuse()
{
	//Check TP is burned into eFuse
	if (esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_TP) == ESP_OK)
	{
		printf("eFuse Two Point: Supported\n");
	}
	else
	{
		printf("eFuse Two Point: NOT supported\n");
	}

	//Check Vref is burned into eFuse
	if (esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_VREF) == ESP_OK)
	{
		printf("eFuse Vref: Supported\n");
	}
	else
	{
		printf("eFuse Vref: NOT supported\n");
	}
}

static void print_char_val_type(esp_adc_cal_value_t val_type)
{
	if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP)
	{
		printf("Characterized using Two Point Value\n");
	}
	else if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF)
	{
		printf("Characterized using eFuse Vref\n");
	}
	else
	{
		printf("Characterized using Default Vref\n");
	}
}

static void print_calibration_status()
{
	// 構造体を一応出力
	Serial.println("-------------Calibration STATUS-------------");
	Serial.println("---------ADC1---------");
	Serial.print("ADC num : ");
	Serial.println(adc_chars_1->adc_num);
	Serial.print("Attenuation : ");
	Serial.println(adc_chars_1->atten);
	Serial.print("Bit width : ");
	Serial.println(adc_chars_1->bit_width);
	Serial.print("Gradient of ADC-Voltage curve : ");
	Serial.println(adc_chars_1->coeff_a);
	Serial.print("Offset of ADC-Voltage curve : ");
	Serial.println(adc_chars_1->coeff_b);
	Serial.print("vref table : ");
	Serial.println(adc_chars_1->vref);
	Serial.println("---------ADC2---------");
	Serial.print("ADC num : ");
	Serial.println(adc_chars_2->adc_num);
	Serial.print("Attenuation : ");
	Serial.println(adc_chars_2->atten);
	Serial.print("Bit width : ");
	Serial.println(adc_chars_2->bit_width);
	Serial.print("Gradient of ADC-Voltage curve : ");
	Serial.println(adc_chars_2->coeff_a);
	Serial.print("Offset of ADC-Voltage curve : ");
	Serial.println(adc_chars_2->coeff_b);
	Serial.print("vref table : ");
	Serial.println(adc_chars_2->vref);
	Serial.println("-------------end Calibration STATUS-------------");
}

void setup()
{
	// put your setup code here, to run once:
	// シリアル通信を開始
	Serial.begin(115200);

	// 一応GPIOピンのモードを変えておく
	pinMode(adc1_pin, ANALOG);
	pinMode(adc2_pin, ANALOG);

	// チップサポートを確認
	check_efuse();

	//Configure ADC
	//	ADC1
	// ADC1は解像度も保持できる
	adc1_config_width(width_bit);
	adc1_config_channel_atten(channel_1, atten);
	//	ADC2
	// ADC2は解像度を保持できないらしい・・・?のでアッテネータの設定のみ
	adc2_config_channel_atten((adc2_channel_t)channel_2, atten);

	// ADC校正構造体のサイズを取得
	//	voidからのポインタキャストとかめんどくさいンゴねぇ
	adc_chars_1 = (esp_adc_cal_characteristics_t *)calloc(1, sizeof(esp_adc_cal_characteristics_t)); //	ADC1
	adc_chars_2 = (esp_adc_cal_characteristics_t *)calloc(1, sizeof(esp_adc_cal_characteristics_t)); //	ADC2

	// 校正APIに設定値と構造体を渡す+ついでにキャリブレーションタイプ取得
	//	ADC1
	esp_adc_cal_value_t val_type_1 = esp_adc_cal_characterize(
		unit1,		  // adc_unit_t ADCユニット番号
		atten,		  // adc_atten_t アッテネータ設定値
		width_bit,	  // adc_bits_width_t ADC解像度
		DEFAULT_VREF, // uint32_t デフォルト校正値
		adc_chars_1); // esp_adc_cal_characteristics_t * キャリブレーション用の構造体へのポインタ

	//	ADC2
	esp_adc_cal_value_t val_type_2 = esp_adc_cal_characterize(
		unit2,		  // adc_unit_t ADCユニット番号
		atten,		  // adc_atten_t アッテネータ設定値
		width_bit,	  // adc_bits_width_t ADC解像度
		DEFAULT_VREF, // uint32_t デフォルト校正値
		adc_chars_2); // esp_adc_cal_characteristics_t * キャリブレーション用の構造体へのポインタ

	// ADCキャリブレーションタイプを確認
	print_char_val_type(val_type_1);
	print_char_val_type(val_type_2);

	// adc2の場合他のadc1のGPIOピンを経由してキャリブレーションできるらしい・・・?
	//	adc2_vref_to_gpio(---GPIONUM---);
}

void loop()
{
	// APIからの取得 ADC1
	uint32_t raw_1 = adc1_get_raw((adc1_channel_t)channel_1);
	// ADC2はちょっと面倒・・・
	int raw_2;
	adc2_get_raw((adc2_channel_t)channel_2, adc_chars_2->bit_width, &raw_2);

	// キャリブレーションの補正値が出てしまうので、raw_1が0なら計算しない
	if (raw_1 > 0)
	{
		Serial.println("---- ADC1 ----");
		Serial.print("Raw Data : ");
		Serial.println(raw_1);

		uint32_t voltage = esp_adc_cal_raw_to_voltage(raw_1, adc_chars_1); // return mV
		Serial.print("Raw To Voltage : ");
		Serial.println(voltage);

		Serial.print("Restored Voltage : ");
		Serial.printf("%4.3f", float_t((voltage / float_t(R2 / (R1 + R2))) / 1000)); // mVなので単位をVに直す
		Serial.println(" V");
	}
	else
	{
		Serial.println("-- ADC1 is No Signal. --");
	}

	if (raw_2 > 0)
	{
		Serial.println("---- ADC2 ----");
		Serial.print("Raw Data : ");
		Serial.println(raw_2);

		uint32_t voltage = esp_adc_cal_raw_to_voltage(raw_2, adc_chars_1); // return mV
		Serial.print("Raw To Voltage : ");
		Serial.println(voltage);

		Serial.print("Restored Voltage : ");
		Serial.printf("%4.3f", float_t((voltage / float_t(R2 / (R1 + R2))) / 1000)); // mVなので単位をVに直す
		Serial.println(" V");
	}
	else
	{
		Serial.println("-- ADC2 is No Signal. --");
	}

	// キャリブレーション等を変更できる様に
	if (Serial.available())
	{
		int inputchar = Serial.read();

		// 面倒なのでアッテネーターを変更できる様に
		if (char(inputchar) == '1')
		{
			adc1_config_channel_atten(channel_1, ADC_ATTEN_DB_0);
			Serial.println("Attenuation mode : 0 db");
		}
		else if (char(inputchar) == '2')
		{
			adc1_config_channel_atten(channel_1, ADC_ATTEN_DB_2_5);
			Serial.println("Attenuation mode : 2.5db");
		}
		else if (char(inputchar) == '3')
		{
			adc1_config_channel_atten(channel_1, ADC_ATTEN_DB_6);
			Serial.println("Attenuation mode : 6db");
		}
		else if (char(inputchar) == '4')
		{
			adc1_config_channel_atten(channel_1, ADC_ATTEN_DB_11);
			Serial.println("Attenuation mode : 11db");
		}
		else if (char(inputchar) == 'A')
		{
			// coeff_a 補正値 + 100
			adc_chars_1->coeff_a = adc_chars_1->coeff_a + 100;
			Serial.print("coeff_a +100 to :");
			Serial.println(adc_chars_1->coeff_a);
		}
		else if (char(inputchar) == 'S')
		{
			// coeff_a 補正値 - 100
			adc_chars_1->coeff_a = adc_chars_1->coeff_a - 100;
			Serial.print("coeff_a -100 to :");
			Serial.println(adc_chars_1->coeff_a);
		}
		else if (char(inputchar) == 'D')
		{
			// coeff_b 補正値 + 5
			adc_chars_1->coeff_b = adc_chars_1->coeff_b + 5;
			Serial.print("coeff_b +5 to :");
			Serial.println(adc_chars_1->coeff_b);
		}
		else if (char(inputchar) == 'F')
		{
			// coeff_b 補正値 - 5
			adc_chars_1->coeff_b = adc_chars_1->coeff_b - 5;
			Serial.print("coeff_b -5 to :");
			Serial.println(adc_chars_1->coeff_b);
		}
		else if (char(inputchar) == 'W')
		{
			// 解像度トグル
			uint8_t _current_val = adc_chars_1->bit_width;
			if (_current_val > 2)
			{
				adc_chars_1->bit_width = (adc_bits_width_t)0;
			}
			else
			{
				adc_chars_1->bit_width = (adc_bits_width_t)(_current_val + 1);
			}
			Serial.print("Bit width change to :");
			Serial.println(adc_chars_1->bit_width);
		}
		else if (char(inputchar) == 'Z')
		{
			// デフォルトリセット
			esp_adc_cal_characterize(unit1, atten, width_bit, DEFAULT_VREF, adc_chars_1);
			esp_adc_cal_characterize(unit2, atten, width_bit, DEFAULT_VREF, adc_chars_2);
			Serial.println("-------------RESET Calibration-------------");
			// 設定値再表示
			print_calibration_status();
		}
		else if (char(inputchar) == 'P')
		{
			// 状態表示
			print_calibration_status();
		}
	}

	delay(1000);
}

ADC2も一応使ってはみてますが、、、

キャリブレーションの変更は面倒なのでADC1の分しか書いてませんが、、

シリアルモニタで文字を送ると値を変更できる様にしています。

1-4:アッテネータの減衰変更
A,S:係数 +100, -100
D,F:オフセット +5, -5
W:解像度 9bit~12bit 押す度にトグル
Z:初期化時の値にリセット
P:構造体の値表示

初期化せずに構造体の値を直接いじってしまっています。

ホントはだめなんでしょうけれど、、

とりあえずこんな感じでADC1のGPIO32ピン とADC2のGPIO27ピン、GNDを繋ぎます。

3.3Vと5Vも取り出せる様にボードに挿しておいてますが、ESP32側には刺しません。(あとで)

回路図は前回のものにADC2のピンGPIO27を足しただけです。

それでは動かしてみます。

おお?

なんかソレっぽい値になってるじゃないですか。

少し実測値よりも多く出ている様に思いますが、前回に比べればかなり誤差の範囲かもしか。

さてではキャリブレーションの値を確認しますと、、

なんかADC1とADC2では値が違うんですね

オフセットはADC1の方が大きいですが、係数はADC2の方が大きく設定されている様です。

この数値をいじれば、ある程度はこちらでキャリブレーションできるかもしれませぬね。

さてじゃ数値を取って実測値との差を確認しましょうか

テスターを、、、

・・・・ジジッ!!

もくもく・・もくもく・・・

いい香りがしてきたンゴねぇorz

なんか真ん中のチップが少し艶やかな様な

やっちまってますんorz

接触が悪く電圧も安定しなかった電源君ですが、3.3V と 5V を両方出せて便利でしたのに、、、

この三端子レギュレータだけ交換したら直らないかしらん

仕方がないので電源はESP32から取る事に。

リファレンスが減ってしまい参考にならないかもしれませんが

3.3V(実測 3.27V)ADC1ADC2
Raw1467-14711526-1530
RawVoltage1370-1372 mV1418-1421 mV
Voltage3.425-3.430 V3.545-3.555 V
5V(実測 4.91V)ADC1ADC2
Raw2275-22782337-2342
RawVoltage2045-2048 mV2097-2101 mV
Voltage5.113-5.120 V5.242-5.245 V

実測との差が0.2V前後となり、一番上の3.6Vスケール、3.9Vスケールの丁度間くらいの数値に。

ADC2は係数が大きいだけあって少し高い数値が出ますね。

元の3.3Vスケールで計算するよりはAPIの係数をいじった方が実測に近い値が出るのではないかと思います。

思うだけですが、、、

目標であったリポバッテリーの2セルを測定するにはもっと広い電圧を測れる様に作り変えないといけませんが、色々調べてみて釈然としない部分がだいぶスッキリしてきました。

ひとまずは少し高い電圧で出るものとして取り扱えばいいのかな。

ESP32の内臓ADCの基本的な使い方が判ったので、きっと後々役に立つに違いない

たぶん、、、いつか、、

次回がありましたら、もう少し高い電圧まで測定できる様にして、実測値との差を確認していこうかなと思います。