AvrX Real Time Kernel

Home
Up
AvrX Fifo
Overview
Theory
Getting Started
AvrX 2.6
AvrX 2.3 IAR
Mail

Getting Started

The easiest way to get started is to build the kernel with one of the samples or test cases.  After that has been verified to work, strip the file down, rename it and add your own code. 

The top level file, in all but the most trivial cases, like the test cases, should contain the at least the following sections:

bulletAvrXTimerHandler interrupt handler
bulletOne or more internal or external TASK definitions
bulletCPU_Reset routine or main(void)

The last item needs to perform any tasks necessary to prepare the system for running.  These are at least the following items, of which the first three are done for you by the C compiler runtime:

bulletSet the hardware Stack Pointer
bulletClear SRAM
bulletClear Registers
bulletInitialize tasks structures (AvrXInitTask or AvrXRunTask)
bulletInitialize hardware (Timer0 or 1, ports, serialio, whatever.)
bulletJump to the routine Epilog().

The last instruction in the startup code needs to be a jump to Epilog.  That will start the scheduling by switching the running context to the first item on the _RunQueue.  If the monitor is included in your program, it should have the highest priority (0) and will be the first thing to run. 

Typically user code would reside in separate files and just the Task Control Blocks (TCB) would be imported into the top level file.  For AvrX 2.6 the C macro AVRX_EXTERNALTASK does the importing.  Please refer to the header files (avrx.inc or avrx.h) for specific details on how each macro works and where they apply.

The startup code runs under the stack set up by C runtime, or where ever you place it, in the case of assembly.  This location is essentially the stack that AvrX will use as the kernel stack.  So, everything done in the main() or reset routine is considered to be running in the kernel context.  At least one task needs to be prepared to run with  AvrXRunTask() before exiting the reset code.  If no tasks have been prepared, Epilog() will find the RunQueue empty and will enter the idle task permanently.

Although I have not done this, it would be possible to start up in the idle mode and have an interrupt handler "AvrXRunTask() a task.  A more reasonable situation would be to run all your tasks and have them do whatever initialization they require and then block on something (timer, semaphore or message)

Optional Stuff

At a minimum, AvrX is simply task initialization and semaphore support.  Single Step, Timer Queue Management and Message Queue Management are all optional services that can be left out if not needed.  The resulting kernel is quite small without these services.  Here are rough sizing for various kernel functions

bulletBasic tasking and Semaphore Queue Management:    ~670 bytes
bulletTime Queue Manager:        ~236
bulletMessage Queue Manager:    48
bulletMiscellaneous (Singe Step, Advanced Tasking):    200
bulletDebug Monitor: ~1300 bytes.
bulletFifo support (written in C) ~300 bytes

Without the debug monitor the total size of AvrX for GCC is only ~600 words (1200 bytes) or 14% of the code space of an 8515.

There is no fundamental reason that timer support has to be included in your AvrX application.  The timer Queue Manager is just one implementation of a mechanism to allow multiple competing tasks to schedule time delays.  For simple applications one might simply have an Real Time Clock interrupt signal a semaphore and have a process that encapsulates all the time dependent stuff.  With every tick, the process will run, do work and, optionally, set other semaphores to signal other processes that it is time to do work.  Alternatively, it could send messages. If your timing requirements are modest, a simple task, or even interrupt routine, might well be more efficient in both code space and processor cycles.

The reason that Message Queue Manager is so small is that many functions are already provided by the basic tasking/semaphore module.  Message queues are a really simple concept that derives from the power of queueing semaphores.

Structure of a task

Although not absolutely necessary, a task typically is a routine with an entry, some initialization and then an endless loop.  The endless loop typically involves blocking, or waiting, on a semaphore.  That might be explicitly as in the case of AvrXWaitSemaphore, or it might be implicit in the case of AvrXWaitTimer or AvrXWaitMessage.  These last two items actually bock on a semaphore embedded in the timer or message data structure.

There are some data structures that need to be defined along with the code.  Most of the work can be avoided by using handy macros defined in AvrX.inc or AvrX.h, depending upon the version being used.

Below is a simple example of a task that simply blocks on a timer and signals a semaphore.  This is code for AvrX 2.5, the GCC compiler.

Mutex Timeout;

AVRX_TASKDEF(myTask, 10, 3)
{
    TimerControlBlock MyTimer;

    while (1)
    {
        AvrXDelay(&MyTimer, 10); // 10ms delay
        AvrXSetSemaphore(&Timeout);
    }
}

The macro, AVRX_TASKDEF, takes three arguments: the task (procedure) name, the additional stack required above the 35 bytes used for the standard context, and the priority.  It builds all required AvrX data structures and declares the C task procedure.  Please refer to the file AvrX.h for details.

Structure of an Interrupt Handler

Interrupt handlers have to have a specific name associated with them for the GCC compiler to set the appropriate interrupt vector name.  Look at the avr-gcc file sig-avr.h for a list of possible vectors.  AvrX completely handles the saving and restoring of the interrupted context so you DON'T want to use the GCC procedure qualifiers SIGNAL or INTERRUPT.  Instead use the following:

AVRX_SIGINT(SIG_OVERFLOW0)
{
    IntProlog();             // Switch to kernel stack/context
    EndCriticalSection();    // Re-enable interrupts
    outp(TCNT0_INIT, TCNT0); // Reset timer overflow count
    AvrXTimerHandler();      // Call Time queue manager
    Epilog();                // Return to tasks
}

IntProlog() does not re-enable the interrupts in an interrupt handler (this is different from AvrX 2.3) so if you want to be able to nest interrupts you need to explicitly enable them in your handler.  Also, beware, some sources of interrupts are not cleared by servicing the interrupt handler, so you need to clear them BEFORE enabling interrupts or you will endlessly re-enter your code and blow your stack.  The serial UART handlers are a good example of this.  Check the serial I/O files to see how this is handled.

AvrX 2.3 enables interrupts by default in IntProlog(), so you need to clear any pending interrupts BEFORE calling IntProlog().

Structure of your main() code:

The main() code simply initializes hardware and any tasks and then jumps to Epilog().  Here is the main() for the sample file "messages.c"   (Please refer to the actual sample as coding styles have changed since this was originally written)

void main(void)                // Main runs under the AvrX Stack
{
    outp((1<<SE) , MCUCR);     // Enable "Sleep" instruction for idle loop

    outp(TCNT0_INIT, TCNT0);   // TCNT0_INIT defined in "hardware.h"
    outp(TMC8_CK256 , TCCR0);  // Set up Timer0 for CLK/256 rate
    outp((1<<TOIE0), TIMSK);   // Enable0 Timer overflow interrupt

    outp(-1, LED-1);           // Make PORTB output and 
    outp(-1, LED);             // drive high (LEDs off)

    AvrXRunTask(TCB(task1));
    AvrXRunTask(TCB(task2));
    AvrXRunTask(TCB(Monitor));

    InitSerialIO(UBRR_INIT);    // Initialize USART baud rate generator

    Epilog();                   // Switch from AvrX Stack to first task
}

Determining the size of various stacks

For AvrX 2.6 the task stack needs to be 35 bytes + any additional stack needed by the task code.  The C compiler makes pretty heavy use of the stack when calling procedures.  It just depends upon how many automatic variables are used in each procedure.  The best way to determine proper stack sizing is to allocate lots of stack (say, 70 bytes) and run your application for a while to see how deep the stack gets.  Since GCC zeros all memory during startup it is pretty easy to tell. However, to be safe, use the emulator or debug monitor to write a few words of 0xFFFF near the perceived end of the stack to verify exactly how deep it gets.  Then, allocate a couple more bytes (at least one or two words) extra to insure you don't run over onto another stack or data structure.

Determining the kernel stack size is a little easier.  AvrX doesn't consume much stack in it's normal operation.  Perhaps 4-6  bytes at the most.  However, the kernel is re-entrant and will stack a full context for each interrupt that is nested.  So, you need to multiply the total number of interrupt sources (assuming they are all active at all times) by 35 and add any additional stack used by the interrupt code + 4-6 bytes used by AvrX to get a rough idea of the total stack needed.  Of, course, if your application has an interrupt that doesn't use AvrX (e.g. no IntProlog()/Epilog()) then it only stacks whatever it uses onto the kernel stack or the user stack.  In either case, use the above procedure to determine the exact size: allocate extra space, run your application for a while (exercising the interrupt sources) and see just how deep the stack gets.  You might have to deduce the maximum possible depth as some interrupt sources might not happen often enough to cover all possible cases.  For high speed stuff (e.g. serial link dumping lots of data, basic timer for the clock) usually all possible combinations will be covered within a short while.