There is an episode of Ren and Stimpy with a big red “history eraser’ button that must not be pressed. Of course, who can resist the temptation of pressing the unpressable button? The same goes for development boards. If there is a button on there, you want to read it in your code, right? The Raspberry Pi Pico is a bit strange in that regard. The standard one lacks a reset button, but there is a big tantalizing button to reset in bootloader mode. You only use it when you power up, so why not read it in your code? Why not, indeed?
Turns out, that button isn’t what you think it is. It isn’t connected to a normal CPU pin at all. Instead, it connects to the flash memory chip. So does that mean you can’t read it at all? Not exactly. There’s good news, and then there’s bad news.
THE GOOD NEWS
The official Raspberry Pi examples show how to read the button (you have read all the examples, right?). You can convert the flash’s chip-select into an input temporarily and try to figure out if the pin is low, meaning that the button is pushed. Sounds easy, right?
THE BAD NEWS
The bad news is really bad. When you switch the flash chip-select to an input, you will lose access to the flash memory. But we are running from the flash memory! So, the first thing to think about is that the code will need to run from RAM.
But that’s not all. The Pico has interrupts and two CPU cores. So even if you are out of the flash memory, there’s no reason to assume someone else won’t want to use it simultaneously. So, to make this work, you need to disable interrupts and shut down the other CPU core while you read the pin.
THE EXAMPLE
Looking at the example, they do everything but disable the second core. I recently had to put this code in an Arduino-style program; there is excellent Arduino support for the Pico. Putting the function in RAM is pretty easy:
1
|
bool __no_inline_not_in_flash_func(_get_bootsel_button)() { |
The rest is just manipulating the I/O pins and turning interrupts on and off:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
uint32_t flags = save_and_disable_interrupts(); // Set chip select to Hi-Z hw_write_masked(&ioqspi_hw->io[CS_PIN_INDEX].ctrl, GPIO_OVERRIDE_LOW << IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB, IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS); // Note we can't call into any sleep functions in flash right now for (volatile int i = 0; i< 1000; ++i); // The HI GPIO registers in SIO can observe and control the 6 QSPI pins. // Note the button pulls the pin *low* when pressed. bool button_state = !(sio_hw->gpio_hi_in & (1u << CS_PIN_INDEX)); // Need to restore the state of chip select, else we are going to have a // bad time when we return to code in flash! hw_write_masked(&ioqspi_hw->io[CS_PIN_INDEX].ctrl, GPIO_OVERRIDE_NORMAL << IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB, IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS); restore_interrupts(flags); |
That leaves the core. I put a wrapper around this function to avoid any possible problems with it being called from RAM (though it would probably work):
1
2
3
4
5
6
7
8
9
10
|
bool get_bootsel_button( void ) { bool rv; // freeze rp2040.idleOtherCore(); rv = _get_bootsel_button(); // unfreeze rp2040.resumeOtherCore(); return rv; } |
It works. But I do worry about how inefficient it must be. You usually want to poll a button often. Turning off the other core, disabling interrupts, and the idle loop to let the pin settle — all that will take time. In practice, it seems to work OK, but it must be slowing things down some.
IN RETROSPECT…
So, can you read the Pico button? Yes. Should you? Maybe. For some applications, it is probably just fine. But if you are worried about performance, it probably isn’t the best idea.
With two 133 MHz cores, a ton of memory, easy debugging, and those cool peripheral processors, there’s a lot to love about the Pico. Just maybe not the BOOTSEL button.
Source: BUTTON, BUTTON, WHO’S GOT THE (PICO) BUTTON?