- Getting to a clean slate. Will have nothing, or as little as possible, on the pico (perhaps less than original?).
- Learning how to inspect the QSPI-Flash.
- Connecting to the pico board and running the micropython repl.
- Wiping the littlefs filesystem on QSPI Flash from within the repl.
- Nuking the firmware section of QSPI Flash from usb-drive mode.
- Flashing an older version of micropython firmware, which is useful enough to inspect flash.
- Intro to some home-rolled tools "rp2040comb" useful for inspecting flash from the repl.
- Flashing current version of micropython firmware, then inspecting the differences.
- Comparing the "firmware" section of QSPI Flash against a micropython-firmware.uf2 file.
Do feel free to skip any of the above steps or to perform them in any order you choose. Note: These are destructive.
- rp2040 Raspberry Pi Pico: I'm using a Pico-W.
- usb to micro-usb cable: to power the board and to physically connect from your computer to the board.
- terminal client: on your computer to communicate with the board;
rshell
,screen
andminicom
work fine (/dev/ttyACM0), I'm using minicom.
Physically connect the pico to your computer via usb to micro-usb cable.
From within a terminal window, see if it's recognized w/ lsusb
:
...
Bus 003 Device 047: ID 2e8a:0005 MicroPython Board in FS mode
...
If seen, the pico board should be accessible via /dev/ttyACM0
, confirm that this is the case with ls -l /dev/ttyACM0
:
crw-rw---- 1 root dialout 166, 0 Jan 13 09:18 /dev/ttyACM0
In the above case, it's readable/writeable by root and by users within the 'dialout' group. Do whatever you need to fit that criteria.
Connect to the board so you can see the micropython repl with minicom -D/dev/ttyACM0 -b115200 -w
Welcome to minicom 2.8
OPTIONS: I18n
Port /dev/ttyACM0, 09:59:46
Press CTRL-A Z for help on special keys
Yours will look different. You may see output scrolling -- depending on what is running.
If nothing is running, then hitting <enter>
will give you the familiar >>>
repl prompt.
Otherwise, you may need to hit CTRL-C
to interrupt whatever is running, then you'll see the repl:
Traceback (most recent call last):
File "main.py", line ...
...
KeyboardInterrupt:
MicroPython v1.22.1 on 2024-01-05; Raspberry Pi Pico W with RP2040
Type "help()" for more information.
>>>
Now you have a "hint" of which micropython firmware is running; maybe avoid high-hopes in the usefulness of "help()".
The uos (micropython's "os" module) can show you what files are in the QSPI-Flash filesystem:
>>> import uos
>>> uos.getcwd()
'/'
>>> uos.listdir()
[]
>>>
Yours will look different. In my case, uos.listdir()
"hints" that the filesystem is empty, but that's just hearsay.
Explore as you like.
Later, to disconnect minicom from the pico, exit w/ CTRL-A q <enter>
You could use any number of tools to remove files from the QSPI-Flash filesystem, but here we will use the rp2.Flash
class which has block-level access to efficient "erase" functionality on the flash chip itself. Specifically, we will
use .ioctl(4)
, .ioctl(5)
, and .ioctl(6)
to know how-many-blocks, size-of-blocks, and to erase-particular-blocks, respectively:
>>> import rp2
>>> flash = rp2.Flash()
>>> flash.ioctl(4,0)
212
>>> flash.ioctl(5,0)
4096
>>> for i in range(flash.ioctl(4,0)):
... print("erasing block", i)
... flash.ioctl(6, i)
...
erasing block 0
0
erasing block 1
0
...
erasing block 210
0
erasing block 211
0
>>> uos.listdir()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OSError: 84
>>>
Now we can have increased confidence that the portion of QSPI-Flash used for the filesystem is truly empty, for now.
Let's reset the board and take another look:
>>> <CTRL>-d
MPY: soft reboot
MicroPython v1.22.1 on 2024-01-05; Raspberry Pi Pico W with RP2040
Type "help()" for more information.
>>> import uos
>>> uos.getcwd()
'/'
>>> uos.listdir()
[]
>>>
I'm guessing that resetting the board, after destructively erasing 212 4K blocks of flash, has auto-reformatted a new filesystem. (by the way, that's 868352 bytes of space, starting at 0x0 up to the last byte before 0xd4000.) My understanding is that pico has 2MB of QSPI-Flash (which would be 21 addressible bits, 2 ** 21, or 2097152 bytes?) and it must be that something else lives up there, like firmware. After all, we just destroyed what we can see of the flash filesystem, we rebooted fine, and still have access to a working python repl w/ a filesystem.
This documentation
claims that flash_nuke.uf2
can be used to reset flash memory on the pico. Download that flash_nuke.uf2
file.
We will reset the pico board and let it boot into "usb drive" mode so that we can flash it with flash_nuke.uf2
.
Unplug the usb cable so that the pico is powered off, then hold the bootsel-button and plug it back in.
Notice the difference via lsusb
and ls -l /dev/ttyACM0
, that the board is seen differently than it was before:
~/Desktop$ lsusb
...
Bus 003 Device 048: ID 2e8a:0003 Raspberry Pi RP2 Boot
...
~/Desktop$ ls -l /dev/ttyACM0
ls: cannot access '/dev/ttyACM0': No such file or directory
We should see "RPI-RP2" as a mountable usb-drive on the desktop. We can mount it and open its root folder. It always has only two files, no matter what you've added in the past. They are:
- INFO_UF2.TXT: a textfile with a short message about the bootloader, model, and board-id.
- INDEX.HTM: a static web page that redirects to official documentation for your particular board.
We need to drag-and-drop the flash_nuke.uf2
file into the root folder of that RPI-RP2 usb-drive,
then wait until the drive disappears. When it disappears, we can assume that it worked.
Eventually, we'll see RPI-RP2 reappear with the same two files. We can disconnect and reconnect the pico and it will continue to boot into this "usb-drive" mode, even without holding the bootsel-button. I'm assuming that this is our "hint" that there is no useful firmware other than the bootloader which realizes there is no firmware and puts the board into "usb drive" mode so that firmware can be added.
We will flash an older, smaller, micropython firmware so that we have a useful python repl again. The oldest/smallest that I could find is from January 2021 by pimoroni, v0.0.1 Alpha having a 498K firmware.uf2 file.
After dragging and dropping it onto the RPI-RP2 usb drive, the pico restarts and we can connect to the repl again:
MPY: soft reboot
MicroPython v1.13-294-g7a72dc4bb-dirty on 2021-01-22; Raspberry Pi Pico with RP2
040
Type "help()" for more information.
>>>
Poking around, we can see some differences:
>>> import uos
>>> uos.getcwd()
'/'
>>> uos.listdir()
[]
>>> dir()
['machine', 'uos', '__name__', 'rp2']
>>> flash = rp2.Flash()
>>> flash.ioctl(4,0)
352
>>> flash.ioctl(5,0)
4096
>>>
So this older version configured the filesystem for 352 4K blocks for a total of 1441792 bytes or just up to 0x160000.
The rp2040comb repository contains some tools that may be useful for examining QSPI-Flash on the rp2040 based Pico. These can also be used outside of Pico's console repl, on a computer where a flashdump file has been previously saved.
While possible, it is NOT required or even expected to copy any of these tools to the QSPI Flash filesystem in order to use them. Arguably, it would be silly to modify the flash that we are about to inspect. Instead, Use cut/paste directly inside
the micropython repl, they'll be available until the board is reset. I find that minicom
is best for this, but other terminal clients may work too (or they may produce syntax/indentation exceptions -- and I'm not sure why).
To cut-and-paste, simply put the micropython repl into "paste" mode by hitting CTRL-E at an empty >>>
prompt. Paste the contents of your clipboard, and hit CTRL-D when done (but just once, otherwise you'll reset the board).
In order to use most of these tools, it is necessary to first cut-paste the mock_Maix_utils code so that these tools have access to read the flash chip arbitrarily, rather than at the block level -- as is provided by rp2.Flash in the rp2040 port of micropython.
From rp2040comb, we can use the hex_dump.py module to examine areas of QSPI-Flash at the byte level.
>>> hd=HexDumpQSPIFlash(lines=40)
>>> hd.run()
000000 01 00 00 00 f0 0f ff f7 6c 69 74 74 6c 65 66 73 |....�.��littlefs|
000010 2f e0 00 10 00 00 02 00 00 10 00 00 60 01 00 00 |/�..........`...|
000020 ff 00 00 00 ff ff ff 7f fe 03 00 00 70 1f fc c8 |�...���.�...p.��|
000030 2a a4 07 eb ff ff ff ff ff ff ff ff ff ff ff ff |*�.�������������|
000040 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |����������������|
... 250 squeezed
000ff0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |����������������|
001000 02 00 00 00 f0 0f ff f7 6c 69 74 74 6c 65 66 73 |....�.��littlefs|
001010 2f e0 00 10 00 00 02 00 00 10 00 00 60 01 00 00 |/�..........`...|
001020 ff 00 00 00 ff ff ff 7f fe 03 00 00 70 1f fc c8 |�...���.�...p.��|
001030 4e 91 d5 ad ff ff ff ff ff ff ff ff ff ff ff ff |N�խ������������|
001040 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |����������������|
... 89850 squeezed
15fff0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |����������������|
160000 00 b5 2f 4b 21 20 58 60 98 68 02 21 88 43 98 60 |.�/K! X`�h.!�C�`|
160010 d8 60 18 61 58 61 2b 4b 00 21 99 60 02 21 59 61 |�`.aXa+K.!�`.!Ya|
160020 01 21 f0 22 99 50 28 49 19 60 01 21 99 60 35 20 |.!�"�P(I.`.!�`5 |
160030 00 f0 3e f8 02 22 90 42 14 d0 06 21 19 66 00 f0 |.�>�."�B.�.!.f.�|
160040 2e f8 19 6e 01 21 19 66 00 20 18 66 1a 66 00 f0 |.�.n.!.f. .f.f.�|
160050 26 f8 19 6e 19 6e 19 6e 05 20 00 f0 29 f8 01 21 |&�.n.n.n. .�)�.!|
...
As expected, we can now "know" that our littlefs filesystem is mostly empty, besides some layout/marker bytes... all the way up to our expected filesystem limit of 0x15ffff. Above that, it must be "other" firmware, maybe bootloaders, etc. Also, we know that "erased" means 0xff bytes. We can page through (painfully) to see where this ends (since we don't know the actual size of the firmware... more on that later w/ uf2parse.py
).
In the above example, it actually completes around 0x19e3ff, then the rest of QSPI-Flash are erased 0xff bytes. Afterwards, addressing wraps and we're back at 0x0, through the mostly-empty filesystem, and back into the firmware section.
...
19e2f0 78 56 00 20 80 56 00 20 80 56 00 20 88 56 00 20 |xV. �V. �V. �V. |
19e300 88 56 00 20 90 56 00 20 90 56 00 20 98 56 00 20 |�V. �V. �V. �V. |
19e310 98 56 00 20 a0 56 00 20 a0 56 00 20 00 00 00 00 |�V. �V. �V. ....|
19e320 00 00 00 00 d1 73 02 10 8d 51 02 10 a1 58 02 10 |....�s..�Q..�X..|
19e330 f1 6b 02 10 4d 03 00 10 e9 11 02 10 99 91 02 10 |�k..M...�...��..|
19e340 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
... 10 squeezed
19e3f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
19e400 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |����������������|
... 25022 squeezed
1ffff0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |����������������|
000000 01 00 00 00 f0 0f ff f7 6c 69 74 74 6c 65 66 73 |....�.��littlefs|
000010 2f e0 00 10 00 00 02 00 00 10 00 00 60 01 00 00 |/�..........`...|
000020 ff 00 00 00 ff ff ff 7f fe 03 00 00 70 1f fc c8 |�...���.�...p.��|
000030 2a a4 07 eb ff ff ff ff ff ff ff ff ff ff ff ff |*�.�������������|
000040 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |����������������|
000050 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |����������������|
... 249 squeezed
000ff0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |����������������|
001000 02 00 00 00 f0 0f ff f7 6c 69 74 74 6c 65 66 73 |....�.��littlefs|
001010 2f e0 00 10 00 00 02 00 00 10 00 00 60 01 00 00 |/�..........`...|
001020 ff 00 00 00 ff ff ff 7f fe 03 00 00 70 1f fc c8 |�...���.�...p.��|
001030 4e 91 d5 ad ff ff ff ff ff ff ff ff ff ff ff ff |N���������������|
001040 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |����������������|
... 89850 squeezed
15fff0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |����������������|
160000 00 b5 2f 4b 21 20 58 60 98 68 02 21 88 43 98 60 |.�/K! X`�h.!�C�`|
160010 d8 60 18 61 58 61 2b 4b 00 21 99 60 02 21 59 61 |�`.aXa+K.!�`.!Ya|
160020 01 21 f0 22 99 50 28 49 19 60 01 21 99 60 35 20 |.!�"�P(I.`.!�`5 |
160030 00 f0 3e f8 02 22 90 42 14 d0 06 21 19 66 00 f0 |.�>�."�B.�.!.f.�|
We'll flash a more recent version of micropython. This documentation has links to pre-built pico micropython firmware that is much more current.
The version I found is 1617920 bytes (or 0x18b000 in hex). It's the same version as original above. It starts with mostly empty littlefs filesystem space, then firmware begins at 0xd4000.
>>> hd = HexDumpQSPIFlash(lines=40)
>>> hd.run()
000000 01 00 00 00 f0 0f ff f7 6c 69 74 74 6c 65 66 73 |........littlefs|
000010 2f e0 00 10 01 00 02 00 00 10 00 00 d4 00 00 00 |/...............|
000020 ff 00 00 00 ff ff ff 7f fe 03 00 00 7f ef fc 10 |................|
000030 00 01 00 00 de 57 57 01 0f f0 00 cc c0 46 33 a6 |.....WW......F3.|
000040 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
... 250 squeezed
000ff0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
001000 02 00 00 00 f0 0f ff f7 6c 69 74 74 6c 65 66 73 |........littlefs|
001010 2f e0 00 10 01 00 02 00 00 10 00 00 d4 00 00 00 |/...............|
001020 ff 00 00 00 ff ff ff 7f fe 03 00 00 7f ef fc 10 |................|
001030 00 01 00 00 de 57 57 01 0f f0 00 cc 10 d3 36 22 |.....WW.......6"|
001040 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
... 54010 squeezed
0d3ff0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
0d4000 00 b5 32 4b 21 20 58 60 98 68 02 21 88 43 98 60 |..2K! X`.h.!.C.`|
0d4010 d8 60 18 61 58 61 2e 4b 00 21 99 60 02 21 59 61 |.`.aXa.K.!.`.!Ya|
0d4020 01 21 f0 22 99 50 2b 49 19 60 01 21 99 60 35 20 |.!.".P+I.`.!.`5 |
0d4030 00 f0 44 f8 02 22 90 42 14 d0 06 21 19 66 00 f0 |..D..".B...!.f..|
0d4040 34 f8 19 6e 01 21 19 66 00 20 18 66 1a 66 00 f0 |4..n.!.f. .f.f..|
0d4050 2c f8 19 6e 19 6e 19 6e 05 20 00 f0 2f f8 01 21 |,..n.n.n. ../..!|
0d4060 08 42 f9 d1 00 21 99 60 1b 49 19 60 00 21 59 60 |.B...!.`.I.`.!Y`|
0d4070 1a 49 1b 48 01 60 01 21 99 60 eb 21 19 66 a0 21 |.I.H.`.!.`.!.f.!|
...
In this example, the ending of the firmware space is up around 0x1997f0 which has plenty of 0x00 fill. It is followed by nothing but erased 0xff bytes which complete the rest of the 2MB. Then addressing wraps back around to 0x0, where we can see our empty filesystem again, and the beginning of firmware.
199570 50 33 00 00 4c 33 00 00 54 33 00 00 52 33 00 00 |P3..L3..T3..R3..|
199580 4d 53 00 00 4d 43 00 00 53 34 00 00 43 34 00 00 |MS..MC..S4..C4..|
199590 70 65 06 10 46 7e 06 10 1d 99 00 20 10 00 0c 00 |pe..F~..... ....|
1995a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
1995b0 00 00 00 00 ff 00 00 00 30 59 00 20 00 00 00 00 |........0Y. ....|
1995c0 00 00 00 00 4c 5d 00 20 b4 5d 00 20 1c 5e 00 20 |....L]. .]. .^. |
1995d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
... 6 squeezed
199640 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
199650 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 |................|
199660 0e 33 cd ab 34 12 6d e6 ec de 05 00 0b 00 00 00 |.3..4.m.........|
199670 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
... 5 squeezed
1996d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
1996e0 48 5d 00 20 00 00 00 00 00 00 00 00 00 00 00 00 |H]. ............|
1996f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
199700 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
199710 00 00 00 00 0d 18 06 10 c5 d4 05 10 85 db 05 10 |................|
199720 c1 de 05 10 5d 4d 00 20 bd 02 00 10 61 99 04 10 |....]M. ....a...|
199730 35 18 06 10 00 00 00 00 00 00 00 00 00 00 00 00 |5...............|
199740 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
... 10 squeezed
1997f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
199800 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
... 26238 squeezed
1ffff0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
000000 01 00 00 00 f0 0f ff f7 6c 69 74 74 6c 65 66 73 |........littlefs|
000010 2f e0 00 10 01 00 02 00 00 10 00 00 d4 00 00 00 |/...............|
000020 ff 00 00 00 ff ff ff 7f fe 03 00 00 7f ef fc 10 |................|
000030 00 01 00 00 de 57 57 01 0f f0 00 cc c0 46 33 a6 |.....WW......F3.|
000040 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
... 250 squeezed
000ff0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
001000 02 00 00 00 f0 0f ff f7 6c 69 74 74 6c 65 66 73 |........littlefs|
001010 2f e0 00 10 01 00 02 00 00 10 00 00 d4 00 00 00 |/...............|
001020 ff 00 00 00 ff ff ff 7f fe 03 00 00 7f ef fc 10 |................|
001030 00 01 00 00 de 57 57 01 0f f0 00 cc 10 d3 36 22 |.....WW.......6"|
001040 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
... 54010 squeezed
0d3ff0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
0d4000 00 b5 32 4b 21 20 58 60 98 68 02 21 88 43 98 60 |..2K! X`.h.!.C.`|
0d4010 d8 60 18 61 58 61 2e 4b 00 21 99 60 02 21 59 61 |.`.aXa.K.!.`.!Ya|
0d4020 01 21 f0 22 99 50 2b 49 19 60 01 21 99 60 35 20 |.!.".P+I.`.!.`5 |
0d4030 00 f0 44 f8 02 22 90 42 14 d0 06 21 19 66 00 f0 |..D..".B...!.f..|
Above, we have opined that firmware starts at 0xd4000 and completes at 0x199800. This would indicate that the data size of firmware is: 0x199800 - 0xd4000 = 0xc5800 or 808960 bytes.
From rp2040comb, we can use the commandline uf2parse.py tool to validate, describe, and or dump the payload from a firmware.uf2 file.
~/Desktop$ python uf2parse.py ../Downloads/RPI_PICO_W-20240105-v1.22.1.uf2
UF2: parsing '~/Downloads/RPI_PICO_W-20240105-v1.22.1.uf2'
crude validation of 3160 512 byte sequences
destined for flash address 0x10000000 to 0x100c5800-1
file size: 808960 bytes, 0xc5800
crc32: 4203250943, 0xfa8884ff
sha256: 7f0ab258c246334a09c6622222fddd1c9b6d553dbde1cafbf11a6182d0ec4b67
From rp2040comb, we can use the hashcrc_flash.py module to calculate the sha256 hash and crc32 checksum of a particular section of QSPI Flash.
>>> start = 0xd4000
>>> size = 808960
>>> hash, crc = hashcrc_flash(start, size)
>>> hash.hex()
'7f0ab258c246334a09c6622222fddd1c9b6d553dbde1cafbf11a6182d0ec4b67'
>>> crc
4203250943
As we recall from the hexdump, nothing else showed up after the end of firmware, but rp2040comb can check this too. We'll use the all_bytes_are.py module so that we can verify that the rest of the flash space is indeed all 0xff bytes:
>>> eof = 2**21
>>> all_bytes_are(b'\xff', start + size, eof - (start+size))
True
So we can finally say, with more confidence, that the bytes in QSPI-Flash are 1) an empty formatted filesystem and 2) payload from firmware.uf2... and nothing else.