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
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.
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
@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
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.