// ============================================================================ // MULE_V2 driver // // Drives the "Mule V2" ESP32 board: 6 x SA40 7-segment LED digit modules, // used by CLOCK_34 ("Mule V2 ESP32, board, 6 x SA40 LED Display", see // clocks.h around line 972). This is a BCD-multiplexed 7-segment LED clock, // NOT a Nixie/VFD tube driver, even though several comments/variable names // below (DP_PIN, "Nixie clock", tube terminology) are inherited boilerplate // copied from the tube-driver files this codebase is built from. // // Hardware / wiring protocol (see clocks.h CLOCK_34 block for actual pins): // - ABCDPins[4] -> CD4543 BCD-to-7-segment latch/decoder/driver IC. // 4 bits carry the digit VALUE (0-9) for whichever // digit position is currently selected; the 4543 // decodes it to segments and drives the LED digit // directly (latching, so no extra data hold time is // required). // - digitEnablePins[3] -> CD4028 BCD-to-Decimal decoder ("A,B,C" inputs). // Only 3 bits are used (not per-digit enable lines!) // to binary-encode which ONE of the 6 SA40 digits // is active right now; the 4028's decoded output // lines 1-6 are wired to the 6 digit commons, output // 0 and 7 are intentionally left unconnected and are // used as a "blank all digits" state (see writeDisplay()). // - This is a classic BCD multiplex scheme: only one digit is illuminated // at any instant, cycling fast enough (via hardware timer ISR) that // persistence of vision makes all 6 digits appear lit simultaneously. // - Digit brightness is done by PWM-ing the "on" time of each digit within // its multiplex slot (see PWMrefresh/PWM_min/PWM_max and PWMtimeBrightness // below), not by varying LED current. // // Board also has optional peripherals declared in clocks.h but NOT touched // by this file directly: NeoPixel backlight (USE_NEOPIXEL/NEOPIXEL_PIN) and // a Dallas temperature sensor pin - both handled by the core .ino. // ============================================================================ #ifdef MULE_V2 char tubeDriver[] = "MULE_V2"; //define here the digit enable pins from 4 to 8 //byte digitEnablePins[] = {15,13,12,14}; //for example... But SET in clocks.h !!! //byte ABCDPins[4] = {2,4,5,0}; //int DP_PIN = -1; // decimalPoint inside Nixie tube, set -1, if not used! int maxDigits = 6; //sizeof(digitEnablePins); // NOTE: maxDigits (6) is the number of physical SA40 digits, NOT the size of // digitEnablePins (which is only 3 - see CLOCK_34 in clocks.h: {5,16,23}). // digitEnablePins holds the 3 CD4028 select lines, so its correct element // count is always 3 regardless of maxDigits. Do not assume they are equal // (setup_pins() below used to get this wrong - now fixed, see the note there). int DRAM_ATTR maxDig = maxDigits; //memory variable version // Local DRAM_ATTR copy of maxDigits, used inside the IRAM_ATTR ISR // (writeDisplay). Keeping a dedicated DRAM-resident copy guarantees the // value stays reachable from the ISR even in situations where flash-cache // backed data access would be unsafe (e.g. during flash/EEPROM writes). //byte convert[] = {1,0,9,8,7,6,5,4,3,2}; //tube pin conversion, if needed (e.g. for a non-standard tube pin layout) int DRAM_ATTR PWMrefresh=11000; //msec, Multiplex time period. Greater value => slower multiplex frequency int DRAM_ATTR PWM_min = 1000; int DRAM_ATTR PWM_max = 10000; //int DRAM_ATTR PWMtiming[11] = {0,1000,2000,3000,4000,5000,6000,7000,8000,9000,10000}; // PWMrefresh is the total time budget (in hardware-timer ticks) for one full // digit's multiplex slot: part of it is "digit ON" (length set by // PWMtimeBrightness, scaled between PWM_min/PWM_max according to // brightness/ambient light) and the remainder is "digit OFF" (blanking), see // case 2 in writeDisplay(). This is how per-digit brightness/dimming is // achieved without a dedicated PWM peripheral per digit. #if defined(ESP32) #else #error "Only ESP32 board is supported by this driver!" #endif void setup_pins() { char tmp[30]; DPRINTLN("Nixie clock - setup pins - Multiplex BCD mode (MULE_V2)..."); // "Nixie clock" text is inherited boilerplate from the tube-driver // templates this file was cloned from; this board drives LED digits. DPRINTLN("BCD digitEnablePins: "); // FIXED (was "i=0 pinMode(DP_PIN,OUTPUT); DPRINT("DP_PIN:"); DPRINTLN(DP_PIN); #endif startTimer(); // arms the hardware timer that drives writeDisplay() as an ISR } void IRAM_ATTR writeDisplay(){ //void IRAM_ATTR writeDisplay(){ // Timer-interrupt multiplex refresh handler. Runs once per "phase" of a // single digit's multiplex slot (see state machine below) and re-arms the // hardware timer itself with the next phase's duration - i.e. this is a // self-rescheduling one-shot timer, not a fixed-period tick. Because it is // IRAM_ATTR it (and everything it touches, hence the DRAM_ATTR statics) must // not depend on code/data that lives in flash, since it can fire while the // SPI flash cache is disabled. static DRAM_ATTR byte pos = 0; static DRAM_ATTR byte state=0; static DRAM_ATTR int timer = PWMrefresh; static DRAM_ATTR int num,brightness; static DRAM_ATTR byte DPpos; static DRAM_ATTR int PWMtimeBrightness=PWM_min; if (EEPROMsaving) { //stop refresh while an EEPROM write is in progress! return; } portENTER_CRITICAL_ISR(&timerMux); noInterrupts(); intCounter++; brightness = displayON ? prm.dayBright : prm.nightBright; if (brightness>MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety timer = PWMrefresh; switch (state) { //state machine... // 4-phase cycle per digit position, advancing `pos` only in case 0: // 0: decide which digit to show next (advance pos, recompute // brightness once per full 6-digit sweep) and pick ON-phase timing // 1: (only reached when animMask[pos]>0, i.e. a digit-change animation // is in progress) show the NEW character for the remaining // fraction of the ON time, crossfading old->new digit by splitting // the ON slot into two sub-phases weighted by animMask (0..20) // 2: blank phase - digit output stays on the last-set value but the // remaining PWMrefresh budget elapses with the digit dark, giving // the dimming effect // 3: sentinel reached via case 2's fallthrough-by-timer only, forces // the blank branch below and resets state to 0 next call case 0: pos++; if (pos>maxDig-1) { pos = 0; //go to the first tube if (autoBrightness && displayON) { PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX); //ambient-light-driven auto brightness } else PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS); //manual day/night brightness setting } if (animMask[pos] > 0) { //Animation? num = oldDigit[pos]; //show old character timer = (PWMtimeBrightness * (20-animMask[pos]))/20; state = 1; //next state is: show newDigit } else { num = digit[pos]; //show active character timer = PWMtimeBrightness; state = 2; //next state is: BLANK display } break; case 1: //show new character, if animation num = newDigit[pos]; timer = (PWMtimeBrightness * animMask[pos])/20; state = 2; //default next state is: BLANK display break; case 2: //blank display timer = PWMrefresh-PWMtimeBrightness; state = 3; break; } //end switch if (timer<500) timer = 500; //safety only... (avoid re-arming the HW timer with an unreasonably short/zero alarm) // if ((pos>0) && (num<=9)) num = convert[num]; //tube character conversion, if needed... (maybe bad pin numbering) // Blank the currently-selected digit when: display globally off/zero // brightness, we're in the "blank" phase (state==3), the character code // isn't a real digit (10=BLANK,11=dash,12=DP,13+=custom glyph - none of // these have a 7-segment BCD encoding this driver understands), or the // radar/PIR presence sensor says nobody is around. if ((brightness == 0) || (state == 3) || (num >9) || (!radarON)) { //blank digit state = 0; for (int i=0;i=0 digitalWrite(DP_PIN,LOW); #endif } else { // Push the digit VALUE (0-9) as 4 bits onto the CD4543 (ABCDPins), // which latches and decodes it to 7-segment outputs for whichever // digit is currently selected below. for (int i=0;i<4;i++) {digitalWrite(ABCDPins[i],num & 1<=0 if (LEFTDECIMAL) DPpos = min(maxDig-1,pos+1); else DPpos = pos; if (digitDP[DPpos]) digitalWrite(DP_PIN,HIGH); //switch ON decimal point, if needed #endif } // COLON_PIN / DECIMALPOINT_PIN are both -1 (unused) for the Mule V2 board // (CLOCK_34 in clocks.h does not define them), so these blocks compile out // entirely on this board; kept for source compatibility with other boards // that reuse this driver file. #if COLON_PIN >= 0 if (num==10) digitalWrite(COLON_PIN,LOW); // Colon pin OFF else digitalWrite(COLON_PIN,colonBlinkState); // Blink colon pin #endif #if DECIMALPOINT_PIN >=0 if (num==10) {digitalWrite(DECIMALPOINT_PIN,LOW);} else {digitalWrite(DECIMALPOINT_PIN,decimalpointON); } #endif portEXIT_CRITICAL_ISR(&timerMux); //ESP32timer = timerBegin(0, PRESCALER, true); //set prescaler, true = edge generated signal //timerAttachInterrupt(ESP32timer, &writeDisplay, true); timerAlarmWrite(ESP32timer, timer, true); // re-arm the one-shot HW timer for the next phase's duration (see state machine above) timerAlarmEnable(ESP32timer); interrupts(); } void clearTubes() { // Blank all digits immediately (outside the ISR/multiplex cycle), using // the same "select CD4028 output 7" trick as the blanking branch in // writeDisplay() above. for (int i=0;i=0) digitalWrite(DP_PIN,LOW); #endif } // Intentionally empty: unlike non-multiplexed drivers, this board's display // is refreshed continuously by the writeDisplay() timer ISR, which reads the // live digit[]/newDigit[]/animMask[] state on every phase. Callers elsewhere // in the core .ino invoke writeDisplaySingle() to force an immediate // blocking redraw after changing digit values (e.g. during animations); for // MULE_V2 no explicit action is needed since the next ISR tick (at most // PWM_min..PWMrefresh ticks away) will already pick up the new state. void writeDisplaySingle() {} #endif