Sunday, August 22, 2010

Building product variations without duplication of code and configuration. Makefile Tip #1

Mitch Frazier published a How To, Turn Make Options into Tool Flags, in The Linux Journal a couple of years ago, that I have found very useful in my Embedded development projects.


Using Mitch's technique I can have several different top level Makefiles to define options, source locations, and target output directories, that then includes a common Makefile called Makefile.mak. This allows me to build several variations of a product from the same set of sources, while keeping their options distinct and outputs separate.


Mitch's original technique is not MISRA friendly, as it relies on using 'ifdef', that is a missing option is considered a disabled option. MISRA does not allow this because the missing option may have been an oversight. Here is my modified MISRA friendly version:


Create a file called MakefileOptions.inc with the following code:


#---------------------- Cut Line ----------------------------------#
# Option names that start with a minus sign are disabled by default,
# option names without a minus sign are enabled by default.

#CONFIG_OPTIONS=\
#    OPTION_A \
#    -OPTION_B \
#    -OPTION_C

SET_CONFIG_OPTIONS=$(filter-out -%,$(CONFIG_OPTIONS))
UNSET_CONFIG_OPTIONS=$(patsubst -%,%,$(filter -%,$(CONFIG_OPTIONS)))
ALL_CONFIG_OPTIONS=$(SET_CONFIG_OPTIONS) $(UNSET_CONFIG_OPTIONS)
#$(info Set: $(SET_CONFIG_OPTIONS))
#$(info Unset: $(UNSET_CONFIG_OPTIONS))

# Turn config options into make variables.
$(foreach cfg,$(SET_CONFIG_OPTIONS),$(eval $(cfg)=1))
$(foreach cfg,$(UNSET_CONFIG_OPTIONS),$(eval $(cfg)=))

# Make sure none of the options are set to anything except 1 or blank.
# Using "make OPTION=0" doesn't work, since "0" is set, you need "make OPTION=".
$(foreach cfg,$(ALL_CONFIG_OPTIONS), \
    $(if $(patsubst %1,%,$(value $(cfg))), \
        $(error Use "$(cfg)=1" OR "$(cfg)=" not "$(cfg)=$(value $(cfg))")))

# Turn them into tool flags (-D).
#                                                   if   cfg > 0       True:      False:
TOOL_DEFINES+=$(foreach cfg,$(ALL_CONFIG_OPTIONS),$(if $(value $(cfg)),-D$(cfg)=1,-D$(cfg)=0))
#$(info $(TOOL_DEFINES))
#---------------------- Cut Line ----------------------------------#

A the top of your normal Makefile 'include' the above file like so:


include MakefileOptions.inc

The for a typical project I'll have a project uniquely named makefile, such as 'widget1.mak', that goes along these lines:


#---------------------- Cut Line ----------------------------------#
# Hey Emacs, this is a -*- makefile -*-
#----------------------------------------------------------------------------
#
TARGET=Widget1
F_CPU=4000000
MCU=atxmega128a1

# Option names that start with a minus sign are disabled by default,
# option names without a minus sign are enabled by default.
# Values on the command line override these:
CONFIG_OPTIONS=\
-ENABLE_CHIRP \
ENABLE_LEDS \
HAVE_RADIO \
USE_CRC_FLASH_CHECK \
-USE_DEBUGGING \
-USE_MOTION_SETTING_SCREEN

# List C source files here. (C dependencies are automatically generated.)
SRC = Accel/accel.c \
MENU_WIDGET1/menu.c \
....

ASRC = HARDWARE_XMega/sp_ReadFuseByte.S HARDWARE_XMega/sp_commoncmd.S ...

EXTRAINCDIRS = MENU_WIDGET1 HARDWARE Accel Alarm Backlight Battery CRC ...

include Makefile.mak

#---------------------- Cut Line ----------------------------------#

Each project variation gets a similar file that is invoked with 'make -f widget1.mak'.


An example C usage:


/*---------------------- Cut Line ----------------------------------*/
#if( USE_CRC_FLASH_CHECK > 0 )
extern uint16_t __data_load_end[1]; /* Defined by the linker script.  Set to address of last byte of .text+.data section */
#include 
static __inline__ uint16_t crc_flash_check_has_error( void );
static __inline__ uint16_t crc_flash_check_has_error( void )
{
...
}
#endif
/*---------------------- Cut Line ----------------------------------*/