Skip to content

Commit 10e4d44

Browse files
committed
WIP baremetal multicore RISC-V
1 parent 888d957 commit 10e4d44

31 files changed

+797
-121
lines changed

builder/sizes.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ func loadProgramSize(path string, packagePathMap map[string]string) (*programSiz
490490
continue
491491
}
492492
if section.Type == elf.SHT_NOBITS {
493-
if section.Name == ".stack" {
493+
if strings.HasPrefix(section.Name, ".stack") {
494494
// TinyGo emits stack sections on microcontroller using the
495495
// ".stack" name.
496496
// This is a bit ugly, but I don't think there is a way to

compileopts/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ func (c *Config) BuildTags() []string {
110110
"math_big_pure_go", // to get math/big to work
111111
"gc." + c.GC(), "scheduler." + c.Scheduler(), // used inside the runtime package
112112
"serial." + c.Serial()}...) // used inside the machine package
113+
switch c.Scheduler() {
114+
case "threads", "cores":
115+
default:
116+
tags = append(tags, "tinygo.unicore")
117+
}
113118
for i := 1; i <= c.GoMinorVersion; i++ {
114119
tags = append(tags, fmt.Sprintf("go1.%d", i))
115120
}

compileopts/options.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
var (
1111
validBuildModeOptions = []string{"default", "c-shared", "wasi-legacy"}
1212
validGCOptions = []string{"none", "leaking", "conservative", "custom", "precise", "boehm"}
13-
validSchedulerOptions = []string{"none", "tasks", "asyncify", "threads"}
13+
validSchedulerOptions = []string{"none", "tasks", "asyncify", "threads", "cores"}
1414
validSerialOptions = []string{"none", "uart", "usb", "rtt"}
1515
validPrintSizeOptions = []string{"none", "short", "full", "html"}
1616
validPanicStrategyOptions = []string{"print", "trap"}

compileopts/options_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
func TestVerifyOptions(t *testing.T) {
1111

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

src/device/riscv/start.S

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,43 @@
33
.type _start,@function
44

55
_start:
6+
// If we're on a multicore system, we need to wait for hart 0 to wake us up.
7+
#if TINYGO_CORES > 1
8+
csrr a0, mhartid
9+
10+
// Hart 0 stack
11+
bnez a0, 1f
12+
la sp, _stack0_top
13+
14+
1:
15+
// Hart 1 stack
16+
li a1, 1
17+
bne a0, a1, 2f
18+
la sp, _stack1_top
19+
20+
2:
21+
// Hart 2 stack
22+
#if TINYGO_CORES >= 3
23+
li a1, 2
24+
bne a0, a1, 3f
25+
la sp, _stack2_top
26+
#endif
27+
28+
3:
29+
// Hart 3 stack
30+
#if TINYGO_CORES >= 4
31+
li a1, 3
32+
bne a0, a1, 4f
33+
la sp, _stack3_top
34+
#endif
35+
36+
4:
37+
// done
38+
39+
#else
640
// Load the stack pointer.
7-
la sp, _stack_top
41+
la sp, _stack0_top
42+
#endif
843

944
// Load the globals pointer. The program will load pointers relative to this
1045
// register, so it must be set to the right value on startup.

src/internal/task/atomic-cooperative.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build !scheduler.threads
1+
//go:build tinygo.unicore
22

33
package task
44

src/internal/task/atomic-preemptive.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build scheduler.threads
1+
//go:build !tinygo.unicore
22

33
package task
44

src/internal/task/futex-cooperative.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build !scheduler.threads
1+
//go:build tinygo.unicore
22

33
package task
44

src/internal/task/futex-cores.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//go:build scheduler.cores
2+
3+
package task
4+
5+
import "runtime/interrupt"
6+
7+
// A futex is a way for userspace to wait with the pointer as the key, and for
8+
// another thread to wake one or all waiting threads keyed on the same pointer.
9+
//
10+
// A futex does not change the underlying value, it only reads it before to prevent
11+
// lost wake-ups.
12+
type Futex struct {
13+
Uint32
14+
15+
waiters Stack
16+
}
17+
18+
// Atomically check for cmp to still be equal to the futex value and if so, go
19+
// to sleep. Return true if we were definitely awoken by a call to Wake or
20+
// WakeAll, and false if we can't be sure of that.
21+
func (f *Futex) Wait(cmp uint32) (awoken bool) {
22+
mask := lockFutex()
23+
24+
if f.Uint32.Load() != cmp {
25+
unlockFutex(mask)
26+
return false
27+
}
28+
29+
// Push the current goroutine onto the waiter stack.
30+
f.waiters.Push(Current())
31+
32+
unlockFutex(mask)
33+
34+
// Pause until this task is awoken by Wake/WakeAll.
35+
Pause()
36+
37+
// We were awoken by a call to Wake or WakeAll. There is no chance for
38+
// spurious wakeups.
39+
return true
40+
}
41+
42+
// Wake a single waiter.
43+
func (f *Futex) Wake() {
44+
mask := lockFutex()
45+
if t := f.waiters.Pop(); t != nil {
46+
scheduleTask(t)
47+
}
48+
unlockFutex(mask)
49+
}
50+
51+
// Wake all waiters.
52+
func (f *Futex) WakeAll() {
53+
mask := lockFutex()
54+
for t := f.waiters.Pop(); t != nil; t = f.waiters.Pop() {
55+
scheduleTask(t)
56+
}
57+
unlockFutex(mask)
58+
}
59+
60+
//go:linkname lockFutex runtime.lockFutex
61+
func lockFutex() interrupt.State
62+
63+
//go:linkname unlockFutex runtime.unlockFutex
64+
func unlockFutex(interrupt.State)

src/internal/task/mutex-cooperative.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build !scheduler.threads
1+
//go:build tinygo.unicore
22

33
package task
44

src/internal/task/mutex-preemptive.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build scheduler.threads
1+
//go:build !tinygo.unicore
22

33
package task
44

src/internal/task/pmutex-cooperative.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build !scheduler.threads
1+
//go:build tinygo.unicore
22

33
package task
44

src/internal/task/pmutex-preemptive.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build scheduler.threads
1+
//go:build !tinygo.unicore
22

33
package task
44

src/internal/task/task.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,28 @@ type Task struct {
2424
// This is needed for some crypto packages.
2525
FipsIndicator uint8
2626

27+
// State of the goroutine: running, paused, or must-resume-next-pause.
28+
// This extra field doesn't increase memory usage on 32-bit CPUs and above,
29+
// since it falls into the padding of the FipsIndicator bit above.
30+
RunState uint8
31+
2732
// DeferFrame stores a pointer to the (stack allocated) defer frame of the
2833
// goroutine that is used for the recover builtin.
2934
DeferFrame unsafe.Pointer
3035
}
3136

37+
const (
38+
// Initial state: the goroutine state is saved on the stack.
39+
RunStatePaused = iota
40+
41+
// The goroutine is running right now.
42+
RunStateRunning
43+
44+
// The goroutine is running, but already marked as "can resume".
45+
// The next call to Pause() won't actually pause the goroutine.
46+
RunStateResuming
47+
)
48+
3249
// DataUint32 returns the Data field as a uint32. The value is only valid after
3350
// setting it through SetDataUint32 or by storing to it using DataAtomicUint32.
3451
func (t *Task) DataUint32() uint32 {

src/internal/task/task_stack.go

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
//go:build scheduler.tasks
1+
//go:build scheduler.tasks || scheduler.cores
22

33
package task
44

55
import (
6-
"runtime/interrupt"
76
"unsafe"
87
)
98

@@ -32,44 +31,12 @@ type state struct {
3231
canaryPtr *uintptr
3332
}
3433

35-
// currentTask is the current running task, or nil if currently in the scheduler.
36-
var currentTask *Task
37-
38-
// Current returns the current active task.
39-
func Current() *Task {
40-
return currentTask
41-
}
42-
43-
// Pause suspends the current task and returns to the scheduler.
44-
// This function may only be called when running on a goroutine stack, not when running on the system stack or in an interrupt.
45-
func Pause() {
46-
// Check whether the canary (the lowest address of the stack) is still
47-
// valid. If it is not, a stack overflow has occurred.
48-
if *currentTask.state.canaryPtr != stackCanary {
49-
runtimePanic("goroutine stack overflow")
50-
}
51-
if interrupt.In() {
52-
runtimePanic("blocked inside interrupt")
53-
}
54-
currentTask.state.pause()
55-
}
56-
5734
//export tinygo_task_exit
5835
func taskExit() {
5936
// TODO: explicitly free the stack after switching back to the scheduler.
6037
Pause()
6138
}
6239

63-
// Resume the task until it pauses or completes.
64-
// This may only be called from the scheduler.
65-
func (t *Task) Resume() {
66-
currentTask = t
67-
t.gcData.swap()
68-
t.state.resume()
69-
t.gcData.swap()
70-
currentTask = nil
71-
}
72-
7340
// initialize the state and prepare to call the specified function with the specified argument bundle.
7441
func (s *state) initialize(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
7542
// Create a stack.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//go:build scheduler.cores
2+
3+
package task
4+
5+
import "runtime/interrupt"
6+
7+
// Current returns the current active task.
8+
//
9+
//go:linkname Current runtime.currentTask
10+
func Current() *Task
11+
12+
// Pause suspends the current task and returns to the scheduler.
13+
// This function may only be called when running on a goroutine stack, not when running on the system stack or in an interrupt.
14+
func Pause() {
15+
lockScheduler()
16+
PauseLocked()
17+
}
18+
19+
// PauseLocked is the same as Pause, but must be called with the scheduler lock
20+
// already taken.
21+
func PauseLocked() {
22+
// Check whether the canary (the lowest address of the stack) is still
23+
// valid. If it is not, a stack overflow has occurred.
24+
current := Current()
25+
if *current.state.canaryPtr != stackCanary {
26+
runtimePanic("goroutine stack overflow")
27+
}
28+
if interrupt.In() {
29+
runtimePanic("blocked inside interrupt")
30+
}
31+
if current.RunState == RunStateResuming {
32+
// Another core already marked this goroutine as ready to resume.
33+
current.RunState = RunStateRunning
34+
unlockScheduler()
35+
return
36+
}
37+
current.RunState = RunStatePaused
38+
current.state.pause()
39+
}
40+
41+
// Resume the task until it pauses or completes.
42+
// This may only be called from the scheduler.
43+
func (t *Task) Resume() {
44+
t.gcData.swap()
45+
t.state.resume()
46+
t.gcData.swap()
47+
}
48+
49+
//go:linkname lockScheduler runtime.lockScheduler
50+
func lockScheduler()
51+
52+
//go:linkname unlockScheduler runtime.unlockScheduler
53+
func unlockScheduler()

src/internal/task/task_stack_tinygoriscv.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
//go:build scheduler.tasks && tinygo.riscv
1+
//go:build (scheduler.tasks || scheduler.cores) && tinygo.riscv
22

33
package task
44

55
import "unsafe"
66

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

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

5258
func (s *state) resume() {
53-
swapTask(s.sp, &systemStack)
59+
swapTask(s.sp, runtime_systemStackPtr())
5460
}
5561

5662
func (s *state) pause() {
57-
newStack := systemStack
58-
systemStack = 0
63+
systemStackPtr := runtime_systemStackPtr()
64+
newStack := *systemStackPtr
65+
*systemStackPtr = 0
5966
swapTask(newStack, &s.sp)
6067
}
6168

6269
// SystemStack returns the system stack pointer when called from a task stack.
6370
// When called from the system stack, it returns 0.
6471
func SystemStack() uintptr {
65-
return systemStack
72+
return *runtime_systemStackPtr()
6673
}

0 commit comments

Comments
 (0)