Avrora Architecture

"Good performance through good architecture, great performance through great compilers."
Avrora has been designed with the goal of a clean, consistent, and flexible architecture from the very beginning. Repeated incremental change and restructuring has given rise to flexibility and extensibly in the areas that matter. This page will walk you through the big picture of the design; more details are always available by browsing the Java API.


In the Beginning was the Program...

Avrora is a program analysis framework and simulation tool for programs. It should come as no surprise to find that the core of the design focuses around representing programs in a clean and straightforward way.


Instruction Representation

Each type of instruction in the AVR instruction set has its own class in Avrora. Instructions of common formats share super classes that collect their functionality together. Instances of these instruction classes represent actual instructions within the program.


The Visitor Pattern

The visitor pattern is common where a fixed class hierarchy exists, but extension by adding operations is common. The fixed set of classes in this case are the instruction classes; the operations are interpretation, abstract interpretation, or any other type of per-instruction-class type of operation.


The Core of the Onion: The Simulator

The avrora.sim Java package contains a set of classes that implement the simulator. Principal among these is the Simulator class that implements that encapsulates the notion of an "execution engine" that can execute an AVR program.


Probing and Monitoring

Simply executing a program with no way of inspecting its results or execution is hardly useful. The program's behavior is of interest--either to debug the program, test its correctness, or study its characteristics. For this reason, the simulator exposes an API that allows probes to be inserted into the program that can inspect the state of the entire program. This one simple, powerful interface is used to implement tracing, profiling, or monitoring.

The Simulator.Probe interface can be implemented by any class that wishes to be inserted at any point of the program. This interface defines two methods, fireBefore() and fireAfter() that are executed before the instruction at which the probe has been inserted and after, respectively. Parameters are passed to the method that allow the probe to easily access any part of the simulator's state.

The Simulator class exposes four methods that allow these probes to be inserted anywhere in the program, or globally (i.e. the probe fires before and after every instruction executed). Probes can be inserted and removed at any point during the execution of the simulation. This allows arbitrary behavior such as intermitten probing, probing only sections of the code, only probing under certain conditions, etc.


Time is Asynchronous

The Simulator is cycle-accurate in the sense that it tracks the number of clock cycles consumed by each executing instruction and at any point records the correct number of clock cycles that have passed since the beginning of the simulation. This time information is vital to the correct simulation of time-variant phenomenon such as the operation of timers or devices connected to the microcontroller. Each Simulator instance is independent of others; it has its own local time in clock cycles and runs in its own thread.

Many things about the execution of a microcontroller are dependent upon time. For example, the onchip timers execute "in the background" and trigger an interrupt when they overflow or their counter value matches a compare register. An external device may send data at some time in the future. A serial device might trigger an interrupt when it has completed a transfer.

In the Simulator, such things are modelled as "events" and an event queue stores pending events that will be executed when the simulator's local time catches up to that point in time. In this way, time-based processing is asynchronous; instead of one massive loop that advances the interpreter and all devices one clock cycle at a time, the Simulator executes instructions and processes a delta queue of in-order events that must be processed. This asynchronous architecture leads to a big win in terms of cleanliness of architecture--and it turns out to be a big boost to performance as well because events occur infrequently. (Additionally, an amortized constant-time delta queue keeps the per-cycle cost to an absolute minimum).

Two good examples of this are the implementation of the timer and timeouts. Timer0 is a hardware timer available on the ATMega family of microcontrollers. It has programmable resolution, an 8-bit count register, and an 8-bit compare register. It is clocked off the main CPU clock with a programmable divisor (i.e. it can run up to full cpu clock speed or as slow as 1/1024th cpu clock speed). The only work it needs to do is increment a counter register when it receives a clock signal. It simply creates a periodic event with the frequency of the divisor and does it work in the event handler.

With asynchronous programming, timeouts are a breeze to program! An event can simply be inserted into the event queue of the simulator that will be fired at the desired time in the future. The simulator will run unimpeded until it reaches that time at which it will fire the event. The event can simply throw a Java exception (or call Simulator.stop() and the simulation will terminate! Here's the REAL Java code that implements this (excerpted from the simulator):


    /**
     * The InstructionCountTimeout class is a probe that
     * simply counts down and throws an exception when the count reaches
     * zero. It is useful for ensuring termination of the simulator, for
     * performance testing, or for profiling and stopping after a 
     * specified number of invocations.
     *
     * @author Ben L. Titzer
     */
    public static class ClockCycleTimeout implements Event {
        public final long timeout;

        /**
         * The constructor for InstructionCountTimeout creates a
         * timeout event with the specified initial value.
         *
         * @param t the number of cycles in the future
         */
        public ClockCycleTimeout(long t) {
            timeout = t;
        }

        /**
         * The fire() method is called when the  timeout is up.
         *
         */
        public void fire() {
            throw new Error("Timeout reached after "+timeout+" clock cycles");
        }

    }

Then, to use this class, one can just do the following, supposing that simulator is an instance of Simulator:

// terminate the simulation 200 clock cycles in the future
simulator.addTimerEvent(new ClockCycleTimeout(200), 200);


Microcontrollers

The Simulator represents the core execution engine of a simulation. It contains an interpreter for all of the instructions in the AVR instruction set, stores the state of the program including the SRAM, the IO registers, and the general purpose registers, and manages a delta queue of events. However, real microcontrollers have real devices built into the chip, and real programs want to use those devices. Another layer of the onion is necessary that encloses the Simulator, and that is the Microcontroller.


Platforms

Just as the Simulator was a just the engine inside of a Microcontroller, the Microcontroller is just an engine inside a real device. Real devices have electronics hooked up externally to the microcontroller such as simple LEDs, sensors, and communications devices. All such external devices communicate to the microcontroller through pins on the physical chip. Again, another layer of the onion is necessary to encapsulate all of these, and that layer is called the Platform.