Nixie Clock Arduino Sketch

Definitions and Variables

/* NIXIECounter.ino This is the wood clock with 4 classic end-view NIXIE tubes.

    It was first made in the 1970's with 3 surplus counters made with many transistors, plus a home-made fourth.

    The original timing source was a tuning fork.

    The second added a large external metal box containing a XT-cut crystal oscillator in a thermos flask with

    a divider to 1 minute with many IC stages and an RF link to the clock.

    The third varsion ditched the external box and used a 32768 Hz crystal, a 14-stage binary counter and a Picaxe.

    This fourth retains only the NIXIE tubes abd the 2 original minutes counter boards.

    Two new boards were made to match the physical layout and pinout of the originals using CD4017 counters (NOT 74HC4017!).

    The new timing source is the temprature-compensated DS3234. It uses a Leonardo micro-controller.

    Software challenges were due to using incremental encoders on a MCP23008 I2C port expander.

    The NIXIEs are showing theur age and needed twice the current. Therefor they timeout and relight from a doppler radar.

  John Saunders 9/18/2023

*/

#include <SPI.h>

#include <Wire.h>

#include <SparkFunDS3234RTC.h>

#include <Adafruit_MCP23008.h>

#include <EEPROM.h>

// ------------------ leonardo Pin Allocations -----------------------

#define minsCtr 9

#define hoursCtr 10

#define minsReset 13

#define hoursReset 12

#define blankIn  11

#define hvEn  A0

#define Cntl_INT 7              // Interrupt for the switch port MCP23008 (optional, not used)

#define DS3234_CS_PIN A1       // DeadOn RTC Chip-select pin

#define SQW_PIN 8              // DeadOn RTC SQW/interrupt pin (optional, not used)

#define PULSE_PORT 4           // For the transmitter prepulse

#define PHOTOCELL_PORT A2

#define SW_RST 5

#define KNOB_RST 6


// ------------------ Switch Port Pin Allocations -----------------------

#define SW_PORT_ADDR 0x02

#define SW_RED_LED 2

#define SW_BLUE_LED 4

#define SW_GREEN_LED 1

#define SW_DATE 3

#define SW_PGM 5

#define SW_OFF 0


// ------------------ Knob Port Pin Allocations -----------------------

#define KNOB_PORT_ADDR 0x03

#define LEFT_PB 7

#define RIGHT_PB 2

#define RADAR 1

#define LEFT_MASK 0x60

#define RIGHT_MASK 0x18


enum encPos  : uint8_t  { left = 0, right = 1 };

enum modePos : uint8_t  { invalid = 0, tools = 1, monDate = 2, present = 3, stbySecs = 4, wkdayYr = 5 };

enum subPos  : uint8_t  { dark = 0, green = 1, blue = 2, red = 3 };

const uint8_t encMask[2] = { LEFT_MASK, RIGHT_MASK};

const uint8_t encLeftShft[2] = { 1, 3 };                 //Puts into bits 6 & 7

const uint8_t encLimit[2] = { 99, 59 };


// ------------------ Object instantiations -----------------------

// The RTC object is in the library

Adafruit_MCP23008 swp;                                  //Switch Port MCP23008

Adafruit_MCP23008 knp;                                  //Knob Port MCP23008


// ------------------ Global variables -----------------------

modePos swMode;

char swChar = 'I';                                     //For transmitting

modePos adjMode;

char pbChar = 'D';                                     //For transmitting

subPos pbMode;

char pwrChar;                                         //For transmitting

uint8_t encCtr[2];

uint8_t dispCtr[2];

uint8_t stbyMinLmt, stbySecLmt;                       // minutes and seconds, get from EEPROM

bool encFlag[2];

bool adjFlag[2];

bool txFlag;                                          //For transmitting

int pcCnt;

Functions

// ------------------ Utility Functions -----------------------


void showLed(subPos v) {

  swp.digitalWrite(SW_RED_LED, HIGH);

  swp.digitalWrite(SW_BLUE_LED, HIGH);

  swp.digitalWrite(SW_GREEN_LED, HIGH);

  switch (v)  {

    case dark:

      break;

    case red :

      swp.digitalWrite(SW_RED_LED, LOW);

      break;

    case blue:

      swp.digitalWrite(SW_BLUE_LED, LOW);

      break;

    case green:

      swp.digitalWrite(SW_GREEN_LED, LOW);

      break;

  }

}


// ------------------ Encoder Functions -----------------------

/* None of the encoder library functions could be used without hacking because of the MCP23008.

   This solution is different. It uses a byte as a software shift register.

   For each input shange the contents are shifted right 2, and bits 6 & 7 replaced by the new encoder reading.

   The byte can be represented as a hex number. As the knob is rotated this number repeats each 4 readings.

   The numbers are unique and are different for the other direction. One per direction is picked to change the counter.

*/

void encTick(encPos pos) {

  static uint8_t encSr[2] = {0, 0};

  static  uint8_t  prevGpioVal[2] = {0, 0};

  uint8_t gpioVal[2];

  gpioVal[pos] = knp.readGPIO() & encMask[pos];

  if (gpioVal[pos] == prevGpioVal[pos] ) return;

  prevGpioVal[pos] = gpioVal[pos];

  gpioVal[pos] = gpioVal[pos] << encLeftShft[pos];;

  encSr[pos] = encSr[pos] >> 2;

  encSr[pos] |= gpioVal[pos];


  if (encSr[pos] == 0xE1) {

    encCtr[pos]++;

    if (encCtr[pos] > encLimit[pos]) {

      encCtr[pos] = 0;

    }

    encFlag[pos] = true;

    adjFlag[pos] = true;

  }

  if (encSr[pos] == 0xD2) {

    if (encCtr[pos] > 0) {

      encCtr[pos]--;

    }

    else {

      encCtr[pos] = encLimit[pos];

    }

    encFlag[pos] = true;

    adjFlag[pos] = true;

  }

}


// ------------------Adjustment -----------------------

void storeLeft(void) {

  switch (adjMode) {

    case present:

      rtc.setHour(encCtr[left]);

      break;

    case monDate:

      rtc.setMonth(encCtr[left]);

      break;

    case stbySecs:

      EEPROM.write(1, encCtr[left]);

      stbyMinLmt = encCtr[left];


      break;

    case wkdayYr:

      rtc.setDay(encCtr[left]);

    case tools:

    default:

      ;

      break;

  }

}


void storeRight(void) {

  switch (adjMode) {

    case present:

      rtc.setMinute(encCtr[right]);

      break;

    case monDate:

      rtc.setDate(encCtr[right]);

      break;

    case stbySecs:

      EEPROM.write(0, encCtr[right]);

      stbySecLmt = encCtr[right];

      break;

    case wkdayYr:

      rtc.setYear(encCtr[right]);

      break;

    case tools:

    default:

      ;

      break;

  }

}


// ------------------Transmit Functions -----------------------


char charBuffer[47] = {"14L1776x,F,?, "};


byte storeVal(unsigned int txData, int loc, int pos) {

  int rem = txData / 10000;

  if (pos > 4) {

    charBuffer[loc++] = rem + '0';

  }

  rem = txData % 10000;

  if (pos > 3) {

    charBuffer[loc++] = rem / 1000 + '0';

  }

  txData = rem % 1000;

  if (pos > 2) {

    charBuffer[loc++] = txData / 100 + '0';

  }

  rem = txData % 100;

  charBuffer[loc++] = rem / 10 + '0';

  charBuffer[loc++] = rem % 10 + '0';

  charBuffer[loc++] = ',';

  return loc;

}


void transmit() {

  byte cksum = 0;

  byte ckhex;

  int pcDec;

  int nextLoc;

  int len;

  charBuffer[11] = pwrChar;

  charBuffer[12] = ',';

  charBuffer[13] = pbChar;

  charBuffer[14] = ',';

  charBuffer[15] = swChar;

  charBuffer[16] = ',';

  nextLoc = storeVal(dispCtr[left], 17, 2);

  nextLoc = storeVal( dispCtr[right], nextLoc, 2);

  pcDec = 1023 - pcCnt;

  len = storeVal(pcDec, nextLoc, 3);

  charBuffer[9] = '<' + len - 12;

  for (int i = 11; i < (len - 1); i++) {

    cksum += charBuffer[i];

  }

  ckhex = cksum / 16;

  if (ckhex < 10) {

    charBuffer[len++] = ckhex + '0';

  }

  else {

    charBuffer[len++] = ckhex + '7';

  }

  ckhex = cksum & 0x0F;

  if (ckhex < 10) {

    charBuffer[len++] = ckhex + '0';

  }

  else {

    charBuffer[len++] = ckhex + '7';

  }

  charBuffer[len++] = 0x0d;

  charBuffer[len++] = 0x0a;

  charBuffer[len++] = 0;

  digitalWrite(PULSE_PORT, HIGH);

  delay(20);

  digitalWrite(PULSE_PORT, LOW);

  delay(10);

  Serial1.write(charBuffer, len);

  //Serial.print("Photolell Count = ");

  //Serial.println(pcDec);

}


// ------------------ NIXIE Functions -----------------------

/*The 4 counters are orgnized as separate /100 and /60 pairs. They each display a byte..

   They appear to be a readout but actually they are reset and then count up to the desired value.

   As the count is very fast and the NIXIEs are turned off while counting the change seems immediate.

   Actually the blank is common because of legacy high voltage considerations.

*/

void dispHrsCtr(uint8_t hrsVal) {

  digitalWrite(blankIn, LOW);

  digitalWrite(hoursReset, HIGH);

  delayMicroseconds(100);

  digitalWrite(hoursReset, LOW);

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

    digitalWrite(hoursCtr, LOW);

    delayMicroseconds(80);

    digitalWrite(hoursCtr, HIGH);

    delayMicroseconds(80);

  }

  digitalWrite(blankIn, HIGH);

}


void dispMinsCtr(uint8_t minsVal) {

  digitalWrite(blankIn, LOW);

  digitalWrite(minsReset, HIGH);

  delayMicroseconds(100);

  digitalWrite(minsReset, LOW);

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

    digitalWrite(minsCtr, HIGH);

    delayMicroseconds(100);

    digitalWrite(minsCtr, LOW);

    delayMicroseconds(100);

  }

  digitalWrite(blankIn, HIGH);

}


void showData(void) {               //Used only for the center and right positions of the main mode switch.

  switch (swMode) {

    case present:

      dispCtr[left] = rtc.hour();

      dispCtr[right] = rtc.minute();

      break;

    case monDate:

      dispCtr[left] = rtc.month();

      dispCtr[right] = rtc.date();

      break;

    case stbySecs:

      dispCtr[left] =  stbyMinLmt;

      dispCtr[right] = stbySecLmt;

      break;

    case wkdayYr:

      dispCtr[left] = rtc.day();

      dispCtr[right] = rtc.year();

      break;

    case tools:

      ;

      break;

    default:

      dispCtr[left] = 0;

      dispCtr[right] = 0;

      break;

  }

  if (swMode != tools) {

    dispMinsCtr(dispCtr[right]);

    dispHrsCtr(dispCtr[left]);

  }

}

// ------------------ Control Functions -----------------------

/* There are 12 control options but some do the same. They are derived from a three-way switch and the left pushbutton

  which cycles through 4 states which are indetified bt a three-color LED.

  The result is 4 displays and the ability to adjust the RTC and the standby timout value, which is stored in EEPROM.

*/

void getSwMode(void) {

  uint8_t tmp;

  tmp = swp.digitalRead(SW_DATE);

  tmp += 2 * swp.digitalRead(SW_PGM);

  if (pbMode == green) {

    tmp += 3;

  }

  switch (tmp) {

    case 1:

    case 4:

      swMode = tools;

      swChar = 'T';

      break;

    case 2:

      swMode = monDate;

      swChar = 'M';

      break;

    case 3:

      swMode = present;

      swChar = 'P';

      break;

    case 5:

      swMode = wkdayYr;

      swChar = 'W';

      break;

    case 6:

      swMode = stbySecs;

      swChar = 'S';

      break;

    default:

      swMode = invalid;

      swChar = 'I';

      break;

  }

}

bool getpbwMode(void) {

  static uint8_t prevLeftPb;

  uint8_t gpioVal;

  bool retVal = false;

  gpioVal = knp.digitalRead(LEFT_PB);

  if ((gpioVal == 0) && (prevLeftPb == 1)) {

    switch (pbMode) {

      case dark:

        pbMode = green;

        pbChar = 'G';

        break;

      case green:

        pbMode = blue;

        pbChar = 'B';

        break;

      case blue:

        pbMode = red;

        pbChar = 'R';

        break;

      case red:

        pbMode = dark;

        pbChar = 'D';

    }

    retVal = true;

  }

  prevLeftPb = gpioVal;

  return retVal;

}

Setup and Loop


int  stbyCount;


void setup() {

  Serial.begin(9600);

  while (!Serial) {

    if (millis() > 1000) break; // wait for serial port to connect. Needed for native USB port only

  }

  pinMode(1, OUTPUT);

  digitalWrite(1, HIGH);

  pinMode(SQW_PIN, INPUT);

  pinMode(DS3234_CS_PIN, OUTPUT);

  rtc.writeSQW(SQW_SQUARE_1);

  pinMode(PULSE_PORT, OUTPUT);

  digitalWrite(PULSE_PORT, LOW);

  pinMode(minsCtr, OUTPUT);

  digitalWrite(minsCtr, LOW);

  pinMode(hoursCtr, OUTPUT);

  digitalWrite(hoursCtr, HIGH);

  pinMode(minsReset, OUTPUT);

  digitalWrite(minsReset, LOW);

  pinMode(hoursReset, OUTPUT);

  digitalWrite(hoursReset, LOW);

  pinMode(hvEn, OUTPUT);

  digitalWrite(hvEn, LOW);

  pinMode(blankIn, OUTPUT);

  digitalWrite(blankIn, HIGH);

  pinMode(Cntl_INT, INPUT_PULLUP);

  pinMode(SW_RST, OUTPUT);

  pinMode(KNOB_RST, OUTPUT);

  digitalWrite(SW_RST, LOW);

  digitalWrite(KNOB_RST, LOW);

  delay(1);

  digitalWrite(SW_RST, HIGH);

  digitalWrite(KNOB_RST, HIGH);

  rtc.begin(DS3234_CS_PIN);

  // rtc.setTime(0, 25, 12, 4, 16, 8, 23);  // 9:2 wed aug 16, 2023

  Serial.println("RTC initialization done.");

  Serial1.begin(2400);

  swp.begin(SW_PORT_ADDR);

  knp.begin(KNOB_PORT_ADDR);

  swp.pinMode(SW_RED_LED, OUTPUT);

  swp.pinMode(SW_BLUE_LED, OUTPUT);

  swp.pinMode(SW_GREEN_LED, OUTPUT);

  swp.digitalWrite(SW_RED_LED, HIGH);

  swp.digitalWrite(SW_BLUE_LED, HIGH);

  swp.digitalWrite(SW_GREEN_LED, HIGH);

  stbyCount = 0;

  pbMode = dark;

  swMode = present;

  adjMode = invalid;

  encCtr[left] = 0;

  encCtr[right] = 0;

  encFlag[left] = false;

  encFlag[right] = false;

  pwrChar = '1';

  txFlag = false;

  stbyMinLmt = EEPROM.read(1);                     //Standby count limit minutes

  stbySecLmt =  EEPROM.read(0);                    //Standby count limit seconds

}


void loop() {

  static uint8_t prevSec, gpioVal;

  static modePos prevSwMode;

  static uint8_t prevRightPb;

  int stbyLmt;

  static bool modeFlag = false;

  static bool subFlag = false;

  static bool goFlag = false;

  rtc.update();                                    //Do not use in a function!

  // ------------------Encoders ------------------======-----

  if ((pbMode == blue) && (swMode == tools)) {

    encTick(left);

    encTick(right);

  }


  // ------------------ Standby On-Off-----------------------

  if (knp.digitalRead(RADAR) == 1) {

    stbyCount = 0;

    if (swp.digitalRead(SW_OFF) == 1) {           //Right hand switch

      digitalWrite(hvEn, LOW);                    //Turns on 200V DC-DC converter.

      pwrChar = '1';

    }

  }

  if (swp.digitalRead(SW_OFF) == 0) {

    digitalWrite(hvEn, HIGH);                     //Turns off 200V DC-DC converter.

    if (pwrChar == '1') {

      txFlag = true;

    }

    pwrChar = '0';

    stbyCount = 0;

  }

  // ------------------Main Mode ------------------=======-----

  getSwMode();

  if (swMode != prevSwMode) {

    modeFlag = true;

    prevSwMode = swMode;

    txFlag = true;

  }

  // ------------------Sub-mode --------------------==========---

  subFlag = getpbwMode();

  // ------------------Action Button ---------------=====--------

  gpioVal = knp.digitalRead(RIGHT_PB);

  if ((gpioVal == 0) && (prevRightPb == 1)) {

    goFlag = true;

    txFlag = true;

  }

  prevRightPb = gpioVal;

  // -----------------per-second operations -----------------------

  if (rtc.second() != prevSec)  {

    // ------------------Encoder Display -------------------===----

    if (swMode == tools) {

      if (encFlag[left]) {

        dispHrsCtr(encCtr[left]);

        encFlag[left] = false;

      }

      if (encFlag[right]) {

        dispMinsCtr(encCtr[right]);

        encFlag[right] = false;

      }

    }

    // ------------------Auto Standby -------------=========----------

    stbyLmt = 60 * stbyMinLmt + stbySecLmt;

    stbyCount++;

    if (stbyCount > stbyLmt) {

      digitalWrite(hvEn, HIGH);                       //Turns off 200V DC-DC converter.

      stbyCount = 0;

      pwrChar = '0';

    }

    if (stbyCount == 10) {

      txFlag = true;

    }

    // ------------------LED Display -----------------------------------

    showLed(pbMode);

    // ------------------Encoder initial display -----------------------

    if ((swMode != invalid) && (swMode != tools) && (pbMode != blue) && (pbMode != red) && goFlag) {

      goFlag = false;

      encCtr[left] = dispCtr[left];

      encCtr[right] = dispCtr[right];

      adjMode = swMode;

    }

    if ((swMode == tools) && modeFlag) {

      dispMinsCtr(encCtr[right]);

      dispHrsCtr(encCtr[left]);

      modeFlag = false;

    }

    // ------------------Adjustment ------------------------------------

    if ((swMode == tools)  && (pbMode == red) && goFlag) {

      if (adjFlag[left] == true) {

        storeLeft();

        adjFlag[left] = false;

      }

      if (adjFlag[right] == true) {

        storeRight();

        adjFlag[right] = false;

      }

      goFlag = false;

    }

    // -----------------Transmit -------------------------------------

    if (txFlag) {

      transmit();

      txFlag = false;

    }

    // ------------------per-minute operations -----------------------

    if ((prevSec == 0) || modeFlag || subFlag) {

      showData();

      modeFlag = false;

      subFlag = false;

      // printTime();

      // -----------------Photoelectric dimming -----------------------

      pcCnt = analogRead(PHOTOCELL_PORT);

    }

    prevSec = rtc.second();

  }

}