2016年04月18日 | 公開。 |
前のページでは、SPIバスを通じて送ったコマンドでAQM1248Aを制御する方法や、SPIバスを通じて送ったデータがVRAMにどの様に書き込まれるかについて説明しました。このページでは、コマンドやデータをSPIバスに対して送信する方法について説明します。
今までの説明ではハードウェアSPI、ソフトウェアSPIの順で説明してきましたが、SPIバスへの送信方法の場合は、ソフトウェアSPIについて先に説明する方がより理解が深まるため、ソフトウェアSPI、ハードウェアSPIの順に説明します。
何度も説明していますが、AQM1248Aにコマンドやデータを送信する時のタイミングチャートは、2ページの図3になります。見やすいように、このページにも同じ図を載せておきます。
ソフトウェアSPIの場合、このタイミングチャートに沿った波形を、GPIOをソフトウェア的に操作する事により、生成します。
この様に、GPIOを操作して、インターフェースの波形を生成する事をbit-bang(ビットバン)といいます。ここでbitはGPIOの各ビットを指しており、bangは「バンバンたたく事」を意味しています。(GPIO出力をHとLの間で激しく切り替える事の比喩的表現)
「ソフトウェアSPI」と「bit-bang方式のSPI」は、どちらも同じ事を指していますが、"bit-bang"という言葉が「GPIOを操作してインターフェースの波形を生成する事」という意味にもっぱら使われるのに対し、「ソフトウェア」という言葉には色々な意味が含まれるため、「bit-bang方式のSPI」と呼ぶ方が、意味がより明瞭になります。
注:bit-bangはSPIインターフェースにのみ使われる言葉ではなく、I2CやUARTなど、インターフェースなら何にでも使われます。例えば、ArduinoのSoftwareSerialライブラリは、bit-bang方式のUARTのライブラリです。
それでは、皆さんにソフトウェアSPI(bit-bang方式のSPI)によるコマンドやデータの送信関数を読んでいただき、ビットをバンバンたたいている様子を実感していただきましょう。
3ページのリスト2の中で、実際にコマンドやデータをSPIバスに送信しているのは、LcdWrite関数です。リスト4に、LcdWrite関数のみを抜き出して示します。ただし、リスト4では、説明を分かりやすくするために、digitalWrite関数に通し番号を振り、括弧でくくってコメント中に記載しています。
void LcdWrite(uint8_t DI, uint8_t dat)
{
digitalWrite(CS_PIN,LOW); // (1)
digitalWrite(DI_PIN,DI); // (2)
digitalWrite(MOSI_PIN,(dat & 0x80 ? HIGH : LOW)); // (3)
digitalWrite(SCK_PIN,LOW) ; // (4)
digitalWrite(SCK_PIN,HIGH); // (5)
digitalWrite(MOSI_PIN,(dat & 0x40 ? HIGH : LOW)); // (6)
digitalWrite(SCK_PIN,LOW) ; // (7)
digitalWrite(SCK_PIN,HIGH); // (8)
digitalWrite(MOSI_PIN,(dat & 0x20 ? HIGH : LOW)); // (9)
digitalWrite(SCK_PIN,LOW) ; // (10)
digitalWrite(SCK_PIN,HIGH); // (11)
digitalWrite(MOSI_PIN,(dat & 0x10 ? HIGH : LOW)); // (12)
digitalWrite(SCK_PIN,LOW) ; // (13)
digitalWrite(SCK_PIN,HIGH); // (14)
digitalWrite(MOSI_PIN,(dat & 0x08 ? HIGH : LOW)); // (15)
digitalWrite(SCK_PIN,LOW) ; // (16)
digitalWrite(SCK_PIN,HIGH); // (17)
digitalWrite(MOSI_PIN,(dat & 0x04 ? HIGH : LOW)); // (18)
digitalWrite(SCK_PIN,LOW) ; // (19)
digitalWrite(SCK_PIN,HIGH); // (20)
digitalWrite(MOSI_PIN,(dat & 0x02 ? HIGH : LOW)); // (21)
digitalWrite(SCK_PIN,LOW) ; // (22)
digitalWrite(SCK_PIN,HIGH); // (23)
digitalWrite(MOSI_PIN,(dat & 0x01 ? HIGH : LOW)); // (24)
digitalWrite(SCK_PIN,LOW) ; // (25)
digitalWrite(SCK_PIN,HIGH); // (26)
digitalWrite(CS_PIN,HIGH); // (27)
} // LcdWrite
LcdWrite関数は、DIとdatの2つの引数(共にuint8_t型)を取ります。
DIはコマンドを出力するかデータを出力するかの区別を表わし、DIをLOWにすればコマンド、DIをHIGHにすればデータの出力となります。DIは、digitalWrite関数により、RS信号にそのまま出力されます。
datは8ビットのコマンドまたはデータです。SCLKの立ち上がりに同期して、SDI信号(MOSI信号)に最上位ビットから順に送信されます。
リスト2を起動した時に、一番最初にInitializeLcd関数内のLcdCommand(0xae);
により、LcdCommand関数を経由して、LcdWrite(LOW,0xae);
が呼び出されます。すなわち、AEHのコマンドを送信します。
この時のSPIバスの波形をロジックアナライザで観察した結果を図44に示します。また、この時の測定風景を写真28に示します。なお、測定にはArduino Unoを用い、6ページの図39の結線によりAQM1248Aを動作させました。使用したArduino IDEは1.7.8です。
参考:ロジックアナライザによる波形測定の話はArduino用ヘッダシールドの製作(3)の記事に詳しく書いたので、そちらも参照してください。
図44を見ると、/CS信号がLの間にSCLK信号の立ち上がりが8つある事が分ります。
AQM1248Aは、SCLK信号の立ち上がりの時のSDI信号を読み取ります。図の中にも赤字で書きましたが、10101110Bのコマンドを送っていることが分かります。10101110Bは16進数に直すとAEHになりますので、波形から、AEHのコマンドがきちんと送れている事が確認できます。
RS信号は、最後のSCLK信号の立ち上がりの際に読み込まれるで、そのタイミングだけ論理が確定していればいいのですが、リスト4のLcdWrite関数では、/CS信号がLになった直後にRS信号をLにしています。(ただし図44では、RS信号が元々Lだったので、RS信号をLに設定したタイミングが波形からは読み取れません)
次に、図44の波形の最初の部分を時間軸方向に拡大し、リスト4の各digitalWrite関数の実行タイミングを書き込んだ物を図45に示します。リスト4と図45を比較してみると、bit-bangにより波形を作っている様子がよく分かると思います。
赤字で書いた括弧付きの数字と矢印は、リスト4内の各digitalWrite関数がどのタイミングで実行されたかを示します。
ところで、SPIバスへの送信の様に、クロック同期方式のシリアルデータの送信をbit-bang方式で行うことは、頻繁にあります。Arduinoには、そういう時に使うshiftOut関数が標準で用意されています。shiftOut関数は、SPI接続の周辺機器を利用する場合だけでなく、74HC595などのシフトレジスタに情報を送信する場合にもよく使われます。
注:74HC595は、シリアル入力、パラレル出力のシフトレジスタで、Arduinoのピン数が足りない時に、出力ピンを増設する用途によく使われます。
shiftOut関数の書式は次の様になっています。
dataPinには、データを送信するのに使うピンの番号を指定します。
clockPinには、データ送信の際のクロックの出力に使うピンの番号を指定します。
bitOrderには、ビットの送信順序を指定します。MSBFIRSTを指定すれば最上位ビットから、LSBFIRSTを指定すれば最下位ビットから送信します。
valueには、送信したいデータを8ビットで指定します。
AQM12448Aにコマンドやデータを送るには、dataPinにSDI信号(MOSI信号)に使うピン番号を指定し、clockPinにSCLK信号に使うピン番号を指定し、bitOrderにMSBFIRSTを指定し、valueに送信したいコマンドまたはデータを指定し、shiftOut関数を呼び出します。そうすれば、いちいちdigitalWrite関数でデータやクロックの信号線をバンバンたたかなくても、shiftOut関数内部で信号波形を作ってくれます。
ただし、shiftOut関数だけでは/CS信号やRS信号の生成はできませんので、これらについてはdigitalWrite関数を用いて生成する必要があります。
この様な方針でLcdWrite関数を書き換えると、リスト5の様になります。
void LcdWrite(uint8_t DI, uint8_t dat)
{
digitalWrite(CS_PIN,LOW);
digitalWrite(DI_PIN,DI);
shiftOut(MOSI_PIN,SCK_PIN,MSBFIRST,dat);
digitalWrite(CS_PIN,HIGH);
} // LcdWrite
リスト4と比較すると、ずいぶん短くなりましたね。(3)~(26)のdigitalWrite関数が、一つのshiftOut関数の呼び出しに置き換わりました。
また、LcdWrite関数の仕様変更に伴い、setup関数も1行だけ変更になります。変更後のsetup関数をリスト6に示します。
void setup() {
pinMode(DI_PIN ,OUTPUT);
pinMode(CS_PIN ,OUTPUT);
pinMode(MOSI_PIN,OUTPUT);
pinMode(SCK_PIN ,OUTPUT);
digitalWrite(CS_PIN ,HIGH);
// digitalWrite(SCK_PIN,HIGH);
digitalWrite(SCK_PIN,LOW); // 前の行をこの行に差し替え
InitializeLcd();
}
リスト2のLcdWrite関数およびsetup関数をそれぞれリスト5、リスト6の様に書き換えて実行した際の観測波形を図46に示します。この波形の観測条件は、LcdWrite関数とsetup関数を書き換えた以外は、図44の観測時と同じです。比較しやすいように、図44も、図46のすぐ後に再掲します。
図44と図46とを比較すると、SCK信号の立ち上がりでSDI信号の10101110BのデータをAQM1248Aが取り込んでいる点では一致しますが、送信していない時(/CS信号がHの時)のSCKのレベルが図44ではHなのに対して、図46ではLとなっている点が異なります。図44の様な波形でSPIバスを使う場合の動作モードをモード3と呼び、図46の様な波形の場合の動作モードをモード0と呼びます。(SPIバスの動作モードについては、Electronic Lives Mfg.のSPIについてという記事に詳しく載っているので、ぜひ参照してください)
AQM1248Aのデータシートに従うならば、SPIバスはモード3で動作させるべきなのですが、モード3とモード0は、SCLK波形の立ち上がりでSDI信号を取り込むという点においては一緒なので、実際にはモード0でも、モード3でも、AQM1248Aが動作する様です。
SPIバスの動作モードはモード0からモード3までの4種類あります。digitalWrite関数でbit-bangすればどのモードにも対応できますが、shiftOut関数を使うとモード0しか使えないのが不便ですね。
またshiftOut関数の動作が遅いのは、利用者の間で不満の種になっています。図44と図46の比較からも分かるように、shiftOut関数を使う場合(図46)は、digitalWrite関数でbit-bangする場合(図44)よりも、わずかですが遅くなります。
スケッチからの関数の呼び出しの回数が減るのですから、それだけ関数呼び出しのオーバーヘッドが減ります。GPIOへのアクセスの仕方を工夫すれば、動作速度を改善する方法が色々とあるはずですが、shiftOut関数は、全く動作速度に対してチューニングされていない様です。コードサイズを意識した関数の作り方をしたのかも知れませんし、 保守性を優先した作り方になっているのかも知れませんが、もう少し工夫があってもいいような気がします。
3ページのリスト1では、ハードウェアSPIによりAQM1248Aの制御を行っています。このリスト1から、実際にSPIバスにコマンドやデータを送信している2つの関数(LcdCommand関数とLcdData関数)のみを抜き出した物をリスト7に示します。
void LcdCommand(uint8_t cmd)
{
digitalWrite(CS_PIN,LOW);
digitalWrite(DI_PIN,LOW);
SPI.transfer(cmd);
digitalWrite(CS_PIN,HIGH);
} // LcdCommand
void LcdData(uint8_t dat)
{
digitalWrite(CS_PIN,LOW);
digitalWrite(DI_PIN,HIGH);
SPI.transfer(dat);
digitalWrite(CS_PIN,HIGH);
} // LcdCommand
LcdCommand関数はコマンドを送信する時に使う関数で、LcdData関数はデータを送信する時に使う関数です。両者は、DI_PIN(RS信号)に対する出力がLOW(LcdCommand関数の場合)かHIGH(LcdData関数の場合)かのみが異なっています。
リスト7とリスト5とを比較すると、リスト5でshiftOut関数を呼び出していた部分が、リスト7ではSPI.transfer関数の呼び出しに置き換わっている事が分ります。
リスト7に出てくる"SPI"というのは、Arduinoのマイコンに内蔵しているSPIインターフェースモジュールを使うためのライブラリです。このSPIライブラリを利用するには、スケッチの先頭に#include <SPI.h>
と宣言する必要があります。
SPI.transfer関数は、SPIバスに8ビットの情報を送るための関数です。書式は次の様になっています。
valは8ビット符号なし整数の引数で、このvalがSPIバスに送信されます。
SPI.transfer関数には返り値(receivedVal)があります。valをMOSI信号に送出するのと同じタイミングで、MISO信号から情報を読み取り、それが返り値となります。
ただし、AQM1248AにはSPIインターフェースを通じて何らかの信号を送り返す機能がありませんから、この記事で説明している回路では、いずれもArduinoのMISO信号のピンは未接続になっています。よって、SPI.transfer関数の返り値には、何の意味もありません。
SPI.transfer関数を使うには、それに先立って、各種の設定をしておく必要があります。
まず、SPIライブラリを使用する際には、ライブラリの使用をSPI.begin関数で宣言する必要があります。これには、SPI.begin();
と、引数なしでSPI.begin関数を呼び出せばOKです。
ハードウェアSPIを使う場合は、クロック信号(SCLK信号)の周波数を設定できるようになっています。このクロック周波数を調整する事により、高速で動作する機器には高速でデータを送信し、低速でしか動作しない機器には低速でデータを送信する事が可能になります。
注:shiftOut関数を用いてソフトウェアSPIによりデータを送信する場合は、クロック周波数の調整の方法がありません。しかしshiftOut関数自体が十分に低速なので、通常は送信速度が速すぎて接続した機器が動作しないという事態は発生しません。どうしても送信速度を遅くしたい場合は、digitalWrite関数を用いて自分でbit-bangを行う関数を作り、要所要所にdelayMicroseconds関数などでウェイトを挿入する必要があります。
クロック周波数の設定には、SPI.setClockDivider関数を用います。関数の書式を次に示します。
SPIインターフェースモジュールは、マイコンのクロックを分周してSPIバスのクロックに利用する仕組みになっています。SPI.setClockDivider関数の引数(divider)には、その分周比を指定します。
分周比の指定には、あらかじめこの用途のために宣言されている定数を使います。表15に、AVRマイコンを用いたArduino(Arduino Unoなど)用のSPIライブラリで宣言されている、クロック分周比指定用の定数を示します。
定数 | 意味 |
---|---|
SPI_CLOCK_DIV2 | マイコンのクロックを2分周してSPIバスのクロックに使用 |
SPI_CLOCK_DIV4 | マイコンのクロックを4分周してSPIバスのクロックに使用 |
SPI_CLOCK_DIV8 | マイコンのクロックを8分周してSPIバスのクロックに使用 |
SPI_CLOCK_DIV16 | マイコンのクロックを16分周してSPIバスのクロックに使用 |
SPI_CLOCK_DIV32 | マイコンのクロックを32分周してSPIバスのクロックに使用 |
SPI_CLOCK_DIV64 | マイコンのクロックを64分周してSPIバスのクロックに使用 |
SPI_CLOCK_DIV128 | マイコンのクロックを128分周してSPIバスのクロックに使用 |
例えばSPI.setClockDivider(SPI_CLOCK_DIV32);
を実行すると、それ以降はマイコンのクロックを32分周してSPIバスのクロックとします。Arduino Unoの場合、マイコン(ATmega328P)のクロックが16MHzですから、SPIバスのクロック周波数は、その1/32の0.5MHz(500kHz)となります。
なお、Arduino DueやArduino M0などのARM系(Cortex系)のArduinoでは、SPI.setClockDivider関数に整数の引数を渡すと、それがそのまま分周比として解釈されます。
例えばマイコンのクロックを32分周してSPIバスのクロックに使う場合はSPI.setClockDivider(32);
とします。
AVRマイコンのArduinoでは、分周比は2、4、8、16、32、64、および128と、2の累乗しか指定できません。これがARM系Arduinoでは、例えば5分周など、2の累乗以外の分周比にも設定できるようになっており、クロック周波数の微調整ができるようになっています。
Arduino DueのSPIライブラリには、AVRマイコンを用いたArduinoと互換性を取るために、表15の定数も定義されています。これらの定数を使うと、16MHzのAVRマイコンでSPI.setClockDivider関数を実行する場合と同じSPIバスのクロック周波数に設定されます。
注:Arduino DueでSPI.setClockDivider(SPI_CLOCK_DIV32);を実行すると、マイコンのクロックを32分周ではなく168分周したものがSPIバスのクロックになります。Arduino Dueのマイコンのクロック周波数は84MHzですから、SPIバスのクロック周波数は84÷168=0.5MHzとなり、Arduino UnoでSPI.setClockDivider(SPI_CLOCK_DIV32);を実行する場合と、同じ周波数に設定されます。
Arduino M0の場合には、どういうわけかSPIライブラリに表15の定数が宣言されていません。どうしても使いたい場合は自分で宣言する必要があります。(3ページのコラム、「Arduino M0を使っていて遭遇した不具合」を参照)
SPIバスのモード(モード0~モード3)は、SPI.setDataMode関数で指定します。この関数の書式を次に示します。
modeはSPIバスの動作モードを指定する引数で、表16の定数を用いてモードの指定を行います。
定数 | 意味 |
---|---|
SPI_MODE0 | SPIバスをモード0で使用 |
SPI_MODE1 | SPIバスをモード1で使用 |
SPI_MODE2 | SPIバスをモード2で使用 |
SPI_MODE3 | SPIバスをモード3で使用 |
ビットの送出順序(最上位ビットから送るか、最下位ビットから送るか)は、SPI.setBitOrder関数で指定します。この関数の書式を次に示します。
引数のorderには、表17の定数を用いて、ビットの送出順序を指定します。(これらの定数は、shiftOut関数の引数でも使います)
定数 | 意味 |
---|---|
LSBFIRST | 最下位ビットから最上位ビットへ順に送出 |
MSBFIRST | 最上位ビットから最下位ビットへ順に送出 |
以上の様な初期設定を、setup関数内で行います。リスト1のsetup関数の部分を抜き出した物を、リスト8に示します。
void setup() {
pinMode(DI_PIN,OUTPUT);
pinMode(CS_PIN,OUTPUT);
digitalWrite(CS_PIN,HIGH);
SPI.begin();
SPI.setClockDivider(SPI_CLOCK_DIV32);
SPI.setDataMode(SPI_MODE3);
SPI.setBitOrder(MSBFIRST);
InitializeLcd();
}
この様に、SPIバスのクロック周波数を500kHz、モードを3、ビット送出順序を最上位ビット→最下位ビットの順に設定しています。
リスト1のスケッチを動作させたときのSPIバス波形をロジックアナライザで観測した結果を、図47に示します。使用したスケッチが異なる以外は、図44や図46を測定した時と同じ条件で測定しています。
注:ハードウェアシリアルの場合は、本来は6ページの図38の結線にしなければいけませんが、Arduino Unoを使う場合は、Arduino内部でSPI-3端子と13番端子、およびSPI-4端子と11番端子がショートしているため、図38と図39のどちらの配線でも電気的に等価となります。そのため、測定は図39の結線のまま行っています。
横軸の目盛りの取り方が図44や図46とは異なることに注意。
SCLK信号を見ると、デューティ比がきれいに50%になっているのが分かります。また、クロック周波数を測定すると、設定値の500kHzに正確に一致している事が分ります。信号の送出全体にかかっている時間も、図44や図46の場合の1/5程度に短縮されています。
Arduino Unoの場合、SPI.setClockDivider関数の引数にSPI_CLOCK_DIV2を渡せば、SPIバスのクロックを8MHzにまで上げられます。この場合の波形を図48に示します。
時間軸の目盛りが図47と一緒になるように調整してあります。
図47と図48を比較すると分かるように、SPIバスのクロック周波数を500kHzから8MHzまで16倍にしたのにも関わらず、コマンドの送信にかかる時間が半分程度にしか短縮されていません。これは、digitalWrite関数の実行にとても時間がかかるため、SPI.transfer関数の部分をいくら高速化しても、/CS信号とRS信号を制御するためのdigitalWrite関数の実行時間がボトルネックとなるためです。
digitalWrite関数を使わずにGPIOを制御して、処理を大幅に高速化する方法もあるのですが、これについてはArduino用ヘッダシールドの製作(4) の後半で、簡単な説明をしています。
また、後でライブラリを使ってAQM1248Aを制御する方法についても説明しますが、このライブラリではdigitalWrite関数がボトルネックになる問題には対策を施してあります。(つまり、ライブラリを使えば、何も考えなくともAQM1248Aが高速動作する)
このページでは、SPIバスの各種設定を、SPI.setClockDivider関数、SPI.setDataMode関数、およびSPI.setBitOrder関数により設定しましたが、実はこの方法は、新しいバージョンのArduino IDEでは推奨されていません。今回は古いバージョンのArduino IDEでも動作するように、これらの関数を用いましたが、現在ではSPISettingsオブジェクトを使って設定を行うことが推奨されています。
SPISettingsオブジェクトを使ったスケッチは、Arduino用ヘッダシールドの製作(3) のページの最後で紹介していますので、興味がある方はそちらをご覧ください。
このページではArduinoからSPIバスに情報を送信する方法について説明しました。次のページでは、いよいよAQM1248Aの画面にビットマップを表示する方法について説明します。
商品名 | 122X32モノクログラフィックLCDシールド | |
税抜き小売価格 | 3333円 | |
販売店 | スイッチサイエンス | |
サポートページ | 122X32モノクログラフィックLCDシールドサポートページ |
商品名 | GLCD学習シールドキット | |
税抜き小売価格 | 1410円 | |
販売店 | スイッチサイエンス | |
サポートページ | GLCD学習シールドキットサポートページ |