Aufteilen bzw. zerteilen von Chars

  • Normalerweise enthält die Klammer der While-Schleife einen Vergleich/eine Bedingung. Ist die Bedingung erfüllt ist sie „wahr“ was dem Wert 1 entspricht. Die While-Schleife wird immer so lange ausgeführt, solange die Bedingung „wahr“ ist.

    Schreibt man eine 1 in die Klammer, so ist die Bedingung für immer und ewig „wahr“. Der Ausdruck „While(true)“ wird auch gerne verwendet.


    Ich habe das Char-Array mit dem Zeichen '\x2D' gefüllt, um das Debuggen zu vereinfachen, falls man (formatierte) Zwischenausgaben mit Serial.print machen möchte. Das entspricht schlichtweg dem hexadezimalen ASCII Code für das "-" Zeichen. Wäre das Array mit Nullen gefüllt, würde von Serial.print nichts angezeigt, weil das Array ja von rechts nach links beschrieben wird, die Ausgabe aber von links nach rechts erfolgt. Und die Null ist ja bekanntlich das String-Ende Zeichen. Man würde erst etwas sehen, wenn alle 30 Zeichen geschrieben wurden.

    Das Reh springt hoch. Das Reh springt weit. Warum auch nicht? Es hat ja Zeit. 8o

  • Hallo ihr Beiden,


    ich finde es super wie ihr euch einsetzt und versucht zu optimieren und Wege zu zeigen wie es gemacht werden kann und auch noch so das es Anfänger verstehen können, naja so gut wie ;).

    Mir hilft das sehr, weil ich das Gezeigte nachstellen kann, um so Schritt für Schritt verstehen zu lernen.

    Ich bin hier ehrlich, alles habe ich noch nicht zu 100% verstanden :S


    KaiR

    ich habe deine Umsetzung mal ausprobiert. Sie funktioniert perfekt.

    Funktioniert id Endlosschleife in void setup() weil die 1 immer 1 in while(1) ist?


    was bedeutet das ‚\x2D’ in memset(msbRFIDCode,'\x2D',LEN_DATA);

  • Ich wollte, dass in der Funktion selbst kein Zeiger verändert werden kann. Auch wenn es sich dabei „nur“ um Kopien handelt. Mehr nicht. Ich wollte NICHT den Inhalt schützen. Siehe #43.


    Dann will ich auch mal eine abgewandelte Version der reverseCode() Funktion beisteuern. Natürlich funktioniert sie im Prinzip genau so, wie die von Pius vorgestellte Variante aus dem Beitrag #42. Allerdings wird hier nicht mit Zeigern gerechnet und der Zugriff auf den Inhalt des Arrays erfolgt auf die für Einsteiger eher verständliche und auch in Lehrbüchern häufig vermittelte Weise.


    Code
    1. void reverseCode(char* const lsbString) {
    2. byte len = strlen(lsbString)-1, y;
    3. char tmp;
    4. while(len > y) {
    5. tmp = lsbString[len];
    6. lsbString[len--] = lsbString[y];
    7. lsbString[y++]=tmp;
    8. }
    9. }


    Welche Variante nun wirklich als allgemein verständlicher einzustufen ist, bleibt dem geneigten Leser überlassen. Auch in der, meiner Meinung nach verständlicheren Variante, ist es wichtig zu erkennen, dass in der Zeile lsbString[len--] = lsbString[y]; ERST der Wert an die Position "len" geschrieben wird und DANN "len "um Eins dekrementiert wird. Das gleiche gilt für die darauf folgende Zeile. Ein Umdrehen in "--len" und/oder "++y" wird für eine Überraschung sorgen.


    Außerdem habe ich die Laufzeit von drei Varianten verglichen. Die oben vorgestellte, die von Pius aus Beitrag #42 und die ebenfalls von Pius gezeigte, pfiffige XOR Variante aus Beitrag #40 (letztes Beispiel):


    Zeitverhalten-Vergleich.png


    Es wurde jeweils vor und nach dem Aufruf einer Funktion die Zeit mit micros() genommen. Die Differenz aus beiden Zeiten ergibt die Laufzeit. Alle drei Funktionen wurden 10 mal ausgeführt. Die Angaben sind natürlich nicht besonders genau, weil die Rechenzeit für das Speichern der Zeit und die Berechnung der Zeitdifferenz auch in den Wert mit einfließt. Außerdem beträgt die Auflösung des micros() Befehls bei 16Mhz nur 4μs. Es wird also immer nur ein Vielfaches von vier zurückgeliefert. Dennoch sind die Werte genau genug, um eine Tendenz aufzuzeigen.


    Es zeigt sich, dass Pius' hochoptimierte Funktion im Durchschnitt die schnellste ist. Erwartungsgemäß ist die XOR Variante die, die am meisten Zeit verbraucht. Es zeigt sich aber auch, dass die reverseCode() Variante nur wenig langsamer als die hochoptimierte strMirror() Funktion ist.


    Wenn es darum geht, in einem Programm einen String zu drehen wenn ein RFID Code empfangen wird, ist eine 4µs langsamere Verarbeitung sicher zu verkraften. Anders wäre es, wenn es um einen Prozess ginge, der tausende Male durchgeführt werden muss. Hier würde sich die Differenz zu einer beachtenswerte Größe aufaddieren.


    Es zeigt sich mal wieder, dass es für die Programmierung kein festes Rezept gibt. Es ist immer ein Abwägen zwischen Les- und Wartbarkeit des Codes und dem herausholen des Optimums. Ziel ist immer ein möglichst fehlerfreies Programm. Dabei helfen möglichst einfache Konstrukte und eine gute Lesbarkeit des Codes.


    Wer die Zeigerarithmetik beherrscht und Code schreibt, den kein anderer verstehen muss außer er selbst, der kann natürlich optimieren und kürzen bis der Arzt kommt. Manchmal ist es ja auch notwendig, wenn beispielsweise ein bestimmtes Laufzeitverhalten/ ein bestimmter Speicherverbrauch erforderlich ist.


    Wer aber nicht so hochbewandert ist und seinen Code auch noch verstehen will, wenn er sich nach längerer Zeit mal wieder damit beschäftigen muss, sollte sich lieber auf einfachere Elemente beschränken. Macht man keine groben Fehler, nimmt einem der Compiler schon einiges an Optimierung ab. Dazu ist er ja eigentlich auch da. In der Regel konzentriert man sich auf das zu lösende Problem und nicht auf die Rechnerarchitektur.


    Wie Pius' Beschreibungen zeigen, ist die Kenntnis dieser Dinge natürlich sehr hilfreich nahe an ein Optimum zu kommen, ohne sich auf die Compilertechnik verlassen zu müssen und sie hilft, von vornherein schnellen und sparsamen Code zu schreiben. Aber... dazu braucht es Zeit zum Lernen und viel Programmierpraxis. Gerade Fehler beim Umgang mit Zeigern sind berüchtigt, weil sie oft nicht so leicht zu erkennen sind.


    Es wird im Forum ja immer wieder betont, dass hier vorwiegend Einsteiger um Rat fragen. Da ist es natürlich nicht immer so einfach einzuschätzen, wieviel Komplexität bei den Antworten zuzumuten ist, ohne gleich abzuschrecken.


    Abgeschweift sind wir eh schon:). Was alles aus einer einfachen Frage werden kann...;)

    Das Reh springt hoch. Das Reh springt weit. Warum auch nicht? Es hat ja Zeit. 8o

  • Richtig

    die Deklaration char* const schützt den Zeiger vor einer Veränderung innerhalb der Funktion, nicht aber deren Inhalt.

    ABER, da die Parameterübergabe auf dem Stack (oder in den Registern) als Kopie erfolgt ist nach einem Aufruf ohne char* const der beim Aufruf übergebene Zeiger nach wie vor unverändert, auch wenn der Zeiger in der Funktion verändert wurde.


    Ich vermute, du wolltest const char * benutzen, weil dies schützt den Inhalt. Die Quelle lsbString willst du vermutlich vor dem irrtümlichen reinschreiben schützen.Das Ziel, da wo das Programm das Resultat hinein schreibt, darf dann logischerweise nicht als

    const char * definiert sein.

     


    Da der Zeiger asl Kopie übergeben wird,darf man das Veränderndes Zeigers innerhalb der Funktion locker zulassen.


    hier noch mein Testbeispiel zum probieren. Du kannst einfach durch Entfernen oder Einsetzen der // die Sitation durchspielen und schnell sehenwann der Compiler Fehler meldet.

    schönen Abend

    Pius

  • Die Zeiger(kopien) wurden mit const deklariert, damit eben diese in der reversCode() Funktion nicht verändert werden können (auch nicht aus versehen). Schließlich sind sie ja die Basis für den indizierten Zugriff auf den Inhalt der Arrays gewesen. Eine Zeigerarithmetik wie bei den Dir vorgestellten Funktionen war ja gar nicht vorgesehen.


    Der „Inhalt“ wiederum sollte ja verändert werden.

    Das Reh springt hoch. Das Reh springt weit. Warum auch nicht? Es hat ja Zeit. 8o

  • Kai, es sind einfache Beispiele, ja, aber es fehlt der Kommentar, der für weniger Geübte wichtig wäre. Vielleicht hoffte ich, dass jemand Murphy's Spruch über das Kürzen eines Programmes ernst nimmt und mir zeigt wie man weiter kürzen könnte.

    Diesen Code kann man sicher noch um eine Zeile kürzen. Der Parameter lsbString ist ja bereits ein Zeiger und weshalb sollte ich einen neuen Zeiger anlegen um pLeft zu bekommen?

    Gut, eine Zeile weniger mit dem Resutat, dass der Code nicht mehr so schön zu lesen ist (nur die Namensgebung).

    Dieses Beispiel zeigt recht gut die Fähigkeiten des Compilers Code zu optimieren, denn die Grösse blieb identisch. Daraus schliesse ich, dass der Compiler die Unnötigkeit von pLeft selber erkannte und ihn ersetzte. Als Kontrolle für meine Behauptung verglich ich die Code Grösse im Debug Modus. Ohne Optimierer zeigen sich deutliche Unterschiede der Grösse.


    Die Grösse eines Programmes ist ein Teil, die Ausführgeschwindigkeit ein Anderer. Da die ATmega Serie viele Register zur Verfügung hat, wollte ich im folgenden Code den Effekt ausprobieren, dem Compiler den Tipp zu geben, möglichst viele Variablen in den Registern zu behalten.



    Das Schlüsselwort register teilt dem Compiler mit, dass er versuchen soll die mit register markierten Variablen in Registern zu halten. Dies würde zu einem verkleinerten Code und zu einer höheren Ausführgeschwindigkeit führen.


    Auch hier hat der Optimierer vom Compiler ganze Arbeit geleistet und hat den Umstand längst vor mir erkannt und optimiert.

    Eben weil die CPU viele Register besitzt, werden die Übergabeparameter an Funktionen in Registern an die Funktion übergeben und erst wenn es nicht mehr reicht, wird der Stack als Zwischenablage benutzt. Dies erspart nicht nur Code sondern vor allem Ausführzeit.

    Bei der Parameterübergabe via Stack muss ein

    • Register mit dem Wert geladen werden
    • das Register auf den Stack gepush't werden
      Funktion aufrufen
    • und nach dem Aufruf der Funktion
    • wird Wert vom Stack in ein Register gepop't

    Kai hat sich in seinem Beispiel überlegt, dass der übergebene Quell-Parameter an die Funktion nur lesend behandelt wird und deshalb mit:

    char* const lsbString

    dem Compiler dies mitgeteilt.


    Nur, das const, direkt vor dem Bezeichner, schützt den Zeiger und nicht den Inhalt worauf der Zeiger verweist und reagiert mit dem Fehler: Error increment of read-only parameter 'lsbString'


    Ein Zeiger, der als Parameter an eine Funktion übergeben wird ist immer eine Kopie des Zeigers. Deshalb ist es meistens sinnvoll, den Zeiger selbst nicht mit const zu schützen, sondern die Daten auf die der Zeiger zeigt
    const char* lsbString Angewendet auf mein Funktionsbeispiel meldet der Compiler: Error assignment of read-only location '*lsbString'

    Das war das was du vermutlich erreichen wolltest. In meinem Fall, wo kein separater Speicher für die Manipulation vorhanden ist, darf weder der Zeiger, noch der Inhalt mit const ausgezeichnet sein, da ich sowohl den Zeiger selbst wie auch den Inhalt verändere.


    So, bis dahin habe ich alles gesagt was man nicht erzählen muss und habe eine Lösung gezeigt, wie man ein Spiegeln eines Stringinhalts auch erreicht. Wie sieht es aus, wenn man irgend welche Werte (nicht nur ASCII Zeichen) spiegeln möchte, resp. wenn man beide Lösungen benötigt?


    Damit beende ich meine Versuche zu diesem Thema.

  • @Pius: Danke für die Beispiele. Die sind wirklich Anfängergerecht ;). Die Beispiele sind für die Bonusrunde....


    Die XOR Variante ist eine, an der auch ich zu knabbern habe. Auf sowas wäre ich nicht gekommen. Da hast Du mir mit dieser Art von Dreieckstausch wirklich was für mich Neues gezeigt.

    Das Reh springt hoch. Das Reh springt weit. Warum auch nicht? Es hat ja Zeit. 8o

  • Super Kai


    das ist beinahe die fertige Lösung. Damit es dir aber nicht langweilig wird, habe ich einen anderen Lösungsansatz

    für deine Funktion benutzt: void reverseCode(char* const lsbString, char* const msbString) ;


    Man braucht ja nicht immer eine Kopie der gespiegelten Zeichenkette, deshalb übergebe ich nur einen Parameter, der natürlich nicht const sein darf.

    Ich finde es immer wieder spannend, dass auch auf einemATmega der Compiler den Umgang mit Zeigern recht effizient umsetzen kann.

    Bei jedem der fünf Beispiele habe ich oben die Code Grösse meines gesamtenTestprogramms markiert. Dies ist immer im Release mit Optimierung übersetzt.

    Den RAM Bedarf kann man sich selbst ausrechnen.

    Grüsse

    Pius


  • Mi Ke :


    Nach der Beschreibung von dem dir genutzten RFID Reader muss ja am Ende alles herumgedreht werden, damit man auf die richtigen Werte für den Länder- und Kartencode kommt. Dazu haben wir uns Gedanken zu einer Routine gemacht, die einen String invertiert.

    In Deinem Fall, ausgehend davon, dass der Code immer 30 Byte lang ist und immer den gleichen Aufbau hat, wäre es doch sinnvoll die Daten direkt "verkehrt herum" einzulesen.


    Dann kann er nämlich sofort weiter verarbeitet werden. Das könnte so aussehen:



    Da ist jetzt einiges an Code mit den Funktionen. Wo ich aber drauf hinaus will ist die Endlosschleife in setup(); Dort ist ein Beispiel zu sehen, wie die Werte statt von der Position 0->30 des Char-Arrays "msbRFIDCode" von der Position 30->0 eingelesen werden. Dann spart man sich das spätere herumdrehen und damit Rechenzeit und Speicherplatz. Die Funktion reverseCode() wird überflüssig.


    Ich habe die Endlosschleife in setup() gepackt, damit man weniger globale Variablen definieren muss.


    Vielleicht kannst Du es ja so gebrauchen. Wenn nicht ... vielleicht ist der ein oder andere Denkanstoß dabei.


    Wenn keine neuen Fragen auftauchen, wars das aber erst mal dazu.

    Das Reh springt hoch. Das Reh springt weit. Warum auch nicht? Es hat ja Zeit. 8o

  • Um das mit dem Speicherverbrauch (Nur Flash) mal zu demonstrieren habe ich in meinem Gesamtprogramm nur folgende Funktion betrachtet:



    Nach dem Compilieren hat das (gesamte) Programm 2302 Bytes verbraucht. Die Funktion allein verbraucht natürlich weniger. Im folgenden wurde die Variable "digit" wegrationalisiert indem die Funktion hex2int() und die "Bitschieberei" zu einem Ausdruck zusammen gefasst wurde.


    Erwartungsgemäß ist der Speicherverbrauch geringer (2296 Bytes). Nun wollen wir noch richtig viel Speicher sparen und die "tmp" Variable wegrationalisieren:


    Code
    1. uint64_t hex2dec(const char* hexString) {
    2. static uint64_t res;
    3. int len = strlen(hexString);
    4. res=0;
    5. for(int i=0,y=len-1; i < len ; i++,y--) {
    6. res += ((uint64_t) 1 << (y*4))*hex2int(hexString[i]);
    7. }
    8. return res;
    9. }

    Entgegen meiner Erwartung beträgt der Speicherverbrauch immer noch 2296 Bytes. Es hat sich nichts verändert.

    Auch beim Speicherverbrauch im RAM gab es keinen Unterschied. Es ist davon auszugehen, dass der Compiler implizit einen Speicherbereich für Zwischenergebnisse schafft.


    Weiterhin denkt man "int" ist ein 16 Bit Datentyp, verbraucht also 2 Byte. Da der zu erwartende Wertebereich von "len" auch in 8 Bit passt, nimmt man doch besser den Datentyp "uint8_t" oder "byte" statt "int".


    Das habe ich gemacht, compiliert und was ist das Ergebnis? 2298 Byte. Weiß der Geier warum (habe ich bis gerade gedacht).


    Code
    1. uint64_t hex2dec(const char* hexString) {
    2. static uint64_t res;
    3. uint8_t len = strlen(hexString);
    4. res=0;
    5. for(int i=0,y=len-1; i < len ; i++,y--) {
    6. res += ((uint64_t) 1 << (y*4))*hex2int(hexString[i]);
    7. }
    8. return res;
    9. }

    Wenn man sich den Code genauer anschaut, wird auffallen, dass in der For-Schleife die "int" Variable "i" mit "uint8_t" "len" verglichen wird. Außerdem wird der "int" Variable y der "len" Wert zugewiesen. Hier macht der Compiler wahrscheinlich intern einen Typencast weil zwei unterschiedliche Datentypen miteinander verglichen bzw. einander zugewiesen werden.

    Code
    1. uint64_t hex2dec(const char* hexString) {
    2. static uint64_t res;
    3. uint8_t len = strlen(hexString);
    4. res=0;
    5. for(uint8_t i=0,y=len-1; i < len ; i++,y--) {
    6. res += ((uint64_t) 1 << (y*4))*hex2int(hexString[i]);
    7. }
    8. return res;
    9. }

    Deklariert man die "i" Variable (und in diesem Fall auch y) ebenfalls als 8-Bit Datentyp (uint8_t oder byte), sinkt der Speicherverbrauch auf 2272 Byte. Die effektivste "Sparmaßnahme" von allen. Man muss also gut aufpassen was man so an Datentypen miteinander "mischt" bei Vergleichen oder Zuweisungen.


    Alles ausprobiert mit Visual Studio Code (PlatformIO) und dem Atmel AVR Framework 3.3.0.

    Das Reh springt hoch. Das Reh springt weit. Warum auch nicht? Es hat ja Zeit. 8o

  • @Pius


    Ja der Code wurde ausprobiert. U.a. in Beitrag #35. Dort wurde auch die Verwendung von strlen() reduziert.


    Ich habe es auch mal auf einem "echten" Nano kaufen lassen. Was mir bei der Arduino Umgebung aufgefallen ist, ist der Umstand, dass die Nutzung von den Typen "byte" oder "uint8_t" mehr Speicherplatz verbraucht als "int". Warum auch immer.

    Aufgefallen ist mir das bei der Speicherung der Stringlänge. Da hatte ich zuerst auch erst uint8_t verwendet. Das hat zwei Byte 8| mehr verbraucht als die Verwendung von "int".


    Dein optimierter Ausdruck ist schwerlich noch weiter zu kürzen:thumbup:. Für Beginner verständlich, ist er allerdings nicht. Jedenfalls für die meisten. Für im Leben auf das Dezimalsystem konditionierte, ist die Bitschieberei erst einmal etwas unübersichtlich. Geht mir auch so.

    Das Reh springt hoch. Das Reh springt weit. Warum auch nicht? Es hat ja Zeit. 8o

  • Hallo Kai


    ich habe mir erlaubt dein Programm etwas genauer anzusehen und die Zeilen hier auf das was für meine Gedanken nötig ist zu reduzieren.
    Das ist dein Code, der so auch funktioniert, wobei ich das Resultat nicht nachgerechnet habe (du hast das sicher gemacht).

    Was mir aufgefallen ist, dass in jedem Schlaufendurchgang 2x die Länge des Strings bestimmt wird, die sich ja in der Schlaufe nicht verändert.

    Beim for() Konstrukt musste ich zuerst überlegen ... aber das geht so tatsächlich ist aber sicher für einen Anfänger etwas schwer lesbar.

    Das Fragment belegt 906 Byte Flash und 30 byte Ram.

    Nun ein Beispiel, das zwar für einen Anfänger auch nicht leicht zu lesen ist, aber zumindest das for() Konstrukt ist simpel.


    Den Schleifenblock kann man natürlich auch verständlicher schreiben und wieder temp. Variablen benutzen, aber damit wurde der RAM Bedarf auch etwas kleiner.


    In deinem Beispiel würde ich lediglich strlen() vor der Schleife, einmal erledigen.

    WIe sagte Murphy: " Jedes Programm lässt sich um eine Zeile kürzen, selbst wenn es nur aus einer Zeile besteht"

    Gruss

    Pius

  • Es ist recht einfach. Um den Wert 1 darzustellen muss ein Bit auf die erste Position (von rechts) einer Variablen geschoben werden, für 16 auf die Position 5, 256 auf die Position 9 usw.. Es wird wieder bei 0 angefangen (y=0 als Laufvariable immer um 1 inkrementiert) zu zählen:

    wert = ( 1 << 0 ) // Entspricht 160 0=y*4 1 20

    wert = ( 1 << 4 ) // Entspricht 161 4=y*4 16 24

    wert = ( 1 << 8 ) // Entspricht 162 8=y*4 256 28

    wert = ( 1 << 12 ) // Entspricht 163 12=y*4 4096 212

    usw.


    Im Programmcode ist es nur anders herum 9*4, 8*4...3*4, 2*4, 1*4, 0*4. Es erfolgt also auch eine Potenzrechnung.

    Ein Bitshift von 36 Bits (9*4) entspricht z.B. pow(16,9), 32 Bits = pow(16,8) usw..

    Das Reh springt hoch. Das Reh springt weit. Warum auch nicht? Es hat ja Zeit. 8o

  • @Pius

    danke für die Erklärung, ich glaube ich habe es verstanden :)


    KaiR

    habe ich noch nicht ganz verstanden, ich muß mir mir die Formel mal step boy step anschauen.

    Finde das Thema richtig spannend.

    Schön auch das du die Erkenntnis mit uns teilst:thumbup:

  • Mi Ke:


    Ich habe das Programm jetzt bei mir auch mal auf den Ardiuno Nano gepackt weil mich interessiert wie das da mit 64Bit Variablen so läuft. Ich muss feststellen, dass die "pow()" Funktion bei mir nur Grütze liefert.


    Die Kombination aus float und uint64_t kommt da offenbar nicht gut an.


    Darum habe ich die Berechnung durch ein Bit-Shift ersetzt. So wird nur noch mit Integer-Variablen gearbeitet. Das wiederum, funktioniert bei mir und zeigt auch bei sehr großen Zahlen das richtige Ergebnis an:



    Durch den Verzicht auf die pow() Funktion ist das Programm auch gute 2kb kleiner (2178Byte statt 4406Byte).

    Das Reh springt hoch. Das Reh springt weit. Warum auch nicht? Es hat ja Zeit. 8o

  • Der Preprocessor ist ein Werkzeug, das jeweils vor dem eigentlichen Compiliervorgang gestartet wird.

    Er hat die Hauptaufgabe, Makros aufzulösen und im Quelltext (da wo ein Makro dann benutzt wird) einzusetzen.

    Code
    1. #define hex2int(C) \
    2. (((C) >= '0' && (C) <= '9' ) ? C-'0' : ((C) >= 'A' && (C) <= 'F') ? C -'A' + 10 : -1)

    Das von Kai gezeigte Makro hat den Namen "hex2int()" und die darauffolgende Zeile wird im Code,überall da wo der Name hex2int() auftaucht

    eingesetzt.

    Wenn ich:

    #define MyName "Pius" // definiere


    und dann irgendwo im Code MyName benutze, wird das Stringliteral "Pius" eingesetzt.

    Gruss MyName

  • Was bedeutet das genau? Die Grundumwandlung von Hex in Dec habe ich verstanden, diese war vorher auch in der Funktion vorhanden.
    Das Thema PreProzessor ist mir neu


    #define hex2int(C) \

    (((C) >= '0' && (C) <= '9' ) ? C-'0' : ((C) >= 'A' && (C) <= 'F') ? C -'A' + 10 : -1)


    Gilt das für jeden Char und muss ich bei der Anwendung irgendetwas beachten?

    Hab den Aufruf nicht direkt gelesen, ist mir nun klar;)

  • Na prima :thumbup:. Dann hast Du ja jetzt was Du brauchst. Hoffen wir mal, dass der SRAM ausreicht für Dein Projekt :).

    Ich habe den Code in #27 nochmal etwas verkürzt.

    Das Reh springt hoch. Das Reh springt weit. Warum auch nicht? Es hat ja Zeit. 8o