2016年12月20日火曜日

Raspberry Pi から A/D コンバータ ADC0832 を使う

最近 Raspberry Pi (RPi) で電子工作を勉強し始めた.『カラー図解 最新 Raspberry Pi で学ぶ電子工作—作って動かしてしくみがわかる—』を参考に勉強をしているのだけれど,対応するキットを買わなかったことによりパーツ特有の接続・制御の部分で少し苦労したので,その時のメモ.

重要: 電子工作について確かな知識も十分な経験もないので,間違ったことを書いている可能性が他の記事と比べても高いので,ご注意ください.

1. 環境と経緯

環境というか,買ったものは以下.
  • Raspberry Pi 1 Model B
    買ったの自体はずいぶん昔だったので,Raspberry Pi 1. (今は) Raspbian をインストールして使用している.
  • cocopar®最新Ultimateスターターキット 初心者専用実験キット 基本部品セットNew Ultimate Starter Learning Kit for Raspberry Pi 3 Model B/2B/B+ Python (https://www.amazon.co.jp/dp/B01EFJ70HC/)
    こちらは最近電子工作を勉強するために買った.
よく見ると,完全に失敗した買い物だった.RPi 1 Model B は GPIO ピンが 26 ピンなのに対し,RPi 2, 3 は GPIO 40 ピン,さらに上記スターターキットは  RPi 2, 3 用ということで 40 ピン用のパーツなどが用意されている.また,参照している『Raspberry Pi で学ぶ電子工作』とも使用パーツが完全に一致はしないので,電子工作初心者としてのチョイスとしては完全に間違いである.でも悔しいのでパーツの買い足しなどはせず,できるところまではそのまま突き進むことにした.

その結果最初に困ったのが,書籍 6 章の A/D 変換である.書籍では MCP3208 という 8 入力,12 ビット A/D コンバータが使用されているが,上記キットに含まれていた A/D コンバータは ADC0832 という 2 入力,8 ビットのもの.なので,配線や A/D コンバータを操作する処理のプログラムは MCP3208 用から ADC0832 用に変更する必要があるのだけれども,不運なことに書籍ではまさに当該部分の処理の説明が「当面は,特に初学者の間はこの実装を理解する必要はない」ということでばっさり省略されていた.これは意地でもなんとか ADC0832 で動かさねばならない,ということで頑張った.きっとこういうのが勉強になるんだと思う.

2.接続

書籍 6 章と同じように,半固定抵抗と A/D コンバータを接続することにより A/D コンバータの動作を確認することにする.書籍には MCP3208 用の接続が書かれているので,AD0832 での接続はデータシート (http://www.ti.com/lit/ds/symlink/adc0832-n.pdf) を参照して,書籍と似たような感じで接続する.また,データシートは検索して一番上位に出てくる Texas Instruments のサイトを参照しているが,おそらく OEM されているからか,いろいろな会社名から同じ製品名で販売されており,どこが本当の一次情報なのかわからない.

結果として,以下のような接続を行った.

写真で見るとこんな感じ.

3. プログラム

これは正直手がかりが全くつかめなかったのだが,Gist に ADC0832 用のコードを上げている方がいて助かった (https://gist.github.com/HeinrichHartmann/27f33798d12317575c6c).

とりあえず最終的なコードを示すと,以下のようになった.
# 6-1_adc0832.py
import RPi.GPIO as GPIO
from time import sleep

def readadc(adcnum, clockpin, mosipin, misopin, cspin):
    if adcnum > 2 or adcnum < 0:
        return -1

    GPIO.output(cspin, GPIO.HIGH)
    GPIO.output(cspin, GPIO.LOW)

    GPIO.output(clockpin, GPIO.LOW)

    for i in [1, 1, adcnum]:
        if i == 1:
            GPIO.output(mosipin, GPIO.HIGH)
        else:
            GPIO.output(mosipin, GPIO.LOW)
        GPIO.output(clockpin, GPIO.HIGH)
        GPIO.output(clockpin, GPIO.LOW)

    adcout = 0
    for i in range(8):
        GPIO.output(clockpin, GPIO.HIGH)
        GPIO.output(clockpin, GPIO.LOW)
        adcout <<= 1
        if GPIO.input(misopin) == GPIO.HIGH:
            adcout |= 0x1

    GPIO.output(cspin, GPIO.HIGH)
    return adcout


GPIO.setmode(GPIO.BCM)
SPICLK = 11
SPIMOSI = 10
SPIMISO = 9
SPICS = 8
GPIO.setup(SPICLK, GPIO.OUT)
GPIO.setup(SPIMOSI, GPIO.OUT)
GPIO.setup(SPIMISO, GPIO.IN)
GPIO.setup(SPICS, GPIO.OUT)

try:
    while True:
        inputVal0 = readadc(0, SPICLK, SPIMOSI, SPIMISO, SPICS)
        inputVal1 = readadc(1, SPICLK, SPIMOSI, SPIMISO, SPICS)
        print("ADC[0]: {}\t ADC[1]: {}".format(inputVal0, inputVal1))
        sleep(0.2)

except KeyboardInterrupt:
    pass

GPIO.cleanup()
肝の部分は readadc 関数のところで,この中身が書籍の MCP3208 用から ADC0832 用に書き換えてある (ほぼ Gist の方のコードのまま) .また,main 部分では入力 2 チャンネルの両方の値を毎回出力するようにした.

このコードとデータシートを睨んでいると,ようやくコードの意味がわかってきた.まず見るべきは,Timing Diagram の図だと思う.
(データシートより引用)

この図を見ると,各ピンがどのタイミングでどの状態になるのかを理解できる.そのとおりに RPi 側のコードを書いてやれば良い.具体的には以下の流れ.

1. CHIP SELECT (CS) ピンを HIGH -> LOW にセット
    GPIO.output(cspin, GPIO.HIGH)
    GPIO.output(cspin, GPIO.LOW)

    GPIO.output(clockpin, GPIO.LOW)
CS ピンの制御により 1 回の値読み取りプロセスが初期化される (のだと理解した).また,各ピンの値の入力・出力は CLOCK (CLK) ピンの状態変化のタイミング (図中に矢印が書かれている場所) で行われる (と推測している).初回の入出力は DATA IN (DI) の START BIT の入力で,CLK ピンが LOW -> HIGH に変化した時に発生するはずなので初期化プロセスとして CLK ピンも LOW にセットしておく.

2. DI に各パラメータをセット
ADDRESS MUX と書かれている部分で, ここでは 3 つのパラメータ START BIT, SGL/DIF, ODD/SIGN を DI ピンにセットし,CLK ピンを操作することによりシーケンスを進めていく.これらのパラメータにより,どの入力チャネルの値を取得するかといったことを指定できる.
    for i in [1, 1, adcnum]:
        if i == 1:
            GPIO.output(mosipin, GPIO.HIGH)
        else:
            GPIO.output(mosipin, GPIO.LOW)
        GPIO.output(clockpin, GPIO.HIGH)
        GPIO.output(clockpin, GPIO.LOW)
ここで,1, 1, adcnum (0 or 1) という値を送っている.この値の選択は,Multiplexer Addressing の表を見ると良いのだと理解した.ここでは純粋に adcnum に指定された入力チャネルの値を取得するように設定したが,2 番目の SGL/DIF に 0 を設定することで 2 チャネルの差分を取得するようにもできるようだ (試してはいない; 理由は後述).
(データシートより引用)

3. DATA OUT (DO) から値の読み取り
DI を使用した値の設定と逆のパターンで,DO から 1 ビットずつ値を読み込んでいく.
    adcout = 0
    for i in range(8):
        GPIO.output(clockpin, GPIO.HIGH)
        GPIO.output(clockpin, GPIO.LOW)
        adcout <<= 1
        if GPIO.input(misopin) == GPIO.HIGH:
            adcout |= 0x1
ここからは,CLK ピンが HIGH -> LOW に変化したタイミングで DO の値がセットされていく.最初の 8 ビットは MSB FIRST DATA, その後の 8 ビットが LSB FIRST DATA ということで同じ値を 1 ビットずつ,CLK ピンの電圧変化のタイミングで読み出していくことができるようだ (LSB の読み取りは試していない).

4. 読み取りプロセスの終了
最後に CS ピンを LOW -> HIGH に戻すことにより,1 回の読み取りプロセスを終了させる.
    GPIO.output(cspin, GPIO.HIGH)

4. 実行

実際に実行してみたところ,完璧とはいかなかった.
  • チャネル 0 の入力が読み取れず,常に値が 0 になってしまう.同じ接続をチャネル 1 に変更すると値を読み取ることができるので,おそらくコンバータが壊れているのではないかと推測する.
  • チャネル 1 の入力は読み取れるが,安定しない.半固定抵抗に触らない状態でも読み取られる値が結構変動する.ジャンパ線をちょっと触っても値が変動するくらいなので,おそらく流れる電流が不安定だからではないかと推測する.
以下に,半固定抵抗の出力をチャネル 0, 1 の双方に入力した時 (最初に示した接続図からは接続を追加している) のコード実行結果を示す.
pi@raspberrypi:~ $ python 6-1_adc0832.py
ADC[0]: 0        ADC[1]: 192
ADC[0]: 0        ADC[1]: 156
ADC[0]: 0        ADC[1]: 128
ADC[0]: 0        ADC[1]: 158
ADC[0]: 0        ADC[1]: 144
ADC[0]: 0        ADC[1]: 144
ADC[0]: 0        ADC[1]: 144
ADC[0]: 0        ADC[1]: 156
ADC[0]: 0        ADC[1]: 144
ADC[0]: 0        ADC[1]: 152
ADC[0]: 0        ADC[1]: 128
ADC[0]: 0        ADC[1]: 144
ADC[0]: 0        ADC[1]: 128
## snip ##
こんな感じ.

参考