Skip to content

rhedshi/hdl-js

 
 

Repository files navigation

hdl-js

Build Status npm version npm downloads

Hardware description language (HDL) parser, and Hardware simulator.

Table of Contents

Installation

The parser can be installed as an npm module:

npm install -g hdl-js

hdl-js --help

Development

  1. Fork https://github.com/DmitrySoshnikov/hdl-js repo
  2. Make your changes
  3. Make sure npm test still passes (add new tests if needed)
  4. Submit a PR

For development from the github repository, run build command to generate the parser module, and transpile JS code:

git clone https://github.com/<your-github-account>/hdl-js.git
cd hdl-js
npm install
npm run build

./bin/hdl-js --help

NOTE: JS code transpilation is used to support older versions of Node. For faster development cycle you can use npm run watch command, which continuously transpiles JS code.

Usage as a CLI

Check the options available from CLI:

hdl-js --help
Usage: hdl-js [options]

Options:
  --help, -h          Show help                                        [boolean]
  --version, -v       Show version number                              [boolean]
  --gate, -g          Name of a built-in gate or path to an HDL file
  --parse, -p         Parse the HDL file, and print AST
  --list, -l          List supported built-in gates
  --describe, -d      Prints gate's specification
  --exec-on-data, -e  Evaluates gate's logic on passed data; validates outputs
                      if passed
  --format, -f        Values format (binary, hexadecimal, decimal)
                                                  [choices: "bin", "hex", "dec"]
  --run, -r           Runs sequentially the rows from --exec-on-data table
  --clock-rate, -c    Rate (number of cycles per second) for the System clock

NOTE: the implementation of some built-in chips, and the HDL format is heavily inspired by the wonderful nand2tetris course by Noam Nisan and Shimon Schocken.

Example of a CLI command to describe Xor gate:

hdl-js --gate Xor --describe

"Xor" gate:

Description:

  Implements bitwise 1-bit Xor ^ operation.

...

Usage from Node

The tool can also be used as a Node module:

const hdl = require('hdl-js');

// Check the API:
console.log(hdl);

The hdl-js exposes the following API:

  • parse(hdl: string) -- parses an HDL code; convenient facade method for parser.parse
  • parseFile(fileName: string) -- parses an HDL file; facade for parser.parseFile
  • fromHDLFile(fileName: string) -- loads a gate class defined in an HDL file; facade for HDLClassFactory.fromHDLFile
  • fromHDL(hdl: string) -- creates a gate class accoding to passed HDL spec; facade for HDLClassFactory.fromHDL
  • parser -- the parser module exposed
  • emulator -- hardware emulator, which includes:
    • Pin - a pin "wire" used to patch inputs and outputs of a gate
    • BuiltInGate -- base class for all built-in gates
    • CompositeGate -- base class used for user-defined gates from HDL; see Composite gates section
    • HDLClassFactory -- class loader for gates defined in HDL
    • Clock -- class to manage clocked gates. Contains:
      • SystemClock -- main System clock used to synchronize all gated chips
    • BuiltInGates` -- map of all built-in gates:
      • And
      • Or
      • ...

Parser

The hdl-js is implemented as an automatic LR parser using Syntax tool. The parser module is generated from the corresponding grammar file.

Format of an HDL file

A hardware chip is described via the CHIP declaration, followed by a chip name, and a set of sections:

CHIP <chip-name> {
  <section>
  <section>
  ...
}

The sections include:

  • IN -- inputs of a gate
  • OUT -- outputs of a gate
  • PARTS -- the actual implementation body of a chip, composed from other chips
  • BUILTIN -- refer to a name of a built-in chip: in this case the implementation is fully take from the built-in gate, and the PARTS section can be omitted
  • CLOCKED -- describes which inputs/outputs are clocked

Let's take a look at the examples/And.hdl file:

/**
 * And gate:
 * out = 1 if (a == 1 and b == 1)
 *       0 otherwise
 */

CHIP And {
  IN a, b;
  OUT out;

  PARTS:

  Nand(a=a, b=b, out=n);
  Nand(a=n, b=n, out=out);
}

Once we have an HDL file, we can feed it to the parser, and get its AST.

Parsing a file to AST

The parser can be used from CLI, and from Node.

Taking the examples/And.hdl file from above, and running the:

./bin/hdl-js --gate examples/And.hdl --parse

We get the following AST (abstract syntax tree):

{
  type: 'Chip',
  name: 'And',
  inputs: [
    {
      type: 'Name',
      value: 'a'
    },
    {
      type: 'Name',
      value: 'b'
    }
  ],
  outputs: [
    {
      type: 'Name',
      value: 'out'
    }
  ],
  parts: [
    {
      type: 'ChipCall',
      name: 'Nand',
      arguments: [
        {
          type: 'Argument',
          name: {
            type: 'Name',
            value: 'a'
          },
          value: {
            type: 'Name',
            value: 'a'
          }
        },
        {
          type: 'Argument',
          name: {
            type: 'Name',
            value: 'b'
          },
          value: {
            type: 'Name',
            value: 'b'
          }
        },
        {
          type: 'Argument',
          name: {
            type: 'Name',
            value: 'out'
          },
          value: {
            type: 'Name',
            value: 'n'
          }
        }
      ]
    },
    {
      type: 'ChipCall',
      name: 'Nand',
      arguments: [
        {
          type: 'Argument',
          name: {
            type: 'Name',
            value: 'a'
          },
          value: {
            type: 'Name',
            value: 'n'
          }
        },
        {
          type: 'Argument',
          name: {
            type: 'Name',
            value: 'b'
          },
          value: {
            type: 'Name',
            value: 'n'
          }
        },
        {
          type: 'Argument',
          name: {
            type: 'Name',
            value: 'out'
          },
          value: {
            type: 'Name',
            value: 'out'
          }
        }
      ]
    }
  ],
  builtins: [],
  clocked: [],
}

The parse command is also available from Node:

const fs = require('fs');
const hdl = require('hdl-js');

const hdlCode = fs.readFileSync('./examples/And.hdl', 'utf-8');

console.log(hdl.parse(hdlCode)); // HDL AST

There is also convenient parseFile method:

const hdl = require('hdl-js');

console.log(hdl.parseFile('./examples/And.hdl')); // AST

Code generator

The code generator module allows exporting to HDL files from gate structures in other forms: from AST, from a composite gate instance, etc.

In general case it's an inverse procedure to parsing. In the simplest case you have a parsed AST, and the code generator can build an HDL code from it.

WIP: track issue #17.

Emulator

Hardware emulator module simulates and tests logic gates and chips implemented in the HDL, and also provides canonical implementation of the built-in chips.

Built-in gates

In general, all the gates can be built manually in HDL from the very basic Nand or Nor gates. However, hdl-js also provides implementation of most of the computer chips, built directly in JavaScript.

You can use these gates as building blocks with a guaranteed faster implementation, and also to check your own implementation, in case you build a custom version of a particular basic chip.

The --list (-l) command shows all the built-in gates available in the emulator. The gates can be analyzed, executed, and used further as basic building blocks in construction of compound gates.

./bin/hdl-js --list

Built-in gates:

- And
- And16
- Or
- ...

Once you know a gate of interest, you can introspect its specification.

Viewing gate specification

To see the specification of a particular gate, we can use --describe (-d) option, passing the name of a needed --gate (-g):

./bin/hdl-js --gate And --describe

Result:

"And" gate:

Description:

  Implements bitwise 1-bit And & operation.

Inputs:

  - a
  - b

Outputs:

  - out

Truth table:

┌───┬───┬─────┐
│ a │ b │ out │
├───┼───┼─────┤
│ 0 │ 0 │  0  │
├───┼───┼─────┤
│ 0 │ 1 │  0  │
├───┼───┼─────┤
│ 1 │ 0 │  0  │
├───┼───┼─────┤
│ 1 │ 1 │  1  │
└───┴───┴─────┘

NOTE: the --gate option handles both, built-in gates by name, and custom gates from HDL files.

From Node the specification of a built-in gate is exposed via Spec option on the gate class:

const hdl = require('hdl-js');

const {And} = hdl.emulator.BuiltInGates;

console.log(And.Spec);

/*

Output:

{
  description: 'Implements bitwise 1-bit And & operation.',

  inputPins: ['a', 'b'],

  outputPins: ['out'],

  truthTable: [
    {a: 0, b: 0, out: 0},
    {a: 0, b: 1, out: 0},
    {a: 1, b: 0, out: 0},
    {a: 1, b: 1, out: 1},
  ]
}

*/

Specifying output format

Using --format option it is possible to control the format of the input/output values. For example, the truth table of the And16 gate in binary (default), and hexadecimal formats:

./bin/hdl-js --gate And16 --describe

Binary output format:

┌──────────────────┬──────────────────┬──────────────────┐
│      a[16]       │      b[16]       │     out[16]      │
├──────────────────┼──────────────────┼──────────────────┤
│ 0000000000000000 │ 0000000000000000 │ 0000000000000000 │
├──────────────────┼──────────────────┼──────────────────┤
│ 0000000000000000 │ 1111111111111111 │ 0000000000000000 │
├──────────────────┼──────────────────┼──────────────────┤
│ 1111111111111111 │ 1111111111111111 │ 1111111111111111 │
├──────────────────┼──────────────────┼──────────────────┤
│ 1010101010101010 │ 0101010101010101 │ 0000000000000000 │
├──────────────────┼──────────────────┼──────────────────┤
│ 0011110011000011 │ 0000111111110000 │ 0000110011000000 │
├──────────────────┼──────────────────┼──────────────────┤
│ 0001001000110100 │ 1001100001110110 │ 0001000000110100 │
└──────────────────┴──────────────────┴──────────────────┘

With --format hex:

./bin/hdl-js --gate And16 --describe --format hex

Hexadecimal output format:

┌───────┬───────┬─────────┐
│ a[16] │ b[16] │ out[16] │
├───────┼───────┼─────────┤
│ 0000  │ 0000  │  0000   │
├───────┼───────┼─────────┤
│ 0000  │ FFFF  │  0000   │
├───────┼───────┼─────────┤
│ FFFF  │ FFFF  │  FFFF   │
├───────┼───────┼─────────┤
│ AAAA  │ 5555  │  0000   │
├───────┼───────┼─────────┤
│ 3CC3  │ 0FF0  │  0CC0   │
├───────┼───────┼─────────┤
│ 1234  │ 9876  │  1034   │
└───────┴───────┴─────────┘

Testing gates on passed data

It is possible to manually test and evaluate the outputs of a gate based on its inputs:

const hdl = require('hdl-js');

const {
  emulator: {

    /**
     * `Pin` class is used to define inputs, and outputs.
     */
    Pin,

    BuiltInGates: {
      And,
    }
  }
} = hdl;

const and = new And({
  inputPins: [
    new Pin({name: 'a', value: 1}),
    new Pin({name: 'b', value: 1}),
  ],

  outputPins: [
    new Pin({name: 'out'}),
  ],
});

// Run the logic.
and.eval();

// Check "out" pin value:
console.log(and.getOutputPins()[0].getValue()); // 1

Input and output pins can also be passed as plain objects, rather than as Pin instances:

const hdl = require('hdl-js');

const {
  And,
  And16,
} = hdl.emulator.BuiltInGates;

// Simple names:

const and1 = new And({
  inputPins: ['a', 'b'],
  outputPins: ['out'],
});

and1.setPinValues({a: 1, b: 0});
and1.eval();

console.log(and1.getPin('out').getValue()); // 0

// Spec with values and sizes:

const and2 = new And16({
  inputPins: [
    {name: 'a', size: 16, value: 1},
    {name: 'b', size: 16, value: 0},
  ],
  outputPins: [
    {name: 'out', size: 16},
  ],
});

and2.eval();
console.log(and2.getPin('out').getValue()); // 0

Pins

As mentioned above, Pins are used to define inputs and outputs of gates. A single pin represents a wire, on which a signal can be transmitted. Logically, a pin can store a number of a needed size.

For example, a pin of size 16 (default is size 1, i.e. a single "wire"):

const hdl = require('hdl-js');

const {
  emulator: {
    Pin,
  }
} = hdl;

const p1 = new Pin({
  value: 'p',
  size: 16,
});

p1.setValue(255);
console.log(p1.getValue()); // 255

Usually when creating a gate instance, explicit usage of the Pin class can be omitted (they are created automatically behind the scene), however, it is possible to get a needed pin using getPin(name) method on a gate. Then one can get a value of the pin, or subscribe to its 'change' event.

Pin size and slices

A pin can be of a needed size. For example, in HDL:

IN sel[3];

tells that the maximum value of the sel pin is 3 bits (0b111), or "3 wires".

Individual bits in HDL can be accessed with direct indices (as in the sel[2]), or using slice notation (as with the sel[0..1]):

Mux4Way16(..., sel=sel[0..1], ...)
Mux16(..., sel=sel[2], ...);

In JS, the individual bits can be manipulated using setValueAt, getSlice, and other methods:

...

const p1 = new Pin({
  value: 'p',
  size: 3,
  value: 0,
});

p1.setValue(0b111); // 7

console.log(p1.getValueAt(1)); // 1

p1.setValueAt(1, 0);
console.log(p1.getValueAt(1)); // 0

console.log(p1.getValue()); // 0b101, i.e. 5
console.log(p1.getSlice(0, 1)); // first 2 bits: 0b01

Pin events

All Pin instances emit the following events:

  • change(newValue, oldValue, fromIndex, toIndex) - an event emitted whenever a pin changes its value.

If the fromIndex is passed, this means a specific bit was updated, e.g. a[2]. If both, fromIndex, and toIndex are passed, this means a slice was updated, e.g. a[1..3]. Otherwise, the whole value was updated.

...

const p1 = new Pin({
  value: 'p',
  size: 16,
  value: 0,
});

p1.on('change', (newValue, oldValue) => {
  console.log(`p1 changed from ${oldValue} to ${newValue}.`);
});

p1.setValue(255);

/*

Output:

p1 changed from 0 to 255.

*/

Creating gates from default spec

All gates known their own specification, so we can omit passing explicit pins info, and use a constructor without parameters, or create gates via the defaultFromSpec method:

const hdl = require('.');

const {And} = hdl.emulator.BuiltInGates;

// Creates input `a` and `b` pins, and
// ouput `out` pin automatically:

const and1 = new And();

and1
  .setPinValues({a: 1, b: 1})
  .eval();

console.log(and1.getPin('out').getValue()); // 1

// The same as:

const and2 = And.defaultFromSpec();

and2
  .setPinValues({a: 1, b: 0})
  .eval();

console.log(and2.getPin('out').getValue()); // 0

Exec on set of data

It is also possible to execute and test gate logic on the set of data:

// const and = new And({ ... });

// Test the gate on set of inputs, get the results
// for the outputs.

const inputData = [
  {a: 1, b: 0},
  {a: 1, b: 1},
];

const {result} = and.execOnData(inputData);

console.log(result);

/*

Output for `result`:

[
  {a: 1, b: 0, out: 0},
  {a: 1, b: 1, out: 1},
]

*/

Validating passed data on gate logic

In addition, if output pins are passed, the execOnData will validates them, and report conflicting pins, if the expected values differ from the actual ones:

// const and = new And({ ... });

// Pass the output pins as well:

const data = [
  {a: 1, b: 0, out: 1}, // invalid output
  {a: 1, b: 1, out: 1}, // valid
];

let {
  result,
  conflicts,
} = and.execOnData(data);

// Result is a correct truth table:
console.log(result);

/*

Output for `result`:

[
  {a: 1, b: 0, out: 0},
  {a: 1, b: 1, out: 1},
]

*/

// Conflicts contain conflicting entries: {row, pins}.
console.log(conflicts);

/*

Conflicts output:

[
  {
    row: 0,
    pins: {
      out: {
        expected: 1,
        actual: 0,
      },
    },
  },
]

*/

From the CLI it's controlled via the --exec-on-data (-e) option.

In the example below we validate the gate logic, passing (incorrect in this case) expected value for the out pin of the Or gate:

./bin/hdl-js -g Or -e '[{"a": 1, "b": 1, "out": 0}]'

Found 1 conflicts in:

  - row: 0, pins: out

┌───┬───┬───────┐
│ a │ b │  out  │
├───┼───┼───────┤
│ 1 │ 1 │ 0 / 1 │
└───┴───┴───────┘

It is possible using actual number values in binary (0b1111), hexadecimal (0xF), and decimal (15) formats. Otherwise, the values have to be passed as strings ('FFFF' for 0xFFFF) with correct --format option:

./bin/hdl-js -g Not16 -e '[{in: 0xFFFF}]' -f hex

Output:

Truth table for data:

┌────────┬─────────┐
│ in[16] │ out[16] │
├────────┼─────────┤
│  FFFF  │  0000   │
└────────┴─────────┘

Sequential run

When the --run (-r) command is passed, it is possible to analyze how the pin values change in time (especially for the clocked gates). This options work with both, --exec-on-data (-e), and --describe (-d).

Here's an example running the Register truth table:

./bin/hdl-js --gate Register --describe --run

Which executes the gate in time:

Register run

Gate events

All gates emit events, which correspond to their internal logic handlers:

  • eval -- an event happening on evaluation of the compositional logic
  • clockUp(value) -- an event happening, when a gate handled the clock's rising edge (aka "tick")
  • clockDown(value) -- an event happening, when a gate handled the clock's falling edge (aka "tock")

Here's an example, how an external observer may subscribe to gate logic events:

const hdl = require('.');

const {
  emulator: {
    BuiltInGates: {
      Register,
    },
    Clock: {
      SystemClock,
    },
  },
} = hdl;

const r1 = Register.defaultFromSpec();

// Handle the event, when `r1` gets its output value:

r1.on('clockDown', () => {
  console.log(`r1 = ${r1.getPin('out').getValue()}`); // 255
});

// Setup the `r1` inputs, on the falling edge (clockDown)
// the value is set to the `out` pin:

r1.setPinValues({
  in: 255,
  load: true,
});

// Run the full clock cycle:

SystemClock
  .reset()
  .cycle();

NOTE: as described in Pins section, it is also possible to subscribe to 'change' event of individual pins.

Main chip groups

All gates are grouped into the following categories:

Very basic chips

This group includes two gates which can be used to build anything else.

  • Nand (negative-And)
  • Nor (negative-Or)

For example, as was shown above, the basic And chip can be built on top of two connected Nand gates:

CHIP And {
  IN a, b;
  OUT out;

  PARTS:

  Nand(a=a, b=b, out=n);
  Nand(a=n, b=n, out=out);
}

Basic chips

The basic group of chips includes primitive building blocks for more complex chips. The basic chips themselves are built from the very basic chips. The group includes:

For example, the more complex HalfAdder chip can be built on top of Xor, and And gates:

CHIP HalfAdder {
  IN a, b;    // 1-bit inputs
  OUT sum,    // Right bit of a + b
      carry;  // Left bit of a + b

  PARTS:

  Xor(a=a, b=b, out=sum);
  And(a=a, b=b, out=carry);
}

The Mux (multiplexer) gate, which provides basic selection (or "if" operation), and being a basic chip, can itself be built from other basic chips from this group, such as Not, And, and Or.

To see the full specification and truth table of a needed gate, use --describe (-d) option from CLI.

ALU

The arithmetic-logic unit is an abstraction which encapsulates inside several operations, implemented as smaller sub-chips. Usually ALU accepts two numbers, and based on the OpCode (operation code), evaluates needed result. This group of chips includes:

The ALU chip itself evaluates both, arithmetic (such as addition), and logic (such as And, Or, etc) operations.

Memory chips

The basic building block for memory chips is a Flip-Flop. In particular, in this specific case, it's the DFF (Data/Delay Flip-Flop).

On top of DFF other storage chips, such as 1 Bit abstraction, or 16-bit Register abstraction, are built. The group includes the following chips:

Memory chips are synchronized by the clock, and operate on rising and falling edges of the clock cycle. Specification, and truth table of such chips contains $clock information, where negative values (e.g. -0) mean low logical level, and positive (+0) -- high logical level, or the rising edge.

The internal state of a clocked chip can only change on the rising edge. While the output is committed (usually to reflect the internal state) on the falling edge of the clock. This delay of the output is exactly reflected in the DFF, that is Delay Flip-Flop, name.

See detailed clock description in the next section.

Clock

The System clock is used to synchronize clocked chips (see example above in memory chips).

A clock operates on the clock rate, that is, number of cycles per second, measured in Hz. The higher the clock rate, the faster machine is.

Clock's runtime consists of cycles, and clock cycle has two phases: rising edge (aka "tick"), and falling edge (aka "tock").

Clock image

As mentioned in the memory chips section, all clocked gates can change their internal state only on the rising edge. And on the falling edge they commit the value form the state to the output pins.

For example, running the:

hdl-js --gate Bit --describe

Shows the clock information:

"Bit" gate:

Description:

  1 bit memory register.

  If load[t]=1 then out[t+1] = in[t] else out does not change.

  Clock rising edge updates internal state from the input,
  if the `load` is set; otherwise, preserves the state.

    ↗ : state = load ? in : state

  Clock falling edge propagates the internal state to the output:

    ↘ : out = state

Inputs:

  - in
  - load

Outputs:

  - out

Truth table:

┌────────┬────┬──────┬─────┐
│ $clock │ in │ load │ out │
├────────┼────┼──────┼─────┤
│   -0   │ 0  │  0   │  0  │
├────────┼────┼──────┼─────┤
│   +0   │ 1  │  1   │  0  │
├────────┼────┼──────┼─────┤
│   -1   │ 1  │  0   │  1  │
├────────┼────┼──────┼─────┤
│   +1   │ 1  │  0   │  1  │
├────────┼────┼──────┼─────┤
│   -2   │ 1  │  0   │  1  │
├────────┼────┼──────┼─────┤
│   +2   │ 0  │  1   │  1  │
├────────┼────┼──────┼─────┤
│   -3   │ 0  │  0   │  0  │
└────────┴────┴──────┴─────┘

From Node the Clock is available on the emulator object, and we can also get access to the global singleton SystemClock, which is used to synchronize the clocked chips:

const hdl = require('hdl-js');

const {
  emulator: {
    Clock,
    Pin,
  },
} = hdl;

const clock = new Clock({rate: 10, value: -5});
const pin = new Pin({name: 'a'});

// Track clock events.
clock.on('tick', value => pin.setValue(value));

clock.tick();

console.log(pin.getValue()); // +5;

Clock events

The clock emits the following events:

  • tick - rising edge
  • tock - falling edge
  • next - half cycle (tick or tock)
  • cycle - full cycle (tick -> tock)
  • change - clock value change

All the clocked gates are automatically subscribed to SystemClock events, and update the value of their $clock pin:

const hdl = require('hdl-js');

const {
  emulator: {
    Gate,
    Clock: {
      SystemClock,
    },
  },
} = hdl;

class MyGate extends Gate {
  static isClocked() {
    return true;
  }

  eval() {
    // Noop, handle only clock signal.
    return;
  }

  clockUp(clockValue) {
    console.log('Handle rising edge:', clockValue);
  }

  clockDown(clockValue) {
    console.log('Handle falling edge:', clockValue);
  }
}

MyGate.Spec = {
  inputPins: ['a'],
  outputPins: ['b'],
};

const gate = MyGate.defaultFromSpec();

// Run full clock cycle.
SystemClock.cycle();

/*

Output:

Handle rising edge: 0
Handle falling edge: -1

*/

It is also possible to start, stop, and reset the clock:

const hdl = require('hdl-js');

const {
  emulator: {
    Clock: {
      SystemClock,
    },
  },
} = hdl;

// Reset the clock:
SystemClock.reset();

// Subscribe to the events:

SystemClock.on('tick', value => console.log('tick:', value));
SystemClock.on('tock', value => console.log('tock:', value));

// Run it:
SystemClock.start();

/*

Output (every second):

tick: +0
tock: -1
tick: +1
tock: -2
tick: +2
tock: -3
...

*/

Clock rate

The --clock-rate (-c) parameter controls the rate of the System clock. For example, the second run executes operations faster:

With default clock rate 1:

./bin/hdl-js --gate Register --describe --run

With clock rate 3:

./bin/hdl-js --gate Register --describe --run --clock-rate 3

Composite gates

The composite gates are created from other, more primitive, gates. By connecting inputs and outputs of the internal chips, it is possible to build an abstraction in a view of a resulting component, which encapsulates inside details of smaller sub-parts.

Although it is possible to create a composite gate manually using CompositeGate class from emulator, usually they are created via HDL.

Building chips in HDL

We already discussed briefly format of the HDL, and here we show how to create custom chips, building them from smaller blocks.

As mentioned, two very basic gates, the Nand, and Nor, can be used to build everything else in the computer chips.

In the example below, we use the Nand gate to implement a custom version of the And gate (even though the built-in And gate implementation exists):

// File: examples/And.hdl

CHIP And {
  IN a, b;
  OUT out;

  PARTS:

  Nand(a=a, b=b, out=n);
  Nand(a=n, b=n, out=out);
}

Here we connect two Nand gates in needed order, patching the output of the first one (via the internal pin n) to the inputs of the second Nand gate.

From a user perspective, the interface of our And gate looks as follows:

And gate interface

While if we look under the hood of the And gate implementation, we'll see the following picture:

And gate implementation

NOTE: as in other systems, in hardware chips there might be multiple implementations for the same interface. E.g. we could build the And chip using Nor gates, instead of Nand.

How this works? The Nand stands for "negative-And" (or "not-And"). And first we feed our own a and b inputs to the first internal Nand chip, and get the "nand-result", saving it to the temporary (internal) pin n:

Nand(a=a, b=b, out=n);

As you can see, the Nand itself defines its inputs as a, and b, and output as out, which is propagated to our internal n.

NOTE: run hdl-js --gate Nand --describe to see its specification.

Then, if to feed the same value to Nand chip, we get the "Not" operation -- and that exactly what we do in the second Nand "call", feeding the value of n to both, a, and b inputs:

Nand(a=n, b=n, out=out);

The resulting out from the second Nand is fed further to our own out pin. Eventually we got "not-not-And", and what is just "And":

Not(Nand(a, b)) == And(a, b)

NOTE: you can also get more details on the implementation in the wonderful nand2tetris course by Noam Nisan and Shimon Schocken.

Viewing composite gate specification

Getting a specification of a composite gate from HDL doesn't differ from getting the specification of a built-in chip, since the --gate option handles both gate types.

For example, to view the specification of our custom And gate from above (see also examples/And.hdl), we can just use the same --describe (-d) option:

hdl-js --gate examples/And.hdl --describe

What results to:

Custom "And" gate:

Description:

  Compiled from HDL composite Gate class "And".

Inputs:

  - a
  - b

Internal pins:

  - n

Outputs:

  - out

Truth table:

┌───┬───┬───┬─────┐
│ a │ b │ n │ out │
├───┼───┼───┼─────┤
│ 0 │ 0 │ 1 │  0  │
├───┼───┼───┼─────┤
│ 0 │ 1 │ 1 │  0  │
├───┼───┼───┼─────┤
│ 1 │ 0 │ 1 │  0  │
├───┼───┼───┼─────┤
│ 1 │ 1 │ 0 │  1  │
└───┴───┴───┴─────┘

As we can see, it correctly determined our internal pin n, and even showed it in the generated truth table.

NOTE: for 1-bit values the generated truth table shows all values. For larger pins, e.g. with size 16, a table with 5 random rows is shown. Try running the hdl-js -g examples/Not16.hdl -d.

The truth table allows us also to check, whether our implementation in the PARTS section is correct (and it really is in this case!).

As an alternative, check also the specification of the built-in And gate -- you'll notice that it doesn't differ much, resulting to the same truth table for inputs and outputs.

And of course it is possible to do a sequential run of a custom gate too:

hdl-js --gate examples/Not16.hdl --describe --run

Using custom and built-in gates in implementation

In the example above, we used built-in native Nand gate to implement our own version of the And gate. However, one you have implemented some custom gate, you are free to use it further as a building block for even more abstracted chips.

For example, if we look at the examples/Mux.hdl file:

/**
 * Multiplexor:
 * out = a if sel == 0
 *       b otherwise
 */

CHIP Mux {
  IN a, b, sel;
  OUT out;

  PARTS:

  Not(in=sel, out=nel);
  And(a=a, b=nel, out=A);
  And(a=b, b=sel, out=B);
  Or(a=A, b=B, out=out);
}

Assuming the Mux.hdl file is in the same directory as the And.hdl, the And gate in the implementation is loaded exactly from our local custom implementation. Whereas, the Not, and Or are loaded from the built-ins. If we remove And.hdl from this directory, it will also be loaded from built-ins then.

Loading HDL chips from Node

In Node it is possible to load a composite HDL gate class using the HDLClassFactory module, which is exposed on the emulator. The hdl-js itself also exposes two convenient wrappers: fromHDLFile, and fromHDL:

const hdl = require('hdl-js');

// Load `And` class from HDL:
const And = hdl.fromHDLFile('./examples/And.hdl');

// Instance of the class:
const and = And.defaultFromSpec();

// Test:
and
  .setPinValues({a: 1, b: 1})
  .eval();

// {a: 1, b: 1, n: 0, out: 1}
console.log(and.getPinValues());

About

Hardware description language (HDL) parser, and Hardware simulator.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • JavaScript 98.2%
  • GAP 1.7%
  • Shell 0.1%