From 34b1cb2646542be491c213fb8d7ddbb05c7682ad Mon Sep 17 00:00:00 2001 From: Luke McKenzie Date: Sun, 14 Oct 2018 23:21:12 -0500 Subject: [PATCH] millis() rollover fixes; reintroduce anti-poisoning options --- README.md | 2 +- .../configs/v8c-6tube-relayswitch-pwm-top.h | 2 +- .../configs/v8c-6tube-relayswitch-pwm.h | 94 +++++++++++++++++++ sixtube_lm/sixtube_lm.ino | 94 +++++++++++-------- 4 files changed, 152 insertions(+), 40 deletions(-) create mode 100644 sixtube_lm/configs/v8c-6tube-relayswitch-pwm.h diff --git a/README.md b/README.md index fa946f5..e20bb31 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Later UNDBs (v8 with mods, or v9+) are equipped with controllable LEDs, as well | 6 | Auto DST | Add 1h for daylight saving time between these dates (at 2am):
0 = off
1 = second Sunday in March to first Sunday in November (US/CA)
2 = last Sunday in March to last Sunday in October (UK/EU)
3 = first Sunday in April to last Sunday in October (MX)
4 = last Sunday in September to first Sunday in April (NZ)
5 = first Sunday in October to first Sunday in April (AU)
6 = third Sunday in October to third Sunday in February (BZ) | | 7 | LED behavior | 0 = always off
1 = always on
2 = on, but follow night/away modes if enabled
3 = off, but on when alarm/timer sounds
4 = off, but on with switched relay (if equipped)
(Clocks with LED control only, UNDB v8+) | | 8 | Temperature format | 0 = Celsius
1 = Fahrenheit
(Clocks with temperature function enabled only) | -| 9 | Anti-cathode poisoning | Briefly cycles all digits to prevent [cathode poisoning](http://www.tube-tester.com/sites/nixie/different/cathode%20poisoning/cathode-poisoning.htm) | +| 9 | Anti-cathode poisoning | Briefly cycles all digits to prevent [cathode poisoning](http://www.tube-tester.com/sites/nixie/different/cathode%20poisoning/cathode-poisoning.htm)
0 = once a day, at "Night ends at" time (or alarm time if applicable)
1 = at midnight
2 = at the top of every hour
3 = at the top of every minute | | | **Alarm** | | | 10 | Alarm days | 0 = every day
1 = work week only (per settings below)
2 = weekend only | | 11 | Alarm signal | 0 = beeper
1 = relay (if in switch mode, will stay on for 2 hours)
(Clocks with both beeper and relay only) | diff --git a/sixtube_lm/configs/v8c-6tube-relayswitch-pwm-top.h b/sixtube_lm/configs/v8c-6tube-relayswitch-pwm-top.h index 7912b31..4be84d9 100644 --- a/sixtube_lm/configs/v8c-6tube-relayswitch-pwm-top.h +++ b/sixtube_lm/configs/v8c-6tube-relayswitch-pwm-top.h @@ -1,4 +1,4 @@ -//v8 with B style modification, and flipped Sel and Alt buttons so Sel is in the front when buttons are mounted display-side with IN-8-A display +//v8 with C style modification and flipped Sel and Alt buttons so Sel is in the front when buttons are mounted display-side with IN-8-A display const byte displaySize = 6; //number of tubes in display module. Small display adjustments are made for 4-tube clocks diff --git a/sixtube_lm/configs/v8c-6tube-relayswitch-pwm.h b/sixtube_lm/configs/v8c-6tube-relayswitch-pwm.h new file mode 100644 index 0000000..8e044a8 --- /dev/null +++ b/sixtube_lm/configs/v8c-6tube-relayswitch-pwm.h @@ -0,0 +1,94 @@ +//v8 with C style modification + +const byte displaySize = 6; //number of tubes in display module. Small display adjustments are made for 4-tube clocks + +// available clock functions, and unique IDs (between 0 and 200) +const byte fnIsTime = 0; +const byte fnIsDate = 1; +const byte fnIsAlarm = 2; +const byte fnIsTimer = 3; +const byte fnIsDayCount = 4; +const byte fnIsTemp = 5; +const byte fnIsTubeTester = 6; //cycles all digits on all tubes 1/second, similar to anti-cathode-poisoning cleaner +// functions enabled in this clock, in their display order. Only fnIsTime is required +const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer, fnIsDayCount}; //, fnIsTemp, fnIsTubeTester +// To control which of these display persistently vs. switch back to Time after a few seconds, search "Temporary-display mode timeout" + +// These are the RLB board connections to Arduino analog input pins. +// S1/PL13 = Reset +// S2/PL5 = A1 +// S3/PL6 = A0 +// S4/PL7 = A6 +// S5/PL8 = A3 +// S6/PL9 = A2 +// S7/PL14 = A7 +// A6-A7 are analog-only pins that aren't quite as responsive and require a physical pullup resistor (1K to +5V), and can't be used with rotary encoders because they don't support pin change interrupts. + +// What input is associated with each control? +const byte mainSel = A6; +const byte mainAdjUp = A0; +const byte mainAdjDn = A1; +const byte altSel = A7; //if not equipped, set to 0 + +// What type of adj controls are equipped? +// 1 = momentary buttons. 2 = quadrature rotary encoder. +const byte mainAdjType = 1; + +//What are the signal pin(s) connected to? +const char piezoPin = 10; +const char relayPin = A3; +// -1 to disable feature (no relay item equipped); A3 if equipped (UNDB v8) +const byte relayMode = 0; //If relay is equipped, what does it do? +// 0 = switched mode: the relay will be switched to control an appliance like a radio or light fixture. If used with timer, it will switch on while timer is running (like a "sleep" function). If used with alarm, it will switch on when alarm trips; specify duration of this in switchDur. +// 1 = pulsed mode: the relay will be pulsed, like the beeper is, to control an intermittent signaling device like a solenoid or indicator lamp. Specify pulse duration in relayPulse. +const word signalDur = 180; //sec - when pulsed signal is going, pulses are sent once/sec for this period (e.g. 180 = 3min) +const word switchDur = 7200; //sec - when alarm triggers switched relay, it's switched on for this period (e.g. 7200 = 2hr) +const word piezoPulse = 500; //ms - used with piezo via tone() +const word relayPulse = 200; //ms - used with pulsed relay + +//Soft power switches +const byte enableSoftAlarmSwitch = 1; +// 1 = yes. Alarm can be switched on and off when clock is displaying the alarm time (fnIsAlarm). +// 0 = no. Alarm will be permanently on. Use with switched relay if the appliance has its own switch on this relay circuit. +const byte enableSoftPowerSwitch = 1; //works with switched relay only +// 1 = yes. Relay can be switched on and off directly when clock is displaying time of day (fnIsTime). This is useful if connecting an appliance (e.g. radio) that doesn't have its own switch, or if replacing the clock unit in a clock radio where the clock does all the switching (e.g. Telechron). +// 0 = no. Use if the connected appliance has its own power switch (independent of this relay circuit) or does not need to be manually switched. + +//LED circuit control with PWM +const char ledPin = 9; +// -1 to disable feature; 11 if equipped (UNDB v8 modded) + +//When display is dim/off, a press will light the tubes for how long? +const byte unoffDur = 10; //sec + +// How long (in ms) are the button hold durations? +const word btnShortHold = 1000; //for setting the displayed feataure +const word btnLongHold = 3000; //for for entering options menu +const byte velThreshold = 150; //ms +// When an adj up/down input (btn or rot) follows another in less than this time, value will change more (10 vs 1). +// Recommend ~150 for rotaries. If you want to use this feature with buttons, extend to ~400. + +// What is the "frame rate" of the tube cleaning and display scrolling? up to 65535 ms +const word cleanSpeed = 200; //ms +const word scrollSpeed = 100; //ms - e.g. scroll-in-and-out date at :30 - to give the illusion of a slow scroll that doesn't pause, use (timeoutTempFn*1000)/(displaySize+1) - e.g. 714 for displaySize=6 and timeoutTempFn=5 + +// What are the timeouts for setting and temporarily-displayed functions? up to 65535 sec +const unsigned long timeoutSet = 120; //sec +const unsigned long timeoutTempFn = 5; //sec + +//This clock is 2x3 multiplexed: two tubes powered at a time. +//The anode channel determines which two tubes are powered, +//and the two SN74141 cathode driver chips determine which digits are lit. +//4 pins out to each SN74141, representing a binary number with values [1,2,4,8] +const char outA1 = 2; +const char outA2 = 3; +const char outA3 = 4; +const char outA4 = 5; +const char outB1 = 6; +const char outB2 = 7; +const char outB3 = 8; +const char outB4 = 16; //A2 - was 9 before PWM fix pt2 +//3 pins out to anode channel switches +const char anode1 = 11; +const char anode2 = 12; +const char anode3 = 13; \ No newline at end of file diff --git a/sixtube_lm/sixtube_lm.ino b/sixtube_lm/sixtube_lm.ino index 754a1fb..c3ff7ba 100644 --- a/sixtube_lm/sixtube_lm.ino +++ b/sixtube_lm/sixtube_lm.ino @@ -69,11 +69,11 @@ Some are skipped when they wouldn't apply to a given clock's hardware config, se //Option numbers/order can be changed (though try to avoid for user convenience); //but option locs should be maintained so EEPROM doesn't have to be reset after an upgrade. // General Alarm Timer Strike Night and away mode -const byte optsNum[] = { 1, 2, 3, 4, 5, 6, 7, 8, 10,11,12,13, 20,21,22, 30,31,32, 40, 41, 42,43,44,45, 46, 47}; // 9, -const byte optsLoc[] = {16,17,18,19,20,22,26,45, 23,42,39,24, 25,43,40, 21,44,41, 27, 28, 30,32,33,34, 35, 37}; //46, -const word optsDef[] = { 2, 1, 0, 0, 5, 0, 1, 0, 0, 0,61, 9, 0, 0,61, 0, 0,61, 0,1320, 360, 0, 1, 5, 480,1020}; // 0, -const word optsMin[] = { 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,49, 0, 0, 0,49, 0, 0,49, 0, 0, 0, 0, 0, 0, 0, 0}; // 0, -const word optsMax[] = { 2, 5, 3, 1,20, 6, 4, 1, 2, 1,88,60, 1, 1,88, 4, 1,88, 2,1439,1439, 2, 6, 6,1439,1439}; // 3, +const byte optsNum[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11,12,13, 20,21,22, 30,31,32, 40, 41, 42,43,44,45, 46, 47}; +const byte optsLoc[] = {16,17,18,19,20,22,26,45,46, 23,42,39,24, 25,43,40, 21,44,41, 27, 28, 30,32,33,34, 35, 37}; +const word optsDef[] = { 2, 1, 0, 0, 5, 0, 1, 0, 0, 0, 0,61, 9, 0, 0,61, 0, 0,61, 0,1320, 360, 0, 1, 5, 480,1020}; +const word optsMin[] = { 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,49, 0, 0, 0,49, 0, 0,49, 0, 0, 0, 0, 0, 0, 0, 0}; +const word optsMax[] = { 2, 5, 3, 1,20, 6, 4, 1, 3, 2, 1,88,60, 1, 1,88, 4, 1,88, 2,1439,1439, 2, 6, 6,1439,1439}; //RTC objects DS3231 ds3231; //an object to access the ds3231 specifically (temp, etc) @@ -86,6 +86,7 @@ byte btnCur = 0; //Momentary button currently in use - only one allowed at a tim byte btnCurHeld = 0; //Button hold thresholds: 0=none, 1=unused, 2=short, 3=long, 4=set by btnStop() unsigned long inputLast = 0; //When a button was last pressed unsigned long inputLast2 = 0; //Second-to-last of above +//TODO the math between these two may fail very rarely due to millis() rolling over while setting. Need to find a fix. I think it only applies to the rotary encoder though. const byte fnOpts = 201; //fn values from here to 255 correspond to options in the options menu byte fn = fnIsTime; //currently displayed fn (per fnsEnabled) @@ -102,7 +103,7 @@ word signalRemain = 0; //alarm/timer signal timeout counter, seconds word snoozeRemain = 0; //snooze timeout counter, seconds word timerInitial = 0; //timer original setting, seconds - up to 18 hours (64,800 seconds - fits just inside a word) word timerRemain = 0; //timer actual counter -unsigned long signalPulseStopTime = 0; //to stop beeps after a time +unsigned long signalPulseStartTime = 0; //to keep track of individual pulses so we can stop them word unoffRemain = 0; //un-off (briefly turn on tubes during full night or away modes) timeout counter, seconds byte displayDim = 2; //dim per display or function: 2=normal, 1=dim, 0=off byte cleanRemain = 11; //anti-cathode-poisoning clean timeout counter, increments at cleanSpeed ms (see loop()). Start at 11 to run at clock startup @@ -136,19 +137,18 @@ void setup(){ initOutputs(); //depends on some EEPROM settings } -unsigned long pollLast = 0; //every 50ms unsigned long pollCleanLast = 0; //every cleanSpeed ms unsigned long pollScrollLast = 0; //every scrollSpeed ms void loop(){ unsigned long now = millis(); //If we're running a tube cleaning, advance it every cleanSpeed ms. - if(cleanRemain && pollCleanLast+cleanSpeed=cleanSpeed) { //account for rollover pollCleanLast=now; cleanRemain--; updateDisplay(); } //If we're scrolling an animation, advance it every scrollSpeed ms. - else if(scrollRemain!=0 && scrollRemain!=-128 && pollScrollLast+scrollSpeed=scrollSpeed) { pollScrollLast=now; if(scrollRemain<0) { scrollRemain++; updateDisplay(); @@ -158,7 +158,6 @@ void loop(){ } } //Every loop cycle, check the RTC and inputs (previously polled, but works fine without and less flicker) - pollLast=now; checkRTC(false); //if clock has ticked, decrement timer if running, and updateDisplay checkInputs(); //if inputs have changed, this will do things + updateDisplay as needed doSetHold(); //if inputs have been held, this will do more things + updateDisplay as needed @@ -197,6 +196,7 @@ void checkBtn(byte btn){ //Changes in momentary buttons, LOW = pressed. //When a button event has occurred, will call ctrlEvt bool bnow = readInput(btn); + unsigned long now = millis(); //If the button has just been pressed, and no other buttons are in use... if(btnCur==0 && bnow==LOW) { btnCur = btn; btnCurHeld = 0; inputLast2 = inputLast; inputLast = millis(); @@ -204,11 +204,11 @@ void checkBtn(byte btn){ } //If the button is being held... if(btnCur==btn && bnow==LOW) { - if(millis() >= inputLast+btnLongHold && btnCurHeld < 3) { + if((unsigned long)(now-inputLast)>=btnLongHold && btnCurHeld < 3) { //account for rollover btnCurHeld = 3; ctrlEvt(btn,3); //hey, the button has been long-held } - else if(millis() >= inputLast+btnShortHold && btnCurHeld < 2) { + else if((unsigned long)(now-inputLast)>=btnShortHold && btnCurHeld < 2) { btnCurHeld = 2; ctrlEvt(btn,2); //hey, the button has been short-held } @@ -232,15 +232,15 @@ void checkRot(){ AdaEncoder *thisEncoder=NULL; thisEncoder = AdaEncoder::genie(); if(thisEncoder!=NULL) { - unsigned long inputThis = millis(); - if(inputThis-inputLast < 70) return; //ignore inputs that come faster than a human could rotate + unsigned long now = millis(); + if((unsigned long)(now-inputLast)<70) return; //ignore inputs that come faster than a human could rotate int8_t clicks = thisEncoder->query(); //signed number of clicks it has moved byte dir = (clicks<0?0:1); clicks = abs(clicks); for(byte i=0; i=250) { + doSetHoldLast = now; if(fnSetPg!=0 && (mainAdjType==1 && (btnCur==mainAdjUp || btnCur==mainAdjDn)) ){ //if we're setting, and this is an adj btn bool dir = (btnCur==mainAdjUp ? 1 : 0); //If short hold, or long hold but high velocity isn't supported, use low velocity (delta=1) @@ -603,20 +604,21 @@ void checkRTC(bool force){ //Checks display timeouts; //checks for new time-of-day second -> decrements timers and checks for timed events; //updates display for "running" functions. + unsigned long now = millis(); //Things to do every time this is called: timeouts to reset display. These may force a tick. - if(pollLast > inputLast){ //don't bother if the last input (which may have called checkRTC) was more recent than poll - //Option/setting timeout: if we're in the options menu, or we're setting a value - if(fnSetPg || fn>=fnOpts){ - if(pollLast>inputLast+(timeoutSet*1000)) { fnSetPg = 0; fn = fnIsTime; force=true; } //Time out after 2 mins - } - //Temporary-display mode timeout: if we're *not* in a permanent one (time, day counter, temp, tester, or running/signaling timer) - else if(fn!=fnIsTime && fn!=fnIsTubeTester && fn!=fnIsDayCount && fn!=fnIsTemp && !(fn==fnIsTimer && (timerRemain>0 || signalRemain>0))){ - if(pollLast>inputLast+(timeoutTempFn*1000)) { fnSetPg = 0; fn = fnIsTime; force=true; } - } - //Stop a signal beep if it's time to - if(signalPulseStopTime && signalPulseStopTime=fnOpts){ + if((unsigned long)(now-inputLast)>=timeoutSet*1000) { fnSetPg = 0; fn = fnIsTime; force=true; } //Time out after 2 mins } + //Temporary-display mode timeout: if we're *not* in a permanent one (time, day counter, temp, tester, or running/signaling timer) + else if(fn!=fnIsTime && fn!=fnIsTubeTester && fn!=fnIsDayCount && fn!=fnIsTemp && !(fn==fnIsTimer && (timerRemain>0 || signalRemain>0))){ + if((unsigned long)(now-inputLast)>=timeoutTempFn*1000) { fnSetPg = 0; fn = fnIsTime; force=true; } + } + //Stop a signal pulse if it's time to + //This is only used for relay pulses, since beeper beep durations are done via tone() + //So we can safely assume the length of the pulse should be relayPulse + if(signalPulseStartTime && (unsigned long)(now-signalPulseStartTime)>=relayPulse) { signalPulseStop(); signalPulseStartTime = 0; } //Update things based on RTC tod = rtc.now(); @@ -636,15 +638,29 @@ void checkRTC(bool force){ fnSetPg = 0; fn = fnIsTime; signalStart(fnIsAlarm,1,0); } //end toddow check } //end alarm trigger - //check if we should trigger the cleaner (at night end time, or alarm time if night end is 0:00) - if(tod.hour()*60+tod.minute()==(readEEPROM(30,true)==0?readEEPROM(0,true):readEEPROM(30,true))) { + //If cleaner is set to option value 0 (at night end time, or alarm time if night end is 0:00), run it at that time + if(readEEPROM(46,false)==0 && tod.hour()*60+tod.minute()==(readEEPROM(30,true)==0?readEEPROM(0,true):readEEPROM(30,true))) { cleanRemain = 11; //loop() will pick this up - } //end cleaner check + } } if(tod.second()==30 && fn==fnIsTime && fnSetPg==0 && unoffRemain==0) { //At bottom of minute, maybe show date - not when unoffing - if(readEEPROM(18,false)>=2) { fn = fnIsDate; inputLast = pollLast; updateDisplay(); } + if(readEEPROM(18,false)>=2) { fn = fnIsDate; inputLast = now; updateDisplay(); } if(readEEPROM(18,false)==3) { startScroll(); } } + if(tod.second()==1) { //If cleaner is set to option value >0, run the cleaner at second :01 as applicable + switch(readEEPROM(46,false)) { + case 1: //at 00:00:01 + if(tod.hour()==0 && tod.minute()==0) cleanRemain = 11; + break; + case 2: //at :00:01 + if(tod.minute()==0) cleanRemain = 11; + break; + case 3: //at :01 + cleanRemain = 11; + break; + default: break; //case 0 is handled at top of minute + } + } //Strikes - only if fn=clock, not setting, not night/away. Setting 21 will be off if signal type is no good //Short pips before the top of the hour @@ -679,7 +695,7 @@ void checkRTC(bool force){ if(readEEPROM(25,false)) { //interval timer: a short signal and restart; don't change to timer fn signalStart(fnIsTimer,0,0); timerRemain = timerInitial; } else { - fnSetPg = 0; fn = fnIsTimer; inputLast = pollLast; signalStart(fnIsTimer,signalDur,0); + fnSetPg = 0; fn = fnIsTimer; inputLast = now; signalStart(fnIsTimer,signalDur,0); } } //end not switched relay } //end timer elapsed @@ -1044,7 +1060,7 @@ void cycleDisplay(){ //But if we're setting, decide here to dim for every other 500ms since we started setting if(fnSetPg>0) { if(setStartLast==0) setStartLast = mils; - dim = 1-(((mils-setStartLast)/500)%2); + dim = 1-(((unsigned long)(mils-setStartLast)/500)%2); } else { if(setStartLast>0) setStartLast=0; } @@ -1065,8 +1081,9 @@ void cycleDisplay(){ // at 0ms, next = (( 0*(6-1))/20)+1 = 1; last = (6-nextDur) = 5; // at 10ms, next = ((10*(6-1))/20)+1 = 3; last = (6-nextDur) = 3; ... // at 20ms, next = ((20*(6-1))/20)+1 = 6; next = total, so fade is over! - //TODO facilitate longer fades by writing a tweening function that smooths the frames, i.e. 111121222 - fadeNextDur = (((mils-fadeStartLast)*(fadeDur-1))/(readEEPROM(20,false)*10))+1; + //TODO facilitate longer fades by writing a tweening function that smooths the frames, i.e. 111121222 - or use delayMicroseconds as below + //TODO does this have more problems with the mils rollover issue? + fadeNextDur = (((unsigned long)(mils-fadeStartLast)*(fadeDur-1))/(readEEPROM(20,false)*10))+1; if(fadeNextDur >= fadeLastDur) { //fade is over fadeStartLast = 0; fadeNextDur = 0; @@ -1079,6 +1096,7 @@ void cycleDisplay(){ } //end curently fading } //end fading enabled + //TODO consider using delayMicroseconds() which, with its tighter resolution, may give better control over fades and dim levels if(displayDim>0) { //if other display code says to shut off entirely, skip this part //Anode channel 0: tubes #2 (min x10) and #5 (sec x1) setCathodes(displayLast[2],displayLast[5]); //Via d2b decoder chip, set cathodes to old digits @@ -1167,7 +1185,7 @@ void signalPulseStart(word pulseDur){ else if(getSignalOutput()==1 && relayPin>=0 && relayMode==1) { //pulsed relay digitalWrite(relayPin,LOW); //LOW = device on //Serial.print(millis(),DEC); Serial.println(F(" Relay on, signalPulseStart")); - signalPulseStopTime = millis()+relayPulse; //always use relayPulse in case timing is important for connected device + signalPulseStartTime = millis(); } } void signalPulseStop(){ @@ -1177,7 +1195,7 @@ void signalPulseStop(){ else if(getSignalOutput()==1 && relayPin>=0 && relayMode==1) { //pulsed relay digitalWrite(relayPin,HIGH); //LOW = device on //Serial.print(millis(),DEC); Serial.println(F(" Relay off, signalPulseStop")); - signalPulseStopTime = 0; + signalPulseStartTime = 0; } } word getSignalPitch(){ //for current signal: time, timer, or (default) alarm