Skip to content

machine: add BOOTSEL button support for RP2040 and RP2350 - #5865

Open
rdon-key wants to merge 1 commit into
tinygo-org:devfrom
rdon-key:feture/rp2040-bootselsw
Open

rdon-key wants to merge 1 commit into
tinygo-org:devfrom
rdon-key:feture/rp2040-bootselsw

Conversation

@rdon-key

@rdon-key rdon-key commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Add machine.BootselButton() to read the BOOTSEL button state on RP2040 and RP2350 boards.

Reading the BOOTSEL button requires temporarily overriding the QSPI flash chip-select pin, which makes flash inaccessible during the read. The implementation uses the existing RP2 flash-safe mechanism to prevent the other core from accessing flash during this operation.

The low-level reading function runs from RAM and follows the official Raspberry Pi Pico BOOTSEL example.

API note

This adds a new exported function, machine.BootselButton() bool, to the machine package for RP2040 and RP2350 targets.

Many RP2040 and RP2350 boards have a BOOTSEL button. On boards such as the Raspberry Pi Pico and Pico 2, it is the only user button. However, it cannot be read like a normal GPIO: reading it requires taking over the QSPI flash chip-select pin, running code from RAM, and keeping the other core away from flash. This is hard to do safely from application code, so I think it belongs in the machine package.

I'm happy to change the name or signature if there is a preferred convention.

Testing

Tested on physical Raspberry Pi Pico and Pico 2 boards.

Raspberry Pi Pico

  • -scheduler=tasks: PASS
  • -scheduler=cores: PASS
  • BOOTSEL press and release detection: PASS
  • Repeated button presses under concurrent goroutine workload: PASS

Raspberry Pi Pico 2

  • -scheduler=tasks: PASS
  • -scheduler=cores: PASS
  • BOOTSEL press and release detection: PASS
  • Repeated button presses under concurrent goroutine workload: PASS

Basic test

package main

import (
    "machine"
    "time"
)

func main() {
    for {
        println("BOOTSEL:", machine.BootselButton())
        time.Sleep(500 * time.Millisecond)
    }
}

Run on RP2040 with:

tinygo flash -target pico -scheduler=tasks -monitor main.go

Run on RP2350 with:

tinygo flash -target pico2 -scheduler=tasks -monitor main.go

Multicore workload test

package main

import (
    "machine"
    "sync/atomic"
    "time"
)

var heartbeat uint32

//go:noinline
func workload(n uint32) uint32 {
    n ^= n << 13
    n ^= n >> 17
    n ^= n << 5
    return n
}

func worker() {
    var n uint32 = 1
    for {
        for i := 0; i < 10000; i++ {
            n = workload(n)
        }
        atomic.AddUint32(&heartbeat, 1)
    }
}

func main() {
    go worker()

    var previous uint32
    for {
        pressed := machine.BootselButton()

        current := atomic.LoadUint32(&heartbeat)
        delta := current - previous
        previous = current

        println("BOOTSEL:", pressed, "heartbeat delta:", delta)
        time.Sleep(500 * time.Millisecond)
    }
}

Run on RP2040 with:

tinygo flash -target pico -scheduler=cores -monitor main_cores.go

Run on RP2350 with:

tinygo flash -target pico2 -scheduler=cores -monitor main_cores.go

Example output on Pico:

BOOTSEL: false heartbeat delta: 545356
BOOTSEL: false heartbeat delta: 545355
BOOTSEL: true heartbeat delta: 545353
BOOTSEL: true heartbeat delta: 545349
BOOTSEL: true heartbeat delta: 545340
BOOTSEL: false heartbeat delta: 545351
BOOTSEL: false heartbeat delta: 545358

Example output on Pico 2:

BOOTSEL: false heartbeat delta: 8644369
BOOTSEL: true heartbeat delta: 8644375
BOOTSEL: true heartbeat delta: 8644348
BOOTSEL: true heartbeat delta: 8644272
BOOTSEL: false heartbeat delta: 8644349

BOOTSEL press and release detection worked while the worker goroutine continued making progress. No hangs or failures were observed.

Note: this test does not verify that the worker goroutine actually runs on the second core.

@rdon-key
rdon-key marked this pull request as draft October 13, 2026 12:28
@rdon-key

Copy link
Copy Markdown
Contributor Author

I’m moving this back to draft for now.

After looking into RP2350 more closely, it seems the BOOTSEL handling is much closer to RP2040 than I initially thought, and the relevant errata does not affect the QSPI pads.

I’m going to add RP2350 support and test it on a Pico 2 before marking this ready for review again.

Add machine.BootselButton() to read the BOOTSEL button state on RP2040
and RP2350 boards.

Reading the button requires temporarily overriding the QSPI flash chip-select
pin, which makes flash inaccessible during the read. The implementation uses
the existing RP2 flash-safe mechanism to prevent the other core from accessing
flash during the operation.

The low-level reading function runs from RAM and follows the official
Raspberry Pi Pico BOOTSEL example.
@rdon-key
rdon-key force-pushed the feture/rp2040-bootselsw branch from 74e957f to cf08b34 Compare October 13, 2026 15:35
@rdon-key rdon-key changed the title machine: add RP2040 BOOTSEL button support machine: add BOOTSEL button support for RP2040 and RP2350 Oct 13, 2026
@rdon-key
rdon-key marked this pull request as ready for review October 14, 2026 02:29
@rdon-key

Copy link
Copy Markdown
Contributor Author

RP2350 support has been added and tested on a physical Pico 2 with both scheduler=tasks and scheduler=cores. CI is also passing, so I’ve marked this ready for review again.

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.

1 participant