Creating a Cube Project

So you’ve installed STM32CubeIDE (a.k.a. “Cube” or “the IDE”) and you’ve got a board with an STM32 on it. Time to get coding ! This page about setting up a project : the project itself is unimportant, so we’ll go with blinking an LED. That’s the “Hello World” of the microcontroller world.

And it just so happens that every Nucleo boards has at least one “user” LED, so you can follow along right away without having to add any hardware.

Fair warning : this is gonna be a long one ! I’m covering everything from unpacking your module all the way to programming it and I’m going to be pedantic about it. Because blinking that LED is just a pretext for me to try and drill into you the whole process of developing code for STM32 using ST’s own tools. If you’re in a hurry, maybe do this another time. STM32 development is not complicated, it’s very quick and painless once you know how to use the tools correctly. It only hurts the first time.

 

1.   Preparing the Hardware

We’ll be using the tiny Nucleo-F303K8 for this little tutorial. You can use any other STM32 board or module you want, but you’ll be on your own if you get stuck : I can’t generalize enough without making this tutorial much longer than it already is. Seriously, get this module. It’s very useful. It’s only worth 9 Euros and you can buy it directly from its product page on the ST website, or from your preferred source of electronic components. This is what you’ll get :

No need for scissors, the package opens nicely and is reusable. The card has useful information on both sides, so you should keep it :

Notice the “Getting Started” part. Yes, there’s already a demo that blinks an LED pre-flashed into the MCU ! Project complete ! Job done ! Achievement unlocked !

Just kidding. It doesn’t count unless we do it ourselves and from scratch. And I’ll be extra-pedantic since blinking an LED is just a pretext to talk about creating projects in general.

Grab a Micro-USB cable. Nucleo ship without one, e-waste reduction or evil capitalist cost-cutting ? You decide. I don’t politic.

Go to the Nucleo module’s page on the ST website. Go to the Documentation tab. This is where you’ll find the module’s user manual. Download it, read it twice. This is microcontroller programming : you need to know your hardware inside and out, there’s no way around it except minefields.

The user manual for any Nucleo contains vital information, notably :

  • What the various jumpers and solder bridges do
  • Module pin-out tables
  • The module’s schematics

For some Nucleo modules, the schematics might be absent from the user manual. You’ll need to download them separately from the same webpage as the user manual.

There are two important things to know about Nucleo schematics :

First and foremost, each Nucleo design (in our case, Nucleo 32, for 32-pin MCU’s) is capable of carrying different target microcontrollers. The schematic in the user manual is common to all eight variants of the Nucleo 32, the F303K8 being just one of them. ST is simply being efficient, designing a single circuit board for several products.

That’s why you need to read the whole manual : it will tell you about any important details specific to the variant you’re using. Don’t cry. It’s only around 30 pages and there’s lots of drawings. Plus, people who read manuals get the right to say RTFM to people who don’t.

Second, each Nucleo carries two STM32 microcontrollers. One of them is your target, the other is programmed to act as an ST-LINK v2 JTAG / SWD probe. Don’t confuse them. You should see a schematic page that looks like this :

Let’s dwell on that schematic.

2.   Default Target I/O

You can see six signals going from the probe to the target microcontroller. Those are designed to help you :

  • SWDIO and SWCLK are, big surprise, the SWD interface. This interface allows for programming (“flashing”) the target microcontroller but also for debugging your code using breakpoints, something an Arduino can’t do and that separates the men from the boys.
  • MCO (Master Clock Output) is an STM32 feature that allows a microcontroller to feed its own clock signal to other chips. On Nucleo boards, it can be used to clock your target from the probe’s precision crystal oscillator, but you can also use the target’s internal RC oscillator instead. Why you might choose one or the other is a discussion for another time.
  • VCP_TX and VCP_RX form a full-duplex UART. VCP means Virtual COM Port : the probe can act as a USB COM port when you plug it into your PC, and your target STM32 can use that bridge to communicate with your PC without extra hardware. It’s a very useful feature that we’ll explore some other time.
  • NRST is the module’s reset signal. Both the probe and the target are always reset together. Reset occurs via a tiny push button or automatically when applying power.

At long last, let’s fire up STM32CubeIDE and do some clicking and typing !

The very first thing you must do is tell the IDE you want to create a new project. And it has to be an STM32 project :

The IDE will open the target selector, which has tabs. The first two tabs are the only ones that really matter :

  • MCU/MPU Selector : if you’re going to code for an STM32 board you designed yourself or for a third-party board not supported by ST, go there. It’ll let you select any STM32 microcontroller in existence.
  • Board Selector : if you’re going to code for an ST board or module such as a Nucleo, go there. This will let the IDE preset I/O definitions to match your board or module.

In the Board Selector you can search for the Nucleo-F303K8 a number of different ways : for example, you can specify “F303K8” in the “Commercial Part Number” box, or you can tick “Nucleo 32” under “Type”, etc…

Select your module and click Next. This takes you to a basic dialog box where you’ll specify your project’s name. For the purpose of our little tutorial, let’s call ours “F303K8_LED_Blink” :

Leave all other settings to their default value :

  • STM32CubeIDE should support C++, but it doesn’t really as of 2021. Stick to C.
  • We want to run our code directly, so it has to be compiled as an executable binary.
  • The STM32Cube project type means that the IDE will add ST’s Cube libraries to your project.

About those Cube libraries : some people like them, some people hate them, it’s a big debate. They are simply helpful libraries to get you started fast. Without them, you’d have to write a whole lot of non-trivial code from scratch just to get your STM32 to start. That wouldn’t “add value” to your project, but it would definitely put a big old brake on any rapid-prototyping endeavor.

That being said, if your project gets really complex then you might run into limitations using the Cube libraries. Why ? Because they are designed to be as generic as possible. They will fit most applications but if you push the envelope you might find that “generic” doesn’t cut it. By that time you get there, you should be skilled enough to either code your own specialized libraries or decide that your project isn’t worth the trouble and find another solution. Like paying someone. But let me be clear on this : it’ll take a while for you to run into those limitations. Maybe when you start playing with real-time operating systems with Ethernet and USB support. Certainly not when you’re just trying to make a thermometer with an LCD display.

Anyway, hit the Next button for more details :

You don’t need to change any settings here. The firmware package will default to the latest version. Cube libraries are packaged on a device family basis : we’re using the STM32F303, so our package is for the F3 family. You can find its documentation on the ST website at https://www.st.com/en/embedded-software/stm32cubef3.html

Library packages are fairly large, up to several hundred megabytes. The first time you use one, and any time you use a new version, it’ll download and install automatically : for that to happen you’ll need an internet connection.

In my experience, new versions are limited to bug fixes and are always backward-compatible. Eventually, the IDE may offer you to update an existing project to the latest version of the library package it relies on. Whether or not you update is up to you. It’s not a matter of cybersecurity, and you might want to stick with a version that you know works. Also, it’s entirely possible to use different package versions for different projects. Finally, if for some reason you’ve updated the libraries for a project and it broke it, you can revert to previous versions of the libraries. Nothing’s irreversible, here.

Leave the Code Generator Options set to “Copy only the necessary library files” : if you use a version control tool such as git and if you plan on distributing your source code, this will ensure that your repository contains all the files necessary for compiling your project.

Click Finish. You will be asked :

You must click Yes. This will let the IDE initialize the microcontroller configuration based on your Nucleo module’s hardware. It’s a great time-saver, especially on the larger ST boards.

You’re now ready to start configuring your STM32. Coding will come later :

Ignore for a second the rest of the IDE and focus on the microcontroller’s picture : the I/O from the schematic is already configured. Perfect !

3.   Project Anatomy

STM32CubeIDE is based on the Eclipse IDE. That’s why it might look familiar if you’ve been around the block a few times. On the left you’ll find a Project Explorer. A project’s root always looks the same :

The Includes folder contains links to all the include paths your project is aware of. That includes the location of the Cube libraries. This (pseudo-) folder is just a nice and easy way to check that all your include locations are known to your project.

The Core folder is where your application’s source files live. It has three subfolders : Inc for header files and Src for source files. Respect that structure. There’s also a Startup subfolder for the assembly language code that runs before anything else. You don’t need to worry about that one today, maybe ever.

The Drivers folder contains the ST’s Cube library source files, along with the CMSIS libraries. CMSIS is a set of vendor-independent libraries for ARM-based microcontrollers. Again, you don’t need to worry about that today.

The .ioc file is the configuration file for your microcontroller. It’s this file you edit through that nifty graphic interface I just showed you. Internally, it’s a human-readable text file. That’s very practical if you’re a git user, as it makes it very easy to spot and understand differences between two versions of this file. But you should always edit it through the IDE’s GUI. I don’t care if you’re one of those Linux snobs who insist on doing everything with a terminal and only edit with vim. Use the GUI !


Don’t argue

And finally, the .ld file is your project’s linker script. Big scary topic. We’ll have to face it at some point, but not today. Soon, though. After compilation, this file instructs the linker where to store your code and variables in the STM32’s Flash and RAM. The IDE generates a default script that is good enough for small devices and/or simple projects.

That’s all for now. We’ll go deeper into the Core and Drivers subfolders as we progress towards blinking that LED. Hey wait a minute… where is that LED ?

4.   Application I/O Configuration

The astute reader will have noticed that although the Nucleo comes with a user LED, it’s not assigned to any pin yet. What gives ? Well, the LED isn’t exactly a vital component of the Nucleo-F303K8. You might be planning to use the pin it’s connected to for something else. And then there’s the fact that this pin can be configured to drive the LED in several different ways. Perhaps the good people at ST thought it best to make no assumption and let you decide how you want to use it.

Let’s look at the schematic. The LED is somewhere around the extension connectors :

This tells us that the LED’s anode is connected to pin PB3 through solder bridge SB15. Somewhere else in the user manual you’ll find out that SB15 is closed by default. Or maybe you’ve already plugged in your Nucleo and noticed the LED blinking, a sure sign that SB15 is closed. Left-click on PB3 in the IDE. This will show us its muxing options :

That pin can do a lot of different things, but only one at a time. The obvious choice would be set it as a GPIO_Output and then have our program’s main loop turn it on and off. But is that the only way to go ? No. It’s just the most boring. PB3 can also be set as TIM2_CH2 : the second channel output of timer TIM2. If you’re new to all this, a timer is basically a hardware counter that can do many interesting things based on just counting. And one of those things is making a signal go on and off. Let’s try that instead. Once you select TIM2_CH2, here’s what the pin will look like :

The label TIM2_CH2 tells us what drives the pin, but it doesn’t tell us what our application uses it for. Right-click on the pin and change its label to LED. Custom pin labels eventually end-up in your code and make your life easier.

Grey pins are those you haven’t assigned a function to. They stay in their reset state and won’t bother you. Green pins are those that have been assigned a function, and that function has been setup. Yellow pins have been assigned a function but the peripheral that’s supposed to drive them (TIM2 in this case) hasn’t been setup yet. That’s what we’ll do next.

5.   Peripheral Configuration

On the left, there is a list of peripherals and subsystems sorted by category. Expand the “Timers” category to find TIM2 and select it. This will display the TIM2 Mode pane :

Right now, TIM2 is disabled. That’ll change as soon as you assign a purpose to it. Assign Channel 2 the mode PWM Generation CH2. This will change a few things :

  • There’s now a green check mark next to TIM2, denoting that it’s enabled and that there’s no configuration issue with it.
  • Below the Mode pane, the Configuration pane now exposes a lot of parameters :

Timers can be very complicated, but this page isn’t about timers so I’ll give you a break. We’re here to blink an LED at a rate the human eye can perceive. Let’s say once per second. And we want this timer to do it for us. That means the Counter Period has to work out to 1 Hz and the PWM Channel 2 Pulse must last half of that period (so the LED is only lit for half of each second).

To work out the counter’s period we need to know the speed at which the counter counts. For that, let’s look at the IDE’s Clock Configuration tab. I know, it looks like the control panel of a nuclear reactor, so for the sake of getting shit done before you grow a beard I’ve highlighted in red the bits that matter. Also, you don’t have to change anything :

Here’s how to read it :

  • HSI RC, found in every STM32, is the High-Speed Internal RC oscillator. This is an 8 MHz clock source that requires no external components. It’s a suitable choice for applications that don’t need quartz precision, like blinking an LED.
  • The System Clock Mux is essentially a manifold that lets you select which clock signal feeds the rest of the STM32 core. The STM32F303 can run at 72 MHz, so if you wanted it to go that fast you’d have to use the PLL (to the left). But here we’re just passing through the 8 MHz signal.
  • We don’t set any prescaler all the way to the peripherals. That means everything on the microcontroller is clocked at 8 MHz.

So now we know exactly how to setup out timer :

  • The Counter Period must be 8 million, since we want a one second period at 8 MHz.
  • The PWM Pulse must be 4 million, half the period.

Go back to the TIM2 Configuration pane and enter those values. Don’t forget to save.

6.   Generating the Source Code

We’ve managed to get this far without typing a single line of C. That’s about to change because we’re about to type exactly one line of C. How is it possible to write a complete application for booting an ARM processor and blinking an LED with just one line of code ? Through automated code generation, that’s how.

In the IDE’s Project menu, hit Generate Code. Or use the Alt+K shortcut if you want to be that guy.

The IDE will generate all sorts of source files based on the device configuration you’ve entered in the GUI. What’s important to note is that :

  • You can generate and regenerate your code as often as you like.
  • It’s a one-way process.

In other words, do not modify any of the code generated by the IDE. The correct way to work with STM32CubeIDE is to use the GUI whenever you need to modify your MCU’s configuration and then regenerate the code. Any changes you make to generated code will not be back-propagated to the GUI or to your .ioc file. Those change will be lost the next time you regenerate your project.

Let’s see what the IDE cooked for us. In the project explorer, expand the Core folder :

Quite a lot of files for such a simple application… but they all serve a purpose. Here are the ones you need to be familiar with :

main.h is your project’s main header file. Among other things, it will contain the macro declarations for pins you’ve assigned a name to. Like that LED pin we labelled earlier :

#define LED_Pin		GPIO_PIN_3
#define LED_GPIO_Port	GPIOB

main.c is your project’s main source file. This is where the main function lives, and it’s also where you’ll be doing your application-programming.

stm32f3xx_it.c contains your project’s interrupt service routines (ISR’s). This is beyond the scope of this tutorial but if you know what interrupts are, you’ll be glad to know where the ISR’s live. Note that the file is named for the STM32 family you’re using : that “f3” may be something else depending on your target.

Very few projects require you modify any of the other files.

You can add your own header and source files to organize your application code. In fact, it’s considered good practice. Libraries you may find on my website will often take the form of .h and .c files you need to copy to your Core\Inc and Core\Src folders.

7.   The main.c File

A little bit earlier I promised you’d write a line of code. It’s going to happen in this file. We’re getting there, be patient.

Open main.c and have a look around. It’s got a lot of stuff for a source file we haven’t even touched yet.

The first thing you need to notice and become very familiar with are all the USER comment block pairs. For example :

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

The main.c file is generated by the IDE and will be regenerated every time you change the microcontroller’s configuration. The IDE will preserve any code that you write between those USER comment blocks.

You must write your code between two existing and matching USER comment blocks.

Your code may well compile and run even if you write your code outside those boundaries, but it’ll be overwritten by the IDE the next time you regenerate your project. So make extra-sure to always follow this rule.

Cube generates the main.c file according to the same blueprint no matter the project or the target :

First, Cube includes some header files, starting with main.h. You can add your own directives.

Second, Cube declares some global variables. One for each STM32 peripheral you’ve enabled :

/* Private variables ---------------------------------------------------------*/
TIM_HandleTypeDef htim2;
UART_HandleTypeDef huart2;

Those are structures that the Cube library uses to interface with each peripheral. Among other things, they map to the registers in each peripheral. This means you never have to deal with register addresses. It helps make your code portable, should you need to port it to a different microcontroller. Make note of those variables : you’ll need to pass them as argument to the Cube library functions, as you’ll soon discover.

Next, Cube declares local functions to initialize the microcontroller and every peripheral you’ve enabled. You do not need to worry about those functions, they are generated and called by Cube on your behalf. If you feel you need to mess with them, don’t : you’re thinking of doing something that should be done in the configuration GUI and left to the IDE’s code generator.

Finally, we get to the main function. By default, all it does is initialize the microcontroller (using the functions I’ve just mentioned) and start an infinite loop with nothing in it.

If you’re coming from Arduino, this part is what you’re familiar with. At its core, an Arduino “sketch” is just two functions : setup and loop. The setup function runs once at the start of execution and allows you to initialize stuff, and then the loop function executes repeatedly forever. Arduino hides all the nuts and bolts from you, but now you’re in the Big Leagues and you’re old enough to see the whole main function :

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();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */
  // Nefastor says : this is where an Arduino would call the "setup" function 
  /* USER CODE END 2 */

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

    /* USER CODE BEGIN 3 */
    // Nefastor says : this is where an Arduino would call the "loop" function
  }
  /* USER CODE END 3 */
}

And that’s it. Not so terrible, am I right ? I wonder why the Arduino guys thought you couldn’t handle it.

Quick sidenote : the infinite loop is absolutely necessary because there’s no mechanism to tell the microcontroller to stop running code. At least none that wouldn’t be a hazardous hack. And if you let the main function return, your MCU is liable to execute all sorts of stuff it shouldn’t. Maybe even variables. There could be serious side effects and you don’t want that on your conscience. Therefore, make sure your MCU is always executing the main function, even if that means locking it in an endless loop doing nothing.

The rest of the main.c file contains the definition of all those initialization functions Cube calls from the main function. It’s good reading if you want to see how Cube translates your configuration into code but, let me reiterate, you should never modify those functions.

OK, let’s get back to the main function.

8.   Telling the LED to Blink

This has been a long journey, so maybe I need to recap : we’ve told the IDE that we’re targeting a Nucleo-F303K8 and that it has an LED. We’ve also told it that we want timer TIM2 to drive that LED using its ability to generate PWM signals. Based on all those requirements, the Cube IDE has generated a metric ton of source code that will take care of all we’ve asked for plus all the things that need to happen anyway (such as starting the microcontroller and calling the main function).

There’s now only one thing left for us to do : make the main function start TIM2’s PWM generator. That’s where all those explanations I’ve given you so far are going to pay off in just one line of code.

In the olden days, starting a PWM would mean finding out in documentation the address of the relevant control register and which bit to flip in order to get the job done. Error-prone, not portable, tough to debug. But we’re using the Cube libraries, so it’s all going to look pretty and smell nice.

Let’s suppose you’ve never used an STM32 timer before. Since you’re using the Cube libraries, go to your project explorer and expand the Drivers folder. From there, expand the STM32F3xx_HAL_Driver subfolder and then the Inc subfolder. This is where the headers for the Cube libraries live. All of those are included by your program, which means all the functions they declare are available for you to use. Notice how each file pertains to a type of STM32 peripheral.

One of them is of immediate interest to us : stm32f3xx_hal_tim.h.

This is the HAL (Hardware Abstraction Layer) header for the timers. HAL is a fancy word for “This is the future, I don’t want to have to deal with register addresses and fields”. Open this header.

Remember, we’re supposing you’ve never used an STM32 timer before. All you know is you’re looking for a function that will start PWM on TIM2. Scroll down the file and sure enough you’ll find this :

/** @addtogroup TIM_Exported_Functions_Group3 TIM PWM functions
  *  @brief   TIM PWM functions
  * @{
  */
/* Timer PWM functions ********************************************************/
HAL_StatusTypeDef HAL_TIM_PWM_Init(TIM_HandleTypeDef *htim);
HAL_StatusTypeDef HAL_TIM_PWM_DeInit(TIM_HandleTypeDef *htim);
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim);
void HAL_TIM_PWM_MspDeInit(TIM_HandleTypeDef *htim);
/* Blocking mode: Polling */
HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
HAL_StatusTypeDef HAL_TIM_PWM_Stop(TIM_HandleTypeDef *htim, uint32_t Channel);
/* Non-Blocking mode: Interrupt */
HAL_StatusTypeDef HAL_TIM_PWM_Start_IT(TIM_HandleTypeDef *htim, uint32_t Channel);
HAL_StatusTypeDef HAL_TIM_PWM_Stop_IT(TIM_HandleTypeDef *htim, uint32_t Channel);
/* Non-Blocking mode: DMA */
HAL_StatusTypeDef HAL_TIM_PWM_Start_DMA(TIM_HandleTypeDef *htim, uint32_t Channel, uint32_t *pData, uint16_t Length);
HAL_StatusTypeDef HAL_TIM_PWM_Stop_DMA(TIM_HandleTypeDef *htim, uint32_t Channel);

Bingo. Looks like function HAL_TIM_PWM_Start is our ticket !

Note that it takes two arguments :

  • A pointer to a TIM_HandleTypeDef variable. We’ve seen that before. That’s the type of the global “handle” variable Cube declared for TIM2, named htim2.
  • A channel number. Instinct would say “well that’s easy, channel 2 = 2”. Not so fast ! Instinct can lead you astray. Plus, clean coding means you never use literals for constants. There has to be an enumeration or some macros to specify the channel.

Scrolling down the stm32f3xx_hal_tim.h file, you may have come across them already :

#define TIM_CHANNEL_1                      0x00000000U 
#define TIM_CHANNEL_2                      0x00000004U 
#define TIM_CHANNEL_3                      0x00000008U 
#define TIM_CHANNEL_4                      0x0000000CU

The lesson here is never assume, always check. Cube saves you a massive amount of time, the least you can do is make sure you read the code it gives you. Or you’ll end-up wasting all the time it saved you.

Well, we have everything we need to start that PWM, now ! Go back to the main function and add the following line before the infinite loop :

  /* USER CODE BEGIN 2 */
  HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
  /* USER CODE END 2 */

This must have been the longest build-up to a single line of code in the history of ever. Maybe I win a prize. But it should make all the sense in the world now that you see it written : “Start PWM, on timer TIM2, for channel 2”. It may have taken a while to get there today… because it was the first time. But imagine how fast you’ll get once you realize that all of the Cube libraries follow the same logic ? Soon you won’t even need to look-up the name of handle variables. Guess what the handle variable for the SPI3 controller is named ? Yup, hspi3. Easy.

What you’ve learned here (if anything) applies to using any feature of any peripheral on any STM32 :

  • Configure the peripheral
  • Generate the code
  • Find out which Cube functions operate it
  • Find out which arguments those functions need
  • Call those functions
  • Enjoy your eons of time saved

9.   Compile, Flash and Run

At this point, everything’s ready to go. You just need to do the usual compile-flash-run.

First, plug your Nucleo module into your PC using a Micro-USB cable. Some automatic driver installation might occur if it’s your first time ever. Nothing to worry about. The green LED on your module will blink, but that’s just the demo the module comes pre-programmed with. We’re about to overwrite it with our own and, as the kids say, “end that program’s whole career”.

Somewhere in the IDE’s tool bars there is a green “Run” button :

Click it. You know you want to.

The first time you try to run a new project, this dialog box will appear :

Do not be intimidated : there’s nothing for you to change in there if you’re running a Nucleo module. Just press OK. The IDE will proceed to compile your project. Assuming that goes through without error, you may then come to a second hurdle :

Each Nucleo module features an integrated ST-LINK probe. Remember, it’s that second STM32 soldered underneath it. From time to time, ST updates their firmware. Again, no problem : just hit Yes to launch the firmware update :

Unless you’ve got multiple modules plugged in at the same time, there should only be one in the drop-list and that’s going to be the one on your Nucleo. Click “Open in update mode” and wait for it to do so, then click “Upgrade”. This takes only a few seconds.

Once that’s done, close the tool and hit the Run button again. You can see the flashing operation progress. At some point, the STM32 you’re flashing is going to be held in Reset, which you’ll notice because the green LED stops blinking. And then as soon as flashing is complete, it will start blinking again. Only this time it’s not the pre-programmed demo, it’s your own code.

10.    What’s Next ?

This has been a long tutorial and maybe I crammed too much into it, but trust me when I say we’ve only scratched the surface of STM32 programming.

The obvious next step is debugging. That’s also going to be a long one, because there are a lot of options beyond the default provided by CubeIDE.