DEV Community

loading...
Cover image for CMake on STM32 | Episode 5: get ready for C++
Younup

CMake on STM32 | Episode 5: get ready for C++

Pierre Gradot
Committed defender of C++ & OOP / Python & CMake lover / Yogi, musician, home brewer, mountain bike rider
・4 min read

I have been using C++ for the last 4 years... there is no way I get back to C! This episode is not about explaining why I think C++ is better than C. It is simply about running C++ on STM32 with CMake. By the way: we will make LEDs blink!

This article is part of a series about CMake and STM32.

Adding user code in code generated by STM32CubeMX

If we change the MCU configuration with STM32CubeMX and generate the code again, the previous files are overwritten. How can we create our own program if everything is erased each time the code is generated?

Let's have look at the main() function in main.c. After board initialization, it runs the classic infinite loop that (almost?) all embedded systems have:

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

Notice the comments: they are tags for the code generator. Code between consecutive USER CODE BEGIN xxx and USER CODE END xxx tags is preserved during code generation.

You will find tags like these in many places in the generated code.

For instance stm32f4xx_it.c (which contains interrupt service routines) contains many of theses tags. For example:

void HardFault_Handler(void)
{
  /* USER CODE BEGIN HardFault_IRQn 0 */

  /* USER CODE END HardFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_HardFault_IRQn 0 */
    /* USER CODE END W1_HardFault_IRQn 0 */
  }
}

Blink LEDs in C++

Currently the application does nothing because the while loop is empty. Let's create a basic application to toggle LEDs using C++.

First, let's create source/application.hpp to declare two functions in a pure Arduino style, setup() and loop() :

#pragma once

#ifdef __cplusplus
extern "C"
{
#endif

void setup();
void loop();

#ifdef __cplusplus
}
#endif

Then, let's create source/application.cpp to define these functions:

#include "application.hpp"

#include "stm32f4xx_ll_gpio.h"
#include "main.h"

extern "C" {

void setup() {
    // For future episodes ;)
}

void loop() {
    LL_GPIO_TogglePin(LD1_GPIO_Port, LD1_Pin);
    LL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
    LL_GPIO_TogglePin(LD3_GPIO_Port, LD3_Pin);

    HAL_Delay(200U);
}

}

We can now call these functions from main(). First, we have to include our file in main.c between USER CODE tags:

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "application.hpp"
/* USER CODE END Includes */

Then we modify the infinite loop:

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  setup();

  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    loop();
  }
  /* USER CODE END 3 */

You can open BSP/BSP.ioc with STM32CubeMX and generate the code again to check that the modifications are kept.

Compile C++ code

Let's modify CMakeLists.txt to build our C++ application.

Language settings

C++ language is enabled by default but it is good to configure the version of the standard. ARM GCC 9 supports C++17 so this is the one to use:

enable_language(CXX C ASM)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

Target configuration

We have new files so we have to add them to add_executable():

add_executable(${EXECUTABLE} ${STM32CUBEMX_GENERATED_FILES}
        source/application.cpp
        source/application.hpp)

We have a new directory with headers to we have to modify the include path:

target_include_directories(${EXECUTABLE} PRIVATE
        BSP/Inc
        BSP/Drivers/STM32F4xx_HAL_Driver/Inc
        BSP/Drivers/CMSIS/Device/ST/STM32F4xx/Include
        BSP/Drivers/CMSIS/Include
        source/
        )

Linker options

Let's build... and get some weird errors from the linker:

[  3%] Linking CXX executable nucleo-f413zh.out
c:/progra~2/gnutoo~1/92019-~1/bin/../lib/gcc/arm-none-eabi/9.2.1/../../../../arm-none-eabi/bin/ld.exe: c:/progra~2/gnutoo~1/92019-~1/bin/../lib/gcc/arm-none-eabi/9.2.1/../../../../arm-none-eabi/lib/thumb/v7e-m+fp/hard\libg_nano.a(lib_a-abort.o): in function `abort':
abort.c:(.text.abort+0xa): undefined reference to `_exit'
c:/progra~2/gnutoo~1/92019-~1/bin/../lib/gcc/arm-none-eabi/9.2.1/../../../../arm-none-eabi/bin/ld.exe: c:/progra~2/gnutoo~1/92019-~1/bin/../lib/gcc/arm-none-eabi/9.2.1/../../../../arm-none-eabi/lib/thumb/v7e-m+fp/hard\libg_nano.a(lib_a-signalr.o): in function `_kill_r':
signalr.c:(.text._kill_r+0xe): undefined reference to `_kill'
c:/progra~2/gnutoo~1/92019-~1/bin/../lib/gcc/arm-none-eabi/9.2.1/../../../../arm-none-eabi/bin/ld.exe: c:/progra~2/gnutoo~1/92019-~1/bin/../lib/gcc/arm-none-eabi/9.2.1/../../../../arm-none-eabi/lib/thumb/v7e-m+fp/hard\libg_nano.a(lib_a-signalr.o): in function `_getpid_r':
signalr.c:(.text._getpid_r+0x0): undefined reference to `_getpid'
collect2.exe: error: ld returned 1 exit status
mingw32-make[3]: Leaving directory 'D:/cmake_stm32/cmake-build-debug'
mingw32-make[3]: *** [CMakeFiles\nucleo-f413zh.out.dir\build.make:435: nucleo-f413zh.out] Error 1
mingw32-make[2]: *** [CMakeFiles\Makefile2:75: CMakeFiles/nucleo-f413zh.out.dir/all] Error 2
mingw32-make[2]: Leaving directory 'D:/cmake_stm32/cmake-build-debug'
mingw32-make[1]: Leaving directory 'D:/cmake_stm32/cmake-build-debug'
mingw32-make[1]: *** [CMakeFiles\Makefile2:82: CMakeFiles/nucleo-f413zh.out.dir/rule] Error 2
mingw32-make: *** [makefile:117: nucleo-f413zh.out] Error 2

We can see in the error messages that the problem comes from libg_nano.a, the C++ standard library used by our toolchain. Internet gave two solutions:

  1. Add -fno-exceptions to the compiler options.
  2. Change -specs=nano.specs to -specs=nosys.specs in the linker option.

I must admit that my knowledge reaches its limits here, especially on specs files... For this reason, I prefer to use the first solution because I clearly understand its ins and outs and because I don't use exceptions in embedded C++. Note that Google C++ Style Guide discourages exceptions in general.

Conclusion

Hurray! LEDs are blinking thanks to C++!

Alt Text

There wasn't a lot to do: create our files, add them to the build, configure the C++ standard. OK, we had a little linker issue but disabling exceptions was enough to solve it.

In a further episode, we will improve our C++ configuration by adding C++ specific options to the compiler.

Discussion (1)

Collapse
siy profile image
Sergiy Yevtushenko

Have you looked into modm?