diff --git a/builder/sizes.go b/builder/sizes.go index 485a652d9..9aa74ffa8 100644 --- a/builder/sizes.go +++ b/builder/sizes.go @@ -490,9 +490,9 @@ func loadProgramSize(path string, packagePathMap map[string]string) (*programSiz continue } if section.Type == elf.SHT_NOBITS { - if section.Name == ".stack" { + if section.Name == ".stack" || section.Name == ".stack1" { // TinyGo emits stack sections on microcontroller using the - // ".stack" name. + // ".stack" (or ".stack1") name. // This is a bit ugly, but I don't think there is a way to // mark the stack section in a linker script. sections = append(sections, memorySection{ diff --git a/compileopts/config.go b/compileopts/config.go index ee5c34537..1c88519cd 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -99,6 +99,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)) } diff --git a/compileopts/options.go b/compileopts/options.go index 78eb17cde..4366dd5c1 100644 --- a/compileopts/options.go +++ b/compileopts/options.go @@ -10,7 +10,7 @@ import ( var ( validBuildModeOptions = []string{"default", "c-shared"} validGCOptions = []string{"none", "leaking", "conservative", "custom", "precise"} - 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"} diff --git a/compileopts/target.go b/compileopts/target.go index a28bf774f..961e69a5d 100644 --- a/compileopts/target.go +++ b/compileopts/target.go @@ -501,6 +501,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) { } spec.ExtraFiles = append(spec.ExtraFiles, "src/runtime/asm_"+asmGoarch+suffix+".S") spec.ExtraFiles = append(spec.ExtraFiles, "src/internal/task/task_stack_"+asmGoarch+suffix+".S") + spec.ExtraFiles = append(spec.ExtraFiles, "src/internal/task/task_cores.c") } // Configure the emulator. diff --git a/compiler/symbol.go b/compiler/symbol.go index ff7ef0550..8ad2184d3 100644 --- a/compiler/symbol.go +++ b/compiler/symbol.go @@ -208,6 +208,8 @@ func (c *compilerContext) getFunction(fn *ssa.Function) (llvm.Type, llvm.Value) // > circumstances, and should not be exposed to source languages. llvmutil.AppendToGlobal(c.mod, "llvm.compiler.used", llvmFn) } + case "tinygo_exitTask", "tinygo_schedulerUnlock": + llvmutil.AppendToGlobal(c.mod, "llvm.used", llvmFn) } // External/exported functions may not retain pointer values. diff --git a/src/internal/task/atomic-cooperative.go b/src/internal/task/atomic-cooperative.go index bd4cba895..e05ea7de0 100644 --- a/src/internal/task/atomic-cooperative.go +++ b/src/internal/task/atomic-cooperative.go @@ -1,4 +1,4 @@ -//go:build !scheduler.threads +//go:build tinygo.unicore package task diff --git a/src/internal/task/atomic-preemptive.go b/src/internal/task/atomic-preemptive.go index 275f36dce..b395ef48a 100644 --- a/src/internal/task/atomic-preemptive.go +++ b/src/internal/task/atomic-preemptive.go @@ -1,4 +1,4 @@ -//go:build scheduler.threads +//go:build !tinygo.unicore package task diff --git a/src/internal/task/futex-cooperative.go b/src/internal/task/futex-cooperative.go index 2a42c28d4..ae9efb5a7 100644 --- a/src/internal/task/futex-cooperative.go +++ b/src/internal/task/futex-cooperative.go @@ -1,4 +1,4 @@ -//go:build !scheduler.threads +//go:build tinygo.unicore package task diff --git a/src/internal/task/futex-cores.go b/src/internal/task/futex-cores.go new file mode 100644 index 000000000..0ffccba2e --- /dev/null +++ b/src/internal/task/futex-cores.go @@ -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 := futexLock() + + if f.Uint32.Load() != cmp { + futexUnlock(mask) + return false + } + + // Push the current goroutine onto the waiter stack. + f.waiters.Push(Current()) + + futexUnlock(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 := futexLock() + if t := f.waiters.Pop(); t != nil { + scheduleTask(t) + } + futexUnlock(mask) +} + +// Wake all waiters. +func (f *Futex) WakeAll() { + mask := futexLock() + for t := f.waiters.Pop(); t != nil; t = f.waiters.Pop() { + scheduleTask(t) + } + futexUnlock(mask) +} + +//go:linkname futexLock runtime.futexLock +func futexLock() interrupt.State + +//go:linkname futexUnlock runtime.futexUnlock +func futexUnlock(interrupt.State) diff --git a/src/internal/task/futex-preemptive.go b/src/internal/task/futex-threads.go similarity index 100% rename from src/internal/task/futex-preemptive.go rename to src/internal/task/futex-threads.go diff --git a/src/internal/task/mutex-cooperative.go b/src/internal/task/mutex-cooperative.go index f1205eea2..90274df2b 100644 --- a/src/internal/task/mutex-cooperative.go +++ b/src/internal/task/mutex-cooperative.go @@ -1,4 +1,4 @@ -//go:build !scheduler.threads +//go:build tinygo.unicore package task diff --git a/src/internal/task/mutex-preemptive.go b/src/internal/task/mutex-preemptive.go index 27f464669..ec83a6135 100644 --- a/src/internal/task/mutex-preemptive.go +++ b/src/internal/task/mutex-preemptive.go @@ -1,4 +1,4 @@ -//go:build scheduler.threads +//go:build !tinygo.unicore package task diff --git a/src/internal/task/pmutex-cooperative.go b/src/internal/task/pmutex-cooperative.go index 0e6c4f828..b61e92d82 100644 --- a/src/internal/task/pmutex-cooperative.go +++ b/src/internal/task/pmutex-cooperative.go @@ -1,4 +1,4 @@ -//go:build !scheduler.threads +//go:build tinygo.unicore package task diff --git a/src/internal/task/pmutex-preemptive.go b/src/internal/task/pmutex-preemptive.go index 10f0a6356..92263ed25 100644 --- a/src/internal/task/pmutex-preemptive.go +++ b/src/internal/task/pmutex-preemptive.go @@ -1,4 +1,4 @@ -//go:build scheduler.threads +//go:build !tinygo.unicore package task diff --git a/src/internal/task/task.go b/src/internal/task/task.go index 546f5ba11..aa563f638 100644 --- a/src/internal/task/task.go +++ b/src/internal/task/task.go @@ -53,3 +53,11 @@ func runtime_alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer //go:linkname scheduleTask runtime.scheduleTask func scheduleTask(*Task) + +//go:linkname runtimePanic runtime.runtimePanic +func runtimePanic(str string) + +// Stack canary, to detect a stack overflow. The number is a random number +// generated by random.org. The bit fiddling dance is necessary because +// otherwise Go wouldn't allow the cast to a smaller integer size. +const stackCanary = uintptr(uint64(0x670c1333b83bf575) & uint64(^uintptr(0))) diff --git a/src/internal/task/task_asyncify.go b/src/internal/task/task_asyncify.go index 637a6b223..bc4197995 100644 --- a/src/internal/task/task_asyncify.go +++ b/src/internal/task/task_asyncify.go @@ -6,14 +6,6 @@ import ( "unsafe" ) -// Stack canary, to detect a stack overflow. The number is a random number -// generated by random.org. The bit fiddling dance is necessary because -// otherwise Go wouldn't allow the cast to a smaller integer size. -const stackCanary = uintptr(uint64(0x670c1333b83bf575) & uint64(^uintptr(0))) - -//go:linkname runtimePanic runtime.runtimePanic -func runtimePanic(str string) - // state is a structure which holds a reference to the state of the task. // When the task is suspended, the stack pointers are saved here. type state struct { diff --git a/src/internal/task/task_cores.c b/src/internal/task/task_cores.c new file mode 100644 index 000000000..ae3f203bb --- /dev/null +++ b/src/internal/task/task_cores.c @@ -0,0 +1,50 @@ +//go:build scheduler.cores + +#include + +__attribute__((naked)) +void tinygo_cores_startTask(void) { + asm volatile( + "bl tinygo_schedulerUnlock\n\t" + + "ldr r0, =tinygo_exitTask\n\t" + "mov lr, r0\n\t" + + "pop {r0, pc}\n\t" + ); +} + +void tinygo_switchTask(uintptr_t *oldStack, uintptr_t newStack) { +#if defined(__thumb__) + register uintptr_t *oldStackReg asm("r0"); + oldStackReg = oldStack; + register uintptr_t newStackReg asm("r1"); + newStackReg = newStack; + asm volatile( + // Push PC to switch back to. + // Note: adding 1 to set the Thumb bit. + "ldr r2, =1f+1\n\t" + "push {r2}\n\t" + + // Save stack pointer in oldStack for the switch back. + "mov r2, sp\n\t" + "str r2, [%[oldStack]]\n\t" + + // Switch to the new stack. + "mov sp, %[newStack]\n\t" + + // Return into the new stack. + "pop {pc}\n\t" + + // address where we should resume + "1:" + + : [oldStack]"+r"(oldStackReg), + [newStack]"+r"(newStackReg) + : + : "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11", "r12", "lr", "cc", "memory" + ); +#else + #error unknown architecture +#endif +} diff --git a/src/internal/task/task_cores.go b/src/internal/task/task_cores.go new file mode 100644 index 000000000..c4f4b558c --- /dev/null +++ b/src/internal/task/task_cores.go @@ -0,0 +1,266 @@ +//go:build scheduler.cores + +package task + +import ( + "runtime/interrupt" + "unsafe" +) + +import "C" // dummy import, to make sure task_cores.c is included in the build + +type runState uint8 + +const ( + runStateRunning runState = iota + runStateResuming + runStatePaused +) + +type state struct { + // Which state the task is currently in. + // The state is protected by the scheduler lock, and must only be + // read/modified with that lock held. + runState runState + + // The stack pointer while the task is switched away. + sp unsafe.Pointer + + // canaryPtr points to the top word of the stack (the lowest address). + // This is used to detect stack overflows. + // When initializing the goroutine, the stackCanary constant is stored there. + // If the stack overflowed, the word will likely no longer equal stackCanary. + canaryPtr *uintptr +} + +var ( + runQueue *Task + sleepQueue *Task +) + +//go:linkname runtimeCurrentTask runtime.currentTask +func runtimeCurrentTask() *Task + +// Current returns the current task, or nil if we're in the scheduler. +func Current() *Task { + return runtimeCurrentTask() +} + +func Init(mainTask *Task, canaryPtr *uintptr) { + // The topmost word of the default stack is used as a stack canary. + *canaryPtr = stackCanary + mainTask.state.canaryPtr = canaryPtr +} + +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 *Current().state.canaryPtr != stackCanary { + runtimePanic("goroutine stack overflow") + } + if interrupt.In() { + runtimePanic("blocked inside interrupt") + } + + // Note: Pause() must be called with the scheduler lock locked! + schedulerLock() + pauseLocked() + schedulerUnlock() +} + +var schedulerIsRunning bool + +func pauseLocked() { + t := Current() + for { + if t.state.runState == runStateResuming { + t.state.runState = runStateRunning + return + } + + // Make sure only one core is calling sleepTicks etc. + if schedulerIsRunning { + schedulerUnlock() + waitForEvents() + schedulerLock() + continue + } + + if runnable := runQueue; runnable != nil { + // Resume it now. + runQueue = runQueue.Next + runnable.Next = nil + if t == runnable { + // We're actually the task that's supposed to be resumed, so we + // are ready! + } else { + // It's not us that's ready, so switch to this other task. + setCurrentTask(runnable) + t.state.runState = runStatePaused + + // Switch away! + switchTask(&t.state.sp, runnable.state.sp) + + // We got back from the switch, so another task resumed us. + t.state.runState = runStateRunning + } + return + } + + // Check whether there's a sleeping task that is ready to run. + if sleepingTask := sleepQueue; sleepingTask != nil { + now := runtimeTicks() + if now >= sleepingTask.Data { + // This task is done sleeping. + // Resume it now. + sleepQueue = sleepQueue.Next + sleepingTask.Next = nil + if t == sleepingTask { + // We're actually the task that's sleeping, so we are ready! + } else { + // It's not us that's ready, so switch to this other task. + setCurrentTask(sleepingTask) + t.state.runState = runStatePaused + + // Switch away! + switchTask(&t.state.sp, sleepingTask.state.sp) + + // We got back from the switch, so another task resumed us. + t.state.runState = runStateRunning + } + return + } else { + // Sleep for a bit until the next task is ready to run. + schedulerIsRunning = true + schedulerUnlock() + delay := sleepingTask.Data - now + runtimeSleepTicks(delay) + schedulerLock() + schedulerIsRunning = false + continue + } + } + } +} + +func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) { + t := &Task{} + stack := runtime_alloc(stackSize, nil) + stackTop := unsafe.Add(stack, stackSize-16) + topRegs := unsafe.Slice((*uintptr)(stackTop), 4) + topRegs[0] = uintptr(unsafe.Pointer(&startTask)) + topRegs[1] = uintptr(args) + topRegs[2] = fn + t.state.sp = stackTop + + canaryPtr := (*uintptr)(stack) + *canaryPtr = stackCanary + t.state.canaryPtr = canaryPtr + + schedulerLock() + addToRunqueue(t) + schedulerUnlock() +} + +func GCScan() { + panic("todo: task.GCScan") +} + +func StackTop() uintptr { + println("todo: task.StackTop") + for { + } +} + +func Sleep(wakeup uint64) { + schedulerLock() + addSleepTask(Current(), wakeup) + pauseLocked() + schedulerUnlock() +} + +func Resume(t *Task) { + schedulerLock() + switch t.state.runState { + case runStatePaused: + // Paused, state is saved on the stack. + addToRunqueue(t) + case runStateRunning: + // Going to pause soon, so let the Pause() function know it can resume + // immediately. + t.state.runState = runStateResuming + default: + println("unknown run state??") + for { + } + } + schedulerUnlock() +} + +// May only be called with the scheduler lock held! +func addToRunqueue(t *Task) { + t.Next = runQueue + runQueue = t +} + +func addSleepTask(t *Task, wakeup uint64) { + // Save the timestamp when the task should be woken up. + t.Data = wakeup + + // Find the position where we should insert this task in the queue. + q := &sleepQueue + for { + if *q == nil { + // Found the end of the time queue. Insert it here, at the end. + break + } + if (*q).Data > t.Data { + // Found a task in the queue that has a timeout before the + // to-be-sleeping task. Insert our task right before. + break + } + q = &(*q).Next + } + + // Insert the task into the queue (this could be at the end, if *q is nil). + t.Next = *q + *q = t +} + +//go:linkname schedulerLock runtime.schedulerLock +func schedulerLock() + +//go:linkname schedulerUnlock runtime.schedulerUnlock +func schedulerUnlock() + +//go:linkname runtimeTicks runtime.runtimeTicks +func runtimeTicks() uint64 + +//go:linkname runtimeSleepTicks runtime.runtimeSleepTicks +func runtimeSleepTicks(duration uint64) + +// startTask is a small wrapper function that sets up the first (and only) +// argument to the new goroutine and makes sure it is exited when the goroutine +// finishes. +// +//go:extern tinygo_cores_startTask +var startTask [0]uint8 + +//export tinygo_exitTask +func exitTask() { + Pause() +} + +//export tinygo_schedulerUnlock +func tinygo_schedulerUnlock() { + schedulerUnlock() +} + +//export tinygo_switchTask +func switchTask(oldStack *unsafe.Pointer, newStack unsafe.Pointer) + +//go:linkname waitForEvents runtime.waitForEvents +func waitForEvents() + +//go:linkname setCurrentTask runtime.setCurrentTask +func setCurrentTask(task *Task) diff --git a/src/internal/task/task_none.go b/src/internal/task/task_none.go index 280f1c4a8..8b667eec3 100644 --- a/src/internal/task/task_none.go +++ b/src/internal/task/task_none.go @@ -7,9 +7,6 @@ import "unsafe" // There is only one goroutine so the task struct can be a global. var mainTask Task -//go:linkname runtimePanic runtime.runtimePanic -func runtimePanic(str string) - func Pause() { runtimePanic("scheduler is disabled") } diff --git a/src/internal/task/task_stack.go b/src/internal/task/task_stack.go index 88a097068..dc846107a 100644 --- a/src/internal/task/task_stack.go +++ b/src/internal/task/task_stack.go @@ -7,14 +7,6 @@ import ( "unsafe" ) -//go:linkname runtimePanic runtime.runtimePanic -func runtimePanic(str string) - -// Stack canary, to detect a stack overflow. The number is a random number -// generated by random.org. The bit fiddling dance is necessary because -// otherwise Go wouldn't allow the cast to a smaller integer size. -const stackCanary = uintptr(uint64(0x670c1333b83bf575) & uint64(^uintptr(0))) - // state is a structure which holds a reference to the state of the task. // When the task is suspended, the registers are stored onto the stack and the stack pointer is stored into sp. type state struct { diff --git a/src/internal/task/task_threads.go b/src/internal/task/task_threads.go index c3531ef9c..327b2df46 100644 --- a/src/internal/task/task_threads.go +++ b/src/internal/task/task_threads.go @@ -244,9 +244,6 @@ func StackTop() uintptr { return Current().state.stackTop } -//go:linkname runtimePanic runtime.runtimePanic -func runtimePanic(msg string) - // Using //go:linkname instead of //export so that we don't tell the compiler // that the 't' parameter won't escape (because it will). // diff --git a/src/runtime/atomics_critical.go b/src/runtime/atomics_critical.go index 2d98881a1..edc6a82a3 100644 --- a/src/runtime/atomics_critical.go +++ b/src/runtime/atomics_critical.go @@ -6,7 +6,6 @@ package runtime import ( - "runtime/interrupt" _ "unsafe" ) @@ -23,27 +22,27 @@ import ( func __atomic_load_2(ptr *uint16, ordering uintptr) uint16 { // The LLVM docs for this say that there is a val argument after the pointer. // That is a typo, and the GCC docs omit it. - mask := interrupt.Disable() + mask := atomicLock() val := *ptr - interrupt.Restore(mask) + atomicUnlock(mask) return val } //export __atomic_store_2 func __atomic_store_2(ptr *uint16, val uint16, ordering uintptr) { - mask := interrupt.Disable() + mask := atomicLock() *ptr = val - interrupt.Restore(mask) + atomicUnlock(mask) } //go:inline func doAtomicCAS16(ptr *uint16, expected, desired uint16) uint16 { - mask := interrupt.Disable() + mask := atomicLock() old := *ptr if old == expected { *ptr = desired } - interrupt.Restore(mask) + atomicUnlock(mask) return old } @@ -61,10 +60,10 @@ func __atomic_compare_exchange_2(ptr, expected *uint16, desired uint16, successO //go:inline func doAtomicSwap16(ptr *uint16, new uint16) uint16 { - mask := interrupt.Disable() + mask := atomicLock() old := *ptr *ptr = new - interrupt.Restore(mask) + atomicUnlock(mask) return old } @@ -80,11 +79,11 @@ func __atomic_exchange_2(ptr *uint16, new uint16, ordering uintptr) uint16 { //go:inline func doAtomicAdd16(ptr *uint16, value uint16) (old, new uint16) { - mask := interrupt.Disable() + mask := atomicLock() old = *ptr new = old + value *ptr = new - interrupt.Restore(mask) + atomicUnlock(mask) return old, new } @@ -112,27 +111,27 @@ func __atomic_add_fetch_2(ptr *uint16, value uint16, ordering uintptr) uint16 { func __atomic_load_4(ptr *uint32, ordering uintptr) uint32 { // The LLVM docs for this say that there is a val argument after the pointer. // That is a typo, and the GCC docs omit it. - mask := interrupt.Disable() + mask := atomicLock() val := *ptr - interrupt.Restore(mask) + atomicUnlock(mask) return val } //export __atomic_store_4 func __atomic_store_4(ptr *uint32, val uint32, ordering uintptr) { - mask := interrupt.Disable() + mask := atomicLock() *ptr = val - interrupt.Restore(mask) + atomicUnlock(mask) } //go:inline func doAtomicCAS32(ptr *uint32, expected, desired uint32) uint32 { - mask := interrupt.Disable() + mask := atomicLock() old := *ptr if old == expected { *ptr = desired } - interrupt.Restore(mask) + atomicUnlock(mask) return old } @@ -150,10 +149,10 @@ func __atomic_compare_exchange_4(ptr, expected *uint32, desired uint32, successO //go:inline func doAtomicSwap32(ptr *uint32, new uint32) uint32 { - mask := interrupt.Disable() + mask := atomicLock() old := *ptr *ptr = new - interrupt.Restore(mask) + atomicUnlock(mask) return old } @@ -169,11 +168,11 @@ func __atomic_exchange_4(ptr *uint32, new uint32, ordering uintptr) uint32 { //go:inline func doAtomicAdd32(ptr *uint32, value uint32) (old, new uint32) { - mask := interrupt.Disable() + mask := atomicLock() old = *ptr new = old + value *ptr = new - interrupt.Restore(mask) + atomicUnlock(mask) return old, new } @@ -201,27 +200,27 @@ func __atomic_add_fetch_4(ptr *uint32, value uint32, ordering uintptr) uint32 { func __atomic_load_8(ptr *uint64, ordering uintptr) uint64 { // The LLVM docs for this say that there is a val argument after the pointer. // That is a typo, and the GCC docs omit it. - mask := interrupt.Disable() + mask := atomicLock() val := *ptr - interrupt.Restore(mask) + atomicUnlock(mask) return val } //export __atomic_store_8 func __atomic_store_8(ptr *uint64, val uint64, ordering uintptr) { - mask := interrupt.Disable() + mask := atomicLock() *ptr = val - interrupt.Restore(mask) + atomicUnlock(mask) } //go:inline func doAtomicCAS64(ptr *uint64, expected, desired uint64) uint64 { - mask := interrupt.Disable() + mask := atomicLock() old := *ptr if old == expected { *ptr = desired } - interrupt.Restore(mask) + atomicUnlock(mask) return old } @@ -239,10 +238,10 @@ func __atomic_compare_exchange_8(ptr, expected *uint64, desired uint64, successO //go:inline func doAtomicSwap64(ptr *uint64, new uint64) uint64 { - mask := interrupt.Disable() + mask := atomicLock() old := *ptr *ptr = new - interrupt.Restore(mask) + atomicUnlock(mask) return old } @@ -258,11 +257,11 @@ func __atomic_exchange_8(ptr *uint64, new uint64, ordering uintptr) uint64 { //go:inline func doAtomicAdd64(ptr *uint64, value uint64) (old, new uint64) { - mask := interrupt.Disable() + mask := atomicLock() old = *ptr new = old + value *ptr = new - interrupt.Restore(mask) + atomicUnlock(mask) return old, new } diff --git a/src/runtime/baremetal-multicore.go b/src/runtime/baremetal-multicore.go new file mode 100644 index 000000000..8395d8f73 --- /dev/null +++ b/src/runtime/baremetal-multicore.go @@ -0,0 +1,13 @@ +//go:build baremetal && !tinygo.unicore + +package runtime + +import "runtime/interrupt" + +func atomicLock() interrupt.State { + return atomicLockImpl() +} + +func atomicUnlock(mask interrupt.State) { + atomicUnlockImpl(mask) +} diff --git a/src/runtime/baremetal-unicore.go b/src/runtime/baremetal-unicore.go new file mode 100644 index 000000000..17b8f1021 --- /dev/null +++ b/src/runtime/baremetal-unicore.go @@ -0,0 +1,13 @@ +//go:build baremetal && tinygo.unicore + +package runtime + +import "runtime/interrupt" + +func atomicLock() interrupt.State { + return interrupt.Disable() +} + +func atomicUnlock(mask interrupt.State) { + interrupt.Restore(mask) +} diff --git a/src/runtime/runtime_rp2040.go b/src/runtime/runtime_rp2040.go index 1d36a771e..c55f0b787 100644 --- a/src/runtime/runtime_rp2040.go +++ b/src/runtime/runtime_rp2040.go @@ -4,8 +4,12 @@ package runtime import ( "device/arm" + "device/rp" "machine" "machine/usb/cdc" + "reflect" + "runtime/interrupt" + "unsafe" ) // machineTicks is provided by package machine. @@ -16,6 +20,8 @@ func machineLightSleep(uint64) type timeUnit int64 +const numCPU = 2 + // ticks returns the number of ticks (microseconds) elapsed since power up. func ticks() timeUnit { t := machineTicks() @@ -50,14 +56,18 @@ func waitForEvents() { } func putchar(c byte) { + mask := serialLock() machine.Serial.WriteByte(c) + serialUnlock(mask) } func getchar() byte { + mask := serialLock() for machine.Serial.Buffered() == 0 { Gosched() } v, _ := machine.Serial.ReadByte() + serialUnlock(mask) return v } @@ -71,9 +81,11 @@ func machineInit() func init() { machineInit() + mask := serialLock() cdc.EnableUSBCDC() machine.USBDev.Configure(machine.UARTConfig{}) machine.InitSerial() + serialUnlock(mask) } //export Reset_Handler @@ -82,3 +94,156 @@ func main() { run() exit(0) } + +func multicore_fifo_rvalid() bool { + return rp.SIO.FIFO_ST.Get()&rp.SIO_FIFO_ST_VLD != 0 +} + +func multicore_fifo_wready() bool { + return rp.SIO.FIFO_ST.Get()&rp.SIO_FIFO_ST_RDY != 0 +} + +func multicore_fifo_drain() { + for multicore_fifo_rvalid() { + rp.SIO.FIFO_RD.Get() + } +} + +func multicore_fifo_push_blocking(data uint32) { + for !multicore_fifo_wready() { + } + rp.SIO.FIFO_WR.Set(data) + arm.Asm("sev") +} + +func multicore_fifo_pop_blocking() uint32 { + for !multicore_fifo_rvalid() { + arm.Asm("wfe") + } + + return rp.SIO.FIFO_RD.Get() +} + +//go:extern __isr_vector +var __isr_vector [0]uint32 + +//go:extern _stack1_top +var _stack1_top [0]uint32 + +var core1StartSequence = [...]uint32{ + 0, 0, 1, + uint32(uintptr(unsafe.Pointer(&__isr_vector))), + uint32(uintptr(unsafe.Pointer(&_stack1_top))), + uint32(uintptr(reflect.ValueOf(runCore1).Pointer())), +} + +func startOtherCores() { + // Start the second core of the RP2040. + // See section 2.8.2 in the datasheet. + seq := 0 + for { + cmd := core1StartSequence[seq] + if cmd == 0 { + multicore_fifo_drain() + arm.Asm("sev") + } + multicore_fifo_push_blocking(cmd) + response := multicore_fifo_pop_blocking() + if cmd != response { + seq = 0 + continue + } + seq = seq + 1 + if seq >= len(core1StartSequence) { + break + } + } +} + +func runCore1() { + // Just blink a LED to show that this core is running. + // TODO: use a real scheduler. + led := machine.GP0 + led.Configure(machine.PinConfig{Mode: machine.PinOutput}) + const cycles = 7000_000 + for { + for i := 0; i < cycles; i++ { + led.Low() + } + + for i := 0; i < cycles; i++ { + led.High() + } + } +} + +func currentCPU() uint32 { + return rp.SIO.CPUID.Get() +} + +const ( + spinlockAtomic = iota + spinlockFutex + spinlockScheduler +) + +func atomicLockImpl() interrupt.State { + mask := interrupt.Disable() + for rp.SIO.SPINLOCK0.Get() == 0 { + } + return mask +} + +func atomicUnlockImpl(mask interrupt.State) { + rp.SIO.SPINLOCK0.Set(0) + interrupt.Restore(mask) +} + +func futexLock() interrupt.State { + // Disable interrupts. + // This is necessary since we might do some futex operations (like Wake) + // inside an interrupt and we don't want to deadlock with a non-interrupt + // goroutine that has taken the spinlock at the same time. + mask := interrupt.Disable() + + // Acquire the spinlock. + for rp.SIO.SPINLOCK1.Get() == 0 { + // Spin, until the lock is released. + } + + return mask +} + +func futexUnlock(mask interrupt.State) { + // Release the spinlock. + rp.SIO.SPINLOCK1.Set(0) + + // Restore interrupts. + interrupt.Restore(mask) +} + +var schedulerLockMasks [numCPU]interrupt.State + +// WARNING: doesn't check for deadlocks! +func schedulerLock() { + //schedulerLockMasks[currentCPU()] = interrupt.Disable() + for rp.SIO.SPINLOCK2.Get() == 0 { + } +} + +func schedulerUnlock() { + rp.SIO.SPINLOCK2.Set(0) + //interrupt.Restore(schedulerLockMasks[currentCPU()]) +} + +func serialLock() interrupt.State { + mask := interrupt.Disable() + for rp.SIO.SPINLOCK3.Get() == 0 { + } + return mask +} + +func serialUnlock(mask interrupt.State) { + rp.SIO.SPINLOCK3.Set(0) + interrupt.Restore(mask) +} diff --git a/src/runtime/scheduler_cores.go b/src/runtime/scheduler_cores.go new file mode 100644 index 000000000..9216cebac --- /dev/null +++ b/src/runtime/scheduler_cores.go @@ -0,0 +1,91 @@ +//go:build scheduler.cores + +package runtime + +import ( + "internal/task" + "unsafe" +) + +const hasScheduler = true + +const hasParallelism = true + +var ( + mainTask task.Task + cpuTasks [numCPU]*task.Task +) + +func deadlock() { + // Call yield without requesting a wakeup. + task.Pause() + trap() +} + +func scheduleTask(t *task.Task) { + task.Resume(t) +} + +func Gosched() { + // TODO +} + +// NumCPU returns the number of logical CPUs usable by the current process. +func NumCPU() int { + // Return the hardcoded number of physical CPU cores. + return numCPU +} + +func addTimer(tn *timerNode) { + runtimePanic("todo: timers") +} + +func removeTimer(t *timer) bool { + runtimePanic("todo: timers") + return false +} + +func schedulerRunQueue() *task.Queue { + println("todo: schedulerRunQueue") + for { + } + return nil +} + +// Pause the current task for a given time. +// +//go:linkname sleep time.Sleep +func sleep(duration int64) { + if duration <= 0 { + return + } + + wakeup := ticks() + nanosecondsToTicks(duration) + task.Sleep(uint64(wakeup)) +} + +func run() { + initHeap() + cpuTasks[0] = &mainTask + task.Init(&mainTask, (*uintptr)(unsafe.Pointer(&stackTopSymbol))) + initAll() + startOtherCores() + callMain() + mainExited = true +} + +func currentTask() *task.Task { + return cpuTasks[currentCPU()] +} + +func setCurrentTask(task *task.Task) { + cpuTasks[currentCPU()] = task +} + +func runtimeTicks() uint64 { + return uint64(ticks()) +} + +func runtimeSleepTicks(delay uint64) { + sleepTicks(timeUnit(delay)) +} diff --git a/targets/arm.ld b/targets/arm.ld index cdf5b1dd4..631ec9cab 100644 --- a/targets/arm.ld +++ b/targets/arm.ld @@ -32,6 +32,15 @@ SECTIONS _stack_top = .; } >RAM + /* Stack for second core (core 1), if there is one. This memory area won't be + * reserved if there is no second core. */ + .stack1 (NOLOAD) : + { + . = ALIGN(4); + . += _stack_size; + _stack1_top = .; + } >RAM + /* Start address (in flash) of .data, used by startup code. */ _sidata = LOADADDR(.data); diff --git a/tools/gen-critical-atomics/gen-critical-atomics.go b/tools/gen-critical-atomics/gen-critical-atomics.go index 75ea32707..f949a7933 100644 --- a/tools/gen-critical-atomics/gen-critical-atomics.go +++ b/tools/gen-critical-atomics/gen-critical-atomics.go @@ -26,7 +26,6 @@ package runtime import ( _ "unsafe" - "runtime/interrupt" ) // Documentation: @@ -41,29 +40,29 @@ import ( func __atomic_load_{{.}}(ptr *uint{{$bits}}, ordering uintptr) uint{{$bits}} { // The LLVM docs for this say that there is a val argument after the pointer. // That is a typo, and the GCC docs omit it. - mask := interrupt.Disable() + mask := atomicLock() val := *ptr - interrupt.Restore(mask) + atomicUnlock(mask) return val } {{end}} {{- define "store"}}{{$bits := mul . 8 -}} //export __atomic_store_{{.}} func __atomic_store_{{.}}(ptr *uint{{$bits}}, val uint{{$bits}}, ordering uintptr) { - mask := interrupt.Disable() + mask := atomicLock() *ptr = val - interrupt.Restore(mask) + atomicUnlock(mask) } {{end}} {{- define "cas"}}{{$bits := mul . 8 -}} //go:inline func doAtomicCAS{{$bits}}(ptr *uint{{$bits}}, expected, desired uint{{$bits}}) uint{{$bits}} { - mask := interrupt.Disable() + mask := atomicLock() old := *ptr if old == expected { *ptr = desired } - interrupt.Restore(mask) + atomicUnlock(mask) return old } @@ -82,10 +81,10 @@ func __atomic_compare_exchange_{{.}}(ptr, expected *uint{{$bits}}, desired uint{ {{- define "swap"}}{{$bits := mul . 8 -}} //go:inline func doAtomicSwap{{$bits}}(ptr *uint{{$bits}}, new uint{{$bits}}) uint{{$bits}} { - mask := interrupt.Disable() + mask := atomicLock() old := *ptr *ptr = new - interrupt.Restore(mask) + atomicUnlock(mask) return old } @@ -111,11 +110,11 @@ func __atomic_exchange_{{.}}(ptr *uint{{$bits}}, new uint{{$bits}}, ordering uin //go:inline func {{$opfn}}(ptr *{{$type}}, value {{$type}}) (old, new {{$type}}) { - mask := interrupt.Disable() + mask := atomicLock() old = *ptr {{$opdef}} *ptr = new - interrupt.Restore(mask) + atomicUnlock(mask) return old, new }