Skip to content
copperhead.sh
Get started

Examples

Servo drive

Three half-bridges, CAN, an encoder and a shunt per phase: the board this toolchain was written for. It is the composition example, the largest program here built from the smallest amount of repetition.

the system view
The system view

servo_drive.py declares the bridge once:

class HalfBridge(Module):
high = Transistor(vds_max=100 * V, ..., package="PowerPAK-SO8")
low = Transistor(vds_max=100 * V, ..., package="PowerPAK-SO8")
shunt = Resistor(resistance=2 * mOhm, power_rating=2 * W, package="R_2512")
def architecture(self):
self.high.source >> self.low.drain
self.low.source >> self.shunt.p1
def constraints(self):
require(self.high.vds_max >= 60 * V)
require(self.shunt.power_rating >= 1 * W)

and instantiates it three times. A block owns its interior: its parts, its connections and its constraints. So the drive connects to the bridge’s edge and never reaches inside it. The three constraint sets become three separate constraints in the graph, decided separately, and a change to one phase does not quietly pass because the other two are fine.

The phase, the encoder and the CAN bus are typed ports, so a connection says what it carries rather than which pad it happens to land on.

24 parts, 26 nets, 310 entities, 46 checks. None failed, seven undecided. It is the biggest graph in the examples by a factor of two.

The three bridge_* blocks sit under the rule at the bottom, because the system view draws electrical connection and a block connects through the parts inside it. Their transistors are labelled bridge_u.high, bridge_v.high and so on. The path is what distinguishes three instances of one declaration.

the power view
The power view
the safety view
The safety view
Terminal window
fang check examples/servo_drive/servo_drive.py
fang graph examples/servo_drive/servo_drive.py
fang view examples/servo_drive/servo_drive.py safety -o safety.svg
examples/servo_drive/servo_drive.py
"""A three-phase servo drive: the board this toolchain was written for.
Show 7 more lines
Everything here is composition. A `HalfBridge` is a block with two transistors and
a shunt: it owns its own interior and its own constraints, and the drive
instantiates three of them from one declaration. The phase, the encoder and the
CAN bus are typed ports, so a connection says what it carries rather than which
pad it happens to land on.
"""
from fang.interfaces import (
AnalogIn,
CANPort,
EncoderPort,
MotorPhase,
PWMPort,
Pin,
PinMap,
PowerIn,
PowerOut,
)
from fang.lang import (
A,
Module,
Ohm,
Part,
Signal,
System,
V,
W,
kHz,
mA,
mOhm,
mW,
nF,
require,
uF,
)
from fang.parts import Capacitor, Connector, Regulator, Resistor, Transistor
from fang.rationale import Assumes, Requires
class HalfBridge(Module):
"""Two FETs, the node between them and the shunt that measures it.
Show 4 more lines
The block is the unit of reuse: one declaration, three instances and one set
of constraints that holds for each of them.
"""
high = Transistor(
vds_max=100 * V,
vgs_threshold=3 * V,
id_max=60 * A,
rds_on=4 * mOhm,
package="PowerPAK-SO8",
)
low = Transistor(
vds_max=100 * V,
vgs_threshold=3 * V,
id_max=60 * A,
rds_on=4 * mOhm,
package="PowerPAK-SO8",
)
shunt = Resistor(resistance=2 * mOhm, power_rating=2 * W, package="R_2512")
def architecture(self):
# The interior: a totem pole over a shunt. The midpoint is the phase and
# the top of the shunt is the current measurement.
self.high.source >> self.low.drain
self.low.source >> self.shunt.p1
def constraints(self):
# The bus is 24 V nominal but a decelerating motor pushes it up; 100 V
# parts on a 24 V bus is the usual margin for a regenerative load.
require(self.high.vds_max >= 60 * V)
require(self.low.vds_max >= 60 * V)
# 20 A through 2 mOhm is 0.8 W. The shunt is rated for more than double
# that, because it sits next to two FETs that are also warm.
require(self.shunt.resistance <= 5 * mOhm)
require(self.shunt.power_rating >= 1 * W)
# A logic-level gate: the driver's rail has to clear the threshold with
# room to spare, or the FET spends its life half on.
require(self.high.vgs_threshold <= 4 * V)
require(self.low.vgs_threshold <= 4 * V)
class GateDriver(Part):
"""One PWM in, a complementary gate pair out, per phase."""
designator_prefix = "U"
power = PowerIn(voltage=12 * V, current_demand=50 * mA)
pwm = PWMPort(vih_min=2 * V, vil_max=0.8 * V, voltage=3.3 * V, bit_rate=20 * kHz)
high_out = Signal()
low_out = Signal()
VCC = Pin("VCC", role="power", number="1")
GND = Pin("GND", role="ground", number="2")
IN = Pin("IN", role="control", number="3")
HO = Pin("HO", role="control", number="7")
LO = Pin("LO", role="control", number="5")
pinmap = PinMap(
{
"power.vcc": "VCC",
"power.gnd": "GND",
"pwm.out": "IN",
"high_out.line": "HO",
"low_out.line": "LO",
}
)
class Controller(Part):
"""The MCU: three PWM outputs, three current sense inputs, CAN, encoder."""
designator_prefix = "U"
power = PowerIn(voltage=3.3 * V, current_demand=120 * mA)
pwm_u = PWMPort(voh_min=2.9 * V, vol_max=0.4 * V, voltage=3.3 * V, bit_rate=20 * kHz)
pwm_v = PWMPort(voh_min=2.9 * V, vol_max=0.4 * V, voltage=3.3 * V, bit_rate=20 * kHz)
pwm_w = PWMPort(voh_min=2.9 * V, vol_max=0.4 * V, voltage=3.3 * V, bit_rate=20 * kHz)
sense_u = AnalogIn(voltage=3.3 * V, impedance=1 * Ohm)
sense_v = AnalogIn(voltage=3.3 * V, impedance=1 * Ohm)
sense_w = AnalogIn(voltage=3.3 * V, impedance=1 * Ohm)
encoder = EncoderPort(vih_min=2 * V, vil_max=0.8 * V, voltage=3.3 * V)
can_tx = Signal()
can_rx = Signal()
VDD = Pin("VDD", role="power", number="1")
VSS = Pin("VSS", role="ground", number="2")
PA8 = Pin("PA8", role="control", number="41")
PA9 = Pin("PA9", role="control", number="42")
PA10 = Pin("PA10", role="control", number="43")
PC0 = Pin("PC0", role="analog", number="8")
PC1 = Pin("PC1", role="analog", number="9")
PC2 = Pin("PC2", role="analog", number="10")
PB4 = Pin("PB4", role="data", number="56")
PB5 = Pin("PB5", role="data", number="57")
PB6 = Pin("PB6", role="data", number="58")
PB8 = Pin("PB8", role="data", number="61")
PB9 = Pin("PB9", role="data", number="62")
pinmap = PinMap(
{
"power.vcc": "VDD",
"power.gnd": "VSS",
"pwm_u.out": "PA8",
"pwm_v.out": "PA9",
"pwm_w.out": "PA10",
"sense_u.signal": "PC0",
"sense_v.signal": "PC1",
"sense_w.signal": "PC2",
"encoder.a": "PB4",
"encoder.b": "PB5",
"encoder.index": "PB6",
"can_tx.line": "PB9",
"can_rx.line": "PB8",
}
)
class CANTransceiver(Part):
"""TTL on the logic side, a differential pair on the bus side."""
designator_prefix = "U"
power = PowerIn(voltage=3.3 * V, current_demand=70 * mA)
bus = CANPort(voltage=3.3 * V, bit_rate=1000 * kHz)
txd = Signal()
rxd = Signal()
VCC = Pin("VCC", role="power", number="3")
GND = Pin("GND", role="ground", number="2")
TXD = Pin("TXD", role="data", number="1")
RXD = Pin("RXD", role="data", number="4")
CANH = Pin("CANH", role="differential_p", number="7")
CANL = Pin("CANL", role="differential_n", number="6")
pinmap = PinMap(
{
"power.vcc": "VCC",
"power.gnd": "GND",
"txd.line": "TXD",
"rxd.line": "RXD",
"bus.canh": "CANH",
"bus.canl": "CANL",
}
)
class MotorConnector(Connector):
"""Three phases out to the windings."""
designator_prefix = "J"
phase_u = MotorPhase(voltage=24 * V, current_demand=15 * A)
phase_v = MotorPhase(voltage=24 * V, current_demand=15 * A)
phase_w = MotorPhase(voltage=24 * V, current_demand=15 * A)
U = Pin("U", role="phase", number="1")
V_ = Pin("V", role="phase", number="2")
W = Pin("W", role="phase", number="3")
pinmap = PinMap({"phase_u.phase": "U", "phase_v.phase": "V", "phase_w.phase": "W"})
class EncoderConnector(Connector):
designator_prefix = "J"
encoder = EncoderPort(voh_min=2.9 * V, vol_max=0.4 * V, voltage=3.3 * V)
A_ = Pin("A", role="data", number="1")
B_ = Pin("B", role="data", number="2")
Z = Pin("Z", role="data", number="3")
pinmap = PinMap({"encoder.a": "A", "encoder.b": "B", "encoder.index": "Z"})
class BusConnector(Connector):
designator_prefix = "J"
can = CANPort(voltage=3.3 * V, bit_rate=1000 * kHz)
H = Pin("CANH", role="differential_p", number="1")
L = Pin("CANL", role="differential_n", number="2")
pinmap = PinMap({"can.canh": "CANH", "can.canl": "CANL"})
class BusTerminal(Connector):
"""The lugs the 24 V bus arrives on."""
designator_prefix = "J"
dc = PowerOut(voltage=24 * V, current_capability=20 * A)
VP = Pin("V+", role="power", number="1")
VN = Pin("V-", role="ground", number="2")
pinmap = PinMap({"dc.vcc": "V+", "dc.gnd": "V-"})
class ServoDrive(System):
dc_bus = PowerIn(voltage=24 * V, current_capability=20 * A)
thermal = Requires(
"Conduction loss per bridge stays under 2 W at 15 A continuous",
validation="analysis",
)
switching_loss = Assumes(
"Switching loss at 20 kHz is small beside conduction loss",
rationale="not yet computed; true for these gate charges at this frequency",
)
bus_in = BusTerminal(
current_rating=30 * A, voltage_rating=60 * V, package="TerminalBlock_1x02_P7.62mm"
)
# The two housekeeping rails. A drive that only had the bus would still
# need these, so they belong to the drive rather than to the board it is on.
gate_rail = Regulator(
input_voltage_max=60 * V,
output_voltage=12 * V,
output_current_max=500 * mA,
package="SOT-223",
)
logic_rail = Regulator(
input_voltage_max=15 * V,
output_voltage=3.3 * V,
output_current_max=1 * A,
package="SOT-23-5",
)
controller = Controller(package="LQFP-64")
transceiver = CANTransceiver(package="SOIC-8")
driver_u = GateDriver(package="SOIC-8")
driver_v = GateDriver(package="SOIC-8")
driver_w = GateDriver(package="SOIC-8")
bridge_u = HalfBridge()
bridge_v = HalfBridge()
bridge_w = HalfBridge()
motor = MotorConnector(
current_rating=20 * A, voltage_rating=60 * V, package="TerminalBlock_1x03_P7.62mm"
)
encoder_port = EncoderConnector(
current_rating=1 * A, voltage_rating=12 * V, package="JST_PH_1x03_P2.00mm"
)
can_port = BusConnector(
current_rating=1 * A, voltage_rating=12 * V, package="JST_PH_1x02_P2.00mm"
)
# 120 Ohm, because this node is one end of the bus rather than a stub on it.
termination = Resistor(resistance=120 * Ohm, power_rating=250 * mW, package="R_0805")
bus_bulk = Capacitor(capacitance=470 * uF, voltage_rating=63 * V, package="Radial_D10")
logic_bypass = Capacitor(capacitance=100 * nF, voltage_rating=16 * V, package="C_0402")
def architecture(self):
self.dc_bus >> self.bus_in.dc
self.bus_in.dc >> self.gate_rail.vin
self.gate_rail.vout >> self.logic_rail.vin
self.logic_rail.vout >> self.controller.power
self.logic_rail.vout >> self.transceiver.power
for driver, bridge, pwm, sense, phase in (
(self.driver_u, self.bridge_u, self.controller.pwm_u, self.controller.sense_u, self.motor.phase_u),
(self.driver_v, self.bridge_v, self.controller.pwm_v, self.controller.sense_v, self.motor.phase_v),
(self.driver_w, self.bridge_w, self.controller.pwm_w, self.controller.sense_w, self.motor.phase_w),
):
self.gate_rail.vout >> driver.power
pwm >> driver.pwm
driver.high_out >> bridge.high.gate
driver.low_out >> bridge.low.gate
self.bus_in.dc.vcc >> bridge.high.drain
self.bus_in.dc.gnd >> bridge.shunt.p2
bridge.low.drain >> phase.phase
bridge.shunt.p1 >> sense.signal
self.controller.can_tx >> self.transceiver.txd
self.controller.can_rx >> self.transceiver.rxd
self.transceiver.bus >> self.can_port.can
self.transceiver.bus.canh >> self.termination.p1
self.transceiver.bus.canl >> self.termination.p2
self.controller.encoder >> self.encoder_port.encoder
self.bus_in.dc.vcc >> self.bus_bulk.p1
self.bus_in.dc.gnd >> self.bus_bulk.p2
self.controller.power.vcc >> self.logic_bypass.p1
self.controller.power.gnd >> self.logic_bypass.p2
def constraints(self):
# The bus capacitor sees the bus, plus whatever the motor puts back into
# it; a 63 V part on a 24 V bus is that allowance, stated.
require(self.bus_bulk.voltage_rating >= 50 * V)
require(self.termination.resistance == 120 * Ohm)
require(self.motor.voltage_rating >= 48 * V)

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

out/netlist.txt
C1 470 uF Package:Radial_D10
C2 100 nF Package:C_0402
J1 BusTerminal Package:TerminalBlock_1x02_P7.62mm
J2 BusConnector Package:JST_PH_1x02_P2.00mm
J3 EncoderConnector Package:JST_PH_1x03_P2.00mm
J4 MotorConnector Package:TerminalBlock_1x03_P7.62mm
Q1 Transistor Package:PowerPAK-SO8
Q2 Transistor Package:PowerPAK-SO8
Q3 Transistor Package:PowerPAK-SO8
Q4 Transistor Package:PowerPAK-SO8
Q5 Transistor Package:PowerPAK-SO8
Q6 Transistor Package:PowerPAK-SO8
R1 2 mOhm Package:R_2512
R2 2 mOhm Package:R_2512
R3 2 mOhm Package:R_2512
R4 120 Ohm Package:R_0805
U1 Controller Package:LQFP-64
U2 GateDriver Package:SOIC-8
U3 GateDriver Package:SOIC-8
U4 GateDriver Package:SOIC-8
Show 29 more lines
U5 Regulator Package:SOT-223
U6 Regulator Package:SOT-23-5
U7 CANTransceiver Package:SOIC-8
Net-(C1-Pad1) C1.1 J1.V+ Q1.D Q3.D Q5.D U5.VIN
Net-(C1-Pad2) C1.2 C2.2 J1.V- R1.2 R2.2 R3.2 U1.VSS U2.GND U3.GND U4.GND U5.GND U6.GND U7.GND
Net-(C2-Pad1) C2.1 U1.VDD U6.VOUT U7.VCC
Net-(J2-PadCANH) J2.CANH R4.1 U7.CANH
Net-(J2-PadCANL) J2.CANL R4.2 U7.CANL
Net-(J3-PadA) J3.A U1.PB4
Net-(J3-PadB) J3.B U1.PB5
Net-(J3-PadZ) J3.Z U1.PB6
Net-(J4-PadU) J4.U Q1.S Q2.D
Net-(J4-PadV) J4.V Q3.S Q4.D
Net-(J4-PadW) J4.W Q5.S Q6.D
Net-(Q1-PadG) Q1.G U2.HO
Net-(Q2-PadG) Q2.G U2.LO
Net-(Q2-PadS) Q2.S R1.1 U1.PC0
Net-(Q3-PadG) Q3.G U3.HO
Net-(Q4-PadG) Q4.G U3.LO
Net-(Q4-PadS) Q4.S R2.1 U1.PC1
Net-(Q5-PadG) Q5.G U4.HO
Net-(Q6-PadG) Q6.G U4.LO
Net-(Q6-PadS) Q6.S R3.1 U1.PC2
Net-(U1-PadPA10) U1.PA10 U4.IN
Net-(U1-PadPA8) U1.PA8 U2.IN
Net-(U1-PadPA9) U1.PA9 U3.IN
Net-(U1-PadPB8) U1.PB8 U7.RXD
Net-(U1-PadPB9) U1.PB9 U7.TXD
Net-(U2-PadVCC) U2.VCC U3.VCC U4.VCC U5.VOUT U6.VIN

Every check that ran, and every one left undecided.

out/checks.txt
UNDECIDED interface_compatibility: current capability is undecided on electrical: current_demand unknown
UNDECIDED interface_compatibility: voltage domain is undecided on electrical: voltage unknown; on PORT-278c14e1e5e7
UNDECIDED interface_compatibility: current capability is undecided on power_output: current_capability unknown
UNDECIDED interface_compatibility: voltage domain is undecided on power_output: voltage unknown; on PORT-15f7df6602b0, PORT-9f797509605f
UNDECIDED interface_compatibility: current capability is undecided on power_input: current_capability unknown
UNDECIDED interface_compatibility: voltage domain is undecided on power_input: voltage unknown; on PORT-98da99673f80
UNDECIDED interface_compatibility: current capability is undecided on power_input: current_demand unknown
46 checks, 0 failed, 7 undecided

What the elaborated graph contains, by entity kind.

out/graph.txt
1 assumption
4 block
23 component
99 connection
21 constraint
10 interface
80 pin
71 port
1 requirement
310 total
snapshot sha256:6152eb4b83c6e72fee27e2648015dbfd9c795e1a8ddf01a30110a1bfd47a50db

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

Terminal window
fang build examples/servo_drive/servo_drive.py