AvrX Real Time Kernel

Theory:

AvrX is a deterministic priority driven fully pre-emptable task scheduler. Boy that was a mouthful.

Terminology

bulletPID - Process ID, small data structure that contains all information needed to execute a process.

Task Switching

The bulk of the task switching logic is in the two routines _Prolog and _Epilog. There are three data structures that are used:

_RunQueue   Pointer to the first PID in the run queue
_Running   Pointer to the current executing PID
_SysLevel   Count of number of time the kernel has been entered, -1=process stack

_SysLevel indicates whether the system is running user code or kernel code.  When _SysLevel = -1 the current stack is a task stack.  When an interrupt occurs, or the user code calls a kernel API, _Prolog save the current context on the stack.  Then _Prolog increments _SysLevel and then switches to the kernel stack if _SysLevel == 0. Subsequent interrupts, or calls to _Prolog will stack the context on the kernel stack and increment _SysLevel. Switching to the kernel stack involves reading the current SP and storing it in the PID pointed to by _Running. Then the SP is loaded with the top of the kernel stack.

_Epilog unwinds the kernel stack by popping off the previous context, decrementing _SysLevel as it goes, when _SysLevel goes negative it has to swap back to the Process stack. This is when a context switch actually occurs:  If _Running == _RunQueue, that means no context switch has been requested and _Epilog simply restores the registers saved by _Epilog (pointed to by _RunQueue). If _Running and _RunQueue are different, that means that a higher priority task became ready to run (either by being queued, or by the current task being blocked e.g. being removed from the _RunQueue). In this case _Epilog, again, just restores the context pointed to by the top of the _RunQueue.  The third case is if _RunQueue == 0, or is empty.  That means all tasks are blocked and the CPU is idle.  In this case _Epilog goes to a special task called the IdleTask.  Currently all IdleTask does is put the CPU to sleep waiting for an interrupt.

The designer of a software system might choose to have a low priority task that never blocks and call that the Idle Task.  It could halt the CPU or do whatever you want.  As long as it never blocks, the internal IdleTask will never get executed.

Queue

When processes are ready to execute, they are placed upon the _RunQueue in inverted priority order. Priority 0 will be inserted at the head of the queue. If there are multiple processes with the same priority, subsequent ones will be inserted behind all other equal processes. As processes run and block they will be re-queued at the end of the like priority task list and effectively be round robin scheduled.

A lower priority process can never interrupt a higher priority process. _Epilog simply looks to the head of the _RunQueue to pick a new process to run. However, if a lower priority process owns a resource needed by the higher priority process, say a semaphore being used to control access to hardware, then the higher priority process will block until the resource have been made available.

Once a process is running, it will run until it is either pre-empted by a higher priority process, or it must block, waiting for some resource.

Suspending a process

Because a "running" process may be blocked on a semaphore, and semaphores are not "known" to the kernel, suspending a process is not as simple as removing it from the Run Queue. AvrXSuspend marks a process for suspension and then attempts to remove it from the run queue. If it succeeds, it then marks it "SUSPENDED" and returns. If it is not successful, it simply returns. Later, when the process becomes eligible to be inserted back into the Run Queue if it is marked for suspension, the procedure _QueuePid will not queue it and will mark it SUSPENDED.  By definition, when _QueuePid attempts to put a task on the run queue the task cannot be waiting for anything and the semaphore associated with it will be cleared.

Blocked Tasks

A task is blocked when it is either suspended or queued onto a semaphore.  In other words, not queued on the RunQueue.  Because AvrX is intended for small systems, only a forward linked list is maintained.  This conserves SRAM by only needing one pointer.  Whenever inserting or deleting something from a queue, AvrX walks the queue to find the object being removed or the insertion point.  In general all queues (RunQueue, TimerQueue, Semaphores, MessageQueues and Messages) can have multiple items queued up waiting.  In this way a semaphore becomes a Mutex: if it is owned by a process, all other processes will queue up waiting for the owner to "Set" the semaphore, thus releasing the head of the queue.  Message queues can have multiple messages waiting for a receiver, or, multiple receivers (tasks) waiting for a message.  The RunQueue will have however many tasks are ready to execute, waiting their turn, queued.  As tasks block, or exit or are suspended, the next item in the RunQueue is then swapped into the CPU and things continue.

Timer Queue

The timer queue is a special case.  It implements a sorted list of adjusted timeouts such that the interrupt handler is only decrementing the first element at all times.  When it goes to zero, the code then signals the semaphore inside  the TimerQueue element.  If a task is waiting on that semaphore, then it is placed onto the RunQueue.  There is a further special case of TimerMessages, where instead of signaling the task, the TimerControlBlock is queued onto a message queue

The TimerHandler could have been implemented as a stand-alone task and timers could have been implemented as messages.  This actually would be pretty powerful but was not done because of the expense of two context switches (one for the timer handler task and one for whatever task becomes available to run) and the expense of another task context (~40 bytes of sram).  AvrX runs three to four tasks comfortably in 512 bytes of SRAM: using one of those tasks for the timer handler seemed too wasteful.

Fifo Support

AvrXFifo support are simple byte oriented FIFOs.  FIFOs are allocated as byte arrays with a const pointer of the correct type cast to the array.  FIFOs can be static or automatic, or, allocated from the heap.  FIFOs include semaphores to implement synchornization between tasks or between interrupt handlers and tasks.  The AvrXSerialIO sample code illustrates how this can be used.  Look here for more details