Files
mangavalkandClaude Opus 5.5 5fbae6247c Fix tube flicker and review fixes
- The bit-banged WS2812 update disables interrupts (about 30 us per
  pixel), which made the lit tube stay on longer and flash. neoShow()
  now only sends when the pixels changed (fixed colour was resent every
  second) and waits for the dark gap between tubes; the bit-bang strip
  has no headroom pixels. This also fixed digits that seemed to hang.
- 4 WS2812 on this board, one per tube (not 8)
- Saving the firmware URL in the web UI no longer starts an HTTP
  update; never update from an empty URL
- Restart once after the first switch from AP mode to WiFi, so
  ArduinoOTA and MQTT start on the real network
- Optional web UI password (WEB_USER/WEB_PASSWORD in secrets.h);
  /getConfiguration returns the stored WiFi and MQTT passwords
- Cathode protect: random mode now starts at a random tube (the +-1
  start digits were swapped and then overwritten)
- Apply the unused gamma table to wheel colours
- LOOP_DIAG (off by default): main-loop stalls in /getCurrentInfos,
  which now also reports uptime and free heap

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

166 lines
6.1 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;
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)) {}
}
// 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);
displayBlank = (w == 0);
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