Forum: Projekte & Code 'multithreading' auf dem AVR


von Harald F. (snuggles)


Angehängte Dateien:

Lesenswert?

Ich habe mir mal den Spaß gemacht und ein bischen Assemblercode 
geschrieben um einen zweiten Thread auf dem AVR zu starten und
schedulen zu können.
Mein Gedanke war, den Webserver von Ulrich Radig als Hauptthread
laufen zu lassen und gleichzeitig einen zweiten Thread abzuzweigen
um meine Daten von der Heizung zusammenzusammeln, in's eeprom
zu speichern und auf serial line auszugeben - ohne daß ich mich
da großartig in den Webserver code mit reinhacken müßte.

Der Code ist getested und funktioniert für meine Boards (NET-IO Board 
von Pollin; Evaluation Board ATMega32 von Pollin; selbstgebastelte 
Verdrahtung mit einem ATMega32 auf Steckboard) einwandfrei.

Es wäre schön, wenn mal der eine oder andere diesen Code anschaut
und mir vielleicht noch ein paar Tipps gibt, eg. wie man diese
ganze Latte an push/pop Befehlen eleganter lösen könnte oder den
Code noch ein bischen eindampfen könnte...

Danke, Snuggles

von Harald F. (snuggles)


Lesenswert?

Hier eine kleine Anleitung auf deutsch (im Code steht's in Englisch):

Diese Zeilen müssen in den C Code mit rein:

    extern volatile unsigend char thread_id;
    extern void thread_switch(void);
    extern void thread_start(void(*thread_function)(void),
                             unsigned char *stack,
                             unsigned int stacksize);
    extern void thread_lock();
    extern void thread_unlock();

Dann wird natürlich noch eine Thread Funktion gebraucht. Z.B so:

    void mysecondthread(void)
    {
        unsigned char counter = 60;
        printf("second thread started\r\n");
        while(counter--) {
            sleep_1_second();
            printf("second thread is still active\r\n");
        }
        printf("second thread terminates\r\n");
    }

Dann wird noch ein bischem Memory für den Stack gebraucht.
Z.B. statisch:
    static unsigned char mystack[256];
geht aber auch dynamisch, z.B. mit malloc():
    unsigned char *mystack = malloc(256);
Wichtig ist nur, daß der stack während der Laufzeit des zweiten
Threads auch vorhanden ist und natürlich groß genug ist.

Der Thread wird dann mit thread_start() gestartet:

    ...
    thread_start(mysecondthread, mystack, sizeof(mystack));
    ...

jetzt ist der Thread aktiv und läuft solange, bis die Thread
Funktion returned oder mit thread_switch() umgeschaltet wird.
Jeder Aufruf von thread_switch() legt den gerade aktiven Thread
'schlafen' und macht den anderen Thread 'aktiv'. Diese Funktion
macht also z.B. im Timer Interrupt Sinn:

    volatile uint16_t time_ms;
    volatile uint32_t time_s;

    void timer_init()
    {
          // stop timer
       TCCR0 = 0;
       // clean time_s, time_ms
       time_s = 0;
       time_ms = 0;
       // set timer 0 value to 0
       TCNT0 = 0;
       // set compare register to 250
       OCR0 = 250;
       // set prescaller for timer 0,1 to CK/64
       // set clear-timer-on-compare-match on
       TCCR0 = (1<<CS01) | (1<<CS00) | (1<<WGM01);
       // now we get every 16000000/(250*64)=1000 CK a Compare Match
       // this is exact every Millisecond
       // enable Timer 0 Compare Match Interrupt
       TIMSK |= (1<<OCIE0);
    }

    ISR(TIMER0_COMP_vect)
    {
       time_ms++;
       if(time_ms >= 1000) {
         time_ms = 0;
         time_s++;
       }
       // switch threads (does no harm if second
             // thread is not running)
       thread_switch();
    }

Aber thread_switch() muß nicht zwingend im Interrupt Kontext aufgerufen 
werden.

Der ganze Code ist darauf ausgelegt, genau einen 'zweiten' Thread zu 
ermöglichen. Der Code ist klein gehalten und braucht nur 3 Bytes für
die interne Verwaltung. thread_start() macht einfach nix, wenn der
zweite Thread bereits läuft. Ein neuer 'zweiter' Thread kann erst
gestarted werden, wenn der vorherige sich beendet hat. Ebenso macht
thread_switch() einfach nichts, wenn kein zweiter Thread läuft.

Mit multithreaded Programmierung muß man sich auch immer ein paar 
Gedanken um gemeinsam genutzte Resourcen machen. Bei 16 Bit IO 
Operationen oder Zugriff auf globale Variablen muß entsprechend ein 
bischen mehr Aufwand getrieben werden. Eine simple Lösung ist schlicht 
und einfach mal schnell die Interrupts während des Zugriffs auszumachen. 
Intelligentere Lösungen sind Mutexe, Semaphoren. Hier habe ich lediglich 
thread_lock() und thread_unlock() implementiert. Die Benutzung ist 
einfach. z.B. wird bei einer von beiden Threads genutzten Variablen 
jeder Zugriff auf diese Variable von thread_lock/thread_unlock umrundet:

    int mycommonvalue = 0;

    ...
    thread_lock();
    mycommonvalue++;
    thread_unlock();
    ...

geht natürlich auch mit Funktionen. Z.b. wenn beide Threads 
'gleichzeitig' auf serial line was ausgeben, kommt nur Buchstabensalat 
raus weil thread_switch() im Timerinterrupt die Threads währenddessen 
hin und her schaltet. Werden die printf() Aufrufe mit thread_lock() und 
thread_unlock() umrundet, ist die Ausgabe auf UART wieder lesbar.

Falls noch Fragen auftauchen, hier reinposten.

von Roland P. (pram)


Angehängte Dateien:

Lesenswert?

sehr interessant, ich hab einen ähnlichen Ansatz in C programmiert.
(allerdings noch nicht fertig, ich stell den Codeschnipsel trotzdem 
gleich mal zur Diskussion rein)

1
void process1() {
2
  for(;;) {
3
    ... mach was
4
    scheduler_sleep(10);  
5
  }
6
}
7
8
void process2() {
9
  for(;;) {
10
    ... mach was
11
    scheduler_sleep(10);  
12
  }
13
}
14
15
int main() {
16
  ... Timer initialisieren
17
  scheduler_create_task(128, process1);
18
  scheduler_create_task(128, process2);
19
  scheduler_run();
20
}
21
22
23
SIGNAL(TIMER2_OVF_vect) {
24
  scheduler_time_tick();
25
}

Bei mir muss jeder Prozess den Prozessor aktiv abgeben (somit brauch ich 
schon mal keine Locks). Bei der Abgabe kann er auch sagen, für wieviele 
Time-Ticks er aussetzen will (ich gehe hier ggf. in den Sleepmode)

Kommentare zu deinem folgen gleich...

Gruß
Roland

von Harald F. (snuggles)


Lesenswert?

@Roland
Wenn du dir das thread_switch() mal genauer anschaust - es muß nicht in 
einer Interrupt Routine aufgerufen werden. Kann genausogut auch z.B vom 
Hauptsthread aufgerufen werden, wenn der erkennt, daß gerade nichts zu 
tun ist. Ich habe bewußt diese Routine so geschrieben, daß ein Interrupt 
Kontext nicht zwingend ist. Für ein 'gerechtes' Scheduling macht 
natürlich z.B. ein Aufruf im Timer interrupt Sinn. Aber z.B. Ulrich 
Radigs Webserver könnte immer dann, wenn nichts zu tun ist, 
thread_switch() aufrufen und so dem 'zweiten Thread' praktisch die 
gesamte Rechenzeit zur Verfügung stellen, wenn der Webserver/TCPIP 
Stack/Telnetserver/... nichts zu tun haben. Wenn man genau weis was man 
tut sind locks nicht notwendig z.B. wenn thread_switch() 'per Hand' 
aufgerufen wird.

Das ganze in Assembler zu schreiben hat natürlich den Hintergedanken, 
möglichst kompakten Code zu schreiben. Der Webserver + einige Dienste + 
ein bischen an eigenen Code sorgen an sich ja schon für ein ziemlich 
volles Flash und auch RAM und deshalb habe ich auch nur eine 
Implementierung für einen 'zweiten' Thread gewählt. Richtiges 
Multithreading kommt z.B. mit mork's OS daher (siehe 'Multithreading-OS 
für AVRs') - sehr interresant übrigens.

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.