Skip to content

Commit caf35cf

Browse files
aykevldeadprogram
authored andcommitted
esp32: implement task based scheduler
This has been a *lot* of work, trying to understand the Xtensa windowed registers ABI. But in the end I managed to come up with a very simple implementation that so far seems to work very well. I tested this with both blinky examples (with blinky2 slightly edited) and ./testdata/coroutines.go to verify that it actually works. Most development happened on the ESP32 QEMU fork from Espressif (https://github.com/espressif/qemu/wiki) but I also verified that it works on a real ESP32.
1 parent abb09e8 commit caf35cf

File tree

5 files changed

+174
-4
lines changed

5 files changed

+174
-4
lines changed

src/device/esp/esp32.S

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ call_start_cpu0:
2424
wsr.ps a2
2525
rsync
2626

27-
// Set WINDOWBASE to 1 << WINDOWSTART.
27+
// Set WINDOWSTART to 1 << WINDOWBASE.
2828
rsr.windowbase a2
2929
ssl a2
3030
movi a2, 1
@@ -43,7 +43,7 @@ call_start_cpu0:
4343
rsync
4444

4545
// Jump to the runtime start function written in Go.
46-
j main
46+
call4 main
4747

4848
.section .text.tinygo_scanCurrentStack
4949
.global tinygo_scanCurrentStack

src/internal/task/task_stack_esp32.S

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
.section .text.tinygo_startTask,"ax",@progbits
2+
.global tinygo_startTask
3+
.type tinygo_startTask, %function
4+
tinygo_startTask:
5+
// Small assembly stub for starting a goroutine. This already runs on the
6+
// new stack, control reaches this function after returning from the initial
7+
// tinygo_swapTask below (the retw.n instruction).
8+
//
9+
// The stack was set up in such a way that it looks as if this function was
10+
// paused using tinygo_swapTask by setting up the parent register window and
11+
// return pointer as a call4 instruction - except such a call never took
12+
// place. Instead, the stack pointer is switched to the new stack after all
13+
// live-but-invisible registers have been flushed to the stack. This means
14+
// that all registers as present in tinygo_swapTask are moved four up (a2 in
15+
// tinygo_swapTask is a6 in this function). We don't use any of those
16+
// registers however. Instead, the retw.n instruction will load them through
17+
// an underflow exception from the stack which means we get a0-a3 as defined
18+
// in task_stack_esp32.go.
19+
20+
// Branch to the "goroutine start" function. The first (and only) parameter
21+
// is stored in a2, but has to be moved to a6 to make it appear as a2 in the
22+
// goroutine start function (due to changing the register window by four
23+
// with callx4).
24+
mov.n a6, a2
25+
callx4 a3
26+
27+
// After return, exit this goroutine. This call never returns.
28+
call4 tinygo_pause
29+
30+
.section .text.tinygo_swapTask,"ax",@progbits
31+
.global tinygo_swapTask
32+
.type tinygo_swapTask, %function
33+
tinygo_swapTask:
34+
// This function gets the following parameters:
35+
// a2 = newStack uintptr
36+
// a3 = oldStack *uintptr
37+
38+
// Reserve 32 bytes on the stack. It really needs to be 32 bytes, with 16
39+
// extra at the bottom to adhere to the ABI.
40+
entry sp, 32
41+
42+
// Disable interrupts while flushing registers. This is necessary because
43+
// interrupts might want to use the stack pointer (at a2) which will be some
44+
// arbitrary register while registers are flushed.
45+
rsil a4, 3 // XCHAL_EXCM_LEVEL
46+
47+
// Flush all unsaved registers to the stack.
48+
// This trick has been borrowed from the Zephyr project:
49+
// https://github.com/zephyrproject-rtos/zephyr/blob/d79b003758/arch/xtensa/include/xtensa-asm2-s.h#L17
50+
and a12, a12, a12
51+
rotw 3
52+
and a12, a12, a12
53+
rotw 3
54+
and a12, a12, a12
55+
rotw 3
56+
and a12, a12, a12
57+
rotw 3
58+
and a12, a12, a12
59+
rotw 4
60+
61+
// Restore interrupts.
62+
wsr.ps a4
63+
64+
// At this point, the following is true:
65+
// WindowStart == 1 << WindowBase
66+
// Therefore, we don't need to do this manually.
67+
// It also means that the stack pointer can now be safely modified.
68+
69+
// Save a0, which stores the return address and the parent register window
70+
// in the upper two bits.
71+
s32i.n a0, sp, 0
72+
73+
// Save the current stack pointer in oldStack.
74+
s32i.n sp, a3, 0
75+
76+
// Switch to the new stack pointer (newStack).
77+
mov.n sp, a2
78+
79+
// Load a0, which is the previous return addres from before the previous
80+
// switch or the constructed return address to tinygo_startTask. This
81+
// register also stores the parent register window.
82+
l32i.n a0, sp, 0
83+
84+
// Return into the new stack. This instruction will trigger a window
85+
// underflow, reloading the saved registers from the stack.
86+
retw.n

src/internal/task/task_stack_esp32.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// +build scheduler.tasks,esp32
2+
3+
package task
4+
5+
// The windowed ABI (used on the ESP32) is as follows:
6+
// a0: return address (link register)
7+
// a1: stack pointer (must be 16-byte aligned)
8+
// a2-a7: incoming arguments
9+
// a7: stack frame pointer (optional, normally unused in TinyGo)
10+
// Sources:
11+
// http://cholla.mmto.org/esp8266/xtensa.html
12+
// https://0x04.net/~mwk/doc/xtensa.pdf
13+
14+
import (
15+
"unsafe"
16+
)
17+
18+
var systemStack uintptr
19+
20+
// calleeSavedRegs is the list of registers that must be saved and restored when
21+
// switching between tasks. Also see task_stack_esp8266.S that relies on the
22+
// exact layout of this struct.
23+
type calleeSavedRegs struct {
24+
// Registers in the register window of tinygo_startTask.
25+
a0 uintptr
26+
a1 uintptr
27+
a2 uintptr
28+
a3 uintptr
29+
30+
// Locals that can be used by tinygo_swapTask.
31+
// The first field is the a0 loaded in tinygo_swapTask, the rest is unused.
32+
locals [4]uintptr
33+
}
34+
35+
// archInit runs architecture-specific setup for the goroutine startup.
36+
func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) {
37+
// Store the stack pointer for the tinygo_swapTask function (implemented in
38+
// assembly). It needs to point to the locals field instead of a0 so that
39+
// the retw.n at the end of tinygo_swapTask will return into
40+
// tinygo_startTask with a0-a3 loaded (using the register window mechanism).
41+
s.sp = uintptr(unsafe.Pointer(&r.locals[0]))
42+
43+
// Start the goroutine at tinygo_startTask (defined in
44+
// src/internal/task/task_stack_esp32.S). The topmost two bits are not part
45+
// of the address but instead store the register window of the caller.
46+
// In this case there is no caller, instead we set up the return address as
47+
// if tinygo_startTask called tinygo_swapTask with a call4 instruction.
48+
r.locals[0] = uintptr(unsafe.Pointer(&startTask))&^(3<<30) | (1 << 30)
49+
50+
// Set up the stack pointer inside tinygo_startTask.
51+
// Unlike most calling conventions, the windowed ABI actually saves the
52+
// stack pointer on the stack to make register windowing work.
53+
r.a1 = uintptr(unsafe.Pointer(r)) + 32
54+
55+
// Store the function pointer and the (only) parameter on the stack in a
56+
// location that will be reloaded into registers when doing the
57+
// pseudo-return to tinygo_startTask using the register window mechanism.
58+
r.a3 = fn
59+
r.a2 = uintptr(args)
60+
}
61+
62+
func (s *state) resume() {
63+
swapTask(s.sp, &systemStack)
64+
}
65+
66+
func (s *state) pause() {
67+
newStack := systemStack
68+
systemStack = 0
69+
swapTask(newStack, &s.sp)
70+
}
71+
72+
// SystemStack returns the system stack pointer when called from a task stack.
73+
// When called from the system stack, it returns 0.
74+
func SystemStack() uintptr {
75+
return systemStack
76+
}

src/runtime/arch_xtensa.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
package runtime
44

5+
import "device"
6+
57
const GOARCH = "arm" // xtensa pretends to be arm
68

79
// The bitness of the CPU (e.g. 8, 32, 64).
@@ -12,4 +14,7 @@ func align(ptr uintptr) uintptr {
1214
return (ptr + 3) &^ 3
1315
}
1416

15-
func getCurrentStackPointer() uintptr
17+
func getCurrentStackPointer() uintptr {
18+
// The stack pointer (sp) is a1.
19+
return device.AsmFull("mov {}, sp", nil)
20+
}

targets/esp32.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
"inherits": ["xtensa"],
33
"cpu": "esp32",
44
"build-tags": ["esp32", "esp"],
5+
"scheduler": "tasks",
56
"linker": "xtensa-esp32-elf-ld",
7+
"default-stack-size": 2048,
68
"cflags": [
79
"-mcpu=esp32"
810
],
911
"linkerscript": "targets/esp32.ld",
1012
"extra-files": [
11-
"src/device/esp/esp32.S"
13+
"src/device/esp/esp32.S",
14+
"src/internal/task/task_stack_esp32.S"
1215
],
1316
"binary-format": "esp32",
1417
"flash-command": "esptool.py --chip=esp32 --port {port} write_flash 0x1000 {bin} -ff 80m -fm dout"

0 commit comments

Comments
 (0)