ここまでの作業でArduinoを使ってOBD2でのデータ取得&ロギングが確認できた。いよいよ常設のデータモニター&データロガーを作成していくことにしよう。最初に断っておくが、はっきり言ってこのページは車のカスタムネタというより電子工作ネタの色合いがはるかに濃くなっている。
まずは、どういった機能を実現させたいのかを明確にしておこう。
データモニターの用途からすると、目線の移動が少なく、かつ、運転の妨げにならない(見るつもりのないときには邪魔にならない)ことが重要かと。となると、ステアリングコラムの上面あたりがベストポジションになりそうだ。その場合、車そのもののメーターの視認性を阻害しないことが重要になってくる。幸いなことに、いつもの運転姿勢ではステアリングはかなり低い位置に下げているので、高さ30mm程度に収まるなら、メーターの視認性を下げることなく設置可能と思われる。
よって、高さ20mm程度のグラフィック液晶(または有機EL)モジュールを選定しなければならない。MOUSERとかDigikeyあたりでサイズと仕様を物色しよう。で、選定したのが、サイズ感とか電源・通信仕様で手頃だった以下の液晶モジュールだ。
カラー液晶でなくモノクロで、解像度がそれなりなのが残念だが、まぁ今回の用途なら問題ないかと。
今回導入した液晶モジュールはパラレル接続で、信号線だけでも13本の接続が必要だ。これだとマイコン(Auduinoの想定)のIOポート点数が厳しくなるはずなのでシリアル接続に変更することにしよう。
ネットを徘徊して調べたところ、シフトレジスタとかいうICを使えばいい、らしいので、それを使った信号変換モジュール基板を作成することにする。その基板にはついでに液晶駆動用電源回路も実装することにしよう。
各部品の使用方法はGoogle先生に教えてもらい、EAGLEを使用して回路設計と基板設計をおこなっていこう。
記号 | 場所 | 内容 |
---|---|---|
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枚での発注だとしてもその安さは恐るべき。送料より製作費のほうが安いという...
ちなみに、上に掲載した回路と基板図は、完成した実物の反省に基づき部品配置を若干変更してある。いやぁ、ちょっと部品実装するには苦しい配置だったのよw
さらに、、、LEDドライバが欠品で手に入らなかったので、はんだブリッジで回路を形成している...こんなこともあろうかと(?)、念のために半固定ボリュームをつけておいてよかった。
Excelで表示イメージを作成してみた。解像度的にはこんなものか。
場所 | 表示内容 |
---|---|
上段 | ブースト圧(インマニ圧) 絶対圧:0~2.56BAR [ゲージ圧:-1~1.56BAR] 途中の2本線は、1BAR、2BARの位置に区切りとして配置し視認性向上(?) |
中段 | スロットル開度:0~100% |
下段 | Fuel-Air比:0~2。 空燃比を逆数にして、燃料が多い場合はバーが長くなるように表示させる(上段、中段とも燃料が多い場合にバーが長くなるのでそれと合わせる)。途中の区切り線は数値1(理論空燃比)。 |
上で製作した専用基板に対する通信方法は以下となる。各信号の意味するところは「参考url」のデータシートを確認してもらいたい。
74HC595ピン番号 | 74HC595_No.2 | 74HC595_No.1 | 備考 | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
QH | QG | QF | QE | QD | QC | QB | QA | QH | QG | QF | QE | QD | QC | QB | QA | ||
コマンド | RES | E3 | E2 | E1 | RW | A0 | 未使用 | 未使用 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | |
Hardware Reset | 0 | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x:不問 |
Software Reset | 1 | y | y | y | 0 | 0 | x | x | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | y:対象chを1 |
displayOn | 1 | y | y | y | 0 | 0 | x | x | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | |
Display start line | 1 | y | y | y | 0 | 0 | x | x | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 一番上の行から開始 |
select ADC | 1 | y | y | y | 0 | 0 | x | x | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | CW |
Static drive | 1 | y | y | y | 0 | 0 | x | x | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | OFF |
Select Duty | 1 | y | y | y | 0 | 0 | x | x | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 1/32 |
Set Column address | 1 | y | y | y | 0 | 0 | x | x | 0 | z | z | z | z | z | z | z | z:カラム位置 |
Set page address | 1 | y | y | y | 0 | 0 | x | x | 1 | 0 | 1 | 1 | 1 | 0 | z | z | z:page位置 |
Write display data | 1 | y | y | y | 0 | 1 | x | x | z | z | z | z | z | z | z | z | z:表示データ |
これをもとに次はソフトの作成だ。ただ、この液晶モジュールはあまり情報がなくて挫けそうにもなったが、動いてみれば何のことは無い、ごく普通に「仕様上は確かにそうだよねぇ」ってコーディングで問題なかったという。そのポイントは以下の2点だ。
コーディングの方針としては、演算結果をした結果としてのデータ表示でなく、可能な限り初期設定データを表示させるだけ、にして動作を軽くできないか? という想いを込めたつもり。
そのためプログラムサイズが大きくなり、ArduinoUNOではメモリが足らず、動作させるためにはArduinoMEGAが必要だ。さらに、MEGAでも実行速度が遅いので、本番環境ではESP32を使用することにする。
// 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
}
上記の動作検証用コードの実行状況だ。いい感じで動作しているが、ランダム表示に入ってからのバーの動きが現時点での最速。コードの作りこみでもう少しは速くなるのか??