#analog
#norelated
#contents
* LCDサンプル [#bd9e4555]

** 回路図 [#n4dd3742]
まずは配線に悩む。~
4bitモードを使うか、8bitモードを使うかだ。~
やはり、ポートを節約するためには、4bitだろうか。~

あとは、busyフラグをチェックするかどうか。~
busyかどうかをチェックせずに、ウェイトでコマンド完了を待つ方式だと、R/WをGNDに落としてWrite固定でいいらしいけど、ここは正攻法でbusyフラグはチェックするようにしたいな。~

となると、必要なピンは、RS、R/W、E、DB4〜DB7で、全部で7ピンか。~
回路図はこんな感じで。~
見にくいかも……あってるよな?~

#ref(LCDTest.png,left,nowrap,LCDサンプル)

4bitモードでは、DB0〜DB3は使用しないので、オープンにしておく。~
このあたりは、[[このページ:http://www.oct.zaq.ne.jp/i-garage/trbl/lcd4bit.htm]]を参考に。~
Eはプルダウンするべきらしい。~
ちなみに、プルダウンしなかったら、起動時に行頭に一つスペースが入ったりしたけど、なんでだろう?~
PD5が不定でも確実にLowであるべきということかな?~
他はプルアップ抵抗が内蔵されているから問題ないとかいうのもぴんとこない。~
もっとデジタル回路を勉強しないとダメか……。~

Voは液晶の濃度調整を分圧でやってて、Vcc5Vの1/(10+1)の、約0.45Vがかかる。~
これでちょうどいい濃度っぽい。~
とりあえず配線はこれでOK。~
動いた後の動作写真も載せておく。~

#ref(LCDTestP.png,left,nowrap,LCDサンプル)

** ソースコード [#t2834105]

*** 定義とウェイト関数 [#w223beb7]

まずは、includeに各種定数定義と、LEDでも使った待ち関数。~
前回のLEDを取り外して、ポートDのみで全てが済むように配線したので、定義もポートDのみです。~
そして、ポートDにおけるLCD接続先のビット定義と、データ部分を一気にマスクするための定義。~

もう一つ、データ書き込み時に、コマンドモードかデータモードかを示すRSの値だけ書き換えれば、他の処理は共通で済むため、引数で渡すためのモードをenumで定義。~

 #include <avr/io.h>
 #include <util/delay.h>
 
 #define LCD_PORT	PORTD	// LCD表示に使用する出力レジスタ
 #define LCD_PIN		PIND	// LCD表示に使用する入力レジスタ
 #define LCD_DDR		DDRD	// LCD表示に使用する方向レジスタ
 
 #define LCD_D4	(1<<0)	// D4ビット位置
 #define LCD_D5	(1<<1)	// D5ビット位置
 #define LCD_D6	(1<<2)	// D6ビット位置
 #define LCD_D7	(1<<3)	// D7ビット位置
 #define LCD_E	(1<<5)	// Eビット位置
 #define LCD_RW	(1<<6)	// R/Wビット位置
 #define LCD_RS	(1<<7)	// RSビット位置
 #define LCD_DATMASK	0x0F	// データビットマスク
 
 // RSモード列挙型定義
 typedef enum _tagRSMode{
 	RS_MODE_COMMAND,	// コマンドモード
 	RS_MODE_DATA,		// データモード
 } RSMode;
 
 void delay_ms( int time )
 {
 	// 指定ms分ループ
 	while( time-- ){
 		_delay_ms( 1 );
 	}
 }

*** 基本動作 [#c6396994]

で、まずはLCDを初期化しなければならないわけだ。~
色々なサイトを巡ってみても、初期化とBusyフラグチェックの2箇所に苦労してるっぽい。~
ご多分に漏れず、結構な難関でした。~

まず基本的な動作として、RS(Register Select)はコマンドモード(0)/データモード(1)の切り替えに使用し、R/Wはライトモード(0)/リードモード(1)の切り替えに。~
D4〜D7は、ライトモードのコマンドモードではコマンドパラメータ、データモードでは文字コードやユーザ定義文字のパターンをセットする。~
リードモードでのコマンドモードでは、Busyフラグとアドレスカウンタ(データを読み書きするアドレス)の取得、データモードではデータ読み出しが出来る。~
アドレスの設定は、ライトモードのコマンドモードのコマンドパラメータで指定できる。~
これら各種命令の一覧は、以下。~

#ref(コマンド.png,left,nowrap,コマンド)

これらコマンド通りに、出力/入力ポートの設定をして、その状態でE(Enable Signal)をLow→High→Lowとすることにより、コマンドが実行される。~
この時、LCDの仕様によって、まずEをLowからHighにする前にRSとR/Wを設定してから数ns(ナノ秒)(セットアップ時間)以上待ち、Highの維持を数ns(イネーブルパルス幅)以上行い、HighからLowにしてからRSとR/W、データの変更を行わない時間(ホールド時間)を数ns以上待つ必要があるらしい。~
あと、前のEの立ち上がりから、次のEの立ち上がりまでも、一定以上の時間(イネーブルサイクル)以上置く必要がある。~
この時間は、データシートに定義されているんだけど、正直、今回買ったやつに関しては、全く当てにできない気がする。~
なぜなら、上の図と下の表の、シンボルがほとんど一致してないんだもの……。~
読み方を間違っているのかしら……?~

#ref(タイミング.png,left,nowrap,タイミング)

この表では、RSとR/Wのセットアップ時間が0nsとなっているため、基本的に待つ必要はないらしい。~
本当か……?~
そして、EをHighにしてからLowにするまで、140ns(イネーブルパルス幅)の維持。~
実際にはEのRise時間、Fall時間の各最大25nsが含まれるので、190nsの維持か。~
ライトモードでのデータのセットアップに40nsとあるが、これはこのイネーブルパルス幅に含まれているのでOK。~
EをLowにしてから、RSとR/W、データのホールド時間が10ns。~
EをHighにしてから、次のEをHighにするタイミングまでのイネーブルサイクルは、1200ns(1.2μs)以上。~

で、これをプログラムで実際にどれくらい意識する必要があるのかということだが。~
msどころの話じゃなく、μsでもなく、さらにその下のnsというのは、普段意識しないレベルなので、なかなかわかりにくい。~

AVRは、最速(レジスタ間計算等)で1MHz/1MIPSであり、この場合、1クロックサイクルで1命令が実行される。~
今回は、ATmega168Pを、1MHz駆動させている。(8MHzの内蔵RCで、CKDIV8ヒューズにより、8分周)~
ということは、1秒間に実行できる命令は最大1000000個であり、1命令にかかる最短の時間は、1秒の100万分の1で、1μsとなる。~
この程度であれば、EをHighにして次の命令ですぐにLowにしたとしても、その間は1μsなので、190ns以上のイネーブルパルス幅を確保しており、さらに次の命令を実行しようとした時には、最初のHighから2μs経過しているので、イネーブルサイクルも仕様を満たしており、ウェイトを置いたりする必要はないと思う。~
(実際問題なかった)~

では、8MHz駆動の場合は、どれくらい待てばいいのか。~
1命令は1MHzの時の8倍の速度になるので、1μs/8で125ns。~
イネーブルパルス幅を確保するのに2命令、イネーブルサイクルを満たすためには、EをHighにして2命令待ちEをLowにする4命令で500nsを消費するので、残りは700nsで6命令は必要なはず。~
なんだけど、これくらいの指定だと、全然まともに動いてくれない。~
何が悪いんだ……。~
イネーブルパルス幅に20命令、イネーブルサイクルを満たすために、もう20〜40命令分入れてやって、ようやく安定するレベル。~
1MHzの時より待ち時間多いじゃん……。~
とりあえずは安定している1MHz駆動で進めていきます……。~
そのうち謎が解ければ書く。~

ちなみにこれは、コマンド発行のタイミングの話であって、実際にコマンドが処理される時間の話ではないはず。~
コマンド実行時間は、上記コマンド表の右側に書いてあり、Busyフラグとアクセスカウンタの読み出し以外には、それなりに時間がかかるようだ。~
とはいえ、ライトモードはなんとなくわかるけど、リードモードの処理時間がよくわからない。~
色々なサイトを巡ってみても、あまりリードに関して触れているところがない。~

ライトモードであれば、EのHigh→Low→Highの間のデータをLCDがその瞬間に読み取って、実際にそれを処理する間Busyとなるというイメージなんだけど、あってるんだろうか?~
だとすると、リードモードはEのHigh→Low→Highの間にデータが読めないとダメなわけだから、Busyの読み出しは0μsだからいいとして(?)、Read Data From RAMの43μsは、どこにかかるんだろう?~
なんかまだ勘違いしているところがあるのかもしれない。~

*** 初期化シーケンス [#s4fe8ac2]

Lcd_initという初期化関数を作成し、この中でLCDの初期化シーケンスを実行。~
この初期化シーケンスは、LCDのデータシートに書いてある。~
今回買った、SD1602HULB-XAの初期化シーケンスは、以下のように書かれている。~

#ref(初期化シーケンス.png,left,nowrap,初期化シーケンス)

今回は4bitモードなので、下の方。~
画像が汚くて読みにくいか。~

まず、PowerONして起動してから、15ms以上待つ。~
次にコマンド表にあるファンクションセットを使用し、DB4のビットを立て、8bitモードに設定して、このDB4〜DB7のコマンドを、4.1ms以上、100μs以上の間をあけて、連続で3回送る。~
印刷ではμが抜けて、100μsが100sになってしまっているという罠。~
100秒も待ってらんねーYO!~
この4bitのコマンドを3回送ることで、元が何のモードだろうと、4bitモードで8bitコマンドの後半の4bitを送る前という中途半端な状態であろうと、8bitモードに初期化されることになる。~

確実に8bitモードになったところで、ここでまたファンクションセットを使用し、4bitモードの設定を1回だけ行う。~

どこのページにも書いてあることだけど、ここまではBusyフラグは使えない。~
そしてほとんどのページに、ここからBusyフラグが使えるとか書いてあるんだけど、少なくともこのLCD(SD1602HULB-XA)では、この後に行うDisplay ON/OFF、Clear Displayと、Entry Modeの設定では、Busyフラグは使えないようだ。~
(以下のデータシートの記述より)~
>BF cannot be checked after the following instructions.~
>When BF is not checked, the waiting time between instructions is longer than the execution time.~
なので、初期化シーケンスの一連の処理では、各コマンド送信の間に、時間決め打ちのウェイトを指定してやるのがよさそう。~

あとは、これらのコマンドを4bit単位で送ってやるので、4bitコマンド送信関数を作って、引数にコマンドを書いてやることにしよう。~
これは後でデータモードの書き込みでも使えるため、引数でコマンドモードを指定してやるようにする。~
以下、初期化部分のソースコード。~
4bitコマンド送信関数の中身はこの後。~

 void Lcd_init( void )
 {
 	// 最初は全てのポートを出力に設定
 	LCD_DDR	|= ( LCD_E | LCD_RW | LCD_RS | LCD_DATMASK );
 	// 信号もLowにしておく
 	LCD_PORT &= ~( LCD_E | LCD_RW | LCD_RS | LCD_DATMASK );
 
 	// 最初はBusyチェックが効かないので、時間待ちする
 	delay_ms( 15 );		// 15ms待ち
 	lcd_setcmd4( RS_MODE_COMMAND, 0x3 );	// 8bitモード設定
 	delay_ms( 5 );		// 4.1ms以上待ちなので、5ms待ち
 	lcd_setcmd4( RS_MODE_COMMAND, 0x3 );	// 8bitモード設定
 	delay_ms( 1 );		// 100μs以上待ちなので、とりあえず1ms待っておく
 	lcd_setcmd4( RS_MODE_COMMAND, 0x3 );	// 8bitモード設定
 	delay_ms( 1 );		// ファンクションセットは39μs以上待ちなので、とりあえず1ms待っておく
 
 	// 初期化シーケンスに書いてある通り、4bitの設定は4bitのみ渡せれば有効なので、1回のみ
 	lcd_setcmd4( RS_MODE_COMMAND, 0x2 );	// 4bitモード設定
 	delay_ms( 1 );		// ファンクションセットは39μs以上待ちなので、とりあえず1ms待っておく
 
 	// ここからは4bitモード動作なので、8bitの命令を4bitずつ2回に分けて送ってやる
 	lcd_setcmd4( RS_MODE_COMMAND, 0x2 );	// ファンクションセット、4bitモード
 	lcd_setcmd4( RS_MODE_COMMAND, 0x8 );	// 2Line表示、5*8ドットフォント
 	delay_ms( 1 );		// ファンクションセットは39μs以上待ちなので、とりあえず1ms待っておく
 	lcd_setcmd4( RS_MODE_COMMAND, 0x0 );	// Display ON/OFF設定
 	lcd_setcmd4( RS_MODE_COMMAND, 0xF );	// Display ON / Cursor ON / Blinking ON
 	delay_ms( 1 );		// Display ON/OFF設定は39μs以上待ちなので、とりあえず1ms待っておく
 	lcd_setcmd4( RS_MODE_COMMAND, 0x0 );	// Clear Displayコマンド
 	lcd_setcmd4( RS_MODE_COMMAND, 0x1 );	// スペースでDDRAMが埋められ、アドレスカウンタがDDRAMの00H(先頭)に移動
 	delay_ms( 2 );		// Clear Displayコマンドは1.53ms以上待ちなので、2ms待ち
 	lcd_setcmd4( RS_MODE_COMMAND, 0x0 );	// Entry Mode
 	lcd_setcmd4( RS_MODE_COMMAND, 0x6 );	// インクリメントモード / ディスプレイシフトなし
 	delay_ms( 1 );		// Entry Modeコマンドは39μs以上待ちなので、とりあえず1ms待っておく
 }

最初に、ポートが変な状態になっていないように、入出力状態と信号レベルを初期化しておく。~
そして、前述の初期化シーケンスを走らせて、4bitモードの2Line表示、5*8ドットフォント、Display ON、Cursor ON、Blinking ONに設定し、画面をクリアする。~
5*11ドットフォントを指定してみたけど、何も変わらなかったんだけど、意味ないのかな?~
Entry Modeは、データを入出力すると、アドレスカウンタが増えるインクリメントモードに設定。~
Entry Modeのシフトモードはよくわかってない。~

では次に、4bitコマンド送信関数lcd_setcmd4の中身。~
以下ソースコード。~

 void lcd_setcmd4( RSMode mode, uint8_t data )
 {
 	uint8_t	tmpval = LCD_PORT;	// ポート状態を一旦変数に入れる
 
 	// 書き込むポートを一旦落とす
 	tmpval &= ~( LCD_RW | LCD_RS | LCD_DATMASK );
 
 	// RSビットコマンドモードであればクリアのまま
 	if( RS_MODE_DATA == mode ){
 		tmpval |= LCD_RS;	// RSビットはデータモードであれば立てる
 	}
 
 	// RWビットはクリアでWriteモードなのでそのまま
 
 	// データビットセット(下位4bitのみセットするのマスク)
 	tmpval |= ( LCD_DATMASK & data );
 
 	// ポートに書き込み
 	LCD_PORT = tmpval;
 
 	// イネーブルパルス出力
 	LCD_PORT |= LCD_E;
 		// 高速駆動時はここでイネーブルパルス幅以上待つ必要があるはず
 	LCD_PORT &= ~LCD_E;
 		// 高速駆動時はここでイネーブルサイクル時間を稼ぐ必要があるはず
 }

I/Oレジスタの中身を、一時変数(汎用レジスタ)内に持っている理由は、なんとなく。~
外部のピンに対する電圧が直接変わるI/Oレジスタには、直接頻繁なアクセスはあまりしたくない感じ。~
イメージでそう思ってるだけなんだけど、実際はどうなんだろう?~

とはいえ、この方法だと、変数に保存してから書き戻すまでに、割り込み等で今回使用しているポートの一部が書き換えられたりとかしたら、書き戻し時にそこをリセットしてしまうので、本当はあまりよろしくないのかも。~

*** 文字表示 [#jf245d1a]

LCDには、以下のようにフォントテーブルが定義されている。~
基本的には[[ANKコード:http://www.technoveins.co.jp/technical/data/ankcharactorset.htm]]とか[[JIS X 0201:http://ja.wikipedia.org/wiki/JIS_X_0201]]とか呼ばれるものと同じ配置なので、普通にソースコード上に半角英数字や半角カナを書いて、その文字コードをそのままLCDに送ってやればいい。~

#ref(フォントテーブル.png,left,nowrap,フォントテーブル)

というわけで、文字列を引数に取る関数と、文字を引数に取る関数を作って、前者から後者を呼び、後者から前述4bit送信関数をコールする。~
今回からは、Busyフラグのチェックをしなければならないので、Busyフラグをチェックするサブ関数もコールする。~

 void Lcd_setchar( char data )
 {
 	// 上位4bit送信
 	lcd_setcmd4( RS_MODE_DATA, LCD_DATMASK & ( data >> 4 ));
 	// 下位4bit送信
 	lcd_setcmd4( RS_MODE_DATA, LCD_DATMASK & data );
 	// Busyチェック
 	lcd_busywait();
 }
 
 void Lcd_setstr( char *str )
 {
 	// 終了(NULL文字までループ)
 	while( *str ){
 		// 1文字ずつ設定
 		Lcd_setchar( *str );
 		str++;
 	}
 }

初期化の時はコマンドモードを設定したけど、今回は文字の出力なので、データモードを指定する。~
RS_MODE_DATAを指定する事により、RSビットが立てられるのは、前述のソースの通り。~

で、次にBusyチェック関数。~
この関数は、Busyフラグが立ってる間、ループで回り続ける。~
今回の設計では、LCDに対して何かしたら、Busyじゃなくなるまで待ってから別の処理を行うという流れになる。~
LCDにコマンドを送った後に、別の作業を無駄なくさせたいような場合は、LCDに対して何かをする前にBusyチェックをして、コマンド送信後は、LCDに対しては何もしないという設計にするべきなのかもしれない。~

 void lcd_busywait( void )
 {
 	uint8_t	bf = LCD_D7;	// Busyフラグが立った状態の変数を初期化
 
 	// RSビットはクリアでコマンドモード
 	LCD_PORT &= ~( LCD_RS );
 
 	// ReadモードはRWビットを立てる
 	LCD_PORT |= ( LCD_RW );
 
 	// データビットを入力に設定
 	LCD_DDR	&= ~( LCD_DATMASK );
 
 	// Busyフラグが落ちるまでループ
 	while( bf ){
 		// イネーブルパルス出力(上位4bit)
 		LCD_PORT |= LCD_E;
 			// 高速駆動時はここでイネーブルパルス幅以上待つ必要があるはず
 		bf = LCD_PIN & ( 1 << LCD_D7 );	// Busyフラグの読み出し
 		LCD_PORT &= ~LCD_E;
 			// 高速駆動時はここでイネーブルサイクル時間を稼ぐ必要があるはず
 
 		// イネーブルパルス出力(下位4bit/4bitモードなので2回送る必要があるがここでは特にすることはない)
 		LCD_PORT |= LCD_E;
 			// 高速駆動時はここでイネーブルパルス幅以上待つ必要があるはず
  		LCD_PORT &= ~LCD_E;
 			// 高速駆動時はここでイネーブルサイクル時間を稼ぐ必要があるはず
 	}
 
 	// データビットを出力に戻す
 	LCD_DDR	|= ( LCD_DATMASK );
 }

とりあえず、以上で文字表示までは出来るようになった。~
ただし、改行を実装していないので、2行目にも表示できるように、カーソル位置を指定できるようにしよう。~

*** 文字出力位置 [#nd83737d]

さっき、文字表示用に、1バイトの文字を4bitずつ2回に分けて送るLcd_setcharという関数を作ったので、同様にコマンドを送るLcd_setcmdという関数も作っておく。~

 void Lcd_setcmd( uint8_t cmd )
 {
 	// 上位4bit送信
 	lcd_setcmd4( RS_MODE_COMMAND, LCD_DATMASK & ( cmd >> 4 ));
 	// 下位4bit送信
 	lcd_setcmd4( RS_MODE_COMMAND, LCD_DATMASK & cmd );
 	// Busyチェック
 	lcd_busywait();
 }

この関数を使用して、表示位置を指定できる関数Lcd_setposを作る。~
表示位置のアドレスは、以下のようになっているので、移動したい位置のDDRAMアドレスの設定コマンドを送信すればいい。~

#ref(DDRAMAddress.png,left,nowrap,DDRAMアドレス)

 void Lcd_setpos( int8_t x, int8_t y )
 {
 	int8_t val = 0x80;	// コマンドコード設定
 
 	if( x ) val |= 0x40;	// 行指定が0(1行目)じゃなかったら、2行目bitを立てる
 	val |= ( y & 0x0f );	// 列指定の4bitを設定する
 
 	// コマンド送信
 	Lcd_setcmd( val );
 }

*** メイン関数 [#i296cc22]

最後に、今までの関数を使用して、上の動作写真のように文字表示をさせるメイン関数。~

 int main( void )
 {
 	Lcd_init();
 
 	Lcd_setstr( "LCDサンプル" );
 	Lcd_setpos( 1, 0 );	// 2行目の1列目(0ベース)
 	Lcd_setstr( "ヒョウジテストチュウ" );
 
 	return 0;
 }

まぁ、ここはもう説明する必要はないねw~
とりあえず、一通り文字を表示させるまでを纏めてみました。~
まだまだ謎なところもありますので、そのうち追記していきたいと思います。~

トップ   編集 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS