How to setup a working VSCode + Cortex-M debugger configuration to develop on STM32 Microcontrollers on Windows
The following links are recommended for deeper configurations and understanding of the following explanations. It is highly recommended that you go through them in case you encounter any problem during the process.
- http://openocd.org/doc-release/html/index.html (official documentation)
- https://github.com/posborne/cmsis-svd/tree/master/data/STMicro (bank of SVD files)
First of all, you need to follow the steps on this blog (except the STM32 Workspace Setup). But don't try to run anything: it is not ready yet. Also, when you download the SVD file, be sure to pick the one that suits the microcontroller you are using. (This page is an adaptation of this blog post, credits to the author)
Now we are going to use openocd instead of st-utils (I couldn't get the breakpoints to work and this tool offer more debugging capabilities).
This first step is of course to download openocd http://openocd.org/getting-openocd/ (for Windows download the binaries, scroll down to the bottom)
Make a new openocd folder at the path C:/VSARM
.
Extract the binaries in this new folder.
Then we will need to modify the configuration files because the tools have been updated since the blog post was written and so the paths don't match anymore.
Go back to the settings file (F1 and type settings (JSON)). Add the openocd path and make sure that the openocdPath is correct. (verify the existance of openocd.exe at this path) You can also use ${env:VSARM}
instead of hardcoding the path.
{ "telemetry.enableTelemetry": false, "cortex-debug.armToolchainPath": "C:\\VSARM\\armcc\\bin\\", "cortex-debug.openocdPath": "C:\\VSARM\\openocd\\bin\\openocd.exe" }
You have to know that is you followed the blog post instructions that the flash task is using the st-flash
command.
Next: in the c_cpp_properties.json
file, you have to make sure that all the paths are correct: I advise you to make sure that the versions (8.2.1) correspond to the versions you actually downloaded. Also don't forget to change the Drivers paths is you are not using the STM32F3. You can find the correct path if you navigate in the code hierarchy. You can also modify the defines (if you want more stack size, if you use another microcontroller, …)
{ "configurations": [ { "name": "STM32 Debug", "includePath": [ "${env:VSARM}/armcc/arm-none-eabi/include/c++/8.2.1", "${env:VSARM}/armcc/arm-none-eabi/include/c++/8.2.1/arm-none-eabi", "${env:VSARM}/armcc/arm-none-eabi/include/c++/8.2.1/backward", "${env:VSARM}/armcc/lib/gcc/arm-none-eabi/8.2.1/include", "${env:VSARM}/armcc/lib/gcc/arm-none-eabi/8.2.1/include-fixed", "${env:VSARM}/armcc/arm-none-eabi/include", "${workspaceRoot}/Drivers/CMSIS/Include/", "${workspaceRoot}/Drivers/CMSIS/Include/", "${workspaceRoot}/Drivers/CMSIS/Device/ST/STM32F3xx/Include/", "${workspaceRoot}/Inc", "${workspaceRoot}/Src", "${workspaceRoot}/Drivers/STM32F3xx_HAL_Driver/Inc", "${workspaceRoot}/Drivers/STM32F3xx_HAL_Driver/Inc/Legacy/", "${workspaceRoot}/Drivers/STM32F3xx_HAL_Driver/Src", "${workspaceRoot}" ], "defines": [ "DEBUG", "DEFAULT_STACK_SIZE=2048", "HSE_VALUE=8000000", "OS_INCLUDE_STARTUP_INIT_MULTIPLE_RAM_SECTIONS", "PB_MSGID", "STM32F303", "STM32F303x8", "USE_DEVICE_MODE", "USE_FULL_ASSERT", "USE_HAL_DRIVER", "USE_USB_OTG_FS" ], "intelliSenseMode": "msvc-x64", "browse": { "path": [ "${workspaceRoot}", "${env:VSARM}/armcc" ], "limitSymbolsToIncludedHeaders": false, "databaseFilename": "" } } ], "version": 4 }
The launch.json
file needs to be adapted as well. The svd file option is not mandatory: but at least it assures you that it is using the one you want. Change the target if required.
{ // 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": [ { "type": "cortex-debug", "request": "launch", "servertype": "openocd", "cwd": "${workspaceRoot}", "executable": "./build/vsarm_firmware.elf", "name": "Debug (openocd)", "device": "STM32F303CCTx", "svdFile": "${workspaceRoot}/STM32F303.svd", "configFiles": [ "C:/VSARM/openocd/scripts/interface/stlink-v2.cfg", "C:/VSARM/openocd/scripts/target/stm32f3x.cfg" ] } ] }
Don't forget to add launch.json
, tasks.json
, and c_cpp_properties.json
in the .gitignore
file. The settings.json
belongs to the global settings of your VSCode editor and thus doesn't need to be in the gitignore.
By now, you should have a working setup! F5 to build; F6 to flash; debug menu → start debugging to debug ;) Breakpoints are useable; can you watch the registers; watch variables… Enjoy the debugging!
However, there is still the SWO that has to be setup.
Known problems & solutions
OpenOCD Timeout error
Check in your settings.json
that the interface is defined before the target.
"configFiles": [ "C:/VSARM/openocd/scripts/interface/stlink-v2.cfg", "C:/VSARM/openocd/scripts/target/stm32f3x.cfg" ]
Check as well that you are using the good ST-Link V2 adapter. You might need to change the interface to st-link-v2-1 if you use an older one.
"configFiles": [ "C:/VSARM/openocd/scripts/interface/stlink-v2-1.cfg", "C:/VSARM/openocd/scripts/target/stm32f3x.cfg" ]
If you experience any other problem I would advise you to verify that all the device names actually match the one you are using.
Installation on Mac OS & Linux
TBD - Modify the path to match the install path and modify the build command from the tasks.json file that comes from the blog post to the correct one
Using the SWO pin & debugging with graphics
Before using the SWO output from the microcontroller, you need to understand how it works.
I strongly recommend checking out the following documentation before proceding further in the setup.
It is good, in order to understand all the graphing capabilities to directly go in the package.json file of the Cortex-M extension and to look at the options that are not yet documented. It is very versatile and there is a huge variety of options available. This can make your debugging experience smoother so I would consider that you invest some time in it.
To get started, open up your previous launch.json file (at the root of your project directory, or using F1 + type launch).
I wrote a basic documentation so you don't have to look at the json file. (written on 4/20/2019)
You must write the configuration in json.
Format of the documentation: parameter: Description [default value] (possible values) (value type)
First we will look at how to setup the swo decoders (how to handle data on each ITM port) and then how to configure the graphs that will be displayed. In order for everything to work, you also have to define some global swo parameters: don't forget to setup up the hardware afterwards.
Finally, here are a few links that helped me have a better understanding of all of this:
A fully working launch.json is avalaible below.
{ // 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": [ { "type": "cortex-debug", "request": "launch", "servertype": "openocd", "cwd": "${workspaceRoot}", "executable": "./build/vsarm_firmware.elf", "name": "Debug (openocd)", "device": "STM32F303CCTx", "v1": false, "svdFile": "${workspaceRoot}/STM32F303.svd", "configFiles": [ "C:/VSARM/openocd/scripts/interface/stlink-v2.cfg", "C:/VSARM/openocd/scripts/target/stm32f3x.cfg" ], "swoConfig": { "enabled": true, "cpuFrequency": 0, "swoFrequency": 0, "decoders": [ { "port": 1, "encoding": "unsigned", "graphId": "data1", "scale": 1 }, { "port": 0, "label": "printf output", "showOnStartup": true } ] }, "graphConfig": [ { "label": "VBAT", "maximum": 3.7, "minimum": 0, "plots": [ { "label": "VBAT", "graphId": "data1" } ] } ] }] }
swoConfig
- enabled: Enable SWO decoding [false] (boolean)
- cpuFrequency: Target CPU frequency in Hz; 0 will attempt to automatically calculate. [0] (number)
- swoFrequency: SWO frequency in Hz; 0 will attempt to automatically calculate. [0] (number)
- source: Source for SWO data. Can either be \“probe\” to get directly from debug probe, or a serial port device to use a serial port external to the debug probe. [probe] (string)
- decoders: see below, each of them need to be defined in a separated { }
Console decoder:
- label: A label for the output window (string)
- showOnStartup: If true, switches to this output when starting a debug session. (bool)
- port: ITM Port Number (0-31) (number)
- type: (console) (string)
- encoding: [utf8] (ascii, utf8, ucs2, utf16le) (string)
REQUIRED: port
Graph decoder:
- scale: This setting will scale the raw value from the ITM port by the specified value. Can be used, for example, to see directly the voltage level of your 3.7V li-ion battery with a 12 bit ADC → 3.7/4096 = 0.0009033203125) [1] (number)
- graphId: The identifier to use for this data in graph configurations. (string)
- port: ITM Port Number (0-31) (number)
- type: (graph) (string)
- encoding: This property is only used for binary and graph output formats. (unsigned, signed, Q16.16, float) (string)
REQUIRED: port, graphId
Binary decoder
- label: A label for the output window (string)
- port: ITM Port Number (0-31) (number)
- scale: This setting will scale the raw value from the ITM port by the specified value. Can be used, for example, to see directly the voltage level of your 3.7V li-ion battery with a 12 bit ADC → 3.7/4096 = 0.0009033203125) [1] (number)
- type: (binary) (string)
- encoding: This property is only used for binary and graph output formats. [unsigned] (unsigned, signed, Q16.16, float) (string)
REQUIRED: port
Decoder using a custom javascript module
- ports: ITM Ports Number (0-31) (array of numbers)
- config: “additionalProperties”: true
- decoder: Path to a javascript module to implement the decoding functionality. (string)
- type: (advanced) (string)
- graphId: The identifier to use for this data in graph configurations (string)
- encoding: This property is only used for binary and graph output formats. [unsigned] (unsigned, signed, Q16.16, float) (string)
REQUIRED: ports, decoder
Example:
"swoConfig": { "enabled": true, "cpuFrequency": 0, "swoFrequency": 0, "decoders": [ { "port": 1, "encoding": "unsigned", "graphId": "data1", "scale": 1 } ] }
graphConfig
Y-Axis (single or multiple data sources displayed on the same graph) and time for X-axis
- annotate: Create annotations on the graph for when the target processor starts and stops execution. (green line for starting execution, red line for stopping execution). [true] (bool)
- label: Label for Graph (string)
- maximum: Maximum value for the Y-Axis [65535] (number)
- minimum: minimum value for the Y-Axis [0] (number)
- plots: Plot configurations. Data sources must be configured for graph (or advanced with a decoder that sends graph data) in the swoConfig section.) (items) Each source has to be defined in a separated { }
- plots{color} “pattern”: “^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$” (string)
- plots{graphId} Graph Data Source Id for the plot. (string) <fc #ff0000>REQUIRED</fc>
- plots{label} A label for this data set (string)
- graphId: The identifier to use for this data in graph configurations (string)
- timespan: Length of time (seconds) to be plotted on screen. [30] (number)
- type: (realtime) (string)
REQUIRED: label, plots, minimum, maximum
XY-Graph
- label: Label for Graph (string)
- yMaximum: Maximum value for the Y-Axis [65535] (number)
- xMaximum: Maximum value for the X-Axis [65535] (number)
- yMinimum: Minimum value for the Y-Axis [0] (number)
- xMinimum: Minimum value for the X-Axis [0] (number)
- xGraphId: Graph Data Source Id for the X axis (string)
- yGraphId: Graph Data Source Id for the Y axis (string)
- type (x-y-plot) (string)
- timespan: The amount of time (seconds) that the XY Plot will show the trace for. [10] (number)
- type: (realtime) (string)
REQUIRED: label, xGraphId, yGraphId
CubeMX SWO Configuration
Before debugging, we need to enable the SWO pin in the CubeMX tool: open your previous configuration and click on SYS. Modify the debug line to Trace Asynchronous Sw. It uses the PB3 pin for the trace channel.
Don't forget to generate the code again.
Rewriting the _write function
When you use the printf (or puts) function, it puts everything in a buffer and then calls the _write(int file, char *ptr, int len) function.
Overwriting it permits you to feed the string to the ITM_SendChar(char) function.
int _write(int file, char *ptr, int len) { for (int i = 0; i < len; i++) ITM_SendChar((*ptr++)); return len; }
ITM_SendChar
. Don't forget to setup a SWO decoder that goes to a console output.
If you find it interesting, the ITM_SendChar function definition can be found in the Drivers/CMSIS/Include/core_cm3.h
file.
__STATIC_INLINE uint32_t ITM_SendChar (uint32_t ch) { if (((ITM->TCR & ITM_TCR_ITMENA_Msk) != 0UL) && /* ITM enabled */ ((ITM->TER & 1UL ) != 0UL) ) /* ITM Port #0 enabled */ { while (ITM->PORT[0U].u32 == 0UL) { __NOP(); } ITM->PORT[0U].u8 = (uint8_t)ch; } return (ch); }
Using ITM and writing value to ports in the code
Example to write a value to port 1:
ITM->PORT[1U].u32 = yourValue;