ニキシー管でいろいろなものを表示するものを作った【ソフトウェア編】

電子工作

関連記事はこちら
・ニキシー管について
ニキシー管でいろいろなものを表示するものを作った【ハードウェア編】

コード全文

次のものがArduinoのスケッチです。機能をどんどん詰め込んだら長くなりました。

//データ受信とダイナミック点灯を統合
//その後、次の受信を待たずにボタンを押したら表示が切り替わるように変更
//updateDigits()を簡潔に
//RTCから時刻、温度を取得
//押しボタンスイッチで表示を切り替え
//digitalWriteFast.h, FastShiftOut.hを使用
//20250909 一通り完成
//20250922
//ハードの構成がほぼ決定
//ダイナミック点灯の処理をmicros()を使うやり方に変更
//温湿度センサ, 空気質センサを実装
//データ取得間隔を100msに設定
//20251008
//DCファンを実装
//ボタンインジケーターを実装
//20251104
//センサをSCD40に変更
//20251121
//室温を-3.178で補正
//20251223
//小規模なミスを修正

//SCL:A5, SDA:A4

#include <digitalWriteFast.h>
#include <FastShiftOut.h>
#include <Bounce2.h>
#include <Wire.h>
#include <RTClib.h>
#include <Arduino.h>
#include <SensirionI2cScd4x.h>

//シフトレジスタ設定
const int PIN_SER = 9;
const int PIN_LATCH = 10;
const int PIN_CLK = 11;
const int PIN_G = 12;
FastShiftOut FSO(PIN_SER, PIN_CLK, LSBFIRST);

//シフトレジスタパターン
byte digPatterns[] = {
  B10000000,
  B01000000,
  B00100000,
  B00010000,
  B00001000,
  B00000100,
  B00000010,
  B00000001
};

//ニキシー管ドライバピン
const int A = 4;
const int B = 5;
const int C = 6;
const int D = 7;
//ニキシー管 "," 制御ピン
const int E = 8;

//ニキシー管ドライバパターン
int drivePattrn[11][4] = {
  { LOW, LOW, LOW, LOW },     // 0
  { HIGH, LOW, LOW, LOW },    // 1
  { LOW, HIGH, LOW, LOW },    // 2
  { HIGH, HIGH, LOW, LOW },   // 3
  { LOW, LOW, HIGH, LOW },    // 4
  { HIGH, LOW, HIGH, LOW },   // 5
  { LOW, HIGH, HIGH, LOW },   // 6
  { HIGH, HIGH, HIGH, LOW },  // 7
  { LOW, LOW, LOW, HIGH },    // 8
  { HIGH, LOW, LOW, HIGH },   // 9
  { LOW, HIGH, LOW, HIGH },   // 消灯
};

// 表示する数字(0〜9, 10で消灯)
int digitValue[8] = { 1, 0, 4, 8, 5, 9, 6, 0 };
// カンマ表示フラグ(trueならその桁のカンマを点灯)
bool digitComma[8] = { false, true, false, false, false, false, false, false };

// 点灯・消灯の時間(us)
const unsigned int onTime = 200;
const unsigned int offTime = 1000;
//const unsigned int onTime = 1000;
//const unsigned int offTime = 500;

int currentDigit = 0;          // 現在の桁
bool isOnPhase = false;        // 点灯中かどうか
unsigned long prevMicros = 0;  // 前回切り替え時刻

// 表示モードの定義
enum DisplayMode {
  DISPLAY_CPU_TEMP,
  DISPLAY_CPU_LOAD,
  DISPLAY_MEMORY_LOAD,
  DISPLAY_TIME,
  DISPLAY_TEMP,
  DISPLAY_RH,
  DISPLAY_CO2
};
DisplayMode displayMode = DISPLAY_TIME;

//記号コードの定義
//digitValue[] にセットする特別な値
enum SymbolCode {
  SYMBOL_OFF = 10,     // 消灯
  SYMBOL_PERCENT = 0,  // "%"
  SYMBOL_M = 9,        // "M"
  SYMBOL_P = 8,        // "p"
  SYMBOL_m = 7,        // "m"
  SYMBOL_k = 6,        // "k"
  SYMBOL_n = 5,        // "n"
  SYMBOL_u = 4,        // "μ"
  SYMBOL_CELSIUS = 3   // "℃"
};

// ボタンピン
const int BUTTON1_PIN = 14;  // 時間&#x2194;気温 A0
const int BUTTON2_PIN = 15;  // CPU関連 A1
const int BUTTON1_IND = 16;  // 時間&#x2194;気温 A2
const int BUTTON2_IND = 17;  // CPU関連 A3
int groupA_index = 0;        // ボタン1
int groupB_index = 0;        // ボタン2
//デバウンス処理
Bounce debouncer1 = Bounce();
Bounce debouncer2 = Bounce();

// 各モード用の桁データを保存する配列
int digitValueSets[7][8];
bool digitCommaSets[7][8];

//RTC
RTC_DS3231 rtc;

// PPS入力ピン
const int PPS_PIN = 2;

// RTCから取得した時刻を保持
int rtcHour = 0;
int rtcMinute = 0;
int rtcSecond = 0;
unsigned long lastPpsMillis = 0;
volatile bool ppsFlag = false;  // PPS割り込みフラグ

//SCD40
SensirionI2cScd4x scd4x;
int16_t error;
#define NO_ERROR 0

static uint16_t co2 = 0;
static float temperature = 0.0;
static float humidity = 0.0;
bool dataReady = false;
unsigned long SCD40DataPrevMillis = 0;

//DCファン
unsigned long DCfanPrevMillis = 0;

void display() {
  unsigned long nowDisplay = micros();
  //unsigned long nowDisplay = millis();

  if (isOnPhase) {
    // 点灯中 → onTime 経過したらオフにする
    if (nowDisplay - prevMicros >= onTime) {
      digitalWriteFast(PIN_G, HIGH);  // 出力ディセーブル
      digitalWriteFast(E, LOW);       // カンマ消灯
      prevMicros = nowDisplay;
      isOnPhase = false;
    }
  } else {
    // 消灯中 → offTime 経過したら次の桁を点灯
    if (nowDisplay - prevMicros >= offTime) {
      currentDigit++;
      if (currentDigit >= 8) currentDigit = 0;

      //ニキシー管ドライバ出力
      digitalWriteFast(A, drivePattrn[digitValue[currentDigit]][0]);
      digitalWriteFast(B, drivePattrn[digitValue[currentDigit]][1]);
      digitalWriteFast(C, drivePattrn[digitValue[currentDigit]][2]);
      digitalWriteFast(D, drivePattrn[digitValue[currentDigit]][3]);

      // カンマ出力
      if (digitComma[currentDigit]) {
        digitalWriteFast(E, HIGH);
      } else {
        digitalWriteFast(E, LOW);
      }

      //シフトレジスタ出力
      digitalWriteFast(PIN_LATCH, LOW);
      FSO.write(digPatterns[currentDigit]);
      digitalWriteFast(PIN_LATCH, HIGH);

      //出力イネーブル
      digitalWriteFast(PIN_G, LOW);

      prevMicros = nowDisplay;
      isOnPhase = true;
    }
  }
}

//RTC PPS信号割り込み
void ppsISR() {
  ppsFlag = true;
}

// 時刻を digitValueSets にセット
void updateTimeDigits() {
  // 経過時間から0.1秒単位を算出
  unsigned long currentMillis = millis();
  unsigned long elapsed = currentMillis - lastPpsMillis;
  int tenth = elapsed / 100;  // 0.1秒単位
  if (tenth > 9) tenth = 9;

  int h = rtcHour;
  int m = rtcMinute;
  int s = rtcSecond;

  // 初期化
  for (int i = 0; i < 8; i++) {
    digitValueSets[DISPLAY_TIME][i] = SYMBOL_OFF;
    digitCommaSets[DISPLAY_TIME][i] = false;
  }

  // hh
  digitValueSets[DISPLAY_TIME][0] = h / 10;
  digitValueSets[DISPLAY_TIME][1] = h % 10;
  digitCommaSets[DISPLAY_TIME][2] = true;

  // mm
  digitValueSets[DISPLAY_TIME][2] = m / 10;
  digitValueSets[DISPLAY_TIME][3] = m % 10;
  digitCommaSets[DISPLAY_TIME][4] = true;

  // ss
  digitValueSets[DISPLAY_TIME][4] = s / 10;
  digitValueSets[DISPLAY_TIME][5] = s % 10;
  digitCommaSets[DISPLAY_TIME][6] = true;

  // 0.1s
  digitValueSets[DISPLAY_TIME][6] = tenth;
  digitCommaSets[DISPLAY_TIME][7] = false;
}

// 温度を digitValueSets にセット(SCD40対応版)
void updateSCD40TempDigits() {

  // 初期化
  for (int i = 0; i < 8; i++) {
    digitValueSets[DISPLAY_TEMP][i] = SYMBOL_OFF;
    digitCommaSets[DISPLAY_TEMP][i] = false;
  }

  //温度の補正
  float temperature2 = temperature -3.178;

  //温度値の分解
  int tempInt = (int)temperature2;                            // 整数部分
  int tempDec1 = (int)((temperature2 - tempInt) * 10) % 10;   // 小数第1位
  int tempDec2 = (int)((temperature2 - tempInt) * 100) % 10;  // 小数第2位  

  if (tempInt >= 10) {
    // 2桁の整数部 (例: 23.75)
    digitValueSets[DISPLAY_TEMP][3] = (tempInt / 10) % 10;
    digitValueSets[DISPLAY_TEMP][4] = tempInt % 10;
    digitCommaSets[DISPLAY_TEMP][5] = true;  // 小数点位置
    digitValueSets[DISPLAY_TEMP][5] = tempDec1;
    digitValueSets[DISPLAY_TEMP][6] = tempDec2;
    digitValueSets[DISPLAY_TEMP][7] = SYMBOL_CELSIUS;
  } else {
    // 1桁の整数部 (例: 5.23)
    digitValueSets[DISPLAY_TEMP][4] = tempInt % 10;
    digitCommaSets[DISPLAY_TEMP][5] = true;  // 小数点位置
    digitValueSets[DISPLAY_TEMP][5] = tempDec1;
    digitValueSets[DISPLAY_TEMP][6] = tempDec2;
    digitValueSets[DISPLAY_TEMP][7] = SYMBOL_CELSIUS;
  }
}

// 湿度を digitValueSets にセット(SCD40対応版)
void updateSCD40HumidityDigits() {

  // 初期化
  for (int i = 0; i < 8; i++) {
    digitValueSets[DISPLAY_RH][i] = SYMBOL_OFF;
    digitCommaSets[DISPLAY_RH][i] = false;
  }  

  // 湿度値の分解
  int rhInt = (int)humidity;                // 整数部分
  int rhDec1 = (int)((humidity - rhInt) * 10) % 10;   // 小数第1位
  int rhDec2 = (int)((humidity - rhInt) * 100) % 10;  // 小数第2位

  if (rhInt >= 10) {
    // 2桁の整数部 (例: 56.42%)
    digitValueSets[DISPLAY_RH][3] = (rhInt / 10) % 10;
    digitValueSets[DISPLAY_RH][4] = rhInt % 10;
    digitCommaSets[DISPLAY_RH][5] = true;  // 小数点位置
    digitValueSets[DISPLAY_RH][5] = rhDec1;
    digitValueSets[DISPLAY_RH][6] = rhDec2;
    digitValueSets[DISPLAY_RH][7] = SYMBOL_PERCENT;
  } else {
    // 1桁の整数部 (例: 7.85%)
    digitValueSets[DISPLAY_RH][4] = rhInt % 10;
    digitCommaSets[DISPLAY_RH][5] = true;  // 小数点位置
    digitValueSets[DISPLAY_RH][6] = rhDec1;
    digitValueSets[DISPLAY_RH][7] = rhDec2;
    digitValueSets[DISPLAY_RH][7] = SYMBOL_PERCENT;
  }
}

// CO2濃度を digitValueSets にセット(SCD40対応版)
void updateCo2Digits() {

  // 初期化
  for (int i = 0; i < 8; i++) {
    digitValueSets[DISPLAY_CO2][i] = SYMBOL_OFF;
    digitCommaSets[DISPLAY_CO2][i] = false;
  }

  int d4 = (co2 / 10000) % 10;  // 万の位
  int d3 = (co2 / 1000)  % 10;  // 千の位
  int d2 = (co2 / 100)   % 10;  // 百の位
  int d1 = (co2 / 10)    % 10;  // 十の位
  int d0 = co2 % 10;            // 一の位

  // 右詰め表示 (例: "1234ppm")
  digitValueSets[DISPLAY_CO2][2] = d4 ? d4 : SYMBOL_OFF;
  digitValueSets[DISPLAY_CO2][3] = (d4 || d3) ? d3 : SYMBOL_OFF;
  digitValueSets[DISPLAY_CO2][4] = (d4 || d3 || d2) ? d2 : SYMBOL_OFF;
  digitValueSets[DISPLAY_CO2][5] = (d4 || d3 || d2 || d1) ? d1 : SYMBOL_OFF;
  digitValueSets[DISPLAY_CO2][6] = d0;
  digitValueSets[DISPLAY_CO2][7] = SYMBOL_P;  // 単位 "P"
}

// PCのデータを digitValueSets にセット
void updatePCDataDigits(DisplayMode mode, const char *str, int symbol) {

  // ---- 1. 小数点の位置を探す ----
  const char *dotPtr = strchr(str, '.');
  int dotIndex = dotPtr ? (dotPtr - str) : -1;

  // ---- 2. 小数点を抜いた数字を右詰めで格納 ----
  int j = 6;  // 右端から
  for (int k = strlen(str) - 1; k >= 0 && j >= 0; k--) {
    if (str[k] == '.') continue;  // 小数点は飛ばす
    if (isdigit(str[k])) {
      digitValueSets[mode][j] = str[k] - '0';
      digitCommaSets[mode][j] = false;
      j--;
    }
  }
  // 残りは消灯で埋める
  for (; j >= 0; j--) {
    digitValueSets[mode][j] = SYMBOL_OFF;
    digitCommaSets[mode][j] = false;
  }

  // ---- 3. 右端の記号 ----
  digitValueSets[mode][7] = symbol;
  digitCommaSets[mode][7] = false;

  // ---- 4. 小数点位置を反映 ----
  if (dotIndex >= 0) {
    int digitsRight = strlen(str) - dotIndex - 1;  // 小数点の右の桁数
    int pos = 6 - (digitsRight - 1);
    if (pos >= 0 && pos <= 6) digitCommaSets[mode][pos] = true;
  }

  //CPU使用率, メモリ使用率の識別
  if (mode == DISPLAY_CPU_TEMP) {
    digitValueSets[mode][0] = 1;        // "1."
    digitCommaSets[mode][1] = true;
  } else if (mode == DISPLAY_CPU_LOAD) {
    digitValueSets[mode][0] = 1;        // "1."
    digitCommaSets[mode][1] = true;
  } else if (mode == DISPLAY_MEMORY_LOAD) {
    digitValueSets[mode][0] = 2;        // "2."
    digitCommaSets[mode][1] = true;
  }
}

//digitValueSets から digitValue, digitCommaへ
void applyDisplayMode(DisplayMode mode) {
  for (int i = 0; i < 8; i++) {
    digitValue[i] = digitValueSets[mode][i];
    digitComma[i] = digitCommaSets[mode][i];
  }
}

void setup() {
  Serial.begin(115200);
  Wire.begin();

  //DCファン
  //analogWrite(3, 128);
  pinMode(3, OUTPUT);
  //digitalWrite(3, LOW);

  //ボタンピン
  pinMode(BUTTON1_PIN, INPUT_PULLUP);
  pinMode(BUTTON2_PIN, INPUT_PULLUP);
  pinMode(BUTTON1_IND, OUTPUT);
  pinMode(BUTTON2_IND, OUTPUT);
  digitalWrite(BUTTON1_IND, HIGH);
  digitalWrite(BUTTON2_IND, LOW);
  debouncer1.attach(BUTTON1_PIN);
  debouncer2.attach(BUTTON2_PIN);
  debouncer1.interval(30);
  debouncer2.interval(30);

  //シフトレジスタ
  pinMode(PIN_SER, OUTPUT);
  pinMode(PIN_LATCH, OUTPUT);
  pinMode(PIN_CLK, OUTPUT);
  pinMode(PIN_G, OUTPUT);

  //ニキシー管ドライバ
  pinMode(A, OUTPUT);
  pinMode(B, OUTPUT);
  pinMode(C, OUTPUT);
  pinMode(D, OUTPUT);
  pinMode(E, OUTPUT);
  digitalWriteFast(A, LOW);
  digitalWriteFast(B, LOW);
  digitalWriteFast(C, LOW);
  digitalWriteFast(D, LOW);
  digitalWriteFast(E, LOW);

  //RTC
  if (!rtc.begin()) {
    Serial.println("RTC not found!");
    while (1)
      ;
  }
  // DS3231を1Hz出力に設定
  rtc.writeSqwPinMode(DS3231_SquareWave1Hz);
  // 現在時刻で設定
  //rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  Serial.println("RTC OK.");

  //SCD40
  scd4x.begin(Wire, SCD41_I2C_ADDR_62);
  scd4x.wakeUp();
  scd4x.stopPeriodicMeasurement();
  scd4x.reinit();
  // 測定開始
  error = scd4x.startPeriodicMeasurement();
  if (error != NO_ERROR) {
    Serial.println("Error: startPeriodicMeasurement()");
  }
  Serial.println("SCD40 OK.");

  //PPSピン
  pinMode(PPS_PIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(PPS_PIN), ppsISR, RISING);

  Serial.println("setup OK.");
}

void loop() {
  // PPS信号が来たらRTCを読み直す
  if (ppsFlag) {
    ppsFlag = false;
    DateTime now = rtc.now();
    rtcHour = now.hour();
    rtcMinute = now.minute();
    rtcSecond = now.second();
    lastPpsMillis = millis();
  }

  //DCファン
  unsigned long DCfanCurrentMillis = millis();
  if (DCfanCurrentMillis - DCfanPrevMillis >= 10) {
    DCfanPrevMillis = millis();
    PIND = _BV(PIND3);
  }

  //SCD40データ取得
  unsigned long SCD40DataCurrentMillis = millis();
  if (SCD40DataCurrentMillis - SCD40DataPrevMillis >= 5000) {
    SCD40DataPrevMillis = millis();
    // データが準備できているか確認
    if (scd4x.getDataReadyStatus(dataReady) != 0) return;
    if (!dataReady) return;

    // 測定値取得
    if (scd4x.readMeasurement(co2, temperature, humidity) != 0) return;
  }

  //表示更新
  if (displayMode == DISPLAY_TIME) {
    updateTimeDigits();
  } else if (displayMode == DISPLAY_TEMP){
    updateSCD40TempDigits();
  } else if (displayMode == DISPLAY_RH){
    updateSCD40HumidityDigits();
  } else if (displayMode == DISPLAY_CO2){
    updateCo2Digits();
  }

  //digitValue[8]とdigitComma[8]に値を反映
  applyDisplayMode(displayMode);

  // 確認用出力
  //Serial.print("digitValue = { ");
  //for (int i = 0; i < 8; i++) {
  //  Serial.print(digitValue[i]);
  //  if (i < 7) Serial.print(", ");
  //}
  //Serial.println(" }");

  //Serial.print("digitComma = { ");
  //for (int i = 0; i < 8; i++) {
  //  Serial.print(digitComma[i] ? "true" : "false");
  //  if (i < 7) Serial.print(", ");
  //}
  //Serial.println(" }");

  // --- ボタン1処理 (時間&#x2194;気温) ---
  debouncer1.update();
  if (debouncer1.fell()) {
    digitalWrite(BUTTON1_IND, HIGH);
    digitalWrite(BUTTON2_IND, LOW);
    groupA_index = (groupA_index + 1) % 4;
    if (groupA_index == 0) displayMode = DISPLAY_TIME;
    else if (groupA_index == 1) displayMode = DISPLAY_TEMP;
    else if (groupA_index == 2) displayMode = DISPLAY_RH;
    else if (groupA_index == 3) displayMode = DISPLAY_CO2;
    applyDisplayMode(displayMode);

    Serial.println("Button 1 pressed!");
  }

  // --- ボタン2処理 (CPU,Load,Memory) ---
  debouncer2.update();
  if (debouncer2.fell()) {
    digitalWrite(BUTTON1_IND, LOW);
    digitalWrite(BUTTON2_IND, HIGH);
    groupB_index = (groupB_index + 1) % 3;
    if (groupB_index == 0) displayMode = DISPLAY_CPU_TEMP;
    else if (groupB_index == 1) displayMode = DISPLAY_CPU_LOAD;
    else displayMode = DISPLAY_MEMORY_LOAD;
    applyDisplayMode(displayMode);

    Serial.println("Button 2 pressed!");
  }

  //PCデータ受信
  if (Serial.available()) {
    char line[64];
    size_t len = Serial.readBytesUntil('\n', line, sizeof(line) - 1);
    line[len] = '\0';

    if (len > 0) {
      // --- CSV分割 ---
      char *cpuTemp = nullptr;
      char *cpuLoad = nullptr;
      char *memoryLoad = nullptr;

      int field = 0;
      char *token = strtok(line, ",");
      while (token != nullptr) {
        if (field == 0) cpuTemp = token;
        else if (field == 1) cpuLoad = token;
        else if (field == 2) memoryLoad = token;
        field++;
        token = strtok(nullptr, ",");
      }
      if (field < 3) {
        Serial.println("Invalid data received");
        return;
      }

      updatePCDataDigits(DISPLAY_CPU_TEMP, cpuTemp, SYMBOL_CELSIUS);
      updatePCDataDigits(DISPLAY_CPU_LOAD, cpuLoad, SYMBOL_PERCENT);
      updatePCDataDigits(DISPLAY_MEMORY_LOAD, memoryLoad, SYMBOL_PERCENT);

      // 受信直後は現在のモードに合わせて反映
      applyDisplayMode(displayMode);
    }
  }
  //ダイナミック点灯の関数の呼び出し
  display();
}

 大きく分けるとダイナミック点灯の関数、時刻・温度・湿度・CO₂濃度・PCデータを取得する関数、PCデータをシリアル通信で受信する処理、ボタンを押されたときの処理となっています。

arduinoピン接続先

ダイナミック点灯に関係する部分

//シフトレジスタ設定
const int PIN_SER = 9;
const int PIN_LATCH = 10;
const int PIN_CLK = 11;
const int PIN_G = 12;
FastShiftOut FSO(PIN_SER, PIN_CLK, LSBFIRST);

//シフトレジスタパターン
byte digPatterns[] = {
  B10000000,
  B01000000,
  B00100000,
  B00010000,
  B00001000,
  B00000100,
  B00000010,
  B00000001
};

//ニキシー管ドライバピン
const int A = 4;
const int B = 5;
const int C = 6;
const int D = 7;
//ニキシー管 "," 制御ピン
const int E = 8;

//ニキシー管ドライバパターン
int drivePattrn[11][4] = {
  { LOW, LOW, LOW, LOW },     // 0
  { HIGH, LOW, LOW, LOW },    // 1
  { LOW, HIGH, LOW, LOW },    // 2
  { HIGH, HIGH, LOW, LOW },   // 3
  { LOW, LOW, HIGH, LOW },    // 4
  { HIGH, LOW, HIGH, LOW },   // 5
  { LOW, HIGH, HIGH, LOW },   // 6
  { HIGH, HIGH, HIGH, LOW },  // 7
  { LOW, LOW, LOW, HIGH },    // 8
  { HIGH, LOW, LOW, HIGH },   // 9
  { LOW, HIGH, LOW, HIGH },   // 消灯
};

// 表示する数字(0〜9, 10で消灯)
int digitValue[8] = { 1, 0, 4, 8, 5, 9, 6, 0 };
// カンマ表示フラグ(trueならその桁のカンマを点灯)
bool digitComma[8] = { false, true, false, false, false, false, false, false };

// 点灯・消灯の時間(us)
const unsigned int onTime = 200;
const unsigned int offTime = 1000;
//const unsigned int onTime = 1000;
//const unsigned int offTime = 500;

int currentDigit = 0;          // 現在の桁
bool isOnPhase = false;        // 点灯中かどうか
unsigned long prevMicros = 0;  // 前回切り替え時刻

//記号コードの定義
//digitValue[] にセットする特別な値
enum SymbolCode {
  SYMBOL_OFF = 10,     // 消灯
  SYMBOL_PERCENT = 0,  // "%"
  SYMBOL_M = 9,        // "M"
  SYMBOL_P = 8,        // "p"
  SYMBOL_m = 7,        // "m"
  SYMBOL_k = 6,        // "k"
  SYMBOL_n = 5,        // "n"
  SYMBOL_u = 4,        // "μ"
  SYMBOL_CELSIUS = 3   // "℃"
};

void display() {
  unsigned long nowDisplay = micros();
  //unsigned long nowDisplay = millis();

  if (isOnPhase) {
    // 点灯中 → onTime 経過したらオフにする
    if (nowDisplay - prevMicros >= onTime) {
      digitalWriteFast(PIN_G, HIGH);  // 出力ディセーブル
      digitalWriteFast(E, LOW);       // カンマ消灯
      prevMicros = nowDisplay;
      isOnPhase = false;
    }
  } else {
    // 消灯中 → offTime 経過したら次の桁を点灯
    if (nowDisplay - prevMicros >= offTime) {
      currentDigit++;
      if (currentDigit >= 8) currentDigit = 0;

      //ニキシー管ドライバ出力
      digitalWriteFast(A, drivePattrn[digitValue[currentDigit]][0]);
      digitalWriteFast(B, drivePattrn[digitValue[currentDigit]][1]);
      digitalWriteFast(C, drivePattrn[digitValue[currentDigit]][2]);
      digitalWriteFast(D, drivePattrn[digitValue[currentDigit]][3]);

      // カンマ出力
      if (digitComma[currentDigit]) {
        digitalWriteFast(E, HIGH);
      } else {
        digitalWriteFast(E, LOW);
      }

      //シフトレジスタ出力
      digitalWriteFast(PIN_LATCH, LOW);
      FSO.write(digPatterns[currentDigit]);
      digitalWriteFast(PIN_LATCH, HIGH);

      //出力イネーブル
      digitalWriteFast(PIN_G, LOW);

      prevMicros = nowDisplay;
      isOnPhase = true;
    }
  }
}

display() は、ニキシー管のダイナミック点灯処理をまとめた関数です。
digitValue[8]digitComma[8] に格納された表示データをもとに各桁の表示を行います。

display() の処理内容は、以下の流れになっています。

  1. digitValue[8]digitComma[8] のデータを参照し、
    デコーダICおよびカンマ制御用トランジスタへ信号を出力する
    (カソード側の制御)
  2. シフトレジスタへ信号を出力し、表示する桁のアノードを選択する
    (アノード側の制御)
  3. シフトレジスタの出力ディセーブル端子を LOW にする
    このタイミングで、アノード側のフォトカプラがオンになり始める
  4. onTime[μs]の間、何もせず待機する
    この時間がニキシー管の点灯時間となる
  5. カンマ制御用トランジスタをオフにし、
    シフトレジスタの出力ディセーブル端子を HIGH にする
    この時点で、アノード側のフォトカプラがオフになり始める
  6. offTime[μs]経過後、次の桁に進み、再び 1 の処理を行う

この一連の処理を高速に繰り返すことでダイナミック点灯を実現しています。

処理速度をできるだけ向上させるため、digitalWriteFast ライブラリとFastShiftOut ライブラリを使用しています。

display()loop() 関数内から毎回呼び出しています。タイマー割り込みを使わずこの構成にしている理由は、別の処理で外部割り込みを使用しており、割り込み同士の競合を避けるためです。

時刻を取得する部分

// 表示モードの定義
enum DisplayMode {
  DISPLAY_CPU_TEMP,
  DISPLAY_CPU_LOAD,
  DISPLAY_MEMORY_LOAD,
  DISPLAY_TIME,
  DISPLAY_TEMP,
  DISPLAY_RH,
  DISPLAY_CO2
};
DisplayMode displayMode = DISPLAY_TIME;

// 各モード用の桁データを保存する配列
int digitValueSets[7][8];
bool digitCommaSets[7][8];

//RTC
RTC_DS3231 rtc;

// PPS入力ピン
const int PPS_PIN = 2;

// RTCから取得した時刻を保持
int rtcHour = 0;
int rtcMinute = 0;
int rtcSecond = 0;
unsigned long lastPpsMillis = 0;
volatile bool ppsFlag = false;  // PPS割り込みフラグ

//RTC PPS信号割り込み
void ppsISR() {
  ppsFlag = true;
}

// 時刻を digitValueSets にセット
void updateTimeDigits() {
  // 経過時間から0.1秒単位を算出
  unsigned long currentMillis = millis();
  unsigned long elapsed = currentMillis - lastPpsMillis;
  int tenth = elapsed / 100;  // 0.1秒単位
  if (tenth > 9) tenth = 9;

  int h = rtcHour;
  int m = rtcMinute;
  int s = rtcSecond;

  // 初期化
  for (int i = 0; i < 8; i++) {
    digitValueSets[DISPLAY_TIME][i] = SYMBOL_OFF;
    digitCommaSets[DISPLAY_TIME][i] = false;
  }

  // hh
  digitValueSets[DISPLAY_TIME][0] = h / 10;
  digitValueSets[DISPLAY_TIME][1] = h % 10;
  digitCommaSets[DISPLAY_TIME][2] = true;

  // mm
  digitValueSets[DISPLAY_TIME][2] = m / 10;
  digitValueSets[DISPLAY_TIME][3] = m % 10;
  digitCommaSets[DISPLAY_TIME][4] = true;

  // ss
  digitValueSets[DISPLAY_TIME][4] = s / 10;
  digitValueSets[DISPLAY_TIME][5] = s % 10;
  digitCommaSets[DISPLAY_TIME][6] = true;

  // 0.1s
  digitValueSets[DISPLAY_TIME][6] = tenth;
  digitCommaSets[DISPLAY_TIME][7] = false;
}

//digitValueSets から digitValue, digitCommaへ
void applyDisplayMode(DisplayMode mode) {
  for (int i = 0; i < 8; i++) {
    digitValue[i] = digitValueSets[mode][i];
    digitComma[i] = digitCommaSets[mode][i];
  }
}

void setup() {
  //RTC
  if (!rtc.begin()) {
    Serial.println("RTC not found!");
    while (1)
      ;
  }
  // DS3231を1Hz出力に設定
  rtc.writeSqwPinMode(DS3231_SquareWave1Hz);
  // 現在時刻で設定
  //rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  Serial.println("RTC OK.");

  //PPSピン
  pinMode(PPS_PIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(PPS_PIN), ppsISR, RISING);

  Serial.println("setup OK.");
}

void loop() {
  // PPS信号が来たらRTCを読み直す
  if (ppsFlag) {
    ppsFlag = false;
    DateTime now = rtc.now();
    rtcHour = now.hour();
    rtcMinute = now.minute();
    rtcSecond = now.second();
    lastPpsMillis = millis();
  }

  //表示更新
  if (displayMode == DISPLAY_TIME) {
    updateTimeDigits();
  } else if (displayMode == DISPLAY_TEMP){
    updateSCD40TempDigits();
  } else if (displayMode == DISPLAY_RH){
    updateSCD40HumidityDigits();
  } else if (displayMode == DISPLAY_CO2){
    updateCo2Digits();
  }

  //digitValue[8]とdigitComma[8]に値を反映
  applyDisplayMode(displayMode);

  //ダイナミック点灯の関数の呼び出し
  display();
}

温度や時刻などの表示内容は、あらかじめ表示モードとして定義しています。各モードに対応する表示データは、digitValueSets[7][8]digitCommaSets[7][8] に格納するようにしています。

PPS信号による割り込み

 RTCから出力されるPPS信号を受信すると、外部割り込みとして ppsISR() が実行されます。これによりRTCの秒が切り替わった瞬間に ppsFlag が立ちます。

loop() 内ではこの ppsFlag を監視しフラグが立ったタイミングで次の処理を行います。

  • RTCから現在時刻を読み取る
  • PPS信号を受信した時点の millis() を記録する

これにより「正確な秒の境界」と「マイコン内部の時刻」を対応づけることができます。

0.1秒単位の時刻の取得

 表示モードが DISPLAY_TIME に設定されている場合、現在の時刻を表示データに反映するため、updateTimeDigits() 関数が実行されます。

updateTimeDigits() では、

  • PPS信号を受信した時点の millis()
  • 現在の millis()

の差を計算し、PPS受信から何ミリ秒経過したかを求めます。この経過時間をもとに0.1秒単位の時刻を算出します。

 算出した時刻はそのまま digitValue[8] に反映され、ニキシー管の表示内容として使用されます。

センサからデータを読み取る処理

次のコードがセンサーから温度を読み取る部分です。

//SCD40
SensirionI2cScd4x scd4x;
int16_t error;
#define NO_ERROR 0

static uint16_t co2 = 0;
static float temperature = 0.0;
static float humidity = 0.0;
bool dataReady = false;
unsigned long SCD40DataPrevMillis = 0;

// 温度を digitValueSets にセット(SCD40対応版)
void updateSCD40TempDigits() {

  // 初期化
  for (int i = 0; i < 8; i++) {
    digitValueSets[DISPLAY_TEMP][i] = SYMBOL_OFF;
    digitCommaSets[DISPLAY_TEMP][i] = false;
  }

  //温度の補正
  float temperature2 = temperature -3.178;

  //温度値の分解
  int tempInt = (int)temperature2;                            // 整数部分
  int tempDec1 = (int)((temperature2 - tempInt) * 10) % 10;   // 小数第1位
  int tempDec2 = (int)((temperature2 - tempInt) * 100) % 10;  // 小数第2位  

  if (tempInt >= 10) {
    // 2桁の整数部 (例: 23.75)
    digitValueSets[DISPLAY_TEMP][3] = (tempInt / 10) % 10;
    digitValueSets[DISPLAY_TEMP][4] = tempInt % 10;
    digitCommaSets[DISPLAY_TEMP][5] = true;  // 小数点位置
    digitValueSets[DISPLAY_TEMP][5] = tempDec1;
    digitValueSets[DISPLAY_TEMP][6] = tempDec2;
    digitValueSets[DISPLAY_TEMP][7] = SYMBOL_CELSIUS;
  } else {
    // 1桁の整数部 (例: 5.23)
    digitValueSets[DISPLAY_TEMP][4] = tempInt % 10;
    digitCommaSets[DISPLAY_TEMP][5] = true;  // 小数点位置
    digitValueSets[DISPLAY_TEMP][5] = tempDec1;
    digitValueSets[DISPLAY_TEMP][6] = tempDec2;
    digitValueSets[DISPLAY_TEMP][7] = SYMBOL_CELSIUS;
  }
}

void setup() {
  //SCD40
  scd4x.begin(Wire, SCD41_I2C_ADDR_62);
  scd4x.wakeUp();
  scd4x.stopPeriodicMeasurement();
  scd4x.reinit();
  // 測定開始
  error = scd4x.startPeriodicMeasurement();
  if (error != NO_ERROR) {
    Serial.println("Error: startPeriodicMeasurement()");
  }
  Serial.println("SCD40 OK.");
}

void loop() {

  //SCD40データ取得
  unsigned long SCD40DataCurrentMillis = millis();
  if (SCD40DataCurrentMillis - SCD40DataPrevMillis >= 5000) {
    SCD40DataPrevMillis = millis();
    // データが準備できているか確認
    if (scd4x.getDataReadyStatus(dataReady) != 0) return;
    if (!dataReady) return;

    // 測定値取得
    if (scd4x.readMeasurement(co2, temperature, humidity) != 0) return;
  }

  //表示更新
  if (displayMode == DISPLAY_TIME) {
    updateTimeDigits();
  } else if (displayMode == DISPLAY_TEMP){
    updateSCD40TempDigits();
  } else if (displayMode == DISPLAY_RH){
    updateSCD40HumidityDigits();
  } else if (displayMode == DISPLAY_CO2){
    updateCo2Digits();
  }

  //digitValue[8]とdigitComma[8]に値を反映
  applyDisplayMode(displayMode);

  //ダイナミック点灯の関数の呼び出し
  display();
}

 SCD40からのデータ取得は、5000ms(5秒)ごとに行っています。取得した測定値は、まず updateSCD40TempDigits() によってdigitValueSets に格納されます。

 その後、現在の表示モードに応じて、digitValue[8]digitComma[8] に値を反映し、実際のニキシー管表示に使用します。

 ケース内に外気を導入するDCファンをつけていましたが、機器の排熱の影響は完全には排除できませんでした。そのため、取得した温度から3.178℃引くことで補償しています。

 湿度およびCO₂濃度についても、処理の流れは同様です。

PCのデータを扱う部分

// PCのデータを digitValueSets にセット
void updatePCDataDigits(DisplayMode mode, const char *str, int symbol) {

  // ---- 1. 小数点の位置を探す ----
  const char *dotPtr = strchr(str, '.');
  int dotIndex = dotPtr ? (dotPtr - str) : -1;

  // ---- 2. 小数点を抜いた数字を右詰めで格納 ----
  int j = 6;  // 右端から
  for (int k = strlen(str) - 1; k >= 0 && j >= 0; k--) {
    if (str[k] == '.') continue;  // 小数点は飛ばす
    if (isdigit(str[k])) {
      digitValueSets[mode][j] = str[k] - '0';
      digitCommaSets[mode][j] = false;
      j--;
    }
  }
  // 残りは消灯で埋める
  for (; j >= 0; j--) {
    digitValueSets[mode][j] = SYMBOL_OFF;
    digitCommaSets[mode][j] = false;
  }

  // ---- 3. 右端の記号 ----
  digitValueSets[mode][7] = symbol;
  digitCommaSets[mode][7] = false;

  // ---- 4. 小数点位置を反映 ----
  if (dotIndex >= 0) {
    int digitsRight = strlen(str) - dotIndex - 1;  // 小数点の右の桁数
    int pos = 6 - (digitsRight - 1);
    if (pos >= 0 && pos <= 6) digitCommaSets[mode][pos] = true;
  }

  //CPU使用率, メモリ使用率の識別
  if (mode == DISPLAY_CPU_TEMP) {
    digitValueSets[mode][0] = 1;        // "1."
    digitCommaSets[mode][1] = true;
  } else if (mode == DISPLAY_CPU_LOAD) {
    digitValueSets[mode][0] = 1;        // "1."
    digitCommaSets[mode][1] = true;
  } else if (mode == DISPLAY_MEMORY_LOAD) {
    digitValueSets[mode][0] = 2;        // "2."
    digitCommaSets[mode][1] = true;
  }
}

void loop() {
  //PCデータ受信
  if (Serial.available()) {
    char line[64];
    size_t len = Serial.readBytesUntil('\n', line, sizeof(line) - 1);
    line[len] = '\0';

    if (len > 0) {
      // --- CSV分割 ---
      char *cpuTemp = nullptr;
      char *cpuLoad = nullptr;
      char *memoryLoad = nullptr;

      int field = 0;
      char *token = strtok(line, ",");
      while (token != nullptr) {
        if (field == 0) cpuTemp = token;
        else if (field == 1) cpuLoad = token;
        else if (field == 2) memoryLoad = token;
        field++;
        token = strtok(nullptr, ",");
      }
      if (field < 3) {
        Serial.println("Invalid data received");
        return;
      }

      updatePCDataDigits(DISPLAY_CPU_TEMP, cpuTemp, SYMBOL_CELSIUS);
      updatePCDataDigits(DISPLAY_CPU_LOAD, cpuLoad, SYMBOL_PERCENT);
      updatePCDataDigits(DISPLAY_MEMORY_LOAD, memoryLoad, SYMBOL_PERCENT);

      // 受信直後は現在のモードに合わせて反映
      applyDisplayMode(displayMode);
    }
  }
  //ダイナミック点灯の関数の呼び出し
  display();
}

 PCからのデータは、CSV形式で送信するようにしています。シリアル通信で受信した文字列を分割し、他のセンサデータと同様に処理してdigitValue[8]digitComma[8] に反映します。

 表示時には、CPUの情報なのか、メモリの情報なのかが一目で分かるよう、数値の先頭に識別用の番号を付けて表示しています。これにより、どの種類のデータが表示されているかを判別できるようにしています。

ボタンを押されたときの処理

// ボタンピン
const int BUTTON1_PIN = 14;  // 時間↔気温 A0
const int BUTTON2_PIN = 15;  // CPU関連 A1
const int BUTTON1_IND = 16;  // 時間↔気温 A2
const int BUTTON2_IND = 17;  // CPU関連 A3
int groupA_index = 0;        // ボタン1
int groupB_index = 0;        // ボタン2
//デバウンス処理
Bounce debouncer1 = Bounce();
Bounce debouncer2 = Bounce();

void setup() {
  //ボタンピン
  pinMode(BUTTON1_PIN, INPUT_PULLUP);
  pinMode(BUTTON2_PIN, INPUT_PULLUP);
  pinMode(BUTTON1_IND, OUTPUT);
  pinMode(BUTTON2_IND, OUTPUT);
  digitalWrite(BUTTON1_IND, HIGH);
  digitalWrite(BUTTON2_IND, LOW);
  debouncer1.attach(BUTTON1_PIN);
  debouncer2.attach(BUTTON2_PIN);
  debouncer1.interval(30);
  debouncer2.interval(30);
}

  // --- ボタン1処理 (時間↔気温) ---
  debouncer1.update();
  if (debouncer1.fell()) {
    digitalWrite(BUTTON1_IND, HIGH);
    digitalWrite(BUTTON2_IND, LOW);
    groupA_index = (groupA_index + 1) % 4;
    if (groupA_index == 0) displayMode = DISPLAY_TIME;
    else if (groupA_index == 1) displayMode = DISPLAY_TEMP;
    else if (groupA_index == 2) displayMode = DISPLAY_RH;
    else if (groupA_index == 3) displayMode = DISPLAY_CO2;
    applyDisplayMode(displayMode);

    Serial.println("Button 1 pressed!");
  }

  // --- ボタン2処理 (CPU,Load,Memory) ---
  debouncer2.update();
  if (debouncer2.fell()) {
    digitalWrite(BUTTON1_IND, LOW);
    digitalWrite(BUTTON2_IND, HIGH);
    groupB_index = (groupB_index + 1) % 3;
    if (groupB_index == 0) displayMode = DISPLAY_CPU_TEMP;
    else if (groupB_index == 1) displayMode = DISPLAY_CPU_LOAD;
    else displayMode = DISPLAY_MEMORY_LOAD;
    applyDisplayMode(displayMode);

    Serial.println("Button 2 pressed!");
  }

 押しボタンスイッチのデバウンス処理は、ライブラリを使用して簡単に実装しています。チャタリングを気にせず安定して入力を読み取れるようにしています。

ボタンの役割は次のとおりです。

  • ボタン1:時刻・温度・湿度・CO₂濃度の表示を切り替え
  • ボタン2:PCから送られてくるデータの表示を切り替え

それぞれのボタンを押すことで表示内容を操作できるようにしています。

DCファンの制御

  //DCファン
  unsigned long DCfanCurrentMillis = millis();
  if (DCfanCurrentMillis - DCfanPrevMillis >= 10) {
    DCfanPrevMillis = millis();
    PIND = _BV(PIND3);
  }

 当初はDCファンをPWM制御する予定でしたが、出力50%(デューティ比50%)の状態で風量・騒音ともにちょうど良かったため、現在はその設定のまま使用しています。

また、analogWrite() を使ったPWM制御を試したところ、モーターからけっこう大きな駆動音が発生したため、今回は採用していません。

わかっているバグ

 PPS信号の割り込みは1秒に1回発生し、PCからのシリアル通信も1秒に1回行われています。そのため、両者のタイミングが重なった場合シリアル通信が失敗することがあります。
 一方で、タイミングが重ならなければ問題なく動作するため、現状ではこの挙動を把握したうえで、そのままの構成としています。

 以上がソフトの説明でした。次が最後で、PCからCPUやメモリの情報をシリアル通信で送る方法を説明します。

関連記事はこちら
・ニキシー管について
ニキシー管でいろいろなものを表示するものを作った【ハードウェア編】

タイトルとURLをコピーしました