Getting Started

Installation

Before installing, make sure your Python version is 3.5 or greater. Older versions may work properly but are unsupported. To check your Python version, use

$ python --version

in a terminal or command prompt.

If this requirement is not met, you can download the latest version of Python here.

Once Python is installed, you can install Bitwise using

$ pip install bitwise

If there are no errors, you can open a Python session and verify the installation by checking the version number:

In [1]: import bitwise as bw

In [2]: bw.__version__
Out[2]: '0.3'

Refer to the changelog for version details. In this documentation, it is canonical to have imported Bitwise as bw.

Basics

The use case of Bitwise is to model logic circuits in software; thus, it is useful to keep in mind that when you are using this library, you are actually building a circuit rather than writing a conventional software program. To facilitate this, there are a plethora of new features that are introduced.

Wires

By far the most important class defined by this library is Wire. As the name suggests, Wire is used to connect different logic elements together, just like a real physical wire. They can be instantiated like so:

In [1]: my_wire = bw.wire.Wire()

In [2]: my_other_wire = bw.wire.Wire()

This creates two objects of type Wire. Objects of this class have a single attribute, value, that can take on 1 of two binary values (0 or 1). The default value is 0, and it can be both accessed and mutated using Wire.value.

In [3]: my_wire.value
Out[3]: 0

In [4]: my_other_wire.value
Out[4]: 0

In [5]: my_wire.value = 1

In [6]: my_wire.value
Out[6]: 1

In [7]: my_wire.value = 0

In [8]: my_wire.value
Out[8]: 0

Other values will throw a ValueError.

Buses are used to group together sets of 4, 8, and 16 wires, as well as 7 wires, meant for use with the SevenSegmentConverter modules. They are implemented using the Bus4, Bus8, Bus16, and BusSevenSegmentDisplay classes, respectively, and can be instantiated by specifying the appropriate number of Wire objects

In [1]: my_bus = bw.wire.Bus4(my_wire_1, my_wire_2, my_wire_3, my_wire_4)

or with no arguments, which will automatically create wires with default value 0.

In [1]: my_bus = bw.wire.Bus4()

After instantiation, the Wire objects can be accessed using Bus.wires, and the wire values can be both accessed and mutated using Bus.wire_values.

Logic Elements

Alongside Wire and Bus, various logic elements are also at your disposal. These range from primitive logic gates to larger-scale logic circuits such as multiplexers and counters. In order to create new logic elements, usually a few Wire objects must be created first in order to make the necessary connections with the rest of your circuit. For example, the class that implements a two-input AND gate, ANDGate2, takes three arguments in its __init__ method, all of type Wire:

In [1]: input_1 = bw.wire.Wire()

In [2]: input_2 = bw.wire.Wire()

In [3]: output = bw.wire.Wire()

In [4]: my_AND_gate = bw.gate.ANDGate2(input_1, input_2, output)

Here, the value of output will take on the result of AND-ing the values of input_1 and input_2. Note that all the arguments must be present and valid for proper instantiation.

Logic elements that require buses as arguments may then be instantiated. For example, the 4-bit parity generator, ParityGenerator4, receives two arguments: an object of type Bus4 and an object of type Wire. It can be instantiated like so:

In [1]: my_bus = bw.wire.Bus4()

In [2]: output = bw.wire.Wire()

In [3]: my_parity_generator = bw.logic.ParityGenerator4(my_bus, output)

For a catalog of all logic elements available, refer to the API documentation.

Sensitivity

The concept of sensitivity is another key feature of this library. In a hardware circuit, when an input changes, the output changes immediately. In Bitwise, this type of behavior is accomplished by retaining a list of all the connections that an object of type Wire has, but it is akin to a sensitivity list in a true hardware description language like Verilog. To see sensitivity in action, consider again the ANDGate2 example from the previous subsection:

In [1]: input_1 = bw.wire.Wire()

In [2]: input_2 = bw.wire.Wire()

In [3]: output = bw.wire.Wire()

In [4]: my_AND_gate = bw.gate.ANDGate2(input_1, input_2, output)

We can examine the value of output for every combination of values for input_1 and input_2. Recall that the default value of a Wire object is 0.

In [5]: output.value
Out[5]: 0

In [6]: input_1.value = 1

In [7]: output.value
Out[7]: 0

In [8]: input_1.value = 0

In [9]: input_2.value = 1

In [10]: output.value
Out[10]: 0

In [11]: input_1.value = 1

In [12]: output.value
Out[12]: 1

In [13]: input_1.value = 0

In [14]: input_2.value = 0

In [15]: output.value
Out[15]: 0

Notice that output.value reacts immediately to changes in the values of input_1 and input_2, mimicking the behavior of a hardware circuit. (Moreover, we’ve verified that the two-input AND gate works as intended.)

Hierarchy

In order to build higher-level logic circuits, the concept of hierarchy must be introduced. Quite simply, logic elements can have instances of other logic elements, which in turn can have instances of yet other logic elements. The result is a hierarchical design pattern, with the primitive logic gates at the bottom (since they do not instantiate any other elements). For example, consider the following Python script, which defines a logic element with three inputs and one output. The value of output is the result of AND’ing the first two inputs and OR’ing the result with the third input.

import bitwise as bw

class MyLogicElement:
    def __init__(self, input_1, input_2, input_3, output):
        wire_1 = bw.wire.Wire()  # used as the output of the AND gate
        bw.gate.ANDGate2(input_1, input_2, wire_1)
        bw.gate.ORGate2(wire_1, input_3, output)

Notice, first and foremost, that the class has only one method, __init__, which only takes in arguments of type Wire (and bus types) and whose purpose is simply to make the necessary wire connections with the rest of the logic circuit. Notice also that the logic element itself instantiates an object of type Wire, wire_1. This is necessary in order to internally connect the output of the two-input AND gate to one of the inputs of the OR gate. Lastly, notice that both the inputs and the output of the logic element are given as arguments to the __init__ method. Again, this is necessary so that both the inputs and the output are connected in some way to the rest of the logic circuit.

Objects of MyLogicElement can now be instantiated:

In [1]: input_1 = bw.wire.Wire()

In [2]: input_2 = bw.wire.Wire()

In [3]: input_3 = bw.wire.Wire()

In [4]: output = bw.wire.Wire()

In [5]: my_logic_element = MyLogicElement(input_1, input_2, input_3, output)

We can test various values for input_1, input_2, and input_3 to verify that the circuit works as intended:

In [6]: output.value
Out[6]: 0

In [7]: input_1.value = 1

In [8]: output.value
Out[8]: 0

In [9]: input_2.value = 1

In [10]: output.value
Out[10]: 1

In [11]: input_1.value = 0

In [12]: input_2.value = 0

In [13]: output.value
Out[13]: 0

In [14]: input_3.value = 1

In [15]: output.value
Out[15]: 1

Additionally, objects of MyLogicElement can now be instantiated in other logic elements and circuits.

Example

As a short example, let us construct a 2-bit adder. An adder simply takes two inputs, of a certain width, and outputs the sum. In this case, since we have two inputs of width 2, four inputs to the adder are needed. Additionally, three outputs are needed, since the sum of two 2-bit numbers can be at most 3 bits wide.

Before a full 2-bit adder can be constructed, we first need a 1-bit adder. This adder must have not two, but three inputs, since we need one input to be a “carry-in” input from the previous 1-bit adder. Two outputs are also needed. Skipping a few details, the following script defines a class that simulates our 1-bit adder:

import bitwise as bw

class OneBitAdder:
    def __init__(self, carry_in, input_1, input_2, sum_1, sum_2):
        # these wires connect the appropriate gates together (trust me, it works)
        wire_1 = bw.wireWire()
        wire_2 = bw.wire.Wire()
        wire_3 = bw.wire.Wire()

        bw.gate.XORGate2(input_1, input_2, wire_1)
        bw.gate.XORGate2(carry_in, wire_1, sum_2)
        bw.gate.ANDGate2(input_1, input_2, wire_2)
        bw.gate.ANDGate2(carry_in, wire_1, wire_3)
        bw.gate.ORGate2(wire_2, wire_3, sum_1)

Here, sum_1 and sum_2 are the most and least significant bits of the sum, respectively. It can be verified that this element behaves as intended:

In [1]: carry_in = bw.wire.Wire()

In [2]: input_1 = bw.wire.Wire()

In [3]: input_2 = bw.wire.Wire()

In [4]: sum_1 = bw.wire.Wire()

In [5]: sum_2 = bw.wire.Wire()

In [6]: myOneBitAdder = OneBitAdder(carry_in, input_1, input_2, sum_1, sum_2)

In [7]: sum_1.value
Out[7]: 0

In [8]: sum_2.value
Out[8]: 0

In [9]: input_1.value = 1

In [10]: sum_1.value
Out[10]: 0

In [11]: sum_2.value
Out[11]: 1

In [12]: input_2.value = 1

In [13]: sum_1.value
Out[13]: 1

In [14]: sum_2.value
Out[14]: 0

In [15]: carry_in.value = 1

In [16]: sum_1.value
Out[16]: 1

In [17]: sum_2.value
Out[17]: 1

A 2-bit adder may now be constructed by creating two instances of OneBitAdder in a hierarchical design pattern and connecting the wires appropriately:

class TwoBitAdder:
    def __init__(self, input_1_a, input_1_b, input_2_a, input_2_b, sum_1, sum_2, sum_3):
        wire_1 = bw.wire.Wire()  # used to connect the two 1-bit adders
        gnd = bw.wire.Wire()
        gnd.value = 0

        OneBitAdder(gnd, input_1_b, input_2_b, wire_1, sum_3)
        OneBitAdder(wire_1, input_1_a, input_2_a, sum_1, sum_2)

Here, input_1_a and input_1_b are the most and least significant bits of the first input, respectively, input_2_a and input_2_b are the most and least significant bits of the second input, respectively, and sum_1 and sum_3 are the most and least significant bits of the sum, respectively. Since the least significant adder has no carry-in, the gnd wire is used for the carry_in input. The wire_1 wire is used to connect the most significant bit of the sum from the least significant adder with the carry-in of the most significant adder.

Again, it can be verified that this element behaves as intended by trying out a few test cases:

In [1]: i_1_a = bw.wire.Wire()

In [2]: i_1_b = bw.wire.Wire()

In [3]: i_2_a = bw.wire.Wire()

In [4]: i_2_b = bw.wire.Wire()

In [5]: sum_1 = bw.wire.Wire()

In [6]: sum_2 = bw.wire.Wire()

In [7]: sum_3 = bw.wire.Wire()

In [8]: myTwoBitAdder = TwoBitAdder(i_1_a, i_1_b, i_2_a, i_2_b, sum_1, sum_2, sum_3)

In [9]: sum_1.value
Out[9]: 0

In [10]: sum_2.value
Out[10]: 0

In [11]: sum_3.value
Out[11]: 0

In [12]: i_1_b.value = 1

In [13]: i_2_b.value = 1

In [14]: sum_1.value
Out[14]: 0

In [15]: sum_2.value
Out[15]: 1

In [16]: sum_3.value
Out[16]: 0

In [17]: i_1_a.value = 1

In [18]: sum_1.value
Out[18]: 1

In [19]: sum_2.value
Out[19]: 0

In [20]: sum_3.value
Out[20]: 0

In [21]: i_2_a.value = 1

In [22]: sum_1.value
Out[22]: 1

In [23]: sum_2.value
Out[23]: 1

In [24]: sum_3.value
Out[24]: 0

All of the sums are as expected.

Issues

Please post all bugs, issues, and feature requests in the issues section of the Github repository.