漢字表示とGLCDシールド作成でSPIをお勉強しました。SPIの動作やArduinoでの基本的な使い方について分かったことを記載します。
SPIのブロック構成
SPIはボード内のデバイス間通信規格です。2つのデバイスがSPIを使って通信する場合、通信を主導する側がMaster、受動的に動作する側がSlaveとなります。余談ですが、私が以前関わったシステム開発ではMaster(主人)・Slave(奴隷)という用語は差別的な響きがあるためよろしくないと言われて、Main・Subordinary(主・従)という用語を使ったことがありました。
図1にSPIの機能ブロックとMaster~ Slave間の接続構成を示します。Arduinoの場合、AVRがMasterになります。
図1に示す通り、Master・SlaveデバイスのShift Registerをぐるっとループ接続した形になっており、MasterのShift Registerに設定したデーターがSPI Clockに同期してShift outされSlave側のShift Registerに書き込まれます。
AVR~Slaveデバイス間のデーター送受信
AVRにはSPI専用のShift RegisterとしてSPDR(SPI Data Register)と呼ばれる8bit長のレジスターが存在します。AVRがMasterになる場合の動作概要は以下のとおりです:
- SPDRに送信データーを書き込む
- SPIクロックがスタートし、データー転送が始まる
- 8bitのデーター転送が終了するとSPIクロックが停止し、SPSR(SPI Status Register)のSPIFフラグをセットする
上記のように、SPIは1byteをサイクルとしたデーター転送になります。複数バイトのデーター転送を行う場合は、SPIFフラグセットを確認した後に後続のbyteをSPDRに書き込みます。
SPIには、以下の組み合わせによって、4つのモード(Mode 0~3)があります。
- クロック極性(CLPOL):クロック停止(アイドル)状態状態にてクロック信号がLow/Highのどちらを取るか
- クロック位相(CPHA):入出力データーの取り込み・送出をクロックの立ち上がり・立ち下がりのどちらで行うか
Ethernet ShieldやFlash Memoryライブラリで使用しているmode 0は以下の動作となります(SPCRレジスタを以下に設定):
- CPOL = 0:アイドル時SCKをLowレベルに保持(正パルスのクロックを生成)
- CPHA = 0:クロックの立ち上がりでMISO(入力)をサンプリング、クロックの立ち下がりでMOSI(出力)を次のデーターに切り替える
データー送信のビット・オーダーはMSB First or LSB FirstをSPCR(SPI Control Register)の設定で指定することができます。ライブラリではMSB Firstになっています。
図2・図3にMasver(AVR) - Slaveデバイス間のデーター送受信イメージを示します。
1) Master(AVR)→ Slaveへの送信(図2)
①SPDRにデーターをセット
②SPIクロックがスタートしデーターを送信(例ではMSB First)
③同時にSlave側のShift Registerから送られたデーターを受信する
2) Slave→ Master(AVR)への送信 (図3)
SPIはMaster主導で通信を開始するため、MasterからSlaveに対してSPI Clockを送る必要があります。そのため以下のシーケンスになります;
①AVRのSPDRにダミーデーターを設定する(値は任意)
②SPI Clockがスタートし、Slaveがデーターを送信する
③Slaveから受信したデーターがSPDRに格納される
上記以外に、AVR側のプログラムにてSS(Slave Select)信号を制御する必要があります(ハード自律制御機能はありません)。即ち、データー転送サイクル開始前にSS信号をLowに落として、転送終了後にHighに戻す必要があります。
実際のSPI制御イメージ
GLCDシールドの制作で使用したSPI Flash Memory(AT45DB161Dライブラリ)を例に、SPIの制御イメージを示します。
Flash MemoryからManufacturer・Device IDを読み出してみます。Arduinoのスケッチは以下となります。
#include <at45db161d.h>
ATD45DB161D dataflash;
void setup()
{
/* Set baud rate for serial communication */
Serial.begin(115200);
uint8_t status;
ATD45DB161D::ID id; // ID情報を保持する構造体
/* Let's wait 1 second, allowing use to press the serial monitor button :p */
delay(1000);
/* Initialize dataflash */
dataflash.Init();
Serial.println("\nDataflash Init");
delay(10);
/* Read status register */
status = dataflash.ReadStatusRegister();
Serial.println("Status Register read");
/* Display status register */
Serial.print("Status register :");
Serial.print(status, BIN);
Serial.print('\n');
/* Read manufacturer and device ID */
dataflash.ReadManufacturerAndDeviceID(&id);
/* Read manufacturer and device ID */
Serial.print("Manufacturer ID :"); // Should be 00011111 (1FH)
Serial.print(id.manufacturer, HEX);
Serial.print('\n');
Serial.print("Device ID (part 1) :"); // Should be 00100110 (26H)
Serial.print(id.device[0], HEX);
Serial.print('\n');
Serial.print("Device ID (part 2) :"); // Should be 00000000
Serial.print(id.device[1], HEX);
Serial.print('\n');
Serial.print("Extended Device Information String Length :"); // 00000000
Serial.print(id.extendedInfoLength, HEX);
Serial.print('\n');
dataflash.EndAndWait(); // 終了処理(SSをLowに落とす)
}
void loop()
{
}
dataflash.ReadManufacturerAndDeviceID(&id);にてManufacturer・Device IDを読み出します。ReadManufacturerAndDeviceIDメソッドはライブラリ上では、以下のコーディングになっています。
void ATD45DB161D::ReadManufacturerAndDeviceID(struct ATD45DB161D::ID *id)
{
DF_CS_inactive; /* Make sure to toggle CS signal in order */
DF_CS_active; /* to reset Dataflash command decoder */
/* Send command */
spi_transfer(AT45DB161D_READ_MANUFACTURER_AND_DEVICE_ID);
/* Read Manufacturer ID */
id->manufacturer = spi_transfer(0x00);
/* Read Device ID (part 1) */
id->device[0] = spi_transfer(0x00);
/* Read Device ID (part 2) */
id->device[1] = spi_transfer(0x00);
/* Read Extended Device Information String Length */
id->extendedInfoLength = spi_transfer(0x00);
}
spi_transfer()関数を呼び出すことによって、ID読み出しのコマンド(0x9F)をSlave(Flash Memory)に送信します。
コマンドを送信した後で、ID情報として4byteを受信する必要があるのですが、図3に示したシーケンスを起こすために、ダミーの0x00を引数としたspi_transfer()の呼び出しを4回行っています。この際の戻り値が受信データーとなります。
spi_transfer関数の定義は以下となっています;
inline uint8_t spi_transfer(uint8_t data)
{
SPDR = data;
while(!(SPSR & (1 << SPIF))) ;
return SPDR;
}
SPDR(SPI Data Register)に送信dataを設定することでデーター転送を起動し、SPSR(SPI Status Register)にSPIFフラグが立ったら受信データー(SPDRの格納値)を返しています。
SPIの特長として、ID読み出しコマンド送信のようにSlaveデバイスからの受信データーを期待しないケースでも、何らかの情報がSlaveから返ってきます。この値がどうなっているのか興味があったため、spi_transfer関数に以下のprint文を入れて実行してみました。
inline uint8_t spi_transfer(uint8_t data)
{
SPDR = data;
Serial.print("Send data="); Serial.println(data, HEX);
while(!(SPSR & (1 << SPIF))) ;
Serial.print("SPDRrx="); Serial.println(SPDR, HEX);
return SPDR;
}
<Manufacturer・Device ID読み出しスケッチの実行結果>
(青字がデバック用のprint文による出力です)
Dataflash Init
Send data=D7
SPDRrx=FF
Send data=0
SPDRrx=AC
Status Register read
Status register :10101100
Send data=9F → ID読み出しコマンド
SPDRrx=FF → Flash MemoryからはFFが返っている
Send data=0 → ID取得のためのダミーデーター送信
SPDRrx=1F → Flash Memoryが送信したID情報
Send data=0
SPDRrx=26
Send data=0
SPDRrx=0
Send data=0
SPDRrx=0
Manufacturer ID :1F
Device ID (part 1) :26
Device ID (part 2) :0
Extended Device Information String Length :0
Send data=D7 → Status要求コマンド
SPDRrx=FF
Send data=0 → Status取得のためのダミーデーター送信
SPDRrx=AC → 取得したStatus値
上記の結果に示すように、コマンド送信時にFlash Memoryから戻ってくる値は0xFFでした。Flash Memoryにて、コマンド受信開始時点でのShift Register値が送られると思いますが、送ったコマンドがそのまま返ってくるということはshift registerの構造的にないということです。
SPIを使ったブロック転送
I2Cを使用したEEPROMでは、ライブラリを使用して指定したアドレスから連続したデーターをブロック転送することができました。SPIを使用したFlash Memoryでも同様にブロック転送が可能です。AT45DB161Dライブラリにはブロック転送のメソッドがないため、コマンドを発行した後でspi_transfer()を連続して呼び出すことによってブロック転送を実現します。例として、漢字表示のライブラリでAT45DB161Dから1文字分のフォントデーターをブロック読み出しするためのコードを示します。
dataflash.ContinuousArrayRead(dfAddress[0], dfAddress[1], 1); // コマンド発行
for (i = 0; i < dataLength; i++) // ブロック転送
fontBuf[i] = spi_transfer(0xff);
dataflash.EndAndWait(); // 終了処理(SSをLowに落とす)
Flash Memoryのアドレスは、ContinuousArrayRead()メソッドにて転送開始アドレスを指定し、後続データー取得時はFlash Memory側でアドレスを自動インクリメントしてくれるため、オーバーヘッドは最小限です。
一方Ethernetライブラリでは、W5100(TCP/IP処理チップ)からのデーター読み出しは以下となっています。(読みやすさのために、ダイレクトバス接続のコードを削除し、SPI接続のコードのみを抜粋)
/**
@brief This function reads into W5100 memory(Buffer)
*/
uint16 wiz_read_buf(uint16 addr, uint8* buf, uint16 len)
{
uint16 idx = 0;
IINCHIP_ISR_DISABLE();
IINCHIP_SpiInit();
for (idx=0; idx<len; idx++)
{
IINCHIP_CSoff(); // CS=0, SPI start
IINCHIP_SpiSendData(0x0F); // W5100へのReadコマンド発行
IINCHIP_SpiSendData(((addr+idx) & 0xFF00) >> 8); // 読み出しアドレス送信
IINCHIP_SpiSendData((addr+idx) & 0x00FF);
IINCHIP_SpiSendData(0); // ダミーデーターの送信
buf[idx] = IINCHIP_SpiRecvData();
IINCHIP_CSon(); // CS=1, SPI end
}
IINCHIP_ISR_ENABLE();
return len;
}
なんと、forループの内側で毎回読み出しアドレス 2byteを指定しています(リストの青字部分)。W5100のデーターシートを見ると、Readコマンド(0x0F)+アドレス2 byte+データー1byteの32bitが、1byteのデーターをW5100から読み出すための基本サイクルだと書いてあります。W5100では、SPI接続でのブロック転送モードという概念はないということになります。ArduinoのEthernet接続でスループットを要求することはないでしょうからまぁ許せるのですがオーバーヘッドがでかくてもったいないです。W5100では、8bit data busと15bit address busを使用したMCUとのダイレクト接続モードがあるため、高速処理を行いたい場合はこちらを使えということなのだと思います(address busを2bit接続に縮退したIndirect Busという接続構成もあります)。
SS信号(コードではCS)のLow/High制御もループの内側で1byte受信毎に行っているためオーバーヘッドが多いように思います。他のSPIデバイスと混在した際に、W5100からパケットデーターの途中まで受信したところで処理を中断して、もう一方のSPIデバイスから優先的に受信を行いたいようなケースを加味してこのようなコードになっていると思われます。(他の割り込み処理を考えなければ、関数の出入り口で一回だけSSのON/OFFを行えばよいです)。
AT45DB161DはSPIインタフェース専用のため、ブロック転送の考慮がされています。AT45DB161Dのデーターシートを見ると、SPIクロックは最大66MHz(High Frequency Mode)まで許容しており結構な高速動作が可能です。また、最大SPIクロックが33MHzのLow Frequency Modeが存在します。High Frequency Modeでは、address指定の後にダミーの1byteを追加転送する必要があります(Flash Memoryがデーターを準備する時間を確保するためのwait代わりなのだと思います)。AVRと接続する場合、AVRのクロック÷2が最大SPIクロック周波数のため(SPIの倍速化を参照)、余計なダミーbyte送信が不要となるLow Frequency Modeが最適です。
SPI接続のFlash MemoryはPCのBIOS格納用に使用されているらしく、そのため高クロック化が進んでいるようです。PC起動時には真っ先にBIOSが動いて、POSTや各デバイスの初期化を行います。BIOSが起動するためにはSPI FlashからDRAMにBIOSイメージを展開してBIOSの開始アドレスにJumpする必要がありますが、誰がこの処理を行っているのか(ひょっとしてチップセット?)など、新たな疑問がわいてきます。
その他
SPIのクロック周波数を調べていくうち、I2Cのクロックはどうなっているのか興味が出てきたのでArduinoのライブラリを調べてみました。答えは、100KHzです(以下に示すwireライブラリのコードより)。
#define CPU_FREQ 16000000L
#define TWI_FREQ 100000L
// initialize twi prescaler and bit rate
cbi(TWSR, TWPS0); // TWI(I2C)のクロック分周比を1:1にセット
cbi(TWSR, TWPS1);
TWBR = ((CPU_FREQ / TWI_FREQ) - 16) / 2; // TWI_FREQにするための調整値をセット
/* twi bit rate formula from atmega128 manual pg 204
SCL Frequency = CPU Clock Frequency / (16 + (2 * TWBR))
note: TWBR should be 10 or higher for master mode
It is 72 for a 16mhz Wiring board with 100kHz TWI */
以前に、漢字フォントの格納先として「I2C/SPI Memoryのどちらが早いか?」と書いた事があったのですが、圧倒的にSPIが早いです。
余談
漢字表示が一段落したため、次のRSSリーダーをどうするか考えているのですが、今ひとつ進んでいません。XMLの構文解析が重そうです。
RSSなら構文(タグの名称など)はある程度決め打ちにできるため、超簡単なタグ解析処理を自分で作るか、TinyXMLのようなXML Parserを移植するのがよいか(そこそこプログラム規模があるXML ParserがArduinoに乗るか)を考えているのですが、自分で作るのも大変そう(作っても中途半端なものしか出来ない)、一方で出来合いのライブラリを持ってくるのもおもしろくなく、今ひとつモチベーションが上がらない状態です。
最近のコメント