From b6c5bfe1ea1240c936683c42e16f32f92e3062b5 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Tue, 9 Sep 2025 20:13:09 -0400 Subject: [PATCH 1/6] fix(rp2350): add software spinlocks --- src/runtime/runtime_rp2.go | 25 --------------- src/runtime/runtime_rp2040.go | 25 +++++++++++++++ src/runtime/runtime_rp2350.go | 58 +++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 25 deletions(-) diff --git a/src/runtime/runtime_rp2.go b/src/runtime/runtime_rp2.go index 93e488d02a..732d4ddbca 100644 --- a/src/runtime/runtime_rp2.go +++ b/src/runtime/runtime_rp2.go @@ -297,31 +297,6 @@ var ( futexLock = spinLock{id: 3} ) -// A hardware spinlock, one of the 32 spinlocks defined in the SIO peripheral. -type spinLock struct { - id uint8 -} - -// Return the spinlock register: rp.SIO.SPINLOCKx -func (l *spinLock) spinlock() *volatile.Register32 { - return (*volatile.Register32)(unsafe.Add(unsafe.Pointer(&rp.SIO.SPINLOCK0), l.id*4)) -} - -func (l *spinLock) Lock() { - // Wait for the lock to be available. - spinlock := l.spinlock() - for spinlock.Get() == 0 { - // TODO: use wfe and send an event when unlocking so the CPU can go to - // sleep while waiting for the lock. - // Unfortunately when doing that, time.Sleep() seems to hang somewhere. - // This needs some debugging to figure out. - } -} - -func (l *spinLock) Unlock() { - l.spinlock().Set(0) -} - // Wait until a signal is received, indicating that it can resume from the // spinloop. func spinLoopWait() { diff --git a/src/runtime/runtime_rp2040.go b/src/runtime/runtime_rp2040.go index 2ca3605e03..d34d10c1f8 100644 --- a/src/runtime/runtime_rp2040.go +++ b/src/runtime/runtime_rp2040.go @@ -10,3 +10,28 @@ const ( sioIrqFifoProc0 = rp.IRQ_SIO_IRQ_PROC0 sioIrqFifoProc1 = rp.IRQ_SIO_IRQ_PROC1 ) + +// A hardware spinlock, one of the 32 spinlocks defined in the SIO peripheral. +type spinLock struct { + id uint8 +} + +// Return the spinlock register: rp.SIO.SPINLOCKx +func (l *spinLock) spinlock() *volatile.Register32 { + return (*volatile.Register32)(unsafe.Add(unsafe.Pointer(&rp.SIO.SPINLOCK0), l.id*4)) +} + +func (l *spinLock) Lock() { + // Wait for the lock to be available. + spinlock := l.spinlock() + for spinlock.Get() == 0 { + // TODO: use wfe and send an event when unlocking so the CPU can go to + // sleep while waiting for the lock. + // Unfortunately when doing that, time.Sleep() seems to hang somewhere. + // This needs some debugging to figure out. + } +} + +func (l *spinLock) Unlock() { + l.spinlock().Set(0) +} diff --git a/src/runtime/runtime_rp2350.go b/src/runtime/runtime_rp2350.go index 91af23212e..facf88b680 100644 --- a/src/runtime/runtime_rp2350.go +++ b/src/runtime/runtime_rp2350.go @@ -3,6 +3,7 @@ package runtime import ( + "device/arm" "device/rp" ) @@ -14,3 +15,60 @@ const ( sioIrqFifoProc0 = rp.IRQ_SIO_IRQ_FIFO sioIrqFifoProc1 = rp.IRQ_SIO_IRQ_FIFO ) + +// Due to hardware errata RP2350-E2 spinlocks are emulated in software as in pico-sdk. +type spinLock struct { + // lock field must be first field so its address is the same as the struct address. + lock uint8 + id uint8 + uint16 // Padding to prevent false sharing +} + +func (l *spinLock) Lock() { + // Original reference: + // https://github.com/raspberrypi/pico-sdk/blob/2.2.0/src/rp2_common/hardware_sync_spin_lock/include/hardware/sync/spin_lock.h#L112 + + // r0 is automatically filled with the pointer value "l" here. + // We create a variable to allow access the lock byte and avoid a memory + // fault when accessing l.lock in assembly. + lock := &l.lock + _ = lock + + // Set a loop start point + arm.Asm("1:") + // Exclusively load the state variable (l.lock) and put its value in r2. + arm.Asm("ldaexb r2, [r0]") + // Store the "locked" state value (1) into r1 to keep things moving. + arm.Asm("movs r1, #1") + // Check if the lock was already taken (r2 != 0). + arm.Asm("cmp r2, #0") + // Jump back to "1:" if the lock is already held + arm.Asm("bne 1b") + + // Attempt to store '1' into the lock address. + // The return code (0 for success, 1 for failure) is placed in r2. + arm.Asm("strexb r2, r1, [r0]") + // Check if the result was successful (r2 == 0). + arm.Asm("cmp r2, #0") + // Jump back to "1:" if the lock was not acquired. + arm.Asm("bne 1b") + + // Memory barrier to ensure everyone knows we're holding the lock now. + arm.Asm("dmb") +} + +func (l *spinLock) Unlock() { + // Original reference: + // https://github.com/raspberrypi/pico-sdk/blob/2.2.0/src/rp2_common/hardware_sync_spin_lock/include/hardware/sync/spin_lock.h#L197 + + // r0 is automatically filled with the pointer value l here. + // We create a variable to allow access the lock byte and avoid a memory + // fault when accessing l.lock in assembly. + lock := &l.lock + _ = lock + // Fill r1 with 0 and store it to the lock address in r0. stlb requires a + // register as a source so we can't use a literal 0 directly. + arm.Asm("movs r1, #0") + // Release the pseudo-spinlock by writing 0 to and releasing l.lock. + arm.Asm("stlb r1, [r0]") +} From c0a5f508382482efd094c07517c58c7f5261754b Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Tue, 9 Sep 2025 21:52:08 -0400 Subject: [PATCH 2/6] chore: clarify comments --- src/runtime/runtime_rp2350.go | 40 +++++++++++++++++------------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/runtime/runtime_rp2350.go b/src/runtime/runtime_rp2350.go index facf88b680..b9b5decd2a 100644 --- a/src/runtime/runtime_rp2350.go +++ b/src/runtime/runtime_rp2350.go @@ -18,9 +18,9 @@ const ( // Due to hardware errata RP2350-E2 spinlocks are emulated in software as in pico-sdk. type spinLock struct { - // lock field must be first field so its address is the same as the struct address. - lock uint8 - id uint8 + // state field must be first field so its address is the same as the struct address. + state uint8 + id uint8 uint16 // Padding to prevent false sharing } @@ -29,28 +29,28 @@ func (l *spinLock) Lock() { // https://github.com/raspberrypi/pico-sdk/blob/2.2.0/src/rp2_common/hardware_sync_spin_lock/include/hardware/sync/spin_lock.h#L112 // r0 is automatically filled with the pointer value "l" here. - // We create a variable to allow access the lock byte and avoid a memory - // fault when accessing l.lock in assembly. - lock := &l.lock - _ = lock + // We create a variable to permit access to the state byte (l.state) and + // avoid a memory fault when accessing it in assembly. + state := &l.state + _ = state - // Set a loop start point + // Set the loop start point. arm.Asm("1:") - // Exclusively load the state variable (l.lock) and put its value in r2. + // Exclusively load (lock) the state byte and put its value in r2. arm.Asm("ldaexb r2, [r0]") - // Store the "locked" state value (1) into r1 to keep things moving. + // Set the r1 register to '1' for later use. arm.Asm("movs r1, #1") // Check if the lock was already taken (r2 != 0). arm.Asm("cmp r2, #0") - // Jump back to "1:" if the lock is already held + // Jump back to the loop start ("1:") if the lock is already held. arm.Asm("bne 1b") - // Attempt to store '1' into the lock address. + // Attempt to store '1' into the lock state byte. // The return code (0 for success, 1 for failure) is placed in r2. arm.Asm("strexb r2, r1, [r0]") // Check if the result was successful (r2 == 0). arm.Asm("cmp r2, #0") - // Jump back to "1:" if the lock was not acquired. + // Jump back to the loop start ("1:") if the lock was not acquired. arm.Asm("bne 1b") // Memory barrier to ensure everyone knows we're holding the lock now. @@ -62,13 +62,13 @@ func (l *spinLock) Unlock() { // https://github.com/raspberrypi/pico-sdk/blob/2.2.0/src/rp2_common/hardware_sync_spin_lock/include/hardware/sync/spin_lock.h#L197 // r0 is automatically filled with the pointer value l here. - // We create a variable to allow access the lock byte and avoid a memory - // fault when accessing l.lock in assembly. - lock := &l.lock - _ = lock - // Fill r1 with 0 and store it to the lock address in r0. stlb requires a - // register as a source so we can't use a literal 0 directly. + // We create a variable to permit access to the state byte (l.state) and + // avoid a memory fault when accessing it in assembly. + state := &l.state + _ = state + // Fill r1 with 0 and store it to the state byte address in r0. stlb + // requires a register as a source so we can't use a literal 0 directly. arm.Asm("movs r1, #0") - // Release the pseudo-spinlock by writing 0 to and releasing l.lock. + // Release the pseudo-spinlock by writing 0 to and releasing l.state. arm.Asm("stlb r1, [r0]") } From 93f5e688b5725442ea9ba7114448a5cd97dedd55 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Tue, 9 Sep 2025 22:10:43 -0400 Subject: [PATCH 3/6] chore: fix rp2040 imports --- src/runtime/runtime_rp2040.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/runtime/runtime_rp2040.go b/src/runtime/runtime_rp2040.go index d34d10c1f8..feaf3f032e 100644 --- a/src/runtime/runtime_rp2040.go +++ b/src/runtime/runtime_rp2040.go @@ -4,6 +4,8 @@ package runtime import ( "device/rp" + "runtime/volatile" + "unsafe" ) const ( From 7cb6017900f6ca222ac92aead585e4afede5b3ae Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Wed, 10 Sep 2025 18:58:50 -0400 Subject: [PATCH 4/6] chore: use atomics instead of artisinal assembly --- src/runtime/runtime_rp2350.go | 59 +++++++---------------------------- 1 file changed, 12 insertions(+), 47 deletions(-) diff --git a/src/runtime/runtime_rp2350.go b/src/runtime/runtime_rp2350.go index b9b5decd2a..aa6c38a188 100644 --- a/src/runtime/runtime_rp2350.go +++ b/src/runtime/runtime_rp2350.go @@ -3,8 +3,8 @@ package runtime import ( - "device/arm" "device/rp" + "sync/atomic" ) const ( @@ -18,57 +18,22 @@ const ( // Due to hardware errata RP2350-E2 spinlocks are emulated in software as in pico-sdk. type spinLock struct { - // state field must be first field so its address is the same as the struct address. - state uint8 - id uint8 - uint16 // Padding to prevent false sharing + atomic.Uint32 } func (l *spinLock) Lock() { - // Original reference: - // https://github.com/raspberrypi/pico-sdk/blob/2.2.0/src/rp2_common/hardware_sync_spin_lock/include/hardware/sync/spin_lock.h#L112 - - // r0 is automatically filled with the pointer value "l" here. - // We create a variable to permit access to the state byte (l.state) and - // avoid a memory fault when accessing it in assembly. - state := &l.state - _ = state - - // Set the loop start point. - arm.Asm("1:") - // Exclusively load (lock) the state byte and put its value in r2. - arm.Asm("ldaexb r2, [r0]") - // Set the r1 register to '1' for later use. - arm.Asm("movs r1, #1") - // Check if the lock was already taken (r2 != 0). - arm.Asm("cmp r2, #0") - // Jump back to the loop start ("1:") if the lock is already held. - arm.Asm("bne 1b") - - // Attempt to store '1' into the lock state byte. - // The return code (0 for success, 1 for failure) is placed in r2. - arm.Asm("strexb r2, r1, [r0]") - // Check if the result was successful (r2 == 0). - arm.Asm("cmp r2, #0") - // Jump back to the loop start ("1:") if the lock was not acquired. - arm.Asm("bne 1b") - - // Memory barrier to ensure everyone knows we're holding the lock now. - arm.Asm("dmb") + // Try to replace 0 with 1. Once we succeed, the lock has been acquired. + for !l.Uint32.CompareAndSwap(0, 1) { + spinLoopWait() + } } func (l *spinLock) Unlock() { - // Original reference: - // https://github.com/raspberrypi/pico-sdk/blob/2.2.0/src/rp2_common/hardware_sync_spin_lock/include/hardware/sync/spin_lock.h#L197 + // Safety check: the spinlock should have been locked. + if schedulerAsserts && l.Uint32.Load() != 1 { + runtimePanic("unlock of unlocked spinlock") + } - // r0 is automatically filled with the pointer value l here. - // We create a variable to permit access to the state byte (l.state) and - // avoid a memory fault when accessing it in assembly. - state := &l.state - _ = state - // Fill r1 with 0 and store it to the state byte address in r0. stlb - // requires a register as a source so we can't use a literal 0 directly. - arm.Asm("movs r1, #0") - // Release the pseudo-spinlock by writing 0 to and releasing l.state. - arm.Asm("stlb r1, [r0]") + // Unlock the lock. Simply write 0, because we already know it is locked. + l.Uint32.Store(0) } From 521d5a4ed72749a2840d4c76cab5668fbdce0383 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Wed, 10 Sep 2025 19:00:44 -0400 Subject: [PATCH 5/6] chore: comment cleanup --- src/runtime/runtime_rp2350.go | 1 - 1 file changed, 1 deletion(-) diff --git a/src/runtime/runtime_rp2350.go b/src/runtime/runtime_rp2350.go index aa6c38a188..09507ccd5c 100644 --- a/src/runtime/runtime_rp2350.go +++ b/src/runtime/runtime_rp2350.go @@ -16,7 +16,6 @@ const ( sioIrqFifoProc1 = rp.IRQ_SIO_IRQ_FIFO ) -// Due to hardware errata RP2350-E2 spinlocks are emulated in software as in pico-sdk. type spinLock struct { atomic.Uint32 } From 1f2642c08b4800d05780ac742c6779841f1cb82d Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Wed, 10 Sep 2025 19:16:21 -0400 Subject: [PATCH 6/6] fix: add id field for hw spinlock compatibility --- src/runtime/runtime_rp2350.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/runtime/runtime_rp2350.go b/src/runtime/runtime_rp2350.go index 09507ccd5c..bb1b477fda 100644 --- a/src/runtime/runtime_rp2350.go +++ b/src/runtime/runtime_rp2350.go @@ -18,6 +18,7 @@ const ( type spinLock struct { atomic.Uint32 + id uint8 } func (l *spinLock) Lock() {