私のプレスカブには自作のタコメータがついています。元々はカブを整備/カスタマイズしていくうえで状態が良くなったのか悪くなったのか、どう変化したのかログを取るのに、その練習というか前段階として製作したものです。
製作してから大分と時間が経ってしまっていますが、覚書としてここに残しておきたいと思います。
自作タコメータの仕様
LCD画面表時
PICマイコンを使用してLCDディスプレイに以下内容を表示します。表示は250msec周期で更新します。
- エンジン回転数表示:0~9,999[RPM]
- 燃料インジェクタON時間表示:0~9,999[us]
燃料インジェクタON時間は、ECUからのインジェクタ制御信号のON時間を計測しています。この部分はポートのポーリングで計測しているのでかなりアバウトな値が表示されます。将来のロガー機能を見越してつけてみました。
タコメータの外観
共立電子かどこかで購入したケースを加工して、基板とLCDを収めました。LCD部は納まりきらなかったので表示部が顔を出しています。一応、開いたところはホットボンドで塞ぎましたが、雨に降られると液晶のフレーム隙間から浸水しそうです。そのうち対策しようと思います。
ハードウェア構成
PICマイコン(PIC16F628A)、キャラクタLCD、ECU信号入力部で構成されています。
PICは5V動作させています。5V電源はダイソーで売っていたシガレットUSB電源を使いました。また、CPUクロックはPIC内蔵(4MHz)を使用しています。
観測する点火信号/インジェクタ信号は12VですがON/OFF時に100V以上のオーバーシュートが発生していました。そこでローパスフィルタを通してからダイオードで12Vにクリッピングしたものをトランジスタで受け、信号反転させてマイコンに入力するようにしました。
コネクタはICSP用とLCD接続用を設けました(LCDを接続すると容器に納まらず、コネクタを少し削りました)。
回路図
タコメータの回路図です。昔のデータから引っ張り出してきたので、もしかすると、ここから何か変更加えているかもしれません。
PICプログラム構成
エンジン回転数の算出
ECUの点火信号間隔を、PICマイコンのCCPモジュールをCaptureモードで使用して計測します。その時間から回転数を算出するようにしました。
燃料インジェクタON時間の算出
ECUの燃料インジェクタ制御信号を観測して、ON時間を計測しました。こちらはポーリングでポートを監視することで時間計測していて、かなりアバウトな値になっています。 (最初は外部割込みのエッジを切り替えて割り込みで制御しようかと考えていたのですが、処理時間が間に合わないのかうまく動作しませんでした)
バー表示
数値だけだとパッと見で分かりにくいので、LCDの空いているところにバー表示するようにしました。バーは横に伸びる形です。初期化時に、LCDのCGRAMにバー表示用データを転送しています。
ソースコード
開発環境はMPLAB X IDE、コンパイラはXC8を使用しました。XC8はフリーライセンスで最適化が使用できませんでしたが、特に問題なく収まりました。 かなり汚くて恥ずかしいのですが、スースコードは以下の通りです。なお、LCD表示部プログラムは以下のサイト様のコードを使わせて頂きました。
- きむ茶工房ガレージハウス - キャラクタLCDモジュールに表示を行う (http://www.geocities.jp/zattouka/GarageHouse/micon/MPLAB/16F819/LCD/LCD.htm)
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が一緒の造りなので、ここからハンドルまで配線を伸ばしました。
将来的にロガーを作るときは、ロガー本体をサイドカバー内に納め、表示部を別に作ってそこまで配線する方がよさそうです。
このときはざっくりと配線を切った張ったしましたが、もしカブが新車だったらここまで思い切って出来なかったかもしれません。理想としてはECUとハーネスコネクタの間に挟む形で入れたいですが、ECUで使われているコネクタはどうやら特殊なもののようで、一般カタログにありませんでした。海外の通販サイトでハウジングだけ販売していましたが、ロット単位の発注のみで手が出ません。何処かの小売店さんがバラ売りしてくれると嬉しいのですが、たぶん、需要ないだろうなぁ。
その後
このタコメータを付けたのが昨年(2005年)の夏頃だったかと思います。この記事の投稿で丁度1年くらいですね。
今でもちゃんと動作してくれていますが、やはり雨水がLCDにしみ込んだのか一部表示されない箇所が出てきました。加えて、インジェクション信号側の表示がお亡くなりになっています(こちらは回路がまずかったかも)。
そろそろ当初の目論みであるカブロガー(仮)の製作をする時かもしれません。
楽しく拝見させていただいております.
回路図のリンクが切れてしまっており閲覧することができません.もし宜しければ更新していただけると幸いです
あれ、確かに表示されませんね。なんでだろう。。。
すみません、お手数なのですが以下URLにアクセスすれば回路図が見れると思います。
https://lh3.googleusercontent.com/PFgL6Zr8vmNkf3v_SA-W8GlmS0WYLUEfMho2s5LK7rvlXvg4YAGTaBkAmDbfMal4K2f16Ptk7-A7BwC3Fbnzp9-UM-buG5s5bfcKZ5foEZrPeHS3SK-T4XWXpxHKf4d29NUs1XtDo8wniAS3lnC2ICOxWHbjAhvVm_rOhsTiPAum0UTIMHMMl_gAfVYeen_FsyZj7ZS2Et0UFocACUhgIgdyI2TH5od9T19LgX_OIL0TKemhXAzM3df-yMJakJ4nWszErxovxVM2CT4uR0nb1u39yLOVoV2lwCDMVcPv6QPZfc_PSvhOAvXQF2ga3nCPPpVsgTPBuykcVOFSDaxlxoQg28-ni-RUFXAGSdx778OS4NecTt8EI8mPrB6Ov87HnBPvtFOqBkKJcegJCRH5h2SDR5ZiS2hkzhXXqafJ8GJwLD2QVRbZdoWCDfU6fdh6Nj9o2waVtqoEEv0OFBc_sD9R0wHLjVAmcYKvD8XQaEOnCLVW5AkYPpT1Qik3EZDNgPLCfCgQWC7TN8wLSmCbWZcmxKcxq9C0LXrbeDgnzqcVmuYX31I-82q6m8cOCAuwCuCj5BxZlMAqJIzgZMACu1D-l2b-bUZZC5GMFSjBmWcw3ssdUcPDsTepScgO6I2J9Y4zuBbTxvq6QmgM6AMUQP__2KO2s-XSbTtT_3zh_Ya2NvYcjHMXpKV0runkov_uGE4-DxOHlz2fGxBcIJ5g4sr8PfygfE442SoDKD6gEXuvaGxh=w1369-h943-no
すけログさま
はじめまして、
いろいろテーマが多岐に及んで楽しませていただきました。電子工作は楽しいですね。
私は、とりあえず簡単にプログラムを走らせたい方向けにTinyBASIC基板というのを作っています。
8月に発表したばかりで、あまり知られてはいません。
ご興味ありましたらご連絡をどうぞ。お待ちしています。
https://www.hinoelectronics.com/
http://www.directpress.jp/pr00035974.html
https://hinoelectronics.com/index.php?main_page=index&cPath=7
コメントありがとうございます。
HP拝見しました。blogにある八つ橋のくだりを読んで思わず笑ってしまいました。RFCの"鳥類キャリアによるIPデータグラムの伝送規格"を思い出します。