molecule
AM/FM Receiver Molecule
Public Made by Adomby adom
USB-powered AM/FM receiver on an Adom Molecule: discrete BJT and op-amp superhet (10.7 MHz and 455 kHz IFs), STM32G0B1 tuning, self-calibration and USB audio over one USB-C cable. 96 x 96 mm pin grid, 4 layers, KiCad 10.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848
"""AM/FM receiver (discrete BJT + op-amp) with STM32G0B1 USB audio streamer.
Single source of truth for the netlist. Both the schematic generator and the
PCB generator read the `DESIGN` object built here.
Signal chain
------------
Antenna (SMA) -> diplexer
FM: 70 MHz HPF + 130 MHz LPF -> BJT RF amp with varactor-tracked tank (88-108 MHz)
-> BJT mixer <- BJT Colpitts VCO 98.7-118.7 MHz (BB910 varactor, DAC tuned)
AM: 2 MHz LPF -> BJT RF preamp -> BJT mixer <- BJT Clapp VCO 11.2-12.4 MHz (up-conversion)
Both mixers -> common 10.7 MHz IF: ceramic filter -> BJT IF amp -> ceramic filter
-> BJT 2nd mixer <- 10.24 MHz BJT crystal oscillator -> 455 kHz
AM: 455 kHz ceramic filter -> 2x BJT diff-pair VGA (AGC) -> BJT post-amp
-> Schottky envelope detector -> op-amp AGC integrator + op-amp 5 kHz audio LPF
FM: RC low-pass -> 2x BJT limiter -> BJT squarer -> diode-pump pulse-count
discriminator into op-amp integrator (75 us de-emphasis) -> op-amp 15 kHz LPF
STM32G0B1 (USB FS, ROM DFU bootloader) samples AM/FM audio with its ADC, drives the
varactor tuning voltages with DAC1/DAC2 (+ PWM for the RF tracking tank), switches
bands, counts the IF, injects crystal-derived calibration markers into the antenna
input to align the oscillators itself, and streams audio to the PC as a USB Audio
Class device. Everything is controlled and reprogrammed over the one USB-C cable.
"""
from collections import OrderedDict
FP = {
'R0603': 'Resistor_SMD:R_0603_1608Metric',
'C0603': 'Capacitor_SMD:C_0603_1608Metric',
'C0805': 'Capacitor_SMD:C_0805_2012Metric',
'L0603': 'Inductor_SMD:L_0603_1608Metric',
'L0805': 'Inductor_SMD:L_0805_2012Metric',
'L1210': 'Inductor_SMD:L_1210_3225Metric',
'FB0603': 'Inductor_SMD:L_0603_1608Metric',
'SOT23': 'Package_TO_SOT_SMD:SOT-23',
'SOT23-5': 'Package_TO_SOT_SMD:SOT-23-5',
'SOT23-6': 'Package_TO_SOT_SMD:SOT-23-6',
'SOT363': 'Package_TO_SOT_SMD:SOT-363_SC-70-6',
'SOIC8': 'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm',
'SOD323': 'Diode_SMD:D_SOD-323',
'LED0603': 'LED_SMD:LED_0603_1608Metric',
'LQFP48': 'Package_QFP:LQFP-48_7x7mm_P0.5mm',
'QFN48': 'AMFM:QFN-48-1EP_7x7mm_P0.5mm_EP5.6x5.6mm_ThermalVias0.3',
'F1206': 'Fuse:Fuse_1206_3216Metric',
'USBC': 'AMFM:USB_C_Receptacle_HRO_TYPE-C-31-M-12_EdgeMount',
'SMA': 'AMFM:SMA_BAT_Wireless_BWSMA-KWE-Z001_Edge',
'SWD': 'Connector_PinHeader_1.27mm:PinHeader_2x05_P1.27mm_Vertical',
'TACT': 'AMFM:SW_XKB_TS-1187A_5.1x5.1mm',
'EC11': 'Rotary_Encoder:RotaryEncoder_Alps_EC11E-Switch_Vertical_H20mm',
'XTAL3225': 'Crystal:Crystal_SMD_3225-4Pin_3.2x2.5mm',
'TP': 'TestPoint:TestPoint_Pad_D1.0mm',
'TP_BIG': 'TestPoint:TestPoint_Pad_D1.5mm',
'MH': 'MountingHole:MountingHole_3.2mm_M3',
'MPIN': 'AdomMachine:MachinePinLargeStandard',
'MCON': 'AdomMachine:MachineContactMedium',
'SOD123': 'Diode_SMD:D_SOD-123',
'CF3': 'AMFM:HCCF3_SMD-3P_7.0x3.0mm',
'CF4': 'AMFM:HCCF2_SMD-4P_11.9x6.5mm',
}
# JLCPCB / LCSC part numbers, verified against the JLCPCB parts API on 2026-09-25.
# 'base' = JLC Basic part (no setup fee); others are Extended / Preferred-Extended.
LCSC = {
'STM32G0B1CCU7': 'C29759424', '40MHz': 'C337678', 'MMBTH10': 'C545531', 'MMBT3904': 'C20526', 'MMBT3906': 'C2143',
'BC847BS': 'C5380687', 'TLV9062IDR': 'C398355', 'AP2112K-3.3': 'C51118', 'USBLC6-2SC6': 'C7519',
'BAT54S': 'C408389', 'BAT54WS': 'C124205', 'BB910': 'C475628', 'TYPE-C-31-M-12': 'C165948',
'10.24MHz': 'C51822681', 'HCCF3-10.700-LTCVS3L': 'C19191232', 'HCCF2-455.000-LTWCGL': 'C19186014',
'TS-1187A-B-A-B': 'C318884', 'BWSMA-KWE-Z001': 'C496551', 'EC11E15244B2': 'C470754',
'PZ1.27-2x5P': 'C22438120', 'nSMD050-33V': 'C70077', 'GZ1608D601TF': 'C1002',
'LED_RED': 'C2286', 'LED_GREEN': 'C12624', 'LED_YELLOW': 'C2287', 'LED_BLUE': 'C2288',
'B5819W': 'C8598',
}
MPN = {
'C29759424': 'STM32G0B1CCU7', 'C337678': 'SX32Y040000BC1T001', 'C545531': 'MMBTH10', 'C20526': 'MMBT3904', 'C2143': 'MMBT3906',
'C5380687': 'BC847BS', 'C398355': 'TLV9062IDR', 'C51118': 'AP2112K-3.3TRG1', 'C7519': 'USBLC6-2SC6',
'C408389': 'BAT54S', 'C124205': 'BAT54WS-7-F', 'C475628': 'BB910', 'C165948': 'TYPE-C-31-M-12',
'C51822681': '0132M4-10.240F12DZNG', 'C19191232': 'HCCF3-10.700-LTCVS3L', 'C19186014': 'HCCF2-455.000-LTWCGL',
'C318884': 'TS-1187A-B-A-B', 'C496551': 'BWSMA-KWE-Z001', 'C470754': 'EC11E15244B2',
'C22438120': 'HX PZ1.27-2x5P ZZ', 'C70077': 'nSMD050-33V', 'C1002': 'GZ1608D601TF',
'C2286': 'KT-0603R', 'C12624': 'KT-0603G', 'C2287': 'KT-0603Y', 'C2288': 'KT-0603B',
}
# 0603 1 % thick-film resistors (UNI-ROYAL 0603WAF... unless noted)
LCSC_R = {
'22': 'C23345', '47': 'C23182', '51': 'C23197', '56': 'C25196', '82': 'C23255', '100': 'C22775',
'120': 'C22787', '150': 'C22808', '220': 'C22962', '330': 'C23138', '470': 'C23179', '1k': 'C21190',
'1.2k': 'C22765', '1.5k': 'C22843', '1.8k': 'C4177', '2k': 'C22975', '2.2k': 'C4190', '2.7k': 'C13167',
'4.3k': 'C23159', '4.7k': 'C23162', '5.1k': 'C23186', '5.6k': 'C23189', '6.8k': 'C23212', '10k': 'C25804',
'15k': 'C22809', '22k': 'C31850', '33k': 'C4216', '39k': 'C23153', '47k': 'C25819', '51k': 'C23196',
'100k': 'C25803', '200k': 'C25811',
}
# MLCC: every value <= 4.7 nF that sets a frequency or filter is C0G/NP0
LCSC_C = {
('2.2p', '0603'): 'C1651', ('3.3p', '0603'): 'C282249', ('4.7p', '0603'): 'C1669', ('15p', '0603'): 'C1644', ('18p', '0603'): 'C1647',
('22p', '0603'): 'C1653', ('33p', '0603'): 'C1663', ('47p', '0603'): 'C1671', ('100p', '0603'): 'C14858',
('150p', '0603'): 'C107038', ('220p', '0603'): 'C388907', ('470p', '0603'): 'C106211', ('680p', '0603'): 'C107055',
('1n', '0603'): 'C1588', ('1.5n', '0603'): 'C107039', ('2.2n', '0603'): 'C296055', ('2.7n', '0603'): 'C77034',
('4.7n', '0603'): 'C576818', ('10n', '0603'): 'C57112', ('100n', '0603'): 'C14663', ('1u', '0603'): 'C15849',
('4.7u', '0805'): 'C1779', ('10u', '0805'): 'C15850', ('22u', '0805'): 'C45783',
}
# RF/tank inductors: Murata LQW18 wire-wound (Q ~35 @ 100 MHz); 1210 wire-wound for the MW parts
LCSC_L = {
('56n', '0603'): 'C98079', ('110n', '0603'): 'C113130', ('120n', '0603'): 'C86138', ('150n', '0603'): 'C113131',
('2.2u', '1210'): 'C89016', ('8.2u', '1210'): 'C52259800',
}
MPN_PASSIVE = {
'C98079': 'LQW18AN56NG00D', 'C113130': 'LQW18ANR11G00D', 'C86138': 'LQW18ANR12G00D', 'C113131': 'LQW18ANR15G00D',
'C89016': 'NLV32T-2R2J-PF', 'C52259800': 'YSCM322522-8R2J',
}
def _pkg(fp):
import re
m = re.search(r'_(\d{4})_', FP[fp])
return m.group(1) if m else ''
class Part:
def __init__(self, ref, lib_id, value, footprint, pins, sheet, block, description='',
fields=None, datasheet='', in_bom=True, units=None):
self.ref = ref
self.lib_id = lib_id
self.value = value
self.footprint = footprint
self.pins = OrderedDict(pins) # pin number -> net name (None = no connect)
self.sheet = sheet
self.block = block
self.description = description
self.fields = OrderedDict(fields or {})
self.datasheet = datasheet
self.in_bom = in_bom
self.units = units or [1]
self.region = None # PCB placement region override (defaults to the sheet)
def __repr__(self):
return '<%s %s %s>' % (self.ref, self.value, dict(self.pins))
class Design:
def __init__(self):
self.parts = []
self.sheets = OrderedDict() # key -> dict(title, file, blocks OrderedDict(key -> title/notes))
self._sheet = None
self._block = None
def sheet(self, key, title, fname, notes='', name=None):
self.sheets[key] = dict(title=title, file=fname, notes=notes, blocks=OrderedDict(),
name=name or key)
self._sheet = key
def block(self, key, title, notes=''):
self.sheets[self._sheet]['blocks'][key] = dict(title=title, notes=notes)
self._block = key
def add(self, ref, lib_id, value, footprint, pins, **kw):
assert ref not in {p.ref for p in self.parts}, 'duplicate ref ' + ref
p = Part(ref, lib_id, value, footprint, pins, self._sheet, self._block, **kw)
self.parts.append(p)
return p
def nets(self):
n = OrderedDict()
for p in self.parts:
for pin, net in p.pins.items():
if net is not None:
n.setdefault(net, []).append((p.ref, pin))
return n
D = Design()
# ------------------------------------------------------------------ helpers
def R(ref, val, a, b, fp='R0603', note=''):
D.add(ref, 'Device:R', val, FP[fp], {'1': a, '2': b}, description=note or 'Resistor, 1 %, 0603',
fields={'LCSC': LCSC_R[val]})
def C(ref, val, a, b, fp=None, note=''):
if fp is None:
fp = 'C0805' if val in ('10u', '22u', '4.7u') else 'C0603'
D.add(ref, 'Device:C', val, FP[fp], {'1': a, '2': b}, description=note or 'Capacitor',
fields={'LCSC': LCSC_C[(val, _pkg(fp))]})
def L(ref, val, a, b, fp='L0603', note=''):
code = LCSC_L[(val, _pkg(fp))]
D.add(ref, 'Device:L', val, FP[fp], {'1': a, '2': b}, description=note or 'Inductor',
fields={'LCSC': code, 'MPN': MPN_PASSIVE[code]})
def FB(ref, a, b):
D.add(ref, 'Device:FerriteBead', '600R@100MHz', FP['FB0603'], {'1': a, '2': b},
description='Ferrite bead, 600 ohm @ 100 MHz, 0603',
fields={'LCSC': LCSC['GZ1608D601TF'], 'MPN': 'GZ1608D601TF'})
def NPN(ref, val, b, c, e, note=''):
f = {'LCSC': LCSC[val], 'MPN': MPN[LCSC[val]]}
D.add(ref, 'Transistor_BJT:MMBT3904', val, FP['SOT23'], {'1': b, '2': e, '3': c},
description=note or ('NPN RF transistor, 650 MHz fT' if val == 'MMBTH10' else 'NPN transistor'),
fields=f)
def PNP(ref, val, b, c, e, note=''):
D.add(ref, 'Transistor_BJT:MMBT3906', val, FP['SOT23'], {'1': b, '2': e, '3': c},
description=note or 'PNP transistor', fields={'LCSC': LCSC['MMBT3906'], 'MPN': 'MMBT3906'})
def DUAL_NPN(ref, b1, c1, e1, b2, c2, e2, note=''):
D.add(ref, 'Transistor_BJT:BC847BS', 'BC847BS', FP['SOT363'],
{'1': e1, '2': b1, '6': c1, '3': c2, '4': e2, '5': b2},
description=note or 'Dual NPN (matched pair), SOT-363', units=[1, 2],
fields={'LCSC': LCSC['BC847BS'], 'MPN': 'BC847BS'})
def OPAMP(ref, a, b, vp='+5VA', vn='GND', note=''):
"""a, b = (in+, in-, out) for units A and B."""
D.add(ref, 'Amplifier_Operational:TLV9062xD', 'TLV9062IDR', FP['SOIC8'],
{'3': a[0], '2': a[1], '1': a[2], '5': b[0], '6': b[1], '7': b[2], '8': vp, '4': vn},
description=note or 'Dual 10 MHz rail-to-rail I/O op-amp', units=[1, 2, 3],
fields={'LCSC': LCSC['TLV9062IDR'], 'MPN': 'TLV9062IDR'})
def VARACTOR(ref, k, a='GND'):
D.add(ref, 'Device:D_Capacitance', 'BB910', FP['SOD323'], {'1': k, '2': a},
description='Low-voltage tuning varactor, 45 pF @ 0.5 V, C0.5/C5 = 5',
fields={'LCSC': LCSC['BB910'], 'MPN': 'BB910'})
def SCHOTTKY(ref, k, a):
D.add(ref, 'Device:D_Schottky', 'BAT54WS', FP['SOD323'], {'1': k, '2': a},
description='Schottky diode 30 V 200 mA, SOD-323', fields={'LCSC': LCSC['BAT54WS'], 'MPN': 'BAT54WS-7-F'})
ADOM_FIELDS = {'Supplier': 'Adom (factory-installed, not assembled by JLCPCB)'}
def MPIN(ref, net):
D.add(ref, 'AdomMachine:MachinePinLargeStandard', 'MachinePinLargeStandard', FP['MPIN'], {'1': net},
description='Adom machine pin, large / standard (corner: pincer grip + %s)' % net,
fields=dict(ADOM_FIELDS, MPN='MachinePinLargeStandard'),
datasheet='https://wiki.adom.inc/adom/machinepinlargestandard', in_bom=False) # Adom hardware, fitted by us
def MCON(ref, net, label, region=None):
p = D.add(ref, 'AdomMachine:MachineContactMedium', label, FP['MCON'], {'1': net},
description='Adom machine contact, medium: ' + net,
fields=dict(ADOM_FIELDS, MPN='MachineContactMedium'),
datasheet='https://wiki.adom.inc/adom/machinecontactmedium', in_bom=False) # Adom hardware, fitted by us
p.region = region
return p
# Debug test points: bare copper pads (no part, not assembled, excluded from BOM/CPL). Rails and ground get
# 1.5 mm pads (scope ground spring / DMM probe), signals 1 mm. Each shows its short label on the silkscreen.
TEST_POINTS = {
# sheet: [(ref, net, silk label), ...]
'power': [('TP101', 'VBUS', 'VBUS'), ('TP102', 'VIN', 'VIN'), ('TP103', 'VIN_OR', 'VOR'), ('TP104', '+5V', '5V'),
('TP105', '+3V3', '3V3'), ('TP106', '+3.3VA', '3V3A'), ('TP107', '+5VA', '5VA'),
('TP108', 'GND', 'GND'), ('TP109', 'GND', 'GND'), ('TP110', 'CAL_MARK', 'CAL')],
'tune': [('TP201', 'DAC_FM', 'DACF'), ('TP202', 'DAC_AM', 'DACA'), ('TP203', 'PWM_RF', 'PWM'),
('TP204', 'VTF_OUT', 'VTF1'), ('TP205', 'VTA_OUT', 'VTA1'), ('TP206', 'VTR_OUT', 'VTR1'),
('TP207', 'VREF_1V65', 'VREF'), ('TP208', '+5V_FM', '5VFM'), ('TP209', '+5V_AM', '5VAM'),
('TP210', 'GND', 'GND'), ('TP211', 'GND', 'GND')],
'fmfe': [('TP301', 'V_RF1', 'VRF'), ('TP302', 'V_LO1', 'VLO'), ('TP303', 'FMLO_E', 'FLO'),
('TP304', 'IF_MIX', 'MIX'), ('TP305', 'GND', 'GND')],
'amfe': [('TP401', 'V_AM1', 'VAM1'), ('TP402', 'V_AM2', 'VAM2'), ('TP403', 'AM_RFIN', 'AMIN'),
('TP404', 'AMRF_C', 'ARF'), ('TP405', 'AMLO_E', 'ALO'), ('TP406', 'GND', 'GND')],
'if': [('TP501', 'V_IF1', 'VIF1'), ('TP502', 'V_IF2', 'VIF2'), ('TP503', 'CF1_OUT', 'CF1'),
('TP504', 'IF_C1', 'IF1'), ('TP505', 'XO_E', 'XO'), ('TP506', 'GND', 'GND')],
'amif': [('TP601', 'V_IF3', 'VIF3'), ('TP602', 'VBIAS', 'VB'), ('TP603', 'AMF_OUT', 'AMF'),
('TP604', 'VGA1_OUT', 'VGA1'), ('TP605', 'VGA2_OUT', 'VGA2'), ('TP606', 'AM_IFOUT', 'AIF'),
('TP607', 'V_AGC', 'AGC'), ('TP608', 'RSSI_AM', 'RSA'), ('TP609', 'AMA_OUT', 'AMA'),
('TP610', 'GND', 'GND'), ('TP611', 'GND', 'GND')],
'fmif': [('TP701', 'V_IF4', 'VIF4'), ('TP702', 'LIM1_C', 'LIM1'), ('TP703', 'LIM2_C', 'LIM2'),
('TP704', 'RSSI_FM', 'RSF'), ('TP705', 'IF_CNT', 'CNT'), ('TP706', 'VREF_FM', 'VRFM'),
('TP707', 'FM_DC', 'FMDC'), ('TP708', 'FMA_OUT', 'FMA'), ('TP709', 'GND', 'GND'),
('TP710', 'GND', 'GND')],
}
TP_RAILS = {'GND', 'VBUS', 'VIN', 'VIN_OR', '+5V', '+3V3', '+3.3VA', '+5VA', '+5V_FM', '+5V_AM',
'V_RF1', 'V_LO1', 'V_AM1', 'V_AM2', 'V_IF1', 'V_IF2', 'V_IF3', 'V_IF4'}
def TP(ref, net, label):
D.add(ref, 'Connector:TestPoint', label, FP['TP_BIG'] if net in TP_RAILS else FP['TP'], {'1': net},
description='Debug test point (bare pad): ' + net, in_bom=False)
def test_points(sheet):
D.block('tp', 'Debug test points', 'Bare copper pads, labelled on the silkscreen; not assembled.')
for ref, net, label in TEST_POINTS[sheet]:
TP(ref, net, label)
# ================================================================== SHEET 1
D.sheet('power', 'Power, USB & MCU', 'power_mcu.kicad_sch', name='Power_MCU', notes=
'USB-C 5 V in -> PTC -> +5V. AP2112K LDO -> +3V3 (MCU). Ferrite-filtered +5VA (all analog/RF) '
'and +3.3VA (MCU VREF+: ADC / DAC reference). The STM32G0B1 runs USB from HSI48 + CRS and '
'its timers from a 40 MHz crystal; a blank chip boots straight into the ROM USB DFU bootloader.')
D.block('usb', 'USB-C / machine-pin power input & ESD',
'Device-only USB 2.0: 5.1k CC pull-downs, USBLC6 on D+/D-. 5 V comes from USB-C or from the '
'VIN machine contact MC1 (Adom control panel PS2 or USB 5 V); B5819W diodes OR the two sources.')
D.add('J101', 'Connector:USB_C_Receptacle_USB2.0_16P', 'USB-C', FP['USBC'],
{'A1': 'GND', 'A12': 'GND', 'B1': 'GND', 'B12': 'GND',
'A4': 'VBUS', 'A9': 'VBUS', 'B4': 'VBUS', 'B9': 'VBUS',
'A5': 'CC1', 'B5': 'CC2', 'A6': 'USB_DP', 'B6': 'USB_DP', 'A7': 'USB_DM', 'B7': 'USB_DM',
'A8': None, 'B8': None, 'SH': 'GND'},
description='USB 2.0 Type-C receptacle, 16P', fields={'LCSC': LCSC['TYPE-C-31-M-12'], 'MPN': 'TYPE-C-31-M-12'})
R('R101', '5.1k', 'CC1', 'GND', note='CC1 pull-down (UFP)')
R('R102', '5.1k', 'CC2', 'GND', note='CC2 pull-down (UFP)')
D.add('U101', 'Power_Protection:USBLC6-2SC6', 'USBLC6-2SC6', FP['SOT23-6'],
{'1': 'USB_DP', '6': 'USB_DP', '3': 'USB_DM', '4': 'USB_DM', '2': 'GND', '5': 'VBUS'},
description='USB ESD protection', fields={'LCSC': LCSC['USBLC6-2SC6'], 'MPN': 'USBLC6-2SC6'})
D.add('D105', 'Device:D_Schottky', 'B5819W', FP['SOD123'], {'1': 'VIN_OR', '2': 'VBUS'},
description='OR-ing diode: USB-C VBUS -> 5 V rail', fields={'LCSC': LCSC['B5819W'], 'MPN': 'B5819W'})
D.add('D106', 'Device:D_Schottky', 'B5819W', FP['SOD123'], {'1': 'VIN_OR', '2': 'VIN'},
description='OR-ing diode: machine-pin VIN (Adom control panel 5 V) -> 5 V rail',
fields={'LCSC': LCSC['B5819W'], 'MPN': 'B5819W'})
D.add('F101', 'Device:Polyfuse', '500mA', FP['F1206'], {'1': 'VIN_OR', '2': '+5V'},
description='Resettable fuse 500 mA hold, 1206', fields={'LCSC': LCSC['nSMD050-33V'], 'MPN': 'nSMD050-33V'})
C('C101', '10u', '+5V', 'GND')
C('C102', '100n', '+5V', 'GND')
R('R107', '2.2k', '+5V', 'PWR_LED_A', note='Power LED current limit')
D.add('D104', 'Device:LED', 'RED', FP['LED0603'], {'1': 'GND', '2': 'PWR_LED_A'}, description='Power LED, red 0603',
fields={'LCSC': LCSC['LED_RED'], 'MPN': 'KT-0603R'})
D.block('reg', '3.3 V LDO & filtered analog rails',
'+5VA feeds every RF/IF stage and the op-amps; each stage adds its own RC decoupling.')
D.add('U102', 'Regulator_Linear:AP2112K-3.3', 'AP2112K-3.3', FP['SOT23-5'],
{'1': '+5V', '2': 'GND', '3': '+5V', '4': None, '5': '+3V3'}, description='600 mA 3.3 V LDO',
fields={'LCSC': LCSC['AP2112K-3.3'], 'MPN': 'AP2112K-3.3TRG1'})
C('C103', '1u', '+5V', 'GND')
C('C104', '10u', '+3V3', 'GND')
C('C105', '100n', '+3V3', 'GND')
FB('FB101', '+5V', '+5VA')
C('C106', '22u', '+5VA', 'GND', fp='C0805')
C('C107', '100n', '+5VA', 'GND')
FB('FB102', '+3V3', '+3.3VA')
C('C108', '1u', '+3.3VA', 'GND', note='VREF+ bulk')
C('C109', '10n', '+3.3VA', 'GND', note='VREF+ HF decoupling')
D.block('mcu', 'STM32G0B1 microcontroller (USB audio, control, DFU)',
'ADC: PA0 AM audio, PA1 FM audio, PA2 AM AGC/RSSI, PA3 FM AFC, PA6 FM RSSI. DAC: PA4 FM LO tune, '
'PA5 AM LO tune. PB1 = TIM3_CH4 PWM (RF tracking). PA15 = TIM2_ETR IF counter. PA8 = TIM1_CH1 '
'calibration marker. PB8/PB9 open-drain band enables (5 V tolerant). PB6/PB7 = USART1 debug '
'console. 40 MHz crystal (harmonics at 80 / 120 MHz, clear of the FM band and the LO range). '
'Programming: a blank chip enumerates as ST ROM DFU (empty check); afterwards the firmware reboots '
'into ROM DFU on a USB request. PA14 = SWCLK doubles as BOOT0 for jumper recovery.')
D.add('U103', 'MCU_ST_STM32G0:STM32G0B1CCUx', 'STM32G0B1CCU7', FP['QFN48'],
{'1': None, '2': None, '3': None, '4': '+3.3VA', '5': '+3.3VA', '6': '+3V3', '7': 'GND',
'8': 'HSE_IN', '9': 'HSE_OUT', '10': 'NRST', '11': 'AM_AUDIO', '12': 'FM_AUDIO', '13': 'RSSI_AM',
'14': 'FM_DC', '15': 'DAC_FM', '16': 'DAC_AM', '17': 'RSSI_FM', '18': None, '19': None, '20': 'PWM_RF',
'21': None, '22': None, '23': None, '24': None, '25': 'LED_FM', '26': 'LED_AM', '27': 'LED_STAT',
'28': 'CAL_MARK', '29': None, '30': None, '31': None, '32': None, '33': 'USB_DM', '34': 'USB_DP',
'35': 'SWDIO', '36': 'SWCLK', '37': 'IF_CNT', '38': None, '39': None, '40': None, '41': None,
'42': None, '43': None, '44': None, '45': 'UART_TX', '46': 'UART_RX', '47': 'FM_EN_N', '48': 'AM_EN_N',
'49': 'GND'},
description='ARM Cortex-M0+ MCU, 256 KB flash, 144 KB RAM, USB FS + ROM DFU, 12-bit ADC + dual 12-bit DAC',
fields={'LCSC': LCSC['STM32G0B1CCU7'], 'MPN': 'STM32G0B1CCU7'})
C('C110', '100n', '+3.3VA', 'GND', note='VBAT (tied to the VREF+ rail: they share an escape at the QFN)')
C('C111', '100n', '+3V3', 'GND', note='VDD decoupling')
C('C114', '4.7u', '+3V3', 'GND', note='MCU bulk')
C('C115', '100n', 'NRST', 'GND', note='Reset filter')
R('R103', '10k', 'SWCLK', 'GND', note='PA14 = SWCLK/BOOT0: keeps BOOT0 low (boot from flash)')
D.add('Y101', 'Device:Crystal_GND24', '40MHz', FP['XTAL3225'], {'1': 'HSE_IN', '3': 'HSE_OUT', '2': 'GND', '4': 'GND'},
description='40 MHz crystal, 12 pF load, 20 ohm ESR, SMD 3225 (MCU timebase)',
fields={'LCSC': LCSC['40MHz'], 'MPN': 'SX32Y040000BC1T001'})
C('C116', '18p', 'HSE_IN', 'GND', note='Crystal load, C0G')
C('C117', '18p', 'HSE_OUT', 'GND', note='Crystal load, C0G')
test_points('power')
# ================================================================== SHEET 2
D.sheet('tune', 'Tuning, band switching & user interface', 'tuning_ui.kicad_sch', name='Tuning_UI', notes=
'DAC outputs are scaled x1.45 by rail-to-rail op-amps to 0-4.8 V and heavily RC filtered '
'(varactor noise = LO phase noise). The RF tracking tank uses a filtered 12-bit PWM.')
D.block('vt', 'Varactor tuning voltage amplifiers (DAC1 -> VT_FM, DAC2 -> VT_AM)',
'Gain = 1 + 10k/22k = 1.45. Two RC poles (10k/4.7u, 10k/1u) after each amplifier.')
OPAMP('U201', ('DAC_FM', 'VTF_FB', 'VTF_OUT'), ('DAC_AM', 'VTA_FB', 'VTA_OUT'))
R('R201', '10k', 'VTF_OUT', 'VTF_FB')
R('R202', '22k', 'VTF_FB', 'GND')
R('R203', '10k', 'VTF_OUT', 'VTF_F1')
C('C201', '4.7u', 'VTF_F1', 'GND')
R('R204', '10k', 'VTF_F1', 'VT_FM')
C('C202', '1u', 'VT_FM', 'GND')
R('R205', '10k', 'VTA_OUT', 'VTA_FB')
R('R206', '22k', 'VTA_FB', 'GND')
R('R207', '10k', 'VTA_OUT', 'VTA_F1')
C('C203', '4.7u', 'VTA_F1', 'GND')
R('R208', '10k', 'VTA_F1', 'VT_AM')
C('C204', '1u', 'VT_AM', 'GND')
C('C205', '100n', '+5VA', 'GND', note='U201 decoupling')
D.block('pwm', 'RF tracking voltage (PWM DAC) & 1.65 V ADC mid-rail reference',
'PB1 PWM -> 2-pole RC (16 Hz) -> x1.45 -> VT_RF. VREF_1V65 = VDDA/2 buffered, biases the audio filters.')
R('R209', '10k', 'PWM_RF', 'PWM_F1')
C('C206', '1u', 'PWM_F1', 'GND')
R('R210', '10k', 'PWM_F1', 'PWM_F2')
C('C207', '1u', 'PWM_F2', 'GND')
OPAMP('U202', ('PWM_F2', 'VTR_FB', 'VTR_OUT'), ('VREF_DIV', 'VREF_BUF', 'VREF_BUF'))
R('R211', '10k', 'VTR_OUT', 'VTR_FB')
R('R212', '22k', 'VTR_FB', 'GND')
R('R213', '1k', 'VTR_OUT', 'VT_RF')
C('C208', '1u', 'VT_RF', 'GND')
R('R214', '10k', '+3.3VA', 'VREF_DIV')
R('R215', '10k', 'VREF_DIV', 'GND')
C('C209', '100n', 'VREF_DIV', 'GND')
R('R216', '100', 'VREF_BUF', 'VREF_1V65', note='Isolates op-amp from reservoir cap')
C('C210', '1u', 'VREF_1V65', 'GND')
C('C211', '100n', '+5VA', 'GND', note='U202 decoupling')
D.block('band', 'Band switches (PNP high-side, MCU open-drain)',
'GPIO low = band powered. GPIO high-Z (reset default) = off; 10k pulls base to 5 V.')
PNP('Q201', 'MMBT3906', 'SWF_B', '+5V_FM', '+5VA', note='FM front-end power switch')
R('R217', '10k', '+5VA', 'SWF_B')
R('R218', '1k', 'SWF_B', 'FM_EN_N')
C('C212', '10u', '+5V_FM', 'GND')
C('C213', '100n', '+5V_FM', 'GND')
PNP('Q202', 'MMBT3906', 'SWA_B', '+5V_AM', '+5VA', note='AM front-end power switch')
R('R219', '10k', '+5VA', 'SWA_B')
R('R220', '1k', 'SWA_B', 'AM_EN_N')
C('C214', '10u', '+5V_AM', 'GND')
C('C215', '100n', '+5V_AM', 'GND')
D.block('ui', 'Status LEDs',
'Band and USB status. Everything else (tuning, band, volume, calibration, firmware update) is '
'controlled from the PC over the USB-C cable; no external controls are needed.')
R('R224', '220', 'LED_FM', 'LEDF_A', note='Green LED has ~2.8 V Vf: small resistor from 3.3 V')
D.add('D201', 'Device:LED', 'GREEN (FM)', FP['LED0603'], {'1': 'GND', '2': 'LEDF_A'}, description='FM band LED',
fields={'LCSC': LCSC['LED_GREEN'], 'MPN': 'KT-0603G'})
R('R225', '1k', 'LED_AM', 'LEDA_A')
D.add('D202', 'Device:LED', 'YELLOW (AM)', FP['LED0603'], {'1': 'GND', '2': 'LEDA_A'}, description='AM band LED',
fields={'LCSC': LCSC['LED_YELLOW'], 'MPN': 'KT-0603Y'})
R('R226', '220', 'LED_STAT', 'LEDS_A', note='Blue LED has ~2.8 V Vf: small resistor from 3.3 V')
D.add('D203', 'Device:LED', 'BLUE (USB/stereo)', FP['LED0603'], {'1': 'GND', '2': 'LEDS_A'},
description='Status LED', fields={'LCSC': LCSC['LED_BLUE'], 'MPN': 'KT-0603B'})
test_points('tune')
# ================================================================== SHEET 3
D.sheet('fmfe', 'Antenna, diplexer & FM front end', 'fm_frontend.kicad_sch', name='FM_Frontend', notes=
'FM: high-side LO, IF 10.7 MHz. Varactor tanks use BB910 through a 22 pF series capacitor '
'(reduces RF swing on the diode and sets the tuning ratio).')
D.block('ant', 'Antenna input & diplexer',
'AM branch: 5-pole 2 MHz Butterworth LPF (series L first = high Z at VHF). '
'FM branch: 3-pole 70 MHz HPF (series C first = high Z at MF) + 3-pole 130 MHz LPF.')
D.add('J301', 'Connector:Conn_Coaxial', 'ANT (SMA)', FP['SMA'], {'1': 'ANT', '2': 'GND'},
description='SMA right-angle PCB jack at the board edge (antenna)',
fields={'LCSC': LCSC['BWSMA-KWE-Z001'], 'MPN': 'BWSMA-KWE-Z001'})
R('R301', '100k', 'ANT', 'GND', note='Static bleed')
R('R317', '47k', 'CAL_MARK', 'CAL_INJ', note='Calibration marker: MCU TIM1 square wave, about -60 dBm per harmonic')
C('C323', '100p', 'CAL_INJ', 'ANT', note='C0G, DC block; marker idles low (quiet) outside calibration')
L('L301', '2.2u', 'ANT', 'AMLPF_1', fp='L1210')
C('C301', '2.7n', 'AMLPF_1', 'GND', note='C0G')
L('L302', '8.2u', 'AMLPF_1', 'AMLPF_2', fp='L1210')
C('C302', '2.7n', 'AMLPF_2', 'GND', note='C0G')
L('L303', '2.2u', 'AMLPF_2', 'AM_RFIN', fp='L1210')
C('C303', '47p', 'ANT', 'FMHP_1', note='C0G')
L('L304', '56n', 'FMHP_1', 'GND', note='Wire-wound, Q>30 @100 MHz')
C('C304', '47p', 'FMHP_1', 'FMHP_2', note='C0G')
C('C305', '22p', 'FMHP_2', 'GND', note='C0G')
L('L305', '120n', 'FMHP_2', 'FM_BPF', note='Wire-wound')
C('C306', '22p', 'FM_BPF', 'GND', note='C0G')
D.block('fmrf', 'FM RF amplifier with varactor-tracked tank (88-108 MHz)',
'Ic ~4 mA. Tank 150 nH || (3.3 pF + BB910 via 22 pF); tuned by VT_RF (firmware tracks the LO).')
R('R302', '56', 'FM_BPF', 'GND', note='Filter termination')
C('C307', '1n', 'FM_BPF', 'FMRF_B')
NPN('Q301', 'MMBTH10', 'FMRF_B', 'FMRF_C', 'FMRF_E', note='FM RF amplifier')
R('R303', '4.7k', 'V_RF1', 'FMRF_B')
R('R304', '1.8k', 'FMRF_B', 'GND')
R('R305', '22', 'FMRF_E', 'FMRF_E2', note='Unbypassed degeneration')
R('R306', '82', 'FMRF_E2', 'GND')
C('C308', '1n', 'FMRF_E2', 'GND')
L('L306', '150n', 'FMRF_C', 'V_RF1', note='Tank inductor, wire-wound')
C('C309', '3.3p', 'FMRF_C', 'GND', note='C0G')
C('C310', '22p', 'FMRF_C', 'VAR_RF', note='C0G, varactor series cap')
VARACTOR('D301', 'VAR_RF')
R('R307', '100k', 'VT_RF', 'VAR_RF')
R('R308', '47', '+5V_FM', 'V_RF1')
C('C311', '100n', 'V_RF1', 'GND')
C('C312', '1n', 'V_RF1', 'GND')
C('C313', '4.7p', 'FMRF_C', 'FM_MIX_B', note='C0G, tank tap to mixer')
D.block('fmlo', 'FM local oscillator 98.7-118.7 MHz (common-base Colpitts)',
'Ie ~2 mA. Tank 110 nH || (3.3 pF + BB910 via 22 pF); feedback 4.7 pF / 15 pF. Tuned by VT_FM.')
NPN('Q302', 'MMBTH10', 'FMLO_B', 'FMLO_C', 'FMLO_E', note='FM VCO')
R('R309', '47', '+5V_FM', 'V_LO1')
C('C314', '100n', 'V_LO1', 'GND')
C('C315', '1n', 'V_LO1', 'GND')
R('R310', '10k', 'V_LO1', 'FMLO_B')
R('R311', '4.7k', 'FMLO_B', 'GND')
C('C316', '1n', 'FMLO_B', 'GND', note='Base AC ground')
R('R312', '330', 'FMLO_E', 'GND')
C('C317', '15p', 'FMLO_E', 'GND', note='C0G')
C('C318', '4.7p', 'FMLO_C', 'FMLO_E', note='C0G feedback')
L('L307', '110n', 'FMLO_C', 'V_LO1', note='Tank inductor, wire-wound')
C('C319', '3.3p', 'FMLO_C', 'GND', note='C0G')
C('C320', '22p', 'FMLO_C', 'VAR_LO', note='C0G, varactor series cap')
VARACTOR('D302', 'VAR_LO')
R('R313', '100k', 'VT_FM', 'VAR_LO')
C('C321', '2.2p', 'FMLO_C', 'FM_MIX_B', note='C0G, LO injection')
D.block('fmmix', 'FM mixer (RF + LO at base, IF at collector)',
'Ie ~2 mA, emitter bypassed. Collector drives the shared 10.7 MHz IF node.')
NPN('Q303', 'MMBTH10', 'FM_MIX_B', 'IF_MIX', 'FMMIX_E', note='FM mixer')
R('R314', '22k', '+5V_FM', 'FM_MIX_B')
R('R315', '6.8k', 'FM_MIX_B', 'GND')
R('R316', '150', 'FMMIX_E', 'GND')
C('C322', '10n', 'FMMIX_E', 'GND')
test_points('fmfe')
# ================================================================== SHEET 4
D.sheet('amfe', 'AM front end (up-conversion to 10.7 MHz)', 'am_frontend.kicad_sch', name='AM_Frontend', notes=
'AM 530-1700 kHz is up-converted with a high-side LO at 11.23-12.4 MHz; the image lands at '
'22-23 MHz and is removed by the 2 MHz LPF, so no tracking preselector is needed.')
D.block('amrf', 'AM RF preamplifier',
'50 ohm terminated input, Ic ~3.8 mA, gain ~24 dB.')
R('R401', '51', 'AM_RFIN', 'GND', note='LPF termination')
C('C401', '100n', 'AM_RFIN', 'AMRF_B')
NPN('Q401', 'MMBT3904', 'AMRF_B', 'AMRF_C', 'AMRF_E', note='AM RF preamp')
R('R402', '47', '+5V_AM', 'V_AM1')
C('C402', '10u', 'V_AM1', 'GND')
C('C403', '100n', 'V_AM1', 'GND')
R('R403', '22k', 'V_AM1', 'AMRF_B')
R('R404', '6.8k', 'AMRF_B', 'GND')
R('R405', '22', 'AMRF_E', 'AMRF_E2')
R('R406', '56', 'AMRF_E2', 'GND')
C('C404', '100n', 'AMRF_E2', 'GND')
R('R407', '470', 'V_AM1', 'AMRF_C')
C('C405', '100n', 'AMRF_C', 'AM_MIX_B')
D.block('amlo', 'AM local oscillator 11.23-12.4 MHz (Clapp)',
'L = 8.2 uH in series with 15 pF || (BB910 via 33 pF); divider 220 pF / 470 pF. Tuned by VT_AM.')
NPN('Q402', 'MMBT3904', 'AMLO_B', 'V_AM2', 'AMLO_E', note='AM VCO')
R('R408', '47', '+5V_AM', 'V_AM2')
C('C406', '10u', 'V_AM2', 'GND')
C('C407', '100n', 'V_AM2', 'GND')
R('R409', '22k', 'V_AM2', 'AMLO_B')
R('R410', '22k', 'AMLO_B', 'GND')
R('R411', '1.5k', 'AMLO_E', 'GND')
C('C408', '220p', 'AMLO_B', 'AMLO_E', note='C0G')
C('C409', '470p', 'AMLO_E', 'GND', note='C0G')
L('L401', '8.2u', 'AMLO_B', 'AMLO_T', fp='L1210', note='High-Q wire-wound')
C('C410', '15p', 'AMLO_T', 'GND', note='C0G')
C('C411', '33p', 'AMLO_T', 'VAR_AM', note='C0G, varactor series cap')
VARACTOR('D401', 'VAR_AM')
R('R412', '100k', 'VT_AM', 'VAR_AM')
C('C412', '47p', 'AMLO_E', 'AM_MIX_B', note='C0G, LO injection')
D.block('ammix', 'AM mixer',
'Same topology as the FM mixer; shares the 330 ohm 10.7 MHz IF load.')
NPN('Q403', 'MMBT3904', 'AM_MIX_B', 'IF_MIX', 'AMMIX_E', note='AM mixer')
R('R413', '22k', '+5V_AM', 'AM_MIX_B')
R('R414', '6.8k', 'AM_MIX_B', 'GND')
R('R415', '150', 'AMMIX_E', 'GND')
C('C413', '100n', 'AMMIX_E', 'GND')
test_points('amfe')
# ================================================================== SHEET 5
D.sheet('if', '10.7 MHz IF & second conversion to 455 kHz', 'if_10m7.kicad_sch', name='IF_10M7', notes=
'Two 10.7 MHz ceramic filters (230 kHz) around one BJT gain stage, then a BJT mixer with a '
'10.24 MHz crystal oscillator converts both bands to 455 kHz.')
D.block('cf1', 'Mixer load & first 10.7 MHz ceramic filter')
R('R501', '330', 'V_IF1', 'IF_MIX', note='Mixer load = filter source impedance')
C('C501', '10n', 'IF_MIX', 'CF1_IN')
C('C502', '22p', 'IF_MIX', 'GND', note='Shunts VHF LO feed-through')
D.add('FL501', 'AMFM:CeramicFilter_3pin', 'HCCF3-10.700-LTCVS3L', FP['CF3'],
{'1': 'CF1_IN', '2': 'GND', '3': 'CF1_OUT'}, description='10.7 MHz SMD ceramic filter, 180 kHz BW, 330 ohm',
fields={'LCSC': LCSC['HCCF3-10.700-LTCVS3L'], 'MPN': 'HCCF3-10.700-LTCVS3L'})
R('R502', '22', '+5VA', 'V_IF1')
C('C503', '10u', 'V_IF1', 'GND')
C('C504', '100n', 'V_IF1', 'GND')
R('R503', '330', 'CF1_OUT', 'GND', note='Filter termination')
C('C505', '10n', 'CF1_OUT', 'IF_B1')
D.block('ifamp', '10.7 MHz IF amplifier & second ceramic filter', 'Ic ~3.9 mA, gain ~15 dB.')
NPN('Q501', 'MMBT3904', 'IF_B1', 'IF_C1', 'IF_E1', note='10.7 MHz IF amplifier')
R('R504', '15k', 'V_IF1', 'IF_B1')
R('R505', '5.6k', 'IF_B1', 'GND')
R('R506', '22', 'IF_E1', 'IF_E2')
R('R507', '120', 'IF_E2', 'GND')
C('C506', '100n', 'IF_E2', 'GND')
R('R508', '330', 'V_IF1', 'IF_C1')
C('C507', '10n', 'IF_C1', 'CF2_IN')
D.add('FL502', 'AMFM:CeramicFilter_3pin', 'HCCF3-10.700-LTCVS3L', FP['CF3'],
{'1': 'CF2_IN', '2': 'GND', '3': 'CF2_OUT'}, description='10.7 MHz SMD ceramic filter, 180 kHz BW, 330 ohm',
fields={'LCSC': LCSC['HCCF3-10.700-LTCVS3L'], 'MPN': 'HCCF3-10.700-LTCVS3L'})
R('R509', '330', 'CF2_OUT', 'GND', note='Filter termination')
C('C508', '10n', 'CF2_OUT', 'MIX2_B')
D.block('mix2', 'Second mixer 10.7 MHz -> 455 kHz', 'Ie ~1.4 mA; 1.5 kohm collector load sources the 455 kHz filters.')
NPN('Q502', 'MMBT3904', 'MIX2_B', 'IF2_OUT', 'MIX2_E', note='2nd mixer')
R('R510', '22k', 'V_IF2', 'MIX2_B')
R('R511', '6.8k', 'MIX2_B', 'GND')
R('R512', '220', 'MIX2_E', 'GND')
C('C510', '100n', 'MIX2_E', 'GND')
R('R513', '1.5k', 'V_IF2', 'IF2_OUT')
C('C511', '100p', 'IF2_OUT', 'GND', note='Rolls off 10 MHz LO feed-through')
R('R514', '22', '+5VA', 'V_IF2')
C('C512', '10u', 'V_IF2', 'GND')
C('C513', '100n', 'V_IF2', 'GND')
D.block('xo', '10.24 MHz crystal oscillator (Colpitts)',
'Ie ~1.1 mA; crystal load ~12 pF (100p/220p divider + 15 pF series). With 10.240 MHz the first '
'IF is 10.695 MHz: firmware tunes LO1 = RF + 10.695 MHz so the second IF lands on 455 kHz.')
NPN('Q503', 'MMBT3904', 'XO_B', 'V_IF2', 'XO_E', note='Crystal oscillator')
R('R515', '47k', 'V_IF2', 'XO_B')
R('R516', '47k', 'XO_B', 'GND')
R('R517', '1.5k', 'XO_E', 'GND')
C('C514', '100p', 'XO_B', 'XO_E', note='C0G')
C('C515', '220p', 'XO_E', 'GND', note='C0G')
D.add('Y501', 'Device:Crystal_GND24', '10.24MHz', FP['XTAL3225'], {'1': 'XO_B', '3': 'XO_X', '2': 'GND', '4': 'GND'},
description='10.240 MHz crystal, 12 pF load, SMD 3225', fields={'LCSC': LCSC['10.24MHz'], 'MPN': '0132M4-10.240F12DZNG'})
C('C516', '15p', 'XO_X', 'GND', note='C0G, crystal load trim (12 pF crystal)')
C('C509', '22p', 'XO_E', 'MIX2_B', note='C0G, LO injection')
test_points('if')
# ================================================================== SHEET 6
D.sheet('amif', 'AM 455 kHz IF, AGC & envelope detector', 'am_if.kicad_sch', name='AM_IF', notes=
'Gain is set by the tail currents of two BC847BS differential pairs (~40 dB range each); '
'an op-amp integrator holds the detected carrier at ~1 V.')
D.block('amf', '455 kHz AM channel filter & bias', 'HCCF2-455 LTWCGL: +/-4.5 kHz @ 6 dB, 2 kohm terminations.')
R('R602', '22', '+5VA', 'V_IF3')
C('C603', '10u', 'V_IF3', 'GND')
C('C604', '100n', 'V_IF3', 'GND')
C('C601', '10n', 'IF2_OUT', 'AMF_IN')
D.add('FL601', 'AMFM:CeramicFilter_4pin', 'HCCF2-455.000-LTWCGL', FP['CF4'],
{'1': 'AMF_IN', '2': 'AMF_OUT', '3': 'GND', '4': 'GND'},
description='455 kHz SMD ceramic filter, +/-4.5 kHz, 2 kohm',
fields={'LCSC': LCSC['HCCF2-455.000-LTWCGL'], 'MPN': 'HCCF2-455.000-LTWCGL'})
R('R601', '2.7k', 'AMF_OUT', 'GND', note='Termination (with VGA input ~2 kohm total)')
C('C602', '10n', 'AMF_OUT', 'VGA1_IN')
R('R603', '6.8k', 'V_IF3', 'VBIAS')
R('R604', '10k', 'VBIAS', 'GND')
C('C605', '10u', 'VBIAS', 'GND')
C('C606', '100n', 'VBIAS', 'GND')
D.block('vga', 'AGC variable-gain IF (2 x BJT differential pair)',
'Gain per stage = Itail*Rc/(4*Vt): 0.9 mA -> x19, 10 uA -> x0.2. AGC_B = V_AGC/2.')
R('R605', '10k', 'VBIAS', 'VGA1_IN')
DUAL_NPN('Q601', 'VGA1_IN', 'V_IF3', 'VGA1_E', 'VBIAS', 'VGA1_OUT', 'VGA1_E', note='VGA1 differential pair')
NPN('Q602', 'MMBT3904', 'AGC_B', 'VGA1_E', 'VGA1_T', note='VGA1 tail current source')
R('R606', '2.2k', 'V_IF3', 'VGA1_OUT')
R('R607', '2k', 'VGA1_T', 'GND')
C('C608', '10n', 'VGA1_OUT', 'VGA2_IN')
R('R610', '10k', 'VBIAS', 'VGA2_IN')
DUAL_NPN('Q603', 'VGA2_IN', 'V_IF3', 'VGA2_E', 'VBIAS', 'VGA2_OUT', 'VGA2_E', note='VGA2 differential pair')
NPN('Q604', 'MMBT3904', 'AGC_B', 'VGA2_E', 'VGA2_T', note='VGA2 tail current source')
R('R611', '2.2k', 'V_IF3', 'VGA2_OUT')
R('R612', '2k', 'VGA2_T', 'GND')
R('R608', '10k', 'V_AGC', 'AGC_B')
R('R609', '10k', 'AGC_B', 'GND')
C('C607', '100n', 'AGC_B', 'GND')
D.block('det', 'IF post-amplifier & envelope detector', 'Ic ~1.1 mA, gain ~30. Detector RC = 22 us.')
C('C609', '10n', 'VGA2_OUT', 'AMP_B')
NPN('Q605', 'MMBT3904', 'AMP_B', 'AM_IFOUT', 'AMP_E', note='455 kHz post-amp')
R('R613', '33k', 'V_IF3', 'AMP_B')
R('R614', '10k', 'AMP_B', 'GND')
R('R615', '47', 'AMP_E', 'AMP_E2')
R('R616', '330', 'AMP_E2', 'GND')
C('C610', '100n', 'AMP_E2', 'GND')
R('R617', '2.2k', 'V_IF3', 'AM_IFOUT')
C('C611', '10n', 'AM_IFOUT', 'DET_A')
R('R618', '47k', 'DET_A', 'GND')
SCHOTTKY('D601', 'AM_DET', 'DET_A')
C('C612', '2.2n', 'AM_DET', 'GND')
R('R619', '10k', 'AM_DET', 'GND')
D.block('agc', 'AGC integrator, RSSI & AM audio low-pass (5 kHz)',
'U601A: PI integrator (100k, 4.7k + 1u) vs 0.98 V reference. U601B: unity-gain Sallen-Key '
'Butterworth 5 kHz, audio re-biased to VREF_1V65 for the ADC.')
OPAMP('U601', ('AGC_REF', 'AGC_N', 'V_AGC'), ('AMA_P', 'AMA_OUT', 'AMA_OUT'))
R('R620', '100k', 'AM_DET', 'AGC_N')
R('R623', '4.7k', 'V_AGC', 'AGC_RC')
C('C614', '1u', 'AGC_RC', 'AGC_N')
R('R621', '39k', 'V_IF3', 'AGC_REF')
R('R622', '10k', 'AGC_REF', 'GND')
C('C613', '1u', 'AGC_REF', 'GND')
R('R624', '10k', 'V_AGC', 'RSSI_AM')
R('R625', '10k', 'RSSI_AM', 'GND')
C('C615', '100n', 'RSSI_AM', 'GND')
C('C616', '1u', 'AM_DET', 'AMA_IN')
R('R626', '100k', 'AMA_IN', 'VREF_1V65')
R('R627', '10k', 'AMA_IN', 'AMA_N1')
R('R628', '10k', 'AMA_N1', 'AMA_P')
C('C617', '4.7n', 'AMA_N1', 'AMA_OUT', note='C0G')
C('C618', '2.2n', 'AMA_P', 'GND', note='C0G')
R('R629', '1k', 'AMA_OUT', 'AM_AUDIO', note='Limits injection into the 3.3 V ADC pin if the 5 V op-amp swings high')
C('C619', '1n', 'AM_AUDIO', 'GND', note='ADC sampling reservoir')
C('C620', '100n', '+5VA', 'GND', note='U601 decoupling')
test_points('amif')
# ================================================================== SHEET 7
D.sheet('fmif', 'FM 455 kHz limiter & pulse-count discriminator', 'fm_if.kicad_sch', name='FM_IF', notes=
'Each IF cycle transfers a fixed charge C*(Vpp-2Vf) into the op-amp summing node, so the '
'average current is proportional to frequency (40 uA at 455 kHz, +/-6.6 uA at +/-75 kHz).')
D.block('lim', 'Input low-pass & two-stage BJT limiter', 'Ic ~0.95 mA per stage, ~33 dB per stage.')
R('R701', '22', '+5VA', 'V_IF4')
C('C701', '10u', 'V_IF4', 'GND')
C('C702', '100n', 'V_IF4', 'GND')
C('C703', '1n', 'IF2_OUT', 'FMF_1')
R('R702', '2.2k', 'FMF_1', 'FMF_2')
C('C704', '150p', 'FMF_2', 'GND')
C('C705', '10n', 'FMF_2', 'LIM1_B')
NPN('Q701', 'MMBT3904', 'LIM1_B', 'LIM1_C', 'LIM1_E', note='Limiter stage 1')
R('R703', '33k', 'V_IF4', 'LIM1_B')
R('R704', '10k', 'LIM1_B', 'GND')
R('R705', '470', 'LIM1_E', 'GND')
C('C706', '100n', 'LIM1_E', 'GND')
R('R706', '2.2k', 'V_IF4', 'LIM1_C')
C('C707', '47p', 'LIM1_C', 'GND')
C('C708', '10n', 'LIM1_C', 'LIM2_B')
NPN('Q702', 'MMBT3904', 'LIM2_B', 'LIM2_C', 'LIM2_E', note='Limiter stage 2')
R('R707', '33k', 'V_IF4', 'LIM2_B')
R('R708', '10k', 'LIM2_B', 'GND')
R('R709', '470', 'LIM2_E', 'GND')
C('C709', '100n', 'LIM2_E', 'GND')
R('R710', '2.2k', 'V_IF4', 'LIM2_C')
C('C710', '47p', 'LIM2_C', 'GND')
D.block('rssi', 'FM signal-strength detector (seek)')
C('C711', '10n', 'LIM1_C', 'RS_A')
R('R711', '47k', 'RS_A', 'GND')
SCHOTTKY('D701', 'RSSI_FM', 'RS_A')
C('C712', '100n', 'RSSI_FM', 'GND')
R('R712', '100k', 'RSSI_FM', 'GND')
D.block('sq', 'Squarer & IF frequency counter output',
'DC-coupled saturating switch, threshold ~2.2 V on LIM2_C. IF_CNT (3.3 V) -> TIM2_ETR for AFC.')
R('R713', '10k', 'LIM2_C', 'SQ_B')
C('C713', '100p', 'LIM2_C', 'SQ_B', note='Speed-up capacitor')
R('R714', '4.7k', 'SQ_B', 'GND')
NPN('Q703', 'MMBT3904', 'SQ_B', 'LIM_SQ', 'GND', note='Squarer')
R('R715', '1k', 'V_IF4', 'LIM_SQ')
R('R716', '4.7k', 'LIM_SQ', 'IF_CNT')
R('R717', '10k', 'IF_CNT', 'GND')
D.block('pump', 'Diode-pump discriminator & 75 us de-emphasis integrator',
'FM_DEMOD = VREF_FM - I*51k (~1.8 V at 455 kHz). 51k || 1.5 nF = 76 us.')
C('C714', '22p', 'LIM_SQ', 'PUMP', note='C0G, pump capacitor (sets detector gain)')
D.add('D702', 'Diode:BAT54S', 'BAT54S', FP['SOT23'], {'1': 'VREF_FM', '2': 'SUMJ', '3': 'PUMP'},
description='Dual series Schottky (diode pump)', fields={'LCSC': LCSC['BAT54S']})
OPAMP('U701', ('VREF_FM', 'SUMJ', 'FM_DEMOD'), ('FMA_P', 'FMA_OUT', 'FMA_OUT'))
R('R718', '51k', 'FM_DEMOD', 'SUMJ')
C('C715', '1.5n', 'FM_DEMOD', 'SUMJ', note='C0G, de-emphasis 75 us')
R('R725', '1.2k', 'V_IF4', 'VREF_FM')
R('R726', '4.3k', 'VREF_FM', 'GND')
C('C722', '10u', 'VREF_FM', 'GND')
C('C723', '100n', 'VREF_FM', 'GND')
C('C721', '100n', '+5VA', 'GND', note='U701 decoupling')
D.block('fmaud', 'AFC output & FM audio low-pass (15 kHz)',
'FM_DC = 2/3 x FM_DEMOD (0.1 s) for AFC/tuning. U701B unity-gain Sallen-Key 15.8 kHz, removes 19 kHz pilot.')
R('R719', '100k', 'FM_DEMOD', 'FM_DC')
R('R720', '200k', 'FM_DC', 'GND')
C('C716', '1u', 'FM_DC', 'GND')
C('C717', '1u', 'FM_DEMOD', 'FMA_IN')
R('R721', '100k', 'FMA_IN', 'VREF_1V65')
R('R722', '10k', 'FMA_IN', 'FMA_N1')
R('R723', '10k', 'FMA_N1', 'FMA_P')
C('C718', '1.5n', 'FMA_N1', 'FMA_OUT', note='C0G')
C('C719', '680p', 'FMA_P', 'GND', note='C0G')
R('R724', '1k', 'FMA_OUT', 'FM_AUDIO', note='Limits injection into the 3.3 V ADC pin if the 5 V op-amp swings high')
C('C720', '1n', 'FM_AUDIO', 'GND', note='ADC sampling reservoir')
test_points('fmif')
# ================================================================== SHEET 8: MOLECULE INTERFACE
# Grid positions are mm from MP1 (the origin corner pin), +x right, +y down in KiCad. The corner pins are
# LARGE machine pins, so they sit on the 32 mm large-class grid (96 x 96 mm = 3 x 3 cells) and the molecule
# mounts straight onto the base scaffold. The contacts are MEDIUM (the small size) on the 2 mm grid, and none
# is closer than 6 mm to a large pin (its pincer keep-out).
MOLECULE_PITCH = (96, 96)
MOLECULE_PINS = [('MP1', 'GND', 0, 0), ('MP2', 'GND', 96, 0), ('MP3', 'GND', 0, 96), ('MP4', 'GND', 96, 96)]
EDGE_CONTACTS = [
# ref, net, silk label, grid x, grid y
('MC1', 'VIN', '5V', 0, 6), ('MC2', 'GND', 'GND', 0, 8),
('MC3', 'USB_DP', 'D+', 0, 24), ('MC4', 'USB_DM', 'D-', 0, 26), ('MC5', 'GND', 'GND', 0, 28),
('MC6', '+3V3', '3V3', 0, 34), ('MC7', 'SWCLK', 'CLK', 0, 36),
('MC8', 'NRST', 'RST', 0, 38), ('MC9', 'GND', 'GND', 0, 40),
('MC10', 'SWDIO', 'DIO', 0, 44), ('MC11', 'GND', 'GND', 0, 46),
('MC12', 'UART_TX', 'TX', 0, 52), ('MC13', 'UART_RX', 'RX', 0, 54), ('MC14', 'GND', 'GND', 0, 56),
('MC15', 'AM_AUDIO', 'AM', 0, 84), ('MC16', 'FM_AUDIO', 'FM', 0, 86), ('MC17', 'GND', 'GND', 0, 88),
('MC18', 'ANT', 'ANT', 96, 20), ('MC19', 'GND', 'GND', 96, 22),
]
PROBE_CONTACTS = [
# ref, net, silk label, placement region (the circuit it probes); snapped to the 2 mm grid
('MC20', 'CF2_OUT', '10M7', 'if'), ('MC21', 'IF2_OUT', '455K', 'if'), ('MC22', 'AM_DET', 'DET', 'amif'),
('MC23', 'FM_DEMOD', 'DEM', 'fmif'), ('MC24', 'LIM_SQ', 'SQ', 'fmif'), ('MC25', 'VT_FM', 'VTF', 'tune'),
('MC26', 'VT_AM', 'VTA', 'tune'), ('MC27', 'VT_RF', 'VTR', 'tune'), ('MC28', '+5VA', '5VA', 'tune'),
]
D.sheet('mol', 'Molecule interface: machine pins & contacts', 'molecule_io.kicad_sch', name='Molecule_IO', notes=
'Adom Molecule (Molecule Design Guide v1.0): large / standard machine pins in the four corners, '
'96 x 96 mm apart on the 32 mm large grid (mounts directly on the base scaffold), and medium machine '
'contacts on the 2 mm grid measured from MP1. All four corner '
'pins are GND; 5 V comes in on the VIN contact (MC1) or USB-C. None of the contacts is needed in use: '
'power, control, audio and firmware updates all run over USB-C. SWD, UART and the audio outputs are '
'optional debug / monitor points; CLK(=BOOT0)/3V3 and RST/GND are adjacent pairs for jumper recovery.')
D.block('pins', 'Corner machine pins, large (pincer grip points)', 'MP1..MP4 = GND (5 V enters on contact MC1 or USB-C).')
for ref, net, gx, gy in MOLECULE_PINS:
MPIN(ref, net)
D.block('edge', 'Edge contacts (wired to the control panel / other molecules)',
'Left edge: power, USB, SWD (CLK doubles as BOOT0), reset, UART, audio out. '
'Right: antenna (in parallel with the SMA jack).')
for ref, net, label, gx, gy in EDGE_CONTACTS:
MCON(ref, net, label)
D.block('probe', 'Probe contacts (machine-probe points; accept probe needles)',
'10.7 MHz and 455 kHz IF, AM detector, FM discriminator, limiter output, the three tuning voltages, +5VA.')
for ref, net, label, region in PROBE_CONTACTS:
MCON(ref, net, label, region=region)
DESIGN = D
# Nets that get a power symbol in the schematic
POWER_NETS = {'GND', '+5V', '+5VA', '+3V3', '+3.3VA', 'VBUS'}
def check():
nets = D.nets()
problems = []
for n, nodes in nets.items():
if len(nodes) < 2:
problems.append('net %s has a single connection %s' % (n, nodes))
return nets, problems
if __name__ == '__main__':
nets, problems = check()
print('%d parts, %d nets' % (len(D.parts), len(nets)))
for p in problems:
print('WARN', p)
by_sheet = {}
for p in D.parts:
by_sheet.setdefault(p.sheet, 0)
by_sheet[p.sheet] += 1
print(by_sheet)