First steps with STM32
In this introductory guide, you will learn how to set up an open toolchain to program any STM32 (and hopefully be able to switch more easily to other manufacturers). The motivation to use only open source tools is that they free you from the licensing of private tools, so your projects and workflow are guaranteed to be persistent over time. Also, we are not going to use any IDE (only Makefiles) so you can use whatever tool you like the most.
But first of all, please note that I am no expert. I am only sharing what I found after hours of digging the internet. With that said, let’s begin.
STM32 microcontrollers are ARM microcontrollers, that means that the CPU is designed by ARM, and the peripherals (ADCs, communication, …) and the manufacturing are done by ST. There are others ARM microcontrollers manufactured by other companies on the market (such as Atmel or NXP). All share the same CPUs, but their peripherals can vary widely.
The motivation to use these microcontrollers in particular is the availability of them, you can get a microcontroller with 2xSPI, 2xI2C, 3xUSART, 1xUSB 2.0, 3 16-bit timers, RTC, 7 channel DMA, ADCs and some more goodies for less than 1.5$ from eBay (although origin is questionable). Also, they are gaining terrain in the Open Source community partly because their documentation and open source libraries
In this guide we are going to use an STM32f103c8t6 minimal development board from eBay, along with the STLink programmer, both can be bought for as little as 2$ each.
I’m going to assume that you are on a Linux machine. If you are not, I’m sorry but you’ll need to look somewhere else.
For compiling, debugging and deploying our software, we will need the ARM GCC toolchain, the ARM GDB and OpenOCD along with stlink respectively.
For Arch based distros, type:
yay -S arm-none-eabi-binutils arm-none-eabi-gdb arm-none-eabi-gcc arm-none-eabi-newlib stlink openocd
All the packages are on the official repositories except for OpenOCD which needs to be downloaded from the AUR, but yay takes care of this.
sudo apt install gcc-arm-none-eabi gdb-arm-none-eabi binutils-arm-none-eabi openocd
and because stlink is not in the official repositories, we need to download it from github:
sudo apt install libusb-1.0-0-dev git clone https://github.com/texane/stlink cd stlink make release cd build/Release sudo make install
And that’s it, you should now be able to compile, debug and program any STM32 microcontroller. In this guide we are not going to use any IDE, as they obscure the compiling process and bury simple parameters in their inmense GUIs. I encourage you follow this guide, understand the compiling process and then, decide whether to use one or not. If you are going to try one, I suggest eclipse with the ARM GCC plugin or System Workbench for STM32 (SW4STM32) by Ac6.
Creating a project from nothing
Now that we have all the tools needed, we need to download the necessary files and libraries from ST in order to use their microcontrollers. In this guide we are going to use the files from STM32F1Cube as we are using an F1 device. If you want to skip this section, you can get the project template from here. If you decided to stay, download and extract STM32CubeF1. Once extracted, the folder structure is the following:
. ├── Documentation │ └── STM32CubeF1GettingStarted.pdf ├── Drivers │ ├── BSP │ ├── CMSIS │ └── STM32F1xx_HAL_Driver ├── _htmresc │ ├── CMSIS_Logo_Final.jpg │ ├── Eval_archi.bmp │ ├── logo.bmp │ ├── ReleaseNotes.html │ ├── st_logo.png │ ├── STM32Cube.bmp │ └── stmtouch.bmp ├── Middlewares │ ├── ST │ └── Third_Party ├── package.xml ├── Projects │ ├── STM3210C_EVAL │ ├── STM3210E_EVAL │ ├── STM32CubeProjectsList.html │ ├── STM32F103RB-Nucleo │ └── STM32VL-Discovery ├── Release_Notes.html └── Utilities ├── CPU ├── Fonts ├── Log ├── Media └── PC_Software
Inside the Documentation folder there is a PDF explaining how to get started with HAL (Hardware Abstraction Layer) and LL (Low Layer). Both the HAL and LL are a set of libraries provided by ST that act as an interface to STM32 configuration registers. HAL is the most used nowadays, but it is more bloated, needs to set the internal SysTick timer to 1ms in order to work and it has some bugs. On the other hand, LL is a newer library and closer to the metal, but it doesn’t have a lot of documentation yet. I should mention there are other options, such as libopencm3, that are free, open-source and independent of any corporation.
In the Drivers folder, we’ll focus on the CMSIS and STM32F1xx_HAL_Driver folders. Inside the CMSIS folder, you will find all the files related to CMSIS (a vendor independent hardware abstraction layer for Cortex-M processors by ARM). Also, we need to pick the system_stm32f1xx.h and the header for your MCU model, in this case stm32f103xb.h from Devices/ST/STM32F1xx/Source. In addition, we need to grab the right startup assembly code from Devices/ST/STM32F1xx/Source/Templates/gcc and the linker script from Devices/ST/STM32F1xx/Include/Templates/gcc/linker. In our case, the files are startup_stm32f103xb.s and STM32F103XB_FLASH.ld respectively. The linker script tells gcc where to map the sections from the input files to the output files, and control the memory layout of the output file. Next, we will need to grab the source and include files from Drivers/STM32F1xx_HAL_Driver.
You can rearrange the files in your project as you wish, we are going to arrange it in the following way:
. ├── bin ├── cmsis │ ├── device │ ├── include │ └── linker ├── compile_commands.json ├── hal │ ├── include │ └── src ├── include │ ├── main.h │ ├── stm32f1xx_hal_conf.h │ └── stm32f1xx_it.h ├── Makefile ├── obj │ ├── hal.a │ ├── main.lst │ ├── stm32f1xx_hal_hcd.lst │ ├── stm32f1xx_hal_msp.lst │ ├── stm32f1xx_it.lst │ └── system_stm32f1xx.lst ├── README.md └── src ├── main.c ├── startup_stm32f103xb.s ├── stm32f1xx_hal_msp.c ├── stm32f1xx_it.c └── system_stm32f1xx.c
If you are not following the previous structure, be aware that you will need to change your Makefile accordingly. Note that inside the Drivers folder there is no stm32f1xx_it.h to be found. This file includes some definitions for handlers, including the SysTick timer handler. Without this include file and its corresponding source file your project is likely to compile, but will not work. You can grab the source file here and the include file here
Now let’s dive into the makefile.
We need to tell the compiler which MCU we are going to use:
CPU = cortex-m3 MCU = STM32F103xB
Next, we define the folders of our project:
SRCDIR = src INCDIR = include BINDIR = bin OBJDIR = obj CMSIS_DIR = cmsis HALDIR = hal CMSIS_DEV_SRC = $(CMSIS_DIR)/device/src CMSIS_DEV_INC = $(CMSIS_DIR)/device/include CMSIS_INC = $(CMSIS_DIR)/include HAL_DIR = hal HAL_SRC = $(HAL_DIR)/src HAL_INC = $(HAL_DIR)/include LDDIR = $(CMSIS_DIR)/linker LDSCRIPT = $(LDDIR)/STM32F103XB_FLASH.ld
We are going to save all our .c files under SRCDIR, and all our header files under the INCDIR folder. The compiler output goes into the OBJDIR folder and the linked results are output to BINDIR. We also define the CMSIS and HAL folders, which contain the vendor provided headers and implementations. LDDIR and LDSCRIPT provide the correct linker script. The linker script tells the linker how to reallocate the data in the object files and map it to the MCU. It is important to select the correct linker script as memory sections change from MCU to MCU. Fortunately, STM32 provides us with those scripts. But if you wanted to do more advanced memory management, such as code overlays, you will need to modify them.
Now that we have our folders defined, we need to fetch all the files we want to compile. The method is the same for all folders (user src and include, HAL and CMSIS). To do this, we can manually write every file that is present in every folder, or we can use wildcard to use a regular expression to fetch them all, like shown here:
HAL_OBJ_SRC = $(wildcard $(HAL_SRC)/*.c) HAL_LIB_OBJS = $(HAL_OBJ_SRC:.c=.o) HAL_LOCAL_LIB_OBJS = $(notdir $(HAL_LIB_OBJS))
Here, we define HAL_OBJ_SRC as all the files that end in .c in the folder hal/src. We need to do this to later tell the compiler which objects to make. Then we define HAL_LIB_OBJS as if we changed the name of all .c ending files inside hal/src with .o, but this includes the folder, so we get rid of it with notdir.
After we do the same with all the project source folders, we create the include flag for all our include folders:
INC = -I$(CMSIS_INC) -I$(HAL_INC) -I$(CMSIS_DEV_INC) -I$(INCDIR)
Next, we define the required compiler flags:
CFLAGS = -std=c99 -Wall -fno-common -mthumb -mcpu=$(CPU) -DSTM32F103xB --specs=nosys.specs -g -Wa,-ahlms=$(addprefix $(OBJDIR)/,$(notdir $(<:.c=.lst))) CFLAGS += $(INC) ASFLAGS = -mcpu=$(CPU) LFLAGS = -T$(LDSCRIPT) -mthumb -mcpu=$(CPU) --specs=nano.specs --specs=nosys.specs -Wl,--gc-sections
Note that we tell it to compile in C99 standard, with all warnings active. -mthumb tells the instruction set (ARM MCU can use either Thumb or ARM) and -mcpu defines which CPU is our target (f.e. cortex-m0, cortex-m3, …). -DSTM32F103xB is necessary if we not define it ourselves in our source files, this is necessary for the HAL to load the correct headers for our microcontroller. TODO: el resto de argumentos The same goes for the linker script.
Now, we need to tell make how to build our files, and which targets depend on which.
The first target that appears in the Makefile is the one make is going to build if you invoke make without specifying a target. Since we want to build everything by default, we put target all at the beginning:
all:: $(BINDIR)/$(PROJECT).bin $(BINDIR)/$(PROJECT).hex
From now, to follow the compiler flow, we begin from the bottom target.
$(OBJDIR)/%.o: $(SRCDIR)/%.c @echo "Compiling c source files in src" @mkdir -p $(dir $@) $(CC) -c -o $@ $< $(CFLAGS) $(OBJDIR)/%.o: $(SRCDIR)/%.s @echo "Compiling asm files in src" @mkdir -p $(dir $@) $(AS) $(ASFLAGS) -o $@ $<
In this section, we build our our project object files from our source. Basically what we are telling make is “for every .c or .s file, the is a file .o with the same name that depends on its source file and here is how to build it”. Note that $@ means what’s before the colon, $< refers to the first element after the colon, and $^ refers to everything after the colon.
Now we have our code compiled and correctly stored inside the obj folder, but we also have to build the vendor’s code.
$(OBJDIR)/hal.a: $(HAL_OBJ_SRC) @echo "Creating core lib (hal.a)" @mkdir -p $(dir $@) $(CC) $(HAL_OBJ_SRC) $(CFLAGS) -c $(AR) rcs $@ $(HAL_LOCAL_LIB_OBJS) @rm -f $(HAL_LOCAL_LIB_OBJS)
This is the same procedure as before, but with one addition. Because we are only going to build the library object files once (or on counted occasions), we create a .a file that contains in itself all the object files, so that in the end we link our application only to this file.
Now we have everything to build our project:
$(BINDIR)/$(PROJECT).elf: $(LIB_OBJS) $(OBJDIR)/hal.a @echo "Creating $(PROJECT).elf" @mkdir -p $(dir $@) $(CC) $^ $(LFLAGS) -o $(BINDIR)/$(PROJECT).elf $(OBJDUMP) -D $(BINDIR)/$(PROJECT).elf > $(BINDIR)/$(PROJECT).lst $(SIZE) $(BINDIR)/$(PROJECT).elf
arm-none-eabi-gcc now acts as the linker, and generates the .elf file. Next, we create a list file using objdump, which displays the assembler content of all sections of the just compiled file. Also, with arm-none-eabi-size, size information of all sections of memory is displayed.
For the last step, we need to get the file that is going to be flashed to the STM32, to do that:
$(BINDIR)/$(PROJECT).bin: $(BINDIR)/$(PROJECT).elf $(OBJCOPY) -R .stack --strip-unneeded -O binary $< $@
With –strip-unneeded, we are preventing from creating a binary image with unused code.
If everything went well, now when you write some code and invoke make, it should compile fine.
Now, to flash it, you need to install stlink (available from the official Arch repos), and write the following command:
st-flash write bin/your_bin_file.bin 0x08000000
Note that we need not to specify which port the USB is connected at, st-flash will handle that for us. The 0x08000000 tells the programmer to start writing at that direction of memory, since if you look up the Reference Manual, program memory starts there.
You could make a make target that when invoked will automatically run this program.