Skip to content
copperhead.sh
Get started

Examples

Sensor board

A regulated board with an MCU and an I2C sensor on it. This is the example to read to understand lowering: what happens between a typed interface and the pads it lands on.

the interfaces view
The interfaces view

sensor_board.py gives the MCU two candidate pin pairs for its I2C peripheral:

pinmap = PinMap({
"power.vcc": "VDD", "power.gnd": "VSS",
"i2c.scl": ["PB8", "PB6"],
"i2c.sda": ["PB9", "PB7"],
})

self.mcu.i2c >> self.imu.i2c is one line, and it has a choice inside it. The lowering resolves that choice and records a Decision entity naming what it picked and what it passed over. out/rationale.md is those decisions, resolved back to the names in the program:

Which pin of system.mcu carries i2c.scl? → system.mcu.PB8

The pull-ups are on the board rather than in the parts, because on an open-drain bus they belong to the net. The regulator constraint states the intent (output_current_max >= 100 * mA) instead of asserting an answer.

7 parts, 4 nets, 71 entities, 11 checks. None failed, four undecided.

The undecided ones are the reason this example exists. out/checks.txt says why each is undecided, naming the value that is missing rather than the check that is failing:

UNDECIDED interface_compatibility: voltage domain is undecided on power_input: voltage unknown; on PORT-b2153fd0d1b8

Nothing here assumed 3.3 V and moved on. Undecided is a third truth value, and it is the honest one until someone supplies the number.

the ground view
The ground view
Terminal window
fang check examples/sensor_board/sensor_board.py
fang view examples/sensor_board/sensor_board.py interfaces -o interfaces.svg
fang build examples/sensor_board/sensor_board.py
examples/sensor_board/sensor_board.py
"""A regulated sensor board: a rail, an MCU and an I2C sensor on it.
Show 5 more lines
Shows the parts the toolchain is actually for: typed interfaces lowering to
pins, a decision recorded where the MCU offered a choice and constraints that
stay undecided until someone supplies the missing datasheet number.
"""
from fang.interfaces import I2CPort, Pin, PinMap, PowerIn
from fang.lang import A, Part, System, V, kHz, kOhm, mA, require, uF
from fang.parts import Capacitor, Regulator, Resistor
class MCU(Part):
"""A microcontroller with two possible pin pairs for its I2C peripheral."""
designator_prefix = "U"
power = PowerIn(voltage=3.3 * V, current_demand=80 * mA)
i2c = I2CPort(
voh_min=2.4 * V,
vol_max=0.4 * V,
voltage=3.3 * V,
bit_rate=400 * kHz,
pull_up_resistance=4.7 * kOhm,
pull_up_supply=3.3 * V,
)
VDD = Pin("VDD", role="power", number="1")
VSS = Pin("VSS", role="ground", number="2")
PB8 = Pin("PB8", role="clock", number="61")
PB9 = Pin("PB9", role="data", number="62")
PB6 = Pin("PB6", role="clock", number="58")
PB7 = Pin("PB7", role="data", number="59")
pinmap = PinMap(
{
"power.vcc": "VDD",
"power.gnd": "VSS",
# Two candidates each: the lowering picks one and records why.
"i2c.scl": ["PB8", "PB6"],
"i2c.sda": ["PB9", "PB7"],
}
)
class IMU(Part):
"""An inertial sensor. Its logic thresholds come from the datasheet."""
designator_prefix = "U"
power = PowerIn(voltage=3.3 * V, current_demand=3 * mA)
i2c = I2CPort(vih_min=2.0 * V, vil_max=0.8 * V, voltage=3.3 * V, bit_rate=400 * kHz)
VDD = Pin("VDD", role="power", number="1")
GND = Pin("GND", role="ground", number="2")
SCL = Pin("SCL", role="clock", number="3")
SDA = Pin("SDA", role="data", number="4")
pinmap = PinMap(
{"power.vcc": "VDD", "power.gnd": "GND", "i2c.scl": "SCL", "i2c.sda": "SDA"}
)
class SensorBoard(System):
supply = PowerIn(voltage=5 * V, current_capability=1 * A)
regulator = Regulator(
input_voltage_max=17 * V,
output_voltage=3.3 * V,
output_current_max=1000 * mA,
package="SOT-23-5",
)
mcu = MCU(package="LQFP-64")
imu = IMU(package="LGA-14")
bulk = Capacitor(capacitance=10 * uF, voltage_rating=16 * V, package="C_0805")
scl_pullup = Resistor(resistance=4.7 * kOhm, package="R_0402")
sda_pullup = Resistor(resistance=4.7 * kOhm, package="R_0402")
def architecture(self):
self.supply >> self.regulator.vin
self.regulator.vout >> self.mcu.power
self.regulator.vout >> self.imu.power
self.mcu.i2c >> self.imu.i2c
self.regulator.vout.vcc >> self.bulk.p1
self.regulator.vout.gnd >> self.bulk.p2
# Open drain: without these the bus never comes back up.
self.regulator.vout.vcc >> self.scl_pullup.p1
self.scl_pullup.p2 >> self.mcu.i2c.scl
self.regulator.vout.vcc >> self.sda_pullup.p1
self.sda_pullup.p2 >> self.mcu.i2c.sda
def constraints(self):
require(self.regulator.output_voltage == 3.3 * V)
# The regulator must carry both loads. Stated as intent; the kernel
# decides once every current figure is known.
require(self.regulator.output_current_max >= 100 * mA)

The parts, then the nets and the pads on them.

out/netlist.txt
C1 10 uF Package:C_0805
R1 4.7 kOhm Package:R_0402
R2 4.7 kOhm Package:R_0402
U1 IMU Package:LGA-14
U2 MCU Package:LQFP-64
U3 Regulator Package:SOT-23-5
Net-(C1-Pad1) C1.1 R1.1 R2.1 U1.VDD U2.VDD U3.VOUT
Net-(C1-Pad2) C1.2 U1.GND U2.VSS U3.GND
Net-(R1-Pad2) R1.2 U1.SCL U2.PB8
Net-(R2-Pad2) R2.2 U1.SDA U2.PB9

Every check that ran, and every one left undecided.

out/checks.txt
UNDECIDED interface_compatibility: current capability is undecided on electrical: current_capability unknown
UNDECIDED interface_compatibility: voltage domain is undecided on electrical: voltage unknown; on PORT-c75102d9787d
UNDECIDED interface_compatibility: current capability is undecided on power_input: current_demand unknown
UNDECIDED interface_compatibility: voltage domain is undecided on power_input: voltage unknown; on PORT-b2153fd0d1b8
11 checks, 0 failed, 4 undecided

What the elaborated graph contains, by entity kind.

out/graph.txt
1 block
6 component
22 connection
2 constraint
4 decision
4 interface
19 pin
13 port
71 total
snapshot sha256:8570fadf033061093c00daeccc09e24d78b137a9940d71a7206cb5b7e50ac56c

All of it, including the KiCad netlist, is in examples/sensor_board/out/. Rebuild it with:

Terminal window
fang build examples/sensor_board/sensor_board.py