Skip to content
fitzsnaggle edited this page Mar 14, 2013 · 7 revisions

Values

Literals

piplin supports several of Clojure's literals: booleans, numbers, keywords. piplin adds bit literals, with the following syntax for the bits in 0xf3: #b1111_00_11. The #b prefix starts a bit literal. _ is skipped over when reading to permit easier visual grouping.

piplin's types

piplin adds several new types: bit arrays, enums with controllable bitwise representation, bundles (aka structures), tagged unions, and unsigned integers of fixed bit width that modulo on over/underflow.

All types can be used as functions on their value to create a new typed piplin object. For example, to represent a vector of 0s and 1s as a bit array, use ((bits 4) [0, 0, 1, 1]). Some types, like bundles and unions, may instead need to be casted, like (cast my-bundle {:a 1, :b 2}) if the values must be recursively converted to appropriate types.

To get the type of an object, use typeof. To get the bit width of a type, use bit-width-of on the type (not the object). Types are regular Clojure records and are thus fully introspective.

booleans

In piplin, boolean values are represented with Clojure and Java's true and false. Their type is (anontype :boolean).

bits

To create a bits type instance n bits wide, use (bits n). (bit-slice bit-inst low high) where low is inclusive and high is exclusive returns a subrange of the given bits. (bit-cat & bits) returns the concatenation of any number of bits. (serialize obj) will convert any object to its bits representation if it is serializable. (deserialize type bits) returns bits deserialized according to the given type. serialize and deserialize are used at the boundaries of piplin programs to meet binary protocols.

enum

(enum #{:x, :y, :z}) creates an enum from the given set (#{...} is a set literal in Clojure, :foo is a keyword named foo, like Ruby). enums can also be specified with a map, which is useful for decoding symbolic data (e.g. (enum {:add #b001, :sub #b010, :noop #b000}) to decode an enum from incoming bits.

(def my-enum (enum #{:a, :b, :c})) ;define an enum called my-enum
(my-enum :a) ;this returns an object with type my-enum and value :a
(= (my-enum :a) :b) ;returns false because :b's type is inferred as my-enum
(= (my-enum :a) :foo) ;throws an exception because :foo is not a member of my-enum

union

;define my-union, which will be 4 bits wide (bits 3) + 1 bit tag needed
(def my-union (union {:foo (bits 3),
                      :bar (anontype :boolean)}))
(cast my-union {:foo 2}) ;works --needs cast to recursively convert 2
(cast my-union {:baz 3}) ;throws exception
   
(def my-union-var (cast my-union {:foo 0}))
(union-match my-union-var ;pattern match on the above var
  (:foo x x) ;if it's tagged :foo, bind value to x, return x
  (:bar q (mux2 q ;if it's tagged :bar, bind value to q, if q is true
            1 2))) ;return 1 else 2

bundle

;define my-bundle, which will be 5 bits wide
(def my-bundle (bundle {:slot1 (bits 3) :slot2 (enum #{:foo :bar :baz})))
;note that these types are composable, anonymously or bound to functions.
    
(def my-bund-instance (cast my-bundle {:slot1 4, :slot2 :baz})) ;works
    
;supports Clojure's destructuring binding
(let [{bit :slot1, key :slot2} my-bund-instance]
  ;in here, bit is a (bits 3) and key is an (enum #{:foo, :bar, :baz})
  )

Note that bundles and unions are fully composable with pattern matching:

(defn maybe [t] (union {:not nil, :just t})) ;use Clojure functions to make parameterized types
(def opcode (enum #{:add, :sub, :mul, :div}))
(def op (bundle {:op opcode, :src1 (bits 8), :src2 (bits 8)})
(def datum (cast (maybe op) {:just {:op :add, :src1 #b0000_1111, :src2 #b1010_0101}}))
(union-match datum
  (:not _ ;ignore binding
    false) ;return false
  (:just {:keys [op src1 src2]} ;use Clojure's destructuring on values
    (mux2 (= src1 src2) ;return src1 == src2
       true false)))

uintm

(uintm n) constructs an unsigned integer of bit width n that does modulo on over/underflow, just like in C/Java. This is valid: (+ ((uintm 8) 3) ((uintm 8) 254)) and it equals ((uintm 8) 1) due to the overflow. (+ ((uintm 2) 1) 1) is equal to ((uintm 2) 2), but (+ ((uintm 2) 1) + 22) is an error because 22 cannot be converted directly to a (uintm 2). The initial value construction rules are stricter than the usage rules.

sints

Signed, saturating integer. On overflow or underflow in clamps to the min or max value.

TODO: document usage and pitfals

sfxpts

Signed, saturing fixed point number. This is good for signal processing--like an sints, but manages the fractional part automatically.

TODO: document usage and pitfalls.

Functions

= and not= work on all types.

and, or, numeric predicates, and other helpers/carryovers need to be documented.

Math

The following functions operate on uintm: +, -, *, >, <, >=, <=, inc, dec

The following functions operate on both uintm and bits: bit-and, bit-or, bit-xor

Conditionals

not negates any boolean. mux2 behaves just like if (but if is a reserved word in Clojure). cond and condp behave like Clojure's eponymous functions, but condp doesn't support the :>> syntax.

Unions

union-match explained above is the only way to safely access a union's members.

Bundles

get gets a key from a bundle. assoc and assoc-in change keys in a bundle or a nested bundle, respectively, just as in Clojure.

Bits

bit-cat concatenates bits and bit-slice slices them. serialize and deserialize convert other types to and from bits. See the earlier section on bits for details.

Modules

Declarations

Piplin code is organized into modules. Modules have 3 sections: inputs, outputs, and feedback. Inputs contain the values from the beginning of each cycle, incoming from the container. Feedbacks contain the value they were set to last cycle--that is, they're registers. Outputs are the same as feedbacks, but they also are exposed to the containing module. The submodule system is complete, but it needs lots of documentation.

module

Our hello world module is an up counter. Let's start by looking at the most basic form:

(module [:outputs [x ((uintm 8) 0)]]
  (connect x (inc x))

This constructs a module with one output, named x, which is initialized to a ((uintm 8) 0, and thus has type (uintm 8). After the bindings is the body, which is made of logic and connections. In this case, only one connection is made, which connects x to x + 1. This forms the basis of our counter. It returns the instantiated counter module with a random, unique name. If you'd like to assign a name, use:

(module counter [:outputs [x ((uintm 8) 0)]]
  (connect x (inc x))

defmodule

This will also return an instance with name counterXXXXX, where XXXXX is a unique number. Usually, you'll want to define a parameterizable module for reuse:

(defmodule counter [n] [:outputs [x ((uintm n) 0)]]
  (connect x (inc x))

In this example, we use defmodule to create a new var called counter, which is a function of 1 argument (defmodule takes an arglist, in this case [n]). (counter 8) returns an 8 bit counter, while (counter 3) returns a 3 bit counter.

Control flow

functional style

Suppose we wanted our counter to switch directions every time it overflows. We can add a feedback, dir, that will configure the direction of counting. We'll also need to change the direction each time it overflows:

(def dir-sym (enum #{:up :down}))
(defmodule bouncer [n] [:outputs [x ((uintm n) 1)]
                        :feedback [dir (dir-sym :up)]]
    (assert (pos? n)) ;can but normal Clojure code here for definition-time logic
    (connect x (mux2 (= dir :up) (inc x) (dec x)))
    (connect dir (cond
                   (= x 0) :up
                   (= (serialize x) ;convert x to bits
                      (reduce bit-cat ;repeatedly concatenate bits
                        (take n (repeat #b1))) ;get n single set bits
                     :down
                   :else dir ;no change))

imperative style

That looks really far to the right. How can we make it nicer? All conditionals support defining a connection along all of their branches. This includes union-match, cond, condp, and mux2. Also, multiple connections can be defined on each branch (not shown below):

(def dir-sym (enum #{:up :down}))
(defmodule bouncer [n] [:outputs [x ((uintm n) 1)]
                        :feedback [dir (dir-sym :up)]]
    (assert (pos? n)) ;can but normal Clojure code here for definition-time logic
    (mux2 (= dir :up)
      (connect x (inc x))
      (connect x (dec x)))
    (cond
      (= x 0) (connect dir :up)
      (= (serialize x) ;convert x to bits
         (reduce bit-cat ;repeatedly concatenate bits
                 (take n (repeat #b1)))) ;get n single set bits
        (connect dir :down)
      :else
        (connect dir dir) ;no change))

Much more readable!

Debugging

Currently the only debugging function is (pr-trace args... the-value), which takes any number of arguments that are concatenated into a tracing string, and the-value, which is printed every cycle. It works on all types.

There is also the more generate trace, which can be used to dump data into any format, such as VCD for a waveform viewer. To dump to VCD, use spit-trace to write it to a file, or use trace->gtkwave to automatically open the trace in GTKWave after dumping. Traces can be acquired

Tracing

A trace is a seq of maps from register names to their values in that cycle.

You can get a trace for a given set of registers by using trace-keys, defined in piplin.sim. trace-keys transforms the fns returned by make-sim into a new set of fns and a trace atom, which will contain the trace after running the sim. To get a seq of all registers in a module hierarchy, use get-all-registers. See module->verilog+testbench in piplin.test.verilog for example usage.

Simulation

See test/piplin/test/math.clj for examples of compiling and simulating a module. (make-sim mod) is used to make an initial-state and set of simulation function callbacks. Use (exec-sim state fns cycles) to simulate the system of fns, starting at the given state, for cycles cycles (i.e. this is a positive integer). The return value of exec-sim is a new state object, which is map-like. See the referenced code for examples of inspecting the state to verify assertions.

Synthesis

Use module->verilog to convert a module instance to Verilog. It returns a printlnable string. If you want to convert an entire module hierarchy, use modules->verilog, which returns a list of pairs where the first element is the module's name and the second element is the module's verilog. You can then write these to a series of files. Verilog also supports defining multiple modules in the same file. This is easiest, so you can just use modules->all-in-one to get everything in one file.

Use make-testbench to make a testbench for a module in Verilog. It returns a printlnable string. Its arguments are the module to test and a trace. It will then generate a Verilog program that will run that test and verify that the module under simulation produced the expected output with the given input.