Skip to content

Instantly share code, notes, and snippets.

@hans-jorg
Last active February 13, 2024 02:03
Show Gist options
  • Save hans-jorg/f12214e1738b687da60d492c0077692e to your computer and use it in GitHub Desktop.
Save hans-jorg/f12214e1738b687da60d492c0077692e to your computer and use it in GitHub Desktop.
VS Code for C Development including for Embedded Systems

Using VS Code for C development including for Embedded Systems

Contents

  • Introduction
  • Installation
  • Single C file project
  • A simple project with two C files
  • A simple project with a subfolder
  • Multiple C files project using Makefile
  • Additional extensions for C/C++ development
  • Using VS Code for ARM development
    • Using a direct approach
    • Using Cortex Debug extension
  • References

Introduction

VS Code is a open source editor based on the editor in (commercial) Microsoft Visual Studio. It was made available by Microsoft as a Github project. There are version for Windows (naturally), Linux (Debian and derivatives, Red Hat and derivatives) and Apple Macs.

The main features of VS Code are:

  • Multilanguage
  • Cross Platform
  • Free
  • Scalability
  • Extensibility
  • Customizability

There is a repository for extensions that makes their installation very simple. One problem is the huge amount of available extensions.

The original version by Microsoft hat some telemetry incorporated and this is whorrisome and disturbing. Since it is a open source project, there are versions without this telemetry. VS Codium is a free open source project without telemetry hosted in Github too.

Installation

At the Visual Studio Code site, it is possible to download and install a package, either for Debian or for Fedora. To install it into Debian and derivatives (Linux Mint, for example), either download it using the browser by clicking on the .deb button or using the command line

wget https://code.visualstudio.com/sha/download?build=stable&os=linux-deb-x64

After that, use the command (xx, y and zzzzzzzzzz are version, subversion and release numbers of the downloaded package).

sudo dpkg -i code_1.xx.y-zzzzzzzzzz_amb64.deb

For Linux, there is also a SNAP version at https://snapcraft.io/code. To install it, use the command

snap install code

For Windows, there are packages in the VS Code site in MSI, ZIP and EXE formats. There is no version in the Windows Store.

A C compiler and a debugger must be available too. In Linux systems, it is simple to install them using the repository manager. For example, in Debian and derivatives, one can use the commands below to install them.

sudo apt update
	sudo apt install build-essential gdb

For Windows systems, install MinGW GCC for Windows 64 & 32 bits. Detailed instructions can be found in Using GCC with MingGW

VS Code is hightly configurable and extensible and does not have builtin support for C/C++ developmento. This is done thru extensions.

The VS Code Marketplace has many extensions for C Development. An extension can be installed by:

  • Command line: At a terminal enter

      code --install-extension extension-name
    
  • From GUI. Select View/Extension and enter extension-name in the field below EXTENSIONS MARKETPLACE. The clock on the desired one to select it. In the main windows, clock on the green Install button.

For C development, the C/C++ extension by Microsoft is needed to help manipulate C files and projects. It provides, as descrived in its description, Intellisense (code coloring, autocompletion, etc), debugging and code browsing support.

Other extensions can be installed but they are not essential for simple projects.

Installed extensions in my machine

C/C++: Enables intellisense, syntax coloring, snippets
Gitlens: Enhance support for git
Makefile tools: Syntax coloring, intellisense for Makefiles
Doxygen Documentation Generator: Makes documentation easier
Global Config: copies {tasks,c_cpp_properties,launch}.json from a folder to >project
CMake Tools: Support for CMake
Markdown All in One: Support for Markdown (README.md and *.md)
gitignore: helps manage .gitignore files
Template: helps reuse of files and folder structures
Cortex Debug: Configure VS Code to debug a Cortex M thru a GDB proxy
Linker Script: Syntax coloring, auto completion for GNU linker scripts
PlatformIO IDE: Embedded development with support for many kernels

A single C file project

To create a one file project, start VS Code, click on Explorer button, on the left toolbar and then click on New File... in the main window. Save the (empty) file as hello.c in a hello folder (create it!). Doing so, by knowing that it is a C file, VS Code helps you to enter C code.

Enter the text below. Note that if you enter main and the press Enter, a complete main function is inserted. When you type printf, some information about the parameters is displayed by pressing Ctrl+Space.

hello.c

#include<stdio.h>

int main(int argc, char const *argv[])
{
    printf("Hello\n");

    return 0;
}

To save it, enter Ctrl-S ou select Save from File menu.

This code is not associated with a project, or in VS Code vocabulary, a folder. To do so, select Open Folder... from File menu and in the dialog window, navigate, if needed, to hello folder and click on the OK button.

To build (compile) it, select the hello.c file and then select Run Build Task... and select C/C++: gcc build active file. The default action is to generate an executable called hello (same name of C source file, but without the .c suffix).

To compile and run it, select the hello.c, select either Start Debugging or Run without Debugging from the Run menu. By the first time, some configuration information must be provided. First, select C++ (GDB/LLDB). Then select gcc - Build and debug active file. The standard configuration is to generate an executable with the same name of the C file, without the .c suffix, as above.

NOTE: The hello.c must be selected in the explorer toolbar in order to build or run the executable.

VS Code uses JSON configuration files in the (hidden in Linux systems) folder, called .vscodes.

  • tasks.json: It instructs VS Code how to compile and build the executable.
  • launch.json: It instruct VS Code how to run and/or debug the executable.
  • c_cpp_properties.json: configures Intelisense for C/C++ files.

The tasks.json and launch.json are created then using the Start debugging or Run debugging of the "Run* menu. An example tasks.json file is shown below.

{
    "tasks": [
        {
            "type": "cppbuild",
            "label": "C/C++: gcc build active file",
            "command": "/usr/bin/gcc",
            "args": [
                "-g",
                "${file}",
                "-o",
                "${fileDirname}/${fileBasenameNoExtension}"
            ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "Task generated by Debugger."
        }
    ],
    "version": "2.0.0"
}

The main compilation parameters are in the args attribute. For example, if the code uses the math library, a "-lm" parameter must be added.

 ...
 "args": [
        "-g",
        "${file}",
        "-o",
        "${fileDirname}/${fileBasenameNoExtension}",
        "-lm"
],
...

The output file is configured to ${fileDirname}/${fileBasenameNoExtension}. This means that the output file will have the same name as the selected source file, but without the .c suffix and it will be placed in the same folder.

It will compile only the selected file, as instructed by the ${file} parameter.

A launch.json file is created when the executable is run, either using Run/Start Debugging ... or Run/Run Without Debugging.

The generated launch.json file needs to be modified.

{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
    {
        "name": "gcc - Build and debug active file",
        "type": "cppdbg",
        "request": "launch",
        "program": "${fileDirname}/${fileBasenameNoExtension}",
        "args": [],
        "stopAtEntry": false,
        "cwd": "${workspaceFolder}",
        "environment": [],
        "externalConsole": false,
        "MIMode": "gdb",
        "setupCommands": [
            {
                "description": "Enable pretty-printing for gdb",
                "text": "-enable-pretty-printing",
                "ignoreFailures": true
            }
        ],
        "preLaunchTask": "C/C++: gcc build active file",
        "miDebuggerPath": "/usr/bin/gdb"
    }
]

}

VS Code uses the configuration described in the launch.json file to start a debug session or to run the executable.

The main attributes are program and args. The program parameter must be the name of the executable generated in the build step in tasks.json, as described above.

A simple workspace was build and it is possible to return to it by selection Open Folder... from the File menu. To exit this workspace, select Close Folder from the File menu.

Additionally, parameters can be added to the project by a file called c_cpp_properties.json. It can be created by pressing Ctrl-Shift-P and entering/choosing C/C++: Edit Configuration (JSON). VS Code tries to use a compiler found search the folders in the PATH environment variable. Generally, it does not generate the correct configuration, because compilerPath and intelliSenseMode are wrong. To correct it, select the value field and enter Ctrl-Space to see the options.

An example c_cpp_properties.json file, already corrected, is

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "gnu17",
            "cppStandard": "gnu++14",
            "intelliSenseMode": "linux-gcc-x64"
        }
    ],
    "version": 4
}

The most important attributes are includePath and defines, that informs the compiler where to find the header files and preprocessor symbols, respectively.

A simple project with two files

A similar approach can be used in the case of two C (.c) files and a header (.h) file, all in the same folder. Create them and then use the "Open Folder..." of the File menu to open the project.

The contents of the files are:

hello.c

#include <stdio.h>
#include "hello.h"

void hello(void)
{
    printf("Hello\n");
}

main.c

#include "hello.h"

int main(int argc, char const *argv[])
{
    hello();

    return 0;
}

hello.h

void hello(void);

This project demands more convoluted configuration files.

In the tasks.json file, an additional filename must be inserted into the args parameter. The modified args shouild be as shown below. The ${file} was removed, so the task can be run without selecting a .c file.

tasks.json

{
    "tasks": [
        {
            "type": "cppbuild",
            "label": "C/C++: gcc build active file",
            "command": "/usr/bin/gcc",
            "args": [
                "-g",
                "main.c",
                "hello.c",
                "-o",
                "${workspaceFolder}/${workspaceFolderBasename}"
            ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "Task generated by Debugger."
        }
    ],
    "version": "2.0.0"
}

The output filename was changed too. It uses now the workspace name instead of the name of selected file for the executable.

The launch.json must be changed too, in order to run the generated executable.

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "gcc - Build and debug active file",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/${workspaceFolderBasename}",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "preLaunchTask": "C/C++: gcc build active file",
            "miDebuggerPath": "/bin/gdb"
        }
    ]
}

A simple project with a subfolder

This example uses the same files used in the last section, but with hello.c and hello.h in a subfolder named functions. All these files must be in folder, e.g., called hello3.

First, since an included file (hello.h) is in another folder, the c_cpp_properties.json must be edited to add the functions folder to the includePath parameter. So VS Code can find it.

c_cpp_properties.json

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "${workspaceFolder}/functions"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "gnu17",
            "cppStandard": "gnu++14",
            "intelliSenseMode": "linux-gcc-x64"
        }
    ],
    "version": 4
}

Next, the file functions/hello.c must be compiled too. It must, then, be included in the args attribute in the tasks.json file. The functions folder must be included again in the command lines as a folder containing header files (-I parameter). This time, for the gcc compiler.

tasks.json

{
    "tasks": [
        {
            "type": "cppbuild",
            "label": "C/C++: gcc build active file",
            "command": "/usr/bin/gcc",
            "args": [
                "-g",
                "main.c",
                "functions/hello.c",
                "-I",
                "functions",
                "-o",
                "${workspaceFolder}/${workspaceFolderBasename}"
            ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "Task generated by Debugger."
        }
    ],
    "version": "2.0.0"
}

The launch.json remains the same, but the executable name is changed.

** launch.json**

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "gcc - Build and debug active file",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/${workspaceFolderBasename}",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "console": "externalTerminal",
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "preLaunchTask": "C/C++: gcc build active file",
            "miDebuggerPath": "/bin/gdb"
        }
    ]
}

Project using make

The previous project are cumbersome to mantain and innefficient, because it compiles all source files by all build processes.

make is a tool for automation of the compiling process. It can be configure to compile only the needed source files.

The compilation process is configured in a Makefile. A simple one is listed below.

Makefile

#
# Name of project and of the generated executable
#
PROGNAME=hello

#
# Source files
#
SRCFILES=main.c functions/hello.c

#
# Compiler flags    ##
# @file Makefile
# Simple makefile
# Basically, a template

#
# Name of project and of the generated executable
#
PROGNAME=hello

#
# Source files
#
SRCFILES=main.c functions/hello.c

#
# Compiler flags
#
CFLAGS+=-I functions
CFLAGS+=-g

##### generated from above definitions ###############

OBJFILES= $(SRCFILES:.c=.o)

# targets
default: build

build: $(PROGNAME)

run: $(PROGNAME)
    ./$(PROGNAME)

clean:
    rm -rf $(PROGNAME) $(OBJFILES) depend.d

depend:
    $(CC) $(CFLAGS) -MM $(SRCFILES) > depend.d

$(PROGNAME): $(OBJFILES)
    $(CC) -o $@ $(CFLAGS) $(OBJFILES) $(LIBS)

.PHONY: default build run clean depend

# could use include(depend.d) instead of the explicit dependencies below
main.o: functions/hello.h
functions/hello.o: functions/hello.h

So, instead of running gcc, the make command must be run. So in the command line, one can run make build to generate the executable or make clean to delete all generated files.

These step can be incorporated in the *Run Build Task..." of VS Code by configurubg the tasks.json file as below.

{
    "tasks": [
        {
            "type": "cppbuild",
            "label": "Make build",
            "command": "make",
            "args": [
                "build"
            ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "Build project manually generated."
        },
        {
            "type": "cppbuild",
            "label": "Make clean",
            "command": "make",
            "args": [
                "clean"
            ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "Clean project manually generated."
        }
    ],
    "version": "2.0.0"
}

When running "Run Build Task..." from the Terminal menu, VS Code will show two options, one to build the executable and other to clean the project.

The executable generated by make and the command in the launch.json must be the same.

This approach can be used in more complex projects.

Additional extensions

A useful extension, when using Makefiles, is the Makefile Tools by Microsoft: It is activated if a Makefile is in workspace folder.

By pressing Ctrl-Shift-P, simultaneosly, you can run the following commands:

  • Makefile: Run the selected binary in the terminal
  • Makefile: Build the current target
  • Makefile: Clean configure
  • Makefile: set the target to be built by make

Other useful extensions are:

  • GitLens
  • Project Manager
  • Doxygen Documentation Generator
  • C/C++ Project Generator
  • Snippet Generator
  • Global Config
  • Git History
  • Markdown All in One
  • Native Debug (ext install webfreak.debug)
  • CMake

Using VS Code for ARM development

The preconditions for ARM development are:

The debugger support includes a flasher (write binary to flash memory) and a GDB proxy, that works as a interface between a debugger on the desktop machine and the debug firmware on the microcontroller board. Different manufacturers have different debugger support, as show in the (reduced) table below.

Manufacturer Software Comments
ST STLink ST-LINK/V2 in-circuit debugger/programmer for STM8 and STM32
Segger JLink J-Link / J-Trace Downloads
- OpenOCD Open On-Chip Debugger
TI st-util Open source version of the STMicroelectronics STlink Tools

The environment variable must be configured to enable access to the tool executable in all folders.

Since this is a complex project and tends to more complexity, an approach based on Makefile will be used.

The tasks.json defines differents builds:

  • build: generates an executable
  • clean: clean all generated files
  • flash: generates a binary file and transfer it to the microcontroller flash memory
  • docs: generates a folder named docs with the project documentation generated from the source files
  • gdbproxy: starts a GDB proxy

** tasks.json**

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "type": "cppbuild",
            "label": "Make: make build",
            "command": "make",
            "args": [
                "build"
            ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "Task manually added."
        },
        {
            "type": "cppbuild",
            "label": "Make: make clean",
            "command": "make",
            "args": [
                "clean"
            ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "Task manually added."
        },
        {
            "type": "cppbuild",
            "label": "Make: make flash",
            "command": "make",
            "args": [
                "flash"
            ],
            "dependsOn": [ "Make: make build" ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "Task manually added."
        },
        {
            "type": "cppbuild",
            "label": "Make: make docs",
            "command": "make",
            "args": [
                "docs"
            ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "Task manually added."
        },
        {
            "type": "cppbuild",
            "label": "Make: make gdbproxy",
            "command": "make",
            "args": [
                "gdbproxy"
            ],
            "dependsOn": [ "Make: make flash" ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "Task manually added."
        }
    ]
}

Using explicit access to GDB and GDB Proxy

** To be done. This is not working (yet)** Configure the debugging by editing the .vscode/launch.json file.

Using Cortex Debug Extension

The Cortex Debug extension simplifies the use of VS Code as a debugger. It can be installing by searching Cortext Debug in the Extension window and clicking in the Install button.

A device specific System View Description (SVD) file is needed. A procedure to obtain them is outlined in SVD File for EFM32 device. The packages that includes an specific SVD can be found in MDK5 Device List . The SVD file is a XML file with a structured description of the device and they are big, about 3 MBytes for a specific microcontroller.Its is structured as follows.

CPU
Peripheral
    Registers
        Fields
            Enumeration
Vendor extensions

The Makefile was modified to store a copy of the required SVD file in the gcc folder.

An extensive configuration is needed to lauch the debugger, because a GDB Proxy must be launched and a communication channel between GDB and GDB Proxy must be stabilished.

The project settings must include the path for the ARM tools (in this case, /opt/gcc-arm-none-eabi/bin/ and for the GDB proxy (in this case, /opt/SEGGER/JLink/JLinkGDBServerCLExe. THe other line was include automatically by Makefile tools.

** settings.json**

{
    "makefile.extensionOutputFolder": "./.vscode",
    "cortex-debug.armToolchainPath": "/opt/gcc-arm-none-eabi/bin/",
    "cortex-debug.JLinkGDBServerPath": "/opt/SEGGER/JLink/JLinkGDBServerCLExe"
}

The launch.json file is more complex. It must define

  • executable: absolute path of the generated executable
  • servertype: jlink, in this case. Can be stlink, stutil, pyocd, openocd.
  • device: Name of device
  • interface: swd or jtag
  • preRestartCommands: a list of debugger commands to flash the microcontroller.
  • preLaunchTask: the name of a task in tasks.json, that must be executed before debugging, generally, build.

launch.json

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    // And https://wiki.segger.com/J-Link_Visual_Studio_Code
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Cortex-M GDB Launch",
            "type": "cortex-debug",
            "request": "launch",
            "executable": "${workspaceFolder}/gcc/blink.axf",
            "cwd": "${workspaceFolder}",
            
            "serverpath":"",
            "servertype":"jlink",
            "device":"EFM32GG990F1024",
            "interface":"swd",
            "serialNumber":"",
          // There are two possibilites: using a JLink script as specified by the 
          // jlinkscript":"${workspaceFolder}/gcc/xxxxx.jlink",
          // line, or embedded the script in a preRestartCommands parameter
            "preRestartCommands": [ 
                "file ${workspaceFolder}/gcc/blink.bin",
                "load",
                "add-symbol-file ${workspaceFolder}/gcc/blink.axf 0x0",
                "enable breakpoint",
                "monitor reset"
            ],
            //"runtoMain": true,
            "runToEntryPoint": "main",
            "svdFile": "${workspaceFolder}/gcc/EFM32GG990F1024.svd",

            "preLaunchTask": "Make: make flash",
        }
    ]
}

Additional extensions for ARM development

Additionallly, some interesting extensions are:

  • PlatformIO IDE
  • Linker Script

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment