Overview
In this tutorial, you will learn how to write STM32 C++ Code while integrating it with the C-based structure generated by STM32CubeIDE. Although projects are typically created using C by default, this guide demonstrates how to safely introduce C++ into your project without modifying the generated main.c file.
Rather than renaming main.c to main.cpp, which can lead to issues with code generation and maintenance, this tutorial shows a cleaner approach by keeping the generated C code intact and interfacing it with C++ classes. This allows you to maintain compatibility with CubeIDE while taking advantage of C++ features such as object-oriented design.
We will also cover how to configure a project for C++ after creation and introduce the key concepts required to link C and C++ code together.
What You Will Learn
- How C and C++ interact inside an STM32CubeIDE project
- Why
main.cshould remain in C instead of being renamed tomain.cpp - How
extern "C"prevents name mangling for C-callable interfaces - How to weave C++ classes into a Cube-generated STM32 project cleanly
Prerequisites
This tutorial assumes basic familiarity with STM32CubeIDE, embedded C/C++, and building STM32 projects. A USART terminal setup is also helpful for viewing output from the example project.
Materials List
- FTDI to USB
- Breadboards Kit Include 2PCS 830 Point 2PCS 400 Point Solderless Breadboards
- NUCLEO-F439Zi
Project Structure
Core/
├── Inc/
│ ├── main.h
│ ├── entryPointCPP.hpp
│ └── ManagedLEDs.hpp
└── Src/
├── main.c
├── entryPointCPP.cpp
└── ManagedLEDs.cpp
Hardware Configuration / Pinouts
Overview
This tutorial is primarily focused on software structure and C/C++ integration. The hardware shown below supports the demonstration project, but the main purpose of the tutorial is how to organize and integrate C++ code into an STM32CubeIDE-generated project.
FTDI Pinouts
FTDI to USB Pinout from right to left. The FTDI module is needed to convert the USART RX/TX signals into RS-232/USB such that you can interface with a dumb terminal.
- Pin 1 – GND
- Pin 2 – CTS
- Pin 3 – VCC
- Pin 4 – TX
- Pin 5 – RX
- Pin 6 – DTR
- USB Mini – Connect to PC via USB cable
Make sure the jumper is set for 5v or 3v which ever is being used to power the FTDI device. If we look below at the schematic, it is showing in this case as 5v. Also, validate that any hardware flow control is turned off.
If the dumb terminal emulator you are using has hardware flow control, you probably will not see the response to the <Enter> key being pressed and therefore the master Bluetooth device will not proceed to get configured.


Schematic

Pinouts & Configurations
Project Setup
To convert a project from C to C++ just right click on the project name and a menu will pop-up. Select “Convert to C++” and follow the prompts.

C++ and C Interoperability (Name Mangling)
When working with STM32 projects in STM32CubeIDE, it is important to understand how C and C++ interact, especially when combining generated C code with user-defined C++ classes.
A common mistake is to rename main.c to main.cpp in order to write C++ code directly in the main file. This approach is not recommended. Every time the project configuration is modified using the IOC editor, STM32CubeIDE regenerates main.c, not main.cpp. This can lead to lost changes and complicate project maintenance. The preferred approach is to leave main.c unchanged and interface it with C++ code through well-defined entry points.
To understand how this works, it is important to know how C++ handles function names differently from C. C++ supports function overloading, which allows multiple functions with the same name but different parameter types. For example, a class may define:
methodA(long x, long y)
methodA(int x, int y)
Although these appear to have the same name, the compiler internally modifies, or “mangles,” the function names to make them unique. This means that the function name seen by the linker is different from the one written in the source code.
Because of this name mangling, C code cannot directly call C++ functions using their original names. To solve this, C++ provides the extern “C” keyword, which tells the compiler not to mangle the function names. This allows C code to call functions defined in C++ source files.
The typical pattern looks like this:
extern “C” {
/* Functions declared here will not be name mangled */
}
Functions declared inside this block can be called from C code, while still being implemented in C++ files. This approach allows you to combine C and C++ cleanly, maintaining compatibility with STM32CubeIDE while benefiting from C++ features.
Code Walkthrough
main.h
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.h
* @brief : Header for main.c file.
* This file contains the common defines of the application.
******************************************************************************
* @attention
*
* Copyright (c) 2025 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef __MAIN_H
#define __MAIN_H
#ifdef __cplusplus
extern "C" {
#endif
/* Includes ------------------------------------------------------------------*/
#include "stm32f1xx_hal.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
/* Exported types ------------------------------------------------------------*/
/* USER CODE BEGIN ET */
/* USER CODE END ET */
/* Exported constants --------------------------------------------------------*/
/* USER CODE BEGIN EC */
/* USER CODE END EC */
/* Exported macro ------------------------------------------------------------*/
/* USER CODE BEGIN EM */
/* USER CODE END EM */
/* Exported functions prototypes ---------------------------------------------*/
/* USER CODE BEGIN EFP */
/* USER CODE END EFP */
/* Private defines -----------------------------------------------------------*/
#define LED_RED_Pin GPIO_PIN_4
#define LED_RED_GPIO_Port GPIOA
#define LED_GREEN_Pin GPIO_PIN_5
#define LED_GREEN_GPIO_Port GPIOA
#define LED_BLUE_Pin GPIO_PIN_6
#define LED_BLUE_GPIO_Port GPIOA
/* USER CODE BEGIN Private defines */
void _Error_Handler(const char *, int);
#define Error_Handler() _Error_Handler((const char *)__FILE__, __LINE__)
#define REDIRECT_PRINTF
/* USER CODE END Private defines */
#ifdef __cplusplus
}
#endif
#endif /* __MAIN_H */
main.c
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2025 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
#include "string.h"
#include "stdbool.h"
#include "entryPointCPP.hpp"
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef huart2;
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART2_UART_Init(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
#ifdef REDIRECT_PRINTF
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#endif
#ifdef REDIRECT_PRINTF
/**
* @brief Retargets the C library printf function to the USART.
* @param None
* @retval None
*/
PUTCHAR_PROTOTYPE
{
/* Place your implementation of fputc here */
/* e.g. write a character to the USART2 and Loop until the end of transmission */
HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
#endif
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART2_UART_Init();
/* USER CODE BEGIN 2 */
printf("\x1b[2J\x1b[H"); // Clear the dumb terminal screen
printf("Good Day, Mate!!!\r\n");
initLED(LED_RED_GPIO_Port, LED_RED_Pin, LED_GREEN_GPIO_Port, LED_GREEN_Pin, LED_BLUE_GPIO_Port, LED_BLUE_Pin);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
blinkRGB();
toggleLEDs();
alternateLEDs();
HAL_Delay(1000);
setStatus();
getStatusAll();
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
/**
* @brief USART2 Initialization Function
* @param None
* @retval None
*/
static void MX_USART2_UART_Init(void)
{
/* USER CODE BEGIN USART2_Init 0 */
/* USER CODE END USART2_Init 0 */
/* USER CODE BEGIN USART2_Init 1 */
/* USER CODE END USART2_Init 1 */
huart2.Instance = USART2;
huart2.Init.BaudRate = 19200;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart2.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart2) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART2_Init 2 */
/* USER CODE END USART2_Init 2 */
}
/**
* @brief GPIO Initialization Function
* @param None
* @retval None
*/
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* USER CODE BEGIN MX_GPIO_Init_1 */
/* USER CODE END MX_GPIO_Init_1 */
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOA, LED_RED_Pin|LED_GREEN_Pin|LED_BLUE_Pin, GPIO_PIN_RESET);
/*Configure GPIO pins : LED_RED_Pin LED_GREEN_Pin LED_BLUE_Pin */
GPIO_InitStruct.Pin = LED_RED_Pin|LED_GREEN_Pin|LED_BLUE_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* USER CODE BEGIN MX_GPIO_Init_2 */
/* USER CODE END MX_GPIO_Init_2 */
}
/* USER CODE BEGIN 4 */
/**
* @brief This function is executed in case of error occurrence.
* @param file: The file name as string.
* @param line: The line in file as a number.
* @retval None
*/
void _Error_Handler(const char *file, int line)
{
/* USER CODE BEGIN Error_Handler_Debug */
__disable_irq();
#ifdef REDIRECT_PRINTF
char buf[80];
sprintf(buf, "Trapped in Error_Handler(). Called from: %s, line: %d\r\n", file, line);
printf(buf);
#endif
/* User can add his own implementation to report the HAL error return state */
while(1)
{
}
}
/* USER CODE END 4 */
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
entryPointCPP.hpp
In the following C Plus Plus include file: .hpp we declare function/method names as being extern “C”. This tells the compiler/linker to prevent name mangling and will allow these to be included in standard C code. See lines #18 and 27 below.
/*
* entryPointCPP.hpp
*
* Created on: Jan 28, 2025
* Author: johng
*/
#ifndef INC_ENTRYPOINTCPP_HPP_
#define INC_ENTRYPOINTCPP_HPP_
#include "main.h"
#include "stdio.h"
#include "strings.h"
#include "stdbool.h"
#ifdef __cplusplus
extern "C" {
#endif
void initLED(GPIO_TypeDef *redPort, uint16_t redPin, GPIO_TypeDef *greenPort, uint16_t greenPin, GPIO_TypeDef *bluePort, uint16_t bluePin);
void blinkRGB();
void alternateLEDs();
void toggleLEDs();
void setStatus();
void getStatusAll();
#ifdef __cplusplus
}
#endif
#endif /* INC_ENTRYPOINTCPP_HPP_ */
entryPointCPP.cpp
Even though we’ve created a source file designated on C Plus Plus or .cpp we can still prevent name mangling by creating stand C-Like methods/function but declare them in the .hpp or include file as extern “C”
All of the highlighted lines of code will NOT be name mangled and will be available to be called within standard C code. All of the proto-type definitions located in the .hpp file are withing the extern “C” clause.
/*
* entryPointCPP.cpp
*
* Created on: Jan 28, 2025
* Author: johng
*/
#include <ManagedLEDs.hpp>
#include "entryPointCPP.hpp"
ManagedLEDs *manageMyLED = NULL;
void initLED(GPIO_TypeDef *redPort, uint16_t redPin, GPIO_TypeDef *greenPort, uint16_t greenPin, GPIO_TypeDef *bluePort, uint16_t bluePin) {
manageMyLED = new ManagedLEDs(redPort, redPin, greenPort, greenPin, bluePort, bluePin);
}
void blinkRGB() {
manageMyLED->setRedLED(ON);
manageMyLED->setGreenLED(ON);
manageMyLED->setBlueLED(ON);
getStatusAll();
HAL_Delay(500);
manageMyLED->setRedLED(OFF);
manageMyLED->setGreenLED(OFF);
manageMyLED->setBlueLED(OFF);
getStatusAll();
HAL_Delay(500);
}
void alternateLEDs() {
manageMyLED->setRedLED(ON);
HAL_Delay(500);
manageMyLED->setRedLED(OFF);
manageMyLED->setGreenLED(ON);
HAL_Delay(500);
manageMyLED->setGreenLED(OFF);
manageMyLED->setBlueLED(ON);
HAL_Delay(500);
manageMyLED->setBlueLED(OFF);
HAL_Delay(500);
}
void toggleLEDs() {
manageMyLED->toggleRedLED();
HAL_Delay(100);
manageMyLED->toggleRedLED();
HAL_Delay(100);
manageMyLED->toggleGreenLED();
HAL_Delay(100);
getStatusAll();
HAL_Delay(100);
manageMyLED->toggleGreenLED();
HAL_Delay(100);
manageMyLED->toggleBlueLED();
HAL_Delay(100);
manageMyLED->toggleBlueLED();
HAL_Delay(500);
}
void setStatus() {
manageMyLED->setRedLED(ON);
manageMyLED->setGreenLED(OFF);
manageMyLED->setBlueLED(ON);
}
void getStatusAll() {
printf("======================================\r\n");
if (manageMyLED->getCurrRedStatus()) {
printf("Red LED is: ON\r\n");
} else {
printf("Red LED is: OFF\r\n");
}
if (manageMyLED->getCurrGreenStatus()) {
printf("Green LED is: ON\r\n");
} else {
printf("Green LED is: OFF\r\n");
}
if (manageMyLED->getCurrBlueStatus()) {
printf("Blue LED is: ON\r\n");
} else {
printf("Blue LED is: OFF\r\n");
}
printf("======================================\r\n");
}
ManagedLEDs.hpp
The follow Include file and it’s .CPP source file are nothing more than a typical C-Plus Plus class. The following methods/functions can be called from the enterPointCPP.cpp source file without any issues.
/*
* ManagedLEDs.hpp
*
* Created on: Jan 28, 2025
* Author: johng
*/
#ifndef MANAGEDLEDS_HPP_
#define MANAGEDLEDS_HPP_
#include "main.h"
#include "stdbool.h"
#define OFF false
#define ON (!OFF)
class ManagedLEDs {
public:
ManagedLEDs(GPIO_TypeDef *redPort, uint16_t redPin,
GPIO_TypeDef *greenPort, uint16_t greenPin, GPIO_TypeDef *bluePort, uint16_t bluePin);
void setRedLED(bool onOff);
void setGreenLED(bool onOff);
void setBlueLED(bool onOff);
bool getCurrRedStatus();
bool getCurrGreenStatus();
bool getCurrBlueStatus();
void toggleRedLED();
void toggleGreenLED();
void toggleBlueLED();
private:
GPIO_TypeDef *redPort;
GPIO_TypeDef *greenPort;
GPIO_TypeDef *bluePort;
uint16_t redPin;
uint16_t greenPin;
uint16_t bluePin;
bool redStatus;
bool greenStatus;
bool blueStatus;
};
#endif /* MANAGEDLEDS_HPP_ */
ManagedLEDs.cpp
/*
* ManagedLEDs.cpp
*
* Created on: Jan 28, 2025
* Author: johng
*/
#include <ManagedLEDs.hpp>
ManagedLEDs::ManagedLEDs(GPIO_TypeDef *redPort, uint16_t redPin,
GPIO_TypeDef *greenPort, uint16_t greenPin, GPIO_TypeDef *bluePort, uint16_t bluePin) {
this->redPort = redPort;
this->greenPort = greenPort;
this->bluePort = bluePort;
this->redPin = redPin;
this->greenPin = greenPin;
this->bluePin = bluePin;
setRedLED(OFF);
setGreenLED(OFF);
setBlueLED(OFF);
}
void ManagedLEDs::setRedLED(bool onOff) {
redStatus = onOff;
if (redStatus) {
HAL_GPIO_WritePin(redPort, redPin, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(redPort, redPin, GPIO_PIN_RESET);
}
}
void ManagedLEDs::setGreenLED(bool onOff) {
greenStatus = onOff;
if (greenStatus) {
HAL_GPIO_WritePin(greenPort, greenPin, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(greenPort, greenPin, GPIO_PIN_RESET);
}
}
void ManagedLEDs::setBlueLED(bool onOff) {
blueStatus = onOff;
if (blueStatus) {
HAL_GPIO_WritePin(bluePort, bluePin, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(bluePort, bluePin, GPIO_PIN_RESET);
}
}
void ManagedLEDs::toggleRedLED() {
redStatus = !redStatus;
if (redStatus) {
HAL_GPIO_WritePin(redPort, redPin, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(redPort, redPin, GPIO_PIN_RESET);
}
}
void ManagedLEDs::toggleGreenLED() {
greenStatus = !greenStatus;
if (greenStatus) {
HAL_GPIO_WritePin(greenPort, greenPin, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(greenPort, greenPin, GPIO_PIN_RESET);
}
}
void ManagedLEDs::toggleBlueLED() {
blueStatus = !blueStatus;
if (blueStatus) {
HAL_GPIO_WritePin(bluePort, bluePin, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(bluePort, bluePin, GPIO_PIN_RESET);
}
}
bool ManagedLEDs::getCurrRedStatus() {
return(redStatus);
}
bool ManagedLEDs::getCurrGreenStatus() {
return(greenStatus);
}
bool ManagedLEDs::getCurrBlueStatus() {
return(blueStatus);
}


