Adds CLOCK_90 and the nixie595.ino driver: two 74HC595s on hardware SPI (GPIO13/14, latch GPIO15), multiplexed from an IRAM-only timer1 ISR. Neon dots on P1; P2 is the decimal-point line on this board. Also: - NeoPixel on any ESP8266 pin via bit-bang (GPIO12), and take the pin back after SPI.begin() claimed it as MISO - ArduinoOTA (USE_ARDUINO_OTA), with an OTA upload env in platformio.ini - DST: EU end date (last Sunday of October) instead of the first Sunday of November, and recompute the time from the rules when DST is toggled instead of shifting it by 1 h - forward declarations so PlatformIO's .ino conversion compiles - FIRMWARE_SERVER can be overridden; empty for CLOCK_90 Verified on the clock: tubes, order, dots, LEDs, LDR, NTP time with DST. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
155 lines
5.5 KiB
Arduino
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)) {
|
|
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
|