はじめに
Arduino UNO R4のADコンバータは入力インピーダンスが低くて使いづらい。測定対象のインピーダンスが高いとUNO R4側に引っ張られてしまう。端的に言えば、正しい結果が得られない。
ならば、測定対象とUNO R4のアナログ入力の間にバッファを入れてやればいいのでは?上手いことに(?)UNO R4にはオペアンプ機能がある。これでボルテージフォロワを組めばいいじゃないか。というのが今回の話。
Arduino UNO R4のオペアンプ機能
ピンの割当ては、+入力がA1、-入力がA2、出力がA3。それぞれのピンはマイコン側で読むこともできる。
オペアンプ機能を動かすためのコードは、公式のチュートリアルにあるとおり。
#include <OPAMP.h>
void setup () {
OPAMP.begin(OPAMP_SPEED_HIGHSPEED);
}
void loop() {}
たったこれだけ。言い換えれば、これしかできない(と思う)。OPAMP.beginの引数はOPAMP_SPEED_LOWSPEEDというのもあるそうで、これだと周波数帯域が狭まる代りに消費電力が抑えられるらしい。通常はHIGHSPEED。
マイコンチップ内には複数のオペアンプが入っているそうだけど、Arduino UNO R4として使えるのは一つだけ(多分、ピン数の制限のためだろう)。
実験準備
回路
以前の実験で使ったボードがあるので、それを再利用する。

回路図左のオペアンプはArduino UNO R4 Minima内蔵のもの。以前の実験ではアナログ入力端子をA0にしていたけど、今回はオペアンプの入力に使うのでA1に変更。A2とA3はショートさせてボルテージフォロワとして使う。
それと、ADコンバータのフルビットと0も見られるように+5VとGNDのピンも付け足した(当然ながら、同時につなぐと+5VとGNDがショートするので要注意)。

ソフトウェア
仕様は次の通り。
- analogRead()で所定の回数(10回)読み取る
- 読み取り値のうち、最高値と最低値をのぞいた平均値を求める(8回分の計測の平均)
- ADコンバータの分解能を10ビットと14ビットを切り替えて、上記の測定を繰り返す
- オペアンプを使う際は、オペアンプの出力も同様に測定する
ソースコードは最後に添付(この場に貼り付けるには長くてじゃまなので)。なお、半分くらいは生成AIを使って作った。
表示する値はADCの読取り値そのもの。10ビットでは最大値は1023、14ビットでは16383。
測定(実験)
基板上で可能なすべての組合せを行うと数が多すぎるので、+5V側の抵抗は10kΩ(R5、9.94kΩ)の場合だけを行う。GND側の抵抗を変えて測定する。
オペアンプ不使用
まずは、オペアンプを使わない場合を確認しておく。ソースコードの「#define USE_OPAMP」をコメントアウトした状態でコンパイル。
測定結果は次の通り。左からR2、R4、R6、R8の結果。一つの図にまとめた。それぞれ、上が分解能10ビット、下が14ビット。

理論値とともにまとめる。
抵抗値[kΩ] | 理論値 (10bit) | 実測値 (10bit) | 理論値 (14bit) | 実測値 (14bit) |
---|---|---|---|---|
0.998 | 93 | 93 | 1495 | 1501 |
4.67 | 327 | 325 | 5237 | 5210 |
9.97 | 513 | 506 | 8204 | 8099 |
99.5 | 931 | 882 | 14896 | 14119 |
測定対象の抵抗値が小さい場合は理論値とだいたい一致するが、抵抗値が大きくなると理論値との乖離が大きくなる。
オペアンプ使用
続いて、オペアンプを使った場合(#define USE_OPAMPを有効化してコンパイル)。それぞれの左がA1(オペアンプへの入力)、右がA3(オペアンプからの出力)。

抵抗値[kΩ] | 理論値 (10bit) | 実測値 OPA入力 | 実測値 OPA出力 | 理論値 (14bit) | 実測値 OPA入力 | 実測値 OPA出力 |
---|---|---|---|---|---|---|
0.998 | 93 | 94 | 93 | 1495 | 1507 | 1497 |
4.67 | 327 | 327 | 327 | 5237 | 5245 | 5248 |
9.97 | 513 | 507 | 513 | 8204 | 8114 | 8208 |
99.5 | 931 | 880 | 932 | 14896 | 14090 | 14911 |
※OPAはオペアンプ
オペアンプの出力は理論値にかなり近い値が得られた。
上の実験のオペアンプ不使用の場合とこちらのオペアンプの入力を比べると、値が少し異なっている。オペアンプの入力として使うとその入力インピーダンスが影響するのかな?
外部電源使用
ふと思い立った実験。ここまでは電源もUSBケーブルから供給。では、外部から電源を供給すると変わるか?ArduinoではUNO R4に限らず、外部からの電源供給に対応しており、ボード内部にレギュレータを持っていて所定の電圧(+5V)を作っている。USBケーブルからの供給よりも安定した動作が期待できるかも。

左はUSBからの電源供給(上の実験の結果の再掲)、右が外部電源使用時のもの。
オペアンプ不使用(正確にはオペアンプの入力)の値が多少改善している。オペアンプ使用時は特段の違いなし(元々、オペアンプを使ったことで理論値とほぼ同じ結果が得られているわけだし)。
ちなみに、+5Vのラインを測定したところ、USBからの電源供給時は4.60V、外部電源では4.98Vだった。
ゼロとフルビット
ここで、GND側の抵抗をなくしてGNDに直結した場合と、逆に+5V側の抵抗をなくして+5Vに直結した場合(GND側の抵抗はR6、9.97kΩ)を見てみる。それぞれ、理論値ではゼロとフルビット(1023と16383)。なお、電源は外部電源を使用。

分解能10ビットの場合は概ね良いが、14ビットにするとノイズを拾っているのが目立つ。
分解能10ビットでもオペアンプ使用時は1や1022が出てくる。このことから、概ねフルスイングではあるが、若干、狭い。まぁ、これくらいは仕方ないかな。分解能を14ビットに上げると細く見える分、フルスイングよりも狭く見える。が、ノイズの影響の方が大きいような気もする。
まとめ
Arduino UNO R4でアナログ入力を使う場合は、オペアンプをボルテージフォロワにして間に入れれば良い結果が得られる。そのために、オペアンプ機能をつけたんだろうかと勘ぐりたくなる。
というか、こういう工夫をしないと正しい測定ができない。オペアンプは一つしか使えないし、オペアンプを使うとアナログ入力ピンが二つ余計に潰れてしまうのも問題。この対処としては、外付けでオペアンプを用意すればいいのだろうが、余計なハードウェアが必要となってしまう。
外部電源供給にすれば、オペアンプ不使用時でも測定結果が若干改善する(本当に極々わずかだけど)。
それから、以前の実験のとき、ADコンバータの分解能の設定を変えたときには1秒待つようにしたが、直後の測定値がそれ以降の測定値と違っていたのが気になっていた。1秒では短いのかと思って、今回は2秒待ちにしてみたが、結果は変わらず。ADコンバータの分解能を変えたときには一度(以上)空読みしたほうが良さそう。
とはいえ、分解能14ビットではノイズを拾いやすすぎるので、どのみち正確な値は取れない(オペアンプ使用・不使用にかかわらず)。結局、分解能は10ビットで使うのが無難。
よっぽどの事情があるのでなければ、Arduino UNO R3を使っておくほうが余計な悩みがなくて良い。
ソースコード
測定に使ったソースコードは以下の通り。オペアンプを使う場合は、「#define USE_OPAMP」を有効化する。
//#define USE_OPAMP
#define MEASURE_COUNT 10
#ifdef USE_OPAMP
#include <OPAMP.h>
#endif
void setup() {
Serial.begin(115200);
delay(3000);
Serial.println("");
#ifdef USE_OPAMP
OPAMP.begin(OPAMP_SPEED_HIGHSPEED);
#endif
}
void read_analog(int resolution) {
if (resolution != 14) {
resolution = 10;
}
analogReadResolution(resolution);
delay(2000);
int a1_values[MEASURE_COUNT];
int a3_values[MEASURE_COUNT];
for (int i = 0; i < MEASURE_COUNT; i++) {
a1_values[i] = analogRead(A1);
Serial.print(a1_values[i]);
#ifdef USE_OPAMP
Serial.print(" ");
a3_values[i] = analogRead(A3);
Serial.print(a3_values[i]);
#endif
Serial.println("");
delay(1000);
}
// A1の平均(min/max除外)
int a1_min = a1_values[0], a1_max = a1_values[0];
int a1_sum = 0;
for (int i = 0; i < MEASURE_COUNT; i++) {
if (a1_values[i] < a1_min) a1_min = a1_values[i];
if (a1_values[i] > a1_max) a1_max = a1_values[i];
a1_sum += a1_values[i];
}
int a1_avg = (int)((float)(a1_sum - a1_min - a1_max) / (MEASURE_COUNT - 2) + 0.5);
Serial.print("A1 avg: ");
Serial.println(a1_avg);
#ifdef USE_OPAMP
// A3の平均(min/max除外)
int a3_min = a3_values[0], a3_max = a3_values[0];
int a3_sum = 0;
for (int i = 0; i < MEASURE_COUNT; i++) {
if (a3_values[i] < a3_min) a3_min = a3_values[i];
if (a3_values[i] > a3_max) a3_max = a3_values[i];
a3_sum += a3_values[i];
}
int a3_avg = (int)((float)(a3_sum - a3_min - a3_max) / (MEASURE_COUNT - 2) + 0.5);
Serial.print("A3 avg: ");
Serial.println(a3_avg);
#endif
Serial.println("");
}
void loop() {
read_analog(10);
read_analog(14);
}
コメント