Firmware/Porting

From Nutwiki
Jump to: navigation, search

Nut/OS Porting Guide

Nut/OS is not just a kernel, it is a full operating system with peripheral drivers, file systems, network protocols etc. Porting it to a new platform is not trivial.

Fortunately the kernel is written in ANSI C and quite simple. In fact, only three basic functions need to be implemented for a new platform to get several of the sample applications running.

  • Hardware initialization
  • System timer
  • Console device

Hardware initialization is the most essential, but also the most complicated part. That's why we will start with the most easiest, the console device. Next we will implement the system timer and finally settle the initialization part.

This guide assumes, that you already have a tools available to create binaries from C and assembly source code. You should be able to build a simple application for your target board, like 'Hello world' or a blinking LED.

Large parts of the following text are still limited. They assume, that your new target CPU is a variant of the ARM architecture. I hope, this page will be still useful, even if your platform is different.

Further note, that this guide is based on Nut/OS version 5.0.

Console Device

This device will become our main user interface. While Nut/OS allows to use many different devices like LCD with keyboard or even network connections as its console, the preferred one is RS-232. If your target board doesn't provide it, I'd highly recommend to upgrade it. Almost all CPUs do have at least one UART and it is usually easier to add the required level shifter and connector, than trying to get an LCD working for fast text output.

The first thing you need to do is to write minimal C code to initialize the UART and send a simple text message to a terminal emulator running on your PC. But you need to do this without using any external UART library, that may have been included with your board. Your minimal UART code should have four functions:

  • void DebugInit(void);
  • void DebugWrite(const char *buffer, int len);
  • int DebugRead(char *buffer, int size);
  • int main(void);

Typically, the following parts are needed to implement DebugInit():

  • Select or enable the UART clock
  • Calculate and set the baud rate divisor
  • Configure the UART, like start, data, stop and parity bits
  • Enable peripheral pins

The datasheet of your CPU should provide all the details. The good news is, that polling mode is just fine. Do not use any interrupts. The most simple console device for Nut/OS is the so called debug device, which intentionally avoids interrupts. This makes the remaining parts, sending and receiving data, much easier. For DebugWrite() we need to

  • check the status register until the transmitter is ready to accept a new character
  • write the next character from the buffer into the transmitter data register
  • do this for each character in the buffer

DebugRead() is optional. If you are impatient, you can implement it later. In general, it will contain the following:

  • check the status register until a new character is available in the receiver
  • read the new character from the receiver data register and store it in the buffer
  • repeat this until the buffer is completely filled

You main program should then look like

int main(void)
{
    char key;

    DebugInit();

    for (;;) {
        DebugWrite("\r\nPress any key ", 16);
        DebugRead(&key, 1);
    }
}

If this works, we nearly have a first Nut/OS device driver, which will later allow you to use C stdio functions out of the box:

int main(void)
{
    int key;

    NutRegisterDevice(&DEV_DEBUG, 0, 0);

    for (;;) {
        printf("\r\nPress any key ");
        key = getchar();
    }
}

Before actually integrating your code into the Nut/OS source tree, let's take a look to the system timer implementation.

System Timer

The system timer is used by the OS for two main purposes. As you probably know, Nut/OS is a multithreading OS. Many threads will wake up in certain time intervals. They use NutSleep() to release CPU control for a specified number of milliseconds. Keeping track of the sleeping threads, Nut/OS uses the system timer to wake them up in time. This is the first purpose of the system timer.

Most threads will sleep until an external event occurs. However, many of them will set a timeout limit. If no event occurs within a given time, they will be woken up with a timeout error. This is the second purpose the system timer is used for. In fact, it is quite similar to NutSleep(), with the exception, that threads may be woken up not only by the system timer, but also by an external event.

Before implementing a final system timer for Nut/OS, let's create another minimal application to evaluate timer interrupts on your target board. This time we need three functions:

  • void NutTimerIntr(void *arg);
  • void NutRegisterTimer(void (*handler) (void *));
  • int main(void);

NutTimerIntr() is the timer interrupt handler of Nut/OS and part of the kernel. Because we are not yet using Nut/OS, we need to provide it in our minimal test program. The following code will initialize the timer and run in an endless loop, which is interrupted on every millisecond to increment a global variable:

unsigned long nut_ticks;

void NutTimerIntr(void *arg)
{
    nut_ticks++;
}

void NutRegisterTimer(void (*handler) (void *))
{
    /* Initialize the timer hardware to 1 millisecond. */
    /* ... */
    /* Set the interrupt handler address. */
    /* ... */
    /* Enable timer interrupts. */
    /* ... */
}

int main(void)
{
    NutRegisterTimer(NutTimerIntr);
    for (;;) {
    }
}

This looks a bit complicated. Actually, it is even more complicated than it looks. The tricky part is, that most platforms require special declarations of interrupt handlers, which are different from the handler given above, NutTimerIntr(). Most likely, the void pointer is not supported, because interrupt calls do not provide parameters.

The background is, that Nut/OS handles interrupts by a special framework, which allows to pass a single pointer to an interrupt handler. This way, a single interrupt routine is able to serve several similar devices. For example, only a single SPI interrupt handler is required to handle all SPI busses.

In a first step it makes sense to avoid the internal interrupt framework. Luckily this is possible, because

  • Nut/OS allows to use native interrupt handlers
  • The system timer interrupt only needs to increment the system tick counter
  • The system tick counter is a public global variable

The NutTimerIntr() function given above is exactly the same as the one in the Nut/OS kernel. As you can see, it ignores the pointer parameter. Now it should be much easier to implement the timer interrupt, right?

Again, the datasheet of your CPU should explain all the details. Note, that many recent CPUs offer a dedicated timer to be used by an operating system. If available, use it, because it is usually easier to handle than those general purpose timers.

Now we have two third of the basic requirements. In the next step we will integrate these into the source tree.

System Integration

Architecture Files
nut/
    arch/
        <ARCHNAME>
            board/
                <BOARDNAME>.c
            debug/
            dev/
                <FAMILYNAME>/
                    dev_debug.c
                    os_timer.c
            init/
                crt<LDNAME>.S
            ldscript/
                <LDNAME>.ld
            os/
                context.c
                nutinit.c

Where to put our working test code? I assume, that you previously looked into the Nut/OS source tree, where you probably discovered a subdirectory named arch, which is further divided into several subdirectories like avr, arm or unix. If your CPU is already part of one of these existing families, use its subdirectory. If not, you need to create a new one plus several new files and configuration items.

To make things easy for now, let's assume that you are porting Nut/OS to a new ARM9 CPU. Those ARM7 and ARM9 CPUs, which are currently ported, are quite similar and their code is found in subdirectory arm.

Let's get down one level deeper, where we find several more subdirectories, on of them is named dev. This looks like the right candidate for our debug device, but in fact it is also the right place for the system timer. By definition, Nut/OS treats the system timer as a device. Well, as you already noticed, we have more subdirectories below, like atmel, gba etc. If your CPU is manufactured by Atmel, then the first one should be the one to use. Unfortunately its name has not been chosen very well, atmel_at91 would have been a better name, because that's the CPU family that's actually handled by the drivers in there. It's easy to be wise after the event.

If the manufacturer or the family of your new target is new to Nut/OS, then create a new subdirectory here. We will later learn, how to tell Nut/OS about this addition. Before you start creating new source files, have a look into subdirectory zero first, which contains two files only:

  • dev_debug.c implements a debug device
  • os_timer.c implements a system timer

Zero is an imaginary CPU only, introduced in Nut/OS Version 5. It doesn't exist in reality and the files do not contain working code. The good news is, that both files are most useful as a template for implementing new platforms. Extensive comments will help, to insert the right code for your target. Copy these files to your new directory and fill in the missing code, using the two test programs that we created above.


Configuration Files
nut/
    conf/
        <BOARDNAME>.conf
        repository.nut
        tools.nut
        arch/
            arch.nut
            <ARCHNAME>.nut

Beside arch, there is a second subdirectory in the Nut/OS source tree, that is dedicated to hardware specific parts. It's named conf and is used by the Nut/OS Configurator. It also contains a prepared configuration file for an imaginary board (zero_ek.conf) with our imaginary Zero CPU, so you can use it for your new target as well. Simply create a copy, using the name of your target board. Now start the Nut/OS Configurator and select the newly created configuration file. In fact, all further configuration like memory size, tool chain etc. can be done in the Configurator. You can't? Almost all parts of the system are not enabled or refer to the Zero CPU? Your new CPU is not available? Don't panic, this can be fixed easily.

Beside the board configuration files, subdirectory conf contains a several files with the extension .nut, some of them in subdirectories below conf. All of them, including the board configuration files, are Lua scripts. You can use any text editor to modify them.

The first one we will touch, is conf/repository.nut. Search for MCU_ZERO, which is part of a Lua array named mcu_names. Simply add the name of your CPU to this list.

The next one is conf/tools.nut. Search for zero, where you will find a list of linker scripts. We didn't create a linker script yet, because we deferred hardware initialization. Anyway, as we are here right now, add a proper entry for your CPU. Note, that most platforms provide two, some even more entries. That's because different initializations are required, when the final binary should run in Flash memory or RAM, for example.


The rest is straight forward. Check conf/arch/arch.nut and conf/arch/arm.nut for entries of the Zero CPU and create a copy of these parts for your CPU, changing the hardware names accordingly.

Here is a template for a new CPU entry in conf/arch/arch.nut:

--
-- <MY NEW CPU>
--
{
    macro = "MCU_<CPUNAME>",
    brief = "Name",
    description = "Description of this CPU.",
    flavor = "boolean",
    exclusivity = mcu_names, 
    file = "include/cfg/arch.h",
    requires = { "TOOL_CC_ARM" },
    provides = {
        "HW_TARGET",
        "HW_MCU_ARM",
        "HW_TIMER_<FAMILYNAME>",
        "HW_UART_<FAMILYNAME>"
    },
    makedefs = { "MCU=arm9" }
},

And this is a template for the related driver entries in conf/arch/arm.nut:

{
    name = "nutarch_ostimer_<FAMILYNAME>",
    brief = "System Timer (<FAMILYNAME>)",
    requires = { "HW_TIMER_<FAMILYNAME>" },
    provides = { "NUT_OSTIMER_DEV" },
    sources = { "arm/dev/<FAMILYNAME>/os_timer.c" },
},
...
{
    name = "nutarch_<FAMILYNAME>_debug",
    brief = "UART Debug Output (<FAMILYNAME>)",
    description = "Polling UART driver for ...",
    requires = { "HW_UART_<FAMILYNAME>" },
    provides = { "DEV_UART", "DEV_FILE", "DEV_WRITE" },
    sources = { "arm/dev/<FAMILYNAME>/dev_debug.c" }
},

After re-opening the board configuration file in the Configurator, you should be able to select your new CPU. Building the system will fail, because the hardware initialization is still missing. This is our last task.

Hardware Initialization

Nut/OS does not require any special hardware initialization. However, in most cases you won't be able to build it or get it running without some modifications of the code, that you normally use when writing code without underlying operating system.

In a previous step we already added the name of a linker script to conf/tools.nut. When building Nut/OS, the compiler expects two files in the arch directory, based on this name.

  • arch/arm/init/crt<NAME>.S
  • arch/arm/ldscript/<NAME>.ld

The first one is an assembly language file and contains the so called C runtime initialization. Every program written in C language needs this, whether it uses and underlying OS or not. As you have done a few samples, one should already exist. Sometimes it is provided with the tools, sometimes it is part of the C library.

The purpose of the initialization code is to initially setup the hardware, like enabling clocks, configure chip selects, initializing SDRAM etc. Then the .bss segment that contains uninitialized global variables is cleared to zero. If the program starts from read-only memory, like flash, the contents of initialized variables is copied from this read-only memory to RAM. Finally a jump to main() is done.

One significant difference between a standard initialization and one, that fits with Nut/OS, is, that the jump to main() is replaced by a jump to NutInit(). Even without further knowledge of assembly language, you should be able to modify the initialization code accordingly.

The second file, the linker script, contains some black magic for most of us. Generally speaking, this file tells the linker, where to place different parts (segments) of the code. For example, code segments and constant variables may be placed in flash memory, but normal variables must be placed in RAM. Jump vectors are typically placed at the bottom or top of address range, and also the stack is often placed at a specific memory location. All this can be defined in the linker script. Like with the runtime initialization, you probably already used a linker script when building target binaries for your board.

The script refers to specific segment names. Some of them are defined by the compiler, some are user definable. Most of the latter are specified in the runtime initialization. On the other hand it is possible to define new symbols in the linker script as well, which are then referred to by the code. For example, in order to clear uninitialized global variables, the runtime initialization needs to know, where this segment (.bss) is located. Thus, the runtime initialization code and the linker script have strong ties. Beside these, the Nut/OS kernel may also refers to memory locations, which are typically provided by the linker script. For most platforms, only __heap_start is required, which specifies the first address of unused RAM. Nut/OS uses this memory area for its heap. If your default linker script doesn't provide it, you must add it at the proper place.

The explanation above is mainly based of what we have, when using the GCC toolchain for ARM CPUs. Depending on the target platform, other items may be required. You should additionally check arch/arm/os/nutinit.c.

Testing

You should now be able to build Nut/OS in the Configurator. After done that, create your first sample directory, selecting Build > Create Sample Directory in the Configurator's main menu. As usual, open a shell (command line) window, change to the new sample directory, make sure that your compiler tools are inlcuded in the PATH variable and run

make clean all

With our minimal patches, at least the following samples should run fine, when uploading the binaries to your target board:

  • caltime
  • editconf
  • events
  • simple
  • threads
  • timers
  • uart

We prefer to start with events, because it is quite simple and test the two main functions, context switching and timer. Since we do not have any other driver than the one that implements a console, all samples using the Ethernet interface should compile, but will not work yet.

What's Next?

As mentioned in the beginning, Nut/OS provides a large number of features, including a full TCP/IP stack, several filesystems and support for several low level devices like I2C or SPI busses.

I assume, that you will be most interested in TCP/IP. For this, you need to implement a driver for the Ethernet interface. The debug device driver that had been created above, was a polling driver and didn't require any interrupt handling. This is an exception, most other drivers must be able to handle interrupts. Of course, it it always possible to skip the Nut/OS interrupt framework by using native handlers. The disadvantage is, that such drivers are always hardware dependant. You must place the code in nut/arch/<ARCHNAME>/dev and it will not be available for other platforms.

This is quite common with network drivers, but a few are already done in a hardware independent way. You can find them in the directory nut/dev/. If one of them fits, you need to adapt the interrupt framework to your architecture as well as some GPIO functions. How to do this, will go beyond the scope of this document. Best, try to follow the code that had been done for other platforms. By the time we will hopefully have more guides to help.

Good luck,
Harald Kipp
Castrop-Rauxel, 24th of October 2011