Remote Debugging with DevContainer and VS Code

This is the second part describing how to use DevContainer for embedded applications with VS Code.

In Optimizing Embedded Development with VS Code and DevContainer I use VS Code with a docker ‘development’ container. The container is created with a recipe to install the development tools and SDK. With VS Code and DevContainer I have the same developer experience as I would develop locally on the host.

One thing not addressed in that earlier article is debugging. Using USB debug probes like a SEGGER J-Link or NXP MCU-Link is a challenge inside a container. This applies to any other debug probe as well.

The solution is to transform an USB based debug probe into one with a network connection:

VS Code DevContainer with Hardware Debugging
VS Code DevContainer with Hardware Debugging

In this article, I show how to do this.

Outline

From a Docker container, one can easily use a networking connection to the host. It can also communicate with other containers, or the internet. A connection is easily possible with anything that uses TCP/IP. Using USB (non-IP) based debug probes is a challenge, especially using Windows as the host OS.

That problem does not exist if you are using a networking debug probe. For example see Debugging ARM Cores with IP based Debug Probes and Eclipse. But such debug probes are expensive, costing around $1000.

LAN Port
LAN Port on Hardware Debug Probe

In this article I’ll show one can use an USB based debug probe from a development container with VS Code. It means making a USB based probe accessible through the network.

I’m using in this article the LPC55S16-EVK from NXP with an on-board NXP LinkServer debug probe:

lpc55s16-evk
lpc55s16-evk

Solution

The solution is to use a normal USB based debug probe like a networking probe:

VS Code DevContainer with Debugging
VS Code DevContainer with Debugging

The concept is to use the remote gdb server mode with the cortex-debug (marus25.cortex-debug) VS Code debug extension. The the concepts are explained in Remote Board Debugging: J-Link Remote Server with Eclipse and Remote Debugging with USB based JTAG/SWD Debug Probes.

NXP LinkServer Debugging with Remote GDB Server

With a NXP LinkServer based debug probe, start the GDB server on the host in a console for your target:

linkserver gdbserver --keep-alive LPC55S16:LPCXpresso55S16

The ‘gdbserver‘ command line option starts LinkServer in gdb server mode, listening by default on port 3333. The --keep-alive keeps it running after a disconnect. Then it waits for a connection:

LinkServer waiting for connection
LinkServer waiting for connection

From the running container, I can use host.docker.internal to get the IP of the host OS.

A typical launch configuration for LinkServer and remote debugging looks like this:

{
  "name": "LinkServer cortex-debug remote",
  "type": "cortex-debug",
  "request": "launch",
  "servertype": "external",
  "gdbTarget": "host.docker.internal:3333",
  "cwd": "${workspaceFolder}",
  "executable": "${command:cmake.launchTargetPath}",
  "armToolchainPath": null,
  "postLaunchCommands": [
     "monitor semihosting enable"
    ],
  "runToEntryPoint": "main",
  "rtos": "FreeRTOS",
  "svdFile": "./sdk/device/LPC55S16.svd",
},        

For information, below is how the gdb server responds on a connection:

c:\tmp>linkserver gdbserver --keep-alive LPC55S16:LPCXpresso55S16
INFO: Exact match for LPC55S16:LPCXpresso55S16 found
INFO: Selected device LPC55S16:LPCXpresso55S16
INFO: Getting available probes
INFO: Selected probe #1 ORAWBQIQ (LPC-LINK2 CMSIS-DAP V5.460)
GDB server listening on port 3333 in debug mode (core cm33)
Semihosting server listening on port 4444 (core cm33)
INFO: [stub (3333)] Ns: LinkServer RedlinkMulti Driver v24.12 (Dec 18 2024 18:34:07 - crt_emu_cm_redlink.exe build 869)
INFO: Connected to core cm33
INFO: [stub (3333)] Pc: (  0) Reading remote configuration
INFO: [stub (3333)] Wc(03). No cache support.
INFO: [stub (3333)] Nc: Found generic directory XML file in C:\Users\ERICHS~1.N00\AppData\Local\Temp\tmpirfqukp7\crt_directory.xml
Pc: (  5) Remote configuration complete
INFO: [stub (3333)] Nc: Reconnected to existing LinkServer process.
INFO: [stub (3333)] Nc: Probe Firmware: LPC-LINK2 CMSIS-DAP V5.460 (NXP Semiconductors)
Nc: Serial Number:  ORAWBQIQ
Nc: VID:PID:  1FC9:0090
Nc: USB Path: \\?\hid#vid_1fc9&pid_0090&mi_00#7&7e77c7e&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
....
Pb: 1 of 1 (  0) Writing sectors 0-91 at 0x00000000 with 46852 bytes
Ps: (  0) at 00000000: 0 bytes - 0/46852
INFO: [stub (3333)] Ps: (100) at 00000000: 47104 bytes - 47104/46852
Nc: Sectors written: 0, unchanged: 92, total: 92
Nc: Closing flash driver LPC551x.cfx
INFO: [stub (3333)] Pb: (100) Finished writing Flash successfully.
Nc: Starting execution using system reset and halt target with a stall address
INFO: [stub (3333)] Nc: Retask read watchpoint 1 at 0x50000040 to use for boot ROM stall
INFO: [stub (3333)] Nc: Boot ROM stalled accessing address 0x50000040 (restoring watchpoint 1)
INFO: [stub (3333)] Nc: Using memory from core 0 after searching for a good core
INFO: [stub (3333)] Pc: ( 30) Emulator Connected
INFO: [stub (3333)] Pc: ( 40) Debug Halt
INFO: [stub (3333)] Pc: ( 50) CPU ID
INFO: [stub (3333)] Nc: debug interface type      = CoreSight DP (DAP DP ID 6BA02477) over SWD TAP 0
Nc: processor type            = Cortex-M33 (CPU ID 00000D21) on DAP AP 0
Nc: number of h/w breakpoints = 8
Nc: number of flash patches   = 0
Nc: number of h/w watchpoints = 4
Nc: Starting execution using system reset and halt target with a stall address
INFO: [stub (3333)] Nc: Retask read watchpoint 1 at 0x50000040 to use for boot ROM stall
INFO: [stub (3333)] Nc: Boot ROM stalled accessing address 0x50000040 (restoring watchpoint 1)

With this, I’m debugging 🙂

Debugging with DevContainer
Debugging with DevContainer

SEGGER JLink Debugging with JLink Remote Server

If using a J-Link, then I can use JLinkRemoteServerCL without tunnel server (see https://mcuoneclipse.com/2017/02/05/remote-board-debugging-j-link-remote-server-with-eclipse/ if you are interested in tunneling).

JLinkRemoteServerCL

Then it waits for a connection:

JLinkRemoteServerCL
JLinkRemoteServerCL

A debug remote configuration for J-Link looks like this:

{
  "name": "J-Link cortex-debug JLinkRemoteServerCL",
  "type": "cortex-debug",
  "request": "launch",
  "serverpath": null,
  "servertype": "jlink",
  "ipAddress": "192.168.65.254", // IP of "host.docker.internal", // run JLinkRemoteServerCL on host
  "cwd": "${workspaceFolder}",
  "executable": "${command:cmake.launchTargetPath}",
  "armToolchainPath": null,
  "device": "LPC55S16",
  "interface": "swd",
  "runToEntryPoint": "main", //"main" or "ResetISR"
  "postLaunchCommands": [
    "monitor semihosting enable",
    "monitor semihosting ioclient 1", // 1: telnet (port 2333); 2: gdb; 3: both telnet and gdbclient output
    ],
  "rtos": "FreeRTOS",
  "svdFile": "./sdk/device/LPC55S16.svd",
},

💡 Note that I have to specify the IP address directly. Ping host.docker.internal from the container to get the address.

When debugging, it gets connected to the server:

Client connected
Client connected

and I’m debugging 🙂

Debugging with J-Link and DevContainer
Debugging with J-Link and DevContainer

SEGGER JLink Debugging with Remote GDB Server

The other way is to use a traditional GDB server. Start the server on the host with:

JLinkGDBServerCL -device LPC55S16 -if SWD

This starts the server and it waits on port 2331:

JLink GDB Server waiting or connection
JLink GDB Server waiting or connection

My debug configuration looks like this:

{
   "name": "J-Link cortex-debug gdbServer",
   "type": "cortex-debug",
   "request": "launch",
   "serverpath": null,
   "servertype": "external",
   "gdbTarget": "host.docker.internal:2331", // start server with JLinkGDBServerCL -device LPC55S16 -if SWD
   "cwd": "${workspaceFolder}",
   "executable": "${command:cmake.launchTargetPath}",
   "armToolchainPath": null,
   "device": "LPC55S16",
   "interface": "swd",
   "runToEntryPoint": "main", //"main" or "ResetISR"
   "postLaunchCommands": [
     "monitor semihosting enable",
     "monitor semihosting ioclient 1", // 1: telnet (port 2333); 2: gdb; 3: both telnet and gdbclient output
       ],
   "rtos": "FreeRTOS",
   "svdFile": "./sdk/device/LPC55S16.svd",
 },

Summary

Using a TCP/IP connection I can use normal USB based debug probes with VS Code and DevContainer. All what I need is to start the server on the host, and I can debug:

Debugging VS Code and DevContainer
Debugging VS Code and DevContainer

My target and debug probe do not have to be connected physically with the host using this approach. The probe and target can be in a remote test farm. I don’t have to use a PC machine for this. The GDB server could be running, for example, on a Raspberry Pi SBC.

If you can spend the money, I recommend adding a TCP/IP based debug probe to your arsenal of tools. That way you even don’t need to run a remote GDB server. Instead you can connect directly to the debug probe.

Happy debugging 🙂

Links

7 thoughts on “Remote Debugging with DevContainer and VS Code

  1. This is a very useful note, thank you! Have you been able to get access to SWO data using the standalone LinkServer application? It seems well integrated with MCUXpresso Eclipse IDE, but I can’t find any documentation on how the IDE obtains the SWO data from LinkServer while gdbserver is running.

    Like

    • Hi Stephen,
      I’ll have to recheck this. I believe currently SWO is directly handled by LinkServer internally and not exposed by other means. I think I had filed a feature request to have an IP port in LinkServer to get that data, but I believe that has not been implemented yet. I’ll have to follow up on this.

      Like

What do you think?

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