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 program
Section titled “The program”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.
What comes out
Section titled “What comes out”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.
out/servo_drive.net: 24 components and 26 netsout/rationale.md: the requirements the drive states, and the assumption it is honest aboutout/checks.txt: 46 checks over 21 constraints, six of them from each bridge and three from the drive itselfout/graph.txt
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.
Running it
Section titled “Running it”fang check examples/servo_drive/servo_drive.pyfang graph examples/servo_drive/servo_drive.pyfang view examples/servo_drive/servo_drive.py safety -o safety.svgThe whole program
Section titled “The whole program”"""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 anda shunt: it owns its own interior and its own constraints, and the driveinstantiates three of them from one declaration. The phase, the encoder and theCAN bus are typed ports, so a connection says what it carries rather than whichpad 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, Transistorfrom 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 files it writes
Section titled “The files it writes”The parts, then the nets and the pads on them.
C1 470 uF Package:Radial_D10C2 100 nF Package:C_0402J1 BusTerminal Package:TerminalBlock_1x02_P7.62mmJ2 BusConnector Package:JST_PH_1x02_P2.00mmJ3 EncoderConnector Package:JST_PH_1x03_P2.00mmJ4 MotorConnector Package:TerminalBlock_1x03_P7.62mmQ1 Transistor Package:PowerPAK-SO8Q2 Transistor Package:PowerPAK-SO8Q3 Transistor Package:PowerPAK-SO8Q4 Transistor Package:PowerPAK-SO8Q5 Transistor Package:PowerPAK-SO8Q6 Transistor Package:PowerPAK-SO8R1 2 mOhm Package:R_2512R2 2 mOhm Package:R_2512R3 2 mOhm Package:R_2512R4 120 Ohm Package:R_0805U1 Controller Package:LQFP-64U2 GateDriver Package:SOIC-8U3 GateDriver Package:SOIC-8U4 GateDriver Package:SOIC-8Show 29 more lines
U5 Regulator Package:SOT-223U6 Regulator Package:SOT-23-5U7 CANTransceiver Package:SOIC-8Net-(C1-Pad1) C1.1 J1.V+ Q1.D Q3.D Q5.D U5.VINNet-(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.GNDNet-(C2-Pad1) C2.1 U1.VDD U6.VOUT U7.VCCNet-(J2-PadCANH) J2.CANH R4.1 U7.CANHNet-(J2-PadCANL) J2.CANL R4.2 U7.CANLNet-(J3-PadA) J3.A U1.PB4Net-(J3-PadB) J3.B U1.PB5Net-(J3-PadZ) J3.Z U1.PB6Net-(J4-PadU) J4.U Q1.S Q2.DNet-(J4-PadV) J4.V Q3.S Q4.DNet-(J4-PadW) J4.W Q5.S Q6.DNet-(Q1-PadG) Q1.G U2.HONet-(Q2-PadG) Q2.G U2.LONet-(Q2-PadS) Q2.S R1.1 U1.PC0Net-(Q3-PadG) Q3.G U3.HONet-(Q4-PadG) Q4.G U3.LONet-(Q4-PadS) Q4.S R2.1 U1.PC1Net-(Q5-PadG) Q5.G U4.HONet-(Q6-PadG) Q6.G U4.LONet-(Q6-PadS) Q6.S R3.1 U1.PC2Net-(U1-PadPA10) U1.PA10 U4.INNet-(U1-PadPA8) U1.PA8 U2.INNet-(U1-PadPA9) U1.PA9 U3.INNet-(U1-PadPB8) U1.PB8 U7.RXDNet-(U1-PadPB9) U1.PB9 U7.TXDNet-(U2-PadVCC) U2.VCC U3.VCC U4.VCC U5.VOUT U6.VINEvery check that ran, and every one left undecided.
UNDECIDED interface_compatibility: current capability is undecided on electrical: current_demand unknownUNDECIDED interface_compatibility: voltage domain is undecided on electrical: voltage unknown; on PORT-278c14e1e5e7UNDECIDED interface_compatibility: current capability is undecided on power_output: current_capability unknownUNDECIDED interface_compatibility: voltage domain is undecided on power_output: voltage unknown; on PORT-15f7df6602b0, PORT-9f797509605fUNDECIDED interface_compatibility: current capability is undecided on power_input: current_capability unknownUNDECIDED interface_compatibility: voltage domain is undecided on power_input: voltage unknown; on PORT-98da99673f80UNDECIDED interface_compatibility: current capability is undecided on power_input: current_demand unknown46 checks, 0 failed, 7 undecidedWhat the elaborated graph contains, by entity kind.
1 assumption 4 block 23 component 99 connection 21 constraint 10 interface 80 pin 71 port 1 requirement 310 totalsnapshot sha256:6152eb4b83c6e72fee27e2648015dbfd9c795e1a8ddf01a30110a1bfd47a50dbAll of it, including the KiCad netlist, is in
examples/servo_drive/out/. Rebuild it with:
fang build examples/servo_drive/servo_drive.py