Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a BlackBox primitive for blackboxes that trivially forward to an RTL module #248

Open
thoughtpolice opened this issue Sep 2, 2017 · 2 comments

Comments

@thoughtpolice
Copy link
Contributor

thoughtpolice commented Sep 2, 2017

I've ended up having this kind of design pop up several times where, essentially, I want to structure my overall project into two components:

  1. A shell for my FPGA board, that abstracts out pin connections and signals to physical parts of the board. This shell exposes these signals in a generalized way to an external RTL module, which acts as the "brain". This part essentially acts as a "Board Support Package" (BSP) for any given kit. While peripherals are generally the same across devices, loosely speaking (e.g. "clock line", "ADC input"), part numbers, configuration values, power configuration, pin physicality (might have a made-up pin on some BSP shells!) are all board specific and need to be handled differently.

An important invariant is that all BSP shells expose the same interface to the RTL core, so the core logic is independent of device characteristics (as much as possible, anyway).

  1. An application that contains the "core logic" or "brain" of the design. This application never touches physical pins or signals directly -- it is defined solely in terms of the API and signals defined by the shell, and always expects the shell to provide these signals.

This kind of layout is used traditionally in partial reconfiguration designs for Altera/Xilinx boards -- for example, the AWS F1 uses this kind of design to support "hot reload" of FPGA cores, and many people use partial reconfiguration for other things. However, in general it's also just good engineering and makes ports and things like incremental recompilation, and device-independent verification, much easier.

Doing this in Clash today is workable, but a bit of a chore.


To do this today, first you define a TopEntity that takes signals from the board, and trivially passes them to a NOINLINE blackbox:

module MyAlteraBoardBSP
  ( shell
  ) where

app# :: ... inputs ... -> ... outputs ...
app# ... = DUMMY DEFINITION
{-# NOINLINE app #-}

shell :: ... inputs ... -> ... outputs ...
shell x y z ... = ... app# x y z ...

{-# ANN shell
  (defTop
    { ...
    }) #-}

So the shell is the top module, and it only calls app#. Shell takes pins and signals from the board and morphs them into the signals expected by app#, however it needs to for the current device. app# then has a blackbox JSON definition that might look like this:

[ { "BlackBox" :
    { "name" : "Shell.app#"
    , "type" :
"app#
  :: HasCallStack    -- ARG[0]
  => ...
"
    , "templateD" :
"app_module ~GENSYM[app_inst][1]
  ( .clk    (~ARG[1])
  , .rstn   (~ARG[2])
  , .inp    (~ARG[3])
  , .outp   (~RESULT)
  );"
    }
  }
]

Then, app# is appropriately forwarded to an instantiation of the app_module Verilog module.

But this is tedious! So this ticket is about having the clash compiler support this use case automatically. While it's relatively "simple" I think it would also be very useful, and it helps encourage people to structure their core logic independent of device signals. This really helps when trying to test things, etc. (Clash is already pretty good at it of course!)


Here's how this might look, I think:

app#
  :: Clock CLK 'Source
  -> Reset CLK 'Asynchronous
  -> Signal CLK Bit
  -> Signal CLK (BitVector 2)
app# = blackbox#
{-# NOINLINE #-}

{-# ANN app#
  (defBlackbox
    { bb_entity = "app_module"
    , bb_inputs = [ PortName "clk", PortName "rst", PortName "input" ]
    , bb_outputs = [ PortName "output" ]
    }) #-}

This looks like a topEntity but rather, the Blackbox says, "Forward calls to this blackbox to app_module using the naming scheme for ports defined by bb_inputs and bb_outputs."

This dovetails nicely as an "inverse" to TopEntity, I think. For example, you could define another Haskell module using a TopEntity annotation that looks near identical to this Blackbox annotation, compile them both, and viola, you have separate generation of the shell from the blackbox, with no needed .json files or custom Verilog/VHDL/SystemVerilog code.

Now, the thing is, with the new multiple topentity support -- this is already pretty close to how the compiler works anyway internally! For example, I could have just given app# a real, but fake definition, and instead of BlackBoxing it, specify it as its own TopEntity. When I compile the code, Clash will then generate two HDL modules, where the shell module calls app just as I expected. Then I could just ignore the app# RTL code and only use the shell RTL code during synthesis. In a sense, the compiler already has to maintain this kind of mapping between dependent TopEntities. (And the "shape" of a dependent topentity (its port names, type, etc) are defined in the manifest Clash spits out, so it must be tracked!)

So in a way, this is just a variation of what TopEntity does already, I feel. It would essentially "inject" a manifest and the compiler generates some boilerplate. The nice part is A) I don't have to write meaningless definitions and pointless TopEntities, B) it's more explicit about what is being accomplished, and C) I don't have to write RTL by hand (3 times!) for these use cases.

There are other reasons this would be good. For example, in a build system, I may want to compile the Haskell code for Shell once, and compile that into an "object file" that I can use later. Altera tools support this, allowing you to save synthesis time by re-using an already compiled shell. However, without BlackBox, I have to rewrite the RTL shell code in Verilog/SystemVerilog/VHDL, each time I change the shell/app interface. That sucks, although the topentity hack would get around it.

Thus, I think this is a pretty useful addition on its own.


Question: Why the blackbox# call? Because there's no meaningful way to simulate the application, when the entire point of the shell is to forward physical board signals. So the definition almost never matters; thus, I'd assume blackbox# is really just blackbox# = errorX "bad! blackbox primitives can not be simulated!" or something instead, and it's merely to make it clear "this is a blackbox definition" for aesthetics.

However, in the past for large circuits, I tried this using error -- and I had many problems with GHC floating out error calls when there were more than one, breaking this approach. That means I instead had to use a bullshit definition that did not use error at all, but simply type checked, and gave back bullshit values. This caused compilation to succeed "as expected".

I haven't investigated this exactly, but perhaps Clash can handle this easier if there's explicit support for it. And if not, blackbox# is not crucial to the whole idea, merely a helpful tool for you to easily "stub out" a definition, thanks to its polymorphic type forall a. a

@christiaanb christiaanb added this to the 1.0 milestone Dec 18, 2018
@christiaanb
Copy link
Member

I think we might sorta have something for this, flagging as a 1.0 feature if we actually do; otherwise it should be bumped to 1.1

@martijnbastiaan
Copy link
Member

@christiaanb Could you elaborate? I assume you're thinking about implementing this with blackbox functions?

@christiaanb christiaanb modified the milestones: 1.0, 1.1 Jun 3, 2019
@christiaanb christiaanb modified the milestones: 1.1, 1.2 Jan 16, 2020
@martijnbastiaan martijnbastiaan removed this from the 1.4 milestone Jan 19, 2022
leonschoorl pushed a commit that referenced this issue Jul 31, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants