Why is it important to build C code from MakeFile(s)

In the period from 199x to 201x, a lot of microcontroller programmers divorced who never climbed out of all sorts of IDEs (IAR, KEIL, Code Composer Studio, AtilocTrueStudio). In my opinion, this is very sad. Largely because a specialist in Keil will not be able to quickly understand how to work in IAR and vice versa. Migrating to another IDE is also a big challenge, as it comes down to GUI mouse flicking. Each version of IAR is not compatible with newer version of IDE.

The fact is that GUI IDEs appeared in 199x … 201x, when there was no DevOps (a) heyday, the programmer worked alone and all actions were performed manually with the mouse. At that time, working in the GUI seemed more fun to microcontroller programmers, because there are a lot of straziks in the IDE.

But with the complication of the code base, with the increase in assemblies, with the increase in development teams, there was a need for autoassemblies, autotests. Appeared methodology

code separately, configs separately

and working with the IDE only slowed down the processes. After all, the configs are stored in the IDE-shnoy XML (ke). I had to duplicate the board configs for every build that used that board. I had to duplicate the config code and this process was accompanied by errors. When working with an IDE, the codebase turned into a zoo in a swamp.

What are the disadvantages of building sources from under the IDE?

1–IDEs eat up a lot of computer resources, both RAM and CPU, while IDEs need a lot of RAM to draw windows with straziks.

2–IDE are monolithic and indivisible. If you want to change the preprocessor, compiler or linker, and leave the rest of the ToolChain(a) phases as they are, then nothing will come of it, since the IDE hood is locked.

3–IDE are expensive, about 3500 EUR per computer

4–IDE xml is very poorly documented or not documented at all. All vendors have their own xml markup language. When making minor changes, a huge git diff appears.

5–It is difficult to assemble from the console. Basically, you can initiate a build in the IDE with the mouse or hotkeys.

6–Backward incompatibility with new IDE versions

7–Under the technology embargo, it is impossible to legally buy an IDE from a European vendor.

In general, the spread of IDE is a vivid example of the now well-known “technological dictate” of the West for the countries of the second and third world.

We give you a sandbox (IDE), and you sit there behind the sides and sculpt your Easter cakes (firmware).

It is clear that in such a framework one cannot count on “doing something serious”.

I had to think. It turned out to be a good decision to take a step back to 197x 198x when on computers everything was done from the console. Collect sorts from scripts. In general, you can write a bat file and it generally initiates the launch of the necessary utilities, however, historically, the C-code was assembled with the make utility.

What are the benefits of building C code from makefiles?

1– Makefile is the most flexible way to manage modularity. You can literally add or exclude one specific software component (dozens of files) from dozens of assemblies with just one line. In the case of building from under the IDE, you would have to manually edit the .xml for each build.

2–Building from a Makefile is very easy to automate. It is enough to execute make all in the console and the build process is initiated in you.

3–After building from scripts, you will get a full build log, while IDEs usually show the last 3-4 screens of a compiler message.

4–It is very easy to change compilers in MakeFile. It literally replaces one line. From GCC to Clang or to GHS. Here is a typical main makefile for any build on ARM Cortex-Mxx

mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
$(info Build  $(mkfile_path) )
BUILD_DIR = build

#@echo $(error SOURCES_C= $(SOURCES_C))
INCDIR := $(subst /cygdrive/c/,C:/, $(INCDIR))
#@echo $(error INCDIR=$(INCDIR))
SOURCES_C := $(subst /cygdrive/c/,C:/, $(SOURCES_C))
#@echo $(error SOURCES_C=$(SOURCES_C))
SOURCES_ASM := $(subst /cygdrive/c/,C:/, $(SOURCES_ASM))
LIBS  := $(subst /cygdrive/c/,C:/, $(LIBS))
LDSCRIPT := $(subst /cygdrive/c/,C:/, $(LDSCRIPT))
#@echo $(error SOURCES_ASM=$(SOURCES_ASM))

# binaries
PREFIX = arm-none-eabi-
GCC_PATH="C:/Program Files (x86)/GNU Arm Embedded Toolchain/10 2021.10/bin"
$(info GCC_PATH=$(GCC_PATH))

# The gcc compiler bin path can be either defined in make command via GCC_PATH variable (> make GCC_PATH=xxx)
# either it can be added to the PATH environment variable.
ifdef GCC_PATH
  CC = $(GCC_PATH)/$(PREFIX)gcc
  AS = $(GCC_PATH)/$(PREFIX)gcc -x assembler-with-cpp
  CP = $(GCC_PATH)/$(PREFIX)objcopy
  SZ = $(GCC_PATH)/$(PREFIX)size
  CC = $(PREFIX)gcc
  AS = $(PREFIX)gcc -x assembler-with-cpp
  CP = $(PREFIX)objcopy
  SZ = $(PREFIX)size
HEX = $(CP) -O ihex
BIN = $(CP) -O binary -S
# float-abi
ifeq ($(NRF5340), Y)
    ifeq ($(CORE_NET), Y)
        FLOAT-ABI = -mfloat-abi=soft
        OPT += -fsingle-precision-constant
    ifeq ($(CORE_APP), Y)
        FLOAT-ABI = -mfloat-abi=hard
   FLOAT-ABI = -mfloat-abi=hard

# mcu
MCU = $(CPU) -mthumb $(FPU) $(FLOAT-ABI)

# macros for gcc
#CSTANDARD = -std=c11
CSTANDARD = -std=gnu99
# AS defines

# AS includes

ifeq ($(DEBUG), Y)
    #@echo $(error DEBUG=$(DEBUG))
    CFLAGS += -g3 -gdwarf-2 -ggdb
    OPT += -O0 
    OPT += -Os
OPT += -fmessage-length=0    
OPT += -fsigned-char
OPT += -fno-common
OPT += -fstack-usage
OPT += -finline-small-functions

#Perform dead code elimination
OPT += -fdce

#Perform dead store elimination
OPT += -fdse

# compile gcc flags
ASFLAGS = $(MCU) $(AS_DEFS) $(AS_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections

CFLAGS += -Wall
#CFLAGS += -Wformat-overflow=1
CFLAGS += $(MCU) $(OPT) -fdata-sections -ffunction-sections $(INCDIR)  

# Generate dependency information
CFLAGS += -MMD -MP -MF"$(@:%.o=%.d)"


# libraries
LINKER_FLAGS += -Xlinker --gc-sections 
ifeq ($(MBR), Y)
    #@echo $(error MBR=$(MBR))
    LIBS += -lnosys
    LDFLAGS += -specs=nano.specs
    LINKER_FLAGS += -u _scanf_float
    LINKER_FLAGS += -u _printf_float
#LINKER_FLAGS += -lrdimon --specs=rdimon.specs

ifeq ($(LIBC), Y)
    #@echo $(error LIBC=$(LIBC))
    LIBS += -lc

ifeq ($(MATH), Y)
    #@echo $(error MATH=$(MATH))
    LIBS += -lm 

#@echo $(error LDSCRIPT=$(LDSCRIPT))

LDFLAGS += $(MCU) -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections $(LINKER_FLAGS)

# default action: build all
all: $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin

# build the application
# list of objects
OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(SOURCES_C:.c=.o)))
vpath %.c $(sort $(dir $(SOURCES_C)))
# list of ASM program objects
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(SOURCES_ASM:.S=.o)))
vpath %.S $(sort $(dir $(SOURCES_ASM)))

$(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR) 
	$(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.c=.lst)) $< -o $@

$(BUILD_DIR)/%.o: %.S Makefile | $(BUILD_DIR)
	$(AS) -c $(CFLAGS) $< -o $@

$(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile
	$(CC) $(OBJECTS) $(LDFLAGS) -o $@
	$(SZ) $@

$(BUILD_DIR)/%.hex: $(BUILD_DIR)/%.elf | $(BUILD_DIR)
	$(HEX) $< $@
$(BUILD_DIR)/%.bin: $(BUILD_DIR)/%.elf | $(BUILD_DIR)
	$(BIN) $< $@	
	mkdir $@		

# clean up
	-rm -fR $(BUILD_DIR)
# dependencies
-include $(wildcard $(BUILD_DIR)/*.d)

# *** EOF ***

5–When you build from Make, you can not only build sources, but also build documentation, build dependency graphs on dot, build a ToolChain(a) schema. Call Latex, Doxyden.

The make utility doesn’t care which console utilities to call. This is a generic way to define software pipelines.

6–For each assembly, you have to write a tiny Makefile yourself

MK_PATH:=$(dir $(realpath $(lastword $(MAKEFILE_LIST))))
#@echo $(error MK_PATH=$(MK_PATH))


#@echo $(error SOURCES_C=$(SOURCES_C))
include $(MK_PATH)config.mk
include $(MK_PATH)cli_config.mk
include $(MK_PATH)diag_config.mk
include $(MK_PATH)test_config.mk

include $(WORKSPACE_LOC)code_base.mk
include $(WORKSPACE_LOC)rules.mk

and build config.

mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
$(info Build  $(mkfile_path) )

#@echo $(error TARGET=$(TARGET))



For each component *.mk file. The make language is simple and it’s basically bash. Here is a typical *.mk file for the DW1000 driver

ifneq ($(DWM1000_MK_INC),Y)

    DWM1000_DIR = $(DRIVERS_DIR)/dwm1000
    mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
    $(info Build $(mkfile_path) )
    $(info + DWM1000)

    INCDIR += -I$(DWM1000_DIR)
    OPT += -DHAS_DWM1000
    OPT += -DHAS_DWM1000_PROC
    OPT += -DHAS_UWB



    SOURCES_C += $(DWM1000_DIR)/dwm1000_drv.c

    include $(DWM1000_DIR)/otp/dwm1000_otp.mk
    include $(DWM1000_DIR)/registers/dwm1000_registers.mk

    ifeq ($(DWM1000_RANGE),Y)
        include $(DWM1000_DIR)/range/dwm1000_range.mk

    ifeq ($(DIAG),Y)
        ifeq ($(DWM1000_DIAG),Y)
            $(info +DWM1000_DIAG)
            OPT += -DHAS_DWM1000_DIAG
            SOURCES_C += $(DWM1000_DIR)/dwm1000_diag.c

    ifeq ($(CLI),Y)
        ifeq ($(DWM1000_COMMANDS),Y)
            $(info +DWM1000_COMMANDS)
            OPT += -DHAS_DWM1000_COMMANDS
            SOURCES_C += $(DWM1000_DIR)/dwm1000_commands.c

7–Building from Make encourages modularity, isolation of software components, and traceability of dependencies between components. If you’re building from make, it’s very likely that you’ll end up with a clean, tidy repository all by itself.

8–Makefile(s) are good because you can add a lot of dependency checks and assert(s) at the Make script processing phase directly in *.mk files even before compiling the code itself, even before starting the preprocessor, since the make programming language supports conditional statements and functions. You can catch a lot of errors at the stage of working out the make utility.

9–The make language is very simple. The whole spec of GNU Make is 226 pages long. Stu Feldman (author of make) is a genius.

10–Makefile(s) transparent text. You can always see where the preprocessor options are, where the keys are for the compiler, and where for the linker. Everything you need can be found with the grep utility in the same console.

11–The build config can be generated just at the makefile stage and passed as keys to the preprocessor. Thus, the configs will be visible in each *.c file of the project and there is no need to insert #include (s) with configs. Everything can be passed as options to the cpp (preprocessor) utility.


Make it like buttons. Old, simple and very useful thing. Compiling your firmware from make is not difficult.





Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *