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
- Display: 4x IV-4 (ИТВ-4) Soviet Numitron Tubes (filament-based 7-segment displays).
- Timekeeping: DS3231 I2C RTC (extremely low drift with built-in temperature compensation).
- Driver Topology: 4-stage cascaded Shift Registers (
74HC595or equivalent) delivering a dedicated 32-bit parallel bus (Static Drive, no flickering). - Interface: 4 tactile navigation buttons with auto-repeat (long-press) acceleration.
- UI Features: Automatic rolling display (Time → Date → Temperature), dynamic alphanumeric scrolling text, and real-time blinking adjustment indicators.
🔌 Hardware Pinout Configuration
1. Shift Register Interface (Static Driving Bus)
| Signal Name | Arduino Pin | Description |
|---|---|---|
| DATA_PIN | D8 | Serial Data Input (DS) |
| LATCH_PIN | D11 | Storage Register Clock / Latch (ST_CP) |
| CLOCK_PIN | D12 | Shift Register Clock (SH_CP) |
2. I2C Real-Time Clock (DS3231)
| Signal Name | Arduino Pin (Uno/Nano) | Description |
|---|---|---|
| SDA | A4 | Serial Data line |
| SCL | A5 | Serial Clock line |
3. User Interface Buttons (Internal Pull-Up Topology)
| Button Label | Arduino Pin | Primary Action | Adjustment Action |
|---|---|---|---|
| BUTTON_1 | D5 | Enter Setup Mode | Next Parameter (HH → MM → DD → MM → Save) |
| BUTTON_2 | D4 | — | Increment Value (Supports Long Press Auto-Repeat) |
| BUTTON_3 | D3 | — | Decrement Value (Supports Long Press Auto-Repeat) |
| BUTTON_4 | D2 | Trigger Text Scroll | Immediate 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 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
|---|---|---|---|---|---|---|---|
| DP | G | F | E | D | C | B | A |
⚠️ 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
- Normal Display Loop: Cycles through Time (5 seconds) → Date (2 seconds) → Ambient Temperature (2 seconds, offset corrected by -2°C).
- Setup Mode: Triggered via Button 1. The active field flashes at 2 Hz (500ms blink phase).
- 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.
- 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);
}