Using a Debugger


Interactive debugging, using for example gdb, is where you set breakpoints in your program, can step through the program line-by-line, and can examine variable values while the program is running. It allows far greater visibility than what you can achieve by just inserting print statements into your program. However, it is considerably more challenging to get interactive debugging working on an embedded device such as the RP2040 on the Pololu robot than on a program running on your host computer. Here, we explain how this can be accomplished, but it requires wiring a secondary embedded device, a Raspberry Pi Pico, to the Pololu robot and then connecting your host computer to the secondary Pico using its Micro USB interface. The secondary Pico looks like this:

Raspberry Pi Pico


Detailed instructions can be found in Appendix A of the Getting Started with Raspberry Pi Pico document.

You will need the following hardware:

Software that you will need:

Your favorite package manager may work to install the on-chip debugger software, openocd, on your host computer. E.g., on a Mac,

$ brew install openocd

Detailed instructions for installing from source on various platforms can be found in Appendix A of the Getting Started with Raspberry Pi Pico document.


The first step is connect the Pico to your computer using the Micro USB cable while holding the BOOTSEL button. The Pico should appear like an external disk on your computer. You can now install applications on it using picotool, just as you installed LF programs on the robot. NOTE: If the Pico does not appear as an external disk, it is possible that your Micro USB cable is of the wrong type. Sadly, it is common for Micro USB cables to only include the wires that supply power, not the wires needed for transferring data. The manufacturers probably saved a few micropennies.

The particular application you will need on the Pico is picoprobe. This application allows a Raspberry Pi Pico to act as a cmsis-dap device which interacts with the target RP2040 on the robot through a serial wire debug (SWD) interface connection. Dowload the picoprobe.elf file (or the picoprobe.uf2). Then, with your Pico in BOOTSEL mode, upload the file:

$ picotool load picoprobe.elf

Now, your Pico is ready to serve as a debugging bridge.


Once the probe device (the Pico) is prepared, wire it up to the target device (the robot) as follows:

Probe GND -> Target GND
Probe GP2 -> Target SWCLK
Probe GP3 -> Target SWDIO
Probe GP4 (UART1 TX) -> Target GP1 (UART0 RX)
Probe GP5 (UART1 RX) -> Target GP0 (UART0 TX)

The last two wires connect the probe to UART0, which by default carries standard I/O and hence should make visible any printf outputs from your program through the probe. You should be able to see this output using screen as explained in the Tools lab.

To make these connections, if you bought the robot assembled, then it likely came without expansion headers, so you will need to solder headers onto your robot so that it looks like this:

Headers needed on robot

The particular pins on the robot headers that you need are labeled within the circles below:

Pins needed on the robot

With wires attached, it should look like this:

robot wiring

The wires needed on the Pico are these:

Pico wiring

Complete wiring looks like this:

Robot to Pico wiring

NOTE: The wiring connects the ground of the Pico to the ground of the robot, but there is no power connection to the robot. You will need to power the robot using its own batteries.

Debug Symbols

For a debugger to be able to meaningfully interact with an executing program, it is necessary for the binary .elf file that you flash onto the robot to include debug symbols. This means that a symbol table is included in the binary that tells the debugger where in memory functions and variables are located. For a Lingua Franca program to include such debug symbols, the target directive needs to include a build-type field set to debug, as follows:

target C {
  platform: {
    name: "rp2040",
    board: "pololu_3pi_2040_robot"
  threading: false,
  build-type: debug

To test this, modify src/Blink.lf to include the build-type field and compile the program:

lfc src/Blink.lf

This will create a file bin/Blink.elf that includes debug symbols.


Open On-Chip Debugger is a program that runs on your host machine. It is a debug translator that understands the SWD protocol and is able to communicate with the probe device while exposing a local debug server for a debugger to attach to.

After wiring, run the following command to flash a Blink.elf binary with debug symbols onto the robot:

openocd -f interface/cmsis-dap.cfg -c "adapter speed 5000" -f target/rp2040.cfg \
    -c "program bin/Blink.elf verify reset exit"

The above will specify the

  • probe type: cmsis-dap
  • the target type: rp2040
  • commands: the -c flag will directly run open ocd commands used to configure the flash operation.
    • adapter speed 5000 makes the transaction faster.
    • program <binary>.elf specifies the elf binary to load into flash memory.
    • verify reads the flash and checks against the binary.
    • reset resets and starts executing the program.
    • exit disconnects openocd.

When this is finished, the LED on the robot should be blinking.


The gnu debugger gdb is an open-source program for stepping through application code. Here we use the remote target feature to connect to the exposed debug server provided by openocd.

First restart openocd using the following command:

openocd -f interface/cmsis-dap.cfg -c "adapter speed 5000" -f target/rp2040.cfg -s tcl

Included in the output from the above command should be something like this:

Info : starting gdb server for rp2040.core0 on 3333
Info : Listening on port 3333 for gdb connections
Info : starting gdb server for rp2040.core1 on 3334
Info : Listening on port 3334 for gdb connections

In a separate terminal window, run the following gdb session specifying the .elf binary. Since this binary was built using the debug directive, it will include a symbol table that will be used for setting up breakpoints in gdb.

gdb bin/Blink.elf

Once the GDB environment is opened, connect to the debug server using the following. Each of the two cores exposes its own port. With threading set to false in the target directive, core0 runs the main thread, so you want to connect gdb to port 3333:

(gdb) target extended-remote localhost:3333

This will connect gdb to the executing program and stop the program at whatever point it happens to be running. You will see output something like this:

Remote debugging using localhost:3333
alarm_pool_get_default () at /Users/eal/git/lf-3pi-template/pico-sdk/src/common/pico_time/time.c:93
93	alarm_pool_t *alarm_pool_get_default() {

This means that the program halted at line 93 of a Pico SDK file called time.c. The LED should no longer be blinking.

From this point on, normal gdb functionality such as breakpoints, stack traces, and register analysis can be accessed through various gdb commands. A gdb tutorial might be helpful at this point.

To set a breakpoint at a Lingua Franca reaction, and relatively easy way to to that is to find the function name for the function generated by lfc for the reaction. For the Blink program, you can examine the generated code using VS Code:

code src-gen/Blink/

In the file _blink_main.c, there are two functions, _blink_mainreaction_function_0 and _blink_mainreaction_function_1. These correspond to the two reactions in Blink.lf, where the main reactor is this:

main reactor {
  timer t(0, 250 ms);
  state led_on:bool = false;

  reaction(startup) {=
    gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT);
  reaction(t) {=
    self->led_on = !self->led_on; 
    gpio_put(PICO_DEFAULT_LED_PIN, !self->led_on);

In gdb, you can set a breakpoint at the second of these reactions, examine the stack, print variable values, etc. For example, here is an interactive session:

(gdb) break _blink_mainreaction_function_1
Breakpoint 1 at 0x10000304: file /Users/eal/git/lf-3pi-template/src/Blink.lf, line 26.
Note: automatically using hardware breakpoints for read-only addresses.
(gdb) continue

Breakpoint 1, _blink_mainreaction_function_1 (instance_args=0x200028f8) at /Users/eal/git/lf-3pi-template/src/Blink.lf:26
26	    self->led_on = !self->led_on; 
(gdb) print self
$1 = (_blink_main_main_self_t *) 0x200028f8
(gdb) print self->led_on
$2 = false
(gdb) next
27	    gpio_put(PICO_DEFAULT_LED_PIN, !self->led_on);
(gdb) print self->led_on
$3 = true
(gdb) next
_lf_invoke_reaction (env=env@entry=0x200018d0 <envs>, reaction=reaction@entry=0x20002970, worker=worker@entry=0)
    at /Users/eal/git/lf-3pi-template/src-gen/Blink/core/reactor_common.c:1351
1351	    ((self_base_t*) reaction->self)->executing_reaction = NULL;

At this point, the LED should have gone from off to on (or on to off, depending on its state when you connected gdb).

Visual Studio Code

Breakpoints can be placed in generated lingua franca project source code within vscode. As a part of the code generation process, each lfc generates a .vscode directory in the program src-gen folder. This is populated with settings to configure and run gdb, openocd and cmake.

Run the following after generating code for the desired lf program:

$ code src-gen/Blink/

In vscode, navigate to the run and debug tab on the left. A Pico Debug option should appear at the top as shown here:

Debugging in VSCode

First, you will need for VS Code to build the project. Clean up from the previous lfc builds as follows:

rm -rf src-gen/Blink/build

Then click on the blue Build button at the bottom of the VS Code window.

Make sure the picoprobe is connected and wired as noted above and click run (the green triangle). The debugger should automatically break at the main method of the application. Breakpoints can be visually inserted and other debugger options are accessible through the IDE gui.

Troubleshooting: Depending on the platform you are running on and how gdb was installed, you may see the following error when try to run the program:

VS Code error screen

Click on Open 'launch.json' and edit this line:

            "gdbPath" : "gdb-multiarch",

This line specifies the gdb program to use. It may be simply:

            "gdbPath" : "gdb",