mikrocontroller.net

Forum: Mikrocontroller und Digitale Elektronik Frage zu Timern in C


Announcement: there is an English version of this forum on EmbDev.net. Posts you create there will be displayed on Mikrocontroller.net and EmbDev.net.
Autor: Stefan H. (stefan_h143)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Hallo zusammen,

Ich hätte da eine Frage, die wahrscheinlich schon tausend mal gestellt 
wurde. Ich bin wahrscheinlich zu doof zum Suchen.

Ich arbeite mich grad in die Timer ein und will eigentlich nur delay 
durch eine Timerabfrage ersetzten. Also ähnlich wie BlinkWithoutDelay 
beim Arduino.

Momentan mache habe ich den Code so:
#include <avr/io.h>
#include <avr/interrupt.h>

uint8_t schritt = 0; //Schrittvariable
uint8_t sechzehn_ms = 0; // alle 16ms ein Inkrement
uint8_t vergangene_zeit = 0; // vergangene Zeit für schritt

int main(void)
{  
  // Timer 0 konfigurieren -> Prescaler auf 1024
  TCCR0B |= (1<<CS02) | (1<<CS00);
  // Overflow Interrupt von Timer 0 einschalten
  TIMSK0 |= (1<<TOIE0);
  // Global Interrupts aktivieren
  sei();


    while (1) 
    {
    

  uint8_t aktuelle_zeit = sechzehn_ms;
  if (aktuelle_zeit - vergangene_zeit >= 63)
  {
    vergangene_zeit = aktuelle_zeit; 

    //Schritt weiter schalten
    if (schritt <2 )
    {
      schritt++;
    } else
    {
      schritt=0;
    }
  }
      
    }
}

ISR (TIMER0_OVF_vect)
{
  sechzehn_ms++;
}

Ich möchte im Prinzip nur ca alle Sekunde den Schritt um eins weiter 
zählen oder eben auf 0 setzten. die Zeit muss nicht genau sein, daher 
hab ich den Prescaler hoch gewählt.
Das ganze läuft einmal durch aber wenn die vergangene_zeit dann bei 
einem zu hohen Wert steht, kann die Bedingung natürlich nicht mehr wahr 
werden.

Wie umgeht man das? Mache ich das grundsätzlich falsch?

Gruß
Stefan

Autor: Fux (Gast)
Datum:

Bewertung
-2 lesenswert
nicht lesenswert
> bei einem zu hohen Wert steht

Da werden die Unixe dann auch stehenbleiben.
Aber erst 2038.
Sei also unbesorgt!

Autor: Stefan H. (stefan_h143)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Fux schrieb:
> Sei also unbesorgt!

bin nicht unbesorgt, denn es funktioniert ja nicht...

Autor: N.N. (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
volatile uint8_t sechzehn_ms = 0; // alle 16ms ein Inkrement

Autor: Samuel C. (neoexacun)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Indem du einen uint64_t wählst. Dann hast du Ruhe.

: Bearbeitet durch User
Autor: Stefan H. (stefan_h143)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Samuel C. schrieb:
> Indem du einen uint64_t für vergangene_zeit wählst. Dann hast du Ruhe

wie meinst du das? meine meine sechzehn_ms sind doch auch nur 8bit.
Selbst bei einem so großen wert wäre doch irgendwann das gleiche Problem 
da oder?

Autor: Stefan H. (stefan_h143)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
N.N. schrieb:
> volatile uint8_t sechzehn_ms = 0; // alle 16ms ein Inkrement

bringt leider nichts.

Autor: Wolfgang (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Auf jeden Fall solltest du sechzehn_ms als volatile deklarieren, 
damit dir der Optimierer da nicht in die Suppe spucken kann.

Autor: Stefanus F. (Firma: Äppel) (stefanus)
Datum:

Bewertung
1 lesenswert
nicht lesenswert
1.) Mache sechzehn_ms volatile.

2.) Solche Zeitzähler soll man im Laufe des Programmes niemals ändern. 
Die einzige Änderung darf das Inkrementieren in der ISR sein.

3.) Du musst subtrahieren, den Part hast du fast richtig gemacht.

Beispiel:
volatile uint8_t sechzehn_ms=0;

int main(void)
{
    // Hier den Timer starten

    uint8_t warte_seit=sechzehn_ms;
    while(1)
    {
        if (sechzehn_ms-warte_seit >= 63)
        {
            warte_seit=sechzehn_ms;
            // Tu etwas
        }
    }
}

ISR (TIMER0_OVF_vect)
{
  sechzehn_ms++;
}

Dann funktioniert das sogar bei einem Timer-Überlauf korrekt. Wichtig 
ist, dass beide Variablen unsigned Integer mit gleicher Größe sind.

: Bearbeitet durch User
Autor: Samuel C. (neoexacun)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Stefan H. schrieb:
> Samuel C. schrieb:
>> Indem du einen uint64_t für vergangene_zeit wählst. Dann hast du Ruhe
>
> wie meinst du das? meine meine sechzehn_ms sind doch auch nur 8bit.
> Selbst bei einem so großen wert wäre doch irgendwann das gleiche Problem
> da oder?

Dann mach auch sechzehn_ms zu uint64_t.

Wieviele Milliarden Jahre willst du das Ding denn betreiben?

Autor: Stefanus F. (Firma: Äppel) (stefanus)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Noch ein Nachtrag:

Wenn du mehr als 8 bits verwendest, musst du beim Lesen des Zählers 
verhindern, dass er wöhrend dessen durch die ISR verändert wird. Zum 
Beispiel so:
volatile uint32_t sechzehn_ms=0;

int main(void)
{
    // Hier den Timer starten

    cli();
    uint32_t warte_seit=sechzehn_ms;
    sei();
    while(1)
    {
        cli();
        uint32_t jetzt=sechzehn_ms;
        sei();
        if (jetzt-warte_seit >= 63)
        {
            warte_seit=jetzt;
            // Tu etwas
        }
    }
}

ISR (TIMER0_OVF_vect)
{
  sechzehn_ms++;
}

Autor: Stefan H. (stefan_h143)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Stefanus F. schrieb:
> Dann funktioniert das sogar bei einem Timer-Überlauf korrekt. Wichtig
> ist, dass beide Variablen unsigned Integer mit gleicher Größe sind.

Danke für deine Antwort!

Leider funktioniert das wieder nicht. es läuft einmal durch, dann ist 
wieder Schluss. Im Prinzip machst du ja fast das gleiche wie ich ganz am 
anfang.

Autor: Stefanus F. (Firma: Äppel) (stefanus)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Stefan H. schrieb:
> Im Prinzip machst du ja fast das gleiche wie ich ganz am
> anfang.

Ja stimmt. Meine Aussage "fast richtig" war falsch, ich hatte mich 
diesbezüglich verguckt.

Das müsste laufen. Zeige nochmal deinen aktuellen Quelltext (mit 
volatile).

: Bearbeitet durch User
Autor: Stefan H. (stefan_h143)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Stefanus F. schrieb:
> Das müsste laufen. Zeige nochmal deinen aktuellen Quelltext (mit
> volatile).

#include <avr/io.h>
#include <avr/interrupt.h>

//Delay Funktion 
#define F_CPU 16000000UL  // 16 MHz
#include <util/delay.h>

//Globale Variablen
uint8_t schritt = 0; //Schrittvariable
volatile uint8_t sechzehn_ms = 0; // alle 16ms ein Inkrement
uint8_t zeit_eins = 0; // vergangene Zeit f�r schritt


int main(void)
{
  
  // Timer 0 konfigurieren -> Prescaler auf 1024
  TCCR0B |= (1<<CS02) | (1<<CS00);
  // Overflow Interrupt von Timer 0 einschalten
  TIMSK0 |= (1<<TOIE0);
  // Global Interrupts aktivieren
  sei();
  
  
 warte_seit=sechzehn_ms; // müsste ich mir eigentlich sparen können, da sechzehn_ms==0
    while (1) 
    {
    
    //uint8_t aktuelle_zeit = sechzehn_ms;
    if (sechzehn_ms - zeit_eins >= 63)
    {
      zeit_eins = sechzehn_ms;
      //Schritt weiter schalten
      if (schritt <2 )
      {
        schritt++;
      } else
      {
        schritt=0;
      }
    }
    
    
    
    }
}

ISR (TIMER0_OVF_vect)
{
  sechzehn_ms++;
}


ich verwende dann den schritt um leds leuchten zu lassen... also immer 
drei Farben abwechselnd. das sollte ja aber keinen Einfluss haben, da 
ich eigentlich nur mit einer switch case schleife den schritt abfrage

: Bearbeitet durch User
Autor: Stefanus F. (Firma: Äppel) (stefanus)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Ich raff' es nicht. Ich sehe den Fehler nicht!

Autor: Stefan H. (stefan_h143)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Stefanus F. schrieb:
> Ich raff' es nicht. Ich sehe den Fehler nicht!

Naja ich denke dass ich den fehler mache:

wenn sechzehn_ms gerade zb 250 ist und ich das dann in zeit_eins 
abspeichere, dann kann bei der nächsten Rechnung ja nie mehr was 
größeres als 5 raus kommen.

Also sechzehn_ms (= 255) - zeit_eins(250) = 5. Damit ist die Bedingung 
nie erfüllt und ich komme nie mehr dazu den schritt weiter zu schalten.


Sehe ich das falsch?

wenn ich nun natürlich alles auf 32bit hoch nehme, dann dauert es ewig 
bis es dazu kommt. aber irgendwann eben schon. das muss man doch 
irgendwie umgehen können oder?


Wenn ich das so abfange, läuft zumindes irgenwie was weiter...
if (sechzehn_ms - zeit_eins >= 63)
    {
      if (sechzehn_ms < 192)
      {
        zeit_eins = sechzehn_ms;
      } else
      {
        zeit_eins = 50;
      }
     
      //Schritt weiter schalten
      if (schritt <2 )
      {
        schritt++;
      } else
      {
        schritt=0;
      }
    }

: Bearbeitet durch User
Autor: Wolfgang (Gast)
Datum:

Bewertung
1 lesenswert
nicht lesenswert
Samuel C. schrieb:
> Indem du einen uint64_t wählst. Dann hast du Ruhe.

Einen kaputten Algorithmus durch größere Variablen etwas länger am Leben 
zu halten, kann nicht die Lösung sein - allenfalls ein schlechter Work 
around.

Autor: Stefan H. (stefan_h143)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Wolfgang schrieb:
> Einen kaputten Algorithmus durch größere Variablen etwas länger am Leben
> zu halten, kann nicht die Lösung sein - allenfalls ein schlechter Work
> around.

Deswegen frage ich ja, wie man das besser lösen könnte :)
Hast du da eine Idee?

Autor: Wolfgang (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Stefan H. schrieb:
> Deswegen frage ich ja, wie man das besser lösen könnte :)

Mein Kommentar bezog sich auf den etwas cruden "Tip" von Samuel C.


> Hast du da eine Idee?

Ich würde das in dieser Art angehen:
volatil uint8_t sechzehn_ms = 0; // alle 16ms ein Inkrement
uint8_t startzeit = 0; //Schrittvariable

if (sechzehn_ms - startzeit > 63)
{
   ... tue etwas
   startzeit  = sechzehn_ms; 
}

Autor: Stefan H. (stefan_h143)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Wolfgang schrieb:
> Ich würde das in dieser Art angehen

Ja aber ich mache doch genau das, mit dem beschriebenen Problem.

in dem Moment wenn die Startzeit zu groß wird, kann die Bedingung ja 
nicht mehr wahr werden oder?
Ich steh auf dem Schlauch

Autor: foobar (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
> wenn sechzehn_ms gerade zb 250 ist und ich das dann in zeit_eins
> abspeichere, dann kann bei der nächsten Rechnung ja nie mehr was
> größeres als 5 raus kommen.

Doch, wegen des Überlaufs: "negative" Ergebnisse sind bei unsigned sehr 
groß.

Du hast allerdings zwei Probleme:

1) Alle Ausdrücke in C werden immer mit mindestens int-Größe 
durchgeführt.  Dein uint8_t wird also zu einem signed int.  Bei a-b 
wird, obwohl a und b uint8_t sind, signed gerechnet, evtl negativ und 
damit kleiner als jeder positive Vergleichswert.  Der Vergleich müsste 
"if ((uint8_t)(a-b) > 63)" oder "if ((unsigned)a-b > 63)" lauten[1].

2) Deine Schleife macht nichts, außer die Variable "schritt" zu 
verändern - da kann der Compiler ordentlich optimieren - evtl bleibt 
nichts übrig ...


[1] Unterschied rausfinden "is left as an exercise for the reader" ;-)

Autor: foobar (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
> "if ((uint8_t)(a-b) > 63)" oder "if ((unsigned)a-b > 63)"

Noch ne dritte Variante: "if (a-b > 63u)"

Autor: Veit D. (devil-elec)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Hallo,

zeige bitte einen kompilierbaren Code.
Woher weißt du das der Code stehen bleibt?
Du hast nirgends eine Ausgabe bzw. Taktausgang.

Autor: Samuel C. (neoexacun)
Datum:

Bewertung
1 lesenswert
nicht lesenswert
Wolfgang schrieb:
> Mein Kommentar bezog sich auf den etwas cruden "Tip" von Samuel C.

Krude seh ich ein. Aber löst das Problem (fast) ohne weiteres bis zum 
Verlöschen unseres Zentralgestirns.

: Bearbeitet durch User
Autor: Stefan H. (stefan_h143)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
foobar schrieb:
> 1) Alle Ausdrücke in C werden immer mit mindestens int-Größe
> durchgeführt.  Dein uint8_t wird also zu einem signed int.  Bei a-b
> wird, obwohl a und b uint8_t sind, signed gerechnet, evtl negativ und
> damit kleiner als jeder positive Vergleichswert.  Der Vergleich müsste
> "if ((uint8_t)(a-b) > 63)" oder "if ((unsigned)a-b > 63)" lauten[1].
>

okay. verstehe ich zwar im prinzip, aber mir ist nicht klar, warum dann 
aus meinem uint8_t ein signed int wird. kannst du mir das vielleicht 
noch erklären.
Das mit dem Überlauf verstehe ich. Stimmt eigentlich.
Ich habe die Variablen jetzt als unsigned int deklariert, dann geht es.
sorry, wenn ich das jetzt noch nicht ganz kapiere.



> 2) Deine Schleife macht nichts, außer die Variable "schritt" zu
> verändern - da kann der Compiler ordentlich optimieren - evtl bleibt
> nichts übrig ...

Ja ich mache schon noch was damit, dass habe ich raus geschnitten, weil 
ich es nicht für wichtig hielt. im Prinzip eben nur drei Farben bei RGB 
leds durch schalten. Dabei schreibe ich aber nicht in die verwendeten 
Variablen rein, daher hab ichs weg gelassen.
Wenn du magst, kann ich den ganzen code rein tun, ist halt dann recht 
lang.

Autor: Stefan H. (stefan_h143)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Samuel C. schrieb:
> Krude seh ich ein. Aber löst das Problem (fast) ohne weiteres bis zum
> Verlöschen unseres Zentralgestirns.

Danke für den Lachanfall :)

wills halt so lernen, wie es sich gehört ;)

Autor: foobar (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
> aber mir ist nicht klar, warum dann aus meinem uint8_t ein signed int wird.

Ist halt eine Regel in C: wenn der kleinere Datentyp in ein "int" passt, 
wird es ein "int".  Google mal nach "C type promotion".

Lustig wird's z.B. bei "uint16_t": je nachdem, was für eine int-Größe 
das System hat, wird es ein "signed int" (32-Bit-ints) oder ein 
"unsigned int" (16-Bit-ints).

> Ja ich mache schon noch was damit, dass habe ich raus geschnitten,

Dann ist's ok - wollte nur drauf hinweisen.

Autor: Viktor B. (coldlogic)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Hey Stefan,

soweit ich das verstanden habe, willst du jedes Mal, wenn die Variable 
sechzehn_ms 63 Schritte macht, den flag "Schritt" weiterschalten. Könnte 
man nicht einfach 64 Schritte machen und eine Modulo-Operation nehmen? 
Mit einer Potenz von 2 ist die Operation gar nicht mal so schwer.
  while (1) 
    {
      if (!(sechzehn_ms%64))
        {
        //Schritt weiter schalten
        if (schritt <2 )
        {
          schritt++;
        } else
        {
          schritt=0;
        }
      }
    }
  }

Autor: Veit D. (devil-elec)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Hallo,

ich verstehe noch nicht warum sechzehn und zeit_eins zu int wird, wenn 
beide mit uint8_t deklariert sind. Wozu gibt man Datentypen an wenn sie 
ignoriert werden. Zudem sehe ich wie sechzehn im Bytebereich sauber 
überläuft und zeit_eins stehen bleibt.

Autor: Apollo M. (Firma: @home) (majortom)
Datum:

Bewertung
-2 lesenswert
nicht lesenswert
Stefan H. schrieb:
> //Globale Variablen
> uint8_t schritt = 0; //Schrittvariable
> volatile uint8_t sechzehn_ms = 0; // alle 16ms ein Inkrement
> uint8_t zeit_eins = 0; // vergangene Zeit f�r schritt

... ich "hasse" redundanz!

- globale variables werden immer automatisch initialisiert mit zero.

- volatile ist hier auch redundant und der sinn dazu unsinnig erklärt

und die weit verbreite "unsitte" singel anweisungen e.g. bei if/else zu 
klammern ist zu mindest hässlich/unästhetisch.


weg damit!


mt

Autor: Irgend W. (Firma: egal) (irgendwer)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Irgendwie würde ich das ganze andersherum aufziehen damit man diese 
ganze merkwürdige Rechnerei garnicht erst benötigt.
uint8_t schritt = 0; //Schrittvariable
uint8_t sechzehn_ms; // alle 16ms ein Inkrement
volatile uint8_t tu_was; // vergangene Zeit für schritt

int main(void)
{  
  tu_was = false;
  sechzehn_ms = 0;

  Timer init...

  while (true) 
  {
    if (tu_was)
    {
      tu_was = false;

      //Schritt weiter schalten
      if (schritt < 2)
      {
        ++schritt;
      }
      else
      {
        schritt = 0;
      }
    }

    GehSchlafenBisZumNaechstenEvent();
  }
}

ISR (TIMER0_OVF_vect)
{
  ++sechzehn_ms;
  if (sechzehn_ms >= 63)
  {
    sechzehn_ms = 0;
    tu_was = true;
  }
}


Denn ganzen Block mit "schritt" wird dir der Compiler/Linker natürlich 
entsorgen wenn du damit nichts vernünftiges Anfängst.

Autor: E. H. (emax)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
In die etwas verzwickte Logik habe ich mich jetzt nicht rein gedacht. 
Aber ginge das nicht:


#include <util/atomic.h>

// ...

while (1)
{
    ATOMIC_BLOCK(ATOMIC_FORCEON)
    {
        if (sechzehn_ms >= 63)
        {
            sechzehn_ms=0;
            ++schritt;
        }
    }

    if (schritt == 2)
        schritt=0;

    // .. schritt auswertung

} // end while

Autor: Apollo M. (Firma: @home) (majortom)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Stefan H. schrieb:
> verstehe ich zwar im prinzip, aber mir ist nicht klar, warum dann
> aus meinem uint8_t ein signed int wird. kannst du mir das vielleicht
> noch erklären.

google mal int promotion rules!

Autor: guest (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
foobar schrieb:
>> "if ((uint8_t)(a-b) > 63)" oder "if ((unsigned)a-b > 63)"
>
> Noch ne dritte Variante: "if (a-b > 63u)"

Und noch eine:
uint8_t t = a - b;
if( t > 63 )

Autor: foobar (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
> ich verstehe noch nicht warum sechzehn und zeit_eins zu int wird, wenn
> beide mit uint8_t deklariert sind. Wozu gibt man Datentypen an wenn sie
> ignoriert werden.

Weil in C immer mit mindestens int-Größe gerechnet wird.  Die 
abstrakte Maschine, die C beschreibt, kann keine Arithmethik mit Zahlen 
kleiner als int.  Datentypen kleiner als int (char/short) dienen nur als 
"Speichergröße" - nur da existieren sie in der reduzierten Form.  Dass 
meist doch 8-Bit-Arithmetik rauskommt, ist dem Optimizer zu verdanken.

Das gleiche passiert übrigens auch bei Fließkommazahlen (immer double).

Ein weitere Grund für Datentypen ist die Fehlererkennung - man gibt dem 
Compiler mehr Infos, mit denen er arbeiten kann.  Wenn du das nicht 
brauchst, geht zurück nach B oder gar BCPL ;-)

Autor: Teo D. (teoderix)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Stefan H. schrieb:
> will eigentlich nur delay
> durch eine Timerabfrage ersetzten.

So?
volatile uint8_t sechzehn_ms_flag;

while (1) {

if(sechzehn_ms_flag){
   sechzehn_ms_flag=0;
   ....
   ...
  }
}

ISR (TIMER0_OVF_vect)
static uint8_t sechzehn_ms=63;
{
  if (!sechzehn_ms--){
     sechzehn_ms=63;
     sechzehn_ms_flag=1;
}

Autor: Apollo M. (Firma: @home) (majortom)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Teo D. schrieb:
> ISR (TIMER0_OVF_vect)
> static uint8_t sechzehn_ms=63;
> {

ISR (TIMER0_OVF_vect) {
  static uint8_t sechzehn_ms=63;
  ...
}

ansonsten alles viel besser als der mist vorher!

Autor: Veit D. (devil-elec)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Hallo,

aha. Danke. Das Problem war mir bis jetzt in der Form nicht bewußt das 
trotz Byte Angabe in int gewandelt wird. Das Literale wie 55+77 in int 
gerechnet werden wußte ich dagegen. Nur hier gibt man keine Datentypen 
an. Deswegen meine Verwunderung.

Auf jedenfall gut zu wissen und nochmal darauf hingewiesen zu werden.

Autor: Nop (Gast)
Datum:

Bewertung
1 lesenswert
nicht lesenswert
Apollo M. schrieb:

> - volatile ist hier auch redundant

Ist es nicht. Wann immer man eine Variable zwischen ISR und 
Hauptschleife teilt, MUSS sie volatile sein, weil der Compiler sie sonst 
wegoptimieren kann.

Autor: Veit D. (devil-elec)
Datum:

Bewertung
1 lesenswert
nicht lesenswert
Teo und Apollo,

wenn man nur ein Einziges Timerintervall benötigt mag das so mehr Sinn 
machen. Benötigt man mehrere verschiedene Zeitintervalle kommt man 
glaube ich mit dem allgemeinen globalen Counter besser weg. Ansonsten 
hätte man dann gefühlt Tausende globale Flags. Oder nicht?

Autor: Apollo M. (Firma: @home) (majortom)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Nop schrieb:
> Ist es nicht. Wann immer man eine Variable zwischen ISR und
> Hauptschleife teilt, MUSS sie volatile sein, weil der Compiler sie sonst
> wegoptimieren kann.

so so  ... dann google und lese nochmal.

use of volatile ...
wenn und nur wenn die variable sich zwischen zwei zugriffen ändern kann, 
daher durch hw geändert wird, was ja der compiler sonst nicht wissen 
kann und diese dann als konstante ansieht und e.g. in ein register 
ablegt.

daher volatile erzwingt, dass die variable vor jeden zugriff immer neu 
eingelesen wird!

... und wegoptimieren wäre auch unsinn, weil nicht weg sondern wird nur 
einmal eingelesen.


mt

Autor: Stefan H. (stefan_h143)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
foobar schrieb:
> Google mal nach "C type promotion".

Apollo M. schrieb:
> google mal int promotion rules!

Hab ich und ich wurde erleuchtet :) Danke für euren Hinweiß! Damit hat 
sich meine eigentliche Frage erledigt.

Vielen Dank für eure Hilfe :) Immer wieder gut, wenn man dann weiter 
kommt!

Autor: Nop (Gast)
Datum:

Bewertung
3 lesenswert
nicht lesenswert
Apollo M. schrieb:

> wenn und nur wenn die variable sich zwischen zwei zugriffen ändern kann

Das ist der Fall, nämlich zwischen zwei Durchläufen der while-Schleife 
in Stefans main().

Da ISRs nie im Callpath stehen, kann der Compiler annehmen, die Variable 
werde mit 0 initialisiert, nie verändert, und die ganze Auswertung in 
main kann wegoptimiert werden.

Autor: Stefan H. (stefan_h143)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Veit D. schrieb:
> Teo und Apollo,
>
> wenn man nur ein Einziges Timerintervall benötigt mag das so mehr Sinn
> machen. Benötigt man mehrere verschiedene Zeitintervalle kommt man
> glaube ich mit dem allgemeinen globalen Counter besser weg. Ansonsten
> hätte man dann gefühlt Tausende globale Flags. Oder nicht?

Ja genau. Ich will dann halt mehrere Zeiten abfragen, daher wollte ich 
das eben so machen.

Autor: Apollo M. (Firma: @home) (majortom)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Nop schrieb:
>> - volatile ist hier auch redundant
>
> Ist es nicht. Wann immer man eine Variable zwischen ISR und
> Hauptschleife teilt, MUSS sie volatile sein, weil der Compiler sie sonst
> wegoptimieren kann.

zur erleuchtung ...
https://barrgroup.com/Embedded-Systems/How-To/C-Volatile-Keyword

: Bearbeitet durch User
Autor: Nop (Gast)
Datum:

Bewertung
1 lesenswert
nicht lesenswert
Apollo M. schrieb:

> zur erleuchtung ...
> https://barrgroup.com/Embedded-Systems/How-To/C-Volatile-Keyword

Hast Du den Link eigentlich selber sinnerfassend gelesen?

Im Abschnitt "Proper Use of C's volatile Keyword" findet sich:
2. Global variables modified by an interrupt service routine

Das ist exakt die Situation in Stefans Code.

Autor: Apollo M. (Firma: @home) (majortom)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Nop schrieb:
> Da ISRs nie im Callpath stehen, kann der Compiler annehmen, die Variable
> werde mit 0 initialisiert, nie verändert, und die ganze Auswertung in
> main kann wegoptimiert werden.

korrekt! und rückwärtsrolle ...

Autor: Veit D. (devil-elec)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Hallo,

ich weiß auch nicht was es am "volatile sechzehn_ms" auszusetzen gibt.

Es ist absolut notwendig und korrekt. Wird eine Variable außerhalb des 
eigentlichen Programmablaufes geändert, was in einer ISR der Fall ist, 
dann muss diese volatile sein. Ist diese Variable zudem größer ein Byte, 
dann muss der Zugriff im Programmablauf darauf atomar erfolgen. 
Ansonsten könnte es passieren das low und high Byte nicht mehr 
zusammenpassen, weil zwischendurch ein neuer Interrupt den Wert 
verändert hat.

Autor: Stefanus F. (Firma: Äppel) (stefanus)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
foobar schrieb:
> Weil in C immer mit mindestens int-Größe gerechnet wird.

Gerade mal auf dem PC ausprobiert:
#include <stdint.h>
#include <stdio.h>

int main()
{
    uint8_t jetzt=1;
    uint8_t warte_seit=192;
    uint8_t diff=jetzt-warte_seit;
    printf("1) %d - %d = %d\n",jetzt,warte_seit,diff);
    printf("2) %d - %d = %d\n",jetzt,warte_seit,jetzt-warte_seit);
    printf("3) %d - %d >= 63 is %d\n",jetzt,warte_seit, jetzt-warte_seit >= 63);
    printf("4) %d - %d >= 63 is %d\n",jetzt,warte_seit, jetzt-warte_seit >= (uint8_t)63);
    printf("5) %d - %d >= 63 is %d\n",jetzt,warte_seit, (uint8_t)(jetzt-warte_seit) >= 63);
}

Erzeugt unter Linux diesen Output:
sfrings@stefanpc:~/Test$ gcc test.c 
sfrings@stefanpc:~/Test$ ./a.out 
1) 1 - 192 = 65
2) 1 - 192 = -191
3) 1 - 192 >= 63 is 0
4) 1 - 192 >= 63 is 0
5) 1 - 192 >= 63 is 1

1) bestätigt, dass die Zeit-Differenz durch die Subtraktion trotz 
Überlauf korrekt berechnet wird.
2) bestätigt, dass der Compiler automatisch mit mehr als 8bit rechnet, 
wenn wir es ihm nicht anders vorgeben. (-191 ist größer als ein Byte).
3) bestätigt das eigentliche Problem des TO: Der if-Ausdruck 
funktioniert nicht wie erwartet.
4) zeigt, dass es nichts nützt, das integer Literal 63 auf unit8_t zu 
casten.
5) zeigt, dass es hilft, die Subtraktion auf uint8_t zu casten.

Veit D. schrieb:
> Das Problem war mir bis jetzt in der Form nicht bewußt
> Auf jedenfall gut zu wissen und nochmal darauf hingewiesen zu werden.

Geht mir genau so.

Ich habe das bisher immer nur mit mindestens 16bit Integer gemacht, 
daher war ich mit diesem Problem bisher noch nie konfrontiert.

: Bearbeitet durch User
Autor: Arno (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Stefanus F. schrieb:
> Gerade mal auf dem PC ausprobiert:
> #include <stdint.h>
> #include <stdio.h>
>
> int main()
> {
>     uint8_t jetzt=1;
>     uint8_t warte_seit=192;
>     uint8_t diff=jetzt-warte_seit;
>     printf("1) %d - %d = %d\n",jetzt,warte_seit,diff);
>     printf("2) %d - %d = %d\n",jetzt,warte_seit,jetzt-warte_seit);
>     printf("3) %d - %d >= 63 is %d\n",jetzt,warte_seit, jetzt-warte_seit
>>= 63);
>     printf("4) %d - %d >= 63 is %d\n",jetzt,warte_seit, jetzt-warte_seit
>>= (uint8_t)63);
>     printf("5) %d - %d >= 63 is %d\n",jetzt,warte_seit,
> (uint8_t)(jetzt-warte_seit) >= 63);
> }

Die nächste Fehlerquelle lauert dann im Format-String von printf - wenn 
der nicht zum tatsächlichen (int), aber zum gewünschten (uint8_t) 
Datentyp passt, kann die richtige Ausgabe herauskommen. Könnte zum 
Beispiel passieren, wenn du %d durch %hhu ersetzt.

MfG, Arno

Autor: Apollo M. (Firma: @home) (majortom)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Stefanus F. schrieb:
> Ich habe das bisher immer nur mit mindestens 16bit Integer gemacht,
> daher war ich mit diesem Problem bisher noch nie konfrontiert.

... interessant, das steht fast in jeder docu über c oder specific 
compiler drin, im sinne von vorsicht falle mit int promotion - wohl auch 
so definiert im c-standard.

ist das bekannt ... struct/array init with zero? hatte ich noch nicht 
auf meinen schirm oder wo im code gesehen.


void test() {
   struct_t struct_test = {}; //initialize with zero
   int array_test[42] = {};

   ...
}

attached gutes buch dazu aus meiner bibo, ... schnell zugreifen bevor 
wieder weg!

Autor: W.S. (Gast)
Datum:

Bewertung
1 lesenswert
nicht lesenswert
Stefan H. schrieb:
> ISR (TIMER0_OVF_vect)
> {
>   sechzehn_ms++;
> }
>
> Ich möchte im Prinzip nur ca alle Sekunde den Schritt um eins weiter
> zählen

Also erst einmal: Was hat ein Timer (d.h. ein Hardware-Peripheriecore) 
mit C zu tun? Antwort: garnichts.

Und dann:
Anstelle deiner wirklich extrem minimalistischen ISR solltest du dir 
eine Art Systemuhr einrichten.

Also wäre es gut, wenn deine o.g. ISR in einem Takt aufgerufen würde, 
der ein ganzzahliger Teil von 1 Sekunde ist. Zum Beispiel 10 ms oder 1 
ms (für ganz Eilige), oder 1/2 1/4..1/128 Sekunde.

Dann zählst du in deiner ISR einfach eine globale Variable hoch - und 
schon hast du die Systemzeit in Sekunden oder eben deinem gewählten 
Takt. Und diese globale Variable darf jeder lesen, aber nicht 
beschreiben.

Und wenn das Ganze mehr als nur 1 Tag laufen soll, dann mußt du eben in 
der ISR auch zur Mitternacht den Tick nullen und den Datumzähler um 1 
erhöhen.

Du scheinst mir nen AVR zu benutzen, also sollte deine ISR auch eine 
Zeitausgabe beinhalten, etwa so:
globale Vars:
unsigned long Time;
bool bitte;
unsigned long da_isses;

ISR: ...
     ++Time;
     if (bitte)
     { da_isses = Time;
       bitte = false;
     }
   ... ISR ende

So, damit können andere Teile deiner Firmware die aktuelle Uhrzeit (in 
Ticks) abholen, indem sie bitte = true setzen und warten, bis bitte 
wieder false ist.

W.S.

Autor: foobar (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Wer noch etwas üben möchte: Wo ist der Unterschied zwischen den drei 
folgenden Varianten (sie erreichen das gewünschte Ziel auf 
unterschiedliche Weise) und welche ist zu bevorzugen:

// uint8_t a, b;
1) if ((uint8_t)(a-b) > 63)
2) if ((unsigned)a-b > 63)
3) if (a-b > 63u)

Autor: Stefanus F. (Firma: Äppel) (stefanus)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Apollo M. schrieb:
> attached gutes buch dazu

Cool, danke

Autor: Stefanus F. (Firma: Äppel) (stefanus)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
W.S. schrieb:
> Und wenn das Ganze mehr als nur 1 Tag laufen soll, dann mußt du eben in
> der ISR auch zur Mitternacht den Tick nullen und den Datumzähler um 1
> erhöhen.

Er hat das mit der Subtraktion prinzipiell richtig gemacht. Einfach die 
Variable breiter machen halte ich für Pfusch, denn

a) Größere Variablen sind langsamer im Zugriff
b) Laufen auch irgendwann über

Autor: foobar (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
> indem sie bitte = true setzen und warten, bis bitte wieder false ist

Dann muss "bitte" und "da_isses" aber volatile sein ...


Die ursprüngliche Variante von Stefan ist schon OK und üblich - das 
Linux-Kernel z.B. macht es genauso (der globale Zähler heißt da 
"jiffies") und einige Systeme haben so einen Counter direkt in der 
Hardware.  Ja, man kann bei der Benutzung Fehler machen, insb wenn man 
wrap-around und atomic-access berücksichtigen muß - im Linux-Kernel 
haben sie deshalb irgendwann Macros eingeführt (time_after, 
get_jiffies_64, etc).

Autor: Stefanus F. (Firma: Äppel) (stefanus)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
foobar schrieb:
> 1) if ((uint8_t)(a-b) > 63)
> 2) if ((unsigned)a-b > 63)
> 3) if (a-b > 63u)
> Wo ist der Unterschied?

Werfen wir dazu einen Blick in den Assembler-Code, den der Compiler 
generiert. Ich musste ein bisschen tricksen, damit der Optimizer den 
wirkungslosen Code nicht komplett weg optimiert:
#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>
#include <avr/io.h>

__attribute__((noinline)) bool test1(uint8_t a, uint8_t b)
{
   return (uint8_t)(a-b) > 63;
}

__attribute__((noinline)) bool test2(uint8_t a, uint8_t b)
{
    return (unsigned)a-b > 63;
}

__attribute__((noinline)) bool test3(uint8_t a, uint8_t b)
{
    return a-b > 63u;
}

int main()
{
    PORTB=test1(1,192);
    PORTB=test2(2,193);
    PORTB=test3(3,194);
}

Ergibt folgendes Listing:
...
00000080 <test1>:
  80:  98 2f         mov  r25, r24
  82:  96 1b         sub  r25, r22
  84:  81 e0         ldi  r24, 0x01  ; 1
  86:  90 34         cpi  r25, 0x40  ; 64
  88:  08 f4         brcc  .+2        ; 0x8c <test1+0xc>
  8a:  80 e0         ldi  r24, 0x00  ; 0
  8c:  08 95         ret

0000008e <test2>:
  8e:  28 2f         mov  r18, r24
  90:  30 e0         ldi  r19, 0x00  ; 0
  92:  26 1b         sub  r18, r22
  94:  31 09         sbc  r19, r1
  96:  81 e0         ldi  r24, 0x01  ; 1
  98:  20 34         cpi  r18, 0x40  ; 64
  9a:  31 05         cpc  r19, r1
  9c:  08 f4         brcc  .+2        ; 0xa0 <test2+0x12>
  9e:  80 e0         ldi  r24, 0x00  ; 0
  a0:  08 95         ret

000000a2 <test3>:
  a2:  28 2f         mov  r18, r24
  a4:  30 e0         ldi  r19, 0x00  ; 0
  a6:  26 1b         sub  r18, r22
  a8:  31 09         sbc  r19, r1
  aa:  81 e0         ldi  r24, 0x01  ; 1
  ac:  20 34         cpi  r18, 0x40  ; 64
  ae:  31 05         cpc  r19, r1
  b0:  08 f4         brcc  .+2        ; 0xb4 <test3+0x12>
  b2:  80 e0         ldi  r24, 0x00  ; 0
  b4:  08 95         ret

000000b6 <main>:
  b6:  60 ec         ldi  r22, 0xC0  ; 192
  b8:  81 e0         ldi  r24, 0x01  ; 1
  ba:  0e 94 40 00   call  0x80  ; 0x80 <test1>
  be:  85 b9         out  0x05, r24  ; 5
  c0:  61 ec         ldi  r22, 0xC1  ; 193
  c2:  82 e0         ldi  r24, 0x02  ; 2
  c4:  0e 94 47 00   call  0x8e  ; 0x8e <test2>
  c8:  85 b9         out  0x05, r24  ; 5
  ca:  62 ec         ldi  r22, 0xC2  ; 194
  cc:  83 e0         ldi  r24, 0x03  ; 3
  ce:  0e 94 51 00   call  0xa2  ; 0xa2 <test3>
  d2:  85 b9         out  0x05, r24  ; 5
  d4:  80 e0         ldi  r24, 0x00  ; 0
  d6:  90 e0         ldi  r25, 0x00  ; 0
  d8:  08 95         ret
...

Wie wir sehen ist Variante 1 deutlich kürzer, da auf 8bit Berechnung 
reduziert.

Die anderen beiden rechnen exakt gleich mit 16bit unsgined Integer. 
Offenbar spielt es keine Rolle, ob die linke oder rechte Seite vom ">" 
Operator den unsigned Integer vorgibt.

: Bearbeitet durch User
Autor: volatile (Gast)
Datum:

Bewertung
-1 lesenswert
nicht lesenswert
Apollo M. schrieb:
>
> attached gutes buch dazu aus meiner bibo, ... schnell zugreifen bevor
> wieder weg!

Scheiß Buch wenn es im Index nicht einmal volatile gibt.

Autor: foobar (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
> Werfen wir dazu einen Blick in den Assembler-Code, den der Compiler
> generiert.

Mir ging es eher um den konzeptuellen Unterschied: warum liefern diese 
Varianten das gewünschte Ergebnis, wo findet welche Konvertierung statt, 
mit welchem Typ werden die beiden Operationen durchgeführt, entstehen da 
möglicherweise Probleme?

Welcher Kode daraus generiert wird, steht auf einem anderen Blatt - im 
Optimalfall kommt bei allen drei Varianten das gleiche raus.

> Offenbar spielt es keine Rolle, ob die linke oder rechte Seite vom ">"
> Operator den unsigned Integer vorgibt.

Korrekt.  Der kleinere Operand wird immer zum größeren konvertiert 
(wobei signed_int < unsigned_int < signed_long < unsigned_long).

Autor: guest (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
foobar schrieb:
> (wobei signed_int < unsigned_int < signed_long < unsigned_long).

Vorsicht. Wenn int und long die gleiche Breite haben stimmt das u.U. 
nicht mehr.

Autor: Apollo M. (Firma: @home) (majortom)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
volatile schrieb:
> Scheiß Buch wenn es im Index nicht einmal volatile gibt.

für dich newbie noch eine "lesehilfe" und keinen mucks mehr!

Autor: Nop (Gast)
Datum:

Bewertung
1 lesenswert
nicht lesenswert
Zum "atomic access" bei 16bit-Variablen auf einer 8-Bit-CPU kann man 
auch ganz einfach ein sequential lock bei der Abfrage nutzen. Dann muß 
man keine Interrupts abschalten:
volatile uint16_t systime;
...

uint16_t Get_Systime(void) {
    uint16_t prev, curr;
    
    curr = systime;
    do {
        prev = curr;
        curr = systime;
    } while (prev != curr);
    
    return curr;
}

Autor: Codix (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Mal ein Beispiel aus der Praxis:
/*
#define F_CPU  3686411UL -> Compiler Option!
*/

volatile uint_fast64_t systick;
extern uint8_t TimeOut;

// Sekundentakt
ISR(TIMER1_COMPA_vect)
{
    PORTD ^= ( 1 << PD5); /* Toggle LED */

    if ( systick )
        systick++;

    if ( TimeOut )
        TimeOut--;  

    


}
void Init_Timer(void)
{
  
 /* Init Timer0 and Timer1*/
   tick2 = 0;

   TIMSK  |= (1 << OCIE0)| (1 << OCIE1A) | ( 1 << OCIE2);  // OCIE0 and
                              // OCIE1A int
                              // OCIE2 int
   TCCR0 |= PRESCALER0| (1 << WGM01);  // Prescaler and CTC Mode WGM01 
    TCNT0  = 0;         
  OCR0 = 4;              // 3,6 => 1 ms
// This is the central clock with 1 Hz 
  TCCR1B = PRESCALER1 | (1 << WGM12);// Prescaler 1024 and CTC Mode
  TCNT1 = 0;                // Set to zero
  OCR1A = (F_CPU/1024);        // calculate the 1 second value = 3600

// Timer2 is used to debounce the keys
  TCCR2 |= PRESCALER2 | (1 << WGM21); // Prescaler 1024 and CTC Mode
  TCNT2 = 0;
  OCR2 = (F_CPU/1024)/20;        // 20 ms

    sei();
}
Ist für einen ATmega32

Autor: volatile (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Apollo M. schrieb:
> volatile schrieb:
>> Scheiß Buch wenn es im Index nicht einmal volatile gibt.
>
> für dich newbie noch eine "lesehilfe" und keinen mucks mehr!

Sie sollten die Bücher selbst lesen um volatile zu verstehen.

Autor: Stefanus F. (Firma: Äppel) (stefanus)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
foobar schrieb:
> warum liefern diese
> Varianten das gewünschte Ergebnis

Weil sie einen Algorithmus erzwingen, der ohne Vorzeichen arbeitet.

: Bearbeitet durch User
Autor: Bernhard R. (bernhard_r874)
Datum:

Bewertung
2 lesenswert
nicht lesenswert
Codix schrieb:
> if ( systick )
>         systick++;
>
>     if ( TimeOut )
>         TimeOut--;


Kleiner Tipp:
    systick++;

    if ( TimeOut && (TimeOut > 1))
        TimeOut--;  

Die Abfrage von systick kann man sich sparen.
TimeOut kann man zwar machen wie vorgeschlagen, ich bevorzuge meine 
Lösung :)

Wenn TimeOut == 1, dann ist er abgelaufen. TimeOut == 0 ist 
abgeschaltet.
So kann man in der Mainloop eine Aktion bei Timeout entweder permanent 
ausführen oder, wenn TimeOut auf 0 gesetzt wird, nur ein Mal.

: Bearbeitet durch User
Autor: Apollo M. (Firma: @home) (majortom)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Bernhard R. schrieb:
> if ( TimeOut && (TimeOut > 1))
>         TimeOut--;

... auch meine bevorzugte lsg!
ich würde nur noch die "klammerorgie" weglassen, da der rang der 
operatoren das regelt, bei langen bedingungen wird das sonst schnell 
unübersichtlich

if (TimeOut && TimeOut>1) TimeOut--;

Autor: Frank M. (ukw) (Moderator) Benutzerseite
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Apollo M. schrieb:
> attached gutes buch dazu aus meiner bibo, ... schnell zugreifen bevor
> wieder weg!

Apollo M. schrieb:
> für dich newbie noch eine "lesehilfe" und keinen mucks mehr!

Es ist eine Urheberrechtsverletzung, so etwas einfach hier reinzusetzen. 
Ich ermahne eindringlich, dieses zukünftig zu unterlassen. Nicht nur, 
dass Andreas durch solche Aktionen rechtliche Schwierigkeiten bekommen 
könnte, sondern auch Du. Hinzu kommt, dass solche "Aktionen" auch den 
Ruf dieses Forums schädigen. Das ist hier keine Raubkopierer-Plattforum.

Bedenke auch, dass Du mit solchen Weitergaben von urheberrechtlich 
geschütztem Material den Autor des Werkes nicht nur schädigst, sondern 
auch den nötigen Respekt gegenüber seiner Arbeit vermissen lässt.

Ich appelliere daher eindringlich, so etwas grundsätzlich zu 
unterlassen. Bei Wiederholung wird es zwangsläufig zu Gegenmaßnahmen 
kommen müssen.

: Bearbeitet durch Moderator

Antwort schreiben

Die Angabe einer E-Mail-Adresse ist freiwillig. Wenn Sie automatisch per E-Mail über Antworten auf Ihren Beitrag informiert werden möchten, melden Sie sich bitte an.

Wichtige Regeln - erst lesen, dann posten!

  • Groß- und Kleinschreibung verwenden
  • Längeren Sourcecode nicht im Text einfügen, sondern als Dateianhang

Formatierung (mehr Informationen...)

  • [c]C-Code[/c]
  • [avrasm]AVR-Assembler-Code[/avrasm]
  • [code]Code in anderen Sprachen, ASCII-Zeichnungen[/code]
  • [math]Formel in LaTeX-Syntax[/math]
  • [[Titel]] - Link zu Artikel
  • Verweis auf anderen Beitrag einfügen: Rechtsklick auf Beitragstitel,
    "Adresse kopieren", und in den Text einfügen




Bild automatisch verkleinern, falls nötig
Bitte das JPG-Format nur für Fotos und Scans verwenden!
Zeichnungen und Screenshots im PNG- oder
GIF-Format hochladen. Siehe Bildformate.

Mit dem Abschicken bestätigst du, die Nutzungsbedingungen anzuerkennen.