UNOにTFTシールドを装着して、画面に文字列を表示して見たり、ちょっとしたゲームを作って見たりしてきたのですが、UNO+TFTシールドでは空き端子がほぼ0本の状態で、「TFT表示」以外の処理が全くできませんでした。
そこで、UNOと似たフォームファクタでI/Oポート数が増えたArduino DUEを導入しましたので、今回はDUEにTFTシールドを装着してみたいと思います。
UNOの時のピン配置表はこちらです。同様なピン配置表をDUEでも作っておきます(下表)。
Arduino | DUE | TFT | 備考 |
D0 | PA8(URXD/PWMH0/WKUP4) |
・ピンソケットの物理的位置は UNOと同じ。 ・端子番号(D2〜D13)も同じ。 ・ただし、SDカードとの接続 位置にSPIのアサインが無い。 ・SOFTWARE_SPIを使って SDカードスロットへ アクセス可能(※)。 |
|
D1 | PA9(UTXD/PWMH3) | ||
D2 | PB25(RTS0/TIOA0) | D2 | |
D3 | PC28(A7/TIOA7) | D3 | |
D4 |
PC26(A5/TIOB6)/ PA29(SPI0_NPCS1/NRD) |
D4 | |
D5 | PC25(A4/TIOA6) | D5 | |
D6 | PC24(A3/PWML7) | D6 | |
D7 | PC23(A2/PWML6) | D7 | |
D8 | PC22(A1/PWML5) | D0/X+ | |
D9 | PC21(A0/NBS0/PWML4) | D1/Y− | |
D10 |
PC29(A8/TIOB7)/ PA28(SPI0_NPCS0/PCK2/WKUP11) |
SD_CS | |
D11 | PD7(A17/BA1/TIOA8) | SD_DI | |
D12 | PD8(A21/NANDALE/TIOB8) | SD_DO | |
D13 | PB27(NCS3/TIOB0) | SD_SCK | |
D54/A0 | PA16(SPCK1/TD/ADC7) | RD |
・ピンソケットの物理的位置は UNOと同じ。 ・アナログ端子番号(A0〜A5)は 同じだが、デジタル端子番号 (D54〜D59)は異なる。 |
D55/A1 | PA24(MCDA3/PCK1/ADC6) | WR | |
D56/A2 | PA23(MCDA2/TCLK4/ADC5) | RS/X− | |
D57/A3 | PA22(MCDA1/TCLK3/ADC4) | CS/Y+ | |
D58/A4 | PA6(TIOB2/NCS0/ADC3) | RESET | |
D59/A5 | PA4(TCLK1/NWAIT/ADC2) |
一部、UNOとはArduinoの端子番号が異なる端子がある(D54〜D58)ので、そこは、プログラム先頭付近の定数宣言箇所にて、#ifdefを使って場合分けします。
UNOの時は、5V信号が直結されているSDカードI/Fの使用を躊躇しました。DUEは3.3V I/Oと言うことで、今度こそSDカードI/Fを使えるか?と、期待したのですが、今度は、SDカードとの接続位置にSPI端子がアサインされていないという残念なお知らせが……。
DUEのSPI端子は、ICSPピンヘッダに引き出されています。うまくない。これはまた別なテーマなので、今回は無視します。
※「#14-7 DUEでTFTシールドのSDカードI/Fを使う」にて、SOFTWARE_SPIを使い、TFTシールド上のSDカードスロットにアクセスできるよう、SDライブラリにパッチを当てています。
バス信号の内、ストローブ信号(CS,RS,WR,RD)に関しては、全てDUEのポートAに集中しています。
一方、データバスについては、ほぼ、PORT-Cが割り当たっているのですが、1本だけPORT-Bが混じっています。更にうまくないのが、ポート内のビット配置です。これについては、後ほどプログラムを設計する所で検討しなくてはならないと思います。
DUEにTFTシールドを装着してみました(下図)。
端子としては、DUEのピンソケットの内、UNOとフォームファクタが同じ部分のピンソケットが埋まってしまうだけの事なのですが、TFTシールドの基板がピンソケットの接続箇所を超えて張り出しているため、思いのほか広い範囲のピンソケットを隠してしまっています。
ただ、「TFTシールドの下に隠れてしまう」だけで、ピンヘッダが挿さっているわけではないので、工夫すれば使用可能です(↓)。
上図の様なTFTシールド基板との隙間から信号線を引っ張り出す必要があります。単にデジタルI/Oポートが使いたいだけならば、フリーで使える2列ピンソケットから引き出して使うことができます。特定のI/Oユニットの入出力を使用したい場合はそうはいかないので、うまく工夫して引き出すしかありません。
まず、最初に作らなければならないのは、TFTシールド(ILI9328)と接続した8ビットバスを使って、ILI9328へアクセスするためのtft_read()関数とtft_write()関数をDUE用に移植することです。この2つの関数はTFTシールドに文字を表示したプログラム(tft_print.ino)等でも使用しているし、そもそも、TFTシールド(ILI9328)制御の基本となる関数ですので、これが無いと始まりません。
UNO+TFTシールドで最初に作ったtest_tft.inoを使って、2つのアクセス関数を設計・デバッグしようと思います。
設計方針:
「#10-9 QVGA TFT液晶を使う」にて引用した、ILI9328のバスアクセスタイミング図を再度引用しておきます(下図および表)。
※ILI9328データシートから引用
※ILI9328データシートから引用
UNOでプログラミングした時は、1命令の実行時間が1/fcpu=1/16MHz=62.5nsでした。このため、連続する2命令でI/Oポートを操作したとしても、62.5nsの時間差が生じることとなります。上の表で62.5ns以下の制約は、ほとんど何も気にすることなく満たすことが出来たのです。……1命令で同時に変化させなければ良かった。
上表で62.5nsを超えるスペックは、PWLR、PWHR、tDDRですので、ここだけ注意して、nop命令を挟むなどの対策を施しました。
今回のDUEでは、1命令の実行時間が1/fcpu=1/84MHz≒11.9nsなので、上表でこの値を超えるスペック全てに注意を払う必要があります。
今回の方針では、「インラインアセンブラを使わない」としましたが、nop命令を挿入するためのインラインアセンブラだけは使用します(↓)。
#define tft_NOP __asm__ __volatile__ ("nop") // 1/F_CPU period
例として、Read data delay time;tDDR=100nsを満たすには、以下の様な計算となります。
tDDR/(1/fcpu)=tDDR×fcpu=100ns×84MHz≒8.4サイクル
Cortex-M3では1×nop命令で1サイクル実行なので、tDDR以上の時間を確保するためには、8.4サイクルを切り上げて9サイクル、9×nop命令が必要となります。
// 9×NOP
tft_NOP; tft_NOP; tft_NOP; tft_NOP; tft_NOP;
tft_NOP; tft_NOP; tft_NOP; tft_NOP;
※SH2Aみたいに、スーパスケイラになっていたりするともう少し複雑でしたが、Cortex-M3はそんな高等なアーキテクチャを採用していないので、スッキリ、簡単・単純です。勿論、筆者はSH2Aの方が好みです(1^^)/。
今回は、インラインアセンブラを使ってARM Cortex-M4のアセンブラ命令を直接記述することはしませんが、C言語をほぼアセンブラとして使って、コーディングしようと思います。
従ってバス制御信号全て、DUEのI/Oポートレジスタを直接叩いてコーディングします。I/Oレジスタを直接制御すると、色々とI/Oユニットの初期設定(クロック供給等)などが面倒なので、以下を前提とします。
前提条件:
ストローブ信号は、CS(PA22),RS(PA23),WR(PA24),RD(PA16)の4本です。処理は以下の様になります。
const uint32 bit_CS=(1U<<22);
const uint32 bit_RS=(1U<<23);
const uint32 bit_WR=(1U<<24);
const uint32 bit_RD=(1U<<16);
:
REG_PIOA_SODR=bit_WR; // WR=1
tft_NOP; // PWHW=50ns → 3×NOP
tft_NOP;
tft_NOP;
REG_PIOA_CODR=bit_WR; // WR=0
バスタイミング制約の内、WR信号のHIGH幅規定(PWHW)を確保する例も、上コードで示しています。
DUEに搭載されたMCU(SAM3X8E)のI/Oレジスタアクセスは、fcpu×2サイクルで実行されているようです(データシートより)。従って、以下の様にI/Oレジスタ操作を連続しても、WRストローブのHIGH幅は2サイクル以下にはなりません。
REG_PIOA_CODR=bit_WR; // WR=0
REG_PIOA_SODR=bit_WR; // WR=1
REG_PIOA_CODR=bit_WR; // WR=0
WRストローブのHIGH幅規定PWHW=50nsを満たすためには、間にPWHW×fcpu−2=2.2サイクル以上確保すれば良いことになります。nop命令の整数倍でタイミングを調整するしかないので、小数点以下を切り上げて、3×nop命令が必要となります。
I/Oレジスタを叩いて → nop命令でタイミングを取って → またI/Oレジスタを叩く……、全く高級言語っぽくなくて、アセンブラでプログラムしているのと変わりません。インラインアセンブラは使わなかったけど、C言語でアセンブラみたいなコーディングをしています。そういう事(1^ー^;)。
※「REG_PIOA_SODR=bit_WR;」が、1命令で終了する前提でnop命令の数を計算していますが、さすがに全部1命令で実行できるわけでは無いはずなので、実際には必要なnop命令数よりだいぶ多めに挟まっているはずです。その分アクセス速度は低下していますが、タイミング制約に対してはマージンが増える方向ですので、安全側です。
普段はあまり気にならないのですが、GCCは、コード全体を見て「最適化」と言うやつをかけてくるので、実は、コードの実行順番はC言語のソースに記述した順番とは限りません。
cc=0;
for (idx=0; idx<10; idx++)
{
aa=7*bb;
cc += idx;
}
上のコードでは、forループ内で変数aaに値を代入していますが、「aa=7*bb」の処理はループの実行に無関係なことを見破って、GCCはこの処理をループの外側に押し出します。その方がループ実行速度が高速になるからです。
この場合、aaへの代入はループの外で1回だけ実行されます。10回ループが回っていますが、10回代入されたりはしません。また、その実行がループの手前で起きるか、ループの後で起きるかもわかりません。
ここで、変数aaの宣言が以下の様に行われると、状況が変わります。
volatile int aa;
volatile宣言を受けた変数は、実行するタイミングで値が変わる可能性があることをコンパイラ(GCC)に宣言しますので、GCCは「aa=7*bb;」の処理を、ちゃんとループの中で10回繰り返すようにコンパイルします。
※蛇足ですが、「7*bb」の処理は相変わらずループ内で不変なので、この演算はループの外で実行される可能性が高いです。その場合、volatile宣言の影響を受けてループ内で10回実行されるのは、変数aaへの代入のみです。
さて、先程示した以下の様なコードは、実行の順番を変えられてしまうとダメなのですが、大丈夫でしょうか?
REG_PIOA_SODR=bit_WR; // WR=1
tft_NOP; // PWHW=50ns → 3×NOP
tft_NOP;
tft_NOP;
REG_PIOA_CODR=bit_WR; // WR=0
結論から申しますと、大丈夫です。
REG_PIOA_SODRなどのI/Oレジスタはvolatile宣言されていますし、tft_NOPもvolatileが付いているので、順番を入れ替えて実行されることはありません。記述した通りの順番で実行されます。
最初に示したDUEとTFTシールドの接続表で、データバスdata[7:0]の並びに少々問題がありました(以下)。
data[7:0]={PC23,PC24,PC25,PC26,PC28,PB25,PC21,PC22}
TFTシールドのデータバスとDUE I/Oポートの対応:
ビット7:PC23(A2/PWML6)
ビット6:PC24(A3/PWML7)
ビット5:PC25(A4/TIOA6)
ビット4:PC26(A5/TIOB6)
ビット3:PC28(A7/TIOA7)
ビット2:PB25(RTS0/TIOA0)
ビット1:PC21(A0/NBS0/PWML4)
ビット0:PC22(A1/PWML5)
PC21〜PC26まで連続して使われているのですが、PC27が飛んで、次はPC28となっています。そこに、1本だけ異なるポートPB25が紛れ込むという状態です。しかも、ポートとしては連続するPC21〜PC26でさえ、ビットの並びが逆順になっていたりと、ひどいことになっています。
これではアクセス対象のデータから、実際のI/Oポートのビットに対応付けするためには、複雑なビット演算が必要となってしまいます。
DUEには潤沢なメモリ(Flash)が有りますので、これを武器にアクセスデータとポートデータの換算テーブルを作って対処しようと思います。
まず、ポートの各ビットにデータをマッピングするための中間変数として、port[7:0]を定義します。
port[7:0]={PC28,PB25,PC26,PC25,PC24,PC23,PC22,PC21}
PC21〜PC26は連続しているのでそのまま並べ、飛んでいるPC27の位置に、1本だけ紛れ込んだPB25を押し込んで8ビットデータとします。すると、data[7:0] ⇔ port[7:0]の変換は、高々256通り(8ビット)のテーブル参照で実行可能となります。
実装では、data → port変換テーブルd2ptbl[256]と、port → data変換テーブルp2dtbl[256]の2テーブルが必要となりますので、計512バイトのメモリ(Flash)が必要となります。DUEのFlashメモリはたっぷり512Kバイトもありますので、この程度は痛くもかゆくもありません。
Excelを使って以下の様な対応表を作り、変換テーブルd2ptbl[256]を生成しました。
hex | data[7:0] | ⇒ | port[7:0] |
d2ptbl [256] |
||||||||||||||
d7 | d6 | d5 | d4 | d3 | d2 | d1 | d0 | PC28 | PB25 | PC26 | PC25 | PC24 | PC23 | PC22 | PC21 | |||
0x00 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0x00 | |
0x01 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0x02 | |
0x02 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0x01 | |
0x03 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0x03 | |
0x04 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0x40 | |
0x05 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0x42 | |
0x06 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0x41 | |
0x07 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 0x43 | |
0x08 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0x80 | |
0x09 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0x82 | |
: | : | : | : | |||||||||||||||
0xfa | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 0 | 1 | 0xbd | |
0xfb | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 0xbf | |
0xfc | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0xfc | |
0xfd | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0xfe | |
0xfe | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0xfd | |
0xff | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0xff |
Excelの数式を駆使すると、機械的に変換ができるのでそれほど難しい作業ではありません。
逆変換テーブルp2dtbl[256]も同様にして生成しました(↓)。
hex | port[7:0] | ⇒ | data[7:0] |
p2dtbl [256] |
||||||||||||||
PC28 | PB25 | PC26 | PC25 | PC24 | PC23 | PC22 | PC21 | d7 | d6 | d5 | d4 | d3 | d2 | d1 | d0 | |||
0x00 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0x00 | |
0x01 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0x02 | |
0x02 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0x01 | |
0x03 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0x03 | |
0x04 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0x80 | |
0x05 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0x82 | |
0x06 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0x81 | |
0x07 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0x83 | |
0x08 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0x40 | |
0x09 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0x42 | |
: | : | : | : | |||||||||||||||
0xfa | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0x7d | |
0xfb | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0x7f | |
0xfc | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0xfc | |
0xfd | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0xfe | |
0xfe | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0xfd | |
0xff | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0xff |
この変換テーブルを使うと、各処理は以下の様になります。
テーブル参照処理ですが、ARMにはレジスタオフセット付きレジスタ間接アドレッシングモード(レジスタプリインデックス)があるので、テーブルアクセスは得意なはずです。確かこんな感じ(↓)。
LDRB Rd,[Rn,Rm]
Rn+Rmが指し示すアドレスから、Rdにデータを読み出します。Rn=p2dtbl、Rm=port[7:0]等としておくと、対応する変換データがRdに読み出せる命令です。下手にビット演算でビット配列を組み替えるより、ずっと高速に処理できます。
※さりげなく、「食わず嫌い」ではないことをアピール。
test_tft.inoでは、tft_read()関数、tft_write()関数以外にも、いくつかTFT制御用関数が有って、UNOでは他の関数にも一部インラインアセンブラを使って高速化していました。DUEでは、MCUの処理速度が84MHzと高速なので、普通にC言語で実装したいと思います。例えば、以下の様な感じです。
void tft_clear (void)
{
// clear gram
int ix;
int iy;
// set rectangle
tft_write (0x50, 0);
tft_write (0x51, tft_SIZH - 1);
tft_write (0x52, 0);
tft_write (0x53, tft_SIZV - 1);
tft_write (0x20, 0);
tft_write (0x21, 0);
// clear
tft_putpix (0, tft_OPT_INIT);
for (iy=0; iy<tft_SIZV; iy++)
for (ix=0; ix<tft_SIZH; ix++)
tft_putpix (0x0000);
tft_putpix (0, tft_OPT_FLUSH);
}
「#11-1 TFTシールドに文字を描画する」で作ったプログラム(tft_print.ino)から、tft_putpix()関数を取り込んで、使っています。
前節の検討によって完成した、DUE対応版プログラムをリンクしておきます。
DUE対応版プログラム:test_tft.ino
いつもの様にオリジナルを差し替えてあります。使い方は「#10-9 QVGA TFT液晶を使う」を参照してください。
実験サンプル:【 】内はシリアルモニタからの送信データ
【rd,0】
OK: read[0x00]=0x9328
ILI9328のIDレジスタを読み出しました。IDコード0x9328が読めて、期待通りです。
【demo】
OK+ start demo
OK: demo
組み込んであったデモを実行しました。内容はUNOの時と同じですが、全画面にグラデーションを描画するところが、UNOより高速に処理できていました。
アクセス関数さえできてしまえば、後はTFTシールド(ILI9328)を制御するだけですので、一気に文字列を描画するプログラムをDUEに対応させてみました。以下にリンクしておきます。
DUE対応版プログラム:tft_print.ino
こちらも、オリジナルを差し替えてあります。使い方は「#11-1 TFTシールドに文字を描画する」を参照してください。
※ASCIIアートの元データは、焼失前の首里城。とても綺麗な建物でした……(1;_;)。
UNOで実験した時と同じテキストファイルは残っていなかったので、別なテキストファイルをTera TermからDUEのtft_print.inoに送信し、その結果を【who】コマンドで確認しました。
実験サンプル:【 】内はTera Termからの送信データ
【who】 …… 適切なテキストファイルをTera Termから送信後、実行
DBG: print 4072039[us]/17037[chr]=239[us/chr]
OK: tft_print ver.1.02
表示された結果の中の1文字当たり描画時間[μs/文字]を比較すると、UNOでは約441μs/文字だったのに対して、DUEでは239μs/文字に向上しています。バスのタイミング制約内でアクセスする都合上、UNOとDUEのクロック周波数比(16MHz:84MHz)程の劇的な向上はありませんが、それでも、UNOの描画速度を約×1.8倍に上回る結果となりました。
まずまず、です。
TFTシールドの基本的な制御はUNOで完成していましたので、DUE対応の為にはtft_read()関数とtft_write()関数という一番低レベルなインターフェイス関数のみ作り込むことが必要でした。この2つをDUEに対応できてしまえば、残りの矩形描画にせよ、文字描画にせよ、C言語の関数で終了なので超簡単でした。TFTシールド制御に関しては、これでほぼほぼ、UNOと並びましたね。おっと、まだタッチ👆センサがあったか……。次はタッチセンサかな?