給自己紀錄。
電路訊號模擬:
https://is.gd/s2uazP
Arduino程式碼:
同步放在GITHUB裡面 https://github.com/Jir8taiwan/engine_RPM_Tachometer_with_OLED_display
// ======================================================================
// RPM 顯示器(External Interrupt 硬體中斷版)
// 版本: 1.0 20260626
// 硬體:
// D2 → 點火訊號輸入(FALLING 邊緣觸發中斷)
// OLED SH1106 SPI:D0=13, D1=11, CS=10, DC=9, Reset=8
// ======================================================================
#include "U8glib.h"
U8GLIB_SH1106_128X64 u8g(13, 11, 10, 9, 8);
// -----------------------------------------------------------------------
// Arduino 腳位定義
// -----------------------------------------------------------------------
const int SIGNAL_PIN = 2; // 硬體中斷腳位(Uno/Nano 只有 D2、D3)
// -----------------------------------------------------------------------
// 脈衝係數(依實車確認,也許要改成2.0)
// 四缸四行程,每轉1個脈衝 → 1.0
// -----------------------------------------------------------------------
const float PULSE_PER_REV = 1.0;
// -----------------------------------------------------------------------
// RPM 計算用變數(volatile:ISR 與主程式共用)
// -----------------------------------------------------------------------
volatile unsigned long last_pulse_time = 0; // 上一個脈衝的時間(微秒)
volatile unsigned long pulse_interval = 0; // 兩個脈衝之間的間隔(微秒)
volatile bool new_pulse = false; // 旗標:有新脈衝進來
// -----------------------------------------------------------------------
// 顯示用變數
// -----------------------------------------------------------------------
int current_rpm = 0;
// -----------------------------------------------------------------------
// 超時設定:超過此時間沒收到脈衝,視為 RPM = 0
// 6000 RPM → 每脈衝 10ms;怠速約 700 RPM → 每脈衝 85ms
// 設 500ms 沒訊號就歸零,合理範圍
// -----------------------------------------------------------------------
const unsigned long RPM_TIMEOUT_US = 500000UL; // 500ms(單位微秒)
// ======================================================================
// ISR:中斷服務函式(訊號 FALLING 邊緣時自動呼叫)
// 注意:ISR 內不能用 Serial.print()、delay()
// ======================================================================
void onPulse() {
unsigned long now = micros();
if (last_pulse_time > 0) {
pulse_interval = now - last_pulse_time; // 計算脈衝間隔
new_pulse = true;
}
last_pulse_time = now;
}
// ======================================================================
// setup()
// ======================================================================
void setup() {
Serial.begin(115200);
pinMode(SIGNAL_PIN, INPUT);
// 綁定中斷:D2 = INT0,FALLING = 訊號從 HIGH 下降到 LOW 時觸發
attachInterrupt(digitalPinToInterrupt(SIGNAL_PIN), onPulse, FALLING);
// OLED 開機畫面
u8g.firstPage();
do {
u8g.setFont(u8g_font_helvB08);
u8g.drawStr(3, 20, "RPM Monitor");
u8g.drawStr(3, 40, "Initializing...");
} while (u8g.nextPage());
delay(1000);
}
// ======================================================================
// loop():不會卡住,可以隨時更新畫面
// ======================================================================
void loop() {
// --------------------------------------------------------------------
// 第一步:讀取 ISR 更新的脈衝間隔(需暫時關中斷保護讀取)
// --------------------------------------------------------------------
unsigned long interval_copy = 0;
bool got_new_pulse = false;
unsigned long last_time_copy = 0;
noInterrupts(); // 暫停中斷,確保讀取期間資料不被 ISR 修改
interval_copy = pulse_interval;
got_new_pulse = new_pulse;
last_time_copy = last_pulse_time;
new_pulse = false; // 清除旗標
interrupts(); // 恢復中斷
// --------------------------------------------------------------------
// 第二步:判斷是否超時(太久沒脈衝 → RPM 歸零)
// --------------------------------------------------------------------
unsigned long now = micros();
bool timed_out = (last_time_copy > 0) &&
((now - last_time_copy) > RPM_TIMEOUT_US);
if (timed_out) {
current_rpm = 0;
// 重置,讓下次重新計算
noInterrupts();
last_pulse_time = 0;
pulse_interval = 0;
interrupts();
}
else if (got_new_pulse && interval_copy > 0) {
// --------------------------------------------------------------------
// 第三步:計算 RPM
// 公式:RPM = (60 × 1,000,000) / (interval_us × PULSE_PER_REV)
// --------------------------------------------------------------------
current_rpm = (int)(60000000.0 / ((float)interval_copy * PULSE_PER_REV));
}
// --------------------------------------------------------------------
// 第四步:Serial 輸出
// --------------------------------------------------------------------
Serial.print("RPM: ");
Serial.println(current_rpm);
// --------------------------------------------------------------------
// 第五步:更新 OLED
// --------------------------------------------------------------------
u8g.firstPage();
do {
u8g.setFont(u8g_font_helvR12);
u8g.drawStr(0, 20, "RPM:");
u8g.setPrintPos(55, 20);
u8g.print(current_rpm);
} while (u8g.nextPage());
delay(100); // 更新頻率約 10Hz,避免畫面閃爍
}

Be the first to reply