Softwareflags in „C“

Im Bereich der Mikrocontroller ist jedes Byte Speicher wichtig. Dies ist den meisten klar, die schon einmal ein größeres Programm auf einem PIC, AVR, ARM, usw. unterbringen wollten. Um gewisse Zustände darzustellen oder zu signalisieren braucht man manchmal nicht immer ein ganzes Byte sondern nur wenige Bit.

Unser Beispiel

In unserem Beispielprojekt haben wir nun 3 Taster, 2 DS18B20 Sensoren (digitale Temperatursensoren) an denen jede Sekunde neue Werte gelesen werden sollen und eine Displayanzeige die alle 10sek auf den neuesten Stand gebracht werden soll. In der ISR wird nur das wichtigste gemacht und alle Verarbeitungen werden im Hauptprogramm erledigt. Wir haben also nun eine ISR für die Zeitmessung und/oder eine ISR für die Taster.

Wer die etwas unschönen Möglichkeiten überspringen möchte, kann gleich bei der Möglichkeit 3 weiterlesen.

Möglichkeit 1 – Einfach aber Speicherintensiv

Wir deklarieren uns für alles eine Variable.

volatile uint8_t taster1_gedrueckt = 0;
volatile uint8_t taster2_gedrueckt = 0;
volatile uint8_t taster3_gedrueckt = 0;
volatile uint8_t ds1_lesen = 0;
volatile uint8_t ds2_lesen = 0;
volatile uint8_t display_refresh = 0;

In der ISR wird dann zum jeweiligen Zeitpunkt oder beim Auftreten die passende Variable auf 1 gesetzt und dem Hauptprogramm wird somit signalisiert, dass dieser Fall nun bearbeitet werden soll. Das kann dann z.B. so aussehen:

if(INT1)
{
    taster1_gedrueckt = 1;
}
// usw.

Das Hauptprogramm arbeitet dann die Anweisung ab und löscht die Variable wieder wenn die Arbeit beendet wurde:

if(taster1_gedrueckt == 1)
{
    //mache xy
    taster1_gedrueckt = 0;
}
// usw.

Für diese Variante brauchen wir nun also 6 Byte Arbeitsspeicher! ggf. sogar auch mehr Programmspeicher weil der Zugriff auf die Variablen nicht optimiert wird (volatile). Es ist also schön zu Programmieren nur leider verbrauchen wir viel zu viel Speicher.

Möglichkeit 2 – Speicher-sparend aber aufwändig und unübersichtlich.

Da wir aber für jede Variable nur 2 Zustände haben (1,0) benötigen wir gar nicht so viel Speicher. Wir können also mit nur einem Byte arbeiten wo jedes Bit eine bestimmte Bedeutung hat.

Hinweis: Hier sollte man nun die wichtigsten Bitoperationen kennen (& | << >> ~).

Wir deklarieren uns also eine Variable für unsere Informationen:

// Bit      Bedeutung
//  0       Taster 1 gedrückt
//  1       Taster 2 gedrückt
// usw.
volatile uint8_t my_flags = 0;

Das Verhalten des ISR ändert sich nicht. Es wird das bit gesetzt dass die jeweilige Information enthält. Dies kann man auch mit einem Makro lösen aber damit es hier etwas anschaulicher ist, habe ich das weggelassen.

 

if(INT1)
{
    my_flags |= 0x01 << 0;
}
else if(INT2)
{
    my_flags |= 0x01 << 1;
}
// usw.

Das Hauptprogramm macht nun also auch wieder genau das gleiche wie vorher.

if(my_flags & (0x01 << 1))
{
    //mache xy
    my_flags & = ~(0x01 << 0);
}</pre>
<pre>if(my_flags & (0x01 << 1))
{
    //mache xy
    my_flags & = ~(0x01 << 1);
}
// usw.

Der Vorteil ist klar im Speicher zu erkennen. Wir haben damit 5 Byte gespart dafür aber unsere Übersichtlichkeit des Quelltextes aufgegeben. Wir müssen uns nun merken, welches Bit für was steht und aufwendig Bitoperationen vornehmen.

Möglichkeit 3 – Einfach und Speicher-sparend

C bietet uns für solche Fälle ein sehr schönes Konstrukt. Nämlich die Union.

Eine Union ermöglicht es uns, mehrere Variablen zu deklarieren die sich einen Speicherplatz teilen. Dabei richtet sich der Speicherbedarf immer nach dem größten Datentypen.

Beispiel:

union
{
    uint8_t foo1;
    uint16_t foo2;
} test;

Diese Union braucht nun also 2 Byte. Ändert man foo1 so ändert man auch 1Byte in foo2! Dies hilft uns nun bei den Flags.

Wie immer, ändert sich die Aufgabe der ISR und des Hauptprogramms nicht.

Zunächst deklarieren wir unsere Union

union
{
	uint8_t all;
	struct
	{
		unsigned taster1_gedrueckt 	: 1;
		unsigned taster2_gedrueckt 	: 1;
		unsigned taster3_gedrueckt 	: 1;
		unsigned ds1_lesen 		: 1;
		unsigned ds2_lesen 		: 1;
		unsigned display_refresh 	: 1;
	}
} my_flags;

Der Schreibaufwand ist nun etwas höher bei der Deklaration aber dafür wird es danach viel einfacher.

Zur Erklärung: Wir haben eine Union mit dem Datentypen uint8_t und einem anonymes struct. In dem struct sind nun die Variablen mit : 1; versehen. Das bedeutet, dass diese Variable in dem struct nur ein einziges Bit verwendet (: 2; Würde der Variable 2 Bit zuweisen.). Es werden also insgesamt 6 Bit verbraucht. Ein struct muss aber mind. 1Byte beanspruchen! Wir erinnern uns, die Union verwendet immer den größten Datentypen. In diesem Fall sind das Struct und die Variable all gleichgroß. Also braucht die Union 1 Byte.

Wir können nun jede Variable im struct namentlich ansprechen und die jeweilige anzahl an Bits ändern. Oder wir ändern alle Bits gleichzeitig indem wir die Variable all ansprechen.

Hier jetzt die ISR:

if(INT1)
{
    my_flags.taster1_gedrueckt = 1;
}
// usw.

und das Hauptprogramm:

<pre>if(my_flags.taster1_gedrueckt == 1)
{
    //mache xy
    my_flags.taster1_gedrueckt = 0;
}
// usw.

Zu beachten ist jetzt noch, dass unsere Union keine definierten Werte hat wenn der Controller startet. Sicherheitshalber sollte man also alle Bits vorher auf die gewünschten Startwerte setzen. z.B. so:

[Code lang=“c“]my_flags.all = 0x00;[/code]

Mit all kann man nun also alle Flags abfragen und setzen. Man kann natürlich auch nur das struct verwenden welche eine einzelne Bitzuweisung hat, aber dann fällt die Möglichkeit weg, alle Bits gleichzeitig ansprechen zu können. Aber es gibt noch etwas zu beachten. Sollte man im struct nun mehr als 8 Bit zuweisen sollte man den Datentypen von all anpassen. Ansonsten kann man nicht mehr alle Bits zuweisen oder abfragen.

Fazit:

Möglichkeit 3 ist eine sehr schöne Methode um sich Softwareflags zu bauen. Sie ist übersichtlich und verbraucht wenig Speicher. Der Compiler übernimmt die Arbeit der Bitoperation (sofern dies notwendig ist).

Schreib einen Kommentar