こんにちは、ENGかぴです。
PIC16F1827のDOとdelay()関数を使用することで16文字×2のLCDに文字を表示することができます。LCDを制御する最小のピン構成でデータを書き込んで文字を表示する方法をまとめました。
I2C通信を使用してLCDに文字を表示する方法は下記記事にまとめています。
PICマイコン(PIC16F1827)のI2C通信を実装する
LCDはQAPASS1602(スターターキットに付属)を使用しています。PIC16F1827で動作確認したことについてリンクをまとめています。
PICマイコン(PIC16F1827)で実現できる機能と解説リンクまとめ
LCDの使い方
LCDの通信方式はバス方式とI2Cなどの通信方式がありますが今回使用するLCDはバス方式とします。制御用のDOとデータを構成するDOを組み合わせることで通信を行いLCDに文字を表示します。
LCDの配線例とインストラクションレジスタ(以下IRとする)の初期化の方法とデータレジスタ(以下DRとする)に書き込む方法を説明します。
広告
PR:わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
PIC16F1827とLCDの配線
PIC16F1827とLCDの配線例を示しています。最小の組み合わせでは制御線2本とデータ用の4本を接続することでLCD表示ができます。配線の組み合わせは以下の通りです。
配線の本数 | 信号名 |
---|---|
最小の組み合わせ(6本) | RS, E, D4, D5, D6, D7 |
制御線を追加(7本) | RS, RW, E, D4, D5, D6, D7 |
データ8ビット(10本) | RS, E, D0, D1, D2, D3, D4, D5, D6, D7 |
最大の組み合わせ(11本) | RS, RW, E, D0, D1, D2, D3, D4, D5, D6, D7 |
データ8ビットの方が高速な応答ができますが、PIC16F1827では使用できるピン数が少ないため配線の本数が最小となる6本で回路を構成します。
V0は可変抵抗で分圧して入力することでコントラストを変更できますが、表示を見やすくするためGND接続しています。DACを使ってLCDモジュールのV0に電圧を印加することでコントラストを調整する方法もあります。
LED照明することで全体が明るくなり文字が見やすいためLED照明を点灯しています。
PIC16F1827のピン配置は以下の通りです。
MCLR以外はすべてGPIOをDOにしています。
IRの初期化
LCDを任意の条件で使用するためにIRに初期設定、画面クリア、表示アドレスの設定などのコードを書き込む必要があります。ST7066Uのデータシートを引用して説明します。
Clear Displayは画面に0x20(スペースコード)を書き込んで画面表示をクリアしDDRAMのアドレスをクリアします。カーソルが一段目の左端になります。
EntryModeSetはカーソル移動モードとシフトモードを設定します。I/D=1を指定するとインクリメントになりI/D=0ではデクリメントになります。S=1を指定するとディスプレイシフトを有効になりS=0で無効になります。
Display ON/OFFはD=1を指定するとディスプレイがONになりD=0を指定するとOFFします。C=1を指定するとカーソルの表示がONになりC=0を指定するとOFFします。B=1を指定するとカーソルの位置がブリンク状態になりB=0を指定するとブリンクが無しになります。
FunctionSetはインターフェースビット長(4ビットまたは8ビット)の選択や表示ラインのフォントサイズを設定します。DL=1を指定すると8ビット、DL=0を指定すると4ビットになります。N=1を指定すると2ライン(2段表示)、N=0を指定すると1ライン(1段表示)になります。F=1を指定するとフォントサイズが5×10dot、F=0を指定すると5×8dotになります。
SetDDRAMaddressはDDRAMのアドレスを指定します。文字を表示するためのカーソル位置を指定します。一段目の左端に移動する場合はClear Displayで代用することもできますが、2段目の左端のアドレスを使用する場合は0x40を指定します。
Write data to DATAはDDRAMまたはCGRAMにデータを書き込みます。アスキーコードに対応している文字を表示する場合はDDRAMに書き込みを行います。書き込む場合はDRを選択するためRS信号をHIGHにしてから行います。
本記事では赤文字の設定を有効にしています。
DOを使ったIRレジスタの書き込み
各信号のタイミング波形をデータシートを引用して説明します。赤で記入している分は説明のため追加したものです。
IRレジスタの書き込みはRSをLow、R/WをLowにしてから行います。R/Wはデータをリードしない場合は常にLowとなります。
4ビットのインターフェースでは8ビットのデータをIR7~IR4の4ビットとIR3~IR0の4ビットに分けてデータを確定させます。データの確定はEの状態変化で先にIR7~IR4のデータをセットした状態でEのHighからLowへの状態変化によって最初の4ビットが確定します。次にIR4~IR0のデータをセットした状態でEのHighからLowへの状態変化によって次の4ビットが確定します。
図はbusy flagを確認する場合の波形の例ですがチェックのためのbusy flag checkを送信しない場合はinstruction write間に十分なウェイトを置く必要があります。
instruction writeのコマンド後、最低でも約37u以上待機する必要がありますが、busy flagを確認しないためコマンド2回分に少しマージンを加えて80usのウェイトを置くようにしています。Clear Displayの場合は2000usのウェイトにしています。
波形のタイミングの詳細と実装例
データを確定させるための波形についての詳細をデータシートを引用して説明します。データの確定はEがHighからLowになる状態変化で行われるためEをLowにするまでにデータをセットする必要があります。
波形の詳細についてはデータシートのAC Characteristicsを確認する必要がありますが、PIC16F1827のクロックを16MHz(動作クロックは4MHz)の場合は1命令当たり250nsなので波形の立上りや立下りについての条件はほとんど満たしています。PIC16F1827で制御する場合に注意が必要な信号のみ赤で記入しています。
Tpwを140ns(Vcc=2.7Vで450ns以上)にする必要があるのでウェイトを置く必要があります。またEのパルスも最低でも1.2us以上が必要なためウェイトが必要です。
void LcdFuncSet0(uint8_t dat){
LCD_RS_L; //RSをLOW
if( dat & 0x80){
LCD_B3_H; //IR7がH
}
else{
LCD_B3_L; //IR7がL
}
//IR6-IR4は0x40,0x20,0x10で論理積
//省略
LCD_E_H; //EをH
__delay_us(1);
LCD_E_L; //EをL(データの確定)
__delay_us(1);
if( dat & 0x08){
LCD_B3_H; //IR3がH
}
else{
LCD_B3_L; //IR3がL
}
//IR2-IR0は0x04,0x02,0x01で論理積
//省略
LCD_E_H; //EをH
__delay_us(1);
LCD_E_L; //EをL(データの確定)
__delay_us(1);
}
IRの書き込みの実装例を示しています。LcdFuncSet0()関数は4ビットのデータを2回書き込む自作の関数です。引数に書き込むIRの値を指定します。
IRに書き込みを行うためRSをLowにします。次にIR7から4ビットのデータをセットするために引数のIR値に対して各ビットが立っているかを確認します。
IR7は0x80、IR6は0x40、IR5は0x20、IR4は0x10と引数の論理積を計算し1以上であればビットが立っているため対象のDOをHighにします。論理積の結果が0であればDOをLowにします。
IR7~IR4までのデータを確定するためにEをHighにして1usウェイトを置き、EをLowにして1usのウェイトを置きます。
続けてIR3から4ビットのデータをセットするためにIR7~IR4と同様に論理積を計算します。
IR3は0x08、IR2は0x04、IR1は0x02、IR0は0x01と引数の論理積を計算し1以上であればビットが立っているため対象のDOをHighにします。論理積の結果が0であれば対象のDOをLowにします。
IR3~IR0までのデータを確定するためにEをHighにして1usウェイトを置き、EをLowにして1usのウェイトを置きます。
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
初期化の手順
- 手順1FunctionSetに0x28をセット
インターフェースビット長を4ビット、表示ラインを2ライン、フォントサイズを5×8dotにするためDL=0、N=1、F=0を指定する。
- 手順2Display ON/OFFに0x0Cをセット
DisplayをON、ブリンクとカーソルは文字を16文字フルに使うため不要なのでD=1、C=0、B=0を指定する。
- 手順3Clear Displayに0x01をセット
初期化中に未確定のデータが表示されないように画面をクリアする。
- 手順4EntryModeSetに0x06をセット
カーソル移動モードをインクリメント、ディスプレイシフトを使用しないため、I/D=1、S=0を指定する。
手順1でFunctionSetをIRに書き込みますが、データのセットがうまくいかないことを考慮して複数回書き込みを行うことが推奨されています。本記事では2回書き込んでいますが、おまじないみたいなものなので1度だけのセットでも問題ありません。
手順2はディスプレイをONします。ブリンクとカーソルの表示は16文字をフルに使うと表示部より右側に移動するためカーソルの表示とブリンクの状態が確認できないため不要とします。
手順3はClear DisplayでDDRAMにスペースコードが書き込まれるため表示部がクリアされます。クリアしない場合は不定のデータが表示されることがあります。
手順4はカーソルの表示を使用しませんが、I/Dについてはインクリメントを書き込んでいます。
LCD表示を行う
void LcdMsgSet(uint8_t *msg){
uint8_t i;
LcdClr(); //Clear Display
//LcdHome(); //Return Home
for(i=0;i < LCD_SZ; i++){
//1LINEの文字をセット
LcdFuncSet(msg[i]);
__delay_us(80);
}
DspLine2Top(); //SetDDRAMaddress(2段目にカーソルを移動)
for(i=0;i < LCD_SZ; i++){
//2LINEの文字をセット
LcdFuncSet(msg[i + LCD_SZ ]);
__delay_us(80);
}
}
LcdMsgSet()関数はLCD表示を自作の関数です。最初にカーソルの位置を1段目の左側のTOPにセットするためClear DisplayもしくはReturn HomeをIRに書き込みます。本記事ではClear Displayで画面をクリアして文字を更新するようにしています。
1LINEから16文字を書き込んでいきますがEの受付待ちのウェイトを入れる必要があるため1文字ごとに80usのウェイトを置いています。
2LINEを書き込む前にカーソルを2LINEのTOPに移動する必要があります。2LINEのTOPのアドレスは0x40なのでIRのSetDDRAMaddressの0x80と論理和を計算した0x84を指定してカーソルを移動します。
2LINEの文字は1LINEと同様な処理で行います。
MCCの設定
PIC16F1827の設定はMCCを使っています。MCCの使い方については下記記事にまとめています。
MPLAB Code Configurator(MCC)の追加と使い方
今回はMCCを使用してSystem clock、TMR0の設定を行っています。
System Moduleの設定
最初に共通事項であるSystem Moduleの設定を行います。MPLAB X IDEを起動しMCCのアイコンをクリックしてMCCを有効にします。
内部クロックを使用するためINTOSCを選択しています。PIC16F1827はシステムクロック4パルスで動作クロックになるため16MHz(動作クロックは4MHz)を選択すると1命令当たり250nsの処理になります。Internal Clock以外は特に変更していません。
TMR0を設定する
TMR0はDevice Resources内のPeripherals欄のTimerを選択しTMR0をクリックして追加します。
Timer Clock内でクロックに関する設定を行います。Clock Sourceは内部クロックを使用しているためFOSC(FOSC/4)を指定します。PrescalerはClock Sourceに対してTMR0のカウントアップする分周比を指定します。
Timer PeriodはTMR0がオーバーフローするタイミングを設定します。1ms毎にオーバーフロー割り込みが発生しますがCallback Function Rateを0x01を指定しているため1ms毎にコールバック関数が呼び出されます。
void mainTimer(void); //タイマー0割り込みのコールバック関数
//コールバック関数の使用例
TMR0_SetInterruptHandler(mainTimer); //コールバック関数を指定
上記例ではmainTimer()が1ms毎にコールされます。
MCCが生成した関数の使用例
IO_RA0_SetHigh();//DOをHIGH
IO_RA0_SetLow();//DOをLOW
IO_RA0_SetHigh()関数はRA0のDO出力をHighにします。IO_RA0_SetLow()関数はRA0のDO出力をLowにします。他のポートのDOの場合はRA0の部分が対象のポート名になります。Header Files内のpin_manager.h内で#defineで定義されています。
#define LCD_RS_L IO_RB4_SetLow() //RSをL
#define LCD_RS_H IO_RB4_SetHigh() //RSをH
#define LCD_E_L IO_RB5_SetLow() //EをL
#define LCD_E_H IO_RB5_SetHigh() //EをH
本記事ではデータを書き込む際の信号名を分かりやすくするため#defineでMCC関数をマクロ化して使用しています。
動作確認
電源を入れると最初にパターン1を表示します。2秒毎にパターンを切り替えていきパターン5まで表示したらパターン1に戻る動作を繰り返し行います。
パターン1からパターン5までがLCDモジュールの設定条件である2LINEで表示されておりうまく動作していることが確認できました。
【クリエイターズファクトリー】卒業がない!挫折する心配なし!Webスクール説明会申し込み
ソースコード全体
以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。
#include "mcc_generated_files/mcc.h"
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define LCD_WAIT 1
#define SHOW_WAIT 2000
#define LCD_RS_L IO_RB4_SetLow()
#define LCD_RS_H IO_RB4_SetHigh()
#define LCD_E_L IO_RB5_SetLow()
#define LCD_E_H IO_RB5_SetHigh()
#define LCD_B0_L IO_RB0_SetLow()
#define LCD_B0_H IO_RB0_SetHigh()
#define LCD_B1_L IO_RB1_SetLow()
#define LCD_B1_H IO_RB1_SetHigh()
#define LCD_B2_L IO_RB2_SetLow()
#define LCD_B2_H IO_RB2_SetHigh()
#define LCD_B3_L IO_RB3_SetLow()
#define LCD_B3_H IO_RB3_SetHigh()
#define FUNK_4BIT 0x28
#define FUNK_DSPON 0x0C
#define FUNK_DSPCLR 0x01
#define FUNK_ENTRY 0x06
#define FUNK_HOME 0x02
#define FUNK_LINE2_TOP (0x40 +0x80)
#define LCD_SZ 16
typedef enum{
INIT_FUNK1=0,
INIT_DSP,
INIT_CLR,
INIT_ENTRY,
INIT_MAX
}INIT_LCD;
typedef enum{
L_NO1 = 0,
L_NO2,
L_NO3,
L_NO4,
L_NO5,
L_MAX
}LCD_SHOW;
int16_t timLcdWait;
int16_t timShowWait;
int8_t initmd;
uint8_t cnt;
uint8_t showcnt;
uint8_t initmoji[2][16] ={"LCD-TEST By PIC "," Ver1.00"};
uint8_t msg1[2][16] ={"Check Data1 ","Current 123.56A"};
uint8_t msg2[2][16] ={"Check Data2 ","Voltage 110.23V"};
uint8_t msg3[2][16] ={"CHECK Message1 ","I only drink tea"};
uint8_t msg4[2][16] ={"CHECK Message2 ","Thank you PIC!! "};
/* プロトタイプ宣言*/
void lcdInit(void);
void mainApp(void);
void mainTimer(void);
void LcdFuncSet0(uint8_t dat);
void LcdFuncSet(uint8_t dat);
void LcdMsgSet(uint8_t *msg);
void LcdHome(void);
void LcdClr(void);
void DspLine2Top(void);
/* Main application */
void main(void)
{
// initialize the device
SYSTEM_Initialize();
TMR0_SetInterruptHandler(mainTimer);
INTERRUPT_GlobalInterruptEnable();
INTERRUPT_PeripheralInterruptEnable();
__delay_us(50);
while(initmd != INIT_MAX){
lcdInit();
}
while (1)
{
mainApp();
}
}
/* TMR0オーバーフロー割り込みでの処理 */
/* タイマ管理関数 */
void mainTimer(void){
if( timLcdWait > TIME_UP ){
timLcdWait--;
}
if( timShowWait > TIME_UP ){
timShowWait--;
}
}
/* LCDの初期化 */
void lcdInit(void){
switch(initmd){
case INIT_FUNK1:
if(timLcdWait == TIME_UP){
timLcdWait = LCD_WAIT;
LcdFuncSet0(FUNK_4BIT);
if(++cnt >= 2){
++initmd;
}
}
break;
case INIT_DSP:
if(timLcdWait == TIME_UP){
timLcdWait = LCD_WAIT;
LcdFuncSet0(FUNK_DSPON);
++initmd;
}
break;
case INIT_CLR:
if(timLcdWait == TIME_UP){
timLcdWait = LCD_WAIT;
LcdFuncSet0(FUNK_DSPCLR);
++initmd;
}
break;
case INIT_ENTRY:
if(timLcdWait == TIME_UP){
timLcdWait = LCD_WAIT;
LcdFuncSet0(FUNK_ENTRY);
++initmd;
}
break;
}
}
/* メイン処理 */
void mainApp(void){
if(timShowWait == TIME_UP){
timShowWait = SHOW_WAIT;
switch(showcnt){
case L_NO1:
LcdMsgSet(&initmoji[0][0]);
break;
case L_NO2:
LcdMsgSet(&msg1[0][0]);
break;
case L_NO3:
LcdMsgSet(&msg2[0][0]);
break;
case L_NO4:
LcdMsgSet(&msg3[0][0]);
break;
case L_NO5:
LcdMsgSet(&msg4[0][0]);
break;
}
if(++showcnt >= L_MAX){
showcnt = L_NO1;
}
}
}
/* 文字をセット */
void LcdMsgSet(uint8_t *msg){
uint8_t i;
LcdClr(); //Clear Display
//LcdHome();
for(i=0;i < LCD_SZ; i++){
//1LINEの文字をセット
LcdFuncSet(msg[i]);
__delay_us(80);
}
DspLine2Top(); //2段目にカーソルを移動
for(i=0;i < LCD_SZ; i++){
//2LINEの文字をセット
LcdFuncSet(msg[i + LCD_SZ ]);
__delay_us(80);
}
}
/* LCDのカーソルをTOP */
void LcdHome(void){
LcdFuncSet0(FUNK_HOME);
__delay_us(2000);
}
/* 画面クリア */
void LcdClr(void){
LcdFuncSet0(FUNK_DSPCLR);
__delay_us(2000);
}
/* LINE2のトップにカーソル */
void DspLine2Top(void){
LcdFuncSet0(FUNK_LINE2_TOP);
__delay_us(80);
}
/* IRに書き込み*/
void LcdFuncSet0(uint8_t dat){
LCD_RS_L; //RSをLOW
if( dat & 0x80){
LCD_B3_H; //IR7がH
}
else{
LCD_B3_L; //IR7がL
}
if( dat & 0x40){
LCD_B2_H; //IR6がH
}
else{
LCD_B2_L; //IR6がL
}
if( dat & 0x20){
LCD_B1_H; //IR5がH
}
else{
LCD_B1_L; //IR5がL
}
if( dat & 0x10){
LCD_B0_H; //IR4がH
}
else{
LCD_B0_L; //IR4がL
}
LCD_E_H; //EをH
__delay_us(1);
LCD_E_L; //EをL(データの確定)
__delay_us(1);
if( dat & 0x08){
LCD_B3_H; //IR3がH
}
else{
LCD_B3_L; //IR3がL
}
if( dat & 0x04){
LCD_B2_H; //IR2がH
}
else{
LCD_B2_L; //IR2がL
}
if( dat & 0x02){
LCD_B1_H; //IR1がH
}
else{
LCD_B1_L; //IR1がL
}
if( dat & 0x01){
LCD_B0_H; //IR0がH
}
else{
LCD_B0_L; //IR0がL
}
LCD_E_H; //EをH
__delay_us(1);
LCD_E_L; //EをL(データの確定)
__delay_us(1);
}
/* DRに書き込み */
void LcdFuncSet(uint8_t dat){
LCD_RS_H; //RSをHIGH
if( dat & 0x80){
LCD_B3_H;
}
else{
LCD_B3_L;
}
if( dat & 0x40){
LCD_B2_H;
}
else{
LCD_B2_L;
}
if( dat & 0x20){
LCD_B1_H;
}
else{
LCD_B1_L;
}
if( dat & 0x10){
LCD_B0_H;
}
else{
LCD_B0_L;
}
LCD_E_H;
__delay_us(1);
LCD_E_L;
__delay_us(1);
if( dat & 0x08){
LCD_B3_H;
}
else{
LCD_B3_L;
}
if( dat & 0x04){
LCD_B2_H;
}
else{
LCD_B2_L;
}
if( dat & 0x02){
LCD_B1_H;
}
else{
LCD_B1_L;
}
if( dat & 0x01){
LCD_B0_H;
}
else{
LCD_B0_L;
}
LCD_E_H;
__delay_us(1);
LCD_E_L;
__delay_us(1);
}
/* End of File */
本ソースコードはMPLAB X IDEにMCCのプラグインをインストールしていることが前提となります。MCCをインストールする方法は下記記事を参考にしてください。
MPLAB Code Configurator(MCC)の追加と使い方
関連リンク
PICマイコンを使ってマイコンのレジスタの設定やMPLAB X IDEのプラグインであるMCCを使用して動作確認したことについてまとめています。
PICマイコン(PIC12F675)で実現できる機能と解説リンクまとめ
PICマイコン(PIC16F1827)で実現できる機能と解説リンクまとめ
最後まで、読んでいただきありがとうございました。