CDK Tutorial

This chapter details the API that all SID components must follow.

CDK Tutorial

This tutorial describes how the various parts of the Component Developer's Kit architecture can be used to create a complete and functional simulation model.

To provide some context, the tutorial guides you through the design and implementation of a sample component--a timer chip. This chip was chosen for several reasons:

The tutorial presents a series of steps leading from the initial examination of the chip specifications to the external configuration script that will be used to exercise the component.

Step 1: Define the Functionality Required

The first step in developing a component is to define its functional requirements. All chips have two aspects that must be represented in their software model

  • The physical interface of buses, pins, and registers

  • The behavioral interface to application software and the external world

This section describes the timer chip in enough detail to resolve the design and functional considerations.

Example

The particular timer used in this tutorial is a dual 16-bit down counter type, in which each timer can be pre-scaled and set in either free-running mode or periodic timer mode. For the purposes of this example, the specifications are taken from the on-board timers of the ARM CPU. However, the principles can be generalized to most timers, and in fact, the interface aspects apply to a broad range of peripherals.

The following details the timer registers:

Table 1. Registers

AddressNameRead LocationWrite Location
basei + 0x0LOAD_REG16-bit timer load value
basei + 0x4VAL_REG16-bit timer load valuereserved
basei + 0x8CTL_REG8-bit timer control
basei + 0xCCLR_REGreservedinterrupt clear

Table 2. Control Registers

31–8765–4321–0
0EnableMode0Prescale0
0

0 = Disabled
1 = Disabled

0 = Free-running
1 = Periodic

0Bits 3–2Divisor0
0

0 = Disabled
1 = Disabled

0 = Free-running
1 = Periodic

00010
0

0 = Disabled
1 = Disabled

0 = Free-running
1 = Periodic

001160
0

0 = Disabled
1 = Disabled

0 = Free-running
1 = Periodic

0102560
0

0 = Disabled
1 = Disabled

0 = Free-running
1 = Periodic

011Undefined0

Each register is 32-bits wide. Since there are two timers, there are two base addresses: base 0 is selectable; base 1 = base 0 + 0x20.

The timers operate by counting down from the 16-bit value in LOAD_REG. The count rate can be varied by the pre-scale settings of the control register. The pre-scale is a divisor applied to the input clock. Therefore, with a pre-scale setting of 1, the timer counts down at the input clock rate; with a pre-scale value of 16, the timer counts down at 1/16 of the input clock rate. The current count can be read at any time from VAL_REG. Once the counter reaches zero, the timer takes one of two actions depending on the Mode setting, which is bit 6 of the control register. If the timer is in free-running mode, it will reload the current count value from LOAD_REG and start counting down again; if the timer is in periodic mode, it will generate an interrupt, and then reload its current count and continue.

The above set of behaviors forms the functional requirements of the simulated timer model. Specifically, the registers must be present at the prescribed addresses, and must appear to application software as if they were the real registers. Similarly, the interrupt and clock behaviors must be modeled accurately with respect to interrupt timing and pre-scale behavior.

Step 2: Design the Model

Once the functionality of the hardware is understood, it must be mapped onto the simulator's API and level of abstraction. There are some design decisions that can be made to raise or lower the level of abstraction. Many of these decisions trade performance of the simulated component against its fidelity to the actual hardware.

The timers in this tutorial are simple and therefore do not require many design decisions, but developing components such as CPUs or Ethernet controllers involves evaluating many trade-offs. As an example, a CPU with a hardware level 1 cache may be modeled without the cache, since cache simulation is time and space consuming and may not affect the correctness of the simulated application. This is a common motivation for reducing the accuracy of a component, since often the simulator is only required to behave with functional correctness from the target software's point of view.

Example

The simulator architecture possesses some basic properties that impact component implementation:

  • Components communicate with other components only through pin and bus connections.

  • Components do not have an internal concept of time.

  • Components must not block (that is, suspend) the main thread of control.

In the case of the timer, the simulator framework dictates that the bus interface must be used to allow access to the registers; interrupt generation should use the pin interface; and the events that change the timer's counters must be externally generated.

There are several approaches to use when setting up the external timer generation of the timer model. The simulator includes an event scheduler component that maintains the concept of simulated time and supports a queue of time-ordered events. The timer's clocking is thus achieved by enqueueing a request to be notified at some specific future time.

  • The simplest approach is to specify the future time as one system clock tick past the current time. This allows the component to examine its pre-scale state and decrement the internal counters appropriately. The problem with this approach is that it is inefficient--there is significant communication with the scheduler on every clock tick.

  • A second approach is to set the notify time to the value of the pre-scaler, which, at best, can reduce the communication with the scheduler by up to a factor of 256, in this example.

  • A third approach achieves minimal communication by calculating the pre-scaled total time, and requesting notification only when the internal counter value reaches zero.

One consideration in timing notification is that the timer must make its internal count value available at any intermediate time, since this is a functional requirement of supporting reads of VAL_REG. In the first two approaches, this requirement can be supported directly. In the third approach, intermediate counts do not have to be kept because the notification from the scheduler arrives only when the count is zero. Intermediate times can be supported on-demand, by querying the scheduler for the current time when a request for the intermediate count is received. If the start time of the current period is also saved, then the intermediate count can be calculated by dividing the elapsed time, which is the current time less the start time, by the pre-scale value.

This discussion is presented to illustrate the reasoning process that the component designer might use. For purposes of illustration, the remainder of the example will assume that the scheduler is set to notify the timer component at the pre-scale rate, the second option given above.

Step 3: Define the Component

The first implementation step is to define the component. All components derive from the abstract sid::component class that provides supervisory access and the methods needed to connect and initialize the model.

Example

The timer and its internal state can be defined as follows. For clarity, nested classes used for pins and buses will be omitted at this point:


class Timer: public virtual sid::component
{
public:
  Timer()
    :scheduler_pin(0), clock_pin(this), bus(this), enabled(false) { }  

  // Provide implementations for abstract methods in sid::component.
  // See include/sidcomp.h.

  vector<string> pin_names();
  sid::pin* find_pin(const string& name);
  sid::component::status connect_pin(const string& name, 
    sid::pin* pin);
  sid::component::status disconnect_pin(const string& name, 
    sid::pin* pin);
  vector<sid::pin*> connected_pins(const string& name);

  vector<string> accessor_names();
  sid::component::status connect_accessor(const string& name,
    sid::bus* bus);
  sid::component::status disconnect_accessor(const string& name,
    sid::bus* bus);

  vector<string> bus_names();
  sid::bus* find_bus(const string& name);
  sid::bus* connected_bus(const string& name);

  vector<string> attribute_names();
  vector<string> attribute_names(const string& category);
  string attribute_value(const string& name);
  sid::component::status set_attribute_value(const string& name,
    const string& value);

  vector<string> relationship_names();
  sid::component::status relate(const string& rel, 
    sid::component* c);
  sid::component::status unrelate(const string& rel, 
    sid::component* c);
  vector<sid::component*> related_components(const string& rel);

private:
  // Data members that represent the timer's internal state.

  bool enabled;
  sid::host_int_2 load_value, prescale, counter;
  enum timer_mode { PERIODIC, FREERUNNING } mode;
};

The Timer class contains all of the internal state needed to model a single timer. As a result, two instances of the class must be created, and mapped at appropriate addresses, to model the hardware. The internal state does not try to mimic the hardware layout; instead, it is organized to simplify the implementation for the component writer. In general, registers that store states as a bit field should be internally represented with separate member variables and they should only be assembled into a packed bit field when necessary.

The Timer class also defines all of the abstract methods from the sid::component class that form the SID component API. Each of these methods must be implemented to fulfill the API. As you will see, these methods quite often can be implemented at a minimum if the component does not require all of the API's facilities.

Step 4: Define the Pins

The SID API only addresses the concept of input pins. An input pin is one which is made visible to other components, through the pin_names() and find_pin() methods. There is no concept of an output pin, except that it is a component's internal collection of other components' pin objects.

Example

The timer component will have two output pins: the interrupt pin and a control pin to be used in conjunction with the scheduler component. Since we do not wish to limit the number of other components that may be interrupted by the timer, it is necessary to maintain a "net list" of pin connections that can be iterated over each time a value is driven across the interrupt pin:

private:
  // A netlist, which tracks pins connected to the interrupt pin.
  typedef set<sid::pin*> netlist_t;
  netlist_t intpin_netlist;

The following methods do just that:

void
Timer::drive_interrupt(sid::host_int_4 value)
{
  // Iterate the netlist, driving the value to all pins connected to
  // the interrupt pin.

  for (netlist_t::const_iterator it = intpin_netlist.begin();
       it != intpin_netlist.end();
       it++)
    {
      (*it)->driven(value);
    }
}


// Return a list of pins that are connected to a named pin.
// We recognize "interrupt" and "divided-clock-control".

vector<sid::pin*>
Timer::connected_pins(const string& name)
{
  vector<sid::pin*> pins;
  netlist_t::const_iterator it;

  if (name == "interrupt")
    {
      for (it = intpin_netlist.begin(); it != intpin_netlist.end();
           it++)
        {
          pins.push_back(*it);
        }
      return pins;
    }
  else if (name == "divided-clock-control")
    {
      pins.push_back(scheduler_pin);
      return pins;
    }
  return vector<sid::pin*>();
}


// Connect a pin to a named pin.
// We recognize "interrupt" and "divided-clock-control".

// We allow multiple pins to be connected to the interrupt pin (with
// infinite fan-out!), so these are kept in a netlist.  For
// efficiency, the STL container chosen for the netlist ensures that
// no duplicate pin handles are stored.

sid::component::status
Timer::connect_pin(const string& name, sid::pin* pin)
{
  if (name == "interrupt")
    {
      // Add this pin to the netlist.
      intpin_netlist.insert(intpin_netlist.end(), pin);
      return sid::component::ok;
    }
  else if (name == "divided-clock-control")
    {
      // Reassign the scheduler pin.
      scheduler_pin = pin;
      return sid::component::ok;
    }
  return sid::component::not_found;
}

// Disconnect a pin from a named pin.

sid::component::status
Timer::disconnect_pin(const string& name, sid::pin* pin)
{
  if (name == "interrupt")
    {
      // Remove this pin from the netlist.
      if (intpin_netlist.erase(pin) > 0)
        return sid::component::ok;
    }
  else if (name == "divided-clock-control"& & scheduler_pin == pin)
    {
      // Elsewhere, we make sure to not use this pin if it's null.
      scheduler_pin = 0;
      return sid::component::ok;
    }
  return sid::component::not_found;
}

The component will also have one input pin which is exported to other components: divided-clock-event. This pin, when connected to the scheduler component, can be used to deliver event notifications to the timer. We can create a specific pin for this, based on the sid::pin class:

class clock_pin_t: public sid::pin
  {
    // clock_pin_t is a specialized pin.
    // It calls timer->tick() whenever a value is driven. 
  public:
    clock_pin_t(Timer* t): timer(t) { }
    void driven(sid::host_int_4 value) { timer->tick(); }
  private:
    Timer* timer;
  };
  friend class clock_pin_t;
  clock_pin_t clock_pin;

  // This method is called whenever the scheduler delivers an event,
  // because the "divided-clock-event" pin will be connected to the
  // scheduler.  It is a specialized pin with these fixed semantics.
  void tick();

And the following component API method is used to publicize this pin:

// Return a list of pin names which are visible to other components.

vector<string>
Timer::pin_names()
{
  vector<string> pins;
  pins.push_back("divided-clock-event");
  return pins;
}


// Find a pin of a given name.
// We only recognize "divided-clock-event".

sid::pin*
Timer::find_pin(const string& name)
{
  if (name == "divided-clock-event")
    return& clock_pin;

  return 0;
}

Step 5: Define the Bus

Like pins, buses work as demand-driven functional callbacks. The simulator abstracts buses to a read/write interface on address and data.

Example

The timer bus interface can be declared as follows. It should be nested within the Timer class to facilitate information hiding:

 // register_bus is a specialized bus.
 // It handles the majority of the component's functionality, since
 // that is mostly controlled by the timer's register set.

 class register_bus: public sid::bus
 {
 public:

  register_bus(Timer* t): timer(t) { }

   // Prototypes for bus read/write methods of all kinds.

   sid::bus::status read(sid::host_int_4 addr, sid::little_int_1& data);
   sid::bus::status read(sid::host_int_4 addr, sid::big_int_1& data);
   sid::bus::status read(sid::host_int_4 addr, sid::little_int_2& data);
   sid::bus::status read(sid::host_int_4 addr, sid::big_int_2& data);
   sid::bus::status read(sid::host_int_4 addr, sid::little_int_4& data);
   sid::bus::status read(sid::host_int_4 addr, sid::big_int_4& data);
   sid::bus::status read(sid::host_int_4 addr, sid::little_int_8& data);
   sid::bus::status read(sid::host_int_4 addr, sid::big_int_8& data);
   sid::bus::status write(sid::host_int_4 addr, sid::little_int_1 data);
   sid::bus::status write(sid::host_int_4 addr, sid::big_int_1 data);
   sid::bus::status write(sid::host_int_4 addr, sid::little_int_2 data);
   sid::bus::status write(sid::host_int_4 addr, sid::big_int_2 data);
   sid::bus::status write(sid::host_int_4 addr, sid::little_int_4 data);
   sid::bus::status write(sid::host_int_4 addr, sid::big_int_4 data);
   sid::bus::status write(sid::host_int_4 addr, sid::little_int_8 data);
   sid::bus::status write(sid::host_int_4 addr, sid::big_int_8 data);

 private:
   Timer* timer;
 };
 friend class register_bus;
 register_bus bus;

Each read() and write() method must be defined before a bus object may be instantiated. There is a read and write method for each byte order and each transaction width. To simplify this example, only 32-bit (4-byte) read and write methods will be supported for the timer's 32-bit register file. All of the remaining methods will return an error, but in a more complete implementation, should manage these smaller bus transactions when it makes sense.

sid::bus::status
Timer::register_bus::read(sid::host_int_4 addr, sid::little_int_1& data)
{
  return sid::bus::unpermitted;
}

It is worth noting that all addresses seen by this component will be relative to 0. This is because the memory mapped address range is not known in advance. In a complete simulated system, the mapper component will handle the mapping from a memory mapped region into the device's address space.

A contrived example illustrates how the bus interface works:

  host_int_4 addr = 0;
  host_int_4 check, val = 0x5555;
  sid::bus* bus = component->find_bus("registers");

  bus->write(addr, val);  // write a value.
  bus->read(addr, check); // read back value.

  assert(val == check);

Other bus-related API methods that must be provided are shown below.

These methods enable supervisor components to connect to this component's bus:

// Return a list of bus names. We have just one--"registers".

vector<string>
Timer::bus_names()
{
  vector<string> buses;
  buses.push_back("registers");
  return buses;
}

sid::bus*
Timer::find_bus(const string& name)
{
  if (name == "registers")
    return& bus;
  return 0;
}

sid::bus*
Timer::connected_bus(const string& name)
{
  // No connected buses; return a null pointer.
  return 0;
}

Step 6: Add Scheduling Support

The scheduler is another SID component that is present in typical simulation systems. In order to request timed events to be delivered to a component, the component must negotiate them using a well-defined protocol. The component connects two pins to the scheduler:

divided-clock-event
divided-clock-control

Events are scheduled by communicating to the scheduler using this pin interface. To schedule an event at a future time, that time (as the number of seconds since UNIX epoch) must be carried across the divided-clock-control pin. If the most significant bit is set, then the event is to be delivered regularly with the specified frequency.

Example

To cancel an event subscription, the value 0 must be carried across the divided-clock-control pin. To simplify the implementation, these routines have been encapsulated within appropriately named methods:

// Schedule an event to be delivered at a later time.

void
Timer::schedule(sid::host_int_4 time)
{
  // The scheduler component tests bit 31 of a value carried on its
  // "control" pin.  If this bit is set, the event will be delivered
  // routinely at the specified interval.  Otherwise, the event will
  // only occur once.

  assert ((time&  0x80000000) == 0);
  assert ((time&  0x7FFFFFFF) != 0);
  
  if (scheduler_pin)
    scheduler_pin->driven(0x80000000 | time);
}


// Cancel any pending event.

void
Timer::cancel()
{
  // Cancel the event by driving a zero value to the scheduler.

  if (scheduler_pin)
    scheduler_pin->driven(0);
}

// Reset the schedule, in case the timer's enable or divisor registers
// have been altered.

void
Timer::reset_schedule()
{
  cancel();
  
  if (!enabled)
    return;
  
  assert (prescale <= 2);
  unsigned divisor = 1 << (prescale * 4);
  
  schedule(divisor);
}

Step 7: Complete All Abstract Methods

Now all of the remaining abstract methods from the sid::component class need to be implemented. Often, these method bodies are trivial when the component does not utilize the entire API.

vector<string>
  Timer::accessor_names()
  {
    // No accessors.
    return vector<string>();
  }
 
  sid::component::status
  Timer::connect_accessor(const string& name, sid::bus* bus)
  {
    // No accessors; any name is unknown.
    return sid::component::not_found;
  }
 
  sid::component::status
  Timer::disconnect_accessor(const string& name, sid::bus* bus)
  {
    // No accessors; any name is unknown.
    return sid::component::not_found;
  }
 
  vector<string>
  Timer::attribute_names()
  {
    return vector<string>();
  }
  
  vector<string>
  Timer::attribute_names(const string& category)
  {
    return vector<string>();
  }
  
  string
  Timer::attribute_value(const string& name)
  {
    // No attributes--return the empty string for any attribute value.
    return string();
  }
  
  sid::component::status
  Timer::set_attribute_value(const string& name, const string& value)
  {
    // No attributes--return not_found regardless of attribute name.
    return sid::component::not_found;
  }
 
  vector<sid::component*>
  Timer::related_components(const string& rel)
  {
    // No related components.
    return vector<sid::component*>();
  }
  
  sid::component::status
  Timer::unrelate(const string& rel, sid::component* c)
  {
    // No related components; always unfound.
    return sid::component::not_found;
  }
  
  sid::component::status
  Timer::relate(const string& rel, sid::component* c)
  {
    // No related components; always unfound.
    return sid::component::not_found;
  }
  
  vector<string>
  Timer::relationship_names()
  {
    // No relations.
    return vector<string>();
  }

Step 8: Complete the Model Functionality

For the timer, the functionality can be easily inferred from the hardware's functional description. For example, the bus' read call-back method would look something like the following:

// Handle 32-bit (little endian) reads.
// If the address is not 32-bit aligned or does not match any register
// address, return an error.

sid::bus::status
Timer::register_bus::read(sid::host_int_4 addr, sid::little_int_4& data)
{
  if (addr % 4 != 0)
    return sid::bus::misaligned;
  
  switch (addr)
    { 
    case 0x0:
      data = timer->load_value;
      break;
      
    case 0x4:
      data = timer->counter;
      break;
      
    case 0x8:
      data =
        (timer->enabled << 7) | 
        (timer->mode << 6) | 
        (timer->prescale << 2);
      break;
      
    case 0xC:
      break;
      
    default:
      return sid::bus::unmapped;
    }

  return sid::bus::ok;
}

The address (addr) is a localized offset into the component's address space. The switch statement responds to these offsets by setting the load or control register value, or by clearing the interrupt, as appropriate.

To simplify the implementation and alleviate duplicated code, the big-endian version of read():

// Handle 32-bit (big endian) reads.
// Just do a little endian read and rearrange the result.

sid::bus::status
Timer::register_bus::read(sid::host_int_4 addr, sid::big_int_4& data)
{
  sid::little_int_4 le_data;
  sid::bus::status st = read(addr, le_data);
  data.set_target_memory_value (le_data.target_memory_value ());
  return st;
}

This is how the write() method might look for a little-endian 32-bit read:

// Handle 32-bit (little endian) writes.
// If the address is not 32-bit aligned or does not match any register
// address, return an error.

sid::bus::status
Timer::register_bus::write(sid::host_int_4 addr, sid::little_int_4 data)
{
  if (addr % 4 != 0)
    return sid::bus::misaligned;
  
  switch (addr)
    {
    case 0x0:
      // A write to LOAD_REG.
      // Clear top 16 bits when loading a new value.
      timer->load_value = data&  0xFFFF;
      // Reset the counter value.
      timer->counter = timer->load_value;
      break;

    case 0x4:
      break;

    case 0x8:
      // A write to CTL_REG.
      timer->prescale = (data&  0x0C) >> 2;
      timer->enabled = ((data&  0x80) == 0x80);
      timer->mode = ((data&  0x40) >> 6) ? PERIODIC : FREERUNNING;
      timer->reset_schedule();
      break;

    case 0xC:
      timer->drive_interrupt(0);
      break;

    default:
      return sid::bus::unmapped;
    }

  return sid::bus::ok;
}

Again,

// Handle 32-bit (big endian) writes.
// Just rearrange the data and do a little endian write.

sid::bus::status
Timer::register_bus::write(sid::host_int_4 addr, sid::big_int_4 data)
{
  sid::little_int_4 le_data;
  le_data.set_target_memory_value (data.target_memory_value ());
  return write(addr, le_data);
}

Step 9: Configure the Connection

Now that the timer is implemented, it is time to hook it up and try it out. The simulator takes its configuration directions from a configuration file.

Example

Here is a configuration file fragment that illustrates the use of the new timer component:

...
# components
new ARM7100 cpu
new arm-timer timer1
new arm-timer timer2
new mapper map
# pin connections
connect-pin timer1 interrupt -> cpu INTR
connect-pin timer2 interrupt -> cpu INTR
# bus connections
connect-bus cpu data-bus map access-port
connect-bus map [0xa0000-0xa0010] timer1 registers
connect-bus map [0xa0020-0xa0030] timer2 registers
...

The configuration file segment instructs the simulator to instantiate the following four components: a CPU, two timers, and an address mapper.

The strings used to identify the components are specified in a shared library wrapper described in the API Reference manual. Like the bus and pin names, they are arbitrary, and must be determined from the component documentation. The configuration file directs SID to connect the output pins of the two timers to the interrupt input pin of the CPU.

NoteNote
 

The names timer1 and timer2 refer to the instance names defined in the script, while the name of the pin, interrupt, is available by virtue of the fact that the sid::component::pin_names() method names it.

The last part of the file connects the CPU data bus to the address mapper, and then adds the timers at address 0xa0000 and 0xa0020, using the address separation defined in the hardware specification (Figure 1).

This is just a fragment of the full configuration file, which must also define search paths to find the component libraries and add some memory so that a target program can be loaded and run.