1カ月集中講座
Intelの開発ボード「Galileo 2」の楽しみ方 第4回
~SPI接続のカラーLCDを使う
(2014/10/31 06:00)
本講座の最終回としてGalileo Gen.2のSPI(Serial Peripheral Interface)を使ってカラーLCDパネルを駆動する例を紹介してみる。結論から言うとあまり芳しい結果ではなかったのだが、カラーLCDの駆動は出来た。だが、いろいろと問題も多いようだ。
Galileo Gen.2のSPI
ご存知のとおり、SPIはI2Cと並んでマイコンの高速シリアルインターフェイスとして広く利用されている。お手軽だが一般にI2Cより高速なデータ転送ができるので、やや高速さが必要とされる用途で利用されることが多いようだ。
Galileo Gen.2ももちろんSPIをサポートする。Galileo Gen.2のSPIサポートはLinuxのSPIドライバを介しているのが特徴と言えるだろう。Galileoのコンソールを開き、/dev/以下を見ると/dev/spidev1.0というデバイスノードが見える。
# ls /dev/spi* /dev/spidev1.0
これは、SPIバス1、SS=0(SSはSlave Select)のデバイスノードで、これがユーザーに開放されているSPIバスだ。
Galileo版ArduinoのSPIライブラリ本体となる「hardware/arduino/x86/libraries/SPI/SPI.cpp」を見ると、このデバイスノードをオープンしてioctlコールでSPIバスを駆動していることが分かる。
ただし、Galileo版Arduinoでも、SSは自分でHigh、Lowを切り替える必要がある点に注意が必要だ(筆者はこれで少しハマってしまった)。LinuxのSPIドライバは選択したSSをドライバ側で駆動してくれるのだが、Galileo版ArduinoのコードではSSは駆動してくれないようだ。おそらく標準的なArduinoとの互換性を持たせるために、こうしているのだろう。
SPIのピンアサインは次のようになっている。
SPIバス | ピン番号 |
---|---|
SS0 | Digital 10 |
MOSI | Digital 11 |
MISO | Digital 12 |
CLK | Digital 13 |
SPIはI/O電圧が3.3Vのものもあるが、Galileo Gen.2の初期設定ではI/O電圧が5Vになっている点に注意が必要だろう。3.3VのSPIデバイスを接続する際にはオンボードのIOREFジャンパを3.3V側に切り替えておかないと壊す恐れがある。本稿で使用するLCDも3.3VのI/O電圧なので、実際に試してみようという方は注意して欲しい。
SPI接続のカラーLCD
今回、筆者が試したのは秋葉原にあるパーツショップaitendoが販売している「M-TM022-SPI」というLCDパネルである。筆者は以前にこのLCDパネルを「Raspberry Pi」に接続して旨く駆動できたという実績があるので、Galileoでもさほど苦労せずに駆動できるだろうという判断で利用してみた。
なお、執筆時点でaitendoでは売り切れになってしまっている模様だが、このLCDパネルはわりとポピュラーなもので、海外を視野に入れれば入手はさほど難しくない。例えば、筆者は実物を確かめていないので保証はできないが、通販サイトdealextremeで市販されているこのLCDパネルも同じものではないかと思う。用心したい方はaitendoの再入荷を待って手に入れるのがいいだろう。ただ、dealextremeは7.59ドルに送料無料と、日本円換算でも1,000円以下で買えるのが魅力だ。
パネルサイズは2.2型、解像度は240×320ドットで、色深度は18bppと16bppをサポートする。SPIを使うので、ごく少ない結線でGalileoなどに接続できるというのが利点だと思う。
このLCDパネルは裏面に9ピンのヘッダが付いている。前述のSPIの4本に加えて電源、GND、さらにバックライトLED用の電源などがある。また、このLCDはLCDコントローラに対するコマンドとデータを区別する入力ピン(D/C)やリセット端子を持っている。これらをGalileoと接続しなければならない。
筆者は今回、次のように接続して利用してみた。
LCD側 | Galileo側 |
---|---|
MISO | Digital 12 |
LED | 50Ω程度の抵抗を介して3.3VのVccへ |
SCK | Digital 13 |
MOSI | Digital 11 |
D/C | Digital 6 |
RESET | Digital 5 |
CS | Digital 10 |
GND | GND |
VCC | 3.3V |
LCD以外の部品が必要なのはLEDのバックパネル電源のみで、おおよそ50Ω程度の抵抗を入れて3.3Vの電源と繋いでおけばいいだろう。抵抗値でバックライトの明るさが変わるので、好みに応じた値を使って欲しい。
LCDを制御するスケッチ
このLCDの制御方法はaitendoのホームページに掲載されている、「LCDコントローラILI9340Cのマニュアル」(PDF)と、組み込みマイコン8051向けに記述されたサンプルプログラムが参考になる。特に後者は低レベルI/OでSPIを操作しているもの以外の関数が、ほぼそのまま利用できたりするので、便利に使えるだろうと思う。
この2つを参考に、以前にRaspberry Piで記述したC言語のソースコードをGalileo版Arduinoに移植したのが次のリストだ。
【リスト】カラーLCDの制御サンプル
#include <SPI.h> #include <stdlib.h> #include <trace.h> #include <SD.h> #define PN_LCD_D_C 6 #define PN_LCD_RESET 5 #define PN_SS_PIN 10 #define LCD_D_C GPIO_FAST_IO6 #define LCD_RESET GPIO_FAST_IO5 #define SS_PIN GPIO_FAST_IO10 #define lcd_color(r,g,b) (unsigned short)( (((unsigned short)r * 31 / 255) << 11) | (((unsigned short)g * 63 / 255) << 5) | ((unsigned short)b * 31 / 255) ) #define LCD_WIDTH 240 #define LCD_HEIGHT 320 #define MY_TRACE_PREFIX "Color_LCD_Test" /* LCDコマンド書き込み */ byte lcd_write_cmd(byte cmd) { byte ret; fastGpioDigitalWrite(LCD_D_C,LOW); // Command fastGpioDigitalWrite(SS_PIN, LOW ); ret = SPI.transfer(cmd); fastGpioDigitalWrite(SS_PIN, HIGH ); fastGpioDigitalWrite(LCD_D_C,HIGH); // Data return ret; } /* LCDデータ書き込み */ /* 8bit */ byte lcd_write_data(byte data){ byte ret; fastGpioDigitalWrite(LCD_D_C, HIGH); // Data fastGpioDigitalWrite(SS_PIN, LOW ); ret = SPI.transfer(data); fastGpioDigitalWrite(SS_PIN, HIGH ); return ret; } /* 16bit */ void lcd_write_data16( unsigned short data ) { lcd_write_data( (data>>8) & 0x00FF ); lcd_write_data( data & 0x00FF ); } /* LCDの初期化~aitendoサンプルプログラムより */ void lcd_init(void) { lcd_write_cmd(0xCB); lcd_write_data(0x39); lcd_write_data(0x2C); lcd_write_data(0x00); lcd_write_data(0x34); lcd_write_data(0x02); lcd_write_cmd(0xCF); lcd_write_data(0x00); lcd_write_data(0xC1); lcd_write_data(0x30); lcd_write_cmd(0xE8); lcd_write_data(0x85); lcd_write_data(0x00); lcd_write_data(0x78); lcd_write_cmd(0xEA); lcd_write_data(0x00); lcd_write_data(0x00); lcd_write_cmd(0xED); lcd_write_data(0x64); lcd_write_data(0x03); lcd_write_data(0x12); lcd_write_data(0x81); lcd_write_cmd(0xF7); lcd_write_data(0x20); lcd_write_cmd(0xC0); lcd_write_data(0x23); lcd_write_cmd(0xC1); lcd_write_data(0x10); lcd_write_cmd(0xC5); lcd_write_data(0x3e); lcd_write_data(0x28); lcd_write_cmd(0xC7); lcd_write_data(0x86); lcd_write_cmd(0x36); lcd_write_data(0x48); lcd_write_cmd(0x3A); lcd_write_data(0x55); lcd_write_cmd(0xB1); lcd_write_data(0x00); lcd_write_data(0x18); lcd_write_cmd(0xB6); lcd_write_data(0x08); lcd_write_data(0x82); lcd_write_data(0x27); lcd_write_cmd(0xF2); lcd_write_data(0x00); lcd_write_cmd(0x26); lcd_write_data(0x01); lcd_write_cmd(0xE0); lcd_write_data(0x0F); lcd_write_data(0x31); lcd_write_data(0x2B); lcd_write_data(0x0C); lcd_write_data(0x0E); lcd_write_data(0x08); lcd_write_data(0x4E); lcd_write_data(0xF1); lcd_write_data(0x37); lcd_write_data(0x07); lcd_write_data(0x10); lcd_write_data(0x03); lcd_write_data(0x0E); lcd_write_data(0x09); lcd_write_data(0x00); lcd_write_cmd(0xE1); lcd_write_data(0x00); lcd_write_data(0x0E); lcd_write_data(0x14); lcd_write_data(0x03); lcd_write_data(0x11); lcd_write_data(0x07); lcd_write_data(0x31); lcd_write_data(0xC1); lcd_write_data(0x48); lcd_write_data(0x08); lcd_write_data(0x0F); lcd_write_data(0x0C); lcd_write_data(0x31); lcd_write_data(0x36); lcd_write_data(0x0F); lcd_write_cmd(0x11); // Exit Sleep delay(60); // delay > 60ms lcd_write_cmd(0x29); // Display on delay(60); } void lcd_set_pixel( unsigned int x, unsigned int y, unsigned short color) { lcd_write_cmd(0x2A); // set column adress = x lcd_write_data16(x); lcd_write_data16(x); lcd_write_cmd(0x2B); // set page address = y lcd_write_data16(y); lcd_write_data16(y); lcd_write_cmd(0x2C); // Memory Write lcd_write_data16(color); } void lcd_write_rect( unsigned int sx, unsigned int sy, unsigned int ex, unsigned int ey, unsigned short *buf ) { byte *rxBuf = (byte *)malloc((ex-sx+1) * (ey-sy+1) * sizeof(unsigned short)); if( rxBuf == NULL ) { trace_error("not enough memory\n"); } lcd_write_cmd(0x2A); lcd_write_data16(sx); // set start column address lcd_write_data16(ex); // set end column address lcd_write_cmd(0x2B); lcd_write_data16(sy); // set start page address lcd_write_data16(ey); // set end page address lcd_write_cmd(0x2C); // Memory Write fastGpioDigitalWrite(SS_PIN, LOW); SPI.transferBuffer( (byte *)buf, rxBuf, (ex-sx+1) * (ey-sy+1) * sizeof(unsigned short)); fastGpioDigitalWrite(SS_PIN, HIGH); free(rxBuf); } void setup() { int x,y; unsigned short *clearBuffer; SPI.begin(); SPI.setBitOrder(MSBFIRST); // MSB First SPI.setDataMode(SPI_MODE0); // CPOL=0, CPHA=0 SPI.setClockDivider(SPI_CLOCK_DIV8);// Clock Divider=8 pinMode(PN_LCD_D_C, OUTPUT); pinMode(PN_LCD_RESET, OUTPUT); pinMode(PN_SS_PIN, OUTPUT); // SS -> High fastGpioDigitalWrite(SS_PIN, HIGH); // LCD起動リセット fastGpioDigitalWrite(LCD_RESET, LOW); delayMicroseconds(20); // reset pulse > 10us fastGpioDigitalWrite(LCD_RESET, HIGH); delayMicroseconds(20); // delay > 20ms // LCD初期化 lcd_init(); // LCD Clear clearBuffer = (unsigned short *)calloc(LCD_WIDTH, sizeof(unsigned short) ); for( y = 0; y < LCD_HEIGHT; y++ ) { lcd_write_rect( 0, y, LCD_WIDTH-1, y,clearBuffer); } free(clearBuffer); /* ここに追加あり */ } void loop() { }
リストから説明しておくと、このスケッチにはLCDを制御する次の3つの関数がある。
・LCDの初期化
「lcd_init()」が初期化を行なう関数で、内容は先にリンクを示した8051用のサンプルにある「lcd_initial()」とほぼ同じだ。初期化は結構、手間がかかる部分なのであまり深く考えずにサンプルのそれを使ってしまっている。
・ドットを打つ
「lcd_set_pixel()」という関数で指定した座標に、指定したカラーでドットを打つことができる。カラー値は16bitで「lcd_color()」というマクロでRGBから生成できるようにしている。
注意点として、まずこのLCDパネルはX方向が240ドット、Y方向が320ドットということが挙げられる。要は縦長のLCDパネルで、一般的な横置きのLCDパネルではないのだ。おそらく、このLCDパネルは安価なMP3プレーヤーなどのために製造された製品と思われ、縦位置が標準的な使い方なのだろう。
そしてもう1つ、lcd_set_pixel()は極めて遅い。どうやらGalileoのSPIドライバはあまり高速ではないことが原因のようで、この関数を使ってまとめてドットを打つのはあまりお勧めできない。まとまった描画は次の矩形の描画関数を使った方がいいだろう。
・矩形にデータを描画する
「lcd_write_rect()」は指定した始点と終点の座標の矩形に指定したバッファのデータを書き込む。バッファの形式は16bitカラーの配列だ。
この関数ではSPIライブラリの「transferBuffer()」というメソッドを利用している。これは標準的なArduinoのSPIライブラリにはないメソッドで、デバイスにバッファの内容をまとめて送受信することができるものだ。
Galileo版ArduinoのSPIライブラリの本体のソースを見るとioctlコールで全二重のtransferを実行していることが分かるが、注意が必要なのは送受信できるサイズに結構きつい制限があるらしい点だ。Raspberry PIなどほかのプラットフォームではかなり大きなデータを送受信できるが、Galileoはそうではないらしい。
LCD全域240×320ドットの矩形を一気に書き込めると描画が高速で都合がいいのだが、サイズ制限でできなかった。
以上の3つの関数があれば、カラーLCDを制御できるだろうと思う。先のスケッチでは単に初期化してLCDを0クリアしているだけだが、実行するとLCDが駆動できることは確認できるはずだ。
ちなみに、このスケッチの作成にあたっていくつか苦労したところがあった。
もっとも苦労したのは「Arduinoに定義されている変数型“word”のサイズが4byteだった」という点だ。Arduinoのリファレンスにはwordが符号なし16bit整数と記されているので当然2byteと思って使っていたらどうもおかしい。どう考えても妙なのでsizeof()で調べたところGalileo版Arduinoでは4(byte)と返ってきた。
標準的なArduinoがどうなのか筆者は知らないため、Galileo版Arduinoが変だとまでは言い切れない。また、変数型のbyte長はプラットフォーム依存が許されるところで、Arduinoのマニュアルにも2byteとは明記されていないようだ。
だが、わざわざwordという変数型を定義するのだから2byteなんだろうと考えてしまった……。いずれにしても「Galileo版Arduinoのword型のサイズは4byte」なので読者も十分に注意して欲しい。
GalileoにImageMagickをインストール
では、LCDに画像を表示してみよう。ここではSDカード上に置いた画像ファイルをLCDに表示するということを考える。
ご存知のように画像形式にはJPEGやPNGなどさまざまなものがある。主要な形式に全て対応しようとすると大変だが、そこはGalileoがLinuxで動く特徴を活かして、多様な画像形式に対応させてみよう。
Linux上では「ImageMagick」という画像の加工・変換ツールが良く利用されている。これをGalileo上にインストールし、Linux上で画像ファイルを扱いやすいPPM形式のファイルに変換し、スケッチではPPM形式のファイルをロードするという方法を採る。
そのために、まずGalileoにImageMagickをインストールしなければならない。ImageMagickは前回のYocotoのビルドツリーに含まれるのだが、そちらではうまくビルドできなかったのでソースをダウンロードして再度ビルドを行なうことにする。
以下の作業はホストPC上の作業だ。前回のビルドに使用したホストPC上で、次のようにGalileo用のmicroSDカードのルートイメージファイルをマウントする。
$ cd ~/Galileo2/meta-clanton_v1.0.1/yocto_build/tmp/deploy/images [Enter] $ sudo mount -o loop ./image-full-galileo-clanton.ext3 /mnt [Enter]
これで/mntにGalileoのSDカードイメージがマウントできる。続いて、/mnt/usr/src以下にImageMagckのソースコードをダウンロードしよう。
$ sudo -s [Enter] # cd /mnt/usr/src [Enter] # wget http://www.imagemagick.org/download/ImageMagick.tar.gz [Enter]
これで/mnt/usr/src/以下にソースコードがダウンロードできた。/mnt/にchrootしてホストPC上で仮想的なGalileo環境を起動する。
# chroot /mnt [Enter]
するとプロンプトが「bash-4.2#」に変わり、ホストPC上でGalileoの環境が再現できる。/usr/srcにカレントディレクトリを移し、先のソースコードを展開しよう。
# cd /usr/src [Enter] # tar xvzf ImageMagick.tar.gz [Enter]
ImageMagick-6.8.9-9/というディレクトリにソースツリーが展開される。カレントを移し、次のようにconfigureを実行する。
# cd ImageMagick-6.8.9-9 [Enter] # ./configure --target=i586 --host=i586-poky-linux-uclibc [Enter]
この作業で注意が必要なのは、上記のように--targetと--hostを指定しないとホストPC用のバイナリが生成されてしまう点だ。GalileoのCPUはPentiumクラス(Linuxでいうi586クラス)で、Pentium Pro以降に新設された命令セットの多くをサポートしない。現在のPCは当然のようにPentium Pro以降のCPUを積んでいるので、Galileoでは動作しないバイナリが生成されてしまう。
configureの実行が終わったら、念のために次のようなCコンパイラのフラグを設定してmakeを実行する。
# make CFLAGS="-march=i586 -mtune=i586" [Enter]
makeが終わったらインストールしよう。
# make install [Enter]
これでGalileoのSDカードイメージにImageMagickがインストールできた。exitコマンドを実行してchrootから抜けよう。
# exit [Enter] # exit [Enter] $ cd [Enter] $ sudo umount /mnt [Enter]
あとは前回を参考にmicroSDカードに「image-full-galileo-clanton.ext3」をコピーすればいい。これでImageMagickが入ったGalileo用microSDカードが出来上がる。
LCDに画像データを表示してみる
ImageMagickをインストールしたmicroSDカードでGalileoを起動させ、先のスケッチの「追加あり」とした部分に次のようなコードを追加してみた。
if( SD.exists("test.jpg") ) { FILE *fp; system("/usr/local/bin/convert -resize 320x240 /media/mmcblk0p1/test.jpg /media/mmcblk0p1/temp.ppm"); system("/usr/bin/tail -c 230400 /media/mmcblk0p1/temp.ppm >/media/mmcblk0p1/temp.bin"); fp = fopen( "/media/mmcblk0p1/temp.bin", "r" ); if( fp != NULL ) { for( x = 0; x < LCD_WIDTH; x++ ) { struct RGB { byte b; byte g; byte r; }; trace_error("sizeof rgb = %d\n", sizeof(struct RGB) ); unsigned short lcdBuff[LCD_HEIGHT]; struct RGB lineBuff[LCD_HEIGHT]; fread(lineBuff, 3, LCD_HEIGHT, fp ); for( y = LCD_HEIGHT-1; y > -1 ; y-- ) { lcdBuff[LCD_HEIGHT - (y+1)] = lcd_color(lineBuff[y].r, lineBuff[y].g, lineBuff[y].b); } lcd_write_rect(x, 0, x , 319, lcdBuff); } fclose(fp); } }
やっていることは単純で、microSDカード上にtest.jpgという画像ファイルがあるならImageMagickに含まれる画像変換コマンドconvertを使ってPPM形式に変換し、PPM形式のRAWイメージ部分を切り出してLCDに表示させている。
例のように、どうも色がおかしいので何かミスしている気がするが、残念ながらまだ原因は突き止められていない。時間の都合で修正できないまま掲載するが、基本的な部分は完成しているので、参考にしていただくことはできるだろうと思う。
以上、4回に分けてGalileo Gen.2を紹介してきた。普通のArduinoとはまた違った開発ができることが、この4回の連載から分かっていただければ幸いだ。Arduino+α的なプラットフォームとしてGalileoをぜひ楽しんでみて欲しい。