ESP32のArduino Core analogRead()関数検証

概要

前回アナログ入力の内部調査して、すこし気になるところがあったので、いろいろ設定を変えながら調べてみました。

減衰

ESP32は1.1Vを基準電圧として、12ビットのADCを行っています。そのため減衰器を使って入力電圧を下げてから測定をしています。

減衰減衰率減衰比減衰前減衰後
ADC_11db0.281838293約1 : 3.63.3 V0.93 V
ADC_6db0.501187234約1 : 21.8 V0.90 V
ADC_2_5db0.749894209約1 : 1.341.2 V0.90 V
ADC_0db11 : 10.9 V0.90 V

減衰器は4種類あり、減衰しない0db、約75%にする-2.5db、約50%にする-6db、約28%にする-11dbがあります。標準は-11dbになっており、3.6Vを入力してもADCには1Vしか渡さなくなっています。

0Vから1.1VまでADCは測定できるはずですが、実際には補正がされており0Vから0.9Vぐらいまでの範囲を測定しているようです。そのため3.3Vを入力した場合、最大値の4095になりますので3.6Vの入力でも最大値のままです。

右側の2列が0.9Vぐらいになる場合の想定電圧です。減衰を変えながら何ボルトでADCの値が4095になるのかを実機にて確認してみました。

利用機材

ESP32は手持ちのM5StickCを使いましたが、標準的なESP32でもそれほど変わらない動きだと思います。2018年以前に製造されたESP32の場合にはADCの補正が若干違うとデータシートに書いてありました。

安定化電源としてZK-DP3Dを利用しました。この機材はUSBから電圧と電流の制御が可能な小型電源です。非常に便利ですが最低電圧が1Vでしたので、一部の実験ができませんでした。

スケッチ

void setup() {
  Serial.begin(115200);
  delay(100);
  pinMode(26, ANALOG);
  analogSetAttenuation(ADC_11db);
}
void loop() {
  Serial.println(analogRead(26));
  delay(10);
}

引数のdbを変えながら、電圧を変えながら出力が安定して4095になる電圧を計測しました。個体差があり、回数も1度だけですので参考程度にしてください。

結果

減衰4095になった電圧
ADC_11db3.13 V
ADC_6db1.83 V
ADC_2_5db1.31 V
ADC_0db

減衰なしの場合には、機材の関係で1V未満の電圧を設定できないので計測できませんでした。手元の環境だと-11dbの標準設定では3.13Vで最大値になってしまいました。手元の安いテスターで確認したところ3.12Vでしたのでそれほどずれてはいないはずです。

事前予測との差を考えると、やはり減衰後に0.9Vぐらいの場合に4095になるのではないかと推測することができます。

サイクル数

ADCの測定時間は、サイクル数で指定しています。CPUサイクルなのか周辺機器サイクルなのかを調べてみました。

Arduino Coreの標準値は8ですが、ESP-IDFは初期値の9になっています。

スケッチ

void setup() {
  Serial.begin(115200);
  delay(100);
  pinMode(36, ANALOG); // ADC1
  pinMode(26, ANALOG); // ADC2
  uint8_t cpu_list[] = {240, 160, 80, 40, 20, 10};
  for (int cpu = 0; cpu < sizeof(cpu_list) ; cpu++) {
    setCpuFrequencyMhz(cpu_list[cpu]);
    for (int cycles = 7; cycles < 10; cycles++) {
      analogSetCycles(cycles);
      // ADC1
      unsigned long start_time = millis();
      for (int i = 0; i < 10000; i++) {
        analogRead(36);
      }
      unsigned long adc1_time = millis() - start_time;
      Serial.printf("CPU = %d, Cycles = %d, adc1_time = %lu\n", cpu_list[cpu], cycles, adc1_time);
      // ADC2
      start_time = millis();
      for (int i = 0; i < 10000; i++) {
        analogRead(26);
      }
      unsigned long adc2_time = millis() - start_time;
      Serial.printf("CPU = %d, Cycles = %d, adc2_time = %lu\n", cpu_list[cpu], cycles, adc2_time);
    }
  }
}
void loop() {
}

ADC1とADC2で内部処理が異なるので、別に確認しています。ループで1万回アナログ入力を行い、経過時間を測定しました。

結果

CPU周波数サイクルADC1(ミリ秒)ADC2(ミリ秒)
240796113
2408101117
2409101119
1607103121
1608103121
1609103121
807120133
808120133
809120134
407158175
408167176
409167175
207253278
208254278
209253279
107524546
108526546
109524546

CPU速度に応じて、経過時間が変化しているのでCPUクロック数で測定しているようです。また、ADC1とADC2では、ADC1の方が高速で取得可能でした。これはADC2は各種設定値を測定のたびに設定しないといけないオーバーヘッドがあるためと思われます。

サンプル数

複数回ADCの値を取得して、誤差をへらす機能があります。標準は1回で、複数回取得を行いません。ただし、データシートで公開されていない機能で、次期Arduino Coreでは廃止予定になります。

スケッチ

void setup() {
  Serial.begin(115200);
  delay(100);
  pinMode(36, ANALOG); // ADC1
  pinMode(26, ANALOG); // ADC2
  for (int samples = 1; samples <= 3; samples++) {
    analogSetSamples(samples);
    // ADC1
    unsigned long start_time = millis();
    for (int i = 0; i < 10000; i++) {
      analogRead(36);
    }
    unsigned long adc1_time = millis() - start_time;
    Serial.printf("Samples = %d, adc1_time = %lu\n", samples, adc1_time);
    // ADC2
    start_time = millis();
    for (int i = 0; i < 10000; i++) {
      analogRead(26);
    }
    unsigned long adc2_time = millis() - start_time;
    Serial.printf("Samples = %d, adc2_time = %lu\n", samples, adc2_time);
  }
}
void loop() {
}

CPUの変更をなくし、デフォルトの1回から3回まで変更した場合の時間を調べました。

結果

サンプルADC1(ミリ秒)ADC2(ミリ秒)
1100118
2127144
3153171

サンプル数を増やすと時間はかかっていますが、思ったより増えませんでした。実際にADCを読み込んでいる時間よりも、analogRead()関数で各種ADCの設定をやっている処理がオーバーヘッドになっている可能性があります。

分周

サイクルでCPUのクロックサイクル数を使ってADCの測定をしていましたが、分周設定もあり、何クロックを1サイクルにするのかを指定することができます。Arduino Coreでは1が標準ですが、ESP-IDFでは無指定でESP32の標準値の2になっています。

スケッチ

void setup() {
  Serial.begin(115200);
  delay(100);
  pinMode(36, ANALOG); // ADC1
  pinMode(26, ANALOG); // ADC2
  for (int div = 1; div <= 3; div++) {
    analogSetClockDiv(div);
    // ADC1
    unsigned long start_time = millis();
    for (int i = 0; i < 10000; i++) {
      analogRead(36);
    }
    unsigned long adc1_time = millis() - start_time;
    Serial.printf("div = %d, adc1_time = %lu\n", div, adc1_time);
    // ADC2
    start_time = millis();
    for (int i = 0; i < 10000; i++) {
      analogRead(26);
    }
    unsigned long adc2_time = millis() - start_time;
    Serial.printf("div = %d, adc2_time = %lu\n", div, adc2_time);
  }
}
void loop() {
}

こちらも1から3まで変化させてみました。

結果

分周ADC1(ミリ秒)ADC2(ミリ秒)
1100117
2127145
3155172

ほぼ、サンプルと同じ結果になっています。

ビットレート

標準は12ビットですが、8ビットから12ビットまで選択することができます。

スケッチ

void setup() {
  Serial.begin(115200);
  delay(100);
  pinMode(36, ANALOG); // ADC1
  pinMode(26, ANALOG); // ADC2
  for (int width = 8; width <= 12; width++) {
    analogSetWidth(width);
    // ADC1
    unsigned long start_time = millis();
    for (int i = 0; i < 10000; i++) {
      analogRead(36);
    }
    unsigned long adc1_time = millis() - start_time;
    Serial.printf("width = %d, adc1_time = %lu\n", width, adc1_time);
    // ADC2
    start_time = millis();
    for (int i = 0; i < 10000; i++) {
      analogRead(26);
    }
    unsigned long adc2_time = millis() - start_time;
    Serial.printf("width = %d, adc2_time = %lu\n", width, adc2_time);
  }
}
void loop() {
}

8ビットから12ビットまで調べました。

結果

ビットADC1(ミリ秒)ADC2(ミリ秒)
895114
995114
1095113
1197114
12100117

結果は変わらないと思っていたのですが、ビット数が少ないほうが早かったです。データ転送時間が減るためでしょうか?

Arduino CoreとESP-IDFの差

設定値Cycles分周
Arduino Core81
ESP-IDF92

Arduino CoreはESP-IDFと違う設定値を使っている場所がありました。ESP-IDFはESP32の標準値ですので、Arduino Coreだけ独自設定値になっています。

Arduino Coreの次期バージョンではサイクル数の指定関数がなくなるので、デフォルト値が8から9に変更される予定です。

厳密な速度差ではないですが、パラメータの差でどれぐらいの速度差がでるのかを測定してみました。

スケッチ

void setup() {
  Serial.begin(115200);
  delay(100);
  pinMode(36, ANALOG); // ADC1
  pinMode(26, ANALOG); // ADC2
  for (int type = 0; type < 3; type++) {
    if (type == 0) {
      analogSetCycles(8);
      analogSetClockDiv(1);
    } else if (type == 1) {
      analogSetCycles(9);
      analogSetClockDiv(1);
    } else if (type == 2) {
      analogSetCycles(9);
      analogSetClockDiv(2);
    }
    // ADC1
    unsigned long start_time = millis();
    for (int i = 0; i < 10000; i++) {
      analogRead(36);
    }
    unsigned long adc1_time = millis() - start_time;
    Serial.printf("type = %d, adc1_time = %lu\n", type, adc1_time);
    // ADC2
    start_time = millis();
    for (int i = 0; i < 10000; i++) {
      analogRead(26);
    }
    unsigned long adc2_time = millis() - start_time;
    Serial.printf("type = %d, adc2_time = %lu\n", type, adc2_time);
  }
}
void loop() {
}

現状のArduino Core1.0.4と次期バージョン、ESP-IDFの標準値の3種類測定しています。

結果

設定値Cycles分周ADC1(ミリ秒)ADC2(ミリ秒)
Arduino Core(1.0.4)81100117
Arduino Core(次期)91101120
ESP-IDF(ESP32標準)92130146

次期バージョンでは若干ADCの速度が遅くなりそうです。ただしESP32の標準値より早い設定にしているのが気になります。今回はADCの精度を測定できていないですが、時間を短くすると精度に影響を与えるはずです。

まとめ

ADCはいろいろな関数があったのですが、やっと中身を理解することができました。ただし次期バージョンからはESP-IDFの関数を呼び出す形に変更が加えられていたので、すっきりはしますが解析が面倒になります。

ただESP-IDFのソースと見比べると、Arduino Coreは排他制御を一切していないので、次期バージョンはESP-IDFの関数にのせ替えるんだと思います。

もしかしたらADC1とADC2は並行で動かすことができるかもしれませんが、次期バージョンからは排他制御が入って同時実行ができなくなっているようです。

追加資料

この記事は結構前に書いていたので、上記の資料を参考にしていないのですがADCは減衰後で0.1V以上から測定しているみたいです。なので0.1Vから0.9Vの範囲になりそうです。両端は0と4095になるべくしたいので、若干甘めに設定していると思います。

コメント