ESP-01でNIXIE管時計

#はじめに
ESP-01はESP8266を搭載したWiFiモジュールを使用してニキシー管時計を作成しました。

R3 : ループのdelay()でハングしwatchdogでリセットする現象を修正しました。delay()の関数を直書きしただけで動作が変る理由がわかりませんが、とりあえずリセットされず動作しています。シリアルでデバッグするように回路図も変更してます。2020/03/29

R4 : どうしてもwatchdogリセットの発生を抑止できません。タイマー割り込みとなにかがバッティングしてハングアップしてそうなのですが解明できませんでした。タイマー止めました。今は安定してます。2020/04/19


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/2c9a0ae5-21e9-be66-38e0-49dcc594fb74.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/54f982d3-0ba7-205d-d4cf-01a59f5b7c1b.png)

当初はATMEGA328をニキシー管駆動、NTPサーバと同期させたESP-01をRTCにするつもりでした。
しかしながらESP-01のESP8266は80MHzで動作し1MBのフラッシュメモリ64KBのRAMとATMEGA328よりパワフルであるため、IOの少なさを解消できればESP-01のみで実現できるかと思いたち作成してみました。

#I/Oの拡張
I/Oと書きましたが、拡張するのは出力のみです。シフトレジスタ74HC595を使用します。使用方法はぐぐっていただくとして、Arduionでの使い方は非常に簡単でshiftOut()関数をコールするのみです。shiftOut()はwaitを含んでいませんので非常に高速(nsレベル)で出力が可能です。ニキシー管をダイナミック制御するには十分な速度を得ることができます。

#必要なもの
##ESP-01
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/1f05f769-cc0d-cff1-4234-75cf7c80e454.png)
WiFiモジュールとしてAliでUS$1程度で売られているものです。**技適を取得していないため日本国内では違法になる可能性あることに注意してください。**

##170V電源
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/9b406edd-1aee-fe17-3c4d-aa8cdc534455.png)
AliでUS$10位だったかと思います。オリジナルの470uHのインダクタは発熱焼損したため330uHに置き換えて使用しています。

##ニキシー管
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/c11ab15a-bfaf-c764-2446-75f89bdbaff5.png)
安価なロシア製のIN-12Aを使用しました。Aliでは異常に高いのでebayで購入しました。新品6個でUS$25位です。同時にソケットも購入しました。

##TLP627
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/29ed4dd2-f569-ff90-73f4-98d0a49dee96.png)
ダイナミック点灯でニキシー管選択にしようします。アノードに電圧を加える用途であるため、300V耐圧のTLP627を選択しています。

##74HC595
ロジックICです。手持ちの関係でCMOS品ですが、TTLの74LS595でも動作するかと思います。
後続の74141を駆動するために5Vで使用しています。

##74141(K155ID1)
74141は高いのでロシア互換品のK155ID1を使用しています。ピンコンパチです。Aliに74141は安価でありますが、全て偽物であると思ったほうが良いです(2/2で偽物でした)。K155ID1をebayでロシアorウクライナから購入したほうが安全です。

##その他
その他抵抗(330Ωx、4.7Kx3、33k)、5Vレギュレータ、3.3Vレギュレータ、コンデンサ、ソケット、配線材、アルミケース、スイッチ、DCソケット、ネジなどが必要となります。

#回路
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/b87e5cae-eed5-084d-5422-e07af00654a0.png)
R3 : デバッグするため使用していない~OEを活用するため、RXとTXを入れ替え。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211703/2d9cdc3c-6107-322a-4e09-e580b8c399d3.png)

ESP-01の出力4本で74HC595を制御し、下位4bitをカソード制御(74141への出力)、上位4bitをニキシー管選択の制御に使用しています。
当初OE端子を使用して74HC595の出力をOFFにしてダイナミック点灯のデッドタイムの制御を行うつもりでしたが、上手く動作しないため廃止にしました。最終的に74HC595のOE端子は使用していませんのでGNDに落しても構いません。配線を変更してTX出力を残した方がデバックに便利です。OE端子をOFFにする代りに0x00を出力することで解決しました。

メモ 2021/10/02
~SRCLRがGNDに落ちているのは間違い。正しくはVCCに接続
HT7333のINとOUTのピン番号が反対

#プログラム
NTP同期、74HC595制御が簡単なArduinoで作成しました。プログラムはUTC+8の地域向けになっていますので適宜修正してください。
プログラム構成としてはメインループで時刻を定期的に出力バッファに設定、点灯制御はタイマー割り込みを使用しています。割り込み一回に一桁ずつ出力しています。割り込みが発生したら都度次の割り込みを設定していますので、割り込み時間を変更するにはその時間を変更してください。
割り込み中にdelay()を使用すると割り込みが正常に動作しなくなります(delayは同じタイマーを使用している?未確認)ので、ゴリゴリにクロックを数えてwait関数と同様にしています。

setup()ではWiFiに接続しNTPへ同期のセットアップしています。WiFi.bigin()の後の処理はWiFi接続完了までにニキシー管を駆動するための処理です。この処理を割り込みで制御しようとしましたがうまく動作しなかったためごりごり記述しています。不要ならばdelay(500)で置き換えても問題ありません。ただし起動してるのかどうか不安になるかもしれません。

configTime()は非同期(NTP接続が完了しなくてもreturnが帰る)なので、loop()中でNTP接続を待っています。

loop()ではNTP接続が完了を待ち、時間の更新に専念します。表示は割り込みで行います。
最初のセンテンスはNTPの同期を待つ間、ニキシー管を表示をクルクルさせています。NTP接続待ちを示すのとニキシー管表示にトラブルが無いか確認も出来ます。

R3 : デバッグにてdelay()にてハングアップしウォッチドッグによりリセットが発生することが分った。delay()の関数をループ内に直書きすると問題ないのだが理由は不明。

```Arduino:NIXIE
////////////////////////////////////////////////////////
//
// ESP_01 & 74HC595 NIXIE DRIVER
//
// 2020/02/27
//
// 2020/02/28 R1 : 割り込みのタイミングを変更してみる(割り込みの絶対値は気にする必要がないので)
// 2020/03/21 R2 : ちらつき改善
// 2020/03/29 R3 : Watchdogリセット改良
////////////////////////////////////////////////////////
//
// shifOut
//
// 0x0000     : DIGIT SELECT
// 0x    0000 : TUBE SELECT
////////////////////////////////////////////////////////
// Timer ref : https://lipoyang.hatenablog.com/entry/20161205/p1
//
////////////////////////////////////////////////////////


#include <ESP8266WiFi.h>  // for WIFI
#include <time.h>     // for time(), localtime()
#include <string.h> // memset

// Wi-Fi設定
#define WIFI_SSID   "WIFI_SSID"
#define WIFI_PASSWORD "WIFI_PASSWORD"

// NTP設定
#define TIMEZONE_SGT  (3600 * 8)  // 日本標準時は、UTC（世界標準時）よりも９時間進んでいる。
#define DAYLIGHTOFFSET_SGT  (0)   // 日本は、サマータイム（夏時間）はない。
#define NTP_SERVER1   "ntp.nict.jpntp.nict.jp"  // NTPサーバー
#define NTP_SERVER2   "ntp.jst.mfeed.ad.jp"   // NTPサーバー

// ミリ秒をクロック数に換算 (@80MHz)
#define MSEC2CLOCK(ms)    (ms * 80000L)
#define USEC2CLOCK(ms)    (ms * 80L)  // Micro Sec ver


#define IO0   0
#define TX    1
#define IO2   2
#define RX    3

//////////////////////////////////////////////////////////
// connection
//
// IO0    --    SER (14)
// TX     --    ST_CP (12)
// IO2    --    SH_CP (11)
// RX     ==    ~OE(13)   // 使用しないことにした
//
// ~MR    ==    VCC
///////////////////////////////////////////////////////////

//RXとTXを入れ替えしてデバッグする
#define OE_Pin TX    //Pin connected to ST_CP of 74HC595
#define CLOCK_Pin IO2   //Pin connected to SH_CP of 74HC595
#define DATA_Pin  IO0   //Pin connected to DS of 74HC595
#define LATCH_Pin    RX    //Pin connected to ~OE of 74HC595

volatile  uint16_t count = 0;
volatile  uint8_t DispData[4];
volatile  uint8_t DispPos;
volatile  uint8_t OutDataBuf;


int8_t NTPReady ;

typedef struct st_time {
  uint8_t hour;
  uint8_t min;
  uint8_t sec;
} ST_TIME;

ST_TIME current_time;
volatile int nLED_STS = 0;

/////////////////////////////////////
// Handler for timer interrupt
void timer0_ISR (void) {
  //r1  timer0_write(ESP.getCycleCount() + MSEC2CLOCK(5) ); // 5msec
  //  nLED_STS = (nLED_STS) ? 0 : 1; // toggle
  //  digitalWrite( LED_BUILTIN, nLED_STS);

  DispPos++;
  if (DispPos >= 4) DispPos = 0;

  dataOut( (OutDataBuf & 0xF0) );  // TUBE SELECTのみOFFする
  //これだめだった  dataOut( DispData[DispPos] << 4 );  // 次のデータをセットしておく
  // digitalWrite(OE_Pin, 1);  // DISP OFF
  // Digit gap, delay, delayMicrosecnodは使えない
  unsigned long sleep = ESP.getCycleCount() + USEC2CLOCK(2000); // 1500 -> 2000 R2
  while ( ESP.getCycleCount() < sleep);

  OutDataBuf = 0x01 << DispPos; // Digit select (LOW)
  OutDataBuf |= DispData[DispPos] << 4; // Number select (HIGH)
  dataOut( OutDataBuf );  // ON

  timer0_write(ESP.getCycleCount() + USEC2CLOCK(1500) ); //r1  // つぎは1.5msecで割り込み呼出

  // Digit gap, delay, delayMicrosecnodは使えない
  //  unsigned long sleep = ESP.getCycleCount() + USEC2CLOCK(2000);
  //  while( ESP.getCycleCount() < sleep);
  //  digitalWrite(OE_Pin, 0);  // DISP ON
}

void setup() {
  //set pins to output so you can control the shift register
  Serial.begin(115200);
  Serial.println("ON");


  pinMode(LATCH_Pin, OUTPUT);
  pinMode(CLOCK_Pin, OUTPUT);
  pinMode(DATA_Pin, OUTPUT);
  //  pinMode(OE_Pin, OUTPUT);

  Serial.println("Start");

  // 電源をONしたことをしめす表示
  uint8_t shiftpos = 0;
  uint8_t countup = 0;
  dataOut( 0x08 >> shiftpos );  // ...0を表示

  // Wi-Fiセットアップ
  // WiFiネットワーク接続
  int i = 0;
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED)
  {
    shiftpos ++;
    if ( shiftpos >= 4)
    {
      shiftpos = 0;
      countup ++;
      if (countup > 9 )  countup = 0;
    }
    dataOut( (0x08 >> shiftpos) | (countup << 4) );  // ...0を表示
    delay(500);
  }

  Serial.println("Wifi Ready");
  // output buffer clear
  memset( (void*)DispData, 0x01, sizeof(DispData));
  dataOut( 0x00 );  // clear
  digitalWrite( OE_Pin, LOW );  // output enable

  // タイマ割り込みの設定
  noInterrupts();
  timer0_isr_init();
  timer0_attachInterrupt(timer0_ISR);
  timer0_write(ESP.getCycleCount() + MSEC2CLOCK(50) ); // 5msec
  interrupts();


  // NTP同期
  configTime( TIMEZONE_SGT, DAYLIGHTOFFSET_SGT, NTP_SERVER1, NTP_SERVER2 );

}

void dataOut(byte data)
{
  //ground latchPin and hold low for as long as you are transmitting
  digitalWrite(LATCH_Pin, 0);
  //move 'em out
  shiftOut(DATA_Pin, CLOCK_Pin, MSBFIRST, data);
  //return the latch pin high to signal chip that it
  //no longer needs to listen for information
  digitalWrite(LATCH_Pin, 1);

}

void loop()
{
  // 同期完了するまで待つ
  int i;
  i = 0;
  time_t timeNow = time(NULL);
  while ( ((timeNow / 100) * 100) == TIMEZONE_SGT )
  {
    timeNow = time(NULL);
    //delay(1000);
    for (uint8_t loop = 0 ; loop < 10; loop++) // delay(1000)の代りに数字を回す
    {
      memset((void*)DispData, loop, sizeof(uint8_t) * 4);
      delay(10);
    }
    i++;
    if (i > 30) // 偶然起動タイミングが8:00だった場合は、強制的にぬける
      break;
  }
  Serial.println("NTP Ready");

  char message[16];

  while (1)
  {
    Serial.print("1");

    if (WiFi.status() == WL_CONNECTED) //Check WiFi connection status
    {
      Serial.print("2");
      // 現在時刻の取得
      time_t timeNow = time(NULL);
      struct tm* tmNow = localtime(&timeNow);
      Serial.print("3");

      current_time.hour = tmNow->tm_hour;
      current_time.min = tmNow->tm_min;
      current_time.sec = tmNow->tm_sec;

      SetTime();
      Serial.print("4");

      // R3 delete      delay(1000);
      // R3 delayの代りに下記を使用する。yield()で割り込みを有効にする
      //unsigned long sleep = millis() + 1000;
      //while ( millis() < sleep)
      //{
      //  yield();
      //}
      uint32_t ms = 1000;
      uint32_t start = micros();

      while (ms > 0) {
        yield();
        if( start > micros() )  // overflowしたら抜ける
        {
          Serial.println("******************************************");
          Serial.println("******************************************");
          Serial.println("******************************************");
          Serial.println("******************************************");
          break;
        }
        while ( ms > 0 && (micros() - start) >= 1000) {
          ms--;
          start += 1000;
        }
      }



      Serial.print("5");
      //ESP.wdtFeed();  // R3
      Serial.print("6");
    }
    else // attempt to connect to wifi again if disconnected
    {
      Serial.println("WIFI_OFF___________WIFI_OFF");
      WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
      delay(1000);
    }
    Serial.println("7");
    sprintf(message, "%lu", micros() );
    Serial.println(message);
    sprintf(message, "%02d:%02d:%02d", current_time.hour, current_time.min, current_time.sec);
    Serial.println(message);

  }
}


void waitNTPReady()
{
  // 同期完了するまで待つ
  int i;
  i = 0;
  time_t timeNow = time(NULL);
  while ( ((timeNow / 100) * 100) == TIMEZONE_SGT )
  {
    timeNow = time(NULL);
    //delay(1000);
    for (uint8_t loop = 0 ; loop < 10; loop++) // delay(1000)の代りに数字を回す
    {
      memset((void*)DispData, loop, sizeof(uint8_t) * 4);
      delay(100);
    }
    i++;
    if (i > 30) // 偶然起動タイミングが8:00だった場合は、強制的にぬける
      break;
  }
}
void SetTime()
{
  DispData[0] = 10 - current_time.hour / 10;
  DispData[1] = 10 - current_time.hour % 10;
  DispData[2] = 10 - current_time.min / 10;
  DispData[3] = 10 - current_time.min % 10;
  if (DispData[0] == 10)DispData[0] = 0;
  if (DispData[1] == 10)DispData[1] = 0;
  if (DispData[2] == 10)DispData[2] = 0;
  if (DispData[3] == 10)DispData[3] = 0;
}
```

### プログラムR4 タイマー廃止,安定動作

```Arduino:NIXIE_R4

////////////////////////////////////////////////////////
//
// ESP_01 & 74HC595 NIXIE DRIVER
//
// 2020/02/27
//
// 2020/02/28 R1 : 割り込みのタイミングを変更してみる(割り込みの絶対値は気にする必要がないので)
// 2020/03/21 R2 : ちらつき改善
// 2020/03/29 R3 : Watchdogリセット改良
// 2020/04/17 R4 : タイマー割り込み取り止め watchdog発生原因がさっぱりわからん

////////////////////////////////////////////////////////
//
// shifOut
//
// 0x0000     : DIGIT SELECT
// 0x    0000 : TUBE SELECT
////////////////////////////////////////////////////////
// Timer ref : https://lipoyang.hatenablog.com/entry/20161205/p1
//
////////////////////////////////////////////////////////


#include <ESP8266WiFi.h>  // for WIFI
#include <time.h>     // for time(), localtime()
#include <string.h> // memset

// Wi-Fi設定
#define WIFI_SSID   "WIFI_SSID"
#define WIFI_PASSWORD "WIFI_PASSWORD"

// NTP設定
#define TIMEZONE_SGT  (3600 * 8)  // 日本標準時は、UTC（世界標準時）よりも９時間進んでいる。
#define DAYLIGHTOFFSET_SGT  (0)   // 日本は、サマータイム（夏時間）はない。
#define NTP_SERVER1   "ntp.nict.jpntp.nict.jp"  // NTPサーバー
#define NTP_SERVER2   "ntp.jst.mfeed.ad.jp"   // NTPサーバー

// ミリ秒をクロック数に換算 (@80MHz)
#define MSEC2CLOCK(ms)    (ms * 80000L)
#define USEC2CLOCK(ms)    (ms * 80L)  // Micro Sec ver


#define IO0   0
#define TX    1
#define IO2   2
#define RX    3

//////////////////////////////////////////////////////////
// connection
//
// IO0    --    SER (14)
// TX     --    ST_CP (12)
// IO2    --    SH_CP (11)
// RX     ==    ~OE(13)   // 使用しないことにした
//
// ~MR    ==    VCC
///////////////////////////////////////////////////////////

//RXとTXを入れ替えしてデバッグする
#define OE_Pin TX    //Pin connected to ST_CP of 74HC595
#define CLOCK_Pin IO2   //Pin connected to SH_CP of 74HC595
#define DATA_Pin  IO0   //Pin connected to DS of 74HC595
#define LATCH_Pin    RX    //Pin connected to ~OE of 74HC595

volatile  uint16_t count = 0;
volatile  uint8_t DispData[4];
volatile  uint8_t DispPos;
volatile  uint8_t OutDataBuf;


int8_t NTPReady ;

typedef struct st_time {
  uint8_t hour;
  uint8_t min;
  uint8_t sec;
} ST_TIME;

ST_TIME current_time;
volatile int nLED_STS = 0;

void setup() {
  //set pins to output so you can control the shift register
  Serial.begin(115200);
  Serial.println("ON");

  //  WiFi.printDiag(Serial);

  pinMode(LATCH_Pin, OUTPUT);
  pinMode(CLOCK_Pin, OUTPUT);
  pinMode(DATA_Pin, OUTPUT);
  //  pinMode(OE_Pin, OUTPUT);

  Serial.println("Start");

  // 電源をONしたことをしめす表示
  uint8_t shiftpos = 0;
  uint8_t countup = 0;
  dataOut( 0x08 >> shiftpos );  // ...0を表示

  // Wi-Fiセットアップ
  // WiFiネットワーク接続
  int i = 0;
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED)
  {
    shiftpos ++;
    if ( shiftpos >= 4)
    {
      shiftpos = 0;
      countup ++;
      if (countup > 9 )  countup = 0;
    }
    dataOut( (0x08 >> shiftpos) | (countup << 4) );  // ...0を表示
    delay(500);
  }

  Serial.println("Wifi Ready");
  // output buffer clear
  memset( (void*)DispData, 0x01, sizeof(DispData));
  dataOut( 0x00 );  // clear
  digitalWrite( OE_Pin, LOW );  // output enable


  // NTP同期
  configTime( TIMEZONE_SGT, DAYLIGHTOFFSET_SGT, NTP_SERVER1, NTP_SERVER2 );

}

void dataOut(byte data)
{
  //ground latchPin and hold low for as long as you are transmitting
  digitalWrite(LATCH_Pin, 0);
  //move 'em out
  shiftOut(DATA_Pin, CLOCK_Pin, MSBFIRST, data);
  //return the latch pin high to signal chip that it
  //no longer needs to listen for information
  digitalWrite(LATCH_Pin, 1);

}

void loop()
{
  int debug_count = 0;
  // 同期完了するまで待つ
  int i;
  i = 0;
  time_t timeNow = time(NULL);
  while ( ((timeNow / 100) * 100) == TIMEZONE_SGT )
  {
    timeNow = time(NULL);
    //delay(1000);
    for (uint8_t loop = 0 ; loop < 10; loop++) // delay(1000)の代りに数字を回す
    {
      memset((void*)DispData, loop, sizeof(uint8_t) * 4);
      delay(10);
    }
    i++;
    if (i > 30) // 偶然起動タイミングが8:00だった場合は、強制的にぬける
      break;
  }
  Serial.println("NTP Ready");

  char message[16];

  while (1)
  {

    if (WiFi.status() == WL_CONNECTED) //Check WiFi connection status
    {
      // 現在時刻の取得
      time_t timeNow = time(NULL);
      struct tm* tmNow = localtime(&timeNow);

      current_time.hour = tmNow->tm_hour;
      current_time.min = tmNow->tm_min;
      current_time.sec = tmNow->tm_sec;

      SetTime();
      Disp_Nixie();
    }
    else // attempt to connect to wifi again if disconnected
    {
      Serial.println("WIFI_OFF___________WIFI_OFF");
      WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
      delay(1000);
    }
  }
}

void Disp_Nixie(void) {
  DispPos++;
  if (DispPos >= 4) DispPos = 0;

  dataOut( (OutDataBuf & 0xF0) );  // TUBE SELECTのみOFFする

  delayMicroseconds(2000);

  OutDataBuf = 0x01 << DispPos; // Digit select (LOW)
  OutDataBuf |= DispData[DispPos] << 4; // Number select (HIGH)
  dataOut( OutDataBuf );  // ON

  //delayMicroseconds(1500);
  delay(1);   // micro delayを2回使うとsoftwdtが発生。何故?
}


void waitNTPReady()
{
  // 同期完了するまで待つ
  int i;
  i = 0;
  time_t timeNow = time(NULL);
  while ( ((timeNow / 100) * 100) == TIMEZONE_SGT )
  {
    timeNow = time(NULL);
    //delay(1000);
    for (uint8_t loop = 0 ; loop < 10; loop++) // delay(1000)の代りに数字を回す
    {
      memset((void*)DispData, loop, sizeof(uint8_t) * 4);
      delay(100);
    }
    i++;
    if (i > 30) // 偶然起動タイミングが8:00だった場合は、強制的にぬける
      break;
  }
}
void SetTime()
{
  DispData[0] = 10 - current_time.hour / 10;
  DispData[1] = 10 - current_time.hour % 10;
  DispData[2] = 10 - current_time.min / 10;
  DispData[3] = 10 - current_time.min % 10;
  if (DispData[0] == 10)DispData[0] = 0;
  if (DispData[1] == 10)DispData[1] = 0;
  if (DispData[2] == 10)DispData[2] = 0;
  if (DispData[3] == 10)DispData[3] = 0;

}

```