タコパルス簡易発生回路

タコパルスジェネレータ
タコパルスジェネレータ

ステッピングタコメータを作成するときに、机上で動作テストを行う際に必要な簡易装置です。
Arduino uno R3で作成しています。
ソースをコンパイルしてR3に書き込み、起動するとマニュアルモードで立ち上がります。

操作方法
A. 通常操作(マニュアルモード)
a. ボリュームを回すとリアルタイムに回転数とHzが変化します。
B. プリセットの呼び出しと保存
a. SW1〜SW3を「短く」押すと、保存されている回転数を呼び出します。
b. マニュアルモードで任意の回転数に合わせた後、SW1〜SW3を「長押し(約2秒)」すると、画面に「Saved!」とモードの位置に表示され、現在の回転数が上書き保存されます。
C. 自動スイープ機能
a. SW4を短く押すと、自動で回転数が上下するスイープモードになります。もう一度押すとマニュアルモードに戻ります。 (0~15000rpmを移動するので長いです)
D. 点火パルス係数の設定モード
a. 電源を入れる際、SW4を押しながら起動してください。
b. 設定モードに入り、ボリュームを回すことでクランク1回転あたりのパルス数(0.5、1.0、2.0など)を変更できます。
c. SW4を長押しすると設定が保存され、再起動します。

必要な機材は
Arduino uno R3
LCDディスプレイとI2Cモジュール
タクトスイッチ4個
50KΩのボリューム1個です。

回路の配線手順
A. LCDディスプレイ(I2C接続)の配線
a. VCCをArduinoの「5V」へ接続します。
b. GNDをArduinoの「GND」へ接続します。
c. SDAをArduinoの「A4」ピンへ接続します。
d. SCLをArduinoの「A5」ピンへ接続します。

B. タクトスイッチの配線(4個すべて同じ方法です)
a. SW1(プリセット1用):一方を「D2」ピンへ、もう一方を「GND」へ接続します。
b. SW2(プリセット2用):一方を「D3」ピンへ、もう一方を「GND」へ接続します。
c. SW3(プリセット3用):一方を「D4」ピンへ、もう一方を「GND」へ接続します。
d. SW4(スイープ・設定用):一方を「D5」ピンへ、もう一方を「GND」へ接続します。

C. ボリュームとパルス出力の配線
a. ボリュームは「5V」「GND」「A0」に接続します。(3配線の両サイドに5VとGNDをそれぞれ、真ん中にA0を繋ぎます。)
b. パルス出力は「9」番ピンから取得します


上記の仕様をもとに書いた、ソースコード
(Arduino IDE 2.3.10 LiquidCrystal_I2Cは2.0.0を使用)

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>

// --- ピン割り当て ---
const int pulsePin = 9;
const int potPin = A0;
const int sw1Pin = 2; // プリセット1
const int sw2Pin = 3; // プリセット2
const int sw3Pin = 4; // プリセット3
const int sw4Pin = 5; // スイープ開始・停止 / 設定モード移行

// --- LCD設定 ---
LiquidCrystal_I2C lcd(0x27, 16, 2); // アドレス0x27, 16文字2行

// --- 定数 ---
const int minRPM = 0;
const int maxRPM = 15000;
const byte MAGIC_NUMBER = 0xA7; // EEPROM初期化確認用

// --- グローバル変数 ---
float pulsesPerRev = 1.0; // 1回転あたりのパルス数
int presetRPM[3] = {1500, 9500, 10700}; //プリセット値

byte currentMode = 0; // 0:手動, 1:P1, 2:P2, 3:P3, 4:スイープ

float currentFreq = 0.0;
int currentRPM = 0;

// スイープ用変数
int sweepRPM = 0;
bool sweepUp = true;
unsigned long lastSweepTime = 0;

// ボタン状態管理用
struct ButtonState {
  int pin;
  bool lastState;
  unsigned long pressTime;
  bool isPressed;
  bool longPressHandled;
};

ButtonState btn1 = {sw1Pin, HIGH, 0, false, false};
ButtonState btn2 = {sw2Pin, HIGH, 0, false, false};
ButtonState btn3 = {sw3Pin, HIGH, 0, false, false};
ButtonState btn4 = {sw4Pin, HIGH, 0, false, false};

// --- EEPROM アドレスマップ ---
// 0: マジックナンバー (1byte)
// 1: pulsesPerRev (float, 4bytes)
// 5: presetRPM[0] (int, 2bytes)
// 7: presetRPM[1] (int, 2bytes)
// 9: presetRPM[2] (int, 2bytes)
// 11: currentMode (byte)

void setup() {
  pinMode(pulsePin, OUTPUT);
  pinMode(sw1Pin, INPUT_PULLUP);
  pinMode(sw2Pin, INPUT_PULLUP);
  pinMode(sw3Pin, INPUT_PULLUP);
  pinMode(sw4Pin, INPUT_PULLUP);

  Serial.begin(115200);

  lcd.init();
  lcd.backlight();

  // Timer1を使ったPWM出力の設定
  // Fast PWM, TOP=ICR1
  TCCR1A = _BV(COM1A1) | _BV(WGM11);
  TCCR1B = _BV(WGM13) | _BV(WGM12) | _BV(CS11) | _BV(CS10);

  // SW4を押しながら起動した場合は設定モードへ
  if (digitalRead(sw4Pin) == LOW) {
    settingsMode();
  }

  // EEPROMの読み込みと初期化チェック
  byte magic = EEPROM.read(0);

  if (magic != MAGIC_NUMBER) {

    // 初回起動時(EEPROM初期化)
    EEPROM.put(1, pulsesPerRev);
    EEPROM.put(5, presetRPM[0]);
    EEPROM.put(7, presetRPM[1]);
    EEPROM.put(9, presetRPM[2]);
    EEPROM.put(11, currentMode);

    EEPROM.write(0, MAGIC_NUMBER);

  } else {

    // 既存設定の読み込み
    EEPROM.get(1, pulsesPerRev);
    EEPROM.get(5, presetRPM[0]);
    EEPROM.get(7, presetRPM[1]);
    EEPROM.get(9, presetRPM[2]);
    EEPROM.get(11, currentMode);
  }

  updateDisplay();
}

void loop() {

  handleButtons();

  switch (currentMode) {

    case 0: // マニュアルモード
      {
        int potValue = analogRead(potPin);

        currentRPM = map(
          potValue,
          0,
          1023,
          minRPM,
          maxRPM
        );

        currentFreq = rpmToFreq(currentRPM);

        setPulseFrequency(currentFreq);
      }
      break;

    case 1: // プリセット1
    case 2: // プリセット2
    case 3: // プリセット3
      {
        currentRPM = presetRPM[currentMode - 1];

        currentFreq = rpmToFreq(currentRPM);

        setPulseFrequency(currentFreq);
      }
      break;

    case 4: // スイープモード
      {
        if (millis() - lastSweepTime > 20) {

          lastSweepTime = millis();

          if (sweepUp) {

            sweepRPM += 50;

            if (sweepRPM >= maxRPM) {
              sweepRPM = maxRPM;
              sweepUp = false;
            }

          } else {

            sweepRPM -= 50;

            if (sweepRPM <= minRPM) {
              sweepRPM = minRPM;
              sweepUp = true;
            }
          }

          currentRPM = sweepRPM;

          currentFreq = rpmToFreq(currentRPM);

          setPulseFrequency(currentFreq);
        }
      }
      break;
  }

  // LCDの表示更新(100msに1回程度)
  static unsigned long lastDisplayUpdate = 0;

  if (millis() - lastDisplayUpdate > 100) {

    lastDisplayUpdate = millis();

    updateDisplay();
  }
}

// ============================================================
// ボタン処理
// ============================================================

void handleButtons() {

  checkButton(&btn1, 1);
  checkButton(&btn2, 2);
  checkButton(&btn3, 3);
  checkButton(&btn4, 4);
}

void checkButton(ButtonState* btn, int btnNum) {

  bool currentState = digitalRead(btn->pin);

  // チャタリング防止用
  static unsigned long lastEdgeTime[5] = {
    0, 0, 0, 0, 0
  };

  if (currentState != btn->lastState) {

    if (millis() - lastEdgeTime[btnNum] < 50) {
      return;
    }

    lastEdgeTime[btnNum] = millis();

  } else {

    // 状態が変化していない場合は長押し監視
    if (currentState == LOW && btn->isPressed) {

      // SW1~SW3のみ長押し判定
      if (btnNum >= 1 && btnNum <= 3) {

        if (millis() - btn->pressTime > 1500 &&
            !btn->longPressHandled) {

          btn->longPressHandled = true;

          handleLongPress(btnNum);
        }
      }
    }

    return;
  }

  // 確実な状態変化(エッジ検出)

  if (currentState == LOW) {

    // HIGH → LOW
    // ボタンが押された
    btn->pressTime = millis();
    btn->isPressed = true;
    btn->longPressHandled = false;

  } else {

    // LOW → HIGH
    // ボタンが離された

    if (btn->isPressed && !btn->longPressHandled) {
      handleShortPress(btnNum);
    }

    btn->isPressed = false;
  }

  btn->lastState = currentState;
}

// ============================================================
// 短押し処理
// ============================================================

void handleShortPress(int btnNum) {

  if (btnNum >= 1 && btnNum <= 3) {

    // 選択中のプリセットボタンをもう一度押すと
    // マニュアルに戻る
    if (currentMode == btnNum) {

      currentMode = 0;

    } else {

      currentMode = btnNum;
    }

    EEPROM.put(11, currentMode);

  } else if (btnNum == 4) {

    if (currentMode == 4) {

      currentMode = 0;

    } else {

      currentMode = 4;

      // 現在の回転数からスイープ開始
      sweepRPM = currentRPM;
    }

    EEPROM.put(11, currentMode);
  }
}

// ============================================================
// 長押し処理
// ============================================================

void handleLongPress(int btnNum) {

  if (btnNum >= 1 && btnNum <= 3) {

    // マニュアルモードの時だけプリセットを上書き可能
    if (currentMode == 0) {

      presetRPM[btnNum - 1] = currentRPM;

      int addr = 5 + (btnNum - 1) * 2;

      EEPROM.put(
        addr,
        presetRPM[btnNum - 1]
      );

      lcd.setCursor(0, 0);

      lcd.print("Preset ");
      lcd.print(btnNum);
      lcd.print(" Saved! ");

      delay(1000);
    }
  }
}

// ============================================================
// パルス出力設定
// ============================================================

void setPulseFrequency(float freq) {

  if (freq < 1.9) {

    // 約1.9Hz未満、または0RPMの場合
    // 出力停止
    OCR1A = 0;

  } else {

    unsigned int icrValue =
      (unsigned int)(125000.0 / freq);

    if (icrValue < 208) {
      icrValue = 208;
    }

    if (icrValue > 65535) {
      icrValue = 65535;
    }

    static unsigned int lastIcrValue = 0;

    if (icrValue != lastIcrValue) {

      ICR1 = icrValue;

      // デューティ比50%
      OCR1A = icrValue / 2;

      lastIcrValue = icrValue;
    }
  }
}

// ============================================================
// 周波数 → RPM
// ============================================================

int freqToRPM(float freq) {

  return (int)(
    (freq * 60.0) / pulsesPerRev
  );
}

// ============================================================
// RPM → 周波数
// ============================================================

float rpmToFreq(int rpm) {

  return (
    (float)rpm * pulsesPerRev
  ) / 60.0;
}

// ============================================================
// LCD表示更新
// ============================================================

void updateDisplay() {

  lcd.setCursor(0, 0);

  // ----------------------------------------------------------
  // 1行目
  // 15文字分をモード表示に使用
  // 16文字目をパルス数表示に使用
  // ----------------------------------------------------------

  switch (currentMode) {

    case 0:
      lcd.print("Mode: Manual   ");
      break;

    case 1:
      lcd.print("Mode: Preset 1 ");
      break;

    case 2:
      lcd.print("Mode: Preset 2 ");
      break;

    case 3:
      lcd.print("Mode: Preset 3 ");
      break;

    case 4:
      lcd.print("Mode: AutoSweep");
      break;
  }

  // ----------------------------------------------------------
  // 16文字目
  //
  // 0.5 pulse/rev → /
  // 1.0 pulse/rev → 1
  // 2.0 pulse/rev → 2
  // 4.0 pulse/rev → 4
  // ----------------------------------------------------------

  if (pulsesPerRev == 0.5) {

    lcd.print("/");

  } else if (pulsesPerRev == 1.0) {

    lcd.print("1");

  } else if (pulsesPerRev == 2.0) {

    lcd.print("2");

  } else if (pulsesPerRev == 4.0) {

    lcd.print("4");

  } else {

    // 想定外の値
    lcd.print("?");
  }

  // ----------------------------------------------------------
  // 2行目
  // ----------------------------------------------------------

  lcd.setCursor(0, 1);

  char buf[17];

  sprintf(
    buf,
    "%5drpm%4d.%1dHz",
    currentRPM,
    (int)currentFreq,
    (int)(currentFreq * 10) % 10
  );

  lcd.print(buf);
}

// ============================================================
// 設定モード
// 起動時にSW4を押しっぱなし
// ============================================================

void settingsMode() {

  lcd.init();
  lcd.backlight();

  lcd.setCursor(0, 0);
  lcd.print("Setup Mode");

  EEPROM.get(1, pulsesPerRev);

  // 利用可能なパルス係数
  float pprOptions[] = {
    0.5,
    1.0,
    2.0,
    4.0
  };

  int optionIndex = 1; // デフォルト 1.0

  for (int i = 0; i < 4; i++) {

    if (abs(pulsesPerRev - pprOptions[i]) < 0.1) {
      optionIndex = i;
    }
  }

  while (true) {

    int potValue = analogRead(potPin);

    int newIndex =
      map(
        potValue,
        0,
        1023,
        0,
        3
      );

    newIndex = constrain(
      newIndex,
      0,
      3
    );

    if (newIndex != optionIndex) {

      optionIndex = newIndex;

      lcd.setCursor(0, 1);

      lcd.print("PPR(Pulse): ");

      lcd.print(
        pprOptions[optionIndex],
        1
      );

      lcd.print("   ");
    }

    // SW4長押しで保存
    if (digitalRead(sw4Pin) == LOW) {

      delay(1500);

      if (digitalRead(sw4Pin) == LOW) {

        pulsesPerRev =
          pprOptions[optionIndex];

        EEPROM.put(
          1,
          pulsesPerRev
        );

        lcd.clear();

        lcd.print("Settings Saved.");

        lcd.setCursor(0, 1);

        lcd.print("Please Restart.");

        // 電源リセット待ち
        while (true);
      }
    }
  }
}

一応、仕様上はそのように作ったつもりです。今のところ問題は見つかってませんがこちらは参考程度に使用してください。
装置のグランドレベルはタコメータと合わせておいてください。