Discovery: Code, Part 2
In this edition of Discovery, we will look at the code generated by ST’s CubeMX system (Cube). Last time, we got an overview of the code generation menu items and pushed the button to generate some code, but what did we get?
Main!
Cube doesn’t just generate the startup and configuration code, it gives you a complete C program including main() and the typical embedded systems infinite loop.
Following along from last time, with our project generated for a STM32F407G-DISC1 development board, Cube generates this set of files:
The Inc folder holds all of the header files for the project.
The Src folder holds all of the C source files. If you create other files in this directory, Cube will show some respect and not clobber them unless your files are named the same as the ones that Cube generates, in which case your file is overwritten. A much better idea would be to create a directory for your source code called source and another called includes and put your header files in there. That way Cube will not clobber your files.
Depending on your IDE, you will get all of the ancillary files necessary to get your project to compile. I am using the SW4STM32 IDE, so Cube also generates a .ld file for the loader.
The .ioc file is the data file where Cube holds your project. Later, you can load it back into Cube, modify your project, and regenerate the code.
Let’s look at some generated code. Inside of main.c, Cube has the typical block of copyrights and disclaimers, followed by an includes block:
1 /* Includes ------------------------------------------------------------------*/
2 #include "stm32f4xx_hal.h"
3 #include "gpio.h"
4
5 /* USER CODE BEGIN Includes */
6
7 /* USER CODE END Includes */
Cube uses a pair of comments like lines 5 and 7 to bracket areas where you can place your code. As you develop your project, you would put your custom code in these areas. Don’t delete these comments, they are used by Cube when you adjust your configuration and regenerate your code. Your code between the comments is preserved.
You can put any C code you want between those comments but, as a matter of good style, put your #include statements in the includes area, static variables in the private variables area, static function prototypes in the Private function prototypes area.
The main function has similar user code block areas:
1 int main(void)
2 {
3
4 /* USER CODE BEGIN 1 */
5
6 /* USER CODE END 1 */
7
8 /* MCU Configuration----------------------------------------------------------*/
9
10 /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
11 HAL_Init();
12
13 /* Configure the system clock */
14 SystemClock_Config();
15
16 /* Initialize all configured peripherals */
17 MX_GPIO_Init();
18
19 /* USER CODE BEGIN 2 */
20
21 /* USER CODE END 2 */
22
23 /* Infinite loop */
24 /* USER CODE BEGIN WHILE */
25 while (1)
26 {
27 /* USER CODE END WHILE */
28
29 /* USER CODE BEGIN 3 */
30
31 }
32 /* USER CODE END 3 */
33
34 }
I don’t particularly care for ST’s code style, I find putting my code into their slots pretty restrictive, so I would replace line 25 with a function call to my main loop in another code file.
24 /* USER CODE BEGIN WHILE */
25 ThermostatMain();
26 {
27 /* USER CODE END WHILE */
Even if you rerun the code generation phase of Cube, line 25 will be preserved. Then in my code, probably in a file called thermostat.c in my source directory, I would have a function called ThermostatMain() which would contain my normal FOREVER loop structured code.
I would let Cube generate code to start the processor and configure the peripherals, after that, I’m done with main.c. It never gets modified again.
Other Files
Last week, I showed the Pinout view in Cube, where ST had given names to the pins on the processor (these are set by right clicking on the processor pin and choose Enter User Label). These names are available in the file mxconstants.h so that you can use them in your C code. Each pin gets two defines in the header file, the port and pin number. So, in the schematics for the board, LEDs 3 and 4 were labeled LD3 and LD4 and the defines end up looking like this:
#define LD4_Pin GPIO_PIN_12
#define LD4_GPIO_Port GPIOD
#define LD3_Pin GPIO_PIN_13
#define LD3_GPIO_Port GPIOD
To turn on LED3, you could use the command:
HAL_GPIO_WritePin( GPIOD, GPIO_PIN_12, GPIO_PIN_SET);
Or better yet:
HAL_GPIO_WritePin( LD4_GPIO_Port, LD4_Pin, GPIO_PIN_SET);
Instead of using LD3 and LD4, I prefer to rename these pins LED_ORANGE and LED_GREEN (since I’m colourblind and need all of the help I can get), which would give me:
HAL_GPIO_WritePin( LED_GREEN_GPIO_Port, LED_GREEN_Pin, GPIO_PIN_SET);
The file stm32f4xx_it.c is used to house the interrupt handlers of the system, initially just the millisecond tick timer interrupt.
The file stm32f4xx_hal_msp.c is a part of the MSP initializing system (I think MSP means MCU Support Package, it’s not documented very well). The hardware abstraction layer (HAL) provides a set of functions that are called to initialize and deinitialize the timers, UARTS, and the system as a whole. This file, using a clever feature of the C language called weak linkage, is used by Cube to call the various functions to initialize the peripherals.
By choosing to put the support code for each peripheral into their own .c/.h files, you will get a collection of files such as gpio.c for the general purpose input/output pins. As you enable more peripherals, you will get more and more support files. If you create your own source directory, you won’t have to look at Cube’s collection of files very often.
The Drivers directory holds the source code for the HAL, which is all of the support code to use the peripherals and not have to worry about register offsets, and minutia of configuring peripherals. Also in this directory is the source code for the accessing the digital signal processing instructions of the processor. If you were to enable the RTOS, TCP/IP stack, USB stack, or DOS file system, their source code would be deposited here as well.
Don’t panic if there is a lot of stuff here, the C compiler will only link in the functions that you actually use, not everything. Unused functions are deleted from your object files.
Now we’ve seen what files are generated by Cube, in the next post we will discuss the various IDEs and compilers that Cube supports directly. Until then, generate some projects, explore the features in Cube, take a look at some of the code in Drivers/STM32F4xx_HAL_Driver/Src. It’s pretty interesting to see how other people write code.
This post is part of a series. Please see the other posts here.
Cooking up a big batch of C code.