// Last Used and/or modified 09-02-26 gsk // // ESP32 4 Channel Generator Ver 2.05 09-02-26 gsk // Board Manager = ESP32 Arduino IDE, selection = Nano32, // // SPECIAL NOTE: This program now compiles with esp32 by Espressif board versions 3.0.0 and // higher. Older versions of the 4-Channel needed version 2.0.18 or lower. // USE "esp32 by Espresif" version 2.0.17 for all versions below 1.40 // // NOTE 34: The NOTE 30 fix was not complete. With the sweep behaving (NOTE 33) one // fault was left standing on its own and became obvious: the step AFTER a // low frequency came out wrong, and nothing else did. Sweeping 2 Hz to // 1000 Hz by 50, the second step asked for 52 Hz and the scope read about // 4.6. The firmware was asking correctly -- the debug trace shows a clean // 2, 52, 102 ... and the prescaler search puts every one of those within // 4.2 ppm. Working back from the reading: 52 Hz programs clkPre 2, // timerPre 17, period 56979; keeping 2 Hz's timerPre of 198 with the new // clock and the new period gives 4.7035 Hz. No other partial write comes // close (22.29, 51.59, 2.02). So the timer prescaler was not latching -- // the same fault as NOTE 30, in a case NOTE 30's fix did not cover. // WHY: timer_start = 0 does not stop a timer, it means "stop at TEZ", the // next time the counter passes zero. haltTimer() froze the counter to make // the stop immediate -- but a frozen counter never reaches zero, so the // pending stop never completed and the timer was never, as far as the // prescaler latch is concerned, stopped at all. Out of a FAST channel this // never showed: the counter passed zero on its own during the prescaler // search, long before haltTimer() ran. Out of 2 Hz, whose cycle is half a // second, it had not got anywhere near zero. Hence exactly one bad step // per sweep, always the one after the slowest frequency. // FIX: bring the zero crossing forward instead of waiting for it. The // period register updates immediately, unlike the prescaler, so haltTimer() // now reads the counter and drops the period to just above it -- TEZ then // arrives within a couple of ticks, the stop completes, and the following // start latches the new prescaler. The wait is bounded and falls back to // the old behaviour rather than hanging. // NOT verified on hardware. This is the part to watch on the bench: sweep // out of the lowest frequency in the range and check the second step. // // NOTE 33: SWEEP (Mode 3) -- three faults, all of which fed each other and made the // mode look random. Mode 2 was never affected. // a) THE START RECORD WAS BEING OVERWRITTEN. sweep() calls // fetchProtoID(startSweepGrpID) and then puts the swept frequency into // F1 -- so while a sweep runs, the globals claim to be the start sweep // group while holding a frequency that group never contained. Nothing put // it back when the sweep stopped. On its own that only mislabels the // display, but saveCurrent() writes the globals into record protoID, and // it runs whenever Start is pressed while a Setup page is showing. So: // run a sweep, stop it, go to Setup=4 to change Freq. Inc., press Start -- // and the start group's F1 in EEPROM is now the frequency the sweep // happened to reach. Every run after that computed a different span and a // different step from a different starting point, which is exactly why // nothing repeated and why a sweep would begin from where the last one // left off instead of from its own range start. The stop path now reloads // the start sweep group before anything can display or save it. // b) THE FIRST FREQUENCY RAN TWICE. The run puts the start frequency out // itself, and then sweep() emitted sweepAccum -- which was still the start // frequency, because the accumulator was not advanced until afterwards. So // the first step lasted two Freq Times and every later one lasted a single // Freq Time. sweep() now advances before it emits, and the recycle at the // end of a sweep is what re-issues the start frequency. // c) FREQ. INC. WAS NOT THE INCREMENT. It was used only to guess how many // steps would fit; that count was truncated to a whole number and the // whole span was then divided evenly by it. 2 Hz to 1000 Hz "by 50" // stepped by 998/19 = 52.53. 2 Hz to 3000 Hz "by 50" stepped by // 2998/59 = 50.81, which is where 52.81 and 103.63 came from. Freq. Inc. // is now the step, exactly as dialled. The sweep takes as many full steps // as fit and then one short one to land ON the upper limit rather than // overshoot it or stop short: 2, 52, 102 ... 902, 952, 1000, then recycle. // If the span happens to divide exactly, no extra step is added. Descending // sweeps work the same way, and leaving Freq. Inc. at 0 still divides the // span evenly across the Progm Time as it always did. // Still open: a scope reading of 12.5 Hz while the display and the firmware // both said 52.81. Not the prescaler search -- every step of 2 to 1000 paired // against Gen2's 7.83 Hz lands within 4.2 ppm -- and not the NOTE 30 latch // fault, which would have produced 1.86 Hz. Measurements taken before (a) was // fixed cannot be trusted, so this needs re-testing from a clean start with // DEBUG = 2 to capture what was actually programmed. // // NOTE 32: Hardening the SD card save, so a card cannot be left in a state an // ordinary user has no way to recover from. Written and simulated // 2026-09-01, held back while the sweep of NOTE 33/34 was debugged so there // was one known-good baseline, and applied 2026-09-03. Not yet run on the // bench -- see the test list at the end of this note. // WHAT IT ADDRESSES, and what the build used to do: // a) saveFile() deletes the target protocols-N.txt FIRST and then rebuilds // it a record at a time -- 276 separate open/append/close cycles for a // full file, several seconds of them. A reset, a power cut, a card // pulled or simply a card that has filled up anywhere in that window // leaves a half-written protocol file and NO original to fall back on. // Nothing checks whether the writes worked and nothing says so after. // The parked version builds the replacement under /save.tmp through ONE // open handle, checks every write for a short count, reopens the result // to confirm it, and only then runs the two metadata operations that // swap it in. Anything short of complete leaves the original untouched, // removes the scratch file and puts the reason on the OLED. This is the // write-then-rename that the USB upload has always used. // b) The end record is written LAST, so an interrupted save leaves exactly // the file that has records and no end record -- and that loads silently // here, taking mode, both run times and all four group IDs from whatever // is left in memory. The parked version reports it as fileRtnCode 4 // ("NO END REC"), and a file with no frequency records at all as 5 // ("NO RECORDS"), alongside the existing 2 and 3. // Steve has corrupted a card this way before. He knows the file format well // enough to repair one; an ordinary user would be stranded. That is the // reason this was worth finishing rather than dropping. // TO TEST: a normal save; a save with the card pulled part way through; a // save onto a full card; a reset during "SDram Card UPDATING". In every // failing case the ORIGINAL protocols-N.txt must still be on the card and // readable, /save.tmp must be gone, and the OLED must say why. Then boot // from a file with its last line chopped off -- that must read NO END REC // rather than loading with junk settings. // // NOTE 31: The screen saver no longer needs a reset to get out of, and its timer // now measures idle time rather than time since the unit stopped. // (Written, then rolled back untested during the sweep work of NOTE 33/34 // so there was one known-good baseline to debug from, and reinstated on // its own afterwards. NOTE 32, the SD save hardening, is still parked.) // It used to be a bare while(1), so once it started, rotary() and // cursorMove() were never called again -- the knob and the black button // were dead, and the red button's interrupt went on setting runFlag with // nothing left to read it. Only a power cycle came back from that. // The quieter half of the problem was the timer. saverTime is armed in // the stop branch's "singleShot == 0" block, which runs ONCE on entering // the stopped state, and nothing pushed it back afterwards. Turning the // knob did not, moving the cursor did not, editing a frequency did not. // So the 20 minutes ran from the moment the unit stopped, whatever the // operator was doing: twenty unbroken minutes on the Setup screens and it // arrived in the middle of an edit. It is re-armed on real activity now // -- rotary() returns early unless the knob actually moved, so "change" // means something happened rather than merely that the loop went round. // displayScreenSaver() now watches the whole front panel. The knob, the // black button and the red button all bring the dial screen back, and NONE // of them does anything else -- every wake press is swallowed. // That last part was Steve's call and he was right. The first cut had the // red button wake the screen AND start the generator, on the reasoning // that a control should mean the same thing everywhere. But the screen // saver can only appear because the unit has sat stopped for twenty // minutes, which means the job it was doing finished long ago. Whoever // walks back up to it has come to decide what to do NEXT, not to repeat // whatever is still loaded -- and Start is a button with real output on // the other end. Waking a screen is not an instruction. // Swallowing each one takes something different. BOTH buttons are waited // out before anything is decided, because deciding while a finger is // still down does not work: the black button would move the cursor as // soon as cursorMove() next ran (it acts on the pin being held, not on // the edge), and the red button's RELEASE bounce is a string of fresh // falling edges -- clearing runFlag on the way past would simply have it // set again a moment later and the generator would start after all. So // the release is waited for, THEN runFlag is cleared and the debounce // held open across the bounce that follows. The knob needs nothing: the // encoder is handed back to rotary() from a known state the way // adjustEncoderStop() does, which discards the turn that woke it. // // NOTE 30: FIXED -- Channel 1 and Channel 3 ran at the wrong frequency while // Channels 2 and 4 were right. Reported case: F1=100, F2=7.83, F3=100, // F4=4 gave 64.9 Hz on Channel 1 and 20 Hz on Channel 3. This one was // introduced by the NOTE 28 speed work, and NOTE 26 had already written // down the reason without either of us seeing what it meant: // "The slow search is load-bearing." // From the ESP32 register description of PWM_TIMERx_CFG0_REG: // timer_prescale ... "takes effect when PWM timer stops and starts // again" // The timer prescaler is not a live register. Written to a timer that is // still turning it is IGNORED, and the channel keeps dividing by whatever // it was last started with. timer_period sits in the same register but is // a shadow with an "immediate" update method, and clk_prescale is a // unit-level register that applies at once. So a botched write changed two // of the three divisors and not the third, which is why the result was a // wrong frequency rather than no frequency. // Why it only appeared now: setFrequency() ran setGen1..setGen4 one at a // time. setGen1 programmed and STARTED Channel 1 against whatever Channel // 2's frequency happened to be -- zero on the first call after a reset -- // and setGen2 then moved the unit's shared clock and tried to correct // Channel 1 on the fly. Under V2.04 that correction never had to happen, // because setGen2 never touched the clock. Under the first cut of V2.05 it // did, but each prescaler search took ~20 ms, and stopAll() before it sets // timer_start = 0, which means "stop at TEZ" -- so those 20 ms were long // enough for the timers to actually reach zero and stop, and the writes // landed on stopped timers. NOTE 28 cut the search to well under a // millisecond and the accidental delay went with it. // The fix is to stop relying on timing at all: // - setGens() (PWM.ino) loads all four channels in one pass, so every // unit is worked out once from final values and started once. Nothing // is ever started against a provisional frequency and corrected later. // - haltTimer() freezes a timer outright (timer_mod = 0) instead of // asking it to stop at TEZ and hoping, so the following start is a // real stop->start and the prescaler latches. It is applied only to a // timer about to be restarted: a channel meant to stay stopped still // needs one last TEZ for stopGen()'s "low at TEZ" action to pull its // pin down. // - writeTimerCfg0() stores the whole register in one 32-bit write // rather than two read-modify-writes across a live period shadow. // This also explains NOTE 26's three failed attempts at a cheap retune: // they held the clock prescaler and re-searched only the TIMER prescaler, // which is the one value that cannot take effect without a stop and start. // It was never going to work, for a reason no amount of retrying reveals. // // NOTE 29: Four latent faults found in a read-through of the whole program, none of // which had bitten yet and none of which touch frequency generation. // a) RECORD CEILING. FLASH_SIZE holds 298 frequency records at most, because // the 44-byte control record has to fit after the last one. Past that, // EEPROM.put() fails its own bounds check and returns WITHOUT WRITING and // WITHOUT SAYING SO, so the control record -- mode, both run times, all // four group IDs, file number -- was never stored, and fetchMem() then // hunted for a terminator that did not exist. When it walked off the end // EEPROM.get() also returned silently, leaving protoID at its last // non-zero value, so that loop could never end: a hang at boot. // protocols-3.txt and protocols-13.txt already hold 275 each. loadFloats() // now refuses record MAX_FREQ_RECORDS+1 and readFileModified() stops // there; fetchMem() has a backstop so it can never run away; and the USB // upload now rejects an oversized file instead of writing it to the card // and failing at the next boot. protocol_tool.py enforces the same // number -- protocol_editor.py already did. // b) '#' HEADER. Everything ahead of the '#' is free text so a file can carry // a descriptive name. read() returns -1 at end of file and -1 is not '#', // so a file with no '#' anywhere -- an empty one, or one truncated by a // card pulled mid-write -- spun in that loop forever and hung the boot. // It now watches for the end and reports the reason on the OLED, as does // the too-many-records case: "NO # MARK" and "TOO MANY" instead of a bare // "FAILURE" that gave nothing to work from. // c) WiFi CLIENT. wifiRoutine() runs inside loop(), and its // "while (client.connected())" let whatever was on the far end of the // socket decide how long the knob, the buttons and the run timers stood // still. A client that connected and then said nothing held the generator // indefinitely, and the request line grew without limit. Now: a 2 second // idle timeout, a 10 second ceiling on the whole exchange, and a 128 // character cap on the line. // d) millis() ROLLOVER. millis() wraps to zero every 49.7 days. Comparisons // of the form "if (deadline < millis())" read a deadline that has wrapped // as already past, so a run started within its own duration of the wrap // would stop the instant it began. Every one of them is now written // "if ((long)(millis() - deadline) >= 0)", which wraps along with the // clock and stays correct. This is not slower -- it is a subtraction and a // signed compare where there used to be an unsigned compare -- and it is // nowhere near the PWM path. Six places: handleInterrupt(), both timers in // checkTime(), the run timer and the repaint timer in adjustModeRun(), the // preset confirm window in displayUpdate(), and the screen saver trigger. // Deliberately NOT changed: saveCurrent()'s (protoID-1) underflow, which is // intentional -- the unsigned value arrives at EEPROM.put() as -36, fails the // bounds check and no-ops by design. // // NOTE 28: SPEED -- the first cut of the NOTE 27 fix worked but made Start/Stop, // Mode 2 frequency changes and memory updates take five seconds or more, // and pressing Start/Stop again during that dead time could leave the // program unsure whether it was running or stopped. Cause: the ESP32 has // no hardware double-precision unit, so every double divide in the // prescaler search is a software routine of a few hundred cycles, and the // new joint search did about 2.2 MILLION of them per setFrequency() where // V2.04 did 132000 -- roughly seventeen times the work. It was also run // four times per setFrequency() (once per setGenN) when two would do. // Four changes, none of which give back the NOTE 27 fix: // 1. The answer is cached per MCPWM unit, keyed on the two frequencies. // Start/Stop and Suspend/Resume call setFrequency() twice with the // SAME frequencies and only the duty cycles changed, and duty has no // bearing on the prescalers -- so those now cost NOTHING at all. // It also makes the second of the two applyGroup calls free. // 2. The timer prescaler is stepped only across the range that can // actually yield a legal period, instead of scanning all 256 and // throwing most away, and it stops the moment it lands exactly. // 3. Each candidate is screened with a multiply and only costs a divide // if it could actually beat the best so far -- and the error is now // worked out with one divide instead of four. // 4. Only the first 32 timer-prescaler steps are tried (TIMER_SCAN_SPAN // in PWM.ino). The period is biggest at the first step and falls off // as 1/step, so that is where the accuracy and the duty resolution // both live. Measured over 15000 random frequency pairs this adds // 0.000 ppm at the median and 1.1 ppm at the 99th percentile, and no // channel that the full scan could reach became unreachable. // Result, measured over the 333 real 4-channel sets in Protocols-x: // about 33000 double divides per setFrequency() where the first cut of // V2.05 needed 2.2 million and V2.04 needed 24000-132000. Worst case is // now roughly 25-50 ms, against 83-165 ms for V2.04 -- so Mode 2 steps // and memory updates are quicker than they ever were on V2.04, and // Start/Stop is instant. // // NOTE 27: Version 2.05 fixes a major bug -- Channels 2 and 4 went dead whenever they // were set below about 10 Hz while their partner channel was set higher, e.g. // Channel 1 at 100 Hz with Channel 2 at 7.87 Hz: no output at all on Channel 2. // Each MCPWM unit has only ONE clock prescaler, shared by both of its timers: // MCPWM0's clk_prescale serves Channels 1 and 2, MCPWM1's serves Channels 3 // and 4. The old code let the "master" of each pair (Channel 1, Channel 3) // pick that prescaler for itself and left the secondary to cope. Because the // old search simply maximised the period register, any master above ~10 Hz // drove its unit to clk_prescale = 0, and at clk_prescale = 0 the lowest // frequency the hardware can reach is 160 MHz / (256 * 65536) = 9.5367 Hz -- // which is exactly where the "below 10 Hz" symptom came from. The secondary's // locked-clock search then found no legal period, returned period = 0 with no // error of any kind, and that zero went straight into the timer. Period 0 is // not a frequency, so the channel simply stopped. // The fix (PWM.ino): the shared prescaler is now chosen for BOTH channels of a // unit together, minimising the worse frequency error of the two and breaking // ties toward finer duty-cycle resolution; and the search can no longer return // period = 0 -- an unreachable request is clamped to the nearest legal setting // instead of killing the output. setGen1..setGen4 keep their old signatures but // now reprogram their whole MCPWM unit, since moving the shared clock for one // channel changes the tick rate under the other. Across the full 0.04 Hz-65 kHz // range roughly 28% of channel pairings used to leave one channel dead; none do // now, and 89% of pairings lose less than 1 ppm to sharing the prescaler. // Related: the frequency floor in rotary_Decode.ino was 0.01 Hz, below the // 0.0373 Hz the hardware can actually produce, so any channel set under // 0.0373 Hz died the same way even as a master. The floor is now 0.04 Hz. // // NOTE 26: Mode=4 (Adjust Mode) is BACK and reworked, replacing the version described in // NOTE 25. Select Mode=4 on Setup=2, return to Setup=0, pick a record, and press // the red button: the knob then moves Channel 1's frequency by the Setup=4 // "Freq. Inc." value, and the black button flips the knob over to Channel 1's // duty cycle in steps of 0.05. Channels 2-4 keep running on the record's own // values. Adjustments are TEMPORARY -- the protocol record is never written to, // so Stop leaves it exactly as it was. Red button stops as always; there is no // Pause in this mode because the black button is the F1/D1 toggle. // The old version failed because it ran setFrequency() -- which stops and // reprograms all four generators, including two full 256x256 double-precision // prescaler searches -- plus a complete OLED repaint, on every pass of the main // loop, all while the encoder was only being polled from that same loop. The // knob went unwatched for tens of milliseconds at a time, so it lagged badly and // lost steps when turned quickly. The rework fixes the cause rather than tuning // the symptoms: adjustModeRun() (support_Routines.ino) is self-contained and // runs until Stop; the encoder moves to an interrupt so a click cannot be missed // (adjustEncoderISR, rotary_Decode.ino); the PWM is touched only when a click // actually arrives; and the screen repaints on a 10-per-second timer that the // knob never waits on. // RETUNING -- this took four attempts, and the reason is worth recording // because it is not obvious and it cost a lot of bench time. // MCPWM's timer_period and operator compare registers are SHADOWED. Writing // them does not change the output; the value sits in a shadow register until // something causes it to load. MCPWM0.clk_cfg.clk_prescale is NOT shadowed // and takes effect immediately. That one asymmetry explains everything that // was seen on the bench: // - Writing period and compare to a live timer did nothing whatsoever. The // display tracked the knob while the scope stayed put. // - "timer_start = 0" did not fix it. That does not mean "stop now", it // means "stop at the next TEZ" -- up to 90 ms away at these frequencies, // and it was being overridden before it ever arrived. // - stopGen(1) did not fix it either, for the same reason. The log showed // "Gen1 STOPPED" on every click with the output not even glitching. The // search had deliberately been done BEFORE the stop to keep the dropout // short, so the write and restart followed microseconds later and the // timer never reached a TEZ to actually stop on. That "optimisation" was // precisely what defeated it. // - setGen1() called bare moved the output but landed nowhere near the // request: clk_prescale latched while the stale period did not. Dialling // down from 10 Hz took clk_prescale from 0 to 1, doubling the divider, // and the output jumped to exactly 5 Hz. That halving is what identified // the cause. // setFrequency() works because stopAll() is followed by setGen1()'s ~20 ms // prescaler search before any write happens -- and during those 20 ms the // timer really does reach TEZ and stop. The slow search is load-bearing. // Adjust Mode therefore calls setFrequency(1), the same call Protocol and // Sweep modes use. VERIFIED WORKING on the bench: frequency and duty both // track the display. The cost is both full searches plus a momentary restart // of channels 2-4 on every retune, which is why fast knob movement outpaces // the output and catches up when the knob stops. // A faster path was proposed and TRIED, and it FAILED -- do not try it again // without new information. The idea was to clear the update-method bits to // "immediate" (TIMER0_CFG0 bits 25:24 and GEN0_STMP_CFG bits 7:0, at MCPWM0 // base 0x3FF5E000 + 0x04 and + 0x3C) so the shadow registers would load as // soon as they were written, removing the need to stop the timer and allowing // the cheap 256-step locked-clock search on Channel 1 alone. On the bench it // was WORSE than setFrequency(1): the output changed but no longer matched the // display, and changes arrived seconds after the knob stopped moving. Reverted // in full (git revert of commit e779419). Either those bit positions are not // what they are believed to be on this silicon -- in which case the write was // corrupting the period field, which sits at bits 23:8 immediately below -- // or immediate loading is not what makes this peripheral latch. Five separate // attempts at a cheaper retune have now failed. setFrequency(1) is slow, costs // both full searches and a restart of all four channels, and works. Leave it // alone unless there is scope evidence for something better. // NOTE 25: Mode=4 (Adjust Mode) was an in-progress, never-finished feature. It was made // unreachable in V2.02c-V2.03 for a clean documentation baseline: Setup=2's mode // list did not mention it and rotaryLimits() clamped modeID to a max of 3. That // clamp is now 4 and the menu entry is back -- see NOTE 26. Setup=3 (Protocol // range) and Setup=4 (Sweep range / Freq. Inc.) remain always visible while // paging through screens, regardless of Mode, from that same cleanup. // NOTE 24: Added secondary program to read the SDram from the 4-Channel card reader inside the // Generator. // // // NOTE 23: Major upgrades. Channel 1 and 3 do fractional frequency now. Frequency now accurate to // to 5 significant digits on Channel 1 and 3. Channel 2 and 4 // use the same prescaler as their counter parts 1 and 3. So keep them close to // channel 1 and 3 in frequency and they will be pretty accurate. There will // be a write-up on how this works someday. :-) // Rotary encoder now super smooth with no bouncing. Thank to help from AI // NOTE 22: Fractional frequency problem. Presently will not do a fraction of // a frequency like 7.83 Hz. It presently ends up as doing it as 7.00 Hz // AND Protocal selection does not work. ****This version resolved the problem **** // **** Disreguard this NOTE **** // NOTE 21: Version 38 adds a single parameter to invert anyone of the 4 channel outputs // This mode is for the heat blanket designed with a P channel MOSFET or for the // Red and InfraRead LED Panels running off the Inverting Controller Box. The // parameter used is a define byte call "dutyFlip" with value set as a Hex number. // //************** Below NOTE 20: Does not presently work in this version ************** // NOTE 20: THE FOLLOWING FEATURE WAS LOST WHEN THE PROGRAM WAS UPGRADED TO DO FRACTIONAL // FREQUENCIES. This NOTE left in here just show history update record // Version 37 adds 180 degree out of phase capability for channel 1 and channel 2 // Where channel 2 is 180 out from channel 1 when channel 1 and 2 are at the same // frequency and the duty cycle is set to 50% for each channel. // Where channel 4 is 180 out from channel 3 when channel 3 and 4 are at the same // frequency and the duty cycle is set to 50% for each channel. // Because of the clock speed this routine works well up to 20khz. This mode will // not work at frequencies above 20 Khz. Great low frequency scalar wave // generator although any channel can still be set to frequencies as high as // 50 Khz with any size duty cycle. Works well with AURORASKY's 225 RED and INFRARED // LED light arrays using two of these arrays running 180 degrees out of phase. // SPECIAL: When changing frequencies by dial, start stop start to insure synchronization // // NOTE 19: Ver 36 fixed SD file read problem. // NOTE 18: Version 37b inverts channels 3 and 4 output (see setFrequency routine fro details) // NOTE 17: Screen Saver added. Goes random stars after 20 minutes in in idle mode // Press reset (the dial knob) to break out of screen saver mode // NOTE 16: Created an (a) & (b) version. Chan 3 & 4 (a) inverted = normal (b) not inverted // NOTE 15: Fixed negative direction Sweeps & improved routine in general // NOTE 14: Added frequency adjust multiplier to componsent for lost interrupt time. // NOTE 13: Added Suspend Button feature // NOTE 12: Added "wifidata.txt" file to allow updating or changing the WiFi SSID and // Password for WiFi connectivity. (See WiFi documentation for details) // NOTE 11: Changed 'VERSION' from a define to const char* & changed NOTE numbering scheme // NOTE 10: Fixed inversion of frequency in setFrequency() routine. Now idles at low vs high // NOTE 9: Version number now a #define value. // NOTE 8: Opened up Flash Memory Storage Size to 10800 bytes. Allows 275 protocol lines // NOTE 7: Now displays a Failed SD RAM if missing or corrupted. Displays file failure and // the file number that failed to read if it was a missing or corrupted file // NOTE 6: Updated file read capability. Now can select 1 of 10 possible generator files // stored on the micro SD ram card. (see Gen. File documentation for details) // NOTE 5: In "calcSweepParam()" changed "sweepFreqInc<0.0" to sweepFreqInc<=0.0 // NOTE 4: Added a debug option to turn on and off the Serial Printer 05-01-21 // This does not effect the setup and initialization functions/routines // NOTE 3: Inverted the channel outputs to compensate for the 2n7000 mosfet inversions // of the output signals. // NOTE 2: Created an UPDATE only function when "Setup" is not zero. In other words the // unit will only run in RUN mode when "Setup=0" else it is in UPDATE mode. // NOTE 1: The "#define wifiEnable" parameter enables the wireless feature added to this // software. When active the OLED will show the IP address generated - to link // to your computer or cell phone via the internet. Presently only offers a // Start/Stop generator feature. However, this addition is expandable. #include //#include "FS.h" #include "SD.h" #include "SPI.h" #include #include //#include //#include #define BAUD 115200 // IDE Serial Monitor baud rate #define DEBUG 1 // debug option where 0 = Serial.print OFF and 1 = ON #define debounceTime 350 // interruupt debouce time in milliseconds #define FLASH_SIZE 10800 // ESP32 Arduino EEPROM equivalent memory area byte size //#define FLASH_SIZE 8192 // ESP32 Arduino EEPROM equivalent memory area byte size #define FLASH_START 0 // ESP32 Arduino EEPROM equivalent memory area byte size // Ceiling on frequency records in one protocol file. The hardware ceiling is 298: // FLASH_SIZE holds 300 x 36-byte EPromFreqObject, but the 44-byte control record // has to fit after the last one, so (10800 - 44) / 36 = 298. Past that, // EEPROM.put() quietly fails its own bounds check and the control record -- mode, // both run times, all four group IDs -- is never stored, and fetchMem() can then // walk off the end looking for a terminator that was never written. 275 is the // documented limit in the manual and in the Python editor; keep all three in step. #define MAX_FREQ_RECORDS 275 //************************** #define PWM1_Pin 32 // define what GPIO pin to use for PWM #1 #define PWM2_Pin 33 // define what GPIO pin to use for PWM #2 #define PWM3_Pin 27 // define what GPIO pin to use for PWM #3 #define PWM4_Pin 14 // define what GPIO pin to use for PWM #4 //************************** #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels #define BLINK_LED 2 // Blinking LED for various status and error states #define line0X 4 // Proto (Frequency set Number) X position #define line00X 64 // #define line1X 0 // F1 starting curX postion #define line2X 1 // F2 starting curX postion #define line3X 2 // F2 starting curX postion #define line4X 3 // F2 starting curX postion #define line0XC 40 // X cursor Coordinate for setupOpt value #define line0Y 0 // Proto (frequency set number) Y position #define line0YC 2 // Proto Y Cursor position 2 #define line1YP 9 // 1st Y address for Protocol numbers #define line1Y 13 // F1 starting curY postion #define line2Y 27 // F2 starting curY postion 18 #define line3Y 41 // F2 starting curY postion 18 #define line4Y 55 // F2 starting curY postion 18 #define line1YC 15 // cursor for F1 starting curY postion #define line2YC 29 // cursor for F2 starting curY postion 20 #define line3YC 43 // cursor for F1 starting curY postion #define line4YC 57 // cursor for F2 starting curY postion 20 #define linePX 0 // Screen 5 Protocol File selection line X #define linePY 41 // Screen 5 Protocol File selection line Y #define linePXC 57 // Screen 5 Protocol File selection line y #define linePYC 43 // Screen 5 Protocol File selection //#define temp 720 // temperature limit for the MOSFET -- unused leftover from a // previous version, never wired to a real sensor; the "T=" OLED // readout that used this was commented out 2026-08-06 // Declaration for an SSD1306 display connected to I2C (SDA, SCL pins) #define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); //--------------------------------------------------------------------- // CODE INSERTION 1 STARTS HERE //--------------------------------------------------------------------- // ESP32 Quad Square Wave Generator V6.0 03-27-26 // Based on V5.2 — now with runtime-callable setGen1–setGen4 functions // and per-channel start/stop control. // // Runs all four MCPWM channels simultaneously: // Gen 1: MCPWM0, Timer 0, Operator 0 → GPIO 32 (MASTER — sets MCPWM0 clk_prescale) // Gen 2: MCPWM0, Timer 1, Operator 1 → GPIO 33 (SECONDARY — inherits MCPWM0 clk) // Gen 3: MCPWM1, Timer 0, Operator 0 → GPIO 27 (MASTER — sets MCPWM1 clk_prescale) // Gen 4: MCPWM1, Timer 1, Operator 1 → GPIO 14 (SECONDARY — inherits MCPWM1 clk) // // USAGE: // Call setGen1(freq, duty) ... setGen4(freq, duty) from loop() to load new parameters. // Call startGen(n) / stopGen(n) to start/stop individual channels (n = 1–4). // Call startAll() / stopAll() for convenience. // The PWMs run in hardware background — loop() is free for other tasks. #include "soc/mcpwm_struct.h" #include "driver/mcpwm.h" // ===== GPIO ASSIGNMENTS ===== #define GEN1_PIN 32 #define GEN2_PIN 33 #define GEN3_PIN 27 #define GEN4_PIN 14 #define BASE_CLK 160000000UL #define PERIP_CLK_EN_REG (*(volatile uint32_t*)0x3FF000C0) #define PERIP_RST_EN_REG (*(volatile uint32_t*)0x3FF000C4) volatile uint32_t* MCPWM0_TIMER_SEL = (volatile uint32_t*)0x3FF5E038; volatile uint32_t* MCPWM1_TIMER_SEL = (volatile uint32_t*)0x3FF6C038; // ===== RUNTIME STATE ===== volatile float gen1_Frequency = 0, gen1_DutyCycle = 0; volatile float gen2_Frequency = 0, gen2_DutyCycle = 0; volatile float gen3_Frequency = 0, gen3_DutyCycle = 0; volatile float gen4_Frequency = 0, gen4_DutyCycle = 0; bool genRunning[5] = {false, false, false, false, false}; // index 1–4 used // Saved prescaler values so secondaries can reference their master's clk_prescale uint16_t savedClkPre0 = 0; // MCPWM0 clk_prescale (set by Gen1) uint16_t savedClkPre1 = 0; // MCPWM1 clk_prescale (set by Gen3) //--------------------------------------------------------------------- // CODE INSERTION 1 ENDS //--------------------------------------------------------------------- bool encoderChanged = false; // Used in rotary_encoder routine byte againOnce; // a single shot to allow reading the SD 1 extra time byte change = 0; // byte change2 = 0; // Set if the cursor has moved byte change3 = 0; // used to designate "protoID" has changed byte curX; // byte curX1; // byte curX2; // byte curY; // byte curY1; // byte curY2; // byte dutyFlip = 0x0F; // Hex number to flip off state of dutyvalue(s) see setFrequency(x) byte fileChange; // file Change flag byte fileNo = 0; // initialize to zero "0" byte fileNoHold; // backup of "fileNo" byte fileRtnCode; // 0 = file loaded, 1 = will not open, 2 = no '#' header, // 3 = more than MAX_FREQ_RECORDS frequency records, // 4 = no 0,... end record, 5 = no frequency records byte sawCtrlRec; // set when readFileModified() meets the end record byte highMem; // byte intFlag; // byte lastAB = 0; // Used in Rotary Encoder routine byte lowFreqSw; // byte modeID; // byte noRead = 0; // readFile() routine inhibit flag byte presentState; // volatile byte runFlag = 0; // volatile: the red button's interrupt writes this, and // adjustModeRun() sits in a while(runFlag==1) loop waiting // for it. Without volatile the compiler is free to cache it // in a register and never notice Stop being pressed. byte runFlagHold; // byte runFmMemory = 0; // initialize "run From Memory" to 0 to read SD card byte setupOpt = 0; // 0=Read, 1=Set, 2=modes, 3=Protocol, 4=Sweep, 5=Time byte singleShot = 0; // byte timeOut = 0; // initialize to zero, used in Wi-fi and rotary routines byte wifiEnable = 0; // define if wifi is enabled = 1 and disabled = 0 const byte cursorPin = 4; // GPio 4 polling pin to monitor Cursor Button const byte interruptPin = 15; // GPio 15 as interrupt for RUNNING start and stop const byte rotaryA = 16; // GPio 16 polling pin for rotary switch const byte rotaryB = 17; // GPio 17 polling pin for rotary switch const char* ssid = "123456789012345678901234567890"; // ssid is a pointer to a 30 byte field const char* password = "0123456789012345678901234567890"; // password is a pointer to the field const char* VERSION = "2.05 09-02-26"; // present version number of this software volatile int rotaryCounter = 0; // volatile long rotaryMicrosA; // volatile long rotaryMicrosB; // char asciiChar; // int counter; // unsigned int eeAddress; // unsigned int eeAddress2; // int encoderPosition = 0; // Used in rotary_encoder routine int intCnt; // int lastDirection; // int pValue; // used for restoring to previous "value" int rotaryDirection; // int value; // int channel1 = 1; // assign a channel for each GPIO used int channel2 = 2; // assign a different channel #2 works well int channel3 = 4; // assign a different channel #3 doesn't work but #4 does int channel4 = 6; // #5 doesn't work but #6 does; unknown quirk ??? int memGrp; int numberOfInterrupts = 0; // float prescaler = 10.0; // Prescaler divide by counter designator, 10 works well float dutyConst = 1024; // Duty Constant 1024 = 100% with a prescaler of 10 int freqRecCnt = 0; // frequency records loaded from the current file, so the // MAX_FREQ_RECORDS ceiling can be enforced while reading int protoID = 1; // Protocal ID int protoIDhold; // used to check if protoID changed int protoIDlast; // obviously the last protoID available int rotaryT1 = 150; // Rotary Debounce Time in milliseconds int dutyValue1; // work field resulting from dutycycle calculation int dutyValue2; // work field resulting from dutycycle calculation int dutyValue3; // work field resulting from dutycycle calculation int dutyValue4; // work field resulting from dutycycle calculation long microDelay12; // Used to phase shift F1 and F2 frequencies 180 degrees long microAdjust12; // Used to phase shift F1 and F2 frequencies 180 degrees long microDelay34; // Used to phase shift F3 and F4 frequencies 180 degrees long microAdjust34; // Used to phase shift F3 and F4 frequencies 180 degrees int sizeofVar1; // Used in the loadfloats() routine int sizeofVar2; // used in the loadIntegers() routine int startFreqGrpID; // starting Frequency Group ID int startSweepGrpID; // starting Sweep Group ID int8_t stepAccumulator = 0; // Used in rotary_encoder routine // ----- Mode 4 (Adjust Mode) real-time knob state ----- // Adjust Mode reads the encoder from an interrupt instead of polling it, so // these are separate from the polled rotary() variables above and must be // volatile. See adjustEncoderISR() in rotary_Decode.ino for why. volatile byte adjustLastAB = 0; // last A/B pin state seen by the ISR volatile int8_t adjustStepAccum = 0; // quadrature accumulator, ISR only volatile int adjustDelta = 0; // detents counted since the loop last looked int stopFreqGrpID; // stopping Frequency Group ID int stopSweepGrpID; // stopping Sweep Group ID //int temper = temp; // temper contains the present MOSFET temperature -- unused, see temp above float FreqAdjust = 1.00065; // A percentage value to add to frequency to compensate for interrupts float F1 = 0.00; // used in Sweep function float F2 = 0.00; // frequency (in Hz) float F3 = 0.00; // frequency (in Hz) float F4 = 0.00; // frequency (in Hz) float D1 = 0.00; // Duty cycle float D2 = 0.00; // Duty cycle float D3 = 0.00; // Duty cycle float D4 = 0.00; // Duty cycle float d1 = 0.00; // used to compensate for harware inversion float d2 = 0.00; // see 'setFrequency' in spport_Routines float d3 = 0.00; // used to compensate for harware inversion float d4 = 0.00; // see 'setFrequency' in spport_Routines float sweepFreqInc = 0.00; // sweep Frequency Increment value float sweepAccum; // sweep Accummulator variable float sweepAccumInc; // sweep Accummulator Increment variable float sweepF1; // sweep function work frequency 1 float sweepF2; // sweep function work frequency 2 float sweepRestart; // sweep restart save area //float sweepTempF1; // used to swap SweepF1 and SweepF2 around //float sweepWork1; // a floating work field for 'calcSweepParams' function //float sweepWork2; // a floating work field for 'calcSweepParams' function long randNumX; // Random number generator for Screen Saver x coordinate long randNumY; // Random number generator for Screen Saver y corrdinate signed long endTime1; // 180 seconds, freq set time, default = 180000 = 3 minutes signed long endTime2; // 1 minute, protocol time, default = 1800000 = 30 minutes unsigned long FLASH_LAST; // Not presently used volatile unsigned long interruptDebounce; // volatile: written by the red button's // interrupt AND by displayScreenSaver() swallowing a wake // press, the same reason runFlag is volatile unsigned long microTime12; // unsigned long microTime34; // unsigned long runTime; // work run Time field unsigned long runTime1; // working field for running time of a frequency set unsigned long runTime2; // working field for running time of a protocol unsigned long savePgmTime; // this is for restoring time left for program time unsigned long saveRunTime; // to restore time left for frequency runtime unsigned long saverTime; // used for Screen Saver start time // *** TEMPORARY FOR TESTING -- PUT THIS BACK TO 1200000 (20 minutes) *** //unsigned long saverStartTime = 60000; // 1 minute, so the screen saver can be tested unsigned long saverStartTime = 1200000; // when to start screen saver in milliseconds // without a twenty minute wait between goes signed int sweepNumCnt; // Sweep Number Count signed int sweepShowID; // Group ID shown as "ID=" while sweeping: the start Sweep Group // for the whole sweep, the stop Sweep Group on the step that // actually lands on the sweep limit frequency unsigned long timeOutVal; // used in Rotary function call unsigned long workLong; // used to translate FLASH data to intergers or floats String ssidStr = "0123456789012345678901234567890"; String passwordStr = "0123456789012345678901234567890"; String workStr = "1234567890123456"; // Collection string for for 'protoID' reserve 16 charachter char fileName[20] = "/protocols-0.txt"; struct EPromFreqObject { // This structure contains the two channel basics int field0; // protoID = Memory Group number float field1; // F1 = Frequency 1 float field2; // D1 = Duty Cycle 1 float field3; // F2 = Frequnecy 2 float field4; // D2 = Duty Cycle 2 float field5; // F3 = Frequency 3 float field6; // D3 = Duty Cycle 3 float field7; // F4 = Frequnecy 4 float field8; // D4 = Duty Cycle 4 }; struct EPromControlObject { // structure contains startup and range defaults int field0; // Last Memory Group used, always identified with a zero '0' int field1; // protoID to start with if in Simple Mode byte field2; // mode of operation SIMPLE, PROTOCOL, or SWEEP unsigned long field3; // endTime1, frequency run time, stored internally in ms; CSV files // hold this as whole seconds (see readFileModified()/saveFile()) unsigned long field4; // endTime2, program run time, stored internally in ms; CSV files // hold this as whole minutes (see readFileModified()/saveFile()) int field5; // start group for Frequency Range int field6; // stop group for Frequency Range int field7; // sweep start group for Sweep Range int field8; // sweep stop group for Sweep Range float field9; // sweep frequency increment value byte field10; // file ID = presently up to 9 different file allowed }; portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; // This function prevents concurrent access of // the 'interrupt' used in two places void IRAM_ATTR handleInterrupt() { // IRAM_ATTR puts this code in special interrupt memory area portENTER_CRITICAL_ISR(&mux); // prevent concurrent access of 'interrupt' // Subtract-then-compare, not a plain <. millis() wraps back to zero every 49.7 // days; on a plain comparison the wrap makes a deadline in the future look // already past. The subtraction wraps with it, so the test stays correct. if ((long)(millis() - interruptDebounce) >= 0) { runFlag ^= true; // Exclusive OR will flip back forth between 1 and 0 interruptDebounce = millis() + debounceTime; } portEXIT_CRITICAL_ISR(&mux); // allow access to 'interrupt' } // ************* See WiFi TAB for available Routines **************** WiFiServer server(80); // ********************INITIAL Part of SETUP ***************************** void setup() { curX = line0XC; // Changes depending what line position is displayed curY = line0YC; // Changes depending what line is displayed pinMode(BLINK_LED, OUTPUT); // do first so you can use it pinMode(interruptPin, INPUT_PULLUP); // setup interrupt pin with pullup interruptDebounce = millis(); // initialize interruptDebounce variable attachInterrupt(digitalPinToInterrupt(interruptPin), handleInterrupt, FALLING); pinMode(cursorPin, INPUT_PULLUP); pinMode(rotaryA, INPUT_PULLUP); pinMode(rotaryB, INPUT_PULLUP); lastAB = (digitalRead(rotaryA) << 1) | digitalRead(rotaryB); Serial.begin(BAUD); // Initialize the internal IDE serial port for (int i = 1; i > 0; --i) { blinker(5, 125); // waste some time before starting interfact to monitor Serial.print("\n"); // This does a nice job on clearing the print buffer } Serial.print("ESP32 4 Channel Generator Ver "); Serial.println(VERSION); Serial.print("\n"); Serial.begin(115200); delay(500); // ===== MCPWM0 PERIPHERAL ENABLE ===== PERIP_RST_EN_REG |= 1 << 17; PERIP_RST_EN_REG &= ~(1 << 17); PERIP_CLK_EN_REG |= 1 << 17; *MCPWM0_TIMER_SEL = (1 << 2); // Op0→T0, Op1→T1 // ===== MCPWM1 PERIPHERAL ENABLE ===== PERIP_RST_EN_REG |= 1 << 20; PERIP_RST_EN_REG &= ~(1 << 20); PERIP_CLK_EN_REG |= 1 << 20; *MCPWM1_TIMER_SEL = (1 << 2); // Op0→T0, Op1→T1 // ===== ATTACH GPIOs ===== mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM0A, GEN1_PIN); mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM1A, GEN2_PIN); mcpwm_gpio_init(MCPWM_UNIT_1, MCPWM0A, GEN3_PIN); mcpwm_gpio_init(MCPWM_UNIT_1, MCPWM1A, GEN4_PIN); if(DEBUG == 2) { Serial.println("==== ESP32 Quad PWM Generator V6.0 ===="); Serial.println("Hardware initialized. Call setGen1–setGen4 to configure."); Serial.println("Call startGen(n)/stopGen(n) or startAll()/stopAll()."); Serial.println("========================================"); } // ===== EXAMPLE: Load parameters and start all four generators ===== /* setGen1(4.0, 60.0); // Gen1: 4 Hz, 60% duty setGen2(7.83, 60.0); // Gen2: 7.83 Hz, 60% duty setGen3(740.0, 60.0); // Gen3: 740 Hz, 60% duty setGen4(10000.0, 60.0); // Gen4: 10 kHz, 60% duty genRunning[1] = true; genRunning[2] = true; genRunning[3] = true; genRunning[4] = true; */ // ************************************************************************** // // ************* initialize EEPROM with predefined size ********************* // EEPROM.begin(FLASH_SIZE); // ESP32 Flash Ram EEPROM look alike area begins FLASH_LAST = FLASH_SIZE - 36; // setup Previous run information // ***************** OLED initialization part of 'setup' ***************** if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { delay(3000); Serial.println("SSD1306 allocation failed"); while (1) { // if here SSD1306 failed to start blinker(50, 25); // Do 50 blink loops with 25 millisecond blink delay } } value = 0; pValue = value; display.setTextSize(1); display.setTextColor(WHITE); display.clearDisplay(); display.display(); // ******************* micro SD part of SETUP *************************** fileFetch(); // unconditionally load file "protocols-0.txt" fileNo = 0; // File = 0 always on boot, regardless of whatever value fileNoHold = 0; // happens to be stored in protocols-0.txt's own control // record -- field10 only ever means something transient, // in-session, while the operator is actively requesting a // different preset (see the preset-switch logic in loop()) // ************* ****** Check if updating the file ********************************** // This part of the code development was started on 06-25-26. It addes direct * // connectiveity to a computer through the ESP32 USB port to modified whatever * // protocol-x.txt that is loaded into the EProm. It was developed as an alternative * // to removing the micro-SD card to make changes to the Generator run parameters * // ********************************************************************************** // if(!digitalRead(cursorPin)){ // check if you are going to run this code usbcomm(); // if here set up to talk to the USB port } // ***************** Wifi portion of the "setup" function ********************* // We start by connecting to a WiFi network if (wifiEnable == 1) { Serial.println(); Serial.println(); Serial.print("Connecting to "); Serial.println(ssid); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { timeOut+=1; delay(500); Serial.print("."); if(timeOut > 9) break; } display.clearDisplay(); // Clear the OLED buffer display.setCursor(0, 0); // Set the OLED cursor position display.setTextSize(2); // Set the OLED text size if(timeOut > 9){ wifiEnable = 0; // Disable WiFi display.print("WiFi Fail-"); // Display Not Connected display.setCursor(0, 16); // Set the OLED cursor position display.print("ed, Moving"); // Display verbiage display.setCursor(0, 32); // Set the OLED cursor position display.print("On in 10"); // Display verbiage display.setCursor(0, 48); // Set the OLED cursor position display.print("Seconds"); // Display verbiage } else { Serial.println(""); Serial.println("WiFi connected."); Serial.println("IP address: "); Serial.println(WiFi.localIP()); wifiEnable = 1; // Set Connected display.print("Connected"); // Display Connected display.setCursor(0, 20); // Set the OLED cursor position display.print("IP Address"); // Display verbiage display.setCursor(0, 48); // Set the OLED cursor position display.setTextSize(1); // Set the OLED text size display.print(WiFi.localIP()); // display connection IP address server.begin(); // connnect to the server } display.display(); // display the message delay(10000); // give it some time to read display.clearDisplay(); // Clear the OLED buffer display.setTextSize(1); // Set the OLED text size } } // ***************** MAIN LOOP of this PROGRAM *********************** void loop() { if (wifiEnable == 1) // check if wifi Enabled wifiRoutine(); // on condition check for wifi on/off request if (runFlag == 1) { // on condition START RUNNING if (setupOpt==0){ // on condition continue with running if (singleShot == 1) { if (modeID != 1 && modeID !=4) { // Modes 1 and 4 are deliberately excluded: both // start from whatever F1..D4 are showing on // Setup=0 right now. Dialing protoID there already // called fetchProtoID(), so those globals hold the // selected record -- including any Setup=1 edits // not yet saved. Re-fetching here would throw // those away. Modes 2 and 3 do need a fetch, // because they start from a range/sweep group. if (modeID == 2) // check if protoID needs updating protoID = startFreqGrpID; // update on condition if (modeID == 3) { // calcSweepParams(); // calculate the Sweep Parameters protoID = startSweepGrpID; // } fetchProtoID(protoID); // update to selected protoID parameters } setFrequency(1); // set the four channel frequencies if(modeID!=4) // Mode 4 paints its own screen from displayRunning(); // inside adjustModeRun(), below saveCurrent(); // Save any changes to Flash Memory ?? singleShot = 0; // only run this part of the loop once setClocks(1); // set the runtime clocks to active mode } if(modeID==4) adjustModeRun(); // real-time knob control of Channel 1. // Does not return until Stop is pressed. else checkTime(); // if here check time } else { checkSaving(); // check if updating the SDram desired displayUpdate(); // display screen in running mode saveCurrent(); // Save any changes to Flash Memory ?? if (fileChange == 1) { byte requestedFileNo = fileNo; // fileFetch() is about to overwrite fileNo from // the loaded file's OWN stored control record -- // remember what was actually requested before that fileFetch(); // on condition go load a new file if (requestedFileNo != 0) { // a preset (non-zero) file becomes the new default fileNo = 0; // protocols-0.txt is always the live/default file fileNoHold = 0; // saveFile() derives its target filename from this saveFile(); // write the freshly-loaded EEPROM data to protocols-0.txt fileNo = 0; // saveFile()'s internal fetchProtoID(0) re-reads // fileNo from EEPROM (the preset's own stored // value) -- force it back to 0 so the display // reflects reality immediately, not just after reboot } } singleShot = 0; // only run this part of the loop once runFlag = 0; // insure RUN mode is cancelled } } else { // on condition STOP RUNNING if (singleShot == 0) { setFrequency(0); // (0) turns off the four channel frequencies setClocks(0); // set the runtime clocks to INactive mode if (modeID == 3) // A sweep dials F1 itself, and it does that fetchProtoID(startSweepGrpID); // while protoID is pointing at the START sweep // group -- so on the way out, that group is // sitting in the globals holding whatever // frequency the sweep had reached, not its own. // Left there it is displayed as the group's F1, // and the next saveCurrent() -- which runs // whenever Start is pressed on a Setup page -- // writes it into the record for real. The sweep // then begins from wherever the last one stopped // instead of from the range start, and nothing // repeats. Put the stored record back. Display(); // display screen in dial mode singleShot = 1; // only run this part of the loop once saverTime=saverStartTime+millis(); // setup to when to start screen saver } protoIDhold = protoID; // save modeID 1 protoID value rotary(); // NOTE: rotary could change things like protoID cursorMove(); // check for cursor move request if (protoID != protoIDhold) // check if protoID got changed fetchProtoID(protoID); // if here fetch the new protoID parameters if (change == 1 || change2 == 1) { //change=1 a number has changed, change2=1 the cursor has changed Display(); saverTime=saverStartTime+millis(); // Somebody is using the panel, so push the } // screen saver back out. Without this the 20 // minutes runs from the moment the unit // STOPPED rather than from the last thing // touched, and it can arrive in the middle // of an edit. rotary() only sets "change" // when the knob really moved -- it returns // early otherwise -- so this is genuine // activity, not every pass of the loop. if((long)(millis() - saverTime) >= 0){ // wrap-safe, see handleInterrupt() displayScreenSaver(); } } }