From 098f90036317ddd9f1df1ed8c49fde871dc51895 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sun, 8 Nov 2020 21:59:47 +0100 Subject: [PATCH] esp8266: implement task based scheduler I have chosed to call this implementation `esp8266` instead of `xtensa` as it has been written specifically for the ESP8266 and there are no other Xtensa chips with the CALL0 ABI (no windowing) that I know of. The only other related chip is the ESP32, which does implement register windowing and thus needs a very different implementation. --- src/internal/task/task_stack_esp8266.S | 54 +++++++++++++++++++ src/internal/task/task_stack_esp8266.go | 70 +++++++++++++++++++++++++ targets/esp8266.json | 5 +- 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 src/internal/task/task_stack_esp8266.S create mode 100644 src/internal/task/task_stack_esp8266.go diff --git a/src/internal/task/task_stack_esp8266.S b/src/internal/task/task_stack_esp8266.S new file mode 100644 index 00000000..e7334edc --- /dev/null +++ b/src/internal/task/task_stack_esp8266.S @@ -0,0 +1,54 @@ +.section .text.tinygo_startTask,"ax",@progbits +.global tinygo_startTask +.type tinygo_startTask, %function +tinygo_startTask: + // Small assembly stub for starting a goroutine. This is already run on the + // new stack, with the callee-saved registers already loaded. + // Most importantly, r4 contains the pc of the to-be-started function and r5 + // contains the only argument it is given. Multiple arguments are packed + // into one by storing them in a new allocation. + + // Set the first argument of the goroutine start wrapper, which contains all + // the arguments. + mov.n a2, a13 + + // Branch to the "goroutine start" function. + callx0 a12 + + // After return, exit this goroutine. This is a tail call. + call0 tinygo_pause +.size tinygo_startTask, .-tinygo_startTask + +.global tinygo_swapTask +.type tinygo_swapTask, %function +tinygo_swapTask: + // This function gets the following parameters: + // a2 = newStack uintptr + // a3 = oldStack *uintptr + // Note: + // a0 is the return address + // a1 is the stack pointer (sp) + + // Save all callee-saved registers: + addi sp, sp, -20 + s32i.n a12, sp, 0 + s32i.n a13, sp, 4 + s32i.n a14, sp, 8 + s32i.n a15, sp, 12 + s32i.n a0, sp, 16 + + // Save the current stack pointer in oldStack. + s32i.n sp, a3, 0 + + // Switch to the new stack pointer. + mov.n sp, a2 + + // Load state from new task and branch to the previous position in the + // program. + l32i.n a12, sp, 0 + l32i.n a13, sp, 4 + l32i.n a14, sp, 8 + l32i.n a15, sp, 12 + l32i.n a0, sp, 16 + addi sp, sp, 20 + ret.n diff --git a/src/internal/task/task_stack_esp8266.go b/src/internal/task/task_stack_esp8266.go new file mode 100644 index 00000000..5c9442f5 --- /dev/null +++ b/src/internal/task/task_stack_esp8266.go @@ -0,0 +1,70 @@ +// +build scheduler.tasks,esp8266 + +package task + +// Stack switch implementation for the ESP8266, which does not use the windowed +// ABI of Xtensa. Registers are assigned as follows: +// a0: return address (link register) +// a1: stack pointer (must be 16-byte aligned) +// a2-a7: incoming arguments +// a8: static chain (unused) +// a12-a15: callee-saved +// a15: stack frame pointer (optional, unused) +// Sources: +// http://cholla.mmto.org/esp8266/xtensa.html +// https://0x04.net/~mwk/doc/xtensa.pdf + +import "unsafe" + +var systemStack uintptr + +// calleeSavedRegs is the list of registers that must be saved and restored when +// switching between tasks. Also see task_stack_esp8266.S that relies on the +// exact layout of this struct. +type calleeSavedRegs struct { + a12 uintptr + a13 uintptr + a14 uintptr + a15 uintptr + + pc uintptr // also link register or r0 +} + +// archInit runs architecture-specific setup for the goroutine startup. +func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) { + // Store the initial sp for the startTask function (implemented in assembly). + s.sp = uintptr(unsafe.Pointer(r)) + + // Initialize the registers. + // These will be popped off of the stack on the first resume of the goroutine. + + // Start the function at tinygo_startTask (defined in + // src/internal/task/task_stack_esp8266.S). + // This assembly code calls a function (passed in a12) with a single argument + // (passed in a13). After the function returns, it calls Pause(). + r.pc = uintptr(unsafe.Pointer(&startTask)) + + // Pass the function to call in a12. + // This function is a compiler-generated wrapper which loads arguments out of a struct pointer. + // See createGoroutineStartWrapper (defined in compiler/goroutine.go) for more information. + r.a12 = fn + + // Pass the pointer to the arguments struct in a13. + r.a13 = uintptr(args) +} + +func (s *state) resume() { + swapTask(s.sp, &systemStack) +} + +func (s *state) pause() { + newStack := systemStack + systemStack = 0 + swapTask(newStack, &s.sp) +} + +// SystemStack returns the system stack pointer when called from a task stack. +// When called from the system stack, it returns 0. +func SystemStack() uintptr { + return systemStack +} diff --git a/targets/esp8266.json b/targets/esp8266.json index 8e4c9aad..c0c7ad3f 100644 --- a/targets/esp8266.json +++ b/targets/esp8266.json @@ -2,13 +2,16 @@ "inherits": ["xtensa"], "cpu": "esp8266", "build-tags": ["esp8266", "esp"], + "scheduler": "tasks", "linker": "xtensa-esp32-elf-ld", + "default-stack-size": 2048, "cflags": [ "-mcpu=esp8266" ], "linkerscript": "targets/esp8266.ld", "extra-files": [ - "src/device/esp/esp8266.S" + "src/device/esp/esp8266.S", + "src/internal/task/task_stack_esp8266.S" ], "binary-format": "esp8266", "flash-command": "esptool.py --chip=esp8266 --port {port} write_flash 0x00000 {bin} -fm qio"