Ethernut Home Hardware Firmware Tools Download Community
 
 
Search | Legals | Deutsch

Playing the Nut/OS Configuration Game

Initially Nut/OS had been written for a fixed hardware, the Ethernut 1 with an ATmega103 MCU and an RTL8019AS Ethernet controller. And the only supported development environment had been AVRGCC running on the Windows PC. Good old days.

Today we have to support different targets with different CPUs, Ethernet controllers, memory layouts and I/O hardware as well as different development platforms with different compilers. Thus, system configuration became an important part.

Since Nut/OS 3.9, a GUI became available to simplify the configuration task. The Nut/OS Software Manual provides a step by step guide on how to use this tool.

In this paper I will present some more details about the possibilities to configure the system and how the Configurator uses these capabibilities.

The Make Tool

Regardless of which development environment is used to build Nut/OS, GNU Make takes over the top level control. Explaining this tool in detail is beyond the scope of this paper and to many developers of embedded systems it looks like C to PASCAL programmers, quite cryptic. But as often, the number of options make a tool look complicated while the main function is quite simple.

When started without any option, Make will look for a file named Makefile, interpret it and perform the related actions. Some Makefiles may contain weird sequencies of characters, but all of them contain something like

target: dependencies
        actions
This is interpreted by Make in the following way: To produce the target, dependencies and actions are required. The three items used here are just placeholders. A real example may look like
simple.hex: simple.c simple.h                  
        avr-gcc -c -mmcu=atmega128 simple.c -o simple.o
        avr-gcc simple.o -mmcu=atmega128 -lnutdev -lnutos -o simple.elf
        avr-objcopy -O ihex simple.elf simple.hex
Be warned, this may be a real example but for Nut/OS some more steps and options are required. Anyway, it presents a good picture. If simple.c and simple.h exist, then the three given steps are required to create simple.hex. In the first step simple.c is compiled to simple.o. In the second step simple.o is linked with libnutdev.a and libnutos.a to create simple.elf. And in the final step simple.elf is converted to simple.hex.

You may now argue, that this could be done by a simple batch file or shell script. You are probably right. Now imagine, what a large number of actions would be required to compile all Nut/OS source files for all supported platforms, if we would do it this way. Make simplifies this by allowing us to specify general rules like

%o : %c
        avr-gcc -c -mmcu=atmega128 $< -o $@
%elf: %o
        avr-gcc $< -mmcu=atmega128 -lnutdev -lnutos -o $@
%hex: %elf
        avr-objcopy -O ihex $< $@
This already looks too cryptic to many of us, but doesn't really differ from the real world example given above. The big advantage with these rules is, that we just need to write
simple.hex: simple.c simple.h
and Make knows, which steps are required to create simple.hex from simple.c and simple.h. That's cool, isn't it?

Another nice feature of Make is, that it checks automatically, which actions are required after you modified something. In other words, if you edit simple.h, save your changes and run Make again, it will run all three steps again. If you modified anything else but neither simple.c or simple.h, then Make will keep the already existing simple.hex.

However, the Nut/OS Makefiles are incomplete here, because they do not list all dependencies. All header files are excluded. Thus, if any header file is changed, you need to run

make clean
make
in the build tree. The first will remove all files previously created by Make. The option clean is an explicit target given on the command line. Somewhere in the Makefile there is something found like
clean:
        rm *.o *.hex *.elf
For the Windows users: The UNIX command rm is similar to the DOS command del. To avoid complete Makefile madness, some basic UNIX commands are included in the Windows distribution of Nut/OS as executables. You can find them in the directory tools/win32. When running Make in a DOS window, make sure that your PATH points to this directory.

When the Configurator builds Nut/OS, it will set the correct path and will always run make clean first. The next chapter will show more details about how the Configurator uses Make.

Make and the Nut/OS Configurator

We assume, that all Configurator settings have been specified by following the Nut/OS Software Manual and that we are ready to create a new Nut/OS build tree.

When selecting Generate Build Tree from the Configurators Build menu, then the directory for building Nut/OS will be created with all required subdirectories. In the top level directory of the build tree, the Configurator will additionally create three files, which will be used by the Make tool.

The first of these files is named Makefile. It's a very simple Makefile with three targets: all, install and clean.

all:
        $(MAKE) -C arch
...
install:
        $(MAKE) -C arch install
...
clean:
        $(MAKE) -C arch clean
...
Two additional features need to be explained to understand this Makefile. First note the term $(MAKE). That's how Make understands definitions similar to C makros. It may be defined somewhere else as MAKE=make, however, specially $(MAKE) is internally predefined. The second feature is the command line option -C directory. This instructs make to change to the specified directory before starting to read the Makefile.

As you can see, for each target Make is called for each Nut/OS module subdirectory with the same target again. Of course, the Configurator also creates the Makefiles in these subdirectories, which are much more interesting than the top level Makefile.

Let's stay at the top level, where the Configurator created two additional files, NutConf.mk and UserConf.mk.

NutConf.mk defines several macros, similar to the internally defined $(MAKE), which I explained above. When building Nut/OS for the ATmega128 using AVRGCC, this file contains

MCU_ATMEGA128=atmega128
MCU_ATMEGA103=atmega103
MCU=$(MCU_ATMEGA128)
HWDEF=-D__HARVARD_ARCH__
CRUROM=crurom

include $(top_blddir)/UserConf.mk
Whenever $(MCU) appears in a Makefile, Make will replace it with $(MCU_ATMEGA128), which in turn is replaced by atmega128.

The last line of NutConf.mk instructs Make to include UserConf.mk, which initially contains a single line

HWDEF += -DETHERNUT2
Here's another interesting feature of Make: We can append new string to existing macros. Together with the definition in NutConf.mk, $(HWDEF) will be finally expanded to -D__HARVARD_ARCH__ -DETHERNUT2. As long as you are using the Ethernut 2 hardware, this is just fine. When building Nut/OS, this compile option is simply ignored. However, the same entry is added by the Configurator when creating the sample directory and should be removed before compiling any of the samples. Hey, wait, we are building Nut/OS. More on the sample applications later.

Each time, when you re-create the build tree, the Configurator will overwrite existing files in the build tree. Thus, do not edit them, because your changes will get lost. UserConf.mk is an exception. It is initially created, if it doesn't exists. If it exists, the Configurator will not touch it. You can use it to add additional compile options, for example.

HWDEF += -g3 -gdwarf-2

Now let's dive into one of the subdirectories and see, what the Configurator created there. We choose subdirectory os, which mostly contains the Nut/OS kernel code. Wonder! It contains nothing but another Makefile.

PROJ =  libnutos

top_srcdir = c:/ethernut/nut
top_blddir = c:/ethernut/nutbld

VPATH = $(top_srcdir)/os

SRCS =   heap.c bankmem.c thread.c timer.c event.c devreg.c confos.c version.c \
         semaphore.c mutex.c msg.c osdebug.c tracer.c
OBJ1 = nutinit.o

OBJS = $(SRCS:.c=.o)
include $(top_blddir)/NutConf.mk

include $(top_srcdir)/Makedefs.avr-gcc

INCFIRST=$(INCPRE)$(top_blddir)/include

all: $(PROJ).a $(OBJS) $(OBJ1)

install: $(PROJ).a $(OBJ1)
        $(CP) $(PROJ).a c:/ethernut/nutbld/lib/$(PROJ).a
        $(CP) $(OBJ1) c:/ethernut/nutbld/lib/$(notdir $(OBJ1))

include $(top_srcdir)/Makerules.avr-gcc

.PHONY: clean
clean: cleancc cleanedit
        -rm -f $(PROJ).a
        -rm -f $(OBJ1)
Here is some of the cryptic stuff like OBJS = $(SRCS:.c=.o), which is hated so much by Makefile ignorants and which we will not explain here either. Instead we concentrate on those parts, which are relevant to the Nut/OS configuration. Most important is the list of source files
SRCS =   heap.c bankmem.c thread.c timer.c event.c devreg.c confos.c version.c \
         semaphore.c mutex.c msg.c osdebug.c tracer.c
When using the Configurator, you may have recognized, that some parts in the module tree are disabled. That's because the Configurator keeps track of module dependencies. For example, some drivers like the AVR USART driver do not make sense for the ARM CPU and will be deactivated when selecting this CPU. Now each Nut/OS module displayed in the Configurator's module tree is associated with one or more source files. When creating the build tree, the Configurator will exactly place all source files of all activated modules in the SRCS line. As a result, the final Nut/OS libraries will contain only those modules, which are available on your target platform.

Not less important lines in os/Makefile are

...                  
include $(top_blddir)/NutConf.mk
...
include $(top_srcdir)/Makedefs.avr-gcc
...
include $(top_srcdir)/Makerules.avr-gcc
...
The first one includes NutConf.mk, which we discussed above. The other two include two files from the Nut/OS source tree. If you look into the top level source directory, you will see a number of files named Makedefs and Makerules with different file extensions. When building with AVRGCC, the Configurator will add includes of Makedefs.avr-gcc and Makerules.avr-gcc. When building with ARMGCC, it will use Makedefs.arm-gcc and Makerules.arm-gcc instead. Again, newbies may consider the contents of these files weird stuff. That's just fine. Under normal circumstances you will have no reason to deal with these. Simply note, that they contain nothing but special definitions for the compiler you are using. If one of the gurus ever passes you a modified version, for example with JTAG debug options, then copy them into the top level source directory with a different extension. The Configurator automatically scans the source directory for such files. Next time you open the second page in the Configurator's settings dialog, you can select the new extension in the platform drop down box.

An important final advice: Each time you changed anything in the Configurator settings, you must recreate the build tree.

Nut/OS Options

Up to this point, configurations were mainly related to the compiler and the target CPU. The majority of configurations is related to hardware specifications and various other build options, though. We will now see, how the Configurator uses C header files to pass these setting to the compiler.

Let's assume, we want to enable floating point support for printf and scanf. In the module tree we expand C Runtime (Traget Specific) and File Streams below that. Then we can enable Floating Point by clicking on the related check box. If we create a new or re-create an existing build tree, then the Configurator adds a new header file named crt.h in the subdirectory include/cfg of your build tree, which contains:

#define STDIO_FLOATING_POINT
The original include/cfg/crt.h in the source directory does not contain this entry.

Here's a snippet form the Nut/OS source code:

                  
#include 
...
#ifdef STDIO_FLOATING_POINT
/* This is floating point stuff. */
....
#endif
...
As you noticed, the floating point code is added only, if include/cfg/crt.h defines STDIO_FLOATING_POINT. The one in the build tree does, the one in the source tree doesn't.

Now we are back to the Makefiles of the build tree, in our case crt/Makefile.

top_blddir = c:/ethernut/icnutbld
...                  
INCFIRST=$(INCPRE)$(top_blddir)/include
This is used in the following line of Makerules.avr-gcc:
%o : %c
        $(CC) -c $(CPFLAGS) $(INCFIRST) -I$(INCDIR) $(INCLAST) $< -o $@
Confused? Don't worry. The result is, that the compiler will search include/cfg/crt.h in the build tree first. If it couldn't be found there, the search will continue in the source tree. This way the Configurator is able to overwrite existing default header files in the source tree without any modification of the source tree. This is a real advantage, if you want to build Nut/OS for different targets. You can use the same source tree for each target. And if you later upgrade to a new version of Nut/OS, you can replace the source tree whithout losing your specific configurations, because all of them are located in the build tree.

The Application Tree

Beside the source and the build tree, a third one is used to build Nut/OS applications. It it created by selecting Create Sample Directory in the Configurator's Build menu. There's not much mystery in here, but it comes quite handy for creating your own applications.

Users of the ImageCraft IDE are lucky people. They can use the GUI to create new project files in the sample directory. Please follow the steps in the Nut/OS Software Manual.

For using GCC on the command line, just copy one of the subdirectories to a new one with a new name within the sample directory. When adding new or removing existing source files, make sure to update the Makefile accordingly. Even if you don't understand Makefiles, this is quite simple. Be aware, that actions in Makefiles are preceded with a TAB character, spaces won't work. Your text editor should preserve tabs.

Remember UserConf.mk? When running the compiler on the command line, the default contents created by the Configurator may hurt now, if we do not intend to run the application on Ethernut 2. All samples have been written for Ethernut 1 (including Charon II and other boards using the Realtek Ethernet Controller) and Ethernut 2. All samples with network code contain the following lines:

                  
#ifdef ETHERNUT2
#include <dev/lanc111.h>
#else
#include <dev/nicrtl.h>
#endif
If ETHERNUT2 is defined in UserConf.mk, then the application will register the LAN91C111 driver. Otherwise the driver for the RTL8019AS is used. If you want the latter, do not forget to remove the related line in UserConf.mk. In newer releases WOLF is another option and uses the AX88796 driver. For this controller simply replace ETHERNUT2 by WOLF in UserConf.mk.

Under the Hood

As stated above, the GNU Make Tool controls the top level. The bottom level is controlled by Lua, an interpreter language. Mainly because of compatibility problems with Lua libraries on various Linux platforms, the decision to use Lua had been criticized sometimes. But the author is still convinced, that this had been a good decision. The Lua interpreter is extremely small and the language is extremely powerfull. Right now this is hard to believe, because the Configurator uses Lua in a static way. But one day it will shine brighter than everything else...:-)

Neither Lua nor all Nut/OS options will be described here. Just a few details will be discussed.

All Lua scripts are located in the subdirectory conf within the source tree. When the Configurator starts, it will read repository.nut first. This file specifies additional scripts.

{
    name = "nutarch",
    brief = "Target",
    description = "Select one only.",
    subdir = "arch",
    script = "arch/arch.nut"
},
Here arch/arch.nut is specified as an additional script to be included, which contains:
{
    macro = "MCU_ATMEGA128",
    brief = "Atmel ATmega 128",
    description = "8-bit RISC microcontroller with 128K bytes flash, 4K bytes RAM, "..
                  "4K bytes EEPROM, 64K bytes data memory space, 2 USARTs, 4 timers, "..
                  "8-channel ADC, SPI and TWI.",
    requires = { "TOOL_CC_AVR" },
    provides = {
        "HW_TARGET",
        "HW_MCU_AVR",
        "HW_MCU_AVR_ENHANCED",
        "HW_MCU_ATMEGA128",
        "HW_NVMEM",
        "HW_TIMER_AVR",
        "HW_UART_AVR"
    },
    flavor = "boolean",
    file = "include/cfg/arch.h",
    makedefs = { "MCU=$(MCU_ATMEGA128)", "HWDEF=-D__HARVARD_ARCH__" }
},
The last two definitions are the interesting ones. The first one specifies the include file, which will be created by the Configurator when Atmel ATmega 128 is activated. In addition, the Configurator will #define MCU_ATMEGA128 in this file.

The last definition makedefs = looks familiar, doesn't it? Indeed it specifies a list of entries, which will be added to NutConf.mk.

However, before you are able to access the module tree in the Configurator's main window, you will be asked to select a configuration file. These files got the extension .conf and are also located in the conf subdirectory of the Nut/OS source tree. They contain simple assignments. Here's the one, which specifies options for running Nut/OS on the STK501 with an ATmega128 and the AVRGCC:

AVR_GCC = ""
MCU_ATMEGA128 = ""
NUTMEM_SIZE = "4096"
NUTMEM_START = "0x100"
NUTMEM_RESERVED = "64"
The entries are related to the same named macro = entries in the Lua scripts. The first two do not assign any values. The fact, that they appear in this file, sets the related boolean Lua macro definitions to true.

There are other options handled by Lua, but you should have got the picture. In the current state of the Configurator this is quite helpful, because this tool is far from being perfect. For example, radio boxes are not implemented and the user must take care to enable one CPU or one compiler only and disable all others. One annoying thing is, that the compiler needs to be specified at two places, in the Configurator's setting notebook as well as in the component tree. Furthermore, not all options seem to make their way into the final build. If something seems to go wrong, check the include/cfg directory in the build tree and further check, wether these files are actually included in the related Nut/OS code. But before panic breaks out: The mainstream works fine, exotic options may fail.

Changing Environments

In the previous chapter we learned, that the configuration of the Configurator's settings notebook isn't always syncronized with the Configurators component tree. That's because they are kept in different places.

All modifications of the component tree are manually stored in platform configuration files with extension .conf. All modifications of the settings notebook are automatically stored in the Windows registry or in nutconf.ini when running Linux. This could be quite annoying, when you regularly have to change your environment. The problem isn't finally solved, but for now it helps to call the Configurator with command line option -s followed by a keyword of your choice. For example, on Windows you can call

nut\nutconf
for projects using ICCAVR and
nut\nutconf -s arm
for projects with ICCARM. In this case the Configurator will use an alternative registry entry for ICCARM settings. [This example had been transfered to us from the future via PastBeam(tm). ICCARM isn't supported yet.]

More Hints

1. After modifying Lua scripts the component tree never appears again. Check for missing commas. The configurator displays the name of the script and the line number of the error.

2. You can use UserConf.mk to create a debug enabled binary with AVRGCC by appending

HWDEF += -g3 -gdwarf-2

3. If you need additional include files to be used by several of your applications, then create an include directory in your application tree and add the following line to UserConf.mk

INCFIRST += $(INCPRE)../include

4. Building Nut/OS fails with something like Unsupported target. Probably the settings in the Configurator's notebook and the component tree specify different compilers.



Harald Kipp
Herne, May 12th, 2005.