Design and implementation details of the executive
Posting Events
Posting events is done by the function execPostPriorityEvent (execPostEvent is a macro which calls execPostPriorityEvent with the default priority for the event).
This must work whether it is called from a state transition function or an interrupt, including interrupts of higher priority that have interrupted other interrupt routines. The posting function therefore has to lock out access to the event queue (by suspending interrupts) during the critical parts of this operation.
In order to prevent the “event retrieving” part of the executive from having to do excessive searching, the posting routine also sets up a request to switch to a higher priority, if necessary. This action also requires a resource lock.
Hence, the following global variables are maintained:
byte execRequestedLevel; // Requested priority level
byte execCurrentLevel; // Current priority level
byte execCurrentStateMachine;
byte execNextState;
Retrieving Events
Retrieving events is done by polling the queues in order of priority, starting with the level requested by the event posting routines. If there are no outstanding events at that level, then the priority moves down one level, and the same exercise is repeated until there is nothing outstanding, at which point the executive enters into idling until an event is posted.
The sequence of processing an event is as follows:
- Read the event number, and use it to look up the state machine and local event number.
- Set the (global) state machine number to the retrieved value.
- If the current state number is zero (disabled), do not process this event, just call the trace function and remove the event from the queue.
- Otherwise, from the state machine and local event numbers, find the state transition.
- Set the (global) next state value from the number in the state transition.
- Call the state transition function (if there is one), passing the appropriate variables.
- Call the user’s trace function with the appropriate parameters.
- If the current state is not zero (i.e. disabled), set the current state of the state machine to the next state value.
Note that the event queue needs to be locked when retrieving an event.
Note also that the general rule is that state machines should manipulate themselves, primarily by making the transitions defined in the state transition tables in response to events. There is provision to override default actions, however.
Handling Timers
The timer functionality requires an interface to a (16-bit) timer hardware resource, and this interface is provided by the user (application).
The intended implementation is that the timer should be set up to run for a given length of time before it issues an interrupt, and which point the timer is set to run for a new length of time, or disabled if the timer function is not in use (for the time being). It is intended that the timer is a continuous free-running timer which wraps round at FFFF. A compare register can then be set up to whatever value is required.
An alternative implementation involves the use of a timer tick interrupt. In this case the timer value is maintained in software, and the timer comparison is also done in software. This kind of implementation is typically used when the scanning of inputs coincides with the timer tick.
The main timer may count up, or it may optionally count down (define COUNTDOWN when compiling).
Internal Data Structures
Event Trace Enable Table
This is defined as follows:
byte execTraceTable[(TOTAL_EVENTS/8) + 1];
This is actually a table of bits where a ‘1’ corresponds to tracing on that event number to be enabled. Bit 0 is a global trace enable bit and this must be 1 for tracing on any of the other events to be performed.
This table is located in RAM and is controlled by debug/diagnostic facilities in the application.
When an event occurs and tracing for that event is enabled, the user defined function execAppTrace is called (see section 2.2.1.7).
Current State Table
This is defined as follows:
byte execCurrentState[STATE_MACHINES];
This is defined in RAM and is used by the executive to maintain the current state of each state machine.
Timer Queue
This is defined as follows (and stored in RAM):
struct execTimerDef
{
word TriggerPoint;
word or byte EventToPost;
word or byte PreviousNumber;
word or byte NextNumber;
};
struct execTimerDef execTimer[TOTAL_TIMERS];
All events are single shot, i.e. they disable themselves once they have triggered.
The timer system is implemented using a single (word-sized) main timer which counts up (or optionally down). When a timer is started, the time delay value is added to the main timer value and stored as the trigger point. The event is then posted when the main timer reaches the trigger point.
The timer system is implemented using a doubly-linked-list queuing mechanism, chronologically ordered. Setting a timer adds an entry to the queue, and it is removed either when the timer times out or when it is killed.
Data Queues
A data queue is defined as follows (and stored in RAM):
struct execDataQueue
{
word or byte Tail;
word or byte Count;
byte Data[];
};
Each element of data in a queue is preceded by a size byte, which contains the number of bytes in the data element which follows it.
The data part of each queue is a circular queue, able to contain variable length elements. Thus buffers are copied in and out of a queue and cannot be accessed directly.
Thus, a data queue is effectively a chain of buffers as follows:
Tail | Count | 1st size | 1st buffer … | 2nd size | 2nd buffer … | 3rd size | 3rd buffer … | … |
---|