mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-06 20:13:40 +00:00
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.
This commit is contained in:
committed by
Ron Evans
parent
bb58783158
commit
abb09e869e
@@ -1,189 +0,0 @@
|
||||
.section .bss.tinygo_systemStack
|
||||
.global tinygo_systemStack
|
||||
.type tinygo_systemStack, %object
|
||||
tinygo_systemStack:
|
||||
.short 0
|
||||
|
||||
.section .text.tinygo_startTask
|
||||
.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, r2r3 contain the pc of the to-be-started function and
|
||||
// r4r5 contain 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.
|
||||
movw r24, r4
|
||||
|
||||
// Branch to the "goroutine start" function. Note that the Z register is
|
||||
// call-clobbered, so does not need to be restored after use.
|
||||
movw Z, r2
|
||||
icall
|
||||
|
||||
// After return, exit this goroutine. This is a tail call.
|
||||
#if __AVR_ARCH__ == 2 || __AVR_ARCH__ == 25
|
||||
// Small memory devices (≤8kB flash) that do not have the long call
|
||||
// instruction availble will need to use rcall instead.
|
||||
// Note that they will probably not be able to run more than the main
|
||||
// goroutine anyway, but this file is compiled for all AVRs so it needs to
|
||||
// compile at least.
|
||||
rcall tinygo_pause
|
||||
#else
|
||||
// Other devices can (and must) use the regular call instruction.
|
||||
call tinygo_pause
|
||||
#endif
|
||||
|
||||
// Get the system stack pointer, independent of whether we're currently on the
|
||||
// system stack or a task stack.
|
||||
.global tinygo_getSystemStackPointer
|
||||
.type tinygo_getSystemStackPointer, %function
|
||||
tinygo_getSystemStackPointer:
|
||||
// Load system stack pointer.
|
||||
lds r24, tinygo_systemStack
|
||||
lds r25, tinygo_systemStack+1
|
||||
|
||||
// Compare against 0.
|
||||
cp r24, r1
|
||||
cpc r25, r1
|
||||
|
||||
// Branch (and then return) if tinygo_systemStack has a non-zero value.
|
||||
brne 1f
|
||||
|
||||
// tinygo_systemStack is zero, so return the current stack pointer.
|
||||
in r24, 0x3d; SPL
|
||||
in r25, 0x3e; SPH
|
||||
|
||||
1:
|
||||
ret
|
||||
|
||||
.global tinygo_switchToTask
|
||||
.type tinygo_switchToTask, %function
|
||||
tinygo_switchToTask:
|
||||
// The sp parameter is the only parameter, so it will take up r24:r25.
|
||||
// r24:r25 = sp uintptr
|
||||
|
||||
// Save all call-saved registers:
|
||||
// https://gcc.gnu.org/wiki/avr-gcc#Call-Saved_Registers
|
||||
push r29 // Y
|
||||
push r28 // Y
|
||||
push r17
|
||||
push r16
|
||||
push r15
|
||||
push r14
|
||||
push r13
|
||||
push r12
|
||||
push r11
|
||||
push r10
|
||||
push r9
|
||||
push r8
|
||||
push r7
|
||||
push r6
|
||||
push r5
|
||||
push r4
|
||||
push r3
|
||||
push r2
|
||||
|
||||
// Save the system stack pointer in a global.
|
||||
in r2, 0x3d; SPL
|
||||
in r3, 0x3e; SPH
|
||||
sts tinygo_systemStack+0, r2
|
||||
sts tinygo_systemStack+1, r3
|
||||
|
||||
// Switch to the task stack pointer.
|
||||
out 0x3d, r24; SPL
|
||||
out 0x3e, r25; SPH
|
||||
|
||||
// Load saved register from the task stack.
|
||||
pop r2
|
||||
pop r3
|
||||
pop r4
|
||||
pop r5
|
||||
pop r6
|
||||
pop r7
|
||||
pop r8
|
||||
pop r9
|
||||
pop r10
|
||||
pop r11
|
||||
pop r12
|
||||
pop r13
|
||||
pop r14
|
||||
pop r15
|
||||
pop r16
|
||||
pop r17
|
||||
pop r28 // Y
|
||||
pop r29 // Y
|
||||
|
||||
// Return into the new task, as if tinygo_switchToScheduler was a regular
|
||||
// call.
|
||||
ret
|
||||
|
||||
.global tinygo_switchToScheduler
|
||||
.type tinygo_switchToScheduler, %function
|
||||
tinygo_switchToScheduler:
|
||||
// The sp parameter is the only parameter, so it will take up r24:r25.
|
||||
// r24:r25 = sp *uintptr
|
||||
|
||||
// Save all call-saved registers on the task stack:
|
||||
// https://gcc.gnu.org/wiki/avr-gcc#Call-Saved_Registers
|
||||
push r29 // Y
|
||||
push r28 // Y
|
||||
push r17
|
||||
push r16
|
||||
push r15
|
||||
push r14
|
||||
push r13
|
||||
push r12
|
||||
push r11
|
||||
push r10
|
||||
push r9
|
||||
push r8
|
||||
push r7
|
||||
push r6
|
||||
push r5
|
||||
push r4
|
||||
push r3
|
||||
push r2
|
||||
|
||||
// Save the task stack.
|
||||
in r2, 0x3d; SPL
|
||||
in r3, 0x3e; SPH
|
||||
movw Y, r24
|
||||
std Y+0, r2
|
||||
std Y+1, r3
|
||||
|
||||
// Switch to the system stack.
|
||||
lds r2, tinygo_systemStack
|
||||
lds r3, tinygo_systemStack+1
|
||||
out 0x3d, r2; SPL
|
||||
out 0x3e, r3; SPH
|
||||
|
||||
// Clear tinygo_systemStack to make sure tinygo_getSystemStackPointer knows
|
||||
// which pointer to return.
|
||||
sts tinygo_systemStack+0, r1
|
||||
sts tinygo_systemStack+1, r1
|
||||
|
||||
// Load saved register from the system stack.
|
||||
pop r2
|
||||
pop r3
|
||||
pop r4
|
||||
pop r5
|
||||
pop r6
|
||||
pop r7
|
||||
pop r8
|
||||
pop r9
|
||||
pop r10
|
||||
pop r11
|
||||
pop r12
|
||||
pop r13
|
||||
pop r14
|
||||
pop r15
|
||||
pop r16
|
||||
pop r17
|
||||
pop r28 // Y
|
||||
pop r29 // Y
|
||||
|
||||
// Return into the scheduler, as if tinygo_switchToTask was a regular call.
|
||||
ret
|
||||
@@ -1,138 +0,0 @@
|
||||
// Only generate .debug_frame, don't generate .eh_frame.
|
||||
.cfi_sections .debug_frame
|
||||
|
||||
.section .text.tinygo_startTask
|
||||
.global tinygo_startTask
|
||||
.type tinygo_startTask, %function
|
||||
tinygo_startTask:
|
||||
.cfi_startproc
|
||||
// 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.
|
||||
|
||||
// Indicate to the unwinder that there is nothing to unwind, this is the
|
||||
// root frame. It avoids the following (bogus) error message in GDB:
|
||||
// Backtrace stopped: previous frame identical to this frame (corrupt stack?)
|
||||
.cfi_undefined lr
|
||||
|
||||
// Set the first argument of the goroutine start wrapper, which contains all
|
||||
// the arguments.
|
||||
mov r0, r5
|
||||
|
||||
// Branch to the "goroutine start" function. By using blx instead of bx,
|
||||
// we'll return here instead of tail calling.
|
||||
blx r4
|
||||
|
||||
// After return, exit this goroutine. This is a tail call.
|
||||
bl tinygo_pause
|
||||
.cfi_endproc
|
||||
.size tinygo_startTask, .-tinygo_startTask
|
||||
|
||||
.section .text.tinygo_getSystemStackPointer
|
||||
.global tinygo_getSystemStackPointer
|
||||
.type tinygo_getSystemStackPointer, %function
|
||||
tinygo_getSystemStackPointer:
|
||||
.cfi_startproc
|
||||
// The system stack pointer is always stored in the MSP register.
|
||||
mrs r0, MSP
|
||||
bx lr
|
||||
.cfi_endproc
|
||||
.size tinygo_getSystemStackPointer, .-tinygo_getSystemStackPointer
|
||||
|
||||
.section .text.tinygo_switchToScheduler
|
||||
.global tinygo_switchToScheduler
|
||||
.type tinygo_switchToScheduler, %function
|
||||
tinygo_switchToScheduler:
|
||||
.cfi_startproc
|
||||
// r0 = sp *uintptr
|
||||
|
||||
// Currently on the task stack (SP=PSP). We need to store the position on
|
||||
// the stack where the in-use registers will be stored.
|
||||
mov r1, sp
|
||||
subs r1, #36
|
||||
str r1, [r0]
|
||||
|
||||
b tinygo_swapTask
|
||||
.cfi_endproc
|
||||
.size tinygo_switchToScheduler, .-tinygo_switchToScheduler
|
||||
|
||||
.section .text.tinygo_switchToTask
|
||||
.global tinygo_switchToTask
|
||||
.type tinygo_switchToTask, %function
|
||||
tinygo_switchToTask:
|
||||
.cfi_startproc
|
||||
// r0 = sp uintptr
|
||||
|
||||
// Currently on the scheduler stack (SP=MSP). We'll have to update the PSP,
|
||||
// and then we can invoke swapTask.
|
||||
msr PSP, r0
|
||||
|
||||
b.n tinygo_swapTask
|
||||
.cfi_endproc
|
||||
.size tinygo_switchToTask, .-tinygo_switchToTask
|
||||
|
||||
.section .text.tinygo_swapTask
|
||||
.global tinygo_swapTask
|
||||
.type tinygo_swapTask, %function
|
||||
tinygo_swapTask:
|
||||
.cfi_startproc
|
||||
// This function stores the current register state to the stack, switches to
|
||||
// the other stack (MSP/PSP), and loads the register state from the other
|
||||
// stack. Apart from saving and restoring all relevant callee-saved
|
||||
// registers, it also ends with branching to the last program counter (saved
|
||||
// as the lr register, to follow the ARM calling convention).
|
||||
|
||||
// On pre-Thumb2 CPUs (Cortex-M0 in particular), registers r8-r15 cannot be
|
||||
// used directly. Only very few operations work on them, such as mov. That's
|
||||
// why the higher register values are first stored in the temporary register
|
||||
// r3 when loading/storing them.
|
||||
// It is possible to reduce the swapTask by two instructions (~2 cycles) on
|
||||
// Cortex-M0 by reordering the layout of the pushed registers from {r4-r11,
|
||||
// lr} to {r8-r11, r4-r8, lr}. However, that also requires a change on the
|
||||
// Go side (depending on thumb1/thumb2!) and so is not really worth the
|
||||
// complexity.
|
||||
|
||||
// Store state to old task. It saves the lr instead of the pc, because that
|
||||
// will be the pc after returning back to the old task (in a different
|
||||
// invocation of swapTask).
|
||||
#if defined(__thumb2__)
|
||||
push {r4-r11, lr}
|
||||
.cfi_def_cfa_offset 9*4
|
||||
#else
|
||||
mov r0, r8
|
||||
mov r1, r9
|
||||
mov r2, r10
|
||||
mov r3, r11
|
||||
push {r0-r3, lr}
|
||||
.cfi_def_cfa_offset 5*4
|
||||
push {r4-r7}
|
||||
.cfi_def_cfa_offset 9*4
|
||||
#endif
|
||||
|
||||
// Switch the stack. This could either switch from PSP to MSP, or from MSP
|
||||
// to PSP. By using an XOR (eor), it will just switch to the other stack.
|
||||
mrs r0, CONTROL // load CONTROL register
|
||||
movs r3, #2
|
||||
eors r0, r0, r3 // flip the SPSEL (active stack pointer) bit
|
||||
msr CONTROL, r0 // store CONTROL register
|
||||
isb // required to flush the pipeline
|
||||
|
||||
// Load state from new task and branch to the previous position in the
|
||||
// program.
|
||||
#if defined(__thumb2__)
|
||||
pop {r4-r11, pc}
|
||||
#else
|
||||
pop {r4-r7}
|
||||
.cfi_def_cfa_offset 5*9
|
||||
pop {r0-r3}
|
||||
.cfi_def_cfa_offset 1*9
|
||||
mov r8, r0
|
||||
mov r9, r1
|
||||
mov r10, r2
|
||||
mov r11, r3
|
||||
pop {pc}
|
||||
#endif
|
||||
.cfi_endproc
|
||||
.size tinygo_swapTask, .-tinygo_swapTask
|
||||
@@ -2,7 +2,16 @@
|
||||
|
||||
package runtime
|
||||
|
||||
import "internal/task"
|
||||
|
||||
// getSystemStackPointer returns the current stack pointer of the system stack.
|
||||
// This is not necessarily the same as the current stack pointer.
|
||||
//export tinygo_getSystemStackPointer
|
||||
func getSystemStackPointer() uintptr
|
||||
func getSystemStackPointer() uintptr {
|
||||
// TODO: this always returns the correct stack on Cortex-M, so don't bother
|
||||
// comparing against 0.
|
||||
sp := task.SystemStack()
|
||||
if sp == 0 {
|
||||
sp = getCurrentStackPointer()
|
||||
}
|
||||
return sp
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user