RP2 - PIO 'remove_programs()' does not correctly remove/de-reference StateMachine code
Port, board and/or hardware
rp2
MicroPython version
MicroPython v1.26.1 on 2025-09-11; Raspberry Pi Pico with RP2040
Reproduction
Minimal example showing issue:
pio_load_test.py.txt
Expected behaviour
When alternately loading PIO StateMachine code, microPython loads the wrong (previously loaded) code.
Example code alternates between 'mac-one' and 'mac-two'; on the 3rd iteration we expect 'mac-one' to be loaded, but instead 'mac-two' is placed (or left) in PIO address space... and that code loads '2' into the RX FIFO.
MicroPython v1.26.1 on 2025-09-11; Raspberry Pi Pico with RP2040
Type "help()" for more information.
>>> %Run -c $EDITOR_CONTENT
MPY: soft reboot
Loading primary: mac-one
0x502000d4 : 0x0000001c = 0x0000e021
0x502000ec : 0x0000000b = 0x00006026
0x50200104 : 0x00000006 = 0x00007e01
0x5020011c : 0x00000001 = 0x0000ae08
RX FIFO = 1
Loading alternate: mac-two
0x502000d4 : 0x0000001c = 0x0000e022
0x502000ec : 0x0000000b = 0x00006026
0x50200104 : 0x00000006 = 0x00007e01
0x5020011c : 0x00000001 = 0x0000ae08
RX FIFO = 2
Loading primary: mac-one
0x502000d4 : 0x0000001c = 0x0000e022
0x502000ec : 0x0000000b = 0x00006026
0x50200104 : 0x00000006 = 0x00007e01
0x5020011c : 0x00000001 = 0x0000ae08
RX FIFO = 2
The code also 'peeks' (mem32) the address space related to PIO0 and we can see the start address and the first instruction (as complied value).
>>> print("0x%8.8x" % rp2.asm_pio_encode("set(x,1)", 0))
0x0000e021
>>> print("0x%8.8x" % rp2.asm_pio_encode("set(x,2)", 0))
0x0000e022
Observed behaviour
Example code alternates between 'mac-one' and 'mac-two'; on the 3rd iteration we expect 'mac-one' to be loaded, but instead 'mac-two' is placed (or left) in PIO address space... and that code loads '2' into the RX FIFO.
Additional Information
As a workaround test script appears to correctly work if you implicitly specify the name of the StateMachine(s) to remove
# clean-up
rp2.PIO(0).remove_program(mac_one)
rp2.PIO(0).remove_program(mac_two)
rp2.PIO(0).remove_program()
Docs suggest this should not be necessary.
https://docs.micropython.org/en/latest/library/rp2.PIO.html#rp2.PIO.remove_program
Code of Conduct
Yes, I agree
rp2: Fix stale program offset cache after remove_program().
PIO.remove_program() without arguments clears SDK instruction-memory tracking but did not reset cached offsets in program objects. A subsequent StateMachine init using such a program would see the non-negative offset, assume the program was still loaded, and skip re-writing the instructions to PIO instruction memory.
Fix by validating the cached offset against the internal usage mask before trusting it. If the bit is clear the program is not present, so invalidate the stale offset and reload.
Summary
I discovered this issue when writing the PIO test suite in #18974; it caused a hang in test_restart.
Testing
I tested this on both RPI_PICO and RPI_PICO2 boards using the test suite from #18974 (and new PIO assembler support from #18975).
Generative AI
I used generative AI tools when creating this PR, but a human has checked the
code and is responsible for the code and the description above.