Skip to content

Commit abb09e8

Browse files
aykevldeadprogram
authored andcommitted
runtime, internal/task: refactor to simplify stack switching
The Cortex-M target isn't much changed, but much of the logic for the AVR stack switcher that was previously in assembly has now been moved to Go to make it more maintainable and in fact smaller in code size. Three functions (tinygo_getCurrentStackPointer, tinygo_switchToTask, tinygo_switchToScheduler) have been changed to one: tinygo_swapTask. This reduction in assembly code should make the code more maintainable and should make it easier to port stack switching to other architectures. I've also moved the assembly files to src/internal/task, which seems like a more appropriate location to me.
1 parent bb58783 commit abb09e8

File tree

9 files changed

+169
-268
lines changed

9 files changed

+169
-268
lines changed

src/internal/task/task_stack.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ func Pause() {
4545
currentTask.state.pause()
4646
}
4747

48+
//export tinygo_pause
49+
func pause() {
50+
Pause()
51+
}
52+
4853
// Resume the task until it pauses or completes.
4954
// This may only be called from the scheduler.
5055
func (t *Task) Resume() {
@@ -58,10 +63,32 @@ func (s *state) initialize(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
5863
// Create a stack.
5964
stack := make([]uintptr, stackSize/unsafe.Sizeof(uintptr(0)))
6065

66+
// Set up the stack canary, a random number that should be checked when
67+
// switching from the task back to the scheduler. The stack canary pointer
68+
// points to the first word of the stack. If it has changed between now and
69+
// the next stack switch, there was a stack overflow.
70+
s.canaryPtr = &stack[0]
71+
*s.canaryPtr = stackCanary
72+
73+
// Get a pointer to the top of the stack, where the initial register values
74+
// are stored. They will be popped off the stack on the first stack switch
75+
// to the goroutine, and will start running tinygo_startTask (this setup
76+
// happens in archInit).
77+
r := (*calleeSavedRegs)(unsafe.Pointer(&stack[uintptr(len(stack))-(unsafe.Sizeof(calleeSavedRegs{})/unsafe.Sizeof(uintptr(0)))]))
78+
6179
// Invoke architecture-specific initialization.
62-
s.archInit(stack, fn, args)
80+
s.archInit(r, fn, args)
6381
}
6482

83+
//export tinygo_swapTask
84+
func swapTask(oldStack uintptr, newStack *uintptr)
85+
86+
// startTask is a small wrapper function that sets up the first (and only)
87+
// argument to the new goroutine and makes sure it is exited when the goroutine
88+
// finishes.
89+
//go:extern tinygo_startTask
90+
var startTask [0]uint8
91+
6592
//go:linkname runqueuePushBack runtime.runqueuePushBack
6693
func runqueuePushBack(*Task)
6794

src/internal/task/task_stack_avr.S

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
.section .bss.tinygo_systemStack
2+
.global tinygo_systemStack
3+
.type tinygo_systemStack, %object
4+
tinygo_systemStack:
5+
.short 0
6+
7+
.section .text.tinygo_startTask
8+
.global tinygo_startTask
9+
.type tinygo_startTask, %function
10+
tinygo_startTask:
11+
// Small assembly stub for starting a goroutine. This is already run on the
12+
// new stack, with the callee-saved registers already loaded.
13+
// Most importantly, r2r3 contain the pc of the to-be-started function and
14+
// r4r5 contain the only argument it is given. Multiple arguments are packed
15+
// into one by storing them in a new allocation.
16+
17+
// Set the first argument of the goroutine start wrapper, which contains all
18+
// the arguments.
19+
movw r24, r4
20+
21+
// Branch to the "goroutine start" function. Note that the Z register is
22+
// call-clobbered, so does not need to be restored after use.
23+
movw Z, r2
24+
icall
25+
26+
// After return, exit this goroutine. This is a tail call.
27+
#if __AVR_ARCH__ == 2 || __AVR_ARCH__ == 25
28+
// Small memory devices (≤8kB flash) that do not have the long call
29+
// instruction availble will need to use rcall instead.
30+
// Note that they will probably not be able to run more than the main
31+
// goroutine anyway, but this file is compiled for all AVRs so it needs to
32+
// compile at least.
33+
rcall tinygo_pause
34+
#else
35+
// Other devices can (and must) use the regular call instruction.
36+
call tinygo_pause
37+
#endif
38+
39+
.global tinygo_swapTask
40+
.type tinygo_swapTask, %function
41+
tinygo_swapTask:
42+
// This function gets the following parameters:
43+
// r24:r25 = newStack uintptr
44+
// r22:r23 = oldStack *uintptr
45+
46+
// Save all call-saved registers:
47+
// https://gcc.gnu.org/wiki/avr-gcc#Call-Saved_Registers
48+
push r29 // Y
49+
push r28 // Y
50+
push r17
51+
push r16
52+
push r15
53+
push r14
54+
push r13
55+
push r12
56+
push r11
57+
push r10
58+
push r9
59+
push r8
60+
push r7
61+
push r6
62+
push r5
63+
push r4
64+
push r3
65+
push r2
66+
67+
// Save the current stack pointer in oldStack.
68+
in r2, 0x3d; SPL
69+
in r3, 0x3e; SPH
70+
movw Y, r22
71+
std Y+0, r2
72+
std Y+1, r3
73+
74+
// Switch to the new stack pointer.
75+
out 0x3d, r24; SPL
76+
out 0x3e, r25; SPH
77+
78+
// Load saved register from the new stack.
79+
pop r2
80+
pop r3
81+
pop r4
82+
pop r5
83+
pop r6
84+
pop r7
85+
pop r8
86+
pop r9
87+
pop r10
88+
pop r11
89+
pop r12
90+
pop r13
91+
pop r14
92+
pop r15
93+
pop r16
94+
pop r17
95+
pop r28 // Y
96+
pop r29 // Y
97+
98+
// Return into the new task, as if tinygo_swapTask was a regular call.
99+
ret

src/internal/task/task_stack_avr.go

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ package task
44

55
import "unsafe"
66

7+
//go:extern tinygo_systemStack
8+
var systemStack uintptr
9+
710
// calleeSavedRegs is the list of registers that must be saved and restored when
8-
// switching between tasks. Also see scheduler_avr.S that relies on the
9-
// exact layout of this struct.
11+
// switching between tasks. Also see task_stack_avr.S that relies on the exact
12+
// layout of this struct.
1013
//
1114
// https://gcc.gnu.org/wiki/avr-gcc#Call-Saved_Registers
1215
type calleeSavedRegs struct {
@@ -23,34 +26,15 @@ type calleeSavedRegs struct {
2326
pc uintptr
2427
}
2528

26-
// registers gets a pointer to the registers stored at the top of the stack.
27-
func (s *state) registers() *calleeSavedRegs {
28-
return (*calleeSavedRegs)(unsafe.Pointer(s.sp + 1))
29-
}
30-
31-
// startTask is a small wrapper function that sets up the first (and only)
32-
// argument to the new goroutine and makes sure it is exited when the goroutine
33-
// finishes.
34-
//go:extern tinygo_startTask
35-
var startTask [0]uint8
36-
3729
// archInit runs architecture-specific setup for the goroutine startup.
3830
// Note: adding //go:noinline to work around an AVR backend bug.
3931
//go:noinline
40-
func (s *state) archInit(stack []uintptr, fn uintptr, args unsafe.Pointer) {
41-
// Set up the stack canary, a random number that should be checked when
42-
// switching from the task back to the scheduler. The stack canary pointer
43-
// points to the first word of the stack. If it has changed between now and
44-
// the next stack switch, there was a stack overflow.
45-
s.canaryPtr = &stack[0]
46-
*s.canaryPtr = stackCanary
47-
32+
func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) {
4833
// Store the initial sp for the startTask function (implemented in assembly).
49-
s.sp = uintptr(unsafe.Pointer(&stack[uintptr(len(stack))-(unsafe.Sizeof(calleeSavedRegs{})/unsafe.Sizeof(uintptr(0)))])) - 1
34+
s.sp = uintptr(unsafe.Pointer(r)) - 1
5035

5136
// Initialize the registers.
5237
// These will be popped off of the stack on the first resume of the goroutine.
53-
r := s.registers()
5438

5539
// Start the function at tinygo_startTask.
5640
startTask := uintptr(unsafe.Pointer(&startTask))
@@ -67,20 +51,17 @@ func (s *state) archInit(stack []uintptr, fn uintptr, args unsafe.Pointer) {
6751
}
6852

6953
func (s *state) resume() {
70-
switchToTask(s.sp)
54+
swapTask(s.sp, &systemStack)
7155
}
7256

73-
//export tinygo_switchToTask
74-
func switchToTask(uintptr)
75-
76-
//export tinygo_switchToScheduler
77-
func switchToScheduler(*uintptr)
78-
7957
func (s *state) pause() {
80-
switchToScheduler(&s.sp)
58+
newStack := systemStack
59+
systemStack = 0
60+
swapTask(newStack, &s.sp)
8161
}
8262

83-
//export tinygo_pause
84-
func pause() {
85-
Pause()
63+
// SystemStack returns the system stack pointer when called from a task stack.
64+
// When called from the system stack, it returns 0.
65+
func SystemStack() uintptr {
66+
return systemStack
8667
}

src/runtime/scheduler_cortexm.S renamed to src/internal/task/task_stack_cortexm.S

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,6 @@ tinygo_startTask:
3030
.cfi_endproc
3131
.size tinygo_startTask, .-tinygo_startTask
3232

33-
.section .text.tinygo_getSystemStackPointer
34-
.global tinygo_getSystemStackPointer
35-
.type tinygo_getSystemStackPointer, %function
36-
tinygo_getSystemStackPointer:
37-
.cfi_startproc
38-
// The system stack pointer is always stored in the MSP register.
39-
mrs r0, MSP
40-
bx lr
41-
.cfi_endproc
42-
.size tinygo_getSystemStackPointer, .-tinygo_getSystemStackPointer
43-
4433
.section .text.tinygo_switchToScheduler
4534
.global tinygo_switchToScheduler
4635
.type tinygo_switchToScheduler, %function

src/internal/task/task_stack_cortexm.go

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
package task
44

5-
import "unsafe"
5+
import (
6+
"device/arm"
7+
"unsafe"
8+
)
69

710
// calleeSavedRegs is the list of registers that must be saved and restored when
8-
// switching between tasks. Also see scheduler_cortexm.S that relies on the
11+
// switching between tasks. Also see task_stack_cortexm.S that relies on the
912
// exact layout of this struct.
1013
type calleeSavedRegs struct {
1114
r4 uintptr
@@ -20,34 +23,15 @@ type calleeSavedRegs struct {
2023
pc uintptr
2124
}
2225

23-
// registers gets a pointer to the registers stored at the top of the stack.
24-
func (s *state) registers() *calleeSavedRegs {
25-
return (*calleeSavedRegs)(unsafe.Pointer(s.sp))
26-
}
27-
28-
// startTask is a small wrapper function that sets up the first (and only)
29-
// argument to the new goroutine and makes sure it is exited when the goroutine
30-
// finishes.
31-
//go:extern tinygo_startTask
32-
var startTask [0]uint8
33-
3426
// archInit runs architecture-specific setup for the goroutine startup.
35-
func (s *state) archInit(stack []uintptr, fn uintptr, args unsafe.Pointer) {
36-
// Set up the stack canary, a random number that should be checked when
37-
// switching from the task back to the scheduler. The stack canary pointer
38-
// points to the first word of the stack. If it has changed between now and
39-
// the next stack switch, there was a stack overflow.
40-
s.canaryPtr = &stack[0]
41-
*s.canaryPtr = stackCanary
42-
27+
func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) {
4328
// Store the initial sp for the startTask function (implemented in assembly).
44-
s.sp = uintptr(unsafe.Pointer(&stack[uintptr(len(stack))-(unsafe.Sizeof(calleeSavedRegs{})/unsafe.Sizeof(uintptr(0)))]))
29+
s.sp = uintptr(unsafe.Pointer(r))
4530

4631
// Initialize the registers.
4732
// These will be popped off of the stack on the first resume of the goroutine.
48-
r := s.registers()
4933

50-
// Start the function at tinygo_startTask (defined in src/runtime/scheduler_cortexm.S).
34+
// Start the function at tinygo_startTask (defined in src/internal/task/task_stack_cortexm.S).
5135
// This assembly code calls a function (passed in r4) with a single argument (passed in r5).
5236
// After the function returns, it calls Pause().
5337
r.pc = uintptr(unsafe.Pointer(&startTask))
@@ -75,7 +59,8 @@ func (s *state) pause() {
7559
switchToScheduler(&s.sp)
7660
}
7761

78-
//export tinygo_pause
79-
func pause() {
80-
Pause()
62+
// SystemStack returns the system stack pointer. On Cortex-M, it is always
63+
// available.
64+
func SystemStack() uintptr {
65+
return arm.AsmFull("mrs {}, MSP", nil)
8166
}

0 commit comments

Comments
 (0)