25 comments

[ 0.19 ms ] story [ 67.2 ms ] thread
From the github:

>> I do not see a practical use for this library, but I think it's interesting.

+100

This is the essence of hacking.

indeed. Yet I might have a use for this library in a project I have no practical use for.
Ahhh this reminded me of when I as a teen saved up and bought "Assembly Language Master Class"[1], the only assembly-related book I could find in my local bookstore that didn't look like a complete intro-book. I had learned a bit of asm from some articles I had downloaded from various BBSes, and wanted to take the next step.

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...

Ah found the code. Not pasting the whole thing, but here's the relevant bits of the task switching. Keep in mind this was me with a few years of programming in general under my belt so yeah, still bit of a noob.

    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 }
    ...
This reminds me how I loved reading old blogs of Turbo Pascal+inline assembly hackers. Shaped a lot of my outlooks on technology.
cool code, but oof. can you make that a link to a pastebin? my scroll-wheel finger would thank you.
Not anymore :(

But point taken for next time!

There's edit for comments.
And there’s a time limit after which that opportunity is gone.
Yes, I also found it very rewarding when my scheduler eventually worked. Saving and restoring the registers was quite tricky. I eventually decided to just save them on the thread's stack so I wouldn't have to worry about registers being clobbered by the context switching code.
>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!

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.

[0]: https://sel4.systems/About/seL4-whitepaper.pdf

I'm looking at these long series of pushes and pops [1]. You have 26 general-purpose registers plus X, Y, Z. I wonder if it would be possible to have e.g. only 4 threads and have dedicated registers for each thread, avoiding the need to store/load all of them on each context switch. This would reduce the overhead and might make this a bit more usable in the real world.

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...

Maybe one could use the option -ffixed-reg and put different threads in different compilation units with different options. Even so, one would not be able to use the standard library, since it is not compiled with the right options, and even custom functions would have to be compiled separately for each thread. It would be a lot of hassle, but it might work.
If you are gonna go through all that work with this chip in particular, you'd probably be better off putting four of them on one board. It's all kinda moot with more powerful chips available for cheaper anyway.
I wasn't planning on actually doing it, I was just thinking of how it could be done.
It's always interesting to explore how far a thing can be pushed software-wise. Copying bits is free and small AVRs are literally cents per piece in large quantities.
I think adding a custom calling convention would be the way to go.
(comment deleted)
> /* Don't change these flag values! */

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.

You're right. I have added a description. Thanks.
Much clearer.

That's a faster reaction to a code review than I used to get when I was doing them 'in real life'!

I wrote a pre-emptive, multi-threaded OS for the AVR atmega 128 back in college as a sensor networking operating system called Mantis OS (mos). For anyone interested in this direction, it also provides a device layer and “drivers” for timers, ADCs, eeprom, serial port, networking, etc. A great group of folks kept improving it for years so there is a lot there to learn from just as a resource too.

https://github.com/rosejn/mantis

I also wrote a preemptive threaded operating system for AVR atmega 128.

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.

https://github.com/andrewguy9/kernelpanic

I love it. The next level of this hack would be integrating it with the Rust/AVR work and building pseudo-threads with memory and thread safety on the platform (for no reason other than to do it).