Using Visual Studio Code for ARM Development – Build Tasks

It isn’t strictly necessary to use Visual Studio Code to build your code. You can still drop to the command line and build it more directly. However, it is handy to be able to build with a shortcut key or not have to manage another window.

For my example code, I have a Makefile that I use with “make all” as the default target, and “make clean” to delete everything.

You can set up custom tasks within VSCode that can execute any shell command, even specifying which shell to use. There is a special “Default Build” task.

To start, select the “Task” menu, then “Configure Tasks”. If this is the first time, it will ask for a type – select “Others”; VSCode will create the .vscode/tasks.json file for you.

I have the following in my tasks.json file:

{
    "version": "2.0.0",
    "tasks": [
        {
            "taskName": "Make",
            "type": "shell",
            "command": "make",
            "presentation": {
                "echo": true,
                "reveal": "always",
                "focus": false,
                "panel": "shared"
            },
            "group": {
                "kind": "build",
                "isDefault": true
            }
        },
        {
            "taskName": "Make Clean",
            "type": "shell",
            "command": "make clean",
            "presentation": {
                "echo": true,
                "reveal": "always",
                "focus": false,
                "panel": "shared"
            },
            "group": "build"
        }
    ]
}

The first task just runs the command-line “make”. The Terminal pane will be opened to show the results. This is the default build task, so it can be performed using ctrl-shift-B.

The second task is similar, but it runs “make clean”. It doesn’t have a defined shortcut key. However, this can be performed by selecting Tasks/Run Task from the menu or alt-shift-B. You can then select from the defined tasks.

(VSCode does have configurable keybindings, so if you want to assign a task to a shortcut key, you can do this easily!)

A caveat: GNU Make does not play particularly well with Windows. While my current project Makefile does work, I have had other Makefiles that will not run under the Windows cmd shell. In those cases, I had to use the MinGW shell. While it is possible to specify other shell executables from the tasks.json file, I have not figured out how use the MinGW shell. The new Linux Subsystem could be an alternative solution, but I have not yet tried it. This should work without problems on Linux and Mac.

Using Visual Studio Code for ARM Development – Defines

In the last post on Include Paths, I started with just a single error, and ended up with quite a large number of errors – it feels like going backwards! Keep in mind that the code actually builds and runs, so these are not actually problems with the code.

These errors are caused since the code I’m using, including the STM32 libraries standard ARM libraries, depend on macros that are not defined within the code itself. In this post, I will describe how to determine and pass the defines to Visual Studio Code so it will properly parse your code.

First, it is common to pass defines to the compiler using flags at compile time. In my case, I am using CMSIS and the new STM32 Low-Level library and that is included with the STM32Cube software, and I am targeting the 1bitsy development board, which uses an STM32F415 chip. In order for the library to use the right options, I need to pass these to the compiler, without them, many references to the library are not properly defined in the scope of my code.

    -DUSE_FULL_LL_DRIVER
    -DSTM32F415xx

Setting these for VSCode is simple: just add them to the “defines” section of c_cpp_properties.json, without the -D flag prefix:

    "defines": [
        "STM32F415xx",
        "USE_FULL_LL_DRIVER"
    ],

Aha – This fixes all of the errors related to things defined in the libraries! But I still have one strange error remaining:

Isn’t uint32_t defined in stdint.h? I’ve included that, I don’t have any include errors… Grrr!

To be honest, this was quite maddening to figure out. The GNU ARM version of stdint.h  actually drills down and leads to stdint-gcc.h, which is dependent upon a macro, but the macro isn’t defined! It’s also dependent on other macros to get there… This leaves uint32_t undefined, and the VSCode parser doesn’t know what to do with it. The same is true of all of the types defined in this file.

So why does this even compile? It turns out, that the GNU ARM compiler has many defines hard-coded into it, including __UINT32_TYPE__. The compiler will process the typedef properly, and then understand my code.

So, we need to figure out what these are, and set them for VSCode as well.

Since there are a very large number of defines (344!), I was able to dump them into a file by running

echo | arm-none-eabi-gcc -dM -E - > gcc-defines.txt

(on Windows).

...
#define __ATOMIC_SEQ_CST 5
#define __DA_FBIT__ 31
#define __UINT32_TYPE__ long unsigned int
#define __UINTPTR_TYPE__ unsigned int
#define __USA_IBIT__ 16
...

I’m not sure what all of these do, or when I might need them in the future, so I added all of them. (I used Excel to do some bulk editing tricks and sort them, but you can accomplish this however you are comfortable. I don’t want to reproduce the list as it may change with different builds of gcc; you should ensure it matches the compiler you are using!)

Again, follow the format of the -D compiler flag, just without the “-D” prefix. A few rules:

    • Put an equals sign between the macro name and the value, like
      __UINT32_TYPE__=long unsigned int
    • If the value contains quotes, escape the quotes  with \”, like
      __VERSION__="6.3.1 20170620 (release) [ARM/embedded-6-branch revision 249437]"
    • If there is no value provided, just use the macro name, like
      __USES_INITFINI__

My defines section is now very long:

 "defines": [
        "STM32F415xx",
        "USE_FULL_LL_DRIVER",
        "__USES_INITFINI__",
        "__ACCUM_EPSILON__=0x1P-15K",
        "__ACCUM_FBIT__=15",
        "__ACCUM_IBIT__=16",
        "__ACCUM_MAX__=0X7FFFFFFFP-15K",

        etc...

But now my error is gone, and hovering over the type shows it is defined!

If the include paths and defines are properly set up in Visual Studio Code, the C/C++ plugin should be able to properly parse your code, identify syntax errors, and will provide Intellisense code completion to improve the speed and accuracy of your writing!

In the next post, I will describe how build your code using Makefiles from within Visual Studio Code.

Using Visual Studio Code for ARM Development – Include Paths

This post will describe how I set up the Include Paths for C/C++ in Visual Studio Code to support ARM development.

Visual Studio Code with the C/C++ plugin has many useful features while you are writing your code: syntax highlighting, error checking while you type, and automatic code completion, where it guesses while you type. This can help you reduce errors and write faster. However, if the plugin is not correctly set up, these features will not work correctly or at all.

In order for Visual Studio Code and the C/C++ language plugin to understand your code, it needs to be able to find all of the header files referenced in your program. A header can further reference other headers, so they may be needed even if you didn’t explicitly call them yourself.

(Note that this is completely separate from the compiling and linking steps, which I will cover in a later post. The examples I am using are from a simple program that does build and run correctly.)

In main.c, VSCode is complaining that it cannot open main.h. (Grrr! It’s right there in my workspace, under the inc folder, and I can open it – it’s fine!) The message is a little obscure, but it’s basically saying that it will interpret the file based on keywords and syntax, but can’t tell if things like function or variable names are correct.

So, how do we fix this? If you cursor is on the error, you will see the lightbulb to the left. Click that, and then Update “includePath” setting. VSCode will create and open a file called “c_cpp_properties.json”, in your project under a “.vscode” folder. By default this will contain configurations for Mac, Linux, and Win32. Unless you are developing your project on multiple computers, you don’t need them. These include default includePaths suitable for desktop applications, but are not correct for ARM. I cut them down to a single one, called STM32.

There are two sections with paths. The first, “includePath”, is where VSCode will look for headers for the purpose of checking your code. Unlike GCC, this is not recursive; you need to explicitly list each folder that contains headers that are referenced, directly or indirectly. While the default included “${workspaceRoot}”, it did not cover the inc folder – we will need to add that.

The second section with just “path” is used by IntelliSense to suggest things for you. If you are using massive libraries, you may not want tons of obscure things showing up. You can limit this list to the headers you actually use. This is recursive but you can make it non-recursive if you want. The default also included “${workspaceRoot}” so Intellesense is good for my own stuff.

We will also need to set a file for the symbol database.

This is the initial c_cpp_properties.json file:

{
    "configurations": [
        {
            "name": "STM32",
            "includePath": [
                "${workspaceRoot}",
                "${workspaceRoot}/inc"
            ],
            "defines": [
            ],
            "intelliSenseMode": "clang-x64",
            "browse": {
                "path": [
                    "${workspaceRoot}"
                ],
                "limitSymbolsToIncludedHeaders": true,
                "databaseFilename": "${workspaceRoot}/.vscode/browse.vc.db"
            }
        }
    ],
    "version": 3
}

Next problem! The original error has changed, and it’s complaining about a dependency.

In main.h, there are now multiple errors showing.

Basically, this is more of the same. Most of these files are part of the STM32Cube library, that is outside of my project. I’ll add those folders to my paths as well.

But where is stdint.h? GCC has built-in include paths that are used even if my compiler flags don’t specify them. GCC will list the defaults by running “arm-none-eabi-gcc -xc -E -v -“, and add those paths as well. Be sure list all subfolders with headers for the includePath section.

{
    "configurations": [
        {
            "name": "STM32",
            "includePath": [
                "${workspaceRoot}",
                "${workspaceRoot}/inc",
                "C:/Develop/STM32Cube_FW_F4_V1.16.0/Drivers/STM32F4xx_HAL_Driver/Inc",
                "C:/Develop/STM32Cube_FW_F4_V1.16.0/Drivers/CMSIS/Include",
                "C:/Develop/STM32Cube_FW_F4_V1.16.0/Drivers/CMSIS/Device/ST/STM32F4xx/Include",
                "c:/Develop/arm-none-eabi/lib/gcc/arm-none-eabi/6.3.1/include",
                "c:/Develop/arm-none-eabi/lib/gcc/arm-none-eabi/6.3.1/include-fixed",
                "c:/Develop/arm-none-eabi/arm-none-eabi/include",
                "c:/Develop/arm-none-eabi/arm-none-eabi/include/machine",
                "c:/Develop/arm-none-eabi/arm-none-eabi/include/newlib-nano",
                "c:/Develop/arm-none-eabi/arm-none-eabi/include/sys"
            ],
            "defines": [
            ],
            "intelliSenseMode": "clang-x64",
            "browse": {
                "path": [
                    "${workspaceRoot}",
                    "C:/Develop/STM32Cube_FW_F4_V1.16.0/Drivers/STM32F4xx_HAL_Driver/Inc",
                    "C:/Develop/STM32Cube_FW_F4_V1.16.0/Drivers/CMSIS/Include",
                    "C:/Develop/STM32Cube_FW_F4_V1.16.0/Drivers/CMSIS/Device/ST/STM32F4xx/Include",
                    "c:/Develop/arm-none-eabi/lib/gcc/arm-none-eabi/6.3.1/include",
                    "c:/Develop/arm-none-eabi/lib/gcc/arm-none-eabi/6.3.1/include-fixed",
                    "c:/Develop/arm-none-eabi/arm-none-eabi/include"
                ],
                "limitSymbolsToIncludedHeaders": true,
                "databaseFilename": "${workspaceRoot}/.vscode/browse.vc.db"
            }
        }
    ],
    "version": 3
}

Once you add all the paths you need, all of the header inclusion errors will go away.

In their place, you will find yourself faced with a whole new set of frightening errors! It’s getting better, I promise. These are caused by missing compiler defines, and I will explain those in the next post.

Using Visual Studio Code for ARM Development – Introduction

It can be quite overwhelming for hobbyist beginners like myself, to get started with ARM microcontrollers. Not only are there many chips, development boards, and programmers to choose from, but also many different development tools, from commercial IDEs to individual editors, compilers, and debuggers. VSCode is a new entrant, but quite a powerful one.

For hardware, I am using on STM32 microcontrollers. I have a Black Magic Probe programmer/Debugger and I am targeting a 1Bitsy development board. I also have an STM32F072B-DISCOVERY board that includes an integrated ST-Link.

I have dabbled with a handful of different tool sets for writing embedded code, but have not really been happy with most of them.

Recently, I have tried Microsoft’s Visual Studio Code, aka VSCode. It is a free, extensible, general-purpose code editor, and I really like it! It doesn’t have any built-in compilers or debuggers, but can integrate with them to act like an IDE.

Using VSCode for embedded software seems a little off the beaten path. I have found bits and pieces of information, and have solved some puzzles on my own. While I don’t have everything figured out yet, I thought I should collect what I’ve done in one place and share it.

First, these are all of the software tools I’m using:

This series will cover the following steps:

  1. Include Paths
  2. Defines
  3. Build Tasks
  4. Debugging with Black Magic Probe
  5. Debugging with ST-Link