Skip to content
dgrnbrg edited this page Apr 23, 2012 · 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.

Functions

= and not= work on all types.

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 isn't complete yet, but it will be very soon.

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.

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.

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 Clojure seq of maps that contain the values of the inputs on each cycle and the expected outputs for each cycle. 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. Although it doesn't yet exist, this can be automatically computed using the exec-sim function or the trace function.

Clone this wiki locally