ArduinoのA/DコンバータをMATLABで評価する（実践編）

## はじめに
　[準備編](https://qiita.com/Monzo_N/items/05609f5b097e6a88f801)で用意した信号源とMATLABによる評価環境を用いてArduino のA/Dコンバータを評価してみましょう。ここではArduino Unoを用いてみます。

## １．Arduino uno のアナログ入力構成
　Arduino unoにはアナログ入力ピンがA0-A5の６本あります。
![Fig01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434947/11be1119-79fb-950b-4eed-3a6d2f63be06.png)
しかし、Arduino unoに搭載されているATmega328Pに内蔵されているA/Dコンバータは１ケのみであり、入力をスイッチで切り替えています。同時に複数のアナログ信号をA/D変換することはできません。
![ADCblock.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434947/18e31410-c8b5-b730-8eb0-0790d28c51a4.png)
入力電圧の範囲ですが、デフォルトでは0～5Vとなっており、 analogReference(INTERNAL) とすることで、0～1.1Vの範囲に変更することができます（デフォルト設定に戻すには analogReference(DEFAULT) 。
この他にanalogReference(EXTERNAL)で外部から供給された電圧を上限とすることができます（下限は0V ）が、参照電圧については、ここでは省略し、デフォルトの0～5Vの場合で説明します。

##　２．信号を入力できるようにする
　PCでやったのと同じようにiPadから信号を入力できるでしょうか。ちょっと待ってください。iPadから出力できる信号は0V中心の正弦波です。なので、信号の下半分はマイナスの電圧となります。
この信号をArduino unoに入力するにはレベルシフタが必要となります。最も原始的な方法はコンデンサと抵抗で構成するものです。一例を下記に示します。
![DCシフタ例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434947/e03570b5-2ccf-6a91-d49b-e62548373701.png)
このレベルシフタにより、正弦波の平均電圧は2.5Vとなり、Arduino unoの入力範囲に合った電圧範囲で供給できるようになります。
![DCシフタ例解説.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434947/ec046745-3313-31f6-9ae5-42b7c124b54d.png)
周波数特性ですが、iPadの出力抵抗は5Ω程度と低いそうであり、また、ATmega328PのA/Dコンバータの入力インピーダンスは100MΩという記述もありますが、控えめに見積もって100kΩとし、この時の入出力特性を回路シミュレータで求めると、下記のような特性になります。
![DCシフタｆ得.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434947/d403199e-cd51-62e7-c18d-f21461d61417.png)
-1dB（振幅が９割に下がる周波数）が5Hzなので、周波数が特に低い部分を除けば使えるという感じです。

##　３．unoでA/D変換した信号をMATLABに取り込む

まず、Arduino uno側のプログラムです。A0から取り込む場合は、次のようなプログラムになります。

```C
char serial_recv;

void setup() {
     Serial.begin(230400) ;//転送速度設定
}

void loop() {
    while(Serial.available() > 0){
      serial_recv = Serial.read(); // シリアル通信を受信
      switch(serial_recv){
        case '1': //1が来たらA/D開始
        int adout ;
        
        for (int i=0; i<8200; i++){ //8200点、転送する
          adout = analogRead(0) ;
          Serial.println(adout) ;
        }
      delay(1000);
      }
    }
}
```
次にMATLAB側です。Arduino unoのポートがCOM6だとすると、こうなります。

```matlab
s = serialport('COM6',230400);
pause(2);% 2秒待つ

write(s,'1',"char")
u=1;
Sout=zeros(1,8192);
while u<8193
    outdum = readline(s);
    if isempty(outdum) == 0
        Sout(u)=str2num(outdum);
        u=u+1;
    end       
end

clear s;

figure(1);
plot(Sout);
xlabel('データNo.');
ylabel('出力値');

figure(2);
plot(Sout(2001:2500));
xlabel('データNo.');
ylabel('出力値');
```

serialportコマンドを出した後、unoの場合は、２秒待ちます。
Arduino unoはメインのATmega328Pのほかに、USBインターフェースとしてATmega16U2が搭載されており、serialportコマンドにより、ATmega16U2がATmega328Pにリセットをかけてしまいます。
リセットをかけられると、ATmega328Pは立ち直るまで約２秒かかり、この間には入力を受け付けないので、２秒待つ必要があります。
で、２秒経ったら、unoに'1'を送り、unoからA/D変換されたデータが下記のように送られてきます。
matlab側で8192点で切っています。
![取得データと拡大.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434947/2bfead5a-14f5-6e3f-c809-fc320e0289d1.png)
一般にA/Dコンバータの出力はゼロを最小として正の値であることが多いです。10bit A/Dでは、出力の最小値はゼロ、最大値は1023となります。オフセットバイナリといいます。
今、上記の波形では、入力周波数が100Hzで１周期分がだいたい38点なので、サンプリング周波数は3.8kHzです。
このように100Hz（周波数）＝10msec（周期）、1/(10msec/38)=3.8kHzという求め方もあるわけですが、MATLABのSignalproccesing toolboxにはmedfreqという関数があります。
medfreq(Sin,fs）でサンプリング周波数fsのときのデータSinの中心周波数を計算できます（ただし、角周波数）。
なので、A/D出力データがSout、入力信号周波数finとすると、サンプリング周波数fsは、

```matlab
fs=2*pi*fin/(medfreq(Sout-mean(Sout)))  % mean(Sout)はオフセット分
```
で求めることができます。この場合、3.78kHzという答えがかえってきました。

##４．uno のA/DコンバータのSNRを求める
さて、得たデータからunoのA/DコンバータのSNRをもとめてみましょう。
`snr(Sout-mean(Sout)),3780)`
で、下記のように SNR = 45.2 [dB] となりますが、入力信号がフルスケールより小さいので補正する必要があります（[前の記事](https://qiita.com/Monzo_N/items/05609f5b097e6a88f801)参照）。
![ArduinoSNR1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434947/811e9b63-14df-a4a2-6d92-58e286fcbc99.png)
どれだけ小さいのかは、

```matlab
S=Sout-mean(Sout);%オフセット除去
S2sum=sum(S.^2);% 二乗和（信号8192点のパワー加算）
Srms=sqrt(S2sum/8192); %信号の実効値
Spp=Srms*(2*sqrt(2)); %信号のpeak-peak値
SdB=20*log10(Spp/1024) %フルスケールよりどれだけ小さいか[dB]
```
SdB = -7.5となり、この分を補正しますと、SNR = 45.2 + 7.5 = 52.7 [dB]となります。
有効ビット数ENOBに直すと、ENOB = (52.7-1.76) / 6.02 = 8.5[bit] であり、マイコン内蔵のA/Dコンバータとしては、まあまあではないでしょうか。

##５．リアルタイム転送の問題点
前の章では、「A/D変換を１回行なっては、シリアルデータを１ケ転送する」という方法を取っていました。しかし、この方法では、いろいろと問題があります。
まず、サンプリング周波数が低くなる、というところが問題です。
Arduino unoのデフォルトでのサンプリング周波数は9.6kHzですが、上記の例では3.8kHzと半分以下になっています。これはA/D変換の間にシリアル転送でデータをPCに送っていることが原因となっています。
Baudrateを220400でPCにデータを転送しており、Arduino unoの性能としては最速に近い状態ですが。
また、シリアル転送は、毎回同じタイミングで送れなかったりすることがあり、上記の例でサンプリング周波数Fsより80Hz程度低い正弦波（ATMega328のアナログ入力にはアンチエイリアスフィルタが入っていません）を入力してPCに取り込むと下図のような歪んだ波形となりSNRも劣化してしまいます。
![サンプリング失敗例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434947/59d6e241-d218-856a-bb4b-1ee5e3929bab.png)

シリアル転送の速度を少しでも早くするべく、Arduinoのプログラムのうちの
`Serial.println(adout) ;` 
を
`Serial.print(adout) ;
Serial.print("\n") ;`
に変えてみると（デリミタをCR/LFからLFに変えた）、サンプリング周波数がわずかに上がり（3.8kHz→3.9kHz）入力周波数が高くなっても波形歪を出さないようにできました。
![サンプリング成功例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434947/b1c0fe32-2f54-6cc3-1738-25a035ce1411.png)
下記に入力信号周波数とSNRの測定例を示します。デリミタがLFのときには入力信号周波数に関わらずSNRが保たれていることがわかります。
![CRLFvsLF.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434947/9ddf819c-a3fa-50a4-eb05-19f67d265215.png)
ただし、デリミタをLFにすることが万能な解決策かというと、そうではないように思われます。今回のプログラムで、A/D変換に関わる部分が軽かったので、なんとかなったように思われます（[シリアル転送用のバッファは64byteしかなく、増やすことは可能だがRAMを消費する](https://internetofhomethings.com/homethings/?p=927)ということなので、あまり楽しくないかもです）。
リアルタイムでArduinoからPCにデータを転送し続けるということは結構難しいと思いました。
今回はやりませんでしたが、バイナリで転送するなどの策もあるかと思います。

##６．もっとサンプリング周波数を上げるためには
前の章では、「A/D変換を１回行なっては、シリアルデータを１ケ転送する」という方法を取っていました。
ですが、それだとサンプリング周波数を上げることができません。
Arduinoの中にA/D変換したデータを蓄積し、まとめてシリアル転送するとどうなるのかをやってみました。
ただし、Arduinoのメモリは容量が小さいのでデータ点数は限られます。今回使ったunoの場合、メモリは2kであり、やってみると900point程度が限界でした。
それと、いろいろ調べてみると、analogReadによるデータ取り込みは時間的に安定しない上に、analogRead実行中は他のプロセスが止まってしまうので望ましくないことがわかりました（[ここ](http://yaab-arduino.blogspot.com/2015/02/fast-sampling-from-analog-input.html)とか、[ここ](https://meettechniek.info/embedded/arduino-analog.html)を参照しました）。
ATmega328の場合、A/D変換が自動的に連続して行われるFree Running modeというものがあり、これを使うようにプログラムを修正しました。

Arduino側のプログラムは、こんな感じになりました。

```C
int numSamples=0;
int adout[512];
int x=0;
int s;
char serial_recv;


void setup()
{
  Serial.begin(220400);

  ADCSRA = 0xe8;          // CLK以外のADC設定
  ADCSRB = 0;             // clear ADCSRB register
  ADMUX = 0x40;           // 参照電圧5V、データ右詰め、入力A0
  //ADMUX = 0xc0;           // 参照電圧1.1V、データ右詰め、入力A0

  ADCSRA = ADCSRA | 0x07;  // 128分周 9.6kHz sampling
  //ADCSRA = ADCSRA | 0x06;  // 64分周 19.2kHz sampling
  //ADCSRA = ADCSRA | 0x05;  // 32分周 38.4kHz sampling
  //ADCSRA = ADCSRA | 0x04;  // 16分周 76.8kHz sampling
  //以下、8,4,2分周は略

}

ISR(ADC_vect)
{
  x = ADCL;  // read Lower 8 bit value from ADC
  x += ADCH <<8; // add Higher 2 bit value from ADC
  if(numSamples<512)
  {
    adout[numSamples] = x;  // read 10 bit value from ADC
  }
  else
  { 
    cli();  //AGC割り込み禁止
  }
  numSamples++;
}
  
void loop()
{
  while(Serial.available() > 0){
    serial_recv = Serial.read(); // シリアル通信を受信
    switch(serial_recv){
        case '1':

        numSamples=0; //カウンタのリセット
        sei(); //ADC割り込み許可

        while(numSamples<512){ //データが溜まるまで待つ
          delay(10);
        }
        
        for (s=0; s<512; s++){
          Serial.println(adout[s]); //PCに転送
        }

    }
    delay(10);
    
    // restart
    numSamples=0;
    sei();
  }
}
```
MATLAB側のプログラムは512点に変更しただけなので、省略します。
データは512点取得して、SNRを求めました。データ点数が少ない場合に、SNRの値が若干良く出る傾向があります。下記は同じ信号について、左は8192点、右は512点でSNRを求めたもので、512点のほうが若干SNRが良くなっています。
![SNR比較.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434947/81ade69a-16f4-dd5a-5323-6f09bf0d26d0.png)
この転送方法により、9.6kHz,19kHz,38kHz,77kHzサンプリングでのSNRの実測値を示します。信号源がiPadのため、20kHz以上の周波数がない点はご容赦いただければと思います。
![Fs_vs_SNR.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434947/0b18a26e-362d-1f0d-7948-5f64793cc292.png)

###終わりに
今回のような実験系のレポートをstay homeで仕上げるのは、機材とか部品とかの面で、骨が折れるものだと思いました。

以上でArduinoのA/DコンバータをMATLABで評価する（実践編）を終わります。
お疲れさまでした。