ADCモジュール ADS1115

 ここまでで一応、電圧は計測することができるようになりました。ただし、Arduinoのアナログ入力分解能は10bit(Arduino Dueは12bit)なので、1024段階でしかデジタル化できません。この分解能の問題は、別途ADC(Analong-to-Digital Converter:アナログ-デジタル変換器)モジュールを接続すれば、改善できます。ここではADS1115という16bit(2^16=65536段階)のADCチップを使います。これも例によって、ブレークアウトモジュールの形で製品になっています。1個あたり500円くらいで買えます。

 

 

ADS1115の特徴は、

・16bit 分解能

・単動4チャネル、差動2チャネルの入力端子

詳しいデータシートはこちら

https://cdn-shop.adafruit.com/datasheets/ads1115.pdf

 

単動と差動の違いについて、なじみがなければ、例えば以下を読んで下さい。

https://www.fa.omron.co.jp/guide/faq/detail/faq05826.html

一言でいえば、差動のほうがノイズに強く、安定して電圧を読めますが、2つの入力端子を1つのチャネルに使うので、入力チャネル数が少なくなります。

 

 つなぎ方の説明に入る前に、分解能の必要性について考えるとよいと思います。例えば、計測範囲が0~1000kPaの圧力センサーを読もうとすると、10bit(1024段階)での分解能は約1kPaになります。水位にすると10cmです。これは、いまいちですね。しかし、例えば計測範囲が-100~+100kPaの絶対圧センサーの場合、分解能は0.2kPaになります。多くの場合、フィールド計測で(実験室でもそうですが)重要なのは、分解能(resolution)ではなく精度(precision)・正確性(accuracy)です。これらの違いは下の図の通りです。

 

 

分解能が低くても、計測回数を増やして平均すれば、実質の分解能が高くなります(いくつの計測回数で真値に近くなるか、が精度の言いかえといってもいいでしょう)ので、やはり最も重要なのは正確性です。分解能が必要なのは、わずかな変化をセンシングする必要があるときです。しかし、それには高い精度が伴わなければ、実際の物理量のわずかな変化なのか、センサーのブレなのかの区別はつきません。自身の用途に対し、10bitより高い分解能が必要か、まず考えてみて下さい。

 

単動入力をする

 では、ADS1115の説明をします。これはI2Cセンサーなので、前回(第7回)のRTCモジュール同様、Vcc・GND・SDA・SCLの4本で接続します。前回までの作品にADS1115を加えたものを下図に示します。なお、VccではなくVddと書いてある場合もありますが、このレベルでは特に気にしなくてよいです。どちらも入力電圧の+側と思えばよいです。

 

 

 

上のfritzing図と少し違いますが(電池ではなくPCからUSBで給電している、SDカードモジュールに3.3Vのものを用いている、ブレッドボードの大きさや使っている穴の位置が違う、など)、参考までに写真を載せると

 

 

RTCと合わせると、I2Cセンサーを2つつなぐことになるので、SDA・SCL端子もブレッドボード上に拡張しましょう。ADS1115のI2Cアドレスですが、「ADDR」端子をどこにつなぐかによって、4つの異なるアドレスを設定することができます。ここの例では、ADDRをGNDにつないでいるので(白い線)、ADS1115のアドレスは0x48になります。この場合も含めて、アドレスは

 

ADDR -> GND: 0x48

ADDR -> VDD: 0x49

ADDR -> SDA: 0x4A

ADDR -> SCL: 0x4B

 

と設定できます。ということは、ADS1115は最大4つまでArduinoに同時接続できるということです。これにより、(ADS1115を4つ買えば)単動16チャネル、差動8チャネルまで読み込めます。ただ、後の回で紹介するように、マルチプレクサーという切り替え装置を使えば、実際にはADS1115を4つ買う必要はありません。上の図は、単動入力の場合で、電圧を計測したいもの(ここでは電池)の+をA0端子につなぎ、チャネル0から読んでいます。GNDは他と共通とします。

 

 次に、スケッチです。ADS1115のライブラリをダウンロードして、自身の環境に加えて下さい(やり方は第7回のRTCモジュールDS3231のときと同じ)。

https://github.com/adafruit/Adafruit_ADS1X15

なお、上記のページに行くとADS1015(12bit版の製品)用であるかのように書かれていますが、このライブラリにはADS1015とADS1115どちらのクラスも含まれていますので安心して下さい。では、上図のように接続できたら下のスケッチを書き込んで下さい。第7回の作品に加筆しています(赤字部分)。ここではADS1115は一つしか接続していないので、スケッチのほうでアドレスを指定する必要はありません。

 

#include <SPI.h>
#include <SD.h>
#include <DS3232RTC.h>    //http://github.com/JChristensen/DS3232RTC
#include <Time.h>
#include <TimeLib.h>
#include <Wire.h>
#include <Adafruit_ADS1015.h>    //https://github.com/adafruit/Adafruit_ADS1X15

const int chipSelect = 10; // Arduino UNOでは10、Arduino MEGAでは53

#define AVERAGENUM 20 // 取得データの平均化に用いるサンプル数

//#define CFACTOR 0.18750 // GAIN_TWOTHIRDS: +/-6.144V range: Calibration factor (mV/bit) for ADS1115 
#define CFACTOR 0.12500 // GAIN_ONE: +/-4.096V range: Calibration factor (mV/bit) for ADS1115
//#define CFACTOR 0.06250 // GAIN_TWO: +/-2.048V range: Calibration factor (mV/bit) for ADS1115
//#define CFACTOR 0.03125 // GAIN_FOUR: +/-1.024V range: Calibration factor (mV/bit) for ADS1115
//#define CFACTOR 0.01563 // GAIN_EIGHT: +/-/0.512V range: Calibration factor (mV/bit) for ADS1115
//#define CFACTOR 0.00781 // GAIN_SIXTEEN: +/-0.256V range: Calibration factor (mV/bit) for ADS1115

Adafruit_ADS1115 ads; // ADS1115のインスタンスを作成

void setup(void)
{
    /* ----- Setting up serial communication with PC ------ */
    /* ここでUSBを介してPCとシリアル通信を始める。9600はシリアル通信のボーレート */
    Serial.begin(9600);
    while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
    //何らかの問題があってシリアルポートに接続できないときは、このループにトラップされる    
    }

    /* ----- Initialisation of SD card ------ */
    Serial.print("Initializing SD card...");
     //see if the card is present and can be initialized:
    if (!SD.begin(chipSelect)) {
       Serial.println("Card failed, or not present");
       // don't do anything more:
      return;
    }
    Serial.println("card initialized.");

    /* ----- Setting time based on RTC ----- */
    setSyncProvider(RTC.get);   // ここでRTCの時刻をもとにArduinoの時刻を合わせる
    if(timeStatus() != timeSet) 
        Serial.println("Unable to sync with the RTC");
    else
        Serial.println("RTC has set the system time");
    delay(1000);

    /* ----- Setting of ADS1115 AD converter ----- */
    ads.setGain(GAIN_ONE);
    ads.begin();
}

void loop(void)
{
    /* データの読み取り */
    int cnt1=0;
    float read_from_ads;
    float averaged_input;
      
    averaged_input=0;
    for(cnt1=0;cnt1<AVERAGENUM;cnt1++) //AVERAGENUMの数だけ繰り返す
    {
      read_from_ads = float(ads.readADC_SingleEnded(0)); //チャネル0から単動(Single End)入力
      averaged_input=averaged_input+read_from_ads/AVERAGENUM; //読んだ値を足していく:AVERAGENUMで割ることで平均化している
    }
    averaged_input=averaged_input*CFACTOR; //校正係数を掛けて電圧とする

    /* PCのシリアルモニタに表示 */
    Serial.print(year());Serial.print("/");Serial.print(month());Serial.print("/");Serial.print(day());Serial.print(" ");
    Serial.print(hour());Serial.print(":");Serial.print(minute());Serial.print(":");Serial.print(second());Serial.println("");
    Serial.print(averaged_input);
    Serial.println(" mV"); //" mV"と表示、printではなくprintlnとすることで改行する

    /* SDカードに書き込み */
    File dataFile = SD.open("datalog.csv", FILE_WRITE);
    if (dataFile)
    {
      dataFile.print(year());dataFile.print("/");dataFile.print(month());dataFile.print("/");dataFile.print(day());dataFile.print(" ");
      dataFile.print(hour());dataFile.print(":");dataFile.print(minute());dataFile.print(":");dataFile.print(second());dataFile.print("  ");
      dataFile.print(averaged_input);
      dataFile.println(" mV"); //" mV"と表示、printではなくprintlnとすることで改行する
    }    
    dataFile.close();
    
    delay(2000); //2000ミリ秒=2秒の停止
}

 

この例では、ads.setGain(GAIN_ONE)により、ゲイン(利得)をGAIN_ONEという設定にしています。これは入力レンジを-4.096~+4.096Vにする設定で、これを16bit(2^16=65536段階)で読むため、公正係数(ビットあたりのmV)は0.12500となります。プログラムの冒頭で、CFACTOR(Calibration factor:校正係数)をそのように設定していることに留意してください(0.12500以外のCFACTORは//によりコメントアウトしてあります)。もしセンサーの出力範囲が大きいあるいは小さい場合は、他のゲイン設定にするとよいでしょう。アナログセンサーの多くは3~5Vで信号を返すので、GAIN_ONEを使うことが多いかと思います。これを走らせると、シリアルモニタ―上では:

 

 

 

SDカードにも書き込まれています。 

 

差動入力をする

 差動入力をする場合、電圧信号(ここでは電池からの線)の-をADS1115につなぎます。ピンアサインは、

A0: チャネル0の+

A1: チャネル0の-

A2: チャネル1の+

A3: チャネル1の-

となります。

 

 

スケッチについては、上の単動入力のスケッチのうち、以下を書き変えてください。

read_from_ads = float(ads.readADC_SingleEnded(0));

read_from_ads = float(ads.readADC_Differential_0_1());

に。

ADS1115のA2・A3をつかって差動のチャネル1を読みたいときは、

read_from_ads = float(ads.readADC_Differential_2_3());

を使います。結果については省略します。うまくいきましたか? 

 

複数のADS1115を接続する

 複数のADS1115を接続する場合、それぞれ異なるアドレスを設定しなければなりません。下の例では、2つのADS1115をつないでいます。ADS1115の一つではADDRをGNDにつなぐことでアドレスを0x48に(これをads1としましょう)、もう一つはADDRをVccにつなぐことで0x49にしています(これをads2とします)。ads2への入力(オレンジの線)は省略しています。

 

 

スケッチは以下のように書くことで、どちらがどちらのADS1115か区別できるようにします。オブジェクト志向のプログラム言語を知らないとわかりづらいかもしれませんが、冒頭の

Adafruit_ADS1115 ads1(0x48);

Adafruit_ADS1115 ads2(0x49);

で2つのADS1115のインスタンスを生成しています。

 

#include <SPI.h>
#include <SD.h>
#include <DS3232RTC.h>    //http://github.com/JChristensen/DS3232RTC
#include <Time.h>
#include <TimeLib.h>
#include <Wire.h>
#include <Adafruit_ADS1015.h>    //https://github.com/adafruit/Adafruit_ADS1X15

const int chipSelect = 10; // Arduino UNOでは10、Arduino MEGAでは53

#define AVERAGENUM 20 // 取得データの平均化に用いるサンプル数

//#define CFACTOR 0.18750 // GAIN_TWOTHIRDS: +/-6.144V range: Calibration factor (mV/bit) for ADS1115 
#define CFACTOR 0.12500 // GAIN_ONE: +/-4.096V range: Calibration factor (mV/bit) for ADS1115
//#define CFACTOR 0.06250 // GAIN_TWO: +/-2.048V range: Calibration factor (mV/bit) for ADS1115
//#define CFACTOR 0.03125 // GAIN_FOUR: +/-1.024V range: Calibration factor (mV/bit) for ADS1115
//#define CFACTOR 0.01563 // GAIN_EIGHT: +/-/0.512V range: Calibration factor (mV/bit) for ADS1115
//#define CFACTOR 0.00781 // GAIN_SIXTEEN: +/-0.256V range: Calibration factor (mV/bit) for ADS1115

Adafruit_ADS1115 ads1(0x48); // ADS1115のインスタンス1を作成
Adafruit_ADS1115 ads2(0x49); // ADS1115のインスタンス2を作成

void setup(void)
{
    /* ----- Setting up serial communication with PC ------ */
    /* ここでUSBを介してPCとシリアル通信を始める。9600はシリアル通信のボーレート */
    Serial.begin(9600);
    while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
    //何らかの問題があってシリアルポートに接続できないときは、このループにトラップされる    
    }

    /* ----- Initialisation of SD card ------ */
    Serial.print("Initializing SD card...");
     //see if the card is present and can be initialized:
    if (!SD.begin(chipSelect)) {
       Serial.println("Card failed, or not present");
       // don't do anything more:
      return;
    }
    Serial.println("card initialized.");

    /* ----- Setting time based on RTC ----- */
    setSyncProvider(RTC.get);   // ここでRTCの時刻をもとにArduinoの時刻を合わせる
    if(timeStatus() != timeSet) 
        Serial.println("Unable to sync with the RTC");
    else
        Serial.println("RTC has set the system time");
    delay(1000);

    /* ----- Setting of ADS1115 AD converter ----- */
    ads1.setGain(GAIN_ONE);
    ads1.begin();
    ads2.setGain(GAIN_ONE);
    ads2.begin();
}

void loop(void)
{
    /* データの読み取り */
    int cnt1=0;
    float read_from_ads;
    float averaged_input1, averaged_input2;

    /* ads1(ADDRピンをGNDピンに接続したほうのADS1115)からの読み込み */
    averaged_input1=0;
    for(cnt1=0;cnt1<AVERAGENUM;cnt1++) //AVERAGENUMの数だけ繰り返す
    {
      read_from_ads = float(ads1.readADC_SingleEnded(0)); //チャネル0から単動(Single End)入力
      averaged_input1=averaged_input1+read_from_ads/AVERAGENUM; //読んだ値を足していく:AVERAGENUMで割ることで平均化している
    }
    averaged_input1=averaged_input1*CFACTOR; //校正係数を掛けて電圧とする

    /* ads2(ADDRピンをGNDピンに接続したほうのADS1115)からの読み込み */
    averaged_input2=0;
    for(cnt1=0;cnt1<AVERAGENUM;cnt1++) //AVERAGENUMの数だけ繰り返す
    {
      read_from_ads = float(ads2.readADC_SingleEnded(0)); //チャネル0から単動(Single End)入力
      averaged_input2=averaged_input2+read_from_ads/AVERAGENUM; //読んだ値を足していく:AVERAGENUMで割ることで平均化している
    }
    averaged_input2=averaged_input2*CFACTOR; //校正係数を掛けて電圧とする
    
    /* PCのシリアルモニタに表示 */
    Serial.print(year());Serial.print("/");Serial.print(month());Serial.print("/");Serial.print(day());Serial.print(" ");
    Serial.print(hour());Serial.print(":");Serial.print(minute());Serial.print(":");Serial.print(second());Serial.println("");
    Serial.print(averaged_input1);Serial.print(" mV, ");
    Serial.print(averaged_input2);Serial.println(" mV");
    
    /* SDカードに書き込み */
    File dataFile = SD.open("datalog.csv", FILE_WRITE);
    if (dataFile)
    {
      dataFile.print(year());dataFile.print("/");dataFile.print(month());dataFile.print("/");dataFile.print(day());dataFile.print(" ");
      dataFile.print(hour());dataFile.print(":");dataFile.print(minute());dataFile.print(":");dataFile.print(second());dataFile.print("  ");
      dataFile.print(averaged_input1);dataFile.print(" mV, ");
      dataFile.print(averaged_input2);dataFile.println(" mV");
    }    
    dataFile.close();
    
    delay(2000); //2000ミリ秒=2秒の停止
}
 

 

ここまでで、単動最大16チャネル(あるいは差動最大8チャネル:異なるADS1115を使って単動と差動の組み合わせもできます)16bitのアナログデータロガーができました。

 

パフォーマンスの確認

 冒頭で分解能・精度・正確性の話をしましたが、ここで、ADS1115による計測とArduinoのアナログピン(ここではA0:第7回のブログの作品を使いました)による電池の電圧計測結果を見てみます。2秒間隔でアルカリ電池の電圧を調べました。20回計測の結果は以下の通り。

 

ADS1115          Arduino A0ピン 

1623.36 mV         1606.45 mV

1623.37 mV         1606.45 mV

1623.36 mV         1606.45 mV

1623.36 mV         1606.45 mV

1623.37 mV         1606.45 mV

1623.36 mV         1606.45 mV

1623.37 mV         1606.45 mV

1623.36 mV         1606.45 mV

1623.36 mV         1606.45 mV

1623.36 mV         1606.45 mV

1623.37 mV         1606.45 mV

1623.37 mV         1606.45 mV

1623.37 mV         1606.45 mV

1623.37 mV         1606.45 mV

1623.37 mV         1606.45 mV

1623.37 mV         1606.45 mV

1623.36 mV         1606.45 mV

1623.36 mV         1606.45 mV

1623.37 mV         1606.45 mV

1623.37 mV         1606.45 mV

 

まず、電圧値そのもの(真の0Vに対するオフセット)が17mVほど異なります。実際の計測では校正(キャリブレーション)を行っておいたほうがよいでしょう。値のばらつきについて見てみて下さい。ADS1115のほうは、0.01mVのうちにおさまっています。一方、Arduinoアナログピン(10bit = 1024段階)のほうは、分解能が5V/1024=5mV(この「5V」というレンジはAREFピンなどを使って変えることができますが)なので、電池のように安定した電圧を持つものに対してはピクリとも動きません。