Skip to content

Baremetal multicore support #4851

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion builder/sizes.go
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ func loadProgramSize(path string, packagePathMap map[string]string) (*programSiz
continue
}
if section.Type == elf.SHT_NOBITS {
if section.Name == ".stack" {
if strings.HasPrefix(section.Name, ".stack") {
// TinyGo emits stack sections on microcontroller using the
// ".stack" name.
// This is a bit ugly, but I don't think there is a way to
Expand Down
5 changes: 5 additions & 0 deletions compileopts/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ func (c *Config) BuildTags() []string {
"math_big_pure_go", // to get math/big to work
"gc." + c.GC(), "scheduler." + c.Scheduler(), // used inside the runtime package
"serial." + c.Serial()}...) // used inside the machine package
switch c.Scheduler() {
case "threads", "cores":
default:
tags = append(tags, "tinygo.unicore")
}
for i := 1; i <= c.GoMinorVersion; i++ {
tags = append(tags, fmt.Sprintf("go1.%d", i))
}
Expand Down
2 changes: 1 addition & 1 deletion compileopts/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
var (
validBuildModeOptions = []string{"default", "c-shared", "wasi-legacy"}
validGCOptions = []string{"none", "leaking", "conservative", "custom", "precise", "boehm"}
validSchedulerOptions = []string{"none", "tasks", "asyncify", "threads"}
validSchedulerOptions = []string{"none", "tasks", "asyncify", "threads", "cores"}
validSerialOptions = []string{"none", "uart", "usb", "rtt"}
validPrintSizeOptions = []string{"none", "short", "full", "html"}
validPanicStrategyOptions = []string{"print", "trap"}
Expand Down
2 changes: 1 addition & 1 deletion compileopts/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
func TestVerifyOptions(t *testing.T) {

expectedGCError := errors.New(`invalid gc option 'incorrect': valid values are none, leaking, conservative, custom, precise, boehm`)
expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, asyncify, threads`)
expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, asyncify, threads, cores`)
expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full, html`)
expectedPanicStrategyError := errors.New(`invalid panic option 'incorrect': valid values are print, trap`)

Expand Down
39 changes: 39 additions & 0 deletions src/device/riscv/start.S
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,47 @@
.type _start,@function

_start:
// If we're on a multicore system, we need to wait for hart 0 to wake us up.
#if TINYGO_CORES > 1
csrr a0, mhartid

// Hart 0 stack
bnez a0, 1f
la sp, _stack_top

1:
// Hart 1 stack
li a1, 1
bne a0, a1, 2f
la sp, _stack1_top

2:
// Hart 2 stack
#if TINYGO_CORES >= 3
li a1, 2
bne a0, a1, 3f
la sp, _stack2_top
#endif

3:
// Hart 3 stack
#if TINYGO_CORES >= 4
li a1, 3
bne a0, a1, 4f
la sp, _stack3_top
#endif

4:
// done

#if TINYGO_CORES > 4
#error only up to 4 cores are supported at the moment!
#endif

#else
// Load the stack pointer.
la sp, _stack_top
#endif

// Load the globals pointer. The program will load pointers relative to this
// register, so it must be set to the right value on startup.
Expand Down
2 changes: 1 addition & 1 deletion src/internal/task/atomic-cooperative.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !scheduler.threads
//go:build tinygo.unicore

package task

Expand Down
2 changes: 1 addition & 1 deletion src/internal/task/atomic-preemptive.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build scheduler.threads
//go:build !tinygo.unicore

package task

Expand Down
2 changes: 1 addition & 1 deletion src/internal/task/futex-cooperative.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !scheduler.threads
//go:build tinygo.unicore

package task

Expand Down
64 changes: 64 additions & 0 deletions src/internal/task/futex-cores.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//go:build scheduler.cores

package task

import "runtime/interrupt"

// A futex is a way for userspace to wait with the pointer as the key, and for
// another thread to wake one or all waiting threads keyed on the same pointer.
//
// A futex does not change the underlying value, it only reads it before to prevent
// lost wake-ups.
type Futex struct {
Uint32

waiters Stack
}

// Atomically check for cmp to still be equal to the futex value and if so, go
// to sleep. Return true if we were definitely awoken by a call to Wake or
// WakeAll, and false if we can't be sure of that.
func (f *Futex) Wait(cmp uint32) (awoken bool) {
mask := lockFutex()

if f.Uint32.Load() != cmp {
unlockFutex(mask)
return false
}

// Push the current goroutine onto the waiter stack.
f.waiters.Push(Current())

unlockFutex(mask)

// Pause until this task is awoken by Wake/WakeAll.
Pause()

// We were awoken by a call to Wake or WakeAll. There is no chance for
// spurious wakeups.
return true
}

// Wake a single waiter.
func (f *Futex) Wake() {
mask := lockFutex()
if t := f.waiters.Pop(); t != nil {
scheduleTask(t)
}
unlockFutex(mask)
}

// Wake all waiters.
func (f *Futex) WakeAll() {
mask := lockFutex()
for t := f.waiters.Pop(); t != nil; t = f.waiters.Pop() {
scheduleTask(t)
}
unlockFutex(mask)
}

//go:linkname lockFutex runtime.lockFutex
func lockFutex() interrupt.State

//go:linkname unlockFutex runtime.unlockFutex
func unlockFutex(interrupt.State)
2 changes: 1 addition & 1 deletion src/internal/task/mutex-cooperative.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !scheduler.threads
//go:build tinygo.unicore

package task

Expand Down
2 changes: 1 addition & 1 deletion src/internal/task/mutex-preemptive.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build scheduler.threads
//go:build !tinygo.unicore

package task

Expand Down
2 changes: 1 addition & 1 deletion src/internal/task/pmutex-cooperative.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !scheduler.threads
//go:build tinygo.unicore

package task

Expand Down
2 changes: 1 addition & 1 deletion src/internal/task/pmutex-preemptive.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build scheduler.threads
//go:build !tinygo.unicore

package task

Expand Down
17 changes: 17 additions & 0 deletions src/internal/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,28 @@ type Task struct {
// This is needed for some crypto packages.
FipsIndicator uint8

// State of the goroutine: running, paused, or must-resume-next-pause.
// This extra field doesn't increase memory usage on 32-bit CPUs and above,
// since it falls into the padding of the FipsIndicator bit above.
RunState uint8

// DeferFrame stores a pointer to the (stack allocated) defer frame of the
// goroutine that is used for the recover builtin.
DeferFrame unsafe.Pointer
}

const (
// Initial state: the goroutine state is saved on the stack.
RunStatePaused = iota

// The goroutine is running right now.
RunStateRunning

// The goroutine is running, but already marked as "can resume".
// The next call to Pause() won't actually pause the goroutine.
RunStateResuming
)

// DataUint32 returns the Data field as a uint32. The value is only valid after
// setting it through SetDataUint32 or by storing to it using DataAtomicUint32.
func (t *Task) DataUint32() uint32 {
Expand Down
35 changes: 1 addition & 34 deletions src/internal/task/task_stack.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
//go:build scheduler.tasks
//go:build scheduler.tasks || scheduler.cores

package task

import (
"runtime/interrupt"
"unsafe"
)

Expand Down Expand Up @@ -32,44 +31,12 @@ type state struct {
canaryPtr *uintptr
}

// currentTask is the current running task, or nil if currently in the scheduler.
var currentTask *Task

// Current returns the current active task.
func Current() *Task {
return currentTask
}

// Pause suspends the current task and returns to the scheduler.
// This function may only be called when running on a goroutine stack, not when running on the system stack or in an interrupt.
func Pause() {
// Check whether the canary (the lowest address of the stack) is still
// valid. If it is not, a stack overflow has occurred.
if *currentTask.state.canaryPtr != stackCanary {
runtimePanic("goroutine stack overflow")
}
if interrupt.In() {
runtimePanic("blocked inside interrupt")
}
currentTask.state.pause()
}

//export tinygo_task_exit
func taskExit() {
// TODO: explicitly free the stack after switching back to the scheduler.
Pause()
}

// Resume the task until it pauses or completes.
// This may only be called from the scheduler.
func (t *Task) Resume() {
currentTask = t
t.gcData.swap()
t.state.resume()
t.gcData.swap()
currentTask = nil
}

// initialize the state and prepare to call the specified function with the specified argument bundle.
func (s *state) initialize(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
// Create a stack.
Expand Down
53 changes: 53 additions & 0 deletions src/internal/task/task_stack_multicore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//go:build scheduler.cores

package task

import "runtime/interrupt"

// Current returns the current active task.
//
//go:linkname Current runtime.currentTask
func Current() *Task

// Pause suspends the current task and returns to the scheduler.
// This function may only be called when running on a goroutine stack, not when running on the system stack or in an interrupt.
func Pause() {
lockScheduler()
PauseLocked()
}

// PauseLocked is the same as Pause, but must be called with the scheduler lock
// already taken.
func PauseLocked() {
// Check whether the canary (the lowest address of the stack) is still
// valid. If it is not, a stack overflow has occurred.
current := Current()
if *current.state.canaryPtr != stackCanary {
runtimePanic("goroutine stack overflow")
}
if interrupt.In() {
runtimePanic("blocked inside interrupt")
}
if current.RunState == RunStateResuming {
// Another core already marked this goroutine as ready to resume.
current.RunState = RunStateRunning
unlockScheduler()
return
}
current.RunState = RunStatePaused
current.state.pause()
}

// Resume the task until it pauses or completes.
// This may only be called from the scheduler.
func (t *Task) Resume() {
t.gcData.swap()
t.state.resume()
t.gcData.swap()
}

//go:linkname lockScheduler runtime.lockScheduler
func lockScheduler()

//go:linkname unlockScheduler runtime.unlockScheduler
func unlockScheduler()
19 changes: 13 additions & 6 deletions src/internal/task/task_stack_tinygoriscv.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
//go:build scheduler.tasks && tinygo.riscv
//go:build (scheduler.tasks || scheduler.cores) && tinygo.riscv

package task

import "unsafe"

var systemStack uintptr
// Returns a pointer where the system stack can be stored.
// This is a layering violation! We should probably refactor this so that we
// don't need such gymnastics to store the system stack pointer. (It should
// probably be moved to the runtime).
//
//go:linkname runtime_systemStackPtr runtime.systemStackPtr
func runtime_systemStackPtr() *uintptr

// calleeSavedRegs is the list of registers that must be saved and restored when
// switching between tasks. Also see scheduler_riscv.S that relies on the
Expand Down Expand Up @@ -50,17 +56,18 @@ func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) {
}

func (s *state) resume() {
swapTask(s.sp, &systemStack)
swapTask(s.sp, runtime_systemStackPtr())
}

func (s *state) pause() {
newStack := systemStack
systemStack = 0
systemStackPtr := runtime_systemStackPtr()
newStack := *systemStackPtr
*systemStackPtr = 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
return *runtime_systemStackPtr()
}
Loading
Loading