前々回前回に引き続きニキシー管時計の製作を行っていきます。今回は最終回で筐体と完全なプログラムを作成します。

当初はラズベリーパイを使ったサイネージと一体化する予定でしたが、世界観が合わないので止めました。(あと市販の小型LCDの解像度が低すぎて遠目から見づらいというのもありました。)

プログラムの作成

今回実装する機能は以下の通りです。

  • 自動調光
  • 日付表示
  • 炎ノイズシミュレート
  • タッチセンサによる設定
  • マルチコアによる時刻表示部の独立

参考にさせて頂いたサイトは前回の記事に記載しています。

また、完成したプログラムは前回のページ下部のリンクからダウンロードできます。

ここからはプログラムを小分けにして解説していきます。

①変数宣言部

#include "WiFi.h"
#include "time.h"
#include "Gaussian.h"
#include "math.h"
#define bright 12
#define colon 16
#define cds 35
#define button_cds T2//GPIO2
#define button_noise T0//GPIO4
#define button_date_time T4//GPIO13

const int PIN_BIT[] = {15,33,32,27};//4bit OUTPUT
const int CLK[] = {26,25,23,19,18,17};//hh:mm:ss output pin

const char* ssid       = "Your-SSID";
const char* password   = "Your-Password";

const char* ntpServer1 = "ntp.nict.jp";
const char* ntpServer2 = "time.google.com";
const long  gmtOffset_sec = 9 * 3600;
const int   daylightOffset_sec = 0;

int cnt = 0;
int brightness = 0;
int threshold1 = 30;
int threshold2 = 50;
int value_button_cds = 0;
int value_button_noise = 0;
int value_button_date_time = 0;
int ave_button_cds = 0;
int ave_button_noise = 0;
int ave_button_date_time = 0;

bool cds_flag = false;
bool noise_flag = false;
bool date_time_flag = false;//true -> Display date. false -> Display time.
volatile bool button_cds_flag = false;
volatile bool button_noise_flag = false;
volatile bool button_date_time_flag = false;

ここでは今後使うピン番号や各種変数、フラグなどを宣言しています。Gaussian.hは正規分布に基づく乱数を生成する際に用います。ライブラリマネージャでGaussian by Ivan Seidelを導入することで使用できます。

②表示部

void displayDigit(unsigned short digit, unsigned short pin){
  for(int i = 0; i < 4; i++){
    digitalWrite(PIN_BIT[i], (1 << i) & digit);
  }
  digitalWrite(pin, HIGH);
  delayMicroseconds(20);
  digitalWrite(pin, LOW);
  delayMicroseconds(20); 
}

void printNixiesLocalTime(){
  struct tm timeinfo;
  getLocalTime(&timeinfo);
  if(!getLocalTime(&timeinfo)){
    for(int i = 0; i < 6; i++){
      displayDigit(9, CLK[i]);
    }
    return;
  }
  displayDigit(timeinfo.tm_hour / 10, CLK[0]);
  displayDigit(timeinfo.tm_hour %% 10, CLK[1]);
  displayDigit(timeinfo.tm_min / 10, CLK[2]);
  displayDigit(timeinfo.tm_min %% 10, CLK[3]);
  displayDigit(timeinfo.tm_sec / 10, CLK[4]);
  displayDigit(timeinfo.tm_sec %% 10, CLK[5]);
}

void display_date(){
  struct tm timeinfo;
  getLocalTime(&timeinfo);
  displayDigit((timeinfo.tm_year-100) / 10, CLK[0]);
  displayDigit((timeinfo.tm_year-100) %% 10, CLK[1]);
  displayDigit((timeinfo.tm_mon+1) / 10, CLK[2]);
  displayDigit((timeinfo.tm_mon+1) %% 10, CLK[3]);
  displayDigit(timeinfo.tm_mday / 10, CLK[4]);
  displayDigit(timeinfo.tm_mday %% 10, CLK[5]);
}

ここでは参考にさせて頂いたサイトをほぼ丸パクリした時刻表示をする部分です。また日付表示部を追加しました。timeinfo.tm_yearは1900年からの年数なので普通は+1900しますが、今回は西暦下二桁を表示するので-2000して結果的に-100になります。(2100年代になったら-200にしてもろて)

③機能部1

void gotTouch1(){
  button_cds_flag = true;
}

void gotTouch2(){
  button_noise_flag = true;
}

void gotTouch3(){
  button_date_time_flag = true;
}

void noise(int value){
  Gaussian myGaussian(0.5, 0.3);
  float myRandom = myGaussian.random();
  if(myRandom<0){
    myRandom=0;
  }
  if(myRandom>1){
    myRandom=1;
  }
  ledcWrite(0,int(value*myRandom));
  ledcWrite(1,int(value*myRandom*0.3));
}

int get_cds_value(){
  float value = analogRead(cds)/8;
  value = int(pow(1.0136,value)); 
  return value;
}

gotTouch1~3は3つあるタッチセンサによる割り込みがあったときに呼び出される関数です。タッチがあったらフラグを立てて以後のプログラムで各機能をトグルします。

noise()は正規分布に従って明るさをPWMで調節し、炎の揺らぎを再現する部分です。コロンは明るめなので、0.3倍しています。

get_cds_value()はCdSで取得した明るさでPWM調光する部分です。デューティ比を上げていくと明るさの違いが分かりにくくなります。多分ニキシー管か、人間の視覚の特性で感度がlog曲線のようになっているものと思われます。そのため指数関数で明るい範囲の変化を急激にしています。

1.0136は512乗してギリギリ1024を超えない値です。analogReadは4096段階(12bit)でそれを8で割って512段階にし、PWM出力は1024段階(10bit)であるためこのような値になりました。

④機能部2

void functions(){
  if(cnt %% 50 == 0){
    cnt = 0;
    if(cds_flag){
      brightness = get_cds_value();
    }else{
      brightness = 1024;
    }
    if(noise_flag){
      noise(brightness);
    }else{
      ledcWrite(0,brightness);
      ledcWrite(1,brightness);
    }
  }
  cnt++;

//toggle auto brightness adjustment feature
  if(button_cds_flag){
    for(int i=0; i<5; i++){
      value_button_cds += touchRead(button_cds);
    }
    if(value_button_cds/5 < threshold1){
      cds_flag = !cds_flag;
      //Wait for the finger to leave the touch sensor
      while(ave_button_cds < threshold2){
        for(int i=0; i<5; i++){
          value_button_cds += touchRead(button_cds);
        }
        ave_button_cds = value_button_cds/5;
        value_button_cds = 0;
        delay(1);//Watchdog measures
      }
      ave_button_cds = 0;
    }
    value_button_cds = 0;
    button_cds_flag = false;
  }

//toggle flame noise simurating feature
  if(button_noise_flag){
    for(int i=0; i<5; i++){
      value_button_noise += touchRead(button_noise);
    }
    if(value_button_noise/5 < threshold1){
      noise_flag = !noise_flag;
      while(ave_button_noise < threshold2){
        for(int i=0; i<5; i++){
          value_button_noise += touchRead(button_noise);
        }
        ave_button_noise = value_button_noise/5;
        value_button_noise = 0;
        delay(1);
      }
      ave_button_noise = 0;
    }
    value_button_noise = 0;
    button_noise_flag = false;
  }

//Display date while touching sensor
  if(button_date_time_flag){
    for(int i=0; i<5; i++){
      value_button_date_time += touchRead(button_date_time);
    }
    if(value_button_date_time/5 < threshold1){
      date_time_flag = true;
      display_date();
      while(ave_button_date_time < threshold2){
        for(int i=0; i<5; i++){
          value_button_date_time += touchRead(button_date_time);
        }
        ave_button_date_time = value_button_date_time/5;
        value_button_date_time = 0;
        delay(1);
      }
      ave_button_date_time = 0;
      date_time_flag = false;
    }
    value_button_date_time = 0;
    button_date_time_flag = false;
  }
}

この部分は明るさの管理や各種機能のオンオフをトグルしている部分です。

炎ノイズ生成機能で高速点滅してしまわないように、明るさの調節は50ループに一回のみ行っています。一応オーバーフローしないようにcnt=0を入れています。cds_flagがtrueの時はcdsの値に基づいて明るさbrightnessを調整します。無効の時はbrightness=1024とします。次にnoise_flagがtrueの時は先ほど決定したbrightnessに0~1の正規分布乱数(平均0.5、分散0.3)を掛けて0~1024の値にして、noise()関数内でPWM出力しています。無効の時はbrightnessをそのままPWM出力します。

つまり以下の図のようにCdS調光と炎ノイズシミュレート機能は共存できる形となっています。

その下の無駄に長いコードは、調光・炎ノイズ・日付表示の3つの機能をオンオフする部分です。タッチセンサがチャタリングしてオンオフが何回も行ったり来たりしないようにするためにかなり長くなってしまいました。

  if(button_cds_flag){
    for(int i=0; i<5; i++){
      value_button_cds += touchRead(button_cds);
    }
    if(value_button_cds/5 < threshold1){
      cds_flag = !cds_flag;
      //Wait for the finger to leave the touch sensor
      while(ave_button_cds < threshold2){
        for(int i=0; i<5; i++){
          value_button_cds += touchRead(button_cds);
        }
        ave_button_cds = value_button_cds/5;
        value_button_cds = 0;
        delay(1);//Watchdog measures
      }
      ave_button_cds = 0;
    }
    value_button_cds = 0;
    button_cds_flag = false;
  }

先ほどの割り込みでbutton_cds_flagがtrueになっているとif文の中に入ります。タッチセンサの測定値の5回平均がthreshold1を下回っている場合(指がセンサに触れている場合)はcds_flagがトグルします。

もう一度5回平均(ave_button_cds)の測定を繰り返し、threshold2を上回るまで(指がセンサから離れるまで)whileループします。代入演算子+=を使っていて、ループ毎に値が増え続けてしまうので、5回平均を測定するたびにvalue_button_cdsをリセットしています。delay(1)はウォッチドッグによる再起動を防ぐために入れてあります。最後にbutton_cds_flagを割り込み前の状態falseに戻します。

指がセンサに触れた時と離れた時の判定基準をthreshold1,2と分けることで、ヒステリシス動作のようにして判定を厳しめにしています。意味があるか分かりませんが。

炎ノイズシミュレート機能(149~168行)のほうも全く同じ仕組みです。

日付表示機能(170~192行)は若干仕組みが違っていて、

//略
	date_time_flag = true;
    display_date();
//略
	date_time_flag = false;//button_date_time_flagではないことに注意

タッチ判定があると、date_time_flagがtrueになり、別コアで動いているメインループがwhileループに入って、時刻表示がフリーズします。(237行目)ここでdisplay_date();を呼ぶと68行目の日付表示機能により時刻表示が上書きされます。指がセンサから離れると、date_time_flagがfalseに戻って時刻表示が再開されます。

⑤設定系

void subProcess(void *pvParameters){
  delay(3000);
  while(1){
    functions();
    delay(1);//Watchdog measures
  }
}

void setup() {
  Serial.begin(115200);
  for(int i = 0; i < 6; i++){
    pinMode(CLK[i],OUTPUT);
  }
  for(int i = 0; i < 4; i++){
   pinMode(PIN_BIT[i],OUTPUT);
  }
  pinMode(bright,OUTPUT);
  pinMode(colon,OUTPUT);
  pinMode(cds,INPUT);

  touchAttachInterrupt(button_cds, gotTouch1, threshold1);
  touchAttachInterrupt(button_noise, gotTouch2, threshold1);
  touchAttachInterrupt(button_date_time, gotTouch3, threshold1);

  ledcSetup(0,500,10);
  ledcSetup(1,500,10);
  ledcAttachPin(bright,0);
  ledcAttachPin(colon,1);
  ledcWrite(0,1024);
  ledcWrite(1,1024);
  for(int i = 0; i < 6; i++){
    displayDigit(0, CLK[i]);
  }
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2);
  xTaskCreatePinnedToCore(subProcess, "subProcess", 8192, NULL, 1, NULL, 0);
}

void loop() {
  printNixiesLocalTime();
  while(date_time_flag){
    delay(1);
  }
}

subProcess()はこれまで説明してきた別コアで動いている時刻表示機能以外の機能です。NTP接続までに時間がかかるので一応3秒待ってから動かし始めてます。delay(1)はいつものウォッチドッグ対策です。

setup()でピンの設定、タッチ割り込み設定、PWM設定、NTP設定、マルチコア設定を行っています。PWMのチャネル0はニキシー管数字桁、チャネル1はコロンに割り当てています。

loop()はメインの時刻表示機能です。date_time_flagの状態によって意図的にフリーズさせられます。delay(1)は案の定ウォッチドッグ対策です。

以上がすべてのプログラムになります。全体のプログラムファイル(.inoファイル)は前回の記事の下部のダウンロードリンクから入手できます。「CompletedProgram.ino」というファイル名です。

筐体の製作

筐体はアクリル板とパンチングアルミ板、秋月で買った丸型スペーサーで作りました。表から見える部分は生意気にもトルクスネジを使用しました。

Pカッターで切りました
アルミ板は気合で曲げて、穴が合わないところは拡張しました
タッチセンサはAmazonで買ったステンレス板を電極にしました。

高電圧供給用のXHコネクタは止めて、基板の裏面にケーブルを直付けしました。

高電圧DC-DCの裏面に薄型ヒートシンクを貼りましたがまだ熱々です。Strawberry-Linuxさんが示している定格電流内で使ってるので、壊れることはないでしょう。

以下にAmazonで買ったものを載せておきます。ワッシャーはスペーサーの微妙な高さ調節に使っています。

秋月で大量買いしました
完成写真①(ゴム足を6個取り付け)
完成写真②(DCジャックはL字金具とUVレジンでアクリル板に固定)

動かすとこんな感じです。

終わりに

というわけで、ニキシー管時計製作プリジェクトはここで一区切りとなります。

グダグダやっていたので、構想から完成までに4か月ほどかかってしまいました。(初めの2か月は殆ど何もやってませんが…)

でも設計通り動いてくれてよかったです。

また暇になったらDAC製作でも再開しようかと思っています。

ここまでお付き合いいただきありがとうございました。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です


次の投稿

I2S信号の量子化ビット数をSPLDで判定する

月 8月 14 , 2023
Determination of the quan […]