Skip to content

Digital Logic Basics

Julian Kemmerer edited this page Dec 27, 2020 · 63 revisions

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

Processes / Modules

First understand how you can translate between PipelineC global functions and regular 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);

output <= the_wire;

The equivalent PipelineC is

some_type_t the_reg;
some_type_t some_func_name(some_type_t input) 
{
    //... 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 'global clock crossing wires' and 'feedback wires discussed below.

... 
{
    // 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 to globals
    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 global 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_reg;
uint32_t counter(uint1_t increment)
{
    if(increment)
    {
       counter_reg += 1
    }
    return counter_reg;
}

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

There is no unified global memory space as on a CPU.

Clock domain crossings are the general mechanism for moving data between main functions. See the documentation here.

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.

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/comple 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 though 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).

Global non volatile clock domain crossing wires out and back into the same function are identical to these locally declared FEEDBACK wires (a clock crossing between the same clock domain == a wire).

Hardware bit stuff

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