さくらのジャンク箱ロゴ

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

Raspberry Pi 2/4でSSD1331搭載0.95インチカラー有機ELを制御

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

皆様ご無沙汰しております。Sakura87です。
最近は天候にも恵まれずいくつか購入したものもありますがリアルも多忙だったのでブログを書く機会がなかったです。

いくつか購入したものに関しては今後公開する予定ですが、とりあえず今日はこの前購入した小型のカラー有機ELディスプレイを制御してみたいと思います。

購入したものは秋月電子で買える上記のものですがSSD1331というコントローラを搭載した96×64ピクセルのカラー有機ELであれば同様に処理が可能です。

お知らせ

2020年4月2日にRaspberry Pi4での動作を確認しました。そのままで動きます。

製品の写真

購入した有機ELはこのようなパッケージに入ってきました。

製品の正面。
特に何の変哲もない有機ELモジュールです。

裏面。こちらに有機ELの制御に必要なものが設置されているようです。

接続

この有機ELモジュールはリセット信号とデータ・コマンドのセレクトピンがある以外はSPI信号そのままで使えます。ピンはこのモジュールの描画面を表にして左から以下のようになっています。

ピン番号 名称 役割
1 GND GND
2 VCC 3.3V入力
3 SCL シリアルクロック
4 SDA データ転送ピン
5 RES リセット信号
6 DC データ・コマンドコントロール
7 CS チップセレクト

このようになっていて、リセット信号はセオリー通りに使うのであればGPIOで制御する必要がありますが、基本的にはVCCにつないでおけばよく、多くのネット作例を見てみても両者半々くらいで存在しているようです。

今回はめんどくさいのでリセット信号は常時VCCに接続しておくことにしました。一応テスト段階ではちゃんと描画開始アドレスを描画前に送っておけば特に画面が乱れることはありませんでした。

Raspberry Pi側の接続ですが、SPIの信号はピンが決まっていますので以下の画像を参考に接続します。

上の画像はラズベリーパイ2・3(4も?)のUSBやLANのコネクタがある方を下にした場合のピン配列で、17~26番のピンを使用します。接続の対応表は以下の通りです。

有機EL Raspberry Pi 有機EL
名称 No 名称 No   No 名称 No 名称
VCC/RES 2/5 3.3V 17   18 GPIO 24  
SDA 4 MOSI 19   20 GND 1 GND
  MISO 21   22 GPIO 25 6 DC
SCL 3 SCLK 23   24 CE.1 7 CS

MISOとGPIO24番は未使用です。GPIO24はリセット信号を処理する場合には活用出来そうです。MISOは基本的に空きポートになると思います。

電源やGND、GPIOに関してはあいているところであればどこでもよいですが、以上の接続であれば使用するピンが一カ所に集中するのですっきりすると思います。

特に有機ELディスプレイは他のデバイスをつないでその情報を表示する事にも使うと思いますのでまとめることで他のデバイスとの接続性もよくなります。SPIは一つのポートにたくさん数珠つなぎをしてCE信号を切り替えることによって複数使用することが可能ですので。接続に関しては各自が使用する他のデバイスとの兼ね合いで決めてください。

コーディング

さて、それではコードを書いていくのですが。今回はGPIOも標準命令を使用するので追加のライブラリは不要です。ただしテキストを使用する場合は以下のFONT-X読み込みライブラリを使用します。また、こちらで紹介するコードもすべてそのライブラリを使用する体で書いてあります。

ラズパイ2資料

なお、前回まではC言語で書いてきましたが、最近は仕事などでC++を利用する機会が多いことから、今回からC++で書いています。ただしC++らしいコードはあまり使っていないので、インクルードを調整すればC言語でもほぼそのままで使えると思います。

大まかな制御方法

この有機ELモジュールは以下のように制御することで動かすことが出来ます。

  1. GPIO・SPI信号を初期化する
  2. DCピンをコマンドモードに設定
  3. ディスプレイモードを設定する
  4. Remapと色深度を設定する
  5. ディスプレイをオンにする
  6. その他必要な設定を行う
  7. DCピンをデータモードに設定
  8. 液晶に表示するデータを転送

以上が一連の大まかな制御方法です。

特に気にするべき所はディスプレイをオンにする所とDCピンの制御ですね。それ以外は難しいものでもないです。

必要なインクルードなどの定義

今回使用するのに必要なインクルードなどの定義を行います。

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/spi/spidev.h>
#include <sys/ioctl.h>
#include <cstring>

// ここからFONTX関係

#define FONT_FILEA "./PAW16A.FNT"   // 全角文字用フォントを指定
#define FONT_FILEJ "./PAW16K.FNT"   // 全角文字用フォントを指定

#include "FONTX_PI/fontxpi.h"

// ここまでFONTX関係

// ここから有機EL用のGPIOとSPI関係

#define DPin 25 // D/C切り替えピンのIO番号
#define SEND_MODE_DATA 1 // D/Cスイッチ データ
#define SEND_MODE_COMMAND 0 // D/Cスイッチ コマンド

#define kHz  * 1000
#define MHz  * 1000000

static char SPIMode = 3, SPIbit = 8; //SPIモード3 bit数8
static int SPISpeed = 10 MHz;    // SPI転送速度設定

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

// ここまで有機EL用のGPIOとSPI関係

インクルードはもしかしたら不要な部分もあるかもしれませんが、これでよいです。

FONTXの部分はこの前使用して気に入ったぱうフォントを使用するように設定しています。他のフォント似する場合は11・12行目をそのフォントに合わせて修正するかフォントのファイル名をこちらに入れ替えてください。

この2行の後にFONTXのライブラリをインクルードしています。

GPIO関係の処理がその後にあります。

20行のDefineがDCピンのIO番号を指定する箇所になります。ここを自由に変更することで他のピンに設定することも可能です。
なお、こちらのプログラムは先ほど掲示したIOリストの「OS標準」のGPIO番号を指定します。

GPIO関連の関数

ということでGPIOを制御する関数を作ります。
関数はGPIOを初期化する関数、クローズする関数、実際にIOを変更する関数の3つを作ります。

GPIO初期化関数

// GPIOピンの初期化
int gpioOpen(int port, int Mode) {
	// 各種IOのパスを格納するための諸々
	char* ps = (char*)new char[2048];
	char* ds = (char*)(ps + 128);
	char* vs = (char*)(ps + 1024);
	// ポート番号
	sprintf(ps, "%d", port);
	//入出力設定用
	sprintf(ds, "/sys/class/gpio/gpio%d/direction", port);
	//I/O数値を書き込むためのもの。これが本体
	sprintf(vs, "/sys/class/gpio/gpio%d/value", port);

	//GPIO初期化の初期化
	int fd = open("/sys/class/gpio/export", O_WRONLY);
	write(fd, (ps), 3);
	close(fd);
	fd = -1;

	//IOモードの設定
	while (fd == -1) fd = open(ds, O_WRONLY);
	write(fd, Mode ? "in" : "out", Mode ? 2 : 3);
	close(fd);
	fd = -1;

	//IO設定を行うデバイスをオープン。これを使う。
	while (fd == -1) fd = open(vs, O_WRONLY);
	free(ps);
	printf("IO %d %s %d\n", port, Mode ? "in" : "out", fd);
	return fd;
}

やっていることはGPIO番号を付与したアドレスをsprintfで生成してOpenし、設定値を設定しているだけです。sprintfは脆弱性の元にもなるようですが、ここでは固定値しか扱わないので使いやすい方を選びました。

GPIOの初期化方法は基本的にはだーいぶ前に紹介したコンソールで処理する方法をそのまま行います。

https://sakura87.net/archives/1442

open命令でオープンしたファイルに、上の記事の通りに制御して設定してあげればよいのですが、最初の初期化の後命令が完了しても処理に少し時間がかかる場合があるようで、その後21行と27行の2つのOpenはWhileで-1以外の数値が返るまで続けています。今のところここでデッドロックするようなことはないですが、心配ならば1万回くらいでやめるようにした方がいいかもしれません。ここで必要なのは一番最後に開いたものです。これだけReturnで返します。それ以外は設定を転送したらCloseしてOKです。

GPIOクローズ関数

次にGPIOをクローズする関数を作ります。
これも上の容量で作れます。基本的にはunexportファイルを開いてポート番号を投げてやればOKです。

// GPIOクローズ 他のアプリが行ったものもクローズして横取り
// 出来るっぽいので最初と最後にやっておく。
void gpioClose(int port) {
	int fd = open("/sys/class/gpio/unexport", O_WRONLY);
	char* ps = (char*)new char[256];
	sprintf(ps, "%d", port);
	write(fd, (ps), 2);
	close(fd);
	free(ps);
}

クローズはまぁ説明は不要ですね。実はなくてもいいような気もします。

GPIO制御命令

次に一番使用するGPIO制御命令です。これでIOピンのH・Lを切り替えます。

// GPIO書き込み
void digitalWrite(int fd, int value) {
	write(fd, value ? "1" : "0", 1);
}

単に先ほど初期化した最後のIOに値を書き込めばよいだけです。

以上でGPIOの必要な関数はすべてそろいました。
次にSPIの初期化を行いますが、ついでに以上のコマンドを使用してGPIOの解放と再取得を行います。

GPIOとSPI信号の取得

//GPIOとSPIの初期化
void resetOLED(int* oled, int* dcpin) {
	int ios[2];
	//IOピンの解放
	gpioClose(DPin);
	int dcp = gpioOpen(DPin, 0);
	dcpin[0] = dcp;
	// SPIの初期化
	int fd = open(SPIDevice, O_RDWR);
	printf("SPI MODE SetResult: %d\n", ioctl(fd, SPI_IOC_WR_MODE, &SPIMode));
	printf("SPI BIT  SetResult: %d\n", ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &SPIbit));
	printf("SPI SPD  SetResult: %d\n", ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &SPISpeed));
	printf("SPI %d\n", fd);
	oled[0] = fd;
}

 

元々リセット信号も転送していたので関数名がそれに沿ったものになっていますが、やっているのはDCピン用GPIOの初期化とSPIの初期化です。

SPIの初期化はいつもやっているので割愛します。いつもと違うのはSPIのモードが3になるくらいですね。一応説明するとSPIのデバイスファイルをオープンしてioctlで設定を転送している感じです。

初期化されたら引数に指定した変数にデータを書き込むアドレスが帰ります。oledのほうにSPIを制御するアドレスを格納する変数。dcpinの方にDCコントロール信号を制御するアドレスを格納する変数を指定して実行するとアドレスが返ってきます。0?か負数であれば失敗です。

さて次は実際にコマンドやデータを転送する関数を作ります。

データ転送関数

int SendData(int spi_fd, int dcpin_fd, int mode, char* data, int size) {
	int r = 0;
	char rb[2048];

	// データ・コマンド切り替え
	digitalWrite(dcpin_fd, mode);

	// ラズパイのSPIはバッファサイズが2048バイトなので2048バイトごとに転送する
	for (int i = 0; i < size; i += 2048) {
		unsigned int ts = (size - i) > 2048 ? 2048 : size - i;

		// 転送するデータの準備
		struct spi_ioc_transfer tr;
		tr.tx_buf = (unsigned long)(data + i); // 転送するデータ
		tr.rx_buf = (unsigned long)rb; // 受信データ。送信専用だがこれがないと安定しない。
		tr.len = ts; // 転送するサイズ
		tr.delay_usecs = 1; // 最終ビット転送後のディレイ。0で使う人が多いようだが1の方が若干安定する 気 が す る
		tr.speed_hz = SPISpeed; // 転送速度
		tr.bits_per_word = SPIbit; // ワードあたりのビット数指定
		tr.cs_change = 1; // CS切り替えを行うかどうか

		r += ioctl(spi_fd, SPI_IOC_MESSAGE(1), &tr); // 何もなければ0を返す
	}

	// printf("SPI Result: %d \n", r);

	// 転送
	return r;
}

 

今までやったことの寄せ集め。普段のSPI命令と違うところは、DCコントロールの制御が先頭に入っているという所だけ。また、今回は見込まれる最大転送サイズが12KBほどあり、ラズパイのSPIバッファを超えるため、2048バイトごとに区切って転送する処理もつけています。

spi_fdに初期化コマンドで引数oledの方に指定した変数で返ってきたSPIのアドレスを、dcpin_fdの方にdcpinで指定した変数に返ってきたDCピンのアドレスを指定し、modeにコマンド転送モードであれば0(L)をデータ転送モードであれば1(H)を指定します。

dataに転送するデータが格納された変数へのポインタを。sizeに転送するバイト数を指定します。

ここまで作ればあとは転送するコードを書き込めばOKです。

文字転送コード

次に文字を転送するコードを書きます。

// 文字の転送
int send_font_data(int spifd, int dcpfd, char* data,
	char row_start, char col_start, unsigned long fontdata, short fore_col, short back_col) {

	// 文字情報を振り分ける
	int fontx = (fontdata >> 24) & 0xff;
	int fonty = (fontdata >> 16) & 0xff;
	int fontb = fontdata & 0xffff;

	// 転送用のデータ領域を確保
	short data2[(fontb * 8)];

	// 色のバイト順を入れ替える
	short t_forecol = (fore_col << 8) | (fore_col >> 8) & 0x00ff;
	short t_backcol = (back_col << 8) | (back_col >> 8) & 0x00ff;


	// ビット配列→short配列へ
	for (int i = 0; i < fontb * 8; ++i)
		data2[i] = ((data[i >> 3] >> (7 - (i & 7))) & 1) == 0 ? t_backcol : t_forecol;

	char rcmd[6];

	rcmd[0] = 0x75;
	rcmd[1] = row_start;
	rcmd[2] = row_start + fonty - 1;
	rcmd[3] = 0x15;
	rcmd[4] = col_start;
	rcmd[5] = col_start + fontx - 1;

	// 描画範囲の設定
	SendData(spifd, dcpfd, SEND_MODE_COMMAND, (char*)rcmd, 6);

	// データ転送
	SendData(spifd, dcpfd, SEND_MODE_DATA, (char*)data2, fontb << 4);
	return 0;
}

やっていることは例のライブラリを使用して文字パターンを取得し、1bitカラーを16bitにかさ上げ、BMPファイルと同じように下から上に入れ替えているという処理です。

22行~32行までは描画範囲を指定するコマンドです。0x75コマンドが行、0x15コマンドが列の指定を行うコマンドです。
どちらが先に転送してもよいようですが、まぁ行列で転送しておくのがよいでしょう。

メイン関数

それではこれらを統合して制御するメイン関数を作成します。チョット長いですが。

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


	int pixels = 96 * 64;

	short data[pixels << 1];
	char  fontb[1024];

	// 初期化用のコマンド準備
	char init_cmd[4];

	init_cmd[0] = 0xA4; // ディスプレイモード設定 Normal Display
	init_cmd[1] = 0xA0; // Remap と 色深度の設定
	init_cmd[2] = 0x72; // ↑の設定値0b0111 0010
	init_cmd[3] = 0xAF; // ディスプレイON

	// D/C用GPIO初期化
	int spifd;
	int dcpfd;
	resetOLED(&spifd, &dcpfd);

	// 初期化用コマンド転送
	SendData(spifd, dcpfd, SEND_MODE_COMMAND, init_cmd, 4);

	// BMP画像を表示する場合の処理 96x64の16bitBMP画像のみ対応
	if (strcmp(argv[1], "i") == 0) {
		// ファイルの読み込みとバッファの確保
		FILE* bmp = fopen(argv[2], "rb");

		int offset;

		// 画像までのオフセットを読み込んでシーク
		fseek(bmp, 10, SEEK_SET);
		fread(&offset, 4, 1, bmp);
		fseek(bmp, offset, SEEK_SET);
		// 画像を読み込んで転送
		fread((data + pixels), 2, pixels, bmp);

		// ピクセル情報のバイト配列を入れ替え
		for (int i = 0; i < pixels; i++) {
			short tmp = (data[pixels + i] << 8) | ((data[pixels + i] >> 8) & 0x00ff);
			data[pixels + i] = tmp;
		}

		// 上下反転
		for (int i = 0; i < 64; i++) memcpy(data + (63 - i) * 96, data + pixels + i * 96, 192);

		// 32bit以下のBMPファイルなので4バイトの境界を考慮する必要があるが
		// 96x64の16bit BMPファイルであればちょうど割り切れるので考慮していない。

		// データ転送

		SendData(spifd, dcpfd, SEND_MODE_COMMAND, (char*)new char[6]{ 0x15,0,95,0x75,0,63 }, 6);
		SendData(spifd, dcpfd, SEND_MODE_DATA, (char*)data, pixels * 2);
	}
	// テキストを表示する処理

	if (strcmp(argv[1], "t") == 0) {
		char ccnt = 0;
		char rcnt = 0;

		// 色々と初期化
		FILE* txt = fopen(argv[2], "rb");
		unsigned char rc[1];
		int code;

		// せっかくなので4行の色を変える。
		short color[4] = { (short)0xffff,(short)0xf800,(short)0x07E0,(short)0x001F };
		int col_idx = 0;

		// 文字列読み込みループ
		while (1) {
			// 1バイト読み込み。読み込めなかったら(終端なら)抜ける
			if (fread(rc, 1, 1, txt) != 1)break;

			// 全角・半角チェック
			bool zenkaku = ZenkakuCheck(rc[0]);

			// 読み込んだ文字コードを代入
			code = rc[0];

			// 全角の場合は追加読み込み
			if (zenkaku) {
				// 読み込めなかったら抜ける
				if (fread(rc, 1, 1, txt) != 1)break;
				// 読み込めた場合は1バイト目のコードを8ビットシフトして
				// OR演算で代入。
				code = (code << 8) | rc[0];
			}
			// フォントの読み込み
			unsigned long r = KanjiReadX(code, (unsigned char*)fontb);

			// 読み込んだフォントの横・縦ピクセル数を取得。
			int fx = (r >> 24) & 0xff;
			int fy = (r >> 16) & 0xff;

			// 文字データ転送。

			// 横方向の終端まで逝ったら改行
			if (ccnt + fx > 96) {
				ccnt = 0;
				rcnt += fy;
				col_idx++;
			}

			// フォントデータ転送
			send_font_data(spifd, dcpfd, (char*)fontb, rcnt, ccnt, r, color[col_idx], 0x0000);

			// 1文字分進める
			ccnt += fx;
		}
		fclose(txt);

		// GPIO解放
		gpioClose(DPin);
	}
	return 0;
}

 

転送などに使用するバッファを確保して有機ELをソフトウェア的に初期化するためのコマンドを転送します。

 // 初期化用のコマンド準備
 char init_cmd[4];
 
 init_cmd[0] = 0xA4; // ディスプレイモード設定 Normal Display
 init_cmd[1] = 0xA0; // Remap と 色深度の設定
 init_cmd[2] = 0x72; // ↑の設定値0b0111 0010
 init_cmd[3] = 0xAF; // ディスプレイON

この部分が液晶初期化コマンドです。
まずはディスプレイモードをノーマルに設定します。次にRemapと色深度を指定するのですが、これは以下の通りになっています。

bit 設定値 説明
7 0 カラーフォーマット
[7:6] 00 256色 01 65k色1 10 65k色2
6 1
5 1 COM Split Enable(0:Disable 1:Enable)
4 1 Scan from 0: COM0 to COM[N-1] 1: COM[N-1] to COM 0
3 0 Left-Right Swapping on COM 0:Disable 1:Enable
2 0 色情報のオーダー0: RGB 1:BGR
1 1 RAM Column maps. 0: 0 to 95 1: 95 to 0
0 0 Address Increment 0:Horizonal 1:Vertical

基本的には0x72が扱いやすいようです。7:6ビットの色情報は上の設定で今回は使用出来ましたが、どうやらSPIは8bitで転送するのでこちらでいいようです。5ビット目は0にすると間延びした画像になります。単純にデータを流すのであればこの設定がよいようです。
4ビット目は縦方向(横長で使う場合)をどう描画するかという設定で0にすれば上下反転(ピンヘッダがあるほうが下)します。
3ビット目はチョットよくわからないのですが。1にすると表示が乱れます。BMPファイルを扱う感覚で使用するのであれば0でいいようです。
2ビット目は色情報の記録方向でBMPファイルと同じにするのであればBGRを選択します。ただしバイトオーダーがIntel方式と異なるのでBGRを選択した後、バイトオーダーを反転させてあげる必要があります。
1ビット目はRAMのカラムアドレスを入れ替える設定の用です。
これを切り替えることで鏡文字にしたりしなかったり出来ます。1ビット目と4ビット目で表示方向を決定します。

0ビット目はアドレスのインクリメント方向の縦横で今回は横向きに使用するので0です。

最後にディスプレイをオンにする信号である0xAFを転送すればOKです。

あとは描画先の範囲を指定してデータを送り込んでやればOK。
この手のカラーディスプレイの特徴として、描画範囲を指定してデータ転送が出来るので、BMPファイルやフォントデータを色深度さえ合わせればそのまま転送出来て高速に転送することが可能だというところがあります。なのでマイコンで制御出来るカラー液晶・有機ELモジュールはカラーだからと難しく思えますが案外カラーの方がいろいろしなくていい分楽です。

以上ですべてのコードが出そろいました。結構長くなりましたが重要なのはこの項で説明した初期化関係とDataSendコマンドの中身です。
あとは教科書通りのSPI転送を行ってあげれば使えます。

コンパイル

コンパイルは今までgccを使用していましたが今回はC++で書いているのでg++コマンドを使用します。インストール方法や使用方法は基本的にはgccと同じです。

動作風景

有機ELを動作させたときの写真をいくつかあげておきます。
動作方法は最後に掲示する全体のコードをコンパイルして。そのファイルの最初の引数に「i」(画像)または「t」(テキスト)を指定して2番目の引数にファイルを指定してあげれば表示されます。

画像を表示してみた写真

照明の関係で少し色が薄いですが、本物はもう少し色鮮やかです。
表示している画像は16bit B5G6R5形式のBMPファイルです。これを作成するにはフォトレタッチソフトが必要で、大半の有名なレタッチソフトは作れると思います。

テキストを表示してみた写真

テキストを表示すると1行目が白、2行目が赤、3行目が緑、4行目が青で表示されます。表示色はメイン関数の68行目で変更することが出来ます。今回は行で色を変えていますが、もちろん文字単位での色指定も出来るようです。

終わりに

さて、今回はカラーの有機ELで遊んでみました。
少し駆け足で説明してしまったので、説明足らずやもっとこう言うところがほしいと言ったことがあればコメントへどうぞ。

最近はフルカラーが当たり前の世の中ですが、65,536色表示とはいえ流石有機ELという発色の良さです。画面サイズも少し小さめですが、16×16ピクセルフォントで6×4行の文字表示も可能なのでそれなりに情報も表示出来ますね。

まぁ1,000円程度ですので、チョット凝った数字二桁程度の表示(音量とか温度とか…)というのもいいと思いますし、4分の3をジャケット表示にしたプレイヤーなんかもアリだと思います。

ともかく小さいなりにいろいろと使えそうな有機ELモジュールだと思います。特にフォントを扱う場合は基本的に色深度をかさ上げして上げるだけでいいのでかなり楽です。

さて次回ですが、いくつかものを買っていますので小ネタをちょいちょい投稿していくことになると思います。

それでは。

全体のソースコード

繰り返しになりますが今回はC++で書いているのでg++を使ってコンパイルしてください。

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/spi/spidev.h>
#include <sys/ioctl.h>
#include <cstring>

// ここからFONTX関係

#define FONT_FILEA "./PAW16A.FNT"   // 全角文字用フォントを指定
#define FONT_FILEJ "./PAW16K.FNT"   // 全角文字用フォントを指定

#include "FONTX_PI/fontxpi.h"

// ここまでFONTX関係

// ここから有機EL用のGPIOとSPI関係

#define DPin 25 // D/C切り替えピンのIO番号
#define SEND_MODE_DATA 1 // D/Cスイッチ データ
#define SEND_MODE_COMMAND 0 // D/Cスイッチ コマンド

#define kHz  * 1000
#define MHz  * 1000000

static char SPIMode = 3, SPIbit = 8; //SPIモード3 bit数8
static int SPISpeed = 10 MHz;    // SPI転送速度設定

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

// ここまで有機EL用のGPIOとSPI関係

// GPIOピンの初期化
int gpioOpen(int port, int Mode) {
	// 各種IOのパスを格納するための諸々
	char* ps = (char*)new char[2048];
	char* ds = (char*)(ps + 128);
	char* vs = (char*)(ps + 1024);
	// ポート番号
	sprintf(ps, "%d", port);
	//入出力設定用
	sprintf(ds, "/sys/class/gpio/gpio%d/direction", port);
	//I/O数値を書き込むためのもの。これが本体
	sprintf(vs, "/sys/class/gpio/gpio%d/value", port);

	//GPIO初期化の初期化
	int fd = open("/sys/class/gpio/export", O_WRONLY);
	write(fd, (ps), 3);
	close(fd);
	fd = -1;

	//IOモードの設定
	while (fd == -1) fd = open(ds, O_WRONLY);
	write(fd, Mode ? "in" : "out", Mode ? 2 : 3);
	close(fd);
	fd = -1;

	//IO設定を行うデバイスをオープン。これを使う。
	while (fd == -1) fd = open(vs, O_WRONLY);
	free(ps);
	printf("IO %d %s %d\n", port, Mode ? "in" : "out", fd);
	return fd;
}

// GPIOクローズ 他のアプリが行ったものもクローズして横取り
// 出来るっぽいので最初と最後にやっておく。
void gpioClose(int port) {
	int fd = open("/sys/class/gpio/unexport", O_WRONLY);
	char* ps = (char*)new char[256];
	sprintf(ps, "%d", port);
	write(fd, (ps), 2);
	close(fd);
	free(ps);
}

// GPIO書き込み
void digitalWrite(int fd, int value) {
	write(fd, value ? "1" : "0", 1);
}

//GPIOとSPIの初期化
void resetOLED(int* oled, int* dcpin) {
	int ios[2];
	//IOピンの解放
	gpioClose(DPin);
	int dcp = gpioOpen(DPin, 0);
	dcpin[0] = dcp;
	// SPIの初期化
	int fd = open(SPIDevice, O_RDWR);
	printf("SPI MODE SetResult: %d\n", ioctl(fd, SPI_IOC_WR_MODE, &SPIMode));
	printf("SPI BIT  SetResult: %d\n", ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &SPIbit));
	printf("SPI SPD  SetResult: %d\n", ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &SPISpeed));
	printf("SPI %d\n", fd);
	oled[0] = fd;
}
int SendData(int spi_fd, int dcpin_fd, int mode, char* data, int size) {
	int r = 0;
	char rb[2048];

	// データ・コマンド切り替え
	digitalWrite(dcpin_fd, mode);

	// ラズパイのSPIはバッファサイズが2048バイトなので2048バイトごとに転送する
	for (int i = 0; i < size; i += 2048) {
		unsigned int ts = (size - i) > 2048 ? 2048 : size - i;

		// 転送するデータの準備
		struct spi_ioc_transfer tr;
		tr.tx_buf = (unsigned long)(data + i); // 転送するデータ
		tr.rx_buf = (unsigned long)rb; // 受信データ。送信専用だがこれがないと安定しない。
		tr.len = ts; // 転送するサイズ
		tr.delay_usecs = 1; // 最終ビット転送後のディレイ。0で使う人が多いようだが1の方が若干安定する 気 が す る
		tr.speed_hz = SPISpeed; // 転送速度
		tr.bits_per_word = SPIbit; // ワードあたりのビット数指定
		tr.cs_change = 1; // CS切り替えを行うかどうか

		r += ioctl(spi_fd, SPI_IOC_MESSAGE(1), &tr); // 何もなければ0を返す
	}

	// printf("SPI Result: %d \n", r);

	// 転送
	return r;
}

// 文字の転送
int send_font_data(int spifd, int dcpfd, char* data,
	char row_start, char col_start, unsigned long fontdata, short fore_col, short back_col) {

	// 文字情報を振り分ける
	int fontx = (fontdata >> 24) & 0xff;
	int fonty = (fontdata >> 16) & 0xff;
	int fontb = fontdata & 0xffff;

	// 転送用のデータ領域を確保
	short data2[(fontb * 8)];

	// 色のバイト順を入れ替える
	short t_forecol = (fore_col << 8) | (fore_col >> 8) & 0x00ff;
	short t_backcol = (back_col << 8) | (back_col >> 8) & 0x00ff;


	// ビット配列→short配列へ
	for (int i = 0; i < fontb * 8; ++i)
		data2[i] = ((data[i >> 3] >> (7 - (i & 7))) & 1) == 0 ? t_backcol : t_forecol;

	char rcmd[6];

	rcmd[0] = 0x75;
	rcmd[1] = row_start;
	rcmd[2] = row_start + fonty - 1;
	rcmd[3] = 0x15;
	rcmd[4] = col_start;
	rcmd[5] = col_start + fontx - 1;

	// 描画範囲の設定
	SendData(spifd, dcpfd, SEND_MODE_COMMAND, (char*)rcmd, 6);

	// データ転送
	SendData(spifd, dcpfd, SEND_MODE_DATA, (char*)data2, fontb << 4);
	return 0;
}


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


	int pixels = 96 * 64;

	short data[pixels << 1];
	char  fontb[1024];

	// 初期化用のコマンド準備
	char init_cmd[4];

	init_cmd[0] = 0xA4; // ディスプレイモード設定 Normal Display
	init_cmd[1] = 0xA0; // Remap と 色深度の設定
	init_cmd[2] = 0x72; // ↑の設定値0b0111 0010
	init_cmd[3] = 0xAF; // ディスプレイON

	// D/C用GPIO初期化
	int spifd;
	int dcpfd;
	resetOLED(&spifd, &dcpfd);

	// 初期化用コマンド転送
	SendData(spifd, dcpfd, SEND_MODE_COMMAND, init_cmd, 4);

	// BMP画像を表示する場合の処理 96x64の16bitBMP画像のみ対応
	if (strcmp(argv[1], "i") == 0) {
		// ファイルの読み込みとバッファの確保
		FILE* bmp = fopen(argv[2], "rb");

		int offset;

		// 画像までのオフセットを読み込んでシーク
		fseek(bmp, 10, SEEK_SET);
		fread(&offset, 4, 1, bmp);
		fseek(bmp, offset, SEEK_SET);
		// 画像を読み込んで転送
		fread((data + pixels), 2, pixels, bmp);

		// ピクセル情報のバイト配列を入れ替え
		for (int i = 0; i < pixels; i++) {
			short tmp = (data[pixels + i] << 8) | ((data[pixels + i] >> 8) & 0x00ff);
			data[pixels + i] = tmp;
		}

		// 上下反転
		for (int i = 0; i < 64; i++) memcpy(data + (63 - i) * 96, data + pixels + i * 96, 192);

		// 32bit以下のBMPファイルなので4バイトの境界を考慮する必要があるが
		// 96x64の16bit BMPファイルであればちょうど割り切れるので考慮していない。

		// データ転送

		SendData(spifd, dcpfd, SEND_MODE_COMMAND, (char*)new char[6]{ 0x15,0,95,0x75,0,63 }, 6);
		SendData(spifd, dcpfd, SEND_MODE_DATA, (char*)data, pixels * 2);
	}
	// テキストを表示する処理

	if (strcmp(argv[1], "t") == 0) {
		char ccnt = 0;
		char rcnt = 0;

		// 色々と初期化
		FILE* txt = fopen(argv[2], "rb");
		unsigned char rc[1];
		int code;

		// せっかくなので4行の色を変える。
		short color[4] = { (short)0xffff,(short)0xf800,(short)0x07E0,(short)0x001F };
		int col_idx = 0;

		// 文字列読み込みループ
		while (1) {
			// 1バイト読み込み。読み込めなかったら(終端なら)抜ける
			if (fread(rc, 1, 1, txt) != 1)break;

			// 全角・半角チェック
			bool zenkaku = ZenkakuCheck(rc[0]);

			// 読み込んだ文字コードを代入
			code = rc[0];

			// 全角の場合は追加読み込み
			if (zenkaku) {
				// 読み込めなかったら抜ける
				if (fread(rc, 1, 1, txt) != 1)break;
				// 読み込めた場合は1バイト目のコードを8ビットシフトして
				// OR演算で代入。
				code = (code << 8) | rc[0];
			}
			// フォントの読み込み
			unsigned long r = KanjiReadX(code, (unsigned char*)fontb);

			// 読み込んだフォントの横・縦ピクセル数を取得。
			int fx = (r >> 24) & 0xff;
			int fy = (r >> 16) & 0xff;

			// 文字データ転送。

			// 横方向の終端まで逝ったら改行
			if (ccnt + fx > 96) {
				ccnt = 0;
				rcnt += fy;
				col_idx++;
			}

			// フォントデータ転送
			send_font_data(spifd, dcpfd, (char*)fontb, rcnt, ccnt, r, color[col_idx], 0x0000);

			// 1文字分進める
			ccnt += fx;
		}
		fclose(txt);

		// GPIO解放
		gpioClose(DPin);
	}
	return 0;
}

 

更新履歴

  1. 令和元年10月16日(水)
    初版発行
  2. 令和元年10月25日(金)
    ラズベリーパイとの接続表が一部間違っていたのと、&や<>などの記号が化けていたで修正
総閲覧数:358 PV

関連記事

コメントを残す

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

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