Skip to content

Instantly share code, notes, and snippets.

@DavidEGrayson
Last active September 12, 2023 04:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DavidEGrayson/1d590834eac281fee61936e7155e9867 to your computer and use it in GitHub Desktop.
Save DavidEGrayson/1d590834eac281fee61936e7155e9867 to your computer and use it in GitHub Desktop.
Building and debugging RP2040 firmware in Windows with MSYS2 + GCC + GDB + OpenOCD + PicoProbe + SWD

This is an opinionated tutorial that shows you how to set up a development environment for the RP2040 on Windows using only open source tools. You should start at the beginning of this tutorial, but you don't have to follow ALL of the instructions: you can stop after completing any level and leave with something that is useful.

Level 0: Set up MSYS2

Download and install MSYS2, the best system for developing open source software on Windows.

Use "Edit environment variables for your account" in the Windows Control Panel to add an environment variable for your user named HOME with a value like C:\Users\david (but with david replaced by your actual Windows user name). Do NOT add an extra slash on the end. MSYS2 uses this variable to determine where your home directory is. If you don't set it, MSYS2 defaults to using a home directory like /home/USERNAME which actually lives inside the MSYS2 installation. That is not your true home directory, and you shouldn't get in the habit of storing files there because then it will be harder to remove and reinstall MSYS2.

image

Start MSYS2 by running "MSYS2 MinGW 64-bit" from your Start menu. (That shortcut starts the MINGW64 environment of MSYS2. These instructions will probably work fine for MINGW32 and UCRT64 as well without modification.) The first line of your shell prompt should say "MINGW64" followed by a ~ (which means you are in your home directory). If you type pwd, you should see something like /c/Users/david.

If you are new to MSYS2, I recommend learning about:

Level 1: Compile an RP2040 program with GCC

Run this in MSYS2 to download and install the prerequisites:

pacman --needed -S git $MINGW_PACKAGE_PREFIX-{cmake,gcc,arm-none-eabi-gcc,python}

Choose a directory where you will be doing your work. I like to use ~/Documents, which is just the standard Documents folder provided by Windows, and future steps of this tutorial will assume you are doing the same (but you can easily modify those steps if that is not the case).

Download the Pico SDK and its most important submodule by running:

cd ~/Documents
git clone https://github.com/raspberrypi/pico-sdk
cd pico-sdk
git submodule update --init lib/tinyusb

Optional: You can run git submodule status to see the other submodules. If you think you'll need any of them, you can run git submodule update --init to download all of them. This can also be done later.

Now we need to set up the PICO_SDK_PATH environment variable so that the firmware projects you compile can find the Pico SDK. This will be the first of many modifications you make to your environment in order to support RP2040 development. I like to keep all of these settings in their own shell script, and I run that script manually whenever I want to start doing RP2040 development. This way, these settings go away when you close the shell and don't interefere with anything else I might use MSYS2 for (this is especially important when we start modifying the PATH later in this tutorial). So you should run mkdir -p ~/Documents/bin and nano ~/Documents/bin/use_rp2040.sh and then write the following in that file:

export PICO_SDK_PATH=~/Documents/pico-sdk

Save the file, exit nano, and then run source ~/Documents/bin/use_rp2040.sh. Now type echo $PICO_SDK_PATH to verify that the environment variable was set correctly.

Generic build instructions

Most RP2040 firmware projects use CMake and you can build them by running the following commands in the project's directory:

mkdir build
cd build
cmake .. -DPython3_EXECUTABLE=$(which python3)
ninja   # or cmake --build .

This should produce output files in the build directory in the .bin, .elf, and .uf2 formats which are ready to be loaded onto an RP2040.

The Python part is necessary because the Pico SDK uses CMake to find Python3. By default, CMake (specifically FindPython3.cmake) might choose to use the MSYS Python from /usr/bin instead of the MinGW version in /mingw64/bin if the MSYS version happens to be newer. However the MSYS version of Python does not handle Windows/Posix PATH conversions the same way as the MinGW version and this will cause an error.

pico-examples build instructions

If you want to build an example from pico-examples, you could just use the generic build instructions above, but it will build all the examples and it could take many minutes.

I recommend modifying the CMakeLists.txt file of the example you want to build so it can be a a standalone project instead of needing to be part of the larger project. Just add these lines to the top of the example's CMakeLists.txt file (for example blink/CMakeLists.txt):

if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
  cmake_minimum_required(VERSION 3.13)
  include("$ENV{PICO_SDK_PATH}/pico_sdk_init.cmake")
  project(project C CXX ASM)
  pico_sdk_init()
  macro(example_auto_set_url)
  endmacro()
endif ()

Then you can compile the example using the generic build instructions above, starting in the example's directory, and it will only build one example. This does work for the blink example, but there might be issues with some of the more complicated examples.

MicroPython build instructions

If you want to build MicroPython for the RP2040, you should use Make instead of Ninja. Run:

make -C ports/rp2 submodules
mkdir build
cd build
cmake ../ports/rp2 -G"MSYS Makefiles" -DPython3_EXECUTABLE=$(which python3) -DMICROPY_BOARD=PICO
make

Level 2: Upload firmware using the RP2040's USB mass storage interface

The RP2040 has a USB bootloader that implements a mass storage device (i.e. USB drive) and a native (vendor-defined) USB interface. To start the bootloader, press your board's BOOTSEL button while powering it on or resetting it, and make sure your board is connected to your computer via USB.

You can write your firmware to the RP2040 via its mass storage interface from MSYS2 using the cp command. For example, if the bootloader is drive E:, run this in MSYS2:

cp *.uf2 /e
  • You can write the full filename instead of *.uf2 if you want, of course.
  • To determine the drive letter, you can look at "This PC" in Windows after starting the bootloader or run mount in MSYS2.

Mass storage rant

The mass storage device of the bootloader is a massive hack. The bootloader pretends to have 128 MiB of space, but it doesn't: the RP2040 can only have up to 16 MiB of flash, depending on what type of flash chip is connected to it. If your computer's operating system ever writes data to the drive and then expects to be able to read back the same data later, it won't work, so this hacky scheme sometimes causes incompatibilities with various operating systems.

To be fair, the RP2040 isn't the only chip employing this hack. The main reason hardware vendors do this kind of hack is so that users can write data to their device without installing any drivers. However, a better solution for that would be to invent a new USB device class that matches your requirements. If you can get it standardized by the USB Implementor's Forum, then all the major operating systems would probably implement drivers for it. Even if you can't get it officially standardized, you could still make it be an unofficial standard and implement driver/software solutions that make it easy for people to use it.

Level 3: Upload firmware using the RP2040's USB vendor-defined interface

This level is optional: you can skip to the next level and learn about debugging if you want.

The RP2040 bootloader has a native (vendor-defined) USB interface which is not a hack like the mass storage device and also provides more features, like reading the flash.

USB driver

As far as I know, the Raspberry Pi people do not provide a signed USB driver for Windows for the native interface of their bootloader. In order to get Windows to recognize that interface and associate it with the WinUSB driver (winusb.sys) that comes with Windows, I recommend downloading a third-party driver provided by the Arduino people. Download this ZIP file:

https://github.com/arduino/ArduinoCore-mbed/archive/4.0.4.zip

In the "drivers" folder of the above download, locate the files named "nanorp2040connect.inf" and "nanorp2040connect.cat". Extract these two files to a folder on your computer. Right-click on the INF file and select "Install".

The next time you get the RP2040 into bootloader mode and connect it to your computer via USB, it should look like this in your Device Manager if you select "Devices by connection" from the "View" menu:

RP2040 in the Device Manager with drivers installed

The highlighted device node, "RP2 boot", represents the RP2040's native interface.

picotool

Now that you have a driver installed, it is time to download and build picotool, the official command-line utility for interacting with RP2040 devices when they are in bootloader mode. Run these commands in your Documents directory:

pacman --needed -S git $MINGW_PACKAGE_PREFIX-{cmake,gcc,libusb}
git clone https://github.com/raspberrypi/picotool
cd picotool
mkdir build
cmake .. -DCMAKE_INSTALL_PREFIX=$MINGW_PREFIX
ninja
ninja install

(If you get an error from CMake saying PICO_SDK_PATH is not defined, remember that you generally need to source the use_rp2040.sh script we wrote earlier whenever you open a new MSYS2 terminal and want to do RP2040 development in it.)

After successfully running the commands above, picotool.exe should be in the appropriate MINGW bin directory (e.g. /mingw64/bin), and you can run it in your terminal at any time by just by typing picotool.

Now you can load firmware onto your RP2040 and start running it with a command like this, assuming it is running its bootloader:

picotool load -x FILE

The file you specify can be a UF2, BIN, or ELF file.

Running picotool with no arguments shows all the options it has. The reboot option attempts to boot the RP2040 into bootloader mode if it is not already running the bootloader. That way you can run a single shell command that reboots your RP2040 and updates its firmware. However, it is not trivial to get it working in Windows, so I might expand this document later to cover it.

If you'd like picotool to be faster, you might consider using my version of it, which lives on the dev/david/master branch of my fork. I made an improvement which increases the loading speed by 50%. At the time of writing, this branch is based on an older version of picotool and cannot be cleanly merged with the latest official version.

Level 4: Upload and debug using a Picoprobe

A Picoprobe is a Raspberry Pi Pico running the picoprobe firmware, which turns it into a USB programmer/debugger for the RP2040's SWD interface, and also a USB-to-serial adapter. (This is not to be confused with the Raspberry Pi Debug Probe, which is a different device and has different firmware. Most of these instructions will probably work with either type of probe, but I have only tested the Picoprobe firmware running on a Raspberry Pi Pico.)

Picoprobe setup and testing

  1. Download the latest Picoprobe firmware from the picoprobe releases page (picoprobe.uf2) and load it onto a Raspberry Pico to turn it into a Picoprobe.
  2. Windows should be automatically recognize the Picoprobe when you connect it via USB without needing any special drivers.

    Picoprobe in Device Manager

  3. Use MSYS2 to build the Raspberry Pi version of OpenOCD:
    pacman -S --needed $MINGW_PACKAGE_PREFIX-{libusb,gdb-multiarch,toolchain}
    cd ~/Documents
    git clone https://github.com/raspberrypi/openocd --branch rp2040
    cd openocd
    ./bootstrap
    mkdir build
    cd build
    ../configure --disable-werror
    make
    
    (The last command will take a while.)
  4. We don't want to install this version of openocd into your system's bin directory since it is specific to the RP2040, but we do want to make it easy to run. Add these lines to `~/Documents/bin/use_rp2040.sh` and then source it again:
    export PATH=~/Documents/openocd/build/src:$PATH
    export OPENOCD_SCRIPTS=~/Documents/openocd/tcl
    alias rp2040_gdb='gdb-multiarch -iex "set osabi none"'
    alias rp2040_openocd='openocd -f interface/cmsis-dap.cfg -c "adapter speed 5000" -f target/rp2040.cfg'
    # only works for ELF files, relative path, no spaces
    rp2040_program() {
      rp2040_openocd -c "program $1 verify reset exit"
    }
    # only works for BIN files, relative path, no spaces
    rp2040_program_bin() {
      rp2040_openocd -c "program $1 verify reset exit 0x10000000"
    }
    
  5. Connect your Picoprobe to the RP2040 you want to program/debug according to the instructions in the "Picoprobe Wiring" section of Getting started with Raspberry Pi Pico. Specifically:
    • Picoprobe GND -> RP2040 GND
    • Picoprobe GP2 -> RP2040 SWCLK
    • Picoprobe GP3 -> RP2040 SWDIO
    Unfortunately, the newer Raspberry Pi Pico boards make it hard to access SWCLK and SWDIO: they are only accessible via the tiny pins in the new debug connector instead of being brought out to normal pins with a 0.1" pitch. If you have one of those boards, maybe you need to get a Raspberry Pi Debug Probe, which comes with a matching connector and handy cables you can use.
  6. Now run `rp2040_program SOME_FIRMWARE.elf` to load some firmware of your choosing onto the RP2040. Note that the `rp2040_program` function only works with ELF files, and due to issues with MSYS2 path conversions it only work with relative paths, and due to the fact that the path is embedded in OpenOCD's scripting language, it probably doesn't work with spaces. (Rant: I'm not a fan of OpenOCD's design: you should be able to do basic things like programming an ELF file by just providing the path to the ELF file as an argument. The fact that you need to embed it into an interpreted scripting language is what causes all three of those issues above.) If the command works and your firmware is no running on the RP2040, then it means your RP2040 and Picoprobe hardware is set up correctly!

Debugging with GDB

When you have some firmware you want to debug, follow these instructions:

  1. In MSYS2, run rp2040_openocd. OpenOCD will connect to the Picoprobe and start running a GDB server on port 3333.
  2. In a second MSYS2 terminal window, run `rp2040_gdb SOME_FIRMWARE.elf` to start GDB.
  3. Reset the target RP2040 by shorting its RUN pin to GND, and then undoing the connection. This isn't always necessary, but I've found that sometimes the Picoprobe/RP2040 system gets into bad states and resetting the target RP2040 helps. You might want to connect a pushbutton between RUN and GND so you can do this just by pressing and releasing the button.
  4. In the GDB you started, run these commands:
    target extended-remote :3333  
    load
    monitor reset init
    b main
    continue
    
  5. The most useful GDB commands for me are b (set breakpoint), n (next line or something like that), s (step), c (continue), where (print stack trace), q (quit), and Ctrl+C (break while the target is running). See the GDB user manual for more help.
@DavidEGrayson
Copy link
Author

DavidEGrayson commented Aug 19, 2023

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