The goal of HiDEOS is to make it simple to create communicating processes which run on a stand alone processor. The processes can be strictly algorithmic, such as running a high level communications protocol, or device drivers, where they communicate with a specific piece or hardware. In order to accomplish this goal, all HiDEOS tasks are created by deriving from a class which knows all about interprocess communications and operating system resources. The primary interprocess communication method is message passing; HiDEOS contains tools for defining messages to the system and base class methods for sending them from process to process. The HiDEOS build tree contains an easy to understand makefile structure for making HiDEOS executables. The tree imposes structure on the creation of messages and tasks, making it simple for the user to incorporate new programs into the system.
HiDEOS is a multitasking operating system, but differs radically from other systems such as Unix. Processes operate within their own context similar to Unix, except they all share the same address space. HiDEOS uses an object oriented approach to process creation. Special classes exist in the system for context/task control. By deriving from one of the special classes, the user is automatically implying that upon construction of an instance of the derived class, a new process will be created. To reiterate, any user class derived from the special task classes will create a process when an instance of the user class is constructed.
Processes within HiDEOS are identified by name. The name is an arbitrary character string assigned by the user at the time of construction. A name registry object exists in the system to maintain the list of available processes in the system. Since all processes created in the system are actually objects, a simple handle (or pointer) to that object can be used to access the public methods of the object and control the major facilities of the process. Only one instance of the name registry is present in a running HiDEOS. The name registry is just a mapping between character string name and process object pointer. The registry provides methods for querying names. The name registry is a globally accessible object, every process in the system has access to it. The main purpose of this facility is to allow other processes, either local or on another cpu, to locate available services by name and communicate with them.
The main interprocess communication facility for HiDEOS is message passing. A HiDEOS message is a user defined class derived from the message base class. All message types in HiDEOS must be derived from this message base class. Each message type in HiDEOS is assigned a unique integer ID for easy identification. The unique ID is automatically assigned by a utility at HiDEOS build time. The ID must be used to identify messages when they are received by user processes. A special message buffer management class exists for allocating and freeing all message buffers. The user requests message buffers from the manager by message type ID. The message buffer is freed using by passing the pointer to the previously allocated message to the message buffer manager. The message buffer manager maintains free lists of all message types in the system to prevent memory fragmentation and facilitate user requests for buffers.
In a similar fashion to task creation, user classes inherit the ability to send and receive messages by deriving off a message passing task class. The message passing task object incorporates an event driven model. All message passing task objects contain an inbound message queue and methods of the user's derived object are automatically invoked in the user's context by the presence of messages in the queue. When a message is received and the process awakes, the user must decode the message type and perform the actions associated with that message. The rule of message passing in HiDEOS is that the receiver of the message owns the buffer and is responsible for freeing the buffer.
Task dispatching in HiDEOS is currently strictly round-robin with all processes running at the same priority. The default scheduling clock frequency is 60Hz. The time slice for processes is 166ms. They values are simple to adjust to suite various applications. Any process running in the system can choose to disable the dispatches, this feature allow the user to take over the entire CPU and be assured that the running process will not be preempted. The dispatcher provides for watchdogs. Users can request watchdog services from the dispatcher.
All board support is accomplished in HiDEOS using classes. All the chips available on the board have a class associated with them for control. The class allows the user to access and control chips with methods such as TurnOnInterrupts() and GetRamSize() instead of using macros, bit masks, and unfriendly structures. A resource manager exists for locating the chip classes. Services which are available across many chips are controlled through special classes. An example of a services such as this are tick timers. Tick timers are available from the VMECHIP2 and the MCCHIP of the MVME162. At initialization time, the VMECHIP2 constructor and MCCHIP constructor register their tick timers with the tick timer control class. User can ask the tick timer control class for a general tick timer, not caring or knowing that chip is came. The general tick timer contains methods common to all tick timers such as SetInterruptHandler() and ClearCounter().
A running HiDEOS system contains a set of classes managing all aspects of operating system resources. There is one instance of a dispatcher class, one name registry, one message buffer manager or message pool, and one instance of a chip class for each of the supported chips such as the IPIC, VMECHIP2, and MCCHIP of the MVME162 board. There will be an instance of the task class for each of the running processes in the system.
#include "msg/message.h" class LongMsg : public Message { public: long x; long y; };All messages header files are found by a set of HiDEOS utilities (explained in the next section) at build time. The utilities generate several files to help HiDEOS work with messages efficiently. All messages are assigned an integer tag for identification, all messages are sent and received using this tag. The utilities generate a simple enumerator for each of the tags (messages) found. The enumerator is always the name of the class follows by the word "Type". In the above example, the tag is some unknown integer and the message is identified by the enumerator LongMsgType. If the user process was to receive a LongMsg, the only way to know this is to check to see if the type is LongMsgType.
HiDEOS manages messages, they are not managed by the malloc/free mechanism. The integer tag is used to manage message free lists for each message type. The user must request and release message buffers using the HiDEOS message pool management system. The message pool system prevents memory fragmentation and facilitates quick message allocation and release.
There are several considerations which must be made before a new message is introduced to the system. Understanding the device for which the message is to be defined is very important. If there is already a device similar to the one being added such as an ADC, an interface to ADCs may already be established and no new messages are needed. Adding a second message interface for the new ADC could cause headaches for users, instead of all ADCs looking similar, the new one looks different from the user standpoint. The first rule is to to check the existing messages and see it one of them matches the needed interface. The second important consideration is methods within messages. The message should be very much like data structures. Try to keep methods to simple inline routines. Prevent actual functions to be generated to run the methods of the message class. The reason for this is simple, messages cross CPU boundaries, the implementations of facilities such as malloc/free and even bit order may be different. If the message contains a complex method to manipulate a chuck of data in the message, the originating machine's code for the method may be quite different then the destination machine's code. An example of this is a method when vxWorks is used as one CPU and the MVME162 CPU is used as a second CPU in stand alone mode. If a method exists which allocates a buffer and another which frees it, the vxWorks CPU could allocate it off it's private heap, and the MVME162 CPU could free it into it's own heap, this will probably could the system to crash at some point.
To summarize, messages are user defined structures derived from the message class. Message are searched for in header files during the build process and assigned tags and enumerator for the HiDEOS message pool system. The user processes use the enumerator to identify message and the allocate and free message from the message pool. Keep messages simple and short, and see if a message exists in the system before generating a new one.
Normally a subclass of the Task class will minimally define a constructor and the Receive(Message*) method discussed above along with private data that defines the users state. The subclass will utilize methods of the base to manipulate operating system resources.
#include "task.h" #include "long_msg.h" class AdderTask : public Task { public: AdderTask(const char* process_name); void Receive(Message* msg); private: int counter; }; AdderTask::AdderTask(const char* process_name) { counter=0; } void AdderTask::Receive(Message* msg) { switch(msg->Type()) { case LongMsgType: LongMsg* lm = (LongMsg*)msg; // cast to known type lm->x=counter++; // set the return value Send(lm->from,msg); // reply to source break; default: Task::Receive(msg); break; } }This process will block until messages appear at the input queue. The message will be processed by the user process if the type is LongMsgType. The only actions performed here are to assign the counter value to the x variable of the message, increment the counter, and response to the sender of the message.
This second task will connect to the above AdderTask and perform a transformation on the value returned from the AdderTask as its function. This task assumes that an instance of AdderTask is running in the HiDEOS system and has been registered with the name "adder_task".
#include "task.h" #include "long_msg.h" class TransformTask : public Task { public: TransformTask(const char* process_name); void Receive(Message* msg); private: Task* connected_task; }; TransformTask::TransformTask(const char* process_name) { if(Bind(connected_task,"adder_task")<0) { printf("adder_task not found!\n"); connected_task=NULL; } } void TransformTask::Receive(Message* msg) { switch(msg->Type()) { case LongMsgType: LongMsg* lm = (LongMsg*)msg; // cast to known type // get a message buffer to send to the connected adder task LongMsg* to_adder_msg = (LongMsg*)GetMessageBuffer(LongMsgType); // send the message to adder task if(connected_task) Send(connected_task,to_adder_msg); // block until message from adder task arrives to_adder_msg=WaitForMessage(connected_task,LongMsgType); lm->x=to_adder_msg->x * 5; // perform a transformation // free the message buffer received from the adder task FreeMessageBuffer(to_adder_msg); Send(lm->from,msg); // reply to source break; default: Task::Receive(msg); break; } }This example illustrates many of the important features of the task base class. Bind() is used to get a handle to a specific adder task. The TransformTask sends a message to the adder task each time it is asked to process a message. Remember that it is the responsibility of the receiver of a message to free the buffer if it is not reused, which is the case with the to_adder_msg receiver from the adder task.
Certain types of devices fit the task group model very well. An Industry Pack such an the Octal Serial contains eight serial ports, each which required a unique thread of control (a process). A special Octal Serial initialization function creates eight OctalSerialTasks (a subclass of task) in the form of an array of pointers and registers the array with the name "OctalSerial". Now users can reference the ports using notation such as "OctalSerial[0]" for port 0, and "OctalSerial[4]" for port 4. When using task groups, the task class constructor with no name will probably be used to delay the names from being registered.
The task group is considered an advanced feature and will be explained in an Advanced Features Document.
// globally constructed entity instance for my initialization function static Entity my_test("user_init",MyInitialization,NULL); static void MyInitialization() { AdderTask* adder = new AdderTask("adder_task"); TransformTask* trans = new TransformTask("trans_task"); }The task constructor with name is used here to automatically register the task name with the system task registry. Users can bind to the "trans_task" or "adder_task" for communications. In all the examples provided in the distribution, there is one Entity or initialization function per HiDEOS executable.
The I/O space is generally registers that control the device. They are always device specific; the device manufacture is free to allocate the space as they wish. For many industry packs, all the registers needed to operate the device are located here.
The ID space is a PROM. The first 22 bytes are defined by the industry pack specification, the remaining 42 bytes are deviced by the industry pack module designer. The fixed portion of the PROM contains information such as the make and model of the industry pack, and revision numbers. The device specific portion usually contains information such as calibration values or error values. The ADC is an example of where the error values are stored in the ID space immediately following the fixed portion.
Each industry pack slot can generate interrupts on two interrupt request lines: IRQ0 and IRQ1. Vectored interrupts are fully supported. Normally the interrupt vector is programmed into the industry pack I/O or memory space if vectored interrupts are supported.
A very simple industry pack driver task for a ficticous ADC will be shown below as an example. The ADC has two independent channels implemented with three 16 bit registers defined in the I/O space. The first register is the current value at the ADC channel 1, the second register is the current value at ADC channel 2. The third register is a global reset, which resets the industry pack.
class FakeAdcIP : public Task { public: FakeAdcIP(const char* name, IpModule* slot, int channel); void Receive(Message*); static Task* InitFunc(const char* name, IpModule* slot); private: unsigned short* value_reg; IpModule* ip; }; FakeAdcIP:FakeAdcIP(const char* name,IpModule* slot,int channel) : Task(name) { unsigned short* regs = slot->GetIoSpace(); printf("Fake ADC %s found in slot %c\n",slot->GetSlot()); ip=slot; value_reg=&(regs[channel]); } void FakeAdcIP::Receive(Message* msg) { switch(msg->Type()) { case LongMsgType: // only interpret long messages LongMsg* lm = (LongMsg*)msg; lm->value=*value_reg; Send(msg->from,msg); break; default: Task::Receive(msg); break; } } Task* FakeAdcIP::InitFunc(const char* name, IpModule* slot) { unsigned short* regs = slot->GetIoSpace(); char* name0 = new char[strlen(name)+5]; char* name1 = new char[strlen(name)+5]; // The name parameter is the manager assign name. // Build unique names for each channel, an alternate is to use // task groups. See the serial drivers for examples of using task groups strcpy(name0,name); strcat(name0,"-one"); strcpy(name1,name); strcat(name1,"-two"); regs[0]=1; // reset the industry pack Task* t0 = new FakeAdcIP(name0,slot,0); // task for channel 0 Task* t1 = new FakeAdcIP(name1,slot,1); // task for channel 1 return t0; }As can be seen, the InitFunc has the job of creating a task for each channel of the industry pack. This example illustrates that the argument list to the task constructor is designed to fit the application. In this case, the constructor need the name, the IP controller, and the channel number. As mentioned in the comments, a better way to manage the registration of the tasks is to use the task groups. By doing this, only one name, name will be registed with two instances or tasks. One advantage of using the task group is that uses can use the notion such as "a-fakeADC[0]" and "a-fakeADC[1]" instead of "a-fakeADC-one" and "a-fakeADC-two".
The ip_modules.h file contains defines for the make/model and name of the industry pack being added to the system. The make/model codes can be found in the book received with the industry pack. The name is assigned by the user. When this industry pack is found in a slot, the name will be appended to the name assigned by the manager. The manager naming convention is to prepend the slot identifier to the user assigned name. If the name of the fake device above was "FakeADC" and a fake ADC was found in slot 'd', then the manager assigned name would be "d-FakeADC".
The ip_modules.cc file contains the database of all the recognized industry pack (ones that have drivers) and simple functions for creating the drivers. Adding a driver requires a new entry in the database and a new function be added to this file.
Defining the above fake ADC example to the system in ip_modules.h (the assumption is that Green Springs made the industry pack):
#define GSIP_FAKE_ADC 0x45 // this is the model definition #define GSIP_FAKE_ADC_N "FakeADC" // the user assigned name
Defining the above fake ADC example to the system in ip_modules.cc:
static Task* IpFakeADCMake(char* name, IpModule* ip) { return new FakeAdcIP::InitFunc(name,ip); } IP_TASK_MAP IpTaskMap[] = { { ... } { ... } { GREEN_STRING_ID,GSIP_FAKE_ADC,GSIP_FAKE_ADC_N,IpFakeADCMake,0 } { NULL } };See ip_task_map for the definition of the IpTaskMap. Here the new entry describing the make and model of the new industry pack is added to the data structure. The manager will call IpFakeADCMake when the make/model is found in an industry pack slot and append GSIP_FAKE_ADC_N to the name it assigns. The function IpFakeADCMake knows how to call the InitFunc() defined as a static member function of the example driver.
The backplane task uses mailbox interrupts to inform other CPUs of pending messages. The task maintains a pool of fixed length buffers which it copies message data to and from. The special buffer pool uses semaphores which are backplane safe. The current implementation does not fragment messages, so a transfer will fail if the message does not fit into one of these fixed size buffers. Developers are encouraged to enhance this interface.
The BPD is a class that can send and receive HiDEOS messages. The BPD can also get task descriptors, which are used in a similar fashion to Unix file descriptors. Each task descriptor is bound to a particular HiDEOS task and is used in calls to send messages for identifying the destination of the message.
The BPD can be used is a synchronous mode or an asynchronous mode. The synchronous mode is the default mode, the assumption here is that the task will block when the BPD is asked to wait for a message to appear at the BPD. The asynchronous mode utilizes a user registered message receive function. In this mode, the user function gets invoked each time a message is destine for the BPD, it is up to the user function to process the message (queue it?) and inform the task owning the BPD that a message has arrived.