RHDL (Ruby Hardware Description Language) is an HDL based on the Ruby programming language. My idea in developing RHDL was to build an HDL on an object oriented programming language to allow HDL features ( concurrent processes, signals, parallelism etc.) in addition to features which come with a modern, object oriented, agile programming language like Ruby (www.ruby-lang.org). The intent is to allow more than just simulation, but also verification and testbench creation features. Ultimately, I would like to be able to translate RHDL to VHDL and/or Verilog. The fact that RHDL is based on Ruby allows modeling at a higher level of abstraction than is possible with VHDL or Verilog.
RHDL users don't need to know much Ruby - this is intentional, I didn't want potential users to have to know Ruby in order to benefit from RHDL. However, RHDL becomes a more powerful tool if the user takes a little time to learn some Ruby (an excellent tutorial and reference book on Ruby is: "Programming Ruby: The Pragmatic Programmer's Guide" by Thomas and Hunt.)
RHDL is not stricly speaking a new language. It is a set of modules (code libraries) that allow Ruby to look like an HDL. Ruby has a concept called code blocks; these blocks are made into lexically scoped closures in order to define a domain specific language like RHDL without having to write a seperate parser.
Comments start with a '#'. The rest of the line following is a comment.
delimited by ''
example '100101'
In most cases string literals are automatically converted to Bit or BitVector values when it makes sense (see below).
A Bit type can take on the following values: 1,0,X,Z
creating a Bit object: b = Bit('0') or b=Bit(0)
NOTE: the Bit constructor can take either a string literal or an integer value of 1 or 0.
OR '+,|': a + b -> 1, a | b -> 1 (NOTE: '->' means 'results in', it is not an operator)
AND '*,&': a * b -> 0, a & b -> 0
XOR '^': a ^ b -> 1
Inversion '~': ~a -> 0 (Or alternatively: a.inv )
A BitVector is an array of values of type Bit with some extra functionality for math.
creating a BitVector object: bv = BitVector('1001',4) or bv = BitVector(9,4) (the two are equivalent). Note that the second argument to the BitVector constructor indicates the number of bits in the vector.
Logical operators (|,&,^,~)
Math operators (*(multiplication), /(division), +(addition), -(subtraction))
Note: Math operators on BitVectors are (mostly) polymorphic unlike in VHDL. For example, the following are equivilent (given that bv=BitVector('1001',4)): bv=bv+2 and bv=bv+'10'.
concatenation: bv3 = bv1.cat bv2
An enumerated type for representing a collection of states (for use in creating state machines)
Example: washStates = EnumType(:start, :wash, :rinse, :spin, :stop)
See state machine example for an example of using EnumType.
inspect and value methods return current state of washStates.
first state in enumeration automatically becomes the 'reset' state (ie. in the example above the initial state is :start)
NOTE: the ':' prefixed to a string a characters (as with :start) creates what is known in Ruby as a Symbol - basically it is an immutable string.
While Bit and BitVector are special to RHDL, users can also use other types native to Ruby: Integer, String (ie. 'This is a string'), Range (ie. 0..5), Symbol (ie. :start) and others (TODO: add more types here). In addition, users can define their own types using Ruby's 'class' definitions.
Signals in RHDL are very similar to signals in VHDL. Signals have a history (and more importantly, a future) and can contain values of various types.
Example to create a Signal with a Bit type: s=Signal(Bit('0')) (NOTE: this is roughly equivalent to the VHDL statement: signal s:BIT := '0';)
Example of creating a Signal with a BitVector type: s=Signal(BitVector('1001',4)) (NOTE: this is roughly equivalent to the VHDL statement: signal s:BIT_VECTOR(0 to 3):='1001'; )
It is essential to understand that '=' is not to be used for signal assignment. The reason being that Ruby uses '=' for assigning object references to variables. Use either 'assign' or '<<' to assign new values to signals.
bv.assign (bv + 1)
bv <= bv + 1 #(equivalent to last assignment)
bv <= '0110' #assign a constant bit string
You can specify that values be assigned to signals after a specified amount of ttime by usign the assign_at method:
bv.assign_at(5) { bv + 1 }
which means that the value bv+1 will be assigned to bv five
timesteps after the current one.
Foo = model {
generics width => 8, length => 8
inputs a, b
outputs c
define_behavior {
#behavioral stuff
#width and length can be used in this scope
}
}
Note the use of '=>'not '='
in the generic declaration. The generic declaration in this case
is specifying two generics width
and length both with
default values of 8. NandGate = model {
inputs aa, bb
outputs a_nand_b
init {
sig_type = aa.type
a_and_b = Signal(sig_type.new) #more generic
andg = AndGate.new(:a=>aa,:b=>bb,:out=>a_and_b,:my_generic=>22)
Inverter.new(:a=>a_and_b, :not_a=>a_nand_b)
}
}
In this example we've built a nand gate
out of an Inverter and an AndGate. [Note the trick used to create the a_and_b Signal;
it causes the Signal to be of the same type as the aa input thus
making the model more generic (see the design reuse section below for
more ways to improve reusabilty).]include RHDL
Latch = model {
inputs g,rst,d
outputs q
init {
reset_val = 0
define_behavior {
process(g,rst,d){
behavior {
if rst == '1'
q <= reset_val
elsif g == '1'
q <= d
end
}
}
}
}
}
define_behavior takes a block of code (surrounded by '{}') in which all behavioral code for the design is placed. (NOTE: a completely structural design doesn't need to have a define_behavior declaration, but it must have an init block).
It is essential that the '{' be on the same line as 'define_behavior'. The reason for this is that define_behavior is a method and the block of code surrounded by '{}' is an argument to the define_behavior method, which means the following would be illegal:
define_behavior
{
#this will result in an error!
}
Very similar to the 'process' statement in VHDL or the 'always' statement in Verilog. The process statement can take a list of sensitive signals. The block following the process declaration is executed when any of the signals in the sensitivity list changes. Example:
process(clk){
puts "clk changed: #{clk}"
}
will print "clk changed: 1|0" whenever the clk signal changes. NOTE: process statements should only be used within define_behavior blocks.
Again, as with the define_behavior declaration, the '{' must appear on the same line as the 'process'.
Used to suspend a process until some condition is met. Example:
process() { wait { $simTime == 1000 } #other stuff }
In this example the process will be initiated, but it will be suspended until $simTime is equal to 1000. NOTE: $simTime is a globally available variable that contains the current simulation time (or the number of steps that have been run so far).
NOTE: You can use wait statements in a process with a sensitivity list, but the results may not be what you expect. It is probably best to use wait statements only in a process without a sensitivity list but this is not enforced by RHDL as it is in VHDL.
Used to suspend a process for a time. Example:
clock=Signal(Bit(0)) define_behavior { process() { wait_for 4 clock <= clock.inv wait_for 3 clock <= clock.inv } }
This example creates a repeating clock signal which starts out low and remains low for four time steps and then goes high and remains high for three time steps and repeats.
A model in RHDL is similar to an entity/architecture pair in VHDL. An RHDL model contains either a structural or behavioral design description. RHDL models are classes in Ruby – in other words they define a template for defining objects. RHDL models can be included in other RHDL models thus implementing hierarchy.
Here's an example of a simple RHDL design that defines a simple OR gate:
include RHDL MyOr = model {
inputs a, b
outputs a_or_b
define_behavior { a_or_b <= (a | b) } }
The name of this model (or class) is My_Or. The first line 'include RHDL' simply makes the various RHDL elements available to your design. The next two lines are where the inputs and outputs of the model are declared. It is called whenever you instantiate a My_Or object (such as: My_Or.new(aa,bb,output) ). After this the behavior of the design is described in a define_behavior block; in this case a is Or'ed with b.
Now to use the My_Or class/design that you've defined:
include RHDL
aa = Signal(Bit('0'))
bb = Signal(Bit('0'))
output = Signal(Bit()) #initialized to 'X'
myOr = MyOr.new(:a=>aa,:b=>bb,:a_or_b=>output)
The include RHDL tells Ruby to allow calls to functions (methods) defined in the RHDL module to be called in the current scope. We then set up two input signals (aa and bb) and an output signal ( output), instantiate a My_Or object and pass in the signals by named association.
Simulation in RHDL is done by including the Simulator module and calling the step method which is defined in that module (NOTE: this is subject to change, I'll probably be changing the name of this to the Simulator module). Here's how the design is simulated:
include Simulator
step { puts "aa=#{aa}, bb=#{bb}, output=#{output}"}
aa <= '1'
step
bb <= '1'
step
aa <= '0'
step
The output from running this is:
step #0: aa=0, bb=0, output=X step #1: aa=0, bb=0, output=0 step #2: aa=1, bb=0, output=1 step #3: aa=1, bb=1, output=1 step #4: aa=0, bb=1, output=1
The step method in the Simulator module is used to step the simulation to the next time interval. Notice that step can take an optional block of code which is executed at each time step. You do not need to specify this block of code for each step as it will be 'remembered' until a new block is given. In this example the first step takes a block which prints out the values of aa, bb and output. The step method takes care of printing the 'step #x:' in front of each line of output (NOTE: the line which starts with 'step #0:' is the output at $simTime==0 prior to any simulation steps, it represents the initial values of the signals specified.)
By now you've noticed that there is no type specification on the signals passed into/out of the My_Or design. This is primarily because Ruby is what is known as a dynamically typed language as opposed to a statically typed language like C or Java (or VHDL or Verilog) where the types of all variables must be declared prior to their use in the program. The main advantage in this case is that you can reuse your My_Or design with different types of signals, for example, if you want to OR four-bit BitVectors:
include RHDL
aa = Signal(BitVector('0000',4))
bb = Signal(Bit('0000',4))
output = Signal(BitVector('XXXX',4))
myOr = MyOr.new(aa,bb,output)
As long as the underlying types of the signals being passed in support an OR operator ( | ) it just works. No need to rewrite your My_Or design to accommodate different types. This greatly improves the chances that you'll be able to reuse your design code.
...More to come... for now do check out the examples.