公開日:2018/12/15
title.png

3rd Season


#3-3 I2C接続 LCD表示モジュール

 前回(「#3-2 Arduino間I2C通信を試す」)、前々回(「#3-1 LCD1602に表示する」)の試行結果を統合して、Arduinoを使ったLCD表示モジュールを完成させます。

目標仕様

 単なる「表示」を実行するシステムではなく、デバッグに使う「シリアルモニタ」の代替として使用できるような、バックログ機能とバックログの閲覧機能を備えたサブシステムを目指します。

 16文字×2行表示だと、デバッグ用に出力した情報は、すぐにスクロールして消えてしまうので、さすがにデバッグ用の表示器として使うのは無理があります。しかし、「第9回 気温・湿度のログを取る(後編、最終回)」でご紹介した様に、PCとの連携によって動作するシステムを考え、そのデバッグを想定した場合、デバッグ情報をPCとの通信に使用するシリアル経由で出力することもまた、無理があります(PC側との、本来の通信をディスターブしてしまう)。

 そこで、今回の「LCD表示サブシステム」が実力を発揮します。デバッグ対象のArduinoからI2C経由で送信されてきたデバッグ情報を、バックログに蓄え、U/Iを使っていつでも戻りながら閲覧することが可能となるのです。

ar33_lcdsub.png

 デバッグ対象のArduinoからは、I2C経由の文字列送信関数を立てて、以下の様な送信方法を想定しています。

  int i2c_print (
  int slv_adr,
  const char *str)
  {
    // send string str to i2c slave address slv_adr
    // return: ErrNo / Wire.endTransmission()
    const char *str_c;
    int rtncod;
    int idx;
    int len;

    rtncod=ErrNo;
    str_c=str;
    for (len=strlen (str); len>=0 && (*str_c)!='\0'; len -= BUFFER_LENGTH)
    {
      Wire.beginTransmission (slv_adr);
      for (idx=0; idx<BUFFER_LENGTH && (*str_c)!='\0'; idx++, str_c++)
        Wire.write (*str_c);
      rtncod=Wire.endTransmission ();
    }

    return (rtncod);
  }

  void loop (void)
  {
    char dbg_buf[64];
    :
    if (errcod)
    {
      sprintf (dbg_buf, "DBG: errcod=%d\r\n",errcod);
      i2c_print (i2c_slv_adr, dbg_buf);
    }
    :
  }

回路の設計

 筆者が所有する、UNOとMicroの両方で実現可能なように、まずは、Arduino UNO (ATmega328)、Micro (ATmega32U4)間のピンアサインをまとめてみます。

Arduinoピン# UNO (ATmega328) Micro (ATmega32U4)
D0 PD0(PCINT16/RXD0) PD2(RXD/INT2)
D1 PD1(PCINT17/TXD0) PD3(TXD/INT3)
D2 PD2(PCINT18/INT0) PD1(SDA/INT1)
D3 PD3(OC2B/INT1/PCINT19) PD0(OC0B/SCL/INT0)
D4 PD4(XCK0/T0/PCINT20) PD4(ICP1/ADC8)
D5 PD5(OC0B/T1/PCIN21) PC6(OC3A/OC4A)
D6 PD6(OC0A/PCINT22/AIN0) PD7(T0/OC4D/ADC10)
D7 PD7(PCINT23/AIN1) PE6(INT6/AIN0)
D8 PB0(ICP1/CLKO/PCINT0) PB4(PCINT4/ADC11)
D9 PB1(OC1A/PCINT1) PB5(PCINT5/OC1A/OC4B/ADC12)
D10 PB2(SS0/OC1B/PCINT2) PB6(PCINT6/OC1B/OC4B/ADC13)
D11 PB3(MOSI0/OC2A/PCINT3) PB7(OC0A/OC1C/RTS/PCINT7)
D12 PB4(MISO0/PCINT4) PD6(T1/OC4D/ADC9)
D13 PB5(PCINT5/SCK0/LED) PC7(ICP3/CLKO/OC4A)
D14 PC0(ADC0/PCINT8) PB3(PDO/MISO/PCINT3)
D15 PC1(ADC1/PCINT9) PB1(SCK/PCINT1)
D16 PC2(ADC2/PCINT10) PB2(PDI/MOSI/PCINT2)
D17 PC3(ADC3/PCINT11) PB0(RXLED/SS/PCINT0)
D18 PC4(ADC4/PCINT12/SDA0) PF7(ADC7/TDI)
D19 PC5(ADC5/PCINT13/SCL0) PF6(ADC6/TDO)
D20 PE2 (ADC6) PF5(ADC5/TMS)
D21 PE3 (ADC7) PF4(ADC4/TCK)
D22 PF1(ADC1)
D23 PF0(ADC0)
D24 PD4(D4)
D25 PD7(D6)
D26 PB4(D8)
D27 PB5(D9)
D28 PB6(D10)
D29 PD6(D12)
D30 PD5(TXLED/XCK/CTS)
A0 PC0(ADC0/PCINT8) PF7(ADC7/TDI)
A1 PC1(ADC1/PCINT9) PF6(ADC6/TDO)
A2 PC2(ADC2/PCINT10) PF5(ADC5/TMS)
A3 PC3(ADC3/PCINT11) PF4(ADC4/TCK)
A4 PC4(ADC4/PCINT12/SDA0) PF1(ADC1)
A5 PC5(ADC5/PCINT13/SCL0) PF0(ADC0)
A6 PE2 (ADC6) PD4(ICP1/ADC8)
A7 PE3 (ADC7) PD7(T0/OC4D/ADC10)
A8 PB4(PCINT4/ADC11)
A9 PB5(PCINT5/OC1A/OC4B/ADC12)
A10 PB6(PCINT6/OC1B/OC4B/ADC13)
A11 PD6(T1/OC4D/ADC9)
アナログ端子と兼用のデジタル端子
デジタル端子と兼用のアナログ端子
ATmega328とATmega32U4で共通かつ連続して使用可能なPORT-B
I2Cで使用する端子
筆者所有のMicro互換機では未接続、他の端子のクローン等

 LCDのバス制御のためにPORT-Bをアセンブラで直接叩くのですが、上表の様に、UNOとMicroでは端子番号が異なりますので、#ifdefで場合分けしないとなりません。所詮は、Micro(互換機)で引き出されていない端子や、肝心なI2C通信で使用する端子の配置などが異なっているため、#ifdefで切り分けないと両方の機体で動くプログラムにはなりません。
 改めて思いますが、どれか1つの種類に統一して購入したほうが楽でしたね……。でも、色々な機体を使ってみたい気持ちもあるし、仕方ないかな〜。

 UNOとMicroで必要な機能を考えながら、試行錯誤して、ようやく固まった回路図がこちら(↓)です。

ar33_schem.png

 一応、UNOとMicroどちらの機体でも動作可能なピンアサインになっているはず。ただし、筆者環境では、Microでしか動作確認していませんので、UNOで実現しようとしている方はご注意を。動くかどうかは保証できません(1^^;)。

寄り道(に、なってしまった……)

 上の表でもお分かりかと思いますが、今回はアナログ端子を使いませんし、アナログ端子であっても、デジタル端子として使用可能なので、A0〜A3に割り当たっているポート4本を使うと、LCDと8ビットバス接続できるのではないか?と考えてしまいました。単純に、「4ビットバスより8ビットバスの方が速いはずだ」などと考えたのが運の尽き(?)。
 意気揚々と8ビットバスのプログラムも組んで、速度を計測してみると、見事に、「速くなって無〜い!」、どゆこと(1@o@)?

 敗因は、8ビットまとめて操作可能なポートが無かったことでした。上下4ビット毎に別々なポートレジスタに操作(アクセス)が必要になってしまい、結果として4ビットバスと同じだけの命令数を実行していました。
 実は、リードの方は圧倒的に速くなっていたのですが、LCDアクセスでは、BFのポーリングにしかリードを使わないので、全く動作速度に貢献できなかった模様。

 と、言うことで、8ビットバス接続に関しては、「効果なし、不採用」としました。まだ、使用可能な空きポートが残っている、と言うことで、別なU/I(スイッチ等)を追加しても良いし、I2Cのスレーブアドレス設定用に使っても良いですね。

プログラムの設計

 基本的な技術としては、前回「#3-2 Arduino間I2C通信を試す」)、前々回(「#3-1 LCD1602に表示する」で示したプログラムのブラッシュアップと、ロータリエンコーダに関しては「#2-10 ミニキーボード付きゲームパッド」からのフィードバックで、新しい話はありません。
 問題はU/Iの制御部分ですね。スイッチの検出部分を抜粋しておきます。

  void int_rotenc_sw (void)
  {
    // interrupt service routine for SW both edge

    if (digitalRead (pin_SW)==LOW)
    {
      msec_swf=millis ();
      rotenc_swf=true;
    }
    else
    {
      msec_swr=millis ();
      rotenc_swr=true;
    }
  }

  void setup (void)
  {
    :
    // interrupt
    attachInterrupt (digitalPinToInterrupt (pin_SW), int_rotenc_sw, CHANGE);
    :
  }

  void loop (void)
  {
    :
    // detect switch
    ulong msec;
    msec=millis ();
    if (rotenc_swr)
    {
      // rising edge (switch released)
      rotenc_swr=false;
      if (!rotenc_swf)
      {
        // rising edge without falling --> chattering, ignore
      }
      else
      {
        rotenc_swf=false;
        if (msec - msec_swf>10)
        {
          // switch pushed >10[ms]
          ui_sw_short=true;
        }
      }
    }
    else if (rotenc_swf)
    {
      // falling edge (switch pushed)
      if (msec - msec_swf>1000)
      {
        // switch pushed >1000[ms]
        rotenc_swf=false;
        ui_sw_long=true;
      }
    }
    :
  }


 スイッチ端子の変化イベント(立ち上がり、立ち下がり両エッジ)で割り込みをかけ、ISR(Interrupt Service Routine、int_rotenc_sw())で立ち上がり(rotenc_swr)か立ち下がり(rotenc_swf)のフラグ立てと、割り込みが発生した時間を記録します。
 loop()処理中に各イベントの発生状態と時間を見て、通常のクリック(ui_sw_short)と、長押し(ui_sw_long)を判定しています。後は、検出したスイッチの状態に従って、以下図のようなステート(モード)を遷移させています。

ar32_uistat.png

 残りの部分については、完成版プログラム(lcd_subsys.ino)をご覧くださいm(1__)m。

 プログラムの解析に必要だと思いますので、U/Iの操作方法についてまとめておきます。

 プログラム中、UNO向けの#ifdefで場合分けが入っていますが、筆者の環境では、Microでしか動作確認していません。前節でも記載しましたが、UNOでは動作未確認です。
 完成版プログラムをリンクしておきます。

  完成版プログラム:lcd_subsys.ino

完成品の評価

 Arduino UNOでデバッグ対象のプログラムが動作している、と言う想定で、Arduino Microを使ったLCDサブシステムにI2C接続します。

ar33__lcdsub.jpg

 Arduino UNO側では、前回作成したプログラム(test_i2c.ino)を転送しておき、シリアルモニタから【test】コマンドを送信します。【test】コマンドは、ランダムに0〜2[秒]間隔でデバッグ用のメッセージをI2C経由でLCDサブシステムに送り続けます。もう一回【test】コマンドを送信すると、停止します。
 では、Arduino Microを使って構築したLCDサブシステムの評価を実施してみましょう。



 まだデバッグが十分とは言えませんので、使ってみようという方はご注意ください。
 また、同じ処理速度のArduino同士でI2C通信していますので、あまり派手に、高頻度で大量のデータを送りつけると、LCDサブシステム側の処理が間に合わずに、データの取りこぼし等が発生する可能性が考えられます。受信データがおかしい、と思ったときは、そういう可能性も疑ってみてください。

 ちなみに、UNO側のプログラム(test_i2c.ino)では、前回(「#3-2 Arduino間I2C通信を試す」)説明した、【clk】コマンドなども使用可能ですので、I2Cの転送速度を変更することが可能です。

 もう一つ、肝心な注意点ですが、バックログのメモリ容量には注意です。1行の最大文字数40文字×bklg_maxl(=48)=1,920バイトとなりますが、UNOはそもそも2KバイトしかRAMを持っていませんし、グローバル変数、スタック等で使う分のメモリも必要なため、長い行を受信し続けると、メモリが足りなくなってしまいます。
 MicroのATmega32U4は、RAMを2.5Kバイト持っているので、UNOよりは余裕がありますが、それでも+512バイトですので、油断はできません。
 メモリフルでデータを受け取れなかったときには、"■"が表示され、データは失われてしまいます。
 デバッグ用にSerial(シリアルモニタ)を使用可能とするマクロ(USE_SERIAL)が設定されていますが、実際に使用する場合には、シリアル出力は不要なはずなので、必ず「#define USE_SERIAL」をコメントしてください。Serialだけで700Kバイト以上のデータ領域を必要としているようなので、コメントすることでその分がバックログ用に使用可能となります。

 また、メモリフルでデータを取りこぼすくらいなら、バックログの行数を減らして、確実にデータを受信できた方が良いと考えるかもしれません。その場合は、先頭付近のbklg_maxlを書き換えてください。

  const int bklg_maxl=48;  // max back-log lines

 ロータリエンコーダでスクロールできるお陰で、だいぶ使い勝手が良くなっていると思います。たぶん、4方向スイッチを使って上下・左右スクロールさせるよりは、かなりマシなはずです。早速、デバッグ用に実践投入したいところですが、さすがに、ブレッドボードのままで使い続けるわけにも行かず……。ユニバーサル基板にはんだ付けするのが面倒くさいなぁ。


後日工作した完成品

 後日、ユニバーサル基板にはんだ付けして完成した、LCD表示ボードです。Arduinoを専用にはできなかったので、相変わらず、Arduinoとの接続はジャンパ線です……(1^^;)。これは……、「半」完成品か?

ar33_prod.jpg

 Arduinoを専用化できて基板に実装できれば、インターフェイス信号線はI2Cの2本だけとなるのですが……。ともあれ、ブレッドボードが一つ空いたし、LCD、ポテンシオメータ、ロータリエンコーダが1つのユニットに収まったおかげで、取り回しも楽になりました。

 今回のお買い物は、以下の通りです。

 ユニバーサル基板1枚と、ピンソケット1個を使いました。Arduinoとの接続信号にはピンソケットを使っているので、ジャンパ線は、使うときだけ繋げはOKです。


 なかなか緊張したのがはんだ付けですね。はんだ吸い取り線も、吸い取り器も、持ち合わせが無いので、「失敗できない」緊張感が半端ない。
 出来上がってみて思ったのは、「足が必要」です。裏面にはんだ付けした端子が棘の様に突き出ているので、足をつけて浮かさないと、机が傷つきます。


btn_arduino.png

btn_prev.png btn_next.png

home