Arduinoで実験 (グラフィック液晶)

2013/04/28

 

グラフィック液晶を制御出来れば、画像や漢字混じりの日本語の表示ができる。 自分的には当初の目標であったグラフィック表示に挑戦する。 いきなりフルカラーディスプレイとは行かずにモノクロ・ディスプレイから入る。

ディスプレイは秋月電子の 128 x 64 ディスプレイモジュール SG12864ASLB-GB を使用して実験する。 TG12864E の方がサイズとしては Arduino に調度良いのだが ピンのピッチが 2mm なのでブレッドボードで扱え無いのが残念。

バックライト付きグラフィック液晶表示器 SUNLIKE SG12864ASLB-GB
128 x 64 dots  72mm x 40mm
動作電圧 5V

ピン配置 (上の写真では右端が 1番で左端が 20番

  1 Vss (GND)
  2 Vdd (5V)
  3 Vo (コントラスト) VR(10kΩ)
  4 D/I
  5 R/W
  6 E
  7 DB0
  8 DB1
  9 DB2
 10 DB3
 11 DB4
 12 DB5
 13 DB6
 14 DB7
 15 CS1
 16 CS2
 17 RST
 18 Vout (コントラスト)
 19 A バックライト(+) 4.2V (MAX)150mA
 20 K バックライト(-)

ピンの名称はほぼキャラクタ液晶と同じですが D/I、CS1、CA2、が出て来ました。

D/I は RS と同じでデータかコマンドかを選択します。

CS1 と CS2 は、本ディスプレイは横 128ドットの指定に 6ビット使用します。当然 0~63 なので足りません。 左半分を CS1、右半分を CS2 で制御しています。 CS は Chip Select の略で、コントローラーが 64 x 64 のドットマトリックスを制御しているためです。 そのため 128 x 64 ドットのディスプレイでは 2つのコントローラーを使用して制御しています。 もし、内蔵コントローラーが 128 x 64 ドットをワンチップで制御できるのであれば、チップセレクトは必要ないでしょう。

独立したチップが2つ内蔵されているので、初期化のプロセスも各々のチップに対して行います。


◆ コマンドを送る関数を作る

基本はキャラクタ液晶の制御と同じくパラレルバスで1バイトのデータを転送する。 キャラクと液晶と比べ扱うデータ量が多くなるので 4ビットモードのようなピンの節約モードは使用しません。 また、現在表示されている内容を読み取る場合があるので R/W も活用します。

手始めにパラレルバスで 1バイトのデータを転送する関数を作成します。 処理速度的に有効なプログラム手法よりも遅いが理解しやすい方法での記述を心がける。

//
// 09-01 グラフィック液晶(基本関数)
//

// ピンの設定をする。
byte pins_DB[] = {0, 1, 2, 3, 4, 5, 6, 7};  // DB0~DB7
#define pins_RS 10
#define pins_RW 9
#define pins_E 8
#define pins_CS1 11
#define pins_CS2 12
#define pins_RST 13

void setup() {
  initGlcd();  // GLCD の初期化
  glcdCLS();   // CLS

// 確認用に A を表示
  chipSelect(0);
  writeData(0x7C);
  writeData(0x12);
  writeData(0x11);
  writeData(0x12);
  writeData(0x7C);
}

void loop() {
}

// チップの選択
void chipSelect(boolean cs) {
  if (cs == 0) {
    digitalWrite(pins_CS1, HIGH); // cs = 0 chip 1
    digitalWrite(pins_CS2, LOW);
  } else {
    digitalWrite(pins_CS1, LOW);  // cs = 1 chip 2
    digitalWrite(pins_CS2, HIGH);
  }
}

// ポートのセットと送信
void writeBUS(boolean rs, boolean rw, byte dat) {

  digitalWrite(pins_RS, rs);  // RS をセット
  for (int i=0; i<8; i++) {   // データをバスにセット
    digitalWrite(pins_DB[i], (dat >> i) & 0x01);
  }

  digitalWrite(pins_E, HIGH); // イネーブルをラッチ
  digitalWrite(pins_E, LOW);
}

// コマンドの送信
void writeCommand(byte dat) {
  writeBUS(0, 0, dat);  // RS=0, RW=0
}

// データの送信
void writeData(byte dat) {
  writeBUS(1, 0, dat);  // RS=1, RW=0
}

// アドレスのセット
void setAddress(byte col, byte row) {
  writeBUS(0, 0, 0x40 | (col & 0x3F)); // set address (0-63)
  writeBUS(0, 0, 0xB8 | (row & 0x07)); // set page (0-7)
}


◆ コマンド

SG12864A に対するコマンドの詳細。 キャラクタ液晶ほど複雑なコマンドではない。

アドレスは、横方向はドット毎のアドレスで指定するが、縦方向はページ単位で指定する。 1ページは 1バイトで上部方向から株方向へ向かい D0~D7 となる。縦 8ドットや 16ドットの場合は扱いやすい単位だが、12ドットだととたんに扱いにくくなる。 本液晶は 1ドット当たりの大きさがキャラクタ液晶並みなので 8ドットのフォントで実用になるが、サイズの小さいディスプレイの場合は工夫をしなければならない。

名称 RS RW DB
7 6 5 4 3 2 1 0
Display
on/off
0 0 0 0 1 1 1 1 1 D
Set Address Y 0 0 0 1 Y address (0-63)
Set page X 0 0 1 0 1 1 1 page (0-7)
Display start line 0 0 1 1 start line (0-63)
Status read 0 1 busy 0 on/off Reset 0 0 0 0
Write data 1 0 data
Read data 1 1 data

・Display on/off

ディスプレイの表示/非表示の切り替え D=1 : on、D=0 : off

・Set Address Y

ディスプレイの横位置アドレスの指定。0 ~ 63 までのアドレスなので CS1 か CS2 の選択が必要

・Set Address X

ディスプレイの縦位置ページの指定。 0~7 での指定。

・Display Start Line

ディスプレイの先頭表示横アドレスを指定する。0 ~ 63 までのアドレスなので CS1 か CS2 の選択が必要

・Status Read

現在の状態を調べる

BUSY = 1 : 処理中 / BUSY = 0 : 待機中

ON = 1 : 表示中 / OFF = 0 : 非表示中

RESET = 1 : リセット / RESET = 0 : 標準

・Write Display Data / Read Display Data

1バイトのデータの書き出し/読み込み


◆ 初期化

データシートにある初期化手順は以下の通り。

初期化手順
RESET = HIGH  リセットを解除
delay 30ms    30ms 待機
(a) Display off.
(b) Display start line register : First line.
(c) Static drive off.
(d) Column addres counter : Address 0.
(e) Page address register : Page 0.
(f) Select duty : 1 / 64 duty.

秋月の AVR サンプルでは RESET ピンを HIGH にしてモジュールで初期化しているようだが、ks0108 等ではソフトウェアでリセット作業を行っている。 学習とピンの節約を含めてソフトウェアでのリセット処理をプログラムする。

//
// 09-02 グラフィック液晶(初期化関数)
//

void setup() {
  initGlcd();  // GLCD の初期化
  glcdCLS();   // CLS
}

void loop() {
// 確認用に A を表示
  chipSelect(0);
  writeData(0x7C);
  writeData(0x12);
  writeData(0x11);
  writeData(0x12);
  writeData(0x7C);

  while (1);
}


// グラフィック液晶の初期化
void initGlcd(void) {

  // ポート設定
  pinMode(pins_RS, OUTPUT);
  pinMode(pins_RW, OUTPUT);
  pinMode(pins_E, OUTPUT);
  pinMode(pins_CS1, OUTPUT);
  pinMode(pins_CS2, OUTPUT);
  pinMode(pins_RST, OUTPUT);
  for(int i=0; i<8; i++) pinMode(pins_DB[i], OUTPUT);

  // 初期状態として LOW に設定
  digitalWrite(pins_RS, LOW);
  digitalWrite(pins_RW, LOW);
  digitalWrite(pins_E, LOW);
  digitalWrite(pins_CS1, LOW);
  digitalWrite(pins_CS2, LOW);
  digitalWrite(pins_RST, HIGH); // Reset 解除

  delay(30);

  // chip 1
  chipSelect(0);
  writeCommand(0xC0); // Display Start Line = 0
  writeCommand(0x3F); // Display On

  // chip 2
  chipSelect(1);
  writeCommand(0xC0);
  writeCommand(0x3F);
}

// Clear Display & Return Home
void glcdCLS(void) {
  byte col, row, i;

  for(i=0; i<2; i++) {
    chipSelect(i);        // チップを選択
    for(row=0; row<8; row++) {
      setAddress(0,row);  // アドレスをセット
      for(col=0; col<64; col++) {
        writeData(0);     // 0x00 を送信
      }
    }
  }
  setAddress(0, 0);       // Return Home
}

上記の2つの関数を最初のスケッチに追加し、setup() と loop() を変更して実行すると、画面の左上に A と表示される。


◆ 文字表示

グラフィック液晶はフォントデータを持っていないので、別途フォントデータを用意する必要があります。 サンプルのフォントデータは 84 文字ですが 420 バイト使います。 ATmega328P は SRAM が 2kバイトなのでこれだけで 20%必要とします。 他にカナや 12 ドットのフォントを使用するとメモリが足りなくなります。 そのためフラッシュメモリ上にデータを置く為の <avr/pgmspace.h> を使用します。

次は 5 x 8 のフォントデータで、ファイル名を font.h としてスケッチと同じフォルダに保存します。 本体スケッチで "font.h" のインクルード前に #include <avr/pgmspace.h> をインクルードする必要があります。

カタカナを含めたフォントデータはここに置いておきます。

// ***********************************************
// * font.h 5 x 8 ドットフォント                 *
// * キャラクタコード 0x20 ~ 0x7F               *
// ***********************************************

static const prog_uint8_t font[][5] PROGMEM = {
  { 0x00, 0x00, 0x00, 0x00, 0x00 },  // 20 (SPC)
  { 0x00, 0x00, 0x4f, 0x00, 0x00 },  // 21 !
  { 0x00, 0x07, 0x00, 0x07, 0x00 },  // 22 "
  { 0x14, 0x7f, 0x14, 0x7f, 0x14 },  // 23 #
  { 0x24, 0x2a, 0x7f, 0x2a, 0x12 },  // 24 $
  { 0x23, 0x13, 0x08, 0x64, 0x62 },  // 25 %
  { 0x36, 0x49, 0x55, 0x22, 0x50 },  // 26 &
  { 0x00, 0x05, 0x03, 0x00, 0x00 },  // 27 '
  { 0x00, 0x1c, 0x22, 0x41, 0x00 },  // 28 (
  { 0x00, 0x41, 0x22, 0x1c, 0x00 },  // 29 )
  { 0x14, 0x08, 0x3e, 0x08, 0x14 },  // 2A *
  { 0x08, 0x08, 0x3e, 0x08, 0x08 },  // 2B +
  { 0x00, 0x50, 0x30, 0x00, 0x00 },  // 2C ,
  { 0x08, 0x08, 0x08, 0x08, 0x08 },  // 2D -
  { 0x00, 0x60, 0x60, 0x00, 0x00 },  // 2E .
  { 0x20, 0x10, 0x08, 0x04, 0x02 },  // 2F /
  { 0x3e, 0x51, 0x49, 0x45, 0x3e },  // 30 0
  { 0x00, 0x42, 0x7f, 0x40, 0x00 },  // 31 1
  { 0x42, 0x61, 0x51, 0x49, 0x46 },  // 32 2
  { 0x21, 0x41, 0x45, 0x4b, 0x31 },  // 33 3
  { 0x18, 0x14, 0x12, 0x7f, 0x10 },  // 34 4
  { 0x27, 0x45, 0x45, 0x45, 0x39 },  // 35 5
  { 0x3c, 0x4a, 0x49, 0x49, 0x30 },  // 36 6
  { 0x01, 0x71, 0x09, 0x05, 0x03 },  // 37 7
  { 0x36, 0x49, 0x49, 0x49, 0x36 },  // 38 8
  { 0x06, 0x49, 0x49, 0x29, 0x1e },  // 39 9
  { 0x00, 0x36, 0x36, 0x00, 0x00 },  // 3A :
  { 0x00, 0x56, 0x36, 0x00, 0x00 },  // 3B ;
  { 0x08, 0x14, 0x22, 0x41, 0x00 },  // 3C <
  { 0x14, 0x14, 0x14, 0x14, 0x14 },  // 3D =
  { 0x00, 0x41, 0x22, 0x14, 0x08 },  // 3E >
  { 0x02, 0x01, 0x51, 0x09, 0x06 },  // 3F ?
  { 0x32, 0x49, 0x79, 0x41, 0x3e },  // 40 @
  { 0x7e, 0x11, 0x11, 0x11, 0x7e },  // 41 A
  { 0x7f, 0x49, 0x49, 0x49, 0x36 },  // 42 B
  { 0x3e, 0x41, 0x41, 0x41, 0x22 },  // 43 C
  { 0x7f, 0x41, 0x41, 0x22, 0x1c },  // 44 D
  { 0x7f, 0x49, 0x49, 0x49, 0x41 },  // 45 E
  { 0x7f, 0x09, 0x09, 0x09, 0x01 },  // 46 F
  { 0x3e, 0x41, 0x49, 0x49, 0x7a },  // 47 G
  { 0x7f, 0x08, 0x08, 0x08, 0x7f },  // 48 H
  { 0x00, 0x41, 0x7f, 0x41, 0x00 },  // 49 I
  { 0x20, 0x40, 0x41, 0x3f, 0x01 },  // 4A J
  { 0x7f, 0x08, 0x14, 0x22, 0x41 },  // 4B K
  { 0x7f, 0x40, 0x40, 0x40, 0x40 },  // 4C L
  { 0x7f, 0x02, 0x0c, 0x02, 0x7f },  // 4D M
  { 0x7f, 0x04, 0x08, 0x10, 0x7f },  // 4E N
  { 0x3e, 0x41, 0x41, 0x41, 0x3e },  // 4F O
  { 0x7f, 0x09, 0x09, 0x09, 0x06 },  // 50 P
  { 0x3e, 0x41, 0x51, 0x21, 0x5e },  // 51 Q
  { 0x7f, 0x09, 0x19, 0x29, 0x46 },  // 52 R
  { 0x46, 0x49, 0x49, 0x49, 0x31 },  // 53 S
  { 0x01, 0x01, 0x7f, 0x01, 0x01 },  // 54 T
  { 0x3f, 0x40, 0x40, 0x40, 0x3f },  // 55 U
  { 0x1f, 0x20, 0x40, 0x20, 0x1f },  // 56 V
  { 0x3f, 0x40, 0x38, 0x40, 0x3f },  // 57 W
  { 0x63, 0x14, 0x08, 0x14, 0x63 },  // 58 X
  { 0x07, 0x08, 0x70, 0x08, 0x07 },  // 59 Y
  { 0x61, 0x51, 0x49, 0x45, 0x43 },  // 5A Z
  { 0x00, 0x7f, 0x41, 0x41, 0x00 },  // 5B [
  { 0x02, 0x04, 0x08, 0x10, 0x20 },  // 5C (YEN)
  { 0x00, 0x41, 0x41, 0x7f, 0x00 },  // 5D ]
  { 0x04, 0x02, 0x01, 0x02, 0x04 },  // 5E ^
  { 0x40, 0x40, 0x40, 0x40, 0x40 },  // 5F _
  { 0x00, 0x01, 0x02, 0x04, 0x00 },  // 60 `
  { 0x20, 0x54, 0x54, 0x54, 0x78 },  // 61 a
  { 0x7f, 0x48, 0x44, 0x44, 0x38 },  // 62 b
  { 0x38, 0x44, 0x44, 0x44, 0x20 },  // 63 c
  { 0x38, 0x44, 0x44, 0x48, 0x7f },  // 64 d
  { 0x38, 0x54, 0x54, 0x54, 0x18 },  // 65 e
  { 0x08, 0x7e, 0x09, 0x01, 0x02 },  // 66 f
  { 0x0c, 0x52, 0x52, 0x52, 0x3e },  // 67 g
  { 0x7f, 0x08, 0x04, 0x04, 0x78 },  // 68 h
  { 0x00, 0x44, 0x7d, 0x40, 0x00 },  // 69 i
  { 0x20, 0x40, 0x44, 0x3d, 0x00 },  // 6A j
  { 0x7f, 0x10, 0x28, 0x44, 0x00 },  // 6B k
  { 0x00, 0x41, 0x7f, 0x40, 0x00 },  // 6C l
  { 0x7c, 0x04, 0x18, 0x04, 0x78 },  // 6D m
  { 0x7c, 0x08, 0x04, 0x04, 0x78 },  // 6E n
  { 0x38, 0x44, 0x44, 0x44, 0x38 },  // 6F o
  { 0x7c, 0x14, 0x14, 0x14, 0x08 },  // 70 p
  { 0x08, 0x14, 0x14, 0x18, 0x7c },  // 71 q
  { 0x7c, 0x08, 0x04, 0x04, 0x08 },  // 72 r
  { 0x48, 0x54, 0x54, 0x54, 0x20 },  // 73 s
  { 0x04, 0x3f, 0x44, 0x40, 0x20 },  // 74 t
  { 0x3c, 0x40, 0x40, 0x20, 0x7c },  // 75 u
  { 0x1c, 0x20, 0x40, 0x20, 0x1c },  // 76 v
  { 0x3c, 0x40, 0x30, 0x40, 0x3c },  // 77 w
  { 0x44, 0x28, 0x10, 0x28, 0x44 },  // 78 x
  { 0x0c, 0x50, 0x50, 0x50, 0x3c },  // 79 y
  { 0x44, 0x64, 0x54, 0x4c, 0x44 },  // 7A z
  { 0x00, 0x08, 0x36, 0x41, 0x00 },  // 7B {
  { 0x00, 0x00, 0x7f, 0x00, 0x00 },  // 7C |
  { 0x00, 0x41, 0x36, 0x08, 0x00 },  // 7D }
  { 0x08, 0x08, 0x2a, 0x1c, 0x08 },  // 7E (Right arrow)
  { 0x08, 0x1c, 0x2a, 0x08, 0x08 }   // 7F (Left arrow)
};
  

文字表示のための関数を作成します。 各チップは横 64 ドットなので 5 ドット + 1(文字間)の 6 で割り切れません。 そのため CS1 は左 4 ドット、CS2 は右 4 ドットに書き込まないようにします。

文字が右端を超える場合は画面上の改行をせずに表示を切り捨てます。

//
// 09-03 グラフィック液晶(文字表示)
//

#include <avr/pgmspace.h>
#include "font.h"

void setup() {
  initGlcd();  // GLCD の初期化
  glcdCLS();   // CLS
}

void loop() {
  chipSelect(0);             // チップの選択
  setAddress(0, 0);          // 表示位置の指定
  glcdPRINT("Hello World");  // 文字列の表示

  while (1);
}

// 1文字出力
void putChar(byte b) {
  for (int i=0; i<5; i++) {
    writeData( pgm_read_byte_near( &font[b-0x20][i] ) );  // フォントを読み込みデータを出力
  }
  writeData(0);  // 文字間隔を 1ドット分あける。
}

// 文字列の表示
void glcdPRINT(char *c) {
  while(*c) {
    putChar(*c++);
  }
}

上記の関数を前述のスケッチに追加して実行すると Hello World の文字が表示されます。 ただし world の d の右がいくらか欠けています。 横 64 ドットを超えてしまい表示ができなくなったためなので、これを回避するためには関数内でチップの選択とアドレスの指定を行わなければなりません。

チップをまたぐ処理の場合いくつかの方法が考えられます。 1つめはモジュールから現在のアドレスを取得する方法ですが 1バイト書き込むごとにアドレスを読み込むと処理が多くなり、実行速度の点であまり実用的ではありません。

それではグローバル変数でアドレスを管理する方法をとれば、モジュール間の通信が不要になるので速度的にも問題のないものとなります。

アドレスの指定時に変数を初期化し、1バイト書き込み毎に変数を加算してゆけばチップをまたぐ個所で判断でき、チップの選択とアドレスの指定を行う事ができます。 変数はアドレスのみでページの管理は不要となります。 ただし、この方法だとプログラム全体でアドレスの管理が必要となり、不意のミスが出かねません。

もうひとつは、文字列表示関数の引数に文字位置を含ませる方法です。

この方法だとアドレスの管理が関数内だけとなり、他の関数との関連も考慮せずに済みます。

以下はチップをまたぐ処理を付け加えた文字列の表示関数です。 一文字表示関数は文字列表示関数からしか呼び出されないので、文字列表示関数内に含めることにする。

//
// 09-04 グラフィック液晶(チップをまたぐ文字列の表示)
//

void loop() {
  glcdPRINT(12, 3, "Hello World");

  while(1);
}

// チップをまたぐ文字列の表示
void glcdPRINT(byte col, byte row, char *c) {

  // 一文字目のアドレスをセット
  if (col < 64) {
    chipSelect(0);
  } else {
    chipSelect(1);
    col -= 64;
  } 
  setAddress(col, row);

  while(*c) {
    byte b = *c++;
    for (int i=0; i<5; i++) {
      writeData( pgm_read_byte_near( &font[b-0x20][i] ) ); // フォントを読み込みデータを出力
      col++;
      if (col > 63) {
        chipSelect(1);
        col = 0;
        setAddress(col, row);
      }
    }
    writeData(0); // 文字間隔を 1ドット分あける。
    col++;
    if (col > 63) {
      chipSelect(1);
      col = 0;
      setAddress(col, row);
    }
  }
}

例のごとく loop() と関数の修正を実行すると文字がチップをまたいでいるのが解ります。


◆ ドット指定での文字表示

先ほどはページ単位の文字表示なのでそんなに手間ではなかったけど、ページをまたぐドット指定での文字表示に挑戦します。

ページをまたぐ場合には、既に表示されているデータを読み取り、書き込みデータを合成してから書き込むという手順を踏むために液晶モジュールからデータを取得する関数を作成します。

以下は一度表示した文字を複写するサンプルです。

//
// 09-05 グラフィック液晶(モジュール内データの読み取り)
//

void loop() {
  char tmp;

  glcdPRINT(12, 3, "Hello World");  // 一度文字表示をする。
  delay(2000);

  chipSelect(0);
  for (int i=0; i<20; i++) {  // 表示データを読み取り別アドレスに書き込み
    setAddress(i + 12, 3);    // 読み取りアドレスの指定
    tmp = readData();         // データ読み取り
    setAddress(i + 12, 4);    // 書き込みアドレスの指定
    writeData(tmp[i]);        // データ書き込み
  }


  while(1);
}

// 既に表示されているデータを読み取る
byte readData(void) {
  byte ret = 0;  // 戻り値用変数

  for (int i=0; i<8; i++) pinMode(pins_DB[i], INPUT);  // DB0~DB7を入力モードに設定
  digitalWrite(pins_RS, HIGH);
  digitalWrite(pins_RW, HIGH);
  digitalWrite(pins_E,  HIGH);
  digitalWrite(pins_E,  LOW);
  digitalWrite(pins_E,  HIGH);

  for (int i=0; i<8; i++) ret += (digitalRead(pins_DB[i]) << i));
  digitalWrite(pins_E,  LOW);
  digitalWrite(pins_RW, LOW);
  for (int i=0; i<8; i++) pinMode(pins_DB[i], OUTPUT); // DB0~DB7を出力モードに設定

  return (ret);
}

上記の読み取り関数を使用すると、2度ラッチするので 1バイト読み込むと液晶モジュールのポインタが2つ進みます。 本来であれば読み取り範囲を指定し、ポインタを使用して配列データ返す方法の方が命令が少なくて済みます。

先般のスケッチでは文字列表示関数内で一文字の表示を行っていましたが、一文字表示関数を作成します。 一文字表示関数の処理がよくわからなくなるので動作アルゴリズムを一度ひも解いてみます。

ドット指定の文字表示アルゴリズム(一文字)

関数への引数には、文字のコードの他に表示開始位置として文字の左上の座標値を与える。

縦の座標値でページ内でのずれを得る。


文字を描画、6回のループ(文字間の空白を 1ドット入れる)

    フォントデータを読み取る
  6回目のループのみデータは 0

  ずれが無い場合の処理
    フォントデータを書き込む

  ずれがある場合の処理
  (フォント上部の処理)
    既に表示されているデータを読み取る。
    読み取りデータの合成部分をマスクする
    フォントデータの合成部分以外をマスクする
    読み取りデータとフォントデータを合成して書き込む
  (フォント下部の処理)
    一つ下のページに表示されているデータを読み取る。
    読み取りデータの合成部分をマスクする
    フォントデータの合成部分以外をマスクする
    読み取りデータとフォントデータを合成して書き込む

上記を参考に実際に関数を作成します。

//
// 09-06 グラフィック液晶(一文字表示関数の作成)
//

void loop() {
  glcdPRINT(0, 0, "HELLO WORLD");
  delay(1000);

  glcdPRINT(2, 2, "HELLO ARDUINO");
  delay(1000);

  glcdPRINT(4, 4, "THIS IS SAMPLE.");

  while (1);
}

// ページをドット単位で指定し、アドレスでチップを選択する関数
void glcdLocate(byte x, byte y) {
  if (x < 64) {         // アドレスでチップを選択
    chipSelect(0);
  } else {
    chipSelect(1);
    x -= 64;
  }
  setAddress(x, y / 8);
}

// ページとチップをまたぐ一文字表示関数
void putChar(byte x, byte y, char c) {
  byte gap = y % 8;
  byte rDat, fDat, tDat;

  for (int i=0; i<6; i++) {
    if (i < 5) {
      fDat = pgm_read_byte( &font[c-0x20][i] ); // フォントデータを準備
    } else {
      fDat = 0;
    } 
    if (gap == 0) {                             // ページをまたがない処理
      glcdLocate(x + i, y);
      writeData(fDat);
    } else {                                    // ページをまたぐ処理
  // 上部の処理
      glcdLocate(x + i, y);
      rDat = readData() & (0xff >> (8 - gap));  // 読み取りデータの合成部にマスクする。
      tDat = fDat << gap;                       // 表示部分だけ左詰めにする
      glcdLocate(x + i, y);
      writeData(tDat + rDat);                   // データを合成し、書き込む。
  // 下部の処理
      glcdLocate(x + i, y + 8);
      rDat = readData() & (0xff << gap);        // 読み取りデータの合成部にマスクする。
      tDat = fDat >> (8 - gap);                 // 表示部分だけ右詰めにする
      glcdLocate(x + i, y + 8);
      writeData(tDat + rDat);                   // データを合成し、書き込む。
    }
  }
}

// 文字列表示関数
void glcdPRINT(byte x, byte y, char *c) {
  while(*c) {
    putChar(x, y, *c++);
    x += 6;
  }
}

◆ 12 x 12 ドットの漢字を表示する。

12 x 12 ドットの漢字を表示する場合、3ページにまたがる表示となるが、基本は先ほどの文字表示と同じである。

ずれとまたがるページ数の関係

ずれ = 0 → 8ドットと 4ドット
ずれ = 1 → 7ドットと 5ドット
ずれ = 2 → 6ドットと 6ドット
ずれ = 3 → 5ドットと 7ドット
ずれ = 4 → 4ドットと 8ドット
ずれ = 5 → 3ドットと 8ドットと 1ドット
ずれ = 6 → 2ドットと 8ドットと 2ドット
ずれ = 7 → 1ドットと 8ドットと 3ドット

下記サンプルは漢字データ、漢字一文字表示関数と、漢字文字列関数です。

//
// 09-07 グラフィック液晶(12x12ドットの漢字表示
//

// 漢字データ(12x12)
static const prog_uint8_t kFont[][24] PROGMEN = {
  {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // (SPC)
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
 ,{0x24, 0x48, 0x00, 0x04, 0x74, 0x5F, 0x54, 0xF4, 0x5F, 0x54, 0x74, 0x00, // 漢
   0x0C, 0x03, 0x00, 0x09, 0x09, 0x05, 0x03, 0x01, 0x03, 0x05, 0x09, 0x00}
 ,{0x1C, 0x04, 0x14, 0x14, 0x14, 0x97, 0x54, 0x34, 0x04, 0x04, 0x1C, 0x00, // 字
   0x01, 0x01, 0x01, 0x09, 0x09, 0x0F, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00}
};

void loop() {
  char test[] = {1,2};

  for (int i=0; i<12; i++) {
    glcdPRINT(0,0,"Hello Wrold");
    glcdPRINT(0,8,"Hello Wrold");
    glcdPRINT(0,16,"Hello Wrold");
    glcdKPRINT(i, i, test);
    delay(1000);
  }
  while (1);
}

// 12x12の漢字一文字を表示する関数
void putKChar(byte x, byte y, char c) {
  byte gap = y % 8;
  byte rDat, tDat;
  unsigned int fDat;

  for (int i=0; i<12; i++) {
    fDat = pgm_read_byte ( &kFont[c][i+12] );   // フォントデータを準備
    fDat = fDat << 8;
    fDat += pgm_read_byte ( &kFont[c][i] );
  // 上部の処理
    glcdLocate(x + i, y);
    rDat = readData() & (0xff >> (8 - gap));    // 読み取りデータの合成部にマスクする。
    tDat = fDat << gap;                         // 表示部分だけ左詰めにする
    glcdLocate(x + i, y);
    writeData(tDat + rDat);                     // データを合成し、書き込む。
  // 下部の処理
    if (gap < 4) {                              // 2ページをまたぐ処理
      glcdLocate(x + i, y + 8);
      rDat = readData() & (0xff << (4 + gap));  // 読み取りデータの合成部にマスクする。
      tDat = fDat >> (8 - gap);
      glcdLocate(x + i, y + 8);
      writeData(tDat + rDat);                   // データを合成し、書き込む。
    } else {                                    // 3ページをまたぐ処理
  // 中間部の処理
      tDat = fDat >> (8 - gap);
      glcdLocate(x + i, y + 8);
      writeData(tDat);                          // 中間部のデータを書き込む。
  // 下部の処理
      glcdLocate(x + i, y + 16);
      rDat = readData() & (0xff << (gap - 4));  // 読み取りデータの合成部にマスクする。
      tDat = fDat >> (16 - gap);
      glcdLocate(x + i, y + 16);
      writeData(tDat + rDat);                   // データを合成し、書き込む。
    }
  }
}

// 文字列表示関数
void glcdKPRINT(byte x, byte y, char *c) {
  while(*c) {
    putKChar(x, y, *c++);
    x += 12;
  }
}

以上で文字表示に関する部分は一応終了します。 結構サンプルスケッチが長くなりわけわからなくなったので書庫にしたものをここに置いておきます。