main
noah amfm-receiver-molecule: KiCad project, fab files, 3D and README (6/7) 53707c4 1d ago
"""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)