M5Stackの内蔵ADCで作るアナログ(っぽい)電圧計

# はじめに
この記事では、Espressif systems社製のESP32を搭載した液晶一体型のコンパクトな開発モジュール「M5Stack」を使って作成した簡易アナログテスター(電圧計)について紹介します。

# M5Stackの概要
M5Stackは5.4cmx5.4cmの筐体にカラーLCDディスプレイ、microSDカードスロット、スピーカーを収めたコンパクトな開発モジュールです。搭載しているSoC「ESP32」によりWi-Fi(2.4GHz帯)とBLEをサポートしており、LCDディスプレイと無線通信を利用したアプリケーションの開発が可能です。

![1_コンパクトな開発モジュール「M5Stack」.JPG](https://qiita-image-store.s3.amazonaws.com/0/120903/42235f9e-c056-da6a-c7e6-30ce00fe3e6d.jpeg)

裏面には独自の拡張コネクタ(M-BUS)が用意されており専用のモジュールを"Stack"して機能を拡張することができます。

![2_「M5Stack」の裏面 左にあるのが「M-BUS」.JPG](https://qiita-image-store.s3.amazonaws.com/0/120903/4cabf96a-5d65-c54b-7d3b-2b7c2b554405.jpeg)

# つくったもの

以下の写真は今回作成したアナログ風DVMの動作画面および拡張基板です。

![3_「アナログ風DVM」の画面.JPG](https://qiita-image-store.s3.amazonaws.com/0/120903/a5c630c1-c471-ea6a-f894-a2fe9af5a668.jpeg)

![4_「アナログADC」用拡張基板.JPG](https://qiita-image-store.s3.amazonaws.com/0/120903/d745cd5b-ebf5-5c5f-d082-acc64f9ef446.jpeg)

## ターゲットとした機能
主な機能は以下となります。
1. デジタル値と合わせてアナログ風メーターによる測定値表示
2. 測定レンジ: 0.15V～20V (自動レンジ切換)
3. 過電圧/逆電圧保護機能搭載
4. HOLDボタンによる測定値のホールド機能

## ハードウエアの作成
ハードウエアとしては、電圧入力とレンジ切替の為に「M-BUS」に接続する拡張基板を作成しました。オシロスコープ等でADCを2チャンネル使う場合も考えて、入力は2系統です。
全般的には秋葉原で普通に購入できる部品でシンプルに実現する事を優先しました。以下は今回作成した拡張基板の回路図です。

![5_拡張基板回路図.JPG](https://qiita-image-store.s3.amazonaws.com/0/120903/e6a763b1-52fe-18a7-518c-8e6ab0679cf6.jpeg)

回路図ではベースモジュールと置き換えて使う場合も考えてBATピンにLiPo電池を取り付けられる用にコネクタを追加しています。ベースモジュールをスタックして使用する場合は追加のLiPo電池は不要です。

### 主な仕様
ハードウエア部分の作成にあたり、目標仕様は以下としました。
1. 入力インピーダンス: 100kΩ以上
2. 電圧測定範囲: 最大20Vまで対応
3. オートレンジ切替
4. 過電圧/逆電圧保護機能

### 入力インピーダンスの決定
本機を接続する事による、測定対象の電圧降下をできる限り少なくするために、入力インピーダンスは100kΩ以上とします。
具体的には、入力に直列に100kΩを挿入しています。入力レンジはシンプルにADコンバータ入力～GND間に並列に接続した抵抗を切り替えることで調整しています。

### 電圧測定範囲とオートレンジ切替
M5StackのADコンバータ入力は直線性があまり良くない為、最大3Vまでのレンジで使用する様に各抵抗値を決定しました。以下の図は測定レンジとADコンバータへの入力値の比率と、それに必要な分割抵抗の値です。測定レンジは3段階(4V/10V/20V)とすると、組み合わせる抵抗値は300kΩ/50kΩ/30kΩとなります。

![6_入力レンジと抵抗値の検討結果.JPG](https://qiita-image-store.s3.amazonaws.com/0/120903/a64bbc5f-3f99-13c6-b4df-c6562166e90c.jpeg)

オートレンジ切替は分割抵抗をFETで切り替える事で実現しました。FETはBSS138(Nch MOSFET)を使用しています。ON抵抗は約8Ωと分割抵抗値に対して十分小さいため測定値への影響は無視できるレベルとなります。
BSS138は秋月電子通商より購入可能です。
http://akizukidenshi.com/catalog/g/gI-04232/

### 過電圧/逆電圧保護機能
ADコンバータ入力への過電圧や逆電圧の保護用に入力から3.3V/GNDの間に保護用のダイオードを追加しています。当初は3.3Vのツエナーダイオードで実験したのですが、リーク電流が大きく直列抵抗での電圧降下が測定値に影響を与えるレベルであったため、スイッチングダイオードを2個使用して保護回路を構成しました。
ダイオードは1N4148を使用しています。こちらも秋月電子通商より購入可能です。
http://akizukidenshi.com/catalog/g/gI-07084/

### 使用するM-BUSピンの決定
ADコンバータ入力およびレンジ切替に使用するM-BUSのピンは以下に決定しました。今回はch1のみを使用しています。
- ADコンバータ入力(ch1): 35番ピン
- レンジ切替1: 16番ピン
- レンジ切替2: 17番ピン

- ADコンバータ入力(ch2): 36番ピン
- レンジ切替3: 12番ピン
- レンジ切替4: 13番ピン

IO選択のポイントとしては、以下のピンは出来る限り避ける様にします。
- M5Stackの内蔵デバイスで使用しているピン
- 起動時のモード設定用のピン(Strapping Pin)

![7_使用するM-BUSピン.JPG](https://qiita-image-store.s3.amazonaws.com/0/120903/b3ddffeb-ba90-a931-d839-275a9ee0402b.jpeg)

特に「起動時のモード設定用のピン」は起動時のプルアップ/プルダウンの極性を間違えるとM5Stack自体が起動しなくなります。内蔵デバイス用で使用されているピンも結構多く、自由に使えるピンは限られているので拡張基板を作成する時は要注意です。
今回ch2のレンジ切替3に使用している12番ピンはStrapping Pinなのですが、デフォルトPull Downなので回路構成上問題ないと判断しました。

## ソフトウエアの作成

### 開発環境
開発環境としてはArduino IDEを使用します。ArduinoIDEでプログラミングするにあたっては「Arduino Core for the ESP32」環境に「M5Stack用ライブラリ」を追加する必要があります。M5Stack用のArduino IDE環境の構築についてはM5Stackより公式のドキュメントが公開されています。
https://docs.m5stack.com/#/en/quick_start/m5core/m5stack_core_get_started_Arduino_Windows

### 主な仕様
ソフトウエアの作成にあたり、目標仕様は以下としました。
1. アナログメーター風の画面上に"針"で電圧値を表示する
2. "針"での表示とは別に測定したデジタル値も画面に表示する
3. オートレンジ切替はヒステリシスを持たせる
4．「HOLD]ボタンを押すことによる測定値のホールド機能

今回作成したスケッチ及び必要な設計データはgithubで公開しています。
https://github.com/tomorrow56/M5Stack_DVM

### ベースにしたスケッチ
アナログメーター風の表示については、M5StackのExampleに含まれている"TFT_Meter_linear.ino"をベースにしました。
"TFT_Meter_linear.ino"はスケッチ例の以下にありますので、これを読み込んで「名前を付けて保存」で任意の名前を付けて保存します。

[ファイル]->[スケッチ例]->[M5Stack->[Advanced]->[Display]->[TFT_Meter_linear]

![8_ベースにするスケッチ.JPG](https://qiita-image-store.s3.amazonaws.com/0/120903/a7cc2dec-900f-0688-c33d-cd0362bb1373.jpeg)

以下、作成したスケッチの主な修正ポイントを説明します。

### 画面表示部分の修正
#### メーターのタイトルの修正
ベースのスケッチは"湿度計"なので"%RH"の表示を検索して"Vdc"に書き換えます。(3か所)

#### analogMeter()関数の修正
アナログメーター画面を表示するanalogMeter()関数を以下の様に修正します。

##### Zone limit colorの無効化
電圧表示にあたり色分けはあまり意味がないので以下をコメントアウトします。

```c:M5Stack_DVM.ino
/*
// Green zone limits
if (i >= 0 && i < 25) {
  M5.Lcd.fillTriangle(x0, y0, x1, y1, x2, y2, TFT_GREEN);
  M5.Lcd.fillTriangle(x1, y1, x2, y2, x3, y3, TFT_GREEN);
}

// Orange zone limits
if (i >= 25 && i < 50) {
  M5.Lcd.fillTriangle(x0, y0, x1, y1, x2, y2, TFT_ORANGE);
  M5.Lcd.fillTriangle(x1, y1, x2, y2, x3, y3, TFT_ORANGE);
}
*/
```
##### レンジ切替対応
メーターのラベルが固定なので、これを変数として測定レンジに応じて切り替えられる様にします。

```c:M5Stack_DVM.ino
/*
case -2: M5.Lcd.drawCentreString("0", x0, y0 - 12, 2); break;
case -1: M5.Lcd.drawCentreString("1", x0, y0 - 9, 2); break;
case 0: M5.Lcd.drawCentreString("2", x0, y0 - 7, 2); break;
case 1: M5.Lcd.drawCentreString("3", x0, y0 - 9, 2); break;
case 2: M5.Lcd.drawCentreString("4", x0, y0 - 12, 2); break;
*/

case -2: M5.Lcd.drawCentreString(MeterLabel[0], x0, y0 - 12, 2); break;
case -1: M5.Lcd.drawCentreString(MeterLabel[1], x0, y0 - 9, 2); break;
case 0: M5.Lcd.drawCentreString(MeterLabel[2], x0, y0 - 7, 2); break;
case 1: M5.Lcd.drawCentreString(MeterLabel[3], x0, y0 - 9, 2); break;
case 2: M5.Lcd.drawCentreString(MeterLabel[4], x0, y0 - 12, 2); break;
```
##### plotNeedle()関数の修正
アナログメーターの"針(Needle)"を描画するplotNeedle()関数にあるデジタル表示値はレンジに応じて変更する必要があるため、ここではコメントアウトしてloop()関数へ移動します。

```c:M5Stack_DVM.ino
//  M5.Lcd.setTextColor(TFT_BLACK, TFT_WHITE);
//  char buf[8]; dtostrf(value, 4, 0, buf);
//  M5.Lcd.drawRightString(buf, M_SIZE*40, M_SIZE*(119 - 20), 2);
```
##### setup()関数の修正

###### ピンモードと初期値の指定を追加
```c:M5Stack_DVM.ino
pinMode(RANGE_PIN1, OUTPUT);
pinMode(RANGE_PIN2, OUTPUT);
digitalWrite(RANGE_PIN1, LOW);
digitalWrite(RANGE_PIN2, LOW);
```

###### Aボタンに"HOLD"ラベルを追加
```c:M5Stack_DVM.ino
M5.Lcd.setTextColor(TFT_GREEN, TFT_BLACK);
M5.Lcd.drawCentreString("HOLD", 70, 220, 2);
```
#### loop()関数の修正

##### Aボタンによる"HOLD"機能の実装
Aボタンを押すことでHOLD変数のfalse/trueを切り替えて、HOLD==trueの時はADコンバータからの読み込み値の更新を停止して表示値を固定(HOLD)します。

```c:M5Stack_DVM.ino
if(Hold == false){
  Vread = analogRead(ADC_PIN0);
}
～中略～
M5.update();
if (M5.BtnA.wasPressed()) {
  Hold  = !Hold;
  if(Hold == true){
    M5.Lcd.setTextColor(TFT_YELLOW, TFT_BLACK);
  }else{
    M5.Lcd.setTextColor(TFT_BLACK, TFT_BLACK);
  }
  M5.Lcd.drawCentreString("DATA HOLDING...", 160, 180, 4);
}
```

##### ADコンバータの入力電圧とAD変換値の補正
M5Stackに搭載されているESP32のADコンバータの入力電圧値とAD変換値を実測した特性を以下に示します。

![9_ADC-Vin特性.JPG](https://qiita-image-store.s3.amazonaws.com/0/120903/b53bcf27-ea3d-358a-2993-2a5908311203.jpeg)

このグラフより以下の特性が読み取れます。
1. ADコンバータ入力で0.11V以下は測定できない(有効レンジから外れている)
2. 入力電圧を上げていくと途中から直線性が悪化している

今回は、簡易化するために以下の対応をしました。
1. ADコンバータ入力で0.11V以下の読みだし値は無効(0)とする
2. 直線性については入力範囲をいくつかに分割し直線で近似する

具体的にはloop()関数内で読みだしたAD変換値(Vread)を以下のコードでADコンバータ入力電圧(Vdc)に変換します。簡易的な近似でも実測で+/-10mV程度の精度で測定できました。

```c:M5Stack_DVM.ino
if(Vread < 5){
  Vdc = 0;
}else if(Vread <= 1084){
  Vdc = 0.11 + (0.89 / 1084) * Vread;
}else if(Vread <= 2303){
  Vdc = 1.0 + (1.0 / (2303 - 1084)) * (Vread - 1084);
}else if(Vread <= 3179){
  Vdc = 2.0 + (0.7 / (3179 - 2303)) * (Vread - 2303);
}else if(Vread <= 3659){
  Vdc = 2.7 + (0.3 / (3659 - 3179)) * (Vread - 3179);
}else if(Vread <= 4071){
  Vdc = 3.0 + (0.2 / (4071 - 3659)) * (Vread - 3659);
}else{
  Vdc = 3.2;
}
```
##### 自動レンジ切替機能
ADコンバータの入力電圧によって自動でRANGE変数の値を変更し、RangeChangeフラグをtrueにします。レンジ切替の境界付近の電圧でバタつかないように簡易的なヒステリシス特性を持たせています。

```c:M5Stack_DVM.ino
if(Vdc > 3.0 && RANGE < 2){
  RANGE = RANGE + 1;
  RangeChange = true;
}
if(Vdc < 0.75 && RANGE > 0){
  RANGE = RANGE - 1;
  RangeChange = true;
}
```

RANGE変数の値に応じて分割抵抗値と画面表示ラベル/入力レンジを切り替えます。

```c:M5Stack_DVM.ino
switch (RANGE){
  case 0:
    PARA_RES = TERM_RES1;
    MeterLabel[0] = "0";
    MeterLabel[1] = "1";
    MeterLabel[2] = "2";
    MeterLabel[3] = "3";
    MeterLabel[4] = "4";
    digitalWrite(RANGE_PIN1, LOW);
    digitalWrite(RANGE_PIN1, LOW);
    VdcDisp = VdcCalc * (100 / 4);
    break;
  case 1:
    PARA_RES = 1/ ((1 / TERM_RES1) + (1 / TERM_RES2));
    MeterLabel[0] = "0";
    MeterLabel[1] = "2.5";
    MeterLabel[2] = "5";
    MeterLabel[3] = "7.5";
    MeterLabel[4] = "10";
    digitalWrite(RANGE_PIN1, HIGH);
    digitalWrite(RANGE_PIN2, LOW);
    VdcDisp = VdcCalc * (100 / 10);
    break;
  case 2:
    PARA_RES = 1/ ((1 / TERM_RES1) + (1 / TERM_RES2) + (1 / TERM_RES3));
    MeterLabel[0] = "0";
    MeterLabel[1] = "5";
    MeterLabel[2] = "10";
    MeterLabel[3] = "15";
    MeterLabel[4] = "20";
    digitalWrite(RANGE_PIN1, HIGH);
    digitalWrite(RANGE_PIN2, HIGH);
    VdcDisp = VdcCalc * (100 / 20);
    break;
}
```
設定レンジの抵抗分割比にあわせて入力電圧を計算し画面にデジタル値を表示します。

```M5Stack_DVM.ino
VdcCalc = Vdc / PARA_RES * (SERIES_RES + PARA_RES);
M5.Lcd.drawRightString(String(VdcCalc), M_SIZE*40, M_SIZE*(119 - 20), 2);
```
アナログ的にメーターの針を一気にではなく更新周期にあわせて徐々に動かします。測定値のフラツキによって針の表示がちらつくので、+/-1の"不感帯"を設けています。

```c:M5Stack_DVM.ino
if(VdcDisp > VdcLCD + 1){
  VdcLCD = VdcLCD + 1;
}else if(VdcDisp < VdcLCD - 1){
  VdcLCD = VdcLCD - 1;
}
```

RangeChangeフラグがtrueの場合はanalogMeter()関数を呼び出してメーターのパネル部分を書き換えます。その後plotNeedle()関数を呼び出してアナログメーターの"針(Needle)"を描画します。

```c:M5Stack_DVM.ino
if(RangeChange == true){
  M5.Speaker.tone(NOTE, 1);
  analogMeter();
  old_analog =  -999;
  RangeChange = false;
}

plotNeedle(VdcLCD, 0); // It takes between 2 and 12ms to replot the needle with zero delay
```

# まとめ
特別なICを使わなくてもESP32の基本機能と抵抗分割の切替だけである程度実用的な計測器(DVM)を作ることが出来ました。直線性が良くないといわれているESP32内蔵のADコンバータですが簡易的に補正することで実用的な精度で測定する事が可能となります。
M5Stackを使う事により画面表示がついたコンパクトなモジュールに仕上げる事ができて、例えば外出先で簡単に電圧チェックするのに結構便利に使っています。

# 今後の予定
さらにM5Stackの特徴を活かすという意味で以下の機能追加を考えています。
- Saveボタンによる測定値のMicroSDカードへの保存機能
- SendボタンによるIFTTTへの測定値送信機能
これらの機能についても今後随時githubにて公開していく予定です。

https://github.com/tomorrow56/M5Stack_DVM