High-level synthesis (HLS) is just that–high level–a design approach that lets you work at a level above having to wade through pins and wires and state machines. There are many factors to consider in choosing an HLS tool, but one of them is so fundamental that it often gets overlooked.
It’s interfaces. I’m not just talking about declaring ports and hooking them up. I’m talking about high-level, modular interfaces that encapsulate very complex I/O protocols into easy-to-use function calls. Most of the tools out there offer some kind of interface solution, but we think they miss the mark in giving you what you need to be successful. Some let you describe I/O behavior with standard ANSI C only to add the actual pin-level interface details later in synthesis (which, unfortunately, will be the first time you can actually verify the interface). Some offer only off-the-shelf interface IP but then provide no means of creating custom interfaces. We developed Cynthesizer with all of this in mind. Cynthesizer designs are written in SystemC, where the clock and pin level activity of an interface can be simulated before the tool is run. And Cynthesizer has a family of standard interface IP in addition to an editor where you can create any interface you want.
This is the first in a four-part series on designing and working with modular interfaces in Cynthesizer. Today I’ll be detailing the benefits of modular interfaces. In the coming weeks we’ll be talking about how useful they are with external memories, we’ll detail some of the techniques we use to build them, and we’ll show what exactly is available interface-wise from Forte.
What Is A Modular Interface?
When we talk to customers or prospects we use the word “transaction” a lot. And this is probably the best word to use in describing a modular interface. A transaction, in essence, is what is being communicated across a modular interface. A modular interface can be, for example, a burst write to a standard AHB bus 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 protocol of your transaction (i.e. ports) with the actual functionality of the transaction. Whatever the case, to you as a user it is a single function call alongside your other high-level code.
Why Use Them?
There are many benefits to designing this way. Your code is much simpler and you can describe an algorithm in fewer total lines. Your design effort can concentrate solely on the core of the algorithm at transaction level, all while retaining the flexibility to write custom interface protocols. Your 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, all you need is one testbench to verify all of the RTL architectures your HLS tool produces. Also, as mentioned above, Cynthesizer interfaces correctly simulate the interaction of their clocks and pins at the behavioral level, before you run any synthesis. Remember that SystemC supports both TLM and pin-level I/O configurations. We can’t state enough how critical this is to your HLS success–once your interface is verified behaviorally, it stays verified all the way down to gates or anywhere you reuse it.
Without modular interfaces you would spend a lot of time down in the trenches of I/O protocol design. You would have to declare whatever ports are needed for your transaction, make sure the direction of the ports was correct, declare signals in the parent module to connect them with, and then make sure all the connections are correct. But then comes the hard part, writing some kind of handshaking or acknowledgement scheme so that you only read input data when it is valid and only write output data when the downstream module is ready for it. This means an manual assertion of a ready signal, followed by a loop that sits and waits on a valid signal, followed by reading the data and storing it properly. And remember, you will repeat this for every interface you have.
Just look at the difference of this code with modular interfaces:
in_data = inp.get(); out_data = my_function( in_data ); outp.put( out_data );
as compared to the same code written without modular interfaces:
inp_rdy = 1;
do {
wait();
} while( !inp_vld );
in_data = inp.read();
inp_rdy = 0;
out_data = my_function( in_data );
do {
wait();
} while( !outp_rdy );
outp.write( out_data );
outp_vld = 1;
wait();
outp_vld = 0;
And this is only for a basic ready/valid handshake. As the interface becomes more complex, the difference in the amount of code becomes more dramatic.
How Does SystemC Help?
We’ve touched on it already, but SystemC really lends itself to modular interface design. A SystemC modular interface has two sides: a tidy, transaction-level side like the .get() and .put() calls you see above; and a rougher, pin-level side where the gritty details of the cycle-accurate protocol are defined. Some people criticize SystemC because the OSCI synthesizable subset requires modules to have pin-level ports like sc_in<> and sc_out<>, but in this case it’s an advantage. While you as a designer work at a high level, it is the pin-level ports that are presented to your HLS tool. With the pins broken out, the tool can more optimally combine your interface protocol and datapath with the control FSM. Using modular interfaces does not mean sacrificing quality or reusability.
Next time, we’ll get into designing with external memories–a specific case where modular interfaces save loads of time and effort.
