Raspberry Pi Pico remote debugging with VS Code

Since I started working with Raspberry Pi Pico, I mostly had it connected to my PC next to me. However, it is possible to configure an environment for remote debugging. There are a couple of things that you need, and of course some configuration of your IDE is needed as well. I will walk you through the main steps that are required to setup everything correctly and start a remote debugging session.

As you might have already read, I develop something called RemoteLab. It is a distributed Hardware-as-a-Service solution that allows you to manage multiple development boards connected to a working station and then connect to it having no physical access to the site. RemoteLab was initially developed at WRUST (Wrocław University of Science and Technology), where I give classes related to embedded system, software development, artificial intelligence and robotics. There, it is heavily used by the students and constantly improved. You might ask yourself why do I even write about it right now since the blog should be about configuring VS Code for remote debugging. Indeed, these two things are tightly connected. Since you need IDE to be configured to access the remote resources via RemoteLab infrastructure. That being said, let us start with setting the environment.

Environment setup

My hardware environment for this particular blog would be a Raspberry Pi and a Raspberry Pi Pico (a target device)with Debug Probe. The debug probe can be either a stand alone device in a nice casing, or you could directly use another Raspberry Pi Pico, flash it with Debug Probe firmware, and you end up having a device with Debug Probe capabilities.

There is one additional prerequisite, namely openocd. Openocd is a software that can create a debug server that allows you to interact with your target debugger (in our case Debug Probe). Thanks to it, you can flash new firmware, inspect memory, add breakpoints, or step-by-step execute the application.

Depending on how you install openocd, you might end up with a slightly different configuration. The software comes with a number of preconfigured interface and target files. These files allow it to seamlessly connect to your target, and they specify how to talk with it, what memory space is available, in what state the target should be in, etc.

With my installation, I found out that some configuration files for rp2040 target are missing. However, since all the configuration files are purely text-based, it was easy to erect the situation.

First, I needed to copy a board configuration to a new file.

sudo cp /usr/local/share/openocd/scripts/board/pico-debug.cfg /usr/local/share/openocd/scripts/board/pico-debug-all-cores.cfg

Finally, in the new file, I have introduced a simple change. I have replaced rp2040-core with rp2040 in the file I just copied.

sudo sed -i 's/rp2040-core0/rp2040/g' /usr/local/share/openocd/scripts/board/pico-debug-all-cores.cfg

The reason for the change was related to the missing rp2040-core0 configuration file. However, in my installation another rp2040 file was available. After inspecting it, I have found that for my needs it will be well suited. Most likely also for yours. To avoid the native files I have decided to just create a copy and adjust it.

Now, when the configuration files are setup for openocd, it is important to discover what Debug Probe openocd needs to connect to. This step is not necessary if a single Debug Probe is connected to Rasberry Pi. However, when there are more than one, it is important to determine which Debug Probe is which. In order to do so, let us serial number which is unique for each Debug Probe. This way the hardware and its logic representation in the system can be determined. In other words, we will know with which device we are communicating.

lsusb -v | grep -C 2 -i "Debug"

The above command will display all information about USB connected devices in verbose mode. The verbose mode will provide information about the serial number of each device that can be used later. If there are many devices, not necessary Debug Probes, connected to the system, it is easier to filter the output of lsusb command. Here, filtering through the “Debug” keyword is exercised. An additional context of two lines surrounding the match of the “Debug” keyword will be displayed. In this surrounding, we will find our serial number. An example of the output is attached below.

Bus 001 Device 021: ID 2e8a:000c Raspberry Pi Debug Probe (CMSIS-DAP)
Device Descriptor:
  bLength                18
--
  bcdDevice            1.01
  iManufacturer           1 Raspberry Pi
  iProduct                2 Debug Probe (CMSIS-DAP)
  iSerial                 3 E6633861A3847738
  bNumConfigurations      1
--

Here, we look for iSerial entry, and next to its right, the serial number is available E6633861A3847738.

Now, when we know which device we should connect, we can spine up openocd instance and connect it to the particular device with given serial number.

openocd -f /usr/local/share/openocd/scripts/board/pico-debug-all-cores.cfg -c "hla_serial E6633861A3847738; gdb_port 3018; tcl_port 5018; telnet_port 4018;"

The above openocd execution command requires some explanation. The -f argument is straightforward. Indicates the location of the configuration file. This is the same file which was copied and modified by us earlier. The -c allows to execute command in openocd. Here, we specify that we select adapter with given serial number, and the other three relate to port exposing for three services. The first service will allow for the incoming connections for GDB debugger exposed on port 3018. This is the main communication interface between openocd and debugger when it is configured for remote debugging sessions. The port 5018 is exposed to the incoming connections for TCL interface. This port is used mainly for the communication between machines to execute tcl commands. Lastly, port 4018 is a telnet port. It also provides an interface for executing TCL commands, but for humans. TCL commands are stateless commands meaning that you work on your target device and manage it, e.g. you can halt the core, reboot the unit, and change interface speed. In fact, the configuration files provide a list of TCL commands that initialise the target and the interface.

Below you can observe the example output when openocd is launched correctly and it communicates with the target.

Open On-Chip Debugger 0.12.0+dev-00574-g2c8376b79 (2024-05-22-16:35)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : Hardware thread awareness created
Info : Hardware thread awareness created
DEPRECATED! use 'adapter serial' not 'hla_serial'
Info : Listening on port 5018 for tcl connections
Info : Listening on port 4018 for telnet connections
Info : Using CMSIS-DAPv2 interface with VID:PID=0x2e8a:0x000c, serial=E6633861A3847738
Info : CMSIS-DAP: SWD supported
Info : CMSIS-DAP: Atomic commands supported
Info : CMSIS-DAP: Test domain timer supported
Info : CMSIS-DAP: FW Version = 2.0.0
Info : CMSIS-DAP: Interface Initialised (SWD)
Info : SWCLK/TCK = 0 SWDIO/TMS = 0 TDI = 0 TDO = 0 nTRST = 0 nRESET = 0
Info : CMSIS-DAP: Interface ready
Info : clock speed 4000 kHz
Info : SWD DPIDR 0x0bc12477, DLPIDR 0x00000001
Info : SWD DPIDR 0x0bc12477, DLPIDR 0x10000001
Info : [rp2040.core0] Cortex-M0+ r0p1 processor detected
Info : [rp2040.core0] target has 4 breakpoints, 2 watchpoints
Info : [rp2040.core0] Examination succeed
Info : [rp2040.core1] Cortex-M0+ r0p1 processor detected
Info : [rp2040.core1] target has 4 breakpoints, 2 watchpoints
Info : [rp2040.core1] Examination succeed
Info : starting gdb server for rp2040.core0 on 3018
Info : Listening on port 3018 for gdb connections

Notice a few interesting things. We have information that the session was started for particular CMSIS-DAPv2 interface with given serial number. This is the Debug Probe. Then we have some configuration of the debugger connection. We can also notice that openocd is waiting for incoming tcl, telnet, and gdb connections. Most importantly, we can see that two Cortex-M0+ cores were detected and each target is capable of inserting 4 breakpoints and 2 watchpoints. These are the features of the Raspberry Pi Pico board with RP2040 MCU.

VS Code Configuration

What is left is to establish a connection between openocd debug server and VS Code. In order to create the connection, the first gdb port from openocd needs to be exposed. Assuming that the openocd was launched on Raspberry Pi available under e.g. 192.168.10.10 IP address, it is important to tunnel the exposed port. For the described case, the command would look like

ssh [email protected] -L 3018:localhost:3018

An SSH session will be established and the 3018 port will be redirected from Raspberry Pi to our local system. This way when connecting at localhost:3018 we will reach port 3018 on the Raspberry Pi running openocd.

Let us make another assumption about a sample application like LED blinky. It compiles successfully, and what is left to do is flash it to remote target (Raspberry Pi Pico). To do so, a launch configuration must be created. But before creating the configuration for debugging it is important to have the Cortex-Debug plugin installed in VS Code. It facilitates the debugging session.

Below, an example configuration was provided.

{
	"name": "Launch Program remote 3018",
	"cwd": "${workspaceRoot}",
	"executable": "${workspaceFolder}/build/blinky.elf",
	"request": "launch",
	"type": "cortex-debug",
	"servertype": "external",
	"gdbPath" : "arm-none-eabi-gdb",
	"gdbTarget": "127.0.0.1:3018",
	"device": "RP2040",     
	"svdFile": "C:/pico-sdk/src/rp2040/hardware_regs/rp2040.svd",
	"runToEntryPoint": "main",
	"showDevDebugOutput": "raw"
}

Let us now walk through it. The name is obvious, it is the name of the configuration, so it can be easily found and selected. cwd defines the path of the current working directory and is set to current workspace path, thus where the VS code project is located. executable is the name of the firmware. Usually, it is located at the top level of build directory. Here, we pass ELF file. request is a type of action we want to take, and thus launching the configuration is a desired action. type specifies the type of the session, and here we place cortex-debug. Since we have Cortex-Debug plugin, it will make use of the provided configuration and set up the connection based on provided information. To notify VS Code that we actually want to connect to an external resource, not the local Debug Probe, servertype has to be specified as debug. gdbPath describes the name of the gdb application valid for the given architecture. If it is available through PATH environment variable there is no need to provide explicit path, just the name of the debugger application will suffice. To inform gdb about what external resource we want to connect, we provide a target as gdbTarget. Here, an address to the host and gdb port needs to be given. That is why we have used SSH tunnelling in the first place. Keep in mind that for the entire duration of the debug session, the connection needs to be active. We provide device type with device keyword, here RP2040 MCU.

Next, it is important to specify SVD file (System View Description) with svdFile keyword. This file holds the structure of an XML file. It contains a description of all registers and addresses available in an MCU. Provides a mapping between what can be found under what address. Instead of inspecting a given address, we would provide register name. It is exactly the same as the domain name and IP address assigned to it.

An example of already mapped data from registers to names is provided below.

runToEntryPoint defines what function we want to reach and stop the debugger at it. The last option showDevDebugOutput is not necessary, and it can be removed. But if you are experiencing issues when establishing a connection with debugger, it should provide a verbose output related to the debug session that will help you during debugging.

When the session is started, the output of the openocd on Raspberry Pi should print out something similar to this:

Info : accepting 'gdb' connection on tcp/3018
Info : Found flash device 'win w25q16jv' (ID 0x001540ef)
Info : RP2040 B0 Flash Probe: 2097152 bytes @0x10000000, in 32 sectors

Now in VS Code, you can debug your code, observe variables, add breakpoints, and in general interact with the code.

The connection was accepted and additional information about used Flash memory is provided. Further, the gdb runs the application, and the output on openocd will continue as below. Until it will reach the main() function.

Info : New GDB Connection: 1, Target rp2040.core0, state: halted
[rp2040.core0] halted due to breakpoint, current mode: Thread
xPSR: 0x61000000 pc: 0x10002828 msp: 0x20041f70
[rp2040.core1] halted due to debug-request, current mode: Thread
xPSR: 0x61000000 pc: 0x10002828 msp: 0x20040f68
[rp2040.core0] halted due to breakpoint, current mode: Thread
xPSR: 0xf1000000 pc: 0x000000ea msp: 0x20041f00
[rp2040.core1] halted due to debug-request, current mode: Thread
xPSR: 0xf1000000 pc: 0x000000ea msp: 0x20041f00
[rp2040.core0] halted due to breakpoint, current mode: Thread
xPSR: 0xf1000000 pc: 0x000000ea msp: 0x20041f00
[rp2040.core1] halted due to debug-request, current mode: Thread
xPSR: 0xf1000000 pc: 0x000000ea msp: 0x20041f00
Info : Padding image section 0 at 0x1000cd4c with 4 bytes
Warn : keep_alive() was not invoked in the 1000 ms timelimit. GDB alive packet not sent! (1452 ms). Workaround: increase "set remotetimeout" in GDB
[rp2040.core0] halted due to breakpoint, current mode: Thread
xPSR: 0xf1000000 pc: 0x000000ea msp: 0x20041f00
[rp2040.core1] halted due to debug-request, current mode: Thread
xPSR: 0xf1000000 pc: 0x000000ea msp: 0x20041f00
[rp2040.core0] halted due to breakpoint, current mode: Thread
xPSR: 0xf1000000 pc: 0x000000ea msp: 0x20041f00
[rp2040.core1] halted due to debug-request, current mode: Thread
xPSR: 0xf1000000 pc: 0x000000ea msp: 0x20041f00
[rp2040.core1] halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x00000184 msp: 0x20041f00

When the debug session is terminated, a message about terminated gdb connection will be displayed.

Info : dropped 'gdb' connection

That is it. And I have a pretty good idea what you are thinking right now. For now let me leave it like such 😉

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.