Forum: Mikrocontroller und Digitale Elektronik Siebensegment Multiplexen in C


von Crashdemon (Gast)


Lesenswert?

Hallo zusammen ,

möchte zwei siebensegmentanzeigen über einen atmel8 gemultiplext 
ansteuern,
habe ich soweit auch schon mehr oder weniger hingekriegt mit 
untenstehenden
Code, allerdings gibt es Probleme wenn sich die Zahl die ausgegeben 
werden soll sehr schnell ändert, dann flackert die anzeige.

Habe leider kein Beispielcode in C gefunden, nur in assembler im 
Tutorial, allerdings, krieg ich es nicht auf die reihe, den in c 
umzuschreiben, wäre schön wenn mir jemand erklären würde wie ich 
vorgehen muss wenn ich siebensegmentanzeigen gemultiplext in c ansteuern 
will.

mfg crashdemon
1
PORTC = DIGIT_1; // Einerstellen einschalten  
2
PrintNumber(number1); // Einerstelle ausgeben
3
_delay_ms(FREQ*4); // 2ms Warten
4
PORTC = DIGIT_OFF; // SegmentAnzeige komplett ausschalten
5
6
PrintNumber(number10); // Zehnerstelle ausgeben
7
PORTC = DIGIT_10; // Zehnerstelle einschalten
8
_delay_ms(FREQ*4); // 2ms Warten
9
PORTC = DIGIT_OFF; // SegmentAnzeige komplett ausschalten

von Peter D. (peda)


Lesenswert?

Crashdemon wrote:
> Hallo zusammen ,
>
> möchte zwei siebensegmentanzeigen über einen atmel8 gemultiplext
> ansteuern,
> habe ich soweit auch schon mehr oder weniger hingekriegt mit
> untenstehenden
> Code, allerdings gibt es Probleme wenn sich die Zahl die ausgegeben
> werden soll sehr schnell ändert, dann flackert die anzeige.
>
> Habe leider kein Beispielcode in C gefunden

Z.B.:

Beitrag "ADC mit Multiplexanzeige"


Peter

von Crashdemon (Gast)


Lesenswert?

Naja hätte vllt. nicht schreiben sollen keinen Beispielcode gefunden, 
das Beispiel hatte ich schon gefunden allerdings habe ich ein Problem 
mit in den Code hineinzudenken, aber ich schau es mir nochmal an.

von Crashdemon (Gast)


Lesenswert?

Hab es auch ein wenig nders gemacht und würde das auch gerne so 
beibehalten nur halt mit einer ordentlichen multiplex funktion, hier 
noch meine komplette multiplex funktion.
1
char digit[10] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90};
2
3
void PrintNumber(int number)
4
{
5
  if(number < 0) // Wenn Zahl Negativ
6
  {
7
    number = number * (-1); // Wenn Zahl negativ, durch multipliziren positiv machen
8
  }
9
 
10
  if(number >= 100)
11
  {
12
    _error(); // Ist ein Fehler aufgetretet Error-Funktion starten
13
  }
14
15
  else
16
  {
17
    if(number < 10)
18
    {  
19
      // Wenn Zahl einstellig
20
      PORTC = DIGIT_1; // Einerstelle einschalten
21
      PORTD = digit[number]; // Hex-Wert aus Array holen  
22
    }
23
24
    if(number > 9)
25
    {  
26
      // Wenn Zahl zweistellig
27
      uint8_t number10; // Bei Nummern größer neun, Zehner
28
      uint8_t number1; // Bei Nummern größer neun, Einer
29
30
      number10 = number / 10; // Zehnerstelle ermitteln, durch zehn dividieren 
31
      number1 = number - (number10 * 10); // Einerstelle ermitteln
32
33
      // Multiplex 
34
      PORTC = DIGIT_1; // Einerstellen einschalten  
35
      PrintNumber(number1); // Einerstelle ausgeben
36
      _delay_ms(FREQ*4); // 2ms Warten
37
      PORTC = DIGIT_OFF; // SegmentAnzeige komplett ausschalten
38
      PrintNumber(number10); // Zehnerstelle ausgeben
39
      PORTC = DIGIT_10; // Zehnerstelle einschalten
40
      _delay_ms(FREQ*4); // 2ms Warten
41
      PORTC = DIGIT_OFF; // SegmentAnzeige komplett ausschalten
42
    }
43
  }
44
}

von crazy horse (Gast)


Lesenswert?

Komplett falscher Ansatz.
Lass einen Timerinterrupt laufen (1,2 oder 5ms).
-alle digits löschen
-statische Variable hochgezählen, je nach Zählerzustand das passende 
Bitmuster
ausgeben
-entsprechendes Digit einschalten.

Dazu gibts ein Array, wo die anzuzeigenden Daten drinstehen. Das machst 
du aus einem beliebigen Programmteil heraus, die Interruptroutine 
schaufelt nur stur die Bits raus.

von crappythumb (Gast)


Lesenswert?

Was Du brauchst ist ein (in Deinem Fall) zweistelliges Array, wo immer 
die aktuellen Ziffern (stellenweise) gespeichert sind.

Dann brauchst Du eine Routine, die periodisch aus diesem Array die 
Zifferninformationen auf den Port umlegt. Am besten machst Du das 
interrupt-gesteuert. Dann läuft das praktisch autonom und Du brauchst 
nur noch in dem normalen Programmablauf Deine Zifferninformationen in 
das Array zu schreiben.

Das erspart Dir nerviges Synchronisieren, was letztlich wahrscheinlich 
Dein Problem (mit den flackenden Ziffern) darstellt.
Vorteil bei dieser Interruptsteuerung: Man kann auch relativ bequem eine 
Dimmfunktion einbauen. Dabei benutzt man zB den T2-Overflow und den 
Comparematch2. Beim Overflow setzt man die jeweils neue (!) Ziffer aus 
dem Array auf die entsprechende 7-Seg-Anzeige/Port um und beim 
Comparematch löscht man alle (!) Ziffern/ den entsprechenden Port. Dann 
kannst Du über das Outputcompare-Register die Pulsweite (=Helligkeit) 
einstellen. Wenn Du ein Beispiel dessen willst, schick mir eine Email - 
nickname von oben und ein @gmx.de anhängen.

Keine Ahnung, wie Peter das in seinem Beispiel gelöst hat, 
möglicherweise ja sogar genauso - ich hab's mir nicht durchgelesen.

von crappythumb (Gast)


Lesenswert?

@verrücktes pferd...

bad timing...aber doppelt hält besser ;)

von Karl H. (kbuchegg)


Lesenswert?

Das ADC Beispiel ist wirklich nicht so besonders gut geeignet
um das Prinzip des Multiplexing isoliert zu verstehen. Die
eigentliche Multiplex-Funktionalität ist in die ADC-ISR
hineinverwoben. Ich kann mir schon vorstellen, dass jemand
der nicht weiss, worauf er achten soll, die nicht findet.
Dazu kommt, dass PeDa hier auch noch darauf achten muss,
den Port während der ADC Messung ruhig zu halten (das ist dann
auch der Grund warum das Multiplexing erfolgt, nachdem der ADC
eine Messung abgeschlossen hat und seine ISR ausführt).

Das Prinzip, wie man sowas im Allgemeinen macht wurde ja schon
angesprochen und zusammen mit der Erläuterung von Multiplexing
im Tutorial sollte es kein Problem mehr sein, sowas umzusetzen.
Nicht den Assembler Code umschreiben. Das hat wenig bis keinen
Sinn, da du dich in Assembler Details verlierst. Aber das Prinzip
aus dem Assembler Tutorial kann 1:1 übernommen werden (und ist
genau das, welches in diesem Fred schon angesprochen wurde)

von Crashdemon (Gast)


Lesenswert?

danke für die vielen antworten,

ich fasse jetzt mal zusammen wie ich denke vorgehen zu sollen.

- timer basteln der in regelmäßigen abständen einen interrupt erzeugt
- die momentan erleuchtete Anzeige abgeschaltet (kling plausibel)
- das Muster für die nächste Anzeige am Port D ausgegeben
- die nächste Anzeige durch eine entsprechende Ausgabe am Port C 
eingeschaltet

ich würde allerdings gerne mein array behalten und einer und 
zehnerstellen getrennt voneinander ermitteln sprich
1
23 = (23 / 10) = 2 = zehnerstelle (kein rest, da integer)
2
23 = 23 - (zehnerstelle(2) * 10 ) = 3 = einerstelle

würde das auch gehen oder muss es umbedingt mit bitmasken gemacht werden

von Karl H. (kbuchegg)


Lesenswert?

Crashdemon wrote:
> danke für die vielen antworten,
>
> ich fasse jetzt mal zusammen wie ich denke vorgehen zu sollen.
>
> - timer basteln der in regelmäßigen abständen einen interrupt erzeugt
> - die momentan erleuchtete Anzeige abgeschaltet (kling plausibel)
> - das Muster für die nächste Anzeige am Port D ausgegeben
> - die nächste Anzeige durch eine entsprechende Ausgabe am Port C
> eingeschaltet

Ganz genau.
Der springende Punkt:
  Bei einem Aufruf der ISR wird nur zur nächsten Anzeige
  umgeschaltet und die Ausgabe für diese Anzeigenstelle gemacht.
  Nicht mehr.
  Da die ISR aber sehr oft aufgerufen wird, bekommst du das
  Umschalten nicht mehr mit und siehst beide Anzeigestellen
  leuchten.

> ich würde allerdings gerne mein array behalten und einer und
> zehnerstellen getrennt voneinander ermitteln sprich

Kannst du ja.
Ganz im Gegenteil: sollst du auch.

Du legst dir ein Array zurecht. In deinem Fall ist es ein
Array mit 2 Elementen. Nennen wir es mal Anzeige
In Anzeige[0] speicherst du das auszugebende Bitmuster für
die 'Hunderterstelle', in Array[1] speicherst du das
auszugebende Bitmuster für die 'Einerstelle'.
Die ISR sorgt dann dafür, dass diese Bitmuster abwechselnd auf
den beiden 7-Segment (natürlich auf den richtigen) ausgegeben
werden.

Dann brauchst du noch eine Funktion, der du eine Zahl
übergibst, und die aus dieser Zahl die den Ziffern
entsprechenden Bitmuster an die richtigen Positionen im
Array schreibt.

Und das wars dann auch schon.

Die ISR zeigt an, was auch immer sie im Array vorfindet.
Und wenn du eine Zahl ausgeben willst, dann rufst du
einfach diese Ausgabefunktion auf, die das Array entsprechend
befüllt. Die grundlegende Funktionalität, nämlich das Zerlegen
einer Zahl und das bestimmen des zugehörigen Bitmusters
hast du ja in deiner Funktion schon gemacht. Das ist also
nichts neues. Aber anders als in deiner Funktion wird nichts
ausgegeben, sondern die Bitmuster lediglich an die richtige
Stelle des Arrays geschrieben, von wo sich die ISR dann diese
Werte holt und die tatsächliche Ausgabe macht.

Das ganze sollte nicht wesentlich mehr als 10 bis 15 Zeilen
Code sein.

von Crashdemon (Gast)


Lesenswert?

So ganz komme ich mit den Interrupts noch nicht klar, habe das Problem 
sobald ich einmal in den Zweistelligen Bereich kommen bei der 
Siebensegmentanzeige wird der Interrupt danach ständig aufgerufen auch 
wenn ich nur eine einstellige zahl ausgeben will, hab hier anbei mal 
meine funktion und isr mit den wichtigen variablen gapackt, wäre nett 
wenn da mal einer drüber schauen könnte.
1
volatile char digit[10] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90}; // Bitmaske für die Zahlen
2
volatile char display[2] = {0x10, 0x08}; // Siebensegmentanzeige (Einer- , Zehnerstelle)
3
4
uint8_t DIGIT1; // Wert der Einerstelle
5
uint8_t DIGIT2; // Wert der Zehnerstelle
6
volatile int counter = 1; // Anzeige die Angeschaltet werden soll
7
8
9
SIGNAL(SIG_OVERFLOW0) // Aufruf bei Interrupt von Timer0 (8 Bit)
10
{
11
  PORTC |= 0xFF; // Alle Siebensegmentanzeigen ausschalten
12
  PORTC |= display[counter]; // Anzeige einschalten (counter == 1 -> Einerstelle | counter == 2 -> Zehnerstelle)
13
14
  for(counter = 1; counter < 2; counter++) // Hochzählen bis 2
15
  {
16
    PORTC = display[counter]; // Anzeige einschalten
17
    
18
    if(counter == 1) // Wenn Einstellig Anzeige an, Einerwert ausgeben
19
    {
20
      PORTD = digit[DIGIT1]; // Einerstelle an Seg. Anzeige ausgeben    
21
    }
22
23
    if(counter == 2) // Wenn Zweistellig Anzeige an, Zehnerwert ausgeben
24
    {
25
      PORTD = digit[DIGIT2]; // Einerstelle an Seg. Anzeige ausgeben
26
      counter = 1; // Anzeige wieder auf Einerstellen umschalten
27
    }
28
  }
29
   
30
  TIFR = (1 << TOV0); // Overflow INterrupt wieder zurücksetzen
31
  cli(); // Globale Interrupts auschalten
32
}
33
34
35
void PrintNumber(int number)
36
{
37
  cli(); // Global Interrupt Disabled
38
39
  if(number < 0) // Wenn Zahl Negativ
40
  {
41
    number = number * (-1); // Wenn Zahl negativ, durch multipliziren positiv machen
42
  }
43
 
44
  if(number >= 100)
45
  {
46
    _error(); // Ist ein Fehler aufgetretet Error-Funktion starten
47
  }
48
49
  else
50
  {
51
    if(number < 10)
52
    {  
53
      // Wenn Zahl einstellig
54
      PORTC = DIGIT_1; // Einerstelle einschalten
55
      PORTD = digit[number]; // Hex-Wert aus Array holen  
56
    }
57
58
    if(number > 9)
59
    {  
60
      sei(); // Global Interrupt Enabled
61
62
      // Wenn Zahl zweistellig
63
      uint8_t number10; // Bei Nummern größer neun, Zehner
64
      uint8_t number1; // Bei Nummern größer neun, Einer
65
66
      number10 = number / 10; // Zehnerstelle ermitteln, durch zehn dividieren 
67
      number1 = number - (number10 * 10); // Einerstelle ermitteln
68
69
      TCCR0 = (1 << CS02); // Timer einschalten mit einen Teiler von 256
70
      TIMSK = (1 << TOIE0); // Overflow Interrupt einschalten
71
72
      number10 = DIGIT2; // In globale Variable verpacken, damit diese in ISR benutzt werden kann
73
      number1  = DIGIT1; // In globale Variable verpacken, damit diese in ISR benutzt werden kann
74
75
      // Multiplex 
76
      /*
77
      PORTC = DIGIT_1; // Einerstellen einschalten
78
        
79
      //PrintNumber(number1); // Einerstelle ausgeben
80
      PORTD = digit[number1];
81
      // **************************
82
83
      _delay_ms(FREQ*4); // 2ms Warten
84
      PORTC = DIGIT_OFF; // SegmentAnzeige komplett ausschalten
85
86
      //PrintNumber(number10); // Zehnerstelle ausgeben
87
      PORTD = digit[number10];
88
      // **************************
89
90
      PORTC = DIGIT_10; // Zehnerstelle einschalten
91
      _delay_ms(FREQ*4); // 2ms Warten
92
      PORTC = DIGIT_OFF; // SegmentAnzeige komplett ausschalten
93
      */
94
    }
95
  }
96
}

von Gabriel W. (gagosoft)


Lesenswert?

interrupr Serviceroutinen schrieb man früher *SIGNAL()* jetzt wird 
*ISR()* verwendet, ein paar Versionen vom GCC wird SIGNAL noch 
funktionieren, doh ist als depricated markiert.

TIMSK und TCCR0 musst Du einmal bei der initialisierung setzten, dann 
bleibt das eh im ConfigRgister stehen.

bei jedem Aufruf der ISR solltest Du EINEN Wert ausgeben und den Zähler 
erhöhen, beim NÄCHSTEN Aufruf kommt dann das nächste Digit dran.
Wenn Du PORTC 0xFF haben willst nimm statt PORTC |= 0xFF PORTC = oxFF, 
da sparst Du Dir das lesen des Ports, ausserdem reicht eine einfache 
Zuweisung
PORTC = display[counter]; (Dir ist ja egal was vorher auf PORTCV war, es 
soll lediglich display[counter] ausgegeben werden (wieder ein unnötiges 
lesen gespart - und mehr lesbarkeit in den Code gebracht)

von Peter D. (peda)


Lesenswert?

Crashdemon wrote:
> So ganz komme ich mit den Interrupts noch nicht klar, habe das Problem
> sobald ich einmal in den Zweistelligen Bereich kommen bei der
> Siebensegmentanzeige wird der Interrupt danach ständig aufgerufen auch
> wenn ich nur eine einstellige zahl ausgeben will

Dem Multiplexen ists wurscht, ob die Zahl 2-, 1- oder 0-stellig ist, 
gemultiplext wird immer.
Sonst würde Deine Zahl ja je nach Stellenzahl unterschiedlich hell 
leuchten.

Du hast 2 Bytes für die Segmentmuster der beiden Anzeigen und der 
Interrupt gibt diese Bytes abwechselnd aus mit dem zugehörigen 
Stellenbit eingeschaltet.



Peter

von Crashdemon (Gast)


Lesenswert?

Danke schonmal für die schnellen antworten, ja hab ich mit signal, jetzt 
in ISR(TIMER0_OVF0_vect)  geändert, werde die die isr jetzt nochmal 
überarbeiten und dann schaune wir mal obs funzt.

von Thorsten (Gast)


Lesenswert?

Vielleicht verstehe ich dich falsch, aber ich habe den Eindruck du 
willst die ISR nur aufrufen, wenn du zwei Stellen auszugeben hast. Wenn 
nur eine ausgegeben werden soll, dann willst du die ISR anscheinend 
nicht nutzen.

Du aktivierst in deinem Code den Overflow Interrupt, wenn eine Zahl >10 
ausgegeben werden soll mit:

TIMSK = (1 << TOIE0); // Overflow Interrupt einschalten

Das heißt aber nicht, dass dieser Interrupt wieder deaktiviert wird, 
wenn die ISR einmal durchlaufen wurde. Der Interrupt bleibt trotzdem 
aktiv und wird somit immer wieder ausgeführt. Das ist genau das was du 
beobachtest hast, einmal eine Zahl größer 10 und die Routine wird immer 
wieder aufgerufen.


Ich würde es an deiner Stelle so machen, wie es oben auch schon 
beschrieben wurde:
Nutze die ISR um deine Zahlen auf der 7-Segment-Anzeige auszugeben und 
zwar immer, egal ob nur eine oder beide Stellen ausgegeben werden soll. 
Lass den Timer einfach von Anfang an laufen, so wird die Anzeige 
zyklisch aktualisiert.

In deiner main oder in der PrintNumber-Methode schreibst du einfach 
immer die Variablen DIGIT1 und DIGIT2 wieder neu, aber nicht die Ports 
selbst. Für die Ports ist nur die ISR zuständig. Meiner Meinung nach ist 
das die sauberste Methode für deine Ausgabe, aber ich lass mich da auch 
gerne eines besseren belehren.

von Thorsten (Gast)


Lesenswert?

das muss natürlich >9 statt >10 heißen

von Crashdemon (Gast)


Lesenswert?

Jo richtig ich wollte nur die zahlen die <9 sind über die ISR ausgeben, 
das ist dann natürlich blöd das die interupt routine nach einmaligen 
aufrufen immer wieder durchlaufen wird, kann man das unterbinden? wenn 
nein das muss ich es wohl so machen wie du es beschrieben hast, indem 
ich einstellige sowie zweistellige zahle per isr ausgebe.

mfg crashdemon

von Thorsten (Gast)


Lesenswert?

ich muss mich korrigieren. Ich hab das cli() am Ende deiner ISR 
übersehen, das sollte alle Interrupts deaktivieren, so dass die Routine 
nicht erneut aufgerufen wird. Ich hoffe, ich hab dich nun nicht 
durcheinander gebracht damit.

Trotzdem würde ich dir empfehlen alle Ausgaben über die ISR laufen zu 
lassen. Was spricht denn aus deiner Sicht dagegen das zu machen?

von Crashdemon (Gast)


Lesenswert?

Das cli() scheint bei mir allerdings nicht zu funktionieren da er nicht 
wieder aus der ISR rausspringt?, bin leider in interrupts nicht so fit 
und dachte mir das ich es am besten so leicht wie möglich mache aber ich 
werde da wohl nicht rumkommen, bei über eine routine abzuwickeln weiß 
nur noch nicht wie diese aussehen soll, schade das es im avr-gcc 
toutorial nicht so ein schönes beispiel in c gibt, wie im assembler 
tutorial.

von Lötlackl (Gast)


Lesenswert?

Hallo,

@Crashdemon

kann Dir noch folgendes Beispiel (von mir)
zum Thema "Anzeige multiplexen" beisteuern: 
Beitrag "mal wieder: UV-Belichtungstimer"

mfg Lötlackl

von Thorsten (Gast)


Lesenswert?

Wie ich jetzt auch erst lesen musste, darfst du kein cli() innerhalb 
einer ISR nutzen. Du sperrst dich damit anscheinend selbst in der ISR 
ein, da das nötige reti nicht ausgeführt werden kann, weil du den 
Interrupt schon gesperrt hast durch das cli().

Falls du weiterhin bei deiner Idee bleiben willst, nur zweistellige 
Zahlen per ISR auszugeben, ersetze mal das cli() durch:

TIMSK = 0;

Damit müsstest du eigentlich dafür sorgen, dass der Interrupt erst 
wieder aufgerufen werden kann, wenn TIMSK wieder gesetzt wird. Das reti 
sollte in diesem Fall aber ausgeführt werden, so dass er zurück in die 
main springen kann.

Vielleicht klappt das, kannst ja mal ausprobieren und Bericht erstatten.

von Crashdemon (Gast)


Lesenswert?

Hat nicht irgendjemand eine funktionierende Interrupt Routine für zwei 
Siebensegmentanzeigen, mit dem Timer0 (8 Bit) und zusätzlichen 
schnickschack, weil es bei rausschrieben aus fremden Code sehr schwer 
ist die Funktionen nachzuvollziehen, erst recht wenn diese über zieg 
headers verstreut sind, wäre dankbar wenn jemand der schonmal so eine 
routine gebastelt hat diese hier posten könnte.

mfg crashdemon

von Peter D. (peda)


Lesenswert?

Crashdemon wrote:
> Das cli() scheint bei mir allerdings nicht zu funktionieren da er nicht
> wieder aus der ISR rausspringt?

Das CLI im Interrupt kannst Du Dir an die Backe schmieren, es bewirkt 
garnichts.

Die globale Interruptfreigabe ist nur ein Bit und kein Zähler.

Du kannst 1000-mal CLI schreiben, das nächste RETI oder SEI gibt 
trotzdem die Interrupts wieder frei.

Du darfst also ruhig im Interrupt CLI oder NOP schreiben, es macht 
keinen Unterschied (also kannst Du es sein lassen).


Peter

von Peter D. (peda)


Lesenswert?

Crashdemon wrote:
> weil es bei rausschrieben aus fremden Code sehr schwer
> ist die Funktionen nachzuvollziehen,

Hast Du schonmal versucht, dem Autor Fragen zu stellen etwa in der Art, 
"was macht Zeile X in File Y von Beispiel Z"?


> erst recht wenn diese über zieg
> headers verstreut sind

Header enthalten normalerweise keinen Code.


Peter

von Matthias L. (Gast)


Lesenswert?

versuch mal das. ist aber trocken programmiert. evtl musst du noch paar 
details (Reg.namen, Ausgabe an PORTC,..) anpassen.
Sollte als Start genügen.
1
//-- constants -------------------------------------------
2
#define  c_u8MaxCol  1         // must be 2^n-1
3
//-- globals ---------------------------------------------
4
volatile  uint8_t  au8SegMem[c_u8MaxCol+1];
5
//-- 7seg data -------------------------------------------
6
char digit[] PROGMEM = { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 
7
                  0x92, 0x82, 0xF8, 0x80, 0x90};
8
9
//*********************************************************************
10
ISR  ( Timer0_Overflow)
11
{
12
  //-- declare locals ------------------
13
  static  uint8_t  _u8Coloumn  = 0x00;
14
  // to next coloumn -------------------
15
  u8Coloumn  =  (u8Coloumn + 1) & c_u8MaxCol;
16
  //-- output this coloumn -------------
17
  PORTC = (1<<u8Coloumn);
18
  PORTD = au8SegMem[u8Coloumn];
19
}
20
//*********************************************************************
21
void  showDez ( uint8_t  _u8Value )
22
{
23
  //-- lower digit (0..9)---------------
24
  au8SegMem[0] =  pgm_read_byte(digit[_u8Value%10]);
25
  //-- next higher digit (00..90) ------
26
  _u8Value     /= 10;
27
  au8SegMem[1] =  pgm_read_byte(digit[_u8Value%10]);
28
}
29
//*********************************************************************
30
void  main  ( void )
31
{
32
  //-- col & 7Seg to output ------------
33
  DDRD  = 0xFF;
34
  DDRC  = 0xFF;
35
  //-- init T0 -------------------------
36
  TCCR0  |=  (1<<CS00);      // runs with clk
37
  TIMSK  |=  (1<<TOIE0);     // enable overfl int
38
  sei ();
39
  //-- pre-init 7seg with 91 -----------
40
  showDez (91);
41
  //-- endless loop --------------------
42
  while (1)
43
  {
44
     // place some code here
45
  }
46
}

von Crashdemon (Gast)


Lesenswert?

Hab den Code ausprobiert und angepasst funzt aber irgendwie nicht, er 
zeigt nichts auf der anzeige an, ich denke ich habe nich ein 
zusätzliches problem was mir bis jetzt noch nicht so aufgefallen ist und 
hatte ich bis jetzt immer schön ein while(1) laufen worin der die werte 
des adc eingelesen hatte und mit in der schleife befand sich der aufruf 
der funktion PrintNumber(ADC_Wert)
wenn ich das jetzt aber über Interupts machen würde würde er die 
funktion immer wieder aufrufen, also wie eine art neuen thread, liege 
ich damit richtig?
1
while(1)
2
{
3
    ReadChannel(0); Messwert des AD-Wandlers 
4
    PrintNumber(ADC_angle); // Wert ausgeben, ganzzahlig nicht negativ
5
}

von Matthias L. (Gast)


Lesenswert?

1) Satzzeichen setzen! Sonst versteht man ja nichts.

2) Der Code muss, so wie er ist, erstmal eine 91 auf der ANzeige 
darstellen.
   Solange das nicht passiert, brauchst du auch nicht mit dem ADC 
rumkaspern.
   => Poste mal die komplette Schaltung (µC + 7Seg Anbindung), dann kann
      ich den COde anpassen.
3) Es ist sehr sinnvoll, sich auf Funktionen "ReadChannel(0)" zu 
berufen, die
   andere hier nicht kennen!

von Crashdemon (Gast)


Lesenswert?

Die Beschaltung der Siebensegmentanzeige ist analog zu der aus dem 
AVR-Tutorial 
(http://www.mikrocontroller.net/articles/AVR-Tutorial:_7-Segment-Anzeige), 
wobei der Transisitor für die Einerstelle an PC3, für die Zehnerstelle 
an PC4 angeschlossen ist.

Die Funktion ReadChannel kann im AVR-GCC Tutorial hier nachgelesen 
werden: 
(http://www.mikrocontroller.net/articles/AVR-GCC-Tutorial#Analoge_Ein-_und_Ausgabe)

danke im voraus

von Karl H. (kbuchegg)


Lesenswert?

> das ist dann natürlich blöd das die interupt routine nach einmaligen
> aufrufen immer wieder durchlaufen wird, kann man das unterbinden?

Das ist überhaupt nicht blöd. Ganz im Gegenteil. Die Funktion
soll immer wieder aufgerufen werden. Denn die ISR-Funktion
sorgt dafür, dass deine Anzeige richtig funktioniert, indem
sie abwechselnd die eine und dann die andere Stelle aktiviert.
Du hast eines immer noch nicht verinnerlicht:
Der eigentliche Anzeigevorgang ist unabhängig von dem Generieren
des Anzuzeigenden!
Das ist in wichtiges Prinzip! Denn du brauchst dich dadurch nicht
mehr um die Anzeige kümmern. Das macht die ISR ganz alleine.
Du schreibst lediglich die auszugebenden Bitmuster in die globalen
Variablen. Mehr hast du nicht zu tun, den Rest macht die ISR,
wenn sie das nächste mal drann ist.

Ich geh mal davon aus, dass deine Lösung von oben die
Hardware richtig anspricht.
Du machst das alles viel zu kompliziert! Ein Aufruf der ISR
gibt eine Steller aus. Beim nächsten Aufruf der ISR wird
die nächste Stelle ausgegeben. Und zwar ständig! Immer und
immer wieder!
1
unsigned char digit[10] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90}; // Bitmaske für die Zahlen
2
3
volatile unsigned char segment[2];
4
unsigned char display[2] = {0x08, 0x10}; // Steuerleitungen für ein Digit
5
unsigned char counter; // Anzeige die Angeschaltet werden soll
6
7
ISR( TIMER0_OVF_vect ) // Aufruf bei Interrupt von Timer0 (8 Bit)
8
{
9
  // Die bisherige Anzeige ausschalten
10
  PORTC |= 0xFF;
11
12
  // welches ist die nächste Stelle
13
  counter = counter + 1;
14
  if( counter == 2 )
15
    counter = 0;
16
17
  // Diese Stelle ausgeben
18
  PORTD = segment[counter];
19
  PORTC = display[counter];
20
}
21
22
void PrintNumber(int number)
23
{
24
  if( number < 0 )
25
    number = -number;
26
27
  segment[0] = digit[ number / 10 ];
28
  segment[1] = digit[ number % 10 ];
29
}
30
31
int main()
32
{
33
  DDRC = 0xFF;
34
  DDRD = 0xFF;
35
36
  TCCR0  |=  (1<<CS00);      // Vorteiler 1
37
  TIMSK  |=  (1<<TOIE0);     // Overflow Interrupt
38
39
  PrintNumber( 0 );
40
41
  sei();
42
43
  while (1)
44
  {
45
  }
46
}

Wann immer du eine Zahl zum Ausgeben hast, dann rufst du die
Funktion PrintNumber auf. Diese zerlegt die Zahl und stellt
die richtigen Bitmuster in das globale Array 'segment'. Wenn
die ISR das nächste mal drann ist, dann gibt sie dann das
neue Bitmuster aus.

Ich hab mich an deine Vorgabe was Hardwareansteuerung geht
gehalten. Wenn da was nicht stimmen sollte, dann wirst du
wahrscheinlich nichts sehen. Versuch wenigstens zu verstehen,
was da passiert, häng dich im Simulator in den Debugger und finde
raus, welche Portzuweisung konkret nicht stimmt. Das ganze ist
viel einfacher als du denkst.

als du denkst.

von Crashdemon (Gast)


Lesenswert?

Danke für das Programm, es funktioniert jetzt, erst hatte ich ein 
Problem dass er nur den gleichen Wert angezeigt hatt, nachdem ich aber 
das sei() vor PrintNumber(0); gesetzt habe funktionierte alles 
einwandfrei.

thanx crashdemon

von Crashdemon (Gast)


Lesenswert?

Hätte nicht gedacht das es so einfach ist, die Interrupt gibt quasi 
pausenlos die Zahlen an die anzeigen und muss sich nicht um irgendwelche 
timings vom adc kümmern, wie bei meinen alten programm, ändert sie die 
auszugebende zahl, so wird das auch einfach von der ISR berücksichtig.

so noch zu allerletzt noch ein paar fragen zu code, dei eigentlich eher 
die c systax selber betreffen, mir würde auch ein link zur eräuterung 
reichen, leider weiß ich nicht nach was ich in google suchen soll.

Korrigiert mich bitte wenn ich falsch liege
1
if( number < 0 ) // Vom Verständnis wenn Zahl negativ mit (-1) multipliziern
2
    number = -number; 
3
4
  segment[0] = digit[ number / 10 ]; // Zahl durch 10 teilen, Zahlen werden nach den Komma nich berücksichtigt, da Integer
5
  segment[1] = digit[ number % 10 ]; // Hmm?? 10% das sagt mir jetzt gar nichts

von Thorsten (Gast)


Lesenswert?

wenn du eine Zahl durch eine andere dividierst, dann ergibt das bei 
Ganzzahlen ja häufig einen Rest, so dass man eine Division auch 
folgendermaßen beschreiben kann:

a / b = c * b + d  (alle Zahlen sind Ganzzahlen)

Beispiel: 36 / 10 = 3 * 10 + 6



In deinem Quellcode finden sich nun zwei Anweisungen wieder:

number / 10 ergibt, wie du selbst schon erkannt hast, das ganzzahlige 
Ergebnis der Division ohne Nachkommastellen.

number % 10 ergibt jetzt einfach nur den Rest der Division number / 10.

Mit dem Beispiel von oben erklärt:
36 / 10 = 3
36 % 10 = 6

Für dich heißt das einfach nur number / 10 berechnet dir die 
Zehnerstelle, number % 10 die Einerstelle.

von Crashdemon (Gast)


Lesenswert?

warum kann ich die PrintNumber Funktion nicht in die while-Schleife 
packen, sobald ich das mache, zeigt die Anzeige nur noch null an.
Würde gerne so etwas in der art Abarbeiten, muss ich dann auch wieder 
zusäzlich mit einen Interrupt vom ADC arbeiten?
1
int main()
2
{
3
  uint8_t analog_value;
4
5
  DDRC = 0xFF;
6
  DDRD = 0xFF;
7
8
  TCCR0  |=  (1<<CS00);      // Vorteiler 1
9
  TIMSK  |=  (1<<TOIE0);     // Overflow Interrupt
10
11
  sei();
12
13
  while (1)
14
  {
15
      analog_value = ReadChannel(0); // Ersten ADC auslesen
16
      calculated = Calculator(analog_value); // Den Analogwert umrechnen 
17
18
      PrintNumber(calculated);
19
  }
20
}

von Karl H. (kbuchegg)


Lesenswert?

Crashdemon wrote:
> warum kann ich die PrintNumber Funktion nicht in die while-Schleife
> packen,

Es gibt keinen Grund dafür, bzw. der Grund ist nicht in PrintNumber
zu suchen.

> sobald ich das mache, zeigt die Anzeige nur noch null an.

Wird wohl daran liegen, dass PrintNumber ständig mit 0
aufgerufen wird.

von Thorsten (Gast)


Lesenswert?

wenn das vorher alles richtig funktioniert hat als es noch außerhalb der 
while-Schleife stand, dann sollte es nun nicht an der 
PrintNumber-Funktion liegen.

Ich würde den Fehler dann eher mal in den Methoden ReadChannel und 
Calcualator suchen.

von Crashdemon (Gast)


Lesenswert?

Suche jetzt schon eine ganze weile nach einen Fehler finde aber leider 
keinen ,ich glaube das sich die Interrupt ins gehäge kommen, der vom ADC 
und der vom Timer. Ich poste hier mal meine Source vllt. findet ja 
jemand einen Fehler?
1
#include <math.h>
2
#include <avr/io.h>
3
#include <avr/wdt.h>
4
#include <avr/iom8.h>
5
#include <util/delay.h>
6
#include <avr/sleep.h>
7
#include <avr/interrupt.h>
8
9
#define XTAL 4000000L    // Systemtakt in Hz
10
11
unsigned char status[2] = {0x01, 0x02}; // Status LED's (Rot, Grün)
12
unsigned char digit[10] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90}; // Bitmaske für die Zahlen
13
unsigned char display[2] = {0x10, 0x08}; // Siebensegmentanzeige (Einer- , Zehnerstelle)
14
volatile unsigned char segment[2];
15
unsigned char counter; // Anzeige die Angeschaltet werden soll
16
17
uint8_t MyADCSRA;
18
19
ISR(TIMER0_OVF_vect) // Aufruf bei Interrupt von Timer0 (8 Bit)
20
{
21
  PORTC |= 0xFF; // Die bisherige Anzeige ausschalten
22
23
    counter = counter + 1; // Welches ist die nächste Stelle
24
25
    if(counter == 2) // Wenn Stelle 2 erreicht, zurücksetzen
26
    {
27
    counter = 0;
28
  }
29
30
    PORTD = segment[counter]; // Jeweiligen Wert ausgeben
31
    PORTC = display[counter]; // Jeweilige Stelle einschalten
32
}
33
34
ISR(ADC_vect)
35
{
36
  MyADCSRA |= (1 << ADIF); // Speicher für Interrupt Flag
37
}
38
39
uint16_t ReadChannel(uint8_t mux)
40
{
41
  uint8_t i;
42
  uint16_t result;
43
44
  MyADCSRA = 0; // Interrupt Variable Funktion 
45
  cli(); // Global Interrupt Disabled
46
  
47
  // AD-Wandler wird eingeschaltet
48
   // Frequenzvorteiler Teilungsfaktor 32
49
  // bei 4Mhz Taktfrequenz der CPU 
50
  //
51
  //  AD_TAKT = CPU_TAKT / TEILUNGSFAKTOR = 4Mhz / 32 = |125kHz|
52
  //  PERIODENDAUER = 1 / 125kHz = |8 mikroSec.|
53
  //
54
  // Es werden alle 8 mikroSec. neue werte vom AD-Wandler eingelesen.
55
  // Zusätzlich noch den ADC Interrupt (ADIE) einschalten zur
56
  // weiterverarbeitung für Sleep mode (ADC Noise Canceler)
57
  ADCSRA |= (1 << ADEN) | (1 << ADPS2) | (1 << ADPS0) | (1 << ADIE);  
58
  
59
  set_sleep_mode(SLEEP_MODE_ADC); // ADC Sleep mode vorwählen
60
  sei(); // Global Interrupt Enabled
61
62
  ADMUX = mux; // Kanal waehlen
63
64
    // Nach Aktivieren des AD-Wandlers wird ein "Dummy-Readout" empfohlen, 
65
  // man liest also einen Wert und verwirft diesen, 
66
  // um den AD-Wandler "warmlaufen zu lassen". 
67
    ADCSRA |= (1 << ADSC);              
68
    while ( ADCSRA & (1 << ADSC)) 
69
  {
70
    }
71
  
72
  // 10Bit Ergenisregister ADCL und ADCH einlesen, und als 16Bit großen 
73
  // Integer Wert speichern. Werte aus ADCL und ADCH rechtsbündig 
74
    result |= ADCL + (ADCH << 8); 
75
76
  // Messung - Arith. Mittelwert aus vier aufeinanderfolgenden Wandlungen 
77
    result = 0; 
78
79
    for(i = 0; i < 90; i++)
80
    {
81
    ADCSRA |= (1 << ADSC); // eine Wandlung "single conversion"
82
    MyADCSRA &= ~(1 << ADIF); // ADC Interrupt Flag setzten  
83
84
    while(!(MyADCSRA & (1 << ADIF))) // Solange Einlesen bis kein Interrupt Flag von ADC vorhanden
85
    {
86
    }      
87
    result += ADCL + (ADCH << 8);  // Nach jeder Wandlung Ergebnisse aufaddieren
88
    }
89
90
  ADCSRA &= ~(1 << ADEN); // Nach vier Wandlungen AD-Wandler deaktivieren 
91
 
92
    result /= 90; // Summe der Wandlungen durch vier teilen = arith. Mittelwert
93
   return result; // den arith. Mittelwert als Rückgabewert der Funktion ReadChannel
94
}
95
96
void status_LED(uint8_t color) // Zeigt den Zustand an
97
{
98
  DDRB = 0x03; // Port (B) 1-3 als Ausgang  
99
  PORTB = 0xFF; // Alle Led'S aus
100
101
  switch(color) // Variable "color" abfragen
102
  {
103
    case 1:
104
      // Rote LED leuchtet - Alarmzustand
105
      PORTB = status[0];
106
    break;
107
108
    case 2:
109
      // Grüne LED Leuchtet - Alles Super! 
110
      PORTB = status[1];
111
    break;
112
  }
113
}
114
115
void _error(void)
116
{
117
  PORTC = 0x10; // Einerstelle einschalten
118
  PORTD = 0x06; // E (ERROR) Ausgeben
119
  status_LED(1); // Status LED auf Rot (Fehler aufgetretet)
120
}
121
122
void PrintNumber(int number)
123
{
124
  if(number < 0) // Wenn Zahl Negativ
125
  {
126
      number = -number; // Wenn Zahl negativ, durch Multiplizieren mit (-1) positiv machen
127
  }
128
129
  if(number < 10) // Einstellig
130
  {
131
    segment[0] = digit[ number % 10 ]; // Einerstelle bestimmen
132
    segment[1] = 0xFF;  // Zehnerstelle leer lassen
133
  }
134
135
  if(number > 9) // Zweistellig
136
  {
137
    segment[0] = digit[ number % 10 ]; // Einerstelle bestimmen
138
      segment[1] = digit[ number / 10 ]; // Zehnerstelle bestimmen
139
  }
140
141
  if(number > 99) // Wenn Zahl größer als 99, als dreistellig
142
  {
143
    _error(); // Ist ein Fehler aufgetretet Error-Funktion starten
144
  }
145
}
146
147
float Angle(uint16_t x, uint16_t y, uint16_t z)
148
{
149
  float tilt; // Winkel in Degree (°)
150
151
  //  x-Value | z-Value | Degree
152
  //   409.6  |  614.4  |    0°
153
  //   614.4  |  409.6  |   90°
154
  tilt = (asin((x - 512.0) / 102.4)) * (180 / M_PI);
155
  
156
  return tilt; // Rückgabewert (Winkel) der Funktion
157
}
158
159
int main(void)
160
{
161
  float ADC_angle;
162
163
  uint16_t accelx; // Messergebnisse des Kanals C0 (X) (0-1024)
164
  uint16_t accely; // Messergebnisse des Kanals C1 (Y) (0-1024)
165
  uint16_t accelz; // Messergebnisse des Kanals C2 (Z) (0-1024)
166
167
  // Eigenschaften der Ports D und C definieren
168
  DDRC = 0x18; // Nur die Ports 3-5 (C) auf Ausgänge schalten
169
  DDRD = 0xFF; // Port D Komplett als Ausgang für 7 Segment
170
171
  wdt_enable(5); // Watchdog hält wache im 500ms Zyklus
172
173
    TCCR0 |= (1 << CS00); // Vorteiler 1
174
    TIMSK |= (1 << TOIE0); // Overflow Interrupt
175
176
  sei(); // Globale Interrupts einschalten
177
178
  while(1)
179
  {
180
    wdt_reset(); // Watchdog Timer zurücksetzen
181
182
    status_LED(2); // Status LED auf Grün (Alles OK)
183
    
184
    accelx = ReadChannel(0); // 10Bit - Messwert der x-Achse 
185
    accely = ReadChannel(1); // 10Bit - Messwert der y-Achse
186
    accelz = ReadChannel(2); // 10Bit - Messwert der z-Achse
187
188
    ADC_angle = Angle(accelx, accely, accelz); // Winkel in Variable verstauen
189
      
190
    PrintNumber(ADC_angle); // Wert ausgeben, ganzzahlig nicht negativ
191
  }
192
}

von Philipp B. (philipp_burch)


Lesenswert?

'segment' und 'display' vielleicht mal als volatile deklarieren...

von Crashdemon (Gast)


Lesenswert?

Habe eine volatile draus gemacht, seh gerade das ich noch gar nicht 
geschrieben habe um was für einen Fehler es sich handelt. Das Problem 
ist das die Siebensegmentanzeige immer Null anzeigt, das tut sie aber 
rst seitdem ich die PrintNumber Funktion mit Interrupts habe, iegentlich 
soll die den Winkel der Platine anzeigen auf der der Sensor 
angeschlossen ist der über die Analog Eingänge eingelesen wird.

von Philipp B. (philipp_burch)


Lesenswert?

Mach' mal beim Start von 'main' eine Überprüfung der Resetquelle 
(Power-On, Reset, Brown-Out oder Watchdog). Ich könnte mir denken, dass 
der Watchdog zuschlägt, bevor 'Angle()' fertig ist. Ist aber nur eine 
Vermutung, hab' mir jetzt nicht den gesamten Code genau angesehn.
Die Verwendung von 'volatile' hat nix geändert?

von Thorsten (Gast)


Lesenswert?

Ich bin mir nicht 100%ig sicher, aber meiner Meinung nach dürftest du 
Probleme mit dem asin in der Angle-Methode bekommen, da soviel ich weiß 
als Argument nur Werte zwischen -1 und 1 erlaubt sind. Deine Berechnung 
(x-512.0)/102.4 mit x-Werten von 0 bis 1024 ergibt aber Werte zwischen 
-5 und 5.

Eine andere Möglichkeit wäre der AD-Wandler. Hast du mal getestet, ob 
die Werte überhaupt richtig eingelesen werden bzw. ob bei die Methode 
ReagChannel wirklich Werte zwischen 0 und 1024 herauskommen?

-------------------------------------------------------

Zwei andere Sachen, die mir bei deinem Code noch aufgefallen sind, die 
aber eigentlich nur kosmetischer bzw. optimierender Natur sind:

1. in deiner Methode PrintNumber kannst du die If-Anweisungen 
verschachteln, denn wenn du weißt, dass eine Zahl zweistellig ist, 
brauchst du ja nicht mehr kontrollieren, ob sie einstellig sein könnte.

2. in deiner ISR toggelst du counter immer zwischen 0 und 1 hin und her. 
Statt
1
counter = counter + 1; // Welches ist die nächste Stelle
2
3
if(counter == 2) // Wenn Stelle 2 erreicht, zurücksetzen
4
{
5
   counter = 0;
6
}

kannst du den Code folgendermaßen einkürzen:
1
counter++;     // oder wie bei dir: counter = counter + 1;
2
3
counter &= 0x01;   // bitweise Verknüpfung mit 1

Erklärung für die Bitverknüpfung:

hat counter den Wert 1, dann ergibt sich die Verknüfung:
0b00000001 & 0b00000001 zu 0b00000001 -> counter bleibt 1


hat counter den Wert 2, dann ergibt sich die Verknüfung:
0b00000010 & 0b00000001 zu 0b00000000 -> counter kippt auf 0 zurück

von Philipp B. (philipp_burch)


Lesenswert?

Thorsten wrote:
> 2. in deiner ISR toggelst du counter immer zwischen 0 und 1 hin und her.
> Statt
>
1
> counter = counter + 1; // Welches ist die nächste Stelle
2
> 
3
> if(counter == 2) // Wenn Stelle 2 erreicht, zurücksetzen
4
> {
5
>    counter = 0;
6
> }
7
>
>
> kannst du den Code folgendermaßen einkürzen:
>
1
> counter++;     // oder wie bei dir: counter = counter + 1;
2
> 
3
> counter &= 0x01;   // bitweise Verknüpfung mit 1
4
>

Er könnte natürlich auch gleich
counter ^= 1 schreiben ;)
Möglicherweise kannst du es aber eh schreiben wie du willst, der 
Optimierer könnte alle Varianten zum selben Ergebnis umwandeln.

von concept (Gast)


Lesenswert?

noch ne variante
1
if(counter++ == 2)
2
  counter = 0;

von concept (Gast)


Lesenswert?

wohl besser
++counter

von Crashdemon (Gast)


Lesenswert?

Am WatchDog liegt es nicht, habe ihne auskommentiert und es funktioniert 
trotzdem nicht, die sache mit dem asin stimmt es muss ein wert in dem 
bereich von +/-1 übergeben werden, allerdings ist das nicht swo wichtig, 
da der sensor sich nur im Breich der 410 - 614 Befindet, ((614,4 -512) / 
102,4 = 1).
Denn Anwandler konnte ich leider noch nicht testen, da ich noch keinen 
Quarz mit Usart Frequenz hab, mit einem 4Mhz Quarzoszillator scheint das 
nicht zu funktionieren.
Die kosmetischen Fehler habe ich behoben, allerdings zeigt alles in 
allem, immer noch nur eine Null an.

von Crashdemon (Gast)


Lesenswert?

Habe eine FAQ im Internet gefunden die meine Frage beantwortet hat, ob 
ich OP's zum angleichen benutzen muss, von 
http://www.dimensionengineering.com/accelerometers.htm
1
Impedance/buffering issues - This is by far the single most common source of problems in projects involving analog accelerometers, because so few people thoroughly read the required documentation. Both PIC and AVR datasheets specify that for A-D conversion to work properly, the connected device must have an output impedance under 10kΩ. Unfortunately, Analog Devices' analog accelerometers have an output impedance of 32kΩ. The solution to this is to use a low input offset rail to rail op amp as a buffer to lower the output impedance. As far as we know, the DE-ACCM is the only accelerometer solution that takes care of this problem for you.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.