Debugging Rust ARM CortexM Programs with Visual Studio Code

I’ve been toying with embedded Rust off and on for some time now. I picked up one of the STM Discovery boards and it’s been an interesting novelty for me to be able to remotely debug code running on the microcontroller. I’ve never been willing to spring for the external hardware debugger required to do so, but the Discovery has a STLink built into the board. The Rust Embedded and Discovery books have great instructions for remote debugging Rust programs, but rely soley on command line tools for doing so. I like the terminal as much as, if not more, than the next person, but it’s also really nice to be able to set breakpoints and step through code right inside of the editor.

I tend to use VS Code as my main editor these days, so that’s what we’ll set up today. We’ll take a look at the hardware and setting up the toolchain first, before moving on to setting up our editor. The prior is essentially a condensed version of the information in the Discovery book, so if you’ve already worked your way through that, feel free to skip to setting up VS Code.

The Hardware

This is the STM32F3DISCOVERY board by ST.

It has a 32 bit ARM CortexM4 processor and, as I mentioned earlier, a super convenient built in debugger. It’s a really nice little board. The mcu itself has 256kb flash memory and 48kb RAM while the eval board also has an accelerometer, magnetometer, gryroscope, and lots of built in leds, IO, and peripherals to play with. To top it all off, its $16 price point brings it in as less expensive than the official Arduino Uno with its 32kb flash memory, 2kb RAM, and lack of any additional trinkets. To be fair the STM32F303VCT6 microcontroller costs 3 times the Uno’s ATMega328p and is quite a bit more complex, but I really like this board.

Installing the ToolChain

Below is a summary of the instructions in the Discovery book for setting up your environment for ARM development.

First, install the necessary build targets and extra cargo tools.

# itmdump
cargo install itm

# bin-utils
rustup component add llvm-tools-preview
rustup target add thumbv7em-none-eabihf
cargo install cargo-binutils

Then install the actual ARM toolchain itself.

If you’re using Mac, be sure to use the instructions from the Rust Embedded Book. There have been multiple pull requests open to fix this in the Discovery book for a little while now.

brew tap ArmMbed/homebrew-formulae
brew install arm-none-eabi-gcc

Install openocd, we’ll be using it to flash and debug our Discovery board.

brew install openocd

Connect the board to your laptop via the STLink USB port and verify you can connect via openocd.

$ openocd -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
    http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
none separate
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : clock speed 950 kHz
Info : STLINK v2 JTAG v27 API v2 SWIM v15 VID 0x0483 PID 0x374B
Info : using stlink api v2
Info : Target voltage: 2.900414
Info : stm32f3x.cpu: hardware has 6 breakpoints, 4 watchpoints

Clone the Discovery repository and build the led-roulette example (it’s not actually a led-roulette at this point, but it’s a good example for using the gdb debugger).

git clone https://github.com/rust-embedded/discovery.git
cd discovery/src/05-led-roulette/
cargo build --target thumbv7em-none-eabihf

Making sure openocd is still running in another terminal, connect to the device with gdb.

Note: If you’re running Linux or Windows, or installed the ARM toolchain without homebrew, the gdb command may be slightly different. See the “Flash It” section of the Discovery book.

$ arm-none-eabi-gdb -q target/thumbv7em-none-eabihf/debug/led-roulette
Reading symbols from target/thumbv7em-none-eabihf/debug/led-roulette

This will drop you into a gdb shell where you can execute the following commands to connect to the target, load the executable, set a breakpoint, and generally debug the code, with gdb commands like next and step.

(gdb) target remote :3333
Remote debugging using :3333
0x00000000 in ?? ()
(gdb) load
Loading section .vector_table, size 0x188 lma 0x8000000
Loading section .text, size 0x4102 lma 0x8000188
Loading section .rodata, size 0xf20 lma 0x8004290
Start address 0x8003df0, load size 20906
Transfer rate: 19 KB/sec, 5226 bytes/write.
(gdb) break main
Breakpoint 1 at 0x800018c: file src/05-led-roulette/src/main.rs, line 9.
(gdb) continue
(gdb) layout src

Improving the Terminal Experience

Eventually we’ll want to use the built in debugging capabilities of VS Code, but first let’s make a few adjustments to our cargo configuration to make our lives a bit easier.

First, we’ll script our gdb session to automatically load the latest code and advance us to main instead of pre-init.

openocd.gdb

target remote :3333
load
break main
continue

Then, we can update our .cargo\config to use this script as part of the runner, as well as set the default build target.

.cargo\config

[target.thumbv7em-none-eabihf]
runner = "arm-none-eabi-gdb -q -x openocd.gdb"
rustflags = [
  "-C", "link-arg=-Tlink.x",
]

[build]
target = "thumbv7em-none-eabihf"

Now, if we run cargo run, we’ll automatically build for the correct target and run our gdb script.

$ cargo run
   Compiling led-roulette v0.1.0 (/Users/rubberduck/src/discovery/src/05-led-roulette)
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
     Running `arm-none-eabi-gdb -q -x openocd.gdb /Users/rubberduck/src/discovery/target/thumbv7em-none-eabihf/debug/led-roulette`
Reading symbols from /Users/rubberduck/src/discovery/target/thumbv7em-none-eabihf/debug/led-roulette...
main () at src/05-led-roulette/src/main.rs:10
10          let x = 42;
Loading section .vector_table, size 0x188 lma 0x8000000
Loading section .text, size 0x66a lma 0x8000188
Loading section .rodata, size 0x2c4 lma 0x8000800
Start address 0x8000196, load size 2742
Transfer rate: 8 KB/sec, 914 bytes/write.
Breakpoint 1 at 0x800018c: file src/05-led-roulette/src/main.rs, line 10.
Note: automatically using hardware breakpoints for read-only addresses.

Breakpoint 1, main () at src/05-led-roulette/src/main.rs:10
10          let x = 42;
(gdb) 

Setting up Visual Studio Code

We’ll need a couple of extensions from the marketplace.
Go ahead and install the Rust Language Server and Cortex-Debug extensions from the marketplace.

Next, we’ll add a new launch configuration.

.vscode\launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "cortex-debug",
            "request": "launch",
            "servertype": "openocd",
            "cwd": "${workspaceRoot}",
            "executable": "./target/thumbv7em-none-eabihf/debug/led-roulette",
            "name": "Debug (OpenOCD)",
            "device": "STM32F303VCT6",
            "configFiles": [
                "interface/stlink-v2-1.cfg",
                "target/stm32f3x.cfg"
            ],
            "runToMain": true
        }
    ]
}

Notice that the configFiles property contains the same list of files that we passed to openocd earlier. Ensure you’ve stopped openocd in the other terminal, run the default build task (CMD + Shift + B), and then start the debug session. The code will break just inside of the main method. You’re currently debugging the code running on the Discovery board from VS Code! You can step over and into the code, as well as see the current values of the individual registers under “CORTEX REGISTERS”.

You may notice that there is also a “CORTEX PERIPHERALS” section in the debug sidebar, but it says “No SVD File Loaded: undefined”. Let’s fix that now. Download the SVD files and extract them. Copy STM32F303.svd into the src/05-led-roulette directory and add the following line to the launch.json file.

{
    "type": "cortex-debug",
    "request": "launch",
    "servertype": "openocd",
    //...
    "svdFile": "${workspaceRoot}/STM32F303.svd"
}

The next time you start the debugger, you’ll now see all of the STM32F303’s peripherals loaded. You can even directly write hex values to them!

Logging

The ARM Cortex M3 series has an Instrumentation Trace Macrocell (ITM) for sending log statements via the debugger. The Rust itm and panic-itm crates use this to send println and panic messages back to your development machine. Doing this from the terminal is described in Chapter 6: Hello World of the Discovery book. It’s worth while running through that excercise before the next section. Note that you either need to jump pins SWO and PB3 or close solder bridge SB10 on the underside of the board. I opted for the latter as it’s a pretty simple job to solder the bridge closed. If you’re uncomfortable with, or don’t have a solder iron, don’t worry, the jumper wire works just fine too.

You’re back? Awesome! Let’s integrate ITM into our editor too!

Copy .vscode/launch.json and .cargo/config from the 05-led-roulette directory into the 06-hello-world directory. Then modify the executable path and add the following swoConfig to your launch.json file.

.vscode/launch.json

"executable": "./target/thumbv7em-none-eabihf/debug/hello-world",
//...
"swoConfig": {
    "enabled": true,
    "cpuFrequency": 8000000,
    "swoFrequency": 2000000,
    "source": "probe",
    "decoders": [
        { "type": "console", "label": "ITM", "port": 0 }
    ]
},

This is essentially equivalent to the gdb commands we ran from the Discovery book.

(gdb) # globally enable the ITM and redirect all output to itm.txt
(gdb) monitor tpiu config internal itm.txt uart off 8000000

(gdb) # enable the ITM port 0
(gdb) monitor itm port 0 on

We enable the monitor, tell the debugger that the CPU is running at 8MHz (the default clock speed for the STM32F303x controllers) and connect to ITM port 0 at 2MHz. Instead of writing the output to a specific file, the Cortex-Debug extension will create a new temporary file for us.

Build the program and begin debugging.
Navigate to the OUTPUT and select the SWO: ITM[port:0, type: console] output.
Step over the iprintln and you’ll see the message written to the log.

If you run the panic example, you’ll see the panic message printed to the log.

[2019-11-29T18:04:58.320Z]   panicked at 'Hello, world!', src/06-hello-world/src/main.rs:19:5

Automatically Build before Uploading/Debugging

Up until this point, we’ve had to manually run cargo build prior to debugging any time we’ve changed the code. I’ve frustrated myself more than once by forgetting to do this, so I added a preLaunchTask to automatically build before upload. The build task is just the default cargo build task, but we need to add a label to it to be able to reference it from launch.json.

.vscode/tasks.json

{
    "label": "build",
    "type": "cargo",
    "subcommand": "build",
    "problemMatcher": [
        "$rustc"
    ],
    "group": {
        "kind": "build",
        "isDefault": true
    }
},

.vscode/launch.json

"preLaunchTask": "build",

Now when we press the “play” button to debug, we ensure we’ve compiled the latest version of our code first.

Full Config Files

That’s about it. We have a fully functioning development environment in VS Code. Complete config files are below. Enjoy!

.vscode/tasks.json

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build",
            "type": "cargo",
            "subcommand": "build",
            "problemMatcher": [
                "$rustc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ]
}

.vscode/launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "cortex-debug",
            "request": "launch",
            "name": "Debug (OpenOCD)",
            "servertype": "openocd",
            "cwd": "${workspaceRoot}",
            "executable": "./target/thumbv7em-none-eabihf/debug/compass",
            "preLaunchTask": "build",
            "device": "STM32F303VCT6",
            "configFiles": [
                "interface/stlink-v2-1.cfg",
                "target/stm32f3x.cfg"
            ],
            "svdFile": "${workspaceRoot}/STM32F303.svd",
            "runToMain": true,
            "swoConfig": {
                "enabled": true,
                "cpuFrequency": 8000000,
                "swoFrequency": 2000000,
                "source": "probe",
                "decoders": [
                    { "type": "console", "label": "ITM", "port": 0 }
                ]
            }
        }
    ]
}

.cargo/config

[target.thumbv7em-none-eabihf]
# Not used by the VS Code Debugger, only `cargo run`
runner = "arm-none-eabi-gdb -q -x openocd.gdb"
rustflags = [
  "-C", "link-arg=-Tlink.x",
]

[build]
target = "thumbv7em-none-eabihf"

openocd.gdb

# Not used by the VS Code Debugger, only `cargo run`
target remote :3333
set print asm-demangle on
set print pretty on
load
monitor tpiu config internal itm.txt uart off 8000000
monitor itm port 0 on
break main
continue

, ,

  1. Leave a comment

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: