Skip to content
Engineer's Beauty Blog
Go back

ITV-4 Aviation Clock

Edit page

I recently bought some interesting clock on https://meshok.net/ – ITV-4. It was installed in many USSR-era combat aircraft and display the time on four IV-9 indicators – subminiature vacuum incandescent indicators (numitrons). I have never encountered them before. Essentially, they are 7-segment indicators with glowing elements in the form of tungsten incandescent filaments.

If anyone is interested, here are its main characteristics:

The seller had mentioned beforehand that the condition of the clocks was unknown. And indeed, it turned out that someone had already managed to remove all the components from the boards. Most likely in search of precious metals. For me, this isn’t that important; the main thing is that the lamps are intact and the case is in good condition.

 

I removed all the old boards and got to the lamp leads.

 

Numitron Pinout Diagram

I finally got around to reverse-engineering the numitron contacts. I made a small diagram for convenience.

Numitron-pin-out-ITV-4-clockDownload

UPD: The project is done! It’s working from an Arduino 9V Power Supply. Below is the technical breakdown explaining the modern hardware implementation and the code driving the clock.


🛠️ Modern Hardware Features & Specifications


🔌 Hardware Pinout Configuration

1. Shift Register Interface (Static Driving Bus)

Signal NameArduino PinDescription
DATA_PIND8Serial Data Input (DS)
LATCH_PIND11Storage Register Clock / Latch (ST_CP)
CLOCK_PIND12Shift Register Clock (SH_CP)

2. I2C Real-Time Clock (DS3231)

Signal NameArduino Pin (Uno/Nano)Description
SDAA4Serial Data line
SCLA5Serial Clock line

3. User Interface Buttons (Internal Pull-Up Topology)

Button LabelArduino PinPrimary ActionAdjustment Action
BUTTON_1D5Enter Setup ModeNext Parameter (HH → MM → DD → MM → Save)
BUTTON_2D4Increment Value (Supports Long Press Auto-Repeat)
BUTTON_3D3Decrement Value (Supports Long Press Auto-Repeat)
BUTTON_4D2Trigger Text ScrollImmediate display of string ANDREY D

🗺️ Schematic & Segment Mapping

The code outputs Common Cathode configurations over 4 cascaded bytes via MSBFIRST shift operations. Each byte maps directly to an IV-4 tube segment anode array layout: — A — | | F B | | — G — | | E C | | — D — (DP / Decimal Point)

Bit-to-Segment Mapping Table (Standard 7-Segment Byte)

Bit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0
DPGFEDCBA

⚠️ Hardware Note: Unlike Nixie tubes that require ~170VDC, IV-4 Numitron tubes function using a low-voltage filament (~1.5V AC/DC) and segment anodes (~4.5V to 5V DC). Ensure your PCB power plane safely splits the logic rail ($V_{CC}$) from the tube filaments to prevent overloading the Arduino’s onboard regulator or burning out the filaments.


💻 Firmware Operations & State Machine

The firmware executes a non-blocking architecture using millis() timing wheels to manage states seamlessly.

State Transitions

  1. Normal Display Loop: Cycles through Time (5 seconds) → Date (2 seconds) → Ambient Temperature (2 seconds, offset corrected by -2°C).
  2. Setup Mode: Triggered via Button 1. The active field flashes at 2 Hz (500ms blink phase).
  3. Timeout Guard: If stuck in Setup Mode without interactions for more than 10 seconds, changes are aborted and the clock reverts safely back to the normal Time loop.
  4. Custom Text Scrolling: Pressing Button 4 runs a custom ticker animation showcasing alphanumeric character rendering across the 4 tubes over a 3-second cycle.

Manual Time Synchronization

Upon hardware reset, the controller listens to the hardware Serial port (9600 Baud) for exactly 5 seconds. If a character '1' is received over UART, the RTC updates its memory bank with the compilation timestamp (__DATE__ & __TIME__).


💾 Source Code Implementation

#include <Arduino.h>
#include <Wire.h>
#include "RTClib.h"

// ─── Shift Register Pins ────────────────────────────────────────────────
const uint8_t DATA_PIN  = 8;
const uint8_t CLOCK_PIN = 12;
const uint8_t LATCH_PIN = 11;

// ─── 7-Segment Patterns (Common Cathode) ────────────────────────────────
const uint8_t digitToSegment[10] = {
  0b00111111,  // 0
  0b00000110,  // 1
  0b01011011,  // 2
  0b01001111,  // 3
  0b01100110,  // 4
  0b01101101,  // 5
  0b01111101,  // 6
  0b00000111,  // 7
  0b01111111,  // 8
  0b01101111   // 9
};

const uint8_t minusSegment = 0b01000000;

// ─── Scrolling Text Configuration ─────────────────────────────────────
String scrollingMessage = "ANDREY D";

uint8_t getSegmentPattern(char c) {
  c = toupper(c);
  switch (c) {
    case 'A': return 0b01110111;
    case 'B': return 0b01111100;
    case 'C': return 0b00111001;
    case 'D': return 0b01011110;
    case 'E': return 0b01111001;
    case 'F': return 0b01110001;
    case 'G': return 0b00111101;
    case 'H': return 0b01110110;
    case 'I': return 0b00000110;
    case 'J': return 0b00011110;
    case 'L': return 0b00111000;
    case 'N': return 0b00110111;
    case 'O': return 0b00111111;
    case 'P': return 0b01110011;
    case 'R': return 0b01010000;
    case 'S': return 0b01101101;
    case 'T': return 0b00110001;
    case 'U': return 0b00111110;
    case 'Y': return 0b01101110;
    case '-': return 0b01000000;
    case '0': case '1': case '2': case '3': case '4': 
    case '5': case '6': case '7': case '8': case '9': 
      return digitToSegment[c - '0'];
    default: return 0;
  }
}

// ─── Button Pins ──────────────────────────────────────────────────────
const uint8_t BUTTON_4 = 2;
const uint8_t BUTTON_3 = 3;
const uint8_t BUTTON_2 = 4;
const uint8_t BUTTON_1 = 5;

// ─── Globals ──────────────────────────────────────────────────────────
RTC_DS3231 rtc;
uint8_t segments[4] = {0, 0, 0, 0};

enum DisplayMode { TIME, DATE, TEMPERATURE, ADJ_HH, ADJ_MM, ADJ_DD, ADJ_MON, SCROLLING_TEXT };
DisplayMode currentMode = TIME;
unsigned long lastModeSwitchMillis = 0;

int adjHH, adjMM, adjDD, adjMON, adjYYYY;
bool blinkState = false;
unsigned long lastBlinkMillis = 0;
unsigned long lastAdjInteractionMillis = 0;

void shiftOut4(const uint8_t bytesOut[4]) {
  digitalWrite(LATCH_PIN, LOW);
  for (uint8_t i = 0; i < 4; ++i) shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, bytesOut[i]);
  digitalWrite(LATCH_PIN, HIGH);
}

void updateSegmentsFromTime(uint8_t hour, uint8_t minute) {
  segments[0] = digitToSegment[hour / 10];
  segments[1] = digitToSegment[hour % 10];
  segments[2] = digitToSegment[minute / 10];
  segments[3] = digitToSegment[minute % 10];
}

void updateSegmentsFromDate(uint8_t month, uint8_t day) {
  segments[0] = digitToSegment[month / 10];
  segments[1] = digitToSegment[month % 10];
  segments[2] = digitToSegment[day / 10];
  segments[3] = digitToSegment[day % 10];
}

void updateSegmentsFromTemperature(float tempC) {
  int t = (int)tempC;
  if (t < -9) t = -9;
  if (t > 99) t = 99;
  if (t < 0) {
    segments[0] = minusSegment;
    segments[1] = digitToSegment[-t / 10];
    segments[2] = digitToSegment[-t % 10];
  } else if (t <= 9) {
    segments[0] = 0; segments[1] = 0; segments[2] = digitToSegment[t];
  } else {
    segments[0] = 0; segments[1] = digitToSegment[t / 10]; segments[2] = digitToSegment[t % 10];
  }
  segments[3] = 0b01100011;
}

void setup() {
  pinMode(DATA_PIN, OUTPUT);
  pinMode(CLOCK_PIN, OUTPUT);
  pinMode(LATCH_PIN, OUTPUT);
  pinMode(BUTTON_1, INPUT_PULLUP);
  pinMode(BUTTON_2, INPUT_PULLUP);
  pinMode(BUTTON_3, INPUT_PULLUP);
  pinMode(BUTTON_4, INPUT_PULLUP);
  Serial.begin(9600);
  delay(2000);
  if (!rtc.begin()) { while (1); }
  unsigned long entryTime = millis();
  bool setTimeManually = false;
  while (millis() - entryTime < 5000) {
    if (Serial.available() && Serial.read() == '1') { setTimeManually = true; break; }
  }
  if (setTimeManually) rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  lastModeSwitchMillis = millis();
}

void incrementValue() {
  if (currentMode == ADJ_HH) adjHH = (adjHH + 1) % 24;
  else if (currentMode == ADJ_MM) adjMM = (adjMM + 1) % 60;
  else if (currentMode == ADJ_DD) adjDD = (adjDD % 31) + 1;
  else if (currentMode == ADJ_MON) adjMON = (adjMON % 12) + 1;
}

void decrementValue() {
  if (currentMode == ADJ_HH) adjHH = (adjHH + 23) % 24;
  else if (currentMode == ADJ_MM) adjMM = (adjMM + 59) % 60;
  else if (currentMode == ADJ_DD) adjDD = (adjDD == 1) ? 31 : adjDD - 1;
  else if (currentMode == ADJ_MON) adjMON = (adjMON == 1) ? 12 : adjMON - 1;
}

void loop() {
  unsigned long nowMillis = millis();
  static bool lastBtn1 = HIGH, lastBtn2 = HIGH, lastBtn3 = HIGH, lastBtn4 = HIGH;
  static unsigned long btn2DownMillis = 0, btn3DownMillis = 0;
  static unsigned long lastRepeatMillis = 0;

  bool btn1 = digitalRead(BUTTON_1), btn2 = digitalRead(BUTTON_2), btn3 = digitalRead(BUTTON_3), btn4 = digitalRead(BUTTON_4);

  // Button 4: Scroll
  if (btn4 == LOW && lastBtn4 == HIGH) {
    if (currentMode != SCROLLING_TEXT) { currentMode = SCROLLING_TEXT; lastModeSwitchMillis = nowMillis; }
    delay(200);
  }
  lastBtn4 = btn4;

  // Button 1: Adjust Step
  if (btn1 == LOW && lastBtn1 == HIGH) {
    lastAdjInteractionMillis = nowMillis;
    if (currentMode < ADJ_HH || currentMode == SCROLLING_TEXT) {
      DateTime now = rtc.now();
      adjHH = now.hour(); adjMM = now.minute(); adjDD = now.day(); adjMON = now.month(); adjYYYY = now.year();
      currentMode = ADJ_HH;
    } else if (currentMode == ADJ_HH) currentMode = ADJ_MM;
    else if (currentMode == ADJ_MM) currentMode = ADJ_DD;
    else if (currentMode == ADJ_DD) currentMode = ADJ_MON;
    else if (currentMode == ADJ_MON) { rtc.adjust(DateTime(adjYYYY, adjMON, adjDD, adjHH, adjMM, 0)); currentMode = TIME; lastModeSwitchMillis = nowMillis; }
    delay(200);
  }
  lastBtn1 = btn1;

  // Button 2: Increment + Long Press
  if (currentMode >= ADJ_HH && currentMode <= ADJ_MON) {
    if (btn2 == LOW) {
      if (lastBtn2 == HIGH) { // Initial press
        incrementValue();
        lastAdjInteractionMillis = nowMillis;
        btn2DownMillis = nowMillis;
        lastRepeatMillis = nowMillis;
      } else if (nowMillis - btn2DownMillis > 500) { // Long press auto-repeat
        if (nowMillis - lastRepeatMillis > 150) {
          incrementValue();
          lastAdjInteractionMillis = nowMillis;
          lastRepeatMillis = nowMillis;
        }
      }
    }
    
    // Button 3: Decrement + Long Press
    if (btn3 == LOW) {
      if (lastBtn3 == HIGH) { // Initial press
        decrementValue();
        lastAdjInteractionMillis = nowMillis;
        btn3DownMillis = nowMillis;
        lastRepeatMillis = nowMillis;
      } else if (nowMillis - btn3DownMillis > 500) { // Long press auto-repeat
        if (nowMillis - lastRepeatMillis > 150) {
          decrementValue();
          lastAdjInteractionMillis = nowMillis;
          lastRepeatMillis = nowMillis;
        }
      }
    }
  }
  lastBtn2 = btn2;
  lastBtn3 = btn3;

  if (currentMode >= ADJ_HH && currentMode <= ADJ_MON && (nowMillis - lastAdjInteractionMillis >= 10000)) {
    currentMode = TIME; lastModeSwitchMillis = nowMillis;
  }

  if (nowMillis - lastBlinkMillis >= 500) { blinkState = !blinkState; lastBlinkMillis = nowMillis; }

  switch (currentMode) {
    case TIME:
      if (nowMillis - lastModeSwitchMillis >= 5000) { currentMode = DATE; lastModeSwitchMillis = nowMillis; }
      { DateTime now = rtc.now(); updateSegmentsFromTime(now.hour(), now.minute()); }
      break;
    case DATE:
      if (nowMillis - lastModeSwitchMillis >= 2000) { currentMode = TEMPERATURE; lastModeSwitchMillis = nowMillis; }
      { DateTime now = rtc.now(); updateSegmentsFromDate(now.month(), now.day()); }
      break;
    case TEMPERATURE:
      if (nowMillis - lastModeSwitchMillis >= 2000) { currentMode = TIME; lastModeSwitchMillis = nowMillis; }
      { float tempC = rtc.getTemperature()-2; updateSegmentsFromTemperature(tempC); }
      break;
    case ADJ_HH: updateSegmentsFromTime(adjHH, adjMM); if (blinkState) { segments[0] = 0; segments[1] = 0; } break;
    case ADJ_MM: updateSegmentsFromTime(adjHH, adjMM); if (blinkState) { segments[2] = 0; segments[3] = 0; } break;
    case ADJ_DD: updateSegmentsFromDate(adjMON, adjDD); if (blinkState) { segments[2] = 0; segments[3] = 0; } break;
    case ADJ_MON: updateSegmentsFromDate(adjMON, adjDD); if (blinkState) { segments[0] = 0; segments[1] = 0; } break;
    case SCROLLING_TEXT:
    {
      String displayStr = "    " + scrollingMessage + "    ";
      int fullLen = displayStr.length();
      unsigned long elapsed = nowMillis - lastModeSwitchMillis;
      if (elapsed >= 3000) { currentMode = TIME; lastModeSwitchMillis = nowMillis; }
      else {
        int offset = (elapsed * (fullLen - 3)) / 3000;
        if (offset > fullLen - 4) offset = fullLen - 4;
        for (int i=0; i<4; i++) segments[i] = getSegmentPattern(displayStr[offset + i]);
      }
    }
    break;
  }
  shiftOut4(segments);
  delay(100);
}

Edit page
Share this post:

Previous Post
Amplifier
Next Post
Clocks