Chapter 3 - Coding for Synthesis
Contents
Introduction
Coding Rules
Doing the Lab
This tutorial introduces coding considerations for synthesis - essentially
how to code to minimize chip area or to maximize operation throughput
(minimize latency), or how to strike a balance between these extremes.
It also introduces you to methods of setting compiler constraints to
alter the synthesis result of a given design.
There are many factors that influence the coding style used for any particular
design. Some of these might be:
- Ease of development - given an algorithm described in C, how quickly
can it be translated into Verilog or VHDL? Some constructs, such as pointers,
are not synthesizable. On the other hand, a struct in C is easily
represented as a RECORD in VHDL.
- Understandability - less code is not always better. What is obvious to
one person may perplex someone else.
- Flexibility through synthesis - in general, a more abstract description
will allow a behavioral synthesis tool greater freedom to explore design space
- Predictability through synthesis - if a particular hardware implementation
is desired, it may be best to code in a manner that restricts the design
space to be explored.
For a thorough discussion of these coding issues, see Chapter 13
in "Understanding Behavioral Synthesis" by John Elliott.
Coding for behavioral compilation is often subtly different from common
practices of Verilog or VHDL coding. In most cases the differences are
actually adherence to behavioral coding rules that are good practice design
rules
that are less stringent for non-behavioral coding.
The behavioral coding rules are:
Basic Rules
- At least one wait (clock) in every loop except for an unrolled loop
(we will discuss rolled and unrolled loops later)
- At least one wait separating successive writes to the same output
Reset Rules
- Only synchronous resets can be modeled although asynchronous can
be forced using the Behavioral Compiler command:
set_behavioral_reset -async
- At least one wait statement after the reset action and before
the main loop
- Only simple (single clock cycle) resets allowed.
Complex resets such as B <= in1 + 4;
are evaluated at the active edge of the following clock cycle, so are not
allowed. Complex resets are more properly called initialization and
must be handled separately from reset.
- Although less intuitive, it is often best to place the reset code after
the main code. By doing so, the behavioral description will simulate more
like the synthesized design. If placed first, the reset code always executes
at least once even if reset is not asserted. If placed after the main code,
reset only executes when reset is asserted. This does place a restriction
on your clock coding in that every clock must be followed immediately by
an exit when reset is true
Rules for Conditionals
- If one branch has a wait, all branches (including the default) must
have at least one wait statement, however, the number of waits in all
branches do not have to be balanced
Loop Coding Rules - General
- Exiting each level of loop hierarchy takes one clock cycle
- Exiting a reset loop is the exception to the rule.
- There are slightly different rules for handling loops when
compiling with cycle-fixed mode or superstate mode (more on these
modes below)
Cycle-Fixed Coding Rules - Basic
- Place the number of wait statements required for computation and
memory access between reading the inputs and writing the outputs
Cycle-Fixed Coding Rules - Conditional Loops
- At least one wait statement before a loop
- With n representing the number of cycles required to evaluate a
loop iteration condition:
- Inside the loop, place n wait (clock) statements immediately
after the loop conditional is evaluated
- Outside the loop, place n wait statements immediately after
the loop exit (n>=1)
- Do NOT put any I/O statements before these wait statements
Superstate-Fixed Coding Rules - Loops
- At least one wait statement after the last write inside a loop
and before a loop continue or exit
- At least one wait statement after the last write before a loop
Rules About the Behavior of I/O and Variables
- I/O is created when accessing inputs and outputs, and when
accessing signals between processes
- I/O are treated differently than internal process variables
- I/O are locked to clock edge boundaries
- Variables are not locked to clock edge boundaries. Operations
on variables can be scheduled in any cycle as long as its data
dependencies are satisfied.
- Use variables as much as possible for maximal flexibility and
for register sharing
In the above rules we mentioned rolled and unrolled loops, and we
mentioned cycle-fixed and superstate modes.
Following is a brief discussion of each issues.
Rolled Versus Unrolled Loops
A rolled loop is one in which no iteration of the loop can start
before the previous one has finished. An unrolled loop is essentially
a series of assignments that can happen in parallel. The illustration
below shows how a loop is unrolled and how a constant is handled.
Because the only iterated item used in the original loop is the loop
index, and the boundaries are known, they can be pre-calculated so each
value is propagated into the series of statements.

On the other hand,
the implication of this for scheduling can be profound.
Take for example
the following loop:
for (i=0; i<=31; i=i+1) begin : outer_loop
Out1[i] = a0 * a1 + a2 * a3 - a4 + a5 + a6 * a7;
for (j=0; j<=15; j=j+1) begin : inner_loop
Out2[j] = b0*b1+b2*b3-b4+b5+b6*b7-b8+b9;
end
end
This loop has 9 operations (multiplies, adds, subtracts) contained
within an inner loop of 16 iterations contained within an outer
loop of 32 iterations that itself contains 7 operations. If the loop is
unrolled it results in 4832 events that need to be individually scheduled.
This will result in a huge amount of computation for the schedule operation,
which means a long schedule run. If we keep both loops rolled
the result is only 16 events to schedule.
So, why would we ever want to unroll a loop? Basically it is a throughput
(latency) versus area issue. A rolled loop requires the smallest
amount of real estate, but it costs the number of clocks used in the loop
multiplied by the number of iterations of the loop to execute the loop.
An unrolled loop schedules individual hardware for each loop iteration,
but it executes the loop only once with the number of clock cycles of the
operations contained within the loop.
Rules (suggestions) for unrolling or not unrolling
- Keep loop rolled when (num iterations) * (num operations)=LARGE
- Keep loop rolled when there are a number of nested loops
- Keep loop rolled when the serial execution of loop iterations
does not cause overhead. That is, when you have a latency budget
to tolerate a rolled loop
- Do not attempt to unroll a loop when data dependencies between
loop iterations prohibit overlapping consecutive loop iterations.
In this case loop iterations must be executed sequentially
Cycle-Fixed Versus Superstate-Fixed Modes
Cycle-Fixed mode is relatively straight forward to understand. You
specify the clocks you want following the rules described earlier in
this tutorial. Then you see if the design can be scheduled into
real hardware.
Superstate fixed mode "changes the rules". While you write your design to
follow a behavior, you may somewhat ignore the actual timing an adder,
multiplier, or other function actually take (in terms of hardware clock
cycles).
Scheduling figures out how many clock cycles are actually required
for a particular technology or architecture, and Behavioral Compiler
"plugs in" those clock cycles.
This does not mean you can totally ignore clocks. For example:
entering, exiting, and iterating in a loop takes a state transition in
the FSM generated. Hence:
- One clock is required to enter a rolled loop conditionally
- At least one clock per iteration of a rolled loop
- One clock is required to exit a rolled loop (for each level of loop hierarchy)
- For superstate-fixed mode, in the absence of I/O before or in a loop, you
do not have to explicitly insert wait statements in your code for these
transitions. But in cycle-fixed mode, you need to specify all the clocks

The benefit of this is that it gives you far more freedom to explore
different technologies or architectures. A possible disadvantage is
that because of the clock cycles superstate-fixed mode inserts, the
RTL or gate level implementation may simulate differently from the
simulation of the behavioral level. This can be overcome with proper
testbench design.
In the interest of not spending too much time waiting for elaboration
and scheduling, we will use a simpler design to illustrate coding for
behavioral compilation.
Change your working directory to coding.
unix% cd coding
unix% bc_shell | tee Log/coding.log
bc_shell > analyze -s -f verilog thresh.v
bc_shell > report_design_lib work
bc_shell > elaborate -s thresh
bc_shell > create_clock clk -p 15
bc_shell > bc_check_design -io cycle
At this point, you will get an error if the design cannot be scheduled.
Use your favorite ASCII editor to view the file
thresh.v which you will find
in the Verilog directory.
Compare the thresh.v
file you analyzed and elaborated to the file below.
Edit thresh.v and save it as
thresh1.v .
// This is to be scheduled in cycle fixed scheduling mode.
// 2. clock period = 15 ns
`define ck begin @(posedge clk); if (reset) disable reset_loop; end
module thresh(clk, reset, in1, in2, threshold, out1);
input clk, reset;
input [7:0] in1;
input [7:0] in2;
input [15:0] threshold;
output out1;
reg out1;
always begin : reset_loop
out1 <= 0;
`ck
forever begin : alg_loop
while (in1 * in2 > threshold) begin: loop1
`ck
`ck
out1 <= 1;
end // while
`ck
`ck
out1 <= 0;
`ck
end // alg_loop
end // reset_loop
endmodule
The change needed is to add a clock in the while statement and after the
while statement's end.
Now that you have corrected and saved your design, clear the design from
the Behavioral Compiler memory and run through the steps with the new
design version.
bc_shell > remove_design -design
bc_shell > analyze -s -f verilog thresh1.v
bc_shell > report_design_lib work
bc_shell > elaborate -s thresh
bc_shell > create_clock clk -p 15
bc_shell > bc_check_design -io cycle
If you completed the edit correctly, you will not get an error.
If you do get an error, go back and make the necessary corrections and repeat
these steps before proceeding with timing the design.
bc_shell > bc_time_design
bc_shell > report_resource_estimates > Reports/estim.rpt
bc_shell > write -h -o DB/thresh_timed.db
Examine the estim.rpt file and note the following:
What is the delay through the multiplier (*)? _____________
What is the chain delay through the multiplier (*)? ________________
What is the word level delay through the Comparator (>)? _________
This information will be helpful in understanding the following steps.
bc_shell > schedule -io cycle -effort medium
bc_shell > report_schedule > Reports/thresh_15ns.rpt
bc_shell > write -h -o DB/thresh_15ns_sch.db
bc_shell > bc_view
Observe the estimated area and latency of the design.
This tutorial section about coding styles stops at this point. If you are
interested, you can go through these
steps trying other clock periods such as 10ns to see the
difference.
You could also re-run the lab this time using superstate mode for the I/O mode.
If you try further experiments, remember to save the scheduled db with
a representative name such as thresh_10ns_sch.db and save your reports with
similar names. Then you can do comparisons between the different runs.
This tutorial section briefly described the
issues related to loops, conditionals, resets, cycle-fixed versus superstate
mode, and other considerations of coding for behavioral compilation.
It showed you the power of behavioral compilation over coding for targeted
architectures and/or technologies.
The Behavioral Compiler class covers these issues in detail.
On to Chapter 4 - Simulation