[カブ] タコメータの自作(PICマイコン+LCD)

私のプレスカブには自作のタコメータがついています。元々はカブを整備/カスタマイズしていくうえで状態が良くなったのか悪くなったのか、どう変化したのかログを取るのに、その練習というか前段階として製作したものです。

製作してから大分と時間が経ってしまっていますが、覚書としてここに残しておきたいと思います。

 

自作タコメータの仕様

LCD画面表時

PICマイコンを使用してLCDディスプレイに以下内容を表示します。表示は250msec周期で更新します。

  • エンジン回転数表示:0~9,999[RPM]
  • 燃料インジェクタON時間表示:0~9,999[us]

燃料インジェクタON時間は、ECUからのインジェクタ制御信号のON時間を計測しています。この部分はポートのポーリングで計測しているのでかなりアバウトな値が表示されます。将来のロガー機能を見越してつけてみました。

タコメータの外観

共立電子かどこかで購入したケースを加工して、基板とLCDを収めました。LCD部は納まりきらなかったので表示部が顔を出しています。一応、開いたところはホットボンドで塞ぎましたが、雨に降られると液晶のフレーム隙間から浸水しそうです。そのうち対策しようと思います。

RIMG1062.JPG

ハードウェア構成

PICマイコン(PIC16F628A)、キャラクタLCD、ECU信号入力部で構成されています。

PICは5V動作させています。5V電源はダイソーで売っていたシガレットUSB電源を使いました。また、CPUクロックはPIC内蔵(4MHz)を使用しています。

観測する点火信号/インジェクタ信号は12VですがON/OFF時に100V以上のオーバーシュートが発生していました。そこでローパスフィルタを通してからダイオードで12Vにクリッピングしたものをトランジスタで受け、信号反転させてマイコンに入力するようにしました。

コネクタはICSP用とLCD接続用を設けました(LCDを接続すると容器に納まらず、コネクタを少し削りました)。

タコメータ基板
タコメータ基板
タコメータ基板(LCD装着)
タコメータ基板(LCD装着)

回路図

タコメータの回路図です。昔のデータから引っ張り出してきたので、もしかすると、ここから何か変更加えているかもしれません。 cub_taco_shematics.png

PICプログラム構成

 

エンジン回転数の算出

ECUの点火信号間隔を、PICマイコンのCCPモジュールをCaptureモードで使用して計測します。その時間から回転数を算出するようにしました。

燃料インジェクタON時間の算出

ECUの燃料インジェクタ制御信号を観測して、ON時間を計測しました。こちらはポーリングでポートを監視することで時間計測していて、かなりアバウトな値になっています。 (最初は外部割込みのエッジを切り替えて割り込みで制御しようかと考えていたのですが、処理時間が間に合わないのかうまく動作しませんでした)

バー表示

数値だけだとパッと見で分かりにくいので、LCDの空いているところにバー表示するようにしました。バーは横に伸びる形です。初期化時に、LCDのCGRAMにバー表示用データを転送しています。

ソースコード

開発環境はMPLAB X IDE、コンパイラはXC8を使用しました。XC8はフリーライセンスで最適化が使用できませんでしたが、特に問題なく収まりました。 かなり汚くて恥ずかしいのですが、スースコードは以下の通りです。なお、LCD表示部プログラムは以下のサイト様のコードを使わせて頂きました。

 

main.c


/******************************************************************************/

/* Files to Include */
/******************************************************************************/

#if defined(__XC)
#include <xc.h> /* XC8 General Include File */
#elif defined(HI_TECH_C)
#include <htc.h> /* HiTech General Include File */
#endif

#include <stdint.h> /* For uint8_t definition */
#include <stdbool.h> /* For true/false definition */
#include <stdio.h>
#include <stdlib.h>

#include "system.h" /* System funct/params, like osc/peripheral config */
#include "user.h" /* User funct/params, such as InitApp */
#include "lcd.h"

#define _XTAL_FREQ 4000000

//forBreadBoard
//#define LCD_RS RA1
//#define LCD_EN RA0
//#define LCD_D4 RB4
//#define LCD_D5 RB5
//#define LCD_D6 RA6
//#define LCD_D7 RA7

//forPrototype
#define LCD_RS RA2
#define LCD_EN RA3
#define LCD_D4 RA0
#define LCD_D5 RA1
#define LCD_D6 RA7
#define LCD_D7 RA6

#define LCD_STROBE() ((LCD_EN=1),(LCD_EN=0))

/******************************************************************************/
/* User Global Variable Declaration */
/******************************************************************************/
uint16_t gIgnitionWidth = 0x0000;
bool gFlagDispUpdate = false;
bool gFlagIgnitionUpdate = false;

/******************************************************************************/
/* LCD function */
/******************************************************************************/

// 遅延の処理
void Wait(unsigned int num)
{
int i ;

// numで指定した回数だけ繰り返す
for (i=0 ; i<num ; i++) {
__delay_ms(10) ; // 10msプログラムの一時停止
}
}
// LCDにデータを実際に送信する処理
void lcd_write(unsigned char c)
{
// 送信データのバイト列上位4ビットを処理
LCD_D4 = ( ( c >> 4 ) & 0x01 ) ;
LCD_D5 = ( ( c >> 5 ) & 0x01 ) ;
LCD_D6 = ( ( c >> 6 ) & 0x01 ) ;
LCD_D7 = ( ( c >> 7 ) & 0x01 ) ;
LCD_STROBE() ;
// 送信データのバイト列下位4ビットを処理
LCD_D4 = ( ( c ) & 0x01 ) ;
LCD_D5 = ( ( c >> 1 ) & 0x01 ) ;
LCD_D6 = ( ( c >> 2 ) & 0x01 ) ;
LCD_D7 = ( ( c >> 3 ) & 0x01 ) ;
LCD_STROBE() ;
}
// LCDにコマンドを発行する処理
void command(unsigned char c)
{
LCD_RS = 0 ;
LCD_D4 = ( ( c ) & 0x01 ) ;
LCD_D5 = ( ( c >> 1 ) & 0x01 ) ;
LCD_D6 = ( ( c >> 2 ) & 0x01 ) ;
LCD_D7 = ( ( c >> 3 ) & 0x01 ) ;
LCD_STROBE() ;
}
/*******************************************************************************
* lcd_clear - LCDモジュールの画面を消す処理 *
*******************************************************************************/
void lcd_clear(void)
{
LCD_RS = 0 ;
lcd_write(0x01) ; // Clear Display : 画面全体に20Hのスペースで表示、カーソルはcol=0,row=0に移動
__delay_ms(2) ; // LCDが処理(1.53ms)するのを待ちます
}
/*******************************************************************************
* lcd_setCursor - LCDモジュール画面内のカーソル位置を移動する処理 *
* col : 横(列)方向のカーソル位置(0-15) *
* row : 縦(行)方向のカーソル位置(0-1) *
********************************************************************************/
void lcd_setCursor(int col, int row)
{
int row_offsets[] = { 0x00, 0x40 } ;

LCD_RS = 0 ;
lcd_write(0x80 | (col + row_offsets[row])) ; // Set DDRAM Adddress : 00H-0FH,40H-4FH
}
/*******************************************************************************
* lcd_putc - LCDにデータを1バイト出力する処理 *
* c : 出力する文字データ *
*******************************************************************************/
void lcd_putc(char c)
{
LCD_RS = 1 ; // RSの制御信号線をセットします
lcd_write( c ) ; // LCDにデータの送信
}
/*******************************************************************************
* lcd_puts - LCDに文字列データを出力する処理(文字列をNULL(0x00)まで繰返し出力)*
* s : 出力する文字列のデータ *
*******************************************************************************/
void lcd_puts(const char * s)
{
LCD_RS = 1 ; // RSの制御信号線をセットします
while(*s) lcd_write(*s++) ;
}
/*******************************************************************************
* lcd_init - LCDの初期化処理 *
*******************************************************************************/
void lcd_init()
{
LCD_RS = 0 ;
LCD_EN = 0 ;

__delay_ms(15) ; // 電源ON後15msまで待ってから初期化

// LCDの立上げ時のチェックデータ(イニシャライズ処理用)を設定
command(0x03) ;
__delay_ms(5) ;
command(0x02) ;
// LCDにコマンドを発行します
lcd_write(0x28) ; // function set : データ線は4本・表示は2行・フォントは5x8ドット
lcd_write(0x0c) ; // display control: 画面表示はON・カーソル表示はOFF・カーソル点滅はOFF
lcd_clear() ; // Clear Display : 画面をクリアし、カーソル位置はcol=0,row=0
lcd_write(0x06) ; // entry mode set : 文字を表示した次にカーソルを移動するを指示
}

/***************************************
* printf stub
***************************************/
void putch(char data)
{
lcd_putc(data);
}

void drawBar(uint8_t value)
{
uint8_t i = 0;

//最大値を超える場合
if(value > 30) value = 30;

//バー表示の描画
lcd_putc(0x06);
for(i=0; i<6; i++)
{
if(value >= 5)
{
lcd_putc(0x05);
value -= 5;
}
else
{
lcd_putc(value);
value = 0;
}
}
}

/******************************************************************************/
/* Main Program */
/******************************************************************************/
void main(void)
{
uint32_t dispRPM = 0;
uint16_t dispINJ = 0;

uint16_t injectionFall = 0;
uint16_t injectionRise = 0;
uint16_t injectionWidth = 0;
uint8_t injectionUpdateFailCount = 0;
bool flagInjectionUpdate = false;

/* Configure the oscillator for the device */
ConfigureOscillator();

/* Initialize I/O and Peripherals for application */
InitApp();

Wait(20);

/* Initialize LCD */
lcd_init();

/* InitMessage */
//LCD Line1
lcd_setCursor(0,0);
printf("CUB LOGGER LCD");

//LCD Line2
lcd_setCursor(0,1);
printf("Ver:0.3");

Wait(100);
lcd_clear();

//main loop
while(1)
{
//update injection pulse
while(!gFlagDispUpdate)
{
//Wait for injection signal low(OFF)
while(PORTBbits.RB0 != 0 && gFlagDispUpdate == false){;}
if(gFlagDispUpdate) break;

//Wait for injection signal high(ON)
do
{
//get timer1 value
injectionRise = TMR1;
}
while(PORTBbits.RB0 != 1 && gFlagDispUpdate == false);
if(gFlagDispUpdate) break;

//Wait for injection signal low(OFF)
do
{
//get timer1 value
injectionFall = TMR1;
}
while(PORTBbits.RB0 != 0 && gFlagDispUpdate == false);
if(gFlagDispUpdate) break;

//インジェクションパルス時間の取得
if(injectionRise < injectionFall)
injectionWidth = (injectionFall - injectionRise) / 2;
else
injectionWidth = ((UINT16_MAX - injectionRise + 1) + injectionFall) / 2;

//インジェクションパルス更新フラグをセット
flagInjectionUpdate = true;

//インジェクションパルス幅更新失敗カウントを0クリア
injectionUpdateFailCount = 0;
break;
}

//update LCD
if(gFlagDispUpdate)
{
//clear update flag
gFlagDispUpdate = false;

//RPM値の取得
if(gFlagIgnitionUpdate)
dispRPM = 60000000 / (gIgnitionWidth * 2);
else
dispRPM = 0;
gFlagIgnitionUpdate = false;

//Injection値の取得
if(flagInjectionUpdate == true)
{
dispINJ = injectionWidth;
}
else if(injectionUpdateFailCount < 5)
{
//Injection値が未更新の場合、表示更新5回までは
//前回測定値を表示
dispINJ = injectionWidth;

//更新失敗カウントをインクリメント
injectionUpdateFailCount++;
}
else
{
dispINJ = 0;
}
flagInjectionUpdate = false;

//LCD Line1
lcd_setCursor(0,0);
printf("RPM:%5u", dispRPM);
drawBar(dispRPM / 200); //一目盛り200RPM

//LCD Line2
lcd_setCursor(0,1);
printf("INJ:%5u", dispINJ);
drawBar(dispINJ / 100); //一目盛り0.1msec
}
}//end of while(1)

}//end of main

 

interrupts.c


/******************************************************************************/
/*Files to Include */
/******************************************************************************/

#if defined(__XC)
#include <xc.h> /* XC8 General Include File */
#elif defined(HI_TECH_C)
#include <htc.h> /* HiTech General Include File */
#endif

#include <stdint.h> /* For uint8_t definition */
#include <stdbool.h> /* For true/false definition */

/******************************************************************************/
/* global variable */
/******************************************************************************/
extern uint16_t gIgnitionWidth;
extern bool gFlagDispUpdate;
extern bool gFlagIgnitionUpdate;

/******************************************************************************/
/* Interrupt Routines */
/******************************************************************************/

/* Baseline devices don't have interrupts. Note that some PIC16's
* are baseline devices. Unfortunately the baseline detection macro is
* _PIC12 */
#ifndef _PIC12

void interrupt isr(void)
{
static uint16_t ignitionBefore = 0;
static uint8_t timer2Counter = 0;
uint16_t temp = 0;

/* This code stub shows general interrupt handling. Note that these
conditional statements are not handled within 3 seperate if blocks.
Do not use a seperate if block for each interrupt flag to avoid run
time errors. */

#if 0
//External Interrupt
if(INTCONbits.INTF == 1)
{
PORTAbits.RA3 = 0;
PORTAbits.RA3 = 1;

INTCONbits.INTF = 0; //clear interrupt flag

//??????TIMER1????????
if(OPTION_REGbits.INTEDG == 1)
{
//rising edge
injectionRise = TMR1;

//????????FallEdge???
OPTION_REGbits.INTEDG = 0;

//????????ON?????(active LOW)
if(injectionRise > injectionFall)
gInjection = injectionRise - injectionFall;
else
gInjection = injectionFall - injectionRise;

//??????
gInjectionUpdateCounter++;
}
else
{
//falling edge
injectionFall = TMR1;

//????????RiseEdge???
OPTION_REGbits.INTEDG = 1;
}
}
#endif

//CCP Interrupt
if(PIR1bits.CCP1IF == 1)
{
PIR1bits.CCP1IF = 0; //clear interrupt flag

//get capture parametor
do
{
temp = CCPR1;
}
while(temp != CCPR1);

//calculate ignition pulse period(1count = 1usec)
if(temp > ignitionBefore)
gIgnitionWidth = temp - ignitionBefore;
else
gIgnitionWidth = (UINT16_MAX - ignitionBefore + 1) + temp;

//save ignition counter value
ignitionBefore = temp;

//set update flag
gFlagIgnitionUpdate = true;
}

//Timer2 Interrupt
if(PIR1bits.TMR2IF == 1)
{
PIR1bits.TMR2IF = 0; //clear interrupt flag

//Count up
timer2Counter++;
// if(timer2Counter >= 16)
if(timer2Counter >= 32)
{
timer2Counter = 0;
gFlagDispUpdate = true; //set disp update flag
}
}

}
#endif

user.c


/******************************************************************************/
/* Files to Include */
/******************************************************************************/

#if defined(__XC)
#include <xc.h> /* XC8 General Include File */
#elif defined(HI_TECH_C)
#include <htc.h> /* HiTech General Include File */
#endif

#include <stdint.h> /* For uint8_t definition */
#include <stdbool.h> /* For true/false definition */

#include "user.h"

/******************************************************************************/
/* User Functions */
/******************************************************************************/

/* <Initialize variables in user.h and insert code for user algorithms.> */

void InitApp(void)
{
/* Setup analog functionality and port direction */
CMCON = 0x07; //Comparator module off

PORTA = 0;
PORTB = 0;

//forPrototype
TRISA = 0x00; //PortA[7:0]:Output
TRISB = 0xFF; //PortB[7:0]:Input
OPTION_REGbits.nRBPU = 0; //PortB Internal pull-up:Enable

//forBreadBoard
// TRISA0 = 0; //RA0:Output(LCD)
// TRISA1 = 0; //RA1:Output(LCD)
// TRISA3 = 0; //RA5:Output(DebugLED)
// TRISA6 = 0; //RA6:Output(LCD)
// TRISA7 = 0; //RA7:Output(LCD)
//
// TRISB0 = 1; //RB0:Input for INT
// TRISB3 = 1; //RB3:Input for Capture mode
// TRISB4 = 0; //RB4:Output(LCD)
// TRISB5 = 0; //RB5:Output(LCD)

/* Initialize peripherals */
//Timer0
TMR0 = 0;
OPTION_REGbits.T0CS = 0; //Timer0 internal clock(CLKOUT)
OPTION_REGbits.PSA = 0; //Timer0 prescaler
OPTION_REGbits.PS = 0x7; //prescaler 1:256

//Capture
CCP1CONbits.CCP1M = 0x04; //Capture mode, every falling edge

//Timer1 for Capture
T1CONbits.TMR1ON = 0x0; //Timer1 stop
TMR1H = 0x00;
TMR1L = 0x00;
// T1CONbits.T1CKPS = 0x0; //Timer1 prescaler 1:1
T1CONbits.T1CKPS = 0x2; //Timer1 prescaler 4:1
T1CONbits.T1OSCEN = 0x0; //Timer1 external oscillator off
T1CONbits.TMR1CS = 0x0; //Internal clock(Fosc / 4)
T1CONbits.TMR1ON = 0x1; //Timer1 start

//Timer2 for LCDUpdate interval
PR2 = 0xFF; //Timer2 period
TMR2 = 0; //Timer2 counter
T2CONbits.TOUTPS = 16; //Timer2 postscaler = 1:16
T2CONbits.T2CKPS = 0xF; //Timer2 prescaler = 1:16
T2CONbits.TMR2ON = 1; //Timer2 On

/* Enable interrupts */
// OPTION_REGbits.INTEDG = 1; //RB0/INT interrupt on rising edge

PIE1bits.CCP1IE = 1; //enable CCP1 interrupt
PIE1bits.TMR2IE = 1; //enable Timer2 interrupt

INTCONbits.PEIE = 1; //enable peripheral interrupt
// INTCONbits.T0IE = 1; //enable Timer0 interrupt
// INTCONbits.INTE = 1; //enable RB0/INT external interrupt
INTCONbits.GIE = 1; //enable global interrupt
}

 

カブにタコメータを装着する

サイドカバーを開けたところにECUがあります。ここに制御信号が集まっているので、点火信号とインジェクタ制御信号の配線を分岐しました。今回はタコメータ本体とLCDが一緒の造りなので、ここからハンドルまで配線を伸ばしました。

RIMG1279.JPG

 RIMG1094.JPG

将来的にロガーを作るときは、ロガー本体をサイドカバー内に納め、表示部を別に作ってそこまで配線する方がよさそうです。

このときはざっくりと配線を切った張ったしましたが、もしカブが新車だったらここまで思い切って出来なかったかもしれません。理想としてはECUとハーネスコネクタの間に挟む形で入れたいですが、ECUで使われているコネクタはどうやら特殊なもののようで、一般カタログにありませんでした。海外の通販サイトでハウジングだけ販売していましたが、ロット単位の発注のみで手が出ません。何処かの小売店さんがバラ売りしてくれると嬉しいのですが、たぶん、需要ないだろうなぁ。

その後

このタコメータを付けたのが昨年(2005年)の夏頃だったかと思います。この記事の投稿で丁度1年くらいですね。

今でもちゃんと動作してくれていますが、やはり雨水がLCDにしみ込んだのか一部表示されない箇所が出てきました。加えて、インジェクション信号側の表示がお亡くなりになっています(こちらは回路がまずかったかも)。

そろそろ当初の目論みであるカブロガー(仮)の製作をする時かもしれません。

コメントを残す

メールアドレスが公開されることはありません。

This blog is kept spam free by WP-SpamFree.