Examples
USB UART bridge
USB to serial: the interface board on nearly every desk. What it adds over the other examples is part selection, the difference between what the design needs and what was bought.
The program
Section titled “The program”usb_uart_bridge.py makes the selection in __init__,
not in the class body:
def __init__(self, **overrides): super().__init__(**overrides) self.bridge.select("WCH", "CH340C", distributor_ids={"lcsc": "C84681"}, datasheet="SRC-DS-CH340")The class body would attach the choice to the template every instance derives from. The instance is where it belongs. The logical part stays “a 3.3 V regulator”, and which one was bought is a separate, cited fact that a second board can answer differently.
The decision behind it is in the graph too, with what it rejected and why:
FT232RL on unit cost, CP2102N on needing an oscillator. It names the
requirement it serves as well.
The UART crossing is worth reading as well:
self.bridge.uart.tx >> self.target.uart.rxself.bridge.uart.rx >> self.target.uart.txConnecting the two ports would pair like names with like, tx to tx, so the two wires are named individually. That a signal of an interface is addressable on its own is the whole reason this works.
What comes out
Section titled “What comes out”17 parts, 11 nets, 162 entities, 24 checks. None failed, six undecided.
out/usb_uart_bridge.net: the KiCad netlist, ESD clamps and series resistors includedout/rationale.md: the requirement, the bridge decision with both rejected alternatives and the USB 2.0 citation behind the D+ pull-upout/checks.txt,out/graph.txt
Running it
Section titled “Running it”fang check examples/usb_uart_bridge/usb_uart_bridge.pyfang netlist examples/usb_uart_bridge/usb_uart_bridge.pyThe whole program
Section titled “The whole program”"""A USB-to-UART bridge: the interface board on nearly every desk.Show 7 more lines
What it adds over the other examples is the part selection. The bridge and theregulator are chosen parts: a manufacturer, an MPN, a distributor code and thedatasheet the numbers came from, all attached in `__init__` so the selectionlands on the instance and not on the class template. The logical part stays "a 3.3 Vregulator"; which one was bought is recorded separately."""
from fang.interfaces import Pin, PinMap, PowerIn, UARTPort, USB2Portfrom fang.lang import ( MHz, Ohm, Part, Signal, System, V, mA, mV, nF, pF, require, uF,)from fang.parts import Capacitor, Crystal, Diode, Regulator, Resistor, TestPointfrom fang.rationale import Chooses, Cites, Requires
class USBReceptacle(Part): """The connector. VBUS and the pair arrive together, so they are one port."""
designator_prefix = "J"
# The pair signals at 3.3 V; the 5 V is VBUS, which is one wire of the port # rather than the domain the port signals in. usb = USB2Port(voltage=3.3 * V, current_capability=500 * mA, bit_rate=12 * MHz)
VBUS = Pin("VBUS", role="power", number="A4") DM = Pin("D-", role="differential_n", number="A7") DP = Pin("D+", role="differential_p", number="A6") GND = Pin("GND", role="ground", number="A1")
pinmap = PinMap( {"usb.vbus": "VBUS", "usb.dm": "D-", "usb.dp": "D+", "usb.gnd": "GND"} )
class Bridge(Part): """The bridge IC: USB on one side, UART on the other, a crystal beneath."""
designator_prefix = "U"
power = PowerIn(voltage=3.3 * V, current_demand=30 * mA) # What the device draws from the bus, which is the number the requirement # about pre-enumeration current is actually about. usb = USB2Port(voltage=3.3 * V, bit_rate=12 * MHz, current_demand=90 * mA) uart = UARTPort( voh_min=2.8 * V, vol_max=0.4 * V, vih_min=2.0 * V, vil_max=0.8 * V, voltage=3.3 * V, ) xin = Signal() xout = Signal()
VDD = Pin("VDD", role="power", number="16") GND = Pin("GND", role="ground", number="1") UD_P = Pin("UD+", role="differential_p", number="6") UD_M = Pin("UD-", role="differential_n", number="5") TXD = Pin("TXD", role="data", number="2") RXD = Pin("RXD", role="data", number="3") XI = Pin("XI", role="clock", number="7") XO = Pin("XO", role="clock", number="8")
pinmap = PinMap( { "power.vcc": "VDD", "power.gnd": "GND", "usb.dp": "UD+", "usb.dm": "UD-", "uart.tx": "TXD", "uart.rx": "RXD", "xin.line": "XI", "xout.line": "XO", } )
class Target(Part): """Whatever is on the other end of the UART."""
designator_prefix = "U"
power = PowerIn(voltage=3.3 * V, current_demand=50 * mA) uart = UARTPort( voh_min=2.9 * V, vol_max=0.4 * V, vih_min=2.0 * V, vil_max=0.8 * V, voltage=3.3 * V, )
VDD = Pin("VDD", role="power", number="1") VSS = Pin("VSS", role="ground", number="2") PA9 = Pin("PA9", role="data", number="30") PA10 = Pin("PA10", role="data", number="31")
pinmap = PinMap( {"power.vcc": "VDD", "power.gnd": "VSS", "uart.tx": "PA9", "uart.rx": "PA10"} )
class USBSerial(System): """Bus power in, 3.3 V made on the board, a serial port out."""
bus_powered = Requires( "The board draws no more than 100 mA before USB enumeration", validation="analysis", ) bridge_choice = Chooses( "Which USB-UART bridge?", selected="CH340C", alternatives=[ {"part": "FT232RL", "reason": "four times the unit cost at this volume"}, {"part": "CP2102N", "reason": "needs an external oscillator we would pay for"}, ], requirements=("bus_powered",), rationale=( "integrated clock removes the crystal from the BOM on the -C variant", "known driver support on all three host operating systems", ), ) dp_pullup_value = Cites( "A full-speed device signals its speed with 1.5k from D+ to 3.3 V", document="SRC-USB-2.0", locator="section 7.1.5.1", )
connector = USBReceptacle(package="USB_C_Receptacle_16P") regulator = Regulator( input_voltage_max=6 * V, output_voltage=3.3 * V, output_current_max=300 * mA, dropout_voltage=250 * mV, package="SOT-23-5", ) bridge = Bridge(package="SOP-16") target = Target(package="LQFP-48")
resonator = Crystal(frequency=12 * MHz, load_capacitance=20 * pF, package="HC-49S") load_c1 = Capacitor(capacitance=22 * pF, voltage_rating=50 * V, package="C_0402") load_c2 = Capacitor(capacitance=22 * pF, voltage_rating=50 * V, package="C_0402")
bulk_in = Capacitor(capacitance=10 * uF, voltage_rating=16 * V, package="C_0805") bulk_out = Capacitor(capacitance=10 * uF, voltage_rating=16 * V, package="C_0805") bypass = Capacitor(capacitance=100 * nF, voltage_rating=16 * V, package="C_0402")
# Series resistors on the pair are the usual defence against a long cable's # reflections; 22 Ohm keeps the impedance near the pair's own. dp_series = Resistor(resistance=22 * Ohm, package="R_0402") dm_series = Resistor(resistance=22 * Ohm, package="R_0402")
esd_dp = Diode(reverse_voltage=5 * V, forward_voltage=1 * V, package="SOD-523") esd_dm = Diode(reverse_voltage=5 * V, forward_voltage=1 * V, package="SOD-523")
tx_probe = TestPoint(package="TestPoint_Pad_1.5x1.5mm") rx_probe = TestPoint(package="TestPoint_Pad_1.5x1.5mm")
def __init__(self, **overrides): super().__init__(**overrides) # The selection attaches to the instance. Doing it in the class body # would attach it to a template that every instance re-derives from. self.bridge.select( "WCH", "CH340C", distributor_ids={"lcsc": "C84681"}, datasheet="SRC-DS-CH340", ) self.regulator.select( "Diodes", "AP2112K-3.3TRG1", distributor_ids={"lcsc": "C51118"}, datasheet="SRC-DS-AP2112", )
def architecture(self): # Bus power in, regulated down, distributed. self.connector.usb.vbus >> self.regulator.vin.vcc self.connector.usb.gnd >> self.regulator.vin.gnd self.regulator.vout >> self.bridge.power self.regulator.vout >> self.target.power
# The pair, through its series resistors, with the clamps on the # connector side where the energy actually arrives. self.connector.usb.dp >> self.dp_series.p1 self.dp_series.p2 >> self.bridge.usb.dp self.connector.usb.dm >> self.dm_series.p1 self.dm_series.p2 >> self.bridge.usb.dm # Cathode on the line, anode on the return: the clamp catches the # negative excursion and stays out of the way of the signal. self.connector.usb.dp >> self.esd_dp.p2 self.connector.usb.gnd >> self.esd_dp.p1 self.connector.usb.dm >> self.esd_dm.p2 self.connector.usb.gnd >> self.esd_dm.p1
# The crystal and its load capacitors. self.bridge.xin >> self.resonator.p1 self.bridge.xout >> self.resonator.p2 self.resonator.p1 >> self.load_c1.p1 self.resonator.p2 >> self.load_c2.p1 self.regulator.vout.gnd >> self.load_c1.p2 self.regulator.vout.gnd >> self.load_c2.p2
# A UART crosses. Connecting the two ports would pair like names with # like, tx to tx, so the two wires are named, which is the whole # reason a signal of an interface is addressable on its own. self.bridge.uart.tx >> self.target.uart.rx self.bridge.uart.rx >> self.target.uart.tx self.bridge.uart.tx >> self.tx_probe.probe self.bridge.uart.rx >> self.rx_probe.probe
self.connector.usb.vbus >> self.bulk_in.p1 self.connector.usb.gnd >> self.bulk_in.p2 self.regulator.vout.vcc >> self.bulk_out.p1 self.regulator.vout.gnd >> self.bulk_out.p2 self.bridge.power.vcc >> self.bypass.p1 self.bridge.power.gnd >> self.bypass.p2
def constraints(self): require(self.regulator.output_voltage == 3.3 * V) # USB gives 5 V nominal but specifies 5.25 V at the port; the regulator # has to survive the top of that, not the nominal. require(self.regulator.input_voltage_max >= 5.25 * V) require(self.dp_series.resistance <= 33 * Ohm) require(self.dm_series.resistance <= 33 * Ohm) require(self.resonator.load_capacitance >= 12 * pF)The files it writes
Section titled “The files it writes”The parts, then the nets and the pads on them.
C1 10 uF Package:C_0805C2 10 uF Package:C_0805C3 100 nF Package:C_0402C4 22 pF Package:C_0402C5 22 pF Package:C_0402D1 1 V Package:SOD-523D2 1 V Package:SOD-523J1 USBReceptacle Package:USB_C_Receptacle_16PR1 22 Ohm Package:R_0402R2 22 Ohm Package:R_0402TP1 TestPoint Package:TestPoint_Pad_1.5x1.5mmTP2 TestPoint Package:TestPoint_Pad_1.5x1.5mmU1 Bridge Package:SOP-16U2 Regulator Package:SOT-23-5U3 Target Package:LQFP-48Y1 12 MHz Package:HC-49SNet-(C1-Pad1) C1.1 J1.VBUS U2.VINNet-(C1-Pad2) C1.2 C2.2 C3.2 C4.2 C5.2 D1.A D2.A J1.GND U1.GND U2.GND U3.VSSNet-(C2-Pad1) C2.1 C3.1 U1.VDD U2.VOUT U3.VDDNet-(C4-Pad1) C4.1 U1.XI Y1.1Show 7 more lines
Net-(C5-Pad1) C5.1 U1.XO Y1.2Net-(D1-PadK) D1.K J1.D- R1.1Net-(D2-PadK) D2.K J1.D+ R2.1Net-(R1-Pad2) R1.2 U1.UD-Net-(R2-Pad2) R2.2 U1.UD+Net-(TP1-Pad1) TP1.1 U1.RXD U3.PA9Net-(TP2-Pad1) TP2.1 U1.TXD U3.PA10Every check that ran, and every one left undecided.
UNDECIDED interface_compatibility: current capability is undecided on electrical: current_capability unknownUNDECIDED interface_compatibility: voltage domain is undecided on electrical: voltage unknown; on PORT-c75102d9787dUNDECIDED interface_compatibility: current capability is undecided on usb2: current_demand unknownUNDECIDED interface_compatibility: voltage domain is undecided on usb2: voltage unknown; on PORT-b2153fd0d1b8UNDECIDED interface_compatibility: current capability is undecided on usb2: current_demand unknownUNDECIDED interface_compatibility: voltage domain is undecided on usb2: voltage unknown; on PORT-b2153fd0d1b824 checks, 0 failed, 6 undecidedWhat the elaborated graph contains, by entity kind.
1 block 16 component 58 connection 5 constraint 1 decision 1 evidence 6 interface 41 pin 32 port 1 requirement 162 totalsnapshot sha256:e177c0af52dc5310c666927a43bed97461e2b60a4bf6b5ed9f308482d3c35227All of it, including the KiCad netlist, is in
examples/usb_uart_bridge/out/. Rebuild it with:
fang build examples/usb_uart_bridge/usb_uart_bridge.py