Eventually, a large collection of other gate generation subroutines were added to this TechMapper. The result was that, for the modules it knew how to generate, the TechMapper would create a highly optimized circuit. This proved especially important for users who wanted to take advantage of specialized functions such fast carry logic or wide gating functions. Other TechMapper classes for new technologies were then added as those technologies appeared. As of the time of the writing of this document (May 2001) TechMappers exist for the Xilinx XC4000 family, the Xilinx Virtex family, and a Virtex-II TechMapper is under construction.
All TechMappers implement the same set of circuits: gates, muxes, flip flops, adders, and wires. To abstract the details of TechMappers away for the user, An API of function calls was created (the Logic class API) to provide a uniform way to access TechMappers.. This is illustrated here: .
Thus, a designer would call the and() function in the Logic API and get an AND gate specifically optimized for the technology being used. The net result is that the Logic API provides a technology-independent way for a user to create simple logic function and the TechMapper creates technology-specific implementations of those functions. The user gets the best of both worlds: technology-independent design and highly optimized logic circuits.
Currently the Virtex TechMapper is the default TechMapper. The Users Manual sections on running the JHDL simulation and netlister tools contain information on how to over-ride this to get a different TechMapper.
Structurally, class Logic extends class LogicGates. Thus, as a designer you need only have your designs extend the Logic class and you will get the functionality of the entire Logic API. However, when you go to look at the Logic API in the API docs you need to note that the routines are spread across both these classes and possibly others.
The Logic API is extremely large to allow the user the maximum flexibility possible in constructing a circuit. Before delving into all the details of the API, it will help you to understand a few basic rules and conventions that the Logic API follows. Understanding these basic rules will help you avoid referring to the documentation to find every function.
Wire cout = or( and(a,b), and(a,ci), and(b,ci) );This is because the and() methods first create a wire, they then create a 2-input and gate with a new wire attached to the output, and then they return the new wire. This nested form of gate instantiation can make your structural design very clean and readable; in fact your structural code almost begins to look behavioral.
Every basic Logic function has at least two versions: one which creates a new wire as the output of the gate, and one which accepts an already-created output wire as a parameter. By convention, the latter case always appends an _o suffix to the method name to indicate that the output is supplied by the user. To illustrate, this creates new output wires for each of the gates instantiated:
Wire cout = or( and(a,b), and(a,ci), and(b,ci) );while this implements precisely the same carry-out function but with user-supplied wires as the outputs to all gates:
or_o( and_o(a, b, t1), and_o(a, ci, t2), and_o(b, ci, t3), cout );Note that the _o methods still return the output wire to allow for nesting (i.e. t1, t2, and t3 will be the return values of the and_o() calls, and cout will be the return value of the or_o() call in the example above).
Finally, note that some methods (such as add()/sub()/addsub()) build circuits with multiple output wires. In the cases of adders and subtractors the sum wire is the return value of the method. In all cases consult the Logic API documentation to verify what is returned.
or_o( and(a,b), and(c,d), q);The two and() calls will each build an 8-bit wide AND gate. The outputs of these two gates will then be fed into an 8-bit OR gate with q being the output wire of the OR gate. Generally, all Logic methods will do similar things with arbitrary-width wires, allowing the JHDL designer to rapidly build up parallel computational structures, but without having to explicitly request the width of the block. All width adaptation is automatic, based on the width of the wire parameters.
An interesting variation on this is this:
Wire allOnes = and(a);Given a single wire as a parameter, the and() function will AND all its bits together into a single output wire. Thus, if a were 4-bits wide, the signal allOnes could be used to determine when the value of a were 15 in decimal. Similar single-parameter functions exist for other logic functions.
As a final example combining both styles, consider the problem of determining whether two busses contain the same value:
Wire equals = not(or(xor(a, b)));Working from the inside out, the xor() does a bit-by-bit comparison of the busses. The or() combines all those bits together into a single bit, a 1 signifying the busses were different and a 0 signifying they were the same. The final not() inverts that signal to use as an equality flag. The advantages of this way of coding are that (a) this statement works for any two signals of equal width and (b) the TechMapper which ultimately gets called to implement this function will use any wide gating functions that may be available in the technology being targeted.
Wire cout = or( and(a,b), and(a,ci), and(b,ci), "carry_cell" );will give the or gate the instance name "carry_cell". Also, the returned wire will be named "carry_cell_o".
Wire w = wire(1, "wireName");However, if your module doesn't extend Logic you can still build wires this way:
Wire w = Logic.wire(this, 1, "wireName");Note that you must specify the Logic class name as shown and you must also pass in the current cell as the parent.
some_method( Cell parent, /* For static methods only */ Wire input0, ... Wire inputN, Wire output0, ... Wire outputN, int any_integer_arguments, String name);
Wire a = wire(3); Wire b = wire(4, "fred");creates a new 3-bit wire with a default name, and a new 4-bit wire with name "fred". As mentioned above, each of these methods has a static counterpart that accepts the parent Cell as the first parameter. This allows new wires to be instantiated in an architecture-independent manner, outside of the Logic class (most useful in test-benches, etc.). To reiterate, this looks like this:
Wire a = Logic.wire(this, 4);In addition to creating new wires, the Logic class will help you to manipulate wires in special ways:
Wire concatenation = Logic.concat(this, a, b, c, d, e, f);Also, as mentioned previously, the same thing can usually be done using the constructors provided in the primitive libraries (the XWire constructor takes multiple wires and constructs a concatenation of them):
Wire concatention = new Xwire(this, a, b, c, d, e, f);
// Create an 8-bit constant wire which contains the value "00001111" Wire fifteen = constant(8, 15, "fifteen");
As with the wire() methods above, static versions also exist which accept the parent Cell as a parameter. Using these methods, the JHDL designer can instantiate constant wires in a platform-independent manner from outside the Logic class like this:
Wire constant5 = Logic.constant(this, 8, 5);
Also, "_o" versions of constant() exist so that the user can drive a pre-created wire with the given constant value:
Wire out1 = wire(16, "out1"); Wire out2 = wire(8, "out2"); constant_o(out1, 5); constant_o(out1, 7);
Finally, the user can explicitly insert a 1 bit "power" or "ground" symbol with the vcc() and gnd() methods:
// Initialize a "n"-bit wire to all 1's constant_o(w, (1 << w.getWidth())-1); // Initialize a "n"-bit wire to all 0's constant_o(w, 0);
and( a, b, c, d );
creates a 4-input and gate while
and( a, b );
creates a 2-input and gate. Boolean gate methods with up to four inputs will accept arbitrary-width wires; methods with five or more inputs only accept 1-bit wires. Thus, the following will create 6 4-input OR gates, one for each bit of a, b, c, and d:
Wire a = wire(6), b = wire(6), c = wire(6), d = wire(6); Wire q = or(a, b, c, d);
Also, each boolean gate type has a special 1-input-parameter version. For example,
Wire a = wire(4); Wire f = and(a);
will build the function a & a & a & a. Thus by concatenating bits into a single wire, the JHDL designer can get a boolean function of any width. By contrast, if we do the following:
Wire a = wire(4); Wire b = wire(4); Wire f = and(a,b);
will build four parallel bitwise functions: f = a & b; f = a & b; f = a & b; f = a & b.
The API also implements the boolean "not" function (i.e. an inverter) with the following:
not(in); not_o(in, out);
add(a, b); add(a, b, ci); add_o(a, b, s); add_o(a, b, ci, s); add_o(a, b, ci, s, co);The first two create a new wire for the output, build the adder, and return the output wire. The last three take the output wire as a parameter. Notice that the only way to get access to the carry-out signal is to use the last method shown, explicitly providing the carry-out wire yourself. Each of these is further overloaded to accept an optional String as the instance name of the adder.
As with all Logic calls, the exact implementation of adders is technology-specific, and is determined by the TechMapper class for the corresponding technology. The user should generally expect the layout of the adder to be done usiing the default style of the architecture. For example in the Xilinx XC4000 architecture, this call will result in an adder with 2-bits-per-CLB and in a south-to-north column layout with the LSB at the bottom. If more control of the adder layout is desired, the user will need to create his own technology-specific adder out of library primitives.
The API for creating subtractors is as follows:
sub(a, b); sub(a, b, ci); sub_o(a, b, s); sub_o(a, b, ci, s); sub_o(a, b, ci, s, co);The API for creating adder/subtractors is as follows:
addsub(a, b, add); addsub(a, b, ci, add); addsub_o(a, b, add, s); addsub_o(a, b, ci, add, s); addsub_o(a, b, ci, add, s, co);The add signal, when high, tells the circuit to add and when low tells the circuit to subtract.
The mux call can implement a 2:1, 4:1, or 8:1 mux as follows:
mux(d0, d1, sel); mux_o(d0, d1, sel, q); mux(d0, d1, d2, d3, sel); mux_o(d0, d1, d2, d3, sel, q); mux(d0, d1, d2, d3, d4, d5, d6, d7, sel); mux_o(d0, d1, d2, d3, d4, d5, d6, d7, sel, q);The sel input determines which input is selected. For example, with the 2:1 mux, if sel == 1, then d1 is selected. With the 8:1 mux, if sel == 110, then d6 is selected.
The buf() call instantiates a wire connection buffer as follows:
buf(in); buf_o(in, out);
In the Xilinx technology at least, such buffers perform no logic on the signals and are thus usually optimized away by the backend tools. However, they perform a value function in JHDL by allowing one to rename a wire by passing it through a buffer.
Again, all methods optionally accept a String instance name parameter, and self-adapt to the width of the wires passed in.
Read up on this next in Level 3 - The Logic.Modules Package.
|| JHDL Home Page | Configurable Computing Lab | Prev (Primitives) | Next (Logic Modules) ||
Copyright (c) 1998-2003 Brigham Young University. All rights reserved.
Last updated on 11 May 2006