loading...
Cover image for Learning FPGA programming, key points for a software developer (part 3, code patterns and inferred behavior)

Learning FPGA programming, key points for a software developer (part 3, code patterns and inferred behavior)

targeted profile image Dmitry Dvoinikov ・6 min read

The previous parts of this article discussed the time and registered logic. This part is specifically about writing VHDL. It opens with general discussion and then presents code samples and analysis.

VHDL is a declarative language, in which you declare an intent, and let the compiler find the way of implementing it. In a good declarative language it must be easy to express the intent, and the intent must be clear from the code.

You know what other common language is declarative ? SQL. VHDL and SQL are absolutely unrelated of course, but for an opening illustration SQL could be useful.

So, in SQL you write queries that manipulate data. Consider the following simplest SQL query:

SELECT x FROM t WHERE y = 1

The intent here is to find matching records from relation t and return a set of rows with just x in them. But it would be a mistake to assume that the engine would go through table t, check every record for equality of y and extract x from each. Why ?

Because nothing even indicates that t is a table and either x or y are fields. t could be a view, and x and y could be calculated expressions, even constants. In the latter case no data access would be required at all. But even if, we have no idea how it is going to be accessed. SQL optimizer might decide to read an index, rather than the table, and then depending on its coverage, a lookup to the table may or may not be required. Also about indexes, nothing suggests that a suitable index exists, heck, that the engine supports indexes in the first place.

Therefore, to write an efficient SQL query you must take a lot vendor-specific things into account. You may want to read query plans, rearrange the query and even resort to optimizer hints. The query could run either way, but could turn out impractically slow.

Same is true about VHDL.

But VHDL is poorly suited for the job. That it looks like a regular imperative language with conditions, loops, functions and even class-like protected structures, suitable for general programming, is extremely misleading. It doesn't follow a theoretical model the way SQL does, nor does it have first class representation for the objects it actually manipulates.

You should not read nor write VHDL as you would Ada (or Pascal). Instead, you should look at VHDL program the same way the compiler does. And when I say "compiler", I mean the entire chain of tools, or any one in particular.

What's the purpose of the compiler ? To turn your code into a binary configuration file for a particular FPGA device. And every device is different, vendors compete by embedding various reusable blocks. Memory blocks, arithmetic units, all kinds of controllers and even entire CPU's make their way to an FPGA chip. Then where one compiler could use a ready block, another will have to implement it from smaller bricks.

The smallest bricks in FPGA are LUT's (programmable wires) and registers. These are effectively the two kinds of objects we have in a VHDL program. Except for vendor-specific attributes (a-la SQL hints) we never mention anything else. How would a compiler know when to reuse a bigger block, when all it sees is wires and registers ? Besides, even with wires and registers, the best way to arrange them depends on the situation, but how would a compiler even know what the intent was ?

It would help, if the compiler knew what we meant from what we wrote.

The compiler tries to guess the meaning by looking for certain patterns in the code. Once a pattern is detected, an expected behavior is inferred and a known solution is applied, hoping that it is correct and efficient.

As with SQL, it may or may not be, and it is inevitable that we have to read the compilation reports and make sure there is no blunder.

Let's have some examples.

1 . The simplest pattern is this:

c <= a * b

is clearly a multiplication, and an embedded multiplier could be used, or a specific algorithm, depending on how large a and b are.

2 . This here

v: std_logic_vector;
if v = (others => '0') then

infers a zero comparator. Notice that it is different from a general comparator

v1, v2: std_logic_vector;
if v1 = v2 then

and that they produce quite different configurations.

3 . Patterns could span multiple lines, and as such are more like wildcard fragments in the parsing tree. This here

if cnt = 0 then
    ...
else
    cnt <= cnt - 1;
end if;

is a counter pattern. Again, notice how counting up produces a very different configuration

if cnt = max then
    ...
else
    cnt <= cnt + 1;
end if;

and is also somewhat less clear, even when max is a constant.

4 . In some cases there is no way the compiler could correctly guess the intent from the declaration and you have to use a vendor-specific attribute:

type ram_t is array (0 to 63) of std_logic_vector(0 to 7);
signal ram: ram_t;
attribute ramstyle: string;
attribute ramstyle of ram: signal is "MEMORY BLOCK";

With this attribute set, the compiler would utilize the embedded block memory the device might have, and without it, it would have just wasted a lot of precious registers.

Undoubtedly there are a lot more cases, but you get the idea.

Another thing, which may not be a pattern per se, but is similar in concept, and made an important paradigm shift for me was about functions. In software we think of a function as of some kind of generator, something that takes inputs and produces output. As if a new object is instantiated inside the function from the input arguments and returned as a new entity.

VHDL functions should be read differently - all they do is transform input signals to output signals. This way, a VHDL function is a wiring fragment, not a generator. So where in C you had

int add(int a, int b) {
    return a + b; // create a new int and return it
}

in VHDL you have

function add(a: integer; b: integer) return integer is
begin
    return a + b; -- wire a and b together and stick new wires out
end;

and

from this observation there is just one step to looking at all the VHDL code like that - that it is only wire fragments decoupled by registers.

Next, I'd like to give you the most important pattern of all, the component itself.

You see, most of the components that you have look like this:

entity edge_detector_with_passthrough is
    port
    (
        clk:      in std_logic;
        i_input:  in std_logic;
        o_output: out std_logic;
        o_edge:   out std_logic
    );
end edge_detector_with_passthrough;

architecture mixed of edge_detector_with_passthrough is

    signal b_input: std_logic;

begin

    o_output <= i_input;

    process (clk) is
    begin

        if rising_edge(clk) then

            b_input <= i_input;
            o_edge <= '0';

            if i_input /= b_input then
                o_edge <= '1';
            end if;

        end if;

    end process;

end mixed;

and the reason for this is, lacking better form of expression, by writing the code in this exact way, we telegraph our intent to the compiler. Specifically, in the above code, when we write

if rising_edge(clk) then

it doesn't mean that we are interested in the actual edge of the clock signal. Instead, we mean that

everything that happens inside that "if" is supposed to be synchronized with the clock signal (i.e. registered)

If you look closely, you see that there are two identically looking signal assignments:

o_output <= i_input;
b_input <= i_input;

but when the first one resides in a combinatorial (i.e. unregistered) area of the component, it gets translated to a direct wire, the second happens to be inside the synchronized block and therefore a register is inferred between b_input and i_input. Similarly o_edge becomes registered just by the fact of being assigned from inside the registered block. Which gives you the following diagram:

Alt Text

Finally, there is a case when it's not even what you wrote that affects the the compiler output, but what you didn't write. I'm talking about latches.

The following fragment

architecture latched of latch is
begin

    if i_enable = '1' then
        o_output <= i_input;
    end if;

end latched;

is hardly worth a second look, but is actually a quite important case of a missing "else". There is no else, you see. From software perspective, it's perfectly normal, if you do nothing, nothing happens.

But as this code resides in the combinatorial area of the component, it describes direct wiring of signals and reads "if enable is high, pass input to output". But what happens to output otherwise ? Unlike in software, nothing cannot happen in hardware. Output must be provided in any case, and if enable is low, we need to keep the previous input somewhere and pass it to output, to create an illusion that "nothing happened". Therefore, in this case a latch is inferred to hold the signal while enable is low.

The moral here is that VHDL makes writing clear programs quite problematic. There is no domain-specific syntax in the language and no first class objects. Therefore, rather than simply declaring the intent, you have to second guess the compiler and write spell-like constructs.

Why it turned out this way, I could guess that back in the days Ada was some kind of a military standard language, and when a committee decided what to use for VHDL, they did what committees do best - stick to the standards. Alas.

Anyway, thank you for reading and I'll be happy to talk to you later.

Posted on by:

targeted profile

Dmitry Dvoinikov

@targeted

I am a programmer and I love my job. 20+ years of programming for money plus 8 years of programming for fun before that

Discussion

pic
Editor guide