Skip to content

Ports

port is a JITX statement that defines the electrical connection points for a component or module.

The port statement can be used in the following contexts:

Each port statement in any of these contexts is public by default. This means that each port can be accessed externally by using dot notation.

Signature

  ; Shorthand for single-pin port
  port <NAME>
  ; Port with concrete type
  port <NAME> : <TYPE>
  • &lt;NAME> - Symbol name for the port in this context. This name must be unique in the current context.
  • &lt;TYPE> - The type of port to construct. This can be the keyword pin for a single-pin port, a pcb-bundle, or an array:
    • pin: A single-pin port. Pins are the fundamental building-blocks of ports.
    • &lt;BUNDLE>: The name of a pcb-bundle representing a set of ports
    • &lt;TYPE>[&lt;ARRAY:Int|Tuple]: An array initializer. Either an integer n, initializing an array of n sequential elements indexed from 0, or a tuple [i_0, i_1, i_2, ..., i_n] of indices. In the latter case, the indices do not need to be sequential.

Port types are recursive: we can initialize arrays of pins, bundles, or even arrays.

Watch Out! - There is no space between &lt;TYPE> and the [ opening bracket of the array initializer.

Syntax

There are multiple forms of the port statement to allow for flexible construction of the interface to a particular component. The following sections outline these structures.

Basic Port Declaration

The user has two options when declaring a single pin:

; Single Pin 'a'
port a : pin
; Equivalent to
port a

The port a structure is a shorthand version for declaring single pins.

Pin Arrays

To construct a bus of single pins, we use the array constructor:

port contiguous-bus : pin[6]
port non-contiguous-bus : pin[ [2, 5, 6] ]

println(indices(non-contiguous-bus))
; Prints:
; [2 5 6]

The contiguous-bus port is defined as a contiguous array of pins with indices: 0, 1, 2, 3, 4, & 5.

The non-contiguous-bus port is defined an array with explicit indices: 2, 5, & 6. The indices function is a helper function for determining what array indices are available on this port.

Attempting to access a port index that does not exist or is negative will result in an error.

We can also define higher-dimensional arrays, and access them in the same way:

pcb-module multi-dim-ports :
  ; A two-dimensional array of pins
  port two-d-bus : pin[2][4]
  println(indices(two-d-bus))
  ; Prints:
  ; [0 1 2 3]
  println(indices(two-d-bus[0]))
  ; Prints:
  ; [0 1]

Bundle Port

A bundle port is a connection point that consolidates multiple individual signals into one entity. The structure of this port is defined by the pcb-bundle type used in its declaration:

pcb-bundle i2c:
  port sda
  port scl

pcb-module mcu:
  port data : i2c

In this example, we define the i2c bundle. Notice that this bundle is constructed from same port statements defined above.

The data port is then constructed with the bundle name i2c as the type.

The individual signals of the data port are accessible using dot notation:

pcb-module top-level :

  inst U1 : mcu

  println(port-type(mcu.data.sda))

; Prints:
;  [SinglePin object]

Bundle Array Port

As with the single pin, we can construct port arrays of bundle types:

pcb-bundle i2c:
  port sda
  port scl

pcb-module mcu:
  port busses : i2c[3]

In this example, we construct a contiguous array of 3 I2C bus ports. We can access the individual signals of these busses as well:

pcb-module top-level :

  inst U1 : mcu

  println(port-type(mcu.busses[0].scl))

; Prints:
;  [SinglePin object]

Port Types

Each port has a type associated with it. That type can be accessed with the function port-type . This function returns a PortType instance that can be one of the following types: Each port has a type associated with it. That type can be accessed with the function port-type . This function returns a PortType instance that can be one of the following types:

  • SinglePin - A port instance of type pin
  • Bundle - A port instance of type Bundle
  • PortArray - An array of ports

We would typically use the result of this function with a match statement:

pcb-module amp:

  port vout : pin[4]

  match(port-type(vout)):
    (s:SinglePin): println("Single")
    (b:Bundle): println("Bundle")
    (a:PortArray): println("Array")

This structure allows a different response depending on the type of port.

The Bundle PortType

The Bundle port type provides the user with access to the type of bundle that was used to construct this port:

pcb-bundle i2c:
  port sda
  port scl

pcb-module mcu:
  port bus : i2c

pcb-module top-level:
  inst U1 : mcu

  match(port-type(U1.bus)):
    (b:Bundle):
      println("Bundle Type: %_" % [name(b)])
    (x): println("Other")

; Prints:
; Bundle Type: i2c

Notice that the b object is a reference to the pcb-bundle i2c definition. This provides a convenient way to check if a given port matches a particular bundle type.

Walking a PortArray

It is often useful to walk a PortArray instance and perform some operation on each of the SinglePin instances of that PortArray. Because PortArray instances can be constructed from either single pins or bundles, they can form arbitrarily complex trees of signals. To work with trees of this nature, recursion is the tool of choice.

Note: This is a more advanced example with recursion. Fear not brave electro-adventurer - These examples will serve you well as you become more comfortable with the JITX Environment.

defn walk-port (f:(JITXObject -> False), obj:JITXObject) -> False :
  match(port-type(obj)):
    (s:SinglePin): f(obj)
    (b:Bundle|PortArray):
      for p in pins(obj) do:
        walk-port(f, p)

pcb-module top-level:
  port bus : i2c[3]

  var cnt = 0
  for single in bus walk-port :
    cnt = cnt + 1

  println("Total Pins: %_" % [cnt])

; Prints
; Count: 1
; Count: 2
; Count: 3
; Count: 4
; Count: 5
; Count: 6

The i2c bundle has 2 pins and there are 3 i2c ports in the array which results in 6 total pins.

In this example, we construct a Sequence Operator that will allow us to walk the pins of a port. The structure of a Sequence Operator is:

defn seq-op (func, seq-obj) :
  ...

Where the seq-obj is a Sequence of objects. The for statement will iterate over the objects in the seq-obj sequence and then call func on each of the objects in the sequence. The value returned by this function can optionally be captured and returned.

In our example, walk-port is a Sequence Operator that doesn't return any result. Notice how walk-port has replaced the do operator that we normally see in a for loop statement.

So where does the func function come from then? The for statement constructs a function from the body of the for statement. In this example, the function effectively becomes:

defn func (x:JITXObject) -> False :
  cnt = cnt + 1

Notice that this function is a closure. It is leveraging the top-level context to access the cnt variable defined before the for statement.

A similar construction in Python might look like:

  cnt = 0
  def func(signal):
    global cnt
    cnt = cnt + 1

  for x in walk-port(bus):
    func(x)

Where walk-port would need to be implemented as a generator in python that constructs a sequence.