How to develop STM32 with VScode? part 2/2

 


So far, everything was working only if you were using the same MCU as mine - STM32F405RGT. Since, arguably, not everybody is programming drones on this exact MCU, in this post I will show how to set up a project from scratch and along this way reveal a bit of the mystery of how everything works and what was the purpose of some files from a previous post. Luckily this will be the last part of this complicated process and ideally, after that, you should be able to develop any STM32-based board.

Create a project structure


Open the folder where you want your project and create new folders: Src, Drivers, bin, build, link. I keep the same folder structure among all projects so that I can just copy an existing one and change only MCU-specific parts. Of course, you can have different folder structures, but be aware of that and make suitable adjustments throughout this post.



 Add files


The next step is downloading and adding all essential files to the project. Let's start with linker scripts (.ld files). These ones provide instructions to the linker on how to combine object files (files generated by a compiler) into an executable program or a library. It contains information about stack, heap sizes, memory layout etc. To get linker scripts you can either write them yourself or use pre-generated from some IDE (or copy them from someone's project). 
Let's use CubeMX since you will likely be using it in some scenarios anyway. Just create a new empty project for your MCU and in the main branch, .ld files can be found. Copy them into your project's link folder.



Next, to use CMSIS-Core we need to add a bunch of header files and two source files (more about it here: link). In essence, they are necessary to start the microcontroller up, set default values and provide constants, structures and functions which you know from the reference manual. In my case, these files are: 
  • startup_stm32f411xe.s, 
  • system_stm32f4xx.c, 
  • system_stm32f4xx.h, 
  • stm32f4xx.h, 
  • stm32f411xe.h,
  • all files from the CMSIS/Include folder (see screenshot below). 


When you see 'x' in these names, files are relevant for any MCU that matches the name, disregarding parts where the 'x' is. It is possible because many MCUs are built similarly and only a few things need to be specified for the exact MCU and these are chosen by specifying a special constant during compilation (-DSTM32F411xE) - more about it later. I copied these files from the blank project in CubeIDE to my project:



The last file is a .svd file (System View Description) - it is not essential and you can compile, flash and even debug code without it. However, if you want to have access to the registers' values, which are often essential for debugging purposes, this file is required. In general, you can find it in many places on the Internet (example) but a good idea is to check the ST site and download it from the manufacturer.

Look for your MCU


Choose the correct one

Download .svd files package

Then choose the right file and copy it into the main branch:



CMake


Next what you need is CMakeList.txt. This file contains all instructions for CMake to conduct a building process for your project. You can write it from scratch or copy mine and make some adjustments.

cmake_minimum_required(VERSION 3.16)
set(CMAKE_SYSTEM_NAME Generic)

# turn off compiler checking (assume that the compiler is working):
set(CMAKE_C_COMPILER_WORKS TRUE)
# set C standard:
set(CMAKE_C_STANDARD 17)
# set path where binary files will be saved:
set(BUILD_DIR ${CMAKE_SOURCE_DIR}/bin)
set(EXECUTABLE_OUTPUT_PATH ${BUILD_DIR})

# create targets variables:
set(TARGET "main")
set(TARGET_ELF "${TARGET}.elf")
set(TARGET_HEX "${TARGET}.hex")
set(TARGET_BIN "${TARGET}.bin")

# create project name and define required languages for building (C and assembler - ASM should be at the end):
project(MY_PROJECT C ASM)

# assign paths into variables:
set(MAIN_SRC_DIR "${CMAKE_SOURCE_DIR}/Src")

set(LINKER_DIR "${CMAKE_SOURCE_DIR}/link")
set(LD_include "-lnosys -L${LINKER_DIR}")
set(linker_script "${LINKER_DIR}/STM32F411RETX_FLASH.ld")

set(MCU_flags   "-mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard -mthumb ")
            
# C definitions (additional arguments parse with cmd line):
set(C_DEFS " -DSTM32F411xE ")
# C-specific flags:
set(C_flags     "${MCU_flags} ${C_DEFS} -Wall -fdata-sections -ffunction-sections -fanalyzer ")
# Assembler-specific flags:
set(AS_flags    "${MCU_flags} -Wall -fdata-sections -ffunction-sections ")
# Linker's flags:
set(LD_flags    "${MCU_flags} -specs=nano.specs -specs=nosys.specs -T${linker_script} ${LD_include} -Wl,--print-memory-usage -u _printf_float ")

# CMake variables setup:
set(CMAKE_C_FLAGS "${C_flags}")
set(CMAKE_ASM_FLAGS "${AS_flags}")
set(CMAKE_EXE_LINKER_FLAGS "${LD_flags}")

# add all your executable files:
add_executable(${TARGET_ELF}
    Src/startup/startup_stm32f411retx.s
    Src/startup/system_stm32f4xx.c
    Src/main.c
)

# include all directories where header files occur:
target_include_directories(${TARGET_ELF} PUBLIC
    Src
    Src/startup
    Drivers
    Drivers/Include
)

# link GNU c and m ("math") libraries (more here: https://www.gnu.org/software/libc/manual/pdf/libc.pdf):
target_link_libraries(${TARGET_ELF} PUBLIC c m)

# set shortcut for command:
set(OBJCOPY arm-none-eabi-objcopy)
# make new targets .hex and .bin from .elf file:
add_custom_target(${TARGET_BIN} ALL COMMAND ${OBJCOPY} -O binary -S ${BUILD_DIR}/${TARGET_ELF} ${BUILD_DIR}/${TARGET_BIN})
add_custom_target(${TARGET_HEX} ALL COMMAND ${OBJCOPY} -O ihex -S ${BUILD_DIR}/${TARGET_ELF} ${BUILD_DIR}/${TARGET_HEX})

# define dependencies so that .hex file is created after .elf and .bin as the last one:
add_dependencies( ${TARGET_HEX} ${TARGET_ELF})
add_dependencies(${TARGET_BIN} ${TARGET_ELF} ${TARGET_HEX})
 
For each project, you only need to adjust MCU_flags, C_DEFS, linker_script name and executables paths. 
MCU_flags are used to tell the compiler about the used family of MCU, FPU type and some other things. How do you know which flags are required for your MCU? You can either check it on the Internet or use CubeMX and copy flags from the generated project. 
For the second option, generate an empty project for your MCU again but remember to choose Project Manager->Project->Toolchain/IDE = Makefile.


Then search in the generated Makefile for CFLAGS and copy them into your CMakeLists.txt.


In the same file, a few lines below you can find C_DEFS from which only the first one is important (for more advanced projects you may want some of the other definitions). This defines some configurations that are specific to your MCU in stm32f4xx.h file (or similar for your MCU family). 


Next update the linker_script variable accordingly to your .ld file (STM32F411RETX_FLASH.ld in my case) and add all of your executables - remember about the assembler startup file (.s-ended file).

Change the linker script name and add executables 


Building


Now it is time to build the project. We can use the terminal (Ctrl+Shift+` ) in the build directory (cd ./build) and write: "cmake --build .".

If everything is ok you should have similar notifications

First task

You may presume that it is not particularly convenient to write everything in the cmd line each time you want to build your program (you need to be in the right directory and remember the commands). Therefore VScode allows you to set some presets (called tasks) and use them with shortcuts. Let's create the first task for building our program:



One file is added (.vscode/tasks.json) containing all your tasks. You can add more or modify the existing ones but for now, just press Ctr+Shift+B and build your program. There should be the same notifications as building from the terminal (completed builds and statistics of used memory).
If you want to use my task "Make Firmware" for building, delete the existing one (from .vscode/tasks.json) and paste the code below (it gives no problems with other tasks that we will soon add).
{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Make Firmware",
            "type": "shell",
            "detail": "build program",
            "command": "cmake --build .",
            "options": {
                "cwd": "${workspaceRoot}/build"
            },
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "problemMatcher": {
				"base": "$gcc",
				"fileLocation": ["absolute"]
        	},
        },
	]
}



OpenOCD


Having built our program it is time to load it into a microcontroller - so far everything was made on your host computer (you don't need to have your MCU connected). 
The bin folder contains binary files (zeros and ones) which could be transferred to any MCU (however it would transferred successfully only for your type of MCU). Moreover, different programming devices use various interfaces (st-link-v1, st-link-v2, jtag ...) and openOCD supports many of these options. Consequently, you have to tell openocd what kind of target device and interface you want to use. You can write it in the terminal (in the project's main directory): 
"openocd.exe -f interface/stlink-v2.cfg -f target/stm32f4x.cfg -c 'program ./bin/main.bin verify reset exit 0x08000000' ". 
Remember to choose the target (stm32f4x.cfg) and interface (stlink-v2.cfg) respectively to the used MCU (you can find it here) and programmer (for Nucleo's programmer leave stlink-v2.cfg).



Task for flashing

For me, rather long command to write each time you want to flash your program, so let's make a task for that (remember to choose the correct interface and target):

...
		{
            "label": "Load Firmware",
            "type": "shell",
            "detail": "flash into MCU",
            "command": "openocd.exe",
            "args": [
                "-f",
                "interface/stlink-v2.cfg",
                "-f",
                "target/stm32f4x.cfg",
                "-c",
                {
                    "value": "program ./bin/main.bin verify reset exit 0x08000000",
                    "quoting": "strong"
                }
            ],
            "options": {
                "cwd": "${workspaceRoot}"
            },
            "group": {
                "kind": "build",
                "isDefault": true
        	}
    	},
	]
}

tasks.json


If you want, you can add 2 more tasks - one for rebuilding a whole project and the second one to combine building and flashing into one task:

...
		{   
            "label": "Clean & Build",
            "type": "shell",
            "detail": "clean first and build program",
            "command": "cmake --build . --clean-first ",
            "options": {
            	"cwd": "${workspaceRoot}/build"
            },
            "group":{
            	"kind": "build",
                "isDefault": true
            },
            "problemMatcher": {
            	"base": "$gcc",
            	"fileLocation": ["absolute"]
        	}
        },
		{
            "type": "shell",
            "label": "Build & Load",
            "dependsOrder": "sequence",
            "dependsOn": ["Make Firmware", "Load Firmware"],
            "group": {
            	"kind": "build",
            	"isDefault": true
            },
            "detail": "build and load program into MCU"
    	}
	]
}
Now when you press Ctrl+Shift+B and choose between the tasks:



Debugging


For debugging, we need to provide some information: where cortex-debug extension can find the .svd file, what kind of interface we are using etc. For this purpose, VScode has a launch.json file. Press Ctrl+Shift+P and write "Debug: Add Configuration...".


A new file is created (.vscode/launch.json). Delete the default config code and copy the below one:
{
    "version": "0.2.0",
    "configurations": [
     	{ 
          // name for debug settings:
          "name": "Debug (OpenOCD)",
          // use cortex-debug:
          "type": "cortex-debug",
          "request": "launch",
          // use openocd for connection:
          "servertype": "openocd",
          "cwd": "${workspaceFolder}",
          // where program will stop (at entrance to main function):
          "runToEntryPoint":"main",
          "showDevDebugOutput": "both",
          // before debugging build program (see task.json):
          "preLaunchTask": "Make Firmware",
          // path for .elf binary:
          "executable": "${workspaceFolder}/bin/main.elf",
          // turn on live variables:
          "liveWatch": {
          	"enabled": true,
          	"samplesPerSecond": 4
          },
          "configFiles": [
            "${workspaceFolder}/openocd.cfg"
          ],
          // .svd file name that you want to use:
          "svdFile":"STM32F411.svd"
    	}
	]
}
Remember to change the name of the .svd file to match with your. After that, we need to add the last file to the main branch - openocd.cfg file, with 4 lines of code. Cortex-debug needs it to have info about the interface etc.. Anyway, create a file and copy my code into it (adjust for your interface and target):



Now, let's debug - press F5 and enjoy. On the left, you can find all the interesting stuff (live watch, registers' values, variables, etc.) but for now, you can set breakpoints and iterate through your code:



This is the end of the configuration. You can program, build, flash and debug your code :). 

Adding HAL library


It is worth remembering that using HAL for programming your MCU is not essential. I have personally created a whole software for a flying drone only on registers. However, at some point (for me, it was adding USB handling), it became too impractical not to use it. Therefore, as much as I think writing your own libraries is enlightening and leads to a better understanding of algorithms and programming itself, it is not the way for casual projects. Having said that, let's add the HAL library to the project. 

Once more, let's use an empty project from CubeMX. Things worth changing in CubeMX: clock configuration (to make your MCU run with desired frequency) and Project Manager->Code Generator->STM32Cube MCU...packs = Copy all used libraries into the project folder. Doing so will generate a folder with a whole HAL library not only with basic functions (since the project is empty and no peripheries are used).


Next, we can copy all the necessary files: stm32f4xx_hal_conf.h, stm32f4xx_it.h, stm32f4xx_hal_msp.c, stm32f4xx_it.c and a whole folder STM32F4xx_HAL_Driver


You can put these files wherever you want (remember to change paths respectively) in my case, the structure looks like this:

Now you have all source code and header files to use HAL library but there are a few additional steps to make it work. Firstly, add all new paths into CMakeLists.txt - you can write it by hand but the easiest way is to copy it from the CubeMX project. Go to Makefile and find "C_SOURCES". Now you have paths to all required HAL files:


Copy them with essential changes to your paths (in my case  stm32f4xx_hal_msp.c and stm32f4xx_it.c have different paths). Also, add new headers directories (below executables):

Add paths to executables and headers directories

Secondly, we need to add 2 functions: SystemClock_Config(void) and Error_Handler(void). Just copy them from the main.c generated by CubeMX to your own main.c


Next, add definitions of these functions at the top of the main.c
The last step is to replace all #include "main.h" with #include "stm32f4xx_hal.h" (in files: stm32f4xx_it.h, stm32f4xx_hal_msp.c and main.c).

Add functions and declarations, change #include "main.h" to #include "stm32f4xx_hal.h"

Now, everything should be working (building with no problem) and you can use the HAL library. However, if you want to use any other peripheral you will need to add adequate paths in CMakeLists.h (I recommend configuring these peripherals in CubeMX and next copy-paste all paths from the generated Makefile - check the example below). 

A simple example of toggling LED on my Nucleo board:

  1. Configure PA5 (LED2 is connected to it) as output,
  2. Configure the clocks,
  3. Check Project Manager->Advanced Settings if you use the HAL library, not the LL (let's keep it simple for now),
  4. Choose Project Manager->Project->Toolchain/IDE Makefile,
  5. Generate the code,
  6. Copy all additional paths from the Makefile (HAL handling for GPIO was added before so only the path to gpio.c needs to be added),
  7. Copy new files (gpio.c and gpio.h) and change #include "main.h" to #include "stm32f4xx_hal.h",
  8. Include gpio.h into your main.c,
  9. Add MX_GPIO_Init() and write a toggle routine in the main loop,
  10. Build and flash your program.
1.

2.

3.

4.

6. 7. 8.

9.

10.



This is the end.

Summary


All in all, after these 2 posts, you can create the project from scratch for any MCU you want.

The project described in this post can be found on my GitHub: link - take anything you like.

There was a lot of information and many complicated tools, each of which could have a separate post. Therefore most of the descriptions and options are kept to a minimum. If you want to learn more, presumably you now understand enough to ask good questions and find the answer on your own. For a good start here are some links for materials which I found really interesting:

Extras


Compiler update

So far we have been using a compiler from 07.2021 but since then GCC has been upgraded a few times so why not take advantage of a new compiler and use it for our projects? 
Everything is pretty simple - we need to download a new toolchain: link.


Next, install it wherever you want. I decided to keep everything in one place C:\tools\Toolchain GNU Arm:


Then remember to check if the path to the bin folder was added into environmental variables and we are ready to use our new compiler. 
Finally, it is necessary to let to know CMake about a new compiler: 

Choose a compiler with the status bar 

Or choose it using the CMake extension 

If you don't have a status bar visible you can change it in the settings:



VScode tips


red squiggles

Errors highlighting and autocompletion are handled in VScode by Intellisense. This software is downloaded with a C/C++ extension for VScode (we did it in part 1). Unfortunately, it often doesn't work properly right after installation. If it doesn't see your header files despite that everything is working and the project can be built that means the Intellisence isn't properly configured. Fortunately, there is a quick solution for that. We need to inform Intellisense to use CMake's files to get paths for all headers that we include. Open C/C++ Configurations (Ctrl+Shift+P) either JSON or UI and add "configurationProvider": "ms-vscode.cmake-tools" to your configuration.

Open configuration

Add this line of code

Now Intellisense should use information about included headers from CMkaeLists.txt and red squiggles should be gone.

auto-save and auto-format

VScode allows to set auto-save on each change of focus and simultaneously formatting code to maintain readability. To set it, go to settings and change the auto-save option as well as formatting on after saving option:
Choose your favourite option

Enable formatting after saving 







Comments

Popular posts from this blog

Hardware - how to start?

USB with STM32

Filters - Theory