WhatOS C Interface
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:
- Compile wossystem.c with a cross-compiler and embed into the target
of your choice.
- Compile wossystem.c natively, link against your object
files/libraries and use it in any way you like.
- Compile wossystem.c natively to libwossystem.dll, link with
libwossimulation.dll, import libwossimulation in Python, and interface
to your system through a very rudimentary C/Python interface.
- Compile wossystem.c natively to libwossystem.dll, link with
libwossimulation.dll, call system.dllload(), and watch all C and
Python tasks communicate.
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.).
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>.
wossystem.h provides an external interface to the system.
- 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;
- 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.
- 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.
Inside a C Task process you may use the following:
- 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.
- 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.
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.
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 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.
- When a signal is inserted into an empty queue, it becomes the
next signal.
- 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).
- 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.
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.
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.
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).
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:
- Acquire a new type block. This new block has reference count 1.
- Reference an existing type block. The block's reference count will
be increased by 1.
- Release an existing type block. The block's reference count will be
decreased by 1.
- All blocks with reference count 0 are not allocated and may be
acquired.
Some of the conventions may be broken, with care, under special,
well-defined circumstances.
- When a task rejects a signal, it may not change state based on that
signal.
- An allocated valued signal has its value released automatically as
soon as it is emitted. A task should consider both the signal and
its value "out of its hands" once it emits it.
- A task receiving an allocated valued signal should only use the
value as read-only. This is because multiple tasks may have received
that value.
- A task receiving an allocated valued signal gets a reference to the
signal value automatically. The task should release that value as
soon as it can, in order to free up system resources.
- A task should not be keeping references to memory blocks which it
does not require.
- A task emitting an allocated valued signal can allocate that value
before-hand and "prepare it for some time", before emitting it.
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.
- 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.
- 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.
- 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.
- a system may have up to 128 tasks
- a system may have up to 256 signals
- a system may have up to 255 types
- a task may have up to 16 inputs