Arduino(7) Arduino Pro MiniにSPI接続のカラーLCDをつないでみる

前回まで使ったArduino UNOと2.4インチTFT液晶は、簡単に動かすことができて良かったのですが、不満点もあります。

一つは、前回触れたように、このLCDを使うとArduino UNOのI/Oポートをほとんど使ってしまうので、センサなど他のデバイスを接続する余地が非常に小さいことです。

もう一つは、Arduino UNOは5V動作なので、3.3V動作のセンサ類を直接つなげられないことです。
3.3Vのデバイスをつなぐには、ブレッドボードなどにArduino UNOの線を引き出し、レベルコンバータを通してから接続する必要があります。

そこで、Arduino UNOの代わりに3.3V動作のArduino Pro Miniを使い、LCDもSPI接続のものにしてはどうかと考えました。

Arduino Pro Miniは、UNOをそのまま小さくして、動作時に不要なUSBシリアル変換やAVRプログラマなどの機能を削除したような製品です。
ただし、PCとの接続では、USB-シリアル変換器が必要になります。
また、3.3V動作製品があり、これをつかえば3.3V動作のセンサ等と直結できます。

SPIとは4つの信号線でシリアル通信を行う規格で、8本のデータバスに加えて制御信号線も必要になるパラレルインタフェースよりも、使用するI/Oポートを大幅に減らせます。
arduino07-02.jpg

というような目論見で、以下の製品を購入しました。

Arduino Pro Mini(3.3V)

arduino07-04.jpg

FTDI USBシリアル変換アダプター(5V/3.3V切り替え機能付き)

arduino07-05.jpg

aitendo TFT液晶with基板 [M-Z18SPI-2P]

arduino07-09.jpg

なお、Pro Miniはピンヘッダを自分でハンダ付けする必要があります。
ピンヘッダを持っていない場合は別途購入が必要です。
(普通のタイプのほかに、L字型も買っておくと良いと思います。)

また、aitendoの上記液晶は、フレキシブル基板をプリント基板にハンダ付けする必要があります。
ただし、このハンダ付けはピン間隔が狭いので、初心者には難しいと思います。

というわけで、今回は実質的にソフトウェアではなくハードウェアの話のみです。
ともかくニッパでベキベキとピンヘッダを切り離していきます。
切ったものが飛ばないように、切るときは指で抑えておきます。

arduino07-06.jpg

Pro Miniは14本×2列の信号線と、PCに接続するための6本×1列の信号線、さらにアナログポートのための2本×1列の信号線が2つ、合計5箇所にピンヘッダを取り付けます。

どのようなピンヘッダ(またはピンソケット)を付けるかは用途次第です。
前回使ったような、足の長いピンソケットを使えば、ブレッドボードに挿すことも、UNOのようにボードにジャンパピンを挿すことも可能になります。

私はブレッドボードを使う想定で、14本×2列のピンを下向きに付けました。
アナログポートの信号線は位置がずらされていますので、ピンヘッダを上向きに取り付けます。
また、PC接続用のピンは裏表を間違えにくいようにL字型ピンで横へ引き出しました。

arduino07-07.jpg

ちなみに、私は上のように2枚重ねのユニバーサル基板を用意して、そこにハンダ付けしたいピンヘッダを挿し、その上に基板を乗せてハンダ付けしています。
こうするとピンヘッダがグラついたり抜けたりしません。

USBシリアル変換アダプターは、特にハンダ付けは必要ありません。
今回のPro Miniは3.3V用ですので、ジャンパピンを3.3Vのほうへ付け直しておきます。

液晶基板は、フレキシブル基板と本体側のパターンを慎重に合わせて、まずカプトンテープでフレキ基板を仮止めします。
カプトンテープは耐熱性があるので、多少ハンダ付けの熱が伝わっても大丈夫です。
カプトンテープ(あるいはポリイミドテープ)もaitendoやamazonなどで入手できます。

arduino07-08.jpg

カプトンテープは褐色の透明なテープです。
分かりにくいですが、上の写真だと、基板の上にフレキをカプトンテープで貼り付けていますので、緑のレジストが一部だけオレンジ色っぽく見えています。その部分がカプトンテープです。

このフレキ基板のハンダ付けですが、ベース基板には予備ハンダをし、フレキ基板は予備ハンダなしで、ハンダ付けの際は追いハンダをそれなりの量を流し込む感じで行いました。
ベース基板のパターンと重なるあたりのフレキ基板のパターンにごく小さな穴が開いていますが、ハンダが十分な量であればそこからハンダが少し出てきます。
最初はハンダが少なすぎて、接触不良になっていました。

液晶モジュールの外部接続端子は、今回はピンソケットを取り付けて、ジャンパピンを挿せるようにしてみました。
また、この基板はバックライトのLEDに流す電流を決める抵抗を自分で取り付ける必要があります。
5V電源のときは150Ωが標準ということですが、今回はバックライトも3.3Vを使いたいので、100Ωを取り付けました。(昼間使うには若干暗いかもしれません。)

完成したら、早速接続です。

その前に、PCにUSBシリアル変換モジュールのドライバを、下記からダウンロードしてインストールする必要があります。

Virtual COM Port Drivers

無事認識されたら、まずはLチカで動作確認です。

Arduino IDEで、

ボード:”Arduino Pro or Pro Mini”
プロセッサ:”ATMega328(3.3V, 8MHz)”

を選択します。
arduino07-01.jpg

あとはUNOのときと同じようにBlinkのスケッチを開いて、コンパイルして転送すればPro Miniボード上のLEDが点滅するはずです。

次に、今回使うSPI接続LCDをArduinoから制御するためのソフトウェアですが、Adafruitから公開されている以下のライブラリを使います。

adafruit/Adafruit-ST7735-Library · GitHub

Arduino IDEにこのライブラリを追加する方法は、以前書いたのと同じです
なお、今回使った液晶モジュールはSDカードのインタフェースやタッチパネルは付いていませんので、利用できるのは描画関係の機能だけです。

ArduinoとLCD基板との接続方法はaitendoのページに書かれている通りです。
実際の配線図は以下のようになります。
arduino07-03.png

ライブラリを追加すると、スケッチの例も追加されます。
これも、今回のLCDで動作するのはgraphicstestとrotationtestだけです。
特に変更は行わなくても、上記の接続であれば動作するはずです。

Arduino(6) ブロック崩しを作ってみる(3)

前回までで、ブロック崩しのグラフィックスは大体出来上がりました。
今回はこのブロック崩しを、実際に遊べる状態にしていきます。

遊べるゲームにするには「入力装置」が必要です。
ラケットを動かすためには、回転型の「ノブ」がいいですね。
そのような入力装置には可変抵抗器(ポテンショメータ)か、ロータリーエンコーダが使えます。

また、ゲームにはやはり「サウンド」も欲しいところです。
Arduinoなどのマイコンで一番お手軽に使えるサウンド出力は圧電ブザーです。
消費電力が非常に小さいので、どんなマイコンでも利用できます。

Arduinoでは、LCDと同じく、これらのデバイスを拡張ポートに「外付け」にします。
入力装置は可変抵抗器、サウンド出力は圧電ブザーをつなぐとして、それぞれアナログ入力、デジタル出力が1つずつ必要になります。

しかし、今回使ったaitendoのLCD「UL024TF」は、下の画像にあるように、Arduino UNOの拡張ポートをほとんど使ってしまっています。
未使用のポートは、アナログ入力のできるA5、デジタル入出力のD0とD1だけです。
ただしD0とD1はシリアルポートと兼用で、PCからプログラムを書き込む時にも使われますので、デバイスを接続するとプログラムが書き込めなくなってしまいます。

UL024TF-468-pinout.png

どうするかですが、SDカードからの出力を読み取るD12をサウンド用に使います。
ブロック崩しではSDカードは使用しませんので、問題は出ません。
ただし、SDカードをカードホルダに挿したままでプログラムを動かさないほうが良いでしょう。

というわけでA5に可変抵抗、D12に圧電ブザーをつなげば入力とサウンドが実現できそうです。

接続イメージは下図のようになります。
これはとりあえずの回路ですが、このままだとアナログ入力のA5を間違ってデジタル出力モードに設定して、可変抵抗を介してGNDへつながっている状態でHigh状態にすると壊れてしまいますので、A5と可変抵抗の間(図の緑のラインの途中)に保護用に1KΩの抵抗を追加するとより安心です。

arduino06-00.png

ちなみにこのような図は、Fritzingというツールで描くのが定番になっているようです。

Fritzing Download

今回初めて使ってみましたが、部品ライブラリがArduino向けに充実していますし、使いやすいソフトでした。
メニューもある程度は日本語化されています。

さて、上図のようにArduinoに可変抵抗器や圧電ブザーを直接配線できればいいのですが、実際には液晶シールドがArduino UNOにかぶさって、拡張ポートを完全にふさいでしまっています。
そのため、他のパーツを接続することができません。

対処の常套手段としては、2階建てになっている基板の間に、もう1枚基板を入れて3階建てにします。
間に挟む基板は、試作用基板「プロトタイプシールド」や、ピンの信号を横方向に引き出せる「ウイングシールド」ないし「スクリューシールド」を使います。
これはTFT LCDのオリジナルを開発したAdafruitでも推奨されている方法です。

しかし、今回は使うピンが少なく、ちょっと試してみるには大げさですので、手元にあったパーツを使ってもう少し簡易な方法を考えてみました。

用意するものは、Arduinoのピン配列に合わせた「ピンソケット」と、ICの足にひっかけて信号を取り出す「ICクリップ」です。

ピンソケットは、Arduinoの基板の拡張ポートに使われているものと同じで、ピンを差し込むことができるソケットです。
6ピンが1つ、8ピンが2つ、10ピンが1つ必要です。
ピンの部分が10mm以上のものでなければなりません。
私が使ったのは、aitendoでArduinoシールド自作用に販売されている「ピンソケットセット」です。

arduino06-01.jpg

ICクリップは、クリップのお尻を押すとカギ状の端子が出てきて、ICのピンに引っ掛けることができます。
バネが入っていて、手を離すとカギが引っ込み、ICのピンに固定されます。

arduino06-02.jpg

arduino06-03.jpg

購入する場合は、コード付きのICクリップが手間がなくて良いです。
「ICテストリード」「プローブ付きジャンパワイヤ」などの名称で販売されています。
両端ともICクリップになっているものが使いやすいでしょう。

では使用方法です。
ショートによる故障を避けるため、USBケーブルはArduinoから外してください。

まず、ピンソケットをArduinoの拡張ポートに取り付けます。
このとき、ピンはソケットに完全には入らず、少し余ります。
この余った部分にICクリップを引っ掛けて信号を取り出そうというわけです。

arduino06-04.jpg

LCDは、取り付けたピンソケットにそのまま取り付けることができます。

arduino06-05.jpg

可変抵抗器は、手元にあった1KΩのものを使いました。
1KΩ~100KΩくらいであれば、どんな値のものでも構いませんが、「Bカーブ」のものを使いましょう。
記載がない場合は、型番に「1KB」とか「B1K」とか、「B」の文字が含まれているものを選びます。

余談ですが「Bカーブ」は角度と抵抗値が比例しますが、「Aカーブ」は角度に対して抵抗値が「等比級数的に」変化します。
例えば20度回転して抵抗値が半分になったら、次の20度では抵抗値がさらに半分になります。
音量調節や明るさ調節などではAカーブを使うほうが人間の感覚にマッチします。

可変抵抗器は、Arduino UNOのA5、5V、GNDの各ピンに以下の写真のように接続します。

arduino06-06.jpg
arduino06-07.jpg

圧電ブザーは、ビニールコードのあるタイプと、基板実装用に硬い裸のリード線が出ているタイプがありますが、この手の実験用にはビニールコードつきのほうが扱いやすいです。
基板実装用タイプはブレッドボードで使うには便利です。

圧電ブザーは極性があります。
プラスのほうを12番ピン、マイナスのほうをGNDに接続します。

arduino06-08.jpg

以上で回路が組みあがりました。
プログラムのほうは、アナログポートからの入力はanalogRead命令、サウンド出力はtone命令が使えますので、ごく簡単な追加・修正で済みます。

まず使用するポートを定義しておきます。

#define GAMEPORT A5
#define SOUNDPORT 12

ラケットの移動の部分は以下のようになります。

  RacketDraw(racket,tft,BLACK);
racket=map(analogRead(GAMEPORT), 0, 1023, -5, SCRNWIDTH-RACKETSIZE+5);
racket=constrain(racket, 0, SCRNWIDTH-RACKETSIZE);
RacketDraw(racket,tft,WHITE);

赤い部分が修正箇所です。
前回は自前で定義したMAX・MINというマクロを使っていましたが、Arduinoの標準のconstrain関数を使うように変更しました。
なお、難易度調整のため、loopの中のdelayの引数は前回の倍の32にしています。

次にサウンドですが、音の高さの定義はIDEのスケッチの例題「toneMelody」(「02.Digital」に含まれています)の中の”pitches.h”で定義されている数値を引用しました。

#define NOTE_C3  131
#define NOTE_CS3 139
#define NOTE_D3  147
#define NOTE_DS3 156
#define NOTE_E3  165
#define NOTE_F3  175
#define NOTE_FS3 185
#define NOTE_G3  196
#define NOTE_GS3 208
#define NOTE_A3  220
#define NOTE_AS3 233
#define NOTE_B3  247
#define NOTE_C4  262

実際に使ったのはC3(ラケットに当たったとき)とC4(ブロックに当たったとき)だけです。

ラケットに当たったときの音は、loop関数内の以下の部分で出しています。

  if (ball.y == RACKETLINE) {
if (ball.x >= racket && ball.x < racket+RACKETSIZE) {
ball.vy = -ball.vy;
tone(SOUNDPORT,NOTE_C3,80);
}
}

ブロックに当たったときの音も、loop関数内で出しています。
まず、ブロックに当たったかどうかをbooleanで返すように、game_ongoing関数を修正しました。

boolean game_ongoing()
{
int8_t block;
boolean blockErased;

blockErased = false;
do {
block=BlocksHit(ball.x,ball.y,&ball.vx,&ball.vy);
if (block >= 0) {
exist[block] = false;
BlocksEraseOne(block);
blockErased = true;
}
} while (block > 0);
return blockErased;
}

その上で、この戻り値がtrueだったら音を出すようにしています。

  switch (gameStatus) {
case GAME_RESTART:
if (game_restart()) {
gameStatus = GAME_ONGOING;
}
break;
case GAME_ONGOING:
if (game_ongoing())
tone(SOUNDPORT,NOTE_C4,40);
if (BlocksLeft() == 0) {
gameStatus=GAME_RESTART;
}
break;
}

これでブロック崩しは完成です。
ゲームとしては、いろいろな拡張が考えられると思います。

たとえば

・ミスする回数に制限をつける
・ブロックを消した数に応じてだんだんスピードを早くする
・ブロックの並び方をステージによって変化させる
・ボールの跳ね返る方向を、ラケットのどの位置に当たったかで変化させる
・ボールやラケットを1ピクセル単位で動かす

などができそうです。

最後に、スケッチファイルと動作の様子のビデオを載せておきます。

Arduino(5) ブロック崩しを作ってみる(2)

前回に続き、今回はブロック崩しのメインとなる、ブロックとボールの衝突処理を実装します。

まずブロックの画面上での配置です。

画面は30×40に分割します。これは前回書いたとおりです。
描画のとき以外は、全てこの30×40の座標系の上で処理を行います。

ブロックのサイズは3×2とします。
これを横方向に10個、縦方向に5個並べます。
ブロックの総数は50個となります。

各ブロックは、下図のように番号で表します。

arduino04-1.jpg

ブロックが存在している・いないを表すために、論理型の配列existを定義します。
例えばexist[k]==trueなら、k番のブロックは消されずに残っています。

#define BLOCKWIDTH 3
#define BLOCKHEIGHT 2
#define BLOCKTOP 3
#define BLOCKBOTTOM 12
#define BLOCKLINES ((BLOCKBOTTOM-BLOCKTOP+1)/BLOCKHEIGHT)
#define NBLOCKS (BLOCKLINES*SCRNWIDTH/BLOCKWIDTH)

boolean exist[NBLOCKS];

関数BlocksInit()は、この配列の全要素をtrueで初期化します。
関数BlocksLeft()は、全要素中のtrueの個数を返します。
BlocksLeftの結果が0であれば、ステージをクリアしたことになります。

void BlocksInit()
{
int8_t i;

for(i=0; i<NBLOCKS; i++) {
exist[i] = true;
}
}

uint8_t BlocksLeft()
{
int8_t i;
int8_t leftBlocks;

leftBlocks = 0;
for (i=0; i<NBLOCKS; i++){
if (exist[i])
leftBlocks++;
}
return leftBlocks;
}

次に、ブロックの描画と消去です。

使用しているLCDは240×320ピクセルですので、1×1の領域は8×8ピクセルとなります。
プログラム内での座標を描画用座標を変換するマクロTOSCRNを定義します。
また、ブロックの各行の色を配列colorsに格納しておきます。

#define TOSCRN(v) (v<<3)
uint16_t colors[BLOCKLINES]={RED,GREEN,BLUE,YELLOW,MAGENTA};

ブロック崩しでは最初に全てのブロックを表示し、ゲーム開始後はボールが当たったブロックを一つずつ消していきます。
関数BlocksDrawAllは全てのブロックを描画します。
関数BlocksEraseOne(n)は、n番のブロックを消去します。

void BlocksDrawAll(){
int8_t x,y;
int8_t w,h;
int8_t c;
int8_t i;

w=TOSCRN(BLOCKWIDTH);
h=TOSCRN(BLOCKHEIGHT);
c=0;
i=0;
for(y=BLOCKTOP; y<=BLOCKBOTTOM; y += BLOCKHEIGHT) {
for(x=0; x<SCRNWIDTH; x += BLOCKWIDTH) {
if (exist[i++]) {
tft.fillRect(TOSCRN(x), TOSCRN(y), w, h, colors[c]);
}
}
c++;
}
}

void BlocksEraseOne(int8_t block) {
int8_t x,y;
int8_t w,h;
int8_t rows;

w=TOSCRN(BLOCKWIDTH);
h=TOSCRN(BLOCKHEIGHT);
rows = SCRNWIDTH/BLOCKWIDTH;
x = (block % rows) * BLOCKWIDTH;
y = (block / rows) * BLOCKHEIGHT + BLOCKTOP;
tft.fillRect(TOSCRN(x), TOSCRN(y), w, h, BLACK);
}

そしていよいよ、ブロックとボールの衝突の処理です。

まず、ブロックとボールが衝突したかどうかの判定は、比較的簡単です。
ボールが移動しようとしている位置にブロックが存在していれば、そのブロックと衝突したと判定できます。

一方、衝突したあと、ボールを跳ね返らせる処理は、やや複雑です。
ぶつかる瞬間のボールとブロックの位置関係により、下図のような場合があることが判ります。
それぞれに応じた跳ね返り方向を設定することが必要です。

arduino04-2.jpg

今回はボールの跳ね返りを以下のように決定しています。

ボールの移動方向を(vx,vy)とすると、

(1)ボールを(vx,0)だけ移動するとブロックに衝突するならば、水平方向に跳ね返る。
(2)ボールを(0,vy)だけ移動するとブロックに衝突するならば、垂直方向に跳ね返る。
(3)ボールを(vx,vy)だけ移動するとブロックに衝突するならば、水平かつ垂直方向に跳ね返る。
(4)以上をボールが跳ね返らなくなるまで繰り返す。

(1)(2)は同時に起こる場合がある(上図の右上のパターン)ので、(4)の処理が必要になります。

関数BlocksFindは、与えられた座標のブロック番号を返します。
ブロックが存在していても存在していなくても動作は同じです。
関数BlocksHitは、ボールの座標と移動ベクトルから、ブロックとの衝突有無を判断します。
衝突していなければ、-1を返します。
衝突していた場合は、衝突したブロックの番号を返すと共に、ボールの跳ね返る方向へ移動ベクトルの値を書き換えます。

int8_t BlocksFind(uint8_t x, uint8_t y)
{
if (x > SCRNWIDTH) {
return -1;
}
if ((y < BLOCKTOP)||(y > BLOCKBOTTOM)) {
return -1;
}
uint8_t rows=SCRNWIDTH/BLOCKWIDTH;
return (y-BLOCKTOP)/BLOCKHEIGHT*rows + x/BLOCKWIDTH;
}

int8_t BlocksHit(uint8_t x, uint8_t y, int8_t *vx, int8_t *vy)
{
int8_t block;

block = BlocksFind(x + *vx, y);
if ((block >= 0) && exist[block]) {
*vx = -*vx;
return block;
}
block = BlocksFind(x, y + *vy);
if ((block >= 0) && exist[block]) {
*vy = -*vy;
return block;
}
block = BlocksFind(x + *vx, y + *vy);
if ((block >= 0) && exist[block]) {
*vx = -*vx;
*vy = -*vy;
return block;
}
return -1;
}

最後に、ゲームの状態遷移です。

ブロックを全部消したら、ゲームを再開する処理が必要になります。
このため、ゲームには

●ゲーム中
●ステージクリア後のゲーム再開待ち

の二つの状態を持たせます。

#define GAME_ONGOING 1
#define GAME_RESTART 0

uint8_t gameStatus;

また、ゲーム再開の処理では、プレイヤーを混乱させないようボールの座標はリセットしないことにします。
すると、ブロックを描画したい領域からボールが出ていくまでは、再開処理を行えなくなります。
ゲーム再開待ち状態では、ボールがブロックの描画領域よりも画面下方にあり、かつ移動方向がも下向きとなったときにブロックの描画を行います。

関数game_restartがこの処理を行います。
この関数は、ブロックを描画できたときにtrue、まだボールが出て行くのを待っている場合はfalseを返します。
関数game_ongoingはボール とブロックの衝突判定を行い、衝突したらそのブロックを消去します。

boolean game_restart()
{
if ((ball.y > BLOCKBOTTOM) && (ball.vy >0)) {
BlocksInit();
BlocksDrawAll();
return true;
} else {
return false;
}
}

void game_ongoing()
{
int8_t block;

do {
block=BlocksHit(ball.x,ball.y,&ball.vx,&ball.vy);
if (block >= 0) {
exist[block] = false;
BlocksEraseOne(block);
}
} while (block > 0);
}

そして、loop()の中で、状態遷移の管理を行います。

void loop()
{
switch (gameStatus) {
case GAME_RESTART:
if (game_restart()) {
gameStatus = GAME_ONGOING;
}
break;
case GAME_ONGOING:
game_ongoing();
if (BlocksLeft() == 0) {
gameStatus=GAME_RESTART;
}
break;
}
(以下は前回と同じなので略)

最後にスケッチファイルと、動作の様子のビデオを置いておきます。