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>
...
...
<TYPE>
- Bundle type that identifies what type of pending port will be constructed<REQUIRE-1>
- Optionalrequire
statements that can be used to created nested relationships.<TYPE>.<PORT-1>
- Target statement for one of theSinglePin
ports of<TYPE>
.<ASSIGN-1>
- A ref to aabstract port
, component/module port, or- The
<TYPE>.<PORT-1> => <ASSIGN-1>
statements areassignments
.
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:
port 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:
port 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]
ORself.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:
port p
pcb-component 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:
port 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 basically 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:
- Every port of a bundle must have an assignment to form a valid
supports
statement. - In each
option:
statement, every port of a bundle must have an assignment to form a validoption
statement.
Invalid Support Example
Consider the following as an example that breaks this rule:
pcb-bundle spi:
port sclk
port poci
port 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 explicit 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:
port p
pcb-bundle i2c:
port sda
port 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 simplified 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 : (port 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:
- Define the set of pins from
PA
that we can select from. - 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 withinmy-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.