Files
nixie-clock/firmware/uniclock/ESP32_UniClock2/nixie595.ino
T

165 lines
6.1 KiB
Arduino
Raw Normal View History

2026-10-25 21:23:10 +01:00
// ============================================================================
// Nixie driver: two cascaded 74HC595s on hardware SPI, multiplexed anodes
// ----------------------------------------------------------------------------
// Hardware: RetroModding NixieIN12v2 (CLOCK_90). The two 595s hold one 16-bit
// word that drives the whole display:
// bits 0..9 cathodes for digits 0..9 (via 2x ULN2003, shared by all tubes)
// bits 11..14 tube anodes (anodeBits[] in clocks.h), one tube at a time
// bits 10, 15 neon dots P1/P2 (NEON_BITS in clocks.h)
// The high byte is shifted out first (ends up in the second 595), MSB first.
// DATA and CLOCK are the HSPI MOSI/SCK pins (GPIO13/GPIO14), PIN_LATCH is the
// 595 storage clock. Pinout and bit map come from the stock firmware, see
// docs/stock-firmware-pins.md in the nixie-clock repo.
//
// The multiplex ISR writes the SPI1 and GPIO registers directly instead of
// calling the SPI library, so it runs entirely from IRAM and keeps refreshing
// while the flash cache is off (EEPROM/SPIFFS writes, OTA).
//
// Platform: ESP8266 ONLY (timer1 + SPI1 registers). The ISR state machine
// (brightness by blanking time, animMask wipe) follows multiplex74141.ino.
// ============================================================================
#ifdef NIXIE595_MULTIPLEX
#include <SPI.h>
char tubeDriver[] = "NIXIE595_MULTIPLEX";
#if !defined(ESP8266)
#error "NIXIE595_MULTIPLEX is for ESP8266 only!"
#endif
int maxDigits = sizeof(anodeBits); //anodeBits is byte[], so sizeof == element count
int maxDig = maxDigits;
// timer1 runs at TIM_DIV16 = 5 MHz (0.2 us per tick): 11000 ticks = 2.2 ms per tube slot,
// about 110 Hz for 4 tubes. PWM_min/PWM_max are the lit part of a slot (brightness).
// const, so the ISR's static locals are constant-initialized (no flash-resident init guard).
const int PWMrefresh = 11000;
const int PWM_min = 1000;
const int PWM_max = 10000;
2026-10-25 22:18:39 +01:00
volatile boolean displayBlank = true; //true while the ISR outputs an all-off word (gap between tubes)
// Wait (at most one tube slot) until the tubes are dark. Used before work that blocks interrupts for a
// few hundred microseconds (the bit-banged WS2812 update): if the ISR is delayed in the dark gap, only the
// gap gets longer. Delayed while a tube is lit, that tube would stay on longer and flash brighter.
void displayWaitBlank() {
uint32_t started = micros();
while (!displayBlank && ((micros() - started) < 3000)) {}
}
2026-10-25 21:23:10 +01:00
// Shift one 16-bit word into both 595s and latch it. Also called from main code via clearTubes(),
// so the caller must keep the ISR out (see clearTubes()).
static inline void IRAM_ATTR write595(uint16_t w) {
while (SPI1CMD & SPIBUSY) {}
SPI1W0 = (w >> 8) | ((w & 0xFF) << 8); //SPI1W0 byte 0 goes out first: high byte first
SPI1CMD |= SPIBUSY;
while (SPI1CMD & SPIBUSY) {}
GPOS = 1 << PIN_LATCH; //rising edge copies the shift registers to the outputs
__asm__ __volatile__("nop; nop; nop; nop;");
GPOC = 1 << PIN_LATCH;
}
void setup_pins() {
DPRINTLN("Nixie clock - setup pins - 74HC595 multiplex mode...");
pinMode(PIN_LATCH, OUTPUT);
digitalWrite(PIN_LATCH, LOW);
regPin(PIN_LATCH, "PIN_LATCH");
SPI.begin(); //HSPI: GPIO14 = SCK, GPIO13 = MOSI. It also claims GPIO12 (MISO), which the NeoPixel setup takes back later.
SPI.setFrequency(SPI_FREQ);
SPI.setBitOrder(MSBFIRST);
SPI.setDataMode(SPI_MODE0);
regPin(13, "PIN_DATA (HSPI MOSI)");
regPin(14, "PIN_CLK (HSPI SCK)");
//16-bit transfers, so one write fills both 595s
SPI1U1 = (SPI1U1 & ~((SPIMMOSI << SPILMOSI) | (SPIMMISO << SPILMISO)))
| (15 << SPILMOSI) | (15 << SPILMISO);
write595(0); //all anodes, cathodes and dots off
DPRINT("Tubes: "); DPRINTLN(maxDigits);
startTimer();
}
void IRAM_ATTR writeDisplay() { //timer1 ISR, single-shot: must re-arm timer1 at the end of every call
static byte pos = 0;
static byte state = 0;
static int timer = PWMrefresh;
static byte num;
static int brightness;
static int PWMtimeBrightness = PWM_min;
uint16_t w;
if (EEPROMsaving) { //keep the tubes dark while settings are written
write595(0);
timer1_write(PWMrefresh);
return;
}
intCounter++;
brightness = displayON ? prm.dayBright : prm.nightBright;
if (brightness > MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
timer = PWMrefresh;
// Same state machine as multiplex74141.ino:
// state 0: next tube; show the old digit (animation) or the current digit
// state 1: show the new digit for the rest of the lit time (only while animating)
// state 2: blank for the rest of the slot (this sets the brightness)
// state 3: blank output, then back to state 0
switch (state) {
case 0:
pos++;
if (pos > maxDig - 1) {
pos = 0;
if (autoBrightness && displayON)
PWMtimeBrightness = max(PWM_min, PWM_max * lx / MAXIMUM_LUX);
else
PWMtimeBrightness = max(PWM_min, PWM_max * brightness / MAXBRIGHTNESS);
}
if (animMask[pos] > 0) {
num = oldDigit[pos];
timer = (PWMtimeBrightness * (20 - animMask[pos])) / 20;
state = 1;
}
else {
num = digit[pos];
timer = PWMtimeBrightness;
state = 2;
}
break;
case 1:
num = newDigit[pos];
timer = (PWMtimeBrightness * animMask[pos]) / 20;
state = 2;
break;
case 2:
timer = PWMrefresh - PWMtimeBrightness;
state = 3;
break;
}
if (timer < 500) timer = 500; //a too short timer1 reload can miss the interrupt
2026-10-25 21:37:25 +01:00
if ((brightness == 0) || (state == 3) || (!radarON) || (!tubesOn)) {
2026-10-25 21:23:10 +01:00
state = 0;
w = 0;
}
else {
w = 0;
if (num <= 9) w |= 1 << num; //cathode; 10 = blank, other glyphs are not available on a Nixie
if ((pos == NEON_POS) && colonBlinkState) w |= NEON_BITS;
if (w) w |= 1 << anodeBits[pos];
}
write595(w);
2026-10-25 22:18:39 +01:00
displayBlank = (w == 0);
2026-10-25 21:23:10 +01:00
timer1_write(timer);
}
void clearTubes() {
noInterrupts(); //the ISR also writes the 595s
write595(0);
interrupts();
}
// Not needed: the ISR refreshes the display continuously (same as multiplex74141.ino).
void writeDisplaySingle() {}
#endif