Skip to content

sd: add SDFlash, a SoftDevice-safe flash block device#463

Open
sago35 wants to merge 1 commit into
devfrom
sdflash
Open

sd: add SDFlash, a SoftDevice-safe flash block device#463
sago35 wants to merge 1 commit into
devfrom
sdflash

Conversation

@sago35

@sago35 sago35 commented Jul 21, 2026

Copy link
Copy Markdown
Member

Bond storage already needed sd_flash_write/sd_flash_page_erase instead of direct NVMC access while the SoftDevice is enabled. Expose that as SDFlash, a machine.BlockDevice-compatible type applications can use to store their own data (for example a keymap) on the same flash region, without conflicting with the SoftDevice. machine.Flash's own SoftDevice-aware path does not work here: it polls sd_evt_get for the completion event, but this package's SWI2 handler drains that queue first, so machine.Flash would wait forever.

Flash operations complete asynchronously and share a single result flag (flashOpResult) between callers, so SDFlash and bond storage must not run concurrently; serialize them with flashOpMu.

Bond storage already needed sd_flash_write/sd_flash_page_erase instead
of direct NVMC access while the SoftDevice is enabled. Expose that as
SDFlash, a machine.BlockDevice-compatible type applications can use to
store their own data (for example a keymap) on the same flash region,
without conflicting with the SoftDevice. machine.Flash's own
SoftDevice-aware path does not work here: it polls sd_evt_get for the
completion event, but this package's SWI2 handler drains that queue
first, so machine.Flash would wait forever.

Flash operations complete asynchronously and share a single result
flag (flashOpResult) between callers, so SDFlash and bond storage must
not run concurrently; serialize them with flashOpMu.
@deadprogram

Copy link
Copy Markdown
Member

@sago35 isn't this something it would be best to solve in https://github.com/tinygo-org/tinygo/blob/dev/src/machine/machine_nrf.go#L400 itself? Since apparently that is not working?

@sago35

sago35 commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

@deadprogram I agree that machine.Flash is the appropriate place for this, but since it cannot be fixed solely within the machine package, this PR adds a temporary fix that works on released TinyGo versions. As noted in the comments below, because of its relationship with sd_evt_get, it cannot be handled by machine.Flash as it is currently.

// While the SoftDevice is enabled, direct NVMC access is forbidden. Note
// that machine.Flash's own SoftDevice-aware path also does not work together
// with this package: it polls sd_evt_get for the completion SOC event, but
// the SWI2 interrupt handler in adapter_sd.go drains those events first, so
// machine.Flash waits forever. Use this type instead after Adapter.Enable.
// Its methods must be called from goroutine context, not from an interrupt.

@sago35

sago35 commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

I wrote two programs. The first one uses machine.Flash, but unfortunately, erase does not complete.

// machine.Flash hangs forever once the bluetooth SoftDevice backend is
// enabled: its completion event is drained by this package's interrupt
// handler, so the sd_evt_get poll in machine_nrf.go never sees it.
//
//	tinygo flash -monitor -target=xiao-ble ./repro-machineflash
//
// Expected: "erasing..." is printed, then nothing more (the heartbeat
// stops too, since the poll loop never yields).
package main

import (
	"machine"
	"time"

	"tinygo.org/x/bluetooth"
)

func main() {
	time.Sleep(3 * time.Second)

	go func() {
		for {
			println("heartbeat")
			time.Sleep(time.Second)
		}
	}()

	println("enabling the SoftDevice...")
	if err := bluetooth.DefaultAdapter.Enable(); err != nil {
		panic("failed to enable BLE stack: " + err.Error())
	}

	var dev = machine.Flash
	// A page in the middle of the flash data area, away from data other
	// programs may keep at either end (bonds live in the last page).
	block := dev.Size() / dev.EraseBlockSize() / 2

	println("erasing one page via machine.Flash...")
	if err := dev.EraseBlocks(block, 1); err != nil {
		println("erase failed:", err.Error())
		return
	}
	println("...erase done") // never reached

	for {
		time.Sleep(time.Second)
	}
}

The second one uses SDFlash, and this one works correctly.

// The same flash operation as repro-machineflash, but through
// bluetooth.SDFlash, whose completion event is delivered by this package's
// own interrupt handler: it completes.
//
//	tinygo flash -monitor -target=xiao-ble ./repro-sdflash
//
// Expected: the erase completes and the heartbeat keeps printing.
package main

import (
	"time"

	"tinygo.org/x/bluetooth"
)

func main() {
	time.Sleep(3 * time.Second)

	go func() {
		for {
			println("heartbeat")
			time.Sleep(time.Second)
		}
	}()

	println("enabling the SoftDevice...")
	if err := bluetooth.DefaultAdapter.Enable(); err != nil {
		panic("failed to enable BLE stack: " + err.Error())
	}

	var dev bluetooth.SDFlash

	// A page in the middle of the flash data area, away from data other
	// programs may keep at either end (bonds live in the last page).
	block := dev.Size() / dev.EraseBlockSize() / 2

	println("erasing one page via bluetooth.SDFlash...")
	if err := dev.EraseBlocks(block, 1); err != nil {
		println("erase failed:", err.Error())
		return
	}
	println("...erase done")

	for {
		time.Sleep(time.Second)
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants