CIVIC(シビック)FL1のOBD2実践編1:モニター&データロガー作成(液晶制御)

目次

このページの目的

ここまでの作業でArduinoを使ってOBD2でのデータ取得&ロギングが確認できた。いよいよ常設のデータモニター&データロガーを作成していくことにしよう。最初に断っておくが、はっきり言ってこのページは車のカスタムネタというより電子工作ネタの色合いがはるかに濃くなっている。

要求仕様[2023-01-02]

まずは、どういった機能を実現させたいのかを明確にしておこう。

モニターの設置場所と機種選定[2023-01-02]

データモニターの用途からすると、目線の移動が少なく、かつ、運転の妨げにならない(見るつもりのないときには邪魔にならない)ことが重要かと。となると、ステアリングコラムの上面あたりがベストポジションになりそうだ。その場合、車そのもののメーターの視認性を阻害しないことが重要になってくる。幸いなことに、いつもの運転姿勢ではステアリングはかなり低い位置に下げているので、高さ30mm程度に収まるなら、メーターの視認性を下げることなく設置可能と思われる。

よって、高さ20mm程度のグラフィック液晶(または有機EL)モジュールを選定しなければならない。MOUSERとかDigikeyあたりでサイズと仕様を物色しよう。で、選定したのが、サイズ感とか電源・通信仕様で手頃だった以下の液晶モジュールだ。

DISPLAY VISIONS(旧? Electronic Assembly) DIP180B-5NLW
url https://www.displayvisions.us/products/dip.html
DISPLAY VISIONS(旧? Electronic Assembly) DIP180B-5NLW

カラー液晶でなくモノクロで、解像度がそれなりなのが残念だが、まぁ今回の用途なら問題ないかと。

購入品:
液晶モジュール:DISPLAY VISIONS(旧? Electronic Assembly) DIP180B-5NLW
解像度:180x32ドット(表示エリア:82.8mm×16.3mm)。
電源:5V、-3.3V。
通信方法:パラレル

▲目次に戻る

液晶モジュールDIP180B-5NLW制御用基板製作[2023-01-02]

シリパラ変換

今回導入した液晶モジュールはパラレル接続で、信号線だけでも13本の接続が必要だ。これだとマイコン(Auduinoの想定)のIOポート点数が厳しくなるはずなのでシリアル接続に変更することにしよう。

ネットを徘徊して調べたところ、シフトレジスタとかいうICを使えばいい、らしいので、それを使った信号変換モジュール基板を作成することにする。その基板にはついでに液晶駆動用電源回路も実装することにしよう。

使用ソフト
AUTODESK EAGLE(無償版)※基板設計ソフト
購入品:
TEXUS INSTRUMENTS 8ビットシフトレジスタ SN74HC595:2個。シリアル→パラレル変換用
HTC スイッチドキャパシタ電圧コンバータ TJ7660:-3.3V作成用
日清紡マイクロデバイス PWM調光機能付きLEDドライバ NJW4616U2:液晶バックライト照度調整
その他:各種抵抗、コンデンサー

基板設計

各部品の使用方法はGoogle先生に教えてもらい、EAGLEを使用して回路設計と基板設計をおこなっていこう。

DIP180B-5NLW制御用シリパラ変換回路
DIP180B-5NLW制御用シリパラ変換回路
DIP180B-5NLW制御用シリパラ変換基板
DIP180B-5NLW制御用シリパラ変換基板
DIP180B-5NLW制御基板IOリスト
記号場所内容
01 GND右端液晶:GND
02 5V右端液晶:5V
03 -3.3V右端液晶:-3.3V
04 A0右端液晶:制御
05 RW右端液晶:制御
06 E1右端液晶:制御
07 D0右端液晶:データ
08 D1右端液晶:データ
09 D2左端液晶:データ
10 D3左端液晶:データ
11 D4左端液晶:データ
12 D5左端液晶:データ
13 D6左端液晶:データ
14 D7左端液晶:データ
15 E2左端液晶:制御
16 RES左端液晶:ハードリセット
17 E3左端液晶:制御
18 LED左端液晶:バックライト
GND1右下マイコンへ接続
5V右下マイコンへ接続
3.3V右上マイコンへ接続
PWM左上バックライト調光用
マイコンへ接続
GND2左上マイコンへ接続
CLK左下マイコンへ接続
(シリアル通信)
RCK左下マイコンへ接続
(シリアル通信)
SER左下マイコンへ接続
(シリアル通信)

設計が完了したら基板を発注だ。業務では使いたくない中国メーカーだが、個人の趣味ならその安さに勝るものは無い。1枚しか使用しないが、最低発注単位5枚での発注だとしてもその安さは恐るべき。送料より製作費のほうが安いという...

DIP180B-5NLW御用基板表面
DIP180B-5NLW制御用基板表面
DIP180B-5NLW制御用基板裏面
DIP180B-5NLW制御用基板裏面
DIP180B-5NLW制御用基板実装
DIP180B-5NLW制御用基板実装

ちなみに、上に掲載した回路と基板図は、完成した実物の反省に基づき部品配置を若干変更してある。いやぁ、ちょっと部品実装するには苦しい配置だったのよw

さらに、、、LEDドライバが欠品で手に入らなかったので、はんだブリッジで回路を形成している...こんなこともあろうかと(?)、念のために半固定ボリュームをつけておいてよかった。

▲目次に戻る

液晶動作確認まで

モニター表示イメージ

Excelで表示イメージを作成してみた。解像度的にはこんなものか。

OBD2モニター表示イメージ
OBD2モニター表示イメージ
OBD2モニター表示内容
場所表示内容
上段ブースト圧(インマニ圧)
絶対圧:0~2.56BAR
[ゲージ圧:-1~1.56BAR]
途中の2本線は、1BAR、2BARの位置に区切りとして配置し視認性向上(?)
中段スロットル開度:0~100%
下段Fuel-Air比:0~2。
空燃比を逆数にして、燃料が多い場合はバーが長くなるように表示させる(上段、中段とも燃料が多い場合にバーが長くなるのでそれと合わせる)。途中の区切り線は数値1(理論空燃比)。

Arduino用コード

上で製作した専用基板に対する通信方法は以下となる。各信号の意味するところは「参考url」のデータシートを確認してもらいたい。

DIP180B-5NLW制御基板へのコマンド
74HC595ピン番号74HC595_No.274HC595_No.1備考
QHQGQFQEQDQCQBQAQHQGQFQEQDQCQBQA
コマンドRESE3E2E1RWA0未使用未使用D7D6D5D4D3D2D1D0
Hardware Reset0xxxxxxxxxxxxxxxx:不問
Software Reset 1yyy00xx11100010y:対象chを1
displayOn1yyy00xx10101111
Display start line1yyy00xx11000000一番上の行から開始
select ADC1yyy00xx10100000CW
Static drive1yyy00xx10100100OFF
Select Duty1yyy00xx101010011/32
Set Column address1yyy00xx0zzzzzzzz:カラム位置
Set page address1yyy00xx101110zzz:page位置
Write display data1yyy01xxzzzzzzzzz:表示データ

これをもとに次はソフトの作成だ。ただ、この液晶モジュールはあまり情報がなくて挫けそうにもなったが、動いてみれば何のことは無い、ごく普通に「仕様上は確かにそうだよねぇ」ってコーディングで問題なかったという。そのポイントは以下の2点だ。

参考url:
DIP180B-5NLW液晶モジュールデータシート
http://www.lcd-module.de/fileadmin/eng/pdf/grafik/dip180-5e.pdf
液晶ドライバICデータシート
http://www.lcd-module.de/fileadmin/eng/pdf/zubehoer/ax6120.pdf
Arduinoフォーラム
https://forum.arduino.cc/t/need-help-to-run-glcd-with-3-sed1520-on-arduino-mega-2560/401566

コーディングの方針としては、演算結果をした結果としてのデータ表示でなく、可能な限り初期設定データを表示させるだけ、にして動作を軽くできないか? という想いを込めたつもり。

そのためプログラムサイズが大きくなり、ArduinoUNOではメモリが足らず、動作させるためにはArduinoMEGAが必要だ。さらに、MEGAでも実行速度が遅いので、本番環境ではESP32を使用することにする。

購入品:
ESP32-DevKitC ESP-WROOM-32開発ボード
// IO pin
const byte LCDclk = 25;
const byte LCDrck = 33;
const byte LCDser = 32;

byte       LCDorg = 19; //グラフ原点位置
byte LCDe1t[2][180];    //液晶のpage1は 上段中段両方を表示させる必要あり。(0:bar0=下位ビット,1:bar1=上位4ビット)
int  LCDpos[3] = {LCDorg,LCDorg,LCDorg}; //前回の描画でのグラフの長さを保持

//LCD
const byte LCDelm0[2][14] = { //page0 の要素 bar0用
  //blk ,B   ,B   ,B   ,A   ,A   ,R   ,R   ,R   ,R   ,R   ,sprt,bar ,sction
   {0x00,0xFC,0x24,0xD8,0xF8,0x24,0xFC,0x24,0x64,0xA4,0x18,0xFF,0x00,0xFF} //frameのみ
  ,{0x00,0xFC,0x24,0xD8,0xF8,0x24,0xFC,0x24,0x64,0xA4,0x18,0x00,0xFF,0xFF} //dataあり
};
const byte LCDpage0[180] = { //page0 の各要素配列パターン
    0, 1, 2, 2, 2, 3, 0, 4, 5, 5  , 5, 4, 0, 6, 7, 8, 9,10, 0,13  ,12,12,12,12,12,12,12,12,12,12
  ,11,12,12,12,12,12,12,12,12,12  ,12,12,11,12,12,12,12,12,12,12  ,12,12,12,12,11,12,12,12,12,12
  ,12,12,12,12,12,12,11,12,12,12  ,12,12,12,12,12,12,12,11,12,11  ,12,12,12,12,12,12,12,12,12,12
  ,11,12,12,12,12,12,12,12,12,12  ,12,12,11,12,12,12,12,12,12,12  ,12,12,12,12,11,12,12,12,12,12
  ,12,12,12,12,12,12,11,12,12,12  ,12,12,12,12,12,12,12,11,12,11  ,12,12,12,12,12,12,12,12,12,12
  ,11,12,12,12,12,12,12,12,12,12  ,12,12,11,12,12,12,12,12,12,12  ,12,12,12,12,12,12,12,12,11, 0
};
const byte LCDelm1[2][2][11] = { //page1 の要素 bar0,bar1用
  { //lower bar0
    //blk  B    %    %A   %A   %    sct   bar  spr0 spr1 sprt01
     {0x0 ,0x1 ,0x0 ,0x1 ,0x1 ,0x0 ,0xB ,0x8 , 0xB ,0x8 ,0xB}  // frame
    ,{0x0 ,0x1 ,0x0 ,0x1 ,0x1 ,0x0 ,0xB ,0xB , 0x8 ,0xB ,0x8}  // data
  }
  ,{ //upper bar1
    //blk  B    %    %A   %A    %   sct  bar  spr0 spr1 sprt01
     {0x00,0x00,0xC0,0xC0,0x80,0x40,0xE0,0x00,0x00,0xE0,0xE0} //frame
    ,{0x00,0x00,0xC0,0xC0,0x80,0x40,0xE0,0xE0,0xE0,0x00,0x00} //data
   }
};
const byte LCDpage1[180] = { //page1 pattern for bar0,bar1
   0,1,1,1,1,0, 2,3,0,0  ,0,4, 5,1,0,0,0,1,0,6  ,7,7,7,7, 7,7,7,7,7,7
  ,8,7,7,7,9,7, 7,7,7,7  ,7,7, 8,7,7,7,7,7,7,7  ,9,7,7,7, 8,7,7,7,7,7
  ,7,7,7,7,7,7,10,7,7,7  ,7,7, 7,7,7,7,7,8,7,8  ,7,7,9,7, 7,7,7,7,7,7
  ,8,7,7,7,7,7, 7,7,9,7  ,7,7, 8,7,7,7,7,7,7,7  ,7,7,7,7,10,7,7,7,7,7
  ,7,7,7,7,7,7, 8,7,7,7  ,9,7, 7,7,7,7,7,8,7,8  ,7,7,7,7, 7,7,9,7,7,7
  ,8,7,7,7,7,7, 7,7,7,7  ,7,7,10,7,7,7,7,7,7,7  ,7,7,7,7, 7,7,7,7,6,0
};
const byte LCDelm2[2][10] { //page2 element for bar1
  //blk  %    %   %     %    %    %    sct  bar  sprt
   {0x00,0x10,0x08,0x04,0x02,0x01,0x18,0x5F,0x40,0x5F} //frame
  ,{0x00,0x10,0x08,0x04,0x02,0x01,0x18,0x5F,0x5F,0x40} //bar
};
const byte LCDpage2[180] = { //page2 pattern for bar1
   0,0,0,0,0,0,1,2,3,4  ,5,6,6,0,0,0,0,0,0,7  ,8,8,8,8,8,8,8,8,8,8
  ,8,8,8,8,9,8,8,8,8,8  ,8,8,8,8,8,8,8,8,8,8  ,9,8,8,8,8,8,8,8,8,8
  ,8,8,8,8,8,8,9,8,8,8  ,8,8,8,8,8,8,8,8,8,8  ,8,8,9,8,8,8,8,8,8,8
  ,8,8,8,8,8,8,8,8,9,8  ,8,8,8,8,8,8,8,8,8,8  ,8,8,8,8,9,8,8,8,8,8
  ,8,8,8,8,8,8,8,8,8,8  ,9,8,8,8,8,8,8,8,8,8  ,8,8,8,8,8,8,9,8,8,8
  ,8,8,8,8,8,8,8,8,8,8  ,8,8,9,8,8,8,8,8,8,8  ,8,8,8,8,8,8,8,8,7,0
};
const byte LCDelm3[2][14] = { //page3 element for bar2
  //blk  F    F    F    %    %    %    %    %    A    A    sct  sprt bar
   {0x00,0xFE,0x12,0x02,0x40,0x20,0x10,0x08,0x04,0xFC,0x12,0xFF,0xFF,0x00}
  ,{0x00,0xFE,0x12,0x02,0x40,0x20,0x10,0x08,0x04,0xFC,0x12,0xFF,0x00,0xFF}
};
const byte LCDpage3[180] = { //page3 pattern for bar2
    0, 1, 2, 2, 2, 3, 0, 4, 5, 6  , 7, 8, 0, 9,10,10,10, 9, 0,11  ,13,13,13,13,13,13,13,13,13,13
  ,13,13,13,13,12,13,13,13,13,13  ,13,13,13,13,13,13,13,13,13,13  ,12,13,13,13,13,13,13,13,13,13
  ,13,13,13,13,13,13,12,13,13,13  ,13,13,13,13,13,13,13,13,13,13  ,13,13,12,13,13,13,13,13,13,13
  ,13,13,13,13,13,13,13,12,13,12  ,13,13,13,13,13,13,13,13,13,13  ,13,13,13,13,12,13,13,13,13,13
  ,13,13,13,13,13,13,13,13,13,13  ,12,13,13,13,13,13,13,13,13,13  ,13,13,13,13,13,13,12,13,13,13
  ,13,13,13,13,13,13,13,13,13,13  ,13,13,12,13,13,13,13,13,13,13  ,13,13,13,13,13,13,13,13,11, 0
};

int   CANdisp;     //CAN bar length for each data
byte  CANbar;      //CAN data graph bar0-2

void setup() {
  Serial.begin(115200);

  //IO
  pinMode(LCDser , OUTPUT);
  pinMode(LCDrck , OUTPUT);
  pinMode(LCDclk , OUTPUT);

  //LCD 初期化
  LCDwrite(0xF0 , 0x00); //RES to H
  delay(100);
  LCDwrite(0x40 , 0x00); //RES を L に変更させたエッジを検出してハードウェアリセット
  delay(100);
  LCDwrite(0xF0 , 0xE2); //念のためソフトウェアリセットもかける
  delay(100);
  LCDwrite(0xF0 , 0xAE); //Display off
  LCDwrite(0xF0 , 0xC0); //Dispay startline=0
  LCDwrite(0xF0 , 0xA0); //ADC=CW
  LCDwrite(0xF0 , 0xA4); //static off
  LCDwrite(0xF0 , 0xA9); //Duty 1/32
  LCDwrite(0xF0 , 0xAF); //Display on

  //LCD 起動時に画面クリアを兼ねて全グラフを描画(オープニング効果も狙っている)
  for (int i=0; i<180; i++) {
    //Page0 data
    LCDdisp(i , 0 , LCDelm0[1][LCDpage0[i]]);
    //Page1 data
    LCDdisp(i , 1 , LCDelm1[0][1][LCDpage1[i]] + LCDelm1[1][1][LCDpage1[i]]);
    //Page2 data
    LCDdisp(i , 2 , LCDelm2[1][LCDpage2[i]]);
    //Page2 data
    LCDdisp(i , 3 , LCDelm3[1][LCDpage3[i]]);
  }
  //LCD 全グラフをクリアして枠だけの表示に(オープニング効果としてカウントダウン表示)
  for (int i=179; i>19; i--) {
    //Page0 data
    LCDdisp(i , 0 , LCDelm0[0][LCDpage0[i]]);
    //Page1 data
    LCDe1t[0][i] = LCDelm1[0][0][LCDpage1[i]]; //page1の下位4ビットはbar0グラフなので、その内容を記憶しておく
    LCDe1t[1][i] = LCDelm1[1][0][LCDpage1[i]]; //page1の上位4ビットはbar1グラフなので、同上
    LCDdisp(i , 1 , LCDe1t[0][i] + LCDe1t[1][i]);
    //Page2 data
    LCDdisp(i , 2 , LCDelm2[0][LCDpage2[i]]);
    //Page3 data
    LCDdisp(i , 3 , LCDelm3[0][LCDpage3[i]]);
    delay(20);
  }
}

void loop() {
  for (int i=0; i<4; i++) {   //CAN通信コマンド(4種類のデータ取得用)の繰り返し。この動作検証では無意味
    CANdisp = random(20,180); //動作検証用のランダムなダミーデータ
    CANbar=random(0,4);       //動作検証用のランダムなダミーデータ
    //output to LCD
    if (CANdisp < LCDorg) {CANdisp = LCDorg;}
    if (CANdisp > 179)    {CANdisp = 179;}
    if (CANbar<3) {
      byte TEMP1 = LCDpos[CANbar];
      byte TEMP2 = CANdisp;
      int  TEMP3 = 1;
      byte TEMP4 = 1;
      if (LCDpos[CANbar]>CANdisp) {
        TEMP3 = -1;
        TEMP4 = 0;
      }
      Serial.print(TEMP1);Serial.print(",");Serial.print(TEMP2);Serial.print(",");
      Serial.print(TEMP3);Serial.print(",");Serial.println(TEMP4);
      switch(CANbar) {
        case 0: //bar0グラフ用
          for (byte j=TEMP1; j*TEMP3<=TEMP2*TEMP3; j=j+TEMP3) { //前回長さより短い場合は徐々に短くしていく表示
            LCDdisp(j , 0 , LCDelm0[TEMP4][LCDpage0[j]]);       //display page0
            LCDe1t[0][j] = LCDelm1[0][TEMP4][LCDpage1[j]];      //store page1 lower element
            LCDdisp(j , 1 , LCDe1t[0][j] + LCDe1t[1][j]);       //display page1
          }
          break;
        case 1: //bar1グラフ用
          for (byte j=TEMP1; j*TEMP3<=TEMP2*TEMP3; j=j+TEMP3) { //グラフにより微妙に処理が違うのであえて個別に記述
            LCDe1t[1][j] = LCDelm1[1][TEMP4][LCDpage1[j]];      //store page1 higher element
            LCDdisp(j , 1 , LCDe1t[0][j] + LCDe1t[1][j]);       //display page1
            LCDdisp(j , 2 , LCDelm2[TEMP4][LCDpage2[j]]);       //display page2
          }
          break;
        case 2: //bar2グラフ用
          for (byte j=TEMP1; j*TEMP3<=TEMP2*TEMP3; j=j+TEMP3) {
            LCDdisp(j , 3 , LCDelm3[TEMP4][LCDpage3[j]]);       //display page3
          }
          break;
        default:
          break;
      }
      LCDpos[CANbar] = CANdisp; // 今回のグラフの長さを記憶
    }
  }
  delay(100);
}

//Function: LCDへのコマンド送信関数   CMD1=parameter CMD2=controll
void LCDwrite(byte LCDcmd2 , byte LCDcmd1){
  byte LCDcmd2b = LCDcmd2 & B10001100; //通信手順として各チップ(E1,E2,E3)の選択をlowにする。RW と A0 は実データとする
  //コマンド開始  Ex=low
  digitalWrite(LCDrck , LOW);
  shiftOut(LCDser , LCDclk , MSBFIRST , LCDcmd2b); //上位から送信。74HC595_No.2のQHからQAへ
  shiftOut(LCDser , LCDclk , MSBFIRST , LCDcmd1);  //74HC575_No.1のQHからQAへ
  digitalWrite(LCDrck , HIGH);
  delayMicroseconds(25); //仕様上、次のコマンドまで最低20nSの間隔が必要
  //送信したいチップをhighに。(実コマンド送信)
  digitalWrite(LCDrck , LOW);
  shiftOut(LCDser , LCDclk , MSBFIRST , LCDcmd2);
  shiftOut(LCDser , LCDclk , MSBFIRST , LCDcmd1);
  digitalWrite(LCDrck , HIGH);
  delayMicroseconds(120); //仕様上、次のコマンドまで最低100nSの間隔が必要
  //チップの選択をlowにしてコマンド終了 
  digitalWrite(LCDrck , LOW);
  shiftOut(LCDser , LCDclk , MSBFIRST , LCDcmd2b);
  shiftOut(LCDser , LCDclk , MSBFIRST , LCDcmd1);
  digitalWrite(LCDrck , HIGH);
  delayMicroseconds(50); //念のための間隔保持
}

//Function: グラフ描画関数定義
void LCDdisp(byte LCDcmd5 , byte LCDcmd4 , byte LCDcmd3){
  //LCDcmd5  Column position(0-179)
  //LCDcmd4  Page(0-3)
  //LCDcmd3  Data
  byte TEMP2 = 0x80 + 0x10 * pow(2 , floor(LCDcmd5/60)); //write parameter to Chip1-3
  byte TEMP1 = LCDcmd5 % 60;            //column position in Chip1-3
  LCDwrite(TEMP2     , TEMP1);          //set Column
  LCDwrite(TEMP2     , 0xB8 + LCDcmd4); //set page
  LCDwrite(TEMP2 + 4 , LCDcmd3);        //write data
}

動作確認

上記の動作検証用コードの実行状況だ。いい感じで動作しているが、ランダム表示に入ってからのバーの動きが現時点での最速。コードの作りこみでもう少しは速くなるのか??

動作検証コードでのグラフ出力状況

▲目次に戻る

続く...

CIVIC(シビック)FL1のOBD2実践編2:モニター&データロガー作成(IO制御)