Show HN: Preemptive Multitasking on an AVR Microcontroller
github.com
github.com
After reading through it a few times I got the idea to try to write a pre-emptive scheduler myself. This was way before the internet so I just knew the concept existed, and the book didn't have anything like that in it per se. So I spent a day or two figuring out by my self how to preserve and restore the registers and flags without disturbing them.
I wrote it in Turbo Pascal, using the inline asm for the task switcher. Each "process" was just a procedure (subroutine) that ran in a loop until a global bool was set.
I decided to hook the RTC[2] for switching tasks, but nothing more. So each time-slice was fixed at 18.2ms which didn't make things terribly interactive.
Never got further, it was more of a "can I do it" type project, but boy that feeling when I saw my three "processes" running one after another without cooperation... priceless!
[1]: https://www.amazon.com/Assembly-Language-Master-Class-Press/...
[2]: https://en.wikipedia.org/wiki/Interrupt_request_%28PC_archit...
Having done this on a 68000 (Amiga) recently, I can't help but agree with this sentiment.
I should really get around writing IPC support so that I can implement services. I'm of course just messing around like a child.
For the adult approach to microkernels, do look at seL4[0]. It really is a sight to behold.
type
task_type = record
stack: array[0..1023] of byte;
eax, ebx, ecx, edx, esi, edi, ebp, esp, eip, flags: longint;
es, ds, cs, ss, fs, gs: word;
end;
task_type_ptr = ^task_type;
procedure MTaskHandler; assembler;
asm
cli
{ save vital information }
{
eax, ebx, ecx, edx, esi, edi, ebp, esp, eip, dflags: longint;
es, ds, cs, ss, fs, gs: word;
}
db $66; mov word ptr [@@tmp], ax
db $66; mov word ptr [@@tmp+4], bx
db $66; mov word ptr [@@tmp+8], cx
db $66; mov word ptr [@@tmp+12], dx
db $66; mov word ptr [@@tmp+16], si
db $66; mov word ptr [@@tmp+20], di
{ get ip }
db $66; xor ax, ax
pop ax { pop the ip from stack }
db $66; mov word ptr [@@tmp+32], ax { ip }
{ get cs }
pop dx
{ get flags }
db $66; pushf
db $66; pop ax
pop ax
db $66; mov word ptr [@@tmp+36], ax { dflags }
{ save bp and sp after the modification }
db $66; mov word ptr [@@tmp+24], bp
db $66; mov word ptr [@@tmp+28], sp
mov word ptr [@@tmp+40], es
mov word ptr [@@tmp+42], ds
mov word ptr [@@tmp+44], dx { cs }
mov word ptr [@@tmp+46], ss
{ setup own stack }
db $66; mov word ptr [@@bp], bp
db $66; mov word ptr [@@sp], sp
mov word ptr [@@ss], ss
db $66; xor bp, bp
db $66; xor sp, sp
mov sp, offset Stack + 1022
mov bp, sp
mov ax, seg Stack
mov ss, ax
{ restore data segment }
mov ax, seg @data
mov ds, ax
{ call old interrupt }
pushf
call dword ptr Int8_Save
{ save task info }
mov dx, word ptr [@@cur_t]
mov bx, dx
shl bx, 2
les di, dword ptr Tasks[bx]
{ skip stack }
add di, 1024
{ save register info }
mov cx, 13
push ds
mov ax, cs
mov ds, ax
mov si, offset @@tmp
db $66; rep movsw
pop ds
{ get next task }
mov dx, word ptr [@@cur_t]
@@task_loop:
inc dx
and dx, 7
mov bx, dx
shl bx, 2
{ load task info pointer }
les di, dword ptr Tasks[bx]
{ if valid task, go out }
mov ax, es
cmp ax, 0
jne @@got_task
{ if looped, exit }
cmp dx, word ptr [@@cur_t]
je @@exit
jmp @@task_loop
@@got_task:
{ save new task id }
mov word ptr [@@cur_t], dx
jmp @@exit
@@exit:
{ restore information }
mov dx, es
mov si, di
{ skip stack }
add si, 1024
{ save register info }
mov cx, 13
mov ds, dx
mov ax, cs
mov es, ax
mov di, offset @@tmp
db $66; rep movsw
{ restore registers from prev. task }
db $66; mov bx, word ptr [@@tmp+4]
db $66; mov cx, word ptr [@@tmp+8]
db $66; mov dx, word ptr [@@tmp+12]
db $66; mov si, word ptr [@@tmp+16]
db $66; mov di, word ptr [@@tmp+20]
db $66; mov bp, word ptr [@@tmp+24]
db $66; mov sp, word ptr [@@tmp+28]
mov es, word ptr [@@tmp+40]
mov ds, word ptr [@@tmp+42]
mov ax, word ptr [@@tmp+46]
mov ss, ax
xor ax, ax
xor ax, ax
{ set new return address and return the proper way }
mov ax, word ptr [@@tmp+36]
push ax
mov ax, word ptr [@@tmp+44]
push ax
mov ax, word ptr [@@tmp+32]
push ax
{ restore used register }
db $66; mov ax, word ptr [@@tmp]
iret
{ temporary storage }
@@tmp:
{
eax, ebx, ecx, edx, esi, edi, ebp, esp, eip, dflags: longint;
es, ds, cs, ss, fs, gs: word;
}
dd 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
dw 0, 0, 0, 0, 0, 0
{ current task }
@@cur_t:
dw 0
@@bp:
dd 0
@@sp:
dd 0
@@ss:
dw 0
end;But point taken for next time!
>> I do not see a practical use for this library, but I think it's interesting.
+100
This is the essence of hacking.
I mean, this is certainly possible when writing assembly. But I wonder if you can convince avr-gcc to do it. To have it limit its use of registers for a part of the compiled C code.
A similar problem appears with interrupt handlers in C [2]. avr-gcc is smart enough to only push/pop registers in a handler that were actually clobbered in the code. But if you call a function pointer from an interrupt handler, it will always push/pop entire CPU state since it will assume that the code the pointer points to will potentially clobber everything. This significantly increases interrupt latency.
[1] https://github.com/TurkeyMcMac/avr_threads/blob/master/avrt....
[2] https://www.avrfreaks.net/forum/calling-function-pointers-is...
Rant:
It always annoys me when I see comments like this in the source. If it is important enough to implore people not to change something then it is surely important enough to explain why.
When I saw comments like this during code reviews I always asked the author to add a comment that briefly explained the reason for the values being the way they are.
Of course this was rather rare because most developers don't write many comments in the first place.
That's a faster reaction to a code review than I used to get when I was doing them 'in real life'!
At the start we had threads, pipes between threads, serial io, interrupts.
After we finished the project, I ported the system to standard UNIX system calls. Now I can tinker on it, and get full access to GDB/prints/profilers. I use signals, timers and pipes to simulate interrupts, clock and serial ports respectively.
Because I’m targeting UNIX it’s pretty easy to get started and build little toy experiments. I haven’t gotten it to compile on AVR for 10 years. But I have been maintaining it as a playground for new ideas.
Some recent additions are resumable functions (like python generators), A lisp user space, and a performance regression test suite.