HDLRuby is a library for describing and simulating digital electronic systems.
Note:
If you want to learn how to describe a circuit with HDLRuby, please jump to the following section:
Many of HDLRuby's features are available through its standard libraries. We strongly recommend consulting the corresponding section:
Samples are also available: Sample HDLRuby descriptions
Finally, HDLRuby can also process Verilog HDL files: Converting Verilog HDL to HDLRuby.
If you are new to HDLRuby, we recommend starting with the following tutorial even if you have a hardware background:
If you would prefer an HTML version, you can generate it by running the
following command. This will create a tuto
folder containing all the
necessary files. Then, simply open tuto/tutorial_sw.html
:
hdrcc --get-tuto
What's New
For HDLRuby version 3.9.0:
-
Added the parallel enumerators to the software sequencers.
-
Added experimental TensorFlow code generation from the software sequencers.
-
Added the possibility to declare vectors of instances.
-
Added the possibility to fix the data type for the accumulation with the hinject and sinject enumerators.
-
Fixed various bugs.
-
Made an overhaul of the documentation.
For HDLRuby version 3.8.3:
-
Fixed various bugs including some in interactive mode.
-
Updated the documentation:
-
Rewrote the beginning of the HDLRuby Programming Guide.
-
Updated the documentation for interactive mode.
-
Updated the High-Level Programming Features chapter.
-
For HDLRuby version 3.8.0:
-
Added parallel enumerators (e.g., heach), allowing Ruby-like iteration for describing parallel hardware.
-
Added genererive programming using standard HDLRuby constructs (e.g., hif) -- there is no need to use Ruby code directly any more.
-
Fixed compile bugs for windows.
For HDLRuby version 3.7.9:
-
Added Python code generation from software sequencers.
-
Added Parallel Enumerators.
For HDLRuby versions 3.7.7/3.7.8:
- Various fixes related to software sequencers.
For HDLRuby version 3.7.6:
-
Added initial value support for signals in software sequencers.
-
Fixed
hprint
in software sequencers.
For HDLRuby versions 3.7.4/3.7.5:
- Various bug fixes.
For HDLRuby version 3.7.3:
- Enabled use of software sequencers within HDLRuby's
program
construct, including use of program ports as if they were input or output signals.
For HDLRuby version 3.7.2:
-
Added the
text
command for software sequencers. -
Added the
value_text
method to software sequencers signal, generating Ruby/C code with correct typing. -
Added the
alive?
andreset!
commands for HDLRuby sequencers. -
Added the
require_ruby
method for loading Ruby (i.e., non-HDLRuby) libraries.
For HDLRuby version 3.7.x:
- Added the possibility to run Sequencers in Software. (WIP) This enables significantly faster simulation and allows reusing the same code for both hardware and software design.
For HDLRuby version 3.6.x:
-
Added a new GUI board element allowing assignment of expressions to signals during simulation.
-
Added a new slider element for the GUI board (from 3.6.1).
For HDLRuby version 3.5.0:
-
Added direct support for Verilog HDL files as input to 'hdrcc'.
-
Added the ability to generate a graphical representation of the RTL code in SVG format using the '--svg' option for 'hdrcc'.
For HDLRuby version 3.4.0:
-
Improved synchronization between the browser-based graphical interface and the HDLRuby simulator.
-
Added a Verilog HDL parsing library for Ruby (to be released separately once stabilized).
-
Added a library for generating HDLRuby code from a Verilog HDL AST (produced by the parsing library).
-
Added v2hdr, a standalone tool for converting Verilog HDL files to HDLRuby (experimental).
-
Added a HDLRuby command for loading a Verilog HDL file from a HDLRuby description.
For HDLRuby version 3.3.0:
-
Redesigned the description of software components using the program construct. The
Code
objects are now deprecated. -
Added HW/SW co-simulation capability for Ruby and compiled C-compatible software programs.
-
Added a browser-based graphical interface simulating a development board that interacts with the HDLRuby simulator.
-
Updated the documentation and tutorial accordingly, and fixed several typos.
For HDLRuby version 3.2.0:
-
Added components for declaring BRAM and BRAM-based stacks to enable efficient memory allocation in FPGAs.
-
Performed internal code overhaul in preparation for version 4.0.0.
-
Multiple bug fixes.
For HDLRuby version 3.1.0:
-
Added functions for sequencers, including support for recursion.
-
Replaced the
function
keyword withhdef
for consistency with sequencer functions (sdef
). -
Added the
steps
command for waiting multiple steps in a sequencer. -
Improved Verilog HDL code generation to better preserve original signal names.
-
Several bug fixes for the sequencers.
For HDLRuby version 3.0.0:
-
Intruduced this changelog section.
-
Added Sequencers for software-like hardware design.
-
Added a tutorial for software developers.
-
The stable Standard Libraries are now loaded by default.
Install:
The recommended method of installation is via RubyGems:
gem install HDLRuby
Developers who wish to contribute to HDLRuby can install it from source using GitHub:
git clone https://github.com/civol/HDLRuby.git
Warning:
-
HDLRuby is still under active development, and the API may change before a stable release.
-
It is highly recommended that users have a basic understanding of both the Ruby programming language and hardware description languages before using HDLRuby.
'hdrcc' is the HDLRuby compiler. It takes an HDLRuby file as input, checks it, and can generate one of several outputs: Verilog HDL, VHDL, or a YAML low-level hardware component description. It can also simulate the input design.
Usage:
hdrcc [options] <input file> <output/working directory>
Where:
-
options
is a list of options (see below) -
<input file>
is the input HDLRuby file to compile (mandatory) -
<output/working directory>
is the directory where output and temporary files will be stored
Options | |
---|---|
-I, --interactive |
Run in interactive mode |
-y, --yaml |
Output in YAML format |
-v, --verilog |
Output in Verilog HDL format |
-V, --vhdl |
Output in VHDL format |
-s, --syntax |
Output the Ruby syntax tree |
-C, --clang |
Output the C code of the standalone simulator |
-S, --sim |
Perform the simulation with the default engine |
--csim |
Perform the simulation with the standalone engine |
--rsim |
Perform the simulation with the Ruby engine |
--rcsim |
Perform the simulation with the Hybrid engine |
--vcd |
Make the simulator generate a VCD (waveform) file |
--svg |
Output a graphical representation of the RTL (SVG format) |
-d, --directory |
Specify the base directory for loading the HDLRuby files |
-D, --debug |
Set the HDLRuby debug mode |
-t, --top system |
Specify the top system describing the circuit to compile |
-p, --param x,y,z |
Specify the generic parameters |
--get-samples |
Copy the hdr_samples directory to the current directory, then exit |
--version |
Show the version number, then exit |
-h, --help |
Show the help message |
Notes:
-
If no top system is specified, it will be automatically inferred from the input file.
-
If no options are provided, the compiler will only check the input file for correctness.
-
If you're new to HDLRuby, or want to see working examples of new features, we strongly recommend downloading the sample files:
hdrcc --get-samples
This will create a
hdr_samples
subdirectory in your current folder, containing various HDLRuby example files. For more details, see the samples.
Examples:
- Compile
adder.rb
and generate a low-level Verilog HDL description in theadder
directory:
hdrcc -v adder.rb adder
- Compile the Verilog HDL file
adder8.v
, usingadder8
as the top module, and generate a graphical RTL diagram in theview
directory:
hdrcc adder8.v -t adder8 --svg view
- Compile a parameterized system
multer
frommulter_gen.rb
, generating a 16x16->32-bit YAML hardware description into themulter
directory:
hdrcc -V -t adder --param 16 adder_gen.rb adder
- Compile system
multer
with inputs and output bit width is generic frommulter_gen.rb
input file to a 16x16->32-bit circuit whose low-level YAML description into directorymulter
:
hdrcc -y -t multer -p 16,16,32 multer_gen.rb multer
- Simulate the circuit described in
counter_bench.rb
using the default simulation engine, outputting files to thecounter
directory:
hdrcc -S counter_bench.rb counter
Note: The default simulation engine is set to the fastest available engine (currently, the hybrid engine).
- Run in interactive mode.
hdrcc -I
- Run in interactive mode using pry as UI.
hdrcc -I pry
When run in interactive mode, the HDLRuby framework launches a REPL (Read-Eval-Print Loop) environment and creates a working directory named HDLRubyWorkspace. By default, the REPL is irb, but it can also be set to pry.
Within the interactive prompt, you can write HDLRuby code just as you would in a standard HDLRuby source file. In addition, a set of special commands is available to compile, inspect, and simulate your design interactively:
- Compile an HDLRuby module (with optional parameters):
hdr_make(<module>[,<parameters])
- Display the internal representation (IR) of the compiled module in YAML format:
hdr_yaml
- Reconstruct and display the HDLRuby source description of the compiled module:
hdr_hdr
- Generate and save Verilog HDL output to the
HDLRubyWorkspace
directory:
hdr_verilog
- Generate and save VHDL output to the HDLRubyWorkspace directory:
hdr_vhdl
- Simulate the compiled module:
hdr_sim
- Simulate the compiled module and save the VCD trace (waveform output) to the directory
HDLRubyWorkspace
:
hdr_sim_vcd
- Simulate the compiled module in mute mode:
hdr_sim_mute
Since HDLRuby is built on top of the Ruby language, it is standard convention to name HDLRuby files with the .rb
extension.
For the same reason, including external HDLRuby files is done using the Ruby methods require
or require_relative
, which behave the same way as in standard Ruby. However, these methods can only be used to include HDLRuby description files, not plain Ruby files.
To include standard Ruby code (e.g., helper libraries or tools), you must use the methods require_ruby
or require_relative_ruby
.
HDLRuby is designed to bring the flexibility and expressiveness of the Ruby language to hardware description, while ensuring that the resulting designs remain synthesizable. The abstractions provided by HDLRuby are meant to aid in describing hardwareÑbut they do not alter the underlying execution model, which is RTL (Register Transfer Level) by construction.
Another key feature of HDLRuby is its native support for all features of the Ruby language.
Notes:
- It is possible to extend HDLRuby to support hardware descriptions at a higher level of abstraction than RTL. See Extending HDLRuby for more details.
- Throughout this guide, HDLRuby constructs are often compared to their Verilog HDL or VHDL equivalents to aid understanding.
This introduction gives a glimpse of what HDLRuby makes possible.
At first glance, HDLRuby resembles other hardware description languages such as Verilog HDL or VHDL. For example, the following code describes a simple D flip-flop:
system :dff do
bit.input :clk, :rst, :d
bit.output :q
par(clk.posedge) do
q <= d & ~rst
end
end
In this example, system
is the keyword used to define a hardware component, similar to the module
construct in Verilog HDL. Signals are declared using a <type>.<direction>
format, where type
is the data type (e.g., bit
) and direction indicates the signal's role (input
, output
, inout
, or inner
). Processes, like Verilog's always
blocks, are described using the par
keyword for non-blocking assignments and seq
for blocking assignments.
Here is a second example: an 8-bit adder.
system :adder8 do
bit[7..0].input :x, :y
bit[7..0].output :z
bit.output :cout
[cout,z] <= x.as(bit[8..0]) + y
end
This example demonstrates how to declare vector types. The signals x
, y
, and z
are 8-bit unsigned vectors. If signed values are needed, you would use signed
instead of bit
.
Line 6 illustrates a connection (similar to the assign
statement in Verilog HDL), where cout
and z
are concatenated and connected to the result of an addition. Note that x
is explicitly cast to a 9-bit value to preserve the carry-out. In HDLRuby, unlike Verilog HDL, operand types are strictly preserved. This means that adding two 8-bit values yields an 8-bit result unless explicitly extended. The goal is to avoid the type-related ambiguities found in Verilog, while keeping syntax lighter than VHDL.
Conditional statements, common in RTL languages, are also supported in HDLRuby. However, unlike in Verilog or VHDL, HDLRuby conditionals can appear anywhere in a system body�not just within processes.
These include:
-
hif
/helsif
/helse
forif
-like conditionals -
hcase
/hwhen
/helse
forcase
-like conditionals -
mux
, an expression-level construct for multiplexers, which supports multiple inputs, unlike the ?: ternary operator in Verilog, which only handles two
Note: These statements are also called "parallel conditionals" in HDLRuby, to contrast with the ones used in the sequencer
constructs (see Sequencer).
For example, we can upgrade the 8-bit adder to an adder-subtractor:
system :adder_suber8 do
bit.input :addbsub
bit[7..0].input :x, :y
bit[7..0].output :z
bit.output :cout
hif(addbsub) { [cout,z] <= x.as(bit[8..0]) + ~y + 1 }
helse { [cout,z] <= x.as(bit[8..0]) + y }
end
The conditional logic above can also be written more compactly using the mux
expression:
[cout,z] <= x.as(bit[8..0]) + mux(addbsub, y, ~y + 1)
Once a module has been described, it can be instantiated. For example, a single instance of the dff
module named dff0
can be declared as follows:
dff :dff0
The ports of the instance can be accessed like regular signals. For example, dff0.d
refers to the d input of the flip-flop.
You can also connect the ports of an instance at the time of declaration. The example above can be extended as follows:
system :counter2 do
bit.input :clk, :rst
bit.output :q
dff(:dff0).(clk: clk, rst: rst, d: ~dff0.q)
dff(:dff1).(~dff0.q, rst, ~dff1.q, q)
end
In this example:
-
dff0
uses named connections for its ports (e.g.,clk: clk
). -
dff1
uses positional connections, in the order the ports were declared in the module.
It is also possible to connect only a subset of the ports at instantiation time, and to reconnect or override ports later in the code.
To simulate a circuit, you must write a test bench using timed
constructs, which describe how signals evolve over time.
Here is an example that simulates the D flip-flop dff
using a 20 ns clock, and toggles the input d
every two clock cycles for ten iterations:
system :dff_bench do
dff :dff0
timed do
dff0.clk <= 0
dff0.rst <= 1
!10.ns
dff0.clk <= 1
!10.ns
dff0.clk <= 0
dff0.rst <= 0
dff0.d <= 1
!10.ns
repeat(10) do
repeat(4) { !10.ns ; dff0.clk <= ~dff0.clk }
dff0.d <= ~dff0.d
end
end
end
In this code:
-
!<time>.<unit>
pauses execution for the specified physical time. Units can range from picoseconds (ps
) to seconds (s
). -
repeat(n)
repeats the blockn
times. -
~dff0.clk
inverts the clock value.
This test bench models both the reset behavior and a clock-driven sequence, demonstrating how to simulate sequential logic in HDLRuby.
The dff
example shown earlier is quite similar to what you would write in other HDLs. However, HDLRuby offers several features to increase productivity and reduce boilerplate in hardware descriptions. Below are a few of these conveniences.
First, HDLRuby supports syntactic sugar that allows for more concise code. For example, the following version of the dff
module is functionally identical to the earlier version:
system :dff do
input :clk, :rst, :d
output :q
(q <= d & ~rst).at(clk.posedge)
end
In this example:
-
The
bit
type is omitted for signal declarations (it is the default type). -
Since the process contains only a single statement, it is expressed more compactly using the
at
method.
Similarly, the adder8
module can be written more concisely:
system :adder8 do
[8].input :x, :y
[8].output :z
output :cout
[cout,z] <= x.as(bit[9]) + y
end
In this example:
-
The vector range
[7..0]
is abbreviated to[8]
, which implies an 8-bit width. -
The
bit
type is omitted for signal declarations (again, because it is the default). -
Note: when casting a signal or using it in expressions where type precision matters, the bit type must still be explicitly specified, as seen in
bit[9]
.
Second, HDLRuby also provides high-level constructs that make it easier to describe complex structures and behaviors in hardware.
For example, the sequencer
construct allows to describe finite state
machines using software code-like statements, including conditionals,
loops and function calls. For example the following module describes a
simple 8-bit serializing circuit that emmits bit every 10 clock cycle.
system :serial do
[8].input :din
input :clk, :rst, :req
output :ack, :bout
[8].inner :buf
bout <= buf[0]
sequencer(clk,rst) do
sloop do
ack <= 0 ; buf <= 0
swhile(~req)
buf <= din ; ack <= 1
8.stimes do
buf <= buf >> 1
9.stimes
end
end
end
end
In this example:
-
sequencer(clk, rst)
creates a clocked finite-state machine initialized on reset(rst = 1)
. -
sloop
is an infinite loop. -
swhile(condition)
loops until the condition becomes false; if it has no body, it waits passively. -
stimes(n)
is a shorthand for looping a blockn
times. -
Each control-flow step in a sequencer (even inside loops) corresponds to one clock cycle, making timing behavior explicit and predictable.
This example uses buf
to hold the 8-bit input and shift it right each cycle to serialize it bit by bit onto the bout
output.
Other HDLRuby high-level contructs includes:
-
Iterators (both parallel and sequential)
-
Decoders
-
Fixed-point arithmetic
-
And more...
These high-level abstractions are built on synthesizable foundations, and help keep hardware descriptions clear, maintainable, and concise, especially for complex control logic.
Third, HDLRuby supports generic parameters that can be used flexibly to define reusable hardware modules. These parameters can represent sizes, types, or any other construct needed to generalize a design.
For instance, the following example defines a simple, fixed-size 8-bit register:
system :reg8 do
input :clk, :rst
[8].input :d
[8].output :q
(q <= d & [~rst]*8).at(clk.posedge)
end
To make this register size configurable, you can introduce a parameter. In this version, n
defines the bit width of the register:
system :regn do |n|
input :clk, :rst
[n].input :d
[n].output :q
(q <= d & [~rst]*n).at(clk.posedge)
end
Going further, you can define a fully generic register by parameterizing not just the size, but the data type itself (e.g., signed, fixed-point, structs, etc.):
system :reg do |typ|
input :clk, :rst
typ.input :d
typ.output :q
(q <= d & [~rst]*typ.width).at(clk.posedge)
end
In this example:
-
typ
is used as a type object (e.g.,bit[8]
,signed[16]
, etc.). -
typ.width
returns the number of bits associated with the type, allowing the reset mask ([~rst] * typ.width
) to scale automatically.
Fourth, HDLRuby allows you to extend modules and instances after their declaration. This makes it easy to add new features without duplicating code.
Let us say you want to extend an existing dff
module to include an inverted output (qb
). There are three ways to do this:
- Inheriting from a Module.
You can define a new system that inherits from the existing dff
:
system :dff_full, dff do
output :qb
qb <= ~q
end
This creates a new module dff_full
that includes all the functionality of dff
, with the additional inverted output.
- Reopening a Module.
You can modify the original dff
module after its declaration using the open method:
dff.open do
output :qb
qb <= ~q
end
This approach modifies dff
itself, and the added behavior (qb <= ~q
) will apply to all future instances of dff
.
- Reopening a Specific Instance.
You can also modify a single instance of a module without affecting the others:
# Declare dff0 as an instance of dff
dff :dff0
# Modify it
dff0.open do
output :qb
qb <= ~q
end
In this case, only dff0
will have the qb inverted output. Other instances of dff
remain unchanged.
In summary, HDLRuby supports:
-
Inheritance: for creating extended modules from existing ones
-
Module reopening: to modify a module after declaration
-
Instance reopening: to customize individual instances
Fifth, HDLRuby allows you to instantiate components in groups, similar to how signals are grouped in arrays. This enables scalable and readable hardware descriptions using familiar Ruby-style iteration.
system :shifter do |n|
input :clk, :rst
input :i0
output :o0, :o0b
dff_full :dffr
dffr.clk <= clk
# Instantiating n D-FF
dff_full[n,:dffIs]
# Connect the clock and the reset.
dffIs.heach { |ff| ff.clk <= clk ; ff.rst <= rst }
# Interconnect them as a shift register
dffIs[0..-1].heach_cons(2) { |ff0,ff1| ff1.d <= ff0.q }
# Connects the input and output of the circuit
dffIs[0].d <= i0
o0 <= dffIs[-1].q
o0b <= dffIs[-1].qb
end
In this example:
-
dff_full[n, :dffIs]
creates an array ofn
instances nameddffIs
. -
heach
iterates over each instance in parallel. -
heach_cons(2)
creates overlapping pairs (like a sliding window) to wire the flip-flops together.
If you don¿t need a specific subcomponent like dff_full
, you can describe the shift register more concisely using a bit vector:
system :shifter do |n|
input :clk, :rst
input :i0
output :o0
[n].inner :sh
par (clk.posedge) do
hif(rst) { sh <= 0 }
helse { sh <= ((sh << 1)|i0) }
end
o0 <= sh[n-1]
end
This version:
-
Uses a single
n
-bit inner register (sh
) to store the shift state. -
Updates the register each clock cycle, inserting
i0
at the least significant bit. -
Outputs the most significant bit (
sh[n-1]
).
HDLRuby supports many more advanced features that enable concise, flexible, and reusable hardware descriptions. The following examples showcase how you can use generic parameters, functional abstractions, and custom types in practice.
Suppose you want to build a circuit that computes a sum of products between several inputs and constant coefficients. For example, with four signed 16-bit inputs and coefficients 3, 4, 5, 6, a basic HDLRuby implementation looks like this:
system :sumprod_16_3456 do
signed[16].input :i0, :i1, :i2, :i3
signed[16].output :o
o <= i0*3 + i1*4 + i2*5 + i3*6
end
This works, but lacks flexibility. Changing the bit width or coefficients requires rewriting the entire module. It also becomes error-prone with large coefficient sets.
A better approach is to create a generic system:
system :sumprod do |typ,coefs|
typ[-coefs.size].input :ins
typ.output :o
o <= coefs.hzip(ins).hreduce(0) do |sum,(i,c)|
sum + i*c
end
end
In this version:
-
typ
defines the data type (e.g.,signed[32]
) -
coefs
is an array of constant coefficients -
ins
is an array of inputs with sizecoefs.size
-
[-coefs.size]
is shorthand for declaring an array indexed in the forward direction ([0..coefs.size - 1]
) -
hzip
pairs each input with its coefficient (like Ruby¿szip
) -
hreduce
accumulates the products into a final sum (like Ruby¿sreduce
)
This version supports any number of coefficients and any data type. Example instantiation (with 16 coefficients):
sumprod(signed[32],
[3,78,43,246, 3,67,1,8,
47,82,99,13, 5,77,2,4]).(:my_circuit)
Note: when passing generic arguments, the instance name (:my_circuit) goes after the parameters, in parentheses.
While the description sumprod
is already usable in a wide range of
cases you may want to use specialized operations (e.g., saturated arithmetic) instead of standard +
and *
. You can do this by replacing operators with functions:
system :sumprod_func do |typ,coefs|
typ[-coefs.size].input :ins
typ.output :o
o <= coefs.hzip(ins).hreduce(0) do |sum,(c,i)|
add(sum, mult(i,c))
end
end
Now you define your custom add
and mult
functions. For example, an addition with saturation at 1000:
hdef :add do |x,y|
inner :res
seq do
res <= x + y
hif(res > 1000) { res <= 1000 }
end
res
end
With HDLRuby functions, the value returned is the result of the last statement, here res
.
To avoid hardcoding saturation values, functions can accept extra arguments:
hdef :add do |max, x, y|
inner :res
seq do
res <= x + y
hif(res > max) { res <= max }
end
res
end
You would then call it like:
add(1000,sum,mult(...))
However, this becomes cumbersome if your functions take inconsistent argument counts. A better approach is to pass code (lambdas or procs) as parameters:
system :sumprod_proc do |add,mult,typ,coefs|
typ[coefs.size].input :ins
typ.output :o
o <= coefs.hzip(ins).hreduce(0) do |sum,(c,i)|
add.(sum, mult.(i*c))
end
end
Note: When calling a proc in HDLRuby, use .()
instead of regular parentheses.
Example usage:
sumprod_proc(
proc { |x,y| add_sat(1000,x,y) },
proc { |x,y| mult_sat(1000,x,y) },
signed[64],
[3,78,43,246, 3,67,1,8,
47,82,99,13, 5,77,2,4]).(:my_circuit)
This lets you reconfigure the arithmetic logic without changing the core circuit.
As second possible approach, HDLRuby also allows you to define custom data types with redefined operators:
signed[16].typedef(:sat16_1000)
sat16_1000.define_operator(:+) do |x,y|
tmp = x + y
mux(tmp > 1000,tmp,1000)
end
In the code above:
-
The first line defines the new type
sat16_1000
to be 16-bit signed, -
The
define_operator
method overloads (redefines) the+
operator for this type.
Then use your original sumprod
with this type:
sumprod(sat16_1000,
[3,78,43,246, 3,67,1,8,
47,82,99,13, 5,77,2,4]).(:my_circuit)
You can also define generic types with parameters:
typedef :sat do |width, max|
signed[width]
end
sat.define_operator(:+) do |width,max, x,y|
tmp = x + y
mux(tmp > max, tmp, max)
end
Now you can instantiate saturated arithmetic with custom precision and bounds:
sumprod(sat(16,1000),
[3,78,43,246, 3,67,1,8,
47,82,99,13, 5,77,2,4]).(:my_circuit)
Note: Any parameters used in a type definition must also be listed when overloading operators.
Unlike high-level HDLs such as SystemVerilog, VHDL, or SystemC, HDLRuby descriptions are not direct descriptions of hardware. Instead, they are Ruby programs that generate hardware descriptions.
In traditional HDLs, executing the code (e.g., in a simulator) simulates the behavior of the described circuit. In contrast, executing HDLRuby code produces a low-level hardware description, which can then be synthesized or simulated like any standard HDL.
This separation between:
-
the user-facing description (written in HDLRuby), and
-
the internal hardware representation (handled by
HDLRuby::Low
)
allows HDLRuby to incorporate advanced programming features¿such as iterators, generics, and metaprogramming -- without affecting the synthesizability of the resulting hardware description.
In HDLRuby, each construct does not directly describe hardware. Instead, it generates a hardware description. For example, consider the following line:
a <= b
This expression creates a connection from signal b
to signal a
. When this line is executed (remember, HDLRuby code runs as Ruby code), it generates an instance of HDLRuby::Low::Connection
-- the internal object representing that hardware connection.
Its execution will produce the actual hardware description of this connection as an object of the HDLRuby::Low library
in this case, an instance of the HDLRuby::Low::Connection
class. Concretely, an HDLRuby system is described by a Ruby block, and the instantiation of this system is performed by executing this block. The actual synthesizable description of this hardware is the execution result of this instantiation.
More generally:
-
an HDLRuby module (
system
) is defined using a Ruby block. -
When the module is instantiated, the block is executed.
-
The result of that execution is a complete, synthesizable hardware description in the internal
HDLRuby::Low
format.
This architecture -- where Ruby is used to dynamically generate HDL constructs -- makes HDLRuby extremely flexible and expressive, while still producing valid, low-level HDL for synthesis or simulation
From here, we will begin to explore HDLRuby’s core constructs in more detail.
Several constructs in HDLRuby -- such as modules and signals -- are identified by names. These names must be specified using Ruby symbols that begin with a lowercase letter.
For example:
-
:hello
-> valid -
:Hello
-> invalid (starts with an uppercase letter)
Once declared, the construct is referred to by the name without the colon (:
). That is, a construct declared as :hello
will later be referenced simply as hello
.
In HDLRuby, a system represents a digital module, similar to a module in Verilog HDL. A system includes:
-
An interface (comprising
input
,output
, andinout
signals), -
as well as structural and behavioral descriptions of the circuit.
A signal represents a piece of state within a system. Each signal has:
-
a data type, and
-
a value that can change over time.
HDLRuby signals abstract both wires and registers:
-
If a signal's value is explicitly assigned at all times, it behaves like a wire.
-
If the value is updated conditionally or based on clocked logic, it behaves like a register.
A system is declared using the system
keyword. It must be given a name (as a Ruby symbol or string) and a block that defines its contents.
For example, the following code declares an empty system named box
:
system(:box) {}
Notes:
-
Since this is Ruby code, the block can also be written using
do...end
syntax. In that case, parentheses around the name are optional:system :box do end
-
Although HDLRuby internally stores names as Ruby symbols, you can also use strings. For example, the following is equally valid:
system("box") {}
A system's interface defines how it communicates with the outside world. It consists of input
, output
, and inout
signals, each of a specified data type.
While interface declarations can appear anywhere in the system body, it is recommended to place them at the beginning for clarity.
Interface signals are declared using the following pattern:
<data type>.<direction> :name1, :name2, ...
For example, to declare a 1-bit input signal named clk
:
bit.input :clk
Since bit
is the default data type in HDLRuby, it can be omitted:
input :clk
Here is a more complete example: the following defines a simple memory module. It has:
-
a 1-bit clock input (
clk
) -
a 1-bit read/write control input (
rwb
, where 1 = read, 0 = write) -
a 16-bit address input (
addr
) -
an 8-bit bidirectional data bus (
data
)
system :mem8_16 do
input :clk, :rwb
[15..0].input :addr
[7..0].inout :data
bit[7..0][2**16].inner :content
par(clk.posedge) do
hif(rwb) { data <= content[addr] }
helse { content[addr] <= data }
end
end
In this example:
-
The memory content is declared as an array of
2**16
8-bit words. -
On each rising edge of
clk
, the module either reads from or writes to memory depending on the value ofrwb
.
In HDLRuby, structural descriptions define how subsystems (i.e., instances of other systems) are instantiated and interconnected.
To instantiate a system, use the following syntax:
<system name> :<instance name>
For example, to instantiate the mem8_16
system:
mem8_16 :mem8_16I
You can also declare multiple instances at once:
mem8_16 [:mem8_16I0, :mem8_16I1]
Or create an array of instances:
mem8_16[5,:mem8_18Is] # Creates an array of 5 instances named mem8_16Is
To interconnect subsystems, you'll often need internal signals. These are declared using the inner direction:
inner :w1
[1..0].inner :w2
If a signal is constant (i.e., its value never changes), use constant instead of inner.
When signals are declared, use the assignment operator <= to define connections:
<destination> <= <source>
For example:
ready <= w1 # Connects internal w1 to ready
w2[0] <= clk # Assigns clk to the first bit of w2
w2[1] <= clk & rst # Assigns AND of clk and rst to w2[1]
You can also refer to the ports of an instance using the dot operator:
<instance name>.<signal name>
For example:
mem8_16I.clk <= clk
Alternatively, you can connect multiple ports at once using the call operator .()
with named arguments:
mem8_16I.(clk: clk, rwb: rwb)
This also allows partial connections (e.g., leaving out addr or data). But you can also list the connections in order of port decleration:
mem8_16I.(clk, rwb, addr, data)
You can even connect ports inline at instantiation:
mem8_16(:mem8_16I).(clk: clk, rwb: rwb)
The following system uses two 8-bit memory modules (mem8_16) to construct a 16-bit wide memory by splitting the data bus:
system :mem16_16 do
input :clk, :rwb
[15..0].input :addr
[15..0].inout :data
mem8_16(:memL).(clk: clk, rwb: rwb, addr: addr, data: data[7..0])
mem8_16(:memH).(clk: clk, rwb: rwb, addr: addr, data: data[15..8])
end
The same can be written using the dot operator and individual assignments:
system :mem16_16 do
input :clk, :rwb
[15..0].input :addr
[15..0].inout :data
mem8_16 [:memL, :memH]
memL.clk <= clk
memL.rwb <= rwb
memL.addr <= addr
memL.data <= data[7..0]
memH.clk <= clk
memH.rwb <= rwb
memH.addr <= addr
memH.data <= data[15..8]
end
In HDLRuby, output, inner, and constant signals can be initialized at the time of declaration using the following syntax:
<signal name>: <intial value>
For example, the following declares a 1-bit inner signal named sig initialized to 0:
inner sig: 0
The following, declares and initialize an 8-word, 8-bit ROM (read-only memory):
bit[8][-8] rom: [ _h00,_h01,_h02,_h03,_h04,_h05,_h06,_h07 ]
Notes:
-
The notation
_hXY
represents an explicit 8-bit hexadecimal value whereX
andY
are hex digits (e.g.,_h0A
is an 8-bit 10). -
By default:
-
Ruby integers (e.g.,
42
) are treated as 64-bit HDLRuby values. -
HDLRuby literals prefixed with
_
(e.g.,_b1010
,_h0F
) have a bit-width corresponding to their representation.
-
-
When initializing ROM or arrays of values, make sure that the bit-width of the values matches the declared type -- otherwise, misalignments or synthesis issues may occur.
HDLRuby uses scopes to control the visibility of signals and instances. Understanding scopes helps avoid naming conflicts and improves modularity and readability. As general rule:
-
Interface signals (
input
,output
,inout
) are globally accessible from anywhere within the system where they are declared. -
Inner signals (
inner
) and instances are local to the scope in which they are declared and cannot be accessed outside of it.
A scope is a region of code where declared objects (signals, instances, etc.) are visible. Each system has its own top-level scope, and scopes can be nested.
For example, the following system has only a top-level scope:
system :div2 do
input :clk
output :q
inner :d, :qb
d <= qb
dff_full(:dffI).(clk: clk, d: d, q: q, qb: qb)
In this example, signals d
and qb
and the instance dffI
are accessible only within system div2
.
You can define additional inner scopes using the sub
keyword:
sub do
# Local declarations and code
end
This is useful for organizing code or isolating declarations. Objects declared inside a sub block are not accessible outside of it.
For example, the following system includes a one-level nested scope:
system :sys do
...
sub
inner :sig
# sig is accessible here
end
# sig is not accessible here
end
And the following system includes two-level nested scopes:
system :sys do
...
sub
inner :sig0
# sig0 is accessible here
sub
inner :sig1
# sig0 and sig1 are accessible here
end
# sig1 is not accessible here
end
# Neither sig0 nor sig1 are accessible here
end
There rules for name collisions are the following:
-
Within the same scope, you cannot declare two signals or instances with the same name.
-
However, inner scopes may reuse names already declared in outer scopes. In such cases, the innermost declaration takes precedence.
You can assign a name to a scope:
sub :<name> do
...
end
Signals and instances declared within a named scope can be accessed from outside using dot notation: <scope_name>.<object_name>
For example:
sub :scop do
inner :sig
...
end
...
# Access sig from outside its scope.
scop.sig <= ...
In HDLRuby, behavioral descriptions is done using processes which are declared using either:
-
par
for non-blocking execution (like Verilogalways
with<=
) -
seq
for blocking execution (like Verilogalways
with=
)
A process consists of:
-
a sensitivity list (i.e., a list of events that trigger it)
-
a block of statements
The general syntax is as follows:
par <list of events> do
<statements>
end
seq <list of events> do
<statements>
end
Each process is activated when any event in its sensitivity list occurs. An event corresponds to a change in a signal, such as:
-
posedge
-- rising edge -
negedge
-- falling edge -
anyedge
-- any edge (can be ommitted)
For example:
par(clk.posedge) do
# This block runs on every rising edge of clk
...
end
The sensitivity list is evaluated at runtime, and processes are executed once per activation. See Events for more details.
Statements include assingments, conditionals and blocks. You can also declare inner signals within these statements; they will be local to the current process. Statements are described in more detail in section statements. In this section, we focus on assignment statements and block statements.
An assignment statement is declared using the arrow operator <=
as follows:
<destination> <= <source>
The destination
must be a reference to a signal, and the source
can be any expression.
An assignment has the same structure as a connection. However, its execution model is different: while a connection is continuously executed, an assignment is only executed during the execution of its block.
A block comprises a list of statements and is used to add hierarchy to a process. Blocks can use either blocking or non-blocking assignments. By default, a top-level block is created when declaring a process, and it inherits its execution mode. For example, in the following code, the top block uses blocking assignments:
system :with_blocking_process do
seq do
<list of statements>
end
end
It is possible to declare new blocks within an existing block.
To declare a sub-block with the same execution mode as its parent, use the keyword sub
. For example, the following code declares a sub-block within a seq block, inheriting the same execution mode:
system :with_blocking_process do
seq do
<list of statements>
sub do
<list of statements>
end
end
end
A sub-block can also use a different execution mode by explicitly using seq
(for blocking assignments) or par
(for non-blocking execution).
For example, the following code declares a par
sub-block inside a seq
block:
system :with_par_in_seq_process do
seq do
<list of statements>
par do
<list of statements>
end
end
end
Sub-blocks have their own scope, so it is possible to declare signals without name collisions.
For example, the following code declares three different inner signals, all named sig
:
...
par(<sensibility list>) do
inner :sig
...
sub do
inner :sig
...
sub do
inner :sig
...
end
end
...
end
To summarize this section, here is a behavioral description of a 16-bit shift register with asynchronous reset (hif
and helse
are keywords used for specifying hardware if
and else
control statements).
system :shift16 do
input :clk, :rst, :din
output :dout
[15..0].inner :reg
dout <= reg[15] # The output is the last bit of the register.
par(clk.posedge) do
hif(rst) { reg <= 0 }
helse do
reg[0] <= din
reg[15..1] <= reg[14..0]
end
end
end
In the example above, the order of assignment statements does not matter.
However, this is not the case in the following example, which implements the same register using a seq
block.
In this second example, placing the statement reg[0] <= din
last would result in incorrect shift register behavior:
system :shift16 do
input :clk, :rst, :din
output :dout
[15..0].inner :reg
dout <= reg[15] # The output is the last bit of the register.
par(clk.posedge) do
hif(rst) { reg <= 0 }
helse seq do
reg[0] <= din
reg <= reg[14..0]
end
end
end
Notes:
-
helse seq
ensures that the block of the hardwareelse
is in blocking assignment mode. -
hif(rst)
could also have been set to blocking assignment mode as follows:hif rst, seq do reg <= 0 end
-
Non-blocking mode can be set the same way using
par
.
It often happens that a process contains only one statement. In such cases, the description can be shortened using the at
operator as follows:
( statement ).at(<list of events>)
For example, the following two code samples are equivalent:
par(clk.posedge) do
a <= b+1
end
( a <= b+1 ).at(clk.posedge)
For the sake of consistency, this operator can also be applied to block statements, as shown below. However, this usage is likely less readable than the standard process declaration:
( seq do
a <= b+1
c <= d+2
end ).at(clk.posedge)
By default, statements in a block are added in the order in which they appear in the code. However, it is also possible to insert statements at the beginning of the current block using the unshift
command, as follows:
unshift do
<list of statements>
end
For example, the following code inserts two statements at the beginning of the current block:
par do
x <= y + z
unshift do
a <= b - c
u <= v & w
end
end
The code above will result in the following block:
par do
a <= b - c
u <= v & w
x <= y + z
end
Note: While this feature has little practical use for simple circuit descriptions, it can be useful in advanced generic component descriptions.
Each process of a system is associated with a list of events, called a sensitivity list, that specifies when the process is to be executed. An event is associated with a signal and represents the instant when the signal reaches a given state.
There are three kinds of events:
-
Positive edge events, which occur when a signal transitions from 0 to 1.
-
Negative edge events, which occur when a signal transitions from 1 to 0.
-
Change events, which occur whenever the signal changes, regardless of direction.
Events are declared directly from the signals, using the posedge
operator for a positive edge, the negedge
operator for a negative edge, and the anyedge
operator for any change. For example, the following code declares 3 processes activated respectively on the positive edge, the negative edge, and any change of the clk
signal:
inner :clk
par(clk.posedge) do
...
end
par(clk.negedge) do
...
end
par(clk.anyedge) do
...
end
Note: The anyedge
keyword can be omitted.
Statements are the basic elements of a behavioral description. They are regrouped in blocks that specify their execution mode (non-blocking or blocking assignments). There are four kinds of statements: the assignment statement which computes expressions and sends the result to the target signals, the control statement which changes the execution flow of the process, the block statement (described earlier), and the inner signal declaration.
Statements are the fundamental elements of a behavioral description. They are grouped into blocks that specify their execution mode—either non-blocking or blocking assignments.
There are four types of statements:
-
Assignment statements, which compute expressions and assign the results to target signals.
-
Control statements, which alter the execution flow of a process.
-
Block statements, which group multiple statements and were described earlier.
-
Inner signal declarations, which define signals local to a process or block.
Notes:
-
A fifth type of statement, called a time statement, will be discussed in the Time section.
-
Unlike in other HDLs such as Verilog or VHDL, statements in this language are not restricted to processes.
An assignment statement is written using the arrow operator <=
within a process. Its right-hand side is the expression to be computed, and its left-hand side is a reference to the target signals (or parts of signals) -- i.e., the signals (or signal slices) that will receive the result of the computation.
For example, the following code assigns the value 3
to the signal s0
, and assigns the sum of signals i0
and i1
to the first four bits of signal s1:
s0 <= 3
s1[3..0] <= i0 + i1
The behavior of an assignment statement depends on the execution mode of the enclosing block:
-
If the mode is non-blocking, the target signals are updated after all statements in the current block have been processed.
-
If the mode is blocking, the target signals are updated immediately after the expression on the right-hand side is evaluated.
There are two types of control statements in HDLRuby: the hardware if (hif
) and the hardware case (hcase
).
The hif
construct consists of a condition and a block that is executed if -- and only if -- the condition is true. It is declared as follows, where the condition can be any expression:
hif <condition> do
<block contents>
end
The hcase
construct consists of an expression and a list of value-block pairs. A block is executed when its corresponding value matches the value of the hcase
expression. It is declared as follows:
hcase <expression>
hwhen <value 0> do
<block contents 0>
end
hwhen <value 1> do
<block contents 1>
end
...
You can add a block that is executed when the condition of an hif
is not met, or when no case in an hcase matches, using the helse
keyword:
<hif or hcase construct>
helse do
<block contents>
end
In addition to helse
, you can define additional conditions in an hif
using the helsif
keyword:
hif <condition 0> do
<block contents 0>
end
helsif <condition 1> do
<block contents 1>
end
...
Outside of sequencer, HDLRuby -- like other HDLs -- does not support runtime looping constructs. It is important not to confuse constructs like Verilog's generate, which are not actual loops but rather generative code structures. Similarly, HDLRuby supports generative loops through parallel enumerators. See the Parallel Enumerators section for more information.
Each signal and expression in HDLRuby is associated with a data type that defines the kind of value it can represent. In HDLRuby, data types represent bit vectors, along with the way they should be interpreted -- i.e., as bit strings, unsigned values, signed values, or hierarchical structures.
There are five basic types, bit
, signed
, unsigned
, integer
, and float
that represent respectively single bit logical values, single-bit unsigned values, single-bit signed values, Ruby integer values, and Ruby floating-point values (double precision). The first three types are HW and support four-valued logic, whereas the two last ones are SW (but are compatible with HW) and only support Boolean logic. Ruby integers can represent any element of Z (the mathematical integers) and have for that purpose a variable bit-width.
There are five basic types in HDLRuby: bit
, signed
, unsigned
, integer
, and float
. These represent, respectively:
-
Single-bit logical values (
bit
) -
Single-bit unsigned values (
unsigned
), equivalent tobit
-
Single-bit signed values (
signed
) -
Ruby integer values (
integer
) -
Ruby floating-point values in double precision (
float
), not supported for simulation or synthesis yet
The first three types are hardware types and support four-valued logic (0
, 1
, Z
, and X
), while the last two are software types. Although software types are compatible with hardware types, they support only Boolean logic.
Additional types can be constructed using a combination of the following two type operators:
The vector operator []
This operator is used to build types that represent vectors of elements, either of a single type or a tuple of multiple types.
-
A uniform vector (all elements of the same type) is declared as:
<type>[<range>]
The
range
specifies the index of the most and least significant bits. A range such asn..0
can also be written asn+1
. For example, the following two declarations are equivalent:bit[7..0] bit[8]
-
A tuple (vector of different types) is declared using square brackets with a list of types:
[<type 0>, <type 1>, ... ]
For example, the following defines a tuple containing an 8-bit logical value, a 16-bit signed value, and a 16-bit unsigned value:
[ bit[8], signed[16], unsigned[16] ]
The structure operator {}
This operator defines hierarchical types made up of named subtypes. It is used as follows:
{ <name 0>: <type 0>, <name 1>: <type 1>, ... }
For instance, the following defines a structure with two fields: an 8-bit header
and a 24-bit data
:
{ header: bit[7..0], data: bit[23..0] }
You can assign names to type constructs using the typedef
method:
<type construct>.typedef :<name>
For example, the following code defines char
as a signed 8-bit type:
signed[7..0].typedef :char
After this, char
can be used like any other type. For instance, the following declares an input signal sig
of type char
:
char.input :sig
Alternatively, a new type can be defined using a block:
typedef :<type name> do
<code>
end
Where:
-
type name
is the name of the type -
code
is a description of the content of the type
For example, the char
type could also be defined as:
typedef :char do
signed[7..0]
end
All HDLRuby types are ultimately based on bit vectors, where each bit can hold one of four values: 0
, 1
, Z
, or X
. Bit vectors are unsigned by default, but can be explicitly set to signed.
When performing operations involving signals of different bit-vector types, the shorter signal is automatically extended to match the length of the longer one, preserving its sign if it is signed.
Even though all types in HDLRuby are ultimately bit vectors, complex types can be defined. When such types are used in computational expressions or assignments, they are implicitly converted to unsigned bit vectors of equivalent size.
Expressions are constructs that represent values associated with types. They include immediate values, reference to signals and operations involving other expressions using expression operators.
mmediate values in HDLRuby can represent vectors of type bit
, unsigned
, or signed
, as well as integer
or float
numbers. They are prefixed with an underscore (_
) and include a header indicating the vector type and the numeric base, followed by the actual number.
By default, the bit width is inferred from the length of the numeral, but it can also be explicitly specified in the header. Underscores (_
) can be inserted anywhere within the number to improve readability—they are ignored by the parser.
Vector type specifiers
-
b
:bit
type (can be omitted) -
u
:unsigned
type (equivalent tob
; provided to avoid confusion with the binary base specifier) -
s
:signed
type (the last digit is sign-extended if required for binary, octal, or hexadecimal bases, but not for decimal)
Base specifiers
-
b
: binary -
o
: octal -
d
: decimal -
h
: hexadecimal
Examples
All the following immediate values represent the value 100
, using different bases and types, all encoded as 8-bit values:
_bb01100100
_b8b110_0100
_u8d100
_s8d100
_uh64
_s8o144
You may omit either the type specifier (default: bit
) or the base specifier (default: binary). For example, all of the following also represent 8-bit unsigned values equal to 100
:
_b01100100
_h64
_o144
Notes:
-
The form
_01100100
was previously treated as equivalent to_b01100100
, but due to compatibility issues with recent versions of Ruby, it is now deprecated. -
You may also use Ruby-style immediate values. Their bit width will be automatically adjusted to match the data type of the expression in which they are used. Note, however, that this adjustment may change the value. For example, in the following code, sig is assigned the value
4
(not100
):[3..0].inner :sig sig <= 100
References are expressions used to designate signals or a part of signals.
The simplest reference is the name of a signal. It refers to the signal with that name in the current scope. For example, in the following code, the inner signal sig0
is declared, and the name sig0
then becomes a reference to that signal:
# Declaration of signal sig0.
inner :sig0
# Access to signal sig0 using a name reference.
sig0 <= 0
To refer to a signal in another system, or to a sub-signal within a hierarchical signal, use the dot (.
) operator:
<parent name>.<signal name>
For instance, in the following code, the input signal d
of system instance dff0
is connected to the sub0
field of the hierarchical signal sig
:
system :dff do
input :clk, :rst, :d
output :q
par(clk.posedge) { q <= d & ~rst }
end
system :my_system do
input :clk, :rst
{ sub0: bit, sub1: bit}.inner :sig
dff(:dff0).(clk: clk, rst: rst)
dff0.d <= sig.sub0
...
end
The following table summarizes the operators available in HDLRuby. More details are provided in the subsequent sections for each group of operators.
Assignment Operators (left-most operator of a statement):
symbol | description |
---|---|
<= |
Connection (outside a process) |
<= |
Assingment (inside a process) |
Arithmetic Operators:
symbol | description |
---|---|
+ |
Addition |
- |
Subtraction |
* |
Multiplication |
/ |
Division |
% |
Modulo |
** |
Power |
+@ |
Unary plus (identity) |
-@ |
Negation |
Comparison Operators:
symbol | description |
---|---|
== |
Equality |
!= |
Inequality |
> |
Greater than |
< |
Less than |
>= |
Greater than or equal |
<= |
Less than or equal |
Logic and Shift Operators:
symbol | description |
---|---|
& |
Bitwise/logical AND |
` | ` |
~ |
Bitwise/logical NOT |
mux |
Multiplex |
<< /ls
|
Left shift |
>> /rs
|
Right shift |
lr |
Left rotate |
rr |
Right rotate |
Conversion Operators:
symbol | description |
---|---|
to_bit |
Cast to bit vector |
to_unsigned |
Cast to unsigned vector |
to_signed |
Cast to signed vector |
to_big |
cast to big-endian |
to_little |
cast to little endian |
reverse |
Reverse the bit order |
ljust |
Increase width from the left, preserving the sign |
rjust |
increase width from the right, preserving the sign |
zext |
zero extension (converts to unsigned if signed) |
sext |
sign extension (converts to sign if unsigned) |
Selection/Concatenation Operators:
symbol | description |
---|---|
[] |
sub-vector selection |
@[] |
concatenation operator |
. |
field selection |
Notes:
-
Operator precedence in HDLRuby follows Ruby’s operator precedence rules.
-
Ruby does not allow overriding of the
&&
,||
, or?:
(ternary) operators, so they are not available in HDLRuby.-
Instead of the
?:
operator, HDLRuby provides the more general mux (multiplexer) operator. -
HDLRuby does not provide replacements for
&&
and||
; see the Logic and Shift Operators section for an explanation.
-
Assignment operators can be used with any type. In HDLRuby, both connection and assignment operations are represented by the <=
symbol.
Note: The first <=
in a statement is always interpreted as an assignment operator. Any subsequent occurrences of <=
in the same statement are interpreted as the standard less than or equal to comparison operator.
Arithmetic operators automatically convert operands to vectors of bit
, unsigned
or signed
values, or to integer
, or float
values. The binary arithmetic operators are +
, -
, *
, %
. The unary arithmetic operators are +
(indentity) and -
(negation). All behave the same way as their Ruby equivalents.
Comparison operators return a result of either true or false. In HDLRuby, true is represented by the bit value 1
, and false by the bit value 0
.
Supported operators include: ==
, !=
, <
, >
, <=
, and >=
. These have the same meaning as in Ruby.
Notes:
-
The
<
,>
,<=
and>=
operators automatically converts operands to one of the following types: vectors ofbit
,unsigned
orsigned
, orinteger
orfloat
. -
When comparing values of other types, they are interpreted as
unsigned
bit vectors, unless they are explicitlysigned
orfloat
.
Logic Operators:
In HDLRuby, all logic operators are bitwise. To perform Boolean logic operations, operands must be single-bit values. The bitwise logic operators are:
-
Binary:
&
,|
,^
-
Unary:
~
These behave the same way as their Ruby counterparts.
Note: There are no Boolean (&&
, ||
) operators in HDLRuby for two reasons:
-
Ruby does not support operator overloading for Boolean operators.
-
In Ruby, any value other than
false
ornil
is considered true -- an assumption valid for software, but not for hardware, where values are often bit vectors. Therefore, Boolean logic is supported only through bitwise operators on single-bit values.
Shift Operators:
The shift operators are <<
(left shift) and >>
(right shift).
These preserve the sign for signed
types and do not change bit width. Their behavior matches that of Ruby.
The rotation operators are rl
(left rotate) and rr
(right rotate).
Like shifts, they preserve sign and bit width. Since Ruby lacks rotation operators, these are implemented as methods and used as follows:
<expression>.rl(<other expression>)
<expression>.rr(<other expression>)
For example, to rotate the bits of signal sig
to the left by 3 positions:
sig.rl(3)
More complex shifts and rotations can also be implemented using selection and concatenation. See the Concatenation and selection operators for details.
The conversion operators are used to change the type of an expression.
-
Type puns, which change the interpretation of a value without modifying its raw bit content.
-
Type casts, which modify both the type and the underlying bit representation.
Type Puns:
The type pun operators include to_bit
, to_unsigned
, and to_signed
. These convert an expression of any type into a vector of bit
, unsigned
, or signed
elements, respectively, without altering the raw value.
For example, the following code converts a hierarchical signal into an 8-bit signed vector:
[ up: signed[3..0], down: unsigned[3..0] ].inner :sig
sig.to_bit <= _b01010011
Type Casts:
Type cast operators change both the type and the bit representation of a value. They are used to change the bit width of vectors of type bit, signed, or unsigned.
The type cast operators include:
-
ljust
-
rjust
-
zext
-
sext
Each performs a specific form of bit-width extension:
-
ljust
andrjust
: these operators increase the width of a bit vector by adding bits on the left (ljust
) or right (rjust
) side. They take two arguments: the target width and the bit value (0
or1
) to be added.Example: Extending
sig0
to 12 bits by adding1
s on the right:[7..0].inner :sig0 [11..0].inner :sig1 sig0 <= 25 sig1 <= sig0.ljust(12,1)
-
zext
: this operator performs zero extension by adding0
s to the most significant side, based on the endianness of the value. It takes a single argument: the desired bit width.Example: Extending
sig0
to 12 bits by adding0
s on the left:signed[7..0].inner :sig0 [11..0].inner :sig1 sig0 <= -120 sig1 <= sig0.zext(12)
-
sext
: this operator performs sign extension by duplicating the most significant bit of the original value. The extension side depends on the endianness. It also takes the target bit width as an argument.Example: Extending
sig0
to 12 bits by duplicating the MSB on the right:signed[0..7].inner :sig0 [0..11].inner :sig1 sig0 <= -120 sig1 <= sig0.sext(12)
Concatenation and selection in HDLRuby are performed using the []
operator. Its behavior depends on the argument it receives:
Concatenation:
When the []
operator takes multiple expressions as arguments, it concatenates them.
For example, the following code concatenates sig0
and sig1
into sig2
:
[3..0].inner :sig0
[7..0].inner :sig1
[11..0].inner :sig2
sig0 <= 5
sig1 <= 6
sig2 <= [sig0, sig1]
Selection:
When applied to an expression with a range as the argument, it selects the corresponding slice of bits.
If only a single bit is to be selected, a single index can be used instead.
For example, the following code selects bits 3 down to 1 from sig0
, and bit 4 from sig1
:
[7..0].inner :sig0
[7..0].inner :sig1
[3..0].inner :sig2
bit.inner :sig3
sig0 <= 5
sig1 <= 6
sig2 <= sig0[3..1]
sig3 <= sig1[4]
When there is no ambiguity, HDLRuby automatically inserts conversion operators when two types are not directly compatible. The following rules apply:
-
The bit width is adjusted to match that of the larger operand.
-
If one operand is signed, the computation is performed as signed; otherwise, it is unsigned.
Like Verilog HDL, HDLRuby provides function constructs for reusing code. Functions in HDLRuby are declared as follows:
hdef :<function_name> do |<arguments>|
<code>
end
Where:
-
function_name
is the name of the function. -
arguments
is the list of function parameters. -
code
is the body of the function.
Notes:
-
Functions have their scope, so any declaration within a function is local. It is also forbidden to declare interface signals (
input
,output
, orinout
) within a function. -
Like Ruby
Proc
objects, the last statement in a function is treated as its return value. For example, the following function returns1
(and takes no arguments):function :one { 1 }
-
Functions can accept any type of object as an argument, including variadic arguments and code blocks. For example, the following function applies a block of code passed via
&code
to each argument passed via*args
:function :apply do |*args, &code| args.each { |arg| code.call(args) } end
This function can be used to connect a signal to multiple others. For example, the following connects sig to
x
,y
, andz
:apply(x,y,z) { |v| v <= sig }
You can invoke a function anywhere in your code using its name and passing arguments in parentheses:
<function name>(<list of values>)
While HDLRuby functions are useful for reusing code, they cannot interact with the context in which they are called. For example, they cannot add interface signals or modify control structures such as hif
. For these kinds of high-level, generic operations, you can use standard Ruby functions, which are declared as follows:
def <function_name>(<arguments>)
<code>
end
Where:
-
function_name
is the name of the function. -
arguments
is the list of function parameters. -
code
is the body of the function.
Ruby functions are invoked in the same way as HDLRuby functions, but they behave differently: their code is inlined directly into the location where they are called.
In addition:
- Ruby functions do not have their own scope, so any inner signals or instances declared within them are added to the enclosing object or scope where they are invoked.
For example, the following function adds an input signal in0
to any system in which it is used:
def add_in0
input :in0
end
This function can be used as follows:
system :sys do
...
add_in0
...
end
As another example, the following Ruby function appends a helse
clause of with a reset assignment to a control structure like hif
or hcase
:
def too_bad
helse { rst <= 1 }
end
This function can be used as follows:
system :sys do
...
par do
hif(sig == 1) do
...
end
too_bad
end
end
Caution:
Ruby functions behave similarly to C macros: they offer flexibility by modifying the code in which they are invoked, but they can also introduce unexpected behavior and hard-to-debug issues if used improperly. As a rule, Ruby functions should be avoided unless you are building a generic library for HDLRuby.
HDLRuby allows the description of hardware-software components using the program construct, which encapsulates software code and provides an interface for communication with the hardware. This interface consists of three types of components:
-
Activation events: 1-bit signals that trigger the execution of a specific software function when they transition from
0
to1
(for positive events) or from1
to0
(for negative events). -
Read ports: Bit-vector signals that can be read from within a software function.
-
Write ports: Bit-vector signals that can be written from within a software function.
Note: A single signal can be used simultaneously as both a read and a write port in multiple contexts. However, from the software perspective, it will appear as two separate ports—one for reading and one for writing.
A software component is declared similarly to a hardware process, within a system block. The syntax is as follows:
program(<programming_language>, <function_name>) do
# location of the software files and description of its interface
end
In this declaration:
-
programming_language
is a symbol indicating the language used for the software. Currently supported options are:-
:ruby
-- for programs written in Ruby. -
:c
-- for programs written in C. (In fact, any language that can be compiled into a shared library linkable with C is supported.)
-
-
function_name
is the name of the software function that is executed when an activation event occurs. Only one such function can be specified per program, but multiple programs can be declared within the same module. -
location of the software files and description of its interface
may include the following declarations:-
actport <list of events>
-- Declares the events that activate the program (i.e., trigger execution of the program’s start function). -
inport <port_name: signal>
-- Declares input ports that can be read by the software. -
outport <port_name: signal>
-- Declares output ports that the software can write to. -
code <list_of_filenames>
-- Specifies the software source file(s).
-
Example:
The following example declares a program in Ruby with a start function named echo
. The program is triggered on the positive edge of signal req
, reads from signal count
through port inP
, and writes to signal val
through port outP
. The software code is located in the file echo.rb
:
system :my_system do
inner :req
[8].inner :count, :val
...
program(:ruby,'echo') do
actport req.posedge
inport inP: count
outport outP: val
code "echo.rb"
end
end
Notes:
-
The bit width of an input or output port matches that of the signal it is connected to. From the software perspective, however, all port values are converted to the C type
long long
. -
If the language is Ruby, the
code
section can use a RubyProc
objecct in place of a file name.
The filenames specified in the code
declaration must indicate paths relative to the directory where the HDLRuby tools are run.
In the earlier example, this means that the echo.rb
file must be located in the same directory as the HDLRuby description. If the source file were placed in a ruby/
subdirectory instead, the declaration would be:
code "ruby/echo.rb"
For Ruby programs, you may declare multiple source files, and plain Ruby code can be used as-is without any compilation.
For C programs, however, the code must first be compiled, and the code declaration must refer to the resulting compiled file (not the source). For instance, if the echo function were implemented in C, the declaration would be:
program(:c, :echo) do
actport req.posedge
inport inP: count
outport outP: val
code "echo"
end
To make this work, you must compile the C code into a file named echo.
Note: The file extension is intentionally omitted so that the system can automatically detect the appropriate format (e.g., .so for a shared library on Linux).
From the software point of view, the hardware interface consists only of a list of ports that can either be read or written. However, the implementation of this interface depends on the language.
In Ruby, the hardware interface is accessed by requiring the rubyHDL library. This library provides the RubyHDL module, which exposes the program's ports as module-level accessors.
For example, the following Ruby function reads from the inP
port and writes the result to the outP
port:
require 'rubyHDL'
def echo
val = RubyHDL.inP
RubyHDL.outP = val
end
Note: As long as a port has been declared in the HDLRuby description of the program, it will automatically be accessible in the software via the RubyHDL
module. No additional declarations or configuration are required.
In C (and other C-compatible compiled languages), the interface is accessed by including the cHDL.h
header file. This file must be generated using the following command:
hdrcc --ch <destination_project>
Here, destination_project
is the folder where the C source code is located.
The generated header provides the following interface functions:
-
void* c_get_port(const char* name)
: Returns a pointer to the port with the specified name. -
int c_read_port(void* port)
: Reads the value from the given port pointer. -
int c_write_port(void* port, int val)
: Writes the valueval
to the specified port pointer.
Here is an example program that reads from port inP
and writes the result to port outP
:
#include "cHDL.h"
void echo() {
void* inP = c_get_port("inP");
void* outP = c_get_port("outP");
int val;
val = c_read_port(inP);
c_write_port(outP,val);
}
Notes:
-
The hdrcc command not only generates the C header (cHDL.h) but also creates additional files to assist in compiling the C source code. See compile for simulation for details.
-
Important for Windows: Functions used as HDLRuby entry points must be declared with the
__declspec(dllexport)
prefix. If this is missing, the simulation will not work properly. For example, the echo function on Windows must be declared as:#include "cHDL.h" __declspec(dllexport) void echo() { void* inP = c_get_port("inP"); void* outP = c_get_port("outP"); int val; val = c_read_port(inP); c_write_port(outP,val); }
As long as your programs a correctly described and the software files provided (and compiled in the case of C), the hardware-software co-simulation will be automatically performed when executing the HDLRuby simulator.
While Ruby programs can be used directly, C programs must be compiled into a shared library before they can be simulated.
To do this, you must generate the necessary files -- most importantly, the hardware interface header cHDL.h
. This is done using the following HDLRuby command:
hdrcc --ch <destination_project>
Here, <destination_project>
refers to both the directory where the C code resides and the name of the resulting shared library.
For example, to prepare a project located in the echo
directory, you would run:
hdrcc --ch echo
This command will create a directory named echo containing the cHDL.h file and supporting files.
Next:
-
Place your C source files (e.g.,
echo.c
) into theecho
directory. -
Change into that directory and compile the C code.
If you prefer to compile manually (e.g., without relying on Ruby tools), you can use a standard command like the following (on Linux):
gcc -shared -fPIC -undefined dynamic_lookup -o c_program.so echo.c
This compiles a single-file project into a shared object file suitable for simulation.
Alternatively, if you want a simpler and more portable option, you can use Ruby's rake-compiler
. First install it:
gem install rake-compiler
Then, from within the echo
directory, run:
rake compile
The rake
tool will automatically handle the compilation process across different platforms.
At its current stage, HDLRuby generates only the hardware portion of a design. For example, when generating Verilog, any program
constructs are ignored. It is the user's responsibility to provide additional infrastructure to implement the hardware-software interface.
This limitation exists because such interfaces are target-specific, and often rely on licensed IP or proprietary components that cannot be integrated directly into HDLRuby.
However, this is not as restrictive as it may seem: you can still write program
constructs that wrap access to such hardware interfaces, enabling you to reuse your HDLRuby and software code directly in your target system.
For an example, see the tutorial section: 7.6. hardware-software co-synthesis.
Since HDLRuby programs can support any compiled software, they can be used to execute arbitrary applications -- not just software targeting the main system CPU. For example, peripheral devices such as a keyboard or monitor can be modeled using HDLRuby programs. This approach is illustrated in the HDLRuby sample with_program_ruby_cpu.rb
.
HDLRuby provides a web-based graphical user interface (GUI) for simulating hardware-software systems. This GUI acts as an extension of the co-design platform and is declared within a module using the board
construct:
board(:<board_name>,<server_port>) do
actport <event>
<GUI description>
end
Where:
-
board_name
is the name of the board. -
server_port
is the port number used to access the GUI (default: 8000). -
event
is the signal event (e.g., a clock's rising edge) that synchronizes the GUI with the simulator.
GUI Elements:
The GUI description consists of a list of visual or hidden elements. Active elements must be named and linked to HDLRuby signals using the format:
<element> <element_name>: <HDLRuby_signal>
Supported elements include:
-
sw
: A set of slide switches (bit-width matches the signal). -
bt
: A set of push buttons (bit-width matches the signal). -
slider
: A horizontal slider for numeric input. -
text
: A text input field. The value is interpreted as a Ruby expression. All display objects (e.g.,leds
) can be referenced as variables. -
hook
: Attaches a signal without displaying it. Useful for referencing intext
fields. -
led
: A set of LEDs (bit-width matches the signal). -
hexa
: A hexadecimal display. The width adjusts to the signal's range. -
digit
: A decimal display. Width is based on the signal's numeric range. -
scope
: An oscilloscope-like display. Vertical axis reflects signal values; horizontal axis shows GUI synchronization steps. -
row
: Inserts a new line in the GUI layout.
Example: Adder Interface with GUI:
The following example creates a GUI for an adder system with 8-bit input signals x
and y
, and an output signal z
displayed using LEDs, a numeric display, and an oscilloscope:
system :adder_with_gui do
[8].inner :x, :y, :z
z <= x + y
inner :gui_sync
board(:adder_gui) do
actport gui_sync.posedge
sw x: x
sw y: y
row
led z_led: z
digit z_digit: z
row
scope z_scope: z
end
timed do
clk <= 0
repeat(10000) do
!10.ns
clk <= ~clk
end
end
end
This code defines a GUI with:
-
Two sets of slide switches for inputs
x
andy
(first row), -
A set of LEDs and a decimal display for output
z
(second row), -
An oscilloscope displaying the evolution of
z
over time (third row).
Running the Simulation:
You can simulate this design as you would any HDLRuby system. The following command runs the simulation and generates a VCD waveform file:
hdrcc --sim --vcd my_adder.rb my_adder
When this command is executed, the simulator will wait for a web browser to connect before starting. To launch the GUI, open a browser and navigate to:
http://localhost:8000
Once connected, the simulation will begin, and you can interact with the design through the GUI.
In HDLRuby, time values can be created using the following time suffix operators:
-
s
for seconds. -
ms
for milliseconds. -
us
for microseconds. -
ns
for nanoseconds. -
ps
for picoseconds.
For example, all of the following expressions represent one second:
1.s
1000.ms
1000000.us
1000000000.ns
1000000000000.ps
Like other HDLs, HDLRuby provides specific statements to model the passage of time. These statements are not synthesizable and are intended for simulation only, such as modeling a hardware component’s environment.
To improve clarity and avoid confusion, time-based statements are only allowed in explicitly non-synthesizable processes declared using the timed
keyword:
timed do
<statements>
end
A time process has no sensitivity list but can include any statements allowed in a standard process, plus time-specific statements.
There are two such time statements:
-
wait
statement: this statement blocks the execution of the process for the specified amount of time. For example:wait(10.ns)
This can also be abbreviated using the
!
operator:!10.ns
-
repeat
statement: This statement repeats a block of code for a specified number of iterations. For example, the following toggles theclk
signal every 10 nanoseconds, repeating 10 times:repeat(10) do !10.ns clk <= ~clk end
Note: These time statements are not synthesizable and can only be used within timed
processes.
Time processes use blocking assignments by default, but both blocking and non-blocking assignment blocks can be used inside them.
The execution semantic is:
-
Blocking assignment blocks are executed sequentially.
-
Non-blocking assignment blocks are executed in a semi-parallel manner, based on the following rules:
-
Statements are grouped in sequence until a time statement is encountered.
-
The grouped blocks are executed in parallel.
-
The time statement is executed.
-
Execution resumes with the next group of statements.
-
Since HDLRuby is built on top of Ruby, you can freely use standard Ruby constructs (such as classes, methods, and modules) without any compatibility issues. Additionally, this Ruby code does not interfere with the synthesizability of the resulting hardware design. In fact, Ruby logic can be used to generate HDLRuby constructs at compile time.
However, pure Ruby code does not interact with the HDLRuby name stack, and its misuse may lead to unintended states during compilation. Unless you're intentionally extending HDLRuby itself, it is recommended to avoid low-level Ruby generation logic for general-purpose hardware generation.
Instead, you should prefer HDLRuby’s high-level hardware generation features, which are safer and clearer—similar to Verilog’s generate
construct. These include:
-
Generic programming (explained in the next section)
-
Parallel statements like
hif
orhcase
-
Parallel enumerators (see Parallel Enumerators)
These constructs can be used anywhere in the code without restriction and are generally sufficient for most hardware generation needs.
Example: Conditional Hardware Generation
The hif
and hcase
statements can be used to generate conditional logic. For instance, the following code generates either a clocked process or a continuous one depending on the value of the clocked
flag:
hif(clocked) do
par(clk.posedge) { ... }
helse
par { ... }
end
Modules can be declared with generic parameters using the following syntax:
system :<system_name> do |<list_of_generic_parameters>|
...
end
For example, the following code defines an empty module with two generic parameters named a
and b
:
system(:nothing) { |a,b| }
Generic parameters in HDLRuby can be anything: values, data types, signals, modules, Ruby variables, and more.
Example: Using Generics for Type, Range, and Module
The following example demonstrates a module with:
-
t
: a generic type used for an input signal -
w
: a bit range used for an output signal -
s
: a generic module used to create an instance
system :something do |t,w,s|
t.input :isig
[w].output :osig
s :sI.(i: isig, o: osig)
end
In this example:
-
t.input :isig
declares an input of typet
-
[w].output :osig
declares an output with bit-width or rangew
-
s :sI.(...)
instantiates modules
and connects its ports
Variadic Generic Parameters
You can declare a module with a variable number of generic parameters using Ruby’s splat operator (*
). The parameters are collected into an array.
system(:variadic) { |*args| }
Here, args
is an array containing any number of arguments.
Data types can be declared with generic parameters as follows:
typedef :<type_name> do |<list_of_generic_parameters>|
...
end
For example, the following code defines a bit-vector type with a generic bit width parameter width
:
type(:bitvec) { |width| bit[width] }
As with modules, the generic parameters of types can be any kind of object. It is also possible to use variadic arguments.
A generic module is specialized by invoking its name and passing values for its generic arguments, as shown below:
<module_name>(<generic_argument_values_list>)
If fewer values are provided than the number of generic arguments, the module is partially specialized. However, only a fully specialized module can be instantiated.
A specialized module can also be used for inheritance. For example, assuming the module sys
has two generic arguments, it can be specialized and used to build the module subsys
as follows:
system :subsys, sys(1,2) do
...
end
This kind of inheritance can only be performed with fully specialized modules. For partially specialized modules, include must be used instead. For example, if sys
is specialized with only one value, it can be used in the generic module subsys_gen
as follows:
system :subsys_gen do |param|
include sys(1,param)
...
end
Note: In the example above, the generic parameter param
of subsys_gen
is used to specialize the module sys
.
A generic type is specialized by invoking its name and passing values corresponding to the generic arguments, as follows:
<type_name>(<generic_argument_values_list>)
If fewer values are provided than the number of generic arguments, the type is partially specialized. However, only a fully specialized type can be used for declaring signals.
In HDLRuby, a module can inherit from one or more parent modules using the include
command, as shown:
include <list_of_modules>
This include
can be placed anywhere within the body of a module. However, the inherited content will only be accessible after the include
statement is executed.
For example, the following code first defines a simple D flip-flop (dff
) and then uses it to define a flip-flop with an additional inverted output (qb
):
system :dff do
input :clk, :rst, :d
output :q
par(clk.posedge) { q <= d & ~rst }
end
system :dff_full do
output :qb
include dff
qb <= ~q
end
It is also possible to declare inheritance in a more object-oriented style by listing the parent modules immediately after the module name, as follows:
system :<new_module_name>, <list_of_parent_modules> do
# Additional module code
end
For example, the following code provides an alternative way to define dff_full
:
system :dff_full, dff do
output :qb
qb <= ~q
end
Note: From an implementation perspective, HDLRuby modules behave more like Ruby mixins than traditional class-based inheritance. Internally, modules are treated as sets of methods used to access constructs such as signals and instances.
By default, inner signals and instances defined in a parent module are not accessible in child modules. To expose them, use the export
keyword:
export <symbol_0>, <symbol_1>, ...
For example, the following code exports signals clk
and rst
, and the instance dff0
from the module exporter
, making them accessible in its child module importer
:
system :exporter do
input :d
inner :clk, :rst
dff(:dff0).(clk: clk, rst: rst, d: d)
export :clk, :rst, :dff0
end
system :importer, exporter do
input :clk0, :rst0
output :q
clk <= clk0
rst <= rst0
dff0.q <= q
end
Notes export
accepts symbols or strings representing the names of the components to export -- not references to them.
For example, the following code is invalid:
system :exporter do
input :d
inner :clk, :rst
dff(:dff0).(clk: clk, rst: rst, d: d)
export clk, rst, dff0
end
Signals and instances cannot be overridden, including those inherited from parent modules. For example, the following code is invalid because the signal rst
is already defined in dff
:
system :dff_bad, dff do
input :rst
end
In HDLRuby, it is possible to declare a signal or instance in a child module with the same name as one from an included module. When this happens, the construct from the parent module becomes shadowed -- it still exists but is no longer directly accessible, even if exported.
To access a shadowed signal or instance, you must reinterpret the current module as the parent using the as
operator:
as(<parent_module)
For example, in the code below, the signal db
defined in dff_shadow
shadows the one from dff_db
. The original db
can still be accessed using the as operator:
system :dff_db do
input :clk,:rst,:d
inner :db
output :q
db <= ~d
(q <= d & ~rst).at(clk.posedge)
end
system :dff_shadow, dff_db do
output :qb, :db
db <= ~d
qb <= as(dff_db).db
end
HDLRuby allows you to continue the definition of a module after it has already been declared by using the open
method, as shown below:
<module>.open do
# Additional description for the module
end
For example, the module dff
, which describes a D flip-flop, can be extended to include an inverted output as follows:
dff.open do
output :qb
qb <= ~q
end
When a modification is required for a specific instance, it may be preferable to modify only that instance rather than creating a new module derived from the original. To do this, you can open the instance for modification using the following syntax:
<instance_name>.open do
# Additional description for the instance
end
For example, an instance of the previously defined dff
module can be extended to include an inverted output as follows:
system :some_system do
...
dff :dff0
dff0.open do
output :qb
qb <= ~q
end
...
end
Operators can be overloaded for specific types. This allows, for example, seamless support for fixed-point computations without requiring explicit adjustment of the decimal point position.
An operator is redefined as follows:
<type>.define_operator(:<op>) do |<args>|
# Operation description
end
Where:
-
type
is the type from which the operator is overloaded. -
op
is the operator being overloaded (e.g.,+
). -
args
are the arguments of the operation. -
operation description
is an HDLRuby expression defining the new behavior of the operator.
Example: Fixed-Point Type
Suppose fix32
is a 32-bit fixed-point type with the decimal point at bit 16, defined as follows:
signed[31..0].typedef(:fix32)
You can overload the multiplication operator to maintain correct decimal alignment as follows:
fix32.define_operator(:*) do |left,right|
(left.as(signed[31..0]) * right) >> 16
end
Note: In the example above, left
is explicitly cast to a plain signed bit-vector to prevent infinite recursive calls to the overloaded * operator.
Overloading with Generic Types
Operators can also be overloaded for generic types. In this case, the generic parameters must be included in the block parameters of the overloaded operator.
For example, consider a generic fixed-point type where the decimal point is set at half the bit width:
typedef(:fixed) do |width|
signed[(width-1)..0]
end
You can overload the multiplication operator for this type as follows:
fixed.define_operator do |width,left,right|
(left.as(signed[(width-1)..0]) * right) >> width/2
end
HDLRuby provides several predicate and access methods to retrieve information about the current state of the hardware description.
predicate name | predicate type | predicate meaning |
---|---|---|
is_block? |
bit | Returns 1 if currently inside a block. |
is_par? |
bit | Returns 1 if the current block is non-blocking. |
is_seq? |
bit | Returns 1 if the current block is blocking. |
is_clocked? |
bit | Returns 1 if the current process is clocked (i.e., triggered by a single rising or falling edge of a signal). |
cur_block |
block | Returns the current block. |
cur_behavior |
process | Returns the current process (behavior). |
cur_systemT |
system | Returns the current module (system). |
top_block |
block | Returns the top block of the current process. |
parent |
any | Returns the parent construct. |
Enumerators
HDLRuby also provides enumerators for accessing internal elements of the current construct in its current state:
enumerator name | accessed elements |
---|---|
each_input |
Iterates over the input signals of the current system. |
each_output |
Iterates over the output signals of the current system. |
each_inout |
Iterates over the inout signals of the current system. |
each_behavior |
Iterates over the processes (behaviors) of the current system. |
each_event |
Iterates over the events of the current process. |
each_block |
Iterates over the blocks of the current process. |
each_statement |
Iterates over the statements in the current block. |
each_inner |
Iterates over the inner signals of the current block (or of the system if not inside a block). |
As with any Ruby program, it is possible to define and execute methods anywhere in HDLRuby using standard Ruby syntax. When a method is defined, it is attached to the enclosing HDLRuby construct. For example:
-
If a method is defined within a module declaration, it can only be used inside that module.
-
If a method is defined outside of any construct, it can be used throughout the HDLRuby description.
A method can include HDLRuby code, in which case the resulting hardware description is appended to the current construct. For example, the following code connects sig0
to Psig1within the module
sys0, and assigns
sig0to
sig1within the process of
sys1`:
def some_arrow
sig1 <= sig0
end
system :sys0 do
input :sig0
output :sig1
some_arrow
end
system :sys1 do
input :sig0, :clk
output :sig1
par(clk.posedge) do
some_arrow
end
end
Warnings:
-
In the example above, the semantics of some_arrow change depending on the context in which it is called:
-
Within a module: interpreted as a static connection.
-
Within a process: interpreted as a behavioral assignment.
-
-
Using Ruby methods to describe hardware can lead to fragile or incorrect code if not used carefully. For example, consider the following:1
def in_decl input :in0 end system :sys0 do in_decl end system :sys1 do input :in0 in_decl end system :sys2 do par do in_decl end end
In this case:
-
sys0
works correctly. -
sys1
raises an error due to redeclaration ofin0
. -
sys2
raises an error becauseinput
declarations are not allowed inside a process.
-
Using Ruby Method Features
Ruby methods in HDLRuby support all standard Ruby features, including:
-
Variadic arguments (
*args
) -
Named (keyword) arguments
-
Block arguments (
&block
)
For example, the following method connects a single driver signal to multiple targets:
def mconnect(driver, *signals)
signals.each do |signal|
signal <= driver
end
end
system :sys0 do
input :i0
input :o0, :o1, :o2, :o3
mconnect(i0,o0,o1,o2,o3)
end
Like any Ruby-based framework, HDLRuby constructs can be dynamically extended. While modifying their internal structure is generally discouraged, it is possible -- and sometimes useful -- to add methods to existing classes for customization and extension.
A global extension refers to the traditional Ruby technique of monkey patching, where new methods are added to an existing class. For example, you can add a method that returns the number of interface signals (inputs, outputs, and inouts) of a module instance as follows:
class SystemI
def interface_size
return each_input.size + each_output.size + each_inout.size
end
end
Once defined, the interface_size
method can be used on any module instance:
<module_instance>.interface_size
The following table shows the HDLRuby class associated with each core construct:
construct | class |
---|---|
Data type | Type |
Module (system) | SystemT |
Scope | Scope |
Module instance | SystemI |
Signal | Signal |
Connection | Connection |
Process (par , seq ) |
Behavior |
Time process (timed ) |
TimeBehavior |
Event | Event |
Block (par , seq , sub ) |
Block |
Assignment | Transmit |
Conditional (hif ) |
If |
Case (hcase ) |
Case |
Program (program ) |
Program |
A local extension of an HDLRuby construct means that only the targeted construct is modified, while all other constructs of the same type remain unaffected. This is accomplished in Ruby by accessing the construct's eigenclass using the singleton_class
method and then modifying it via class_eval
.
Local Extension of a Specific Module
In the following example, only the module dff
is extended with the interface_size
method:
dff.singleton_class.class_eval do
def interface_size
return each_input.size + each_output.size + each_inout.size
end
end
After this extension, only dff
responds to interface_size
; other modules remain unchanged.
Local Extension of a Specific Instance
Similarly, you can extend a single instance of a module. In this example, only the instance dff0
gains the interface_size
method:
dff :dff0
dff0.singleton_class.class_eval do
def interface_size
return each_input.size + each_output.size + each_inout.size
end
end
Other instances of the same module will not be affected.
Local Extension of All Instances of a Module
To extend all instances of a particular module, use the singleton_instance
method instead of singleton_class
. For example:
dff.singleton_instance.class_eval do
def interface_size
return each_input.size + each_output.size + each_inout.size
end
end
Now, any instance of the dff
module will respond to the interface_size
method.
The primary purpose of supporting global and local extensions for HDLRuby constructs is to allow users to customize and control the hardware generation process. This is especially useful when implementing synthesis algorithms tailored to specific types of modules.
For example, suppose you want to implement a generation algorithm for a category of modules. You can define an abstract module -- one without hardware content -- that holds the generation logic:
system(:my_base) {}
my_base.singleton_instance.class_eval do
def my_generation
<some code>
end
end
When the module my_base
is used as a parent (i.e., included in another module), the child module inherits the my_generation
method. For example:
system :some_system, my_base do
# Some system description
end
Generation Invocation
To use the custom generation logic before converting to a low-level hardware description, you would typically write:
some_system :instance0
instance0.my_generation
low = instance0.to_low
However, this manual invocation can be avoided by overriding the to_low
method to automatically include the generation step:
system(:my_base) {}
my_base.singleton_instance.class_eval do
def my_generation
<some code>
end
alias :_to_low :to_low
def to_low
my_generation
_to_low
end
end
With this modification, calling to_low
on any instance of a module that inherits from my_base will automatically execute my_generation beforehand:
some_system :instance0
low = instance0.to_low # Automatically runs my_generation
The standard libraries are included in the Std
Ruby module.
They can be loaded as follows, where library_name
is the name of the library:
require 'std/<library_name>'
After loading a library, you must include the Std
Ruby module as follows:
include HDLRuby::High::Std
However,
hdrcc
loads the stable components of the standard library by default, so you do not need to require or include anything additional to use them.
As of the current version, the stable components are:
-
std/clocks.rb
-
std/fixpoint.rb
-
std/decoder.rb
-
std/fsm.rb
-
std/sequencer.rb
-
std/sequencer_sync.rb
-
std/hruby_enum.rb
The clocks
library provides utilities to simplify clock synchronization handling.
It allows you to multiply an event by an integer. The result is a new event whose frequency is divided by the integer multiplier.
For example, the following code describes a D flip-flop that captures data every three clock cycles:
system :dff_slow do
input :clk, :rst
input :d
output :q
( q <= d & ~rst ).at(clk.posedge * 3)
end
Note: This library automatically generates the RTL code required to implement the frequency division circuitry.
The decoder library provides a new set of control statements for easily describing instruction decoders.
A decoder can be declared anywhere within a module definition using the decoder
keyword, as shown below:
decoder(<signal>) <block>
Here, signal
is the signal to decode, and block
is a procedure block (i.e., a Ruby proc) that defines the decoding behavior. This block can contain any code normally allowed in a standard process, and it also supports the special entry
statement.
The entry
statement defines a bit-pattern to match and the corresponding action to perform when the signal matches that pattern. Its syntax is:
entry(<pattern>) <block>
-
pattern
is a string that defines the bit pattern to match. -
block
is a procedure block (HDLRuby code) specifying the actions to execute when the pattern matches.
The pattern string can include:
-
0
and1
characters to match fixed bit values. -
Alphabetical characters to define named fields within the pattern.
These named fields can be used as variables in the action block. If the same letter appears multiple times in the pattern, the corresponding bits are concatenated to form a multi-bit signal.
For example, the following code defines a decoder for the signal ir with two entries:
-
The first entry sums fields
x
andy
and assigns the result to signals
. -
The second entry sums fields
x
,y
, andz
and assigns the result tos
.
decoder(ir) do
entry("000xx0yy") { s <= x + y }
entry("10zxxzyy") { s <= x + y + z }
end
Note that field bits do not need to be contiguous. For example, field z
in the second entry spans non-adjacent bits.
The fsm library provides a set of control statements for easily describing finite state machines (FSMs).
An FSM can be declared anywhere in a module, provided it is outside any process, using the fsm
keyword:
fsm(<event>,<reset>,<mode>) <block>
Where:
-
event
is the event (e.g., rising or falling edge of a signal) that triggers state transitions. -
reset
is the reset signal. -
mode
is the default execution mode of the FSM, either:sync
(synchronous/Moore) or:async
(asynchronous/Mealy). -
block
is a procedure block that defines the FSM's states and transitions.
Defining States
FSM states are declared with the following syntax:
<kind>(<name>) <block>
Where:
-
kind
is the type of state (reset, state, sync, or async). -
name
is he state name (as a symbol). -
block
is the actions to execute when the FSM is in that state.
The available state kinds are:
-
reset
: The state entered when the FSM is reset.-
If name is
:sync
, the reset is forced to be synchronous. -
If name is
:async
, the reset is forced to be asynchronous. -
If name is omitted, the mode defaults to that of the FSM.
-
-
state
: A regular state that follows the FSM’s default mode. -
sync
: A state that is always synchronous, regardless of the FSM mode. -
async
: A state that is always asynchronous, regardless of the FSM mode.
Default Actions
You can define actions that run in every state using the default
statement:
default <block>
This block will execute alongside the states' block.
State Transitions
By default, state transitions follow the order in which the states are declared. When the last state is reached, the next transition loops back to the first state -- unless otherwise specified.
To define specific transitions, use the goto
statement at the end of a state's action block:
goto(<condition>,<names>)
Where:
-
condition
: A signal whose value is used as an index. -
names
: A list of target states. The condition’s value selects one of them by index.
For example:
goto(cond,:st_a,:st_b,:st_c)
This means:
-
If
cond == 0
, transition tost_a
-
If
cond == 1
, transition tost_b
-
If
cond == 2
, transition tost_c
-
Otherwise, this
goto
is ignored
Multiple goto
statements can be used in the same block. If more than one is taken, the last matching one takes precedence.
If no goto
is taken, the FSM continues with the next declared state.
For example, the following code describes an FSM describing a circuit that checks if two buttons (but_a
and but_b
) are pressed and released in sequence for activating an output signal (ok
):
Example
The following example defines an FSM that detects a sequence of button presses (but_a
followed by but_b
) and sets the output ok accordingly:
fsm(clk.posedge,rst,:sync) do
default { ok <= 0 }
reset do
goto(but_a, :reset, but_a_on)
end
state(:but_a_on) do
goto(but_a, :but_a_off, :but_a_on)
end
state(:but_a_off) do
goto(but_b, :but_a_off, :but_b_on)
end
state(:but_b_on) do
goto(but_b, :but_b_off, :but_b_on)
end
state(:but_b_off) do
ok <= 1
goto(:but_b_off)
end
end
About Goto Behavior
goto
statements are global within a state. Their position in the block does not affect execution order. For example, both of the following result in an unconditional transition to :st_a
:
state(:st_0) do
goto(:st_a)
end
state(:st_1) do
hif(cond) { goto(:st_a) }
end
However, to make the transition conditional, write:
state(:st_1) do
goto(cond,:st_a)
end
Static FSM Mode
While goto
simplifies FSM design in most cases, sometimes finer control is needed. You can configure the FSM in :static
mode, where transitions are explicitly defined using next_state
statements.
To enable static mode, use :static
as the FSM's execution mode:
fsm(clk.posedge,rst,:static)
state(:st_0) do
next_state(:st_1)
state(:st_1) do
hif(cond) { next_state(:st_1) }
helse { next_state(:st_0) }
end
end
In this mode, each state explicitly defines its next state(s), allowing precise transition logic.
HDLRuby parallel enumerators are objects used to generate hardware processes that operate on series of signals in parallel.
They are created using the heach
method on parallel enumerable objects.
Parallel Enumerable Objects
Parallel enumerable objects include:
-
Arrays of signals
-
Ranges
-
Expressions (enumerating on each bit)
You can generate a parallel enumerable object from an integer value using one of the following methods:
-
<integer>.htimes
: Equivalent to the range0..<integer-1>
. -
<integer>.supto(<last>)
: Equivalent to the range<integer>..<last>
. -
<integer>.sdownto(<last>)
: Equivalent to the range<last>..<integer>
.
Parallel Enumerator Control Methods
Parallel enumerators provide several control methods:
-
hsize
: Returns the number of elements accessible by the enumerator. -
htype
: Returns the type of the elements accessed. -
heach
: Returns the enumerator itself. If a block is given, it performs the iteration. -
heach_with_index
: Iterates over each element and its index. Returns an enumerator or performs iteration if a block is given. -
heach_with_object(<obj>)
: Iterates over each element with a custom object. Returns an enumerator or performs iteration if a block is given. -
with_index
: Identical toseach_with_index
. -
with_object(<obj>)
: Identical toseach_with_object
. -
clone
: Creates a new enumerator over the same elements. -
+
: Concatenates two enumerators.
Hardware Implementations of Enumerable Methods
Using parallel enumerators, HDLRuby provides hardware implementations of many Ruby Enumerable methods. These are available for any enumerable object and can be used inside or outside processes.
Each method name corresponds to its Ruby counterpart, prefixed with an h
(for "hardware"). For example, hall?
is the hardware implementation of Ruby's all?
.
-
hall?
: Hardware implementation ofall?
. Returns a 1-bit signal (0
= false,1
= true). -
hany?
: Hardware implementation ofany?
. Returns a 1-bit signal. -
hchain
: Hardware implementation ofchain
. -
hmap
: Hardware implementation ofmap
. Returns a vector signal of the computed results.
-
hcount
: Hardware implementation ofcount
. Returns a signal whose bit width matches the size of the enumerator containing the count result.
-
hfind
: Hardware implementation offind
. Returns the found element or0
if not found. -
hdrop
: Hardware implementation ofdrop
. Returns a vector signal of the remaining elements.
-
heach_cons
: Hardware implementation ofeach_cons
. -
heach_slice
: Hardware implementation ofeach_slice
. -
heach_with_index
: Hardware implementation ofeach_with_index
. -
heach_with_object
: Hardware implementation ofeach_with_object
. -
hto_a
: Hardware implementation ofto_a
. Returns a vector signal of all enumerated elements.
-
hfind_index
: Hardware implementation offind_index
. Returns the index of the found element or-1
if not found. -
hfirst
: Hardware implementation offirst
. Returns a vector signal of the first elements. -
hinclude?
: Hardware implementation ofinclude?
. Returns a 1-bit signal. -
hinject
: Hardware implementation ofinject
. Returns a signal containing the accumulated result. The data type of the result can be passed as initialization argument. -
hmax
: Hardware implementation ofmax
. Returns a vector signal of the maximum values.Note: Only one maximum value is supported at the moment.
-
hmax_by
: Hardware implementation ofmax_by
. Returns a vector signal of the maximum values.Note: Only one maximum value is supported at the moment.
-
hmin
: Hardware implementation ofmin
. Returns a vector signal of the minimum values.Note: Only one minimum value is supported at the moment.
-
hmin_by
: Hardware implementation ofmin_by
. Returns a vector signal of the minimum values.Note: Only one minimum value is supported at the moment.
-
hminmax
: Hardware implementation ofminmax
. Returns a 2-element vector signal with the minimum and maximum values. -
hminmax_by
: Hardware implementation of the Rubyminmax_by
method. Returns a 2-element vector signal with the minimum and maximum values. -
hnone?
: Hardware implementation ofnone?
. Returns a 1-bit signal. -
hone?
: Hardware implementation ofone?
. Returns a 1-bit signal.
-
hreverse_each
: Hardware implementation ofreverse_each
.Note: To be used inside a
seq
process. -
hsort
: Hardware implementation ofsort
. Returns a vector of sorted elements.Note: When the number of elements is not a power of 2, you must provide the maximum (or minimum for descending sort) value as an argument.
-
hsort_by
: Hardware implementation ofsort_by
. Returns a vector signal containing the sorted elements.Note: When the number of elements is not a power of 2, you must provide the maximum (or minimum for descending sort) value as an argument.
-
hsum
: Hardware implementation ofsum
. Returns a signal with the total sum. -
htake
: Hardware implementation oftake
. Returns a vector of the selected elements.
This library provides a set of software-like control statements for describing the behavior of a circuit. Behind the scenes, these constructs generate a finite state machine (FSM), where states are inferred from control points in the description.
Although sequencers are intended for hardware design, they are software-compatible and can efficiently execute as software programs. For more information, see the section on software sequencers.
Declaring a Sequencer
A sequencer can be declared anywhere in a system, as long as it is outside of a process, using the sequencer
keyword:
sequencer(<clock>,<start>) <block>
Where:
-
clock
is the signal (or event, such asposedge
ornegedge
) that advances the sequencer. -
start
is the signal (or event) that starts the sequencer. -
block
is the sequence of operations to perform.
Sequencer Constructs
The sequence block behaves like a seq
block but includes the following software-like control statements:
-
step
: Waits until the next event (as defined by the sequencer’sevent
). -
steps(<num>)
: Repeatsstep
fornum
cycles.num
can be any expression. -
sif(<condition>) <block>
: Executesblock
if condition is true (not0
). -
selsif(<condition>) <block>
: Executes block if all previoussif
/selsif
conditions were false (0
) and this one is true (not0
). -
selse <block>
: Executesblock
if none of the previous conditions were met. -
swait(<condition>)
: Waits untilcondition
becomes true (not0
). -
swhile(<condition>) <block>
: Repeatsblock
while condition is true (not0
). -
sfor(<enumerable>) <block>
: Iterates over each element of an enumerable object or signal. -
sbreak
: Exits the current loop. -
scontinue
: Skips to the next iteration. -
sterminate
: Ends the sequencer’s execution.
Controlling Sequencers Externally
Two methods can be used to control a sequencer from outside:
-
alive?
: Returns1
if the sequencer is still running;0
otherwise. -
reset!
: Resets the sequencer to its initial state.
To use these methods, assign the sequencer to a reference variable:
ref_sequencer = sequencer(clk,start) do
# Some sequencer code
end
# ... Somewhere else in the code.
# Reset the sequencer if it ended its execution.
hif(ref_sequencer.alive? == 0) do
ref_sequencer.reset!
end
Using Enumerators in Sequences
Within sequencer blocks, HDLRuby provides enumerator methods similar to Ruby’s each
. These include:
-
<object>.seach
:object
can be any Ruby enumerable or HDLRuby signal. If a block is given, it behaves like sfor; otherwise, it returns an HDLRuby enumerator (see enumerator for details). -
<object>.stimes
: Can be used on integers and is equivalent to calling seach on the range0..object-1
. -
<object>.supto(<last>)
: Can be used on integers and is equivalent to callingseach
on the rangeobject..last
. -
<object>.sdownto(<last>)
: Can be used on an integer and is equivalent to callingseach
on the rangeobject..last
in reverse order.
Objects that support these methods are called enumerable objects. These include HDLRuby signals, HDLRuby enumerators, and all Ruby enumerable types (e.g., ranges, arrays).
Examples
Below are a few examples of sequencers synchronized on the positive edge of clk
, starting when start
becomes 1
.
Example 1: Fibonacci Sequence
his sequencer computes the Fibonacci sequence up to 100, producing a new term in the signal v
on each clock cycle:
require 'std/sequencer.rb'
include HDLRuby::High::Std
system :a_circuit do
inner :clk, :start
[16].inner :a, :b
sequencer(clk.posedge,start) do
a <= 0
b <= 1
swhile(v < 100) do
b <= a + b
a <= b - a
end
end
end
Example 2: Squaring Integers
This sequencer computes the square of integers from 10 to 100, producing one result per cycle in signal a
:
inner :clk, :start
[16].inner :a
sequencer(clk.posedge,start) do
10.supto(100) { |i| a <= i*i }
end
Example 3: Reversing a String in Memory
This sequencer reverses the contents of memory mem
. The final result will be "!dlrow olleH":
inner :clk, :start
bit[8][-12].inner mem: "Hello world!"
sequencer(clk.posedge,start) do
mem.size.stimes do |i|
[8].inner :tmp
tmp <= mem[i]
mem[i] <= mem[-i-1]
mem[-i-1] <= tmp
end
end
Example 4: Summing Elements with Early Termination
This sequencer computes the sum of the elements in memory mem
, stopping if the sum exceeds 16:
inner :clk, :start
bit[8][-8].inner mem: [ _h02, _h04, _h06, _h08, _h0A, _h0C, _h0E ]
bit[8] :sum
sequencer(clk.posedge,start) do
sum <= 0
sfor(mem) do |elem|
sum <= sum + elem
sif(sum > 16) { sterminate }
end
end
HDLRuby sequential enumerators are objects used to perform iterations within sequencers. They are created using the seach
method on enumerable objects, as presented in the previous section.
Enumerators can be controlled using the following methods:
-
size
: Returns the number of elements the enumerator can access. -
type
: Returns the type of elements accessed by the enumerator. -
seach
: Returns the current enumerator. If a block is given, it performs the iteration instead of returning an enumerator. -
seach_with_index
: Returns an enumerator over the elements of the current enumerator, paired with their index positions. If a block is given, it performs the iteration instead. -
seach_with_object(<obj>)
: Returns an enumerator over the elements of the current enumerator, each paired with the given objectobj
(any object, HDLRuby or otherwise). If a block is given, it performs the iteration instead. -
with_index
: Identical toseach_with_index
. -
with_object(<obj>)
: Identical toseach_with_object
. -
clone
: Creates a new enumerator over the same elements. -
speek
: Returns the current element pointed to by the enumerator without advancing it. -
snext
: Returns the current element pointed to by the enumerator and then advances to the next one. -
srewind
: Restarts the enumeration from the beginning. -
+
: Concatenates two enumerators.
You can also define a custom enumerator using the following syntax:
<enum> = senumerator(<typ>,<size>) <block>
Where:
-
enum
is a Ruby variable referring to the enumerator, -
typ
is the data type of the elements, -
block
is the code block that defines how to access each element by index.
For example, an enumerator over a memory can be defined as follows:
bit[8][-8].inner mem: [ _h01, _h02, _h03, _h04, _h30, _h30, _h30, _h30 ]
[3].inner :addr
[8].inner :data
data <= mem[addr]
mem_enum = senumerator(bit[8],8) do |i|
addr <= i
step
data
end
In the code above, mem_enum
is a variable referring to the enumerator that accesses memory mem
. The access assumes that one clock cycle must pass after setting the address before the data becomes available. Therefore, a step command is used in the block before returning data.
Enumeration Algorithms
Based on the enumerator functionality, several algorithms have been implemented in HDLRuby using sequential enumerators. These algorithms mirror the behavior of Ruby's Enumerable methods and are compatible with all HDLRuby enumerable objects. Each algorithm is implemented in hardware for HDLRuby sequencers and is accessible via the corresponding Ruby method, prefixed with the letter s
.
Here are the available methods in detail:
-
sall?
: Sequencer implementation ofall?
. Returns a 1-bit signal (0
for false,1
for true). -
sany?
: Sequencer implementation ofany?
. Returns a 1-bit signal. -
schain
: Sequencer implementation ofchain
. -
smap
: Sequencer implementation ofmap
. When used with a block, returns a vector signal containing each computation result. -
scompact
: Sequencer implementation ofcompact
. Since there is nonil
in HDLRuby, the value0
is used instead. Returns a vector signal containing the compacted result. -
scount
: Sequencer implementation ofcount
. Returns a signal whose bit width matches the enumerator’s size, representing the count result. -
scycle
: Sequencer implementation ofcycle
. -
sfind
: Sequencer implementation offind
. Returns a signal containing the found element, or 0 if not found. -
sdrop
: Sequencer implementation ofdrop
. Returns a vector signal containing the remaining elements. -
sdrop_while
: Sequencer implementation ofdrop_while
. Returns a vector signal containing the remaining elements. -
seach_cons
: Sequencer implementation ofeach_cons
. -
seach_slice
: Sequencer implementation ofeach_slice
. -
seach_with_index
: Sequencer implementation ofeach_with_index
. -
seach_with_object
: Sequencer implementation ofeach_with_object
. -
sto_a
: Sequencer implementation ofto_a
. Returns a vector signal containing all the elements of the enumerator. -
sselect
: Sequencer implementation ofselect
. Returns a vector signal containing the selected elements. -
sfind_index
: Sequencer implementation offind_index
. Returns the index of the found element or -1 if not. -
sfirst
: Sequencer implementation offirst
. Returns a vector signal containing the first elements. -
sinclude?
: Sequencer implementation ofinclude?
. Returns a 1-bit signal. -
sinject
: Sequencer implementation ofinject
. Returns a signal of the same type as the enumerator’s elements, containing the result. -
smax
: Sequencer implementation ofmax
. Returns a vector signal containing the found maximum value(s). -
smax_by
: Sequencer implementation ofmax_by
. Returns a vector signal containing the found maximum value(s). -
smin
: Sequencer implementation ofmin
. Returns a vector signal containing the found minimum value(s). -
smin_by
: Sequencer implementation ofmin_by
. Returns a vector signal containing the found minimum value(s). -
sminmax
: Sequencer implementation ofminmax
. Returns a 2-element vector signal containing the resulting minimum and maximum values. -
sminmax_by
: Sequencer implementation ofminmax_by
. Returns a 2-element vector signal containing the resulting minimum and maximum values. -
snone?
: Sequencer implementation ofnone?
. Returns a 1-bit signal. -
sone?
: Sequencer implementation ofone?
. Returns a 1-bit signal. -
sreject
: Sequencer implementation ofreject
. Returns a vector signal containing the remaining elements. -
sreverse_each
: Sequencer implementation ofreverse_each
. -
ssort
: Sequencer implementation ofsort
. Returns a vector signal containing the sorted elements. -
ssort_by
: Sequencer implementation ofsort_by
. Returns a vector signal containing the sorted elements. -
ssum
: Sequencer implementation ofsum
. Returns a signal of the same type as the enumerator’s elements, containing the sum result. -
stake
: Sequencer implementation oftake
. Returns a vector signal containing the taken elements. -
stake_while
: Sequencer implementation oftake_while
. Returns a vector signal containing the taken elements. -
suniq
: Sequencer implementation ofuniq
. Returns a vector signal containing the selected elements.
s with any other process, multiple sequencers cannot write to the same signal. Doing so would cause race conditions, which can physically damage the device if permitted. In standard RTL design, this issue is typically handled using three-state buses, multiplexers, and arbiters.
However, HDLRuby sequencers introduce a special kind of signal called a shared signal, which abstracts away these implementation details and prevents race conditions.
Shared signals are declared similarly to regular signals, based on their type. The syntax is:
<type>.shared <list of names>
They can also be initialized with default values as follows:
<type>.shared <list of names with initialization>
For example, the following code declares two 8-bit shared signals x
and y
, and two signed 16-bit shared signals u
and v
, both initialized to 0:
[8].shared :x, :y
signed[8].shared u: 0, v: 0
A shared signal can be read from and written to by any sequencer, from anywhere in the subsequent code within the current scope. However, shared signals cannot be written to outside of a sequencer.
Valid example:
input :clk, :start
[8].inner :val0, :val1
[8].shared :x, :y
val0 <= x+y
par(clk.posedge) { val1 <= x+y }
sequencer(clk.posedge,start) do
10.stimes { |i| x <= i }
end
sequencer(clk.posedge,start) do
5.stimes { |i| x <= i*2 ; y <= i*2 }
end
Invalid example:
[8].shared w: 0
par(clk.posedge) { w <= w + 1 }
By default, a shared signal acknowledges writes from the first sequencer that attempts to write to it (in order of declaration). All other writes are ignored. In the valid example above, the value of x
is always set by the first sequencer, producing values from 0 to 9, changing once per clock cycle. The signal y
, however, is only written by the second sequencer and thus reflects its values.
This default behavior avoids race conditions but offers limited flexibility. To gain better control, you can explicitly select which sequencer is allowed to write to a shared signal. This is done using the select
sub-signal of the shared signal:
<shared signal>.select <= <index>
The selection index starts at 0 for the first sequencer writing to the signal, 1 for the second, and so on.
For example, to allow the second sequencer to write to x, you can add the following line after declaring x
:
x.select <= 1
This selection can also be changed dynamically at runtime. For instance, to alternate the writer every clock cycle:
par(clk.posedge) { x.select <= x.select + 1 }
Note: The select
sub-signal is a standard RTL signal and is subject to the same rules and limitations as any other non-shared signal. It is not itself a shared signal.
In most cases, it's not the signals themselves that we want to share, but rather the resources they control. For example, in a CPU, it's the ALU that is shared as a whole -- not each of its inputs separately. To support such scenarios and simplify the handling of shared signals, HDLRuby provides arbiter components.
An arbiter is instantiated like a standard module. The syntax is as follows, where name
is the name of the arbiter instance:
arbiter(:<name>).(<list_of_shared_signal>)
When instantiated, the arbiter takes control of the select
sub-signals of the specified shared signals. As a result, you can no longer manually set the select
values for those signals. In exchange, the arbiter allows sequencers to request or release write access to the shared signals.
To request access, a sequencer assigns the value 1 to the arbiter. To release access, it assigns 0. If a sequencer attempts to write to a shared signal under arbitration without first requesting access, the write will be ignored.
Example
The following example defines an arbiter named ctrl_xy
that manages access to shared signals x
and y
, along with two sequencers that request and release access to them:
input :clk, :start
[8].shared x, y
arbiter(:ctrl_xy).(x,y)
sequencer(clk.posedge,start) do
ctrl_xy <= 1
x <= 0 ; y <= 0
5.stime do |i|
x <= x + 1
y <= y + 2
end
ctrl_xy <= 0
end
sequencer(clk.posedge,start) do
ctrl_xy <= 1
x <= 2; y <= 1
10.stime do |i|
x <= x + 2
y <= y + 1
end
ctrl_xy <= 0
end
In this example, both sequencers request access before writing to the shared signals and release it afterward.
Note: Requesting access does not guarantee that access will be granted. If access is not granted, write operations will be ignored.
By default, the arbiter grants access based on the order of sequencer declaration. That is, if multiple sequencers request access simultaneously, the one declared first in the code has priority.
In the example above, the first sequencer is granted write access to x
and y
and holds it for five cycles. Once it releases access, the second sequencer gains control and begins writing. The second sequencer runs its first five iterations without affecting the shared signals—only the last five are effective.
To avoid wasting cycles in such situations, a sequencer can check whether it currently holds write access by using the arbiter’s acquired
sub-signal. This signal is 1 if the sequencer has been granted access and 0 otherwise. For example, the following line will increment x
only when access is granted:
hif(ctrl_xy.acquired) { x <= x + 1 }
Changing Arbiter Policy
You can change the arbiter's access-granting policy using the policy
method. One option is to provide a priority list -- a vector of sequencer indices in order of decreasing priority (i.e., the first entry has the highest priority). Sequencers are numbered in the order they are declared and use the arbiter.
For example, to give the second sequencer priority over the first in the earlier example, you could write:
ctrl_xy.policy([1,0])
You can also define more complex arbitration logic by passing a block to policy
. This block receives a vector (acq
) indicating which sequencers are currently requesting access (each bit set to 1 means a request is active), and returns the index of the sequencer to be granted access.
Here’s an example that alternates the priority at each access:
inner priority_xy: 0
inner grant_xy
ctrl_xy.policy do |acq|
hcase(acq)
hwhen(_b01) do
grant_xy <= 0
priority_xy <= ~priority_xy
end
hwhen(_b10) do
grant_xy <= 1
priority_xy <= ~priority_xy
end
hwhen(_b11) do
grant_xy <= priority_xy
priority_xy <= ~priority_xy
end
grant_xy
end
In this example:
-
acq
is a bit vector where bit 0 corresponds to sequencer 0, bit 1 to sequencer 1, etc. -
he policy toggles
priority_xy
after each access, thereby switching priority between sequencers.
Arbiters are especially useful when sequencers accessing the same resource do not overlap in time or do not need to synchronize with each other. However, when synchronization is required -- meaning a sequencer must wait until it has exclusive access before proceeding -- a monitor is more appropriate.
Monitors are instantiated in the same way as arbiters:
monitor(:<name>).(<list_of_shared_signals>)
Like arbiters, monitors manage shared signals and support the same write-access granting policies. However, unlike arbiters, monitors block the execution of a sequencer that requests access until the access is granted. This guarantees that a sequencer’s operations on shared signals are performed without interruption or interference from other sequencers.
Example
Let’s revisit the previous arbiter-based example. If we replace the arbiter with a monitor:
monitor(:ctrl_xy).(x,y)
Then the second sequencer will be paused until it is granted access to shared signals x
and y
. This ensures that all iterations of its loop are performed as intended, without being skipped or ignored.
Since monitors block execution, they implicitly insert a step
. To make this behavior explicit and clear, acquiring access to a monitor is done using the lock
method (instead of assigning 1), and releasing access is done using the unlock
method (instead of assigning 0).
Here is the rewritten version of the previous example using a monitor:
input :clk, :start
[8].shared x, y
monitor(:ctrl_xy).(x,y)
sequencer(clk.posedge,start) do
ctrl_xy.lock
x <= 0 ; y <= 0
5.stime do |i|
x <= x + 1
y <= y + 2
end
ctrl_xy.unlock
end
sequencer(clk.posedge,start) do
ctrl_xy.lock
x <= 2; y <= 1
10.stime do |i|
x <= x + 2
y <= y + 1
end
ctrl_xy.unlock
end
In this example:
-
Each sequencer waits to acquire exclusive access before proceeding.
-
The monitor guarantees mutual exclusion, ensuring no interleaved writes occur.
-
The
lock
andunlock
methods clearly define the critical section.
HDLRuby functions defined with hdef
can be used within sequencers like any other HDLRuby construct. However, just like process constructs such as hif
, the body of an hdef
function cannot include any sequencer-specific constructs.
To define functions that do support sequencer-specific constructs, use sdef
instead of hdef
. The syntax is:
sdef :<function_name> do |<arguments>|
# Sequencer code
end
Functions defined with sdef
can be declared anywhere in an HDLRuby description but can only be called from within a sequencer.
Recursion Support
Since sdef
is intended to support software-like control structures, it also supports recursion. For example, a recursive factorial function can be defined as follows:
sdef(:fact) do |n|
sif(n > 1) { sreturn(n*fact(n-1)) }
selse { sreturn(1) }
end
As shown above, the sreturn
construct is used to return a value from within the body of an sdef
function.
When recursion is used, HDLRuby automatically allocates a stack to store the return state and the function arguments. The stack size is heuristically determined based on the maximum bit width of the function arguments at the time of the recursive call.
For example, if the argument n
in the fact function is 16 bits, the stack will support up to 16 recursive calls.
If this heuristic is insufficient, you can manually set the stack size by providing a second argument to sdef
:
sdef(:fact,32) do |n|
sif(n > 1) { sreturn(n*fact(n-1)) }
selse { sreturn(1) }
end
Notes:
-
Each recursive function call takes one sequencer cycle, and each return takes two cycles.
-
Tail-call optimization is currently not supported.
-
If the number of recursive calls exceeds the available stack size (i.e., a stack overflow occurs), the current recursion is terminated, and the sequencer continues execution normally.
-
To handle stack overflows explicitly, you can attach a handler process using a proc block as a third argument to
sdef
:sdef(:<name>,<depth>, proc <block>) do <function code> end
Important: The overflow handler block cannot contain sequencer-specific constructs.
For example, the factorial function can be modified to set a
stack_overflow
signal in case of overflow:sdef(:fact,32, proc { stack_overflow <= 1 }) do |n| sif(n > 1) { sreturn(n*fact(n-1)) } selse { sreturn(1) } end
In the code above, the signal
stack_overflow
must be declared before calling the fact function.
Sequencers can be executed in software using a Ruby interpreter while maintaining functional equivalence with the hardware implementation. To achieve this, the following headers must be added to your Ruby source code:
require 'HDLRuby/std/sequencer_sw'
include RubyHDL::High
using RubyHDL::High
After this, signals and sequencers can be described exactly as in HDLRuby. However, unlike in hardware simulation, sequencer objects are not executed immediately -- they must be assigned to a variable for later execution.
For example, the following Ruby code defines a sequencer (referenced by the variable my_seq
) that increments the signal counter
up to 1000:
require 'HDLRuby/std/sequencer_sw'
include RubyHDL::High
using RubyHDL::High
[32].inner :counter
my_seq = sequencer do
counter <= 0
1000.stimes do
counter <= counter + 1
end
end
You may notice that no clock or start signal is provided to the sequencer. This is because, in software execution, everything runs sequentially -- no clock or control signals are needed. Instead, you start the sequencer by calling it directly using the function call syntax:
my_seq.()
To check whether the sequencer executed correctly, you can read signal values outside the sequencer using the value
method. For instance, the code below initializes counter
to 0, runs the sequencer, and then prints the final value:
require 'HDLRuby/std/sequencer_sw'
include RubyHDL::High
using RubyHDL::High
[32].inner :counter
counter.value = 0
my_seq = sequencer do
counter <= 0
1000.stimes do
counter <= counter + 1
end
end
my_seq.()
puts "counter=#{counter.value}"
Note: When printing the value of a signal, the value
method can be omitted, as signals are implicitly converted to their current value. For example, the last line above can also be written as:
puts "counter=#{counter}"
Internally, the HDLRuby code of a sequencer is translated to Ruby before execution. This generated Ruby code can be accessed using the source
method. You can save it to a file for standalone execution, as shown below:
File.open("sequencer_in_ruby.rb","w") do |f|
f << my_seq.source
end
You can also generate C or Python code from the sequencer using the to_c
and to_python
methods, respectively. The following commands create equivalent C and Python files from my_seq
:
File.open("sequencer_in_c.c","w" do |f|
f << my_seq.to_c
end
File.open("sequencer_in_python.py","w" do |f|
f << my_seq.to_python
end
Notes:
-
Currently, synchronization commands (presented in section Synchronizing Sequencers for Pseudo-Parallel Execution are not yet supported in the C and Python backends.
-
The Ruby code for sequencers is compatible with mruby, making it suitable for execution on embedded systems.
-
You can also generate experimental TensorFlow code using the
to_tf
method.
There are two main reasons for executing sequencers in software:
- High-speed simulation
Software-executed sequencers run approximately 10 times faster than those simulated using the HDLRuby simulator.
- Seamless transition from software to hardware
In early design stages, it is often unclear whether a given component will ultimately be implemented in software or hardware. Using the same code for both provides:
-
Reliability -- guaranteed functional equivalence between software and hardware.
-
Reduced design time -- no need to rewrite or duplicate code.
While software-based sequencers are functionally equivalent to their hardware counterparts, they differ fundamentally in how they handle time and parallelism:
-
In hardware, sequencers are implemented as finite state machines that respond to a clock and run in parallel with the rest of the circuit.
-
In software, sequencers are implemented as fibers that execute sequentially.
This distinction means that software sequencers may not be suitable for designs that rely heavily on timing or parallelism, such as communication protocols.
However, there are ways to introduce hardware-like timing and concurrency, which are described in the following sections.
As mentioned earlier, software execution does not involve a hardware clock. However, you can simulate a clock during the execution of a software sequencer to estimate its performance as if it were implemented in hardware.
This is done by passing a signal as an argument to the sequencer. That signal will be incremented at each simulated clock cycle:
sequencer(<clock_counting_signal>) do
...
end
After execution, the total number of estimated clock cycles is stored in the clock count signal. For example, the following code displays 1000 clocks
, which represents the number of cycles the sequencer would take if implemented in hardware:
[32].inner :clk_count
clk_count.value = 0
sequencer(clk_count) do
1000.stimes
end.()
puts "#{clk_count} clocks"
Note: In the example above, the sequencer is not stored in a variable because it is executed immediately upon definition.
In addition to a clock counter signal, you can pass a start signal to control when a software sequencer begins execution—just like in hardware implementations.
To do this, pass the start signal as the second argument to the sequencer
function. For example, in the code below, the sequencer begins executing when the start signal
is set to 1
:
[32].inner :clk_count
[1].inner :start
clk_count.value = 0
sequencer(clk_count,start) do
1000.stimes
end
start.value = 1
puts "#{clk_count} clocks"
In this mode, you don’t need to store the sequencer in a Ruby variable. Execution begins just like in hardware, and the sequencer can also be triggered from another sequencer.
Controlling One Sequencer from Another
The example below shows two sequencers, where the first sequencer controls the start of the second by setting the start1
signal to 1
:
[1].inner :start0, :start1
[8].inner :count0, :count1
sequencer(nil,start0) do
count0 <= 0
swhile(count0<100) { count0 <= count0 + 1 }
start1 <= 1
end
sequencer(nil,start1) do
count1 <= 0
swhile(count1<100) { count1 <= count1 + 1 }
end
In software, sequencers normally run to completion before any other code is executed. However, you can simulate parallel execution by using the sync
command. While sync
has no hardware equivalent, it can be used in software to pause and resume sequencers in a controlled, cooperative manner.
When a sync
command is encountered during execution:
-
The sequencer is paused.
-
Control is returned to the code following the sequencer's start.
-
The paused sequencer can later be resumed by either:
-
Calling it again using the call operator (
my_seq.()
), or -
Setting its associated start signal to
1
.
-
Example: Pausing and Resuming a Sequencer
In the following example, the sequencer runs until count
reaches 20, then pauses. After resuming, it continues up to 40:
[32].inner :count
my_seq = sequencer do
count <= 0
20.stimes
count <= count + 1
sync
20.stimes
count <= count + 1
end
end
my_seq.()
puts "stop at count=#{count}"
my_seq.()
puts "end at count=#{count}"
Cycle-Accurate Synchronization
To simulate cycle-accurate synchronization, you could insert a sync
call at each estimated clock cycle. However, this comes with a performance cost. Depending on the Ruby interpreter and system configuration, heavy use of sync
may cause software execution to become slower than the HDLRuby hardware simulator.
Recommendation: Use sync
only when necessary for modeling concurrency or interleaving. For cycle-accurate simulation, prefer using HDLRuby's hardware simulation mode.
Checking If a Sequencer Is Still Running
To check whether a sequencer is still active or paused (e.g., waiting at a sync
), use the alive?
method. For example, the following loop resumes the sequencer until it finishes:
my_seq.() while(my_seq.alive?)
When running a sequencer in software, HDLRuby provides an additional command called ruby
, which allows execution of plain Ruby code inside a sequencer block.
For example, the following code prints Hello
ten times using Ruby's puts
method:
sequencer do
stimes.10 do
ruby { puts "Hello" }
end
end.()
Alternatively, you can generate Ruby code dynamically using the text
or expression
commands:
-
text
inserts a Ruby statement. -
expression
inserts a Ruby expression.
Both functions format their arguments similarly to the C printf
function.
For example, the following code prints Hello 0
through Hello 9
when executed:
sequencer do
stimes.10 do |i|
text("puts \"Hello %d\"",i)
end
end.()
Choosing Between ruby, text, and expression
-
ruby
is safer, as errors are checked at compile time, but it is slower and incompatible with separate code generation (e.g., for C or Python). -
text
andexpression
allow faster execution and code export, but offer less safety, as errors are only detected at run time.
Accessing Signal Values in text and expression Generated Code
Since the string passed to text
and expression
is inserted as-is into the generated Ruby (or C) code, you cannot directly embed signal values into it. To include signal values safely and correctly, use:
-
to_ruby
,to_c
, orto_python
to get the raw value in the corresponding language. -
value_text
for a hardware-accurate representation (handling overflow/underflow).
sequencer do
text("puts #{sig0.to_ruby}")
text("puts #{sig1.value_text}")
end
HDLRuby supports hardware/software co-design through the program
construct. Since software sequencers are software components, they can be used within this construct when the selected language is Ruby.
To enable software sequencer functionality in Ruby, you must insert the following command at the beginning of the code block:
activate_sequencer_sw(binding)
Software sequencers can also be used with the C language, but in that case, the corresponding C code must be explicitly generated beforehand using the to_c
method.
Connecting Signals to Program Ports
When writing Ruby software within a program
, the signals used by the software sequencer can be automatically connected to the RTL-level ports by declaring them as:
-
inport
for input signals, and -
outport
for output signals.
The following example describes a software sequencer that copies the value from the input port inP
to the output port outP
. The signals sig0
and sig1
come from the surrounding RTL design.
program(:ruby) do
actport clk.posedge
inport inP: sig0
outport outP: sig1
code do
activate_sequencer_sw(binding)
input :inP
output :outP
sequencer do
outP <= inP
end
end
end
In this example:
-
actport
specifies that the Ruby code is triggered on the positive edge of the clock signal. -
The
input
andoutput
declarations inside the code block mirror the port names, making them accessible within the sequencer. -
activate_sequencer_sw(binding)
initializes the environment for using HDLRuby software sequencers.
This library provides a set of fixed-point data types for use in HDLRuby designs. These types can represent:
-
Bit (or unsigned) values.
-
Signed values.
They are declared using the following syntax:
bit[<integer_part_range>,<fractional_part_range>]
unsigned[<integer_part_range>,<fractional_part_range>]
signed[<integer_part_range>,<fractional_part_range>]
For example, the following code declares a signed fixed-point signal named sig
with 4 bits for the integer part and 4 bits for the fractional part:
bit[4,4].inner :sig
When performing arithmetic operations on fixed-point types, HDLRuby automatically adjusts the decimal point position to maintain correct precision in the result.
Converting Literals to Fixed-Point
A method is also provided to convert numeric literals (such as integers or floats) to fixed-point format:
<litteral>.to_fix(<number_of_bits_after_the_decimal_point>)
For example, the following code converts the floating-point number 3.178 to a fixed-point representation with 16 fractional bits:
3.178.to_fix(16)
Several samples HDLRuby descriptions are available in the following directory:
path/to/HDLRuby/lib/HDLRuby/hdr_samples
If you installed HDLRuby as a gem, you can find the installation path by running:
gem which HDLRuby
However, the recommended way to access the samples is to import them into your local directory using the following command:
hdrcc --get-samples
Naming Conventions for Sample Files
The samples follow a naming convention:
-
<name>.rb
:A standard sample, requiring no parameters.
-
<name>_gen.rb
:A sample that requires generic parameters for processing.
-
<name>_bench.rb
:A sample that includes a simulation benchmark. These are the only samples that can be simulated using the hdrcc -S command.
-
with_<name>.rb
:A sample that illustrates a specific feature of HDLRuby or one of its libraries. These usually include a benchmark.
While the HDLRuby framework does not yet support Verilog HDL files as direct input, a standalone tool is provided to convert Verilog files to HDLRuby. To perform this conversion, use the following command:
v2hdr <input_Verilog_HDL_file> <output_HDLRuby_file>
For example, assuming you have a Verilog HDL file named adder.v
that describes an adder circuit, you can convert it to HDLRuby using:
v2hdr adder.v adder.v.rb
Alternative: Loading Verilog HDL Directly from HDLRuby
Instead of manually converting a Verilog file, you can load it from a HDLRuby description using the require_verilog
command.
Assuming adder.v
contains the following Verilog code:
module adder(x,y,z);
input[7:0] x,y;
output[7:0] z;
assign z = x + y;
endmodule
You can load and instantiate this module in HDLRuby just like any other system:
require_verilog "adder.v"
system :my_IC do
[8].inner :a, :b, :c
adder(:my_adder).(a,b,c)
...
end
Notes:
-
Verilog HDL allows signal and module names to start with uppercase letters. In HDLRuby, however, identifiers starting with a capital letter are reserved for constants. To avoid naming conflicts, Verilog names beginning with a capital letter are prefixed with an underscore (
_
) when imported into HDLRuby.For example, if the Verilog module were named
ADDER
, it would be imported as_ADDER
in HDLRuby, and instantiated like this:_ADDER(:my_add).(a,b,c)
-
In the current version of HDLRuby, Verilog HDL files are converted to HDLRuby using the v2hdr tool before being loaded with
require_verilog
.
Bug reports and pull requests are welcome on GitHub at https://github.com/civol/HDLRuby.
- Find and fix the (maybe) terrifying number of bugs.
The gem is available as open-source under the terms of the MIT License.