Rudimentäres Event System für AVR

#1193408
Lesenswert?

Hallo Leute,

nachdem ich jetzt schon eine Weile mit den AVR rumspiele und die 
Projekte immer grösser werden hab ich mir mal ein paar Gedanken bzgl. 
eine "Event" Systems gemacht. Dabei werden die Events an sich mittels 
eines enums (in der events.h) definiert. Hier werden auch die maximale 
Anzahl der benötigten Events festgelegt (MAXEVENTS). Danach kann man 
dann eine Funktion bei dem Event - System registrieren und wenn dann so 
ein Event (das man natürlich noch auslösen muss) eintritt, dann wird das 
von der Funktion HandleEvents() abgearbeitet. Hier mal der entsprechende 
Quellcode. Mich würde sehr Eure Meinung dazu interessieren:

events.h
1
//***********************************************************************************************************************
2
#ifndef _EVENTS_H__
3
#define _EVENTS_H__
4

5
//***********************************************************************************************************************
6
#include <inttypes.h>
7

8

9
//***********************************************************************************************************************
10
#define  MAXEVENTS    3          // Number of max. functions to be used in event system [max is 32]
11

12
//***********************************************************************************************************************
13
// EVENT enum list
14
typedef enum
15
{
16
  EVENT_0,
17
  EVENT_1,
18
  EVENT_2
19
} E_EVENTS;
20

21
// Function pointer type
22
typedef void (*pt2func)();
23

24

25
//***********************************************************************************************************************
26
// Function prototypes
27
uint8_t RegisterEvent (uint8_t event, void (*funcp));
28
void  HandleEvents ();
29
void   RaiseEvent (uint8_t  event);
30
void  ClearEvent (uint8_t  event);
31

32
#endif


events.c
1
//***********************************************************************************************************************
2
// Includes
3
#include "events.h"
4

5
//***********************************************************************************************************************
6
// Globals
7
#if   (MAXFUNCS <= 8)
8
  volatile uint8_t  EventFlagBuffer  = 0;
9
#elif (MAXFUNCS <= 16)
10
  volatile uint16_t  EventFlagBuffer  = 0;
11
#elif (MAXFUNCS <= 32)
12
  volatile uint32_t  EventFlagBuffer  = 0;
13
#else
14
  #error "TOO MANY EVENTS ... check MAXEVENTS";
15
#endif
16

17
volatile pt2func FuncArray[MAXEVENTS]  = { 0 };    // Function pointer array
18

19
//***********************************************************************************************************************
20
// Function to register an event
21
uint8_t  RegisterEvent (uint8_t event, void (*fp))
22
{
23
  if ((fp != 0) && (event < MAXEVENTS))    // Check if function pointer if not NULL or reg. event bigger than MAXEVENTS
24
  {
25
    FuncArray[event] = fp;
26
    return 0;
27
  }
28

29
  return -1;
30
}
31

32
//***********************************************************************************************************************
33
inline void ClearEvent (uint8_t  event)
34
{
35
  EventFlagBuffer &= ~(1 << event);
36
}
37

38
//***********************************************************************************************************************
39
inline void RaiseEvent (uint8_t event)
40
{
41
  EventFlagBuffer |= (1 << event);
42
}
43

44
//***********************************************************************************************************************
45
// Function to handle all active events
46
inline void HandleEvents ()
47
{
48
  // Locals
49
  uint8_t  i;
50

51
  for (i = 0; i < MAXEVENTS; i++)
52
  {
53
    if (EventFlagBuffer & (1 << i))
54
    {
55
      if (FuncArray[i] != 0)      // Check if function pointer is not NULL
56
        FuncArray[i] ();      // Call function
57
      ClearEvent (i);          // Clear the event
58
    }
59
  }
60
}


main.h
1
#ifndef _MAIN_H__
2
#define _MAIN_H__
3

4
#include <avr/io.h>
5
#include <inttypes.h>
6
#include "events.h"
7

8
#endif


und noch die main.c:
1
#include "main.h"
2

3
volatile uint8_t  test = EVENT_1;
4

5
//***********************************************************************************************************************
6
// Test function 0
7
void Test0 ()
8
{
9
  uint8_t i; 
10
  i = 3;
11
  i = i * i;
12
}
13

14
//***********************************************************************************************************************
15
// Test function 1
16
void Test1 ()
17
{
18
  uint8_t i;
19
  i = 4;
20
  i = i * i;
21
}
22

23
//***********************************************************************************************************************
24
// Test function 2
25
void Test2 ()
26
{
27
  uint8_t i;
28
  i = 4;
29
  i = i * i;
30
}
31

32
//***********************************************************************************************************************
33
// Main function
34
int main ()
35
{
36
  // Locals
37
  volatile uint8_t  dummy = 0;
38

39
  // Register EVENT_0 and EVENT_1 with their corresponding handle function
40
  RegisterEvent (EVENT_0, Test0);
41
  RegisterEvent (EVENT_1, Test1);
42
  RegisterEvent (EVENT_2, Test2);
43
  
44
  // Endless loop
45
  for (;;) 
46
  {
47
    ++dummy;              // Increment dummy var
48
  
49
    if (dummy == 30)          
50
      RaiseEvent (EVENT_0);      // Raise EVENT_0
51
    else if (dummy == 100)
52
      RaiseEvent (EVENT_1);      // Raise EVENT_1
53
    else if (dummy == 150)
54
      RaiseEvent (EVENT_2);      // Raise EVENT_1
55

56
    HandleEvents ();          // Handle all events
57
  }
58
}

Beste Grüße,
Michael
#1193430
Lesenswert?

@STK500-Besitzer: Ich möchte eigentlich nix sagen. Eher was dazu gesagt 
bekommen. In die Codesammlung hab' ich noch nicht geschoben, da ich 
dachte, dass da nur Zeugs reinkommt, was schon "approved" ist ... beim 
nächsten mal, versprochen ;-)

@Gast: Genau das hat mich bzgl. der Übersichtlichkeit immer ein wenig 
gestört ...

Grüße,
Michael
#1193462
Lesenswert?

@Gast: Das ist schon richtig. Meist aber werden die Events ja von 
beispielsweise Interrupts ausgelöst, in denen ein Flag gesetzt wird und 
in der Main wird dieses abgefragt und abgearbeitet. Dabei muss dann in 
der ausführenden Funktion dieses Flag auch wieder gelöscht werden. Bei 
größeren Projekten wurde mit das Ganze dann einfach zu unübersichtlich. 
Angenommen man hat 12 Flags die an unterschiedlichen Stellen gesetzt 
werden, dann hättest Du einen if - else if - else if ... - else Baum mit 
12 Einträgen. So steht bei mir das HandleEvents () in der main -- auf 
der anderen Seite muss ich aber auch alle Events registrieren, eben 12 
mal. Ist wohl Geschmackssache ... mir gefällt so einfach besser ...

Grüße,
Michael
Gast #1193491
Lesenswert?

> if (EventFlagBuffer & (1 << i))

>Braucht das shiften so viel Zeit ?

Ja. Da dieses Shiften zur Laufzeit gemacht werden muss(es ist ja niht 
bekannt, wieviel Bits zu schiften sind), entsteht dadurch eine Schleife.

Das hingegen:
(1<<PC4) ist eine Konstante und wird zur Compilezeit ermittelt.

(Sieh dir doch einfach mal den ASM-COde dazu an)


Ich würde Dir raten, das eher über eine Maske (im Flash) zu machen:
Das sollte einiges schneller als die Schleife sein
1
uint8_t   au8Maske[]  PROGMEM = {  0x0001,  0x0002,  0x0004,  0x0008
2
                                   0x0010,  0x0020,  0x0040,  0x0080,
3
                                   ....
4
                                   0x1000,  0x2000,  0x4000,  0x8000  );
5

6
if (  EventFlagBuffer & pgm_read_word(au8Maske[i])  )
7
...
#1193496
Lesenswert?

Statt Euch an programmtechnischen Kleinigkeiten aufzuhängen, sollte doch 
eher eine Diskussion über die Notwendigkeit eines Eventsystems auf einem 
AVR in Gang kommen. Manche Menschen fangen halt rechtzeitig an, zu 
abstrahieren und behalten später den Überblick...;-)

Aber mal im Ernst: Immer dann, wenn von einem Programmm viele Dinge 
gleichzeitig zu erledigen sind, sollte man über ein Eventsystem 
nachdenken. Wie rudimentär das sein muss, hängt dann nur noch von der 
Komplexität der Aufgabe ab.

Lohnend war es für mich beispielsweise bei der Umsetzung eine 
Bedienoberfläche, die Messwerte, Uhrzeit usw. anzeigt, die sich 
asynchron vom Vordergrundprogramm ändern. In so einer Umgebung sendet 
der Timer ein Event, das der Bildschirm neu gezeichnet werden möge, weil 
sich die Uhrzeit verändert hat. Auf diese Weise können viele unabhängige 
Prozesse das Neuzeichnen des Bildschirms veranlassen. Gleichzeitig wird 
aber auch auf Tastendrücke per Event reagiert.
#1193504
Lesenswert?

@Matthias Lipinsky: Noch ne kurze Frage: Statt uint8_t sollte es wohl 
uint16_t sein, oder?

@Eddy Current: Genau dafür mache ich das Ganze. Das Projekt ist eine 
Ansammlung von Tools für meinen Modellbaukram. Also Ladekurven plotten 
mittels der Daten von den Ladegeräten (mit Log auf SD - Karte). Dann ein 
Frequenzscanner mit externem Scan - Empfänger, Servotest, Empfängertest, 
EWD - Messung, Drehzahlmessung, ... etc. Bei den Dingen die ich vorhabe 
ist Geschwindigkeit nicht so wirklich wichtig, da meist das GLCD bedient 
werden muss. Also Controller kommt ein Mega128 zum Einsatz. Aber wenn 
man von vornherein optimieren kann dann bin ich immer sehr dankbar für 
Tipps!

Grüße,
Michael
Gast #1193507
Lesenswert?

>Noch ne kurze Frage: Statt uint8_t sollte es wohl uint16_t sein, oder?
Ja. Das war nur, um zu prüfen ob du aufpasst ;-)

>einfach vor der Schleife eine Variable mit 1 initialisieren und sie dann in >der 
Schleife mit 2 multiplizieren
Ja, wäre auch möglich. Nur statt multiplizieren, dann einfach schieben.


>In so einer Umgebung sendet der Timer ein Event, das der Bildschirm neu 
>gezeichnet werden möge, weil sich die Uhrzeit verändert hat.
Meine Philosophie ist eher andersrum: Die Bedienung/Anzeige ist 
unabhängig von der Steuerung (also der eigentlichen Aufgabe). Somit muss 
die Bedienung/Anzeige selbständig entscheiden, ob irgendwas neu 
aufgebaut werden muss..
Gast #1193521
Lesenswert?

>Dann müsste die Display Funktion..

Ja. zB bei der Uhrzeit könnte das so aussehen:
1
if ( SekundeLast != Sekunde )
2
{
3
  SekundeLast  = Sekunde;
4

5
  // Uhrzeit aktualisieren
6
}

Das kommt aus meinem Beruf. Da programmiere ich SPS. DOrt vertrete ich 
dasselbe. Somit kann der Ablauf programmiert werden, mit all seinen 
Variablen.

Unabhängig davon kann sich eine Visualisierung um die Anzeige von 
(einigen) Varaiblen kümmern. Evtl. auch Eingabemöglichkeiten bieten.
Wann nun das Display aktualisiert werden muss, ist der Steuerung, also 
dem Ablauf doch sch*egal...
#1193532
Lesenswert?

Das was du vor hast bezeichnet man in der Softwarearchitektur als 
Observer-Pattern. Dort können sich interessierte Klassen für ein 
bestimmtes Ereignis registrieren, sobald dieses Ereignis dann eintritt, 
werden die registrierten Objekte (Observer) informiert.

Hast du aber mal daran gedacht, dass die Ereignisse dann alle im 
Interrupt Kontext deiner ISR aufgerufen werden?

Beispielsweise registrierst du 20 Module darauf, dass der RTC seine 1 
Millisekunde Periode erreicht hat.

In der ISR der RTC werden nun 20 registrierte Event-Funktionen 
aufgerufen. Sagen wir mal jede braucht im Schnitt 100 Mikrosekunden zur 
Abarbeitung. Das klingt nicht viel, ABER 20 * 100 Mikrosekunden = 2 
Millisekunden.. und schon hättest du dank deines geschickten Designs 
dein System total gesprengt ...

Vielleicht habe ich dein Design, aber gerade auch nur falsch verstanden 
:-)

Du kannst mich gerne aufklären!

Gute Nacht!
#1193533
Lesenswert?

1
//***********************************************************************************************************************
2
// Function to register an event
3
uint8_t  RegisterEvent (uint8_t event, void (*fp))
4
{
5
  if ((fp != 0) && (event < MAXEVENTS))    // Check if function pointer if not NULL or reg. event bigger than MAXEVENTS
6
  {
7
    FuncArray[event] = fp;
8
    return 0;
9
  }
10

11
  return -1;
12
}
Achso... ich sehe gerade, dass du immer nur eine Funktion auf ein Event 
registrieren kannst.. stimmt das?
Persönliche Seite #1193540
Lesenswert?

Eine "-1" zurückzugeben, bei einem uint8_t als Typ sorgt meiner Meinung 
nach für Verwirrung.

Besser finde ich:
1
#define RE_ERROR ((uint8_t) -1)

Da ist die Absicht direkt raus abzulesen.

@Lippy: Meinst du, dass das dereferenzieren mit LPM aus dem Flash 
schneller ist als eine kleine Shift-Schleife? Wenn überhaupt dann nur 
ein/zwei Takte, würde ich sagen.
Gast #1193549
Lesenswert?

Deine HandleEvents funktion ist übrigens nicht interrupt-sicher. Normal 
sollte die Reihenfolge so aussehen.

1.) Interrupts sperren
2.) Flag checken -> weiter zu drei, oder raus
3.) Flag clearen
4.) Interrupts freigeben
5.) Funktion aufrufen.

Am Anfang von HandleEvents würde ich erstmal schauen ob EventBuffer 
überhaupt einen Wert hat (EventBuffer != 0). Sonst machst du da bis zu 
32 checks für nichts unter wieder nichts. ;)
#1193560
Lesenswert?

1)
Ich würde mich von dem Gedanken verabschieden, die Events zu 
nummerieren. Benutze als Identifikation gleich die Maske, das spart das 
ineffiziente Geschiebe. Du kannst ja ein paar Makros definieren, dann 
können die Events auch gleich aussagekräftige Namen bekommen:
1
#define EVENT_TIMER    (1<<0)
2
#define EVENT_KEY      (1<<1)
3
#define EVENT_DISPLAY  (1<<2)
4
...
5
RaiseEvent( EVENT_DISPLAY );
Und auch das Raisen mehrerer Events ist dann effizienter:
1
RaiseEvent( EVENT_KEY + EVENT_DISPLAY );

2)
µC-Programme sind praktisch immer starre Konstrukte, keine dynamischen. 
Schon beim Schreiben des Programms steht fest, welche Funktion zu 
welchem Event gehört. Die Funktion RegisterEvent finde ich daher 
reichlich überflüssig. Initialisiere FuncArray doch gleich mit den 
richtigen Werten. Dann kann das Array auch im Flash liegen, was 
wertvolles RAM spart.

3)
Du musst dir unbedingt mehr Gedanken zur Interruptsicherheit machen. 
Das gilt insbesondere für ClearEvent. Aber auch für RaiseEvent, wenn es 
auch außerhalb von Interrupts aufgerufen wird. Bei RaiseEvent ist es 
dann aber mit einem cli/sei-Pärchen nicht getan. Benutze am besten die 
Makros der AVR-Libc.
#1193611
Lesenswert?

Ach, noch was:

1
#if   (MAXFUNCS <= 8)
2
  volatile uint8_t  EventFlagBuffer  = 0;
3
#elif (MAXFUNCS <= 16)
4
  volatile uint16_t  EventFlagBuffer  = 0;
5
#elif (MAXFUNCS <= 32)
6
  volatile uint32_t  EventFlagBuffer  = 0;
7
#else
8
  #error "TOO MANY EVENTS ... check MAXEVENTS";
9
#endif
Das ist zwar nett gedacht, aber der restliche Code funktioniert bei 
MAXFUNCS>16 nicht. Da bedarf es schon noch mehr, wenn das alles 
"automatisch" per Präprozessor gehen soll.
(wobei eine Beschränkung auf max 16 Events das einfachste wäre, und 16 
sollten eigentlich auch immer reichen)
#1193813
Lesenswert?

@All: Erst mal vielen vielen Dank für Eure Kommentar! Hab viel gelernt !

@Sebastian B.: Richtig. Nur eine Funktion kann sich für ein Event 
registrieren.

@Simon K.: Alles klar. Ist umgebaut. Bzgl. dem shiften hab ich jetzt so 
gemacht wie STK500-Besitzer es vorgeschlagen hat. Ich initialisiere ein 
lokale Variable mit 1 und shifte diese dann pro Durchlauf immer um eine 
Stelle nach links.

@Nico: Die Handle - Funktionen erledigen normalerweise "low-priority" 
Dinge wie einen Uart String parsen bzw. die Displayausgabe. Die Sollen 
eigentlich unterbrechbar sein, damit der Rest ungestört laufen kann. Der 
Tip mit dem Check "(EventBuffer != 0)" ist klasse und schon eingebaut!

@Stefan Ernst: Das ist natürlich ein Punkt mit den #defines. Auch das 
mehrere Event gleichzeitig "geraised" werden können wäre eine gute 
Sache. Ich hatte am Anfang auch überlegt es so zu machen, aber mit den 
enums muss ich selbst einfach nicht aufpassen, ob die Events auch 
chronologisch definiert sind. Effektiver ist es mit Deinem Vorschlag ... 
keine Frage!
Bzgl. der Interruptsicherheit: Ich verstehe den Punkt, aber leider nicht 
warum es mit einem cli/sei am Anfang und Ende der Raise- bzw. ClearEvent 
Funktionen nicht getan ist? Welche Makros meinst Du genau?
Bzgl. der Präprozessorgeschichte: Habs mittlerweile fest für max. 16 
Events gemacht. MAXEVENTS kann dann Werte von 1 - 16 annehmen.

Grüße,
Michael
#1193839
Lesenswert?

Michael K. wrote:

> @Stefan Ernst: Das ist natürlich ein Punkt mit den #defines. Auch das
> mehrere Event gleichzeitig "geraised" werden können wäre eine gute
> Sache. Ich hatte am Anfang auch überlegt es so zu machen, aber mit den
> enums muss ich selbst einfach nicht aufpassen, ob die Events auch
> chronologisch definiert sind.

Der eigentliche Punkt dabei ist, aus RaiseEvent und ClearEvent die 
Konstrukte à la (1<<event) rauszubekommen, denn die sind wirklich sehr 
ineffizient.

> Bzgl. der Interruptsicherheit: Ich verstehe den Punkt, aber leider nicht
> warum es mit einem cli/sei am Anfang und Ende der Raise- bzw. ClearEvent
> Funktionen nicht getan ist?

Weil RaiseEvent sowohl außerhalb, wie auch innerhalb von Interrupts 
aufgerufen werden kann, und innerhalb eines Interrupts macht sich ein 
sei nicht so gut, um es milde auszudrücken. Die Interrupts dürfen 
daher nur dann wieder freigegeben werden, wenn sie auch vorher schon 
freigegeben waren (was auch ganz grundsätzlich ein gutes Vorgehen bei 
atomic Blöcken ist). Und genau um sowas kümmern sich die vorgefertigten 
Makros.

> Welche Makros meinst Du genau?

http://www.nongnu.org/avr-libc/user-manual/group__util__atomic.html
#1193858
Lesenswert?

@Stefan Ernst: Oooookay. Ich denke ich habs verstanden. Das mit dem sei 
in einer ISR ist natürlich "unschön".

> Der eigentliche Punkt dabei ist, aus RaiseEvent und ClearEvent die
> Konstrukte à la (1<<event) rauszubekommen, denn die sind wirklich sehr
> ineffizient.

Das stimmt. Werde mal drauf rumdenken ...

Bzgl. der atomic.h:

Vielen Dank für den Link! Hab auch schon ne Rund google gefragt, aber 
das hab ich leider nicht gefunen. Werde mit das Ganze mal zu Gemüte 
führen !

Grüße,
Michael
#1193873
Lesenswert?

> @Nico: Die Handle - Funktionen erledigen normalerweise "low-priority"
> Dinge wie einen Uart String parsen bzw. die Displayausgabe. Die Sollen
> eigentlich unterbrechbar sein, damit der Rest ungestört laufen kann.

Ja, ABER, das problem ist halt, wenn während deine Handle-Funktion läuft 
und dann der selbe Event nochmal raised wird, dann löscht ClearEvent das 
neue Event wieder. Deine handle funktion startet dadurch beim nächsten 
durchlauf nicht. Das ist in der Form eine sehr labile Konstruktion.

Nico
#1194108
Lesenswert?

Für zeitliche Events benutze ich das hier.

Beitrag "Wartezeiten effektiv (Scheduler)"


Wenn ich Flags in einem Interrupt setze, "verschwende" ich meistens ein 
ganzes Byte, das ergibt weniger Code.

In der Regel sind Interrupt-Events zu speziell, um sie zusammen zu 
fassen.
Z.B. ob die UART-FIFO ein Byte empfangen hat, hat überhaupt kein Flag, 
die Funktion kbhit() überprüft, ob beide FIFO-Indexe gleich sind.
Der Timerinterrupt wiederum incrementiert das Flag-Byte. Falls das Main 
mal länger Busy ist, geht somit kein Interrupt verloren.


Peter

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren