Files
nixie-clock/firmware/uniclock/ESP32_UniClock2/nixie595.ino
T
mangavalkandClaude Opus 5.5 9a7061383c Add Home Assistant controls over MQTT
Upgrade ArduinoHA from 1.3.0 to 2.1.0 and rewrite mqtt.ino for it.
The clock now shows up in Home Assistant (discovery prefix
"homeassistant") with:
- switches Tubes (blank the display, runtime only) and Auto dim
- numbers Day/Night brightness
- light Backlight (on/off, brightness, RGB mapped to the colour wheel)
- select LED effect, button Cathode protect
- sensors Light (LDR) and WiFi signal, plus the upstream sensors when
  the clock has them; master-clock subscriptions are kept

Settings changed from HA are saved to EEPROM 3 s after the last change.
Fixes the unbounded memcpy in the MQTT message handler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:37:25 +01:00

155 lines
5.5 KiB
Arduino

// ============================================================================
// 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;
// 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
if ((brightness == 0) || (state == 3) || (!radarON) || (!tubesOn)) {
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);
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