High-level design (HLD) represents a hardware design at a more abstract level than register transfer level (RTL). A high-level synthesis (HLS) tool then can be used to produce the RTL necessary to implement the design in a standard tool chain. The design language typically supplies the mechanism that provides the “layer of abstraction” the designer can use.
SystemC has become a widely accepted hardware design language because it has both a base “hardware abstraction layer” and the full C++ set of object-oriented features, notably the ability to create objects that encapsulate data and access methods. The use of objects in SystemC is a powerful means of creating abstraction by extension (see “Abstraction Mechanics,” below).
Encapsulation In SystemC
SystemC is nothing more than a C++ class library. It uses the C++ object-oriented features to create a hardware abstraction layer, which can be used to describe a hardware design. The objects that are built into SystemC include the primitive bit-accurate datatypes sc_int<w> and sc_uint<w>, which simply represent bit vectors that are treated like signed or unsigned integers.
SystemC provides another data type, sc_fixed<w,i>, which illustrates the use of encapsulation to create an abstraction by extension.
An object can be declared as sc_fixed<w,i>, which declares it as a bit vector w bits wide, but to be interpreted as a fixed point number with i bits to the left of the binary point. Operations to be performed on such a datum will be performed according to the rules of fixed-point arithmetic, including rounding and overflow. All of the specification of the operations and the data representation is done in the class, and not in the HLS tool. Thus, the base abstraction layer of SystemC was extended by the addition of the sc_fixed<w,i> class.
While a few built-in objects provide abstractions, the power of SystemC comes from its C++ base. This base allows the designer to implement custom abstractions using the same facilities that SystemC uses. These abstractions can be simulated directly in SystemC, because the code is standard C++, and they can be synthesized by an HLS tool with no extra modification to the tool.
Modular interfaces are a particularly useful example of abstraction by encapsulation. They can be implemented entirely in standard SystemC. An HLS tool that is aware of the general feature does not need to be modified in any way to support any particular interface.
A modular interface is simply an encapsulated representation of the communication between a pair of modules across a subset of the signals that connect them. That subset of signals is usually called a channel, and the functions that implement the communication along those signals makes up a protocol (Fig. 1).
In SystemC, transaction level modeling (TLM) was created so interfaces could be encapsulated, and the protocol functions abstracted away to improve simulation runtime efficiency. This is a very effective abstraction mechanism. Here, we describe a similar approach to abstracting interfaces that has enough additional detail to allow high-quality RTL creation by HLS.
At the RTL, simple signals carry single-bit or scalar values from one module to another, either from a parent to a child module or from one sibling module to another. Operations in each module implement the communication protocol that governs transactions between modules.
By encapsulating the low-level signals, ports, and protocol functions in modular interface socket classes, the entire protocol can be abstracted, relieving designers of the tedious connection of individual signals and the recoding of the protocol operation. Instead, they can connect an entire interface with a single binding function and represent the input/output operations with simple function calls. In addition, the modular interface code can be thoroughly tested once, and then reused many times without modification, avoiding common errors and reducing debug time.
A modular interface is defined by a set of C++ classes containing synthesizable SystemC code implementing constructs such as signals, ports, and synthesizable protocol code. Interfaces conforming to common protocols can be written in SystemC and used as library components while interfaces implementing custom protocols can be written in the same fashion and used in one-off projects.
Transactions are communicated across a modular interface. A modular interface can be, for example, a burst write to a standard Advanced High-performance Bus (AHB) model. Or it can be an exchange of data that follows a strict protocol in a fixed number of clock cycles. Or it can simply be the writing of a vector or datatype value with ready/valid handshaking. The modular interface combines the I/O structure of the transaction (i.e., the ports) with the functionality, or protocol, of the transaction. Whatever the case, in the high-level representation of the design, it is a single function call alongside the other high-level code.
The benefits of abstracting the interfaces this way include simpler code at the design level while retaining the flexibility to write custom interface protocols. Connections become easier because all the pins involved in the interfaces are encapsulated in high-level channels. But the real benefit is in verification.
Since handshaking is built into a modular interface, a common testbench can be used to verify both the input to the HLS process and the output of the HLS process. Importantly, modular interfaces allow the correct simulation of the interaction of their clocks and pins at the behavioral level, before synthesis is done. Since SystemC supports both TLM and pin-level I/O configurations, once an interface is verified behaviorally, it stays verified all the way down to gates.
Figure 2 shows a simple function that uses a ready/valid handshake. The code below shows it written using modular interfaces:
in_data = inp.get();
out_data = my_function( in_data );
outp.put( out_data );
As the interface becomes more complex, the difference in the amount of code becomes more dramatic.
Modular Interface Implementation
The C++ class mechanism can be used in SystemC to encapsulate the signal-level connections (i.e., ports) along with the code that implements the signal-level protocol. Two complementary interfaces, input and output, generally are implemented as two modular interface “socket” classes. They’re connected by binding calls to a modular interface “channel” class. The processes in the modules containing the sockets call transaction-level interface functions defined in the socket classes to execute their interface behaviors.
In its simplest form, an output socket for a ready/valid handshake interface might look like the code in Figure 3. Note that the sc_in/sc_out ports are incorporated into the modular interface socket as data members. The two transactions that the port implements, reset() and put(), are also implemented as member functions. The corresponding input socket implements the reciprocal protocol (Fig. 4). Note that the direction of the ports is reversed from that of the output socket.
The modular interface socket can be used in a design in a way similar to how a simple sc_in or sc_out port would be used. The instantiation and binding of the socket look just like an sc_in or sc_out port. To execute the protocol, the code in an SC_CTHREAD calls the transaction functions of the modular interface socket (Fig. 5).
The signals that are needed to provide connectivity for this interface also can be encapsulated in a channel class (Fig. 6). The addition of a couple of binding functions to the modular interface socket allows the entire interface to be bound using a single function call. This reduces the number of lines of code needed to use an interface, allows the interchange of different interfaces with minimal code modification, and prevents trivial errors due to misspelling and misconnecting individual signals.
Figure 7 shows the binding functions in the output socket for our example. The addition of these functions allows the binding to be done using the conventional SystemC port binding syntax:
or, more conveniently:
Also note that the binding functions are defined as templates. This lets the same ports and binding functions be used for port-to-port binding in a hierarchical design.
Using Modular Interfaces In A Hierarchy
In addition to the process control constructs, SystemC synthesis supports the SystemC constructs for the construction of structural hierarchies. Each module can contain any number of cooperating SC_CTHREADs, SC_METHODs, and sub-modules. Communication between modules is achieved using a port-to-signal binding mechanism that is very similar to the one used in RTL representations. Figure 8 shows an example of a hierarchical design using the modular interfaces described previously.
The use of SystemC constructs rather than tool constructs for the implementation of hierarchy and communication improves the overall verification process dramatically. The complete structural hierarchy can be simulated at a behavioral level, accurately representing the concurrency of all the modules and threads, and accurately verifying the pin-level communication protocols between them. The functional verification then can be performed using higher-speed behavioral simulation, eliminating the need for many slow RTL simulations.
Modular Memory References
High-level algorithms, by nature, make extensive use of loops that operate on arrayed data types. Typically, arrays are synthesized either into registers or into a memory. Consider an array in the high-level source code being mapped into a memory external to the module:
for ( int i = 0; i < 16; i++)
acc = acc + coeff * mem[i];
The HLS tool can synthesize a modular interface that will contain the ports and protocol necessary to communicate with this memory. Once these details are encapsulated, the designer no longer has to be concerned with explicitly controlling the address, data, or enable ports.
Supporting such a flow requires a model of the memory component that follows the style described above. Such a model can be constructed in a straightforward fashion, or a memory generation script or tool can be built to create such SystemC classes for a wide range of memory parts.
An HLS tool will need a number of parameters to properly schedule accesses to a memory including latency, setup time, and output delay, as well as structural characteristics like number of ports, width of ports, and number of storage elements in the memory. The memory system will comprise similar components as the RV class in the previous section, namely a port class encapsulating the ports and protocol for accessing the memory, and a memory model.
Consider the following example:
To turn this array declaration into a memory port instantiation, simply replace the declaration with:
mem_part::port< ioConfig > mem;
port<> is a class template defined within the SystemC memory model that represents an external port connection. The template parameter is the I/O configuration, which can be defined for TLM or pin-level simulation. The memory model will also have to be connected to clock and reset. The port<> class makes it possible to accurately simulate inter-module communication through a shared memory at the behavioral level.
Without interfaces like this that explicitly implement the communication protocols, the behavior of a system of communicating modules cannot be verified using the behavioral level input to HLS. It can only be verified using the RTL output of HLS. The ability to express this protocol and perform this kind of multi-module simulation is one big advantage of using SystemC for HLS as opposed to languages such as C or C++ alone that don’t have the needed hardware constructs.
Consider the use of this external memory. In the simplest form, the read and write access protocols could be encapsulated in member functions of the port class, and used like this:
mem_part::port< ioConfig > mem;
for ( int i=0; i<16; i++ )
acc = acc + coeff * mem.read(i);
Alternatively, by using the C++ overloading technique to write our own implementation of the operator , it is possible to make the memory read look just like an array read:
operator( sc_uint<10> addr)
Given this, the access looks the same whether it is an array or an external memory:
mem_part::port< ioConfig> mem;
for ( int i = 0; i < 16; i++)
acc1 = acc1 + coeff * mem[i];
acc2 = acc2 + coeff * array[i];
The array accesses look exactly the same. The HLS tool can associate the mem array access with an external memory port, and it can schedule the interface automatically based on the latency constraints. The external memory has become a modular interface.
Encapsulation shows the power of abstraction by extension. Modular interfaces are a very effective example of encapsulation to provide abstraction in high-level design and the corresponding synthesis process. The method is broadly applicable and follows the spirit of SystemC transaction level modeling.
This enables the bulk of the verification effort to be completed before the synthesis step, where it is most efficient. The abstraction supported is meaningful and effective. The way modular interfaces have been implemented in SystemC as shown here has been used in production in hundreds of industrial design projects along with Cynthesizer, Forte Design Systems’ HLS product, and has proven to be practical, effective, and easy to deploy.
Academics have studied the implementation of levels of abstraction in computing systems. Most of this work was performed in the context of software systems, primarily operating system design. In 1975, Jack Dennis summarized this work* and identified two techniques that are applicable to the design of hardware systems:
- Translation: This is the most natural means of implementing abstraction levels in the hardware design environment and the one we are the most familiar with. A logic synthesis tool translates an RTL input description to a gate level output description, much the way a C compiler translates C source code to machine code. In the context of high-level design (HLD), translation provides abstraction of time and space. A computation to be synthesized is written in normal procedural code, which represents the computations to be performed. In that code, there is no indication of time required to perform the computation. The HLS tool translates that into a state machine that schedules the operations appropriately. Space is similarly abstracted by the HLS tool deciding how many functional units to use and how they should be combined to perform the computation.
- Extension: Abstraction by extension is less commonly recognized, but is an equally useful means of implementing an abstraction layer. Procedures are added to the lower level to express the primitive operations of the new, higher level. These new data structures and operations are added to the lower level so the new level includes both the new facilities and the lower-level ones. This is the mechanism that is provided by object-oriented languages and used by C++-based hardware description languages, notably SystemC.