Sketch für Servo mit 3 Potis,Drehgeber & LCD; Verbesserungsvorschläge ?

Hallo zusammen,

ich habe vor ca. 2 Wochen angefangen mich mit Arduino auseinanderzusetzen. Jetzt habe ich mir meinen ersten Sketch zusammengebastelt.

Würde sich von Euch jemand mal den Sketch anschauen und mir vielleicht Tipps geben, was man besser machen könnte ?

Ich beschreibe hier kurz mal die Funktion:

Ich möchte über das UNO R3-Board einen einzigen Servo ansteuern. Ich habe 3 Potis (10K), 1 Drehgeber (mit Pushfunktion), 1 LCD (mit I2C Converter) und den Servo am Board angeschlossen.

Mit Poti 1&2 lege ich einen Wert für den Winkel für den maximalen Ausschlag nach Rechts und Links fest (Werte L&R auf dem Display), mit Poti 3 lege ich einen Wert für den Speed des Servos fest (Wert S auf dem Display). Mit dem Drehgeber Steuer ich die Anzahl der Servobewegungen (Wert Z auf dem Display) und über die Pushfunktion starte ich die Servobewegung mit den eingestellten Werten.

Ich habe mir diesen Sketch aus verschiedenen Beispielen zusammengebastelt.

#include <Servo.h>
Servo myservo;  // create servo object to control a servo
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#define CLK 6
#define DT 3
#define SW 7
int currentStateCLK;
int lastStateCLK;
int counter = 5;
int climit = 99;
int const potPin0 = A0;  // analog pin used to connect the potentiometer1
int const potPin1 = A1;  // analog pin used to connect the potentiometer2
int const potPin2 = A2;  // analog pin used to connect the potentiometer3
int negativ;
int positiv;
int lowest;
int last_lowest;
int highest;
int last_highest;
int maxlim1 = 50;
int maxlim2 = 50;
int still = 90;
int pos = 90;
int way = 3;
int maxway = 50;
LiquidCrystal_I2C lcd = LiquidCrystal_I2C(0x27, 16, 2);
String currentDir ="";
unsigned long lastButtonPress = 0;
int case_state = 1;
int last_counter;
 
void setup() 
     {
        myservo.attach(8);  // attaches the servo on pin 8 to the servo object 
        //myservo.attach(8, 500, 2500); // some motors need min/max setting
        pinMode(LED_BUILTIN, OUTPUT);
        pinMode(CLK,INPUT);
        pinMode(DT,INPUT);
        pinMode(SW, INPUT_PULLUP);
        lastStateCLK = digitalRead(CLK);
        lcd.init();
        lcd.backlight();
        //delay(250);
        //lcd.noBacklight();
        //delay(1000);
        //lcd.backlight();
        //delay(1000);
        lcd.setCursor(0, 0);  //move cursor to row 1
        lcd.print("L:      R:      ");  // Print a message to the LCD
        lcd.setCursor(0, 1);  //move cursor to row 1
        lcd.print("S:      Z:      ");  // Print a message to the LCD
        lcd.setCursor(10, 1);
        lcd.print(counter); 
     }
 
void loop() {
  switch(case_state){
    case 1:                 
        lowest = map(analogRead(potPin1), 0, 1023, 0, maxlim1);  // read the value of the potentiometer1
        negativ = (still - lowest);
        highest = map(analogRead(potPin0), 0, 1023, 0, maxlim2);  // read the value of the potentiometer2
        positiv = (still + highest);
        way = map(analogRead(potPin2), 0, 1023, 3, maxway);  // read the value of the potentiometer3
        if(lowest < 10)
        {
          lcd.setCursor(3, 0);
          lcd.print(" ");
        }
        lcd.setCursor(2, 0);
        lcd.print(lowest);
        if(highest < 10)
        {
          lcd.setCursor(11, 0);
          lcd.print(" ");
        }
        lcd.setCursor(10, 0);
        lcd.print(highest);
        if(way < 10)
        {
          lcd.setCursor(3, 1);
          lcd.print(" ");
        }
        lcd.setCursor(2, 1);
        lcd.print(way);
        currentStateCLK = digitalRead(CLK);
          if (currentStateCLK != lastStateCLK  && currentStateCLK == 1)
           {
              if (digitalRead(DT) != currentStateCLK) 
                 {
                          counter = counter + 1;
                    if (counter > climit) 
                       {
                         counter = 0;
                       }
                          currentDir ="UP";
                     } else 
                      {
                                counter = counter - 1;
                          if (counter < 0) 
                             {
                                counter = climit;
                             }
                          currentDir ="DOWN";
                          }
                  if(counter < climit){
                    lcd.setCursor(10, 1);
                    lcd.print("      ");
                  }
                  lcd.setCursor(10, 1); 
                  lcd.print(counter); 
            }
    break;
    case 2:
      for(pos = negativ; pos <= positiv; pos += 1) // goes from negativ degrees to positiv degrees
      {       
        digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)                           
        myservo.write(pos);              // tell servo to go to position in variable 'pos'
        delay(way);                       // waits for the servo to reach the position 
      } 
      for(pos = positiv; pos>=negativ; pos -= 1)     // goes from positiv degrees to negativ degrees 
      {        
        digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW                        
        myservo.write(pos);              // tell servo to go to position in variable 'pos' 
        delay(way);                       // waits for the servo to reach the position 
      }
      if(counter != 0)
      {
        counter = (counter - 1);
        if(counter < climit){
                    lcd.setCursor(10, 1);
                    lcd.print("      ");
                  }
                  lcd.setCursor(10, 1); 
                  lcd.print(counter); 
      }
      else
      {
        myservo.write(still);  
        //delay(way);
        counter = last_counter;
        if(counter < climit)
        {
          lcd.setCursor(10, 1);
          lcd.print("      ");
        }
        lcd.setCursor(10, 1); 
        lcd.print(counter); 
        case_state = 1;
      }
    break;     
  }
  lastStateCLK = currentStateCLK;
  int btnState = digitalRead(SW);
    if (btnState == LOW) 
        {
               if (millis() - lastButtonPress > 50) 
                {
                  if(counter != 0){
                    if(case_state == 1){
                      last_counter = counter;
                      case_state = 2;
                    }
                    else{
                      case_state = 1;
                    }
                  }
                }
               lastButtonPress = millis();
          }
    //delay(1);
    }

Video des Servo-Projekt

Was mir persönlich noch nicht gefällt ist, daß der Servo beim Starten mit voller Geschwindigkeit auf den ersten Maximalwert stellt und dann erst mit der eingestellten Geschwindigkeit den Counter ablaufen lässt. Das gleiche passiert auch nochmal, wenn der Zähler fertig gelaufen ist und er wieder auf die Grundstellung fährt. Zudem reagiert der Drehgeber seltsam auf schnelle Drehbewegungen. Wenn ich diesen Wert langsam einstelle geht es.

Ich bin gerne für Verbesserungsvorschläge offen ;o)

Schönen Feiertag schonmal an Alle hier.

Gruss

Markus

da kannst mit normalen Mitteln nicht viel dagegen tun.
Du könntest vor dem Abschalten des Gerätes die Servo-Position im Eeprom speichern, und beim nächsten Start diese Position als Start-Position annehmen und von der "langsam" zu deiner Wunsch Start-Postion fahren.

Zur Laufzeit kanns du das eigentlich im Code machen. Du machst einfach langsam "kleine" Schritte mit einer Pause dazwischen.
Ich mach das z.B. so:

Das versuche ich gerne mal. Danke für den Link :+1:

Schau mal, ob Dir meine schon etwas angestaubte Anleitung: RC-Servo mit Istpositionsmeldung - Anfangszucken unterdrücken hilft.

Unabhängig davon ist die Bibliothek MobaTools gut für langsame Servobewegungen geeignet. Die kannst Du mittels Bibliotheksverwaltung der IDE installieren und ausprobieren. Viele Beispiele und eine gute Beschreibung helfen Dir. Mein Tipp :wink:

Auch dir schonmal dank für den Tipp und den Link. Ich teste mal, was vielleicht gut funktioniert.

#include <Servo.h>
Servo myservo;  // create servo object to control a servo
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
constexpr uint8_t CLK {6};
constexpr uint8_t DT {3};
constexpr uint8_t SW {7};
bool currentStateCLK;
bool lastStateCLK;
uint8_t counter = 5;
uint8_t climit = 99;
constexpr uint8_t potPin0 {A0};  // analog pin used to connect the potentiometer1
constexpr uint8_t potPin1 {A1};  // analog pin used to connect the potentiometer2
constexpr uint8_t potPin2 {A2};  // analog pin used to connect the potentiometer3
uint8_t negativ;
uint8_t positiv;
uint8_t lowest;
uint8_t last_lowest;
uint8_t highest;
uint8_t last_highest;
constexpr uint16_t maxlim1 {50};
constexpr uint16_t maxlim2 {50};
constexpr uint8_t still {90};
uint8_t pos = 90;
uint8_t way = 3;
constexpr uint8_t maxway {50};
LiquidCrystal_I2C lcd = LiquidCrystal_I2C(0x27, 16, 2);
String currentDir = "";
unsigned long lastButtonPress = 0;
uint8_t case_state = 1;
uint8_t last_counter;

void setup()
{
  myservo.attach(8);  // attaches the servo on pin 8 to the servo object
  //myservo.attach(8, 500, 2500); // some motors need min/max setting
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(CLK, INPUT);
  pinMode(DT, INPUT);
  pinMode(SW, INPUT_PULLUP);
  lastStateCLK = digitalRead(CLK);
  lcd.init();
  lcd.backlight();
  //delay(250);
  //lcd.noBacklight();
  //delay(1000);
  //lcd.backlight();
  //delay(1000);
  lcd.setCursor(0, 0);  //move cursor to row 1
  lcd.print("L:      R:      ");  // Print a message to the LCD
  lcd.setCursor(0, 1);  //move cursor to row 1
  lcd.print("S:      Z:      ");  // Print a message to the LCD
  lcd.setCursor(10, 1);
  lcd.print(counter);
}

void loop()
{
  checkBtn();

  switch (case_state)
  {
    case 1:
      getAnalogData();
      printAnalogData();
      getRotary();
      break;

    case 2:
      driveAndCount();
      break;
  }
}

void getRotary()
{
  currentStateCLK = digitalRead(CLK);

  if (currentStateCLK != lastStateCLK && currentStateCLK == 1)
  {
    if (digitalRead(DT) != currentStateCLK)
    {
      counter++;

      if (counter > climit)
      { counter = 0; }

      currentDir = "UP";
    }
    else
    {
      if (counter > 0)
      { counter--; }
      else
      { counter = climit; }

      currentDir = "DOWN";
    }

    if (counter < climit)
    {
      lcd.setCursor(10, 1);
      lcd.print("      ");
    }

    lcd.setCursor(10, 1);
    lcd.print(counter);
  }

  lastStateCLK = currentStateCLK;
}

void getAnalogData()
{
  lowest = map(analogRead(potPin1), 0, 1023, 0, maxlim1);  // read the value of the potentiometer1
  negativ = (still - lowest);
  highest = map(analogRead(potPin0), 0, 1023, 0, maxlim2);  // read the value of the potentiometer2
  positiv = (still + highest);
  way = map(analogRead(potPin2), 0, 1023, 3, maxway);  // read the value of the potentiometer3
}

void printValue(const int val)
{
  if (val < 10)
  { lcd.print(' '); }

  lcd.print(val);
}

void printAnalogData()
{
  lcd.setCursor(2, 0);
  printValue(lowest);
  lcd.setCursor(10, 0);
  printValue(highest);
  lcd.setCursor(2, 1);
  printValue(way);
}

void printCounter()
{
  if (counter < climit)
  {
    lcd.setCursor(10, 1);
    lcd.print("      ");
  }

  lcd.setCursor(10, 1);
  lcd.print(counter);
}

void driveAndCount()
{
  for (pos = negativ; pos <= positiv; pos += 1) // goes from negativ degrees to positiv degrees
  {
    digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
    myservo.write(pos);              // tell servo to go to position in variable 'pos'
    delay(way);                       // waits for the servo to reach the position
  }

  for (pos = positiv; pos >= negativ; pos -= 1)  // goes from positiv degrees to negativ degrees
  {
    digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
    myservo.write(pos);              // tell servo to go to position in variable 'pos'
    delay(way);                       // waits for the servo to reach the position
  }

  if (counter != 0)
  {
    counter--;
    printCounter();
  }
  else
  {
    myservo.write(still);
    //delay(way);
    counter = last_counter;
    printCounter();
    case_state = 1;
  }
}

void setState()
{
  if (counter != 0)
  {
    if (case_state == 1)
    {
      last_counter = counter;
      case_state = 2;
    }
    else
    {
      case_state = 1;
    }
  }
}

void checkBtn()
{
  static bool lastBtnState = HIGH;
  bool btnState = digitalRead(SW);

  if (btnState == LOW)                           // Taste gedrückt
  {
    if (lastBtnState == HIGH)                    // Vorher nicht gedrückt
    {
      setState();                                // State / Counter
      lastBtnState = LOW;                        // Merken, dass gedrückt
      lastButtonPress = millis();                // Auslösezeit merken
    }
  }
  else                                           // Taste losgelassen
  {
    if (lastBtnState == LOW)                     // Vorher war gedrückt
    {
      if (millis() - lastButtonPress > 50)       // Zeit ist abgelaufen
      { lastBtnState = HIGH; }                   // Merken, dass losgelassen
    }
  }
}

driveAndCount() muss noch aufgelöst werden, aber es ist ganz viel passiert, da haste genug zum lesen :slight_smile:

Oha, da haste aber was umgebaut :upside_down_face: ... Da habe ich nun erstmal genug zu verstehen :slightly_smiling_face:

Ich danke dir schon einmal für deine Mühe. Ich habe sicher noch Fragen dazu :joy:

Grüße

Ich habe direkt mal noch eine Frage zu deinem Umbau. Was ist der Grund, diese ganzen Funktionen in eigene Blöcke zu schieben ?
Ist es nur wegen der Übersicht oder hat es einen anderen Grund nicht alles einfach in den Loop zu schreiben ?
Gruss.

Ein Spaghetticode hat den Nachteil, dass Du unten schon nicht mehr weisst, was oben passiert ist und ob die Variable, die Du da nutzt irgendwo anders abhängig ist.

Bestes Beispiel ist

Schau Dir mal an, wo das bei Dir steht und wo bei mir :wink:

Alles was zusammengehört fasse zusammen.
Was nicht dazu gehört mache in eine weitere Funktion.
Mache Funktionen Wiederverwendbar!
Schau Dir den Unterschied:

und das:

an. Das ist so schön runter gekürzt.
printValue() liesse sich auch noch für andere Ausgaben verwenden - ist dann nur eine Zeile irgendwo im Code.

Vermeide Codeduplikate!
Du suchst Dich später dumm und dämlich, wenn Du bei einer Änderng an einer Stelle die andere Stelle vergisst.

Und als letztes:
Wenn Du z.B. die Poti-Werte nicht angezeigt haben möchtest, reicht eine einzige Kommentierung

wird dann:
//printAnalogData();
und schon ist alles erledigt.
Das ist z.B. zum schnellen debuggen unerlässlich, anstatt zu suchen, was Du von wo bis wo auskommentieren müsstest...

Diejenigen, die Dir hier helfen, haben üblicherweise größere Projekte vor Augen, bei denen man schnell die Übersicht verliert, wenn man nicht von Anfang an Ordnung hält, was beim Programmieren dann auch Modularisierung - oder wie immer man es nennen mag - bedeutet.

Beispielsweise arbeitet eine Funktion mit lokalen Variablen. Die Parameter bilden eine definierte Schnittstelle nach außen. Hat eine Funktion die gewünschte Funktionalität, kann man deren Inhalt quasi "vergessen". Hat man eine lange Zeit an einem Programm gefeilt, lernt man die Vorteile schätzen.

Bei größeren Projekten kann man ein Programm auch auf mehrere Dateien verteilen, was Du bei der Nutzung von Bibliotheken schon machst. Die IDE unterstützt mehrere Dateien durch die Anzeige in Tabs.

Bei Fips findest Du mehrere Tabs, die man sich nach Bedarf zusammenstellt.

Einige Themen in diesem Forum starteten mit ein paar Zeilen und wuchsen dann auf viele Zeilen. Daher ist eine strukturierte Programmierung von Anfang an eine gute Sache :wink:

Vielen Dank für eure Antworten. Das mit der Übersichtlichkeit verstehe ich voll und ganz :+1:

Das bedeutet ich kann Funktionen in den voids schön von einander abgrenzen und über eine Zeile im loop dann aufrufen ?

Spielt es demnach keine Rolle ob die voids über oder unter dem loop stehen oder ?

Grüße

Zunächst mal: es gibt keine 'voids'. Das sind alles die Funktionen. Das void am Anfang der Funktionsdefinition bedeutet nur, dass diese Funktion keinen Wert zurückgibt. Man kann Funktionen nämlich auch so schreiben, dass sie einen Wert zurückgeben. Ein Beispiel dafür ist die millis() Funktion.

Normalerweise schon. Grundsätzlich gilt bei C/C++, dass alles, was Du verwendest vorher dem Compiler bekannt gemacht werden muss. So musst Du auch eine Variable definieren bevor Du sie das erste Mal benutzt. Das gleiche gilt für Funktionen. Insofern spielt die Reihenfolge eigentlich immer eine Rolle. Die Arduino IDE erleichtert die Dir da etwas das Leben, indem sie dies bei Funktionen für dich erledigt. D.h., wenn Du die Funktion, die Du im loop() aufrufst, erst hinter loop() definierst, fügt die IDE automatisch am Anfang eine Zeile ein, um die Funktion dem Compiler bekannt zu machen ( nennt sich 'Funktionsdeklaration :wink: ).
Meistens funktioniert das auch ...

P.S. Diese eingefügte(n) Zeilen(n) siehst Du in deinem Sketch nicht. Die IDE erzeugt dazu einen Zwischendatei, die dann kompiliert wird

Das was Du voids nennst, sind Funktionen :slight_smile:
Die geben void - also nix zurück.

Und ja, das ist der Sinn dahinter, dass Du Funktionsblöcke nicht kopierst, sondern einmal einen schreibst und den dann an jeder Stelle im Code wieder aufrufen kannst.
(Siehe mein Beispiel mit der numerischen Ausgabe auf dem LCD)

Eigentlich spielt es eine Rolle, dass Du Funktionen erst bekannt machen musst, bevor Du sie verwenden kannst.
Das geht entweder mittels "Prototypen" Also nur den Funktionsnamen am Anfang bekannt machen und dann irgendwo im Code die Funktion schreiben, oder besser die Anordnung im Code so gestalten, dass die Funktionen die aufgerufen werden vorher schon bekannt sind.

Der Compiler arbeitet eigentlich von oben nach unten, hier liegt aber der Vorteil der IDE die das Prototyping vorher macht, wenn es die Funktionen erkennt und Du damit an jeder Stelle im Code neue Funktionen schreiben oder Teile ausgliedern kannst.

Es empfiehlt sich aber grundsätzlich auch die Funktionen so zu platzieren, dass ein möglicher Zusammenhang erkennbar bleibt.
@MicroBahner war nen Ticken schneller :slight_smile: aber wir schreiben das selbe...

Doppelt hält besser :smiling_face_with_sunglasses: :joy:

Haha, ich danke euch beiden trotzdem für die ausführliche Erklärung.

Ich hatte mich schon etwas gewundert, warum du in deinem Beispiel die Funktionen nach dem loop eingefügt hast.

Merci :upside_down_face:

Ich hätte da auch einen Verbesserungsvorschlag:

Programmcode
// https://forum.arduino.cc/t/sketch-fur-servo-mit-3-potis-drehgeber-lcd-verbesserungsvorschlage/1385507

// This code is orientated to the llvm coding standard
// https://llvm.org/docs/CodingStandards.html

#include <Servo.h>               // https://github.com/arduino-libraries/Servo
#include <LiquidCrystal_I2C.h>   // https://github.com/johnrickman/LiquidCrystal_I2C
#include <RotaryEncoder.h>       // https://github.com/mathertel/RotaryEncoder
#include <Button_SL.hpp>         // https://github.com/DoImant/Button_SL

//
// Global constants (gc)
//
namespace gc {
constexpr uint8_t LcdCols {16};
constexpr uint8_t LcdRows {2};

constexpr uint8_t EncPinClk {6};
constexpr uint8_t EncPinDt {3};
constexpr uint8_t EncPinSw {7};   // Button of encoder
constexpr uint8_t ServoPin {8};

constexpr int CounterStart {5};
constexpr int CounterMax {99};

constexpr int HomePosition {90};
constexpr int LimitLowest {50};
constexpr int LimitHighest {50};
constexpr int LimitWay {50};
constexpr int DelayMin_ms {10};                   // Minimum Delay for servo movement in milliseconds;

constexpr int AnalogResolution {(1 << 10) - 1};   // 10 Bit
}   // namespace gc

//
// Data definitions
//
enum class State : uint8_t { Input, Run };
enum class PotiName : uint8_t { Lowest, Highest, Speed };

// Return enumerators of class <T> as number
template <typename T> constexpr uint8_t toIndex(T Enumerator) noexcept { return static_cast<uint8_t>(Enumerator); }

struct PotiData {
  const uint8_t Pin;
  const int MaxValue;
  int Value;
};

//
// global Objects / variables
//

// {PinNr, Max. possible Value, measured Value}
PotiData Potis[] {
    {A1, gc::LimitLowest,  0}, // lowest
    {A0, gc::LimitHighest, 0}, // highest
    {A2, gc::LimitWay,     0}  // way
};

LiquidCrystal_I2C Lcd = LiquidCrystal_I2C(0x27, gc::LcdCols, gc::LcdRows);
RotaryEncoder Encoder {gc::EncPinClk, gc::EncPinDt, RotaryEncoder::LatchMode::FOUR3};
Servo MyServo;
Btn::ButtonSL Button {gc::EncPinSw};

//
// Functions
//

//////////////////////////////////////////////////////////////////////////////
/// \brief Get the Encoder Value object
///
/// \param Enc        Reference to Encoder Object.
/// \param Value      Value to be set using the encoder.
/// \param ValueMax   Maximum value that can be set.
/// \return true      Value has been changed.
/// \return false     No change.
//////////////////////////////////////////////////////////////////////////////
bool getEncoderValue(RotaryEncoder& Enc, int& Value, const int ValueMax) {
  int SaveValue = Value;
  Enc.tick();
  switch (Enc.getDirection()) {
    case RotaryEncoder::Direction::CLOCKWISE: Value = (Value + 1) % ValueMax; break;
    case RotaryEncoder::Direction::COUNTERCLOCKWISE: Value = (Value != 0) ? Value - 1 : ValueMax; break;
    case RotaryEncoder::Direction::NOROTATION: break;
  }
  return (Value != SaveValue);   // true if the Value has been changed.
}

//////////////////////////////////////////////////////////////////////////////
/// \brief Display data
///
/// \param Disp Reference to Display Object
/// \param PD   Pointer to Array of Datasructure (analog values)
/// \param Cnt  Value of a counter to be displayed
//////////////////////////////////////////////////////////////////////////////
void dispPrint(LiquidCrystal_I2C& Disp, PotiData* PD, int Cnt) {
  char buffer[gc::LcdCols + 1];
  snprintf(buffer, sizeof(buffer), "L: %2d  R: %2d", PD[toIndex(PotiName::Lowest)].Value,
           PD[toIndex(PotiName::Highest)].Value);
  Disp.setCursor(0, 0);
  Disp.print(buffer);
  snprintf(buffer, sizeof(buffer), "S: %2d  Z: %2d", PD[toIndex(PotiName::Speed)].Value, Cnt);
  Disp.setCursor(0, 1);
  Disp.print(buffer);
}

//////////////////////////////////////////////////////////////////////////////
/// \brief Reading out the analog values of a set of potentiometers.
///
/// \tparam (&PD)[N]  Reference to Structure array and number of elements [N]
/// \return true      At least one value has changed
/// \return false     No changes
//////////////////////////////////////////////////////////////////////////////
template <size_t N> bool getPotiValues(PotiData (&PD)[N]) {
  decltype(PD[0].Value) SavedValue[N];
  for (size_t i = 0; i < N; ++i) {
    SavedValue[i] = PD[i].Value;
    PD[i].Value = map(analogRead(PD[i].Pin), 0, gc::AnalogResolution, 0, PD[i].MaxValue);
  }
  // Check whether one of the read analog values has changed.
  for (size_t i = 0; i < N; ++i) {
    if (SavedValue[i] != PD[i].Value) { return true; }   // Yes. At leased one value has been changed
  }
  return false;
}

//
// Main Program
//
void setup() {
  Serial.begin(115200);
  MyServo.attach(gc::ServoPin);   // attaches the servo on pin 8 to the servo object
  pinMode(LED_BUILTIN, OUTPUT);

  Button.begin();
  Button.setDebounceTime_ms(30);
  Lcd.init();
  Lcd.backlight();

  getPotiValues(Potis);
  delay(100);
  dispPrint(Lcd, Potis, gc::CounterStart);
}

void loop() {
  static State CaseState {State::Input};   // Statemachine
  static int Counter {gc::CounterStart};   // Run Counter (Set via rotary encoder)
  static int SaveCounter {Counter};

  switch (CaseState) {
    case State::Input:
      // Returns true if at least one analog value or counter has changed. If so display new Value(s).
      if (getPotiValues(Potis) || getEncoderValue(Encoder, Counter, gc::CounterMax)) { dispPrint(Lcd, Potis, Counter); }
      if (Button.tick() != Btn::ButtonState::notPressed) {
        // Do only run if both values aren't zero.
        if ((Potis[toIndex(PotiName::Lowest)].Value + Potis[toIndex(PotiName::Highest)].Value) && Counter > 0) {
          CaseState = State::Run;
          SaveCounter = Counter;
        }
      }
      break;
    case State::Run: {   // <-This curly bracket is necessary so that local variables can be defined in the case branch.
     
      --Counter;
      // Calculate the range of movement
      int Negative = (gc::HomePosition - Potis[toIndex(PotiName::Lowest)].Value);
      int Positive = (gc::HomePosition + Potis[toIndex(PotiName::Highest)].Value);

      // Repeat the movement until the counter is zero.
      for (int Pos = Negative; Pos <= Positive; ++Pos)   // goes from negativ degrees to positiv degrees
      {
        digitalWrite(LED_BUILTIN, HIGH);                 // turn the LED on (HIGH is the voltage level)
        MyServo.write(Pos);                              // tell servo to go to position in variable 'pos'
        delay(Potis[toIndex(PotiName::Speed)].Value + gc::DelayMin_ms);   // waits for the servo to reach the position
      }
      for (int Pos = Positive; Pos >= Negative; --Pos)   // goes from positiv degrees to negativ degrees
      {
        digitalWrite(LED_BUILTIN, LOW);                  // turn the LED off by making the voltage LOW
        MyServo.write(Pos);
        delay(Potis[toIndex(PotiName::Speed)].Value + gc::DelayMin_ms);   // waits for the servo to reach the position
      }
      // Movement sequence completed if Counter = 0. Enable new input.
      if (Counter < 1) {
        MyServo.write(gc::HomePosition);
        Counter = SaveCounter;
        CaseState = State::Input;
      }
      dispPrint(Lcd, Potis, Counter);
    } break;
  }
}

Weil der Servo mit Delays betrieben wird, ist die Eingabe so angelegt, dass nach dem Start der Bewegungssequenz keine Eingabe mehr möglich ist, bis diese beendet ist. Wegen der Delays ist da nämlich keine Vernünftige Eingabe möglich.

Für den Encoder und die Tastenabfrage wurden Bibliotheken verwendet. Wenn man den Umgang mit Tastern und Encodern lernen will, erscheint mir ein spezielles Programm für Taster oder Encoder geeigneter. Durch die Nutzung von Bibliotheken wird der Code meistens kürzer und auch lesbarer.

Eine Fehlerfreiheit des vorgeschlagenen Codes ist nicht garantiert :wink:.

Zum Ausprobieren:

Für einen Einsteiger sicher nicht leicht zu verstehen.... aber was zum Durchbeissen...

Warum die Konstanten mit constexpr und nicht mit const definiert sind?
Weil "const" konstante Variablen anlegt, die im RAM Speicher verbrauchen. Bei der Nutzung von "constexpr" werden die konstanten Werte gleich in den Quellcode einkompiliert und verbrauchen deshalb keinen RAM.

Die Nutzung von #define ist nicht typensicher. Namespaces schützen vor Mehrfachdefinitionen....

Ergänzung:
Eine nicht blockierende Variante. Hier kann die Bewegungssequenz Jederzeit duch einen Druck auf den Taster unterbrochen werden:

Programmcode 2
// https://forum.arduino.cc/t/sketch-fur-servo-mit-3-potis-drehgeber-lcd-verbesserungsvorschlage/1385507

// This code is orientated to the llvm coding standard
// https://llvm.org/docs/CodingStandards.html

#include <Servo.h>               // https://github.com/arduino-libraries/Servo
#include <LiquidCrystal_I2C.h>   // https://github.com/johnrickman/LiquidCrystal_I2C
#include <RotaryEncoder.h>       // https://github.com/mathertel/RotaryEncoder
#include <Button_SL.hpp>         // https://github.com/DoImant/Button_SL

//
// Global constants (gc)
//
namespace gc {
constexpr uint8_t LcdCols {16};
constexpr uint8_t LcdRows {2};

constexpr uint8_t EncPinClk {6};
constexpr uint8_t EncPinDt {3};
constexpr uint8_t EncPinSw {7};   // Button of encoder
constexpr uint8_t ServoPin {8};

constexpr int CounterStart {5};
constexpr int CounterMax {99};

constexpr int HomePosition {90};
constexpr int LimitLowest {50};
constexpr int LimitHighest {50};
constexpr int LimitWay {50};
constexpr int DelayMin_ms {5};                   // Minimum Delay for servo movement in milliseconds;

constexpr int AnalogResolution {(1 << 10) - 1};   // 10 Bit
}   // namespace gc

using MillisType = decltype(millis());

//
// Class definitions
//
//////////////////////////////////////////////////////////////////////////////
/// \brief Helperclass for a non blocking delay
///
//////////////////////////////////////////////////////////////////////////////
class NbDelay {
public:
  void start() { timestamp = millis(); }
  boolean operator()(const MillisType duration) { return millis() - timestamp >= duration; }

private:
  MillisType timestamp {0};
};

//
// Data definitions
//
enum class PotiName : uint8_t { Lowest, Highest, Speed };
enum class State : uint8_t { Input, Run };
enum class ServoState : uint8_t { InitN, InitP, RunN, RunP };

// Return enumerators of class <T> as number
template <typename T> constexpr uint8_t toIndex(T Enumerator) noexcept { return static_cast<uint8_t>(Enumerator); }

struct PotiData {
  const uint8_t Pin;
  const int MaxValue;
  int Value;
};

//
// Global objects / variables
//

// {PinNr, Max. possible Value, measured Value}
PotiData Potis[] {
    {A1, gc::LimitLowest,  0}, // lowest
    {A0, gc::LimitHighest, 0}, // highest
    {A2, gc::LimitWay,     0}  // way
};

LiquidCrystal_I2C Lcd = LiquidCrystal_I2C(0x27, gc::LcdCols, gc::LcdRows);
RotaryEncoder Encoder {gc::EncPinClk, gc::EncPinDt, RotaryEncoder::LatchMode::FOUR3};
Servo MyServoDev;
Btn::ButtonSL Button {gc::EncPinSw};
NbDelay Wait;

//
// Functions
//

//////////////////////////////////////////////////////////////////////////////
/// \brief Get the Encoder Value object
///
/// \param Enc        Reference to Encoder Object.
/// \param Value      Value to be set using the encoder (reference).
/// \param ValueMax   Maximum value that can be set.
/// \return true      Value has been changed.
/// \return false     No change.
//////////////////////////////////////////////////////////////////////////////
bool getEncoderValue(RotaryEncoder& Enc, int& Value, const int ValueMax) {
  int SaveValue = Value;
  Enc.tick();
  switch (Enc.getDirection()) {
    case RotaryEncoder::Direction::CLOCKWISE: Value = (Value + 1) % ValueMax; break;
    case RotaryEncoder::Direction::COUNTERCLOCKWISE: Value = (Value != 0) ? Value - 1 : ValueMax; break;
    case RotaryEncoder::Direction::NOROTATION: break;
  }
  return (Value != SaveValue);   // true if the Value has been changed.
}

//////////////////////////////////////////////////////////////////////////////
/// \brief Display data
///
/// \param Disp Reference to Display Object
/// \param PD   Pointer to Array of Datasructure (analog values)
/// \param Cnt  Value of a counter to be displayed
//////////////////////////////////////////////////////////////////////////////
void dispPrint(LiquidCrystal_I2C& Disp, PotiData* PD, int Cnt) {
  char buffer[gc::LcdCols + 1];
  snprintf(buffer, sizeof(buffer), "L: %2d  R: %2d", PD[toIndex(PotiName::Lowest)].Value,
           PD[toIndex(PotiName::Highest)].Value);
  Disp.setCursor(0, 0);
  Disp.print(buffer);
  snprintf(buffer, sizeof(buffer), "S: %2d  C: %2d", PD[toIndex(PotiName::Speed)].Value, Cnt);
  Disp.setCursor(0, 1);
  Disp.print(buffer);
}

//////////////////////////////////////////////////////////////////////////////
/// \brief Reading out the analog values of a set of potentiometers.
///
/// \tparam (&PD)[N]  Reference to Structure array and number of elements [N]
/// \return true      At least one value has changed
/// \return false     No changes
//////////////////////////////////////////////////////////////////////////////
template <size_t N> bool getPotiValues(PotiData (&PD)[N]) {
  decltype(PD[0].Value) SavedValue[N];
  for (size_t i = 0; i < N; ++i) {
    SavedValue[i] = PD[i].Value;
    PD[i].Value = map(analogRead(PD[i].Pin), 0, gc::AnalogResolution, 0, PD[i].MaxValue);
  }
  // Check whether one of the read analog values has changed.
  for (size_t i = 0; i < N; ++i) {
    if (SavedValue[i] != PD[i].Value) { return true; }   // Yes. At leased one value has been changed
  }
  return false;
}

//////////////////////////////////////////////////////////////////////////////
/// \brief Non blocking servo move
///
/// \param PD       Pointer to Array of Datasructure (analog values)
/// \param MyServo  Reference to servo object
/// \param State    Reference to status variable for the servo movement.
/// \return true    when movement sequence is finished
/// \return false   not finnished yet
//////////////////////////////////////////////////////////////////////////////
bool runMyServo(PotiData* PD, Servo& MyServo, ServoState& State) {
  constexpr uint8_t Highest {toIndex(PotiName::Highest)};
  constexpr uint8_t Lowest {toIndex(PotiName::Lowest)};
  constexpr uint8_t Speed {toIndex(PotiName::Speed)};

  static int Pos {0};
  static int Threshold {0};

  switch (State) {
    case ServoState::InitN:
      digitalWrite(LED_BUILTIN, HIGH);
      Pos = (gc::HomePosition - PD[Lowest].Value);
      Threshold = gc::HomePosition + PD[Highest].Value;
      State = ServoState::RunN;
      [[fallthrough]];
    case ServoState::RunN:
    if (Wait(PD[Speed].Value + gc::DelayMin_ms)) {
      if (Pos < Threshold) {
          Wait.start();
          ++Pos;
          MyServo.write(Pos);
        } else {
          State = ServoState::InitP;
        }
      }
      break;
    case ServoState::InitP:
      digitalWrite(LED_BUILTIN, LOW);
      Threshold = gc::HomePosition - Potis[Lowest].Value;
      State = ServoState::RunP;
      [[fallthrough]];
      case ServoState::RunP:
      if (Wait(PD[Speed].Value + gc::DelayMin_ms)) {
        if (Pos > Threshold) {
          Wait.start();
          --Pos;
          MyServo.write(Pos);
        } else {
          State = ServoState::InitN;
          return true;
        }
      }
      break;
  }
  return false;
}

//
// Main Program
//
void setup() {
  // Serial.begin(115200);
  MyServoDev.attach(gc::ServoPin);   // attaches the servo on pin 8 to the servo object
  pinMode(LED_BUILTIN, OUTPUT);

  Button.begin();
  Button.setDebounceTime_ms(30);
  Lcd.init();
  Lcd.backlight();

  getPotiValues(Potis);
  dispPrint(Lcd, Potis, gc::CounterStart);
}

void loop() {
  static State CaseState {State::Input};
  static ServoState MyServoState {ServoState::InitN};

  static int Counter {gc::CounterStart};   // Run Counter (Set via rotary encoder)
  static int SaveCounter {Counter};

  switch (CaseState) {
    case State::Input:
      // Returns true if at least one analog value or counter has changed. If so display new Value(s).
      if (getPotiValues(Potis) || getEncoderValue(Encoder, Counter, gc::CounterMax)) { dispPrint(Lcd, Potis, Counter); }
      if (Button.tick() != Btn::ButtonState::notPressed) {
        // Do only run if both values aren't zero.
        if ((Potis[toIndex(PotiName::Lowest)].Value + Potis[toIndex(PotiName::Highest)].Value) && Counter > 0) {
          CaseState = State::Run;
          SaveCounter = Counter;
        }
      }
      break;
    case State::Run:
      if (runMyServo(Potis, MyServoDev, MyServoState)) { 
        --Counter; 
        dispPrint(Lcd, Potis, Counter);
      }
      if (Counter < 1 || Button.tick() != Btn::ButtonState::notPressed) {   // leave State::Run if true
        MyServoDev.write(gc::HomePosition);
        Counter = SaveCounter;
        CaseState = State::Input;
        MyServoState = ServoState::InitN;
        dispPrint(Lcd, Potis, Counter);
      }
      break;
  }
}

Zum Ausprobieren:

Auch dir ein Danke. Gibt wirklich noch ne ganze Menge, was ich da wohl zu lernen habe :smiling_face_with_sunglasses:

Das garantiert der Compiler zwar nicht, ist ihm aber erlaubt. Sowohl bei const wie bei constexpr

constexpr  int i=123;
void setup() {
  Serial.begin(115200);
  Serial.println(i);
  Serial.println((uint32_t)&i,HEX);
}

constexpr gibt einen Übersetzungsfehler, wenn es nicht möglich ist, den Wert schon zur Compilezeit zu ermitteln.

Ja. Natürlich.

Was den RAM-Bedarf angeht, kannst du bei beiden (const / constexpr) erzwingen, dass der Compiler RAM belegt, oder dich freuen wie gut er optimieren kann.
Das ist nicht der Gegensatz zwischen const und constexpr, wollte ich klugscheißend den Beitrag #16 ergänzen