Since we are working with containers and VS Code is pretty popular this post aims to show you how to integrate a Zephyr containers into VS Code, the procedure basically is already written in the following post https://embedded-house.ghost.io/part-7-docker-for-vscode-lovers/ , but this time will be specific for the modularmx/zephyros container.

đź’ˇ
I’m going to assume you’re now a container ninja and have already taken our world-famous, award-winning, mom-approved training at Embedded House: https://embedded-house.ghost.io/tag/dcoker-for-embedded/. If not… what are you waiting for? A handwritten invitation? Go check it out!

Make a new file called compose.yml on the root folder of your project that we previously made in https://embedded-house.ghost.io/working-with-manifests/

$ code compose.yml

And write the following content

services:
  zephyr:           # container name been used by the image to connect to make
    image: modularmx/zephyros:latest
    working_dir: /home/user/workspace
    volumes:        # volume mapping
      - type: bind
        source: ./
        target: /home/user/workspace
    command:        # keep the container running
      sleep infinity

Also create the corresponding devcontianer file in the same directory

{
    // Name of the dev container
    "name": "devtools",     
    // compose file to run containers     
    "dockerComposeFile": "compose.yml",
    // indicate the container to run from the compose file
    "service": "zephyr",
    // indicate the working directory inside the container
    "workspaceFolder": "/home/user/workspace",
    // set bash as the default shell    
    "postStartCommand": "/bin/bash",

    // set some usefull vs code configuration only exsiting in our contianer
    "customizations": {
        "vscode": {
            // Add some extensions
            "extensions": [
                "ms-vscode.cpptools",
                "trond-snekvik.gnu-mapfiles",
                "twxs.cmake"
            ],
            // Set a fancy themes
            "settings": {
                "workbench.colorTheme": "One Dark Pro Night Flat"
            }
        }
    }
}

Let me refresh your mind about our project directory structure, and yes I’m using a manifest, so get used to it.

.
├── app
├── build
├── compose.yml
├── deps
├── .devcontainer.json
└── .west

In VS Code press Ctrl + p and select Dev Contianers: Reopen in Container. Now you are basically running the container in VS Code, how cool is that, and from here you can build your project, and so on. Again I’m assuming you have the docker plugin installed in your VSCode

Debugging with VS Code

What about debugging?, I know we can debug using the cortex debug plugin inside VS Code AN003: Debugging with VS Code , Well, lets get to it, add JLink just like we did in the previous post Using Containers for good. Modify the compose.yml to change image field with any name you decided to gave, in my case is “west_jlink” and grant access to USB

services:
  zephyr:           # container name
    image: west_jlink  # image with zephyr and j-Link
    working_dir: /home/user/workspace
    volumes:           # mount directory
      - type: bind
        source: ./
        target: /home/user/workspace
    devices:                # device mapping to usb ports
      - /dev/bus/usb:/dev/bus/usb
    command:        # keep the container running
      sleep infinity

Also add a the cortex-debug plugin to our devcontainer file.

            // Add some extensions
            "extensions": [
                "ms-vscode.cpptools",
                "trond-snekvik.gnu-mapfiles",
                "twxs.cmake",
                "marus25.cortex-debug"
            ],

Write a launch.json file as usually inside a .vscode folder, it is important to indicate the SDV file and the most important one the gdbPath, this is the path where the arm gdb can be found inside our container. In case using openocd adjust accordingly.

{
    "version": "1.12.1",
    "configurations": [
        {
            "type": "cortex-debug",
            "request": "launch",
            "name": "Debug (J-Link)",
            "servertype": "jlink",
            "interface": "swd",
            "cwd": "${workspaceRoot}",
            "runToEntryPoint": "main",
            "executable": "${workspaceRoot}/build/zephyr/zephyr.elf",
            "device": "nrf54l15_m33",
            "svdFile": "${workspaceRoot}/deps/modules/hal/nordic/nrfx/mdk/nrf54l15_application.svd",
            "armToolchainPath": "/opt/zephyr-sdk/arm-zephyr-eabi/bin",
            "toolchainPrefix": "arm-zephyr-eabi"
        }
    ]
}

Launch a debug session as you normally would do, and that’s it!, now you can build, flash and debug with your lovely favorite editor.

Using two containers

Now lets imagine you already have a separate container with your J-Link/OpenOCD server and don’t want to modify our Zephyr container, we can orchestrate both with compose Part 4: Time to Docker Compose and use the same devcontianer of course, just modify the compose file in the following way

services:
  server:      # container name 
    image: myjlink
    ports:          # port mapping
      - 2331:2331
    devices:        # device mapping to usb ports
      - /dev/bus/usb:/dev/bus/usb
    networks:
      static_net:         # use network static_net
        ipv4_address: 172.25.0.2  # assing ip address to container
    command: JLinkGDBServer -nolocalhostonly -device nrf54l15_m33 -if swd -speed 4000
    
  zephyr:      # container name 
    image: modularmx/zephyros:latest
    working_dir: /home/user/workspace
    volumes:        # volume mapping
      - type: bind
        source: ./
        target: /home/user/workspace
    depends_on:
      - server
    networks:
      static_net:         # use network static_net
        ipv4_address: 172.25.0.3  # assing ip address to container
    command:                # keep the container running
      sleep infinity
      
networks:
  static_net:          # network name to be used by containers
    driver: bridge    # network driver
    ipam:     
      config:         # network configuration
        - subnet: 172.25.0.0/16 # network subnet
          gateway: 172.25.0.1   # network gateway

We need to modify our .vscode/launch.json changing the server type to “external“ and indicating the IP assigned to our debug server container. With this you can launch you debug session as before, to see the same results. This is my favorite of both ways since I have the flexibility to change my tools and avoid modifications in my containers.

{
    "version": "1.12.1",
    "configurations": [
        {
            "type": "cortex-debug",
            "request": "launch",
            "name": "Debug (J-Link)",
            "servertype": "external",
            "gdbTarget": "172.25.0.2:2331",
            "interface": "swd",
            "cwd": "${workspaceRoot}",
            "runToEntryPoint": "main",
            "executable": "${workspaceRoot}/build/zephyr/zephyr.elf",
            "svdFile": "${workspaceRoot}/deps/modules/hal/nordic/nrfx/mdk/nrf54l15_application.svd",
            "armToolchainPath": "/opt/zephyr-sdk/arm-zephyr-eabi/bin",
            "toolchainPrefix": "arm-zephyr-eabi"
        }
    ]
}

Serial monitor

By the way if you want to monitor the serial port in you container you can use the plugin serial monitor from Microsoft, just add it in the devcontainer file, but also you need add one extra package for your container using postCreateCommand option and set permission to our “user” in dialout group, otherwise the plugin could not connected to the serial port

    // execute commands after the container is created
    "postCreateCommand": "sudo apt-get update && sudo apt-get install -y udev && sudo usermod -aG dialout user",
    // set bash as the default shell    
    "postStartCommand": "/bin/bash",
    
    // set some usefull vs code configuration only exsiting in our contianer
    "customizations": {
        "vscode": {
            // Add some extensions
            "extensions": [
                "ms-vscode.cpptools",
                "trond-snekvik.gnu-mapfiles",
                "twxs.cmake",
                "plorefice.devicetree",
                "marus25.cortex-debug",
                "ms-vscode.vscode-serial-monitor"
            ],

The serial port in the case of our Nordic board will appear as ttyACM1

Using Ozone

But you know I really like Ozone, can we use ozone instead of cortex-debut, still I want VS Code to run the zephyr container

services:
  ozone:      # container name 
    image: myozone
    devices:        # device mapping to usb ports
      - /dev/bus/usb:/dev/bus/usb
    working_dir: /home/user/workspace
    volumes:        # volume mapping
      - type: bind
        source: ./
        target: /home/user/workspace
    environment:
      - DISPLAY=${DISPLAY}
    network_mode: host
    depends_on:     # run until zephyr is up and running first
      - zephyr
    command:        # keep the container running
      ozone
    
  zephyr:      # container name 
    image: modularmx/zephyros:latest
    working_dir: /home/user/workspace
    volumes:        # volume mapping
      - type: bind
        source: ./
        target: /home/user/workspace
    command:                # keep the container running
      sleep infinity

Once you run the devcontainer ozone will open, from here and after you build your project you have to proceed as in previous post or as in shown in this video Ozone – The J-Link Debugger | Introduction . If you close ozone window you will have to rebuild to reopen your devcontianer again…

If you want to launch in auto you debug session, meaning with the device and the source file selected you can append the following to line 17 in the previous compose file, but you need to previously build your project

command:        # keep the container running
    ozone -device nrf54l15_m33 -select USB -if SWD -speed 4000 -programfile /home/user/workspace/build/zephyr/zephyr.elf

If you don’t want to luach ozone inmedialty you can replace the prevosus commnad

command:                # keep the container running
    sleep infinity

Then, in another terminal outside VS Code connect to the ozone running container

$ docker exec -it zephyr-ozone-1 ozone -device nrf54l15_m33 -select USB -if SWD -speed 4000 -programfile /home/user/workspace/build/zephyr/zephyr.elf

Hope this can help you to work with containers in a more automated way, and choose wherever option you think is more appropriate for you, see you in the road