Using Blackbox to instantiate some HDL

I’m trying to use BlackBox, but I’m having a hard time getting things to work
in the way I want.

What I wanna do is have the Clash generated Verilog instantiate into my
BlackBox’d Verilog module.

First, my verilog file. I checked that this compiles / has valid syntax using
Verilator. Didn’t really check to see if it works:

module counter(input CLK, input RST, input EN, output [15:0] DATA);

reg [15:0] iDATA;
initial iDATA = 0;

assign DATA = iDATA;

always @(posedge CLK)
  begin
    if (RST) 
      iDATA <= 0;
    else if (EN)
      begin
        iDATA <= iDATA + 1;
      end
  end

endmodule

Next, my clash file:

import Clash.Prelude
import Clash.Annotations.Primitive

{-# ANN counter (Primitive [Verilog] "C:/path/to/my/verilog/counter.v") #-}
{-# ANN counter hasBlackBox #-}
{-# NOINLINE counter #-}
counter :: Clock System
        -> Reset System
        -> Enable System
        -> Signal System (Unsigned 16)
counter = undefined

top :: Clock System
    -> Reset System
    -> Enable System
    -> Signal System (Unsigned 16)
top = counter

{-# ANN top 
   (Synthesize
    { t_name = "myCounter"
    , t_inputs = [ PortName "CLK"
                 , PortName "RST"
                 , PortName "EN"
                 ]
    , t_output = PortName "DATA"
    }) #-}

When I call clash, I get an error:

GHC: Setting up GHC took: 2.285s
GHC: Compiling and loading modules took: 0.192s

<no location info>: error:
    Clash error call:
    No BlackBox definition for '<MODULE>.counter' even though thi
s value was annotated with 'HasBlackBox'.
    CallStack (from HasCallStack):
      error, called at src\Clash\Primitives\Util.hs:150:11 in clash-lib-1.8.1-
JBnXUEN9hOK2TSlskJFs4:Clash.Primitives.Util

If I remove the hasBlackBox annotation, clash generates Verilog. But the
Verilog doesn’t do what I expect.

/* AUTOMATICALLY GENERATED VERILOG-2001 SOURCE CODE.
** GENERATED BY CLASH 1.8.1. DO NOT MODIFY.
*/
`default_nettype none
`timescale 100fs/100fs
module myCounter
    ( // Inputs
      input wire  CLK // clock
    , input wire  RST // reset
    , input wire  EN // enable

      // Outputs
    , output wire [15:0] DATA
    );


  <MODULE>_top_counter <MODULE>_top_counter_DATA
    ( .result (DATA)
    , .c$arg (CLK)
    , .c$arg_0 (RST)
    , .c$arg_1 (EN) );


endmodule

I’d like the generated verilog to be:

module myCounter
    ( // Inputs
      input wire  CLK // clock
    , input wire  RST // reset
    , input wire  EN // enable

      // Outputs
    , output wire [15:0] DATA
    );


  counter <MODULE>_top_counter_DATA
    ( .DATA (DATA)
    , .CLK (CLK)
    , .RST (RST)
    , .EN (EN) );


endmodule

Any ideas? My guess is that I’m not providing the path to BlackBox in the right
way.

As you can tell from the generated verilog that you want, you don’t need to provide the path to your module in Clash since it would not end up in the HDL.

There’s multiple ways that you can solve this problem. The first one is writing your own blackbox instantiation template, the second method is using the currently experimental inst function.

Using the experimental inst function
Creating instances is pretty straightforward and have an experimental feature to create instances. We’ll probably move it later, but on our master branch you can find it under
Clash.Cores.Xilinx.Xpm.Cdc.Internal.

This allows you to write the blackbox as follows:

module Example where

import Clash.Prelude
import Clash.Cores.Xilinx.Xpm.Cdc.Internal

counter :: Clock System
        -> Reset System
        -> Enable System
        -> Signal System (Unsigned 16)
counter clk rst ena = unPort @(Port "DATA" System (Unsigned 16)) $
  inst
    (instConfig "counter")
      (ClockPort @"CLK" clk)
      (ResetPort @"RST" @'ActiveHigh rst)
      (Port @"ENA" $ fromEnable ena)

-- Create your top entity
top :: Clock System
    -> Reset System
    -> Enable System
    -> Signal System (Unsigned 16)
top = counter

{-# ANN top
   (Synthesize
    { t_name = "myCounter"
    , t_inputs = [ PortName "CLK"
                 , PortName "RST"
                 , PortName "EN"
                 ]
    , t_output = PortName "DATA"
    }) #-}

There are some more examples to be found across our clash-compiler repository and the bittide-hardware repository, but I’ll leave it up to you to find them :slight_smile:

Like I said, this feature is still experimental but I much prefer it over the alternative.

Creating a custom blackbox template
The recommended method of creating your own static blackboxes is using the InlineYamlPrimitive. You can use this to write a template for the HDL you want to be inserted in your design.

For your counter, I’d write:

module Example where

import Clash.Prelude
import Clash.Annotations.Primitive
import Data.String.Interpolate

counter :: Clock System
        -> Reset System
        -> Enable System
        -> Signal System (Unsigned 16)
counter !_clock !_reset !_enable = deepErrorX "counter: simulation output undefined"
--      ^ This exclamation mark is a bang pattern

-- Prevent compiler optimizations to affect our counter function
{-# OPAQUE counter #-}

-- Create a Verilog and Systemverilog primitive for the counter function
{-# ANN counter (
    let
      funcName = 'counter -- Get the name of the counter function as a string
    in
      InlineYamlPrimitive [Verilog, SystemVerilog]
        [__i|
          BlackBox:
            kind: Declaration
            name: #{funcName}
            template: |
              counter ~GENSYM[counter][0] // Generate a unique name "counter" with index 0
                ( .DATA (~RESULT) // Connect the DATA output to the RESULT of counter
                , .CLK (~ARG[0]) // Connect the CLK input to the first argument of counter
                , .RST (~ARG[1]) // Connect the RST input to the second argument of counter
                , .EN (~ARG[2]) // Connect the EN input to the third argument of counter
                );
        |]) #-}

-- Create your top entity
top :: Clock System
    -> Reset System
    -> Enable System
    -> Signal System (Unsigned 16)
top = counter

{-# ANN top
   (Synthesize
    { t_name = "myCounter"
    , t_inputs = [ PortName "CLK"
                 , PortName "RST"
                 , PortName "EN"
                 ]
    , t_output = PortName "DATA"
    }) #-}

For a more extensive overview of how to use the templating language, see Clash.Tutorial.
* The indices of
* BangPatterns Force GHC to evaluate an argument, making sure that whatever is driving it ends up in the HDL.

EDIT: Adjusted template comment style to match verilog

Thank you so much @Imbollen! I am very interested in the inst functionality,
I’ll have to play with that later.

The BlackBox code you provided works, with some surprising effects I’d love to
talk about.

Firstly, the generated Verilog:

/* AUTOMATICALLY GENERATED VERILOG-2001 SOURCE CODE.
** GENERATED BY CLASH 1.8.1. DO NOT MODIFY.
*/
`default_nettype none
`timescale 100fs/100fs
module myCounter
    ( // Inputs
      input wire  CLK // clock
    , input wire  RST // reset
    , input wire  EN // enable

      // Outputs
    , output wire [15:0] DATA
    );


  counter counter -- Generate a unique name "counter" with index 0
    ( .DATA (DATA) -- Connect the DATA output to the RESULT of counter
    , .CLK (CLK) -- Connect the CLK input to the first argument of counter
    , .RST (RST) -- Connect the RST input to the second argument of counter
    , .EN (EN) -- Connect the EN input to the third argument of counter
    );


endmodule

I was really surprised to see the Haskell comments in the Verilog, but that’s not
too big a deal.

Quick update to the Annotation, which updates the comments to be //:

{-# ANN counter (
    let
      funcName = 'counter -- Get the name of the counter function as a string
    in
      InlineYamlPrimitive [Verilog, SystemVerilog]
        [__i|
          BlackBox:
            kind: Declaration
            name: #{funcName}
            template: |
              counter ~GENSYM[counter][0] // Generate a unique name "counter" with index 0
                ( .DATA (~RESULT) // Connect the DATA output to the RESULT of counter
                , .CLK (~ARG[0]) // Connect the CLK input to the first argument of counter
                , .RST (~ARG[1]) // Connect the RST input to the second argument of counter
                , .EN (~ARG[2]) // Connect the EN input to the third argument of counter
                );
        |]) #-}

Quick notes on the magic from the Clash.Tutorial:

  • ~ARG[N]: (N+1)'th argument to the function.
  • ~GENSYM[<NAME>][N]: Create a unique name, trying to stay as close to the given as possible. This unique symbol can be referred to in other places using ~SYM[N].
  • ~RESULT: Signal to which the result of a primitive must be assigned to. NB: Only used in a declaration primitive.

I really like the flexibility here - we can change function parameters from
Haskell into Verilog parameters:

counter :: Clock System
        -> Reset System
        -> Enable System
        -> Unsigned 16 
        -> Signal System (Unsigned 16)
counter !_clk !_rst !_en !_thresh = deepErrorX "TODO: Define counter simulation output"

Update to Blackbox:

...
            template: |
                counter \#(.THRESHOLD (~ARG[3])) ~GENSYM[counter][0] // Generate a unique name "counter" with index 0
...

Update to top:

top clk rst en = counter clk rst en 10 + counter clk rst en 12

Which generates the following Verilog:

/* AUTOMATICALLY GENERATED VERILOG-2001 SOURCE CODE.
** GENERATED BY CLASH 1.8.1. DO NOT MODIFY.
*/
`default_nettype none
`timescale 100fs/100fs
module myCounter
    ( // Inputs
      input wire  CLK // clock
    , input wire  RST // reset
    , input wire  EN // enable

      // Outputs
    , output wire [15:0] DATA
    );
  wire [15:0] c$app_arg;
  wire [15:0] c$app_arg_0;

  counter #(.THRESHOLD (16'd12)) counter // Generate a unique name "counter" with index 0
  ( .DATA (c$app_arg) // Connect the DATA output to the RESULT of counter
  , .CLK (CLK) // Connect the CLK input to the first argument of counter
  , .RST (RST) // Connect the RST input to the second argument of counter
  , .EN (EN) // Connect the EN input to the third argument of counter
  );

  counter #(.THRESHOLD (16'd10)) counter_0 // Generate a unique name "counter" with index 0
  ( .DATA (c$app_arg_0) // Connect the DATA output to the RESULT of counter
  , .CLK (CLK) // Connect the CLK input to the first argument of counter
  , .RST (RST) // Connect the RST input to the second argument of counter
  , .EN (EN) // Connect the EN input to the third argument of counter
  );

  assign DATA = c$app_arg_0 + c$app_arg;


endmodule

So so cool!

1 Like

Aahh the comment style is an oversight on my part. Thanks for pointing that out!