Skip to content

Instantly share code, notes, and snippets.

@jdlcdl
Last active January 15, 2024 10:30
Show Gist options
  • Save jdlcdl/a464bc6cb4b8ea8b621a1bb4f57ba436 to your computer and use it in GitHub Desktop.
Save jdlcdl/a464bc6cb4b8ea8b621a1bb4f57ba436 to your computer and use it in GitHub Desktop.

QSPI-Flash on the Raspberry Pi Pico rp2040

Goals:

  • 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.

Steps:

  • 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.

Requirements:

  • 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 and minicom work fine (/dev/ttyACM0), I'm using minicom.

Connecting to the board and micropython repl

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>


Wiping the filesystem

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.


Nuking the firmware

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.


Flashing an older (and smaller) micropython firmware

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.


Intro to rp2040comb to inspect QSPI-Flash

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.�|                    

Flashing a current micropython firmware, inspecting

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..|

Comparing what's on the pico to micropython-firmware.uf2 files

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.

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