Skip to content

Digital Logic Basics

Julian Kemmerer edited this page Mar 27, 2023 · 61 revisions

This page describes basic examples common to most HDLs. Ultimately these should not be very interesting.

For real designs see something the Arty blinking example or all examples.

Top Level IO

See more information on specifying top level modules+connections.

Processes / Procedures / Functions / Modules

PipelineC stateful functions (functions that maintain state w/ static local variables) are just combinatorial logic and registers (can't be autopipelined), so they translate directly to HDL processes.

Consider the following generic VHDL:

-- Combinatorial logic with a storage register
signal the_reg : some_type_t;
signal the_wire : some_type_t;
process(input_wire, the_reg) is -- inputs sync to clk
    variable input_variable: some_type_t;
    variable the_reg_variable : some_type_t;
begin
    input_variable := input_wire;
    the_reg_variable := the_reg;

    -- ... Do work with 'input_variable', 'the_reg_variable'
    -- and other variables, functions, etc and it kinda looks like C ...

    the_wire <= the_reg_variable;
end process;
the_reg <= the_wire when rising_edge(clk);

-- Connects output port
output <= the_wire;

The equivalent PipelineC is

some_type_t some_func_name(some_type_t input) 
{
    static some_type_t the_reg;
    //... Do work with 'input', 'the_reg'
    //... and other variables, functions, etc...

    // Return connects output port
    return the_reg;
}

PipelineC functions are a single clock domain, rising edge assumed. Function arguments are input ports, the return value is the output port. Function bodies are combinatorial logic dataflow graphs.

Modules + Composability

For more information on modules check out this page.

Wires

These wires are contained within a single function/module/process. As in C, data flows from inputs to return, thus these wires are all unidirectional (typical C 'execution order'). These wires differ from the 'clock crossing wires' and 'feedback wires' discussed below. To be clear: using and assigning to non-static local variables creates wires, as opposed to those variables being the single wire.

... 
{
    // Assigning to local variables creates wires; standard C assignment behavior
    // Data flows from function inputs to return
    float x = y; // Wire from whatever is driving y to x
}

Registers

//input_t input_reg;
//output_t output_reg;
output_t func(input_t in_wire)
{
    static input_t input_reg; // Prefer static locals
    static output_t output_reg;

    // Reading directly from a register and assigning it to the return value is an output reg
    // Good practice to do at the start of function to ensure any future logic driving to output_reg
    // is not what is going out the output port
    output_t out_wire = output_reg;

    // ... function combinatorial logic using output_reg and and input_reg variables as desired ...

    // Assigning to a register reading directly from an input is an input reg
    // Good practice to do the end of function to ensure any prior reads from input_reg 
    // are from the static state any not just a pass through of the in_wire
    input_reg = in_wire;

    return out_wire;
}

Simple Gates - or any combinatorial logic really - its just plain C code...

uint1_t a_gate_example(uint1_t in0, uint1_t in1)
{
    return in0 & in1; // Or, xor, etc
}

Counter

uint32_t counter(uint1_t increment)
{
    static uint32_t counter_reg;
    if(increment)
    {
       counter_reg += 1
    }
    return counter_reg;
}

'Global Memory' / Moving data between functions / Clock domain crossings

See pages on how to use global variables and clock crossings to move data between functions.

Feedback / Backwards propagating signals / Flow control

In PipelineC signal flow is from inputs to return output. However, in digital logic it is often necessary to send signals in the opposite direction of data flow. FEEDBACK wires can be used to construct backwards flowing wires as described below. (All assignments to a variable prior to it's FEEDBACK pragma are removed/ignored, do reads to variable before writes).

Pseudo Code Example 1:

feedback1

main(i)
{
    bar_to_foo
    #pragma FEEDBACK bar_to_foo    

    foo_to_bar = foo(i, bar_to_foo)

    rv, bar_to_foo = bar(foo_to_bar)
    
    return rv;

}

Example 2:

feedback2

uint32_t main(uint32_t x, uint32_t y)
{
  uint32_t x_feedback;
  #pragma FEEDBACK x_feedback
  
  // This doesnt make sense unless FEEDBACK pragma'd
  // x_feedback has not been assigned a value
  uint32_t x_looped_back = x_feedback;
  
  // This last assignment to x_feedback is what 
  // shows up as the first x_feedback read value
  x_feedback = x + 1;
  
  return x_looped_back + y;
}

These backwards flowing signals can be thought of as leaving a function, outside of the function propagating backwards towards the inputs, and then reentering the function in a forward direction to be read like a normal wire (though carrying backwards propagated value).

Clock crossing/wires traveling out and then back into the same function are identical to these locally declared FEEDBACK wires (a clock crossing between the same clock domain == a wire).

Hardware description specifics

C syntax isn't ideal hardware description language. There is a fair amount of autogenerated code to help with that.

Raw HDL Escape Hatch + Existing IP

Write raw HDL if you must.

More Examples

Continue onto more examples like LEDs and UART examples on a real board.

Clone this wiki locally