hwsim

package module
v0.0.0-...-dfe2263 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 25, 2018 License: MIT Imports: 6 Imported by: 0

README

HW (sim)

This package provides the necessary tools to build a virtual CPU using Go as a hardware description language and run it.

This includes a naive hardware simulator and an API to compose basic components (logic gates, muxers, etc.) into more complex ones.

The API is designed to mimmic a basic Hardware description language and reduce typing overhead.

DISCLAIMER: This is a self-educational project. I have no electrical engineering background, so please bear with me if some of the terms used are inaccurate or just plain wrong. If you spot any errors, please do not hesitate to file an issue or a PR.

Implementation details

The simulation is built around wires that connect components together. While a wire can recieve a signal by only one component, it can broadcast that signal to any number of components (fanout).

The simulation works by updating every half clock cycle the components that implement the PostUpdater interface (i.e. that have side effects or somehow "drive" the circuit, like outputs and clocked data flip-flops). The signals are then propagated through the simulation by "pulling" them up: calling Recv on a Wire triggers an update of the component feeding that Wire.

Time in the simulation is simply represented as a boolean value: false during the call to Circuit.Tick() and true during the call to Circuit.Tock(). Wires use this information to prevent recursion and provide loop detection.

As a result:

  • most components like logic gates have no propagation delay
  • other components like Data Flip-Flops have a one clock cycle propagation delay.
  • direct wire loops are forbidden. Loops must go through a DFF or similar component.

The DFF provided in the hwlib package works like a gated D latch and can be used as a building block for all sequential components. Its output is considered stable only during calls to Circuit.Tick(), i.e. when the clk argument of Updater.Update(clk bool) is true.

Project Status

The simulation implementation is sub-optimal: a push model would allow updating only a few components at every clock cycle (those for which inputs have changed). This would however make the implementation much more complex as components would need to wait for all input signals to be updated before updating themselves. Additionally, performance is not a major goal right now since it can always be somewhat improved by using cutstom components (with logic written in Go).

The main focus is on the API: bring it in a usable and stable state. Tinkering with the simulation must be fun, not a type typing fest or a code scaffolding chore. Not all the features I have in mind are implemented yet. TODO's with high priority are listed in the issue tracker. Contributions welcome!

I don't really have plans for any form of GUI yet, so please don't ask.

Quick tour

Building a chip

By chip I mean these tiny black boxes with silver pins protuding from them that can do all kind of marvelous things in a digital circuit.

A chip is defined by its name, it's input and output pin names, and what circuitry is inside, performing its actual function.

For example, building an XOR gate from a set of NANDs is done like this:

XOR gate

The same in Go with hwsim and the built-in components provided by the hwlib package:

    import (
        // use shorter names for the package imports. It will help your CTS...
        hw "github.com/db47h/hwsim"
        hl "github.com/db47h/hwsim/hwlib"
    )

    xor, err := hw.Chip(
        "XOR",
        "a, b",   // inputs of the created xor gate
        "out",    // outputs
        hl.Nand("a=a,      b=b,      out=nandAB"), // leftmost NAND
        hl.Nand("a=a,      b=nandAB, out=outA"),   // top NAND
        hl.Nand("a=nandAB, b=b,      out=outB"),   // bottom NAND
        hl.Nand("a=outA,   b=outB,   out=out"),    // rightmost NAND
    )

The returned xor function can then be reused as a part with inputs a, b and output out in another chip design. Intermediate (chip internal) wires like nandAB in the above example can be declared and used on the fly.

Custom parts

Custom parts are components whose logic is written in Go. Such components can be used to interface with Go code, or simply to improve performance by rewriting a complex chip in Go (adders, ALUs, RAM).

A custom component is just a struct with custom field tags that implements the Updater interface:

    // sample custom XOR gate

    // xorInstance represents an instance of an xor gate.
    // one such instance is created for every xor gate in a circuit.
    type xorInstance struct {
        A   *hw.Wire `hw:"in"`  // the tag hw:"in" indicates an input pin
        B   *hw.Wire `hw:"in"`
        Out *hw.Wire `hw:"out"` // output pin
    }

    // Update is a Component function that reads the state of the inputs
    // and sends a result to the outputs. It will be called at every
    // half clock cycle of the simulation.
    func (g *xorInstance) Update(clk bool) {
        a, b := g.A.Recv(clk), g.B.Recv(clk) // get input signals
        g.Out.Send(clk, a && !b || !a && b)  // send output
    }

    // Now we turn xorInstance into a part usable in a chip definition.
    // Just pass a nil pointer to an xorInstance to MakePart which will return
    // a *PartSpec
    var xorSpec = hw.MakePart((*xorInstance)(nil))
    // And grab its NewPart method
    var xor = hw.MakePart((*xorInstance)(nil)).NewPart

Another way to do it is to create a PartSpec struct with some closure magic:

    var xorSpec = &hw.PartSpec{
        Name:    "XOR",
        Inputs:  hw.IO("a, b"),
        Outputs: hw.IO("out"),
        Mount:   func(s *hw.Socket) hw.Updater {
            a, b, out := s.Wire("a"), s.Wire("b"), s.Wire("out")
            return hw.UpdaterFn(
                func (clk bool) {
                    a, b := g.A.Recv(clk), g.B.Recv(clk)
                    g.Out.Send(clk, a && !b || !a && b)
                })
            }}
    var xor = xorSpec.NewPart

The reflection approach is more readable but is also more resource hungry.

If defining custom components as functions is preferable, for example in a Go package providing a library of components (where we do not want to export variables):

    func Xor(c string) hw.Part { return xorSpec.NewPart(c) }

Now we can go ahead and build a half-adder:

    hAdder, _ := hw.Chip(
        "Half-Adder",
        "a, b",                 // inputs
        "s, c",                 // output sum and carry
        xor("a=a, b=b, out=s"), // our custom xor gate!
        hw.And("a=a, b=b, out=c"),
    )
Running a simulation

A circuit is made of a set of parts connected together. Time to test our adder:

    var a, b, ci bool
    var s, co bool
    c, err := hw.NewCirtuit(
        // feed variables a, b and ci as inputs in the circuit
        hw.Input(func() bool { return a })("out=a"),
        hw.Input(func() bool { return b })("out=b"),
        hw.Input(func() bool { return c })("out=ci"),
        // full adder
        hAdder("a=a,  b=b,  s=s0,  c=c0"),
        hAdder("a=s0, b=ci, s=sum, c=c1"),
        hl.Or(" a=c0, b=c1, out=co"),
        // outputs
        hw.Output(func (bit bool) { s = bit })("in=sum"),
        hw.Output(func (bit bool) { co = bit })("in=co"),
    )
    if err != nil {
        // panic!
    }
    defer c.Dispose()

And run it:

    // set inputs
    a, b, ci = false, true, false

    // run a single clock cycle
    c.TickTock()

    // check outputs
    if s != true && co != false {
        // bug
    }

Contributing

A good API has good names with clearly defined entities. This package's API is far from good, with some quirks.

The whole Socket thing, along with the wiring mess in Chip(), are remnants of a previous implementation and are overly complex. They will probably be dusted off when I get to implement static loop detection. Until then, it works and doesn't affect the performance of the simulation, so it's not top priority. It won't have a major impact on the API either since Socket will remain, possibly renamed, but as an interface with the same API.

If you have any suggestions about naming or other API changes that would make everyone's life easier, feel free to file an issue or open a PR!

License

Copyright 2018 Denis Bernard db047h@gmail.com

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Documentation

Overview

Package hwsim provides the necessary tools to build a virtual CPU using Go as a hardware description language and run it.

This includes a naive hardware simulator and an API to compose basic components (logic gates, muxers, etc.) into more complex ones.

The API is designed to mimmic a basic hardware description language and reduce typing overhead.

The sub-package hwlib provides a library of built-in logic gates as well as some more advanced components.

The simulation is built around wires that connect components together. While a Wire can recieve a signal by only one component, it can broadcast that signal to any number of components (fanout).

The simulation works by updating every half clock cycle the components that implement the PostUpdater interface (i.e. that have side effects or somehow "drive" the circuit, like outputs and clocked data flip-flops). The signals are then propagated through the simulation by "pulling" them up: calling Recv on a Wire triggers an update of the component feeding that Wire.

Time in the simulation is simply represented as a boolean value, false during the call to Circuit.Tick() and true during the call to Circuit.Tock(). Wires use this information to prevent recursion and provide loop detection.

As a result:

  • Most components ignore the clk argument to Updater.Update(clk bool) and just forward it to the Send/Recv methods of their connected Wires.
  • Most components like logic gates have no propagation delay.
  • Other components like Data Flip-Flops (DFF) have a one clock cycle propagation delay.
  • Direct wire loops are forbidden. Loops must go through a DFF or similar component.

The DFF provided in the hwlib package works like a gated D latch and can be used as a building block for all sequential components. Its output is considered stable only during calls to Circuit.Tick(), i.e. when the clk argument of Updater.Update(clk bool) is true.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	False = "false" // always false input
	True  = "true"  // alwyas true input
	Clk   = "clk"   // clock signal. False during Tick, true during Tock.

)

Constant Wire names. These wires can only be connected to the input pins of a part.

Those are reserved names and should not be used as input or output names in custom chips.

Functions

func IO

func IO(spec string) []string

IO is a wrapper around ParseIOSpec that panics if an error is returned.

Example
package main

import (
	"fmt"

	"github.com/db47h/hwsim"
)

func main() {
	fmt.Println(hwsim.IO("a,b"))
	fmt.Println(hwsim.IO("a[2],b"))
	fmt.Println(hwsim.IO("a[0..0],b[1..2]"))

}
Output:

[a b]
[a[0] a[1] b]
[a[0] b[1] b[2]]

func ParseIOSpec

func ParseIOSpec(names string) ([]string, error)

ParseIOSpec parses an input or output pin specification string and returns a slice of individual pin names suitable for use as the Input or Output field of a PartSpec.

The input format is:

InputDecl  = PinDecl { "," PinDecl } .
PinDecl    = PinIdentifier | BusIdentifier .
BusId      = identifier "[" size | Range "]" .
PinId      = identifier .
Range      = integer ".." integer .
identifier = letter { letter | digit } .
size       = { digit } .
letter     = "A" ... "Z" | "a" ... "z" | "_" .
digit      = "0" ... "9" .

Buses are declared by simply specifying their size. For example, the I/O specification string "a, b, bus[4]" will be expanded to:

[]string{"a", "b", "bus[0]", "bus[1]", "bus[2]", "bus[3]"}

Ranges can also be used to force a specific range of bus indices:

ParseIOSpec("p, g, c[1..4]")

will expand to:

[]string{"p", "g", "c[1]", "c[2]", "c[3]", "c[4]"}

Types

type Bus

type Bus []*Wire

A Bus is a set of Wires.

func (Bus) Recv

func (b Bus) Recv(clk bool) uint64

Recv returns the int64 value of the bus. Wire 0 is the LSB.

func (Bus) Send

func (b Bus) Send(clk bool, v uint64)

Send sets the int64 value of the bus. Pin Wire is the LSB.

type Circuit

type Circuit struct {
	// contains filtered or unexported fields
}

Circuit is a runnable circuit simulation.

func NewCircuit

func NewCircuit(parts ...Part) (*Circuit, error)

NewCircuit builds a new circuit simulation based on the given parts.

func (*Circuit) ComponentCount

func (c *Circuit) ComponentCount() int

ComponentCount returns the number of components in the circuit.

func (*Circuit) Tick

func (c *Circuit) Tick()

Tick runs the simulation until the beginning of the next half clock cycle.

func (*Circuit) TickTock

func (c *Circuit) TickTock()

TickTock runs the simulation for a whole clock cycle.

func (*Circuit) Ticks

func (c *Circuit) Ticks() uint64

Ticks returns the value of the step counter.

func (*Circuit) Tock

func (c *Circuit) Tock()

Tock runs the simulation until the beginning of the next clock cycle. Once Tock returns, the output of clocked components should have stabilized.

func (*Circuit) WireCount

func (c *Circuit) WireCount() int

WireCount returns the number of components in the circuit.

type Connection

type Connection struct {
	PP string
	CP []string
}

A Connection represents a connection between the pin PP of a part and the pins CP in its host chip.

func ParseConnections

func ParseConnections(c string) (conns []Connection, err error)

ParseConnections parses a connection configuration like "partPinX=chipPinY, ..." into a []Connection{{PP: "partPinX", CP: []string{"chipPinX"}}, ...}.

Wire       = Assignment { [ space ] "," [ space ] Assignment } .
Assignment = Pin "=" Pin .
Pin        = identifier [ "[" Index | Range "]" ] .
Index      = integer .
Range      = integer ".." integer .
identifier = letter { letter | digit } .
integer    = { digit } .
letter     = "A" ... "Z" | "a" ... "z" | "_" .
digit      = "0" ... "9" .

type MountFn

type MountFn func(s *Socket) Updater

A MountFn mounts a part into socket s. MountFn's should query the socket to get Wires connected to a part's pins and return closures around these Wires.

For example, a Not gate can be defined like this:

notSpec := &hwsim.PartSpec{
	Name: "Not",
	In: hwsim.IO("in"),
	Out: hwsim.IO("out"),
	Mount: func (s *hwsim.Socket) hwsim.Updater {
		in, out := s.Wire("in"), s.Wire("out")
		return hwsim.UpdaterFn(
			func (clk bool) { out.Send(clk, !in.Recv(clk)) }
		)
	}}

type NewPartFn

type NewPartFn func(c string) Part

A NewPartFn is a function that takes a connection configuration and returns a new Part. See ParseConnections for the syntax of the connection configuration string.

func Chip

func Chip(name string, inputs string, outputs string, parts ...Part) (NewPartFn, error)

Chip composes existing parts into a new chip.

The pin names specified as inputs and outputs will be the inputs and outputs of the chip (the chip interface).

A XOR gate could be created like this:

xor, err := hwsim.Chip(
	"XOR",
	hwsim.IO("a, b"),
	hwsim.IO("out"),
	hwlib.Nand("a=a, b=b, out=nandAB"),
	hwlib.Nand("a=a, b=nandAB, out=w0"),
	hwlib.Nand("a=b, b=nandAB, out=w1"),
	hwlib.Nand("a=w0, b=w1, out=out"),
)

The created chip can be composed with other parts to create other chips simply by calling the returned NewPartFn with a connection configuration:

xnor, err := Chip(
	"XNOR",
	hwsim.IO("a, b"),
	hwsim.IO("out"),
	// reuse the xor chip created above
	xor("a=a, b=b, out=xorAB"),
	hwlib.Not("in=xorAB, out=out"),
)

func Input

func Input(f func() bool) NewPartFn

Input returns a 1 bit input. The input value is the return value of f.

func InputN

func InputN(bits int, f func() uint64) NewPartFn

InputN returns an input bus of the given bits size.

func Output

func Output(f func(value bool)) NewPartFn

Output returns a 1 bit output. f is called with the output value.

func OutputN

func OutputN(bits int, f func(uint64)) NewPartFn

OutputN returns an output bus of the given bits size.

type Part

type Part struct {
	*PartSpec
	Conns []Connection
}

A Part wraps a part specification together with its connections within a host chip.

type PartSpec

type PartSpec struct {
	// Part name.
	Name string
	// Input pin names. Must be distinct pin names.
	// Use the IO() function to expand an input description like
	// "a, b, bus[2]" to []string{"a", "b", "bus[0]", "bus[1]"}
	// See IO() for more details.
	Inputs []string
	// Output pin name. Must be distinct pin names.
	// Use the IO() function to expand an output description string.
	Outputs []string
	// Pinout maps the input and output pin names (public interface) of a part
	// to internal (private) names. If nil, the In and Out values will be used
	// and mapped one to one.
	// In a MountFn, only internal pin names must be used when calling the Socket
	// methods.
	// Most custom part implementations should ignore this field and set it to
	// nil.
	Pinout map[string]string

	// Mount function (see MountFn).
	Mount MountFn
}

A PartSpec represents a part specification (its blueprint).

Custom parts are implemented by creating a PartSpec:

notSpec := &hwsim.PartSpec{
	Name: "Not",
	In: hwsim.IO("in"),
	Out: hwsim.IO("out"),
	Mount: func (s *hwsim.Socket) hwsim.Updater {
		in, out := s.Wire("in"), s.Wire("out")
		return hwsim.UpdaterFn(
			func (clk bool) { out.Send(clk, !in.Recv(clk)) }
		)
	}}

Then get a NewPartFn for that PartSpec:

var notGate = notSpec.NewPart

or:

func Not(c string) Part { return notSpec.NewPart(c) }

Which can the be used as a NewPartFn when building other chips:

c, _ := Chip("dummy", In("a, b"), Out("notA, d"),
	notGate("in: a, out: notA"),
	// ...
)

func MakePart

func MakePart(t Updater) *PartSpec

MakePart wraps an Updater into a custom component. Input/output pins are identified by tags on fields of type *Wire.

The field tag must be `hw:"in"“ or `hw:"out"` to identify input and output pins. By default, the pin name is the field name in lowercase. A specific field name can be forced by adding it in the tag: `hw:"in,pin_name"`.

Buses must be arrays of *Wire (not slices).

Example

MakePart example with a custom Mux4

package main

import (
	"fmt"

	hw "github.com/db47h/hwsim"
)

// mux4 is a custom 4 bits mux.
type mux4Impl struct {
	A   [4]*hw.Wire `hw:"in"`     // input bus "a"
	B   [4]*hw.Wire `hw:"in"`     // input bus "b"
	S   *hw.Wire    `hw:"in,sel"` // single pin, the second tag value forces the pin name to "sel"
	Out [4]*hw.Wire `hw:"out"`    // output bus "out"
}

// Update implements Updater.
func (m *mux4Impl) Update(clk bool) {
	if m.S.Recv(clk) {
		for i, b := range m.B {
			m.Out[i].Send(clk, b.Recv(clk))
		}
	} else {
		for i, a := range m.A {
			m.Out[i].Send(clk, a.Recv(clk))
		}
	}
}

// no need to import reflect, just cast a nil pointer to mux4
var m4Spec = hw.MakePart((*mux4Impl)(nil))

// m4Spec is the *PartSpec for our mux4. In order to use it like the built-ins
// in hwlib, we need to get its NewPartFn method as a variable, or make it a function:
func Mux4(c string) hw.Part { return m4Spec.NewPart(c) }

// MakePart example with a custom Mux4
func main() {
	var a, b, out uint64
	var sel bool
	c, err := hw.NewCircuit(
		// IOs to test the circuit
		hw.InputN(4, func() uint64 { return a })("out=in_a"),
		hw.InputN(4, func() uint64 { return b })("out=in_b"),
		hw.Input(func() bool { return sel })("out=in_sel"),
		// our custom Mux4
		Mux4("a=in_a, b=in_b, sel=in_sel, out=mux_out"),
		// IOs continued...
		hw.OutputN(4, func(v uint64) { out = v })("in=mux_out"),
	)
	if err != nil {
		panic(err)
	}

	a, b, sel = 1, 15, false
	c.TickTock()
	fmt.Printf("a=%d, b=%d, sel=%v => out=%d\n", a, b, sel, out)
	sel = true
	c.TickTock()
	fmt.Printf("a=%d, b=%d, sel=%v => out=%d\n", a, b, sel, out)

}
Output:

a=1, b=15, sel=false => out=1
a=1, b=15, sel=true => out=15

func (*PartSpec) NewPart

func (p *PartSpec) NewPart(connections string) Part

NewPart is a NewPartFn that wraps p with the given connections into a Part.

type PostUpdater

type PostUpdater interface {
	Updater              // Update updates outputs
	PostUpdate(clk bool) // Update inputs
}

PostUpdater is implemented by Updaters that have side effects outside of a circuit or that somehow drive the circuit. All sequential components must implement PostUpdater.

type PostUpdaterFn

type PostUpdaterFn func(clk bool)

A PostUpdaterFn is a single update function that implements PostUpdater.

func (PostUpdaterFn) PostUpdate

func (f PostUpdaterFn) PostUpdate(clk bool)

PostUpdate implements PostUpdater.

func (PostUpdaterFn) Update

func (f PostUpdaterFn) Update(clk bool)

Update implements Updater.

type Socket

type Socket struct {
	// contains filtered or unexported fields
}

A Socket maps a part's internal pin names to Wires in a circuit. See PartSpec.Pinout.

func (*Socket) Bus

func (s *Socket) Bus(name string, size int) Bus

Bus returns the Bus connected to the given bus name.

func (*Socket) Wire

func (s *Socket) Wire(pinName string) *Wire

Wire returns the Wire connected to the given pin name.

type Updater

type Updater interface {
	// Update is called every time an Updater's output pins must be updated.
	// The clk value is the current state of the clock signal (false during a
	// tick, true during a tock). Non-clocked components should ignore this
	// signal and just pass it along in the Recv and Send calls to their
	// connected wires.
	Update(clk bool)
}

Updater is the interface for components in a circuit.

Clocked components must also implement PostUpdater.

type UpdaterFn

type UpdaterFn func(clk bool)

A UpdaterFn is a single update function that implements Updater.

func (UpdaterFn) Update

func (u UpdaterFn) Update(clk bool)

Update implements Updater.

type Wire

type Wire struct {
	// contains filtered or unexported fields
}

A Wire connects pins together. A Wire may have only one source pin and multiple destination pins. i.e. Only one component can send a signal on a Wire.

func (*Wire) Recv

func (c *Wire) Recv(clk bool) bool

Recv recieves a signal at time clk. It may trigger an update of the source component.

func (*Wire) Send

func (c *Wire) Send(clk bool, value bool)

Send sends a signal a time clk.

func (*Wire) SetSource

func (c *Wire) SetSource(u Updater)

SetSource sets the given Updater as the wire's source.

type Wrapper

type Wrapper interface {
	Updater
	Unwrap() []Updater
}

A Wrapper is a part that wraps together several other parts and has no additional function. When a Circtuit is built, Wrappers are unwrapped and discarded ─ their Update function is never called.

Directories

Path Synopsis
Package hwlib provides a library of reusable parts for hwsim.
Package hwlib provides a library of reusable parts for hwsim.
Package hwtest provides utility functions for testing circuits.
Package hwtest provides utility functions for testing circuits.
internal
hdl
lex
Package lex provides a functional lexer.
Package lex provides a functional lexer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL