ESP32のADC変換係数をArduinoスケッチで抽出してMicroPythonで電圧測定する

別記事でまとめた通りESP32のADCを使って電圧測定するためにはADCの読み値を補正する必要がある。

IDFやArduinoなどC/C++ベースのESP32開発環境であれば電圧取得のためのAPIが用意されているが、MicroPythonでは自前で変換が必要。そこで、ArduinoでADCの読み値から電圧への変換係数を読み出すスケッチを書いて実行し、得られた係数を使ってMicroPython上でADCの読み値から電圧値へ変換することに成功したので方法を記事に残しておく。

スポンサーリンク

変換式をAPIのソースコードから拾う

ESP32内蔵ADCのキャリブレーションについては、下記公式ドキュメントに解説がある。

Page Not Found - ESP32 - — ESP-IDF Programming Guide latest documentation

このドキュメントによるとESP32で用意されているAPI関数の内部では、xをADCの読み値として一次式 y = coeff_a * x + coeff_b に基づいて電圧値を算出するとある(ESP32チップのバージョンによっては更に補正をかけて精度を上げる物もあるようだが、ここでは割愛)。ESP32のAPIソースから該当箇所を探すと以下に引用するcalculate_voltage_linear関数が見つかり、この関数がADCの読値をmV単位の電圧値へ変換していることが判った。

static uint32_t calculate_voltage_linear(uint32_t adc_reading, uint32_t coeff_a, uint32_t coeff_b)
{
    //Where voltage = coeff_a * adc_reading + coeff_b
    return (((coeff_a * adc_reading) + LIN_COEFF_A_ROUND) / LIN_COEFF_A_SCALE) + coeff_b;
}

一次式の割に演算が少々複雑に見えるのは傾きcoeff_aが固定小数表現されているせいで、ADC値とcoeff_aの整数での積を取ってからLIN_COEFF_A_SCALEで正規化を行っている。LIN_COEFF_A_ROUNDを足し算するのは四捨五入が目的で、値はLIN_COEFF_A_SCALEとともにマクロで定義されている。

#define LIN_COEFF_A_SCALE               65536
#define LIN_COEFF_A_ROUND               (LIN_COEFF_A_SCALE/2)

Arduinoスケッチで係数を抽出する

一次式の傾きcoeff_aと切片coeff_bの係数値はAPI関数のesp_adc_cal_characterizeを呼び出すことで取得出来る。esp_adc_cal_characterize関数にはアッテネータやビット幅の設定を引数で指定出来、その指定次第でcoef_aやcoef_bが変化する可能性があるため実際にプログラムを動かして確認しておく。ESP32にはADC1とADC2の2つのADCがあり、それぞれ専用の電圧変換係数を持っているのだが、MicroPythonではADC2はWiFiなどで専有されておりユーザープログラムからはADC1しか使えないためADC1に絞って調べることにする。

別記事で取り上げたIDF用のサンプルプログラムを少し改造して作った以下のような簡単なArduinoスケッチで係数が変化する要因を確認する(ArduinoはESP32対応の設定をしておくこと)。

#include <esp_adc_cal.h>

static esp_adc_cal_characteristics_t *adc_chars = NULL;

#define DEFAULT_VREF    1100        //Use adc2_vref_to_gpio() to obtain a better estimate
static const adc_channel_t channel = ADC_CHANNEL_6;     //GPIO34 if ADC1, GPIO14 if ADC2
static const adc_unit_t unit = ADC_UNIT_1;

void setup() {
  if (esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_VREF) == ESP_OK) {
    printf("eFuse Vref: Supported\n");
    adc_chars = (esp_adc_cal_characteristics_t *)calloc(1, sizeof(esp_adc_cal_characteristics_t));
    for(int atten = ADC_ATTEN_DB_0; atten <= ADC_ATTEN_DB_11; atten++) {
      for(int width = ADC_WIDTH_BIT_9; width <= ADC_WIDTH_BIT_12; width++) {
    esp_adc_cal_value_t val_type = esp_adc_cal_characterize(unit, (adc_atten_t)atten, (adc_bits_width_t)width, DEFAULT_VREF, adc_chars);
    if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) {
      printf("atten=%d,bit_width=%d,coeff_a=%d,coeff_b=%d,vref=%d\n", adc_chars->atten, adc_chars->bit_width, adc_chars->coeff_a, adc_chars->coeff_b, adc_chars->vref);
    } else {
      printf("N/A\n");
    }
      }
    }
  }
}

void loop() {
}

スケッチの実行結果は以下の通り。ビット幅(width)が何であっても係数は同じだが、アッテネータ(atten)の指定が変わると係数が変化するのがわかる。

eFuse Vref: Supported
[atten=0,bit_width=0]coeff_a=15717,coeff_b=75,vref=1121
[atten=0,bit_width=1]coeff_a=15717,coeff_b=75,vref=1121
[atten=0,bit_width=2]coeff_a=15717,coeff_b=75,vref=1121
[atten=0,bit_width=3]coeff_a=15717,coeff_b=75,vref=1121
[atten=1,bit_width=0]coeff_a=20864,coeff_b=78,vref=1121
[atten=1,bit_width=1]coeff_a=20864,coeff_b=78,vref=1121
[atten=1,bit_width=2]coeff_a=20864,coeff_b=78,vref=1121
[atten=1,bit_width=3]coeff_a=20864,coeff_b=78,vref=1121
[atten=2,bit_width=0]coeff_a=28868,coeff_b=107,vref=1121
[atten=2,bit_width=1]coeff_a=28868,coeff_b=107,vref=1121
[atten=2,bit_width=2]coeff_a=28868,coeff_b=107,vref=1121
[atten=2,bit_width=3]coeff_a=28868,coeff_b=107,vref=1121
[atten=3,bit_width=0]coeff_a=53806,coeff_b=142,vref=1121
[atten=3,bit_width=1]coeff_a=53806,coeff_b=142,vref=1121
[atten=3,bit_width=2]coeff_a=53806,coeff_b=142,vref=1121
[atten=3,bit_width=3]coeff_a=53806,coeff_b=142,vref=1121

MicoroPython用の係数を出力するスケッチ

係数はアッテネータの設定値のみに依存すると分かったので、上記のスケッチをベースにsetup関数だけを以下のように変更してMicroPython用の係数として使える物をプログラムで計算して出力させてみる。

void setup() {
  int coeff_a[4], coeff_b[4];
  const adc_bits_width_t width = ADC_WIDTH_BIT_9;

  if (esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_VREF) == ESP_OK) {
    adc_chars = (esp_adc_cal_characteristics_t *)calloc(1, sizeof(esp_adc_cal_characteristics_t));
    for(int atten = ADC_ATTEN_DB_0; atten <= ADC_ATTEN_DB_11; atten++) {
      esp_adc_cal_value_t val_type = esp_adc_cal_characterize(unit, (adc_atten_t)atten, width, DEFAULT_VREF, adc_chars);
      if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) {
    coeff_a[atten] = adc_chars->coeff_a;
    coeff_b[atten] = adc_chars->coeff_b;
      } else {
    printf("N/A\n");
      }
    }
    // output coeff_a
    printf("coeff_a = [%f", (float)coeff_a[ADC_ATTEN_DB_0]/65536.0);
    for(int atten = (ADC_ATTEN_DB_0+1); atten <= ADC_ATTEN_DB_11; atten++)
      printf(", %f", (float)coeff_a[atten]/65536.0);
    printf("]\n");
    // output coeff_b
    printf("coeff_b = [%d", coeff_b[ADC_ATTEN_DB_0]);
    for(int atten = (ADC_ATTEN_DB_0+1); atten <= ADC_ATTEN_DB_11; atten++)
      printf(", %d", coeff_b[atten]);
    printf("]\n");
  }
}

実行結果は以下の通りで、アッテネータの設定値ごとに係数がリストで出力されている。これをそのままMicroPythonコードに貼り付けて使う。

coeff_a = [0.239822, 0.318359, 0.440491, 0.821014]
coeff_b = [75, 78, 107, 142]

手持ちのESP32を3台調べたところ2台は全く同じ係数で、残り1台は傾きcoeff_aだけが異なっていた。coeff_bは共通だが、coeff_aはチップの個体ごとに違う値が書き込まれているのだろう。

電圧を測定するMicroPythonコード

Arduinoスケッチで読み出した係数を使って、MicroPythonで実際に電圧値の測定を行ってみる。係数の値は上で調べたとおりチップの個体ごと異なる可能性があり、Arduinoスケッチを実行したESP32と同じ個体でMicroPythonを動かす方が無難。なのでArduinoスケッチを動作させた後、同じESP32にMicroPythonのファームウエアを書き込む(MicroPythonのファーム書き込みについてはこの記事を参照)。

MicroPythonへの書き込みが完了したら、以下のMicroPythonコードを実行する。シリアルコンソール経由の対話型プロンプト(REPL)の場合はctrl-Eを押して貼り付けモードでコピペして実行する。(ampy runだと何故かうまく実行出来なかった)

  • ファイルダウンロード:volt.py
from machine import ADC, Pin

coeff_a = [0.239822, 0.318359, 0.440491, 0.821014]
coeff_b = [75, 78, 107, 142]

class MilliVolt(ADC):
    def __init__(self, pin):
        super().__init__(pin)
        self.atten(ADC.ATTN_0DB)
        self.width(ADC.WIDTH_12BIT)

    def atten(self, atten_id):
        self.atten_id = atten_id
        super().atten(self.atten_id)

    def width(self, width_id):
        self.width_id = width_id
        self.scale = 2**(3-self.width_id)
        super().width(self.width_id)

    def get(self):
        raw_scaled = super().read()*self.scale
        return coeff_a[self.atten_id]*raw_scaled + coeff_b[self.atten_id]

if __name__ == '__main__':
    mv = MilliVolt(Pin(34))

    while True:
        val = mv.get()
        print(str(val) + 'mV', end='\r')

MicroPythonでも電圧測定OK!

上記プログラムを実行すると、GPIO34をADCで読んで得られたmV単位の電圧値が無限ループで繰り返し表示される。可変抵抗を使うなどしてGPIO34に印加する電圧を変えながらマルチメータで同時測定すると良いだろう。0V付近で電圧を変えながら測定すると、変換式の切片(coeff_b)より下の電圧は全く測定出来ない事がよく分かる。精度的にもESP32 APIで変換したのと同等の感触。

上記MicroPythonプログラムで定義したMilliVoltクラスはデフォルトのADCクラスを継承しており、ADCクラスと同様attenメソッドでアッテネータの設定したりwidthメソッドでビット幅の設定も出来るので、電圧測定したい時はADCクラスの代わりに使える。

まとめ

ESP32のArduinoスケッチで変換係数を取り出し、MicroPythonで電圧を測定出来るようにした。作ったMilliVoltクラスはデフォルトのADCクラスを継承しているので置き換えて使うことも出来る。ESP32内蔵ADCを使った手軽な電圧測定に使えそうだ。

コメント