GroveBeginnerKit を、C++(Arduino)とRuby(ラズパイ,Rboard) で使う　ADC編

[しまねソフト研究開発センター](https://www.s-itoc.jp)（略称 ITOC）にいます、東です。

[Grove Beginner Kit for Arduino を使ってみる記事](https://qiita.com/HirohitoHigashi/items/dc43ea72d572b21a6823)の第2回、今回は、アナログ入力を題材にします。
このキットに付属しているアナログ入力センサーは、以下の3種類です。
* [Rotary Potentiometer](https://wiki.seeedstudio.com/Grove-Rotary_Angle_Sensor/)
* [Light Sensor](https://wiki.seeedstudio.com/Grove-Light_Sensor/)
* [Sound Sensor](https://wiki.seeedstudio.com/Grove-Sound_Sensor/)

ターゲットは、以下の通り。
* Arduino　- 付属の Arduino Uno 互換機
* Ruby - Raspberry Pi + [Grove Base HAT for Raspberry Pi](https://www.seeedstudio.com/Grove-Base-Hat-for-Raspberry-Pi.html)
* mruby/c - [RBoard](https://www.sjc-inc.co.jp/service/rboard)


# 1. Rotary Potentiometer

ロータリーポテンショメーターは、一般にボリュームとか可変抵抗器とか呼ばれているものです。メーカーの方でも、Rotary Potentiometer と言ったり、Rotary Angle Sensor と言ったりと言い方が揺らいでいますが同じものです。
このモジュールの場合、抵抗器の両端に電圧をかけておいて、出力端子で回転軸の位置に比例した分圧電圧を出力させています。

| 外観 | 回路図 |
|:-:|:-:|
| ![grove_RotaryPotentiometer.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/b6f072aa-38bf-cf30-0d46-38cea8723c62.jpeg) | ![grove_RotaryPotentiometer_Schema.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/87e1b2da-38c2-6138-58bc-fc10331edcc7.png) |

（[メーカーWiki: Grove - Rotary Angle Sensor](https://wiki.seeedstudio.com/Grove-Rotary_Angle_Sensor/) より引用）

この記事では、簡単のため読み込んだ値をコンソールに表示させるだけにします。


## Arduino
`A0` 番に接続します。切り離さなければ、そこに接続されています。
```c++
void setup() {
  Serial.begin(9600);
}

void loop() {
  int v = analogRead( A0 );   // 0 to 1023
  Serial.println( v );
}
```
### 実行結果
![rotary_result_arduino.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/f9210c6b-57a8-396e-2fbb-acaea331c7ab.gif)

ゼロから最大値1023まで、値は整数で取得できます。


## Raspberry Pi (CRuby)
ラズパイには、アナログ入力端子はありません。そのため、この HAT は別途マイコンを搭載しており、その内蔵 A/D によってアナログ入力を実現しています。マイコンとラズパイ間は、I2Cによって通信する仕組みです。
I2C　の仕様は、公式ウェブページなどには公開されていないようですが、同ページからダウンロードできるサンプルプログラムのコメントで解説されていますので、容易に使うことが可能です。

### ADC クラスの用意
前述したようにI2Cを使うので、I2Cクラスを使って直接操作しても良いのですが、[mruby 共通 API の ADC仕様](https://github.com/mruby/microcontroller-peripheral-interface-guide/blob/main/mruby_io_ADC_ja.md) に合わせておくと都合が良いため、Wrapperを作っておきます。
```ruby:adc.rb
#
# ADC class for
#  Grove Base Hat for RaspberryPi
#
require "mruby/i2c"

class ADC
  # Grove Base Hat for RPI I2C Registers
  #   0x00 ~ 0x01:
  #   0x10 ~ 0x17: ADC raw data
  #   0x20 ~ 0x27: input voltage
  #   0x29: output voltage (Grove power supply voltage)
  #   0x30 ~ 0x37: input voltage / output voltage

  I2C_ADRS = 0x04
  BUS = I2C.new()

  def initialize( pin )
    raise ArgumentError  if pin < 0 || pin > 7
    @pin = pin
  end

  def read_voltage()
    data = BUS.read( I2C_ADRS, 2, 0x20 + @pin ).bytes
    ((data[1] << 8) + data[0]) / 1000.0
  end
  alias read read_voltage

  def read_raw()
    data = BUS.read( I2C_ADRS, 2, 0x10 + @pin ).bytes
    (data[1] << 8) + data[0]
  end
end
```

Grove `A0` 端子に接続します。
```ruby:RotaryPotentiometer.rb
require_relative "adc"

adc = ADC.new( 0 )
while true
  puts adc.read_voltage
  sleep 1
end
```

### 実行結果
![rotary_result_raspi.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/fd1ebe0b-84ba-4334-fa9a-173410779628.gif)

read_voltage メソッドを使っていますので、値は 0.0 から、約 3.3 まで、電圧（浮動小数点値）で取得できています。


## RBoard (mruby/c)
Grove `ADC` 端子へ接続します。

```ruby:RotaryPotentiometer.rb
adc = ADC.new( 20 )

while true
  puts adc.read_voltage
  sleep 1
end
```

ラズパイと同じプログラムが動きます。結果もほぼ同じなので、省略します。



# 2. Light Sensor
フォトトランジスタを使った光センサーです。

| 外観 | 回路図 |
|:-:|:-:|
| ![grove_LightSensor.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/314f96fa-cd57-8ddb-2943-54beeb43e5a6.jpeg) | ![スクリーンショット 2024-03-18 14.03.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/9e88c2b4-129e-fe39-954b-959dcc5e17be.png) |

（[メーカーWiki: Grove - Light Sensor](https://wiki.seeedstudio.com/Grove-Light_Sensor/) より引用）

使用しているフォトトランジスタ（LS06-MΦ5）の特性は、引用してあるデータシートを見てもあまり詳しい情報が得られないですし、R1 (68kΩ）の値なども影響するので、軽くキャリブレーションして使ってみます。

## キャリブレーション

標準として使用する照度計は、東京前川科学社製 No.04-0354 A-BS 型です。これも相当古いものですのでずれはあるでしょうが、参考にはなるでしょう。
![IMG_3071.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/e557b059-1059-ef15-57f1-bc8f4ae83ab9.jpeg)

光源は、最初クリプトン電球とスライダックで試みたのですが、センサーが赤外領域で感度が強いらしく標準器との相関をとるのが難しかったため、太陽光を適度に遮光して行いました。

### Arduino (Vcc=5V) での結果。

| 出力電圧 (V) | 照度 (lx) |
|:-:|:-:|
| 0.60 | 10 |
| 1.09 | 20 |
| 1.95 | 40 |
| 2.52 | 60 |

![grove_LightSensor_CAL_Arduino.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/9588bcc4-7486-214c-e0f2-83efd196afb9.png)

### RBoard (Vcc=3.3V) での結果

| 出力電圧 (V) | 照度 (lx) |
|:-:|:-:|
| 0.49 | 10 |
| 0.91 | 20 |
| 1.23 | 30 |
| 1.80 | 50 |

![grove_LightSensor_CAL_RBoard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/96ab272f-702f-24c8-4676-7c0ddd3392ee.png)

双方とも、同じような傾きでデータが取得できました。
電圧の高い部分（＝明るい）は、ほぼこの電圧でサチります。つまり、50〜60lx 以上の照度は、このセンサー単体では測れません。
電圧の低い部分は、このカーブを伸ばしてもゼロ点でクロスはしないと思います。これは、測定誤差ももちろん大きいですが、暗電流と呼ばれるゼロルクスでもフォトトランジスタにある程度の電流が流れる現象のためです。


## Arduino
`A6` 番に接続します。切り離さなければ、そこに接続されています。

```c++:LightSensor
const double CAL_TBL[][2] = {
//  (V)  (lx)
  {0.60, 10},
  {1.09, 20},
  {1.95, 40},
  {2.52, 60},
};

#define NUMOF(x) (sizeof(x) / sizeof((x)[0]))

void setup() {
  Serial.begin(9600);
}

void loop() {
  double v, lx, v0, v1, lx0, lx1;

  v = analogRead( A6 ) * 5.0 / 1023;
  Serial.print("Volt: ");
  Serial.print( v );

  v0  = CAL_TBL[0][0];
  lx0 = CAL_TBL[0][1];

  int i = 1;
  while( 1 ) {
    v1  = CAL_TBL[i][0];
    lx1 = CAL_TBL[i][1];
    if( v <= v1 ) break;
    if( ++i >= NUMOF(CAL_TBL) ) break;
    v0 = v1;
    lx0 = lx1;
  }

  lx = (lx1 - lx0) / (v1 - v0) * (v - v0) + lx0;
  if( lx < 0.0 ) lx = 0.0;
  Serial.print("  Lux:");
  Serial.println( lx );

  delay( 1000 );
}
```
`analogRead` で読んだ値を電圧に換算し、先ほど求めたキャリブレーション値を直線補間して照度を求めています。

### 実行結果
![実行結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/632fb0d8-c143-9033-0310-1081e8964bd1.gif)


## Raspberry Pi (CRuby)
Grove `A6` 端子に接続します。

```ruby:LightSensor.rb
require_relative "adc"

CAL_TBL = [
  # (V)  (lx)
  [ 0.49, 10 ],
  [ 0.91, 20 ],
  [ 1.23, 30 ],
  [ 1.80, 50 ],
]

adc = ADC.new( 6 )

while true
  v = adc.read_voltage

  v0,lx0 = CAL_TBL[0]
  i = 1
  while true
    v1,lx1 = CAL_TBL[i]
    break if v <= v1
    break if (i += 1) >= CAL_TBL.size
    v0,lx0 = v1,lx1
  end

  lx = (lx1 - lx0) / (v1 - v0) * (v - v0) + lx0
  lx = 0.0 if lx < 0
  printf "%4.2f V  %5.1f lx\n", v, lx

  sleep	1
end
```

アルゴリズムはArduinoと同じですが、Vcc 電圧が Raspberry Pi と RBoard は同じ 3.3Vなので校正表は RBoard で取得したものを使います。Arduinoと比べて、こちらの方は電圧値に変換しなくても良い事とRubyの多重代入構文のおかげで、少しだけ短く書くことができます。

### 実行結果
![実行結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/01e8f87d-d411-cfb1-891a-c496f0ada52c.gif)


## RBoard (mruby/c)
Grove `ADC` 端子へ接続します。

```ruby:LightSensor.rb
CAL_TBL = [
  # (V)  (lx)
  [ 0.49, 10 ],
  [ 0.91, 20 ],
  [ 1.23, 30 ],
  [ 1.80, 50 ],
]

adc = ADC.new( 20 )

while true
  v = adc.read_voltage

  v0,lx0 = CAL_TBL[0]
  i = 1
  while true
    v1,lx1 = CAL_TBL[i]
    break if v <= v1
    break if (i += 1) >= CAL_TBL.size
    v0,lx0 = v1,lx1
  end

  lx = (lx1 - lx0) / (v1 - v0) * (v - v0) + lx0
  lx = 0.0 if lx < 0
  printf "%4.2f V  %5.1f lx\n", v, lx

  sleep 1
end
```

ラズパイと同じプログラムが動きます。結果もほぼ同じなので、省略します。



# 3. Sound Sensor

単なるマイクです。ちょっとした外部回路がついています。
このモジュールに関しては、後述する理由によりサンプルプログラムは省略します。

| 外観 | 回路図 |
|:-:|:-:|
| ![grove_SoundSensor.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/ac04d3f6-8a03-096b-8673-3ec7b28d3dd2.jpeg) | ![grove_SoundSensor_Schema.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/57455f87-4d4c-bb7b-022b-1a2401638a01.png)　|

（[メーカーWiki: Grove - Sound Sensor](https://wiki.seeedstudio.com/Grove-Sound_Sensor/) より引用）

回路図を見ると、最終出力段は 220nF で AC カップリングしてあるだけです。つまり直流的に電位が定まっていません。また、マイクの出力を約100倍に増幅していますが、バイアスがかかっていないのでマイナス側がクリップされます。

念のため、オシロスコープで実測してみました。

![grove_SoundSensor_scope1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/47e2e7d3-1252-a1a2-deea-a739a9d8fc07.png)

図は、RaspberryPi + HAT のアナログ入力に接続し、約820Hzの音を入力した時です。
* Ch2（上のライン）　オペアンプ1段目の入力
* Ch1（下のライン）　Grove 出力ピン

入力 32mVp-p 程度で出力 1.9Vp-p 程度の波形が観測できました。出力のマイナス側がクリップされており、グラウンド電位も中途半端です。これを Arduino に接続すると、Arduino 内部でプルダウンされているのか、たまたまなのか、無音状態で0Vに安定します。
また波形は掲載していませんが、少し大きい音だとプラス側の波形もすぐクリップします。音が鳴っているかどうか、おおまかに音量はどのぐらいかが分かれば良いという設計思想でしょうか。

接続する機器によって無音時の電圧が特定できないこと、ある程度連続してサンプリングをして、Peak to Peak から音の大きさを類推する程度がせいぜいであるだろうことから、このモジュールに関してはここで終了にします。

# おわりに

今回は、アナログ入力を試してみました。任意のタイミングで電位を測定する用途であれば、とても簡単に試すことができます。一方、ある程度速く一定速度での連続サンプリングが必要なケースだと、ハードウェアの助けを必要とするので、それを Wrap したライブラリがあると便利な場合がありそうですね。

次回は、PWM を試してみたいと思います。