Release 2016.8.17 / Update 2017.2.5

Arduinoで可変抵抗器のゆらぎを軽減する

analogRead関数を使う

Arduinoでは可変抵抗器、いわゆるボリュームつまみを簡単に読み取ることが出来ます。

vr_breadboard

上記のようにArduinoUnoを配線し、スケッチを書きます。

int vol_val = 0;             // 変数Vol_valを作成、とりあえず0を代入

void setup() {
  pinMode(A0, INPUT);        // A0ピンを入力に指定
  Serial.begin(9600);        // PCとのシリアル通信を9600bpsにして開始
}

void loop() {
  vol_val = analogRead(A0);  // 変数vol_valにアナログ読み取りしたA0ピンの値を代入
  Serial.print(vol_val);     // vol_valに代入された値をPCへシリアルプリント
  Serial.println();          // 改行シリアルプリント
}

これだけでつまみの値がIDEのシリアルモニタへ表示できるようになります。

5voltが可変抵抗器で分圧された電圧になり、それをA0ピンで読み取ることで値を得ているわけです。この値を特定の値に計算、変換してやることでボリュームがコントローラーとしての役割できるようになります。例えばモータのスピードを変えたりとか。

read_vr_monitor_01

ただ、この値、何もしてなくても微妙に動いてます。これは多分アナログゆえの問題です。

5voltっていうのは「ほぼ5volt」ってことなんですよね。例えれば、流しそうめんの水量を500mlキッカリで流し続けたとしても、全ての水面が同じ高さにはならない、ってのと同じことだと(と、僕は理解しています)

大まかでいいならこれでもどうにかなりそうですが、これで反応よく操作できるコントローラーを作りたいって言うと難ありです。

で、この問題、やっぱり基本的な話らしく、調べるとweb上では打開策が色々書かれています。その解決方法は大体3つに絞られます。

  1. 読み取り間隔を伸ばす
  2. 分解能を下げる
  3. 平均値化する

結論から言うと、これ以外にオススメの方法があるのですが、1個ずつ検証しながらどういうことか説明していきたいと思います。

標準的な回避

1.読み取り間隔を伸ばす

最初のスケッチだと、「A0ピンを読んでシリアル出力する」というのをひたすらloopしてるので、細かいゆらぎも拾ってしまいます。「ひたすら」ってのは普通の感覚ではありません。命令文の実行速度はマイクロセコンド(1/1000秒の更に1/1000秒)単位です。

じゃあ、その読み取る間隔を伸ばしてやれば、グラグラも気にならなくなるんじゃないかっていう話です。簡単な話、delay(一時停止)を挟みます。

int vol_val = 0;              // 変数Vol_valを作成、とりあえず0を代入

void setup() {
  pinMode(A0, INPUT);         // A0ピンを入力に指定
  Serial.begin(9600);        // PCとのシリアル通信を9600bpsにして開始
}

void loop() {
  vol_val = analogRead(A0);   // 変数vol_valにアナログ読み取りしたA0ピンの値を代入
  Serial.print(vol_val);     // vol_valに代入された値をPCへシリアルプリント
  Serial.println();          // 改行シリアルプリント
  delay(1000);                // 1000ms(1秒)待つ
}

12行目に1秒delay関数入れました。これで読み込みの処理は1秒おきになります。でも、動作自体を止めてしまうので、その間何もできません。他にさせたい処理も遅らせてしまいます。つまり反応は芳しくありません。

read_vr_monitor_02_delay

そこで時間を測るmillis()関数を利用してもう少し実用的な方法を考えます。

int vol_val = 0;              // 変数Vol_valを作成、とりあえず0を代入
unsigned long time_read = 0;  // 変数time_readを作成、とりあえず0を代入

void setup() {
  pinMode(A0, INPUT);         // A0ピンを入力に指定
  Serial.begin(9600);        // PCとのシリアル通信を9600bpsにして開始
}

void loop() {
  int intval = millis() - time_read;  // 現在時から前回読み取り時間を引いた値をintvalに代入

  if (intval >= 1000)           // intvalの時間差が1000ミリ秒を超えたら実行
  {
    vol_val = analogRead(A0);   // 変数vol_valにアナログ読み取りしたA0ピンの値を代入
    Serial.print(vol_val);     // vol_valに代入された値をPCへシリアルプリント
    Serial.println();          // 改行シリアルプリント

    time_read = millis();       // time_readに読み取りとした現在時間を代入
  }
}

現在時間(millis())から前回読み取りした時間(time_read)を引けば、どれだけ時間が経過したかが分かります。12行目のif文でその差が1000ミリ秒を超えていたら、読み取り作業をするように仕向けます。そしてその作業が完了した時間をまたtime_read変数に代入します。こうするとLoopする中で、一定時間超えると動作するタイマー的な仕組みができます。この方法結構勝手がいいので、覚えておくといいかもしれません。

read_vr_monitor_03_mills

ですが、この方法では結局ゆらぎがなくなるわけじゃありません。

2.分解能を下げる

ArduinoUnoではanalogReadは10bitで1024段階の変化を区別できます、値0を含むので表示では0-1023ですが。分解能を下げるってのは、要は値を割って端数をはしょっちゃえって話です。

int vol_val = 0;              // 変数Vol_valを作成、とりあえず0を代入

void setup() {
  pinMode(A0, INPUT);         // A0ピンを入力に指定
  Serial.begin(9600);        // PCとのシリアル通信を9600bpsにして開始
}

void loop() {
  vol_val = analogRead(A0);   // 変数vol_valにアナログ読み取りしたA0ピンの値を代入
  vol_val = vol_val / 10;     // 一旦値を10分の1に(1の位をもみ消す)
  vol_val = vol_val * 10;     // 値を10倍して元の桁数と同じにする
  Serial.print(vol_val);     // vol_valに代入された値をPCへシリアルプリント
  Serial.println();          // 改行シリアルプリント

}

この場合、一旦読み取った値を10で割って1の位の変化を切り捨てます。その後10倍して一応、前と同じ桁数に戻します。ゆらぎが1の位で収まっていれば、きれいに取り除かれます。ただ、この場合、一度10分の1にしているので、分解能自体は102段階に落ちてしまします。

この範囲でも事足りるならこれでもアリですが、analogWrite出力などで扱うPWMは256段階だったりするので足りません。試しにそのレンジに相当する1024/4にしてみると、

int vol_val = 0;              // 変数Vol_valを作成、とりあえず0を代入

void setup() {
  pinMode(A0, INPUT);         // A0ピンを入力に指定
  Serial.begin(9600);        // PCとのシリアル通信を9600bpsにして開始
}

void loop() {
  vol_val = analogRead(A0);   // 変数vol_valにアナログ読み取りしたA0ピンの値を代入
  vol_val = vol_val / 4;     // 一旦値を4分の1に
  vol_val = vol_val * 4;     // 値を4倍して元の桁数と同じにする
  Serial.print(vol_val);     // vol_valに代入された値をPCへシリアルプリント
  Serial.println();          // 改行シリアルプリント

}

read_vr_monitor_05_low_resPWM

やはりちょっと不安定です。惜しい。

3.平均値化する

例えばテストの平均点を取るにはクラス全員の点数を足して人数分で割ります。これと同じです。何回か読み取りして、その合計点を回数分で割ります。

「繰り返し読んで足す」ためにFor文を使います。

int vol_val = 0;              // 変数Vol_valを作成、とりあえず0を代入

void setup() {
  pinMode(A0, INPUT);         // A0ピンを入力に指定
  Serial.begin(9600);        // PCとのシリアル通信を9600bpsにして開始
}

void loop() {

  for (byte i = 0 ; i < 30 ; i++)  // {}内を30回繰り返し
  {
    vol_val += analogRead(A0);     // 変数vol_valにA0ピンの値を付け足す
  }

  vol_val = vol_val / 30;          // vol_valにvol_val30回分で割った値を代入

  Serial.print(vol_val);          // vol_valに代入された値をPCへシリアルプリント
  Serial.println();               // 改行シリアルプリント

  vol_val = 0;                     // vol_valを一回リセット
}

「vol_val +=~」ってのは変数vol_valの今の値に付け足すって意味なので気をつけてください。

逆に今まで書いてたスケッチ内の「vol_val = analogRead(A0);」はvol_valの中身をまるっと差し替えているって事です。

こうしてみると結構安定して見えてるのですが、少しおぼつかない時があります。

read_vr_monitor_06_avaraged

以上3種類の方法を試してきましたが、web上で解説されているさまざまな解決方法はだいたいこれらを1つ、或いは複数を組み合わせる方法を使っています。

でも、これだと分解能を落とすか、反応を鈍らせるかの犠牲があって、今ひとつ歯がゆい感じです。

ないものねだたりのわがままですが、もっといい方法はないのかと探しまくったら、こういう方法を提案しているところがありました。それが、

ちょっとずつ更新する

この方法は理系の友人に言わせると「ローパス処理」ということなんですが、文系の僕にはこれがローパスフィルタを指し示す計算式なのか分かりません。

やり方は超シンプルです。

int vol_val = 0;              // 変数Vol_valを作成、とりあえず0を代入

void setup() {
  pinMode(A0, INPUT);         // A0ピンを入力に指定
  Serial.begin(9600);        // PCとのシリアル通信を9600bpsにして開始
}

void loop() {

  vol_val = (vol_val * 0.9) + (analogRead(A0) * 0.1);


  Serial.print(vol_val);          // vol_valに代入された値をPCへシリアルプリント
  Serial.println();               // 改行シリアルプリント

}

これだけ。

前回のvol_valの値9割と、そこへ新しく読み取ったA0ピンの値1割を足し、100%にした値をvol_valに代入します。C言語では整数の変数には小数点は入らないですけど、数字にピリオドを打つと、その計算だけは小数点以下までしてくれるそうです。

read_vr_monitor_07_lowpass

どうですか?かなり安定して読み取れていると思います。ただし、分解能は少し下がってフルビット分の値が出てくれません(小数点以下の端数の影響だと思います)。それでも1000以上の分解能があるので、扱うには充分です。

少しずつ更新している訳なので、繰り返しの回数が多いほうが反応がよくなるということことに注意してください。delayを使ったりしてloop全体のインターバルが遅くなると意味がありません。つまり、他の命令文に対しても工夫が必要になってくると思います。

最後に、これに対して「平均値化」も付け足したスケッチを残しておきます。試してみてください。

int vol_val = 0;

void setup() {
  pinMode(A0, INPUT);
  Serial.begin(9600);
}

void loop() {
  int read_val = 0;

  for (byte i = 0 ; i < 10 ; i++)
  {
    read_val += analogRead(A0);
  }

  vol_val = vol_val * 0.9 + (read_val / 10) * 0.1;


  Serial.print(vol_val);
  Serial.println();

}

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください