さくらのジャンク箱ロゴ

Sakura87がほぼ月刊でお届けするPCや電子工作と写真のブログ @なんと20周年

Raspberry Pi 2でSPI接続のグラフィック液晶の制御

この記事は9年ほど前に投稿されました。内容が古くなっている可能性がありますので更新日時にご注意ください。

IMG_4748_20150712

前回グラフィック液晶の制御を実施しました。この液晶はパラレル接続で全部で20本のピンヘッダがあり、データ転送及び制御で合計14本のGPIOポートを専有していました。
Raspberry Pi 2(とPi B+)はGPIOポートが26本あるのでそれなりに余裕がありますが、初代モデル(Pi ModelA/B)はGPIOポートが17本しかないので3本しか残りませんね。

そこで今回はSPI接続のグラフィック液晶を使用してみたいと思います。

関連記事リンク

用意するもの

必要なものは配線類を除けば SPI接続のグラフィック液晶ディスプレイのみで特別必要な物はありません。今回は秋月電子で買える超小型グラフィック液晶 AQM1248Aを使用しました。
この液晶はSPI接続+制御線2本で制御でき、3.3Vで消費電力1mAという仕様。
バックライトはついていません。

ちなみに…(2018年1月21日追加

どうやらバックライトの搭載されたタイプが発売されたみたいです。見た感じバックライトが追加されただけのようですので、こちらを使ってみるのも良いでしょう。

SPI通信とは

モトローラ社が提唱した接続方式で送受信2本、クロック、スレーブ選択信号にGNDの計5本でデバイス間の通信を可能としたシンプルなシリアル通信方式である。

古くから使われている形式でデファクトスタンダードとなっているものです。
この規格は決まりがいい加減で汎用性が高いのが特徴で、低速通信から数十Mbpsの高速通信まで対応したインターフェイスです。スレーブ側になるデバイスは数Mbpsの接続に対応した製品が多いようです。

また、一般的なパラレルとシリアルの比較になりますが、SPIとパラレル接続の比較として

  • 信号線が少ない
    データ線はパラレル通信は8bitバスの場合は8本16bitバスの場合は16本もあるのに対し、シリアル通信ではシリアルなので何bitのバスであろうと信号線は1本です。
  • 通信速度が速い
    低速通信の場合は8本のデータ線を使って通信をするパラレル接続のほうが高速ですが、高速通信をしようとすると全ポートにデータが到着するのを待ってそれを処理する必要が出てくるため、シリアル通信の方が数Gbps以上の高速化が容易になるようです。
    この程度のグラフィック液晶を制御する場合においてはそこまで気にする必要もないと思います。実際にインターフェイスの後はパラレル接続になっているものが多いみたいなので。

という2つが大きな特徴でしょう。

というわけで信号線が少なくて済むSPIインターフェイスの液晶を今回は制御したいと思います。

ちなみにもっと本数が少なくて済むI2Cという通信規格も存在します。こちらは転送速度が1Mbps程度のあまり高速な通信速度を必要としない機器向けで、その代わり信号線は3本(クロックとデータ送受信にGND)で済むという規格です。Raspberry PiはI2Cにも対応してますのでこちらもそのうち使ってみたいと思います。

接続方法

gpoio

Raspberry PiのSPI接続用のポートは決まっていて、物理品の19、21、23とスレーブ選択およびデバイス選択の24、26番を使用します。GPIO番号で言うと12、13、14と10or11ということになります。

それぞれ名称として MOSI MISO SCLK CE0/1と名付けられていてそれぞれのピンは

  • MOSI(Master-Out Slave-In)
    マスター(制御をする側)がスレーブ(制御される側)にデータを転送するためのポートです。Raspberry Piから見ると「送信」ポートになります。なお、このポートのみを使用して双方向通信をする事もできます。 RS-232Cで言うところのTxDですね。
  • MISO(Master-In Slave-Out)
    マスターにスレーブがデータを転送するためのポートです。Raspberry Pi側から見ると「受信」ポートになります。 RS-232Cで言うところのRxDになります。 ちなみに今回の液晶は「受信専用」なのでこのポートは使いません。宙ぶらりんにするなり、GNDに接続するなり、MOSIとループバックするなり好きにしてください。
  • SCLK(Serial Clock)
    データ転送タイミングを合わせるためのクロックです。
    この線で出力されるクロックによって決められたタイミングでデータを送る事によってシリアル通信を実現しています。
  • CE0/CE1
    本来はマスターとスレーブを決定する信号線のようですが、今回使用する液晶やセンサー類などの基本的にスレーブにしかなりえないデバイスの場合はそのデータをどのデバイスが受け取るのかを決めるための信号となっている場合が多いようです。

となっていて、このピンに+GNDでデータ転送をしています。

その配線に加え、この液晶を制御するにはリセット信号とレジスタ選択信号が必要になります。

接続

 

 

IMG_4684_20150705
今回購入した液晶は単体ではピンがたくさん出ていますが、変換基板を使用することでマイコンとのI/Fは7本にすることが出来ます。セットで700円程度なのでセットを買えばいいと思います。
というより、この変換基板を使わないと配線がめんどくさい事になるので基本的にセットで買うべきだと思います。

なお、液晶側の配線ピッチが細いですがハンダ付けの際の先が細いはんだごてを使うか、適当に盛って吸い取り線で吸い取るという方法をすると簡単にはんだづけできます。

IMG_4739_20150712
合体させた

IMG_4740_20150712
裏面

IMG_4741_20150712

こんなかんじで接続しました。

IMG_4742_20150712
液晶アップ
せっかく7本なので信号線を虹色にしてみました。(水色がないので一番は白ですが。)

コーディング

まず制御用のRSとリセットは今までどおりwiringPiを用いて制御します。
そしてSPIも一応wiringPiに制御用ライブラリがありますが、何故かそのライブラリが使えなかったのと、Piを起動するたびに下準備が必要なので今回は低水準入出力命令系を用いて直接叩いてやります。なんか難しそうに聞こえますが、やっていることはGPIO制御初回でGPIOのファイルを書き換えてGPIOを制御した方法とそんなに変わりません。

まず、必要なライブラリをインクルードします。
今回低水準入出力でSPI制御をするのに参考にしたのは公式サイトのドキュメントおよびそこに記載されているテストファイルなのですが。このインクルードはそれのコピペ+wiringPiなのでもしかしたら不要なライブラリもあるかもしれません。

#include <stdint.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>
#include <wiringPi.h>

次にSPI以外のIOピンの番号を定義します。

#define RSpin 5 // レジスタ選択ピン指定
#define REpin 6 // リセット信号ピン指定

RS信号を5番 リセット信号を6番に設定しました。
このピンなら5番がMOSIポートの斜め上、6番がCE0の上となり、3.3VをMOSIポートの上のピンから、GNDを5と6の間のピンから取り出せば配線が4×2ピンに収まるのでスッキリします。

次に必要な定数などを定義します。

#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))

static uint8_t SPIMode = 3,SPIbit=8;   //SPIモード3 bit数8
static uint32_t SPISpeed = 20000000;  // 20MHz

// SPIデバイス定義
// # ls -l /dev/spidev* で確認できる。
// Pi2の場合CE1に接続した場合は0.0
//          CE2に接続した場合は0.1 だった。
static char *SPIDevice = "/dev/spidev0.0";
int fd,ret=0;

// 液晶解像度定義 _w=横 _h=縦(ドット数 あとでページ数に変換している)
int dot_w = 128,dot_h = 48 ;
  • SPIMode = SPIモードの設定
    この液晶は説明書によれば クロックアイドル時H 立ち上がり読み込みとなっているため、SPIモードは3となります。 SPIモードについてはこのへんがわかりやすいです。
  • SPIbit = 1ワードあたりのビット数指定
    1ワードあたり何bitで表現するかという設定。デバイスが受け取る仕様に合わせて変更するが、基本8か16bitらしいが、詳しく説明されたサイトが(日本語で)なくてよくわからなかった。とりあえず8で動く。
  • SPISpeed = 転送速度(動作クロック)設定
    SCLKのクロック速度を設定します。この液晶のコントローラの仕様書によるとSCLKのパルス幅H/Lの最小値が25nsつまり1サイクルあたり50ナノ秒が最小値ということなので20MHzが最高ということで、今回は最高値である20MHzを指定しました。
    この液晶がデータ転送だけですべてを書き換えるのに6144回転送してやる事になり、アドレス指定命令も含めると4回命令を送っているので、1秒で画面を書き換えようと思ったらその4倍の24576kHz以上に設定しないといけないですね。スムーズな画面変更を行うなら最低でも0.5~1MHz程度はあったほうがいいでしょう。
  • *SPIDevice = SPIデバイスの設定。
    SPIデバイスはRaspberry Piの場合は以下のようになるっぽいです。
    .   CE0 → /dev/spidev0.0
    .   CE1 → /dev/spidev0.1
  • dot_w / dot_h
    液晶のドット数を指定します。_wが横で_hが縦になります。
    指定はピクセル数で指定しますが、縦は実際にはバイト単位で使用するのであとでバイト単位に直しています。

次にメイン関数を定義します。

int main(int argc,char *argv[]){

今回も画像ファイル指定を引数で行うので受け取れるようにします。

次にGPIOを初期化して液晶をリセットします。

	dot_h =(dot_h+7)/8; // ドット数をページ数に変換(8bitで割る)
	
	// wiringPi初期化
	if(wiringPiSetup()==-1) printf("wiringPi初期化エラー\n");
	
	// ピンの初期化
	pinMode(RSpin,OUTPUT);
	pinMode(REpin,OUTPUT);
	
	// 液晶リセット信号送信
	digitalWrite(REpin,1);
	delay(1);
	digitalWrite(REpin,0);
	delay(1);
	digitalWrite(REpin,1);

1行目でドット数を8で割ってバイト単位に置き換えています。+7の部分は例えば50pxや62pxみたいなバイト単位で端数が出てしまう場合にInt型だと切り捨てなので切り上げになるように+7しています。後はいつもどおりwiringPiを初期化してピンを初期化、液晶のリセット信号ON→OFF→ONをしています。

次にSPIの初期化を行います。

	// SPIデバイスの初期化
	fd = open(SPIDevice,O_RDWR);if(fd<0) printf("SPI デバイス初期化エラー\n");
	ret = ioctl(fd,SPI_IOC_WR_MODE,&SPIMode);if(ret <0) printf("SPI Mode設定エラー\n");
	ret =ioctl(fd,SPI_IOC_WR_BITS_PER_WORD,&SPIbit);if(ret <0) printf("SPI bit/Word設定エラー\n");
	ret =ioctl(fd,SPI_IOC_WR_MAX_SPEED_HZ,&SPISpeed);if(ret <0) printf("SPI Speed設定エラー\n");

open命令にてファイルとして読み込み、その後ioctlでSPIモード、1ワードあたりのビット数、通信速度を指定しています。今回はライト(送信)のみを使用するのでWR系の設定のみをしていますが受信もする場合は下3行のWRの部分をRDに変更したものを追加してください。

次に液晶を初期化します。この後作成するDataSend命令を利用します。
基本的にはメーカー指定値を送っていますが、一部好みとペイントで出力されるBMP形式の仕様に合わせて変更しています。詳細はコード内のコメントを見て下さい。

	//液晶初期化コマンド送信
	// 表示オフ→ADC設定→Common Output設定→バイアス設定
	DataSend(0,0xAE);
	DataSend(0,0xA0);
	DataSend(0,0xC8);
	DataSend(0,0xA3);

	// 内部レギュレーターをオンにする
	DataSend(0,0x2C);
	delay(2);
	DataSend(0,0x2E);
	delay(2);
	DataSend(0,0x2F);


	// 液晶コントラスト設定
	// 3行目のコントラスト設定部分は入手した個体はメーカー指定値では
	// 若干濃かったので少しコントラストを下げています。
	DataSend(0,0x23);
	DataSend(0,0x81);
	DataSend(0,0x17); // メーカー指定値0x1C

	// 表示設定
	// 全点灯OFF→スタートライン0→白黒反転(BMPが0=白 黒=1なので)
	// →表示オン
	DataSend(0,0xA4);
	DataSend(0,0x40);
	DataSend(0,0xA7); // 白黒反転させる させない場合=0xA6
	DataSend(0,0xAF);
	
	// 液晶カーソル初期化
	DataSend(0,0xB0); // ページアドレス0
	DataSend(0,0x10); // Columnアドレス0指定(上位ビット)
	DataSend(0,0x00); // Columnアドレス0指定(下位ビット)

次にBMPファイルを読み込む部分を書きます。

	//BMPファイル読み込み
	unsigned char bhd[62],BMPData[dot_w][(((dot_h+3)/4)*4)];
	FILE *bmpr;
	bmpr=fopen(argv[1],"rb");
	
	// 読み込み ファイル形式が(2bit BMP 48x128px)だと前提して
	// 色々と端折ってデータ読み込み。↑の形式じゃないとエラー
	fread(bhd,1,62,bmpr);fread(BMPData,1,dot_w*(((dot_h+3)/4)*4)*8,bmpr);
	fclose(bmpr);
	
	BMPSend(BMPData); // データ転送

白黒BMPをBMPの仕様に合わせて読み込んでいます。
前回の128×64の液晶は丁度4バイトだったので気にする必要は無かったのですが、今回は128×48なので6バイトとなってちょっと足が出ますので穴埋め処理をしています。(本来は128×64でもするべきですが…。 それと実は48pxを穴埋めすると64pxになるので実際前のコードそのままでも動いたりする

BMPSend関数もこの後作ります。

最後にSPIデバイスを開放して終了

	close(fd); // SPIデバイス開放

次に実際にデータを転送する関数を作ります。

// LCDへのデータ転送
// DataSend(RS,TrData);
// RS=レジスタ選択
//     0=コマンド  1=データ
// TrData=転送するデータ
//         8bit値
// この液晶は受信専用なのだが今後のために一応送受信可能に
// しておく。
int DataSend(int RS,unsigned long TrData){
	digitalWrite(RSpin,RS); // RS信号切替
	uint8_t ReData;         //受信用変数
	
	// 転送するデータの準備
	struct spi_ioc_transfer tr={
	.tx_buf       =(unsigned long)&TrData,
	.rx_buf       =(unsigned long)&ReData,
	.len          = 1,
	.delay_usecs  = 1,
	.speed_hz     = SPISpeed,
	.bits_per_word= SPIbit,
	};
	
	// 転送
	ret =ioctl(fd,SPI_IOC_MESSAGE(1),&tr);
	if(ret<1){printf("SPI 転送エラー\n");exit(1);}
	return ReData; // 受信データを返す
}

wiringPiにてレジスタ選択信号を切り替え、先ほど作成したSPIデバイス変数に転送するデータを設定し、転送しています。この変数の戻り値は受信データになるようにしていますが、この液晶はPiからみて送信専用なので実際はデータは来ません。

この命令はこう使います。

DataSend(レジスタ選択信号,送信データ(1バイト));

※一部記号を全角にしていますが実際は半角で使用します。

次にBMPデータを転送する命令を作成します。

int BMPSend(unsigned char BMPData[dot_w][(((dot_h+3)/4)*4)]){
	int d=0,n=0;
	// 転送 BMPファイルは左下から記録されているので後ろから読み込む
	DataSend(0,0xAE); // 液晶表示オフ
	for(d=0;d<dot_w;d++){
		for(n=0;n<dot_h;n++){
			DataSend(0,0xB0+n);DataSend(0,0x10+(d/16));DataSend(0,0x00+(d%16));
			DataSend(1,BMPData[dot_w-1-d][dot_h-1-n]);
		}
	}
	DataSend(0,0xAF); //液晶表示オン
}

やっていることは前回のLCD_BMP();と同じで、使い方も同じです。BMPSend(BMPデータが入った配列);で行えます。やっていることが同じなので作成するデータも前回と同じで縦だけ48pxにすればいいだけです。(先ほど少し話したとおり、穴埋めすると64pxになるので実は全く同じデータが使えたりする。)

というわけでコーディングが完了しました。
基本的にパラレル接続とやっている手間はそこまで変わりません。ただデバイスとのやりとりの間に仲介業者が入るかどうかという感じです。

コンパイル方法やコンパイルしたプログラムの使用方法は前回と全く同じなので省略します。

SPIの設定

Raspberry PiでSPIを使う設定を行います。
といっても難しい設定はしなくてもよく、一番最初の記事で使用したraspi-configを利用することで簡単に設定できます。設定手順は以下のとおりです。

1.rootで raspi-config を起動する
2.8 Advanced Options を選択してエンター
spisetup01
3.A6 SPI を選択してエンターspisetup02

この後の問に <はい> → <了解> → <はい> → <了解> で設定を終了させてリブートしたら設定完了。

再起動後、「ls -l /dev/spidev*」と「lsmod」コマンドを実行して、
「/dev/spidev0.0」と「/dev/spidev0.1」、「spi_bcm2708」が表示されていたらSPIが使えるようになっているはずです。

spisetup04

 

なお、もしこの設定で使えない場合は手動でファイルを変更する必要があります。その方法は以下のとおりです。

1. /etc/modprobe.d/raspi-blacklist.conf から spi-bcm2708 が含まれている行を削除
2./etc/modules に spidev を追加する
3.再起動する

動作させてみる

上のコードを使ってBMPを表示してみます。

12848

例えばこのBMPファイルを液晶に転送してみると以下のようになります。

IMG_4748_20150712

 

ちなみにこの液晶で連続的にBMPを転送して見たところ、かなりかくかくしていました。何か方法があるのかもしれないけど…。

以下に完成したコードを載せておきます。

#include <stdint.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>
#include <wiringPi.h>

#define RSpin 5 // レジスタ選択ピン指定
#define REpin 6 // リセット信号ピン指定

#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))

static uint8_t SPIMode = 3,SPIbit=8;   //SPIモード3 bit数8
static uint32_t SPISpeed = 20000000;  // 20MHz

// SPIデバイス定義
// # ls -l /dev/spidev* で確認できる。
// Pi2の場合CE1に接続した場合は0.0
//          CE2に接続した場合は0.1 だった。
static char *SPIDevice = "/dev/spidev0.0";
int fd,ret=0;

// 液晶解像度定義 _w=横 _h=縦(ドット数 あとでページ数に変換している)
int dot_w = 128,dot_h = 48 ;


int main(int argc,char *argv[]){
	dot_h =(dot_h+7)/8; // ドット数をページ数に変換(8bitで割る)
	
	// wiringPi初期化
	if(wiringPiSetup()==-1) printf("wiringPi初期化エラー\n");
	
	// ピンの初期化
	pinMode(RSpin,OUTPUT);
	pinMode(REpin,OUTPUT);
	
	// 液晶リセット信号送信
	digitalWrite(REpin,1);
	delay(1);
	digitalWrite(REpin,0);
	delay(1);
	digitalWrite(REpin,1);
	
	
	// SPIデバイスの初期化
	fd = open(SPIDevice,O_RDWR);if(fd<0) printf("SPI デバイス初期化エラー\n");
	ret = ioctl(fd,SPI_IOC_WR_MODE,&SPIMode);if(ret <0) printf("SPI Mode設定エラー\n");
	ret =ioctl(fd,SPI_IOC_WR_BITS_PER_WORD,&SPIbit);if(ret <0) printf("SPI bit/Word設定エラー\n");
	ret =ioctl(fd,SPI_IOC_WR_MAX_SPEED_HZ,&SPISpeed);if(ret <0) printf("SPI Speed設定エラー\n");

	//液晶初期化コマンド送信
	// 表示オフ→ADC設定→Common Output設定→バイアス設定
	DataSend(0,0xAE);
	DataSend(0,0xA0);
	DataSend(0,0xC8);
	DataSend(0,0xA3);

	// 内部レギュレーターをオンにする
	DataSend(0,0x2C);
	delay(2);
	DataSend(0,0x2E);
	delay(2);
	DataSend(0,0x2F);


	// 液晶コントラスト設定
	// 3行目のコントラスト設定部分は入手した個体はメーカー指定値では
	// 若干濃かったので少しコントラストを下げています。
	DataSend(0,0x23);
	DataSend(0,0x81);
	DataSend(0,0x17); // メーカー指定値0x1C

	// 表示設定
	// 全点灯OFF→スタートライン0→白黒反転(BMPが0=白 黒=1なので)
	// →表示オン
	DataSend(0,0xA4);
	DataSend(0,0x40);
	DataSend(0,0xA7); // 白黒反転させる させない場合=0xA6
	DataSend(0,0xAF);
	
	// 液晶カーソル初期化
	DataSend(0,0xB0); // ページアドレス0
	DataSend(0,0x10); // Columnアドレス0指定(上位ビット)
	DataSend(0,0x00); // Columnアドレス0指定(下位ビット)

	//BMPファイル読み込み
	unsigned char bhd[62],BMPData[dot_w][(((dot_h+3)/4)*4)];
	FILE *bmpr;
	bmpr=fopen(argv[1],"rb");
	
	// 読み込み ファイル形式が(2bit BMP 48x128px)だと前提して
	// 色々と端折ってデータ読み込み。↑の形式じゃないとエラー
	fread(bhd,1,62,bmpr);fread(BMPData,1,dot_w*(((dot_h+3)/4)*4)*8,bmpr);
	fclose(bmpr);
	
	BMPSend(BMPData); // データ転送
	
	close(fd); // SPIデバイス開放
	}
	
// LCDへのデータ転送
// DataSend(RS,TrData);
// RS=レジスタ選択
//     0=コマンド  1=データ
// TrData=転送するデータ
//         8bit値
// この液晶は受信専用なのだが今後のために一応送受信可能に
// しておく。
int DataSend(int RS,unsigned long TrData){
	digitalWrite(RSpin,RS); // RS信号切替
	uint8_t ReData;         //受信用変数
	
	// 転送するデータの準備
	struct spi_ioc_transfer tr={
	.tx_buf       =(unsigned long)&TrData,
	.rx_buf       =(unsigned long)&ReData,
	.len          = 1,
	.delay_usecs  = 1,
	.speed_hz     = SPISpeed,
	.bits_per_word= SPIbit,
	};
	
	// 転送
	ret =ioctl(fd,SPI_IOC_MESSAGE(1),&tr);
	if(ret<1){printf("SPI 転送エラー\n");exit(1);}
	return ReData; // 受信データを返す
}

int BMPSend(unsigned char BMPData[dot_w][(((dot_h+3)/4)*4)]){
	int d=0,n=0;
	// 転送 BMPファイルは左下から記録されているので後ろから読み込む
	DataSend(0,0xAE); // 液晶表示オフ
	for(d=0;d<dot_w;d++){
		for(n=0;n<dot_h;n++){
			DataSend(0,0xB0+n);DataSend(0,0x10+(d/16));DataSend(0,0x00+(d%16));
			DataSend(1,BMPData[dot_w-1-d][dot_h-1-n]);
		}
	}
	DataSend(0,0xAF); //液晶表示オン
}

 

総閲覧数:637 PV

関連記事

“Raspberry Pi 2でSPI接続のグラフィック液晶の制御” への2件のフィードバック

  1. […] 基本的には前回の白黒グラフィック液晶のコマンドコードと転送量が増えるだけですので結線は同じ。SPIの3線にRS信号とリセット信号。そしてGNDだけ。これは他のST7735R搭載の液晶でも同じはず。 […]

  2. […] 基本的には前回の白黒グラフィック液晶のコマンドコードと転送量が増えるだけですので結線は同じ。SPIの3線にRS信号とリセット信号。そしてGNDだけ。これは他のST7735R搭載の液晶でも同じはず。 […]

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください