|
液晶モニタ
イントロダクション ここまではきっとつまらない内容だったと思います。書いてる本人もあまり面白い話題ではありませんでした。唯一手応えがあったのはシリアル通信だけではないでしょうか? そう思う理由はきっと、いくらプログラムを組んでみたところで、マイコンの反応が乏しかったからだと思います。
その点、液晶モニタ(LCD)が使えだすと状況が変わります。デバッグの役にも立ちますし、実用的にも役に立ちます。そして何より自分が動かしているという実感が持てます。
ここで扱うのはノートパソコンなどに搭載されているような巨大なフルカラーの物ではなく、もっとショボイ液晶ですが、動かす理屈が分からなければウンともスンとも言いません。まったくのゼロから始めた場合、この基礎講座の中ではSRAM増設に次ぐ難易度ではないでしょうか。他はやってることはそこそこ高度でも、結局は適切にレジスタに値を書けばやってくれるので案外簡単なんですよ。
LCDの紹介 これから扱うLCDモニタはサンライクという海外メーカーの液晶モニタです。半角英数字と、半角カタカナを扱うことができる、16桁×2行のSC1602BSLBという物です。 これに慣れたら同メーカーから20桁×4行の製品も出ていますので、挑戦してみると面白いと思います。
LCDの概要 LEDやスイッチといったなじみ安い物とは違って、初めて本格的な外部機器を繋ぐわけですが、そもそもどうやれば動く物なのか最初は見当も付かないと思います。 まず、表示できる文字の種類を上に述べましたが、これらはアスキーコード表にある文字ばかりで、言い換えれば1バイトで表現できる文字です。ということは、液晶モニタに送りつけるデータはchar型かunsigned
char型であることは見当が付くと思います。 次に、文字を表示しようにも、任意の場所に表示するにはそうすればよいとか、画面を消去するにはどうすればいいとか、そういう命令コード(インストラクションコードと言います)が存在することも予測が付くと思います。 さらに、電気を流せば灯るとか、そういう単純な物でない以上、初期化という作業が必要であるということも分かるでしょう。
さて、ここまでの事から考えると、まずこの液晶モニタにはchar型のデータを送りつけるために8本の線(DB0〜DB7)が必要であることが分かります。次に、それが文字コードなのかインストラクションコードなのかを見分けるためにもう1本信号線(R/S線)が必要です。さらに、RS-232C通信のように厳密なコンマ数msecの時間誤差も許されないような制御はしたくないので、データの書き込みタイミングを知らせる信号線(E)も有った方が良いでしょう。さらに、瞬時に文字を表示できるわけではないので、液晶の状態をマイコン側に伝える線も必要です(RW)。もちろん電源線(Vcc)とGND(Vss)は必須です。 というわけで、必要性から液晶モニタには最低でも13本の端子があることが分かります(実際にはバックライト用電源もあるので14本です)。存在理由さえ分かれば、これであのやたらといっぱい生えている端子も怖くなくなったのではないでしょうか。
さて、たしかに13本とも繋いでも良いのですが、「ハードウェアはできるだけシンプルに。ソフトでできる物はできるだけソフトでやる」が回路を設計する上での全世界共通の揺るがぬ鉄則です。そこで、この液晶に付いてもいくつか細工をします。
まず、液晶の状態(Busy or Ready)を調べるために必要なRWの線ですが、よほど急がない限り要はBusyの時にデータを送らなければいいので、たっぷりと待ち時間をとってやることで解決します。というわけで、RW線(Read
/ Write)はWriteの一方通行と言うことでGNDに落とします。 次に、データバスですが、幸いにもこのLCDモニタには4ビットモードというモードがあります。つまり、上位4ビットと下位4ビットを分けて書き込むことでデータバスを半分にできるのです。というわけで、使わないデータバス(DB0〜DB3)もGNDに落とします。これで配線は13本から一気に8本に減りました。
LCDとH8を接続 秋月電子のマザーボードキットでは標準でこのLCDモニタを接続するパターンが有りますので、プログラムもそれに合わせましょう。マザーボードの結線は次のようになっています。
|
H8/3048F
|
SC1602BSLB
|
|
Port3-0
|
DB4
|
|
Port3-1
|
DB5
|
|
Port3-2
|
DB6
|
|
Port3-3
|
DB7
|
|
Port3-4
|
RS
|
|
Port3-5
|
E
|
|
5V
|
Vcc
|
|
GND
|
Vss
|
ところで、ここまでのサンプルを見ていると、出力というとPort-Bを使いたい衝動に駆られる人もいるかも知れませんが、あれはLEDを直接駆動したいから大電力ポートを使ったのであって、今回は信号さえ送れればそれでいいのでPort-3で十分役を果たせるのです。
LCDへの書き込みタイミング LCDへの書き込みにはデータバス4本と書き込みトリガ(E)と、レジスタセレクト(RS)の6本を使います。 インストラクションコードの場合はRSをLowに維持し、文字コードの場合にはHighにします。 それぞれのタイミングは次のようになります。
|
インストラクションコード
|
|
|
|
文字コード
|
|

|
この中でもっとも重要なのはデータバスに書き込む順序と書き込みトリガの順序です。赤い矢印で示した部分でLCDモニタにデータが書き込まれるわけですが、これよりも先にデータバスの内容が書き変わっていなければ意味がありません。もちろんRSもこの時点でHighかLowの目的とする方(インストラクション or 文字)になっていなければいけません。それ以外の部分のタイミングについては、仕様書には細かく明記されていますが、それほどこだわらなくても認識してくれるようです。 次に、図中のtw
の時間ですが、これは最低でも220nsec以上待たなければいけません。
データバス4ビット設定ではEの最初のサイクルで上位4ビット分が書き込まれます。引き続き下位4ビット分のデータを書き込むわけですが、そのときにはRSは現状維持のままで、データバスと書き込みトリガだけ操作すればOKです。ただし、第1サイクルと第2サイクルの間隔は500nsec以上空ける必要があります。
LCDの初期化 ここからはようやく実際のプログラミングの話に入っていきます。まずはこのファイルをダウンロードしてください。 このLCDモニタを使うにはまず初期化をしなければいけません。手順としては次のようになります。
- 電源投入後15msec以上待つ
- インストラクションコードの0x03を書き込む
- 4.1msec以上待つ
- インストラクションコードの0x03を書き込む
- 0.1msec以上待つ
- インストラクションコードの0x03を書き込む
- 0.1msec以上待つ
- インストラクションコードの0x02を書き込む
- 0.1msec以上待つ
- インストラクションコードの0x28を書き込む(モード設定。他のモードもあり)
- 0.1msec以上待つ
- インストラクションコードの0x0eを書き込む(表示設定。他の設定もあり)
- 0.1msec以上待つ
- インストラクションコードの0x06を書き込む(カーソル設定など。他の設定もあり)
以上の一連の作業で初期化は終わりです。ちょっと長いですね。なお、10番目以降の0.1msec以上待つ部分は実は省略可能です。私のプログラムでは上位と下位のデータを送った後に少しのウェイトが入っているので、その部分でこの0.1msec分を吸収できるのです。
これをC言語で書くとこのようになります。
|
main.c
void initLCD(void) { P3.DDR
= 0xff; //
Port-3を出力設定 P3.DR.BYTE
= 0x00;
longwait(50);
//
電源投入後15msec以上待つ
lcd_out(0x03,0);
// 8ビットモードにする
その壱 longwait(50);
//
4.1msec以上待つ
lcd_out(0x03,0);
// 8ビットモードにする
その弐 longwait(50);
//
0.1msec以上待つ
lcd_out(0x03,0);
// 8ビットモードにする
その参 longwait(50);
//
0.1msec以上待つ
lcd_out(0x02,0);
// 4ビットモードにする
lcd_write(0x28,0);
// 4ビットモード、2行表示、ドットサイズ5*10 lcd_write(0x0e,0);
// 文字表示、カーソルON lcd_write(0x06,0);
// 通常表示、カーソルは右移動
cls_lcd(); }
|
この中にある lcd_out()は4ビットのデータを1回だけ出力します。似ていますが
lcd_write()は8ビットデータを書き込むための関数なので、4ビットずつ2回に分けてlcd_out()を呼び出しています。どちらも2つの引数を持っていますが、1つ目の引数は実際に送るコード、だからlcd_out()の方は上位4ビットは必ず0で、下位4ビットだけがしか値が入っていません。lcd_write()は上位下位の合計8ビットとも値を入れれます。2つ目の引数はインストラクションコードならば「0」、文字コードならば「1」を書き込みます。それによりRSのHighとLowを切り換えます。lcd_out()とlcd_write()の本体は次のようになっています。
|
main.c
void lcd_out(unsigned char dat,char
id) { P3.DR.BYTE =
dat; //
下位4ビットのデータを出力
if(id
== 1) //
id = 1の時は文字コード P3.DR.BIT.B4
= 1; // →RSをHigh else
//
id = 0の時はインストラクションコード P3.DR.BIT.B4
= 0; // →RSをLow
wait(20);
//
500msec 待機
P3.DR.BIT.B5
= 1; //
E端子をHigh wait(20);
//
待機
P3.DR.BIT.B5
= 0; //
E端子をLow wait(20);
//
待機 }
|
|
main.c
void lcd_write(unsigned char dat,char
id) { lcd_out((( dat
>> 4 ) & 0x0f),id); //
上位4ビット lcd_out(((
dat >> 0 ) & 0x0f),id); //
下位4ビット }
|
lcd_write()を使えば1文字ずつ文字をLCDモニタに表示できるわけですが、それだけではちょっと面倒です。文字列を扱うには次のようなprint_lcd()という関数を使います。
|
main.c
void print_lcd(char* dat) { while(*dat){ lcd_write(*dat,1); dat++; } }
|
やっていることは簡単ですね。文字列というものは最後に「\0」が入っているので、それが来るまで順番にlcd_write()に文字を1文字ずつ渡しているのです。 なお、ポインタを使っているのにはちゃんと理由があります。途方もない大容量を持つパソコンのプログラミングと違い、マイコンでは常にメモリの残量を気にしなければいけません。だから、配列を新たに用意したりするのではなく、メモリはできるだけ共用することが望ましいのです。
ここまでの関数(初期化ルーチン、4ビット転送ルーチン、文字表示ルーチン、文字列表示ルーチン)だけでとりあえずLCDモニタに文字を映すことができます。ではプログラミングの世界ではお約束の「Hello
World」を表示してみましょう。
|
main.c
#include "iodefine.h"
void main(void); void initLCD(void); //
LCD初期化 void cls_lcd(void); //
LCD画面クリア void cur_pos(char x,char y); //
LCDカーソル移動 void print_lcd(char* dat); //
LCD文字列表示 void wait(int t); //
ウェイト(短い) void longwait(int t); //
ウェイト(長い) void lcd_out(unsigned char dat,char
id);// LCD出力(下位4ビット) void lcd_write(unsigned char dat,char
id); // 1バイトのデータをLCDへの出力
#ifdef __cplusplus } #endif
void main(void) {
initLCD();
print_lcd("Hello
World.");
while(1);
}
|
なんだかプロトタイプ宣言が大半を占めていますね。この中で
cls_lcd()とcur_pos()の2つは説明がまだでしたね。それぞれ画面クリアとカーソル移動の為の関数です。
|
main.c
void cls_lcd(void) { lcd_write(0x01,0);
//
クリアコマンド発行 longwait(50); }
|
使う頻度が高いので一つの関数として独立させましたが、実際にやっていることはクリアコマンド(0x01)を lcd_write()を使って書いているだけです。
cur_pos()の方はもう少しだけ複雑です。
|
main.c
void cur_pos(char x,char y) { char
i; lcd_write(0x02,0); //
カーソルを(0,0)に一旦戻す longwait(50); for(i
= 0; i < (y*40)+x; i++){ // 2行目へは40桁進むと行ける lcd_write(0x14,0);
//
表示そのままカーソル右シフト } }
|
カーソル移動はダイレクトに座標を指定することはできません。現在地から相対座標で左右方向に進むか、もしくは原点(0,0)に戻るかの2通りです。ここでは現在地を知る方法が有りませんので、一旦原点に戻り、それから移動することにしています。
原点に戻るインストラクションコードは0x02です。2番目の引数はインストラクションコードなので「0」を渡しています。
次のforループはちょっとわかりにくいですね。というのも、(y×40)という部分が理解に苦しむのではないでしょうか。このLCDモニタは表示部は16桁なのですが、内部的には40桁ある事になっています。そして、40桁目を超えると2行目に移るのです。だから、2行目=y座標で1ですから、結局1行下に移動することになるのです。なお、今表示されている文字を消さずにカーソルだけを右に移動させるインストラクションコードは0x14です。
なんで?
液晶表示部は16桁なのに、どうして内部座標は40桁まであることになっているのでしょうか?
これにはちゃんと理由があります。
まず、LCDモニタというものは本来キャラクタパターンなど持ってはいません。つまり、0x41と書き込んだところで「A」とは表示してくれません。それを実現しているのはLCDコントローラドライバというチップです。今回使用したサンライク社のSC1602BSLBには沖データのMSM6562B-01というコントローラドライバが使われています。沖データとしてはもちろんSC1602BSLB専用に開発したつもりなど無く、もっと汎用的に使われることを想定しています。そしてその上限のスペックが40桁というわけです。だから、世の中にはきっとこのLCDモニタとまったく同じ手順で起動できる40桁表示のLCDモニタが存在すると思われます(現行機種で有るかどうかは別問題ですが)。
さて、この2つの命令ですが、プログラム中で使う場合にはそれぞれ次のように使います。
|
cls_lcd(void)
|
|
cur_pos(char
x,char y)
cur_pos(5,1);
//
2行目の5桁目に移動するとき
|


|