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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
"""Functional-block floorplan for the molecule: one silkscreen-outlined, labelled rectangle per circuit block.
Coordinates are board-local mm (0, 0 = top-left board corner; the board is 102.5 x 102.5 mm and MP1 sits at
3.25, 3.25). Signal flow: antenna (top right) -> front ends (right column) -> 10.7 MHz IF (top middle) ->
455 kHz AM / FM IF (middle / bottom middle) -> MCU (left column). The LDO sits under the MCU, and the LED /
audio-out block below it (its contacts continue the left-edge contact column); tuning sits bottom right, so only
five slow DC control lines cross the IF column. Rectangles are separated by 1 mm routing channels.
"""
from collections import Counter, defaultdict
from design import DESIGN
# key: (sheet, block) -> (silk label, (x0, y0, x1, y1))
BLOCKS = {
# ---- left column: power in, MCU, LDO (the left-edge contacts sit inside their block)
('power', 'usb'): ('USB-C / POWER IN', (0.6, 7.3, 20.6, 33.4)),
('power', 'mcu'): ('STM32 MCU', (0.6, 34.4, 20.6, 64.6)),
('power', 'reg'): ('3.3 V LDO / RAILS', (0.6, 65.6, 20.6, 78.6)),
# ---- middle column, top: 10.7 MHz IF and second conversion
('if', 'xo'): ('10.24 MHz XO', (21.6, 1.2, 39.6, 14.4)),
('if', 'ifamp'): ('10.7 MHz IF AMP', (40.6, 1.2, 63.3, 14.4)),
('if', 'mix2'): ('2ND MIXER', (21.6, 15.4, 41.6, 28.6)),
('if', 'cf1'): ('10.7 MHz FILTER', (42.6, 15.4, 63.3, 28.6)),
# ---- middle column: AM 455 kHz IF
('amif', 'amf'): ('455 kHz AM FILTER', (21.6, 29.6, 45.1, 47.1)),
('amif', 'vga'): ('AGC VGA', (46.1, 29.6, 63.3, 47.1)),
('amif', 'agc'): ('AGC / RSSI / AM AUDIO', (21.6, 48.1, 45.1, 65.6)),
('amif', 'det'): ('IF AMP / AM DETECTOR', (46.1, 48.1, 63.3, 65.6)),
# ---- middle column, bottom: FM 455 kHz IF
('fmif', 'lim'): ('FM LIMITER', (21.6, 66.6, 45.1, 85.6)),
('fmif', 'rssi'): ('FM RSSI', (46.1, 66.6, 63.3, 74.1)),
('fmif', 'sq'): ('SQUARER / IF COUNT', (46.1, 75.1, 63.3, 85.6)),
('fmif', 'fmaud'): ('AFC / FM AUDIO', (21.6, 86.6, 38.6, 101.4)),
('fmif', 'pump'): ('FM DISCRIMINATOR', (39.6, 86.6, 63.3, 101.4)),
# ---- right column, top: antenna and FM front end
('fmfe', 'fmmix'): ('FM MIXER', (64.3, 1.2, 77.8, 12.6)),
('fmfe', 'fmlo'): ('FM LO', (64.3, 13.6, 77.8, 36.0)),
('fmfe', 'fmrf'): ('FM RF AMP', (78.8, 1.2, 88.0, 36.0)),
('fmfe', 'ant'): ('ANT / DIPLEXER', (89.0, 7.3, 101.9, 36.0)),
# ---- right column, middle: AM front end
('amfe', 'ammix'): ('AM MIXER', (64.3, 37.0, 71.3, 58.0)),
('amfe', 'amlo'): ('AM LO', (72.3, 37.0, 87.0, 58.0)),
('amfe', 'amrf'): ('AM RF AMP', (88.0, 37.0, 101.9, 58.0)),
# ---- right column, bottom: tuning, band switching, UI
('tune', 'vt'): ('VARACTOR DRIVE', (64.3, 59.0, 83.0, 81.5)),
('tune', 'pwm'): ('RF TRACKING / VREF', (84.0, 59.0, 101.9, 81.5)),
('tune', 'band'): ('BAND SWITCH', (64.3, 82.5, 101.9, 95.2)),
('tune', 'ui'): ('LEDS / AUDIO OUT', (0.6, 79.6, 20.6, 95.2)),
}
# Edge contacts drawn inside a block's outline (their function belongs to it); the rest stay outside
CONTACT_BLOCK = {}
for _i in range(1, 6):
CONTACT_BLOCK['MC%d' % _i] = ('power', 'usb') # 5V/GND, D+/D-/GND
for _i in range(6, 15):
CONTACT_BLOCK['MC%d' % _i] = ('power', 'mcu') # 3V3/CLK(BOOT0)/RST, SWD, UART (all optional)
for _i in range(15, 18):
CONTACT_BLOCK['MC%d' % _i] = ('tune', 'ui') # audio out (left edge, bottom)
for _i in (18, 19):
CONTACT_BLOCK['MC%d' % _i] = ('fmfe', 'ant') # antenna
# Ground test pads: one per roomy block
GND_TP_BLOCK = {
'TP108': ('tune', 'ui'), 'TP109': ('power', 'mcu'), 'TP210': ('tune', 'band'), 'TP211': ('tune', 'band'),
'TP305': ('fmfe', 'fmrf'), 'TP406': ('amfe', 'ammix'), 'TP506': ('if', 'xo'), 'TP610': ('amif', 'det'),
'TP611': ('amif', 'agc'), 'TP709': ('fmif', 'rssi'), 'TP710': ('fmif', 'fmaud'),
}
# Board-local boxes kept free of parts for free-standing silkscreen text (the LED names above D201-D203)
TEXT_KEEPOUTS = {'~LEDTXT': (8.4, 80.4, 20.0, 82.0)}
# Signal test pads whose net is shared with the MCU: put them in the analog block they probe, not around the MCU
TP_BLOCK = {'TP201': ('tune', 'vt'), 'TP202': ('tune', 'vt'), 'TP203': ('tune', 'pwm')}
# Inset from a block outline to the placement area (the label sits on the top edge)
INSET_SIDE, INSET_TOP = 0.45, 0.9
def block_of_parts():
"""(sheet, block) for every part: test pads and probe contacts join the block that owns their net."""
netpins = defaultdict(list)
for p in DESIGN.parts:
for n in p.pins.values():
netpins[n].append(p)
out = {}
for p in DESIGN.parts:
key = (p.sheet, p.block)
if p.ref in GND_TP_BLOCK:
key = GND_TP_BLOCK[p.ref]
elif p.ref in TP_BLOCK:
key = TP_BLOCK[p.ref]
elif p.ref in CONTACT_BLOCK:
key = CONTACT_BLOCK[p.ref]
elif p.block == 'tp' or (p.sheet == 'mol' and p.block == 'probe'):
net = list(p.pins.values())[0]
c = Counter((q.sheet, q.block) for q in netpins[net]
if (q.sheet, q.block) in BLOCKS and not q.ref.startswith(('TP', 'MC')))
if c:
key = c.most_common(1)[0][0]
out[p.ref] = key if key in BLOCKS else None
return out
def place_rect(key):
x0, y0, x1, y1 = BLOCKS[key][1]
return (x0 + INSET_SIDE, y0 + INSET_TOP, x1 - INSET_SIDE, y1 - INSET_SIDE)