Run ESP_UniClock on the NixieIN12v2 clock

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>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-25 21:23:10 +01:00
1 parent b347f1c63f
commit 0d3e878051
7 files changed
+301 -31

No files matched your search

+27 -21
View File
@@ -6,8 +6,9 @@ with ArduinoOTA and Home Assistant (MQTT) support.
## The device ## The device
- On the network at **10.1.1.97** (WiFi `dlink-iot`). Stock web UI on port 80, nothing else - On the network at **10.1.1.97** (WiFi `dlink-iot`). Since 2026-10-25 it runs ESP_UniClock
open: no OTA, no API, no MQTT. (`firmware/uniclock/`, CLOCK_90): web UI on port 80 (JSON at `/getConfiguration`,
`/getCurrentInfos`), ArduinoOTA on UDP 8266 (hostname `nixie-in12`, no password yet).
- Board: RetroModding (Konstantin Repnikov) "NixieIN12v2 HV2.0", stock firmware **SV1.3** - Board: RetroModding (Konstantin Repnikov) "NixieIN12v2 HV2.0", stock firmware **SV1.3**
(©2020). ESP-12 (ESP8266, MAC 30:83:98:99:BF:DE, Espressif). (©2020). ESP-12 (ESP8266, MAC 30:83:98:99:BF:DE, Espressif).
- **Powered by USB-C 5 V only** (no DC jack). Most likely the maker's "v2.0L lightweight - **Powered by USB-C 5 V only** (no DC jack). Most likely the maker's "v2.0L lightweight
@@ -34,28 +35,29 @@ with ArduinoOTA and Home Assistant (MQTT) support.
## Pinout ## Pinout
From the 12 V v2 schematic (`docs/schematic_v2_12V.jpg`), checked against the stock firmware From the 12 V v2 schematic (`docs/schematic_v2_12V.jpg`), checked against the stock firmware
by disassembly (`docs/stock-firmware-pins.md`, tools in `tools/`). "Binary" = confirmed in by disassembly (`docs/stock-firmware-pins.md`, tools in `tools/`). "Hardware" = also works
the stock SV1.3 code; it has not yet been tested on the hardware. on the clock with UniClock; "Binary" = only confirmed in the stock SV1.3 code.
| Function | GPIO | Source | | Function | GPIO | Source |
|---|---|---| |---|---|---|
| 74HC595 DATA / CLOCK (OE tied to GND) | 13 / 14 = HSPI MOSI / SCK (hardware SPI, MSB first) | Binary | | 74HC595 DATA / CLOCK (OE tied to GND) | 13 / 14 = HSPI MOSI / SCK (hardware SPI, MSB first) | Hardware |
| 74HC595 LATCH | 15, plain `digitalWrite` (has 1k pull-down) | Binary | | 74HC595 LATCH | 15, plain `digitalWrite` (has 1k pull-down) | Hardware |
| WS2812 data | 12 (NeoPixel bit-bang, GRB 800 kHz; stock assumes 12 LEDs, 8 fitted) | Binary | | WS2812 data | 12 (NeoPixel bit-bang, GRB 800 kHz; 8 fitted, stock assumes 12). `SPI.begin()` turns GPIO12 into MISO: set it back to OUTPUT afterwards | Hardware |
| Photoresistor (LDR, 10k to GND) | ADC (A0) | Binary | | Photoresistor (LDR, 10k to GND) | ADC (A0) | Hardware |
| BTN1 / BTN2 (not fitted) | 0 / 2, INPUT_PULLUP. Stock: GPIO0 is a button, GPIO2 a motion/presence input that wakes the display | Binary | | BTN1 / BTN2 (not fitted) | 0 / 2, INPUT_PULLUP. Stock: GPIO0 is a button, GPIO2 a motion/presence input that wakes the display | Binary |
| DFPlayer Mini (stock feature) | UART0: GPIO1 TX / GPIO3 RX, 9600 Bd | Binary | | DFPlayer Mini (stock feature) | UART0: GPIO1 TX / GPIO3 RX, 9600 Bd | Binary |
| DS3231M RTC SDA / SCL | 4 / 5. **Stock firmware has no I2C code** (NTP only); if fitted, unknown | Schematic only | | DS3231M RTC SDA / SCL | 4 / 5: **no I2C device answers** (UniClock scan), so no RTC on this board; NTP only | Hardware |
Two chained 74HC595s make one 16-bit word, all bits active-high (confirmed from the binary; Two chained 74HC595s make one 16-bit word, all bits active-high (confirmed from the binary;
the high byte is sent first and goes to U2): the high byte is sent first and goes to U2):
- U1 QA..QH = cathodes 0..7 (bits 0..7), U2 QA = cathode 8 (bit 8), QB = cathode 9 (bit 9) - U1 QA..QH = cathodes 0..7 (bits 0..7), U2 QA = cathode 8 (bit 8), QB = cathode 9 (bit 9)
(via 2x ULN2003, shared by all tubes) (via 2x ULN2003, shared by all tubes)
- U2 QC = P1 (neon dot 1, bit 10), QD..QG = anodes A1..A4 (bits 11..14, MMBTA42/92), QH = P2 - U2 QC = P1 (bit 10) drives **both** IN-3 neon dots. QD..QG = anodes A1..A4 (bits 11..14,
(bit 15; neon dot 2, and the decimal point if jumper JP2 is closed) MMBTA42/92). QH = P2 (bit 15) drives the IN-12B **decimal points** on this board (JP2 closed),
not a neon.
- The display is multiplexed: one anode on at a time. **A1 = leftmost tube (hour tens), - The display is multiplexed: one anode on at a time. **A1 = leftmost tube (hour tens),
A4 = rightmost (minute ones)**, from the stock digit order. Stock lights P1/P2 in only one A4 = rightmost (minute ones)** (verified on the tubes). The dots are lit in the minute-tens
multiplex slot. slot only, as on stock.
## Programming header ## Programming header
@@ -65,7 +67,12 @@ Wiring: GND-GND, adapter TXD -> clock RXD, adapter RXD -> clock TXD, adapter 3.3
connected (the clock is powered from its own USB-C). No auto-reset: to enter flash mode, hold connected (the clock is powered from its own USB-C). No auto-reset: to enter flash mode, hold
GPIO0 to GND, tap RST to GND, then release GPIO0. GPIO0 to GND, tap RST to GND, then release GPIO0.
- esptool is installed for the user (`python -m esptool`, v5.6.0). - esptool is installed for the user (`python -m esptool`, v5.6.0). After `--after no-reset`
the stub keeps running at the new baud rate and the next esptool call can't connect: do
everything in one call, or re-enter flash mode.
- Normal updates go over WiFi: `pio run -e nixie_in12_ota -t upload` in `firmware/uniclock/`.
ArduinoOTA does not answer after the clock switched from AP mode to WiFi without a reboot
(first setup); a power cycle fixes it.
- `firmware/stock/` holds the stock flash dump. **Never overwrite or delete it**; it is the - `firmware/stock/` holds the stock flash dump. **Never overwrite or delete it**; it is the
only way back to SV1.3 (`write-flash 0 <dump>`). Dumps are gitignored because the flash only way back to SV1.3 (`write-flash 0 <dump>`). Dumps are gitignored because the flash
holds the WiFi password; never commit them. holds the WiFi password; never commit them.
@@ -79,10 +86,9 @@ GPIO0 to GND, tap RST to GND, then release GPIO0.
## Plan ## Plan
1. Dump and inspect the stock firmware (version strings, pins if possible). 1. Done: dump and inspect the stock firmware.
2. Write a test sketch that steps through the 16 shift-register bits, to verify the pinout 2. Skipped (the disassembly was enough): bit-stepping test sketch.
and the tube order. 3. Done: 74HC595-multiplex driver (`nixie595.ino`) + CLOCK_90 in ESP_UniClock, ArduinoOTA,
3. Add a 74HC595-multiplex driver + CLOCK config to ESP_UniClock, plus ArduinoOTA and EU DST fix. Runs on the clock.
Home Assistant controls (display on/off, brightness, LEDs); its stock MQTT only 4. Next: Home Assistant controls over MQTT (display on/off, brightness, LEDs; upstream MQTT
publishes sensors. only publishes sensors), OTA password via `secrets.h`.
4. Work continues in VS Code (PlatformIO).
@@ -44,6 +44,11 @@
#include <stddef.h> #include <stddef.h>
//#define byte uint8_t //#define byte uint8_t
#include "clocks.h" //DEFINE YOUR CLOCKS SETUP IN THIS FILE!!! - clocks.h picks the single active CLOCK_NN block, which #defines the driver macro, pins and feature flags read throughout this file #include "clocks.h" //DEFINE YOUR CLOCKS SETUP IN THIS FILE!!! - clocks.h picks the single active CLOCK_NN block, which #defines the driver macro, pins and feature flags read throughout this file
// PlatformIO turns the .ino files into one .cpp and puts prototypes for every function near the top,
// without evaluating #ifdefs. Declare the types of optional libraries here so those prototypes compile
// when the feature (and its #include) is switched off.
class AsyncWiFiManager;
struct RgbColor;
// Detect which generation of the ESP8266 Arduino core is in use: older cores (2.6.x/2.7.x) name the // Detect which generation of the ESP8266 Arduino core is in use: older cores (2.6.x/2.7.x) name the
// RAM-resident-ISR attribute ICACHE_RAM_ATTR, while newer cores renamed it to IRAM_ATTR (shared with // RAM-resident-ISR attribute ICACHE_RAM_ATTR, while newer cores renamed it to IRAM_ATTR (shared with
@@ -161,7 +166,9 @@ unsigned long intCounter = 0; //for testing only, interrupt counter
byte c_MinBrightness = RGB_MIN_BRIGHTNESS; //minimum LED brightness byte c_MinBrightness = RGB_MIN_BRIGHTNESS; //minimum LED brightness
byte c_MaxBrightness = RGB_MAX_BRIGHTNESS; //maximum LED brightness byte c_MaxBrightness = RGB_MAX_BRIGHTNESS; //maximum LED brightness
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
#ifndef FIRMWARE_SERVER
#define FIRMWARE_SERVER "http://c.landventure.hu/store" //default OTA update server (see doFirmwareUpdate()); overridable per-clock via prm.firmwareServer / the web admin "firmware" field #define FIRMWARE_SERVER "http://c.landventure.hu/store" //default OTA update server (see doFirmwareUpdate()); overridable per-clock via prm.firmwareServer / the web admin "firmware" field
#endif
String usedPinsStr; //human-readable list of registered GPIOs, built by listPins(), shown on the web admin "clock details" page String usedPinsStr; //human-readable list of registered GPIOs, built by listPins(), shown on the web admin "clock details" page
String driverSetupStr; //driver-specific extra setup info string (set by some drivers), also shown on the web admin page String driverSetupStr; //driver-specific extra setup info string (set by some drivers), also shown on the web admin page
@@ -237,6 +244,9 @@ DNSServer dnsServer;
#include <Wire.h> #include <Wire.h>
#include <EEPROM.h> #include <EEPROM.h>
#include "ArduinoJson.h" #include "ArduinoJson.h"
#ifdef USE_ARDUINO_OTA
#include <ArduinoOTA.h>
#endif
#if defined(USE_NEOPIXEL) || defined(WORDCLOCK) #if defined(USE_NEOPIXEL) || defined(WORDCLOCK)
#include <NeoPixelBrightnessBus.h> #include <NeoPixelBrightnessBus.h>
@@ -441,8 +451,10 @@ IPAddress ip;
// switchover DATES are not configurable from the web UI. myTZ is rebuilt with the current offsets // switchover DATES are not configurable from the web UI. myTZ is rebuilt with the current offsets
// every time updateTimefromTimeserver() runs (WiFi/NTP path only - the GPS-only fallback in gps.ino // every time updateTimefromTimeserver() runs (WiFi/NTP path only - the GPS-only fallback in gps.ino
// uses a simpler, always-on +1h DST rule, see CODE_REVIEW.md). // uses a simpler, always-on +1h DST rule, see CODE_REVIEW.md).
// NOTE: upstream ended DST on the first Sunday of November (the US date). The EU switches at 01:00 UTC:
// last Sunday of March 02:00 standard time, last Sunday of October 03:00 summer time.
TimeChangeRule myDST = {"DST", Last, Sun, Mar, 2, 0}; TimeChangeRule myDST = {"DST", Last, Sun, Mar, 2, 0};
TimeChangeRule mySTD = {"STD", First, Sun, Nov, 2, 0}; TimeChangeRule mySTD = {"STD", Last, Sun, Oct, 3, 0};
Timezone myTZ(myDST, mySTD); Timezone myTZ(myDST, mySTD);
time_t prevTime = 0; //last second() value that was actually rendered to the display - timeProgram() only redraws when now()!=prevTime, i.e. at most once per real second time_t prevTime = 0; //last second() value that was actually rendered to the display - timeProgram() only redraws when now()!=prevTime, i.e. at most once per real second
time_t protectTimer = 0; //timestamp of the last cathode-protect animation, used by timeProgram()'s prm.interval-minute scheduler time_t protectTimer = 0; //timestamp of the last cathode-protect animation, used by timeProgram()'s prm.interval-minute scheduler
@@ -589,6 +601,9 @@ void Fdelay(unsigned long d) {
#ifdef USE_GPS #ifdef USE_GPS
smartDelay(1); //feed GPS smartDelay(1); //feed GPS
#endif #endif
#ifdef USE_ARDUINO_OTA
ArduinoOTA.handle(); //also during long animations/startup waits
#endif
yield(); yield();
} }
} }
@@ -877,6 +892,39 @@ void startMDNS() {
#endif #endif
} }
// Optional ArduinoOTA (USE_ARDUINO_OTA in clocks.h): push a new firmware from PlatformIO/Arduino IDE
// over WiFi ("pio run -t upload --upload-port <ip>"). Shows the progress in percent on the two right
// tubes. Password from OTA_PASSWORD (secrets.h) if defined.
void startArduinoOTA() {
#ifdef USE_ARDUINO_OTA
static boolean otaStarted = false;
if (otaStarted) return;
ArduinoOTA.setHostname(OTA_HOSTNAME);
#ifdef OTA_PASSWORD
ArduinoOTA.setPassword(OTA_PASSWORD);
#endif
ArduinoOTA.onStart([]() {
DPRINTLN(F("ArduinoOTA: update started"));
clearDigits();
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
int p = (int)(progress / (total / 100 + 1));
digit[1] = (p / 10) % 10;
digit[0] = p % 10;
});
ArduinoOTA.onEnd([]() {
DPRINTLN(F("ArduinoOTA: done, rebooting"));
});
ArduinoOTA.onError([](ota_error_t error) {
DPRINTF("ArduinoOTA: error %u\n", error);
clearDigits();
});
ArduinoOTA.begin();
otaStarted = true;
DPRINT(F("ArduinoOTA ready: ")); DPRINTLN(OTA_HOSTNAME);
#endif
}
// The Standalone(AP)-mode path (prm.wifiMode==false, or a fallback from a fully-failed STA connect): // The Standalone(AP)-mode path (prm.wifiMode==false, or a fallback from a fully-failed STA connect):
// the clock hosts its own WiFi hotspot at the fixed address 192.168.4.1 (prm.ApSsid/prm.ApPsw) and // the clock hosts its own WiFi hotspot at the fixed address 192.168.4.1 (prm.ApSsid/prm.ApPsw) and
// runs a DNS server that answers every query with its own IP (dnsServer.start(53,"*",...)) so most // runs a DNS server that answers every query with its own IP (dnsServer.start(53,"*",...)) so most
@@ -1242,6 +1290,9 @@ void handleConfigChanged(AsyncWebServerRequest *request) {
if (old_utc_offset != prm.utc_offset) { //Change time zone if (old_utc_offset != prm.utc_offset) { //Change time zone
setTime(now()+(prm.utc_offset-old_utc_offset)*3600); setTime(now()+(prm.utc_offset-old_utc_offset)*3600);
updateRTC(); updateRTC();
mySTD.offset = prm.utc_offset * 60; //keep myTZ current for a later enableDST change
myDST.offset = mySTD.offset + (prm.enableDST ? 60 : 0);
myTZ = Timezone(myDST, mySTD);
} }
} }
else if (key == "set12_24") { else if (key == "set12_24") {
@@ -1262,13 +1313,15 @@ void handleConfigChanged(AsyncWebServerRequest *request) {
// running clock by +-1h right away and re-syncs the RTC, instead of only taking effect on the // running clock by +-1h right away and re-syncs the RTC, instead of only taking effect on the
// next scheduled NTP refresh (myTZ itself is rebuilt from prm.enableDST inside // next scheduled NTP refresh (myTZ itself is rebuilt from prm.enableDST inside
// updateTimefromTimeserver() on that next refresh). // updateTimefromTimeserver() on that next refresh).
// FIXED: this used to shift the clock by a blind +-1h, which is wrong outside the summer period
// (switching DST on in winter put the clock 1h ahead until the next NTP sync). Now the local
// time is converted back to UTC with the old rules and forward with the new ones.
prm.enableDST = (value == "true"); prm.enableDST = (value == "true");
if (oldDST && !prm.enableDST) { //Switching off DST if (oldDST != prm.enableDST) {
setTime(now()-3600); time_t utc = myTZ.toUTC(now());
updateRTC(); myDST.offset = mySTD.offset + (prm.enableDST ? 60 : 0);
} myTZ = Timezone(myDST, mySTD);
if (!oldDST && prm.enableDST) { //Switching on DST setTime(myTZ.toLocal(utc));
setTime(now()+3600);
updateRTC(); updateRTC();
} }
} }
@@ -1889,6 +1942,7 @@ void setup() {
startMDNS(); startMDNS();
startServer(); startServer();
startArduinoOTA();
#ifdef USE_MQTT #ifdef USE_MQTT
setupMqtt(); setupMqtt();
#endif #endif
@@ -3221,6 +3275,9 @@ void loop(void) {
} }
} }
editor(); editor();
#ifdef USE_ARDUINO_OTA
ArduinoOTA.handle();
#endif
if (makeFirmwareUpdate) { if (makeFirmwareUpdate) {
makeFirmwareUpdate = false; makeFirmwareUpdate = false;
doFirmwareUpdate(); doFirmwareUpdate();
+34 -1
View File
@@ -52,6 +52,7 @@
ZM1500_ESP32 (CURRENTLY ACTIVE, CLOCK_72 below) -> ZM1500.ino ZM1500_ESP32 (CURRENTLY ACTIVE, CLOCK_72 below) -> ZM1500.ino
WORDCLOCK -> wordclock.ino (+ font5x7.h for its scrolling/overlay font) WORDCLOCK -> wordclock.ino (+ font5x7.h for its scrolling/overlay font)
DUMMY -> dummy.ino (sensor-only "clock", no physical display) DUMMY -> dummy.ino (sensor-only "clock", no physical display)
NIXIE595_MULTIPLEX (CLOCK_90) -> nixie595.ino
*/ */
//#define CLOCK_1 //8266, UNFI PCB clock, 4x IN-16 tubes //#define CLOCK_1 //8266, UNFI PCB clock, 4x IN-16 tubes
@@ -110,11 +111,43 @@
//#define CLOCK_57A //V2-WROOM ESP32, UNFI 2xHV5222 6xZ57xM clock /flat panel //#define CLOCK_57A //V2-WROOM ESP32, UNFI 2xHV5222 6xZ57xM clock /flat panel
//#define CLOCK_70 //1 tube esp8266 Nixie Clock by UNFI //#define CLOCK_70 //1 tube esp8266 Nixie Clock by UNFI
//#define CLOCK_71 //2x VQC10 clock by UNFI //#define CLOCK_71 //2x VQC10 clock by UNFI
#define CLOCK_72 //ESP32, UNFI board, ZM1500 //#define CLOCK_72 //ESP32, UNFI board, ZM1500
//#define CLOCK_80 //word clock, english version //#define CLOCK_80 //word clock, english version
//#define CLOCK_81 //word clock, german version //#define CLOCK_81 //word clock, german version
//#define CLOCK_82 //word clock, hungarian version //#define CLOCK_82 //word clock, hungarian version
//#define CLOCK_99 //Dummy clock, sensor box with MQTT connector //#define CLOCK_99 //Dummy clock, sensor box with MQTT connector
#define CLOCK_90 //8266, RetroModding NixieIN12v2 HV2.0 (5 V): 4x IN-12, 2x IN-3, 2x 74HC595 multiplex
#ifdef CLOCK_90 //8266, RetroModding NixieIN12v2 HV2.0 (5 V): 4x IN-12, 2x IN-3, 2x 74HC595 multiplex
// Pins and shift-register bit map confirmed from the stock firmware SV1.3
// (docs/stock-firmware-pins.md in the nixie-clock repo).
#define DEBUG
#define FW "fw90" //firmware name
#define MAXBRIGHTNESS 100
#define NIXIE595_MULTIPLEX
#define PIN_LATCH 15 //74HC595 storage clock. DATA = GPIO13 (HSPI MOSI), CLOCK = GPIO14 (HSPI SCK)
#define SPI_FREQ 4000000 //stock uses 25 MHz; 16 bits per slot need far less
byte anodeBits[] = {14, 13, 12, 11}; //595 word bit of each tube's anode: position 0 = rightmost (A4) ... 3 = leftmost (A1)
#define NEON_BITS (1 << 10) //IN-3 dots on P1. P2 (bit 15) drives the IN-12B decimal points on this board (JP2 closed)
#define NEON_POS 1 //the dots are lit in this tube's multiplex slot, as on stock
#define USE_NEOPIXEL
#define NEOPIXEL_PIN 12 //bit-banged, GRB 800 kHz
byte tubePixels[] = {0, 0, 1, 1, 2, 2, 3, 3}; //8 WS2812, two per tube (strip order not verified yet)
#define USE_RTC //DS3231 on I2C, detected at startup (address 0x68); the stock firmware never used it
#define PIN_SDA 4
#define PIN_SCL 5
#define LIGHT_SENSOR_PIN A0 //LDR to Vcc, 10k to GND (REF_RESISTANCE default)
#define MAXIMUM_LUX 100 //Lux level for maximum tube brightness
#define AP_NAME "UNICLOCK"
#define AP_PASSWORD ""
#define WEBNAME "IN-12 Nixie Clock"
#define FIRMWARE_SERVER "" //no web-triggered HTTP update; use ArduinoOTA
#define USE_ARDUINO_OTA
#define OTA_HOSTNAME "nixie-in12"
#if __has_include("secrets.h")
#include "secrets.h" //optional, gitignored: DEFAULT_SSID, DEFAULT_PSW, OTA_PASSWORD (see secrets.h.example)
#endif
#endif
//______________________ESP8266 CLOCKS by UNFI and GP ______________________________________________________ //______________________ESP8266 CLOCKS by UNFI and GP ______________________________________________________
#ifdef CLOCK_1 //8266, UNFI PCB clock, 4x IN-16 tubes #ifdef CLOCK_1 //8266, UNFI PCB clock, 4x IN-16 tubes
@@ -0,0 +1,154 @@
// ============================================================================
// 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
@@ -0,0 +1,6 @@
// Copy to secrets.h (gitignored) and fill in. All entries are optional.
// DEFAULT_SSID/DEFAULT_PSW are only applied on a factory reset (first boot with a new EEPROM layout);
// otherwise set the WiFi from the clock's web page.
#define DEFAULT_SSID ""
#define DEFAULT_PSW ""
#define OTA_PASSWORD "" //ArduinoOTA password; remove the line for no password
@@ -100,13 +100,17 @@ inline byte tubeForPixel(int i) {
//than NeoEsp32Rmt7Ws2812xMethod, which can glitch the strict WS2812 bit //than NeoEsp32Rmt7Ws2812xMethod, which can glitch the strict WS2812 bit
//timing (~1.25us/bit) if an ISR delays the RMT hardware mid-frame. //timing (~1.25us/bit) if an ISR delays the RMT hardware mid-frame.
NeoPixelBrightnessBus<NeoGrbFeature, NeoEsp32I2s1800KbpsMethod> strip(PixelCount+4,PixelPin); //NeoEsp32I2s1800KbpsMethod is used here because it is more reliable than NeoEsp32Rmt7Ws2812xMethod! NeoPixelBrightnessBus<NeoGrbFeature, NeoEsp32I2s1800KbpsMethod> strip(PixelCount+4,PixelPin); //NeoEsp32I2s1800KbpsMethod is used here because it is more reliable than NeoEsp32Rmt7Ws2812xMethod!
#else #elif NEOPIXEL_PIN == 3 //default (see clocks.h)
#define NEOPIXEL_PIN 3
byte PixelPin = 3; // on 8266 it MUST use GPIO3 (RX pin) byte PixelPin = 3; // on 8266 it MUST use GPIO3 (RX pin)
//8266 has no I2S/RMT WS2812 helper here; Neo800KbpsMethod bit-bangs the //8266 has no I2S/RMT WS2812 helper here; Neo800KbpsMethod bit-bangs the
//protocol with cycle-counted delays, which is why GPIO3/RX is mandated - //protocol with cycle-counted delays, which is why GPIO3/RX is mandated -
//it's one of the few pins whose timing isn't disturbed by the UART/boot ROM. //it's one of the few pins whose timing isn't disturbed by the UART/boot ROM.
NeoPixelBrightnessBus<NeoGrbFeature, Neo800KbpsMethod> strip(PixelCount+4); NeoPixelBrightnessBus<NeoGrbFeature, Neo800KbpsMethod> strip(PixelCount+4);
#else
//Any other 8266 pin: cycle-counted bit-bang. Interrupts are off while Show()
//runs (about 30 us per pixel), which briefly delays the multiplex ISR.
byte PixelPin = NEOPIXEL_PIN;
NeoPixelBrightnessBus<NeoGrbFeature, NeoEsp8266BitBang800KbpsMethod> strip(PixelCount+4, PixelPin);
#endif #endif
//Gamma-correction lookup table object - NOTE: currently declared but never //Gamma-correction lookup table object - NOTE: currently declared but never
@@ -121,6 +125,9 @@ void setupNeopixel() {
DPRINT("Pixel count: "); DPRINTLN(PixelCount); DPRINT("Pixel count: "); DPRINTLN(PixelCount);
DPRINT("Brightness:"); DPRINT(c_MinBrightness); DPRINT(" - "); DPRINTLN(c_MaxBrightness); DPRINT("Brightness:"); DPRINT(c_MinBrightness); DPRINT(" - "); DPRINTLN(c_MaxBrightness);
neoBrightness = prm.rgbBrightness; neoBrightness = prm.rgbBrightness;
//The bit-bang method sets the pin to OUTPUT in its constructor (before setup()), and Begin()
//does not. A driver may have claimed the pin since (nixie595.ino: SPI.begin() makes GPIO12 MISO).
pinMode(PixelPin, OUTPUT);
strip.Begin(); //initialize the underlying I2S/RMT/bit-bang driver and GPIO strip.Begin(); //initialize the underlying I2S/RMT/bit-bang driver and GPIO
strip.Show(); //push the (still all-black, default-constructed) buffer out so the strip starts dark, not showing stale/garbage pixel data strip.Show(); //push the (still all-black, default-constructed) buffer out so the strip starts dark, not showing stale/garbage pixel data
strip.SetBrightness(neoBrightness); strip.SetBrightness(neoBrightness);
+7
View File
@@ -28,3 +28,10 @@ upload_resetmethod = ck
monitor_port = COM7 monitor_port = COM7
monitor_speed = 115200 monitor_speed = 115200
monitor_filters = esp8266_exception_decoder monitor_filters = esp8266_exception_decoder
; Same firmware, uploaded over WiFi with ArduinoOTA (espota). Add --auth=<OTA_PASSWORD> to
; upload_flags once secrets.h sets one.
[env:nixie_in12_ota]
extends = env:nixie_in12
upload_protocol = espota
upload_port = 10.1.1.97