How to use STM32 CubeMX without losing your mind
Posted on May 25, 2021CubeMX is kinda nice, but it also sucks donkey ass… But it’s kinda nice! But it really sucks absolute donkey ass!
It’s kinda nice because it lets you configure your stuff easily with a GUI and then generates some questionable code that actually configures your stuff (most of the time). It sucks donkey ass because of the way they chose to do the code generation thing. You’re reading this so I’m guessing you know. I know. It’s really dumb. Literally any other approach would have most likely sucked less.
Anyway, the way to use CubeMX is the way you deal with anything messy: keep it well contained, and far away from anything you appreciate. This translates to the following project structure.
very_cool_project
├── very_cool_project_mxproject
│ ├── Core
│ ├── Drivers
│ ├── very_cool_project_mxproject.ioc
│ ├── Makefile
│ ├── startup_stm32g030xx.s
│ └── STM32G030C8Tx_FLASH.ld
├── Makefile
├── mxproject.mk
└── [other stuff...]
I just put the entire CubeMX project inside a directory on my project. I do not touch anything in there, that way CubeMX cannot delete my changes. The key to make this work is to do a few things when setting up the CubeMX project:
- Make sure you do not let it generate the main() function
- Make sure you set the option to generate pairs of .c/.h files instead of just one big file.
- There’s also an option to only copy necessary files that you should check.
- And finally, make sure the project kind is Makefile.
If you do this, it kinda gets out of your way to let you do whatever you want. It’ll just vomit some C files and that’s it.
Now you just need to build the aforementioned vomit, which is what the mxproject.mk file does. It builds only the MX project objects.
HAL_SRC += $(wildcard $(MXPROJECT)/Drivers/*_HAL_Driver/Src/*.c)
HAL_SRC += $(shell find $(MXPROJECT)/Core/Src -type f -iname '*.c' ! -iname '*_it.c')
HAL_OBJ = $(HAL_SRC:%.c=$(OBJDIR)/%.o)
HAL_INCLUDES += -I$(MXPROJECT)/Core/Inc
HAL_INCLUDES += -I$(wildcard $(MXPROJECT)/Drivers/CMSIS/Device/ST/*/Include)
HAL_INCLUDES += -I$(wildcard $(MXPROJECT)/Drivers/CMSIS/Include)
HAL_INCLUDES += -I$(wildcard $(MXPROJECT)/Drivers/*_HAL_Driver/Inc)
STARTUP_SRC = $(wildcard $(MXPROJECT)/startup_*.s)
STARTUP_OBJ = $(STARTUP_SRC:%.s=$(OBJDIR)/%.o)
CMSIS_CFLAGS = $(ARCHFLAGS)
CMSIS_CFLAGS += -std=c99 -Os -ggdb3 -c
CMSIS_CFLAGS += -D$(DEVICE) -DUSE_FULL_LL_DRIVER -DUSE_HAL_DRIVER
CMSIS_CFLAGS += $(HAL_INCLUDES)
$(HAL_OBJ): $(OBJDIR)/%.o: %.c
mkdir -p $(@D)
$(CC) $< $(CMSIS_CFLAGS) -c -o $@
$(STARTUP_OBJ): $(STARTUP_SRC)
mkdir -p $(@D)
$(CC) $< $(CMSIS_CFLAGS) -c -o $@
(Feel free to message me about using wildcard and find to find source files here.)
Hopefully you’re familiar with CubeMX to roughly figure out what the above does, and how to extend it. In a nutshell, it just compiles the thing and puts it in $OBJDIR.
The more astute of you might have realised that there are some variables in
that script that are not defined anywhere else. Indeed, those come from the
Makefile
:
CC = arm-none-eabi-gcc
LD = arm-none-eabi-gcc
AR = arm-none-eabi-ar
OBJCOPY = arm-none-eabi-objcopy
SIZE = arm-none-eabi-size
MXPROJECT = very_cool_project_mxproject
FAMILY = STM32G0xx
DEVICE = STM32G030xx
LDSCRIPT = $(MXPROJECT)/STM32G030C8Tx_FLASH.ld
ARCHFLAGS = -mcpu=cortex-m0plus -mthumb
OBJDIR = obj
BINDIR = bin
OPT ?= -Os
CSTD ?= -std=c11 -U__STRICT_ANSI__
CFILES += $(shell find src -name '*.c')
CFILES += $(shell find rezit/src -name '*.c')
INCLUDES = -I.
# Third party includes shouldn't generate warnings:
INCLUDES += -isystem mxproject/Drivers/CMSIS/Include
INCLUDES += -isystem $(wildcard mxproject/Drivers/*_HAL_Driver/Inc)
INCLUDES += -isystem $(wildcard mxproject/Drivers/CMSIS/Device/ST/*/Include)
INCLUDES += -isystem mxproject/Core/Inc
CFLAGS += $(OPT) $(CSTD) -ggdb3
CFLAGS += $(ARCHFLAGS)
CFLAGS += -Wall # look at all these -W's, might as well use Rust, huh
CFLAGS += -Wextra -Wshadow -Wunused-variable -Wimplicit-function-declaration
CFLAGS += -Wredundant-decls -Wstrict-prototypes -Wmissing-prototypes
CFLAGS += -Wwrite-strings -Wundef -Wconversion -Wsign-conversion
CFLAGS += -Wformat-security -Wformat
CFLAGS += -pipe
CFLAGS += -fno-common -ffunction-sections -fdata-sections
CFLAGS += -fno-builtin -ffreestanding
CFLAGS += $(INCLUDES)
CFLAGS += -D$(DEVICE) -DUSE_FULL_LL_DRIVER -DUSE_HAL_DRIVER
LDFLAGS += $(ARCHFLAGS)
LDFLAGS += -T$(LDSCRIPT)
LDFLAGS += -specs=nano.specs
LDFLAGS += -specs=nosys.specs
LDFLAGS += -Wl,--gc-sections
LDFLAGS += -Wl,--print-memory-usage
LDFLAGS += -lc -lm
default: $(BINDIR)/main.bin
clean:
rm -rf $(OBJS)
distclean:
rm -rf $(OBJDIR)
include mxproject.mk
OBJS = $(CFILES:%.c=$(OBJDIR)/%.o)
$(OBJS): $(OBJDIR)/%.o : %.c
mkdir -p $(@D)
$(CC) $< $(CFLAGS) -c -o $@
infer run --force-integration gcc -- $(CC) $< $(CFLAGS) -c -o $@
$(BINDIR)/main.elf: $(HAL_OBJ) $(STARTUP_OBJ) $(OBJS)
mkdir -p $(@D)
$(LD) $^ $(LDFLAGS) -o $@
$(BINDIR)/main.bin: $(BINDIR)/main.elf
$(OBJCOPY) -O binary $(BINDIR)/main.elf $(BINDIR)/main.bin
.PHONY: default clean flash
And that should about do it. We can start vomiting our own C files now! I
generally have some sort of mx.c/h
files where I do all the stuff that CubeMX
would have done in its own main function, as well as calls to some helpers that
I wrote myself, something like this.
void mx_init(void)
{
SystemClock_Config();
HAL_Init();
MX_GPIO_Init();
MX_FMC_Init();
initialise_sdram();
MX_USART1_UART_Init();
MX_USART2_UART_Init();
MX_SPI1_Init();
MX_SPI2_Init();
MX_SPI6_Init();
MX_I2S3_Init();
MX_DAC1_Init();
LL_DAC_Enable(DAC1, LL_DAC_CHANNEL_1);
LL_DAC_Enable(DAC1, LL_DAC_CHANNEL_2);
octospi_init();
tim2_init();
tim4_init();
SCB_EnableICache();
}
Then in the main function I just call that. You can imagine what that looks like.
There are some considerations to have if you use the HAL and not just the LL library. For example, some of the magic that the HAL does requires interrupts, so the handlers must be somewhere.
If you dare peek into the generated code, you will see a couple files named
something like stm32[family]_it.c/.h
. Pay attention to the symbols defined in
the header, you will most likely need to implement those functions, perhaps you
can just link the ones in the C file, or perhaps you want to do something a bit
fancier. I usually need to do something a little bit fancier myself, so I tend
to just copy and paste the definitions to my own code and adapt away.
And that’s it basically. This is working pretty well for me and I’m reasonably happy with it. If you want to see a medium sized project that I’m working on that uses this approach, check Reziter, an easy to use sound synthesis device I’m working on.