on
AVR CMake Toolchain
Background
CMake is a very powerful and flexible build system. Enabling you to generate makefiles for your C and C++ projects.
With CMake, you can use a toolchain file to specify a different set of build tools for cross compiling.
In this blog post I will show how I setup the AVR toolchain in CMake.
First lets look at what we will end up with:
cmake_minimum_required(VERSION 2.8)
# specify MCU type and include toolchain file
set(AVR_MCU "atmega328p")
set(CMAKE_TOOLCHAIN_FILE "${CMAKE_SOURCE_DIR}/cmake/avr-gcc.toolchain.cmake")
project(MyProject C CXX ASM)
add_definitions(-DF_CPU=16000000)
add_avr_executable(${PROJECT_NAME}
src/main.cpp
)
Finding Build Tools
First we need to find the AVR toolchain on the system. We can use cmake’s find_path
function for that.
# toolchain prefix
set(TRIPLE "avr")
# attempt to find avr-gcc
find_path(TOOLCHAIN_ROOT
NAMES
${TRIPLE}-gcc
PATHS
/usr/bin
/usr/local/bin
/bin
$ENV{AVR_ROOT}
)
# Error, could not find toolchain
if(NOT TOOLCHAIN_ROOT)
message(FATAL_ERROR "Toolchain root could not be found!!!")
endif(NOT TOOLCHAIN_ROOT)
This will search all of the ‘paths’ for avr-gcc and populate TOOLCHAIN_ROOT
with the result. If the toolchain could not be found we send a fatal error.
Now we need to configure cmake with the new build tools. CMake has a built in variable that corresponds to most of the tools in our toolchain, we just have to set them.
CMAKE_C_COMPILER
corresponds to avr-gcc
and CMAKE_CXX_COMPILER
corresponds to avr-g++
etc.
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR avr)
set(CMAKE_CROSS_COMPILING 1)
set(CMAKE_C_COMPILER "${TOOLCHAIN_ROOT}/${TRIPLE}-gcc${OS_SUFFIX}" CACHE PATH "gcc" FORCE)
set(CMAKE_CXX_COMPILER "${TOOLCHAIN_ROOT}/${TRIPLE}-g++${OS_SUFFIX}" CACHE PATH "g++" FORCE)
set(CMAKE_AR "${TOOLCHAIN_ROOT}/${TRIPLE}-ar${OS_SUFFIX}" CACHE PATH "ar" FORCE)
set(CMAKE_LINKER "${TOOLCHAIN_ROOT}/${TRIPLE}-ld${OS_SUFFIX}" CACHE PATH "linker" FORCE)
set(CMAKE_NM "${TOOLCHAIN_ROOT}/${TRIPLE}-nm${OS_SUFFIX}" CACHE PATH "nm" FORCE)
set(CMAKE_OBJCOPY "${TOOLCHAIN_ROOT}/${TRIPLE}-objcopy${OS_SUFFIX}" CACHE PATH "objcopy" FORCE)
set(CMAKE_OBJDUMP "${TOOLCHAIN_ROOT}/${TRIPLE}-objdump${OS_SUFFIX}" CACHE PATH "objdump" FORCE)
set(CMAKE_STRIP "${TOOLCHAIN_ROOT}/${TRIPLE}-strip${OS_SUFFIX}" CACHE PATH "strip" FORCE)
set(CMAKE_RANLIB "${TOOLCHAIN_ROOT}/${TRIPLE}-ranlib${OS_SUFFIX}" CACHE PATH "ranlib" FORCE)
set(AVR_SIZE "${TOOLCHAIN_ROOT}/${TRIPLE}-size${OS_SUFFIX}" CACHE PATH "size" FORCE)
CMake does not has a variable for avr-size
so we include one ourselves.
Setting avr linker libraries:
set(AVR_LINKER_LIBS "-lc -lm -lgcc")
Adding Build targets
Now we need to add a cmake macro to build our binary files. For development with AVR we must build a .elf
file and a .hex
files but we will also build .map
and .lst
files to help with debugging.
macro(add_avr_executable target_name)
# define file names we will be using
set(elf_file ${target_name}-${AVR_MCU}.elf)
set(map_file ${target_name}-${AVR_MCU}.map)
set(hex_file ${target_name}-${AVR_MCU}.hex)
set(lst_file ${target_name}-${AVR_MCU}.lst)
...
endmacro(add_avr_executable)
The elf file target is our sources compiled together, we can use cmake’s add_executable
now that cmake is using our build tools.
...
# add elf target
add_executable(${elf_file}
${ARGN}
)
# set compile and link flags for elf target
set_target_properties(
${elf_file}
PROPERTIES
COMPILE_FLAGS "-mmcu=${AVR_MCU} -g -Os -w -std=gnu++11 -fno-exceptions -ffunction-sections -fdata-sections"
LINK_FLAGS "-mmcu=${AVR_MCU} -Wl,-Map,${map_file} ${AVR_LINKER_LIBS}"
)
${ARGN}
contains all parameters after target_name
.
Note: Do not miss -mmcu=${AVR_MCU}
in link flags as your code will compile and link, but not correctly!
Now we will add commands to generate lst
and hex
file targets. We will use cmake’s add_custom_command
.
...
# generate the lst file
add_custom_command(
OUTPUT ${lst_file}
COMMAND
${CMAKE_OBJDUMP} -h -S ${elf_file} > ${lst_file}
DEPENDS ${elf_file}
)
# create hex file
add_custom_command(
OUTPUT ${hex_file}
COMMAND
${CMAKE_OBJCOPY} -j .text -j .data -O ihex ${elf_file} ${hex_file}
DEPENDS ${elf_file}
)
...
We also want a command to print elf
file size.
...
add_custom_command(
OUTPUT "print-size-${elf_file}"
COMMAND
${AVR_SIZE} ${elf_file}
DEPENDS ${elf_file}
)
...
Notice how they depend on elf_file
target.
Now that we defined commands to generate the lst
and hex
files, we need to call these commands.
...
# build the intel hex file for the device
add_custom_target(
${target_name}
ALL
DEPENDS ${hex_file} ${lst_file} "print-size-${elf_file}"
)
set_target_properties(
${target_name}
PROPERTIES
OUTPUT_NAME ${elf_file}
)
endmacro(add_avr_executable)
By adding a custom target using add_custom_target
and having it depend on our custom commands, we can get cmake to generate our files. Our target is added to ALL
so it will be invoked with make all
or just make
Flash Targets
The flash targets aren’t part of the build, but are useful. I use avrdude
as my upload tool.
...
# avr uploader config
find_program(AVR_UPLOAD
NAME
avrdude
PATHS
/usr/bin/
$ENV{AVR_ROOT}
)
if(NOT AVR_UPLOAD_BAUD)
set(AVR_UPLOAD_BAUD 57600)
endif(NOT AVR_UPLOAD_BAUD)
if(NOT AVR_UPLOAD_PROGRAMMER)
set(AVR_UPLOAD_PROGRAMMER "arduino")
endif(NOT AVR_UPLOAD_PROGRAMMER)
if(NOT AVR_UPLOAD_PORT)
if(UNIX)
set(AVR_UPLOAD_PORT "/dev/ttyUSB0")
endif(UNIX)
if(WIN32)
set(AVR_UPLOAD_PORT "COM3")
endif(WIN32)
endif(NOT AVR_UPLOAD_PORT)
...
Find the upload tool and specify some default values for avrdude.
These values can be overridden at the command line:
cmake .. -DAVR_UPLOAD_PORT=/dev/ttyACM0 -DAVR_UPLOAD_BAUD=115200
The flash target is also created using add_custom_target
and add_custom_command
# flash command
add_custom_command(
OUTPUT "flash-${hex_file}"
COMMAND
${AVR_UPLOAD} -b${AVR_UPLOAD_BUAD} -c${AVR_UPLOAD_PROGRAMMER} -p${AVR_MCU} -U flash:w:${hex_file} -P${AVR_UPLOAD_PORT}
)
add_custom_target(
"flash-${target_name}"
DEPENDS "flash-${hex_file}"
)
And thats it! A working AVR toolchain in cmake!
Use it
cd project/path/build
cmake ..
make
make flash-MyProject