Arduino 'constrain' issues - Page 1
Forum section: electronics / general

author: PSR B1257
author_rank: Frequent Contributor
timestamp: October 20, 2020, 05:27:24 pm
content: Hello there.I am currently working on a Arduino-sketch to generate a configurable PWM-signal. Frequency and pulsewidth can be independently adjustet via a little menu-function. All in all it is working sufficently and accurate enugh for the given application.It would be a nice touche, to limit the possible set-values, which I indendet to do with a constrain-function (seems easy enugh). But since the code is a litte convoluted, the constrain-function does not work (and I commented out the according lines).Here is the code. I left the original autor (credit where credit's due) in the header and mengelt my lines in between (hence the englisch an german commentry).Code:/*******Interrupt-based Rotary Encoder Menu Sketch******** by Simon Merrett, based on insight from Oleg Mazurov, Nick Gammon, rt and Steve Spence, and code from Nick Gammon* 3,638 bytes with debugging on UNO, 1,604 bytes without debugging*/#include <Wire.h>#include <LiquidCrystal_I2C.h> // Library for LCD//#define LED 13// Wiring: SDA pin is connected to A4 and SCL pin to A5.// Connect to LCD via I2C, default address 0x27 (A0-A2 not jumpered)LiquidCrystal_I2C lcd = LiquidCrystal_I2C(0x27, 16, 2);// Rotary encoder declarationsstatic int pinA = 2; // Our first hardware interrupt pin is digital pin 2static int pinB = 3; // Our second hardware interrupt pin is digital pin 3volatile byte aFlag = 0; // let's us know when we're expecting a rising edge on pinA to signal that the encoder has arrived at a detentvolatile byte bFlag = 0; // let's us know when we're expecting a rising edge on pinB to signal that the encoder has arrived at a detent (opposite direction to when aFlag is set)volatile byte encoderPos = 0; //this variable stores our current value of encoder position. Change to int or uin16_t instead of byte if you want to record a larger range than 0-255volatile byte oldEncPos = 0; //stores the last encoder position value so we can compare to the current reading and see if it has changed (so we know when to print to the serial monitor)volatile byte reading = 0; //somewhere to store the direct values we read from our interrupt pins before checking to see if we have moved a whole detent// Button reading, including debounce without delay function declarationsconst byte buttonPin = 4; // this is the Arduino pin we are connecting the push button tobyte oldButtonState = HIGH; // assume switch open because of pull-up resistorconst unsigned long debounceTime = 10; // millisecondsunsigned long buttonPressTime; // when the switch last changed stateboolean buttonPressed = 0; // a flag variable// Menu and submenu/setting declarationsbyte Mode = 0; // This is which menu mode we are in at any given time (top level or one of the submenus)const byte modeMax = 2; // This is the number of submenus/settings you wantbyte setting1 = 0; // a variable which holds the value we setbyte setting2 = 0; // a variable which holds the value we setunsigned long Frequenz = 0;unsigned long Pulsbreite = 0;unsigned long Pausenbreite = 0;unsigned long previousMillis = 0;const int PWMpin=13;bool PWMstate = false;/* Note: you may wish to change settingN etc to int, float or boolean to suit your application.Remember to change "void setAdmin(byte name,*BYTE* setting)" to match and probably add some"modeMax"-type overflow code in the "if(Mode == N && buttonPressed)" section*/void setup() {//Display initial beschreibenlcd.begin();lcd.backlight();lcd.setCursor(0, 0);lcd.print("Pulsbreite=");lcd.setCursor(0, 1);lcd.print("Frequenz=");//Rotary encoder section of setuppinMode(pinA, INPUT_PULLUP); // set pinA as an input, pulled HIGH to the logic voltage (5V or 3.3V for most cases)pinMode(pinB, INPUT_PULLUP); // set pinB as an input, pulled HIGH to the logic voltage (5V or 3.3V for most cases)attachInterrupt(0,PinA,RISING); // set an interrupt on PinA, looking for a rising edge signal and executing the "PinA" Interrupt Service Routine (below)attachInterrupt(1,PinB,RISING); // set an interrupt on PinB, looking for a rising edge signal and executing the "PinB" Interrupt Service Routine (below)// button section of setuppinMode (buttonPin, INPUT_PULLUP); // setup the button pinpinMode(PWMpin, OUTPUT);digitalWrite(PWMpin,PWMstate);}void loop() {rotaryMenu();PWM();// carry out other loop code here}void rotaryMenu() { //This handles the bulk of the menu functions without needing to install/include/compile a menu library//DEBUGGING: Rotary encoder update display if turnedif(oldEncPos != encoderPos) { // DEBUGGINGoldEncPos = encoderPos;// DEBUGGING//Pfeile löschenif (Mode == 0 && encoderPos == 0){lcd.setCursor(15, 0);lcd.print(" ");lcd.setCursor(15, 1);lcd.print(" ");}//Pfeil zeichnen auf Wert "Pulsbreite"if(Mode == 0 && encoderPos == 1){lcd.setCursor(15, 0);lcd.print("<");lcd.setCursor(15, 1);lcd.print(" ");}//Wert von Drehencoder an Stelle "Pulsbreite" schreibenif(Mode == 1){lcd.setCursor(11, 0);if (encoderPos <10) lcd.print(" ");lcd.print(encoderPos);lcd.print("ms");}//Pfeil zeichnen auf Wert "Frequenz"if(Mode == 0 && encoderPos == 2){lcd.setCursor(15, 0);lcd.print(" ");lcd.setCursor(15, 1);lcd.print("<");}//Wert von Drehencoder an Stelle "Frequenz" schreibenif(Mode == 2){lcd.setCursor(11, 1);if (encoderPos <10) lcd.print(" ");lcd.print(encoderPos);lcd.print("Hz");}}// DEBUGGING// Button reading with non-delay() debounce - thank you Nick Gammon!byte buttonState = digitalRead (buttonPin);if (buttonState != oldButtonState){if (millis () - buttonPressTime >= debounceTime){ // debouncebuttonPressTime = millis (); // when we closed the switcholdButtonState = buttonState; // remember for next timeif (buttonState == LOW){buttonPressed = 1;}else {buttonPressed = 0;}} // end if debounce time up} // end of state change//Main menu sectionif (Mode == 0) {//Berechnung der Pausenbreite in µsPausenbreite=(1000000/Frequenz)-1000*Pulsbreite;if (encoderPos > (modeMax+10)) encoderPos = modeMax; // check we haven't gone out of bounds below 0 and correct if we haveelse if (encoderPos > modeMax) encoderPos = 0; // check we haven't gone out of bounds above modeMax and correct if we haveif (buttonPressed){Mode = encoderPos; // set the Mode to the current value of input if button has been pressedbuttonPressed = 0; // reset the button status so one press results in one actionif (Mode == 1) {encoderPos = setting1; //letzten eingestellten Wert wiederherstellen}if (Mode == 2) {encoderPos = setting2; //letzten eingestellten Wert wiederherstellen}}}if (Mode == 1 && buttonPressed) {setting1 = encoderPos; // Einstellung Pulsbreite//setting1 = constrain (setting1,2,15);Pulsbreite = setting1;setAdmin(1,setting1);}if (Mode == 2 && buttonPressed) {setting2 = encoderPos; // Einstellung Frequenz//setting2 = constrain (setting2,1,50);Frequenz = setting2;setAdmin(2,setting2);}}// Carry out common activities each time a setting is changedvoid setAdmin(byte name, byte setting){encoderPos = 0; // reorientate the menu index - optional as we have overflow check code elsewherebuttonPressed = 0; // reset the button status so one press results in one actionMode = 0; // go back to top level of menu, now that we've set values}//Rotary encoder interrupt service routine for one encoder pinvoid PinA(){cli(); //stop interrupts happening before we read pin valuesreading = PIND & 0xC; // read all eight pin values then strip away all but pinA and pinB's valuesif(reading == B00001100 && aFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edgeencoderPos --; //decrement the encoder's position countbFlag = 0; //reset flags for the next turnaFlag = 0; //reset flags for the next turn}else if (reading == B00000100) bFlag = 1; //signal that we're expecting pinB to signal the transition to detent from free rotationsei(); //restart interrupts}//Rotary encoder interrupt service routine for the other encoder pinvoid PinB(){cli(); //stop interrupts happening before we read pin valuesreading = PIND & 0xC; //read all eight pin values then strip away all but pinA and pinB's valuesif (reading == B00001100 && bFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edgeencoderPos ++; //increment the encoder's position countbFlag = 0; //reset flags for the next turnaFlag = 0; //reset flags for the next turn}else if (reading == B00001000) aFlag = 1; //signal that we're expecting pinA to signal the transition to detent from free rotationsei(); //restart interrupts}void PWM(){unsigned long currentMillis = micros();if( PWMstate == HIGH ){if( (unsigned long)(currentMillis - previousMillis) >= 1000*Pulsbreite){PWMstate = LOW;// change the state of LEDdigitalWrite(PWMpin,PWMstate);previousMillis=micros();}}else{if( (unsigned long)(currentMillis - previousMillis) >= Pausenbreite){PWMstate = HIGH;// change the state of LEDdigitalWrite(PWMpin,PWMstate);previousMillis=micros();}}}Any ideas, how to solve this issue, are greatly appreciated.Best wishes, Matt

author: Pack34
author_rank: Frequent Contributor
timestamp: October 20, 2020, 11:26:08 pm
content: Are you getting an error? Or is it not behaving as intended?

author: ledtester
author_rank: Super Contributor
timestamp: October 20, 2020, 11:52:02 pm
content: You are constraining setting1:Code:if (Mode == 1 && buttonPressed) {setting1 = encoderPos; // Einstellung Pulsbreite//setting1 = constrain (setting1,2,15);but you are not also constraining encodePos which is what gets displayed:Code:if(Mode == 1){lcd.setCursor(11, 0);if (encoderPos <10) lcd.print(" ");lcd.print(encoderPos);lcd.print("ms");Is that what your problem is?This code looks suspicious:Code:if (encoderPos > (modeMax+10)) encoderPos = modeMax; // check we haven't gone out of bounds below 0 and correct if we haveelse if (encoderPos > modeMax) encoderPos = 0; // check we haven't gone out of bounds above modeMax and correct if we haveThe bounds on encodePos depend on what Mode is set to and in menu mode you want it wrap around, but when setting the PWM parameters probably not.

author: PSR B1257
author_rank: Frequent Contributor
timestamp: October 21, 2020, 06:04:44 am
content: QuoteThe latter.QuoteYep, that's because I want a live updating display, as the encoder is turned.QuoteIt's part of the original sketch (which I admittedly not fully understand).

author: newbrain
author_rank: Super Contributor
timestamp: October 21, 2020, 07:04:46 am
content: Quote from: PSR B1257 on October 21, 2020, 06:04:44 amCare to elaborate?What is the expected result and what are the differences with the actual outcome?The program logic is quite messy - but being Arduino, that's to be expected.I agree with ledtester that the display should be constrained too (though there's no constrain function in the code as shown).Their second point on suspicious code is instead moot: limiting encoderPos is only performed whenModeis 0 (menu mode).

author: Pack34
author_rank: Frequent Contributor
timestamp: October 21, 2020, 01:31:36 pm
content: I noticed all your variables are in bytes, but the constrain function uses ints. I'm not sure if that would be an issue with the compiler or not. Have you tried just using two IF statements instead of the constrain function? It's just bounding a number. It's not immediately clear why you're using bytes there. You seem to go up to using unsigned_long variables in the PWM function.Your setAdmin function isn't doing anything with the passed in variables.

author: PSR B1257
author_rank: Frequent Contributor
timestamp: October 21, 2020, 03:40:00 pm
content: QuoteIt kinda sorta works. The value is constrained, but not during the adjustment via the encoder. You can (at first glance) input i.e. 20ms pulsewith (maximum should be 15ms) and the display reads 20ms. However if you recall the value (by the "setAdmin" function, I suspect), in order to change it, the correcty constrained value of 15ms is shown.So it is working in the background (verified by frequencycounter), but displayed wrong. I didn't even noticed that before

author: ledtester
author_rank: Super Contributor
timestamp: October 21, 2020, 04:09:02 pm
content: There are a bunch of race conditions in this code -- encodePos is updated in the ISRs but accessed and modified multiple times in the main program.This library looks like it does a better job:https://github.com/0xPIT/encoder

author: PSR B1257
author_rank: Frequent Contributor
timestamp: October 22, 2020, 06:02:02 pm
content: QuoteWell possible...I'm to much of a non-programmer to assess this.QuoteI tried a bunch of different encoder-reading-sketches and found this one beeing the most reliable. Regarding your link, I'm sure it's fine, but I don't even remotly understand itTo make my code even more messy, I added a few IF-statements to at least limit the displayed value (under the motto 'Keep it stupid simple').Code:if(Mode == 1){lcd.setCursor(11, 0);if (encoderPos <3){lcd.print(" 2ms");}else if (encoderPos <16){if (encoderPos <10) lcd.print(" ");lcd.print(encoderPos);lcd.print("ms");}else{lcd.print("15ms");}#worstcodeinhistory

author: Pack34
author_rank: Frequent Contributor
timestamp: October 22, 2020, 08:34:13 pm
content: Quote from: PSR B1257 on October 22, 2020, 06:02:02 pmYou're changing what's printed to the LCD, not the actual value.

author: brucehoult
author_rank: Super Contributor
timestamp: October 22, 2020, 09:51:00 pm
content: Quote from: PSR B1257 on October 22, 2020, 06:02:02 pmYou're still doing it. You're using the value of "volatile byte encoderPos" at multiple places in the code and it might be different every time because the interrupt routine can be changing it.For example when you do "if (encoderPos < 3)" it might be greater than 3, but by the time you test "if (encoderPos < 16) it might have changed to something less than 3.Similarly, when you test if it is less than 10 to print a leading space, by the time you print the actual number it might have changed. So you can print a leading space for a number that doesn't need one by the time you print it, or vice versa.What you *should* be doing is something like this:Code:byte constrainedPos = encoderPos;if (constrainedPos < 3) constrainedPos = 3;if (constrainedPos > 15) constrainedPos = 15;if (oldEncPos != constrainedPos){if (constrainedPos <10) lcd.print(" ");lcd.print(constrainedPos);lcd.print("ms");oldEncPos = constrainedPos;}

author: PSR B1257
author_rank: Frequent Contributor
timestamp: October 24, 2020, 08:42:43 pm
content: QuoteYes, since 'encoderPos' is live updating.QuoteYou would have to turn the encoder really fast, to achieve this behavior, wouldn't you?QuoteUnfortunately, the controller goes bananas, if I try this. It prints " 2ms" all over the LCD

author: brucehoult
author_rank: Super Contributor
timestamp: October 24, 2020, 11:00:46 pm
content: Quote from: PSR B1257 on October 24, 2020, 08:42:43 pmAnd you don't want it to update in the middle of one set of operations on it, only once per loop.QuoteNo. The value has to change sometime. Sometimes it will happen in a safe place, sometimes it will happen in a place that breaks your logic.It might only break once every five seconds, or once a day, or once a month, but it *will* eventually happen at the worst possible time for your program logic. Or it might break 100 times a second. It depends how much time your program spends in which part of the code. It's bad programming to allow this to happen.QuoteThen you presumably need to do an lcd.setCursor() somewhere. I'm interested in the constraining the value part, which you are having problems with.

author: PSR B1257
author_rank: Frequent Contributor
timestamp: October 25, 2020, 09:46:39 am
content: QuoteNot really. 'encoderPos' is updated live to the LCD, while the encoder is turned (as you would expect from any menu-function. Only after pressing the encoder button, the value is transferred to 'setting1' and 'Pulsbreite' (pulse width) for example.Even if the code glitches during the adjustment, it wouldn't be the end of the world.

author: brucehoult
author_rank: Super Contributor
timestamp: October 25, 2020, 10:24:08 am
content: Quote from: PSR B1257 on October 25, 2020, 09:46:39 amWhy write code that can glitch, when it's just as easy to write code that can't glitch?

author: PSR B1257
author_rank: Frequent Contributor
timestamp: October 26, 2020, 08:05:55 pm
content: QuoteEasier for who?I did not get your code-example to work.

author: free_electron
author_rank: Super Contributor
timestamp: October 26, 2020, 08:50:51 pm
content: i typically use an array for parameters and use named indexes to refer to themint settings ;#define minval 0#define currentval 1#define maxval 2#define nothing 0#define frequency 1#define dutycycle 2#define level 3int parameter;void rotaryhandler_up (current_parameter){if (settings(currentparameter,currentval) < settings{currentparameter,maxval)) settings(currentparameter,currentval) = settings(currentparameter,currentval) +1;event = evnt_rotary_up;}void rotaryhandler_down (current_parameter){if (settings(currentparameter,currentval) < settings{currentparameter,maxval)) settings(currentparameter,currentval) = settings(currentparameter,currentval) -1;event = evnt_rotary_down;}void rotary_handler_click (void){current_parameter = 0;event = evnt_rotary_click;}void eventhandler(event){switch (event){event_rotary_up:event_rotary_down:switch(parameter) {frequency :lcdprint (0,0,'Frequency = %i',settings(parameter,currentval);break;dutycycle :lcdprint (0,0,'Dutycycle = %i',settings(parameter,currentval);break;level :lcdprint (0,0,'Level = %i',settings(parameter,currentval);break;}break;event_rotary_click:default:}event = 0;}something along those lines.basically create an array that stores your settings. each ' setting has a minimum , maximum and current_valuethe menuhandler sets an integer that determines which of the array entries is being manipulated. : current_parameterthe rotary encoder handler has 3 hardware events : up , down and click. it uses 'current_parameters to index the array and determines the new value of 'current_value' based on which direction and the minimum and maximum.the 'click event' sets the index to zero. you can turn all you want : nothing increments or decrements. the zero index is a dummy.There is a software event handler that is responsible for postprocessing. That one updates the display , sends the data into registers or peripherals and whatnot. The hardware handlers set a software_event so it can be post-processed.upon post-processing the software_event is set to zero.you can have multiple hardware rotary encoders. lets say you have an encoder for mulitple functions , and one dedicate to the output level.the hardware handler for output level would simply 'force' the current_parameter to 'level' prior to changing the values. upon calling the softwareeventhandler , the correct parameter will be displayed as the current_parameter has changed.the software eventhandler is not limited to rotary encoders. i can have pushbutton_events in there or other eventsevent output_overload : display ' overload 'event numberkeynumber_key_0number_key_1number_key_2....numerical = numerical * 10 + numberkey;event backspacenumerical = numerical \10event enterif numerical > settings ( current_parameter,minval) and numerical < settings ( current_parameter,manval) settings(current_parameter,current_value) = numerical;I split things between hardware interrupts ( encoders, buttons , scanned events ). those hardware interrupts do minimal processing and critical processing and then fire a software event that deal with the rest.critical events would be an overload : the hardware handler would switch off output , then fire the software event to notify the user.scanned events are events triggered by a hardware timer. for example a loop that reads data over i2c or spi or io pins on a periodical sweep. those are for critical scans only.i have a general purpose system_tick as well. that one can fire software scanned events.

author: PSR B1257
author_rank: Frequent Contributor
timestamp: October 28, 2020, 04:20:08 pm
content: QuoteSound's interesting. Thanks for your comprehensive reply.But once again, silly me can't really reenact your code. I'm more the having-a-working-code-and-modify-it-somewhat-programmer.

author: Ground_Loop
author_rank: Frequent Contributor
timestamp: October 30, 2020, 01:52:32 pm
content: Quote from: PSR B1257 on October 24, 2020, 08:42:43 pmThe display problem may be related to using interrupts to service the encoder position. I've had lcds get interrupted in the middle of updates then they're scrambled until I can send a reset.

author: brucehoult
author_rank: Super Contributor
timestamp: October 30, 2020, 10:56:20 pm
content: Quote from: Ground_Loop on October 30, 2020, 01:52:32 pmThere are easy ways to program to avoid that.He's refusing to consider them, even after it being demonstrated.

author: PSR B1257
author_rank: Frequent Contributor
timestamp: November 01, 2020, 04:23:40 pm
content: QuoteI'm not. I "just" can't implement them.Whilst being messy, the code works fine, without glitches or something. Apart from the initial constraining-problem of course.The display only gets scrambled, if I try to make the *should*-code work. Obviously, I don't know, what I'm doing....