The Ultimate Guide to STM32

STM32 is probably the most popular 32-bit microcontroller out there. And I’d say that it’s definetly a good first step into 32-bit processors. It does, however, have quite a jump in complexity if you’re coming from something like PIC or AVR. Just take a look at the datasheet, it’s 900 pages long. That’s why I’ve made this video. I want to help you make the jump from 8-bit to 32-bit. So without further ado, let’s dive in!

Just a quick note before this video starts, this isn’t a video made for complete beginners. I’ll assume that you have at least a basic understanding of C code, pointers, memory addresses, and microcontrollers in general.

Selecting a Microcontroller

Before we can start programming an stm32, we first have to pick one. There are several boards that you can find on places like amazon or ebay. For example, you could pick one of these blue pill boards. They come with all of the required circuitry and all you need to do is program them. You could also decide to make a board of your own for testing, which is what I did. This is a bit more challenging however, so weigh up your options.

The specific microcontroller that I picked was the STM32L011F4U. It’s a Cortex-M0+, which makes it suitable for low-power applications. It also comes with 16kB of flash and 2kB of SRAM. You can also clock the CPU all the way up to 32MHz. And, of course, it comes with a selection of peripherals, including a 12-bit ADC, a USART, an SPI, an I2C, and 7 timers. In all, it’s nothing too crazy, and everything should be at least somewhat familiar considering that you have previous microcontroller experience.

Here is the schematic that I ended up using for my testing board. Your RESET pin should be pulled-up to prevent unintentional resets. Likewise, your BOOT0 pin should be pulled down to ground to boot from FLASH. You can optionally add a crystal if you need one. And that’s it for the bare minimum circuitry, but we need a bit more to make it useful. I went ahead and added an FTDI chip to the circuit. This allows us to have a serial interface with the STM32 over USB. This can make debugging easier. I also added a 1.8V regulator to power the STM32. For this specific chip, you can choose any power voltage from 1.65V to 3.6V. And just one more thing, I added a programming header.

This header aligns with the official STM32 ST-LINK, which will communicate over SWD. Speaking of programmers, aside from the official programmer, you can get some cheap ST LINK clones. They usually come in this smaller package. Use one of these if you’d like, but I’ll stick with the official programmer for ease of use. And in case you were curious, I used a stencil and some solder paste to apply solder. I then placed the components on top and activated the solder by using a hotplate. Anyways, we are now ready to starting programming this thing.

Necessary Software

The official programming method is to simply use the STM32CubeIDE along with HAL. However for this video, I’d like to explore a more fundamental approach. This should help you to understand the STM32 at a much deeper level. You can download the GNU ARM toolchain on this website. The link will be in the description. Alternatively, if you are using Linux, you may download it using your package manager. On Arch Linux, it’s called arm-none-eabi-gcc. Go ahead and get the libnew and gdb packages as well.

For Windows, go to this website where you can download the arm gnu package. Simply run the installer and you should be good to go. You may also opt to download a program that can run makefiles, but this is kind of annoying and you can consider it optional.

Now you need a programmer to send our compiled code over to the ST-LINK. You could use something like OpenOCD for this purpose. For the programmer though, I went with the official STM32Programmer tool. The link for this will also be in the description. With this software prepared, we can start on making blink.

Creating Blink (The Linker File)

Now, you may expect to just jump into your new main.c file and write to a GPIO register to blink an LED as you might with an AVR microcontroller. However, there is actually a lot more setup you need to complete first. Let’s start with a linker file. A linker file’s purpose is to layout where the compiled data will go into the memory of the microcontroller. AVRs have these as well, but they are premade for you. We are going to have to make one for ourselves.

Go to the reference manual for your microcontroller. Since I’m using the STM32L011, I’ll go to the STM32L0x1 reference manual. Included is a memory map. This shows us which addresses correspond to the RAM, the FLASH and the peripherals. For example, the FLASH memory starts at address 0x08000000 and the SRAM starts at 0x20000000. Let’s put this into a new file. I’m calling my linker file linker.ld. Start by declaring the memory locations that we just found inside of a MEMORY header. The ORIGIN keyword is the starting address that we just found and the LENGTH is how large the FLASH and RAM are. The parenthesis around the letters represent the permissions of this memory. r means read, x means execute, and w means write. So this means that we can read and execute from FLASH, but not write to it while the microcontroller is in normal operation.

The next line you should add is this ENTRY line. This tells us the name of the function that our program will start with. You can choose any name you’d like since you are going to create this function. I’d encourage you to name it reset though, since there are some setup code that needs to run before your main program starts. We will write this reset function later. Now we get to the hard part of the linker file, the SECTIONs header.

Basically, your code is divided into a few different sections. isr_vector, text, rodata, data, and bss. The isr_vector section is our interrupt table. This is a list of memory addresses that point to functions. Each entry in the list corresponds to a specific interrupt. We create this subheader to represent our isr_vector. Inside we specifiy that all entries with the .isr_vector should be included in this section. We end the subheader with a placement in FLASH. It’s important to place .isr_vector first since it should be the first data in FLASH.

The next section is the .text section. This is where your executable code will be stored. We use the asterisk in a second entry to ensure that all sections with .text in the name are included. Again, place this in FLASH since we won’t change this during runtime. The next section is .rodata. It’s basically all of your read-only variables that you include. Place this in FLASH.

The next section is a bit more interesting. The .data section is all of your initialized data that will be changed during runtime. For example, writing int x = 5 will store a value of 5 so that our code can use the inital value later. Basically, we need to save this data in the FLASH, but place it into RAM when we are initializing the variable. We can do this with this line. This tells the program that the data will be found in RAM during runtime, but we have to load it in from FLASH first. You may have also noticed the other lines like the _DATA_RAM_START line. These function as variables that we can use in our C code. The period in this case represents the linker’s current memory location. So, we can use these to tell our C code where the data starts and ends inside of the RAM. We can also use LOADADDR to tell the C code where the data is saved in FLASH so that we can transfer it to RAM. We will write code for this in a bit.

And finally, we have the .bss section. This section is simply variables that should be initialized to zero. We will use these _BSS_START and _BSS_END variables to zero out the variables later on. Place this section in RAM. Now that our linker file is completed, we can start writing some setup code.

Creating Blink (Setup Code)

We now have to write some code that will setup the sections that we just created in our linker file. First, make a new c file and create some useful definitions. The TABLE_SIZE definition is how many interrupt entries are in the interrupt vector table. The reference manual has a list of interrupt and exeception vectors. Please note that some of the blank spaces in the exceptions take up multiple spots. An easy way to find the number of spaces that you need is to look at the address column. Look at the address of the last entry. Divide this number by four and add one to get the total number of entries that you will need to account for. For the STM32L011, that’s 46 entries in decimal. The next definition is RAM_START. We’ve already found this earlier. Finally, we have RAM_SIZE. Since our microcontroller has 2kB of RAM, I put that number in as the number of bytes.

We can finally start writing our reset function that we mentioned earlier in the linker script. We have to do a few things: first, we need to copy the .data section from FLASH to RAM. Next, we need to zero out the .bss section in the RAM, and finally we enter our main function to get the program started. To do this, create our void reset function. Also grab the linker variables that we made earlier. Use the extern keyword to let the compiler know that the data is found elsewhere. Now we can copy over the .data section.

I find it easiest at this point to convert the memory addresses given by the linker and turn them into pointer variables. From here, we can simply use a for loop to copy the data over. We use a very similar process on the bss section but instead of copying data, we just set it to zero. We can finally enter main. I’d recommend that you add an infinite while loop in the case that you somehow exit your main function as a safety measure. We aren’t done with this file yet though, we still need to setup our interrupt table.

Create a default_handler method. I don’t imagine that you would have every interrupt planned out, so we can use this handler in the case that you somehow accidentally call an unimplemented interrupt. Next, create a list of every interrupt. The names of all of these can be found on the interrupt table. Place an attribute with a weak alias to default handler. This essentially means that if you don’t implement a specific handler, it will act as the default handler, but if you later on write that specific handler, it will act as that handler instead. It’s just an easy way to keep things organized. I’d also recommend placing them in a header file so that you overwrite them in your main.c file.

Now you just need to organize your vector table. You should create an array for this purpose. Include the stdint.h header so that we can access the uint32_t keyword. uint32_t is another way of saying an unsigned 32-bit integer. Also give your array the section(".isr_vector") attribute. This will let the linker know that this is the vector table. Anyways, the first entry actually isn’t an interrupt vector, but rather the initial stack pointer. This should point to the very top of the RAM, so you can simply add the RAM_START and RAM_SIZE definitions together to get the proper value. After this, you can follow the reference manual’s guide to get the right order. Pay attention to the reserved spaces, some of them take up several spots. You can go to the programming manual, which is another document, to get the right number of reserved spaces. And when you do need to fill in a reserved spot, just place a zero. And with that complete, we can finally get to our main function.

Creating Blink (main.c)

Let’s start writing our main function. We need to setup our output pin and then toggle it in an infinite loop. We would do that by manipulating the GPIO registers, but we don’t have them defined yet. At this point I created an stm32.h header file along with an stm32l011f4.h header. In the header I’ve added this macro: _REG_PTR32. This will make it easier to modify the registers in our code later on. I’m basically aiming for a code style similar to the way that I write for AVRs. I’ve also added a 16bit version as well.

Now we can start sorting out where exactly the GPIO register is located in memory. If we take a look at the memory map, we can see the peripherals section and it is divided up into the AHB, APB1, APB2, and IOPORT. I recommend that you define each of these at the top of your header. I also recommend that you define CORTEX_M0PLUS_PERIPHERALS since it will be useful for setting up interrupts later on. Now, at this point you may be expecting to jump straight into the I/O port registers, but there is just one more thing that we need to do. Every peripheral has to have their clock enabled for proper operation.

To activate the clocks of other peripherals, you need to take a look at the RCC peripheral. It contains clock settings for the microcontroller in general and also the individual peripherals. To access the RCC registers, you first need to find its base address. You can find it on this peripheral register boundary addresses table. As you can see, the RCC is located at hex 1000 from the start of the AHB. Now we can go to the RCC register descriptions. You’ll find that each of the registers has this offset value. We can use it in the same way that we did for the RCC to easily assign addresses. For example, the RCC clock control register is at offset zero from the RCC, so we can just add zero to it. At this point, since we are dealing with the actual registers, use that _REG_PTR32 macro that we made earlier. This will allow you to access the register in our code. I also recommend going through and making a definition for all of the bits in that register. For example PLLRDY is bit 25, so I defined it as 25.

Do this for a few more registers. I made one for the configuration registers at offset 0C, the GPIO clock enable register at offset 2C, the APB1 peripheral clock enable register at offset 38 and the APB2 peripheral clock enable register at offset 34. Again defining the pins used in those registers. It’s worth noting that I did not make a definition for every RCC register since I won’t need many of them. But if you do need the others, just follow the pattern and make a new definition. Anyway, you may have noticed that I included the GPIO clock enable register in that list, and it’s the register that we will use to enable the clock to our GPIO.

I’m planning on running blink on PA1, so we will have to enable the clock for GPIOA. Now we can finally get around to manipulating the GPIO register. If we go back to the boundary addresses, we will see that GPIOA is placed at offset zero from IOPORT. At this point, I recommend going through and defining all of the registers with their offsets. The ones that you should focus on on are MODER, ODR, AFRL, and AFRH. Keep in mind that the x means that it goes for the other GPIO peripherals, including B and C.

For the MODER pins, I recommend defining them in this fashion, with the pin number followed by either one or zero. This is because each GPIO pin has two MODE bits in the register and defining them in this way allows you to keep the pairs organized. With this, let’s set PA1 for a general purpose output. The datasheet tells us that setting the bit pair to 01 will put the pin in output mode. We can do this with our bitwise operators. Now that the pin is an output, we can finally get to blinking it. Inside of the while loop, toggle the pin by using the XOR operator on the ODR register. For the delay, simply use a very long for loop.

Compiling and Programming

Like we setup earlier, you should have your ARM C compilier. I recommend that you have a way to run the Makefile I have provided, since it will make compilation easier. Let’s review the makefile so that you are able to understand what it will do. The first line is the target, we are going to output a final elf file named out.elf. The SRC_FILES are simply the names of the files that we need to compile. Fill in your CPU and MCU lines. For me, I am using the cortex-m0plus and the STM32L011F4. Please note the MCU line is custom to my setup and it isn’t needed by the compiler itself. CC is your c-complier. Simply put the name of the ARM complier that we installed earlier, arm-none-eabi-gcc. The CFLAGS are options that we use in the compliation process. We want all errors and warnings reported along with debugging capability. The OBJECTS are simply the src files but we replace the .c with a .o. The next section differentiates between Window’s and Linux rm or del for deleting a file.

The next couple of lines represent linking our object files together into the final out.elf file. The entry after this is how we created those object files, where the percent sign represents every c file and every object file. If you don’t want to mess with the Makefile, feel free to run the command on its own like so.

At this point, we can now turn our attention to the programmer. I will be using the official stm programming tool to make this a bit easier. Make sure that you select ST-LINK as your programmer. Also set your mode to under reset and set your reset mode to hardware reset. This should make it easier to program everything otherwise you may have to manually reset the microcontroller yourself. Afterwards click on this button that has the arrow on it. From here, browse and find that elf file that we created when compiling. Then click the start programming button. At this point, if everything was setup correctly, the programming should have been successful.

At this point we can see our LED blinking. I’d just like to say congratulations if you’ve made it this far. The setup to get to this point was not easy. At this point, we are now free to experiment with the microcontroller as normal.

Changing The System Clock

When using this microcontroller, you have four different clocking options: the HSI16 which is an internal high speed oscillator clock, the HSE which is an external high speed oscillator clock, the PLL, and the MSI which is a multispeed internal oscillator clock. The MSI at 2.1MHz is used as the default clock at startup. For the remainder of this video, I want to switch to the HSI16 internal clock. To do this, let’s make a new function called init_system_clock. Also make sure to define the RCC control register and the RCC clock configuration register along with the bits that correspond to these registers.

The first thing that we will do is start the HSI clock with the HSI16ON bit. Afterwards, wait on the HSI16RDYF bit to become a one. Once it does, the HSI16 clock is now ready to use. Switch the system clock by using the clock configuration register and the SW bits. After selecting your clock source, make sure to check for its validity in another while loop. You can determine that the switch was successful if your LED starts blinking at a faster rate.

Luckily for our case, this clock change was actually pretty easy. However, depending on your setup and desired frequency you may run into other problems. Table 43 shows us the maximum frequency that you can run your clock at depending on your voltage. Luckily, the HSI16 works in range 1, which is the 1.8V range. You will also have to take a look at your flash latency. There are instructions in the datasheet if you need them, but since the default works for us I’ll just leave it at that.

Timers

As ususal when testing a new microcontroller, we need to take a look at the timers. We will be looking at TIM2 specifically. It’s a 16 bit timer. The features are basically what you’d expect from a microcontroller’s timer, including PWM and input capture. For the next test, we are going to setup a PWM output that requires little CPU involvement aside from the inital setup. To start, we are going to have to enable Timer 2’s clock in the RCC, much like we did with the GPIO clock from earlier.

We are also going to have to configure the output GPIO pin to one of its alternate functions. We will use PA0 for the output this time around. The first step is setting the alternate function mode in the MODER register. Next, we need to select the correct alternate function in the AFRL register. Annoyingly, the table for alternate functions is in the overview datasheet and not the reference manual. Either way, we need to set PA0 to alternate function 2 to get it connected to TIM2_CH1. With the GPIO out of the way, we can focus on the timer itself.

The first thing you should do is determine how much of a prescale you want. I selected zero to skip any prescaling. Next, select a value for the ARR register. ARR stands for auto-reload register and its basically the maximum value you want the timer to count to. I set it to 255. The prescaler, your input clock, and the ARR value all determine the output frequency. Next, change the value of CCR1 to select your duty cycle. It should be a value between 0 and your ARR value. The next register your need is the CCMR1 register. We are interested in the OC1M bits, since they determine what PWM mode we will use. Set them to 110 to get PWM mode 1. Enable the output on OC1 with the CC1E bit in the CCER register. We are almost done, simply set the CEN bit in CR1 to enable the timer and set the UG bit in the EGR register to force all of your settings to update properly.

We can see the PWM output in our oscilloscope. And it is working as expected. We can change the duty cycle by adjusting the CCR1 value in the code. Here’s 25%, 50%, and 75% duty cycles.

USART

Why don’t we put that FTDI chip to use? The USART will allow us to serially communicate with a PC through a USB port. To start, let’s take a look at the USART section of the datasheet. You should see that this peripheral comes with a lot of features and configuration. For our case, we will use a rather standard configuration of 8 bits of data, 1 start bit, 9600 baud, 1 stop bit and no parity bit. These settings should make it simple to set everything up properly.

Starting on the code, we first need to enable the USART’s clock in the RCC. We will be using USART2 for this project. We also need to set alternate pin functions to expose our RX and TX pins. PA9 is the TX pin and PA10 is the RX pin. They are both alternate function number four. Now we can get to the actual USART configuration. As usual, go ahead and define the USARTs registers and their bits.

Let’s start with setting up our baud rate. Remember, we are aiming for a standard baud rate of 9600. We need to put our special baud number into the USART_BRR register, but unfortunately, we can’t just put 9600 into it. Instead, the datasheet has an equation that we need to follow to find the proper value for this register. Remember from earlier how we switched to the HSI16 clock source, so we will use 16MHz as our clock. The USARTDIV value is simply our desired baud rate. Depending on your oversampling, you may need to multiply by 2, but we will use oversampling by 16. That gives us a USART_BRR value of 1667 decimal.

There are a few more settings that you may wish to change depending on your requirements, such as the word length in the CR1 register. If you’re reading this for the first time, you may become overwhelmed by the sheer number of options you will come across the in the register descriptions. Luckily, most of them are for other purposes such as SmartCard mode. Since we are using a simple asyncronous serial communcation configuration, you can ignore most of these settings. The only thing left for us to do is to enable the transmitter, the receiver, and the USART itself.

Now that we’ve initialized the USART, we need a way to receive and transmit data to and from the peripheral. Make two new functions called send_usart2 and recv_usart2. Sending with the send function, we simply need to fill in the transmit data register with the data we’d like to send. But before we do that, we first need to check the TC bit in the status register to make sure that the previous data has already been sent so that we don’t accidentally overrwrite the previous data. For the receive function, we need to wait for the RXNE bit to make sure that there is actually data to receive. Once it is received, we can simply return it.

Now we can test the USART in our main function by waiting for a character to be received and then sending it back. On my PC, I booted up a program called minicom which allows me to serially communicate. As you can see, when I type a character it is printed on the screen. The USART works as expected.

Interrupts

Now that we’ve proved that we can actually use the microcontroller by setting up the other peripherals, we can finally return back to the interrupts. Let’s focus specifically on the USART2 interrupts. It’d be nice to automatically echo back any sent characters without polling the USART on a loop. You may think that we simply have to overrwrite the usart2_handler, but it goes a bit deeper than that.

To enable a specific interrupt handler, you have to enable it in the NVIC. Unfortunately, the important information we need about the NVIC isn’t in the reference manual but rather the programming manual. This is the part of the tutorial where your specific STM32 microcontroller may differ greatly from what I am doing with mine. Read everything carefully and make sure that it lines up with yours. The NVIC isn’t stored in the normal peripheral address space, but rather the cortex m0+ peripheral address space with an offset of hex E100. There are 12 registers that are involved with the NVIC. The addresses for each can be found in the programming manual once again. Pay attention that NVIC_IPR has eight registers.

Out of these 12 registers, the one that we are most interested is the ISER register, since it enables a specific interrupt. Each bit in this register enables its corresponding interrupt. Go back to the NVIC section in the reference manual to find the position value. For my microcontroller, the USART2 interrupt is at position 28. I’d recommend that you convert these positions to definitions so that you may enable any interrupts that you come across. Finally, enable the interrupt by setting the bit in the NVIC_ISER.

Now that we have general USART interrupts enabled, we have to configure the USART itself to interrupt. I set the receive interrupt bit in the CR1 register. Now, the microcontroller should jump into the usart2_handler whenever data is received. Let’s now write the code for the usart2_handler. I’d recommend that you check for the RXNE or the ORE bit in the status register. These are to insure that we have the correct interrupt and that there is actually data for us to read. Afterwards, receive and manipulate the data like normal.

At this point, the microcontroller can now blink normally in the main loop while still being able to interrupt and echo back any characters I enter through the serial interface. Just as a final word and reminder about interrupts: this NVIC system I showed in today’s video is the least compatibile with other stm32 microcontrollers with different M types. Please be careful when implementing one of your own.

Review

Overall, I’d say that the stm32 platform is extremely powerful and cost effective. New projects will likely benefit from using an stm32 over something like an AVR. You get a lot more performance from these more modern microcontrollers. Unfortunately, they are a great deal more complicated. One gripe that I had with the process was having to reference three different datasheets just to get all of the information that I needed. Otherwise, I’d say that you should give the stm32 a shot. I especially like the open-source gnu compiler that I was able to use for this project.

I also took a very low-level approach when making this video. You may decide to use a library like HAL along with the CubeIDE to simplify the process.

Once again, I will host links to all of the datasheets that I’ve used and I will put them in the description. I’ll also give you the schematic for the development board. My code will be posted in the description.

That should cover it for STM32 microcontrollers. Hopefully, you’ve learned something new. If you did, please consider subscribing so that you can see my future videos. Also, visit my buymeacoffee page. With your support, I can keep making these videos. I’d like to thank Mr. devNull, Cognisent and Mark for being channel members. Have a good one!

 Share!