第44回 液晶モジュール動作確認プログラム

液晶モジュールに温湿度・気圧データを表示するプログラムを作成する前に、動作確認目的として簡単な文字列を表示するプログラムを作成します。

動作確認で行うこと

温湿度・気圧センサから読み取った値を液晶モジュールに表示する前に、簡単な文字列を液晶モジュールに表示するプログラムを作成して動作確認します。

この記事では、固定の文字列(例えば「Hello!」など)を、液晶ディスプレイの左上の位置から表示するプログラムを作成してみます。作成したプログラムをベースに、ご自分で表示文字列や表示位置を変更して、液晶モジュールの制御方法の理解を深めていただければと思います。

 

関数作成

実践編第40回でI2C通信の関数を作成しました。今回はそれらのI2C通信関数を使用して順番に機能関数を作成していきます。

I2C通信関数

最初に作成する関数は、以下のI2C通信手順を実現する関数です。

Pic practice 44 i2c protocol lcd

なお、ACK/NACKについてはチェックしないことにします。というのは、ACK/NACKチェックをしてNACK(了解できなかった)という場合のPICマイコン側での対処方法が難しいためです。

このI2C通信手順を実現する関数名は「lcdI2CProtocol」とし、引数は、スレーブアドレス(address)、データ種別を示す1バイト(control_code)、データ1バイト(data)にします。

ACK/NACKチェックはしませんので、関数は以下のように簡単に作成してみました。

void lcdI2CProtocol(uint8_t address, uint8_t control_code, uint8_t data) {

i2cProtocolStart();                 // スタートコンディション
i2cProtocolSendData(address);       // アドレス送信
i2cProtocolSendData(control_code);  // 制御コード送信 (動作設定=0x00/文字表示=0x40)
i2cProtocolSendData(data);          // データ送信
i2cProtocolStop();                  // ストップコンディション

return;
}

液晶モジュールのスレーブアドレスは0x7cですので、例えば表示文字として0x41(=’A’)を送りたい場合は、

lcdI2CProtocol(0x7c, 0x40, 0x41);

と記述すればOKです。

 

動作設定関数と文字表示関数

動作設定や文字表示を行う場合、先ほどのように「lcdI2CProtocol(0x7c, 0x40, 0x41);」などと関数コールすればとりあえず文字表示できますが、このようにするとプログラム記述がちょっと面倒になります。

というのは、動作設定を行う場合、動作設定のためのデータを液晶モジュールに送信したあと、液晶モジュール側で設定処理が行われるためちょっとの時間待ってあげる必要があります。動作確認したところ、動作設定の場合は1つの設定を行うたびに1ms程度時間待ちをした方が安定して動作するようです。また文字表示の場合は待ち時間なしで動作しました。

実はデータシートを見ると、以下のようにそれぞれの動作設定後は26.3μsより長い時間待つ必要があります。

Pic practice 44 command wait

ただ、いろいろな条件で試したところ、50μs〜70μsの処理待ちをしてもうまく動作しないケースがありました。特に深い意味はありませんが、動作設定後の待ち時間は1msでプログラムを作成しました。

先ほどのlcdI2CProtocol関数を使用して、待ち時間込みの関数として「lcdSendCommandData」という関数を作成します。引数は動作設定データの1バイトとしました。

void lcdSendCommandData(uint8_t command){

// コマンドを送信する場合の制御コードは0x00
lcdI2CProtocol(LCD_I2C_ADDRESS, 0x00, command);

// ウエイト
//   データシートではウエイト時間は26.3us以上になっているが、
//   それより長くしないと初期化できないケースがあるため1msのウエイトを入れる
__delay_ms(1);

return;
}

また文字表示ですが、こちらは色々試したところ表示文字データを液晶モジュールに送信したあと、特に待ち時間なしでも問題なく表示されていました。ただ電源の状態によっては問題が発生する可能性がありますので、コメントとして記述しておきました。関数名は「lcdSendCharacterData」とし、引数は表示文字データの1バイトとしました。

void lcdSendCharacterData(uint8_t data){

// 表示文字のデータを送信する場合の制御コードは0x40
lcdI2CProtocol(LCD_I2C_ADDRESS, 0x40, data);

// ウエイト
//   文字表示の場合はウエイトを入れなくても動作しているが
//   表示されない場合は1ms程度のウエイトを入れる
// __delay_ms(1);

return;
}

 

液晶モジュール初期化関数

液晶モジュールの初期化関数は、データシートを元に作成しました。関数名は「lcdInitialization」として引数はありません。先ほどの初期化処理を参考に以下のように作成しました。

void lcdInitialize(void){

// 初期化コマンド送信
lcdSendCommandData(0x38); // 2行モードに設定
lcdSendCommandData(0x39); // 拡張コマンド選択
lcdSendCommandData(0x14); // 内部クロック周波数設定
lcdSendCommandData(0x70); // コントラスト設定(C3:C0 = 0b0000に設定)
lcdSendCommandData(0x56); // 電源電圧が3.3VなのでBooster=ONに設定。コントラスト設定はC5:C4 = 0b10
lcdSendCommandData(0x6c); // オペアンプのゲイン設定

// モジュール内電源安定化のための時間待ち
__delay_ms(200);

// 初期化コマンド続き
lcdSendCommandData(0x38); // 通常コマンド選択
lcdSendCommandData(0x01); // ディスプレイ表示内容クリア
lcdSendCommandData(0x0c); // ディスプレイ表示

return;
}

なお、コントラスト設定はC5〜C0の6ビットで行います。上のプログラムの設定は0b100000(=32)としていますが、濃度調整をしたい場合はこの値を変更してみてください。

また、0x6cの設定のあとはデータシートによると200ms以上のウエイトが必要となっていますので、200msのウエイトを入れてあります。

 

表示内容クリア関数

表示内容をクリアする関数も作成しておきます。関数にするまでもないですが、わかりやすくしたいので「lcdClearDisplay」という関数を作成しておきます。表示内容をクリアするには、動作設定の0x01を液晶モジュールに送信するだけでOKです。以下のように作成しました。

void lcdClearDisplay(void){

lcdSendCommandData(0x01);

return;
}

 

表示位置指定関数

文字の表示位置を指定する関数も作成しておきます。前回の記事で説明しましたが、以下のように座標位置指定できるようにしておきます。

Pic practice 43 positioning

関数名は「lcdLocateCursor」とします。引数は座標(x, y)として、position_xとposition_yとしました。

表示位置はアドレスで指定できることを説明しましたが、アドレスの計算は以下のようになります(前回の説明内容です)。

アドレスがわかったら、コマンドは第7ビットを1にすればいいので、コマンドは以下のようになります。

コマンド = アドレス + 0x80
または、
コマンド = アドレス | 0x80
(|は論理和)

これをこのままプログラムにすると以下のようになります。

void lcdLocateCursor(uint8_t position_x, uint8_t position_y){

uint8_t address;  // 文字表示位置(アドレス)
uint8_t command;  // 表示位置指定コマンド

// Y座標位置のアドレス算出
switch( position_y ) {
// 1行目の場合
case 0:
address = 0x00;
break;
// 2行目の場合
case 1:
address = 0x40;
break;
}

// X座標を加えてアドレスを算出する
address += position_x;

// 第7ビットを1にしてコマンドにする
command = address + 0x80;

// 文字表示位置指定コマンド送信
lcdSendCommandData( command );

return;
}

ただ、このようにプログラムを作成すると冗長になってしまいますので、短くしたいと思います。

y座標が0の時は0x00、1のときは0x40です。これは条件判断で算出するのではなく、「0x40 * y」を計算すれば求めることができます。

0x40 * 0 = 0x00
0x40 * 1 = 0x40

となりますので、ここまで計算できれば、あとはx座標を加算して、コマンドにするためにさらに0x80を加算すればアドレスが求められます。これらのことから、表示位置指定の関数は以下のように作成してみました。

void lcdLocateCursor(uint8_t position_x, uint8_t position_y){

lcdSendCommandData( 0x80 + 0x40 * position_y + position_x );

return;
}

 

文字列表示

文字を表示する場合、1文字の場合は簡単です。例えば「A」という文字を表示したい場合、

lcdSendCharacterData(0x41);

または、’A’は0x41ですので、

lcdSendCharacterData(‘A’);

とすれば、現在設定されている表示位置に「A」が表示されます。

文字列を表示した場合はどうすればよいでしょうか。「Hello!」と表示したい場合は、一文字ずつ上のようにlcdSendCharacterData関数で表示すればよいですが、この方法ですと面倒です。

ちょっと話が飛びますが、C言語を勉強された時、一番最初に出てきたのは「printf」だったのではないでしょうか。「printf(“Hello, world.”);」というようなプログラムが出てきたのではないかと思います。このprintfを使って液晶モジュールに文字列が表示できれば便利ですよね。

XCコンパイラでは、このprintf関数の出力先をカスタマイズできるようになっています。printf関数の引数の文字列は、通常ディスプレイなどに表示されます。XCコンパイラのprintf関数は、文字表示の部分はputch関数を使用しており、自分でputch関数を定義することにより、出力先をカスタマイズすることができるようになっています。

putchは引数がchar型の1バイトですので、以下のようにputc関数を記述しておくと液晶モジュールに文字列を表示することができます。

void putch(char character) {

lcdSendCharacterData(character);

return;
}

この関数をプログラム内に記述しておけば、通常のprintf関数の表示先が液晶モジュールになります。printf関数ですので、文字列だけではなく、以下のように数字を表示することが可能となります。

printf(“Humidity = %d”, 57);
printf(“Temp. = %2.1f”, 27.5);

(実行結果)
Humidity = 57
Temp. = 27.5

次回の記事では、温湿度と気圧データを表示するプログラムを作成しますが、数値の表示はこのようなprintf関数を使用します。

 

動作確認プログラム

これで必要な関数ができました。動作確認のプログラムは以下のようになります。注意点としては、電源投入後は液晶モジュールの動作が安定するまで100ms1程度ウエイトします。

/*
* 液晶モジュール動作確認プログラム
*
* https://tool-lab.com/
*
* 更新
*   2018. 7.28: 新規作成
*
*/

//
// PIC16F18857コンィグレーション設定
//
// CONFIG1
#pragma config FEXTOSC = OFF    // External Oscillator mode selection bits (Oscillator not enabled)
#pragma config RSTOSC = HFINT1  // Power-up default value for COSC bits (HFINTOSC (1MHz))
#pragma config CLKOUTEN = OFF   // Clock Out Enable bit (CLKOUT function is disabled; i/o or oscillator function on OSC2)
#pragma config CSWEN = ON       // Clock Switch Enable bit (Writing to NOSC and NDIV is allowed)
#pragma config FCMEN = ON       // Fail-Safe Clock Monitor Enable bit (FSCM timer enabled)
//
// CONFIG2
#pragma config MCLRE = OFF      // Master Clear Enable bit (IO)
#pragma config PWRTE = ON       // Power-up Timer Enable bit (PWRT enabled)
#pragma config LPBOREN = OFF    // Low-Power BOR enable bit (ULPBOR disabled)
#pragma config BOREN = ON       // Brown-out reset enable bits (Brown-out Reset Enabled, SBOREN bit is ignored)
#pragma config BORV = LO        // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (VBOR) set to 1.9V on LF, and 2.45V on F Devices)
#pragma config ZCD = OFF        // Zero-cross detect disable (Zero-cross detect circuit is disabled at POR.)
#pragma config PPS1WAY = ON     // Peripheral Pin Select one-way control (The PPSLOCK bit can be cleared and set only once in software)
#pragma config STVREN = ON      // Stack Overflow/Underflow Reset Enable bit (Stack Overflow or Underflow will cause a reset)
//
// CONFIG3
#pragma config WDTCPS = WDTCPS_31// WDT Period Select bits (Divider ratio 1:65536; software control of WDTPS)
#pragma config WDTE = OFF       // WDT operating mode (WDT Disabled, SWDTEN is ignored)
#pragma config WDTCWS = WDTCWS_7// WDT Window Select bits (window always open (100%); software control; keyed access not required)
#pragma config WDTCCS = SC      // WDT input clock selector (Software Control)
//
// CONFIG4
#pragma config WRT = OFF        // UserNVM self-write protection bits (Write protection off)
#pragma config SCANE = available// Scanner Enable bit (Scanner module is available for use)
#pragma config LVP = ON         // Low Voltage Programming Enable bit (Low Voltage programming enabled. MCLR/Vpp pin function is MCLR.)
//
// CONFIG5
#pragma config CP = OFF         // UserNVM Program memory code protection bit (Program Memory code protection disabled)
#pragma config CPD = OFF        // DataNVM code protection bit (Data EEPROM code protection disabled)

//
// ヘッダファイル
//   int_t型を使用するため、stdint.hをインクルード
//
#include <xc.h>
#include <stdint.h>
#include <stdio.h></stdio.h></stdint.h></xc.h>

//
// 定数
//

// I2C Ack/Nack定義
#define I2C_ACK  0x00
#define I2C_NACK 0xff

// LCDモジュール
#define LCD_I2C_ADDRESS 0x7c  // LCDモジュールのI2Cアドレス

//
// 関数プロトタイプ宣言
//

// LCDモジュール表示制御関数
void lcdInitialize(void);               // LCD初期化
void lcdClearDisplay(void);             // ディスプレイ全消去
void lcdSendCommandData(uint8_t);           // コマンド送信
void lcdSendCharacterData(uint8_t);              // 1文字表示
void lcdLocateCursor(uint8_t,uint8_t);  // カーソル位置指定

// LCDモジュールI2Cプロトコル関数
void lcdI2CProtocol(uint8_t, uint8_t, uint8_t);

// I2Cプロトコル各信号の生成関数
void    i2cProtocolStart(void);        // スタートビット生成
void    i2cProtocolStop(void);         // ストップビット生成
void    i2cProtocolSendData(uint8_t);  // 1バイトデータ送信
uint8_t i2cProtocolCheckAck(void);     // ACK信号チェック

// クロック周波数
// __delay_ms()関数が時間基準に使用する
#define _XTAL_FREQ 4000000

//
// main関数
//
void main(void) {

// 動作周波数設定
OSCCON1bits.NDIV = 0b0000;  // 分周1:1
OSCFRQbits.HFFRQ = 0b010;   // 4MHz

// ピン属性設定
ANSELC = 0b00000000;
TRISC  = 0b01001100;

//
// I2C通信ピンのPPS設定
//
// 設定ロック解除
PPSLOCK = 0x55;
PPSLOCK = 0xAA;
PPSLOCKbits.PPSLOCKED = 0x00;

// SCL, SDAピンの割り当て
SSP1DATPPS = 0x12;   // RC2をMSSP1:SDA1に設定
SSP1CLKPPS = 0x13;   // RC3をMSSP1:SCL1に設定
RC3PPS = 0x14;   // RC3をMSSP1:SCL1に設定
RC2PPS = 0x15;   // RC2をMSSP1:SDA1に設定

// 設定ロック
PPSLOCK = 0x55;
PPSLOCK = 0xAA;
PPSLOCKbits.PPSLOCKED = 0x01;

//
// I2C通信設定
//
// SMP Standard Speed; CKE disabled;
SSP1STAT = 0x80;
// SSPEN enabled; CKP Idle:Low, Active:High; SSPM FOSC/4_SSPxADD_I2C;
SSP1CON1 = 0x28;
// SBCDE disabled; BOEN disabled; SCIE disabled; PCIE disabled; DHEN disabled; SDAHT 300ns; AHEN disabled;
SSP1CON3 = 0x00;
// Baud Rate Generator = 100kHz
SSP1ADD = 0x09;

// LCDモジュール電源安定化時間待ち
__delay_ms(100);

// LCD初期化
lcdInitialize();

// LCD表示クリア(初期化関数内でクリアしているので必要ないですが...)
lcdClearDisplay();

// LCD表示位置を左上に設定
lcdLocateCursor(0, 0);

// 表示位置からtest_文字列を表示
printf("Hello!");

// 動作ストップ
while(1){
}

}

//
// LCDモジュールに制御コードまたはデータを送信
//
void lcdI2CProtocol(uint8_t address, uint8_t control_code, uint8_t data) {

i2cProtocolStart();                 // スタートコンディション
i2cProtocolSendData(address);       // アドレス送信
i2cProtocolSendData(control_code);  // 制御コード送信 (動作設定=0x00/文字表示=0x40)
i2cProtocolSendData(data);          // データ送信
i2cProtocolStop();                  // ストップコンディション

return;
}

//
// 表示文字データ送信
//   0x40の後にデータを送信
void lcdSendCharacterData(uint8_t data){

// 表示文字のデータを送信する場合の制御コードは0x40
lcdI2CProtocol(LCD_I2C_ADDRESS, 0x40, data);

// ウエイト
//   文字表示の場合はウエイトは必要なくても動作しているが
//   表示されない場合は1ms程度のウエイトを入れる
// __delay_ms(1);

return;
}

//
// コマンド送信
//   0x00の後にコマンドを送信
//
void lcdSendCommandData(uint8_t command){

// コマンドを送信する場合の制御コードは0x00
lcdI2CProtocol(LCD_I2C_ADDRESS, 0x00, command);

// ウエイト
//   データシートではウエイト時間は26.3us以上になっているが、
//   それより長くしないと初期化できないケースがあるため1msのウエイトを入れる
__delay_ms(1);

return;
}

//
// ディスプレイ消去
//
void lcdClearDisplay(void){

lcdSendCommandData(0x01);

return;
}

//
// カーソル位置移動
//    引数は水平方向右側プラスのX軸、垂直方向下側プラスのY軸で、それぞれ0から開始
//    左上の座標が(x=0, y=0)
//
void lcdLocateCursor(uint8_t position_x, uint8_t position_y){

lcdSendCommandData( 0x80 + 0x40 * position_y + position_x );

return;
}

//
// printf関数の文字出力部分
//
void putch(char character) {

lcdSendCharacterData(character);

return;
}

//
// LCDモジュール初期化
//
void lcdInitialize(void){

// 初期化コマンド送信
lcdSendCommandData(0x38); // 2行モードに設定
lcdSendCommandData(0x39); // 拡張コマンド選択
lcdSendCommandData(0x14); // 内部クロック周波数設定
lcdSendCommandData(0x70); // コントラスト設定(C3:C0 = 0b0000に設定)
lcdSendCommandData(0x56); // 電源電圧が3.3VなのでBooster=ON、コントラスト設定(C5:C4 = 0b10に設定)
lcdSendCommandData(0x6c); // オペアンプのゲイン設定

// モジュール内電源安定化のための時間待ち
__delay_ms(200);

// 初期化コマンド続き
lcdSendCommandData(0x38); // 通常コマンド選択
lcdSendCommandData(0x01); // ディスプレイ表示内容クリア
lcdSendCommandData(0x0c); // ディスプレイ表示

return;
}

//
// I2Cプロトコル制御関数
//

// スタートコンディション生成
void i2cProtocolStart() {

// SSP1CON2レジスタのSENビットを1に設定すると
// スタートコンディションが生成される
// 発行が完了するとSSP1IFが1になるのでwhile文で待つ
SSP1IF = 0;
SSP1CON2bits.SEN = 1;
while (SSP1IF == 0) {}
SSP1IF = 0;

return;
}

// ストップコンディション生成
void i2cProtocolStop() {

// SSP1CON2レジスタのPENビットを1に設定すると
// ストップコンディションが生成される
// 発行が完了するとSSP1IFが1になるのでwhile文で待つ
SSP1IF = 0;
SSP1CON2bits.PEN = 1;
while (SSP1IF == 0) {}
SSP1IF = 0;

return;
}

// 1バイトデータ送信
void i2cProtocolSendData(uint8_t data) {

// SSP1BUFに送信したいデータをセットすると、そのデータが送信される
// 発行が完了するとSSP1IFが1になるのでwhile文で待つ
SSP1IF = 0;
SSP1BUF = data;
while (SSP1IF == 0) {}
SSP1IF = 0;

return;
}

// Ack/Nackチェック
uint8_t i2cProtocolCheckAck() {

uint8_t ackStatus;

if (SSP1CON2bits.ACKSTAT) {
ackStatus = I2C_NACK;
} else {
ackStatus = I2C_ACK;
}

return ackStatus;
}

 

更新履歴

日付 内容
2018.7.29 新規投稿