The Hardcaml_hobby_board library provides a simple for for targeting demo designs on
FPGA boards.
We currently support two Xilinx boards:
- Nexys-A7 100t.
- Arty-A7 35t and 100t.
The framework coordinates access to the FPGA pins, provides some useful peripheral designs, and generates scripts to drive the FPGA build process.
The FPGA pins are split up into logical units according to their function. A group of pins, which we refer to as a subsystem, might be used to drive a SPI interface, or ethernet or VGA or something else.
This grouping of pins may consist of:
- Inputs into the FPGA
- Outputs driven from the FPGA
- Tristate signals which are both read and driven within the FPGA.
It is not necessarily the case that a particular subsystem has all 3 types of pin - a bank of LEDs for example will only have outputs driven by the FPGA.
At it's most general, we interact with a subsystem via two functions which are defined in
Board_intf.M_IOT. The first is the create:
val create : board -> Signal.t I.t * Signal.t T.t
This returns the inputs coming into the FPGA for this subsystem and the value of any tristate pins. It also registers the subsystem with the board infrastructure so it knows how to generate constraints and project files for FPGA vendor tools.
The second function is complete:
val complete : board -> Signal.t O.t -> Signal.t T_enabled.t -> unit
This takes the values which with we want to drive subsystem outputs and a type
T_enabled.t which controls how tristate values are to be driven.
Looking more closely at T_enabled it has type 'a With_valid.t T.t'. So each tristate
pin is defined using With_valid.t. Digging into With_valid.t we see it is an interface type with two fields
- A 1-bit
validsignal - An arbitrary width
valuesignal
For tristates we interpret this to mean:
- if
validis high, then we drivevalueon the tristate output pin - otherwise, we drive high-impedance (ie
Z)
As noted before, subsystems will often not use inputs and outputs and tristates. In this
case we can simplify the above create and complete functions.
For example if we do not have any tristates, we can define our create and complete
functions as per Board_info.M_IO.
val create : board -> Signal.t I.t
val complete : board -> Signal.t O.t -> unit
As we can see the tristate types are not mentioned. Another example is something like LEDs which only have outputs. These have an even more cut-down API.
val complete : board -> Signal.t O.t -> unit
The module Nexys_a7_100t defines a bunch of subsystems according the the resources
provided by the board. Support is provided for buttons and leds, vga, ethernet and more.
To use subsystems we must first create a Board.t type. Once we have defined our required
logic we can call generate_top to construct a RTL project suitable for running through
Xilinx Vivado.
The following is basic example which simply maps switches through to LEDs.
let () =
let board = Board.create () in
Nexys_a7_100t.Leds.complete board (Nexys_a7_100t.Switches.create board);
Nexys_a7_100t.generate_top board
;;
A verilog file containing the a special top level module generated by the board infrastructure, and whatever verilog modules that were defined by the user logic.
Technically, the top most module is built using Hardcaml.Structural which allows us to
instantiate tristates (if any) appropriately. A standard hierarchical Hardcaml.Circuit
is constructed for the user design.
Note that one may obtain Scope.t from the board type to implement hierarchical designs:
let scope = Board.scope board in
A Xilinx Design Constraints (.xdc) file is generated with the mapping on of top level
ports to physical pins.
In addition, the top level clock period is constrained and some custom properties related to configuration file generation and configuration modes are added.
A TCL file is produced to drive Vivado. It will load the RTL and constraints then perform synthesis, placement, routing, timing analysis, bitstream generation.
This is done using an in-memory project with Vivado.
After each step we have commented out a line like:
#write_checkpoint ...
You may uncomment that line and then load the resulting dcp file into Vivado to perform the rest of the build process manually.