Growing the Script: Adding buffered_channel_1to3()
Take our Stage 3 power supply script and grow it by adding @subcircuit def buffered_channel_1to3(...) around a single quad TL074H op-amp: wire input ESD/TVS clamping, Stage A input buffering, three loop-generated output followers with 470R series isolation, and builtins.NC on unused jack switches.
Anatomy of buffered_channel_1to3
In Lesson 0003, our script reached 155 lines with a verified eurorack_power(vcc, vee, gnd) subcircuit. Now, let's add our second reusable @subcircuit directly below eurorack_power: def buffered_channel_1to3(vcc, vee, gnd, norm_in, buffered_tap_out, ch_num=1):.
Why do we need active buffering in a Eurorack multiple in the first place? If you passively split a 1V/Octave pitch CV signal across three oscillator inputs using a passive ninja-star or unbuffered mult, the parallel input impedances form a slight voltage divider with the source's output resistor. Even a few millivolts of droop will pull your oscillators noticeably flat.
So, each 1-to-3 channel in our design maps all four internal operational amplifiers of one TL074H (SOIC-14) package to a two-tier buffer hierarchy:
- Stage A (Input Follower, Pins 1, 2, 3): Presents a high-impedance load (
100Kpull-down) to the patched CV/audio source and produces a low-impedance buffered signal onbuffered_tap_out. - Stages B, C, D (Output Followers, Pins 5–14): Three independent unity-gain followers driven by
buffered_tap_out, each driving its own output jack through a470Rseries protection resistor so a short circuit or cable capacitance on one output never disturbs the other two outputs—or any downstream normalled channel.
flowchart LR NORM["norm_in<br/>(GND on CH1)"] --> JIN_TN["J2: IN_1<br/>Pin TN"] JIN_TN -. "Normalled when<br/>unpatched" .-> JIN_T["J2: IN_1<br/>Pin T (CH1_JACK_IN)"] JIN_T --> PROT["D3: PESD12VL1BA TVS<br/>+ R1: 100K to GND"] PROT --> RIN["R2: 1K Series<br/>(CH1_IN)"] RIN --> U1A["U1A Follower<br/>+ (3), - (2), Out (1)"] U1A --> TAP["buffered_tap_out<br/>(CH1_BUF)"] TAP --> U1B["U1B Follower<br/>+ (5), - (6), Out (7)"] --> R3["R3: 470R"] --> J3["J3: OUT_1A<br/>(TN → builtins.NC)"] TAP --> U1C["U1C Follower<br/>+ (10), - (9), Out (8)"] --> R4["R4: 470R"] --> J4["J4: OUT_1B<br/>(TN → builtins.NC)"] TAP --> U1D["U1D Follower<br/>+ (12), - (13), Out (14)"] --> R5["R5: 470R"] --> J5["J5: OUT_1C<br/>(TN → builtins.NC)"]
buffered_channel_1to3 showing all 14 signal/power pins of U1 (TL074H).Step 1. Add Channel Templates, TL074H Power & Local Decoupling
At the top of buffered_channel_1to3, we define three local TEMPLATE blueprints (r_t, c_t, jack_t), instantiate the quad TL074H op-amp (u), connect its supply pins by name (u["V+"] and u["V-"]), and wire two 100nF ceramic decoupling capacitors across the rails using SKiDL's series & operator:
@subcircuit
def buffered_channel_1to3(vcc, vee, gnd, norm_in, buffered_tap_out, ch_num=1):
"""
Single 1-to-3 buffered multiple channel built around one quad TL074H op-amp:
- Unit A (pins 1, 2, 3): Unity-gain input buffer
- Units B, C, D: Three independent unity-gain output buffers with 470R series protection
- Input jack TN pin connects to `norm_in`
"""
r_t = Part("Device", "R", dest=TEMPLATE, footprint=FP_R_0603)
c_t = Part("Device", "C", dest=TEMPLATE, footprint=FP_C_0603)
c_t.fields["LCSC"] = LCSC_C_100NF_0603
c_t.fields["MPN"] = "CC0603KRX7R9BB104"
c_t.fields["JLCPCB_Class"] = "Basic"
jack_t = Part(
"Connector_Audio",
"AudioJack2_SwitchT",
dest=TEMPLATE,
footprint=FP_JACK_EURORACK,
)
jack_t.fields["MPN"] = "WQP-PJ398SM"
jack_t.fields["JLCPCB_Class"] = "Manual THT (DNP for SMT)"
# Quad high-voltage CMOS/FET-input op-amp (TL074H in SOIC-14)
u = Part(
"Amplifier_Operational",
"TL074",
value="TL074H",
footprint=FP_OPAMP_SOIC14,
tag=f"u_ch{ch_num}",
)
u.fields["LCSC"] = LCSC_U_TL074H_SOIC14
u.fields["MPN"] = "TL074HIDR"
u.fields["JLCPCB_Class"] = "Extended"
# Op-amp power pins & local 100nF ceramic decoupling capacitors
vcc += u["V+"] # Pin 4
vee += u["V-"] # Pin 11
c_dec_pos, c_dec_neg = c_t(2, value="100nF", tag=f"c_dec_ch{ch_num}")
vcc & c_dec_pos & gnd
gnd & c_dec_neg & vee
Step 2. Add the Input Jack, ESD Protection & Stage A Input Follower
Next, we wire the switched input jack (j_in), connecting its Sleeve ("S") to gnd, its Switched Tip Normal Contact ("TN") to the incoming norm_in net, and its Tip ("T") to in_jack. Then we use the & operator to wire the PESD12VL1BA bidirectional TVS diode (d_tvs), the 100K input pull-down (r_pd), and the 1K series current-limiting resistor (r_in), feeding Stage A of the TL074H (pins 3, 2, 1):
# Input jack, bidirectional TVS ESD diode, 100k input pull-down, and 1k series input protection resistor
j_in = jack_t(value=f"IN_{ch_num}", tag=f"j_in_ch{ch_num}")
in_jack = Net(f"CH{ch_num}_JACK_IN")
in_sig = Net(f"CH{ch_num}_IN")
gnd += j_in["S"]
norm_in += j_in["TN"]
in_jack += j_in["T"]
d_tvs = Part(
"Device",
"D_TVS",
value="PESD12VL1BA",
footprint=FP_DIODE_SOD323,
tag=f"d_tvs_ch{ch_num}",
)
d_tvs.fields["LCSC"] = LCSC_D_TVS_PESD12VL1BA_SOD323
d_tvs.fields["MPN"] = "PESD12VL1BA,115"
d_tvs.fields["JLCPCB_Class"] = "Extended"
in_jack & d_tvs & gnd
r_pd = r_t(value="100K", tag=f"r_pd_ch{ch_num}")
r_pd.fields["LCSC"] = LCSC_R_100K_0603
r_pd.fields["MPN"] = "0603WAF1003T5E"
r_pd.fields["JLCPCB_Class"] = "Basic"
in_jack & r_pd & gnd
r_in = r_t(value="1K", tag=f"r_in_ch{ch_num}")
r_in.fields["LCSC"] = LCSC_R_1K_0603
r_in.fields["MPN"] = "0603WAF1001T5E"
r_in.fields["JLCPCB_Class"] = "Basic"
in_jack & r_in & in_sig
# Stage A (Pins: + = 3, - = 2, Out = 1): Unity-gain input buffer
in_sig += u.p[3]
buffered_tap_out += u.p[1], u.p[2]
Notice how buffered_tap_out += u.p[1], u.p[2] attaches both the op-amp output (pin 1) and the inverting input (pin 2) to buffered_tap_out at once—closing the unity-gain feedback loop AND feeding the downstream output stages (u.p[5], u.p[10], u.p[12]) on the same net!
Step 3. Loop Over Output Stages B, C, D & Mark Unused Switches with builtins.NC
Now, here is one of my favourite moments in hardware-as-code. In a GUI schematic editor, wiring three identical output buffers means drawing, dragging, and labelling the exact same op-amp + resistor + jack cluster three times. In Python, we simply express Stages B, C, and D as a compact list of (suffix, pin_pos, pin_neg, pin_out) tuples and write a for loop:
| Op-Amp Stage | Role | Non-Inverting (+) |
Inverting (-) |
Output | Unity-Gain Feedback Expression |
|---|---|---|---|---|---|
| Unit A | Input Buffer | u.p[3] |
u.p[2] |
u.p[1] |
buffered_tap_out += u.p[1], u.p[2] |
Unit B ("A") |
Output Buffer A | u.p[5] |
u.p[6] |
u.p[7] |
op_out += u.p[7], u.p[6] |
Unit C ("B") |
Output Buffer B | u.p[10] |
u.p[9] |
u.p[8] |
op_out += u.p[8], u.p[9] |
Unit D ("C") |
Output Buffer C | u.p[12] |
u.p[13] |
u.p[14] |
op_out += u.p[14], u.p[13] |
# Stages B, C, D: 3 independent output buffers driving 3 output jacks via 470R resistors
output_stages = [
("A", 5, 6, 7),
("B", 10, 9, 8),
("C", 12, 13, 14),
]
for suffix, pin_pos, pin_neg, pin_out in output_stages:
op_out = Net(f"CH{ch_num}_OP_{suffix}")
jack_tip = Net(f"CH{ch_num}_OUT_{suffix}")
# Unity-gain follower fed by the buffered input tap
buffered_tap_out += u.p[pin_pos]
op_out += u.p[pin_out], u.p[pin_neg]
# Series output protection resistor (470R)
r_out = r_t(value="470R", tag=f"r_out_ch{ch_num}_{suffix}")
r_out.fields["LCSC"] = LCSC_R_470R_0603
r_out.fields["MPN"] = "0603WAF4700T5E"
r_out.fields["JLCPCB_Class"] = "Basic"
op_out & r_out & jack_tip
# Output jack
j_out = jack_t(value=f"OUT_{ch_num}{suffix}", tag=f"j_out_ch{ch_num}_{suffix}")
jack_tip += j_out["T"]
gnd += j_out["S"]
j_out["TN"] += builtins.NC
j_out["TN"] += builtins.NC (Pin on the Left)All 12 jacks on the board use the same physical part (WQP-PJ398SM / AudioJack2_SwitchT) to keep purchasing and panel assembly simple. On the output jacks, however, the Switched Tip Normal Contact (TN) is unused. Turns out, SKiDL's No-Connect Net (builtins.NC) has two quirks worth knowing. First, it is injected into Python's builtins module at import time and is not exported in skidl.__all__ (so from skidl import NC raises an ImportError). Second, if you write builtins.NC += j_out["TN"], Python's augmented assignment rules reassign the attribute. Putting the pin on the left—j_out["TN"] += builtins.NC—reads cleanly and keeps both Python and ruff happy.
Step 4. Complete Stage 4 Script & Terminal Verification
Putting our Stage 3 power supply together with our new buffered_channel_1to3 subcircuit gives us the complete, standalone 255-line Stage 4 Python script (saved at tutorial/apps/0004-single-buffered-channel/single_channel.py):
#!/usr/bin/env python3
"""
Lesson 0004: Single 1-to-3 Buffered Channel (`TL074H` & Switched Jacks).
Combines `eurorack_power` with one `buffered_channel_1to3` instance to verify:
- Op-amp power pins (`u["V+"]`, `u["V-"]`) and local 100nF ceramic decoupling (`&`)
- Input protection network (`PESD12VL1BA` TVS, `100K` pull-down, `1K` series resistor)
- Stage A unity-gain input buffer (`u.p[3]`, `u.p[1], u.p[2]`)
- Loop-generated Stages B, C, D output buffers with `470R` resistors and `builtins.NC`
"""
import builtins
import os
from pathlib import Path
# Keep generated .erc, .log, .net, .xml, and _sklib.py files inside this lesson's app directory
os.chdir(Path(__file__).resolve().parent)
# Configure writable cache directories and KiCad 10 library paths BEFORE importing skidl
os.environ.setdefault("XDG_DATA_HOME", "/tmp/xdg_data")
os.environ.setdefault("MPLCONFIGDIR", "/tmp/mpl_config")
if os.path.isdir("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"):
os.environ.setdefault(
"KICAD10_SYMBOL_DIR",
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols",
)
os.environ.setdefault(
"KICAD_SYMBOL_DIR",
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols",
)
if os.path.isdir("/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints"):
os.environ.setdefault(
"KICAD10_FOOTPRINT_DIR",
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints",
)
from skidl import ( # noqa: E402
ERC,
POWER,
TEMPLATE,
Net,
Part,
generate_netlist,
generate_xml,
subcircuit,
)
FP_OPAMP_SOIC14 = "Package_SO:SOIC-14_3.9x8.7mm_P1.27mm"
FP_R_0603 = "Resistor_SMD:R_0603_1608Metric"
FP_C_0603 = "Capacitor_SMD:C_0603_1608Metric"
FP_C_ELEC = "Capacitor_THT:CP_Radial_D5.0mm_P2.00mm"
FP_DIODE_SOD323 = "Diode_SMD:D_SOD-323"
FP_JACK_EURORACK = (
"Connector_Audio:Jack_3.5mm_QingPu_WQP-PJ398SM_Vertical_CircularHoles"
)
FP_PWR_HEADER_10P = "Connector_IDC:IDC-Header_2x05_P2.54mm_Vertical"
LCSC_C_100NF_0603 = "C14663"
LCSC_R_100K_0603 = "C25803"
LCSC_R_1K_0603 = "C21190"
LCSC_R_470R_0603 = "C23179"
LCSC_D_1N5819WS_SOD323 = "C191023"
LCSC_D_TVS_PESD12VL1BA_SOD323 = "C38558"
LCSC_U_TL074H_SOIC14 = "C2878713"
@subcircuit
def eurorack_power(vcc, vee, gnd):
"""10-pin Eurorack power input with Schottky reverse-polarity protection and bulk caps."""
j_pwr = Part(
"Connector_Generic",
"Conn_02x05_Odd_Even",
value="EURORACK_PWR_10PIN",
footprint=FP_PWR_HEADER_10P,
tag="j_pwr",
)
j_pwr.fields["JLCPCB_Class"] = "Manual THT (DNP for SMT)"
vcc_raw = Net("+12V_RAW")
vee_raw = Net("-12V_RAW")
vee_raw += j_pwr[1, 2]
gnd += j_pwr[3, 4, 5, 6, 7, 8]
vcc_raw += j_pwr[9, 10]
d_t = Part(
"Device",
"D_Schottky",
dest=TEMPLATE,
value="1N5819WS",
footprint=FP_DIODE_SOD323,
)
d_t.fields["LCSC"] = LCSC_D_1N5819WS_SOD323
d_t.fields["MPN"] = "1N5819WS"
d_t.fields["JLCPCB_Class"] = "Basic"
d_pos, d_neg = d_t(2, tag="d_prot")
vcc_raw += d_pos["A"]
vcc += d_pos["K"]
vee_raw += d_neg["K"]
vee += d_neg["A"]
c_bulk_t = Part(
"Device",
"C_Polarized",
dest=TEMPLATE,
value="10uF",
footprint=FP_C_ELEC,
)
c_bulk_t.fields["MPN"] = "ECEA1EKS100"
c_bulk_t.fields["JLCPCB_Class"] = "Manual THT (DNP for SMT)"
c_bulk_pos, c_bulk_neg = c_bulk_t(2, tag="c_bulk")
vcc += c_bulk_pos[1]
gnd += c_bulk_pos[2]
gnd += c_bulk_neg[1]
vee += c_bulk_neg[2]
@subcircuit
def buffered_channel_1to3(vcc, vee, gnd, norm_in, buffered_tap_out, ch_num=1):
"""
Single 1-to-3 buffered multiple channel built around one quad TL074H op-amp:
- Unit A (pins 1, 2, 3): Unity-gain input buffer
- Units B, C, D: Three independent unity-gain output buffers with 470R series protection
- Input jack TN pin connects to `norm_in`
"""
r_t = Part("Device", "R", dest=TEMPLATE, footprint=FP_R_0603)
c_t = Part("Device", "C", dest=TEMPLATE, footprint=FP_C_0603)
c_t.fields["LCSC"] = LCSC_C_100NF_0603
c_t.fields["MPN"] = "CC0603KRX7R9BB104"
c_t.fields["JLCPCB_Class"] = "Basic"
jack_t = Part(
"Connector_Audio",
"AudioJack2_SwitchT",
dest=TEMPLATE,
footprint=FP_JACK_EURORACK,
)
jack_t.fields["MPN"] = "WQP-PJ398SM"
jack_t.fields["JLCPCB_Class"] = "Manual THT (DNP for SMT)"
# Quad high-voltage CMOS/FET-input op-amp (TL074H in SOIC-14)
u = Part(
"Amplifier_Operational",
"TL074",
value="TL074H",
footprint=FP_OPAMP_SOIC14,
tag=f"u_ch{ch_num}",
)
u.fields["LCSC"] = LCSC_U_TL074H_SOIC14
u.fields["MPN"] = "TL074HIDR"
u.fields["JLCPCB_Class"] = "Extended"
# Op-amp power pins & local 100nF ceramic decoupling capacitors
vcc += u["V+"] # Pin 4
vee += u["V-"] # Pin 11
c_dec_pos, c_dec_neg = c_t(2, value="100nF", tag=f"c_dec_ch{ch_num}")
vcc & c_dec_pos & gnd
gnd & c_dec_neg & vee
# Input jack, bidirectional TVS ESD diode, 100k input pull-down, and 1k series input protection resistor
j_in = jack_t(value=f"IN_{ch_num}", tag=f"j_in_ch{ch_num}")
in_jack = Net(f"CH{ch_num}_JACK_IN")
in_sig = Net(f"CH{ch_num}_IN")
gnd += j_in["S"]
norm_in += j_in["TN"]
in_jack += j_in["T"]
d_tvs = Part(
"Device",
"D_TVS",
value="PESD12VL1BA",
footprint=FP_DIODE_SOD323,
tag=f"d_tvs_ch{ch_num}",
)
d_tvs.fields["LCSC"] = LCSC_D_TVS_PESD12VL1BA_SOD323
d_tvs.fields["MPN"] = "PESD12VL1BA,115"
d_tvs.fields["JLCPCB_Class"] = "Extended"
in_jack & d_tvs & gnd
r_pd = r_t(value="100K", tag=f"r_pd_ch{ch_num}")
r_pd.fields["LCSC"] = LCSC_R_100K_0603
r_pd.fields["MPN"] = "0603WAF1003T5E"
r_pd.fields["JLCPCB_Class"] = "Basic"
in_jack & r_pd & gnd
r_in = r_t(value="1K", tag=f"r_in_ch{ch_num}")
r_in.fields["LCSC"] = LCSC_R_1K_0603
r_in.fields["MPN"] = "0603WAF1001T5E"
r_in.fields["JLCPCB_Class"] = "Basic"
in_jack & r_in & in_sig
# Stage A (Pins: + = 3, - = 2, Out = 1): Unity-gain input buffer
in_sig += u.p[3]
buffered_tap_out += u.p[1], u.p[2]
# Stages B, C, D: 3 independent output buffers driving 3 output jacks via 470R resistors
output_stages = [
("A", 5, 6, 7),
("B", 10, 9, 8),
("C", 12, 13, 14),
]
for suffix, pin_pos, pin_neg, pin_out in output_stages:
op_out = Net(f"CH{ch_num}_OP_{suffix}")
jack_tip = Net(f"CH{ch_num}_OUT_{suffix}")
# Unity-gain follower fed by the buffered input tap
buffered_tap_out += u.p[pin_pos]
op_out += u.p[pin_out], u.p[pin_neg]
# Series output protection resistor (470R)
r_out = r_t(value="470R", tag=f"r_out_ch{ch_num}_{suffix}")
r_out.fields["LCSC"] = LCSC_R_470R_0603
r_out.fields["MPN"] = "0603WAF4700T5E"
r_out.fields["JLCPCB_Class"] = "Basic"
op_out & r_out & jack_tip
# Output jack
j_out = jack_t(value=f"OUT_{ch_num}{suffix}", tag=f"j_out_ch{ch_num}_{suffix}")
jack_tip += j_out["T"]
gnd += j_out["S"]
j_out["TN"] += builtins.NC
def run_single_channel():
"""Verify a single 1-to-3 buffered channel powered by `eurorack_power`."""
vcc = Net("+12V")
vee = Net("-12V")
gnd = Net("GND")
ch1_buf = Net("CH1_BUF")
vcc.drive = POWER
vee.drive = POWER
gnd.drive = POWER
eurorack_power(vcc, vee, gnd, tag="pwr")
buffered_channel_1to3(vcc, vee, gnd, gnd, ch1_buf, ch_num=1, tag="ch1")
ERC()
generate_netlist(file_="single_channel.net")
generate_xml(file_="single_channel_bom.xml")
print("=== Single 1-to-3 Buffered Channel Verification ===")
print(f" Total instantiated parts : {len(builtins.default_circuit.parts)}")
sorted_buf_pins = sorted(ch1_buf.pins, key=lambda p: (p.part.ref, int(p.num)))
buf_pins = ", ".join(f"{p.part.ref}.{p.num}" for p in sorted_buf_pins)
print(f" CH1_BUF connected pins : {buf_pins}")
if __name__ == "__main__":
run_single_channel()
.venv/bin/python tutorial/apps/0004-single-buffered-channel/single_channel.py
ruff check --fix tutorial/apps/0004-single-buffered-channel/*.py
ruff format tutorial/apps/0004-single-buffered-channel/*.py
ERC INFO: No errors or warnings found while running ERC.
INFO: 0 errors found while generating netlist.
INFO: 0 errors found while generating XML.
=== Single 1-to-3 Buffered Channel Verification ===
Total instantiated parts : 18
CH1_BUF connected pins : U1.1, U1.2, U1.5, U1.10, U1.12
Retrieval Practice: Wire the Input Protection, Follower & Output Jack
Fill in the four blanks below to wire the input jack's switched normal contact ("TN"), place the 1K input resistor in series using &, close Stage A's unity-gain feedback loop across pins 1 and 2, and mark the output jack's unused "TN" pin as no-connect via builtins:
gnd += j_in["S"]
norm_in += j_in[""]
in_jack += j_in["T"]
# 1K series resistor between in_jack and in_sig
in_jack r_in & in_sig
# Stage A unity-gain follower (+ = 3, - = 2, Out = 1)
in_sig += u.p[3]
buffered_tap_out += u.p[1], u.p[]
# Mark output jack TN switch contact as intentionally unconnected
j_out["TN"] += builtins.
Knowledge Check
1. Inside buffered_channel_1to3, why do we wire each output jack's unused normal contact using j_out["TN"] += builtins.NC instead of from skidl import NC?
2. What does the SKiDL statement buffered_tap_out += u.p[1], u.p[2] accomplish on Stage A of the TL074H op-amp?
Primary Sources: SKiDL API & Pin Indexing Cheat Sheet · Eurorack Power & Audio Jack Normalling Reference.
Ask the Agent: Have a question about why 470R was chosen for the output series resistors or how PESD12VL1BA clamps ESD spikes? Ask in chat, or continue to Lesson 0005 to finish growing buffered_multiple.py with 3-channel cascaded normalling and JLCPCB BOM generation!