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

Merged
merged 5 commits into from
Jun 12, 2025
Merged
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
6 changes: 3 additions & 3 deletions builder/sizes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ func TestBinarySize(t *testing.T) {
// This is a small number of very diverse targets that we want to test.
tests := []sizeTest{
// microcontrollers
{"hifive1b", "examples/echo", 4560, 280, 0, 2268},
{"microbit", "examples/serial", 2924, 388, 8, 2272},
{"wioterminal", "examples/pininterrupt", 7383, 1489, 116, 6912},
{"hifive1b", "examples/echo", 4556, 280, 0, 2264},
{"microbit", "examples/serial", 2920, 388, 8, 2272},
{"wioterminal", "examples/pininterrupt", 7379, 1489, 116, 6912},

// TODO: also check wasm. Right now this is difficult, because
// wasm binaries are run through wasm-opt and therefore the
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
26 changes: 17 additions & 9 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1856,15 +1856,7 @@ func (b *builder) createBuiltin(argTypes []types.Type, argValues []llvm.Value, c
//
// This is also where compiler intrinsics are implemented.
func (b *builder) createFunctionCall(instr *ssa.CallCommon) (llvm.Value, error) {
var params []llvm.Value
for _, param := range instr.Args {
params = append(params, b.getValue(param, getPos(instr)))
}

// Try to call the function directly for trivially static calls.
var callee, context llvm.Value
var calleeType llvm.Type
exported := false
// See if this is an intrinsic function that is handled specially.
if fn := instr.StaticCallee(); fn != nil {
// Direct function call, either to a named or anonymous (directly
// applied) function call. If it is anonymous, it may be a closure.
Expand Down Expand Up @@ -1900,13 +1892,29 @@ func (b *builder) createFunctionCall(instr *ssa.CallCommon) (llvm.Value, error)
return llvm.ConstInt(b.ctx.Int8Type(), panicStrategy, false), nil
case name == "runtime/interrupt.New":
return b.createInterruptGlobal(instr)
case name == "runtime.exportedFuncPtr":
_, ptr := b.getFunction(instr.Args[0].(*ssa.Function))
return b.CreatePtrToInt(ptr, b.uintptrType, ""), nil
case name == "(*runtime/interrupt.Checkpoint).Save":
return b.createInterruptCheckpoint(instr.Args[0]), nil
case name == "internal/abi.FuncPCABI0":
retval := b.createDarwinFuncPCABI0Call(instr)
if !retval.IsNil() {
return retval, nil
}
}
}

var params []llvm.Value
for _, param := range instr.Args {
params = append(params, b.getValue(param, getPos(instr)))
}

// Try to call the function directly for trivially static calls.
var callee, context llvm.Value
var calleeType llvm.Type
exported := false
if fn := instr.StaticCallee(); fn != nil {
calleeType, callee = b.getFunction(fn)
info := b.getFunctionInfo(fn)
if callee.IsNil() {
Expand Down
21 changes: 15 additions & 6 deletions compiler/defer.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,11 @@ func (b *builder) createLandingPad() {
b.CreateBr(b.blockEntries[b.fn.Recover])
}

// createInvokeCheckpoint saves the function state at the given point, to
// continue at the landing pad if a panic happened. This is implemented using a
// setjmp-like construct.
func (b *builder) createInvokeCheckpoint() {
// Create a checkpoint (similar to setjmp). This emits inline assembly that
// stores the current program counter inside the ptr address (actually
// ptr+sizeof(ptr)) and then returns a boolean indicating whether this is the
// normal flow (false) or we jumped here from somewhere else (true).
func (b *builder) createCheckpoint(ptr llvm.Value) llvm.Value {
// Construct inline assembly equivalents of setjmp.
// The assembly works as follows:
// * All registers (both callee-saved and caller saved) are clobbered
Expand Down Expand Up @@ -217,11 +218,19 @@ li a0, 0
// This case should have been handled by b.supportsRecover().
b.addError(b.fn.Pos(), "unknown architecture for defer: "+b.archFamily())
}
asmType := llvm.FunctionType(resultType, []llvm.Type{b.deferFrame.Type()}, false)
asmType := llvm.FunctionType(resultType, []llvm.Type{b.dataPtrType}, false)
asm := llvm.InlineAsm(asmType, asmString, constraints, false, false, 0, false)
result := b.CreateCall(asmType, asm, []llvm.Value{b.deferFrame}, "setjmp")
result := b.CreateCall(asmType, asm, []llvm.Value{ptr}, "setjmp")
result.AddCallSiteAttribute(-1, b.ctx.CreateEnumAttribute(llvm.AttributeKindID("returns_twice"), 0))
isZero := b.CreateICmp(llvm.IntEQ, result, llvm.ConstInt(resultType, 0, false), "setjmp.result")
return isZero
}

// createInvokeCheckpoint saves the function state at the given point, to
// continue at the landing pad if a panic happened. This is implemented using a
// setjmp-like construct.
func (b *builder) createInvokeCheckpoint() {
isZero := b.createCheckpoint(b.deferFrame)
continueBB := b.insertBasicBlock("")
b.CreateCondBr(isZero, continueBB, b.landingpad)
b.SetInsertPointAtEnd(continueBB)
Expand Down
12 changes: 12 additions & 0 deletions compiler/inlineasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,15 @@ func (b *builder) emitCSROperation(call *ssa.CallCommon) (llvm.Value, error) {
return llvm.Value{}, b.makeError(call.Pos(), "unknown CSR operation: "+name)
}
}

// Implement runtime/interrupt.Checkpoint.Save. It needs to be implemented
// directly at the call site. If it isn't implemented directly at the call site
// (but instead through a function call), it might result in an overwritten
// stack in the non-jump return case.
func (b *builder) createInterruptCheckpoint(ptr ssa.Value) llvm.Value {
addr := b.getValue(ptr, ptr.Pos())
b.createNilCheck(ptr, addr, "deref")
stackPointer := b.readStackPointer()
b.CreateStore(stackPointer, addr)
return b.createCheckpoint(addr)
}
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
Loading