// AI developed 04-12-26 New Rotary Encoder gsk // --------- Pin Definitions ---------- //const byte rotaryA = 16; //const byte rotaryB = 17; // --------- State Machine Variables ---------- // Valid quadrature transition table: // Index = (lastAB << 2) | currentAB // Values: 0 = invalid/no change, 1 = CW step, -1 = CCW step // NOTE: A and B bit positions swapped vs original to fix direction // DRAM_ATTR keeps this table in RAM rather than flash so that the Adjust Mode // interrupt handler can read it at any time -- an interrupt that reaches into // flash while flash is busy will crash the ESP32. DRAM_ATTR const int8_t encoderTable[16] = { 0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0 }; // ================== MODE 4 (ADJUST MODE) ENCODER ================== // Every other screen in this program polls rotary() from loop(), which is fine // when nothing else is competing for time. Adjust Mode is different: the same // loop also has to repaint the OLED (~25 ms of I2C for a full frame) and retune // the PWM hardware. A polled encoder is blind for the whole of both, which is // exactly why the original Mode 4 lagged behind the knob and lost steps when // turned quickly. // // So Adjust Mode drives the encoder from an interrupt instead. The handler is // deliberately tiny -- read two pins, one table lookup, bump a counter -- and // it uses its own state variables, so nothing here can disturb the polled // rotary() that every other screen depends on. The interrupts are attached on // entry to Adjust Mode and detached on exit, leaving normal operation exactly // as it was. void IRAM_ATTR adjustEncoderISR() { byte currentAB = (digitalRead(rotaryA) << 1) | digitalRead(rotaryB); if (currentAB == adjustLastAB) return; int8_t delta = encoderTable[(adjustLastAB << 2) | currentAB]; if (delta != 0) adjustStepAccum += delta; if (currentAB == 3) { // detent rest position -- both pins high if (adjustStepAccum >= 2) ++adjustDelta; // one click clockwise else if (adjustStepAccum <= -2) --adjustDelta; // one click counter-clockwise adjustStepAccum = 0; } adjustLastAB = currentAB; } void adjustEncoderStart() { // begin interrupt-driven encoder reading adjustLastAB = (digitalRead(rotaryA) << 1) | digitalRead(rotaryB); adjustStepAccum = 0; adjustDelta = 0; attachInterrupt(digitalPinToInterrupt(rotaryA), adjustEncoderISR, CHANGE); attachInterrupt(digitalPinToInterrupt(rotaryB), adjustEncoderISR, CHANGE); } void adjustEncoderStop() { // hand the encoder back to polled rotary() detachInterrupt(digitalPinToInterrupt(rotaryA)); detachInterrupt(digitalPinToInterrupt(rotaryB)); lastAB = (digitalRead(rotaryA) << 1) | digitalRead(rotaryB); stepAccumulator = 0; // start rotary() from a known clean state } int adjustEncoderTake() { // read the pending clicks and clear them noInterrupts(); int d = adjustDelta; adjustDelta = 0; interrupts(); return d; } // --------- Call this frequently from loop() ---------- void rotary() { // Read B in bit1, A in bit0 (swapped to correct direction) byte currentAB = (digitalRead(rotaryA) << 1) | digitalRead(rotaryB); if (currentAB == lastAB) return; byte index = (lastAB << 2) | currentAB; int8_t delta = encoderTable[index]; if (delta != 0) { stepAccumulator += delta; } // Only register a count when encoder returns to detent rest state // With pull-ups, rest state is both pins HIGH = 0b11 = 3 if (currentAB == 3) { if (stepAccumulator >= 2) { encoderPosition++; encoderChanged = true; rotaryPositive(); // on condition set positive direction } else if (stepAccumulator <= -2) { encoderPosition--; encoderChanged = true; rotaryNegative(); // on condition set negative direction } stepAccumulator = 0; } lastAB = currentAB; rotaryLimits(); // insure no number limits happened } // ****************** END OF NEW ROTARY ENCODER TECHNIQUE *** void rotaryPositive(){ switch (curX){ // if here it was a positive direction case line0XC: // cursor position 38 is where "setupOpt' sits on display ++setupOpt; // if here increment the value break; // break out of this switch function case 107: // where the "protoID" and "mode" sits on the display if(setupOpt<2) // if not is MODE setup then increment the "protoID" protoID=protoID+1; // increment else ++modeID; // if here than it is MODE setup so increment break; // you know what this does // F1 positive rotary direction row decode section case 18: // where Frequency 'F1' sits on the display F1=F1+10000; // if here than increment the 10000th position of F1 break; case 24: // where Frequency "F1" sits on the display F1=F1+1000; // if here than increment the 1000th position of F1 break; case 30: F1=F1+100; // if here than increment the 100th position of F1 break; case 36: F1=F1+10; // if here than increment the 10th position of F1 break; case 42: F1=F1+1; // if here than increment the units position of F1 break; case 54: F1=F1+0.1; // if here than increment the tenth position of F1 break; case 60: F1=F1+0.01; // if here than increment the hundredth position of F1 break; case 72: endTime1=endTime1+1000000; // if here than inc the 1000th position of endTime1 second count break; case 78: endTime1=endTime1+100000; // if here than inc the 100th position of endTime1 second count break; case 84: endTime1=endTime1+10000; // if here than inc the 10th position of endTime1 second count break; case 90: endTime1=endTime1+1000; // if here than inc the units position of endTime1 second count break; case 102: // starting group ID. Which one you are editing depends on if(setupOpt==3) // the SCREEN you are on, not on the Mode -- Setup=3 and ++startFreqGrpID; // Setup=4 are both reachable in every Mode, so keying this if(setupOpt==4) // off modeID left the knob dead on both screens whenever ++startSweepGrpID; // Mode was 1 or 4. break; case 108: // where Duty Cycle 1 sits on the display D1=D1+0.1; // if here than increment the tenth position this duty cycle break; // you know what this does case 114: D1=D1+0.01; // if here than increment the hundredth position of duty break; } // F2 positive rotary direction row decode section switch (curX){ // Now it time to check if Frequency 2 needs updating case 19: // where the Frequency 2 sits on the display F2=F2+10000; // if here increment the 10000th position of F2 break; case 25: F2=F2+1000; // if here increment the 1000th position of F2 break; case 31: F2=F2+100; // if here increment the 100th position of F2 break; case 37: F2=F2+10; // if here increment the 10th position of F2 break; case 43: F2=F2+1; // if here increment the units position of F2 break; case 55: F2=F2+0.1; // if here increment the tenth position of F2 break; case 61: F2=F2+0.01; // if here increment the hundredth position of F2 break; case 79: endTime2=endTime2+60000000; // if here inc the 1000th position of endTime2 time in minutes break; case 85: endTime2=endTime2+6000000; // if here inc the 100th position of endTime2 time in minutes break; case 91: endTime2=endTime2+600000; // if here inc the 10the position of endTime2 time in minutes break; case 97: endTime2=endTime2+60000; // if here inc the units position of endTime2 time in minutes break; case 103: // stopping group ID -- keyed off the screen, see case 102 if(setupOpt==3) ++stopFreqGrpID; // Setup=3 edits the Protocol range if(setupOpt==4) ++stopSweepGrpID; // Setup=4 edits the Sweep range break; case 109: // where Duty Cycl 2 sits on the display D2=D2+0.1; // if here increment the tenth position break; case 115: D2=D2+0.01; // if here increment the hundredth position break; // get out of this case } // F3 positive rotary direction row decode section switch (curX){ // Now it time to check if Frequency 2 needs updating case 20: // where the Frequency 2 sits on the display F3=F3+10000; // if here increment the 10000th position of F2 break; case 26: F3=F3+1000; // if here increment the 1000th position of F2 break; case 32: F3=F3+100; // if here increment the 100th position of F2 break; case 38: F3=F3+10; // if here increment the 10th position of F2 break; case 44: F3=F3+1; // if here increment the units position of F2 break; case 56: F3=F3+0.1; // if here increment the tenth position of F2 break; case 62: F3=F3+0.01; // if here increment the hundredth position of F2 break; case 82: sweepFreqInc+=1000; // if here inc the 1000th position break; case 88: sweepFreqInc+=100; // if here inc the 100th position break; case 94: sweepFreqInc+=10; // if here inc the 10th position break; case 99: fileNo=fileNo+1; // if here increment the file number break; case 100: sweepFreqInc+=1.0; // if here inc the units position break; case 112: sweepFreqInc+=0.1; // if here inc the fractional tenth position break; case 118: sweepFreqInc+=0.01; // if here inc the fractional hundredth position break; case 110: // where Duty Cycl 2 sits on the display D3=D3+0.1; // if here increment the tenth position break; case 116:{ D3=D3+0.01; // if here increment the hundredth position break;} // get out of this case } // F4 positive rotary direction row decode section switch (curX){ // Now it time to check if Frequency 2 needs updating case 21: // where the Frequency 2 sits on the display F4=F4+10000; // if here increment the 10000th position of F2 break; case 27: F4=F4+1000; // if here increment the 1000th position of F2 break; case 33: F4=F4+100; // if here increment the 100th position of F2 break; case 39: F4=F4+10; // if here increment the 10th position of F2 break; case 45: F4=F4+1; // if here increment the units position of F2 break; case 57: F4=F4+0.1; // if here increment the tenth position of F2 break; case 63: F4=F4+0.01; // if here increment the hundredth position of F2 break; case 111: // where Duty Cycl 2 sits on the display D4=D4+0.1; // if here increment the tenth position break; case 117: D4=D4+0.01; // if here increment the hundredth position break; // get out of this case } } void rotaryNegative(){ switch (curX){ // use switch to decided what to do case line0XC: // this is where the "setupOpt" sits on the display --setupOpt; // if here decrement the setup Option break; // get out of this switch function case 107: // where "mode" and "protoID" sits on the display if(setupOpt<2) // on condition than not a mode change protoID=protoID-1; // if here decrement the "protoID" else --modeID; // if here than decrement the "mode" break; // you know what this does // F1 negative rotary direction row decode section case 18: // this is where Frequency 1 F1 sits on the display if(F1>=10000) F1=F1-10000; // if here decrement the 10000th position of F1 break; case 24: if(F1>=1000) F1=F1-1000; // if here decrement the 1000th position of F1 break; case 30: if(F1>=100) F1=F1-100; // if here decrement the 100th position of F1 break; case 36: if(F1>=10) F1=F1-10; // if here decrement the 10th position of F1 break; case 42: if(F1>=1) F1=F1-1; // if here decrement the units position of F1 break; case 54: F1=F1-0.1; // if here decrement the tenth position of F1 break; case 60: F1=F1-0.01; // if here decrement the hundredth position of F1 break; case 72: endTime1=endTime1-1000000; // if here dec 1000th position of endTime1 Time in seconds break; case 78: endTime1=endTime1-100000; // if here dec 100th position of endTime1 Time in seconds break; case 84: endTime1=endTime1-10000; // if here dec 10th position of endTime1 Time in seconds break; case 90: endTime1=endTime1-1000; // if here dec units position of endTime Time in seconds break; case 102: // starting group ID -- keyed off the screen, see case 102 if(setupOpt==3) // in rotaryPositive() for why this is not modeID --startFreqGrpID; // Setup=3 edits the Protocol range if(setupOpt==4) --startSweepGrpID; // Setup=4 edits the Sweep range break; case 108: // where Duty Cycl 1 sits on the display D1=D1-0.1; // if here decrement the tenth position of the duty cycle break; case 114: D1=D1-0.01; // if here decrement the hundredth position of duty cycle break; } // F2 negative rotary direction row decode section switch (curX){ // do the same as above for Frequency 2 F2 case 19: // where Frequency 2 F2 sits on the display if(F2>=10000) F2=F2-10000; // if here dec Frequency 2 10000th position break; case 25: if(F2>=1000) F2=F2-1000; // if here dec Frequency 2 1000th position break; case 31: if(F2>=100) F2=F2-100; // if here dec Frequency 2 100th position break; case 37: if(F2>=10) F2=F2-10; // if here dec Frequency 2 10th position break; case 43: if(F2>=1) F2=F2-1; // if here dec Frequency 2 units position break; case 55: F2=F2-0.1; // if here dec Frequency 2 tenth position break; case 61: F2=F2-0.01; // if here dec Frequency 2 hundredth position break; case 79: endTime2=endTime2-60000000; // if here dec endTime2 1000th position in minutes break; case 85: endTime2=endTime2-6000000; // if here dec endTime2 100th position in minutes break; case 91: endTime2=endTime2-600000; // if here dec endTime2 10th position in minutes break; case 97: endTime2=endTime2-60000; // if here dec endTime2 units position in minutes break; case 103: // stopping group ID -- keyed off the screen, see case 102 if(setupOpt==3) --stopFreqGrpID; // Setup=3 edits the Protocol range if(setupOpt==4) --stopSweepGrpID; // Setup=4 edits the Sweep range break; case 109: // this is where Duty Cycle 2 sits on the display D2=D2-0.1; // if here decrement the tenth position of Duty Cycle 2 break; case 115: D2=D2-0.01; // if here decrement the hundredth position break; } // F3 negative rotary direction row decode section switch (curX){ // Now it time to check if Frequency 3 needs updating case 20: // where the Frequency 3 sits on the display F3=F3-10000; // if here dec the 10000th position of F3 break; case 26: F3=F3-1000; // if here dec the 1000th position of F3 break; case 32: F3=F3-100; // if here dec the 100th position of F3 break; case 38: F3=F3-10; // if here dec the 10th position of F3 break; case 44: F3=F3-1; // if here dec the units position of F3 break; case 56: F3=F3-0.1; // if here dec the tenth position of F3 break; case 62: F3=F3-0.01; // if here dec the hundredth position of F3 break; case 82: sweepFreqInc-=1000; // if here inc the 1000th position break; case 88: sweepFreqInc-=100; // if here inc the 100th position break; case 94: sweepFreqInc-=10; // if here inc the 10th position break; case 99: fileNo=fileNo-1; // if here decrement the file number break; case 100: sweepFreqInc-=1.0; // if here inc the units position break; case 112: sweepFreqInc-=0.1; // if here inc the fractional tenth position break; case 118: sweepFreqInc-=0.01; // if here inc the fractional hundredth position break; case 110: // where Duty Cycl 3 sits on the display D3=D3-0.1; // if here dec the tenth position break; case 116: D3=D3-0.01; // if here dec the hundredth position break; // get out of this case } // F4 negative rotary direction row decode section switch (curX){ // Now it time to check if Frequency 2 needs updating case 21: // where the Frequency 2 sits on the display F4=F4-10000; // if here increment the 10000th position of F2 break; case 27: F4=F4-1000; // if here increment the 1000th position of F2 break; case 33: F4=F4-100; // if here increment the 100th position of F2 break; case 39: F4=F4-10; // if here increment the 10th position of F2 break; case 45: F4=F4-1; // if here increment the units position of F2 break; case 57: F4=F4-0.1; // if here increment the tenth position of F2 break; case 63: F4=F4-0.01; // if here increment the hundredth position of F2 break; case 111: // where Duty Cycl 2 sits on the display D4=D4-0.1; // if here increment the tenth position break; case 117: D4=D4-0.01; // if here increment the hundredth position break; // get out of this case } } void rotaryLimits(){ if(setupOpt>200) // Test if byte went negative setupOpt=0; // if here make it a zero if(setupOpt>5) // check if greater than 5 setupOpt=5; // on condition make it a 5 if(protoID<1) // This can be no lower than 1 protoID=protoIDlast; // if here make it a 1 if(protoID>protoIDlast) // This can be no higher than 50t protoID=1; // if here make it a 1 if(modeID<1){ // This can be no lower than 1 modeID=1; // if here make it a 1 } if(modeID>4){ // This can be no higher than 4. Mode=4 is Adjust Mode, modeID=4; // re-enabled in V2.04 -- see NOTE 26 in the main .ino } if(startFreqGrpID<1) // This can be no lower than 1 startFreqGrpID=protoIDlast; // if here make it a 1 if(startFreqGrpID>protoIDlast) // This can be no higher than 50 startFreqGrpID=1; // if here make it a 50 if(stopFreqGrpID<1) // This can be no lower than 1 stopFreqGrpID=protoIDlast; // if here make it a 1 if(stopFreqGrpID>protoIDlast) // This can be no higher than 50 stopFreqGrpID=1; // if here make it a 50 if(startSweepGrpID<1) // This can be no lower than 1 startSweepGrpID=protoIDlast; // if here make it a 1 if(startSweepGrpID>protoIDlast) // This can be no higher than 50 startSweepGrpID=1; // if here make it a 50 if(stopSweepGrpID<1) // This can be no lower than 1 stopSweepGrpID=protoIDlast; // if here make it a 1 if(stopSweepGrpID>protoIDlast)// This can be no higher than 50 stopSweepGrpID=1; // if here make it a 50 if(sweepFreqInc>9999.99) // if it reached limit cycle to zero sweepFreqInc=0.00; if(sweepFreqInc<0.00) // if it less than 0 cycle to limit sweepFreqInc=9999.99; if(F1>65535.00) // Frequency 1 can be no higher than 65535 (16-bit timer max) F1=65535.00; // if higher make it 65535 if(F1<0.04) // 0.0373 Hz is the MCPWM hardware floor -- below F1=0.04; // that the timer has no legal period and the channel dies if(D1>1.00) // Duty Cycle 1 can be no higher than 100% D1=1.00; // if higher make it 100% if(D1<0.00) // Duty Cycle 1 can be no lower than 0% D1=0.00; // if lower make it 0% if(F2>65535.00) // Frequency 2 can be no higher than 65535 (16-bit timer max) F2=65535.00; // if higher make it 65535 if(F2<0.04) // 0.0373 Hz is the MCPWM hardware floor -- below F2=0.04; // that the timer has no legal period and the channel dies if(D2>1.00) // Duty Cycle 2 can be no higher than 100% D2=1.00; // if higher than make it 100% if(D2<0.00) // Duty Cycle 2 can be no lower than 0% D2=0.00; // if lower than make it 0% if(F3>65535.00) // Frequency 3 can be no higher than 65535 (16-bit timer max) F3=65535.00; // if higher make it 65535 if(F3<0.04) // 0.0373 Hz is the MCPWM hardware floor -- below F3=0.04; // that the timer has no legal period and the channel dies if(D3>1.00) // Duty Cycle 3 can be no higher than 100% D3=1.00; // if higher than make it 100% if(D3<0.00) // Duty Cycle 3 can be no lower than 0% D3=0.00; // if lower than make it 0% if(F4>65535.00) // Frequency 4 can be no higher than 65535 (16-bit timer max) F4=65535.00; // if higher make it 65535 if(F4<0.04) // 0.0373 Hz is the MCPWM hardware floor -- below F4=0.04; // that the timer has no legal period and the channel dies if(D4>1.00) // Duty Cycle 4 can be no higher than 100% D4=1.00; // if higher than make it 100% if(D4<0.00) // Duty Cycle 4 can be no lower than 0% D4=0.00; // if lower than make it 0% if(endTime1<1000) // endTime1 can be lower than 1 seconds endTime1=500; // make it 1/2 second if(endTime1>1000){ // make sure fraction of a second are removed endTime1/=1000; // integer math will remove and fractions endTime1*=1000;} // restore to whole integer second(s) number if(endTime1>9999000) // endTime can be no higher than 9999 second endTime1=1000; // if here make it cycle around to 1 second if(endTime2<60000) // endTime1 can be lower than 1 minute endTime2=30000; // if here make it make 30 seconds = 1/2 minute if(endTime2>60000){ // make sure fraction minutes are removed endTime2/=60000; // integer math will remove any fractions endTime2*=60000;} // restore to whole INTEGER minute(s) number if(endTime2>599940000) // endTime can be no larger than 9999 minutes endTime2=60000; // if here make it 1 minute if(fileNo>99){ // fileNo is unsigned (byte), so decrementing below 0 if(fileNo==255) // wraps to 255, not a negative number -- 255 is the fileNo=99; // only way this happens, so it means "wrap to the top"; else // any other out-of-range value means an increment past fileNo=0; // 99, so wrap back to 0 } change=1; // indicate there has be a frequency or duty change }