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);
}
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)
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:
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
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
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
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 ).
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
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 aber wir schreiben das selbe...
// 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 .
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;
}
}
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