2026年9月4日金曜日

M5StickC Nixie tube Clock

マイコンボード系を入れいている箱の奥深くから、久々にM5StickCを見つけました。結構長いこと使用していなかったので、M5StickCでよくある起動しないモードになっていました。とりあえず、純正ケーブルと某端子のショートで復活!

使い道が思いつかないのでネットを検索していたところ、GitHubにCarlos Orts氏の M5StickC_Nixie_tube_Clockを見つけました。ニキシー管時計は持っていますが、ちょっと書き込んでみようかと思ったのが間違い!

コンパイル、書き込みともに正常なのに、画像が表示されない(バックライトが消えているように見える)。マイコンボードの設定、フェード処理などいろいろ書き換えても治りません。挙句には画像データをただ表示しただけでも現象が変わらない。とりあえず頭を冷やすために数日放置しちゃいました

で、復活したんですよ。もう開き直って大筋そのままで画像表示に関する内容を全部書き換えます。そのためにライブラリもM5StickC.hからM5Unified.hに変更ですよ。ちなみにM5StickC.hからM5Unified.hは混在するとエラーになります

最初に画像データに関しては、デジタルゴチゾウを作成した際に使用した画像データImageData化サイト(Row Dampを選択)にて画像データを再作成しました(vfd_18x34.h, vfd_35x67.h)。drawPng関数を使用して画像表示するように変更(テスト表示で問題なく表示されました)

次にM5Unified.h に変更したことで、スクリーンの初期設定、RTCなどいくつかの関数が使用出来なくなっていますので全て置き換え。おまけでボタン監視やフェードイン/アウトのエフォート部分も処理の安全化を行いました

NTC接続もと考えましたが、とりあえずオリジナル状態を再現するだけに止めました






#include 
#include "vfd_18x34.h"
#include "vfd_35x67.h"

m5::rtc_time_t RTC_TimeStruct;
m5::rtc_date_t RTC_DateStruct;

int mode_ = 3; // 3:2Lines 2: 2Lines(YYMM), 1:1Line

const uint8_t* n[] =  { 
  vfd_18x34_0, vfd_18x34_1, vfd_18x34_2, vfd_18x34_3, vfd_18x34_4,
  vfd_18x34_5, vfd_18x34_6, vfd_18x34_7, vfd_18x34_8, vfd_18x34_9
  }; // vfd font 18x34
 
 const uint32_t ns[] =  { 
  sizeof(vfd_18x34_0), sizeof(vfd_18x34_1), sizeof(vfd_18x34_2), sizeof(vfd_18x34_3), sizeof(vfd_18x34_4),
  sizeof(vfd_18x34_5), sizeof(vfd_18x34_6), sizeof(vfd_18x34_7), sizeof(vfd_18x34_8), sizeof(vfd_18x34_9)
  }; // vfd font 18x34

const uint8_t* m[] =  { 
  vfd_35x67_0, vfd_35x67_1, vfd_35x67_2, vfd_35x67_3, vfd_35x67_4,
  vfd_35x67_5, vfd_35x67_6, vfd_35x67_7, vfd_35x67_8, vfd_35x67_9
  }; // vfd font 35x67

const uint32_t ms[] =  { 
  sizeof(vfd_35x67_0), sizeof(vfd_35x67_1), sizeof(vfd_35x67_2), sizeof(vfd_35x67_3), sizeof(vfd_35x67_4),
  sizeof(vfd_35x67_5), sizeof(vfd_35x67_6), sizeof(vfd_35x67_7), sizeof(vfd_35x67_8), sizeof(vfd_35x67_9)
  }; // vfd font 35x67

const char *monthName[12] = {
  "Jan", "Feb", "Mar", "Apr", "May", "Jun",
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};


// 時間管理用の変数
unsigned long lastClockUpdate = 0;
unsigned long lastButtonCheck = 0;

// フェードエフェクト管理用の変数
bool isFading = false;
unsigned long lastFadeStepTime = 0;
int fadeStep = 0;
const int fadeBrightnessSequence[] = {120, 130, 140, 150, 160, 170, 180, 190, 200, 190, 180, 170, 160, 150, 140, 130, 120, 200};
const int FADE_STEPS_COUNT = 18;

// 関数のプロトタイプ宣言
void vfd_1_line();
void vfd_2_line();
void vfd_3_line();
void handleFade();
void triggerFade();

void setup(void){ 
  auto cfg = M5.config();
  M5.begin(cfg);

  // 画面の向きを設定 (1: 横向きなど)
  M5.Display.setRotation(1);
  M5.Display.fillScreen(TFT_BLACK);
  M5.Display.setBrightness(200);

  // rtc setup start ---------------------------------------------
  String pt = (__DATE__ " " __TIME__); // PC DATE TIME READ
  //000000000011111111112  Read data
  //012345678901234567890
  //Jun  6 2019 07:20:41

  // ----- 月 (Month) の取得 -----
  char m1[4]; // 3文字 + 終端文字('\0') 
  (pt.substring(0, 3)).toCharArray(m1, 4);
  String mStr = pt.substring(0, 3);
  int m2 = 1; // 該当しない場合のデフォルト値(例: 1月)を設定
  for (int mx = 0; mx < 12; mx ++) {
    if (mStr == monthName[mx]){ // 安全なString比較に変更
      m2 = mx + 1; 
      break;
    }
  }

  m5::rtc_date_t DateStruct;         // Month, Date, Year 
  DateStruct.month   = m2;
  // ----- 日 (Day) の取得 -----
  // 注意: 1桁の日は " 6" のようにスペースが含まれるため、
  // substring(4, 6) で切り出した後、toInt() を使えば自動で数値化されます。
  DateStruct.date    = (pt.substring(4, 6)).toInt();
  // ----- 年 (Year) の取得 -----
  DateStruct.year    = (pt.substring(7,11)).toInt();
  M5.Rtc.setDate(&DateStruct);
  m5::rtc_time_t TimeStruct;
  // ----- 時・分・秒 (Time) の取得 -----
  TimeStruct.hours   = (pt.substring(12,14)).toInt();
  TimeStruct.minutes = (pt.substring(15,17)).toInt();
  TimeStruct.seconds = (pt.substring(18,20)).toInt();
  M5.Rtc.setTime(&TimeStruct);
  // rtc setup end -----------------------------------------------
}
 
void loop(void){ 
  unsigned long currentMillis = millis();
  M5.update();
  // 1. ボタン入力の監視(200msごとにサクサク判定)
  if (currentMillis - lastButtonCheck >= 200) {
    if (M5.BtnA.isPressed()) {
      lastButtonCheck = currentMillis;
      if (mode_ == 3)      { mode_ = 1; }
      else if (mode_ == 2) { mode_ = 3; }
      else if (mode_ == 1) { mode_ = 2; }
      M5.Display.fillScreen(TFT_BLACK); // モード変更時に画面クリア
    }
  }

  // 2. 時計の定期描画(500msごと)
  if (currentMillis - lastClockUpdate >= 500) {
    lastClockUpdate = currentMillis;
    
    if (mode_ == 3)      { vfd_3_line(); }
    else if (mode_ == 2) { vfd_2_line(); }
    else if (mode_ == 1) { vfd_1_line(); }
  }

  // 3. フェードエフェクトの進行管理(バックグラウンド処理)
  if (isFading) {
    handleFade();
  }
}

void vfd_3_line(){
  M5.Rtc.getTime(&RTC_TimeStruct);
  M5.Rtc.getDate(&RTC_DateStruct);
  int h1 = int(RTC_TimeStruct.hours / 10 );
  int h2 = int(RTC_TimeStruct.hours - h1*10 );
  int i1 = int(RTC_TimeStruct.minutes / 10 );
  int i2 = int(RTC_TimeStruct.minutes - i1*10 );
  int s1 = int(RTC_TimeStruct.seconds / 10 );
  int s2 = int(RTC_TimeStruct.seconds - s1*10 );

  M5.Display.drawPng (m[h1], ms[h1],   3,  0, 35, 67);
  M5.Display.drawPng (m[h2], ms[h2],  41,  0, 35, 67);
  M5.Display.drawPixel( 79, 22, ORANGE); M5.Display.drawPixel( 79, 48, ORANGE); 
  M5.Display.drawPixel( 79, 21, YELLOW); M5.Display.drawPixel( 79, 47, YELLOW); 
  M5.Display.drawPng (m[i1], ms[i1],  83,  0, 35, 67);
  M5.Display.drawPng (m[i2], ms[i2], 121,  0, 35, 67);
  M5.Display.drawPng (n[s1], ns[s1], 120, 45, 18, 34);
  M5.Display.drawPng (n[s2], ns[s2], 140, 45, 18, 34);

  // フェード中でない、かつ00秒のときだけ1回だけ起動する
  if (s1 == 0 && s2 == 0 && !isFading) { triggerFade(); }
}

void vfd_2_line(){
  M5.Rtc.getTime(&RTC_TimeStruct);
  M5.Rtc.getDate(&RTC_DateStruct);

  int y1 = int(RTC_DateStruct.year    / 1000 );
  int y2 = int((RTC_DateStruct.year   - y1*1000 ) / 100 );
  int y3 = int((RTC_DateStruct.year   - y1*1000 - y2*100 ) / 10 );
  int y4 = int(RTC_DateStruct.year    - y1*1000 - y2*100 - y3*10 );
  int j1 = int(RTC_DateStruct.month   / 10);
  int j2 = int(RTC_DateStruct.month   - j1*10 );
  int d1 = int(RTC_DateStruct.date    / 10 );
  int d2 = int(RTC_DateStruct.date    - d1*10 );
  int h1 = int(RTC_TimeStruct.hours   / 10) ;
  int h2 = int(RTC_TimeStruct.hours   - h1*10 );
  int i1 = int(RTC_TimeStruct.minutes / 10 );
  int i2 = int(RTC_TimeStruct.minutes - i1*10 );
  int s1 = int(RTC_TimeStruct.seconds / 10 );
  int s2 = int(RTC_TimeStruct.seconds - s1*10 );

  // 日時表示
  M5.Display.drawPng (n[y1], ns[y1],   0,  0, 18, 34);
  M5.Display.drawPng (n[y2], ns[y2],  19,  0, 18, 34); 
  M5.Display.drawPng (n[y3], ns[y3],  38,  0, 18, 34);
  M5.Display.drawPng (n[y4], ns[y4],  57,  0, 18, 34);
  M5.Display.drawPixel(  77, 13, ORANGE); M5.Display.drawPixel(  77, 23, ORANGE); 
  M5.Display.drawPng (n[j1], ns[j1],  80,  0, 18, 34);
  M5.Display.drawPng (n[j2], ns[j2], 100,  0, 18, 34);
  M5.Display.drawPixel( 118, 13, ORANGE); M5.Display.drawPixel( 119, 23, ORANGE); 
  M5.Display.drawPng (n[d1], ns[d1], 120,  0, 18, 34);
  M5.Display.drawPng (n[d2], ns[d2], 140,  0, 18, 34);

  // 時間表示
  M5.Display.drawPng (n[h1], ns[h1],   0, 40, 18, 34);
  M5.Display.drawPng (n[h2], ns[h2],  20, 40, 18, 34); 
  M5.Display.drawPixel(  48, 54, ORANGE); M5.Display.drawPixel(  48, 64, ORANGE); 
  M5.Display.drawPng (n[i1], ns[i1],  60, 40, 18, 34);
  M5.Display.drawPng (n[i2], ns[i2],  80, 40, 18, 34);
  M5.Display.drawPixel( 108, 54, ORANGE); M5.Display.drawPixel( 108, 64, ORANGE); 
  M5.Display.drawPng (n[s1], ns[s1], 120, 40, 18, 34);
  M5.Display.drawPng (n[s2], ns[s2], 140, 40, 18, 34);

  // フェード中でない、かつ00秒のときだけ1回だけ起動する
  if (s1 == 0 && s2 == 0 && !isFading) { triggerFade(); }
}

void vfd_1_line(){
  M5.Rtc.getTime(&RTC_TimeStruct);
  M5.Rtc.getDate(&RTC_DateStruct);
  int i1 = int(RTC_TimeStruct.minutes / 10 );
  int i2 = int(RTC_TimeStruct.minutes - i1*10 );
  int s1 = int(RTC_TimeStruct.seconds / 10 );
  int s2 = int(RTC_TimeStruct.seconds - s1*10 );

  // 時間表示
  M5.Display.drawPng (m[i1], ms[i1],   2,  6, 35, 67);
  M5.Display.drawPng (m[i2], ms[i2],  41,  6, 35, 67);
  M5.Display.drawPixel( 79, 28, ORANGE); M5.Display.drawPixel( 79, 54, ORANGE); 
  M5.Display.drawPixel( 79, 27, YELLOW); M5.Display.drawPixel( 79, 53, YELLOW); 
  M5.Display.drawPng (m[s1], ms[s1],  83,  6, 35, 67);
  M5.Display.drawPng (m[s2], ms[s2], 121,  6, 35, 67);

  // フェード中でない、かつ00秒のときだけ1回だけ起動する
 if (s1 == 0 && s2 == 0 && !isFading) { triggerFade(); }
}

// フェードを開始させるフラグ制御
void triggerFade() {
  if (!isFading) {
    isFading = true;
    fadeStep = 0;
    lastFadeStepTime = millis();
  }
}

// delayを使わずにバックグラウンドで輝度をじわじわ変化させる処理
void handleFade() {
  unsigned long currentMillis = millis();
  // 25msごとに輝度を1段階変化させる
  if (currentMillis - lastFadeStepTime >= 25) {
    lastFadeStepTime = currentMillis;
    
    M5.Display.setBrightness(fadeBrightnessSequence[fadeStep]);
    fadeStep++;
    
    // すべてのステップが終了したらエフェクトを終了
    if (fadeStep >= FADE_STEPS_COUNT) {
      isFading = false;
    }
  }
}


ニキシー管表示は良い感じなので、他でこの画像データを使用して表示させるのも面白いかもと思いながら、M5StickCにはメモリ容量的にキツかなって感じです。小さい画像データだと8桁表示できますので、ドットの画像を作ってダイバージェンスメーター(古)にしても面白いかも(桁数の関係でπ(パイ)世界線、ρ(ロー)世界線は無理ですねw)

0 件のコメント:

コメントを投稿