How to harness the power of generated code in STM32CubeIDE with your custom code

Code generation from ioc

STM32CubeIDE is a powerful piece of software, including code generation from a GUI formerly known as CubeMX, a code editing environment and a full debugger. The code generation feature should not be dismissed as a beginner level gimmick because it can be used within a professional environment, saving a huge effort compared to doing that work yourself, when you know how to keep it under control.

If you're not aware of how to keep the code generator under control, you could lose code, be unable to get your code where you need it in the files, and feel restricted by it's imposed code structure.

The CubeMX part of STM32CubeIDE can be used as a standalone application or integrated into the IDE and uses the [project name].ioc file to store the configuration of the peripherals, drivers and middleware in a text file which can then be displayed in an interactive GUI. One ioc file is used for the project, even for multicore processors, which are configured using two columns of checkboxes.

Avoiding the beginner mistake of writing code anywhere in the files

When working with the files generated by CubeMX, it is easy to be so overwhelmed by all the boilerplate lines that you just ignore them and start writing your own functionality wherever you think you need it. However, you will soon discover that it is easy to lose your changes. This happens if you go back to the ioc and regenerate the code either by clicking on the Regenerate button or saving the ioc - although that should ask "Do you want to generate Code?" before doing so, you may have previously checked Remember my decision. Unfortunately no warning is given if the code generator overwrites your code.

Do you want to regenerate code?

Professional developers should, of course, have their code committed in a repository before they regenerate. Git having cheap branches is ideal for this.

The only safe place to write your code is between the comment blocks and not outside them.

/* USER CODE BEGIN ... */

/* USER CODE END ... */

This does not apply to files you have added yourself, either in folders you have added to the project or in the CubeMX generated folders such as Code\Src.

Always check in Project Manager - Code Generator - Generated files that Keep User Code when re-generating is checked, otherwise you will lose your changes even if they are between USER CODE blocks.

Giving up on the code generator

It's tempting to say that CubeMX imposes itself too much on the project structure and code, and we want to do things our way instead, so let's run the generator once, then throw the ioc and that project away and start again in a fresh project, just extracting what we needed from the generated project. Having seen that approach being used, I've found that it is not a good route in the long term. Let's examine the reasons.

This approach assumes both that the ioc configuration will never need to be changed during the life of the project and that the clock tree will not need even to be viewed, never mind changed. With an ioc for the project it is very much harder to work out how peripherals have been configured and to keep the code and comments for that code in sync. It doesn't seem to be a hard problem if you're only thinking about UART baud rates, timer intervals and ADC settings, but for any of the more complex peripherals you will regret not being able to view their configuration graphically. It's not just being able to see the Parameter Settings in a readable way, it's also about how the other tabs bring in related configuration such as DMA.

SPI with DMA being configured in ioc

Clocks for peripherals are generated by a clock tree whose configuration is not easy to determine just by looking at the output code. Changes here are much better managed graphically. Compare this clock tree visualisation against the code:

Clock tree for dual core STM32H745
/** 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_BYPASS;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLM = 1;
  RCC_OscInitStruct.PLL.PLLN = 18;
  RCC_OscInitStruct.PLL.PLLP = 2;
  RCC_OscInitStruct.PLL.PLLQ = 2;
  RCC_OscInitStruct.PLL.PLLR = 2;
  RCC_OscInitStruct.PLL.PLLRGE = RCC_PLL1VCIRANGE_3;
  RCC_OscInitStruct.PLL.PLLVCOSEL = RCC_PLL1VCOMEDIUM;
  RCC_OscInitStruct.PLL.PLLFRACN = 6144;
  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_CLOCKTYPE_D3PCLK1|RCC_CLOCKTYPE_D1PCLK1;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.SYSCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB3CLKDivider = RCC_APB3_DIV2;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_APB1_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_APB2_DIV2;
  RCC_ClkInitStruct.APB4CLKDivider = RCC_APB4_DIV2;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_1) != HAL_OK)
  {
    Error_Handler();
  }

Parallel project approach

Keeping a project with just the ioc and generated code in it so that changes can be diff'd across into the main project with the custom code could be a solution, but if you don't keep the same structure and filenames this is going get increasingly difficult to manage and lead to mistakes due to missing off a change. A good diff tool, or maybe a script could make this easier but there's always another stage to changing the hardware configuration so there's no real advantage - once you know the techniques shown here.

Re-ordering or eliminating generated code

Sometimes there is a need to keep the generated code but add your own lines between theirs. At other times, we need the majority of the generated code but not some small part, usually the initialisation which might need to be specially modified. Finally, there are occasions where we need CubeMX to generate a handler but the generated code isn't quite right and can't be fixed with additional lines - the generated code needs to be changed.

The solution for this is to allow CubeMX to generate the code but replace it with our custom code lines using the preprocessor #ifdef 0 and #endif in the USER CODE sections like this

/* USER CODE BEGIN ... */
#if 0
/* USER CODE END ... */
---generated code that we don't want---
/* USER CODE BEGIN ... */
#endif
/* USER CODE END ... */

Here's an example where the ethernet interrupt handler wasn't what was wanted:

/**
  * @brief This function handles Ethernet global interrupt.
  */
void ETH_IRQHandler(void)
{
  /* USER CODE BEGIN ETH_IRQn 0 */
#if 0    //workaround Cube generating function call with no param
  /* USER CODE END ETH_IRQn 0 */
  HAL_ETH_IRQHandler();
  /* USER CODE BEGIN ETH_IRQn 1 */
#endif
  HAL_ETH_IRQHandler(&heth);
  /* USER CODE END ETH_IRQn 1 */
}

If you've had to copy the generated code to modify it, this becomes a similar situation to the parallel project approach and you have to watch out for any changes to be manually updated in your copy. Whilst this isn't ideal from a maintenance point of view, it's preferable to the complete replacement technique which is discussed next.

Major changes to generated code

If the changes to the generated code are so widespread that the above techniques aren't workable, then you can get even closer to the parallel project idea. Add your peripherals, middleware components etc in CubeMX, generate the code, then move that code into folders not generated by CubeMX so that it no longer knows about them. (You can delete all the USER CODE comment blocks at this point). Then uncheck those components in CubeMX, since you are not using their versions of them, and you are safe to regenerate.

Bear in mind that this technique can be confusing to others who see that the component is used but can't configure it in the ioc. Treat it as a last resort because of the loss of the use of the GUI for this component.

Starting a peripheral after startup

Once you configure a peripheral in CubeMX, it automatically calls the Init functions at startup but you might not always want that. You can still configure the peripheral in CubeMX, but go to the Project Manager tab in the ioc view, Advanced Settings and tick Do Not Generate Function Call for the peripheral instances that you want this to apply to. Then regenerate code.

Template Editing

If you're feeling really adventurous, you could try editing the template files. These are  found under the Cube IDE folder eg C:\ST\STM32CubeIDE_<version>\STM32CubeIDE\plugins\com.st.stm32cube.common.mx_<version>\db\templates\*.ftl Apart from working out the syntax and testing your changes being time-consuming, the other problem is that you'd have to ensure all the developers on the team use this modified template and keep using it throughout the life of the project. They would also have to be in your project repository, or linked to from there as well as the use of non-standard templates being clearly documented.

Moving peripherals between cores

CubeMX will forget the configuration of a peripheral as soon as it is unchecked in the GUI, so the safe way to move a peripheral between cores is to add it to the other core before unchecking it from the first one. Then you can save and regenerate.

Timers being configured on dual core processor

This article was updated on March 4, 2025