@ -*- asm -*- @ Einkornix: simple cooperative task switching operating @ system for ARM EABI. .syntax unified .thumb .fpu fpv4-sp-d16 .cpu cortex-m4 @ An eintask is an 8-byte chunk of RAM. Its first word holds @ the thread's stack pointer. Its second word points to the @ next task context. All the eintasks form a circularly @ linked list linked by their second words. There is a @ statically allocated word of memory called @ `current_task_pointer` which points to the eintask for @ the currently running task. @ The core task-switching primitive is called `einyield`. @ It permits the other tasks to run. Normally it returns to a @ point where the other task called `einyield`, so it restores @ all the callee-saved registers to what they were in that task @ at the time, but when a new task starts up, it might instead @ be at a function entry point, in which case it only cares @ about its arguments. (Hopefully it only takes one, because @ that's all we give it.) Restoring the extra register r0 in @ all cases seems wasteful, but we have to maintain the 8-byte @ alignment of the stack pointer anyway. .thumb_func .globl einyield einyield: push {r0, r4-r11, lr} @ save all callee-saved regs plus r0 ldr r0, =current_task_pointer ldr r1, [r0] str sp, [r1] @ save stack pointer in current eintask ldr r1, [r1, #4] @ load pointer to next eintask str r1, [r0] ldr sp, [r1] @ switch to next eintask's stack pop {r0, r4-r11, pc} @ return into einyielded context there .data current_task_pointer: .word 0 .text @ `einstart` is a subroutine that can be called at startup @ from the main thread to enable the use of einyield above. @ r0 should point to an 8-byte uninitialized eintask. @ Probably `einstart`, `einspawn`, and `einexit` should be @ rewritten in C. Untested C rewrites are provided in @ comments. .thumb_func .globl einstart einstart: str r0, [r0, #4] @ initialize circularly linked list ldr r1, =current_task_pointer str r0, [r1] bx lr @ void einstart(eintask *me) @ { @ current_task_pointer = me->next = me; @ } @ `einspawn` is a subroutine to spawn a new thread. r0 again @ should point to an 8-byte eintask, but this time its @ first word needs to be initialized to point to the first @ address past the end of a stack space for the new thread, @ which should be 8-byte aligned to comply with the EABI. r1 @ should be the pc to start execution at. Normally you want @ this to be the entry point to a function. r2 is an argument @ to pass to it at startup. .thumb_func .globl einspawn einspawn: ldr r3, [r0] @ load new thread stack pointer stmdb r3!, {r1} @ push desired pc as return address, then stmdb r3!, {r2, r4-r11} @ push arg and trash for the other registers str r3, [r0] @ and save the new stack pointer ldr r2, =current_task_pointer ldr r2, [r2] @ fetch the pointer to the current eintask ldr r3, [r2, #4] @ current eintask's next-pointer str r3, [r0, #4] @ becomes new eintask's next-pointer str r0, [r2, #4] @ and new eintask becomes next task bx lr @ but we don't yield to it automatically @ void einspawn(eintask *kid, void (*start)(void*), void *userdata) @ { @ kid->stack -= 10; // this assumes redeclaring stack as, say, void** @ kid->stack[9] = start; @ kid->stack[0] = userdata; @ kid->next = current_task_pointer->next; @ current_task_pointer->next = kid; @ } @ Finally, if we spawn threads at runtime, we likely want to end @ them too, so `einexit` handles that. Reusing the eintask and @ stack space is up to you. This subroutine is inefficient if @ there are many threads because we don't have a doubly-linked @ list. .thumb_func .globl einexit einexit: ldr r0, =current_task_pointer ldr r0, [r0] @ load the pointer to the actual eintask mov r1, r0 @ start searching for our predecessor 1: ldr r2, [r1, #4] @ what is r1's successor? cmp r2, r0 @ is it us? beq 1f @ if equal, r1 is our predecessor mov r1, r2 @ if not, try the next eintask b 1b 1: ldr r2, [r0, #4] @ our successor eintask str r2, [r1, #4] @ now replaces us in the task list b einyield @ now that we aren't in the list, yield @ void einexit() @ { @ eintask *me = current_task_pointer, *p = me->next; @ while (p->next != me) p = p->next; @ p->next = me->next; @ einyield(); @ }