ArduinoでLCD制御(QAPASS1602)と、c++のお話。

ArduinoでLCDを制御する++

いまで、このサイトでは使用するLCDとしてとして AQM1602Y-RN-GBW を使ってきました。
 ・Arduinoを使ってみよう
 ・Arduinoでタイマー割り込み

これらの記事でも触れていますが、このLCDモジュールは3.3Vで動作しますがレベル変換など入れず、モジュールの実力で動作しています。
ソフト屋は、こういったトコロがいいかんげんで電気屋が怒るポイントです。 (:P)

そこで、今回はArduinoで動作する5VのLCDモジュール、QAPASS1602を手に入れ、マトモな5Vの回路で動作させてみました。

今回は、デバイスを動かすだけ?
で、終わらないのがLightning Brains。

そこで、今回はArduino開発環境(c++)についてちょいとばかし
掘ってみました。



 目次:          

 ・ QAPASS 1602とは?

 ・ さっそくLCDモジュールを使ってみよう

 ・ まずは、”Hello World.”を表示させてみよう 

 ・ Arduino IDEを掘ってみる!

 ・ Arduino IDEって、c++じゃん?

 ・ main.cppからいじってみる

 ・ Arduino IDEを使ったc++開発は可能か?



QAPASS 1602とは?

Amazonでも入手できるので、Arduinoな世界では標準的なデバイスなんでしょうか?
価格も500円前後、ずいぶん安いトコロもありますがソコは各自で判断して購入ください。

モジュールの仕様も、バックライトあり・ナシなど違いがあるので気をつけてください。

   

このLCDモジュールですが、コントローラーは HD44780 という日立が大昔に作ったモノが今でも標準として使われています。これらのモジュールはその互換品が使われているようです。

このコントローラーの特徴としては、データ線を4本して通信できるところです。その他に制御線が2本必要なので、全部で6本のデジタル出力が必要になります。
データ線が4本なので、実際にデータを1バイト送る際には、上位4ビットと下位4ビットに分けて送る必要があります。

それでも、I2Cと比較すると通信線が多いので、このLCDモジュールをI2Cに変換する基盤もあります。


しかし、この記事では、4ビット通信の6本のデジタル出力での制御とします。

 制御線:
  ・RS:レジスタ選択、送るデータがレジスタ設定か表示データかを識別
  ・RW:リード/ライト、今回は書き込みだけなのでGNDに固定
  ・Enable:データ取り込みのタイミング
 データ線(4ビット):
  ・D4~D7:上位4ビットを使用。下位4ビットは使わない



さっそくLCDモジュールを使ってみよう

今回も、Arduino Microで動作させてみましょう。
(Arduino UNOの記事は世の中に溢れてますからね)

回路的には、コントラストを制御するための半固定抵抗を入れる必要があります。


回路図

Arduinoからは、5V電源とGND、デジタル出力として6本のデジタル出力を使います。

 LCDモジュールとArduino Microのデジタル出力ピンの接続
  LCD 4:RS ← Micro D6
  LCD 6:E ← Micro D4
  LCD 11:D4 ← Micro D3
  LCD 12:D5 ← Micro D2
  LCD 13:D6 ← Micro D0
  LCD 14:D7 ← Micro D1

※ データ線の接続がこうなった理由は後ほど。。。

VDDとAは+5Vへ。
VOは半固定抵抗の可変値に。
VSSとKはGNDへ。また、RWはW固定なのでGNDへ接続します。

AとKはバックライトです。


まずは、”Hello World.”を表示させてみよう

以下、"Hello World"のソース、LCD.ino

#include 

LiquidCrystal lcd(6, 4, 3, 2, 0, 1);

void setup() {
  // put your setup code here, to run once:
  lcd.begin(16, 2);             // LCDモジュール初期化

  lcd.setCursor(0, 0);          // 書き出し位置を指定(1行目、1文字目)
  lcd.print("Lightning Brains");

  lcd.setCursor(2, 1);          // 書き出し位置を指定(2行目、2文字目)
  lcd.print("Hello World.");

}

void loop() {
  // put your main code here, to run repeatedly:

}


QAPASS1602表示の様子

ライブラリを使うと簡単にできてしまいますね。

先程、このLCDモジュールのコントローラーは大昔に日立が作ったモノだと書きました。
実は私もその昔、日立のマイコンH8でよく使ったものです。

いまでも秋月電子通商で、売っています。

そうだ、探せばまだ基盤があるんじゃないか?
いでよ!古のH8基盤!!

他人からはガラクタと呼ばれる宝箱を引っ掻き回すこと数十分。
あった、あった。
H8/3052F LAN開発キット!

いまでも秋月電子通商で、売っているので、HPから回路図をダウンロードしてLCDモジュールを制御しているピンに接続すると、



一発で動いた!

ライブラリを使うと簡単にできてしまいますね。
 でも、つまんないなー

Arduino IDE ってどうなってんだろう?
ココから先は、コアな世界。。。



Arduino IDEを掘ってみる!

そう言えば、まだArduino IDEの探検をしていませんでした。
そこで、今回はArduino IDEの正体に迫ってみたいと思います。
(ちなみにこの記事を書いているときのArduino IDEのバージョンは、1.8.10)

まずは、今回使った”LiquidCrystal.h”。
コヤツはドコに居るのか?

...\arduino-1.8.10\libraries\LiquidCrystal\src
この下に、LiquidCrystal.h と LiquidCrystal.cpp があります。
コレが、LiquidCrystalライブラリの正体です。

では、ディレクトリ構造を探ってみましょう。
...\arduino-1.8.10\libraries\
 この下に、各種のライブラリが存在しています。

今回の、LiquidCrystalライブラリは
...\arduino-1.8.10\libraries\LiquidCrystal\
にあり、この下にソース(src)とサンプル(examples)があります。
ライブラリの使い方はexamplesを見ればわかりますね。

では、以前に使ったI2Cのライブラリ”Wire”はドコにいるのでしょうか?
マイコンコアに含まれるハードウェア機能は、マイコンごとのハードウェアライブラリとして存在しています。

Arduino Microの場合、マイコンはavrなので、
...\arduino-1.8.10\hardware\arduino\avr\libraries\Wire\src
に、Wire.h と Wire.cpp として存在しています。

Arduinoの機能を構成するソースは、
.cppファイルや.cファイル、それと.hファイルから成り立っているようです。
一部、アセンブラのコードもあったりしますね。


また、別のハードウェア(マイコン)を使う場合、ライブラリがダウンロードされます。


Arduino IDEって、c++じゃん?

そうなんです、Arduino IDEの正体はc++です。

c++ならば、main()から始まるはずでは?

実は、main()もマイコンごとのコードとして存在しています。この、main()も含めたコア機能(インクルード指定の必要がない機能)一式が、
...\arduino-1.8.10\hardware\arduino\avr\\cores\arduino
以下に存在しています。

Arduinoの手始めとして、シリアルに”Hallo World”を出力したりしたと思います。
このとき、いきなりSerialを使って”Hallo World”が出力されます。
Serial1を使っても同様に、出力されます。
これらのコアに含まれるSerialが使われています。

では、
.inoとは何か?
c++との関係は?

Arduino IDE のビルド動作を探るため、コンパイル動作の詳細を調べてみましょう。
メニューから[ファイル] → [環境設定] → "設定"タブの「より詳細な情報を表示する:」 の "コンパイル" にチェックを入れてください。



この状態でビルドすると、コンパイル時の詳細が表示されます。
実際にどのようなコマンドで一通りの作業が行われるのかが確認できます。

以下、先程のLCD.inoがどのようにビルドされていくかを簡単に説明すると、

まず、最初にarduino-builderが実行されます。
arduino-builderは、コンパイルを行うための設定や、.inoフィアルをプリコンパイルして.cppファイルに変換します。

 LCD.ino → LCD.ino.cpp

"~\\AppData\\Local\\Temp\\arduino_build_126451\\sketch\\LCD.ino.cpp"
このarduino_build_126451というテンポラリなディレクトリで作業が進められます。

arduino-builderは多くの機能を持っています。引数で/?または—helpを与えることでヘルプが表示されます。

arduino-builderによって生成された.cppファイルは、avr-g++でコンパイルされます。
 \\arduino-1.8.10\\hardware\\tools\\avr/bin/avr-g++

 LCD.ino → LCD.ino.cpp → LCD.ino.cpp.o

このavr-g++はGNUコンパイラで、--versionでバージョンを確認できます。

avr-g++ (GCC) 7.3.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

今回使っている外部ライブラリ LiquidCrystal もavr-g++でコンパイルされます。

同様に、コア機能一式もavr-g++でコンパイルし、avr-gcc-arで一度core.aにアーカイブされます。
 ~\\AppData\\Local\\Temp\\arduino_build_126451\\core\\core.a

作成されたオブジェクトファイルはavr-gccでリンクされて、LCD.ino.elfが作成されます。
作成したelfファイルは、avr-objcopyでLCD.ino.eepとしてeeprom格納形式に変換され、
さらにavr-objcopyでLCD.ino.hexファイルに変換されます。

基本、ログに出てくる通りコマンドプロンプトで操作すれば同様のことはできるようですね。
(一部パスを指定してやるところがあったが。。。)

また、.inoファイルはプリコンパイルで.cppファイルに変換されてからg++でコンパイルされるので実態はGNU c/c++の開発環境であることがわかります。

ディレクトリ構成や、マクロなどのオプションはバージョンアップで変化する可能性もあるのでコレを覚えてコマンドで操作できるようになっても意味はないでしょうね。


main.cppからいじってみる

main()はコア機能の中で main.cpp として定義されています。
 ~\arduino-1.8.10\hardware\arduino\avr\cores\arduino\main.cpp

本当にこのmain()が、このc++開発環境のmain()なのでしょうか?

実験してみましょう!

下記のコードでは、main()内で直接、Arduino Microボード上のLEDを点滅するコードを書き加えてみました。(setup()やloop()はナニも書かずでOK!)

修正したmain.cpp

/*
  main.cpp - Main loop for Arduino sketches
  Copyright (c) 2005-2013 Arduino Team.  All right reserved.

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

#include 

// Declared weak in Arduino.h to allow user redefinitions.
int atexit(void (* /*func*/ )()) { return 0; }

// Weak empty variant initialization function.
// May be redefined by variant files.
void initVariant() __attribute__((weak));
void initVariant() { }

void setupUSB() __attribute__((weak));
void setupUSB() { }

int main(void)
{
 init();

 initVariant();

#if defined(USBCON)
 USBDevice.attach();
#endif
// Lightning Brains Update Start
 DDRC |=  0x80;
 PORTC |= 0x80;
// Lightning Brains Update END
 setup();
    
 for (;;) {
  loop();
  if (serialEventRun) serialEventRun();
// Lightning Brains Update Start
  PORTC ^= 0x80;
  delay(500);
// Lightning Brains Update END
 }
        
 return 0;
}

LEDの点滅が確認できると思います。
つまり、初期化手順や関数名を変えたい場合などmain.cppを直接変更することで対応が可能ということです。

※ このmain.cppのコメントにあるように、GPLv2.1でライセンスされていますので修正時はライセンスに従った対応が必要になります。


Arduino IDEを使ったc++開発は可能か?

結論から言うと、可能です。

注意点として、.inoファイルは必ず必要。.inoファイルは、あればよいので、プロジェクトのメモ代わりにでもしておきましょうか。

そこで、前回と同様のストップウオッチをc++で実装したいと思います。
特にmain()、setup()、loop()のフレームワークが不要なわけではないので、これらの構造についてはそのまま利用します。

ソース構成は下記のようになります。
 StopWatch.ino → すべてコメント、ライセンス表記やソース構成について
 main.cpp → そのまま利用
 mainLoop.cpp → setup()、loop()、割り込みハンドラなど
 LcdLib.cpp → LCD制御クラス
 LcdLib.h → LCD制御クラスのヘッダ

ストップウオッチ、
StopWatch.ino
/*!
 * Copyright <2020> 
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do
 * so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
 * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/**
   Project Files:
     StopWatch.ino : This file. 
     mainLoop.cpp : setup(), loop(), ISR routines
     LcdLib.cpp   : HD44780 Compatible Controller
     LcdLib.h     : LcdLib header file.
**/


MainLoop.cpp
#include <Arduino.h>
#include "LcdLib.h"

const byte INPUT_PIN = 7;
const byte RESET_PIN = 5;

// Global
LcdLib* lcd;

unsigned long msTime;
bool intF = true;

int hh = 0;
int mm = 0;
int ss = 0;
int ms = 0;

void switchON();

void setup() {
  
  lcd = new LcdLib(2);
  delay(1000);
  lcd->lcdWrite("Lightning Brains");

  TCCR1A = 0b00000000;  // Mode4(MGM10=0,MGM11=0) CTC
  TCCR1B = 0b00001011;  // Mode4(MGM12=1,MGM13=0):clkI/64 (From prescaler)
  TIMSK1 = 0b00000000;
  OCR1A = 250;

  // 割り込み設定:入力ピン、割り込みハンドラ、割り込み条件はHIGH→LOW
  attachInterrupt(digitalPinToInterrupt(INPUT_PIN), switchON, FALLING);
  msTime = millis();

  // リセット入力
  pinMode(RESET_PIN, INPUT);
}

void loop() {

  if(intF) {
    char  buf[18];            // 表示バッファ

    // 経過時刻表示
    sprintf(buf, "%02d:%02d:%02d.%03d", hh, mm, ss, ms);
    lcd->lcdCursor(2, 1);
    lcd->lcdWrite(buf);

    intF = false;
  }
  if(TIMSK1 == 0b00000000) {
    if(digitalRead(RESET_PIN)==LOW) {
      hh = 0;
      mm = 0;
      ss = 0;
      ms = 0;
      intF = true;
    }
  }
}

// タイマー割り込みハンドラ
//   IMPUT_PIN HIGH→LOW
//
ISR(TIMER1_COMPA_vect) {

  intF = true;
  
  // 経過時刻更新
  ++ms;
  if(ms > 999) {
    ms = 0;
    ++ss;
    if(ss > 59) {
      ss = 0;
      ++mm;
      if(mm > 59) {
        mm = 0;
        ++hh;
        if(hh > 23) {
          hh = 0;
          mm = 0;
          ++hh;
          if(hh > 23)
            hh = 0;
        }
      }
    }
  }
}

// スイッチ割り込みハンドラ
//   IMPUT_PIN HIGH→LOW
//
void switchON() {
  if((millis() - msTime) > 50) {
    if(TIMSK1 == 0b00000000) {
      TIMSK1 = 0b00000010;  // OCIE1A: Timer/Countern, Output Compare A Match Interrupt Enable
    }
    else {
      TIMSK1 = 0b00000000;  // OCIE1A: Timer/Countern, Output Compare A Match Interrupt Disable
    }
    TCNT1 = 0;
    msTime = millis();
  }
}


LCD制御、LcdLib.cpp
#include <Arduino.h>
#include "LcdLib.h"

#define PORTD4BIT(a)  PORTD = ((PORTD & 0xF0) | (a & 0x0F))
#define RS    0b10000000;
#define E     0b00010000;

LcdLib::LcdLib(int line) {

  // Initialize LCD module PIN
  DDRD |= 0x9F;
  PORTD &= 0x9F;

  delay(500);         // Power ON after delay

  // 4Bit mode function set
  lcd4BitWrite(0, 0x03);
  delay(50);
  lcd4BitWrite(0, 0x03);
  delay(2);
  lcd4BitWrite(0, 0x03);
  delay(2);
  lcd4BitWrite(0, 0x02);
  delay(2);

  if(line > 1)
    lineMode = true;
  else
    lineMode = false;

  // Display clear
  lcdClear();
  
  return;

}

LcdLib::~LcdLib() {
  
}

void LcdLib::lcdClear() {

  if(lineMode)
    lcdByteWrite(0, 0x28);
  else
    lcdByteWrite(0, 0x20);
    
  delay(2);

  lcdByteWrite(0, 0x0C);
  delay(2);
  lcdByteWrite(0, 0x06);
  delay(2);
  lcdByteWrite(0, 0x01);
  delay(200);

  return;
}

void LcdLib::lcdCursor(int x, int y) {
  byte cmd = 0x80;

  if ( y )
    cmd |= 0x40;       /* y = 0 or 1 */
  cmd |= 0x0f & (char)x;   /* x = 0 - 15 */

  lcdByteWrite(0, cmd);

}

int LcdLib::lcdWrite(byte* c) {
  int size = 0;
  
  while(*(c+size)) {
    lcdByteWrite(1, *(c+size));
    size++;
  }

  return(size);
}

void LcdLib::lcdByteWrite(byte rs, byte c) {
  lcd4BitWrite(rs, (c >> 4));
  lcd4BitWrite(rs, (c & 0x0F));

  return;
}

void LcdLib::lcd4BitWrite(byte rs, byte c) {

  if(rs) {
    PORTD |= RS;
  }
  else {
    PORTD &= ~RS;
  }

  PORTD |= E;

  PORTD4BIT(c);
  delay(2);

  PORTD &= ~E;

  return;
}


LCD制御、LcdLib.h
class LcdLib {
  private:
    bool lineMode;
    void lcdByteWrite(byte rs, byte c);
    void lcd4BitWrite(byte rs, byte c);
    
  public:
    LcdLib(int line);
    ~LcdLib();

    void lcdClear();
    void lcdCursor(int x, int y);
    int lcdWrite(byte* c);
};

なお、このコードは秋月電子のH8/3052F LAN開発ボードで動作させました。



GPIOへのアクセスは最終的にはポートレジスタを操作となります。

Arduino純正のLiquidCrystalでは、ボード毎に接続が定義されたヘッダファイルを参照して1ビットづつ操作しています。

ボードごとのピン設定は、
\arduino-1.8.10\hardware\arduino\avr\variants
以下に、Arduinoのボード毎に存在するディレクトリにある、pins_arduino.hで定義されています。

多数のボードに汎用的に作成するわけではないので、今回作成したLcdLib.cppではデータは4ビットまとめて書き込みを行いたいと思います。

が!!
Arduino MicroではデジタルI/Oが順番に使われていない!
と、言う事情がありました。
      D0 - PD2
      D1 - PD3
      D2 - PD1
      D3 - PD0
      D4 - PD4
      D5 - PC6
      D6 - PD7
      D7 - PE6

D5はポートCを、D7はポートEを使っていたりする
しかもD0~D4は同じポートDでも0~4の順番になっていない
現場だと電気屋とのバトルが勃発しそうなポイントです。

参考まで、Arduino UNOの方が大分マシな感じです。
// ATMEL ATMEGA8 & 168 / ARDUINO
//
//                 +-\/-+
//           PC6 1|   |28 PC5 (AI 5)
//     (D 0) PD0 2|   |27 PC4 (AI 4)
//     (D 1) PD1 3|   |26 PC3 (AI 3)
//     (D 2) PD2 4|   |25 PC2 (AI 2)
// PWM+ (D 3) PD3 5|   |24 PC1 (AI 1)
//     (D 4) PD4 6|   |23 PC0 (AI 0)
//           VCC 7|   |22 GND
//           GND 8|   |21 AREF
//           PB6 9|   |20 AVCC
//           PB7 10|   |19 PB5 (D 13)
// PWM+ (D 5) PD5 11|   |18 PB4 (D 12)
// PWM+ (D 6) PD6 12|   |17 PB3 (D 11) PWM
//     (D 7) PD7 13|   |16 PB2 (D 10) PWM
//     (D 8) PB0 14|   |15 PB1 (D 9) PWM
//                 +----+
//

そこで、冒頭の回路図の所で示したような結線となりました。

 LCDモジュールとArduino Microのデジタル出力ピンの接続
  LCD 4:RS ← Micro D6 ← Avr PD7
  LCD 6:E ← Micro D4 ← Avr PD4
  LCD 11:D4 ← Micro D3 ← Avr PD0
  LCD 12:D5 ← Micro D2 ← Avr PD1
  LCD 13:D6 ← Micro D0 ← Avr PD2
  LCD 14:D7 ← Micro D1 ← Avr PD3

 参考までにH8ボードの接続は、すべてCN1で下記のようになります。
  CN1 8:SW0 ← Micro D7 ← Avr PE6
  CN1 11:SW3 ← Micro D5 ← Avr PC6
  CN1 16:PB0 ← Micro D3 ← Avr PD0
  CN1 17:PB1 ← Micro D2 ← Avr PD1
  CN1 18:PB2 ← Micro D0 ← Avr PD2
  CN1 19:PB3 ← Micro D1 ← Avr PD3
  CN1 20:RS ← Micro D6 ← Avr PD7
  CN1 23:E ← Micro D4 ← Avr PD4
  CN1 36:+5V ← +5V
  CN1 37:GND ← GND

ボード上のSW0にD7を接続してStart/Stopが行えます。また、SW4にD5を接続してStop状態のときにリセットできるようにしています。

インターフェイスもいろいろあるので、このボードは便利かもしれません。

このH8LAN開発のベースボードにはLAN制御のカニチップやSRAMなんかも搭載されているので、ナニかの役に立つかも?





Have a Happy Hucking!!
Lightning Brains

コメント

このブログの人気の投稿

Linuxシステムコール、共有メモリの使い方

Linuxシステムコール、メッセージキューの使い方

Linuxシステムコール、セマフォの使い方