How to use a BlackBox to specify a predefined type in existing HDL

I would like Synthesize topEntity to generate the following code Note packet_control_t is predefined type:

library nuand;
    use nuand.fifo_readwrite_p.all;

entity gnodeb_top is
  port (
    tx_clock                :   in      std_logic ;
    tx_reset                :   in      std_logic ;
    tx_packet_control       :   in      packet_control_t;
    tx_packet_empty         :   in      std_logic;
    tx_packet_ready         :   out     std_logic;
    tx_leds                 :   out     std_logic_vector( 2 downto 0)
  ) ;
end gnodeb_top ;

Instead the generated code looks as follows:

entity gnodeb_top is
 port(-- clock
      tx_clock          : in gnodeb_top_types.clk_System;
      -- reset
      tx_reset          : in gnodeb_top_types.rst_System;
      tx_packet_control : in std_logic_vector(50 downto 0);
      tx_packet_empty   : in boolean;
      tx_packet_ready   : out boolean;
      tx_leds           : out std_logic_vector(2 downto 0));
end;

My Clash code looks as follows:

import Clash.Prelude
import Clash.Signal
import Clash.Annotations.TopEntity
import Clash.Annotations.Primitive
import Data.String.Interpolate      (__i)
-- PacketControl type matching VHDL packet_control_t record
data PacketControl = PacketControl
  { pktCoreId  :: Vec 8 Bit    -- std_logic_vector(7 downto 0)
  , pktFlags   :: Vec 8 Bit    -- std_logic_vector(7 downto 0)
  , pktSop     :: Bool         -- std_logic
  , pktEop     :: Bool         -- std_logic
  , pktData       :: Vec 32 Bit   -- std_logic_vector(31 downto 0)
  , dataValid  :: Bool         -- std_logic
  } deriving (Show)


{-# ANN type PacketControl (InlinePrimitive [VHDL] [__i|
  BlackBox:
    kind: Declaration
    name: GNodeBTop.PacketControl
    template: |-
      packet_control_t
   |] ) #-}

-- Top entity definition
topEntity
  :: "tx_clock" ::: Clock System
  -> "tx_reset" ::: Reset System
  -> "tx_packet_control" ::: PacketControl
  -> "tx_packet_empty" ::: Signal System Bool
  -> ( "tx_packet_ready" ::: Signal System Bool
     , "tx_leds" ::: Signal System (Vec 3 Bit)
     )
topEntity txClock txReset txPacketControl txPacketEmpty =
  ( txPacketReady
  , txLeds
  )
  where
    -- These are placeholder implementations
    txPacketReady = pure False
    txLeds = pure (repeat low)

{-# ANN topEntity
  (Synthesize
    { t_name   = "gnodeb_top"
    , t_inputs = [ PortName "tx_clock"
                 , PortName "tx_reset"
                 , PortName "tx_packet_control"
                 , PortName "tx_packet_empty"
                 ]
    , t_output = PortProduct ""
                   [ PortName "tx_packet_ready"
                   , PortName "tx_leds"
                   ]
    }) #-}

How can I define my BlackBox to achieve the desired VHDL?

Hello tich and welcome!

I’m afraid the user cannot control the types of ports in a generated entity like that. By far the simplest solution would be to write a wrapper in VHDL that simply converts the port types of this entity to what you want to have. If your Clash-generated entity isn’t the true top entity of the design and you combine it with stuff that is not part of what Clash generates, that might be a pretty natural solution.

But while custom primitives generally only control part of the architecture of a generated VHDL entity rather than the whole entity, you can actually have Clash generate additional files where you do have full control. The following example combines two features:

module IncludeFile where

import Clash.Annotations.Primitive
import Clash.Prelude
import Data.String.Interpolate (__i)

topEntity ::
  Unsigned 8 ->
  Unsigned 8
topEntity x = includeFile `hwSeqX` x
{-# OPAQUE topEntity #-}

includeFile :: ()
includeFile = ()
{-# OPAQUE includeFile #-}
{-# ANN includeFile hasBlackBox #-}
{-# ANN includeFile (
   let
     primName = 'includeFile
   in InlineYamlPrimitive [VHDL] [__i|
      BlackBox:
        name: #{primName}
        kind: Declaration
        renderVoid: RenderVoid
        template: |-
          -- You could reference ~INCLUDENAME[0].txt
        includes:
          - name: pickaname
            extension: txt
            template: |-
              Hello world!

              We have control over the contents of the complete file.
   |]) #-}

With the includes key, we can list one or several files to be generated whenever this primitive is referenced. Here, I’ve just created a literal text file, but it could be a VHDL file.

Using clash-lib’s BlackBoxFunction, you could even generate the actual text using Haskell code, which can reference arguments you give to the primitive to control the result. But this requires a deeper dive into clash-lib I won’t do right now, and also, I’d like to note that not only is it hardly documented, it is also not a stable API, so we can change clash-lib between minor releases. But it’s out there, and if you want more info we can provide it.

Anyway, back to what I’ve done here. If your code references includeFile (correctly…), Clash will generate a file with a name like topEntity_pickaname_6367058288311B08.txt, where it prefixes the name with the synthesis entity and the hexadecimal number is a hash of the context, such that if you reference it multiple times but the context for the primitive is identical, it will only generate one such file and reference that one file multiple times instead of creating a new file for each reference. The template for the include file has the exact same syntax and substitution features as the template for the primitive itself, but now we control the whole generated file instead of just a part of the architecture.

I use another feature: the combination of RenderVoid and hwSeqX. Normally, the output of your primitive needs to be used before it will be rendered. These two together allow us to generate HDL even when the output of the primitive is unused. This might be a useful trick if you want to try and use this approach with a separate file to glue things together in your design.

The hasBlackBox annotation here prevents us from accidentally generating Verilog or SystemVerilog for which we did not define a black box. HDL generation will only look for primitive annotations for the language being targetted, so for Verilog and SystemVerilog it would not even notice the InlineYamlPrimitive [VHDL] and happily render the body of the function instead of the black box. hasBlackBox turns this case into an error.

All functions annotated with primitive or top entity annotations need to also have an OPAQUE pragma. Or if you’re using a GHC older than 9.4, NOINLINE, since OPAQUE doesn’t exist there.

You can get library and use calls in your generated VHDL with the libraries and imports keys of the primitive YAML. For instance, this primitive generates a VHDL package with a function definition it needs and then adds a use ~INCLUDENAME[0].all line to every Clash-generated VHDL file that needs that package (with the proper name substituted).

I will finally note that if you replace PortName "tx_packet_control" in your original code by

                 , PortProduct "tx_packet_control"
                   [ PortName "core_id"
                   , PortName "flags"
                   , PortName "sop"
                   , PortName "eop"
                   , PortName "data"
                   , PortName "data_valid"
                   ]

you’ll at least get this entity from Clash:

entity gnodeb_top is
  port(-- clock
       tx_clock                     : in gnodeb_top_types.clk_System;
       -- reset
       tx_reset                     : in gnodeb_top_types.rst_System;
       tx_packet_control_core_id    : in std_logic_vector(7 downto 0);
       tx_packet_control_flags      : in std_logic_vector(7 downto 0);
       tx_packet_control_sop        : in boolean;
       tx_packet_control_eop        : in boolean;
       tx_packet_control_data       : in std_logic_vector(31 downto 0);
       tx_packet_control_data_valid : in boolean;
       tx_packet_empty              : in boolean;
       tx_packet_ready              : out boolean;
       tx_leds                      : out std_logic_vector(2 downto 0));
end;

Thanks DigitalBrains, I appreciate your explanation. My clash generated entity is the true top entity so I will follow your advice to write a VHDL wrapper.

Thank you once again.