さくらのジャンク箱ロゴ

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

Raspberry Pi 2/4でI2C接続、SSD1306採用のグラフィック有機ELディスプレイを使う

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

どうもお盆休み9連休のSakura87です。家の片付けと法事で大体潰れました。
ということで久しぶりのラズパイ記事です!なんと2年ぶり?くらいになります。
今回はこの有機ELディスプレイを操ってみたいと思います。

プロジェクタースクリーン?アレは書く事がなさ過ぎて没になりました。まぁ画用紙で作ったスクリーン(自称)に比べたら良い感じでした。

なぜ今更ラズパイ2なのか?それは持っているからだ。今度4が日本で発売されたら買い換える予定。(買いました

お知らせ

2020年4月2日、Raspberry Pi4での動作確認を行いました。
特に変更の必要はなくそのまま動きます。

使用するもの

  • Raspberry Pi本体
    なんでも良いだろうけど性能的には2B以降を推奨。
    2B以下だとプログラムの修正が必要かも。
  • 0.96インチ 128×64ドット有機ELディスプレイ(OLED)
    今回は秋月電子で売っていたこちらにしました。ICがSSD1306のものであれば同様に処理できるはず。
  • 接続するための配線類
    なんでもいいです。ラズパイで上記有機ELなら、両方雌のジャンパ線があれば良いんじゃないですかね。最悪半田付けすれば何とでもなりますし。

購入した有機ELディスプレイ正面。実はこの有機ELディスプレイは1年以上前に買って積んでいたものなので外見は若干の仕様変更があるかもしれません。

裏面。ちょっとぼけてますが、アドレスは0x78に設定されていました。
ここで0x78ですが、Raspberry Piで認識されるのは1ビット右シフトした3Cになるようです。

今回はこの有機ELディスプレイにモノクロビットマップとテキストファイルを表示してみたいと思います。

有機ELの制御

それでは早速有機ELディスプレイの制御を行っていきたいと思います。

データの転送方法

I2C接続でデータを転送します。I2Cですので以下の図のSDA.1/SCL.1に液晶のSDA/SCLを接続して後は3.3V出力とGNDをつなげば物理接続はOK。

正しく接続されていれば、Raspberry Pi側はこうなっているはずです。

(VCCが3.0Vになっていますが3.3Vの間違いです。)
GNDは図解しやすいようにここから取りましたが、SCL.1の向かいのピンからとっても良いです。

Raspberry Pi側の設定は以下の通りです。

  1. raspi-configにてRaspberry PiのI2Cを有効にする
  2. i2c-toolsとlibi2c-devをインストールする

最新のRaspbianでフルインストールした場合は1番のみで良かったです。
前回がもう5年近く前なので最新の操作方法を以下に記しておきます。

I2Cの有効化

端末を起動して以下のコマンドを入力してラズベリーパイの設定を開く

sudo raspi-config

5 Interfacing Options を選択

P5 I2C を選択

<はい>を選択

上のメッセージがThe ARM I2C interface is enabledである事を確認し、<了解>を選択。

<Finish>で終了。

次に以下のコマンドを入力してi2c-toolsをインストールします。

sudo apt-get install i2c-tools

インストールが完了したらlibi2c-devをインストールします。

sudo apt-get install libi2c-dev

以上2つのコマンドを実行する事でRaspberry PiでI2Cが使えるようになります。
実行結果は以下の通りでした(こちらの環境ではすでにどちらも最新版が入っていました。)

pi@raspberrypi:~ $ sudo apt-get install i2c-tools
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています
状態情報を読み取っています... 完了
i2c-tools はすでに最新バージョン (4.1-1) です。
i2c-tools は手動でインストールしたと設定されました。
以下のパッケージが自動でインストールされましたが、もう必要とされていません:
  libboost-system1.62.0 libboost-thread1.62.0 libreoffice-gtk2
これを削除するには 'sudo apt autoremove' を利用してください。
アップグレード: 0 個、新規インストール: 0 個、削除: 0 個、保留: 0 個。
pi@raspberrypi:~ $ sudo apt-get install libi2c-dev
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています
状態情報を読み取っています... 完了
libi2c-dev はすでに最新バージョン (4.1-1) です。
以下のパッケージが自動でインストールされましたが、もう必要とされていません:
  libboost-system1.62.0 libboost-thread1.62.0 libreoffice-gtk2
これを削除するには 'sudo apt autoremove' を利用してください。
アップグレード: 0 個、新規インストール: 0 個、削除: 0 個、保留: 0 個。

今回はWiringPiおよび特別なライブラリは使用しません。(自作のFONTXライブラリは使いますが)

あとはRaspberry PiのI2Cは標準で100kbpsですので/boot/config.txtに以下の行を追加してI2Cの速度を400kbpsに上げておきましょう。

dtparam=i2c_baudrate=400000

これを追加すると(実際に転送速度が体感速くなっているし)転送速度が上がっている…はず!

ここまで設定すれば、以下のコマンドを実行してデバイスIDが取得する事ができる。

i2cdetect -y 1

すると以下のようなデータが返ってくるので、30行のC列に「3c」と表示されていればOK。

pi@raspberrypi:~ $ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

 転送するデータの仕様

転送データの仕様は秋月にある仕様書の8章に詳細に書いていてI2C関連は19ページの8.1.5から、転送仕様はFigure8-7で図解されていますが。基本的なフォーマットはRaspberry Pi側がやってくれるので、基本的には転送するデータの仕様を伝えるコントロールバイトと実際に転送するコマンド等のデータを管理しておけば良いです。

コントロールバイトの仕様は以下の通りです。

7 6 5 4 3 2 1 0
Co D/C Co:Continuation Bit:
1:1バイトコマンド
0:2バイト以上のコマンド
——————————————
D/C:Data/Command Selection Bit:
0:Command
1:Data

Coに1を立てると1バイトのコマンドとして認識されます。2バイト以上のコマンドやデータは0に設定するようです。
別にどちらでも0でよいようですが、セオリーとしてはこうするようです、
D/Cに1を立てるとデータ、0にするとコマンドとして認識されます。という事でコントロールバイトは以下のようになります。

データ 内容
0x00 2バイト以上のコマンドデータ
0x40 画面に表示する画像データ
0x80 1バイトのコマンドデータ

このデータを転送後、転送したいコマンドやデータを転送すればデータやコマンドが転送できます。

コマンドは一度に必要分転送しても特に問題ないようでした。

データ転送プログラム

さて、以上で前準備は終わりました。続いてデータ転送プログラムを書いていきます。
今回も例に漏れずC言語で(Pythonは嫌いなので)

I2Cデバイスの初期化

まずI2Cデバイスを初期化します。今までI2C通信はWiringPiを使っていましたが、中身を見てみると大したことがなかったので今回は個別の命令に切り出して見ました。

//***** I2Cの初期化コマンド
// devID:転送するI2CデバイスのID
int setup_i2c(const int devID) {
	int fd;
	if ((fd = open(I2CDevicePath, O_RDWR)) < 0) return -1;
	if (ioctl(fd, I2C_SLAVE, devID) < 0)return -1;
	return fd;
}

なんと必要なのはこれだけでした。
デバイスをファイルとしてOpenしてioctlでデバイスを取得。その戻り値を今後I2Cデバイスとして使用すればOKみたいです。

これならデバイス名さえ分かればほとんどのLinux/Unix系オペレーションシステムで同じ方法が使えると思います。

これを利用するには以下のインクルードが必要です。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <asm/ioctl.h>
#include <linux/i2c-dev.h>
#include <fcntl.h>
#include <string.h>
#include <stdbool.h>

なお、上記のインクルードには他の部分に必要なインクルードも含まれています。
今回のプログラムは以上のインクルードがあれば動作します。

上記プログラム中の「I2CDevicePath」にI2Cデバイスのファイルパスを設定します。
ファイルパスは「ls /dev/i2c*」コマンドを打てば出てきます。自分の環境では「/dev/i2c-1」でした。Raspberry Pi 2B以降であれば同じのようです。

このコードを一番最初に実行する事でI2Cデバイスの初期化が行えます。

I2Cデバイスへのデータの転送

I2Cデバイスにデータを転送するのは1行あればで可能です。

write([I2C_Device], [Data], [DataLength]);

[I2C_Device]の所に先ほどI2Cデバイスを初期化した際に戻ってきたInt型の変数を指定します。
[Data]に転送するデータのポインタを渡します。
[DataLength]に転送するデータの大きさを指定します。

ちなみにこの有機ELの場合、コマンドはある程度一括して転送可能のようですが、コマンド系(0x00、0x80で送信するデータ列)とデータ(0x40で送信する実際に表示するビットマップデータ)は別に送る必要があるみたいです。(仕様書には一括転送出来そうな感じに書いてあるのですが)

今回は読み込みは使用しないのですが。ここの方法は前回温度計をいじった時と同じですのでそちらを参考に。

Raspberry Pi 2 で I2C接続の温湿度センサーを使用する(HDC1000)

コマンドは大したことがないので適当に変数作って転送させれば良いと思います。
データ転送コマンドはLSB→MSBの順で描画されるのでWindows系をメインで作成されたデータだとMSBとLSBを入れ替える必要があるので、関数を作りました。

//***** データ転送
// fd:I2Cデバイスを初期化したアレ。
// data:転送するフォントデータの配列
//	rows: 行の開始位置(0-7)  	cols: 列の開始位置(0-127) length:転送するバイト数
//	rowe: 行の終了位置(0-7)    	cole: 列の終了位置(0-127)  revbit:LSBとMSBを反転させるかどうか
int send_data(int fd, char* data, char rows, char rowe, char cols, char cole, int length, bool revbit) {
	const int headersize = 8;
	int cw = length + 1;

	//***** 転送するバッファを作成
	unsigned char* sd = malloc(cw);
	*(sd + 0) = 0x40; //データである事を示すコントロールバイトを先頭に設定
	
	//***** 範囲指定コマンドを作成
	unsigned char sdh[] = {
		0x00,0x21,cols,cole, //列の範囲を設定
		0x00,0x22,rows,rowe //行の範囲を設定
	};

	//***** ビット反転させる場合は反転処理
	if (revbit) {
		int l = length;
		unsigned char x;
		while (l--)
		{
			//参考: 15年4月16日 18:48の回答
			//https://ja.stackoverflow.com/questions/7494/
			x = *(data + l);
			x = ((x & 0x0f) << 4) | ((x >> 4) & 0x0f);
			x = ((x & 0x33) << 2) | ((x >> 2) & 0x33);
			x = ((x & 0x55) << 1) | ((x >> 1) & 0x55);
			*(sd + 1 + l) = x;//コントロールバイト分オフセットする事。
		}
	}
	else {
	//***** 反転させない場合は転送するデータをコピー
		memcpy(sd + 1, data, length);
	}
	//***** 範囲指定コマンドを転送
	write(fd, sdh, headersize);
	//***** データを転送して終了
	write(fd, sd, cw);
	free(sd);
	return 0;
}

大したことをしていないので細かい説明はコメントを見ていただくとして、まず行列の範囲を設定してビットを反転させ、それらのデータを転送しています。

有機ELの初期化

基本的な制御用コマンドができあがったら、有機ELの初期化関数を作ります。
有機ELの初期化は仕様書の最後にチャートがあります。()内はコマンド。16進数2桁0xは省略
特に記載がないものは1バイトコマンド。

  1. SET MUX Ratio(A8)
    横方向の解像度を設定
    2バイトコマンド。
    2バイト目が設定値で0x00から0x3F(63)がの範囲。この数値が直接画面サイズとして使用される。
    この有機ELなら通常0x3Fでよい。
  2. Set Display Offset(D3)
    ディスプレイの縦方向(64pxのほう)のオフセット。1px単位。指定したバイト分オフセットされて循環する。
    2バイトコマンド。
    2バイト目が設定値で0x00から0x3Fが範囲。通常0x00でよい
  3. Det Display Start Line(40-7F)
    ディスプレイの縦方向のスタート位置。1px単位。
    0x40が0pxで0x7F(0x40+0x3F)が63。通常初期化時は0x40でよい。
  4. Set Segment re-map(A0/A1)
    セグメントを左右(長いほう)どちらの方から書き始めるか。
    A0が左から右、A1が右から左
  5. Set COM Output Scan Direction(C0/C8)
    COMをページの上下どちらから描画し始めるか
    C0が上から下、C8が下から上。なおビットの反転はされない模様。
  6. Set COM Pins hardware configuration(DA)
    2バイトコマンド
    よく分からんがCOMのデータをどのように描画するか。という事らしい。
    取説の10.1.18に内容が書かれているのだけどいまいちよく分からん。
    説明書の説明では0x02が設定値だが、0x12に設定したものが正常に表示できた。
  7. Set Contrast Control(81)
    2バイトコマンド
    画面の明るさ(0x00~0xFF 0~255)
    当然大きくなる方が明るくなるが、自分が入手した個体は最大~最小はあまり変わらなかった。
    まぁ初期値の0x7Fで問題になる事はない。
  8. Disable Entire Dispaly On(A4)
    画面の描画をオン。
    これを受けるとRAMのデータを画面に転送するようになる。
    コマンドA5は逆に画面にRAMのデータを転送しなくなる。A5を転送した後画面を描画してA4を転送すれば画面がチラチラしない。
  9. Set Normal Display(A6)
    ビットの表示方法を選択。
    A6が通常の点灯方法(1が点灯で0が消灯)A7がその逆である。
    有機ELは点灯で電力を消費し、寿命が短いので黒背景になるように使うのが良い。
  10. Set Osc Frequency(D5)
    2バイトコマンド。
    ディスプレイクロックとオシレーターの設定。
    メーカー指定値の0x80で良いと思う。
  11. Enable chargepump regulator(8D)
    2バイトコマンド。
    チャージポンプレギュレーターの設定。
    こちらもメーカー指定値の0x14でOK。
  12. Display On(AF)
    ディスプレイ表示をオン。
    AEでディスプレイオフ(スリープする時とか使う。)

以上が仕様書の最後に書いてある初期化方法で、あとはどこかのタイミングで以下の3つを設定してあげれば転送できます。

  • Set Memory Addressing Mode(20)
    2バイトコマンド
    メモリアドレスモード。送られたデータをどのような規則に従って描画するかを決定。
    00:横(列)→縦(行)方向で循環
    指定範囲を列方向で書き込んだ後、行の方向に移動する
    01:縦(行)→横(列)方向で循環
    00の逆。
    この2つが以下の21・22コマンドで描画範囲が設定できるモード。
    10:ページアドレスモード
    B0~B7コマンドを転送してページアドレス(縦 狭いほう)を指定するモード。
    00~0Fで下限スタートコラム、10~1Fで上限スタートコラムを指定して範囲指定するのかな?
    よく分からないので制御できなかった。仕様書の説明だとこれでいいはずだけど。
  • Set Column Address(21)
    3バイトコマンド
    描画する列(長いほう)のアドレスの範囲を設定する。
    コマンド,開始位置,終了位置 の順で3バイト構成。開始終了はそれぞれ0x00~0x7F(0~127)
    ここで指定した横方向のエリア内および下のコマンドで指定した縦方向のエリア内でデータが循環する。
  • Set Page Address(22)
    3バイトコマンド
    描画する行(短いほう)のアドレスの範囲を設定する。
    要領は上のコマンドと同じ。指定値が0~7というだけ。

Set Memory Addressing Modeは今回はすべて同じでかまわないので上記の初期化コマンドの中に入れてしまいました。

Set Column AddressとSet Page Addressはデータを転送する時に設定するのが一番やりやすいので上記のsend_dataコマンドに入れ込みました。

以上の条件でコマンドを組み立てると以下のようになりました。

	unsigned char init_cmd[33] = {
	0x00,0xA8,0x3F,// MUXレートセット 0-3F(64) このOLEDは128x64なので3F
	0x00,0xD3,0x00,// ディスプレイオフセットを0に
	0x80,0x40,// ディスプレイスタートライン0
	0x80,0xA0,//セグメントReMap A0:0→127 A1:127→0
	0x80,0xC0,// COM出力方向 C0:0→7 C8:7→0
	0x00,0xDA,0x12,// COMPin設定 このままでよい。
	0x00,0x81,0x7F,// コントラスト調整 0~255 使ったELだとあまり変わらなかった。
	0x80,0xA4,// ディスプレイOFF
	0x80,0xA6, // ディスプレイ表示 A6:Normal(0:消灯 1:発光)A7:Inverse(0:発光 1:消灯)
	0x00,0xD5,0x80,// OSC周波数 
	0x00,0x8D,0x14,// チャージポンプON
	0x00,0x20,0x01,// メモリアドレスモード設定 00:縦→横 01:横→縦 10:ページアドレスモード ColとRowが入れ替わるわけではない。
	0x80,0xAF // ディスプレイON
	};

これを一度に転送してやると有機ELが使えるようになります。転送コマンドはこの変数を転送するのですが。変数は一度初期化すれば良いのでグローバルで持っておく。よって転送コマンドは非常にシンプルなものになります。

//***** OLEDを初期化する
// fd:I2Cデバイスを初期化したアレ。
int InitOLED(int fd) {
	//***** コマンド転送
	return write(fd, init_cmd, 33);
}

基本的にはwriteコマンドでコマンドデータを転送してあげれば良い。

これでこの有機ELを使う一通りのコマンドは用意できた。

これだけでも十分使えるが転送時に画面の描画を切った方がすっきりするので以下の関数を追加で作成した。

	unsigned char draw_begincmd[] = { 0x80,0xA5 };
	unsigned char draw_ebdcmd[] = { 0x80,0xA4 };

//***** データ表示待ちコマンド これをしておくとチラチラしない
void beginDraw(int fd) {
	write(fd, draw_begincmd, 2);
}
//***** データ表示完了コマンド
void endDraw(int fd) {
	write(fd, draw_ebdcmd, 2);
}

beginDraw関数を実行すれば画面の更新がオフになり、endDrawで描画される。
これを利用すればある程度更新中の画面が隠せる。

さて、ここまで出来れば後は表示したいデータを送るだけなので。ここで終了……でもよいのだが。せっかくなので画像表示とテキストファイル表示を行ってみたい。

画像の転送

今回は64×128のモノクロビットマップを転送しようと思う。
正直Raspberry Piで制御するのであれば、何らかの画像処理ライブラリでメモリに画像として保存してそれを受け取るのが手っ取り早いと思われるので真っ先に紹介する。

画像を64×128にしたのは液晶のRAM構造に合わせるため。RAM構造が縦(短いほう)が1バイト単位で区切られているのでモノクロビットマップと相性が良い。さらにBMP形式は4バイトの境界に合わせる必要があるが、横64でも128でも4バイトで割り切れるので気にする必要がないという利点がある。

処理は教科書にも載ってるような基本的なファイルIOなのでいきなり正解コードを出す。

		//***** ファイルの読み込みとバッファの確保
		FILE* bmp = fopen(argv[2], "rb");
		unsigned char glcd[10240];
		int offset;

		//***** 画像までのオフセットを読み込んでシーク
		fseek(bmp, 10, SEEK_SET);
		fread(&offset, 4, 1, bmp);
		fseek(bmp, offset, SEEK_SET);
		//***** 画像を読み込んで転送
		fread(glcd, 1, 1024, bmp);
		send_data(fd, glcd, 0, 7, 0, 127, 1024, true);
		fclose(bmp);

argv[2]は引数の2番目を拾ってきている。
ここに読み込むBMPファイルを指定する。

プログラムが実行されると、ファイルを開いて、10バイトシーク。そこからInt型として4バイト読み込む。
この4バイトがヘッダーのサイズ(画像データまでのバイトオフセット)なので、先頭からこのサイズ分オフセットをして、1024バイト(128×8バイト)読み込んで先ほど組み立てたsend_dataを使い転送しているだけ。

どうせこの後プログラムは終了するので開放する必要はないが、一応ファイルクローズして終了。

これを実行して何らかの画像を表示すると以下のようになります。

画像であれば事前に90度回転させておくなり好きにして処理が出来るので、表示する項目が決まっている温湿度などの表示であれば画像化して用意しておいて条件に合った画像を転送させるのが簡単で良いでしょう。

テキストデータの表示

テキストデータは文字通り文字コードの集合体なので、何らかの方法で文字コードから文字パターンに変換してあげる必要があります。そこで今回は以前電光掲示板で使用したFONTXのライブラリを使用して表示したいと思います。

FONTX関係の説明はArduinoで使った時に詳細な説明を行っていますのでそちらを。

今回使用するFONTXライブラリはラズパイ2資料にあります。

このライブラリのZIPを解凍して「fontxpi.h」をインクルードすれば良いです。

//オリジナルのFONTX2読み込みライブラリ。
#include "FONTX_PI/fontxpi.h"

上のインクルードパスは実際にこのヘッダファイルを解凍した所に指定します。
解凍して出てきたフォルダをそのままソースファイルと同じ階層に置いた場合は上記のコードそのままです。

テキストファイルを読み込んで表示する処理はこんな感じです。

  1. テキストファイルを開く
  2. 1バイト読み込む 読み込めなかったら終了 ※
    読み込めない=ファイル終端に到達と判断する。
  3. 全角半角をチェック。
    読み込んだ1バイトが全角文字のコードであるかを確認する。
  4. 全角文字であればさらに1バイト読み込む。
  5. 全角文字であれば追加の1バイトと元々の1バイトを結合する。
  6. KanjiReadX関数でフォントパターンを読み込む
  7. 読み込んだフォントパターンを転送する
  8. ※に戻る

これにさらにこのフォントファイルを有機ELの使用にあわせて修正を加えてやる必要があります。
修正する内容は以下の通り

  • Y方向を上下反転させる
    BMP画像が下→上に記録されているから。それにあわせる。
  • さらにX方向が2バイト以上の場合はX方向も左右反転させる
  • そしてビットも左右反転させて転送

読み込むフォントファイルの作成

フォントファイルは既存のFONTXファイルを引っ張ってくるなり、オープンソース系のフォントを変換する等で対応します。
今回はBDFファイルが存在する東雲フォントを変換ツールで変換して使用しました。(ツールの紹介等はArduinoの方でやってますのでそちらで。

変換なんてめんどくせえよ!という方はこの辺のフォントが良いんじゃないかと思います。

  • ぱうフォント公式ミラーサイト
    FONTXファイルあり 16×16 視認性○
    フリーフォント 商用利用×?
  • Izumi Bitmap Font
    FONTXファイルあり 16×16 視認性○
    フォントの中では比較的新しめでJIS X 0213:2004対応版もあり。
    パブリックドメイン
  • 美咲フォント
    FONTXファイルあり 8×8ドット 視認性△
    いつも使ってる美咲フォントにもFONTX版がありますが、最新の書式にするにはBDF等からの変換が必要。

上2つのフォントはもう15年近くまえのものになりますがまぁ使えます。上2つが視認性が良いですが、この液晶サイズで使うならIzumi Bitmapの方が良いかなと思います。

なお、フォントファイルはバイトの境界で穴埋めされますが、今回のプログラムは14や20といった中途半端なフォントサイズのフォントは考慮しておりませんので、表示はされますがその分空白が生まれます。

フォントファイルはプログラムと同じ階層に、全角が「FONTX-J.FNT」半角が「FONTX-A.FNT」というファイル名に変更して配置するか、fontxpi.hを直接編集するもしくはソースコードの方に追加して以下のデファインを変更します。

#define FONT_FILEJ "./FONTX-J.FNT"   // 全角文字用フォントを指定
#define FONT_FILEA "./FONTX-A.FNT"   // 半角文字用フォントを指定

ソースコードに追加する場合はfontxpi.hのインクルードの「前」に追加してください。

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

//オリジナルのFONTX2読み込みライブラリ。
#include "FONTX_PI/fontxpi.h"

↑ぱうフォントを読み込むように設定した例。

フォント転送コアプログラムの作成

という事で以上の条件を元にフォントを転送するコマンドを作ります。最後のビット左右反転はsend_dataでやってあるのでそれ以外の処理をします。

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

	//***** 文字情報からバイト数を計算
	//***** 一応縦横計算しているがX情報のみ使用している。
	int fntyb = (fonty + 7) / 8;
	int fntxb = (fontx + 7) / 8;
	//***** 転送用のデータ領域を確保

KanjiReadX関数の戻り値を分割させています。使用するフォントが全角8×8であれば半角は4×8でどちらも8×8として出てきますので計算する必要はないです。16×16はバイト数が変わってくるので要計算。まぁ汎用性を持たせるなら計算します。
計算内容は上3つがXドット数Yドット数、データサイズの抜き出し。下二つがXYそれぞれのバイト数の計算。

Xのドット数はXバイト数の計算にのみ使用しています。Yのバイト数は現状回転などを行わないので不要です。
よってこの二つは統合・削除できますが一応残してあります。

	//***** 文字コード処理
	if (fntxb == 1) {
		//横が1バイトの場合は単純に上下反転させる
		int c = fontb;
		while (c--)* (data2 + c) = negative ? ~*(data + fontb - c - 1) : *(data + fontb - c - 1);
	}
	else {
		//横が1バイト以上の場合は。1行内で反転させ、上下も反転させる
		for (int i = 0; i < fontb; i += fntxb) {
			int c = fntxb;
			while (c--)* (data2 + i + c) = negative ? ~*(data + fontb - i + c) : *(data + fontb - i + c);
		}
	}
	//***** 文字データを転送しメモリ解放
	send_data(fd, data2, row_start, row_start + fntxb - 1, col_start, col_start + fonty - 1, fontb, true);
	free(data2);
	return 0;

これらの情報を元に、文字データの上下反転とX方向の反転をしている箇所がこれです。
今回は別に元のデータが変わっても問題ないのでバッファ変数を1バイト作っておけば良いのでしょうが。念のため別の変数にメモリを確保してそちらに転送しています。ついでにネガポジ反転も織り込んである。
計算が終わったらデータを転送して終了。

という事でこれをsend_font_dataとして作成します。

ファイルを読み込んでフォントを表示する部分

それではファイルを読み込んで文字コードを解析、フォントデータを取りだして表示する部分を作ります。

		//***** 文字列読み込みループ
		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, data);

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

			//***** 縦方向をビット単位→バイト単位に変換
			int ro = (fx + 7) / 8;

			//***** 文字データ転送。

			//***** 初回のみ1文字分最初に引く
			if (first)ccnt -= fy;
			//***** 横方向の終端まで逝ったら改行
			if (rcnt + ro > 8) {
				ccnt -= fy;
				rcnt = 0;
			}
			//***** フォントデータ転送
			send_font_data(fd, data, rcnt, ccnt, r, neg);
			//***** 1文字分進める
			rcnt += ro;
			first = false;
		}

最後に示す全体のコードではDefineの有無で縦書き・横書きを切り替えられるようにしていますが、これは横書き用のコードです。
まずテキストファイルから1バイト読み込みます。この読み込んだコードの文字コードによって全角・半角を判定し判定結果を保存。

次に最初に読み込んだ1バイトを文字コード用の変数に代入します。

もし全角文字の場合は追加で1文字読み込んで元々読み込んだ1バイトを8bitシフトしてOR演算であわせる。

その後は読み込んでただ転送して転送後は描画位置を転送したフォントサイズ分開始位置を移動させて終了。

という事でこれでShift-JIS形式のテキストファイルを指定すれば読み込まれて有機EL上に表示されます。

例えば以下の文字をテキストファイルに保存して読み込ませるとこうなります。

1月 睦月2月 如月3月 弥生4月 卯月5月 皐月6月 水無7月 文月8月 葉月

上記のコードであれば半角全角対応しており、画面端に到達したら改行されます。
ただし改行コードなどは一切関知しません。試してませんが空白として認識されるかと。

終わりに

という事でグラフィック型有機ELディスプレイを動かしてみました。

この有機ELに搭載されたSSD1306は調べてみると結構多くのSSD。特に128×64の有機ELであれば日本で入手可能なほとんどの有機ELに採用されているみたいなので使用する幅が広がりますね。今回は実証なのであまり複雑な処理は入れていませんけど、基本は押さえているはずなので、各々工夫してみてください。

次回ですがスマートフォンを買い換えたのでそちらのレビューを書こうと思います。
ただし1つ前の機種になるのであまり凝ったものは書きません。

全体のソースコード

以下が全体のソースコード。コメントをたっぷり入れたので詳細はコメントを見てください。
コンパイルは単純にgccでソースファイル名と出力ファイル名を指定すればOK。
ちなみに今回は必要な処理がわかりやすいように最低限のコードのみに絞っているためエラー処理はしていないので適宜追加するようにしてください。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <asm/ioctl.h>
#include <linux/i2c-dev.h>
#include <fcntl.h>
#include <string.h>
#include <stdbool.h>
#define FONT_FILEA "./PAW16A.FNT"   // 全角文字用フォントを指定
#define FONT_FILEJ "./PAW16K.FNT"   // 全角文字用フォントを指定

//オリジナルのFONTX2読み込みライブラリ。
#include "FONTX_PI/fontxpi.h"

//I2Cデバイスのアドレス。
//Address Select が 0x78なら0x3C 0x7Aなら0x3D 表記より1ビット右シフトされている模様。
#define DeviceAdr 0x3C
//Raspberry PiのI2Cデバイスのパス。
// 最後の数字はRaspberry Piによって異なる模様。2B以降は1?
// sudo i2cdetect -y x のxが0でデバイスを認識すれば0、1で認識すれば1。
#define I2CDevicePath "/dev/i2c-1" 
//
//#define I2C_SLAVE	0x0703
//#define TATEGAKI //コメントアウトで横書き

//***** I2Cの初期化コマンド
// devID:転送するI2CデバイスのID
int setup_i2c(const int devID) {
	int fd;
	if ((fd = open(I2CDevicePath, O_RDWR)) < 0) return -1;
	if (ioctl(fd, I2C_SLAVE, devID) < 0)return -1;
	return fd;
}

//***** コマンド転送フォーマット
// ヘッダ:
// 0x00:マルチバイトコマンド用 2バイト以上のコマンドデータはこれを転送後に転送する
// 0x40:データ転送用のヘッダ データを転送する場合はこれを設定
// 0x80:1バイトコマンド転送用ヘッダ
// ヘッダの後にコマンドないしデータをそのまま転送する。
// コマンドは一度に複数転送しても良い模様。

	unsigned char init_cmd[33] = {
	0x00,0xA8,0x3F,// MUXレートセット 0-3F(64) このOLEDは128x64なので3F
	0x00,0xD3,0x00,// ディスプレイオフセットを0に
	0x80,0x40,// ディスプレイスタートライン0
	0x80,0xA0,//セグメントReMap A0:0→127 A1:127→0
	0x80,0xC0,// COM出力方向 C0:0→7 C8:7→0
	0x00,0xDA,0x12,// COMPin設定 このままでよい。
	0x00,0x81,0x7F,// コントラスト調整 0~255 使ったELだとあまり変わらなかった。
	0x80,0xA4,// ディスプレイOFF
	0x80,0xA6, // ディスプレイ表示 A6:Normal(0:消灯 1:発光)A7:Inverse(0:発光 1:消灯)
	0x00,0xD5,0x80,// OSC周波数 
	0x00,0x8D,0x14,// チャージポンプON
	0x00,0x20,0x01,// メモリアドレスモード設定 00:縦→横 01:横→縦 10:ページアドレスモード ColとRowが入れ替わるわけではない。
	0x80,0xAF // ディスプレイON
	};
	unsigned char draw_begincmd[] = { 0x80,0xA5 };
	unsigned char draw_ebdcmd[] = { 0x80,0xA4 };

//***** データ表示待ちコマンド これをしておくとチラチラしない
void beginDraw(int fd) {
	write(fd, draw_begincmd, 2);
}
//***** データ表示完了コマンド
void endDraw(int fd) {
	write(fd, draw_ebdcmd, 2);
}

//***** データ転送
// fd:I2Cデバイスを初期化したアレ。
// data:転送するフォントデータの配列
//	rows: 行の開始位置(0-7)  	cols: 列の開始位置(0-127) length:転送するバイト数
//	rowe: 行の終了位置(0-7)    	cole: 列の終了位置(0-127)  revbit:LSBとMSBを反転させるかどうか
int send_data(int fd, char* data, char rows, char rowe, char cols, char cole, int length, bool revbit) {
	const int headersize = 8;
	int cw = length + 1;

	//***** 転送するバッファを作成
	unsigned char* sd = malloc(cw);
	*(sd + 0) = 0x40; //データである事を示すコントロールバイトを先頭に設定
	
	//***** 範囲指定コマンドを作成
	unsigned char sdh[] = {
		0x00,0x21,cols,cole, //列の範囲を設定
		0x00,0x22,rows,rowe //行の範囲を設定
	};
	
	//***** ビット反転させる場合は反転処理
	if (revbit) {
		int l = length;
		unsigned char x;
		while (l--)
		{
			//参考: 15年4月16日 18:48の回答
			//https://ja.stackoverflow.com/questions/7494/
			x = *(data + l);
			x = ((x & 0x0f) << 4) | ((x >> 4) & 0x0f);
			x = ((x & 0x33) << 2) | ((x >> 2) & 0x33);
			x = ((x & 0x55) << 1) | ((x >> 1) & 0x55);
			*(sd + 1 + l) = x;//コントロールバイト分オフセットする事。
		}
	}
	else {
	//***** 反転させない場合は転送するデータをコピー
		memcpy(sd + 1, data, length);
	}
	//***** 範囲指定コマンドを転送
	write(fd, sdh, headersize);
	//***** データを転送して終了
	write(fd, sd, cw);
	free(sd);
	return 0;
}

//***** 文字の転送
// fd:I2Cデバイスを初期化したアレ。
// data:転送するフォントデータの配列
//	row_start: 行の開始位置(0-7)  	col_start: 列の開始位置(0-127)
// fontdata:KanjiReadXの戻り値のフォント情報 negative:白黒反転するかどうか
int send_font_data(int fd, char* data, char row_start, char col_start, unsigned long fontdata, bool negative) {
	//***** 文字情報を振り分ける
	int fontx = (fontdata >> 24) & 0xff;
	int fonty = (fontdata >> 16) & 0xff;
	int fontb = fontdata & 0xffff;

	//***** 文字情報からバイト数を計算
	//***** 一応縦横計算しているがX情報のみ使用している。
	int fntyb = (fonty + 7) / 8;
	int fntxb = (fontx + 7) / 8;
	//***** 転送用のデータ領域を確保
	unsigned char* data2 = calloc(fontb, 1);

	//***** 文字コード処理
	if (fntxb == 1) {
		//横が1バイトの場合は単純に上下反転させる
		int c = fontb;
		while (c--)* (data2 + c) = negative ? ~*(data + fontb - c - 1) : *(data + fontb - c - 1);
	}
	else {
		//横が1バイト以上の場合は。1行内で反転させ、上下も反転させる
		for (int i = 0; i < fontb; i += fntxb) {
			int c = fntxb;
			while (c--)* (data2 + i + c) = negative ? ~*(data + fontb - i + c) : *(data + fontb - i + c);
		}
	}
	//***** 文字データを転送しメモリ解放
	send_data(fd, data2, row_start, row_start + fntxb - 1, col_start, col_start + fonty - 1, fontb, true);
	free(data2);
	return 0;
}

//***** OLEDを初期化する
// fd:I2Cデバイスを初期化したアレ。
int InitOLED(int fd) {
	//***** コマンド転送
	return write(fd, init_cmd, 33);
}


//***** メイン関数
// 引数の意味:
// [0] 通常プログラム名が入る。入らない環境の場合は無視されるのでなんか入れておく事。
// [1] 転送するデータの切り替え i:BMP画像 t:テキスト(Shift-JIS形式)
// [2] 転送するファイルのパス
void main(int argc, char* argv[]) {
	bool neg = false;
	//***** デバイス初期化
	int fd = setup_i2c(DeviceAdr);

	//***** OLED表示の初期化
	InitOLED(fd);

	//***** データ用・画面削除用のバッファを作成
	char* data = malloc(1024);
	char* clsbuf = calloc(1024, 1);

	//***** 背景を白にする場合以下2行をコメントアウト
	//for (int i = 0; i < 1024; ++i)* (clsbuf + i) = 255;
	//neg = true;
	beginDraw(fd);


	//***** BMP画像を表示する場合の処理 64x128のモノクロBMP画像のみ対応
	if (!strcmp(argv[1], "i")) {
		//***** ファイルの読み込みとバッファの確保
		FILE* bmp = fopen(argv[2], "rb");
		unsigned char glcd[10240];
		int offset;

		//***** 画像までのオフセットを読み込んでシーク
		fseek(bmp, 10, SEEK_SET);
		fread(&offset, 4, 1, bmp);
		fseek(bmp, offset, SEEK_SET);
		//***** 画像を読み込んで転送
		fread(glcd, 1, 1024, bmp);
		send_data(fd, glcd, 0, 7, 0, 127, 1024, true);
		fclose(bmp);
		endDraw(fd);
		return;
	}

	//***** テキストを読み込んで表示。
	//***** 読み込むテキストファイルはShift-JIS形式で保存する事。
	if (!strcmp(argv[1], "t")) {

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

		//***** Row方向はバイト単位で処理される。
		//***** 縦書きモードの時は7→0、横書きの時は0→7で進める。
#ifdef TATEGAKI
		char rcnt = 8;
#else
		char rcnt = 0;
#endif

		//***** Col方向は1pxごとに処理。こちらはどちらでも最後から最初に。
		char ccnt = 128;
		bool first = true;
		//***** 画面クリア
		send_data(fd, clsbuf, 0, 7, 0, 127, 1024, false);

		//***** 文字列読み込みループ
		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, data);

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

			//***** 縦方向をビット単位→バイト単位に変換
			int ro = (fx + 7) / 8;

			//***** 文字データ転送。
#ifdef TATEGAKI
			//***** 縦方向の終端まで行ったら改行処理
			if (ccnt <= 0 || first) {
				rcnt -= ro;
				ccnt = 128;
			}
			//***** 1文字分進める
			ccnt -= fy;
			//***** フォントデータの転送
			send_font_data(fd, data, rcnt, ccnt, r, neg);
#else
			//***** 初回のみ1文字分最初に引く
			if (first)ccnt -= fy;
			//***** 横方向の終端まで逝ったら改行
			if (rcnt + ro > 8) {
				ccnt -= fy;
				rcnt = 0;
			}
			//***** フォントデータ転送
			send_font_data(fd, data, rcnt, ccnt, r, neg);
			//***** 1文字分進める
			rcnt += ro;
#endif
			first = false;
		}
		endDraw(fd);
		fclose(txt);

		//***** メモリ解放
		free(data);
		free(clsbuf);
		return;
	}
}

以上のデータを次のコマンドでコンパイルした場合

gcc ./code/i2c_oled.c -o el

tを第一引数に、その後にファイル名を付けるとテキストファイルとして該当のファイルを読み込んで画面に表示します。

./el t ./code/info.txt

iを第一引数にするとビットマップファイルとして画像を表示します。

./el i ./code/oled.bmp

それでは。

総閲覧数:419 PV

関連記事

コメントを残す

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

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