Skip to content

Bundle Statements

A pcb-bundle statement defines a hierarchical group of ports, used to organize and structure a JITX design. Bundles can include ports or even other bundles. Using bundles allows us to structure our designs: we can connect bundles with net statements, or use supports and require statements with bundles for automated pin assignment.

Signature

pcb-bundle bundle-name (arg1:<Type1>, ...) :
  name = <String|False>
  description = <String|False>
  ; Any number of port statements:
  port <REF>
  port <REF> : <PortType>

The expression bundle-name identifies this bundle definition.

The argument list (arg1:&ltType1>, ...) is optional, providing a way to parameterize bundles by arbitrary JITX values. We will refer to a parameterized bundle as a bundle generator.

Statements: * name - This is an optional statement setting the name of the bundle * description - This is an optional statement providing a description of the bundle * port - A bundle can contain an arbitrary number of port statements. There are two ways to construct a port: - port &ltREF> is a shorthand for constructing a single-pin port. - port &ltREF> : &ltPortType> allows the construction of arbitrary ports -- either single-pins, bundle instances, or arrays of ports. For more information, see the port statement.

Usage

Bundles are the key to providing structured interfaces between different components and modules. The most straightforward use of bundles is to provide a named and reusable set of ports:

pcb-bundle i2c:
  port sda
  port scl

This allows us to easily instantiate an i2c bundle with a port statement:

pcb-component my-component :
  ; An instance of the i2c bundle
  port my-i2c-port : i2c

Importantly, bundles can be parametric, and can reference other bundles hierarchically. Here is a definition for a variable-size bank of lvds ports:

; A bundle representing a differential pair
pcb-bundle diff-pair :
  port N
  port P

; lvds-bank is a hierarchical bundle definition
; It includes a clock differential pair `clk`
; and a port array `data` of multiple differential pairs
pcb-bundle lvds-bank (width:Int) :
  port clk : diff-pair
  port data : diff-pair[width]

Our lvds-bank bundle is parameterized by the size of the data bus. This concept is reminiscent of record types in VHDL, or struct in SystemVerilog.

Another use of parametric bundles is to represent optional ports. The definition of uart in the JITX standard library does just this in order to provide a flexible definition of a UART bundle.

; An enumeration of possible UART ports 
public pcb-enum jsl/bundles/comms/UARTPins :
  UART-DTR
  UART-CTS
  UART-DCD
  UART-RI
  UART-DST
  UART-RTS
  UART-CK
  UART-DE
  UART-RX
  UART-TX

; A parameterized bundle definition 
pcb-bundle uart (pins:Collection<UARTPins>):
  name = "UART"
  for p in pins do :
    switch(p) :
      UART-DTR : make-pin(`dtr)
      UART-CTS : make-pin(`cts)
      UART-DCD : make-pin(`dcd)
      UART-RI  : make-pin(`ri)
      UART-DST : make-pin(`dst)
      UART-RTS : make-pin(`rts)
      UART-CK  : make-pin(`ck)
      UART-DE  : make-pin(`de)
      UART-RX  : make-pin(`rx)
      UART-TX  : make-pin(`tx)

We can then use this parameterized bundle to define a minimal UART interface:

public defn minimal-uart () :
  uart(UART-RX, UART-TX)

However, the parent bundle is flexible, and can be used to define more sophisticated variants of UART as well.

Pin Assignment

Bundles are the key to automatic pin assignment. Supports and require statements use bundles exclusively.

In this simple example, we request one of the available i2c ports from a microcontroller.

pcb-module my-module :
  inst mcu : stm32f405G7

  require bus:i2c from mcu

Parameterized bundles allow a notion of inheritance during pin assignment. Consider two versions of a UART bundle, and a component that supports both:

public defn minimal-uart () :
  uart(UART-RX, UART-TX)

; "flow control" UART
public defn fc-uart () :
  uart(UART-TX, UART-RX, UART-CTS, UART-RTS)

pcb-component mcu :
  port p : pin[100]

  supports minimal-uart() :
    minimal-uart().UART-RX => self.p[1]
    minimal-uart().UART-TX => self.p[2]

  supports fc-uart() :
    fc-uart().UART-RX => self.p[3]
    fc-uart().UART-TX => self.p[4]
    fc-uart().UART-CTS => self.p[5]
    fc-uart().UART-RTS => self.p[6]

Now, if we instantiate mcu, and require a minimal-uart port, JITX can solve the pin assignment problem by using either our microcontroller's support for minimal-uart or fc-uart.

pcb-module main-module :
  inst mcu : mcu
  require uart-port : minimal-uart() from self.mcu

JITX is free to use either ports mcu.p[1], mcu.p[2] or ports mcu.p[3], mcu.p[4] to satisfy the require statement above. Even though ports mcu.p[3] and mcu.p[4] support the fc-uart() bundle instead of the required minimal-uart() bundle, the following conditions are met: 1. fc-uart() is a strict superset of minimal-uart(); it contains every port found in minimal-uart(). 2. Both fc-uart() and minimal-uart() come from the same generator. Both are instances of the same parameterized bundle, in this case, uart(). If we had instead defined two different bundles (pcb-bundle minimal-uart and pcb-bundle fc-uart), the pin solver would not know that fc-uart "inherits" from minimal-uart, even if it still had a superset of its ports.

For example, consider hypothetical bundles for i2c and i3c:

pcb-bundle i2c :
  port sda
  port scl

pcb-bundle i3c :
  port sda
  port scl

Even though i3c has the same port statements as i2c, and in practice i3c is compatible with i2c, JITX will not be able to satisfy i2c constraints using i3c ports, since they do not share a parent.

Introspection

Consider once again the following simple bundles:

pcb-bundle diff-pair :
  port N
  port P

pcb-bundle lvds-bank (width:Int) :
  port clk : diff-pair
  port data : diff-pair[width]

In a JITX program, the values diff-pair and lvds-bank(4) both have type Bundle, where Bundle is a subtype of JITXDef.

This gives us access to powerful introspection functionality. For example, we can write ports(diff-pair) to iterate over all ports in the bundle definition. In fact, all of the normal JITX introspection idioms will work on bundle definitions; however, it is important to remember that we are reasoning about the definition, not an instance.

For example, consider the following simple design:

pcb-component my-component :
  port dp : diff-pair

pcb-module my-module :
  inst c : my-component

  ; Will print "c" for both ports, since they
  ; belong to the component instance 
  for p in ports(c.dp) :
    println(ref(containing-instance(p)))

  ; Will print "false" for both ports.
  ; These come from the bundle definition.
  for p in ports(diff-pair) :
    println(containing-instance(p))

Besides the usual introspection functions in jitx/commands, it is often useful to reason directly about the Bundle. For example, we can write code like this:

val pt = port-type(bus)
match(pt):
  (b:Bundle): if b == diff-pair : ...