THIS TUTORIAL IS UNDER CONSTRUCTION. I'm updating it to support Pico SDK 2.0.0 and the new RP2 chips!
This is an opinionated tutorial that shows you how to set up a development environment for the RP2 series of microcontrollers (RP2040, RP2350A, RP2350B, RP2354A, RP2354B) 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.
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.
Start MSYS2 by running "MSYS2 MinGW UCRT 64-bit" from your Start menu. (That shortcut starts the UCRT44 environment of MSYS2. These instructions will probably work fine for MINGW32 and MINGW64 as well without modification.) The first line of your shell prompt should say "UCRT64" 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:
- MSYS2 environments
- Updating MSYS2
- GNU Bash - the shell used by MSYS2
- nano - a convient text editor you can run inside the MSYS2 terminal
Run this in MSYS2 to download and install the prerequisites:
pacman --needed -S git $MINGW_PACKAGE_PREFIX-{cmake,gcc,arm-none-eabi-gcc,python,picotool}
Choose a directory where you will be doing your work. For this tutorial we will use ~/rp2, but if you are using some other directory you can easily modify the steps of this tutorial to work with your setup.
Download the Pico SDK and its most important submodules by running:
mkdir -p ~/rp2
cd ~/rp2
git clone https://github.com/raspberrypi/pico-sdk
cd pico-sdk
git submodule update --init lib/tinyusb lib/mbedtls
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 RP2 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 RP2 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 nano ~/rp2/use_rp2.sh and then write the following in that file:
export PICO_SDK_PATH=~/rp2/pico-sdk
Save the file, exit nano, and then run source ~/rp2/use_rp2.sh. Now type echo $PICO_SDK_PATH to verify that the environment variable was set correctly.
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 RP2.
The Python part is necessary because the Pico SDK uses CMake to find Python3. By default, CMake (specifically the over-complicated script 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.
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.
If you want to build MicroPython for the RP2040, you should use Make instead of Ninja. Run:
cd ~/rp2
git clone https://github.com/micropython/micropython
cd micropython
make -C ports/rp2 submodules
mkdir build
cd build
cmake ../ports/rp2 -G"MSYS Makefiles" -DPython3_EXECUTABLE=$(which python3) -DMICROPY_BOARD=RPI_PICO
make
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
*.uf2if you want, of course. - To determine the drive letter, you can look at "This PC" in Windows after starting the bootloader or run
mountin MSYS2.
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.
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.
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:
The highlighted device node, "RP2 boot", represents the RP2040's native interface.
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:
cd ~/rp2
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 bin directory (e.g. /ucrt64/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.
(By the way, I once made a pull request to improve the loading speed of picotool with the RP2040 by 50%. However, it was never merged in and I gave up on it after they released the RP2350 and I realized the poor design of its bootloader would not allow my technique to work.)
In this document, "debug probe" refers to any device running the debugprobe firmware: either a Raspberry Pi Debug Probe, a Pico, or a Pico 2. The debug probe acts as a USB programmer/debugger for the RP2040's SWD interface, and also a USB-to-serial adapter. We will use OpenOCD and GDB to debug a program on the target board using the debug probe. These instructions are based on Appendix A of Getting started with Raspberry Pi Pico-series.
-
Debug probe firmware: If you have a Raspberry Pi Debug Probe, it should already have firmware so you can skip this step, but you might check the firmware version and consider updating it. Otherwise, download the latest debugprobe firmware from the debugprobe releases page and load it onto a Pico to turn that Pico into a debug probe. Use debugprobe_on_pico.uf2 or debugprobe_on_pico2.uf2 depending on which type of Pico you have.
- Windows should automatically recognize the debug probe when you connect it via USB without needing any special drivers.
-
Use MSYS2 to build the Raspberry Pi version of OpenOCD:
pacman -S --needed $MINGW_PACKAGE_PREFIX-{libusb,gdb-multiarch,gcc} libtool autoconf automake cd ~/rp2 git clone https://github.com/raspberrypi/openocd cd openocd git submodule update --init jimtcl ./bootstrap mkdir build cd build ../configure --prefix=$HOME/rp2 --program-prefix=rp2- --enable-internal-jimtcl --disable-werror make -j make install -
Add these lines to
~/rp2/use_rp2.shand then source it again:export PATH=~/rp2/bin:$PATH alias rp2_gdb='gdb-multiarch -iex "set osabi none" -ex "target extended-remote :3333"' alias rp2040_openocd='rp2-openocd -f interface/cmsis-dap.cfg -c "adapter speed 5000" -f target/rp2040.cfg' alias rp2350_openocd='rp2-openocd -f interface/cmsis-dap.cfg -c "adapter speed 5000" -f target/rp2350.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" } -
Connect your Picoprobe to the RP2040 you want to program/debug according to the instructions in the "debugprobe Wiring" section of Getting started with Raspberry Pi Pico-series. Specifically:
- Picoprobe GND -> RP2040 GND
- Picoprobe GP2 -> RP2040 SWCLK
- Picoprobe GP3 -> RP2040 SWDIO
-
Now run
rp2040_program SOME_FIRMWARE.elfto load some firmware of your choosing onto the target RP2040. Note that therp2040_programfunction 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 now running on the RP2040, then it means your RP2040 and Picoprobe hardware is set up correctly!
For a good debugging experience, you want to make sure that the firmware is compiled with the -g and -Og compiler options. The -g option instructs the compiler to record debugging info in the .o and .elf files, allowing debug software like GDB to convert low-level information from the debug probe like the program counter into high-level info like the source file and line number of the code that is running. The -Og option instructs the compiler to optimize while keeping in mind debugging experience.
To use both of these options, use the -DCMAKE_BUILD_TYPE=Debug option to CMake. Either add that option the first time you run CMake, or add it later by running cmake . -DCMAKE_BUILD_TYPE=Debug in an existing build directory.
You can build your firmware with ninja --verbose to see the exact commands being executed and ensure and ensure the right options are being passed to the compiler.
After the firmware is built, you can also check for the presence of debugging info using the addr2line utility from GNU binutils. For example, in the shell session below, we extract the address of main from the map file produced by the linker and then we pass the address to addr2line to find out where main is defined. We also pass in 0x20000000 (top of RAM) to find out where the variable at that location is defined.
$ grep main$ blink.elf.map
0x100002dc main
$ arm-none-eabi-addr2line -e blink.elf 0x100002dc 0x20000000
C:/Users/david/rp2/pico-examples/blink/blink.c:44
C:/Users/david/rp2/pico-sdk/src/rp2_common/pico_runtime_init/runtime_init.c:212
When you have some firmware you want to debug, follow these instructions:
- In MSYS2, run
rp2040_openocd. OpenOCD will connect to the Picoprobe and start running a GDB server on port 3333. - In a second MSYS2 terminal window, run `rp2_gdb SOME_FIRMWARE.elf` to start GDB.
- 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.
- In the GDB you started, run these commands:
load monitor reset init break main continue
- 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.



Note to self: post this Gist to this Reddit thread when it's done: https://www.reddit.com/r/raspberrypipico/comments/sop8vo/ide_for_rp2040_debugging/
and post here:
https://forums.raspberrypi.com/viewtopic.php?t=303509