// ============================================================================ // 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 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