REXIS (V2): The Subsumption Architecture OS

Subsumption Architecture was defined by MIT’s Rodney Brooks as a new method of building intelligent systems based on reactive behavior based control. While 20+ years later, the promise of the subsumption architecture has not produced an intelligent robot yet (although I suppose we don’t *really* know the full capability of the PackBots used by the military 🙂 ), the subsumption architecture remains an enticing theory for building control systems. With the ever increasing use of embedded micrcontrollers and sensors, the subsumption architecture becomes an appealing choice for such system due to the following properties:

  • The architecture is inherently multiple process and processor friendly. This is useful as the number of control units increase.
  • In real world, messages are inherently unreliable and missed deliveries are possible (e.g. imagine a noisy wi-fi network). The subsumption architecture design are divided into levels of competence, providing a high level of robustness in unreliable message delivery environment.

Our first commercial product was REXIS (Real time Executive for Intelligent Systems), a C library implementation of the subsumption architecture. It is a multitasking RTOS with support for subsumption calls. I am now designing V2 of this system.

To make the system as user friendly as possible and to obtain the best performance, the system consists of:

  • a RTOS (real time OS) with subsumption API, plus traditional API such as semaphores.
  • a language preprocessor.

The source file is a C source file decorated with the REXIS language elements, which are prefixed by the @ sign. Some are commands and some are used in place of C expressions. The full power of C is available since the REXIS elements are just small part of the source program.

The core construct is a “behavior” and a set of input and output ports. Message passing between ports is done through a (virtual) connection wires. Typically, a set of cooperating behavior implements a layer of control. The power of subsumption architecture comes in that a new behavior (in another layer of control) can suppress and override a connection wire to an input port, or it can inhibit messages coming out of an output port. Some examples:

// Level 0 Control
 @Behavior Forward       ( head_in, halt )
 @Behavior Turn          ( head_in             | head_out -> Forward.head_in )
 @Behavior Runaway       ( force               | heading -> Turn.head_in )
 @Behavior FeelForce     (                     | force -> Runaway.force )
 @Task Sonar, Collide;

@Behavior declares a behavior, following the name is the list of input and output ports enclosed by (). The behavior body (which we will see late on) is a plain C function and the ports are defined as local C variables of type “unsigned int.” A | separates the input ports from the output ports and both port lists are optional. If a behavior does not have any IO ports, then the @Task syntax may be used instead. As with C type declaration, multiple behaviors or tasks can be declared after a @Behavior or @Task, each one separated by a comma.

An output port declaration may have an option “-> portname” following it. This set up a default connection from the output port to the portname. A portname is in the form of behavior_name.input. A message is written using either

@write(output)

or

@send(behavior.input, value)

@write writes the value of the output to the connection wire. If the receiving behavior is “watching” the conenction, it will receive the message. Otherwise, the message is lost. @send writes a ad hoc value to an input port without setting up a connection. A @send cannot be inhibited (but can be suppressed) since it does not used a named output port.

There is no forward reference and everything must be declared before its reference. Connections can also be made explicitly:

// Level 1 Control
 @Behavior Avoid         ( heading, force      | head_out )
 @Behavior Wander        (                     | heading -> Avoid.heading )
 @connect(FeelForce.force, Avoid.force)
 @supress(Avoid.head_out, Turn.head_in)

@connect connects two ports and @suppress does the same but when a message is sent on a suppressed connection, no other message can be sent to the specified input port for a period of time. For example, in this code fragment, in the level 0 control, the behavior Runaway would normally controls the behavior Turn by affecting Turn’s “head_in” input port. In the level 1 control, Turn’s head_in connection is suppressed by the behavior Avoid and when Avoid sends a message, it will suppress the output of Runaway from reaching Turn for a period of time.

Below are some code fragments on the implementation. As you may guess, @defBehavior starts a behavior definition, @watch waits for a message to arrive at an input port, @watch-any waits for a message to arrive at any of the ports, and @write writes a value through the connection wire. @peek, @pause, and @restart round out the list of REXIS elements used.

// Level 0 implementation
 @defBehavior Turn
     {
     while (@watch(head_in))
         {
         turn_angle = head_in >> 8;
         // command robot to turn "turn_angle"
         head_out = head_in & 0xFF;
         @write(head_out);
         @pause();
         }
     }
@defBehavior Forward
     {
     while (@watch(head_in))
         {
         // command robot to move forward for "head_in"
        while (!robotIdle())
             if (@peek(halt))
                 // command robot to stop
         @restart(Turn);
         }
      }
 ...
@defBehavior Avoid
    {
    while (@watch-any(force, heading))
         {
         head_out = function_of(heading, force);
         @write(head_out);
         }
     }
// Level 1
@defBehavior Wander
     {
     while (1)
         {
         @write(heading, <random heading>);
         RexisSleep(<10 seconds>);
         }
     }
   ...
// in main somewhere
AvoidTCB = CreateTask(Avoid, 3, ....);    // 3 ports

The REXIS preprocessor examines the @REXIS elements and perform the necessary symbol table management and syntactic and semantic checking. C code (usually involving REXIS low level API functions) are emitted. For example, the Avoid behavior definition above may generate the following C code:

void Avoid()
     {
     _rexisConnect(&AvoidTCB, HEAD_OUT_INDEX, &TurnTCB, HEAD_IN_INDEX);
     while (_rexisWatchAny(2, &AvoidTCB, FORCE_INDEX, HEADING_INDEX))
         {
         *_rexisVar(&AvoidTCB, HEAD_OUT_INDEX) = function_of(*_rexisVar(&AvoidTCB, HEADING_INDEX),
                                                                         *_rexisVar(&AvoidTCB, FORCE_INDEX));
         _rexisWrite(&AvoidTCB, HEAD_OUT_INDEX);
         }
     }
Scroll to Top