ラズパイでアナログ電圧を扱う (5) MCP3208のプログラム①

 前回、ライブラリgpiozeroを使って製作したMCP3208ボードの動作を確認しました。ここでは、PythonのSPIのライブラリspidevを利用しながらアナログ電圧を読み取るプログラムを作っていきます。spidevを使えば、新しいデバイスが出てきた直後、ライブラリが用意されていない製品にも対応ができます。

SPIバスの概略

 SPIバスには、I2Cバスと同様に、いくつものデバイスをつなげられます。I2Cではスレーブ・デバイスのアドレスで個々を判別していましたが、SPIバスでは物理的なチップ・セレクト信号を使います。
 マスタであるラズパイは、チップ・セレクト信号CSをHIGHからLOWにすると、スレーブであるデバイスが、自分が選択されたことを知ります。

 マスタであるラズパイが、クロックCLKを出します。データは、ラズパイからスレーブに対してはMOSI端子に、スレーブからラズパイに対してはMISO端子に乗ります。

 spidevで利用できるCS端子はGPIOピンに2本あります。したがって、SPIバスに二つのデバイスをつないで利用できます。デフォルトで使えるSPIバスは0です。

 クロックは、MOSI、MISOの両方が必要とします。つまり、

  • マスタであるラズパイがデバイスに対してレジスタの設定などのコマンドを送るとき
  • デバイス側が用意できたデータをマスタへ送るとき

の両方です。

 マイコンとつながったSPIバスの多くが、8ビット単位でデータを扱います。

 次のタイムチャートは、MCP3208のデータシートに掲載されているものを拡張しています。青色で囲った部分がデータシートに書かれているタイミングです。
 CSはCE0(24ピン)、CLKはCLK(23ピン)、DinはMOSI(19ピン)、DoutはMISO(21ピン)につなぎます。

 Din(MOSI)のタイミングを見ます。
 Startから始まってD0までは、マスタのラズパイがスレーブのMCP3208へコマンドを送る部分です。

 Dout(MISO)のタイミングを見ます。
 その1クロック後から、MCP3208はマスタに向けて13ビットのA-D変換したデータを送ります。受け取るマスタは8ビット単位でしか受信できません。最後のB0ビットから逆に数えて16ビット目は、マスタの送るD1ビット目までに相当します。さらに8ビットさかのぼるためにStartの前に'0'を5ビット分を追加します。

 今度をマスタ側からデータの流れを見ます。

  • 最初に8ビットのデータ(0、0、0、0、0、Start、SGL/DIFF、D2)
  • 次に8ビット(D1、D0、何でもよいから6ビット分)
  • 最後になんでもよいから8ビット分

を送り出します。3バイトを送る間、クロックは出続けています。

 マスタはMISO端子に、

  • 最初の8ビットの不定なデータ
  • 2番目の8ビットは、不定なデータ4ビット、NullBit、B11、B10、B9、B8を
  • 3番目の8ビットはB7~B0

を受信します。
 したがって、最初の1バイトは捨てて、2番目の1バイトの下位4ビットだけ取り出し、3番目のデータと合成すれば12ビット・データが読み出せます。

(※)何でもよい=0もしくは1のどちらでもよい。不定なデータ=1か0かがわからない。

 SPIバスでは、マスタから、クロックに合わせてデータが送り出されると、同時に、スレーブのデバイスからマスタに向けてデータが押し出されてくるイメージです。

spidev

 プログラムの最初に、ライブラリの利用をするためにインポートします。今のラズパイでは、このライブラリはインストールされています。バージョンはpip3 listで調べられます。現在3.3です。ソースはpip3 show spidevで表示されるここにありますが、バージョンが3.4なので、機能などが少し異なるような気がします。

import spidev

 チップ・セレクトのCE0(24ピン)を使うときは、

spi = spidev.SpiDev()
spi.open(0,0) #bus0,cs 0

 CE1(26ピン)につなぐときは、

spi.open(0,1)

です。次に転送速度を指定します。2018年年末ごろのバージョンからデフォルトでは何も設定されないようで、動きません。

spi.max_speed_hz = 1000000 # 1MHz

 そのほかの引数はデフォルト値が用意されているようで、指定しなくても動きます。

  • クロックの極性、どのエッジでデータを確定させるを決めるmode デフォルトは不明。0b00と思われる(※)
  • MSB/LSBのどちらから送るかを決めるlsbfirst デフォルトはMSBから

 modeには0~3の4種類あります。

  • mode0 クロックの極性CPOL=0 正極性(LOW状態からHIGH)、立ち上がりエッジCPHA=0
  • mode1 クロックの極性CPOL=0 正極性(LOW状態からHIGH)、立ち下がりエッジCPHA=1
  • mode2 クロックの極性CPOL=1 負極性(HIGH状態からLOW)、立ち下がりエッジCPHA=0
  • mode3 クロックの極性CPOL=1 負極性(HIGH状態からLOW)、立ち上がりエッジCPHA=1

(※)ほとんどのSPIバスをもつデバイスはmode0で通信できる。このWebではADS1220がmode1を使った。

 マスタからスレーブに対してコマンドを送信するには、二つの方法があります。

  • xfer(list of values[, speed_hz, delay_usec, bits_per_word])
  • xfer2(list of values[, speed_hz, delay_usec, bits_per_word])

 オプションを除けば、リスト形式でdataをセットするだけで使えます。

  • xfer(list of values)
  • xfer2(list of values)

 二つの違いはCSの扱いです。xfer()はChip-select should be released and reactivated between blocks. (チップ・セレクトはブロック間で解放され再開されるべき)、xfer2()はChip-select should be held active between blocks.(チップ・セレクトはブロック間でアクティブにしておく必要がある)の違いがあります。

 SPIでデータを読み書きする間はCSはLOWのままで、やり取りがすむとCSをHIGHにするので、xfer2()を使うべきといえます。ただ、xfer()でも問題なくSPIの転送ができる場合もあるようです。

具体的にマスタが送るデータ

 どのライブラリでも同じですが、マスタがスレーブに対してデータを送り出す間、クロックが出ます。タイミングのところで説明した、

最初に8ビットのデータ(0、0、0、0、0、Start、SGL/DIFF、D2)、次に8ビット(D1、D0、何でもよいから6ビット分)、最後に何でもよいから8ビット分を送り出します。

 何でもよいから、というのはクロックを出すために必要なデータです。

  • Start '1'と思われる
  • SGL/DIFF シングルエンド入力か疑似差動入力かを示し、'1'でシングルエンド、'0'で疑似差動入力
  • D2、D1、D0は次のようにチャネル選択を指示する。D1、D0は2バイト目のデータ
D2 D1 D0 チャネル
0 0 0 ch0
0 0 1 ch1
0 1 0 ch2
0 1 1 ch3
1 0 0 ch4
1 0 1 ch5
1 1 0 ch6
1 1 1 ch7
0 0 0 ch0=in+,ch1=in-
0 0 1 ch0=in-,ch1=in+
0 1 0 ch2=in+,ch3=in-
0 1 1 ch2=in-,ch3=in+
1 0 0 ch4=in+,ch5=in-
1 0 1 ch4=in-,ch5=in+
1 1 0 ch6=in+,ch7=in-
1 1 1 ch6=in-,ch7=in+

 実際のコードは、シングルエンド、チャネル0では、0b00000110 0b00xxxxxx 0bxxxxxxxxなので、x=0とすれば、0b00000110 0b00000000 0b000000 = 0x06 0x00 0x00になります。0から4チャネルまでをシングルエンド入力とすれば、次のコードで指定できます。

チャネル 1st byte 2nd byte 3rd byte
0 0x06 0x00 0x00
1 0x06 0x40 0x00
2 0x06 0x80 0x00
3 0x06 0xc0 0x00

(2020/10/05)バイナリ表記部分で、最初のバイトで先頭に'0'が1個足りなく7ビットになっていたのを修正。

実験

 シングルエンド、チャネル0から入力するプログラムを作ります。入力には乾電池を1本つないでいます。
 xfer2(ch0)を実行した結果、adc[0]に最初のデータが入ってますが、A-D変換のデータではないので捨てます。adc[1]の下位4ビットに12ビットの上位データが入っているので、0x0f(0b00001111)でマスクをして取り出し、8ビット上位へシフトし、下位8ビットがadc[2]に入っているので結合し12ビット・データとします。

import spidev
Vref = 2.5
ch0 = [0x06,0x00,0x00]

spi = spidev.SpiDev()
spi.open(0,0) #bus 0,cs 0
spi.max_speed_hz = 1000000 # 1MHz

adc = spi.xfer2(ch0)
data = ((adc[1] & 0x0f) << 8) | adc[2]
print(str(Vref*data/4096) + "V")

spi.close()


(2020/05/09)4095は間違いなので、4096に変更した。上記のスケッチも修正したが、実行結果は変更していない

 ターミナルで実行しました。

 このときの波形です。