DOWNLOAD

CHANGES

SCREENSHOTS

DOCUMENTATION



WhatOS C Interface

Contents

Introduction

A system generates the following files:

wossystem.h
all defines, typedefs, and function prototypes, etc.
wossystem.c
the operating system containing tasks, memory manager, scheduler, etc.

The generated files can be used in many ways, for example:

The first section of this document deals with the external system interface provided by wossystem.h (inserting signals into the signal queue, processing signals, etc.). The second section deals with the task internal interface (everything that is available when writing the "process" of a C task, like accept(), emit(), etc.). The third section deals with various topics concerning the OS behaviour (scheduling rules, interrupts, etc.).

Compile-time configuration

A convenient method of providing these defines is by setting the system.header attribute.

Define the following to modify the debugging mechanisms used within wossystem.c:

#define WOS_DBG_SIM 1
Define as 1 if you intend to use the Python interface to wossystem.c (libwossimulation.c). TRACE/ASSERT messages will be routed in Python to the originator task. Do not define at the same time as WOS_DBG_SHELL.
#define WOS_DBG_SHELL 1
Define as 1 if you want TRACE and ASSERT to output messages to stderr. Do not define at the same time as WOS_DBG_SIM.
#define WOS_ASSERT_INTERNALS 1
Define as 1 to enable ASSERT testing of OS internals. This is highly recommended during simulation testing.
#define WOS_TRACE_<taskname> 1
Define as 1 to enable TRACE for task <taskname>.
#define WOS_ASSERT_<taskname> 1
Define as 1 to enable ASSERT for task <taskname>.
#define WOS_TRACEE_<taskname> 1
Define as 1 to enable TRACEE for task <taskname>. This is like TRACE but messages are just 8-bit integers, useful for embedded system debugging.
#define WOS_ASSERTE_<taskname> 1
Define as 1 to enable ASSERTE for task <taskname>. This is like ASSERT but messages are just 8-bit integers, useful for embedded system debugging.

If you don't use the Python interface to wossystem.c (libwossimulation.dll), you must define the following. Empty defaults will otherwise be used in wossystem.c.

#define wos_periodic()
Callback "function" called by the OS periodically. It permits other signals to be inserted into the signal queue; if any of those signals are destined to a task of higher priority the current task (if any) is interrupted. It is called in two places: (1) upon exiting a task after processing a single signal, (2) whenever a task process calls periodic (). (Please see Event loop for details.)
#define wos_time_get()
Return the current time as a wuint16 (used for CPU usage). How you do this is up to you. The time should overflow back to zero if it gets to 2^16.
#define hook_<signalname>(wuint8 sigid, void * sigval)
Called by the OS for every emission of signal <signalname>.

External interface

wossystem.h provides an external interface to the system.

Typedefs

target-dependent type definitions
e.g. typedef char wint8;
generic compiler-dependent type definitions
e.g. typedef char wchar;
user-defined type definitions
e.g. typedef struct Packet {wuint8 len; wuint8 data[8];} Packet;

Defines

all signal ids
(wuint8) e.g. #define wos_sig_foo (5)
all allocated type ids
(wuint8) e.g. #define wos_type_T (7)
WOS_ENVIRONMENT
(wuint8) A fictitious task id used when emitting/receiving signals outside of the system.

Macros/functions

wos_init()
Initialize the OS. You must call before calling anything else.
wos_process(wuint8 forever)
Feed signals from the signal queue to tasks. If forever is non-zero, return only if the next signal in the signal queue is destined to a running task or a running task group. If forever is zero, also return if there are no more signals in the signal queue. (Please see Event loop for details.)
wos_emit(wuint8 sigid, void * sigval, wuint8 source_task_id)
Insert a signal into the signal queue. Use WOS_ENVIRONMENT for the third argument.
void * wos_acquire(wuint8 typeid)
Allocate a new block of type typeid.
wos_reference(void * p)
Increase reference count for block pointed to by p.
wos_release(void * p)
Decrease reference count for block pointed to by p.

Internal task interface

Inside a C Task process you may use the following:

Defines

all local signal ids
(wuint8) e.g. #define foo wos_sig_foo
all local allocated type ids
(wuint8) e.g. #define t_T wos_type_T
all local allocated typedefs
e.g. #define local_T global_T
masks for all local input signals
(wuint16) e.g. #define m_foo (4)
this task id
(wuint16) e.g. #define wos_task_id (7)
task defines
All other defines found in the defines attribute of your task.

Macros/functions

input
(wuint8) The current input signal id presented to the task.
inputval
(void *) The current input signal value presented to the task.
inputsrc
(wuint8) The id of the task which emitted this signal. You should really avoid using this.
accept()
Return, accepting the current signal.
reject()
Return, rejecting the current signal.
await(wuint16 mask)
Specify that you want to receive only signals in mask, e.g. await(m_foo | m_bar)
require(wuint8 typeid)
Specify that you have attempted and failed to allocate a block of type typeid and you need to that block to process this signal.
emit(<signame>, (void *) sigval)
Emit a signal. Pass sigval = 0 if the signal is a pure signal. Note: <signame> has to be the actual name of the signal (e.g. emit(foo, 0)). It cannot be a variable (e.g. wuint8 x = foo; emit(x, 0);).
void * acquire(wuint8 typeid)
Allocate a new block of type typeid.
reference(void * p)
Increase reference count for block pointed to by p.
release(void * p)
Decrease reference count for block pointed to by p.
periodic()
Calls wos_periodic () to allow other signals to be processed. Call it when this task takes a very long time to process a signal and you need a faster system response time. The system response time is proportional to the maximum time between two calls to wos_periodic (). (Please see Event loop for details.)
TRACE<n>(const char* format, arg1, arg2, ..., arg<n-1>), n=1..8
Generate a trace message; use like the C printf function.
ASSERT<n>(expression, const char* format, arg1, arg2, ..., arg<n-1>), n=1..8
Generate an assert message if the test expression evaluates to false. Everything after the test works like a C printf function.
TRACEE(wuint8 v)
Generate a wuint8 trace message. Useful for small embedded systems.
ASSERTE(expression, wuint8 v)
Generate a wuint8 trace message if expression evaluates to false. Useful for small embedded systems.
MTRACEE(wuint8 v)
Same as TRACEE, but a Monitor task will convert the received value to a string representation by looking in the task's defines attribute.
MASSERTE(expression, wuint8 v)
Same as ASSERTE, but a Monitor task will convert the received value to a string representation by looking in the task's defines attribute.

Scheduling

When a signal is emitted, it is copied once for each destination task that it must reach and inserted into a signal queue. For example, if there are no destinations for a signal, it is not inserted in the signal queue. If there is one destination, a signal copy is inserted in the signal queue. If there are two destinations, two signal copies are inserted in the signal queue and so on.

For convenience, each signal copy in the signal queue will be referred to as a signal.

Priorities

The order that each signal is inserted into the signal queue is dependent on the priority of the destination task that it must reach. Traversing the signal queue from the head to the tail, a signal is inserted into the queue right in front of the first encountered signal of lower priority. A consequence is that all signals of the same priority are inserted into the signal queue in the order in which they are emitted.

The next signal

The next signal is the next signal in the signal queue to be presented to a task. All signals in front of the next signal (closer to the head of the queue) were rejected by the tasks to which they were presented and were skipped.

The next signal is adjusted according to the following rules.

  1. When a signal is inserted into an empty queue, it becomes the next signal.
  2. When a signal is inserted into the queue and that signal is being inserted in front of the next signal, the newly inserted signal becomes the next signal. It is assumed that all signals in front of this newly inserted signal would be again rejected if presented to their destinations, and therefore they are not presented (see Task rules and conventions).
  3. A signal can only be removed from the queue when that signal has been accepted by the destination. The destination task is regarded to have possibly changed state, now possibly being able to accept some of the signals which it previously rejected. If there are any signals in front of the next signal destined to the task which has just accepted this signal, the first of those signals now becomes the next signal.

Event loop

An event loop constantly presents signals to tasks. The most straight-forward way of setting up an event loop is using wos_process (), wos_periodic (), and periodic (). wos_process() is made to run forever, calling wos_periodic() as often as it can (see Response time). Below is the algorithm of this event loop setup:

#################### OUTSIDE OS ####################

main():
    wos_init()
    wos_process(forever = 1)

wos_periodic():
    if some event happened:
       # insert signal in signal queue
       wos_emit(event)

#################### INSIDE OS ####################

wos_process(forever):
    while 1:
        signal = signal_queue.next()
        if !signal:
            if !forever:
                return
            else:
                wos_periodic()
                continue
        task = signal.destination
        if task.isrunning or task.group.isrunning:
            return
        if signal.resource_needed and !resource.available:
            signal_queue.advance()
            continue
        if signal not in task.mask:
            # task does not want this signal now
            signal_queue.advance()
            continue
        task.isrunning = 1
        # call task with signal
        # task may call periodic() while executing
        accepted = task()
        if accepted:
            signal_queue.remove()
        signal_queue.advance()
        wos_periodic()

periodic():
    wos_periodic()
    if higher priority signal exists in signal queue:
        wos_process(0)

wos_periodic() exists because OS functions are not re-entrant. In other words, you cannot call wos_process() from an interrupt while wos_process() (or another OS function) is running. However, inside the wos_periodic() call-back you may call any OS function safely. wos_periodic() is called as often as possible: (1) it is called every time a task exits and (2) within a task, every time the task calls periodic().

Keeping in mind that OS functions are not re-entrant, you may set up different event loops; for example, you may call repeatedly wos_process(0) which returns when there are no more signals in the signal queue.

Interrupts

Calling periodic() allows another task of higher priority to start (and complete) before the current task continues. This permits two (or more) tasks to execute simultaneously.

Response time

The maximum time between wos_periodic() calls is critical in many applications because it limits the time it takes the system to respond to an external event. This may be reduced by calling periodic() in long-running tasks. The minimum time which can be reached is the OS latency (designed to be very small but not yet characterized). The OS maintains timing statistics about wos_periodic() calls (as well as CPU usage); this may be accessed using a Monitor task (please see Tutorial and Python Interface).

Memory manager

A reference counting memory manager exists. It manages allocation, reference counting, and deallocation of all allocated system types. It may only manage a fixed number of blocks of each type, determined at system generation time.

The basic functionality is as follows:

Task rules and conventions

Some of the conventions may be broken, with care, under special, well-defined circumstances.

Task groups

Optionally, multiple tasks may be assigned to the same task group. At the expense of a little flexibility, communication between tasks in the same group is more efficient, in terms of speed and RAM usage. Task groups are typically used to split up a task into smaller independent sub-tasks while avoiding the inter-task communication overhead.

Tasks in the same group behave somewhat more like a single task, although every effort is made to maintain the conventional behavior. Other than the differences mentioned below, operation follows the conventional behavior.

Rules

  • By default all tasks are in group zero, i.e., not assigned to any group. There may be up to eight groups.
  • A group internal signal is a signal emitted by a task in a group having as destination at least one other task in the same task group.
  • A group internal signal may not be emitted anywhere outside the task group.
  • A group internal signal may not have any destinations outside the task group.
  • All signals which are not group internal signals are routed using the conventional mechanism (signal queue, task priorities, etc.).
  • An autonomous task must be in an autonomous state when emitting a group internal signal, and a non-autonomous task must be in a non-autonomous state when emitting a group internal signal. That is, the task must be in it's "normal" autonomous state when emitting a group internal signal.

Routing of group internal signals

  • When a task emits a group internal signal, execution will immediately switch to the destination of the signal. All destination tasks will execute, in the current context, one by one, in an undefined order. Execution of the emitting task will continue after the completion of all destination tasks. During an emission of a group internal signal task priorities are not respected.
  • If there is at least one running task in the group, all tasks in that group are considered to be running, and no external signals will be presented to any task in that group, until the completion of all tasks.

Task API limitations

  • The result of accept/reject will be ignored. However, accept and reject should still be used in order to permit the task to be instantiated on its own, i.e. not part of a group.
  • await and require must not be called when responding to a group internal signal. Calling them causes undefined errors. Since await is not supported, input signal masks for group internal signals is also not supported.

Limitations

© Mircea Hossu () WhatOS 2.0.3  (2006 Feb 26)