-
Notifications
You must be signed in to change notification settings - Fork 2
Forwarding Architecture Description Language (FAD)
The Forwarding Architecture Description Language (FAD)
allows designers to create an abstract specification of the hierarchical structural
model of forwarding devices. FAD provides a few simple
declarative constructs to succinctly specify the hardware-software
platform. This description is used by pfpgen
to control the generation of
SystemC code.
The PE
keyword defines a processing element type. It can represent either a
hardware module, such as parser or CPU core, or a software module, such as
application or drivers.
In FAD, the PE top
has a special role similar to the main
function
in C/C++ programs. It is a required part of every complete FAD model.
The simplest possible FAD model is just:
PE top {
};
It is analogous to the simplest possible C/C++ program:
int main() {
return 0;
}
The final concept needed to create a minimal useful example is the concept of hierarchies of nested PEs. Complex models are created by composing simple PEs together.
A simple, complete FAD model of a CPU core could look like this:
PE Core {
};
PE top {
Core core;
};
Graphically this can also be represented very simply:
The interface
keyword is used to define a hardware port type which is used for the
connections between hardware modules. Interface types are purely abstract, with all details
other than the name of the interface being left to the behavioural description.
We can build on our previous example by adding a port to the CPU core that can be used for
accessing a memory. To do so we add a memory interface type, and create an instance of it
inside Core
.
interface MemoryIf;
PE Core {
MemoryIf memory_if;
};
PE top {
Core core;
};
Graphically the addition of the memory port can be represented like this:
The CE
keyword is used to define communication element types, such as passive
memories or links between modules. CEs are very simple elements, a CE instances simply process
transactions initiated by PEs.
CEs and interfaces are closely related. While interfaces provide purely abstract port types,
CEs are the concrete modules providing those port types. In FAD this is represented using the implements
keyword, which specifies the interface or interfaces that a given CE provides.
To continue the simple CPU example, we can add a CE representing a memory, which implements the MemoryIf
interface.
interface MemoryIf;
CE Memory implements MemoryIf;
PE Core {
MemoryIf memory_if;
};
PE top {
Core core;
Memory mem;
};
We can represent this graphically as follows.
The FAD binding statement is used to specify the connections between modules. The bind
keyword is used within
PEs to describe the connections between that PE's children.
Bind statements are quite flexible, here we'll use a very simple bind statement to complete our basic CPU and memory example:
PE top {
Core core;
Memory mem;
bind core.memory_if {mem};
};
Following along graphically, the model now looks like this:
The service
keyword is used to define a software service type which is used for the
connections between software modules. Services are very similar to interfaces, the key difference being
that services are implemented by PEs, and that they represent software connections rather than
hardware connections.
Conceptually, any PE implementing a service should be thought of as representing a software layer. To continue with our CPU example, we could add a Hardware Abstraction Layer (HAL) service and a PE implementing it.
This will also introduce a second form of binding. Rather than binding an interface to a CE, we will bind one interface to another, crossing a hierarchy. This type of binding allows PEs to forward their ports to those of their children.
service HALService;
PE HAL implements HALService {
MemoryIf memory_if;
};
PE Core {
HAL hal;
MemoryIf memory_if;
bind hal.memory_if {memory_if};
};
Graphically, the addition of the hardware abstraction layer looks like this:
Finally, to complete the example we can add a PE representing the application being run on the CPU. The application will not access the memory directly, instead it will use the facilities provided by the HAL. This will demonstrate another new type of binding, from a service port to a PE implementing that service. Putting everything together, the complete example code is now as follows.
interface MemoryIf;
CE Memory implements MemoryIf;
service HALService;
PE HAL implements HALService {
MemoryIf memory_if;
};
PE Application {
HALService hal_port;
};
PE Core {
Application app;
HAL hal;
MemoryIf memory_if;
bind app.hal_port {hal};
bind hal.memory_if {memory_if};
};
PE top {
Core core;
Memory mem;
bind core.memory_if {mem};
};
The final complete example looks like this:
The previous example demonstrates the basic set of features necessary to build a FAD model. The language provides additional features to make the design and simulation process easier, and more efficient.
FAD has support for declaring arrays of instances inside PEs. For example, if we want to connect our CPU core to multiple memories, we could give it an array of ports as follows.
PE Core {
MemoryIf memories[2];
// ...
};
Binding an array of ports to multiple CEs is simple. The right hand side of a bind statement represents a list of destinations to bind to. A valid bind statement must have the same number of source ports as destinations.
PE top {
Core core;
Memory slow_mem, fast_mem
bind core.memories {slow_mem, fast_mem};
};
In the future we hope to add more support for different types of array bindings. Currently the main supported feature is binding from an array of ports to a list of CEs or ports.
In some cases it's inconvenient to have to hard-code the size of an array. FAD allows array sizes to be unspecified,
as long as the required size can be deduced from the bind statements in which the array is used. In the example above,
the size of the memories
array could be left unspecified, and would be deduced based on the bind statement:
PE Core {
MemoryIf memories[];
// ...
};
PE top {
Core core;
Memory slow_mem, fast_mem;
bind core.memories {slow_mem, fast_mem};
};
This feature is used extensively in our NPU model for the routers in the on-chip network. The number of inputs and outputs of each router is left unspecified, and is deduced separately for each router instance based on which other modules it is bound to.
FAD allows configuration file paths to be specified for each PE and CE, and additionally allows those configuration paths to be overridden by instance-specific configuration paths.
PE and CE type declarations can optionally be augmented by specifying a configuration file path. When one is specified,
the generated SystemC module corresponding to the PE or CE will automatically read from that file, parse its contents
as JSON and make them available to the behavioural code. For example, the following would add core.cfg
as the
type configuration of the core
PE type.
PE Core("core.cfg") {
// ...
}
Instances of PE and CE types with configurations may further specify their own instance configurations which override the values in their type configuration. All of the keys in the instance configuration must be present in the type configuration. At start-up time when the configuration files are parsed this condition is checked and the simulation will abort if the check fails.
Specifying an instance config is done as follows.
PE Core("core.cfg") {
// ...
}
PE top {
Core core("core-instance.cfg");
}
In the behavioural description, when searching for a configuration value associated with a given key, a hierarchical search is performed. First the instance configuration is searched, then the type configuration. If the key is not present in either, the search moves up to the current module's parent. This allows configurations to be made available to entire sub-trees of the FAD model's hierarchy.
FAD currently has basic support for splitting models between multiple source files. This feature is currently a
work-in-progress. The import
keyword is used to cause the compiler to parse additional source files, adding their
types to the current model. The syntax is as follows.
import path.to.module;
The module is converted to a relative file path by replacing .
s with /
s and appending .fad
. The specified file
(relative to the current directory) is then read and parsed. For example, the above import statement would cause the file
./path/to/module.fad
to be included in the current compilation.
In the future we hope to add support for namespaces to allow more modular designs, and support for installing packaged FAD models as libraries.
The formal specification of the grammar and semantics can be found as a work-in-progress on our wiki.