In the opening entry of this series, we introduced modular interfaces and talked about their importance in high-level synthesis (HLS). We touched on the ways that other HLS tools force you to create interfaces–with limiting approaches like ANSI C or off-the-shelf IP–and contrasted that against the strengths of our Cynthesizer tool. By designing with Cynthesizer using SystemC, we showed examples of how the verification, standardization and customization of interfaces is all made easier.
The second part of our series on modular interfaces deals with external memories. It’s unavoidable when you’re designing at an abstract level–at some point you’re going to need a memory. High level algorithms, by nature, make extensive use of loops that operate on arrayed datatypes. Not all HLS tools handle external memories well. With some you have to schedule the memory interface by hand! There are several different ways that Cynthesizer can implement those arrays for you, but it is when they become external memories that Cynthesizer’s modularity really shines.
Arrays In HLS
Let’s say you have an array in your design, maybe something like this:
sc_uint<16> mem[16];
…
for ( int i = 0; i < 16; i++)
{
acc = acc + coeff * mem[i];
}
What can you do with it? Well, in Cynthesizer there are three things you can do. You can:
- Flatten it
- Using a directive or command-line switch, Cynthesizer will flatten the array into individual registers. This is helpful if you want a short latency—your architecture will have access to all array locations in the fewest possible cycles.
- Make it an internal memory
- Using Cynthesizer’s Memory Model Editor, you can create a memory model that Cynthesizer will allocate to store your array. This is good if you have a few extra cycles to spare and don’t want the added muxing of a flattened architecture.
- Make it an external memory
- Using the same memory model, you can instruct Cynthesizer to implement the memory externally. Cynthesizer will synthesize an interface to this memory for you. You will not have to worry about controlling the address, data or enable ports.
It is the ease of making a memory external (and the lack of changes required to your SystemC source code) that we will get into now.
Going External
So you want your array to be an external memory? First thing you need to do is build it. To do that you can invoke the Memory Model Editor in the Cynthesizer Workbench GUI. Here’s what it looks like:
[Click to enlarge]
There’s actually not that much you have to do here. We set “Word Size:” and “Number of Words:” both to 16 to match our array’s size and indices. The “Latency:” is 1 by default, and the “Setup time:” and “Output Delay:” should be a nonzero time value based on the data from your technology library. And since this memory will be external, the “Area:” is 0.
By default this will be a single-port memory. If you want something different you can click the “Ports” tab, where you can increase the number of ports, edit the names of the address and data lines, specify any enable lines or masking, associate each port with a clock or reset, and configure the ports as read-only, write-only or both.
But the real power for us is in the “Internal memories” and “External memories” boxes where you can specify chaining or registering of the memory I/O. That’s right, the SystemC memory model we create will be usable as either an internal or external memory.
So once we’ve generated the memory model (I called it “mem_part”), how do we use it? All you have to do is turn your array declaration into a memory port instantiation. Previously we had this:
sc_uint<16> mem[16];
now we have this:
mem_part::port<ioConfig, sc_uint<16> > mem;
port<> is a templated class in the generated SystemC memory model that represents an external port connection. Its arguments are the I/O configuration, which can be defined for TLM or pin-level simulation, and its datatype. The memory model will also have to be connected to clock and reset but there are high-level functions available to do that. Why, you ask, do we have to do all this? Chances are you chose an external memory because it needs to be shared with other modules. The classes like port<> make it possible to accurately simulate intermodule communication through a shared memory at the behavioral level. With other tools that use ANSI C or don’t have customizable interfaces, you have to wait till you have RTL to verify anything like this.
What do you have to do to the source code? Well, nothing:
for ( int i = 0; i < 16; i++)
{
acc = acc + coeff * mem[i];
}
The array access looks exactly the same as before. Cynthesizer knows to associate your array access with an external memory port, and it schedules the interface automatically based on your latency constraints. The external memory has become a truly modular interface, and it was all created for you. If you generate RTL for this design you will see the synthesized interface in the port list:
module dut(clock, RSTN, inp_busy, inp_vld, inp_data, outp_busy,
outp_vld, outp_data, mem_WE0, mem_DINw, mem_DOUTw,
mem_Aw, mem_REQ0);
input clock;
input RSTN;
…
input [15:0] mem_DOUTw;
output mem_WE0;
reg mem_WE0;
output [15:0] mem_DINw;
output [3:0] mem_Aw;
reg [3:0] mem_Aw;
output mem_REQ0;
…
Next time, we’ll take a look at the techniques used to build custom modular interfaces using Cynthesizer’s Interface Editor.

