Supports

supports statements can be used in coordination with require statements to automate pin assignment. We use supports to describe valid ways pins can be assigned. A supports statement creates a pending port.

Often supports are used to describe pin mappings on a processor (e.g. a i2c peripheral can map to pins here or here). The support mechanism is very flexible and can make arbitrarily complex mapping constraints.

The supports statement is valid in the following contexts:

Signature

; Single Option Form
supports <TYPE> :
  <REQUIRE-1>
  ...
  <TYPE>.<PORT-1> => <ASSIGN-1>
  <TYPE>.<PORT-2> => <ASSIGN-2>
  ...

; Multiple Option Form
supports <TYPE> :
  <REQUIRE-1>
  ...
  option:
    <REQUIRE-1>
    ...
    <TYPE>.<PORT-1> => <ASSIGN-1>
    <TYPE>.<PORT-2> => <ASSIGN-2>
    ...
  option:
    <REQUIRE-1>
    ...
    <TYPE>.<PORT-1> => <ASSIGN-3>
    <TYPE>.<PORT-2> => <ASSIGN-4>
    ...
  ...

  • &lt;TYPE> - Bundle type that identifies what type of pending port will be constructed
  • &lt;REQUIRE-1> - Optional require statements that can be used to created nested relationships.
  • &lt;TYPE>.&lt;PORT-1> - Target statement for one of the SinglePin ports of &lt;TYPE>.
  • &lt;ASSIGN-1> - A ref to a abstract port, component/module port, or
  • The &lt;TYPE>.&lt;PORT-1> => &lt;ASSIGN-1> statements are assignments.

Overview

The supports statement provide a means of constructing a pending port. A pending port on a component or module instance is used to satisfy require statement. A pending port isn't an object or instance like a port, abstract port, or net. It is more ethereal. It provides a means of defining the constraints for making a particular connection as opposed to being a particular port or pin.

Supports Example

pcb-bundle reset:
  pin p

pcb-component mcu:

  pin-properties:
    [pin:Ref | pads:Int ...]
    [RESET_n | 5 ]

  supports reset:
    reset.p => self.RESET_n

In this case, the support reset: statement is constructing a single pending port of type reset. It has one port with only one matching option, self.RESET_n. This support reset: statement acts like an interface definition for the reset signal.

Option Statements

The option statement is a mechanism by which we can define a set of acceptable options for this bundle type. You can think of a supports with option statements as a big element-wise OR gate.

Option Example

pcb-bundle gpio:
  pin p

pcb-component mcu:
  port PA : pin[16]

  supports gpio :
    option :
      gpio.p => self.PA[1]
    option :
      gpio.p => self.PA[4]

This supports statement constructs a single pending port of type gpio. This pending port can map to either:

  • self.PA[1] OR
  • self.PA[4]

In this case, there are only 2 options, but there is no limit. We could add an arbitrary number of option statements.

An Arbitrary Number You Say...

"An arbitrary number seems nice but wouldn't that get rather tedious?" - well yes, but actually no.

This is where typical programming control flow and loop constructs, like for, if, and while, come in. If we take our previous example and expand it from 2 options to 16 options, it might look something like this:

pcb-bundle gpio:
  pin p

pcb-compoent mcu:
  val num-pins = 16
  port PA : pin[ num-pins ]

  supports gpio :
    for i in 0 to num-pins do:
      option:
        gpio.p => self.PA[i]

Here - again - we only get 1 gpio pending port from this statement. But now that one gpio can use any of the available IO pins on the PA port of the microcontroller. Progress - now let's open it up a bit more...

Less is More

If we take one more crack at this example and expand our desire from 1 gpio pending port to 16 gpio pending ports with full pin-assignment across the port, we might end up here:

pcb-bundle gpio:
  pin p

pcb-component mcu:
  val num-pins = 16
  port PA : pin[ num-pins ]

  for i in 0 to num-pins do:
    supports gpio :
      gpio.p => self.PA[i]

We don't actually need the option: statement at all to achieve our goal. Just constructing 16 supports gpio: statements is sufficient to create a full cross-bar. Lets consider an example of how this gets used:

  inst host : mcu 
  require switches:gpio[4] from host

This statement basically says, "Give me 4 gpio ports - I don't care which ones right now, we'll decide that later. They just need to be GPIO ports."

This basicaly makes an implicit OR between all of the defined gpio type pending ports that aren't used for some other purpose.

Don't Forget All the Ports

Up to now we've been talking about bundles with a single port, but we can implement supports statements on arbitrarily complex bundles. The key things to remember are:

  1. Every port of a bundle must have an assignment to form a valid supports statement.
  2. In each option: statement, every port of a bundle must have an assignment to form a valid option statement.

Invalid Support Example

Consider the following as an example that breaks this rule:

pcb-bundle spi:
  pin sclk
  pin poci
  pin pico

pcb-component mcu:
  port PB : pin[16]

  supports spi:
    spi.sclk => self.PB[0]
    option:
      spi.poci => self.PB[1]
      spi.pico => self.PB[2]
    option:
      spi.poci => self.PB[3]
      spi.pico => self.PB[4]

On its face, this looks like a very reasonable structure, but unfortunately it doesn't follow the signatures defined above. The likely goal of this statement is spi.sclk => self.PB[0] for all options. We can implement that logic with explict statements in each option:

Correct Implementation

  supports spi:
    option:
      spi.sclk => self.PB[0]
      spi.poci => self.PB[1]
      spi.pico => self.PB[2]
    option:
      spi.sclk => self.PB[0]
      spi.poci => self.PB[3]
      spi.pico => self.PB[4]

Adding Properties on Assignment

It is often useful to annotate pins that are selected for a particular support function. To support this, properties can be supplied in either the supports statement or the option statements:

pcb-bundle gpio:
  pin p

pcb-bundle i2c:
  pin sda
  pin scl

pcb-component mcu:
  val num-pins = 16
  port PA : pin[ num-pins ]

  for i in 0 to num-pins do:
    supports gpio :
      gpio.p => self.PA[i]
      property(self.PA[i].is-gpio?) = true

  supports i2c:
    option:
      i2c.sda => self.PA[4]
      i2c.scl => self.PA[5]
      property(self.PA[4].is-i2c?) = true
      property(self.PA[5].is-i2c?) = true

    option:
      i2c.sda => self.PA[11]
      i2c.scl => self.PA[12]
      property(self.PA[11].is-i2c?) = true
      property(self.PA[12].is-i2c?) = true

The property statement at the end of the support/option statement only activates if this support/option is selected by the pin assignment solver. Note that this action does not necessarily happen at compile time. It may happen days or weeks later when a component in the layout moves or a route is completed.

Modules Work Too!

Up to now, we've primarily been referencing components, but don't forget that these techniques apply to pcb-module as well. In fact, modules are where pin assignment can really shine.

Whenever you have multiple components that have a strict constraint between them, use of supports statements in the module can help abstract the details of these complex constraints. They make our code more readable and grok-able.

Let's consider an example where we want to make multiple simple RC filters. Here is a module definition for an RC filter.

This example is intentionally simplfied and does not contain a lot of the detailed engineering you might want in a real circuit, like voltage specs, tolerances, etc. This is primarily for demonstration purposes.


pcb-module low-pass (freq:Double, R-val:Double = 10.0e3) :
  port vin : pin
  port vout : pin
  port gnd : pin

  val C-val =   1.0 / ( 2.0 * PI * R-val * freq)
  val C-val* = closest-std-val(C-val, 0.2)

  inst R : chip-resistor(R-val)
  inst C : ceramic-cap(C-val*)

  net (vin, R.p[1])
  net (R.p[2], C.p[1], vout)
  net (C.p[2], gnd)

This filter has a 3-pin interface: vin, vout, and gnd

We want to construct a module that allows us to instantiate some number of these filters, but not necessarily lock ourselves into a specific pin assignment. This is where the supports statements come in at the module level.

We do need to make sure that the input and output pins match though. It would do us no good if the input for channel 1 matched to the output of channel 3. This is where pass-through bundle comes into play:

pcb-bundle pass-through:
  port A : pin
  port B : pin

pcb-module multi-lpf (ch:Int, freq:Double) :
  port gnd : pin

  inst lpfs : low-pass(freq)[ch]

  for i in 0 to ch do:
    net (gnd, lpfs[i].gnd)

  for i in 0 to ch do:
    supports pass-through:
      pass-through.A => lpfs[i].vin
      pass-through.B => lpfs[i].vout

The pass-through bundle defines two ports that are matched. These are our vin/vout pair.

Then we construct a pending port of type pass-through for each channel of the multi-lpf definition. With this construction, we get the ability to use the RC filters in any channel location on the board.

Note that the above video just shows the first solution that the Pin Assignment solver was able to deduce. You can route to any valid pin as defined by the require/supports statements of the pin assignment problem.

Here is a link to a complete code example

Nested Require/Support Statements

To make complex constraints, we will often use a cascade of supports statements. We generally call this a Nested require/supports statement. This structure allows us to break down a complex constraint into several simpler constraints and combine them together.


public val UART:Bundle = uart([UART-RX UART-TX UART-RTS UART-CTS])

public pcb-component my-component :
  port PA : pin[10]

  ; Internal (Private) Bundle Definition
  pcb-bundle io-pin : (pin p)

  for i in 5 to 10 do:
    supports io-pin :
      io-pin.p => PA[i]

  supports UART :
    require pins:io-pin[4]
    UART.tx => pins[0].p
    UART.rx => pins[1].p
    UART.rts => pins[2].p
    UART.cts => pins[3].p

Our goal is to create a UART pending port that uses any of PA[5], PA[6], PA[7], PA[8], or PA[9] as any of the tx, rx, rts, or cts pins of our UART. This is combinatorics problem.

We accomplish this in two steps:

  1. Define the set of pins from PA that we can select from.
  2. Select from that set to full fill one UART pending port

We create two kinds of supports statements:

  • io-pin - This is a private bundle type that only exists within my-component.
    • This defines the set of pins from PA that we can select from.
  • UART - Customized UART bundle with our specific pin configuration.
    • This implements the "Select N from K" using a nested require statement.