You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 53 Next »

Draft 4.0 (2011 Feb 18)

Concepts

RCE code is divided into three layers:

  1. The core is the same for all RCEs of a given generation. It contains low-level hardware management code, RTEMS, generic C/C++ support libraries, etc.
  2. The protocol plug-in (PPI) software modules.
  3. Application code which is entered only after the first two layers are fully initialized.

Each layer's code is stored independently somewhere on the RCE, e.g., in configuration flash.

The low-level interface to an RCE's protocol plug-ins uses abstractions called ports, virtual channels, frames, frame buffers, conduits and factories.

A virtual channel is the RCE end of a two-way communications link similar in concept to a BSD socket. They are globally visible but not MT-safe; at a given time at most one thread may be waiting for data from, receiving data from or delivering data to any given virtual channel.

Virtual channels receive and transfer data packaged in frames. The content of a frame depends on the protocol being used. One frame corresponds to one I/O message and is delivered in a single frame buffer. In other words all virtual channels implement datagram rather than byte-stream protocols. It's up to higher-level software such as a TCP stack to provide any operations that cross frame boundaries. Each virtual channel uses two queues of frames; one for frames which have arrived and have not yet been consumed by the application, the other for frames generated by the application but which have not yet been transmitted.

In addition to a frame, a frame buffer contains administrative information that is private to the implementation of the plug-in interface; the user gives and takes only void* pointers to the beginning of the frame proper. Information such as the size of the frame has to be fixed by the protocol or has to be included in the frame itself.

A port represents a hardware I/O engine capable of DMA to and from system RAM, i.e., a protocol plug-in of a particular type. An RCE has at most eight ports (which limit derives from the limit on the number of plugins). Each port may connect to pins leading out of the FPGA, though some may simply offer access to FPGA resources such as DSPs. No pin may be used by more than one port. Each port has its own virtual channel space where each virtual channel is identified by a 16-bit unsigned integer. Each virtual channel represents a different source and/or sink of data such as a UDP port or a section of Petacache memory. The size of the virtual channel number space on a port depends on the port type and may be as low as one.

Every virtual channel object created is allocated a virtual channel number from its port's space but no two virtual channels of a port may have the same virtual channel number. Each incoming frame must contain information from which a virtual channel number can be derived. If it doesn't, or if it specifies a virtual channel number that is invalid or not allocated to a virtual channel object, then the port's lost-frame count is incremented and the frame is recycled.

The FPGA is connected to the larger system by data paths called conduits. Each conduit connects to one or more pins on the FPGA; no pin may belong to more than one conduit. At boot time each port that uses pins will be matched to the conduit that connects to exactly the same set of pins. If the resulting mapping is not one-to-one then the boot process fails. Each conduit has associated type and version numbers which the software for the matching port checks for validity at boot time. If the software rejects the conduit then again booting will fail.

Each type of port has both an official (unsigned) number and an official short name such as "eth" or "config". Ethernet ports that differ only in the number of pins they use, e.g., 10 Gb/s ethernet (4) and a slower ethernet (1) have the same port type and short name.

PPI-handling software is held in relocatable modules recorded on the RCE. A module for plugin type FOO holds:

  • An implementation of a class FooPort derived from Port.
  • An implementation of class FooFactory derived from Factory. A FooFactory creates FooChannel instances, returning Port* values.
  • An entry point that creates an instance of FooFactory, returning a Factory* value.

Each module is loaded, relocated and bound to the system core before its first use. The core code knows only the abstract base classes Factory and Port, not the derived classes specific to plugin type.

Both PPI hardware and the port factory modules have version numbers which will allow some measure of compatibility checking at boot time. Any incompatibility detected causes the boot to fail.

Configuration information

RCE boot code discovers the set of protocol plugins and conduits available using a set of configuration registers. These registers have their own address space, the "configuration space". For each plugin the configuration space registers yield plugin type, version number and the set of FPGA pins connected to it. For each conduit they yield conduit type, version number and its set of FPGA pins.

Access to configuration registers is via an object which given an abstract register number reads or writes register contents. Another object uses the abstract register layer to provide all the configuration info for a plugin or a conduit given its index number (or supplies an indication that the given entity doesn't exist).

Differences between Gen I and Gen II

Gen II

The configuration space registers are hardware memory locations filled with information by the IPMI controller (IPMC).

I/O buffers are allocated and deallocated by firmware, which also controls the size and content of frame headers and the maximum payload size. All the client gets to see in each buffer is its payload.

Gen I

There is no IPMC and there are no hardware configuration registers. The low-level configuration information is burned into a configuration flash container. At boot time the container is read and its contents fed into an object which simulates the Gen II configuration register space. Above that layer the handling of the information is just like that in Gen II.

Protocol plugins are assembled from Protocol Interface Core (PIC) blocks which have no counterpart in Gen II. We want to make the configuration information handling like that in Gen II, so rather than extending it with PIC block assignments we sweep those under the rug by embedding them in the plugin software modules.

There is no memory management for I/O buffers in firmware; software must allocate the buffer pools. Frame headers are fully exposed to the client who must know for a given plugin the header size, the maximum payload size and the recommeded number of buffers for a given plugin. This information can also be embedded in the plugin-handling module at the cost of having to fit the module to the RCE application.

Application interface

The class declarations given in this section contain only those members intended for use by the application after booting is complete and all plugins are on-line. Whether a method is virtual is not specified, nor are friend declarations shown; these are considered implementation details.

Classes and their responsibilities

Class name

Instance responsibilities

Port

Represent a single protocol plugin. Allocate, deallocate and track virtual channels. Derive virtual channel numbers from frame headers. Retain the configuration information for the plugin and the index number of the conduit (if any) assigned to it at boot time. Print multi-line reports on the plugin state and configuration.

PortList

Keep a linked list of all Port instances. Assign each Port an both a global index number and an index number within its type. Search the list by global index number, by type and type index number and by conduit number. Print a brief report on the status of all ports, one line per port.

VirtualChannel

Represent a single virtual channel associated with the allocating Port. Accept frames for transmission. Return frames that have been received (waiting if needed).

Universal constants (constants.hh)

All RCEs whether of Gen I or Gen II each have the same limits on the number of plugin instances (MAX_PLUGINS).

static const unsigned MAX_PLUGINS  =  8;

Port-type enumeration (PortTypes.hh)

The numbers are members of an enumeration assigned by the DAQ project.

enum PortType {
    CONFIG_FLASH,
    ETHERNET,
    PGP1,
    PGP2,
    etc.,
    INVALID_PORT_TYPE
};

The header file also contains a specialization of the template RCE::service::EnumInfo which allows one to use the function templates emin<>(), emax<>(), ecount<>(), evalid<>(), enext<>(), eprev<>() and estr<>():

emin<PortType>() == CONFIG_FLASH
emax<PortType>() == PortType(INVALID_PORTTYPE - 1)
ecount<PortType>() == int(INVALID_PORTTYPE)
evalid(x) is true for all from emin() to emax() inclusive, else false
enext(emax()) == eprev(emin()) == INVALID_PORTTYPE
enext(CONFIG_FLASH) == ETHERNET, etc.
eprev(ETHERNET) == CONFIG_FLASH, etc.
estr(CONFIG_FLASH) == "CONFIG_FLASH", etc.
estr(x) == "**INVALID**" if and only if evalid(x) is false

ecount<>() can't be used as a dimension for static arrays since the compiler considers it to be non-constant; in that case use EnumInfo<PortType>::count.

Port list (PortList.hh)

This class is a Borg-type singleton; the constructor makes a stateless object whose member functions access the true (shared) state defined elsewhere. The destructor destroys these stateless objects but does not touch the true state information. You can therefore just use the constructor whenever you need to access the One True List, e.g., PortList().head().

You can get a count of the number of ports or the first port on the list (the list can't be empty). The report() member function will print informational messages in the system log which show the contents of the port list in brief form, one line per port.

The location and form of the system log depends on how the system logging package was initialized at application startup. Client code making log entries is not aware of this initialization.

A particular port may be looked up in several different ways:

  • By its global index number, assigned in sequence starting from zero as ports are created.
  • By its type number and the index number within the type, e.g., (ETHERNET,0), (ETHERNET,1), etc.
  • By the number of the conduit the port is connected to.

Lookup methods return the null pointer if the search fails.

class PortList {
public:
    PortList() {}
    ~PortList() {}
    int numPorts() const;
    Port* head() const;
    Port* lookup(PortType type, unsigned typeIndex) const;
    Port* lookup(unsigned index) const;
    Port* lookupByConduit(unsigned conduit) const;
    void report() const;
);

Port (Port.hh)

A port object represents a particular instance of a protocol plug-in. Each port object is created at boot time. Port objects live until system shutdown and may not be copied or assigned.

Each port creates and destroys VirtualChannel objects on demand. During its lifetime each VirtualChannel object has exclusive use of one of the port's virtual channel numbers; the virtual channel number becomes available again once the VirtualChannel object is deallocated. The client code may request a specific, unused virtual channel number for the type of port, e.g., a well-known TCP port number. The client may also allow the port to assign a number not currently in use by any VirtualChannel.

Every port object is a member of the linked list accessed though class PortList and may not be removed from the list. Use the next() member function to iterate over the list.

A short name for the type and a short description of the port are also provided.

A "lost" counter is provided which counts the number of incoming frames that were discarded, for whatever reason, instead of being queued in a virtual channel.

Other information provided:

  • The index number of the conduit associated with the port.
  • The hardware version number of the associated plugin.
  • The software version number of the associated plugin module.

The report() member function produces detailed multi-line description of the port in the system log, including all platform-specific information.

Application code isn't allowed to create or destroy instances; only the RCE boot code is allowed to do that.

class Port {
public:
    VirtualChannel* allocate(int portNum);
    VirtualChannel* allocate();
    void deallocate(VirtualChannel *);
    unsigned lost() const;
    unsigned index() const;
    unsigned type() const;
    const char* name() const;
    unsigned typeIndex() const;
    unsigned conduit() const;
    unsigned versionHard() const;
    unsigned versionSoft() const;
    Port* next() const;
    const char* description() const;
    void report() const;
};

Virtual channel (VirtualChannel.hh)

Each VirtualChannel object is created by a port and is assigned a unique ID in the port's virtual channel number space.

Incoming frames on the associated port may be waited for and retrieved using the wait() member function. Client code will normally keep a frame for a short time then give it back to the virtual channel they got it from using the virtual channel's giveBack() member function. It's an error to try to give back a frame to a virtual channel which didn't produce it; the results of doing so will be unpredictable.

A virtual channel takes frames given to its send() member function and queues them for output. The frame buffer is NOT reclaimed after sending nor does the plugin interface tell you when it's safe to re-use the buffer; that's up to the message protocol.

class VirtualChannel {
public:
    unsigned index() const;
    void* wait();
    void giveBack(void *frame);
    void send(void *frame);
};

Interface for boot code and plugin software

In this section we describe the code used manage configuration information and construct the global PortList. Some of the classes already introduced above will have new members described here; other classes will be completely new.

New members of old classes

The PortList class has a static member function build() whose main purpose is to produce the list of Port instances. To do so it will have to read configuration information about plugins and conduits, load and activate plugin software and match conduits to ports.

class PortList {
  public:
  static void build();
};

Classes and their responsibilities

Class name

Instance responsibilities

ConduitConfig

Hold the configuration information for one conduit.

ConfigReader

Collect all the available information about a given plugin (conduit) from ConfigSpace and put it into an instance of PluginConfig (ConduitConfig). Indicate when the given plugin or conduit doesn't exist.

ConfigSpace

Provide an address space containing abstract 32-bit registers providing configuration info for plugins and conduits, whether or not such registers exist in hardware.

PluginConfig

Hold the configuration information for one plugin instance.

PortFactoryList

Hold all PortFactory objects created during boot. Look up factory instances by type.

Class/enum name

Class/enum responsibilities

ConduitType

Enumerate the different types of conduit.

PortFactory

Abstract base class for objects that given an instance of PluginConfig and an instance of ConduitConfig produce a Port instance. The ConduitConfig is optional for plugins that don't connect to a conduit.

ConduitConfig (ConduitConfig.hh)

This is a Plain Old Data (POD) class describing a single conduit. The default constructor sets all members to zero.

Member

Description

index

The order of appearance, starting from zero, of the information in ConfigSpace.

type

The type of conduit.

version

The version number of the conduit definition.

pins

Has a 1 bit for each FPGA pin connected to the conduit.

The index value is set to 0xffffffff by ConfigReader to indicate a nonexistent conduit.

struct ConduitConfig {
  unsigned index;
  ConduitType type;
  unsigned version;
  unsigned long long pins;
  ConduitConfig();
  ConduitConfig(unsigned ind, ConduitType, unsigned ver, unsigned long long);
};

ConduitType (ConduitType.hh)

These types are not well defined yet so for now we just define a generic type code. The header also provides a specialization of rce::service::EnumInfo<> similar to that provided for PluginType.

enum ConduitType {
  CONDUIT,
  INVALID_CONDUIT_TYPE
};

ConfigReader (ConfigReader.hh)

One member function returns instances of PluginConfig, the other returns instances of ConduitConfig. Both take an argument that is the index of the object whose configuration you want to look up; plugins and conduits are numbered separately staring from zero. Both set the index to 0xffffffff when there's no object associated with the given index.

Instances have no data of their own but get what they need from ConfigSpace; you can generate and throw away instances as often as you want.

class ConfigReader {
  public:
  void lookupConduit(unsigned index, ConduitConfig&);
  void lookupPlugin(unsigned index, PluginConfig&);
};

ConfigSpace (ConfigSpace.hh)

Each instance implements an abstract space of configuration registers. How the abstract registers are used to collect configuration information is an implementation decision which will however be the same for both Gen I and II. Register addresses start at zero; an attempt to read or write a register at an invalid address, or to write to a read-only register, will throw std::logic_error.

Instances have no data of their own but get what they need from some central source on the RCE; exactly where differs between Gen I and Gen II. You can create and destroy instances at will.

class ConfigSpace {
  public:
  ConfigSpace();
  unsigned read(unsigned address);
  void write(unsigned address, unsigned value);
};

PluginConfig (PluginConfig.hh)

This is a Plain Old Data (POD) class describing a single plugin instance. The default constructor sets all members to zero.

Member

Description

index

The order of appearance, starting from zero, of the information in ConfigSpace.

type

The type of plugin.

version

The version number of the plugin definition.

pins

Has a 1 bit for each FPGA pin connected to the plugin.

The index value is set to 0xffffffff by ConfigReader to indicate a nonexistent plugin.

struct PluginConfig {
  unsigned index;
  PluginType type;
  unsigned version;
  unsigned long long pins;
  PluginConfig();
  PluginConfig(unsigned ind, PluginType, unsigned ver, unsigned long long);
};

PortFactory (PortFactory.hh)

This is an abstract base class. Once the boot code knows the types of the available plugins it will load the plugin software module for each type. It will call the entry point of each plugin software module once to obtain an instance of a class derived from PortFactory.

Once it has matched a PluginConfig instance with a ConduitConfig instance, or determines that the plugin needs no conduit, the boot code uses factory object to create Port instances for the given type of plugin. If the plugin and conduit versions are incompatible the factory member function will throw std::logic_error. It will do the same if no ConduitConfig is supplied when one is required.

The report function will log full details of the factory, including any platform-dependent information.

class PortFactory {
public:
  PortType type() const;
  Port* makePort(const PluginConfig&);
  Port* makePort(const PluginConfig&, const ConduitConfig&);
  void report() const;
};

PortFactoryList (PortFactoryList.hh)

Another Borg singleton, very similar in concenpt to PortList; the underlying list of factories is built at about the same time as the list of ports. There is at most one factory per port type. The lookup function returns a null pointer if no matching factory is on the list. The report function logs a one-line summary per factory.

class PortFactoryList {
public:
  PortFactoryList();
  PortFactory* head() const;
  unsigned numFactories() const;
  PortFactory* lookup(PortType) const;
  void report() const;
};

Plugin software module interface

Each module's entry point is named rce_appmain; this symbol is recognized by the module build system which places its value in the transfer address slot of the module's ELF header. The prototype of the entry point function is

extern "C" PortFactory* rce_appmain();

The boot code uses placement new to construct an instance of PluginModule atop the image of the module then calls the run() member function to obtain the factory object.

class PluginModule: public RCE::ELF::Module {
public:
  typedef Factory* (*EntryPoint)();
  Factory* run();
};

The module's entry point is run directly without creating a new thread (otherwise we'd need synchronization in order to wait for the factory to be produced).

Gen I-specific application interface

On Gen I RCEs the application software is left with the job of allocating I/O buffers and it can't do that without knowing for each plugin the header sizes, max payload sizes and max number of frame buffers for import and export. That information is available from the port factories but it means downcasting the PortFactory* values gotten from the PortFactoryList. Not only must the buffers be allocated but administrative information must be placed in them (instances of class Buffer, see below). Import buffers must be pushed into an FLB before any data may be received. The interface described here lets the application do the needed initialization without exposing the innards of the plugin-handling system.

Ethernet (TBD)

PGP version 1

PgpBuffers (PgpBuffers.hh)

An instance of this class provides the following:

  • The size of the header.
  • The maximum payload size of a payload.
  • The maximum number of import and export buffers that may be allocated.
  • A means to allocate a pool of import buffers that have the required administration information added and which are given to the FLB that all the PGP1 plugins share.
  • A means to allocate an export buffer pool with the required admin. info. in each buffer.
  • A means to extract the allocated export buffers which returns a null pointer when all have been extracted.
class PgpBuffers {
public:
  PgpBuffers();
  ~PgpBuffers();

  class Allocator {
  public:
    virtual void* allocate(unsigned nbytes) = 0;
    virtual void  deallocate(void*) = 0;
  };

  unsigned importHeaderSize() const;
  unsigned exportHeaderSize() const;

  unsigned payloadOffset() const;
  unsigned maxImportPayloadSize() const;
  unsigned maxExportPayloadSize() const;

  unsigned maxImportBuffers() const;
  void allocateImportBuffers(unsigned numImport);
  void allocateImportBuffers(unsigned numImport, Allocator &);

  unsigned maxExportBuffers() const;
  void allocateExportBuffers(unsigned numExport);
  void allocateExportBuffers(unsigned numExport, Allocator &);
  void* getExportBuffer();
};

In each buffer the administrative information comes first followed by the header and then the payload. getExportBuffer() returns pointers to headers rather than pointers to the beginnings of buffers; these are the kinds of pointers that virtual channel objects deal with. Header and payload sections are both aligned on a cache line boundary; the buffer itself is aligned on a 64-byte boundary. payloadOffset() returns the offset of the payload from the pointer returned by getExportBuffer(). If the application doesn't provide its own allocator a default will be used.

The PgpBuffers object retains ownership of the buffers; the destructor resets all PGP firmware blocks and deallocates all the buffers. The application need only destroy the current instance and make a new one to restart PGP.

Gen I-specific plugin software interface

Buffer (Buffer.hh)

Before a frame buffer may be used for either import or export an instance of class Buffer must be created at the beginning of the buffer using placement new.

A Buffer contains regions of fixed size used in the management of the frame itself and its buffer:

  • The firmware's in-memory descriptor. The firmware TDE for the frame points to the descriptor.
  • Status. The firmware writes operation completion status here.
  • One or more links used to make singly-linked lists.

Immediately following the end of the Buffer we have first the frame's header and then its payload.

The links, descriptor and status areas have the same sizes for all port types.

The descriptor must begin on a 64-byte boundary; for ease of layout the entire buffer also has that alignment. The header and payload areas should begin on cache-line boundaries (32-byte).
Buffer instances may not be copied or assigned.

For details about the descriptors see chapter 4 of the Cluster Element Module document at http://www.slac.stanford.edu/exp/npa/design/CEM.pdf

struct Buffer {

  struct ErrorDescription {
    unsigned parameter: 24;
    unsigned wasBlockError: 1;
    unsigned reason: 6;
    unsigned wasError: 1;
  };

  // Transaction Descriptor
  uint32_t  payloadLen;
  uint8_t*  headerPtr;
  uint8_t*  payloadPtr;
  ErrorDescription* completionPtr;
  uint8_t   zeros0[16];

  // Transaction Completion Descriptor
  ErrorDescription errDesc;
  uint32_t  transferCount;
  uint8_t   zeros1[24];

  static const unsigned NUM_LINKS = 8;
  Buffer* links[NUM_LINKS];

  Buffer(unsigned headerBytes);
  ~Buffer();
};

Ethernet (TBD)

PGP version 1

Pgp1Factory (Pgp1Factory.hh)

This class adds accessors to Gen-I specific information not found in the base class. The application interface class PgpBuffers will use it.

class Pgp1Factory: public PortFactory {
public:
  unsigned importHeaderSize() const;
  unsigned exportHeaderSize() const;
  unsigned maxImportPayloadSize() const;
  unsigned maxExportPayloadSize() const;
  unsigned maxImportBuffers() const;
  unsigned maxExportBuffers() const;
  unsigned pebBlock(unsigned index) const;
  unsigned ecbBlock() const;
  unsigned flbBlock() const;
  unsigned pibBlock(unsigned index) const;
};

Gen I PGP1 plugins all share the same ECB and FLB but each is assigned its own PEB and PIB. The index you pass to the accessor function is the index within plugin type PGP1, the same number you would obtain from Port::typeIndex().

Gen I initialization of ConfigSpace

Configuration container zero in the configuration flash contains tables of information about the hardware and firmware; this information can't be gotten directly from the hardware and firmware.

Not knowing the actual layout of the RCE's circuit board(s) I've arbitrarily assigned conduit 0 to the 10 Gb ethernet and conduits 1, 2, 3 and (for petacache) 4 to PGP1. The four MGT's assigned to the ethernet I call pins 0-3 while the the ones assigned to the PGP plugins I call pins 4, 5, 6 and (peta) 7.

The container contents consist of eight instances of PluginConfig followed immediatly by eight instances of ConduitConfig, except that the "index" members are not recorded. For a Petacache RCE board (PGP only) the tables look like this (substitute 0xffffffff for "EMPTY" and the enumerator values for "PGP1" and "CONDUIT"):

Type

Version

Pins

PGP1

1

0x00000000 00000010

PGP1

1

0x00000000 00000020

PGP1

1

0x00000000 00000040

PGP1

1

0x00000000 00000080

EMPTY

0

0x00000000 00000000

EMPTY

0

0x00000000 00000000

EMPTY

0

0x00000000 00000000

EMPTY

0

0x00000000 00000000

Type

Version

Pins

CONDUIT

1

0x00000000 00000010

CONDUIT

1

0x00000000 00000020

CONDUIT

1

0x00000000 00000040

CONDUIT

1

0x00000000 00000080

EMPTY

0

0x00000000 00000000

EMPTY

0

0x00000000 00000000

EMPTY

0

0x00000000 00000000

EMPTY

0

0x00000000 00000000

Gen I storage of plugin software modules

Only a few different types of protocol plugins are found on Gen I systems so each type is assigned to a fixed Configuration container:

Plugin type

Container name

ETHERNET

1

PGP1

2

PGP2

3

future expansion

4-9

Later a container may be used for CONFIG_FLASH, if we ever manage to make a usable wrapper for the FCI package that makes it look like another plugin.

Use case: Gen I booting

  1. Boot code:
    1. Loads and starts the system core.
  2. System core
    1. Initializes the CPU.
    2. Initializes RTEMS.
    3. Initializes any extra C++ support.
    4. Sets up the MMU's TLB and enables the MMU.
    5. Creates the default instance of the dynamic linker.
    6. Reads ConfigSpace.
      1. The first read triggers the reading of Configuration container zero.
      2. Builds the Port and PortFactory lists, reading, linking and running plugin software modules as required.
    7. Performs other initialization, e.g., ethernet, BSD stack.
    8. Loads and links the application code using the default dynamic linker.
    9. Calls the application entry point.

Source code organization

All the classes, enums and other declarations will appear in the namespace RCE::ppi.

Platform-neutral header files for the application interface are immediately below the directory rce/ppi/.

Platform-neutral header files for the boot code and plugin software module interfaces are directly below rce/ppi/src/.

Platform-specific header files are in subdirectories gen1/, gen2/ and i86-linux/ below src/.

Platform-neutral compilation units are in src/ while those that depend on the platform are in src/gen1/, etc.

Inline definitions are broken out into their own header files in src/ or its subdirectories unless they are few and trivial for a given class. Example: Foo-inl.hh.

Unit test code goes in rce/ppi/test/ whose subdirectory structure mirrors that of src/.

  • No labels