This article explains how to start a C++ project on an STM32F4 board. The focus is on setting up the project, configuring the system, and enabling a ConsoleTask for debug messaging.
Scope
- We will cover:
- Creating a new STM32 project using an .ioc file
- Basic system configuration (Clock, Timer, Drivers)
- Setting up a ConsoleTask for debugging via USART6
- Building the project and verifying debug messages
1. Creating the Project
STM32CubeIDE allows us to generate a project from an existing .ioc file. This way, pin mapping and initial configuration are applied automatically.
To use the example .ioc file, go to the Download menu and select “TW100PCTest.ioc.
Select “STM Project from Existing STM32CubeMX Configuration File(.ioc)”

- Enter a project name (e.g., TW100PCTest)
- Targeted Language를 C++로 지정 → 이 단계가 중요합니다. 기본값은 C 언어이기 때문에 반드시 C++로 바꿔줘야 이후 ConsoleTask 코드를 원활히 사용할 수 있습니다.

2. Reviewing Configuration
Clock
- HSE: 12MHz
- PLL: 168MHz
- LSI: 32kHz
Driver Selector
- I2C, RTC → HAL
- Others → LL
- 조합을 사용합니다.
This hybrid approach combines simplicity of HAL for communication and timekeeping with efficiency of LL for performance-critical parts.
3. Setting Up ConsoleTask
Debugging via serial output is essential. In this setup, we dedicate USART6 for Console messages using DMA. Steps:
- Rename main.c → main.cpp
- Rename stm32f4xx_it.c → stm32f4xx_it.cpp
- Create a Libraries folder and add twlabcpp-stm32f4-base

4. main.cpp Implementation
ConsoleTask.h and ConsoleTask Object
/* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ #include <ConsoleTask.h> /* USER CODE END Includes */ ... /* USER CODE BEGIN PV */ ConsoleTask CTask; __attribute__ ((section(".ccmram"))) volatile uint8_t consoleBuf[2048]; ...
Initialization
- 초기화 함수를 별도로 정의하고 implementation문은 main.cpp 내에 /* USER CODE … */ 블록 내에 둔다.
/* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_DMA_Init(void); static void MX_TIM2_Init(void); static void MX_SPI1_Init(void); static void MX_USART6_UART_Init(void); static void MX_I2C3_Init(void); static void MX_RTC_Init(void); /* USER CODE BEGIN PFP */ void InitConsoleTask(void); ... ... /* USER CODE BEGIN 4 */ /* * */ void InitConsoleTask(void) { CTask = ConsoleTask(USART6, DMA2, LL_DMA_STREAM_6, DMA2, LL_DMA_STREAM_1); CTask.setBufPtr((uint8_t *)consoleBuf); CTask.uart.DMARxEnable(); LL_USART_EnableIT_IDLE(USART6); CTask.PRINTF((char *)"\r\n\r\n"); CTask.PRINTF((char *)"=====================================\r\n"); CTask.PRINTF((char *)"Hello. This is TW100PCTest Application\r\n"); CTask.PRINTF((char *)"\r\nBuild Date: %s, %s\r\n", __DATE__, __TIME__); CTask.PRINTF((char *)"=====================================\r\n"); CTask.flushTxBuf(); }
Main Function
- Call InitConsoleTask(); after CubeMX initialization
/* Initialize all configured peripherals */ MX_GPIO_Init(); MX_DMA_Init(); MX_TIM2_Init(); MX_SPI1_Init(); MX_USART6_UART_Init(); MX_I2C3_Init(); MX_RTC_Init(); /* USER CODE BEGIN 2 */ InitConsoleTask(); /* USER CODE END 2 */
- Inside the infinite loop, call CTask.run(); to process messages
/* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { CTask.run(); /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */
5. Interrupt Handlers
- Include "ConsoleTask.h" and declare ConsoleTask Object as extern
/* Includes ------------------------------------------------------------------*/ #include "main.h" #include "stm32f4xx_it.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ #include <ConsoleTask.h> /* USER CODE END Includes */ ... /* External variables --------------------------------------------------------*/ /* USER CODE BEGIN EV */ extern ConsoleTask CTask; /* USER CODE END EV */
- Modify DMA and USART6 interrupt handlers to redirect events to `ConsoleTask`:
/** * @brief This function handles DMA2 stream1 global interrupt. */ void DMA2_Stream1_IRQHandler(void) { /* USER CODE BEGIN DMA2_Stream1_IRQn 0 */ CTask.uart.RXDMAInterruptHandler(); /* USER CODE END DMA2_Stream1_IRQn 0 */ /* USER CODE BEGIN DMA2_Stream1_IRQn 1 */ /* USER CODE END DMA2_Stream1_IRQn 1 */ } ... /** * @brief This function handles DMA2 stream6 global interrupt. */ void DMA2_Stream6_IRQHandler(void) { /* USER CODE BEGIN DMA2_Stream6_IRQn 0 */ CTask.uart.TXDMAInterruptHandler(); /* USER CODE END DMA2_Stream6_IRQn 0 */ /* USER CODE BEGIN DMA2_Stream6_IRQn 1 */ /* USER CODE END DMA2_Stream6_IRQn 1 */ } ... /** * @brief This function handles USART6 global interrupt. */ void USART6_IRQHandler(void) { /* USER CODE BEGIN USART6_IRQn 0 */ CTask.uart.IDLEInterruptHandler(); /* USER CODE END USART6_IRQn 0 */ /* USER CODE BEGIN USART6_IRQn 1 */ /* USER CODE END USART6_IRQn 1 */ }
6. Build & Test
- Build project → generate .bin and .hex files
- Flash with stlink
- Open Putty at 2 Mbps baudrate

- Press reset → you should see startup messages displayed

Conclusion
- We now have a working C++ development environment on the TW100PC board, complete with a DMA-based ConsoleTask for debugging.
- In the next steps, we’ll build on this foundation to implement actual application features
0 Comments