今回はPCをマスタ、Arduinoをスレーブとした
SPI通信をロジックアナライザで
実際の電気信号を観測してみます。
PCとArduinoの通信というと、
UARTがしばしば使用されます。
UART通信の場合はArduino基板に既に、
USB-UART変換が搭載されているので、
変換モジュールを用意しなくても
USBケーブルでつなぐだけで、
簡単に通信が出来てしまいます。
しかしSPI通信の場合には、
USB-SPI変換が可能なモジュールが必要です。
FTDI社のFT232HQチップを搭載したモジュールを使用します。
モジュールは、CJMCU-232Hです。
チップにはFT232HQと刻印されているのですが、
PC上で操作すると分かりますが、
FT232HやFT232HSと表記されていたりします。
どっちやねん、という気持ちですが、
ここではFT232Hと統一します。
ちなみに使用したのは基盤が紫色のモジュール↓↓

今回の実験の全体図はこのようになっています。

PC(マスタ)からArduino(スレーブ)へは、
何かかしらの文字を送信します。
そしてスレーブからの返信をマスタで受信します。
今回はpythonを使用して操作します。
またスレーブで受信した文字を確認するために、
UARTでその結果をPCへ送信しています。
ではマスタ、スレーブ、ロジアナ
それぞれの設定について見ていきましょう!
PC(mater)上の設定
まずUSB-SPI変換のためのモジュールを
どうやってPC上から操作するかを考えます。
今回はFT232Hを搭載したモジュールを使用します。
調べてみるとFT232ファミリーを扱える
pythonのライブラリがあるみたいです。
加えてそれ以外にも必要な
ライブラリをインストールします。
- $ pip install pyftdi
- $ pip install pyusb
pyftdiの公式ページ
https://pypi.org/project/pyftdi/
環境によってはモジュールを
USB口に挿しただけでは認識されません。
ドライバをインストールする必要があります。
FTDI社のホームページ上にドライバがありますが、
それでは期待通りの操作が出来ません。
python上からのFT232Hの認識が出来ないのです。
そこでZadigというソフトを使用します。
https://zadig.akeo.ie/
Zadigを使用してlibusbKをインストールします。
使用するモジュールを挿してから行いましょう。
まずは、Options >> List All Devices を選択します。
すると以下のように選択できるようになります。

上の画像のようにSingle RS232-HSを選択します。
使用するモジュール(ICチップ)によって、
多少は名前が変わると思いますが、
USB口に挿さってものに近い名前を選んでください。
あと、USB ID="0403 6014"になっていることを確認して下さい。
上の画像だとusbserの文字がありますが、
これは現在適用されているドライバで、
使用環境によって異なります。
もしFTDI社公式ページのドライバが既にある場合は、
FTDIBUSという文字が入力されているかと思います。
変更したいドライバはlibusbKを選択します。
▽をクリックすれば変わっていきます。
ちなみにlibusb-win32でも動作しますが、
新しい方のlibusbKを選びましょう。
ドライバがインストール出来たら、
しっかり認識されているか確認します。
1 2 3 4 5 6 |
import usb import usb.util dev = usb.core.find() print(dev) |
FT232Hの情報が出力されます。

認識がされていることが確認できたら今度は、
コード上で指定するためのIDを取得します。
1 2 3 4 |
from pyftdi.ftdi import Ftdi Ftdi().open_from_url('ftdi:///?') |
実行するとこんな感じでIDが出力されます。

ftdi://ftdi:232h:xxxxxのように値が返ってきます。
他にもUSBデバイスが挿さっていると、
IDが変わる可能性がありますので注意が必要です。
それからもしドライバがないと、
No USB-Serial deivce has been detectedのエラーが出ます。
最後に、pythonから信号を送るコードです。
1 2 3 4 5 6 7 8 9 10 |
from pyftdi.spi import SpiController spi = SpiController() spi.configure('ftdi://ftdi:232h:0:ff/1') slave = spi.get_port(cs=0, freq=2E6, mode=0) #cs:chip select write_buf = b'A' #write_buf = b'\x41' read_buf = slave.exchange(write_buf) print(read_buf) |
spi.configure()で先ほど取得したIDを指定し、
spi.get_port()のところでSPIに関する設定をします。
csはチップセレクタのことで、
今回のようにスレーブが1機の場合は0を指定します。
freqでクロック周波数を指定しており、2E6は2[MHz]です。
modeはSPI用語のモードを意味します。
クロックの極性とデータ取得タイミングを設定するもので、
mode=0 はCPOL=0, CPHA=0を意味します。
modeとCPOL / CPHAの関係は以下になります。
CPOL=0 | CPOL=1 | CPHA=0 | CPOL=1 | |
---|---|---|---|---|
mode=0 | クロック時Highでアイドル時Low | クロックの立ち上がりでサンプリング | ||
mode=1 | クロック時Highでアイドル時Low | クロックの立ち下がりでサンプリング | ||
mode=2 | クロック時Lowでアイドル時High | クロックの立ち下がりでサンプリング | ||
mode=3 | クロック時Lowでアイドル時High | クロックの立ち上がりでサンプリング |
肝心の信号は"A"という文字を送っています。
16進数表記で0x41になります。
コード上では b'A' と直接指定してますが、
b'\x41' のように16進数で指定することも可能です。
受信したいバイト数を指定したい場合は、
slave.exchange()に引数にreadlenを指定します。
例えば、1バイト受信したいなら、
slave.exchange(write_buf, readlen=1) と記述します。
この readlen=1 が意味するところは、
1バイト受信するまではcsをLowに保つという意味です。
受信後はcsをHighに戻します。
readlenがなければ、送信完了後すぐにcsをHighに戻します。
Highに戻るとクロックがなくなるので、
スレーブはマスタへ返信できなくなります。
ここでマスタが1バイトを送信した場合を見てみましょう。
一般的なSPI通信を想定しております。
readlen=1を指定した場合のSPI通信で、
MOSIやMISOのデータは適当に描いたもので、
特に意味はないので気にしないでください。

もしreadlenがなければ、
以下の画像の①番に該当する箇所が、
read_bufの中に格納されます。
そして①番より後はそもそもクロックも発生せずに、
ピーっとLowの一本線になります。
画像中ではreadlen=1を指定してあるので、
read_bufには②番の領域が格納されることになります。
1バイト受信するまでcsはLow、クロックも発生し続けます。
ArduinoでSPI通信する上での注意
ここで、、、
PCからAを送信して、ArduinoからBを返す
という本来の目的と少し話しがそれます。
SPIは本来、上の画像中のような信号が想定されますが、
Arduino(スレーブ)はどういうわけか、
マスタからデータを受信するとそれをエコーバックします。
コード上でマスタへ返信する、
という記述をしていなくても
受信したデータをマスタへそのまま返すという意味です。
ロジアナを見る限りだとそういう挙動になっています。
例えば、PCから "Hello" をArduinoに送信してみます。
少し見にくいかもしれませんのでその場合は、
恐縮ですが拡大していただけると幸いです。

MOSIから "Hello\n" を送信しています。
\nを加えたのは Aruduino側での制御の都合によるもので、
\nがあってもなくても今回の話には関係ありません。
Arduino側へクロックが入ったら、
マスタ側へHelloをエコーバックしているのが
画像から見て取れます。
このときマスタへデータを送信する、
というコードは記述をしておりません。
少し話がそれましたがこのことを踏まえて、
Arduinoではエコーバックする際に'B'を指定し、
PC側のコードではreadlenを指定しておりません。
補足ですが、 "Hello" を送信すると、
Arduinoはクロック周波数2MHzだと、
受信したデータが "Hlo" になり、
処理が追いつきませんでした。
なのでspi.get_portでfreq=1E6に落としました。
これでPC(マスタ)側の設定は完了です!
Arduino(slave)上の設定
続いてArdudino(スレーブ)側の設定をしていきましょう。
マスタからデータを受信したら、
前述のとおり 'B' を返す、という内容です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
//Slave #include<SPI.h> byte c; int recv_flag = 0; void SlaveInit(void) { //pinMode(SCK, INPUT); //pinMode(MOSI, INPUT); pinMode(MISO, OUTPUT); //pinMode(SS, INPUT); SPCR = (1<<SPIE)|(1<<SPE)|(0<<DORD)|(0<<MSTR)|(0<<CPOL)|(0<<CPHA)|(0<<SPR1)|(0<<SPR0); SPI.attachInterrupt(); } void setup(){ Serial.begin(9600); SlaveInit(); Serial.println("-----------spi_slave starts!-----------"); } void loop(){ if (recv_flag){ Serial.println(char(c)); recv_flag = false; } } ISR(SPI_STC_vect){ recv_flag = true; c = SPDR; SPDR = 'B'; } |
なおArduino Unoの場合、ピンは以下になっています。
SS | D10 |
---|---|
MOSI | D11 |
MISO | D12 |
SCK | D13 |
SS、SCK、MISO、MOSIは <SPI.h> で定義済みなので、
変数名をそのままコード上で使用出来ます。
SPCR は SPI Contoroll Registerの略で、
SPI通信の基本設定を保持するレジスタです。
設定はビット単位で指定し、内容は以下になっています。
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | | SPIE | SPE | DORD | MSTR | CPOL | CPHA | SPR1 | SPR0 | | |
SPIE | Enables the SPI interrupt when 1 |
---|---|
SPE | Enables the SPI when 1 |
DORD | Sends data least Significant Bit First when 1, most Significant Bit first when 0 |
MSTR | Sets the Arduino in master mode when 1, slave mode when 0 |
CPOL | Sets the data clock to be idle when high if set to 1, idle when low if set to 0 |
CPHA | Samples data on the falling edge of the data clock when 1, rising edge when 0 |
SPR1 and SPR0 | Sets the SPI speed, 00 is fastest (4MHz) 11 is slowest (250KHz) |
SPEなどの名前は変数名で、該当する数字を意味しています。
例えば、1<<SPEの場合、SPEは6なので、
0x00000001を左に6ビットずらします。
したがって、0x01000000となります。
他の項目に関しても同様です。
また他の表記として以下があります。
ISRは割り込みハンドラで、
受信すると割り込み処理が発生します。
注意ですが、シリアルモニタで確認したいからといって、
割り込みハンドラ内にSerial.println()を入れないようにしましょう。
期待通りの動作をしてくれません。
c = SPDR で受信したデータを取得し、
SPDR = 'B' としてあげることで送信データを指定します。
ロジアナの設定行く前に配線を確認しておきましょう。
USB-SPI変換モジュールとArduinoの配線
今回使用したモジュール(CJMCU-232H)と
Arduino側の配線は以下になります。
CJMCU-232H | SPI端子名 | Arduino Uno |
---|---|---|
AD0 | SCK | D13 |
AD1 | MOSI | D11 |
AD2 | MISO | D12 |
AD3 | SS | D10 |
ここでよく忘れがちなのがGND線です。
同一基盤内でSPI通信を行う分には不要ですが、
今回のようにデバイス間でSPI通信を行う場合は、
GND線もデバイス間を結びましょう。
当記事の一番上にあるアイキャッチ画像が、
実際に接続している画像になります。
PCに直接接続されている白い機器は、
USB TypeC-TypeA変換するためで、実験には関係がありません。
最後にロジアナを設定して完了です。
ロジックアナライザでSPIの双方向信号を見てみる
そう。もともとこれがやりたかった!
ということでロジックアナライザを用意します。
今回はSaleae社のLogic Pro 8を使用します。

画像引用元:Amazon
専用ソフトのインストーラはこちらでダウンロードできます。
https://www.saleae.com/downloads/
それでは開いてSPI通信の設定をしていきましょう。
右のアイコンで1Fとなっているものをクリックします。

SPIの文字があればそれを選択します。
もしSPIが未表示であれば、Analyzersの右の+を押します。
その中からSPIを探して選択します。
SPIをクリックすると、SPIの設定画面になります。

各端子に該当する箇所のチャネルを選択します。
これが出来たら最後にサンプリングレートを設定します。
サンプリングレートがクロック周波数より低いと上手く観測出来ません。

クロック周波数に設定した値より大きな値を選択します。
ここでは一応、100MS/sを選択します。
今回はクロック周波数が2MHzで指定しましたので、
6.25MS/sでも十分ですが、もし下回っている場合は、
サンプリングレートを上げましょう。
準備が出来たら右上の緑色の△を押せば観測開始です。
これでロジックアナライザのSPIに関する設定は完了です。
最後に、実際に観測した波形がこちらになります。
波形にカーソルを持っていくと、
観測された波形の情報が見れます。


期待通りに観測されていますね。
MOSIでは A の文字が、MISOでは B が確認出来ています。
他にも2点確認しておきましょう。
まずはクロック周波数をコード上では2MHzを指定しました。
画像の赤枠内を見ていただくと、
ロジアナ上で観測された周波数とほぼ一致していますね!
また、今回はSPI通信にmode=0を指定しました。
これはクロックは通信時Highでそれ以外はLow
そしてエッジ立ち上がりでサンプリングする
という内容でした。

緑枠のところ、つまり
クロックの立ち上がり箇所で0、1を判断しています。
画像中のチャネル2:MISOでは01000010になります。
10進数で66、Asciiコードだと 'B' を意味します。
めでたし、めでたし、
これで1バイトの簡単な送受信が出来ましたね!
最後まで読んでいただきありがとうございました。
参考:
http://www.gammon.com.au/spi