Till recently, for writing code for given microcotroller, I was using dedicated GUI, which very often include embedded tools for compiling, building and quite often programming. As experience and complication of projects grow, one starts feeling natural need of test sophisticated architectures before uploading into device. This allows save time and money in future. That is why I wanted to include also unit tests into my code. Unfortunately, my currently used tools didn't offer such ability. Habit from work caused, that I wanted use GTest. I had to somehow combine two types of building into one. That forced me to dive a bit deeper into compilers world.
First choose was CMake. It is a natural evolution of popular Make file. After first look into the details I discovered, that compiler used in building process is defined in environment variable. I heard that there are some hacks that allows to achieve my goal, but they are dirty hacks. As it turned out later, most of building systems are based on such env var. That forced me to learn more about building code and write my own, simpler tool in Python.
Script language as Python is, contains many plugins/tools ready to use. That makes life easier. As a main compiler I chose GCC which can also build code for AVR. And so, firstly simple application evaluated into a bit powerful tool. I like it for its flexibility, which allows me to add and change features as I want.
Building process:
We can compile each single C/CPP file, getting as a result output object file.
g++.exe -c example_code.cpp -o ex_code.o
I compiled file includes some header files inclusion, then they all must be listed in command line. Fortunately, we can give a path to directory where those files are stored. For this purposes, we need to use switch '-I' (capital i), because we pass path to directory not to file. IMPORTANT: don't add a space between switch and path.
g++.exe -c example_code.cpp -Icode/example/headers -Icode/different/example/headers -o ex_code.o
Result file is a collection of declarations and definitions. To see what is inside, we can use a objdump.exe. As a parameter it accepts object file.
Other interesting switch, which is worth to include, is '-O'. It informs a compiler how to optymize code. There are different types of it: time, code size, performance or without optimazation
Additionally, using preprocessor, we can define some variables in code (#define), which will be considered during compilation. For this purposes we use '-D' switch. And similarly to include paths, this time we also don't add a space between.
During writing a code, we can make many mistakes. Not only lexical but also logic one. They can exist and will not stop compliation process. We call them Warnings. Using switch '-W' we can tell the compiler for which of them, it should be sensitive.
The last one from many very interesting about which I want to mention is the one that defines a version of language standard in which code was written. This is '-std='.
Example of command line with all mentioned switches:
g++.exe -Ifirmware. -Ifirmwareusbheaders -Wall -gdwarf-2 -std=c++11 -DF_CPU=16000000UL -Os -c main.cpp -o main.o
Linking process:
When all files are ready, then we can combine (link) them into one piece. Linker tries to connect (resolve) all declarations and definitions into one code. If some of them are missing, linker will rise an error and stop his work. To make it work properly, we need to enclose all object files and connect them into one output file. The easiest way can look like this:
g++.exe main.o usb.o uart.o -o my_program.exe
Of course, other switches also work, until we use the same application (g++).
Generating outputs and statistics:
Sometimes for programmers devices hex file is required. To generate them we can use application objcopy.exe
objcopy.exe -O ihex program.exe output_program.hex
If we want to know, how code is located in different types of memory and how large is it, then we should use applications size.exe
size.exe -dt program.exe