diff --git a/design/visualizer-preview.fragment.html b/design/visualizer-preview.fragment.html new file mode 100644 index 0000000..7a3ec4a --- /dev/null +++ b/design/visualizer-preview.fragment.html @@ -0,0 +1,285 @@ +UHD Visualizer — Screen Mockups + +
+
+
OpenUHD · design/visualizer.pen
+

UHD Pair Visualizer — screen mockups

+

Faithful render of the locked visual designs for protoboard.ai/validate and /parts. Four production screens, then the concept and iteration boards (B-series) that the visualizer spec references.

+
+ +
Production screens
+
+
+

Validate — Results

+

The hero screen at protoboard.ai/validate — two module cards with the B8 shared-region pair scene, verdict banner, and per-connection DRC cards.

+ 1440px +
+
protoboard
/ validate
Parts
Validate
Docs
GitHub
Interface Validator
Check how two UHD modules connect — compatibility, capacity, and parametric constraints.
?a=arduino-nano&b=vl53l0x
MODULE A
arduino-nano
Change
MODULE B
vl53l0x
Change
i2c · master
i2c · slave · 0x29
i2c ↔ i2c
bus · protocol · VALID
sda
A4
sda ↔ sda
sda
SDA
scl
A5
scl ↔ scl
scl
SCL
power · source
power · sink · bridged 2.6–5.5 V
5v ↔ vin
wire · protocol · WARNING
5v
500 mA
5v → vin · regulator onboard
vin
VIN
digital · out
digital · in
d2 ↔ xshut
wire · protocol · VALID
d2
D2
d2 → xshut · shutdown ctl
xshut
XSHUT
UNCONNECTED
3v3
compatible · alternative supply (one_of with 5v)
spi
no counterpart
uart
no counterpart
usb · a0–a3
no counterpart
UNCONNECTED
gpio1
compatible: d3 · interrupt
2v8_out
no counterpart
Select Module A
Select Module B
3
DRC Results
Compatible with warnings
3 connections found · 2 passed · 1 warning · 0 errors
Export report
arduino-nano.i2c ↔ vl53l0x.i2c
bus · 2 conductors
PASS
PROTOCOL
i2c master ↔ slave — roles compatible
SLOT MAP
sda ↔ sda (A4) · scl ↔ scl (A5) — profile: default
PARAMETRIC
logic level 5.0 V within slave tolerant range 2.6 – 5.5 V
CAPACITY
bus address 0x29 — 1 of 112 addresses in use
arduino-nano.5v ↔ vl53l0x.vin
wire
1 WARNING
PROTOCOL
power source ↔ sink — roles compatible
PARAMETRIC
supply 4.5 – 5.5 V exceeds bare-die VCC range 2.6 – 3.5 V — onboard regulator required (present on this breakout)
CAPACITY
5V rail budget: 19 mA of 500 mA
arduino-nano.d2 ↔ vl53l0x.xshut
wire
PASS
PROTOCOL
digital out ↔ in — shutdown control, optional connection
UNMATCHED INTERFACES — NOT ERRORS, JUST NO COUNTERPART
arduino-nano
spi
uart
3v3
d3–d13
a0–a3
vin
vl53l0x
gpio1 (interrupt)
Other matches for arduino-nano
Browse all parts →
3 ifaces match
bme280
Bosch · env sensor
2 ifaces match
ssd1306
Solomon · OLED 128×64
4 ifaces match
l298n
ST · motor driver
2 ifaces match
sg90-servo
TowerPro · micro servo
+
+
+
+

Validate — Select Module B

+

Step ② of the validator wizard: search/filter the library, pre-ranked by compatibility with the module already in slot A.

+ 1440px +
+
protoboard
/ validate
Parts
Validate
Docs
GitHub
Interface Validator
Step 2 — choose the module to validate against arduino-nano.
?a=arduino-nano
MODULE A
Change
arduino-nano
Arduino · A000005 · rev3
i2c
spi
uart
5v
Connections will appear here once Module B is selected
Select Module B
search below or pick a suggested match
Select Module A
2
Select Module B
3
DRC Results
Search 168 parts — name, protocol, manufacturer…
Filters
FILTER:
compatible with A
i2c
sensor
electrical
mechanical
Suggested matches — ranked by shared interfaces with arduino-nano
3 ifaces match
vl53l0x
STMicro · ToF distance sensor
i2c
2.8v
gpio
3 ifaces match
bme280
Bosch · temp / humidity / pressure
i2c
spi
3v3
2 ifaces match
ssd1306
Solomon · OLED display 128×64
i2c
3v3
4 ifaces match
l298n
ST · dual H-bridge motor driver
motor
pwm
12v
All parts
sg90-servo
TowerPro · micro servo 9 g
pwm
5v
hc-sr04
Elecfreaks · ultrasonic ranger
digital
5v
raspberry-pi-pico
Raspberry Pi · RP2040 dev board
i2c
spi
uart
usb
buck-mp1584
MPS · 3 A step-down module
power
12v→5v
+
+
+
+

Parts — Library

+

protoboard.ai/parts — the step.parts-style dense card grid with search, filter, and pagination.

+ 1440px +
+
protoboard
/ parts
Parts
Validate
Docs
GitHub
Parts Library
168 open UHD module definitions — interfaces, parameters, and artifacts. Apache-2.0.
Contribute a part
Search parts — name, protocol, manufacturer, taxonomy…
Show filters
1–12 of 168
Sort: recently added
arduino-nano
Arduino · ATmega328P dev board
i2c
spi
uart
5v
uhd.json
vl53l0x
STMicro · ToF distance sensor
i2c
2.8v
gpio
uhd.json
bme280
Bosch · temp / humidity / pressure
i2c
spi
3v3
uhd.json
ssd1306
Solomon · OLED display 128×64
i2c
3v3
uhd.json
l298n
ST · dual H-bridge motor driver
motor
pwm
12v
uhd.json
raspberry-pi-pico
Raspberry Pi · RP2040 dev board
i2c
spi
uart
usb
uhd.json
sg90-servo
TowerPro · micro servo 9 g
pwm
5v
uhd.json
hc-sr04
Elecfreaks · ultrasonic ranger
digital
5v
uhd.json
buck-mp1584
MPS · 3 A step-down module
power
12v→5v
uhd.json
nema17-stepper
Generic · bipolar stepper 1.8°
motor
12v
mech
uhd.json
esp32-devkitc
Espressif · Wi-Fi + BLE dev board
i2c
spi
uart
wifi
uhd.json
battery-12v-3s
Generic · LiPo pack 3S 2200 mAh
power
12v
uhd.json
1
2
3
14
+
+
+
+

Parts — Detail

+

A single part page: metadata, the UHD interface table, artifacts, and the raw uhd.json toggle.

+ 1440px +
+
protoboard
/ parts / arduino-nano
Parts
Validate
Docs
GitHub
Parts Library
arduino-nano
Arduino Nano rev3 — ATmega328P development board. 14 digital I/O, 8 analog inputs, composed I2C / SPI / UART interfaces with pin-profile bindings.
MANUFACTURER
Arduino
PART NUMBER
A000005
VERSION
1.2.0
DOMAINS
electrical
TAXONOMY
mcu.dev-board
LICENSE
Apache-2.0
i2c
spi
uart
pwm
5v
atmega328p
Validate against another part
uhd.json
Datasheet
UHD definition — interfaces
View raw JSON
INTERFACE
DOMAIN
PROTOCOL · ROLE
SLOTS / PROFILE
PARAMETERS
i2c
electrical
i2c · master
sda → A4 · scl → A5 (profile: default)
f_max 400 kHz
spi
electrical
spi · master
mosi → D11 · miso → D12 · sck → D13 · cs → D10
f_max 8 MHz
uart
electrical
uart · peer
tx → D1 · rx → D0
115 200 baud max
5v
electrical
power · source
4.5 – 5.5 V · 500 mA max
3v3
electrical
power · source
3.3 V · 150 mA max
d2 – d13
electrical
digital · in / out
12 pins · pwm on 6
40 mA per pin
a0 – a7
electrical
analog · in
8 pins · 10-bit ADC
0 – 5 V
interface group · power: one_of( usb, vin, 5v-input ) — only one supply may be active
Artifacts
atmega328p-datasheet.pdf
datasheet
external · microchip.com
arduino-nano.step
3d_model
hosted
2.1 MB
nano-pinout.png
documentation
hosted
340 KB
blink-example.hex
firmware
external · github.com
+
+ +
Concept & iteration boards
+
+
+

Concept 0 — Pairing Vocabulary

+

The tier × state × port visual vocabulary the renderings are built from.

+ 720px +
+
Pairing vocabulary — tier × state
Two orthogonal encodings. HOW the pair was established (tier) = line pattern + badge. WHAT the DRC concluded (state) = color. Shared by all three concepts.
TIER — LINE PATTERN
protocol
1.0
Both sides declare the protocol. Solid, full-weight line.
compositional
0.8
One side composed from slots; other side fills them. Solid line with slot junction dot.
inferred
0.5
Counterpart never declares the interface — structure implies it. Dashed line, ghost chip, * prefix.
manual
0.3
User-asserted wiring, no protocol backing. Dotted line + lock.
none
0.0
No valid interpretation. Broken line with ✕.
STATE — COLOR
VALID
VALID_INFERRED
WARNING
CONFIG_NEEDED
INCOMPATIBLE
+
+
+
+

Concept A — Harness Table (WireViz)

+

Export-only build-sheet rendering: connector tables with wire colors.

+ 1240px +
+
A
Harness Table — WireViz-style connector tables
Each side is a connector table (physical pin · leaf interface). Every conductor is a row with a wire-color swatch, like a real harness drawing. Wire color = physical conductor; DRC state lives in badges and the footer pairing row. Strength: closest to build documentation — doubles as wiring instructions. Weakness: interface/slot hierarchy is flattened into grouping headers.
CASE 1 — EXPLICIT PROTOCOL MATCH (i2c)
arduino-nano.i2c
i2c · master · profile: default
bus · 2 conductors
protocol · 1.0
VALID
vl53l0x.i2c
i2c · slave · addr 0x29
A4
sda
YE · sda ↔ sda
sda
SDA
A5
scl
GN · scl ↔ scl
scl
SCL
interface pairing: i2c.master ↔ i2c.slave — all 2 required slots resolved · params pass (f_max 400 kHz ∩ 400 kHz)
CASE 2 — COMPOSITIONAL MATCH, LEFT SIDE INFERRED (motor_control)
*motor_control
inferred
not declared — derived from 3 free digital pins
wire · 3 conductors
inferred · 0.5
VALID_INFERRED
l298n.motor_control
motor_control · target · profile: channel_a
D5
d5
capability: pwm
d5 → slot en
slot en
en_a
D6
d6
capability: digital
d6 → slot in1
slot in1
in1
D7
d7
capability: digital
d7 → slot in2
slot in2
in2
*motor_control.source ↔ target — interface synthesized on arduino-nano · 3/3 required slots resolved · confidence 0.5 · [ accept as manual lock ]
+
+
+
+

Concept B — Breakout Funnel

+

Early funnel-view exploration of a connection.

+ 1240px +
+
B
Breakout Funnel — nested containment (ASCII view, evolved)
Direct translation of renderPairAscii. Composition is shown by physical nesting: interface ⊃ slots ⊃ bound leaves. The center lane carries one pill per pairing level — the interface pairing on top (Tier 2), conductor crossings below (Tier 1/0). An inferred interface is a ghost container drawn around bare pins that never declared it. Strength: teaches the UHD model itself — you can see composition. Weakness: taller than A; needs care at high pin counts.
CASE 1 — EXPLICIT PROTOCOL MATCH (i2c)
arduino-nano
i2c — master
composed · profile: default
slot sda
a4
slot scl
a5
i2c.master ↔ i2c.slave
protocol · 1.0
VALID · bus
sda ↔ sda
scl ↔ scl
vl53l0x
i2c — slave
leaf · addr 0x29
sda
scl
CASE 2 — COMPOSITIONAL MATCH, LEFT SIDE INFERRED (motor_control)
arduino-nano
*motor_control — source
inferred — not declared
d5
pwm · free
d6
digital · free
d7
digital · free
These pins never declare motor_control. The container exists only because l298n's slot requirements are structurally satisfiable here.
*motor_control.source ↔ target
inferred · 0.5
VALID_INFERRED · wire
d5 → en
d6 → in1
d7 → in2
l298n
motor_control — target
composed · profile: channel_a
slot en
en_a
slot in1
in1
slot in2
in2
+
+
+
+

Concept C — Tier Stack Inspector

+

The per-connection DRC card stages: pairing / slot table / conductors + parameters.

+ 1240px +
+
C
Tier Stack — connection list + expandable DRC pipeline
Connections are compact rows in a list; expanding one reveals three stacked bands that mirror the DRC evaluation stages exactly — interface pairing (tier determination), slot resolution (sub-link derivation), conductors & parameters. Confidence is a segmented meter. Strength: scales to many connections, 1:1 with the engine so diagnostics slot in naturally. Weakness: least spatial — you don't 'see' the two modules.
arduino-nano.i2c ↔ vl53l0x.i2c
protocol · 1.0
VALID
arduino-nano.5v ↔ vl53l0x.vin
protocol · 1.0
WARNING
arduino-nano.*motor_control ↔ l298n.motor_control
inferred · 0.5
VALID_INFERRED
TIER 2 — INTERFACE PAIRING
stage 2: tier determination
*motor_control · source — inferred, not declared
motor_control · target — declared, profile channel_a
confidence
No protocol match at tier 1 (arduino-nano declares no motor_control). findCompositionalMatch() succeeded: l298n.motor_control's 3 required slots are all fillable from free pins → interface synthesized via deriveInferredInterface().
TIER 1 — SLOT RESOLUTION
stage 3: sub-link derivation
3 / 3 required slots resolved
SLOT (l298n)
REQUIREMENT
METHOD
BOUND LEAF (nano)
NOTE
en
capability: pwm · required
inferred
d5 (D5)
auto-derived — 5 other pwm pins eligible
in1
capability: digital · required
inferred
d6 (D6)
auto-derived
in2
capability: digital · required
inferred
d7 (D7)
auto-derived · [ lock assignment ]
TIER 0 — CONDUCTORS & PARAMETERS
stage 4: parameter checking per sub-link
d5 ↔ en
logic 5.0 V within en tolerant range 2.3 – 7.0 V · pwm 490 Hz within 0 – 25 kHz
d6 ↔ in1
logic level compatible
d7 ↔ in2
in2 current sink unspecified on l298n — cannot validate, no error
Inferred pairings are suggestions — accepting converts each sub-link to a locked manual link (tier manual → confidence set by you, not the engine).
Accept & lock wiring
Dismiss suggestion
+
+
+
+

Recommendation

+

Design recommendation note.

+ 720px +
+
Recommendation — compose all three, one per altitude
These aren't competing skins — they're different altitudes of the same data. Proposed composition for /validate: + +• C (Tier Stack rows) = the DRC results list. Compact, scales, maps 1:1 to the engine's pipeline stages and diagnostic codes. +• B (Breakout Funnel) = the spatial view inside an expanded row (or the slot-rail edge detail). It's the view that teaches interface ⊃ slot ⊃ leaf composition and makes inference visible as a ghost container. +• A (Harness Table) = the export/'build it' view — printable WireViz-style wiring sheet with wire colors, one per harness. + +Shared vocabulary board (left) is the contract all three obey: tier → line pattern + badge, state → color, inferred → purple + dashed + * + sparkle.
+
+
+
+

B1 — Region Nesting

+

Depth encoded by fill, not indentation.

+ 1300px +
+
B1
Region nesting — interface as territory, 3 levels deep
Each composed interface is a soft-filled region; sub-interfaces are lighter regions inside it; leaves are chips. Depth = fill, not indentation. The harness lane mirrors the same tree: conductors are grouped into labeled bundles per sub-interface, so the wiring reads as the same shape as the composition. Example: a smart-actuator port (Dynamixel-style) — actuator_port ⊃ { uart ⊃ tx/rx, power_12v ⊃ v+/gnd }.
3-LEVEL COMPOSED INTERFACE · PROTOCOL TIER · VALID
arm-controller
actuator_port_1
actuator_port · host
uart
uart · host
tx
→ D16
rx
← D17
power_12v
power · source
v+
12 V rail
gnd
common
actuator_port.host ↔ device
protocol · 1.0
VALID · wire · 4 conductors in 2 bundles
bundle · uart
tx → rx
rx ← tx
bundle · power_12v
v+ ↔ v+
gnd ↔ gnd
dynamixel-servo
actuator_port · device
port
uart · device
uart
rx
half-duplex
tx
half-duplex
power · sink
power_12v
v+
9 – 14.8 V
gnd
common
DEPTH KEY:
L1 · composed interface
L2 · sub-interface
L3 · leaf
+
+
+
+

B2 — Depth Rails (compact)

+

Collapsed outline-tree rendering.

+ 980px +
+
B2
Depth rails — same tree, collapsed density
The fallback treatment when region fills get heavy: a 40-pin MCU with six composed interfaces would drown in nested boxes. Same hierarchy, rendered as an outline with depth rails and collapse chevrons. Regions (B1) and rails (B2) can coexist — rails while browsing, region fill only for the interface participating in the active connection.
arm-controller
actuator_port_1
actuator_port · host
in use → dynamixel-servo
uart
uart · host
tx
D16
rx
D17
power_12v
power · source
v+
12 V rail
gnd
common
actuator_port_2
actuator_port · host
4 leaves · available
i2c
i2c · master
2 leaves · available
power_in
power · sink
one_of group
Active-connection highlight
Rows participating in the selected harness get the L1 region tint; everything else stays flat. Region fill becomes a signal, not decoration.
Port dot = connection state
Green dot: leaf is wired in the current pair. No dot: available. The dot column doubles as the attach point for edge lines when this panel sits in the slot rail.
Collapsed rows keep their contract
A collapsed interface still shows protocol · role and availability, so scanning a 40-pin module never requires expansion.
+
+
+
+

B3 — Missing Visualizations

+

Harness-lane feature vignettes (buses, capacity bars…).

+ 1340px +
+
B3
What's still missing — six visualizations to add to B
Gaps found by walking the UHD type system against concept B: three on the harness side (bus topology, capacity, unresolved slots) and three on the module side (claims, interface groups, parameter ranges). Each maps to an existing type or a validatePair() check.
HARNESS · BUS MULTI-DROP
nano.i2c · master
vl53l0x · 0x29
bme280 · 0x76
add drop · 109 addresses free
Topology 'bus' and 'split' need more than two endpoints. The spine stays horizontal; extra drops hang below with their own state dot and address chip. Maps to HarnessDef.endpoints[2..n].
HARNESS · CAPACITY BUDGET
5v rail — 3 sinks
340 / 500 mA
5v rail + servo — 4 sinks
620 / 500 mA · OVER BUDGET
i2c bus — addresses
3 / 112
Aggregated draws vs supply limit, rendered on the harness spine itself. This is the net-new capacity check in validatePair() — sums current_draw across every connection claiming the rail.
HARNESS · UNRESOLVED SLOT
spi.cs — required
?
CONFIG_NEEDED · 1 of 4 slots unresolved
assign leaf…
A required slot with no counterpart leaf renders as an empty socket and an interrupted conductor — the visual for CONFIGURATION_NEEDED. Click assigns manually → becomes a locked manualLink.
MODULE · CLAIMED LEAVES
a4
in use · Sensor I2C bus (sda)
a5
in use · Sensor I2C bus (scl)
d5
available · pwm
A leaf already consumed by another connection is grayed with a lock and names its claimant. Backed by findClaimConflicts() — prevents double-booking A4 for both i2c and analog read.
MODULE · ONE_OF GROUP
power_in
group · one_of
usb
5 V · active — powering this pair
vin
7 – 12 V · disabled while usb active
5v-input
disabled while usb active
InterfaceGroup policies render as a bracketed radio group inside the module card. Selecting one supply disables the siblings — validateInterfaceGroups() already enforces this; the UI just shows it.
MODULE · PARAMETER RANGE OVERLAP
nano.5v · 4.5 – 5.5 V
vl53l0x.vcc · 2.6 – 3.5 V
∅ no overlap — ERROR
suggestion: route via 3v3 rail (3.0 – 3.6 V ∩ vcc = 3.0 – 3.5 V ✓)
checkParameterCompatibility() output drawn as ranges on a shared axis — the intersection (or its absence) is the diagnostic. Far clearer than prose for voltage mismatches.
+
+
+
+

B4 — Sketch Integrations

+

More harness-lane vignettes (connectors, bridges, groups).

+ 1340px +
+
B4
From the notebook — four ideas integrated from design/concept sketches
1) Interfaces sit ON the module boundary, not inside the card. 2) Conductors carry explicit connector glyphs (pluggable points → connector_type + proprietary exclusivity). 3) One composed interface can be fulfilled by leaves from MULTIPLE modules — per-slot provenance, 'Partial!' state. 4) External interfaces bridge to internal ones inside the module (bridgesTo / can_bridge).
EDGE-MOUNTED INTERFACE — the boundary is the junction
l298n
motor driver
motor_control
target · channel_a
en
in1
in2
wire · 3 conductors
*motor_control
source · inferred
arduino-nano
dev board
Interface blocks dock on the card edge (half chrome, half harness). The module body stays quiet; composition regions (B1) open on selection. Matches how every sketch draws it: module → edge interface → harness.
CONNECTOR GLYPHS — pluggable points on the conductor
nano.i2c
jst-sh-4p · qwiic
vl53l0x.i2c
dxl-ctrl.port
molex-51065 · dynamixel
generic-servo.in
✕ ecosystem lock: dynamixel port mates only within its ecosystem — exclusivity from docs/NOTES.md
The sketches' rings become connector glyphs: ○—○ mated pair with a connector_type chip. Hooks directly into the NOTES.md concept — proprietary ecosystems render as a keyed connector that refuses foreign mates.
SPLIT FULFILLMENT — one interface, multiple source modules
sg90-servo.control — composed · sink
PARTIAL · 2 of 3 slots
slot sig
arm-controller.d9 · pwm
slot 5v
? unfulfilled — needs power · source
slot gnd
? unfulfilled — needs power · source
source 1: arm-controller
add source 2 · suggested: battery-5v, buck-mp1584
Your servo sketch: sig comes from the controller, 5v/gnd from a battery. Each slot carries a provenance chip naming its source module; a slot whose source is missing renders the sketch's 'Partial!' — CONFIGURATION_NEEDED naming exactly what's absent. Requires the pair view to admit third-party ghost modules, and validatePair() to accept context modules.
INTERNAL BRIDGES — what happens behind the external interface
vl53l0x-breakout — interior view
vin · 2.6–5.5 V
can_bridge · ldo regulator
3v3 rail → vdd core
i2c · slave
can_bridge · level shifter
sensor core · 2.8 V logic
DRC consequence: parametric checks should evaluate against the BRIDGED range (2.6–5.5 V), not the bare-die range — this resolves the false warning in the earlier 5v↔vin example.
Your A↔B sketch fans the external interface into internal interfaces. That's the can_bridge trait: power_in bridges to the 3v3 rail via the regulator; i2c bridges to the MCU core. Rendered as an expandable interior lane on the module card — explains WHY a 5 V supply is fine for a 3.3 V sensor board.
+
+
+
+

B5 — PCB Symbol Layout + Match Altitude

+

Two-column schematic-symbol card layout with match-altitude rules.

+ 1420px +
+
B5
B1 evolved — PCB-symbol columns, full interface census, match altitude
Every interface on both modules is shown, PCB-symbol style: two columns per module, interior sides reserved for connectable interfaces, exterior sides carry the rest, both populated evenly. Port state carries the census: ● connected · ◉ compatible but unconnected · ○ no counterpart on the other module. Below: the match-altitude problem — when a child sub-interface matches on its own, the connection attaches at that child's port and the parent reports partial engagement.
FULL CENSUS — ALL INTERFACES VISIBLE, INTERIOR = CONNECTABLE
arduino-nano
9 interfaces · 3 connected · 1 compatible
spi
spi · master · 4 leaves
uart
uart · peer
usb
usb · device
a0–a3
analog · in
i2c
i2c · master
5v
power · source · 500 mA
d2
digital · out
3v3
power · source · 150 mA
vin
power · sink
i2c ↔ i2c
protocol · VALID
5v ↔ vin
protocol · VALID
d2 ↔ xshut
protocol · VALID
3v3 ↔ vin
alternative · one_of with 5v
vl53l0x-breakout
6 interfaces · 3 connected · 1 compatible
i2c
i2c · slave · 0x29
vin
power · sink · 2.6–5.5 V
xshut
digital · in
gpio1
digital · out · interrupt
2v8_out
power · source · 20 mA
gnd
power · common
PORT STATE:
connected
compatible · unconnected (click to connect)
no counterpart on the other module
MATCH ALTITUDE — CHILD MATCHES ON ITS OWN, PARENT PARTIALLY ENGAGED
arm-controller
actuator_port_1
partial · 1 of 2 subs engaged
uart
uart · host · tx rx
power_12v
power · source · v+ gnd
available — needs power·sink (add module: B4 split)
uart.host ↔ uart.device
matched at sub-interface · protocol · VALID
actuator_port_1 itself: no counterpart at this altitude
radio-module
uart
uart · device — this IS its top level (leaf interface)
3v3_in
power · sink — incompatible with power_12v (range ∅)
MATCH-ALTITUDE RULES
1 · The engine pairs top-down and connects at the HIGHEST level both sides declare — one pill per match, attached to the port of the exact region that matched.
2 · When only a child matches, the line exits through the parent's region edge; the parent port becomes a donut (partially engaged) and reports n of m subs.
3 · Deeper regions under a connected parent don't re-pair — the bundle view (B1) already shows their conductor crossings. Their ports render solid, inherited.
4 · Remaining unengaged children keep live hollow ports — power_12v here can still pair with a third module via split fulfillment (B4).
5 · Altitude asymmetry is fine: A connects at sub-interface level while B connects at its top level. The pill names both roles; tier/params evaluate on the matched pair only.
+
+
+
+

B6 — Depth Rails, Both Sides Connected

+

Depth rails with engaged branches.

+ 1360px +
+
B6
B5 census × B2 depth rails — both sides connected
Mirrored outline trees, ports on the interior edges. Connected rows carry the region tint (B2's active highlight) and their ancestor rails turn green, so the engagement path is readable at a glance even in a deep tree. Everything else from the B5 vocabulary carries over: ◉ compatible/unconnected, ○ no counterpart, donut = partially engaged parent, dashed pill = potential connection.
arm-controller
2 connected · 1 partial
actuator_port_1
actuator_port · host
1/2 engaged
uart
uart · host
tx
D16
rx
D17
power_12v
power · source
needs sink
i2c
i2c · master
sda · scl
A4 · A5
actuator_port_2
actuator_port · host
no counterpart
spi
spi · master
no counterpart
usb
usb · device
no counterpart
uart.host ↔ uart.device
sub-interface ↔ top level · VALID
i2c.master ↔ i2c.slave
protocol · VALID
d3 ↔ gpio_int
potential · click ◉ to connect
2 connected · 1 compatible
telemetry-widget
uart
uart · device
top level
rx ← tx
pin 3
tx → rx
pin 4
i2c
i2c · slave
0x62
sda · scl
pins 6 · 7
gpio_int
digital · out
compatible: d3
3v3_in
power · sink
∅ range vs power_12v
antenna
rf · sma
no counterpart
PORT STATE:
connected
partially engaged parent
compatible · unconnected
no counterpart
row tint = engaged in current pair
Reading: green rails trace the engagement path through each tree — on the left, actuator_port_1's rail is green only along the uart branch (donut parent, power_12v still hollow); on the right, uart connects at its top level, so the asymmetric altitude of the same pill is visible as different depths at each end. Inherited children (tx/rx, sda·scl) show no port of their own — their crossings belong to the bundle detail, expandable per pill.
+
+
+
+

B7a — Regions + All Child Wires (flat)

+

Iteration toward the shared-region hero.

+ 1400px +
+
B7a
Expanded regions + a wire for every linked child — flat lane
Connected interfaces expand into B1 regions inline in the tree; every linked leaf gets its own port and its own crossing wire. The lane stacks them flat: parent pairing pill first, child conductor wires beneath.
arm-controller
actuator_port_1
partial 1/2
uart
uart · host
tx
D16
rx
D17
power_12v
available
i2c
i2c · master
sda
A4
scl
A5
spi
no counterpart
usb
no counterpart
uart ↔ uart
sub ↔ top · VALID
tx → rx
rx ← tx
i2c ↔ i2c
protocol · VALID
sda ↔ sda
scl ↔ scl
telemetry-widget
uart
uart · device · top level
rx
pin 3
tx
pin 4
i2c
i2c · slave · 0x62
sda
pin 6
scl
pin 7
gpio_int
compatible: d3
antenna
no counterpart
+
+
+
+

B7b — Regions + Lane Bundles

+

Iteration: bundled conductor lanes.

+ 1400px +
+
B7b
Expanded regions + lane bundles — the harness mirrors the nesting
Same panels; the lane wraps each connected interface's wires in a bundle box whose header IS the pairing (name · tier · state). Region → bundle → region reads as one shape; the box edge doubles as the cable jacket.
arm-controller
actuator_port_1
partial 1/2
uart
uart · host
tx
D16
rx
D17
power_12v
available
i2c
i2c · master
sda
A4
scl
A5
spi
no counterpart
usb
no counterpart
uart ↔ uart
sub ↔ top · VALID
tx → rx
rx ← tx
i2c ↔ i2c
protocol · VALID
sda ↔ sda
scl ↔ scl
telemetry-widget
uart
uart · device · top level
rx
pin 3
tx
pin 4
i2c
i2c · slave · 0x62
sda
pin 6
scl
pin 7
gpio_int
compatible: d3
antenna
no counterpart
+
+
+
+

B7c — Continuous Band

+

Iteration: continuous connection band.

+ 1400px +
+
B7c
Continuous band — the connection is one shared territory
The two matched regions and the lane between them share one tinted band: region → band → region is a single continuous surface. The parent match is the thick spine at the band's top edge; child wires are thin threads inside. Strongest statement that a match at level N merges two territories into one.
arm-controller
actuator_port_1
partial 1/2
uart
uart · host
tx
D16
rx
D17
power_12v
available
i2c
i2c · master
sda
A4
scl
A5
spi
no counterpart
usb
no counterpart
uart ↔ uart
sub ↔ top · VALID
tx → rx
rx ← tx
i2c ↔ i2c
protocol · VALID
sda ↔ sda
scl ↔ scl
telemetry-widget
uart
uart · device · top level
rx
pin 3
tx
pin 4
i2c
i2c · slave · 0x62
sda
pin 6
scl
pin 7
gpio_int
compatible: d3
antenna
no counterpart
+
+
+
+

B8 — Shared Region, Dot-to-Dot

+

The canonical hero rendering — connection as co-owned shared territory.

+ 1400px +
+
B8
Shared region — a match creates ONE region owned by both modules
The matched interface is a single bordered box spanning both cards and the lane. Leaves hold fixed row heights; every child link is a straight dot→dot wire at its row height (row order mirrored so pairs align — no diagonals). Match altitude = how deep the shared box starts inside each card: uart begins one level deep on the left (inside actuator_port_1) but at the top level on the right.
arm-controller
telemetry-widget
actuator_port_1
partial 1/2
power_12v
available · needs power sink
uart · host
uart · device · top level
uart ↔ uart
sub ↔ top · VALID
tx
D16
tx → rx
rx
pin 3
rx
D17
rx ← tx
tx
pin 4
i2c · master
i2c · slave · 0x62
i2c ↔ i2c
protocol · VALID
sda
A4
sda ↔ sda
sda
pin 6
scl
A5
scl ↔ scl
scl
pin 7
spi
no counterpart
usb
no counterpart
gpio_int
compatible: d3
antenna
no counterpart
Reading: the shared uart box starts INSIDE actuator_port_1's territory on the left (one level deep) and at the card surface on the right (top level) — match altitude is literally visible as where the box begins. Wires never bend: row order is mirrored per pair (A.tx row = B.rx row), so every conductor is a straight dot→dot line. power_12v stays behind in A's parent region with a hollow port; census rows sit below, outside any shared territory.
+
+
\ No newline at end of file diff --git a/design/visualizer-preview.html b/design/visualizer-preview.html new file mode 100644 index 0000000..467c714 --- /dev/null +++ b/design/visualizer-preview.html @@ -0,0 +1,294 @@ + + + + + +UHD Visualizer — Screen Mockups + + + +
+
+
OpenUHD · design/visualizer.pen
+

UHD Pair Visualizer — screen mockups

+

Faithful render of the locked visual designs for protoboard.ai/validate and /parts. Four production screens, then the concept and iteration boards (B-series) that the visualizer spec references.

+
+ +
Production screens
+
+
+

Validate — Results

+

The hero screen at protoboard.ai/validate — two module cards with the B8 shared-region pair scene, verdict banner, and per-connection DRC cards.

+ 1440px +
+
protoboard
/ validate
Parts
Validate
Docs
GitHub
Interface Validator
Check how two UHD modules connect — compatibility, capacity, and parametric constraints.
?a=arduino-nano&b=vl53l0x
MODULE A
arduino-nano
Change
MODULE B
vl53l0x
Change
i2c · master
i2c · slave · 0x29
i2c ↔ i2c
bus · protocol · VALID
sda
A4
sda ↔ sda
sda
SDA
scl
A5
scl ↔ scl
scl
SCL
power · source
power · sink · bridged 2.6–5.5 V
5v ↔ vin
wire · protocol · WARNING
5v
500 mA
5v → vin · regulator onboard
vin
VIN
digital · out
digital · in
d2 ↔ xshut
wire · protocol · VALID
d2
D2
d2 → xshut · shutdown ctl
xshut
XSHUT
UNCONNECTED
3v3
compatible · alternative supply (one_of with 5v)
spi
no counterpart
uart
no counterpart
usb · a0–a3
no counterpart
UNCONNECTED
gpio1
compatible: d3 · interrupt
2v8_out
no counterpart
Select Module A
Select Module B
3
DRC Results
Compatible with warnings
3 connections found · 2 passed · 1 warning · 0 errors
Export report
arduino-nano.i2c ↔ vl53l0x.i2c
bus · 2 conductors
PASS
PROTOCOL
i2c master ↔ slave — roles compatible
SLOT MAP
sda ↔ sda (A4) · scl ↔ scl (A5) — profile: default
PARAMETRIC
logic level 5.0 V within slave tolerant range 2.6 – 5.5 V
CAPACITY
bus address 0x29 — 1 of 112 addresses in use
arduino-nano.5v ↔ vl53l0x.vin
wire
1 WARNING
PROTOCOL
power source ↔ sink — roles compatible
PARAMETRIC
supply 4.5 – 5.5 V exceeds bare-die VCC range 2.6 – 3.5 V — onboard regulator required (present on this breakout)
CAPACITY
5V rail budget: 19 mA of 500 mA
arduino-nano.d2 ↔ vl53l0x.xshut
wire
PASS
PROTOCOL
digital out ↔ in — shutdown control, optional connection
UNMATCHED INTERFACES — NOT ERRORS, JUST NO COUNTERPART
arduino-nano
spi
uart
3v3
d3–d13
a0–a3
vin
vl53l0x
gpio1 (interrupt)
Other matches for arduino-nano
Browse all parts →
3 ifaces match
bme280
Bosch · env sensor
2 ifaces match
ssd1306
Solomon · OLED 128×64
4 ifaces match
l298n
ST · motor driver
2 ifaces match
sg90-servo
TowerPro · micro servo
+
+
+
+

Validate — Select Module B

+

Step ② of the validator wizard: search/filter the library, pre-ranked by compatibility with the module already in slot A.

+ 1440px +
+
protoboard
/ validate
Parts
Validate
Docs
GitHub
Interface Validator
Step 2 — choose the module to validate against arduino-nano.
?a=arduino-nano
MODULE A
Change
arduino-nano
Arduino · A000005 · rev3
i2c
spi
uart
5v
Connections will appear here once Module B is selected
Select Module B
search below or pick a suggested match
Select Module A
2
Select Module B
3
DRC Results
Search 168 parts — name, protocol, manufacturer…
Filters
FILTER:
compatible with A
i2c
sensor
electrical
mechanical
Suggested matches — ranked by shared interfaces with arduino-nano
3 ifaces match
vl53l0x
STMicro · ToF distance sensor
i2c
2.8v
gpio
3 ifaces match
bme280
Bosch · temp / humidity / pressure
i2c
spi
3v3
2 ifaces match
ssd1306
Solomon · OLED display 128×64
i2c
3v3
4 ifaces match
l298n
ST · dual H-bridge motor driver
motor
pwm
12v
All parts
sg90-servo
TowerPro · micro servo 9 g
pwm
5v
hc-sr04
Elecfreaks · ultrasonic ranger
digital
5v
raspberry-pi-pico
Raspberry Pi · RP2040 dev board
i2c
spi
uart
usb
buck-mp1584
MPS · 3 A step-down module
power
12v→5v
+
+
+
+

Parts — Library

+

protoboard.ai/parts — the step.parts-style dense card grid with search, filter, and pagination.

+ 1440px +
+
protoboard
/ parts
Parts
Validate
Docs
GitHub
Parts Library
168 open UHD module definitions — interfaces, parameters, and artifacts. Apache-2.0.
Contribute a part
Search parts — name, protocol, manufacturer, taxonomy…
Show filters
1–12 of 168
Sort: recently added
arduino-nano
Arduino · ATmega328P dev board
i2c
spi
uart
5v
uhd.json
vl53l0x
STMicro · ToF distance sensor
i2c
2.8v
gpio
uhd.json
bme280
Bosch · temp / humidity / pressure
i2c
spi
3v3
uhd.json
ssd1306
Solomon · OLED display 128×64
i2c
3v3
uhd.json
l298n
ST · dual H-bridge motor driver
motor
pwm
12v
uhd.json
raspberry-pi-pico
Raspberry Pi · RP2040 dev board
i2c
spi
uart
usb
uhd.json
sg90-servo
TowerPro · micro servo 9 g
pwm
5v
uhd.json
hc-sr04
Elecfreaks · ultrasonic ranger
digital
5v
uhd.json
buck-mp1584
MPS · 3 A step-down module
power
12v→5v
uhd.json
nema17-stepper
Generic · bipolar stepper 1.8°
motor
12v
mech
uhd.json
esp32-devkitc
Espressif · Wi-Fi + BLE dev board
i2c
spi
uart
wifi
uhd.json
battery-12v-3s
Generic · LiPo pack 3S 2200 mAh
power
12v
uhd.json
1
2
3
14
+
+
+
+

Parts — Detail

+

A single part page: metadata, the UHD interface table, artifacts, and the raw uhd.json toggle.

+ 1440px +
+
protoboard
/ parts / arduino-nano
Parts
Validate
Docs
GitHub
Parts Library
arduino-nano
Arduino Nano rev3 — ATmega328P development board. 14 digital I/O, 8 analog inputs, composed I2C / SPI / UART interfaces with pin-profile bindings.
MANUFACTURER
Arduino
PART NUMBER
A000005
VERSION
1.2.0
DOMAINS
electrical
TAXONOMY
mcu.dev-board
LICENSE
Apache-2.0
i2c
spi
uart
pwm
5v
atmega328p
Validate against another part
uhd.json
Datasheet
UHD definition — interfaces
View raw JSON
INTERFACE
DOMAIN
PROTOCOL · ROLE
SLOTS / PROFILE
PARAMETERS
i2c
electrical
i2c · master
sda → A4 · scl → A5 (profile: default)
f_max 400 kHz
spi
electrical
spi · master
mosi → D11 · miso → D12 · sck → D13 · cs → D10
f_max 8 MHz
uart
electrical
uart · peer
tx → D1 · rx → D0
115 200 baud max
5v
electrical
power · source
4.5 – 5.5 V · 500 mA max
3v3
electrical
power · source
3.3 V · 150 mA max
d2 – d13
electrical
digital · in / out
12 pins · pwm on 6
40 mA per pin
a0 – a7
electrical
analog · in
8 pins · 10-bit ADC
0 – 5 V
interface group · power: one_of( usb, vin, 5v-input ) — only one supply may be active
Artifacts
atmega328p-datasheet.pdf
datasheet
external · microchip.com
arduino-nano.step
3d_model
hosted
2.1 MB
nano-pinout.png
documentation
hosted
340 KB
blink-example.hex
firmware
external · github.com
+
+ +
Concept & iteration boards
+
+
+

Concept 0 — Pairing Vocabulary

+

The tier × state × port visual vocabulary the renderings are built from.

+ 720px +
+
Pairing vocabulary — tier × state
Two orthogonal encodings. HOW the pair was established (tier) = line pattern + badge. WHAT the DRC concluded (state) = color. Shared by all three concepts.
TIER — LINE PATTERN
protocol
1.0
Both sides declare the protocol. Solid, full-weight line.
compositional
0.8
One side composed from slots; other side fills them. Solid line with slot junction dot.
inferred
0.5
Counterpart never declares the interface — structure implies it. Dashed line, ghost chip, * prefix.
manual
0.3
User-asserted wiring, no protocol backing. Dotted line + lock.
none
0.0
No valid interpretation. Broken line with ✕.
STATE — COLOR
VALID
VALID_INFERRED
WARNING
CONFIG_NEEDED
INCOMPATIBLE
+
+
+
+

Concept A — Harness Table (WireViz)

+

Export-only build-sheet rendering: connector tables with wire colors.

+ 1240px +
+
A
Harness Table — WireViz-style connector tables
Each side is a connector table (physical pin · leaf interface). Every conductor is a row with a wire-color swatch, like a real harness drawing. Wire color = physical conductor; DRC state lives in badges and the footer pairing row. Strength: closest to build documentation — doubles as wiring instructions. Weakness: interface/slot hierarchy is flattened into grouping headers.
CASE 1 — EXPLICIT PROTOCOL MATCH (i2c)
arduino-nano.i2c
i2c · master · profile: default
bus · 2 conductors
protocol · 1.0
VALID
vl53l0x.i2c
i2c · slave · addr 0x29
A4
sda
YE · sda ↔ sda
sda
SDA
A5
scl
GN · scl ↔ scl
scl
SCL
interface pairing: i2c.master ↔ i2c.slave — all 2 required slots resolved · params pass (f_max 400 kHz ∩ 400 kHz)
CASE 2 — COMPOSITIONAL MATCH, LEFT SIDE INFERRED (motor_control)
*motor_control
inferred
not declared — derived from 3 free digital pins
wire · 3 conductors
inferred · 0.5
VALID_INFERRED
l298n.motor_control
motor_control · target · profile: channel_a
D5
d5
capability: pwm
d5 → slot en
slot en
en_a
D6
d6
capability: digital
d6 → slot in1
slot in1
in1
D7
d7
capability: digital
d7 → slot in2
slot in2
in2
*motor_control.source ↔ target — interface synthesized on arduino-nano · 3/3 required slots resolved · confidence 0.5 · [ accept as manual lock ]
+
+
+
+

Concept B — Breakout Funnel

+

Early funnel-view exploration of a connection.

+ 1240px +
+
B
Breakout Funnel — nested containment (ASCII view, evolved)
Direct translation of renderPairAscii. Composition is shown by physical nesting: interface ⊃ slots ⊃ bound leaves. The center lane carries one pill per pairing level — the interface pairing on top (Tier 2), conductor crossings below (Tier 1/0). An inferred interface is a ghost container drawn around bare pins that never declared it. Strength: teaches the UHD model itself — you can see composition. Weakness: taller than A; needs care at high pin counts.
CASE 1 — EXPLICIT PROTOCOL MATCH (i2c)
arduino-nano
i2c — master
composed · profile: default
slot sda
a4
slot scl
a5
i2c.master ↔ i2c.slave
protocol · 1.0
VALID · bus
sda ↔ sda
scl ↔ scl
vl53l0x
i2c — slave
leaf · addr 0x29
sda
scl
CASE 2 — COMPOSITIONAL MATCH, LEFT SIDE INFERRED (motor_control)
arduino-nano
*motor_control — source
inferred — not declared
d5
pwm · free
d6
digital · free
d7
digital · free
These pins never declare motor_control. The container exists only because l298n's slot requirements are structurally satisfiable here.
*motor_control.source ↔ target
inferred · 0.5
VALID_INFERRED · wire
d5 → en
d6 → in1
d7 → in2
l298n
motor_control — target
composed · profile: channel_a
slot en
en_a
slot in1
in1
slot in2
in2
+
+
+
+

Concept C — Tier Stack Inspector

+

The per-connection DRC card stages: pairing / slot table / conductors + parameters.

+ 1240px +
+
C
Tier Stack — connection list + expandable DRC pipeline
Connections are compact rows in a list; expanding one reveals three stacked bands that mirror the DRC evaluation stages exactly — interface pairing (tier determination), slot resolution (sub-link derivation), conductors & parameters. Confidence is a segmented meter. Strength: scales to many connections, 1:1 with the engine so diagnostics slot in naturally. Weakness: least spatial — you don't 'see' the two modules.
arduino-nano.i2c ↔ vl53l0x.i2c
protocol · 1.0
VALID
arduino-nano.5v ↔ vl53l0x.vin
protocol · 1.0
WARNING
arduino-nano.*motor_control ↔ l298n.motor_control
inferred · 0.5
VALID_INFERRED
TIER 2 — INTERFACE PAIRING
stage 2: tier determination
*motor_control · source — inferred, not declared
motor_control · target — declared, profile channel_a
confidence
No protocol match at tier 1 (arduino-nano declares no motor_control). findCompositionalMatch() succeeded: l298n.motor_control's 3 required slots are all fillable from free pins → interface synthesized via deriveInferredInterface().
TIER 1 — SLOT RESOLUTION
stage 3: sub-link derivation
3 / 3 required slots resolved
SLOT (l298n)
REQUIREMENT
METHOD
BOUND LEAF (nano)
NOTE
en
capability: pwm · required
inferred
d5 (D5)
auto-derived — 5 other pwm pins eligible
in1
capability: digital · required
inferred
d6 (D6)
auto-derived
in2
capability: digital · required
inferred
d7 (D7)
auto-derived · [ lock assignment ]
TIER 0 — CONDUCTORS & PARAMETERS
stage 4: parameter checking per sub-link
d5 ↔ en
logic 5.0 V within en tolerant range 2.3 – 7.0 V · pwm 490 Hz within 0 – 25 kHz
d6 ↔ in1
logic level compatible
d7 ↔ in2
in2 current sink unspecified on l298n — cannot validate, no error
Inferred pairings are suggestions — accepting converts each sub-link to a locked manual link (tier manual → confidence set by you, not the engine).
Accept & lock wiring
Dismiss suggestion
+
+
+
+

Recommendation

+

Design recommendation note.

+ 720px +
+
Recommendation — compose all three, one per altitude
These aren't competing skins — they're different altitudes of the same data. Proposed composition for /validate: + +• C (Tier Stack rows) = the DRC results list. Compact, scales, maps 1:1 to the engine's pipeline stages and diagnostic codes. +• B (Breakout Funnel) = the spatial view inside an expanded row (or the slot-rail edge detail). It's the view that teaches interface ⊃ slot ⊃ leaf composition and makes inference visible as a ghost container. +• A (Harness Table) = the export/'build it' view — printable WireViz-style wiring sheet with wire colors, one per harness. + +Shared vocabulary board (left) is the contract all three obey: tier → line pattern + badge, state → color, inferred → purple + dashed + * + sparkle.
+
+
+
+

B1 — Region Nesting

+

Depth encoded by fill, not indentation.

+ 1300px +
+
B1
Region nesting — interface as territory, 3 levels deep
Each composed interface is a soft-filled region; sub-interfaces are lighter regions inside it; leaves are chips. Depth = fill, not indentation. The harness lane mirrors the same tree: conductors are grouped into labeled bundles per sub-interface, so the wiring reads as the same shape as the composition. Example: a smart-actuator port (Dynamixel-style) — actuator_port ⊃ { uart ⊃ tx/rx, power_12v ⊃ v+/gnd }.
3-LEVEL COMPOSED INTERFACE · PROTOCOL TIER · VALID
arm-controller
actuator_port_1
actuator_port · host
uart
uart · host
tx
→ D16
rx
← D17
power_12v
power · source
v+
12 V rail
gnd
common
actuator_port.host ↔ device
protocol · 1.0
VALID · wire · 4 conductors in 2 bundles
bundle · uart
tx → rx
rx ← tx
bundle · power_12v
v+ ↔ v+
gnd ↔ gnd
dynamixel-servo
actuator_port · device
port
uart · device
uart
rx
half-duplex
tx
half-duplex
power · sink
power_12v
v+
9 – 14.8 V
gnd
common
DEPTH KEY:
L1 · composed interface
L2 · sub-interface
L3 · leaf
+
+
+
+

B2 — Depth Rails (compact)

+

Collapsed outline-tree rendering.

+ 980px +
+
B2
Depth rails — same tree, collapsed density
The fallback treatment when region fills get heavy: a 40-pin MCU with six composed interfaces would drown in nested boxes. Same hierarchy, rendered as an outline with depth rails and collapse chevrons. Regions (B1) and rails (B2) can coexist — rails while browsing, region fill only for the interface participating in the active connection.
arm-controller
actuator_port_1
actuator_port · host
in use → dynamixel-servo
uart
uart · host
tx
D16
rx
D17
power_12v
power · source
v+
12 V rail
gnd
common
actuator_port_2
actuator_port · host
4 leaves · available
i2c
i2c · master
2 leaves · available
power_in
power · sink
one_of group
Active-connection highlight
Rows participating in the selected harness get the L1 region tint; everything else stays flat. Region fill becomes a signal, not decoration.
Port dot = connection state
Green dot: leaf is wired in the current pair. No dot: available. The dot column doubles as the attach point for edge lines when this panel sits in the slot rail.
Collapsed rows keep their contract
A collapsed interface still shows protocol · role and availability, so scanning a 40-pin module never requires expansion.
+
+
+
+

B3 — Missing Visualizations

+

Harness-lane feature vignettes (buses, capacity bars…).

+ 1340px +
+
B3
What's still missing — six visualizations to add to B
Gaps found by walking the UHD type system against concept B: three on the harness side (bus topology, capacity, unresolved slots) and three on the module side (claims, interface groups, parameter ranges). Each maps to an existing type or a validatePair() check.
HARNESS · BUS MULTI-DROP
nano.i2c · master
vl53l0x · 0x29
bme280 · 0x76
add drop · 109 addresses free
Topology 'bus' and 'split' need more than two endpoints. The spine stays horizontal; extra drops hang below with their own state dot and address chip. Maps to HarnessDef.endpoints[2..n].
HARNESS · CAPACITY BUDGET
5v rail — 3 sinks
340 / 500 mA
5v rail + servo — 4 sinks
620 / 500 mA · OVER BUDGET
i2c bus — addresses
3 / 112
Aggregated draws vs supply limit, rendered on the harness spine itself. This is the net-new capacity check in validatePair() — sums current_draw across every connection claiming the rail.
HARNESS · UNRESOLVED SLOT
spi.cs — required
?
CONFIG_NEEDED · 1 of 4 slots unresolved
assign leaf…
A required slot with no counterpart leaf renders as an empty socket and an interrupted conductor — the visual for CONFIGURATION_NEEDED. Click assigns manually → becomes a locked manualLink.
MODULE · CLAIMED LEAVES
a4
in use · Sensor I2C bus (sda)
a5
in use · Sensor I2C bus (scl)
d5
available · pwm
A leaf already consumed by another connection is grayed with a lock and names its claimant. Backed by findClaimConflicts() — prevents double-booking A4 for both i2c and analog read.
MODULE · ONE_OF GROUP
power_in
group · one_of
usb
5 V · active — powering this pair
vin
7 – 12 V · disabled while usb active
5v-input
disabled while usb active
InterfaceGroup policies render as a bracketed radio group inside the module card. Selecting one supply disables the siblings — validateInterfaceGroups() already enforces this; the UI just shows it.
MODULE · PARAMETER RANGE OVERLAP
nano.5v · 4.5 – 5.5 V
vl53l0x.vcc · 2.6 – 3.5 V
∅ no overlap — ERROR
suggestion: route via 3v3 rail (3.0 – 3.6 V ∩ vcc = 3.0 – 3.5 V ✓)
checkParameterCompatibility() output drawn as ranges on a shared axis — the intersection (or its absence) is the diagnostic. Far clearer than prose for voltage mismatches.
+
+
+
+

B4 — Sketch Integrations

+

More harness-lane vignettes (connectors, bridges, groups).

+ 1340px +
+
B4
From the notebook — four ideas integrated from design/concept sketches
1) Interfaces sit ON the module boundary, not inside the card. 2) Conductors carry explicit connector glyphs (pluggable points → connector_type + proprietary exclusivity). 3) One composed interface can be fulfilled by leaves from MULTIPLE modules — per-slot provenance, 'Partial!' state. 4) External interfaces bridge to internal ones inside the module (bridgesTo / can_bridge).
EDGE-MOUNTED INTERFACE — the boundary is the junction
l298n
motor driver
motor_control
target · channel_a
en
in1
in2
wire · 3 conductors
*motor_control
source · inferred
arduino-nano
dev board
Interface blocks dock on the card edge (half chrome, half harness). The module body stays quiet; composition regions (B1) open on selection. Matches how every sketch draws it: module → edge interface → harness.
CONNECTOR GLYPHS — pluggable points on the conductor
nano.i2c
jst-sh-4p · qwiic
vl53l0x.i2c
dxl-ctrl.port
molex-51065 · dynamixel
generic-servo.in
✕ ecosystem lock: dynamixel port mates only within its ecosystem — exclusivity from docs/NOTES.md
The sketches' rings become connector glyphs: ○—○ mated pair with a connector_type chip. Hooks directly into the NOTES.md concept — proprietary ecosystems render as a keyed connector that refuses foreign mates.
SPLIT FULFILLMENT — one interface, multiple source modules
sg90-servo.control — composed · sink
PARTIAL · 2 of 3 slots
slot sig
arm-controller.d9 · pwm
slot 5v
? unfulfilled — needs power · source
slot gnd
? unfulfilled — needs power · source
source 1: arm-controller
add source 2 · suggested: battery-5v, buck-mp1584
Your servo sketch: sig comes from the controller, 5v/gnd from a battery. Each slot carries a provenance chip naming its source module; a slot whose source is missing renders the sketch's 'Partial!' — CONFIGURATION_NEEDED naming exactly what's absent. Requires the pair view to admit third-party ghost modules, and validatePair() to accept context modules.
INTERNAL BRIDGES — what happens behind the external interface
vl53l0x-breakout — interior view
vin · 2.6–5.5 V
can_bridge · ldo regulator
3v3 rail → vdd core
i2c · slave
can_bridge · level shifter
sensor core · 2.8 V logic
DRC consequence: parametric checks should evaluate against the BRIDGED range (2.6–5.5 V), not the bare-die range — this resolves the false warning in the earlier 5v↔vin example.
Your A↔B sketch fans the external interface into internal interfaces. That's the can_bridge trait: power_in bridges to the 3v3 rail via the regulator; i2c bridges to the MCU core. Rendered as an expandable interior lane on the module card — explains WHY a 5 V supply is fine for a 3.3 V sensor board.
+
+
+
+

B5 — PCB Symbol Layout + Match Altitude

+

Two-column schematic-symbol card layout with match-altitude rules.

+ 1420px +
+
B5
B1 evolved — PCB-symbol columns, full interface census, match altitude
Every interface on both modules is shown, PCB-symbol style: two columns per module, interior sides reserved for connectable interfaces, exterior sides carry the rest, both populated evenly. Port state carries the census: ● connected · ◉ compatible but unconnected · ○ no counterpart on the other module. Below: the match-altitude problem — when a child sub-interface matches on its own, the connection attaches at that child's port and the parent reports partial engagement.
FULL CENSUS — ALL INTERFACES VISIBLE, INTERIOR = CONNECTABLE
arduino-nano
9 interfaces · 3 connected · 1 compatible
spi
spi · master · 4 leaves
uart
uart · peer
usb
usb · device
a0–a3
analog · in
i2c
i2c · master
5v
power · source · 500 mA
d2
digital · out
3v3
power · source · 150 mA
vin
power · sink
i2c ↔ i2c
protocol · VALID
5v ↔ vin
protocol · VALID
d2 ↔ xshut
protocol · VALID
3v3 ↔ vin
alternative · one_of with 5v
vl53l0x-breakout
6 interfaces · 3 connected · 1 compatible
i2c
i2c · slave · 0x29
vin
power · sink · 2.6–5.5 V
xshut
digital · in
gpio1
digital · out · interrupt
2v8_out
power · source · 20 mA
gnd
power · common
PORT STATE:
connected
compatible · unconnected (click to connect)
no counterpart on the other module
MATCH ALTITUDE — CHILD MATCHES ON ITS OWN, PARENT PARTIALLY ENGAGED
arm-controller
actuator_port_1
partial · 1 of 2 subs engaged
uart
uart · host · tx rx
power_12v
power · source · v+ gnd
available — needs power·sink (add module: B4 split)
uart.host ↔ uart.device
matched at sub-interface · protocol · VALID
actuator_port_1 itself: no counterpart at this altitude
radio-module
uart
uart · device — this IS its top level (leaf interface)
3v3_in
power · sink — incompatible with power_12v (range ∅)
MATCH-ALTITUDE RULES
1 · The engine pairs top-down and connects at the HIGHEST level both sides declare — one pill per match, attached to the port of the exact region that matched.
2 · When only a child matches, the line exits through the parent's region edge; the parent port becomes a donut (partially engaged) and reports n of m subs.
3 · Deeper regions under a connected parent don't re-pair — the bundle view (B1) already shows their conductor crossings. Their ports render solid, inherited.
4 · Remaining unengaged children keep live hollow ports — power_12v here can still pair with a third module via split fulfillment (B4).
5 · Altitude asymmetry is fine: A connects at sub-interface level while B connects at its top level. The pill names both roles; tier/params evaluate on the matched pair only.
+
+
+
+

B6 — Depth Rails, Both Sides Connected

+

Depth rails with engaged branches.

+ 1360px +
+
B6
B5 census × B2 depth rails — both sides connected
Mirrored outline trees, ports on the interior edges. Connected rows carry the region tint (B2's active highlight) and their ancestor rails turn green, so the engagement path is readable at a glance even in a deep tree. Everything else from the B5 vocabulary carries over: ◉ compatible/unconnected, ○ no counterpart, donut = partially engaged parent, dashed pill = potential connection.
arm-controller
2 connected · 1 partial
actuator_port_1
actuator_port · host
1/2 engaged
uart
uart · host
tx
D16
rx
D17
power_12v
power · source
needs sink
i2c
i2c · master
sda · scl
A4 · A5
actuator_port_2
actuator_port · host
no counterpart
spi
spi · master
no counterpart
usb
usb · device
no counterpart
uart.host ↔ uart.device
sub-interface ↔ top level · VALID
i2c.master ↔ i2c.slave
protocol · VALID
d3 ↔ gpio_int
potential · click ◉ to connect
2 connected · 1 compatible
telemetry-widget
uart
uart · device
top level
rx ← tx
pin 3
tx → rx
pin 4
i2c
i2c · slave
0x62
sda · scl
pins 6 · 7
gpio_int
digital · out
compatible: d3
3v3_in
power · sink
∅ range vs power_12v
antenna
rf · sma
no counterpart
PORT STATE:
connected
partially engaged parent
compatible · unconnected
no counterpart
row tint = engaged in current pair
Reading: green rails trace the engagement path through each tree — on the left, actuator_port_1's rail is green only along the uart branch (donut parent, power_12v still hollow); on the right, uart connects at its top level, so the asymmetric altitude of the same pill is visible as different depths at each end. Inherited children (tx/rx, sda·scl) show no port of their own — their crossings belong to the bundle detail, expandable per pill.
+
+
+
+

B7a — Regions + All Child Wires (flat)

+

Iteration toward the shared-region hero.

+ 1400px +
+
B7a
Expanded regions + a wire for every linked child — flat lane
Connected interfaces expand into B1 regions inline in the tree; every linked leaf gets its own port and its own crossing wire. The lane stacks them flat: parent pairing pill first, child conductor wires beneath.
arm-controller
actuator_port_1
partial 1/2
uart
uart · host
tx
D16
rx
D17
power_12v
available
i2c
i2c · master
sda
A4
scl
A5
spi
no counterpart
usb
no counterpart
uart ↔ uart
sub ↔ top · VALID
tx → rx
rx ← tx
i2c ↔ i2c
protocol · VALID
sda ↔ sda
scl ↔ scl
telemetry-widget
uart
uart · device · top level
rx
pin 3
tx
pin 4
i2c
i2c · slave · 0x62
sda
pin 6
scl
pin 7
gpio_int
compatible: d3
antenna
no counterpart
+
+
+
+

B7b — Regions + Lane Bundles

+

Iteration: bundled conductor lanes.

+ 1400px +
+
B7b
Expanded regions + lane bundles — the harness mirrors the nesting
Same panels; the lane wraps each connected interface's wires in a bundle box whose header IS the pairing (name · tier · state). Region → bundle → region reads as one shape; the box edge doubles as the cable jacket.
arm-controller
actuator_port_1
partial 1/2
uart
uart · host
tx
D16
rx
D17
power_12v
available
i2c
i2c · master
sda
A4
scl
A5
spi
no counterpart
usb
no counterpart
uart ↔ uart
sub ↔ top · VALID
tx → rx
rx ← tx
i2c ↔ i2c
protocol · VALID
sda ↔ sda
scl ↔ scl
telemetry-widget
uart
uart · device · top level
rx
pin 3
tx
pin 4
i2c
i2c · slave · 0x62
sda
pin 6
scl
pin 7
gpio_int
compatible: d3
antenna
no counterpart
+
+
+
+

B7c — Continuous Band

+

Iteration: continuous connection band.

+ 1400px +
+
B7c
Continuous band — the connection is one shared territory
The two matched regions and the lane between them share one tinted band: region → band → region is a single continuous surface. The parent match is the thick spine at the band's top edge; child wires are thin threads inside. Strongest statement that a match at level N merges two territories into one.
arm-controller
actuator_port_1
partial 1/2
uart
uart · host
tx
D16
rx
D17
power_12v
available
i2c
i2c · master
sda
A4
scl
A5
spi
no counterpart
usb
no counterpart
uart ↔ uart
sub ↔ top · VALID
tx → rx
rx ← tx
i2c ↔ i2c
protocol · VALID
sda ↔ sda
scl ↔ scl
telemetry-widget
uart
uart · device · top level
rx
pin 3
tx
pin 4
i2c
i2c · slave · 0x62
sda
pin 6
scl
pin 7
gpio_int
compatible: d3
antenna
no counterpart
+
+
+
+

B8 — Shared Region, Dot-to-Dot

+

The canonical hero rendering — connection as co-owned shared territory.

+ 1400px +
+
B8
Shared region — a match creates ONE region owned by both modules
The matched interface is a single bordered box spanning both cards and the lane. Leaves hold fixed row heights; every child link is a straight dot→dot wire at its row height (row order mirrored so pairs align — no diagonals). Match altitude = how deep the shared box starts inside each card: uart begins one level deep on the left (inside actuator_port_1) but at the top level on the right.
arm-controller
telemetry-widget
actuator_port_1
partial 1/2
power_12v
available · needs power sink
uart · host
uart · device · top level
uart ↔ uart
sub ↔ top · VALID
tx
D16
tx → rx
rx
pin 3
rx
D17
rx ← tx
tx
pin 4
i2c · master
i2c · slave · 0x62
i2c ↔ i2c
protocol · VALID
sda
A4
sda ↔ sda
sda
pin 6
scl
A5
scl ↔ scl
scl
pin 7
spi
no counterpart
usb
no counterpart
gpio_int
compatible: d3
antenna
no counterpart
Reading: the shared uart box starts INSIDE actuator_port_1's territory on the left (one level deep) and at the card surface on the right (top level) — match altitude is literally visible as where the box begins. Wires never bend: row order is mirrored per pair (A.tx row = B.rx row), so every conductor is a straight dot→dot line. power_12v stays behind in A's parent region with a hollow port; census rows sit below, outside any shared territory.
+
+
+ + \ No newline at end of file diff --git a/library/parts/adafruit-938-128x64-oled.ts b/library/parts/adafruit-938-128x64-oled.ts new file mode 100644 index 0000000..387ee29 --- /dev/null +++ b/library/parts/adafruit-938-128x64-oled.ts @@ -0,0 +1,604 @@ +/** + * Adafruit 938 — Monochrome 1.3" 128x64 OLED Graphic Display breakout — + * datasheet-honest part definition. + * + * Primary sources: + * - Adafruit 938 product datasheet (DigiKey mirror, 938_Web.pdf) — pads, + * VIN range, onboard regulator/level shifting, jumper-selected bus mode + * - Adafruit product page (adafruit.com/product/938) — default I2C mode, + * address jumper, SPI conversion via solder-jumper cuts + * - Solomon Systech SSD1306 controller datasheet — 10 MHz SPI clock limit, + * reset pulse width, operating temperature (cited where inherited) + * All electrical values below are carried over from the validated ProtoPart + * definition (adafruit-938-128x64-oled, schema 1.4.0); nothing is invented + * beyond that source. + * + * Architecture notes honoured by this file: + * - Pin-honest like a schematic: all 8 header pads are leaf interfaces in + * board order, ids matching the ProtoPart resource ids, with the silk + * label as the `pin` designator and the official pad name displayed. + * (The ProtoPart source gives no numeric header positions, so the silk + * labels are the designators.) + * - Every multiplexed pad carries a verbatim per-mode function list in a + * `pin_functions` trait — display data separated from the canonical + * capability tags slot matching needs. + * - I2C-default vs SPI-selectable duality: the two bus targets are + * composed interfaces bound to the same CLK/DATA pads; a `one_of` + * interface group plus `co_requirement` traits state the honest + * constraint — mode is chosen by cutting two solder jumpers, a + * one-time physical board-prep step, never runtime-switchable, and + * never simultaneous. + * - Write-only SPI modelled honestly: the SPI target is hand-rolled (the + * SPI builder would fabricate a required MISO slot) with only + * MOSI/SCK/CS/DC slots — there is no MISO/CIPO contact on the breakout. + * - Onboard passives (`onboard_passives` traits): the breakout ships its + * own I2C pull-ups; implied external passives are limited to the VIN + * decoupling capacitor the design rules require. + * - Shareability exemption (`net_shareable`): GND is inherently + * shareable. + */ + +import type { + InterfaceDef, + ModuleDef, + TraitDef, +} from "../../src/types/index.js"; +import { + Ground, + I2C, + Pin, + PowerIn, + PowerOut, + clockFreqHz, + defineModule, +} from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Electrical constants — ProtoPart power domain / metadata +// --------------------------------------------------------------------------- + +/** VIN accepts 3 V to 5 V; onboard regulator + level shifters normalise to 3.3 V. */ +const VIN_RANGE: [number, number] = [3, 5]; +/** Regulated logic rail behind the onboard LDO. */ +const LOGIC_NOMINAL_V = 3.3; +/** metadata.i2c_max_frequency_hz — 400 kHz max SCL (SSD1306 datasheet Table 13-6: I2C clock cycle time tcycle min 2.5 µs). */ +const I2C_MAX_HZ = 400_000; +/** metadata.spi_max_frequency_hz — "Maximum SPI clock is 10 MHz per the SSD1306 datasheet." */ +const SPI_MAX_HZ = 10_000_000; +/** metadata.i2c_addresses_7bit — 0x3D default, 0x3C jumper-selectable. */ +const I2C_ADDRESS_DEFAULT = 0x3d; +const I2C_ADDRESS_ALTERNATE = 0x3c; + +/** Every pad sits behind the level shifters on the single 3.3 V logic domain. */ +const POWER_DOMAIN_TRAIT: TraitDef = { + type: "power_domain", + params: { + domain: "vdd_3v3", + note: "Full level-shifted logic: pads accept 3 V or 5 V signalling to match VIN; the SSD1306 itself runs from the onboard 3.3 V regulator.", + }, +}; + +function withTraits(iface: InterfaceDef, traits: TraitDef[]): InterfaceDef { + return { ...iface, traits: [...(iface.traits ?? []), ...traits] }; +} + +/** Attach traits to the interface with the given id inside a builder result. */ +function amend(ifaces: InterfaceDef[], id: string, traits: TraitDef[]): InterfaceDef[] { + return ifaces.map((i) => (i.id === id ? withTraits(i, traits) : i)); +} + +// --------------------------------------------------------------------------- +// Header pads — 8 leaf interfaces, in board order (ProtoPart resource order) +// --------------------------------------------------------------------------- + +const vin: InterfaceDef = { + ...PowerIn({ id: "vin", name: "VIN", pin: "VIN", voltageV: VIN_RANGE }), + capabilities: ["power_in"], + traits: [ + POWER_DOMAIN_TRAIT, + { + type: "internal_regulator", + params: { + description: + "Raw 3 V to 5 V power input pad to the onboard regulator and level-shifters. An onboard 3.3 V regulator and full-level-shifted logic give the SSD1306 controller a 3.3 V CMOS supply regardless of VIN.", + source: "Adafruit 938 product page / ProtoPart power domain vdd_3v3", + }, + }, + { + type: "implied_passives", + params: { + purpose: "Supply decoupling", + components: [{ kind: "capacitor", value: "≥0.1 µF", connection: "VIN to GND, local to the breakout" }], + source: "ProtoPart design rule: 'Decouple VIN to ground with at least 0.1 uF locally.'", + }, + }, + ], +}; + +const oled3vo: InterfaceDef = { + ...PowerOut({ id: "oled_3vo", name: "3Vo", pin: "3Vo", voltageV: LOGIC_NOMINAL_V, maxCurrentA: 0.1 }), + capabilities: ["power_out", "regulator_tap"], + traits: [ + POWER_DOMAIN_TRAIT, + { + type: "internal_regulator", + params: { + description: "Regulated 3.3 V output from the onboard LDO. Optional tap to drive small downstream peripherals at <=100 mA.", + }, + }, + { + type: "current_derating", + params: { + rule: "The 3Vo regulator output can drive small downstream peripherals up to roughly 100 mA, but check thermal margin if loading heavily.", + source: "ProtoPart design rules", + }, + }, + { type: "optionality", params: { note: "Optional — may be left unconnected when no auxiliary 3.3 V tap is needed." } }, + ], +}; + +const gnd: InterfaceDef = { + ...Ground({ id: "gnd", name: "GND", pin: "GND" }), + traits: [ + { type: "net_shareable", params: { net: "gnd", policy: "single_ground_instance_may_serve_all_members" } }, + ], +}; + +/** CLK — serial clock pad, routed to SPI SCK or I2C SCL by the mode jumpers. */ +const spiClk: InterfaceDef = withTraits( + Pin({ + id: "spi_clk", + name: "CLK (SPI SCK / I2C SCL)", + pin: "CLK", + voltageV: VIN_RANGE, + capabilities: { inputOnly: true, spiSck: true, i2cScl: true }, + }), + [ + { + type: "pin_functions", + params: { + source: "ProtoPart resource spi_clk (Adafruit 938 documentation)", + functions: [ + { name: "SPI SCK", direction: "I", mode: "spi" }, + { name: "I2C SCL", direction: "I", mode: "i2c" }, + ], + note: "Routed to SPI SCK in SPI mode or I2C SCL in I2C mode (mode chosen by solder jumpers on the back of the breakout).", + }, + }, + POWER_DOMAIN_TRAIT, + ], +); + +/** DATA — serial data pad, routed to SPI MOSI or I2C SDA by the mode jumpers. */ +const spiData: InterfaceDef = withTraits( + Pin({ + id: "spi_data", + name: "DATA (SPI MOSI / I2C SDA)", + pin: "DATA", + voltageV: VIN_RANGE, + capabilities: { spiMosi: true, i2cSda: true }, + }), + [ + { + type: "pin_functions", + params: { + source: "ProtoPart resource spi_data (Adafruit 938 documentation)", + functions: [ + { name: "SPI MOSI (COPI)", direction: "I", mode: "spi", note: "Controller-out, peripheral-in. Write-only: there is no MISO/CIPO contact on the breakout." }, + { name: "I2C SDA", direction: "I/O", mode: "i2c" }, + ], + }, + }, + POWER_DOMAIN_TRAIT, + ], +); + +/** DC — Data/Command select, SPI mode only. */ +const displayDc: InterfaceDef = { + ...withTraits( + Pin({ + id: "display_dc", + name: "DC", + pin: "DC", + voltageV: VIN_RANGE, + capabilities: { inputOnly: true }, + }), + [ + { + type: "pin_functions", + params: { + source: "ProtoPart resource display_dc (Adafruit 938 documentation)", + functions: [ + { name: "DC", direction: "I", mode: "spi", note: "Data/Command select: high = data, low = command. Drive from MCU GPIO." }, + ], + }, + }, + POWER_DOMAIN_TRAIT, + { + type: "usage_restriction", + params: { + restriction: "Used in SPI mode only. DC is electrically unused in I2C mode; do not drive it in I2C-only builds.", + source: "ProtoPart warnings", + }, + }, + ], + ), + capabilities: ["digital_io", "display_dc"], +}; + +/** RST — active-low reset, optional in both bus modes: onboard auto-reset + * circuitry resets the display at power-up (Adafruit 938 datasheet, Nov 2019 + * STEMMA QT revision). (Absorbs the ProtoPart `display_rst_input` interface + * the way the gold standard absorbs single-pin digital interfaces into their + * leaf pad.) */ +const displayRst: InterfaceDef = { + ...withTraits( + Pin({ + id: "display_rst", + name: "RST", + pin: "RST", + voltageV: VIN_RANGE, + capabilities: { inputOnly: true }, + }), + [ + { + type: "pin_functions", + params: { + source: "ProtoPart resource display_rst (Adafruit 938 documentation)", + functions: [ + { name: "RST", direction: "I", note: "Active-low reset input. Optional in both SPI and I2C modes: onboard auto-reset circuitry resets the display at power-up (Adafruit 938 datasheet, Nov 2019 revision). May be driven from an MCU GPIO for explicit hardware resets." }, + ], + }, + }, + POWER_DOMAIN_TRAIT, + { + type: "reset_timing", + params: { + min_low_pulse_us: 3, + description: "Pulse RST low for at least 3 us during init before issuing any SPI or I2C commands.", + source: "ProtoPart design rules (SSD1306 datasheet)", + }, + }, + ], + ), + capabilities: ["digital_io", "display_reset", "reset_input"], +}; + +/** CS — active-low SPI chip select, SPI mode only. */ +const displayCs: InterfaceDef = withTraits( + Pin({ + id: "display_cs", + name: "CS", + pin: "CS", + voltageV: VIN_RANGE, + capabilities: { inputOnly: true, spiSs: true }, + }), + [ + { + type: "pin_functions", + params: { + source: "ProtoPart resource display_cs (Adafruit 938 documentation)", + functions: [ + { name: "CS", direction: "I", mode: "spi", note: "Active-low SPI chip-select input. Drive from MCU GPIO." }, + ], + }, + }, + POWER_DOMAIN_TRAIT, + { + type: "usage_restriction", + params: { + restriction: "Used in SPI mode only. CS is electrically unused in I2C mode; do not drive it in I2C-only builds.", + source: "ProtoPart warnings", + }, + }, + ], +); + +/** All 8 header pads in board order — schematic-honest. */ +const pads: InterfaceDef[] = [vin, oled3vo, gnd, spiClk, spiData, displayDc, displayRst, displayCs]; + +// --------------------------------------------------------------------------- +// Bus targets — I2C default, SPI selectable by solder-jumper cuts. +// The two composed interfaces bind the SAME CLK/DATA pads; the one_of +// interface group + co_requirement traits carry the mutual exclusion. +// --------------------------------------------------------------------------- + +const BUS_MODE_CO_REQUIREMENT = (other: string, condition: string, effect: string): TraitDef => ({ + type: "co_requirement", + params: { + with: other, + condition, + effect, + source: "Adafruit 938 product documentation: SPI vs I2C mode is set by physical solder jumpers and is not runtime-switchable.", + }, +}); + +const i2cTarget = amend( + I2C({ + id: "display_i2c_target", + name: "OLED I2C target", + roles: ["slave"], + clockFreqHz: [0, I2C_MAX_HZ], + address: I2C_ADDRESS_DEFAULT, + sda: "spi_data", + scl: "spi_clk", + maxInstances: 1, + defaultActive: true, // I2C is the factory-default mode (jumpers intact). + }), + "display_i2c_target", + [ + { type: "display_notation", params: { latex: "I^{2}C" } }, + { + type: "i2c_addressing", + params: { + default_7bit: "0x3D", + alternate_7bit: "0x3C", + selection: "jumper-selectable", + note: "7-bit address 0x3D by default, jumper-selectable to 0x3C.", + }, + }, + { + type: "onboard_passives", + params: { + purpose: "Open-drain bus pull-ups", + components: [{ kind: "resistor", connection: "SDA and SCL to the 3.3 V rail (fitted on the breakout)" }], + note: "The breakout includes onboard I2C pull-ups; if sharing a multi-target I2C bus, make sure overall pull-up loading still meets the bus rules.", + source: "ProtoPart design rules", + }, + }, + { + type: "unused_pins_in_mode", + params: { + pins: ["display_dc", "display_cs"], + note: "DC and CS pads are electrically unused in I2C mode; do not drive them in I2C-only builds.", + }, + }, + BUS_MODE_CO_REQUIREMENT( + "display_spi_target", + "Bus-mode solder jumpers intact (factory default)", + "The I2C target exists only while the two solder jumpers on the back of the breakout are intact. Cutting them for SPI mode permanently disables I2C — the modes are mutually exclusive, never simultaneous.", + ), + ], +); + +/** + * Hand-rolled SPI slave: the SPI() builder would fabricate a required MISO + * slot, but this bus is 3-wire write-only (SCK + MOSI + CS) plus the DC + * Data/Command digital input — there is no MISO/CIPO contact on the breakout. + */ +const spiTarget: InterfaceDef = { + id: "display_spi_target", + name: "OLED SPI target", + domain: "electrical", + exposed: true, + default_active: false, // Only after the one-time solder-jumper cuts. + protocols: [{ type: "spi", roles: ["slave"] }], + parameters: [clockFreqHz([0, SPI_MAX_HZ])], // "Maximum SPI clock is 10 MHz per the SSD1306 datasheet." + slots: [ + { id: "mosi", label: "MOSI/COPI (write-only)", required: true, match: { protocol: "spi", role: "data_out", capability: "spi_mosi" } }, + { id: "sck", required: true, match: { protocol: "spi", role: "clock", capability: "spi_sck" } }, + { id: "ss", label: "CS", required: true, match: { protocol: "spi", role: "select", capability: "spi_ss" } }, + { id: "dc", label: "Data/Command select", required: true, match: { protocol: "digital", role: "input", capability: "display_dc" } }, + ], + profiles: [ + { + id: "display_spi_pads", + label: "DATA/CLK/CS/DC header pads", + bindings: { mosi: "spi_data", sck: "spi_clk", ss: "display_cs", dc: "display_dc" }, + }, + ], + max_instances: 1, + traits: [ + { + type: "bus_topology", + params: { + wires: 3, + write_only: true, + note: "3-wire write-only SPI bus (SCK + MOSI + CS) plus a Data/Command digital input. The OLED is write-only over SPI — firmware that reads back display data over SPI will not work.", + source: "ProtoPart design rules / warnings", + }, + }, + BUS_MODE_CO_REQUIREMENT( + "display_i2c_target", + "Both bus-mode solder jumpers cut", + "Active only when the breakout's two solder jumpers are cut for SPI mode — a one-time physical board-prep step, not runtime configurable. Cutting them disables the default I2C target.", + ), + ], +}; + +// --------------------------------------------------------------------------- +// Mechanical +// --------------------------------------------------------------------------- + +/** Derived from the ProtoPart mechanical metadata (0.1" header, breakout PCB). */ +const headerMount: InterfaceDef = { + id: "header_mounting", + name: '0.1" Header Breakout Mounting', + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["mounting_point"] }], + capabilities: ["pcb_header_pins", "header_0_1in", "breakout_pcb"], +}; + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const ADAFRUIT_938_128X64_OLED: ModuleDef = defineModule({ + id: "adafruit-938-128x64-oled", + name: 'Adafruit Monochrome 1.3" 128x64 OLED Graphic Display', + version: "1.0.0", + manufacturer: "Adafruit Industries LLC", + part_number: "938", + description: + '1.3" diagonal monochrome 128x64 OLED graphic display breakout based on the Solomon Systech SSD1306 controller. Default-mode I2C, with SPI selectable by cutting two solder jumpers on the back of the breakout PCB. Onboard 3.3 V regulator and full level-shifted logic accept 3 V or 5 V at VIN. White pixels.', + tags: ["adafruit", "938", "oled", "1.3-inch", "128x64", "ssd1306", "monochrome", "display", "spi", "i2c", "breakout"], + categories: ["expansion.breakout"], + + interfaces: [ + // All 8 header pads in board order — schematic-honest. + ...pads, + + // Bus targets (mutually exclusive — see interfaceGroups.bus_mode) + ...i2cTarget, + spiTarget, + + // Mechanical + headerMount, + ], + + interfaceGroups: [ + { + id: "bus_mode", + label: "Host Bus Mode (solder-jumper selected)", + members: ["display_i2c_target", "display_spi_target"], + policy: "one_of", + }, + { + id: "required_power_pins", + label: "Required Power Pins", + members: ["vin", "gnd"], + policy: "all_of", + }, + { + id: "spi_mode_control_pins", + label: "SPI-Mode-Only Control Pads (unused in I2C mode)", + members: ["display_dc", "display_cs"], + policy: "all_of", + }, + ], + + requirements: [ + { + type: "power", + description: + "VIN accepts 3 V to 5 V into the onboard 3.3 V regulator; typical draw 25 mA, peak 40 mA (~132 mW). Decouple VIN to ground with at least 0.1 µF locally.", + voltage_V: [3, 5], + current_mA: 40, + }, + { + type: "interface", + description: + "Exactly one host bus (see bus_mode group): the default-mode I2C master (7-bit address 0x3D, jumper-selectable to 0x3C, up to 400 kHz per the SSD1306 datasheet), or — after cutting the two solder jumpers — a write-only SPI master (SCK/MOSI/CS, up to 10 MHz) plus a DC Data/Command GPIO.", + interface_protocol: "i2c", + }, + { + type: "interface", + description: + "RST is optional in both bus modes: onboard auto-reset circuitry resets the display at power-up (Adafruit 938 datasheet, Nov 2019 revision). If driven by a host GPIO, pulse low for at least 3 µs (SSD1306 datasheet) before issuing any SPI or I2C commands.", + interface_protocol: "digital", + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + { + id: "vdd_3v3", + name: "Display 3.3 V logic supply", + nominal_voltage_V: 3.3, + // ProtoPart voltage_range_V is the VIN acceptance window; the + // onboard regulator holds the SSD1306 at 3.3 V regardless. + voltage_range_V: VIN_RANGE, + max_current_mA: 50, + regulation_type: "regulated", + }, + ], + metadata: { + pin_count: 8, + supply_voltage_V: VIN_RANGE, + power_consumption_mW: 132, + current_typical_mA: 25, + current_peak_mA: 40, + panel_size_inches: 1.3, + resolution_pixels: "128x64", + controller: "SSD1306", + pixel_color: "white", + default_interface: "I2C", + spi_requires_jumper_cuts: true, + i2c_addresses_7bit: ["0x3D", "0x3C"], + i2c_max_frequency_hz: I2C_MAX_HZ, + spi_max_frequency_hz: SPI_MAX_HZ, + regulated_3v3_output: true, + logic_level_shifted: true, + source: "ProtoPart adafruit-938-128x64-oled definition (Adafruit 938 documentation / SSD1306 datasheet)", + }, + }, + { + domain: "mechanical", + dimensions_mm: { length: 35.6, width: 33, height: 6.2 }, + metadata: { + package_type: "Adafruit breakout PCB", + mounting_method: "pcb_header_pins", + connector_system: "0.1 inch header", + field_serviceable: true, + }, + }, + { + domain: "thermal", + operating_temperature_C: [-40, 85], + metadata: { + requires_thermal_management: false, + operating_temperature_source: + "Inherited from Solomon Systech SSD1306 controller datasheet; not stated on Adafruit product page.", + }, + }, + ], + + traits: [ + { + type: "bus_mode_selection", + params: { + default: "I2C", + alternate: "SPI", + mechanism: + "To enable SPI mode, cut the two solder jumpers on the back of the breakout (this is a one-time physical board-prep step, not runtime configurable).", + runtime_switchable: false, + mutually_exclusive: true, + source: "Adafruit 938 product documentation / ProtoPart design rules", + }, + }, + { + type: "display_panel", + params: { + controller: "SSD1306", + resolution_pixels: "128x64", + panel_size_inches: 1.3, + color: "monochrome (white pixels)", + }, + }, + { + type: "software_compatibility", + params: { + libraries: ["u8g2", "Adafruit_SSD1306", "luma.oled"], + note: "SSD1306 is broadly supported. Default I2C address is 0x3D (jumper-selectable to 0x3C); SPI mode requires physically cutting two solder jumpers on the breakout's back side.", + source: "ProtoPart compatibility_notes", + }, + }, + ], + + artifacts: [ + { + id: "art_datasheet", + name: "Adafruit 938 Product Datasheet (938_Web.pdf)", + type: "datasheet", + url: "https://mm.digikey.com/Volume0/opasdata/d220001/medias/docus/197/938_Web.pdf", + filePath: "./ProtoPart/protoparts/adafruit-938-128x64-oled/artifacts/datasheet/938_Web.pdf", + mimeType: "application/pdf", + }, + { + id: "art_product_page", + name: "Adafruit Product Page (product 938)", + type: "documentation", + url: "https://www.adafruit.com/product/938", + }, + ], + + geometry: { + xScale: 1, + yScale: 1, + outline: { preset: "rectangle" }, + }, +}); diff --git a/library/parts/adafruit-938-128x64-oled/artifacts/thumbnail.png b/library/parts/adafruit-938-128x64-oled/artifacts/thumbnail.png new file mode 100644 index 0000000..4ec105c Binary files /dev/null and b/library/parts/adafruit-938-128x64-oled/artifacts/thumbnail.png differ diff --git a/library/parts/adafruit-feather-nrf52-bluefruit-le-3406.ts b/library/parts/adafruit-feather-nrf52-bluefruit-le-3406.ts new file mode 100644 index 0000000..cbf91b3 --- /dev/null +++ b/library/parts/adafruit-feather-nrf52-bluefruit-le-3406.ts @@ -0,0 +1,1198 @@ +/** + * Adafruit Feather nRF52 Bluefruit LE (product 3406) — source-honest part definition. + * + * Primary sources: + * - Adafruit Bluefruit nRF52 Feather Learning Guide + * (https://cdn-learn.adafruit.com/downloads/pdf/bluefruit-nrf52-feather-learning-guide.pdf) + * - ProtoPart definition.json, id "adafruit-feather-nrf52-bluefruit-le-3406", + * version 1.1.0 (schema 1.4.0) — the audited source of truth. Pin functions, + * power domains, design rules, usage notes, and warnings below are carried + * verbatim from that file; nothing is filled in from nRF52832 datasheet + * knowledge the JSON does not itself state. + * + * Documentation audit (2026-07, against the Adafruit learn guide pages, + * product page 3406, the Adafruit nRF52 BSP feather_nrf52832 variant, the + * Zephyr board docs, and the Nordic nRF52832 Product Specification) corrected + * four source claims and resolved two data gaps, each cited inline: + * - A4/A5 ports un-swapped (A4 = P0.28, A5 = P0.29); + * - 3V3 regulator output corrected 400 mA -> 500 mA peak (not continuous); + * - per-GPIO current limits replaced with the Nordic-documented values + * (standard ~2 mA; high drive 14/15 mA at VDD >= 2.7 V; 15 mA chip total + * recommended) — the source's 10/5/30 mA figures are undocumented; + * - the verbatim 0.001 Mbps UART constraint is flagged as a source error; + * - ADC characteristics (8/10/12-bit, default 10-bit, 0-3.6 V via internal + * 0.6 V ref with 1/6 gain) and the A7 double-100K divider filled in; + * - LED2 colour (blue) filled in. Charge current and JST polarity remain + * genuine gaps: no fetched documentation page states them. + * + * Architecture notes honoured by this file: + * - Pin-honest like a schematic: every header pin, test point, and on-board + * LED the source defines is a leaf interface with the official silkscreen + * name; where the source records the routed nRF52832 port pin (e.g. + * "SDA (P0.25)") that port designator is carried as the `pin` field. + * The source assigns no header-position ordinals, so pins appear in + * source order, not numbered header order. + * - Every leaf pin carries its verbatim source function list (name, + * direction, signal_class) in a `feather_pin_functions` trait — display + * data is separated from the canonical capability tags the matching + * engine needs. + * - Peripheral controllers exposed at the headers (I2C, SPI, UART, ADC) + * are composed interfaces whose slots bind the leaf pins through named + * profiles; `max_instances` mirrors the source's `max_connections`. + * - The USB micro-B connector, JST-PH battery connector, and BLE radio are + * modeled exactly as far as the source models them (power-domain + * descriptions, the CP2104 USB-UART bridge, and board metadata); RF + * parameters, charge current, JST polarity, and the VBUS/VBAT ORing + * topology are NOT specified by the source and are flagged as gaps + * rather than fabricated. + * - Board-level design_rules / usage_notes / warnings / compatibility_notes + * from the source are preserved verbatim as module traits, and their + * pin-specific consequences (A7 battery divider, P0.28/P0.29 low-drive, + * DFU/FRST boot behaviour, per-GPIO current limits) are additionally + * attached to the affected leaf interfaces. + */ + +import type { + InterfaceDef, + ModuleDef, + SlotDef, + TraitDef, +} from "../../src/types/index.js"; +import type { Parameter } from "../../src/types/parameter.js"; +import type { PinCapabilities } from "../../src/protocols/index.js"; +import { + Ground, + Pin, + PowerIn, + SPI, + UART, + I2C, + defineModule, +} from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Electrical constants — ProtoPart power_domains + design_rules/warnings +// --------------------------------------------------------------------------- + +const SOURCE = + "Adafruit Bluefruit nRF52 Feather Learning Guide; ProtoPart definition.json v1.1.0"; + +/** Power domain vbus_5v: "USB micro-B power input." */ +const VBUS_RANGE: [number, number] = [4.5, 5.2]; +/** Power domain vbat_lipo: "Single-cell LiPo battery input via JST-PH." */ +const VBAT_RANGE: [number, number] = [3, 4.2]; +/** Power domain vdd_3v3: "Board 3.3V rail from on-board LDO; available on 3V pin." */ +const VDD_3V3_RANGE: [number, number] = [3, 3.4]; + +/** + * nRF52832 GPIO drive, per the Nordic nRF52832 Product Specification (GPIO + * electrical specification) as confirmed by Nordic on DevZone Q&A 12614: + * standard drive ~2 mA nominal; high drive max 14 mA source / 15 mA sink at + * VDD >= 2.7 V; Nordic recommends <= 15 mA total GPIO current for the chip. + * NOTE: corrects the ProtoPart source's "10 mA (5 mA recommended) / 30 mA + * package" figures, which do not appear in the nRF52832 PS. + */ +const GPIO_STANDARD_DRIVE_MA = 2; + +const GPIO_CURRENT_TRAIT: TraitDef = { + type: "gpio_current_limits", + params: { + per_gpio_standard_drive_mA: 2, + per_gpio_high_drive_source_max_mA: 14, + per_gpio_high_drive_sink_max_mA: 15, + high_drive_condition: "VDD >= 2.7 V", + chip_total_recommended_mA: 15, + source: + "Nordic nRF52832 Product Specification — GPIO electrical specification (high drive max 14 mA source / 15 mA sink at VDD >= 2.7 V); Nordic DevZone Q&A 12614 (15 mA total recommended for the chip). Corrects the ProtoPart figures (10/5/30 mA), which are not documented.", + }, +}; + +/** Design rule + warning: 3.3 V logic only, never 5 V. */ +const LOGIC_LEVEL_TRAIT: TraitDef = { + type: "logic_level_restriction", + params: { + logic_V: 3.3, + five_volt_tolerant: false, + note: "All GPIO are 3.3V only; not 5V tolerant. Do not apply >3.3V to any GPIO.", + source: `${SOURCE} — design_rules / warnings`, + }, +}; + +// --------------------------------------------------------------------------- +// Source-honest per-pin function metadata +// --------------------------------------------------------------------------- + +/** Verbatim function entry from the ProtoPart resource's `functions` array. */ +interface FeatherFunction { + name: string; + /** Source `direction` field, carried verbatim. */ + direction: "source" | "sink" | "bidirectional" | "input"; + /** Source `signal_class` field, carried verbatim. */ + signal_class: string; +} + +/** Wrap a resource's verbatim function list + description as a display trait. */ +function fnTrait(functions: FeatherFunction[], description: string): TraitDef { + return { + type: "feather_pin_functions", + params: { source: SOURCE, functions, description }, + }; +} + +/** ProtoPart per-resource `connector_type`, preserved verbatim. */ +function connectorTrait(connector: string): TraitDef { + return { type: "connector_type", params: { connector } }; +} + +/** ProtoPart per-resource `power_domain_id`, preserved verbatim. */ +function powerDomainTrait(domain: string, note?: string): TraitDef { + return { type: "power_domain", params: { domain, ...(note ? { note } : {}) } }; +} + +function withTraits(iface: InterfaceDef, traits: TraitDef[]): InterfaceDef { + return { ...iface, traits: [...(iface.traits ?? []), ...traits] }; +} + +/** Attach traits to the interface with the given id inside a builder result. */ +function amend(ifaces: InterfaceDef[], id: string, traits: TraitDef[]): InterfaceDef[] { + return ifaces.map((i) => (i.id === id ? withTraits(i, traits) : i)); +} + +// --------------------------------------------------------------------------- +// GPIO / signal header pins +// --------------------------------------------------------------------------- + +interface FeatherGpioSpec { + id: string; + /** Official silkscreen name (source `name`, minus the routed port pin). */ + name: string; + /** Routed nRF52832 port pin (from the source `name`, e.g. "SDA (P0.25)"). */ + port: string; + capabilities?: PinCapabilities; + functions: FeatherFunction[]; + /** Verbatim source description. */ + description: string; + connector?: string; + extraTraits?: TraitDef[]; +} + +/** + * Build one source-honest 3.3 V GPIO header pin. Digital I/O is retained as + * the base capability because the source's design_rules address "All GPIO" + * collectively; per-pin functions beyond that are only what the source lists. + */ +function featherGpio(spec: FeatherGpioSpec): InterfaceDef { + const base = Pin({ + id: spec.id, + name: spec.name, + pin: spec.port, + voltageV: VDD_3V3_RANGE, + driveCurrentmA: GPIO_STANDARD_DRIVE_MA, + capabilities: spec.capabilities, + }); + return { + ...base, + traits: [ + fnTrait(spec.functions, spec.description), + powerDomainTrait("vdd_3v3"), + connectorTrait(spec.connector ?? "through_hole_header_0.1in"), + GPIO_CURRENT_TRAIT, + LOGIC_LEVEL_TRAIT, + ...(spec.extraTraits ?? []), + ], + }; +} + +// --- Power and control pins — source order --------------------------------- + +const vbusPin: InterfaceDef = { + ...PowerIn({ id: "pin_vbus", name: "VBUS", voltageV: VBUS_RANGE, nominalV: 5 }), + capabilities: ["power_in", "vbus_5v"], + traits: [ + fnTrait([{ name: "VBUS_IN", direction: "sink", signal_class: "power" }], "USB 5V input pin."), + powerDomainTrait("vbus_5v"), + connectorTrait("through_hole_header_0.1in"), + { + type: "current_limit", + params: { max_mA: 500, note: "USB VBUS max 500 mA.", source: `${SOURCE} — warnings` }, + }, + ], +}; + +const vbatPin: InterfaceDef = { + ...PowerIn({ id: "pin_vbat", name: "VBAT", voltageV: VBAT_RANGE, nominalV: 3.7 }), + capabilities: ["power_in", "vbat_lipo"], + traits: [ + fnTrait([{ name: "VBAT_IN", direction: "sink", signal_class: "power" }], "LiPo battery positive."), + powerDomainTrait("vbat_lipo", "Single-cell LiPo battery input via JST-PH."), + connectorTrait("through_hole_header_0.1in"), + ], +}; + +/** "3.3V regulated output/input." — the source models this rail bidirectional. */ +const pin3v3: InterfaceDef = { + id: "pin_3v3", + name: "3V3", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "power", roles: ["input", "output"] }], + capabilities: ["power_in", "power_out", "vdd_3v3"], + parameters: [ + { id: "voltage", unit: "V", value: 3.3, range: VDD_3V3_RANGE }, + { id: "max_current", name: "Max regulator output (peak)", unit: "A", value: 0.5 }, + ], + traits: [ + fnTrait( + [{ name: "VDD_3V3", direction: "bidirectional", signal_class: "power" }], + "3.3V regulated output/input.", + ), + { + type: "internal_regulator", + params: { + description: "Board 3.3V rail from on-board LDO; available on 3V pin.", + source: `${SOURCE} — power domain vdd_3v3`, + }, + }, + { + type: "current_limit", + params: { + max_mA: 500, + peak: true, + note: "500 mA peak regulator output; not sustainable continuously from 5 V (regulator overheats).", + source: + "Adafruit learn guide, Power Management: 'We use a 500mA peak regulator. While you can get 500mA from it, you can't do it continuously from 5V as it will overheat the regulator.' Corrects the ProtoPart 400 mA figure.", + }, + }, + { + type: "co_requirement", + params: { + with: "pin_en", + condition: "EN pulled low", + effect: "Pull low to disable 3V3 regulator — the 3V3 rail (and everything on it) shuts down.", + source: `${SOURCE} — pin EN description / usage notes`, + }, + }, + powerDomainTrait("vdd_3v3"), + connectorTrait("through_hole_header_0.1in"), + ], +}; + +const gndPin: InterfaceDef = { + ...Ground({ id: "pin_gnd", name: "GND" }), + traits: [ + fnTrait([{ name: "GND", direction: "sink", signal_class: "ground" }], "Ground pins."), + { + type: "net_shareable", + params: { + net: "gnd", + policy: "single_ground_instance_may_serve_all_members", + note: "The source models the header's ground positions ('Ground pins.', plural) as one shared ground resource.", + }, + }, + // The source records power_domain_id "vdd_3v3" on the GND resource; + // preserved verbatim rather than corrected. + powerDomainTrait("vdd_3v3", "As recorded in the source for the GND resource."), + connectorTrait("through_hole_header_0.1in"), + ], +}; + +const enPin: InterfaceDef = { + id: "pin_en", + name: "EN", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input"] }], + capabilities: ["regulator_enable"], + parameters: [{ id: "voltage", unit: "V", range: VDD_3V3_RANGE }], + traits: [ + fnTrait( + [{ name: "EN", direction: "input", signal_class: "control" }], + "Regulator enable. Pull low to disable 3V3 regulator.", + ), + powerDomainTrait("vdd_3v3"), + connectorTrait("through_hole_header_0.1in"), + LOGIC_LEVEL_TRAIT, + ], +}; + +const rstPin: InterfaceDef = { + id: "pin_rst", + name: "RST", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input"] }], + capabilities: ["reset_input"], + parameters: [{ id: "voltage", unit: "V", range: VDD_3V3_RANGE }], + traits: [ + fnTrait([{ name: "RESET", direction: "input", signal_class: "reset" }], "Active-low reset."), + powerDomainTrait("vdd_3v3"), + connectorTrait("through_hole_header_0.1in"), + LOGIC_LEVEL_TRAIT, + ], +}; + +// --- Bus signal pins -------------------------------------------------------- + +const sdaPin = featherGpio({ + id: "pin_sda", + name: "SDA", + port: "P0.25", + capabilities: { i2cSda: true }, + functions: [{ name: "I2C_SDA", direction: "bidirectional", signal_class: "data" }], + description: "I2C data.", +}); + +const sclPin = featherGpio({ + id: "pin_scl", + name: "SCL", + port: "P0.26", + capabilities: { i2cScl: true }, + functions: [{ name: "I2C_SCL", direction: "source", signal_class: "clock" }], + description: "I2C clock.", +}); + +const sckPin = featherGpio({ + id: "pin_sck", + name: "SCK", + port: "P0.12", + capabilities: { spiSck: true }, + functions: [{ name: "SPI_SCK", direction: "source", signal_class: "clock" }], + description: "SPI clock.", +}); + +const mosiPin = featherGpio({ + id: "pin_mosi", + name: "MOSI", + port: "P0.13", + capabilities: { spiMosi: true }, + functions: [{ name: "SPI_MOSI", direction: "source", signal_class: "data" }], + description: "SPI MOSI.", +}); + +const misoPin = featherGpio({ + id: "pin_miso", + name: "MISO", + port: "P0.14", + capabilities: { spiMiso: true }, + functions: [{ name: "SPI_MISO", direction: "sink", signal_class: "data" }], + description: "SPI MISO.", +}); + +const txPin = featherGpio({ + id: "pin_tx", + name: "TX", + port: "P0.06", + capabilities: { uartTx: true }, + functions: [{ name: "UART_TX", direction: "source", signal_class: "uart_tx" }], + description: "UART TXD to CP2104.", + extraTraits: [ + { + type: "shared_with_onboard", + params: { + peripheral: "CP2104 USB-UART bridge", + note: "This header pin is the same net that feeds the on-board CP2104 (programming and serial console).", + source: SOURCE, + }, + }, + ], +}); + +const rxPin = featherGpio({ + id: "pin_rx", + name: "RX", + port: "P0.08", + capabilities: { uartRx: true }, + functions: [{ name: "UART_RX", direction: "sink", signal_class: "uart_rx" }], + description: "UART RXD from CP2104.", + extraTraits: [ + { + type: "shared_with_onboard", + params: { + peripheral: "CP2104 USB-UART bridge", + note: "This header pin is the same net that is driven by the on-board CP2104 (programming and serial console).", + source: SOURCE, + }, + }, + ], +}); + +// --- Analog inputs A0–A7 ---------------------------------------------------- + +const LOW_DRIVE_TRAIT: TraitDef = { + type: "usage_restriction", + params: { + restriction: "P0.28 and P0.29 recommended for low-drive, low-frequency GPIO only.", + note: "Incomplete in the source: the Nordic nRF52832 Product Specification pin assignments mark P0.22-P0.31 (10 pins) as recommended low drive, low frequency (<10 kHz) I/O to protect radio performance.", + source: `${SOURCE} — design_rules; full pin list: Nordic nRF52832 Product Specification, pin assignments`, + }, +}; + +interface AnalogSpec { + id: string; + name: string; + port: string; + description: string; + extraTraits?: TraitDef[]; +} + +// A4/A5 port mapping corrected: the Adafruit nRF52 BSP variant for this board +// (variants/feather_nrf52832/variant.h: PIN_A0..PIN_A7 = 2, 3, 4, 5, 28, 29, +// 30, 31) maps A4 = P0.28 and A5 = P0.29. The ProtoPart source had them +// swapped (A4 = P0.29, A5 = P0.28). +const ANALOG_SPECS: AnalogSpec[] = [ + { id: "pin_a0", name: "A0", port: "P0.02", description: "Analog input A0." }, + { id: "pin_a1", name: "A1", port: "P0.03", description: "Analog input A1." }, + { id: "pin_a2", name: "A2", port: "P0.04", description: "Analog input A2." }, + { id: "pin_a3", name: "A3", port: "P0.05", description: "Analog input A3." }, + { + id: "pin_a4", name: "A4", port: "P0.28", + description: "Analog input A4. Low-drive GPIO.", + extraTraits: [LOW_DRIVE_TRAIT], + }, + { + id: "pin_a5", name: "A5", port: "P0.29", + description: "Analog input A5. Low-drive GPIO.", + extraTraits: [LOW_DRIVE_TRAIT], + }, + { id: "pin_a6", name: "A6", port: "P0.30", description: "Analog input A6." }, + { + id: "pin_a7", name: "A7", port: "P0.31", + description: "Analog input A7 connected to VBAT divider.", + extraTraits: [ + { + type: "battery_monitor", + params: { + note: "A7/P0.31 tied to VBAT divider for battery measurement; avoid using as general input.", + divider: "double-100K resistor divider — ADC reads VBAT/2", + source: `${SOURCE} — design_rules; divider ratio: Adafruit learn guide, Power Management ("a double-100K resistor divider")`, + }, + }, + { + type: "usage_restriction", + params: { + restriction: "Avoid using as general input — permanently connected to the VBAT divider.", + source: `${SOURCE} — design_rules`, + }, + }, + ], + }, +]; + +const analogPins: InterfaceDef[] = ANALOG_SPECS.map((spec) => + featherGpio({ + id: spec.id, + name: spec.name, + port: spec.port, + capabilities: { analogIn: true }, + functions: [{ name: "ADC_IN", direction: "sink", signal_class: "analog" }], + description: spec.description, + extraTraits: spec.extraTraits, + }), +); + +// --- Boot-control test points ----------------------------------------------- + +const dfuPin: InterfaceDef = { + id: "pin_dfu", + name: "DFU", + pin: "P0.20", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input"] }], + capabilities: ["bootloader_entry"], + parameters: [{ id: "voltage", unit: "V", range: VDD_3V3_RANGE }], + traits: [ + fnTrait([{ name: "DFU_MODE", direction: "input", signal_class: "control" }], "Bootloader entry if low at reset."), + { + type: "boot_control", + params: { + action: "Pull DFU (P0.20) low at reset to enter serial bootloader.", + source: `${SOURCE} — design_rules`, + }, + }, + powerDomainTrait("vdd_3v3"), + connectorTrait("test_point"), + LOGIC_LEVEL_TRAIT, + ], +}; + +const frstPin: InterfaceDef = { + id: "pin_frst", + name: "FRST", + pin: "P0.22", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input"] }], + capabilities: ["factory_reset"], + parameters: [{ id: "voltage", unit: "V", range: VDD_3V3_RANGE }], + traits: [ + fnTrait([{ name: "FACTORY_RESET", direction: "input", signal_class: "control" }], "Factory reset if low at boot."), + { + type: "boot_control", + params: { + action: "FRST (P0.22) low at boot forces factory reset.", + source: `${SOURCE} — design_rules`, + }, + }, + powerDomainTrait("vdd_3v3"), + connectorTrait("test_point"), + LOGIC_LEVEL_TRAIT, + ], +}; + +// --- On-board LEDs ------------------------------------------------------------ + +function onboardLed(config: { + id: string; + name: string; + port: string; + description: string; +}): InterfaceDef { + return { + id: config.id, + name: config.name, + pin: config.port, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["output"] }], + capabilities: ["onboard_led"], + parameters: [{ id: "voltage", unit: "V", range: VDD_3V3_RANGE }], + traits: [ + fnTrait([{ name: "GPIO_OUT", direction: "source", signal_class: "digital" }], config.description), + powerDomainTrait("vdd_3v3"), + connectorTrait("smd_led"), + GPIO_CURRENT_TRAIT, + ], + }; +} + +const led1 = onboardLed({ id: "pin_led1", name: "LED1", port: "P0.17", description: "User LED red." }); +// The ProtoPart source says only "Status LED"; the colour (blue) is documented +// by the Adafruit BSP variant (feather_nrf52832 variant.h: LED_BLUE = 19) and +// the Zephyr board docs ("LED1 (blue) = P0.19"). +const led2 = onboardLed({ id: "pin_led2", name: "LED2", port: "P0.19", description: "Status LED (blue)." }); + +/** All leaf electrical resources, in source order (the source assigns no header ordinals). */ +const pins: InterfaceDef[] = [ + vbusPin, + vbatPin, + pin3v3, + gndPin, + enPin, + rstPin, + sdaPin, + sclPin, + sckPin, + mosiPin, + misoPin, + txPin, + rxPin, + ...analogPins, + dfuPin, + frstPin, + led1, + led2, +]; + +// --------------------------------------------------------------------------- +// Composed peripheral controllers — ProtoPart `interfaces` array +// --------------------------------------------------------------------------- + +function composed(config: { + id: string; + name: string; + domain?: InterfaceDef["domain"]; + protocolType: string; + roles: string[]; + slots: SlotDef[]; + profiles?: InterfaceDef["profiles"]; + parameters?: Parameter[]; + maxInstances?: number; + defaultActive?: boolean; + traits?: TraitDef[]; +}): InterfaceDef { + return { + id: config.id, + name: config.name, + domain: config.domain ?? "electrical", + exposed: true, + default_active: config.defaultActive ?? false, + protocols: [{ type: config.protocolType, roles: config.roles }], + slots: config.slots, + ...(config.profiles ? { profiles: config.profiles } : {}), + ...(config.parameters ? { parameters: config.parameters } : {}), + ...(config.maxInstances !== undefined ? { max_instances: config.maxInstances } : {}), + ...(config.traits ? { traits: config.traits } : {}), + }; +} + +// I2C — "I2C master on SDA=P0.25, SCL=P0.26; external pull-ups required." +// clock_freq 400 kHz is the source's max_lane_rate_mbps 0.4 (= 400 kbit/s). +const i2cMaster = amend( + I2C({ + id: "i2c_master", + name: "I2C (Wire)", + roles: ["master"], + clockFreqHz: 400_000, + sda: "pin_sda", + scl: "pin_scl", + maxInstances: 1, // source constraint: max_connections 1 + }), + "i2c_master", + [ + { type: "display_notation", params: { latex: "I^{2}C" } }, + { + type: "implied_passives", + params: { + purpose: "Open-drain bus pull-ups", + components: [ + { kind: "resistor", value: "external pull-ups (value per bus speed)", connection: "SDA and SCL to the 3.3 V rail" }, + ], + source: `${SOURCE} — "external pull-ups required" / design rule: "I2C requires external pull-up resistors; not present on board."`, + }, + }, + { + type: "protopart_constraints", + params: { + max_connections: 1, + max_lane_rate_mbps: 0.4, + requires_matching_voltage_domain: true, + source: "ProtoPart definition.json interfaces.i2c_master.constraints (verbatim)", + }, + }, + ], +); + +// SPI — "SPI on SCK=P0.12, MOSI=P0.13, MISO=P0.14." +// clock_freq 8 MHz is the source's max_lane_rate_mbps 8 (= 8 Mbit/s). +const spiMaster = amend( + SPI({ + id: "spi_master", + name: "SPI", + roles: ["master"], + clockFreqHz: 8_000_000, + mosi: "pin_mosi", + miso: "pin_miso", + sck: "pin_sck", + maxInstances: 1, // source constraint: max_connections 1 + }), + "spi_master", + [ + { + type: "protopart_constraints", + params: { + max_connections: 1, + max_lane_rate_mbps: 8, + source: "ProtoPart definition.json interfaces.spi_master.constraints (verbatim)", + }, + }, + { + type: "chip_select_note", + params: { + note: "The source defines no dedicated chip-select pin; the SS slot is left unbound.", + }, + }, + ], +); + +// UART — "UART connected to on-board CP2104 USB-UART for programming and +// serial console." The source's max_lane_rate_mbps (0.001) is preserved +// verbatim in a trait; no baud-rate specification exists elsewhere in it. +const uartDebug = amend( + UART({ + id: "uart_debug", + name: "UART to USB (via CP2104)", + roles: ["device"], + rx: "pin_rx", + tx: "pin_tx", + maxInstances: 1, // source constraint: max_connections 1 + }), + "uart_debug", + [ + { + type: "usb_uart_bridge", + params: { + bridge: "CP2104", + note: "UART connected to on-board CP2104 USB-UART for programming and serial console.", + source: SOURCE, + }, + }, + { + type: "protopart_constraints", + params: { + max_connections: 1, + max_lane_rate_mbps: 0.001, + note: "Carried verbatim from the source, but 0.001 Mbps (= 1 kbps) is contradicted by documentation: the CP2104 bridge supports 300 bps-2 Mbaud (Silicon Labs CP2104 datasheet) and the Adafruit guide's serial examples run at 115200 baud. Treat the verbatim figure as a source error, not a hardware limit.", + source: "ProtoPart definition.json interfaces.uart_debug.constraints (verbatim); correction context: Silicon Labs CP2104 datasheet, Adafruit learn guide serial examples", + }, + }, + { + type: "co_requirement", + params: { + with: "FeatherWings requiring a dedicated UART", + condition: "UART in use by the CP2104 bridge", + effect: "Works with most FeatherWings except those requiring dedicated UART if UART is in use by CP2104 bridge. For FeatherWings, keep UART free if required by specific Wing.", + source: `${SOURCE} — compatibility_notes / usage_notes`, + }, + }, + ], +); + +// ADC — the source defines eight ADC_IN header pins but no converter +// parameters; resolution/reference/range below are supplied from the Adafruit +// learn guide (Device Pinout page) and the product 3406 page. +const adc = composed({ + id: "adc", + name: "ADC (A0-A7)", + protocolType: "analog", + roles: ["input"], + slots: [ + { id: "channel", required: true, count: 8, match: { protocol: "analog", role: "input", capability: "analog_in" } }, + ], + profiles: [ + { + id: "adc_channels", + label: "A0-A7 header pins", + bindings: { + channel: ["pin_a0", "pin_a1", "pin_a2", "pin_a3", "pin_a4", "pin_a5", "pin_a6", "pin_a7"], + }, + }, + ], + maxInstances: 1, + traits: [ + { + type: "channels", + params: { + count: 8, + mapping: "A0=P0.02, A1=P0.03, A2=P0.04, A3=P0.05, A4=P0.28, A5=P0.29, A6=P0.30, A7=P0.31 (VBAT divider)", + }, + }, + { + type: "adc_characteristics", + params: { + resolution_bits_options: [8, 10, 12], + default_resolution_bits: 10, + default_reference: "internal 0.6 V reference with 1/6 gain", + default_input_range_V: [0, 3.6], + note: "Resolves the ProtoPart data gap (the source omitted all converter parameters).", + source: + "Adafruit learn guide, Device Pinout: 'The 8 available analog inputs can be configured to generate 8, 10 or 12-bit data'; 'Default voltage range: 0-3.6V (uses the internal 0.6V reference with 1/6 gain)'; 'Default resolution: 10-bit (0..1023)'. Adafruit product page 3406: '8 x 12-bit ADC pins'.", + }, + }, + ], +}); + +// Power inputs — "Power via USB 5V or LiPo VBAT; board regulates to 3.3V." +const powerInputs = composed({ + id: "power_inputs", + name: "Power Inputs", + protocolType: "power", + roles: ["input"], + slots: [ + { id: "vbus", required: true, match: { protocol: "power", role: "input", capability: "vbus_5v" } }, + { id: "vbat", required: true, match: { protocol: "power", role: "input", capability: "vbat_lipo" } }, + { id: "gnd", required: true, match: { protocol: "power", role: "ground", capability: "ground" } }, + ], + profiles: [ + { + id: "power_inputs_default", + label: "VBUS / VBAT / GND", + default_active: true, + bindings: { vbus: "pin_vbus", vbat: "pin_vbat", gnd: "pin_gnd" }, + }, + ], + maxInstances: 1, + defaultActive: true, + traits: [ + { + type: "power_sources", + params: { + description: "Power via USB 5V or LiPo VBAT; board regulates to 3.3V.", + alternatives: "Power from USB (VBUS), LiPo (VBAT), or regulated 3.3V on 3V pin.", + source: `${SOURCE} — interfaces.power_inputs / usage_notes`, + }, + }, + { + type: "lipo_charger", + params: { + note: "'Built-in LiPo charger' (board metadata). Adafruit learn guide (Power Management) confirms: when USB power is present the board automatically switches over to USB and starts charging an attached battery, with a CHG LED lit while charging (the LED may flicker with no battery attached). Charge current, JST polarity, and the VBUS/VBAT power-ORing topology are stated neither by the source nor in the guide text — those gaps remain unrepresented rather than fabricated.", + source: `${SOURCE} — metadata.description; charging behaviour: Adafruit learn guide, Power Management`, + }, + }, + ], +}); + +// --------------------------------------------------------------------------- +// Connectors — modeled exactly as far as the source describes them +// --------------------------------------------------------------------------- + +const usbMicroB: InterfaceDef = { + id: "usb_micro_b", + name: "USB Micro-B", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [ + { type: "usb", roles: ["device"] }, + { type: "power", roles: ["input"] }, + ], + capabilities: ["usb_device", "usb_power_input"], + parameters: [ + { id: "voltage", unit: "V", value: 5, range: VBUS_RANGE }, + { id: "max_current", unit: "A", value: 0.5 }, // warnings: "USB VBUS max 500 mA" + ], + traits: [ + { + type: "connector", + params: { + kind: "USB micro-B", + source: `${SOURCE} — power domain vbus_5v: "USB micro-B power input."`, + }, + }, + { + type: "usb_uart_bridge", + params: { + bridge: "CP2104", + note: "USB data path is the on-board CP2104 USB-UART bridge, used for programming and the serial console.", + source: `${SOURCE} — interfaces.uart_debug description`, + }, + }, + ], + bridgesTo: ["pin_vbus", "uart_debug"], +}; + +const jstBattery: InterfaceDef = { + id: "jst_ph_battery", + name: "JST-PH Battery Connector", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "power", roles: ["input"] }], + capabilities: ["battery_input", "jst_ph"], + parameters: [{ id: "voltage", unit: "V", value: 3.7, range: VBAT_RANGE }], + traits: [ + { + type: "connector", + params: { + kind: "JST-PH", + source: `${SOURCE} — power domain vbat_lipo: "Single-cell LiPo battery input via JST-PH."`, + }, + }, + { + type: "data_gap", + params: { note: "Connector polarity and charge current are not specified in the source." }, + }, + ], + bridgesTo: ["pin_vbat"], +}; + +// --------------------------------------------------------------------------- +// Radio — the source names BLE only in board metadata; no RF data exists in it +// --------------------------------------------------------------------------- + +const bleRadio: InterfaceDef = { + id: "ble_radio", + name: "Bluetooth LE Radio (nRF52832)", + domain: "network", + exposed: true, + default_active: false, + protocols: [{ type: "bluetooth", roles: ["peer"] }], + capabilities: ["bluetooth_le"], + traits: [ + { + type: "radio_characteristics", + params: { + compliance: "Bluetooth LE (Nordic nRF52832 SoC)", + note: "The source names the BLE radio only in board metadata ('Nordic nRF52832 SoC with Bluetooth LE'); it defines no RF parameters, antenna feed pin, or TX/RX characteristics — none are fabricated here.", + source: `${SOURCE} — metadata.description / tags / taxonomy`, + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// Mechanical — Feather footprint +// --------------------------------------------------------------------------- + +const featherHeaders: InterfaceDef = { + id: "feather_headers", + name: "Feather header rows", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["connector"] }], + capabilities: ["mechanical_connector", "through_hole_header_0.1in"], + traits: [ + { + type: "feather_pin_functions", + params: { + source: SOURCE, + functions: [ + { name: "MECHANICAL_CONNECTOR", direction: "bidirectional", signal_class: "mechanical_drive" }, + ], + description: 'Two 0.1" header rows for Feather/Wing stacking.', + }, + }, + connectorTrait("through_hole_header_0.1in"), + ], +}; + +const featherFootprint = composed({ + id: "feather_footprint", + name: "Feather footprint", + domain: "mechanical", + protocolType: "mechanical_connection", + roles: ["mounting_point"], + slots: [ + { id: "headers", required: true, match: { protocol: "mechanical_connection", role: "connector", capability: "mechanical_connector" } }, + ], + profiles: [ + { + id: "feather_footprint_default", + label: "Feather header rows", + default_active: true, + bindings: { headers: "feather_headers" }, + }, + ], + maxInstances: 1, // source: max_instances 1 + defaultActive: true, + traits: [ + { + type: "form_factor", + params: { + standard: "Adafruit Feather", + note: "Standard Feather board outline with 4 mounting holes.", + source: SOURCE, + }, + }, + ], +}); + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const ADAFRUIT_FEATHER_NRF52_BLUEFRUIT_LE_3406: ModuleDef = defineModule({ + id: "adafruit-feather-nrf52-bluefruit-le-3406", + name: "Adafruit Feather nRF52 Bluefruit LE (nRF52832)", + version: "1.1.0", + manufacturer: "Adafruit Industries", + part_number: "3406", + description: + "Feather-format development board featuring Nordic nRF52832 SoC with Bluetooth LE, built-in LiPo charger, USB-UART, and Arduino IDE support. MCU: Nordic nRF52832 (Cortex-M4F, 64 MHz, 512KB flash, 64KB RAM).", + tags: [ + "Feather", + "nRF52832", + "Bluetooth-LE", + "Arduino-compatible", + "LiPo-charger", + "USB-UART", + "3.3V-logic", + ], + categories: ["microcontroller.adafruit_mcu", "connectivity.wireless"], + + interfaces: [ + // Leaf header pins, test points, and on-board LEDs — source order. + ...pins, + + // Bus controllers exposed at the headers + ...i2cMaster, + ...spiMaster, + ...uartDebug, + adc, + + // Power + powerInputs, + + // Connectors + usbMicroB, + jstBattery, + + // Radio + bleRadio, + + // Mechanical + featherHeaders, + featherFootprint, + ], + + interfaceGroups: [ + { + id: "power_sources", + label: "Power Sources (USB VBUS, LiPo VBAT, or regulated 3.3 V)", + members: ["pin_vbus", "pin_vbat", "pin_3v3"], + policy: "any_of", + }, + { + id: "boot_control_pins", + label: "Bootloader Control Test Points (DFU / FRST)", + members: ["pin_dfu", "pin_frst"], + policy: "all_of", + }, + { + id: "analog_inputs", + label: "Analog Inputs A0-A7", + members: ["pin_a0", "pin_a1", "pin_a2", "pin_a3", "pin_a4", "pin_a5", "pin_a6", "pin_a7"], + policy: "all_of", + }, + ], + + requirements: [ + { + type: "power", + description: + "Power from USB VBUS (5 V nominal, 4.5-5.2 V, max 500 mA), a single-cell LiPo on VBAT via JST-PH (3.7 V nominal, 3.0-4.2 V), or regulated 3.3 V applied to the 3V pin; the on-board LDO regulates the 3.3 V rail (3.0-3.4 V, 3V3 pin max 500 mA peak output, not continuous from 5 V).", + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + // vbus_5v max_current_mA is from the source's `warnings`. vdd_3v3 + // max_current_mA is corrected to the Adafruit guide's "500mA peak + // regulator" (Power Management page); the source's 400 mA is not + // documented anywhere in the guide. + { id: "vbus_5v", name: "USB VBUS 5V", nominal_voltage_V: 5, voltage_range_V: VBUS_RANGE, max_current_mA: 500 }, + { id: "vbat_lipo", name: "VBAT LiPo", nominal_voltage_V: 3.7, voltage_range_V: VBAT_RANGE }, + { id: "vdd_3v3", name: "3V3 Regulated", nominal_voltage_V: 3.3, voltage_range_V: VDD_3V3_RANGE, max_current_mA: 500, regulation_type: "regulated" }, + ], + metadata: { + pin_count: 28, + supply_voltage_V: VDD_3V3_RANGE, + logic_levels: { vih_min: "0.7*VDD", vil_max: "0.3*VDD" }, + mcu: "Nordic nRF52832 (Cortex-M4F, 64 MHz, 512KB flash, 64KB RAM)", + power_domain_descriptions: { + vbus_5v: "USB micro-B power input.", + vbat_lipo: "Single-cell LiPo battery input via JST-PH.", + vdd_3v3: "Board 3.3V rail from on-board LDO; available on 3V pin.", + }, + isolation: "All rails non_isolated, common ground reference (source power_domains).", + source: SOURCE, + }, + }, + { + domain: "mechanical", + dimensions_mm: { length: 51, width: 22.9, height: 7.1 }, + metadata: { + package_type: "Adafruit Feather", + mounting_method: "through_hole_headers", + mounting_holes: 4, + }, + }, + { + domain: "thermal", + operating_temperature_C: [-40, 85], + metadata: { + requires_thermal_management: false, + }, + }, + ], + + traits: [ + { + // Source design_rules; pin-level consequences are also attached to the + // affected leaf interfaces above. The per-GPIO current rule is corrected: + // the source's "10 mA (5 mA recommended) / 30 mA package" figures do not + // appear in the nRF52832 Product Specification. + type: "design_rules", + params: { + rules: [ + "All GPIO are 3.3V only; not 5V tolerant.", + "I2C requires external pull-up resistors; not present on board.", + "GPIO drive (nRF52832 PS, GPIO electrical specification): standard drive ~2 mA nominal; high drive max 14 mA source / 15 mA sink at VDD >= 2.7 V; Nordic recommends <= 15 mA total GPIO current for the chip.", + "A7/P0.31 tied to VBAT divider for battery measurement; avoid using as general input.", + "P0.28 and P0.29 recommended for low-drive, low-frequency GPIO only.", + "Pull DFU (P0.20) low at reset to enter serial bootloader; FRST (P0.22) low at boot forces factory reset.", + ], + source: SOURCE, + }, + }, + { + type: "usage_notes", + params: { + note: "Power from USB (VBUS), LiPo (VBAT), or regulated 3.3V on 3V pin. Use EN pin low to disable 3V3 regulator. Provide local decoupling for external peripherals. For FeatherWings, keep UART free if required by specific Wing.", + source: SOURCE, + }, + }, + { + type: "warnings", + params: { + warnings: [ + "ESD sensitive. Handle with care.", + "Do not apply >3.3V to any GPIO.", + "USB VBUS max 500 mA; 3V3 pin max 500 mA peak output from regulator (not continuous from 5 V — Adafruit learn guide, Power Management; corrects the ProtoPart 400 mA figure).", + ], + source: SOURCE, + }, + }, + { + type: "compatibility", + params: { + note: "Standard Feather footprint. Works with most FeatherWings except those requiring dedicated UART if UART is in use by CP2104 bridge. Arduino IDE supported via Adafruit nRF52 BSP.", + source: SOURCE, + }, + }, + { + type: "application_examples", + params: { + examples: [ + "BLE sensor node with I2C sensors", + "Battery-powered BLE data logger", + "FeatherWing-compatible wireless controller", + ], + source: SOURCE, + }, + }, + { + type: "wireless_soc", + params: { + radios: ["ble_radio"], + note: "No RF feed pin or antenna interface is defined by the source.", + }, + }, + { + // ProtoPart purchaseInfo — no OpenUHD home; preserved verbatim. + type: "purchase_info", + params: { + vendor: "amazon", + link: "https://amzn.to/4nz8Mmg", + isAffiliate: true, + vendorPartId: "B071ZSQDSJ", + currentPriceUSD: "24.95", + availabilityStatus: "in_stock", + priceTimestamp: "2025-11-02T23:25:57.000Z", + }, + }, + ], + + artifacts: [ + { + id: "art_datasheet", + name: "Bluefruit nRF52 Feather Learning Guide", + type: "datasheet", + url: "https://cdn-learn.adafruit.com/downloads/pdf/bluefruit-nrf52-feather-learning-guide.pdf", + }, + { + id: "art_thumbnail", + name: "Thumbnail", + type: "custom", + filePath: "./ProtoPart/protoparts/adafruit-feather-nrf52-bluefruit-le-3406/thumbnail.png", + mimeType: "image/png", + tags: ["image", "thumbnail", "preview"], + description: "Board product photo (source previewArtifactId: art_thumbnail).", + }, + ], + + geometry: { + // The source defines no node_geometry; only the rectangular Feather + // outline (51 x 22.9 mm, mechanical domain) justifies the preset. + outline: { preset: "rectangle" }, + }, +}); diff --git a/library/parts/adafruit-feather-nrf52-bluefruit-le-3406/artifacts/thumbnail.png b/library/parts/adafruit-feather-nrf52-bluefruit-le-3406/artifacts/thumbnail.png new file mode 100644 index 0000000..f63fb9b Binary files /dev/null and b/library/parts/adafruit-feather-nrf52-bluefruit-le-3406/artifacts/thumbnail.png differ diff --git a/library/parts/arduino-nano.ts b/library/parts/arduino-nano.ts new file mode 100644 index 0000000..07eb20d --- /dev/null +++ b/library/parts/arduino-nano.ts @@ -0,0 +1,1075 @@ +/** + * Arduino Nano (A000005) — ProtoPart-honest part definition. + * + * Primary source: ProtoPart definition `protoparts/arduino-nano/definition.json` + * (schema 1.4.0, definition version 0.4.0) — the audited source of truth. + * - electrical domain: 30 resources (pin_count 30), 10 interfaces, + * 6 power domains (VIN, USB 5V, Regulated 5V, Regulated 3.3V, + * Digital I/O, Analog Reference) + * - mechanical domain: 4× M3 mounting holes + PCB mounting interface, + * 45 × 18 × 7 mm, 7 g + * - thermal domain: -40..85 °C, 0.5 W TDP, passive cooling + * - top-level warnings / design_rules / usage_notes / application_examples / + * compatibility_notes / validation_requirements (preserved as module traits) + * Secondary source (cited, not mined for new values): Arduino Nano datasheet, + * https://docs.arduino.cc/resources/datasheets/A000005-datasheet.pdf + * + * Architecture notes honoured by this file: + * - Pin-honest like a schematic: all 30 ProtoPart electrical resources are + * leaf interfaces, in ProtoPart resource order, with the resource id as + * the interface id. The source definition carries NO physical package pin + * designators (unlike the ESP32 gold standard), so `pin` numbers are + * deliberately omitted rather than invented — see the audit report. + * - Every pin carries its verbatim ProtoPart description and function list + * in a `nano_pin_functions` trait — display data is separated from the + * canonical capability tags the matching engine needs. + * - Buses/archetypes are composed interfaces with slots; the fixed + * silicon routings (SDA=A4/SCL=A5, SPI on D10-D13, UART on D0/D1, the + * two power sources and two regulated rails) are captured as profiles. + * - ProtoPart `constraints` blocks (max_connections, + * requires_matching_voltage_domain) and bus flags (exclusive, + * supports_clock_stretching, pullups_on_master) are preserved as traits. + * - Implied passives (`implied_passives` trait): 4.7 kΩ I²C bus pull-ups + * from `recommended_pullup_res_kΩ` on A4/A5 and the design rule "Use + * external pull-up resistors for I2C communication". + * - Mutual exclusions: VIN vs USB power ("USB and VIN power sources are + * mutually exclusive") is an interfaceGroup `one_of` plus a + * co_requirement trait; the three UART views (transceiver/TX-only/ + * RX-only) share the D0/D1 pins and form a `one_of` group. + */ + +import type { + InterfaceDef, + ModuleDef, + SlotDef, + TraitDef, +} from "../../src/types/index.js"; +import type { Parameter } from "../../src/types/parameter.js"; +import { + Ground, + I2C, + Pin, + PowerIn, + PowerOut, + SPI, + UART, + baudRate, + defineModule, + maxCurrentA, + voltageRangeV, + voltageV, +} from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Electrical constants — ProtoPart electrical domain `power_domains` +// --------------------------------------------------------------------------- + +/** Verbatim citation used by every `nano_pin_functions` trait below. */ +const SOURCE = "ProtoPart arduino-nano definition.json (schema 1.4.0)"; + +/** "VIN (Raw Input)": 7-12 V unregulated, nominal 9 V, max 500 mA. */ +const VIN_RANGE: [number, number] = [7, 12]; +/** "USB 5V": 4.5-5.5 V regulated, nominal 5 V, max 500 mA. */ +const USB_RANGE: [number, number] = [4.5, 5.5]; +/** "Digital I/O": 5 V logic, 4.5-5.5 V range, 40 mA per-pin domain limit. */ +const DIGITAL_IO_RANGE: [number, number] = [4.5, 5.5]; +/** + * "Analog Reference": 1.1-5.5 V, nominal 5 V — the ADC reference span the + * ProtoPart definition assigns to A0-A7 and AREF (not a logic-level spec). + */ +const ANALOG_REF_RANGE: [number, number] = [1.1, 5.5]; + +/** ProtoPart `io_drive_strength_mA` on every digital-capable pin. */ +const IO_DRIVE_MA = 40; + +type NanoPowerDomain = "digital_io" | "analog_ref"; + +const DOMAIN_VOLTAGE: Record = { + digital_io: DIGITAL_IO_RANGE, + analog_ref: ANALOG_REF_RANGE, +}; + +/** ProtoPart `has_internal_pullup: true` — no resistance value is given. */ +const INTERNAL_PULLUP_TRAIT: TraitDef = { + type: "internal_pulls", + params: { + pull_up_available: true, + pull_up_resistance_kohm: [20, 50], + note: "Resistance not specified in the source definition (has_internal_pullup: true only; no pull-down is claimed). Resolved from the ATmega328P datasheet, Common DC Characteristics: I/O-pin pull-up resistor Rpu = 20-50 kOhm (the RESET pull-up RRST is 30-60 kOhm).", + source: `${SOURCE}; ATmega328P datasheet (Microchip), Electrical Characteristics — Common DC Characteristics`, + }, +}; + +/** ProtoPart per-interface `constraints` block, preserved verbatim. */ +function connectionConstraints( + maxConnections: number, + requiresMatchingVoltageDomain: boolean, +): TraitDef { + return { + type: "connection_constraints", + params: { + max_connections: maxConnections, + requires_matching_voltage_domain: requiresMatchingVoltageDomain, + source: SOURCE, + }, + }; +} + +// --------------------------------------------------------------------------- +// ProtoPart-honest per-pin metadata +// --------------------------------------------------------------------------- + +interface NanoPinSpec { + /** ProtoPart resource id — kept as the interface id. */ + id: string; + /** Short display name derived from the resource id + functions. */ + name: string; + /** Verbatim ProtoPart resource description. */ + description: string; + /** Verbatim ProtoPart function names. */ + functions: string[]; + domain: NanoPowerDomain; + /** General-purpose digital I/O (default true; A6/A7 are analog-only). */ + digital?: boolean; + analogIn?: boolean; + pwm?: boolean; + interrupt?: boolean; + i2cSda?: boolean; + i2cScl?: boolean; + spiMosi?: boolean; + spiMiso?: boolean; + spiSck?: boolean; + spiSs?: boolean; + uartRx?: boolean; + uartTx?: boolean; + /** D13 carries the ProtoPart "led" function (built-in LED). */ + led?: boolean; + /** ProtoPart `has_internal_pullup`. */ + internalPullup?: boolean; + /** ProtoPart `io_drive_strength_mA`. */ + driveCurrentmA?: number; + traits?: TraitDef[]; +} + +/** Build one schematic-honest header pin from its ProtoPart resource. */ +function nanoPin(spec: NanoPinSpec): InterfaceDef { + const base = Pin({ + id: spec.id, + name: spec.name, + voltageV: DOMAIN_VOLTAGE[spec.domain], + ...(spec.driveCurrentmA !== undefined ? { driveCurrentmA: spec.driveCurrentmA } : {}), + capabilities: { + digital: spec.digital ?? true, + analogIn: spec.analogIn, + pwm: spec.pwm, + interrupt: spec.interrupt, + i2cSda: spec.i2cSda, + i2cScl: spec.i2cScl, + spiMosi: spec.spiMosi, + spiMiso: spec.spiMiso, + spiSck: spec.spiSck, + spiSs: spec.spiSs, + uartRx: spec.uartRx, + uartTx: spec.uartTx, + }, + }); + + const capabilities = [ + ...(base.capabilities ?? []), + ...(spec.led ? ["led"] : []), + ]; + + const traits: TraitDef[] = [ + { + type: "nano_pin_functions", + params: { + source: SOURCE, + description: spec.description, + functions: spec.functions, + }, + }, + { type: "power_domain", params: { domain: spec.domain } }, + ...(spec.internalPullup ? [INTERNAL_PULLUP_TRAIT] : []), + ...(spec.traits ?? []), + ]; + + return { ...base, capabilities, traits }; +} + +// --------------------------------------------------------------------------- +// Digital pins D0-D13 — ProtoPart resources d0..d13, in resource order +// --------------------------------------------------------------------------- + +const DIGITAL_PIN_SPECS: NanoPinSpec[] = [ + { + id: "d0", name: "D0 / RX", description: "Digital pin 0 / UART RX", + functions: ["digital_io", "uart_rx"], domain: "digital_io", + uartRx: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "d1", name: "D1 / TX", description: "Digital pin 1 / UART TX", + functions: ["digital_io", "uart_tx"], domain: "digital_io", + uartTx: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "d2", name: "D2", description: "Digital pin 2 / External interrupt", + functions: ["digital_io", "interrupt"], domain: "digital_io", + interrupt: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "d3", name: "D3", description: "Digital pin 3 / PWM / External interrupt", + functions: ["digital_io", "pwm", "interrupt"], domain: "digital_io", + pwm: true, interrupt: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "d4", name: "D4", description: "Digital pin 4", + functions: ["digital_io"], domain: "digital_io", + internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "d5", name: "D5", description: "Digital pin 5 / PWM", + functions: ["digital_io", "pwm"], domain: "digital_io", + pwm: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "d6", name: "D6", description: "Digital pin 6 / PWM", + functions: ["digital_io", "pwm"], domain: "digital_io", + pwm: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "d7", name: "D7", description: "Digital pin 7", + functions: ["digital_io"], domain: "digital_io", + internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "d8", name: "D8", description: "Digital pin 8", + functions: ["digital_io"], domain: "digital_io", + internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "d9", name: "D9", description: "Digital pin 9 / PWM", + functions: ["digital_io", "pwm"], domain: "digital_io", + pwm: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "d10", name: "D10 / SS", description: "Digital pin 10 / PWM / SPI SS", + functions: ["digital_io", "pwm", "spi_ss"], domain: "digital_io", + pwm: true, spiSs: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "d11", name: "D11 / MOSI", description: "Digital pin 11 / PWM / SPI MOSI", + functions: ["digital_io", "pwm", "spi_mosi"], domain: "digital_io", + pwm: true, spiMosi: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "d12", name: "D12 / MISO", description: "Digital pin 12 / SPI MISO", + functions: ["digital_io", "spi_miso"], domain: "digital_io", + spiMiso: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "d13", name: "D13 / SCK / LED", description: "Digital pin 13 / SPI SCK / Built-in LED", + functions: ["digital_io", "spi_sck", "led"], domain: "digital_io", + spiSck: true, led: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + traits: [ + { type: "onboard_indicator", params: { indicator: "Built-in LED on D13", source: SOURCE } }, + ], + }, +]; + +// --------------------------------------------------------------------------- +// Analog pins A0-A7 — ProtoPart resources a0..a7 (A6/A7 are analog-only) +// --------------------------------------------------------------------------- + +/** 4.7 kΩ pull-up from ProtoPart `recommended_pullup_res_kΩ` on A4/A5. */ +function i2cPullupTrait(signal: "SDA" | "SCL", pinName: string): TraitDef { + return { + type: "implied_passives", + params: { + purpose: "I²C bus pull-up (the master provides none)", + components: [ + { kind: "resistor", value: "4.7 kΩ (recommended)", connection: `${signal} (${pinName}) to the bus supply` }, + ], + source: `${SOURCE}: recommended_pullup_res_kΩ = 4.7; design rule "Use external pull-up resistors for I2C communication"`, + }, + }; +} + +const ANALOG_PIN_SPECS: NanoPinSpec[] = [ + { + id: "a0", name: "A0", description: "Analog pin A0 / Digital I/O", + functions: ["analog_input", "digital_io"], domain: "analog_ref", + analogIn: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "a1", name: "A1", description: "Analog pin A1 / Digital I/O", + functions: ["analog_input", "digital_io"], domain: "analog_ref", + analogIn: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "a2", name: "A2", description: "Analog pin A2 / Digital I/O", + functions: ["analog_input", "digital_io"], domain: "analog_ref", + analogIn: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "a3", name: "A3", description: "Analog pin A3 / Digital I/O", + functions: ["analog_input", "digital_io"], domain: "analog_ref", + analogIn: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + }, + { + id: "a4", name: "A4 / SDA", description: "Analog pin A4 / Digital I/O / I2C SDA", + functions: ["analog_input", "digital_io", "i2c_sda"], domain: "analog_ref", + analogIn: true, i2cSda: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + traits: [i2cPullupTrait("SDA", "A4")], + }, + { + id: "a5", name: "A5 / SCL", description: "Analog pin A5 / Digital I/O / I2C SCL", + functions: ["analog_input", "digital_io", "i2c_scl"], domain: "analog_ref", + analogIn: true, i2cScl: true, internalPullup: true, driveCurrentmA: IO_DRIVE_MA, + traits: [i2cPullupTrait("SCL", "A5")], + }, + { + // A6/A7: analog_input is the only ProtoPart function — no digital I/O, + // no internal pull-up, no drive-strength rating in the source. + id: "a6", name: "A6", description: "Analog pin A6", + functions: ["analog_input"], domain: "analog_ref", + digital: false, analogIn: true, + }, + { + id: "a7", name: "A7", description: "Analog pin A7", + functions: ["analog_input"], domain: "analog_ref", + digital: false, analogIn: true, + }, +]; + +// --------------------------------------------------------------------------- +// Power, reference, and reset pins — ProtoPart resources, in resource order +// --------------------------------------------------------------------------- + +const vinPin: InterfaceDef = { + ...PowerIn({ id: "vin", name: "VIN", voltageV: VIN_RANGE, nominalV: 9, maxCurrentA: 0.5 }), + capabilities: ["power_in", "power_input"], + traits: [ + { + type: "nano_pin_functions", + params: { source: SOURCE, description: "External power input pin", functions: ["power_input"] }, + }, + { + type: "power_domain", + params: { + domain: "vin", + regulation_type: "unregulated", + note: "VIN (Raw Input): 7-12 V, nominal 9 V, max 500 mA, ±10% tolerance, 100 mV ripple, 85% downstream efficiency.", + }, + }, + ], +}; + +const usb5vPin: InterfaceDef = { + ...PowerIn({ id: "usb_5v", name: "USB 5V", voltageV: USB_RANGE, nominalV: 5, maxCurrentA: 0.5 }), + capabilities: ["power_in", "power_input"], + traits: [ + { + type: "nano_pin_functions", + params: { source: SOURCE, description: "USB power input", functions: ["power_input"] }, + }, + { type: "connector", params: { connector_type: "usb" } }, + { + type: "power_domain", + params: { + domain: "usb_5v", + regulation_type: "regulated", + note: "USB-supplied power with built-in protection: 4.5-5.5 V, max 500 mA, ±10% tolerance, 50 mV ripple.", + }, + }, + ], +}; + +const fiveVOutPin: InterfaceDef = { + ...PowerOut({ id: "5v_out", name: "5V", voltageV: 5, maxCurrentA: 0.8 }), + capabilities: ["power_out", "power_output"], + traits: [ + { + type: "nano_pin_functions", + params: { source: SOURCE, description: "5V power output pin", functions: ["power_output"] }, + }, + { type: "power_domain", params: { domain: "regulated_5v" } }, + ], +}; + +const threeV3OutPin: InterfaceDef = { + ...PowerOut({ id: "3v3_out", name: "3V3", voltageV: 3.3, maxCurrentA: 0.05 }), + capabilities: ["power_out", "power_output"], + traits: [ + { + type: "nano_pin_functions", + params: { source: SOURCE, description: "3.3V power output pin", functions: ["power_output"] }, + }, + { + type: "power_domain", + params: { + domain: "regulated_3v3", + note: "Low-power 3.3V output for sensors and low-power devices — max 50 mA. Corrected from ProtoPart's 150 mA: the Arduino Nano user manual (pin 17) states the 3V3 pin is '+3.3V output (from FTDI)', and the FTDI FT232R datasheet rates the 3V3OUT LDO at 50 mA maximum external load.", + }, + }, + ], +}; + +/** + * Two ground pins. The ProtoPart definition associates gnd1 with the + * regulated_5v domain and gnd2 with regulated_3v3 (all domains share + * `ground_reference: "common"`); the association is carried verbatim. + */ +function groundPin(n: 1 | 2, powerDomainId: string): InterfaceDef { + return { + ...Ground({ id: `gnd${n}`, name: "GND" }), + traits: [ + { + type: "nano_pin_functions", + params: { source: SOURCE, description: `Ground pin ${n}`, functions: ["ground"] }, + }, + { type: "power_domain", params: { domain: powerDomainId, ground_reference: "common" } }, + ], + }; +} + +const arefPin: InterfaceDef = { + id: "aref", + name: "AREF", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "analog", roles: ["input"] }], + capabilities: ["analog_reference"], + parameters: [ + voltageRangeV(ANALOG_REF_RANGE[0], ANALOG_REF_RANGE[1]), + { id: "max_current", name: "Analog-reference domain max current", unit: "mA", value: 1 }, + ], + traits: [ + { + type: "nano_pin_functions", + params: { source: SOURCE, description: "Analog reference voltage input", functions: ["analog_reference"] }, + }, + { + type: "power_domain", + params: { + domain: "analog_ref", + note: "Precision reference for ADC measurements: 1.1-5.5 V, ±1% tolerance, max 1 mA.", + }, + }, + ], +}; + +const resetPin: InterfaceDef = { + id: "reset", + name: "RESET", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input"] }], + capabilities: ["reset"], + parameters: [voltageRangeV(DIGITAL_IO_RANGE[0], DIGITAL_IO_RANGE[1])], + traits: [ + { + type: "nano_pin_functions", + params: { source: SOURCE, description: "Reset pin", functions: ["reset"] }, + }, + { type: "power_domain", params: { domain: "digital_io" } }, + INTERNAL_PULLUP_TRAIT, + ], +}; + +/** + * All 30 electrical resources, in ProtoPart resource order (the source + * definition carries no physical package pin numbers to sort by). + */ +const pins: InterfaceDef[] = [ + vinPin, + usb5vPin, + fiveVOutPin, + threeV3OutPin, + groundPin(1, "regulated_5v"), + groundPin(2, "regulated_3v3"), + ...DIGITAL_PIN_SPECS.map(nanoPin), + ...ANALOG_PIN_SPECS.map(nanoPin), + arefPin, + resetPin, +]; + +// --------------------------------------------------------------------------- +// Composed interfaces — ProtoPart electrical `interfaces` +// --------------------------------------------------------------------------- + +function withTraits(iface: InterfaceDef, traits: TraitDef[]): InterfaceDef { + return { ...iface, traits: [...(iface.traits ?? []), ...traits] }; +} + +/** Attach traits to the interface with the given id inside a builder result. */ +function amend(ifaces: InterfaceDef[], id: string, traits: TraitDef[]): InterfaceDef[] { + return ifaces.map((i) => (i.id === id ? withTraits(i, traits) : i)); +} + +function composed(config: { + id: string; + name: string; + protocolType: string; + roles: string[]; + slots: SlotDef[]; + profiles?: InterfaceDef["profiles"]; + parameters?: Parameter[]; + maxInstances?: number; + defaultActive?: boolean; + traits?: TraitDef[]; + domain?: InterfaceDef["domain"]; +}): InterfaceDef { + return { + id: config.id, + name: config.name, + domain: config.domain ?? "electrical", + exposed: true, + default_active: config.defaultActive ?? false, + protocols: [{ type: config.protocolType, roles: config.roles }], + slots: config.slots, + ...(config.profiles ? { profiles: config.profiles } : {}), + ...(config.parameters ? { parameters: config.parameters } : {}), + ...(config.maxInstances !== undefined ? { max_instances: config.maxInstances } : {}), + ...(config.traits ? { traits: config.traits } : {}), + }; +} + +// Power input — one supply + one ground. The two profiles are the two +// power_input resources; VIN and USB are mutually exclusive per the +// ProtoPart design rules. Ground binds to gnd1 as the representative +// ground pin (both grounds share the common reference). +const powerInput = composed({ + id: "power_input", + name: "Power Input", + protocolType: "power", + roles: ["input"], + slots: [ + { id: "supply", required: true, match: { protocol: "power", role: "input", capability: "power_input" } }, + { id: "gnd", required: true, match: { protocol: "power", role: "ground", capability: "ground" } }, + ], + profiles: [ + { id: "power_via_vin", label: "VIN (7-12 V, unregulated)", bindings: { supply: "vin", gnd: "gnd1" } }, + { id: "power_via_usb", label: "USB 5 V", bindings: { supply: "usb_5v", gnd: "gnd1" } }, + ], + maxInstances: 1, + traits: [ + connectionConstraints(1, true), + { + type: "co_requirement", + params: { + with: "vin, usb_5v", + condition: "both power sources present", + effect: "USB and external power should not be used simultaneously — USB and VIN power sources are mutually exclusive.", + source: `${SOURCE}: warnings / design_rules`, + }, + }, + { + type: "usage_restriction", + params: { restriction: "Do not reverse power supply polarity.", source: `${SOURCE}: warnings` }, + }, + ], +}); + +// Power output — the regulated rails. The ProtoPart power_delivery block +// describes the 5 V rail; the 3.3 V rail limit lives on the 3v3_out leaf. +const powerOutput = composed({ + id: "power_output", + name: "Power Output", + protocolType: "power", + roles: ["output"], + parameters: [voltageV(5), maxCurrentA(0.8)], + slots: [ + { id: "rail", required: true, match: { protocol: "power", role: "output", capability: "power_output" } }, + { id: "gnd", required: true, match: { protocol: "power", role: "ground", capability: "ground" } }, + ], + profiles: [ + { id: "rail_5v", label: "Regulated 5 V (≤800 mA)", bindings: { rail: "5v_out", gnd: "gnd1" } }, + { id: "rail_3v3", label: "Regulated 3.3 V (≤50 mA, FT232RL 3V3OUT)", bindings: { rail: "3v3_out", gnd: "gnd2" } }, + ], + maxInstances: 2, // both regulated rails exist concurrently + traits: [ + connectionConstraints(10, false), + { + type: "power_delivery", + params: { + max_current_mA: 800, + max_voltage_V: 5, + regulation_tolerance_percent: 4, + ripple_voltage_mV: 10, + efficiency_percent: 85, + note: "ProtoPart power_delivery block — figures describe the regulated 5 V rail; the 3.3 V rail is limited to 50 mA (FT232RL 3V3OUT — see 3v3_out).", + }, + }, + ], +}); + +// Digital / analog archetype interfaces — ProtoPart declares these as +// generic single-pin interfaces matching any eligible resource, so they +// carry slots but no fixed profiles. Protocol roles are ProtoPart-verbatim. +const digitalOutput = composed({ + id: "digital_output", + name: "Digital Output", + protocolType: "digital", + roles: ["transmitter"], + slots: [ + { id: "pin", required: true, match: { protocol: "digital", role: "output", capability: "digital_io" } }, + ], + traits: [connectionConstraints(1, true)], +}); + +const digitalInput = composed({ + id: "digital_input", + name: "Digital Input", + protocolType: "digital", + roles: ["receiver"], + slots: [ + { id: "pin", required: true, match: { protocol: "digital", role: "input", capability: "digital_io" } }, + ], + traits: [connectionConstraints(1, true)], +}); + +const analogInput = composed({ + id: "analog_input", + name: "Analog Input", + protocolType: "analog", + roles: ["receiver"], + slots: [ + { id: "pin", required: true, match: { protocol: "analog", role: "input", capability: "analog_in" } }, + ], + traits: [ + connectionConstraints(1, true), + { + type: "channels", + params: { count: 8, mapping: "A0-A7 (A6/A7 are analog-only)", source: SOURCE }, + }, + ], +}); + +// I²C master — hardwired to A4 (SDA) / A5 (SCL). 400 kHz max +// (protocol_max_freq_Hz). The master provides no pull-ups. +const i2cMaster = amend( + I2C({ + id: "i2c_master", + name: "I2C Master", + roles: ["master"], + clockFreqHz: 400_000, // ProtoPart protocol_max_freq_Hz (maximum) + maxInstances: 1, + profiles: [{ id: "i2c_a4_a5", label: "SDA = A4, SCL = A5", sda: "a4", scl: "a5" }], + }), + "i2c_master", + [ + { + type: "bus_characteristics", + params: { supports_clock_stretching: true, pullups_on_master: false, exclusive: false, source: SOURCE }, + }, + connectionConstraints(8, true), + { + type: "implied_passives", + params: { + purpose: "I²C bus pull-ups (pullups_on_master: false)", + components: [ + { kind: "resistor", value: "4.7 kΩ (recommended)", connection: "SDA (A4) to the bus supply" }, + { kind: "resistor", value: "4.7 kΩ (recommended)", connection: "SCL (A5) to the bus supply" }, + ], + source: `${SOURCE}: recommended_pullup_res_kΩ on A4/A5; design rule "Use external pull-up resistors for I2C communication"`, + }, + }, + ], +); + +// SPI master — hardwired to D11/D12/D13 with default SS on D10. 8 MHz max. +const spiMaster = amend( + SPI({ + id: "spi_master", + name: "SPI Master", + roles: ["master"], + clockFreqHz: 8_000_000, // ProtoPart protocol_max_freq_Hz (maximum) + maxInstances: 1, + profiles: [ + { id: "spi_d10_d13", label: "MOSI = D11, MISO = D12, SCK = D13, SS = D10", mosi: "d11", miso: "d12", sck: "d13", ss: "d10" }, + ], + }), + "spi_master", + [ + { type: "bus_characteristics", params: { exclusive: false, source: SOURCE } }, + connectionConstraints(8, true), + ], +); + +// UART — ProtoPart declares three views of the single D0/D1 port: +// a full transceiver plus TX-only and RX-only interfaces. The transceiver +// uses the canonical UART builder; its ProtoPart-verbatim role list +// ("transceiver"/"transmitter"/"receiver") is preserved as a trait since +// the builder normalises the port roles to host/device. +const uartTransceiver = amend( + UART({ + id: "uart_transceiver", + name: "UART Transceiver", + // Corrected 115 200 → 2 000 000: the ATmega328P USART reaches 2 Mbps at + // fosc = 16 MHz with U2Xn = 1 (ATmega328P datasheet, USART "Examples of + // Baud Rate Setting" tables). ProtoPart's protocol_max_freq_Hz of 115200 + // was a practical default, not the silicon maximum. + baudRate: 2_000_000, + maxInstances: 1, + profiles: [{ id: "uart_d0_d1", label: "RX = D0, TX = D1", rx: "d0", tx: "d1" }], + }), + "uart_transceiver", + [ + connectionConstraints(1, true), + { + type: "protopart_roles", + params: { + roles: ["transceiver", "transmitter", "receiver"], + note: "ProtoPart-declared protocol roles, verbatim; the OpenUHD UART builder normalises the port to host/device.", + }, + }, + ], +); + +const uartTransmitter = composed({ + id: "uart_transmitter", + name: "UART Transmitter", + protocolType: "uart", + roles: ["transmitter"], + // 2 Mbps max at 16 MHz, U2Xn = 1 — ATmega328P datasheet baud-rate tables. + parameters: [{ ...baudRate(2_000_000), name: "Max baud rate" }], + slots: [ + { id: "tx", required: true, match: { protocol: "uart", role: "transmitter", capability: "uart_tx" } }, + ], + profiles: [{ id: "uart_tx_d1", label: "TX = D1", bindings: { tx: "d1" } }], + maxInstances: 1, + traits: [connectionConstraints(1, true)], +}); + +const uartReceiver = composed({ + id: "uart_receiver", + name: "UART Receiver", + protocolType: "uart", + roles: ["receiver"], + // 2 Mbps max at 16 MHz, U2Xn = 1 — ATmega328P datasheet baud-rate tables. + parameters: [{ ...baudRate(2_000_000), name: "Max baud rate" }], + slots: [ + { id: "rx", required: true, match: { protocol: "uart", role: "receiver", capability: "uart_rx" } }, + ], + profiles: [{ id: "uart_rx_d0", label: "RX = D0", bindings: { rx: "d0" } }], + maxInstances: 1, + traits: [connectionConstraints(1, true)], +}); + +// --------------------------------------------------------------------------- +// Mechanical — 4× M3 corner mounting holes + PCB mounting interface +// --------------------------------------------------------------------------- + +/** + * Hole positions come from the mechanical domain's `mount_holes` table; + * the descriptions pair with coordinates by corner (origin at the + * top-left of the board outline). + */ +function mountHole(n: 1 | 2 | 3 | 4, description: string, x: number, y: number): InterfaceDef { + return { + id: `mount_hole_${n}`, + name: description, + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "threaded_connection", roles: ["mounting_point"] }], + capabilities: ["mounting_hole"], + parameters: [ + { id: "hole_diameter", unit: "mm", value: 3.2 }, + { id: "max_force", unit: "N", value: 50 }, + ], + traits: [ + { type: "fastener", params: { thread_spec: "M3", connector_type: "through_hole" } }, + { type: "position", params: { x_mm: x, y_mm: y, source: `${SOURCE}: mechanical mount_holes table` } }, + ], + }; +} + +const mountHoles: InterfaceDef[] = [ + mountHole(1, "Top-left mounting hole", 1.27, 1.27), + mountHole(2, "Top-right mounting hole", 43.18, 1.27), + mountHole(3, "Bottom-left mounting hole", 1.27, 16.51), + mountHole(4, "Bottom-right mounting hole", 43.18, 16.51), +]; + +const pcbMounting = composed({ + id: "pcb_mounting", + name: "PCB Mounting", + domain: "mechanical", + protocolType: "threaded_connection", + roles: ["mounting_point"], + parameters: [ + { id: "installation_torque", unit: "Nm", value: 0.3 }, + { id: "working_load", unit: "N", value: 200 }, + ], + slots: [{ id: "hole", required: true, count: 4, match: { capability: "mounting_hole" } }], + profiles: [ + { + id: "pcb_mounting_corners", + label: "PCB mounting interface using 4 corner holes", + bindings: { hole: ["mount_hole_1", "mount_hole_2", "mount_hole_3", "mount_hole_4"] }, + }, + ], + maxInstances: 1, + traits: [{ type: "connection_type", params: { type: "removable" } }], +}); + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const ARDUINO_NANO: ModuleDef = defineModule({ + id: "arduino-nano", + name: "Arduino Nano", + version: "1.0.0", + manufacturer: "Arduino", + part_number: "A000005", + description: + "Arduino Nano microcontroller board with USB connectivity, based on ATmega328P. Compact form factor ideal for embedded projects requiring a small footprint with full Arduino compatibility.", + tags: ["mcu", "board", "5v", "usb", "atmega328p", "arduino", "microcontroller", "embedded"], + categories: ["microcontroller.arduino"], + + interfaces: [ + // All 30 electrical resources, in ProtoPart resource order. + ...pins, + + // Power interfaces + powerInput, + powerOutput, + + // Digital / analog archetypes + digitalOutput, + digitalInput, + analogInput, + + // Serial / bus controllers + ...i2cMaster, + ...spiMaster, + ...uartTransceiver, + uartTransmitter, + uartReceiver, + + // Mechanical + ...mountHoles, + pcbMounting, + ], + + interfaceGroups: [ + { + id: "power_sources", + label: "Power Sources (mutually exclusive)", + members: ["vin", "usb_5v"], + policy: "one_of", + }, + { + id: "ground_pins", + label: "Ground Pins (common reference)", + members: ["gnd1", "gnd2"], + policy: "any_of", + }, + { + // All three UART views bind the same D0/D1 pins. + id: "uart_port_views", + label: "UART Port Views (one D0/D1 port)", + members: ["uart_transceiver", "uart_transmitter", "uart_receiver"], + policy: "one_of", + }, + { + id: "mounting_holes", + label: "M3 Corner Mounting Holes", + members: ["mount_hole_1", "mount_hole_2", "mount_hole_3", "mount_hole_4"], + policy: "all_of", + }, + ], + + requirements: [ + { + type: "power", + description: + "External power: 7-12 V on VIN (unregulated, max 500 mA domain rating). 'VIN must be 7-12V when using external power.' Mutually exclusive with USB power.", + voltage_V: [7, 12], + current_mA: 500, + }, + { + type: "power", + description: + "Alternative: 5 V via USB (4.5-5.5 V, max 500 mA, built-in protection). 'Do not exceed 5.5V on any pin when powered by USB.'", + voltage_V: [4.5, 5.5], + current_mA: 500, + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + { id: "vin", name: "VIN (Raw Input)", nominal_voltage_V: 9, voltage_range_V: VIN_RANGE, max_current_mA: 500, regulation_type: "unregulated" }, + { id: "usb_5v", name: "USB 5V", nominal_voltage_V: 5, voltage_range_V: USB_RANGE, max_current_mA: 500, regulation_type: "regulated" }, + { id: "regulated_5v", name: "Regulated 5V", nominal_voltage_V: 5, voltage_range_V: [4.8, 5.2], max_current_mA: 800, regulation_type: "regulated" }, + // max_current_mA corrected 150 → 50: 3V3 is the FT232RL's 3V3OUT LDO + // (Arduino Nano user manual, pin 17: "+3.3V output (from FTDI)"; + // FTDI FT232R datasheet: 3V3OUT sources up to 50 mA). + { id: "regulated_3v3", name: "Regulated 3.3V", nominal_voltage_V: 3.3, voltage_range_V: [3.1, 3.5], max_current_mA: 50, regulation_type: "regulated" }, + { id: "digital_io", name: "Digital I/O", nominal_voltage_V: 5, voltage_range_V: DIGITAL_IO_RANGE, max_current_mA: 40, regulation_type: "regulated" }, + { id: "analog_ref", name: "Analog Reference", nominal_voltage_V: 5, voltage_range_V: ANALOG_REF_RANGE, max_current_mA: 1, regulation_type: "regulated" }, + ], + metadata: { + pin_count: 30, + supply_voltage_V: VIN_RANGE, + power_consumption_mW: 500, + package_type: "PCB Module", + max_operating_freq_Hz: 16_000_000, + supports_usb: true, + supports_hot_plug: true, + safety_standards: ["UL", "CSA"], + emc_compliance: ["CE", "FCC"], + io_limits: { + per_pin_max_mA: 40, + total_io_max_mA: 200, + max_pin_voltage_on_usb_V: 5.5, + source: `${SOURCE}: design_rules / warnings`, + }, + // PowerDomainDef has no home for these ProtoPart fields — preserved here. + power_domain_details: { + vin: { description: "External power input through barrel jack or VIN pin", isolation_type: "non_isolated", ground_reference: "common", voltage_tolerance_percent: 10, voltage_ripple_mV: 100, efficiency_percent: 85, compatible_domains: [] }, + usb_5v: { description: "USB-supplied power with built-in protection", isolation_type: "non_isolated", ground_reference: "common", voltage_tolerance_percent: 10, voltage_ripple_mV: 50, efficiency_percent: 90, compatible_domains: [] }, + regulated_5v: { description: "Regulated 5V output for powering external components", isolation_type: "non_isolated", ground_reference: "common", voltage_tolerance_percent: 4, voltage_ripple_mV: 10, efficiency_percent: 85, compatible_domains: ["usb_5v"] }, + regulated_3v3: { description: "Low-power 3.3V output for sensors and low-power devices", isolation_type: "non_isolated", ground_reference: "common", voltage_tolerance_percent: 6, voltage_ripple_mV: 5, efficiency_percent: 80, compatible_domains: ["regulated_5v"] }, + digital_io: { description: "5V logic level for digital I/O pins", isolation_type: "non_isolated", ground_reference: "common", voltage_tolerance_percent: 10, compatible_domains: ["regulated_5v"] }, + analog_ref: { description: "Precision reference for ADC measurements", isolation_type: "non_isolated", ground_reference: "common", voltage_tolerance_percent: 1, compatible_domains: ["regulated_5v"] }, + }, + }, + }, + { + domain: "mechanical", + dimensions_mm: { length: 45, width: 18, height: 7 }, + weight_g: 7, + metadata: { + package_type: "PCB Module", + enclosure_type: "open_pcb", + mounting_method: "through_hole", + field_serviceable: true, + assembly_time_min: 5, + mount_holes: [ + { x: 1.27, y: 1.27, diam_mm: 3.2 }, + { x: 43.18, y: 1.27, diam_mm: 3.2 }, + { x: 1.27, y: 16.51, diam_mm: 3.2 }, + { x: 43.18, y: 16.51, diam_mm: 3.2 }, + ], + }, + }, + { + domain: "thermal", + operating_temperature_C: [-40, 85], + metadata: { + cooling_method: "passive", + thermal_design_power_W: 0.5, + requires_thermal_management: false, + thermal_monitoring_available: false, + }, + }, + ], + + traits: [ + { + type: "io_current_limits", + params: { + per_pin_max_mA: 40, + total_io_max_mA: 200, + source: `${SOURCE}: design_rules ("Maximum 40mA per I/O pin", "Maximum 200mA total I/O current")`, + }, + }, + { + // ProtoPart top-level fields with no structural OpenUHD home, + // preserved verbatim. + type: "warnings", + params: { + items: [ + "Do not exceed 40mA per I/O pin", + "Do not reverse power supply polarity", + "Ensure proper grounding for all connections", + "USB and external power should not be used simultaneously", + "Avoid exposing to high humidity or corrosive environments", + "Maximum operating temperature is 85°C", + ], + }, + }, + { + type: "design_rules", + params: { + rules: [ + "Maximum 40mA per I/O pin", + "Maximum 200mA total I/O current", + "VIN must be 7-12V when using external power", + "USB and VIN power sources are mutually exclusive", + "Do not exceed 5.5V on any pin when powered by USB", + "Use external pull-up resistors for I2C communication", + "Ensure proper decoupling capacitors for stable operation", + ], + }, + }, + { + type: "usage_notes", + params: { + note: "Arduino Nano is a compact microcontroller board perfect for embedded projects requiring a small footprint. Excellent for prototyping and small-scale production. Compatible with most Arduino shields and libraries. Can be powered via USB or external VIN, with onboard regulator providing 5V and 3.3V outputs.", + }, + }, + { + type: "compatibility_notes", + params: { + note: "Compatible with 5V and 3.3V sensors and actuators. Works with most Arduino libraries and shields. Can be programmed via USB or ISP. Compatible with Arduino IDE and other development environments.", + }, + }, + { + type: "application_examples", + params: { + examples: [ + "IoT sensor nodes", + "Wearable electronics", + "Small robotics projects", + "Educational electronics", + "Prototype development", + "Embedded control systems", + ], + }, + }, + { + type: "validation_requirements", + params: { + items: [ + "Check power supply compatibility", + "Verify I/O voltage levels", + "Validate current limits", + "Check communication protocol compatibility", + "Ensure proper grounding", + "Verify analog reference voltage", + ], + }, + }, + ], + + artifacts: [ + { + id: "art_datasheet", + name: "Arduino Nano (A000005) Datasheet", + type: "datasheet", + url: "https://docs.arduino.cc/resources/datasheets/A000005-datasheet.pdf", + }, + { + id: "art_thumbnail", + name: "Arduino Nano Thumbnail", + type: "custom", + filePath: "./ProtoPart/protoparts/arduino-nano/thumbnail.png", + mimeType: "image/png", + tags: ["image", "thumbnail"], + }, + ], + + geometry: { + xScale: 1, + yScale: 1, + outline: { preset: "rectangle" }, + }, +}); diff --git a/library/parts/arduino-nano/artifacts/thumbnail.png b/library/parts/arduino-nano/artifacts/thumbnail.png new file mode 100644 index 0000000..23b5feb Binary files /dev/null and b/library/parts/arduino-nano/artifacts/thumbnail.png differ diff --git a/library/parts/attiny85.ts b/library/parts/attiny85.ts new file mode 100644 index 0000000..e7b8875 --- /dev/null +++ b/library/parts/attiny85.ts @@ -0,0 +1,1612 @@ +/** + * Microchip (Atmel) ATtiny85-20PU — datasheet-honest part definition. + * + * Primary sources: + * - ATtiny25/45/85 Datasheet (Atmel-2586) — pin configuration, alternate + * port functions, fuse behaviour (RSTDISBL/DWEN/CKSEL/CKOUT), DC + * characteristics, absolute maximum ratings. + * - ProtoPart `attiny85` definition (audited source of truth): per-pin + * function lists, logic thresholds, current ratings, design rules, + * usage notes, and warnings are carried over verbatim from it. Values + * not present there are omitted, not invented. + * + * Architecture notes honoured by this file: + * - Pin-honest like a schematic: all 8 physical PDIP-8 pins are leaf + * interfaces, in package order, with ids "pin_N" and the datasheet + * port/pin name as the displayed name (PB5, PB3, PB4, GND, PB0, PB1, + * PB2, VCC). + * - Every pin carries its verbatim ProtoPart function list (name, + * direction, signal class, description) in an `attiny85_pin_functions` + * trait — display data is separated from the canonical capability tags + * the matching engine needs. + * - Instances vs combinations: the ATtiny85 has no pin matrix — every + * peripheral signal is fixed to exactly one pad, so each controller + * exposes a single fixed profile and `max_instances: 1`. + * - Co-requirements (`co_requirement` traits): programming the RSTDISBL + * fuse frees PB5 (GPIO/ADC0/PCINT5) but permanently disables ISP + * programming; the USI is shared between SPI (three-wire) and I2C + * (two-wire) modes — only one protocol at a time; a crystal on + * XTAL1/XTAL2 commits PB3 and PB4 entirely; CLKI external-clock mode is + * mutually exclusive with XTAL1 crystal mode; ISP reuses the USI pins. + * - Implied harness connections (`implied_passives` traits): 100 nF VCC + * decoupling, 10 kΩ RESET pull-up, USI-I2C bus pull-ups + * (4.7 kΩ @ 100 kHz / 2.2 kΩ @ 400 kHz), crystal load capacitors. + * - Shareability exemptions (`net_shareable` traits): single VCC and GND + * pins are shared rails. + * - ProtoPart node-layout preservation: the ProtoPart definition places + * the 8 electrical pins at positions 1/2/5/8/11/12/14/15 of a 20-position + * node layout, with 12 "DNC" footprint-only mounting positions. Physical + * PDIP-8 pin numbers (1-8, confirmed by the ProtoPart design rules: + * "VCC (pin 8) and GND (pin 4)", "RESET pin (PB5, pin 1)") are used for + * the `pin` designators; the node-layout positions are preserved in + * `protopart_node_position` traits and the DNC positions in the `dnc` + * mechanical interface. + */ + +import type { + InterfaceDef, + ModuleDef, + SlotDef, + TraitDef, +} from "../../src/types/index.js"; +import type { Parameter } from "../../src/types/parameter.js"; +import { + Ground, + Pin, + PowerIn, + SPI, + I2C, + defineModule, + clockFreqHz, + resolutionBits, +} from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Electrical constants — ProtoPart power domain / absolute maximum ratings +// --------------------------------------------------------------------------- + +/** 2.7-5.5 V for operation up to 10 MHz (ATTINY85-20PU industrial variant). */ +const VCC_RANGE: [number, number] = [2.7, 5.5]; +/** 4.5-5.5 V required for the 20 MHz speed grade. */ +const VCC_RANGE_20MHZ: [number, number] = [4.5, 5.5]; +/** Per-pin continuous source/sink current limit (absolute max). */ +const PIN_CURRENT_MA = 40; +/** Total continuous current through the VCC or GND pin (absolute max). */ +const VCC_GND_CURRENT_MA = 200; + +/** + * Standard GPIO logic thresholds at VCC = 5 V (ProtoPart per-pin + * logic_levels): V_IH min = 0.6*VCC, V_IL max = 0.3*VCC; V_OH min = 4.3 V at + * 10 mA source, V_OL max = 0.6 V at 10 mA sink. + */ +const STD_LOGIC_LEVELS_TRAIT: TraitDef = { + type: "logic_levels", + params: { + v_ih_min_V: 3.0, + v_il_max_V: 1.5, + v_oh_min_V: 4.3, + v_ol_max_V: 0.6, + conditions: + "At VCC = 5 V: V_IH min = 0.6*VCC, V_IL max = 0.3*VCC; V_OH min at 10 mA source, V_OL max at 10 mA sink.", + source: "ATtiny25/45/85 Datasheet (Atmel-2586), DC characteristics", + }, +}; + +/** + * PB5 in its default RESET role uses tighter thresholds than regular GPIO: + * V_IH min = 0.9*VCC, V_IL max = 0.2*VCC (values shown at VCC = 5 V). + */ +const RESET_LOGIC_LEVELS_TRAIT: TraitDef = { + type: "logic_levels", + params: { + v_ih_min_V: 4.5, + v_il_max_V: 1.0, + conditions: + "RESET function thresholds at VCC = 5 V: V_IH min = 0.9*VCC, V_IL max = 0.2*VCC — tighter than regular GPIO.", + source: "ATtiny25/45/85 Datasheet (Atmel-2586), DC characteristics", + }, +}; + +// --------------------------------------------------------------------------- +// Datasheet-honest per-pin function metadata +// --------------------------------------------------------------------------- + +/** Verbatim per-pin function row from the ProtoPart resource list. */ +interface Attiny85Function { + /** Signal/function name exactly as recorded in the ProtoPart definition. */ + name: string; + /** ProtoPart direction vocabulary: sink = input, source = output. */ + direction?: "sink" | "source" | "bidirectional"; + signal_class?: "data" | "clock" | "sense" | "power" | "ground"; + description: string; +} + +interface Attiny85PinSpec { + /** Physical PDIP-8 package pin number. */ + pin: number; + /** Datasheet port pin name — used as the displayed interface name. */ + name: string; + /** Position of this pin in the ProtoPart 20-position node layout. */ + nodePosition: number; + /** ProtoPart resource description (the slash-separated function summary). */ + summary: string; + functions: Attiny85Function[]; + /** Timer compare-match output present (OC0x/OC1x). */ + pwm?: boolean; + /** Any analog input function (ADC channel, comparator input, AREF). */ + analogIn?: boolean; + i2cSda?: boolean; + i2cScl?: boolean; + spiMosi?: boolean; + spiMiso?: boolean; + spiSck?: boolean; + /** Capability tags for fixed peripheral signals (slot matching). */ + extraCaps?: string[]; + /** PB5 uses the RESET thresholds instead of the standard GPIO ones. */ + resetLogic?: boolean; + /** Extra pin-specific traits appended after the shared ones. */ + traits?: TraitDef[]; + /** Verbatim ProtoPart per-pin notes. */ + notes: string; +} + +function unique(values: string[]): string[] { + return [...new Set(values)]; +} + +/** Build one schematic-honest port pin from its ProtoPart resource row. */ +function attiny85PortPin(spec: Attiny85PinSpec): InterfaceDef { + const base = Pin({ + id: `pin_${spec.pin}`, + name: spec.name, + pin: spec.pin, + voltageV: VCC_RANGE, + capabilities: { + // Every PB pin is a PCINT source (pin-change interrupt). + interrupt: true, + pwm: spec.pwm, + analogIn: spec.analogIn, + i2cSda: spec.i2cSda, + i2cScl: spec.i2cScl, + spiMosi: spec.spiMosi, + spiMiso: spec.spiMiso, + spiSck: spec.spiSck, + }, + }); + + const capabilities = unique([ + ...(base.capabilities ?? []), + "pcint", + ...(spec.extraCaps ?? []), + ]); + + const parameters: Parameter[] = [ + ...(base.parameters ?? []), + { id: "source_current", name: "Max continuous source current", unit: "mA", value: PIN_CURRENT_MA }, + { id: "sink_current", name: "Max continuous sink current", unit: "mA", value: PIN_CURRENT_MA }, + ]; + + const traits: TraitDef[] = [ + { + type: "attiny85_pin_functions", + params: { + source: "ATtiny25/45/85 Datasheet (Atmel-2586), Pin Configurations / Alternate Port Functions; ProtoPart attiny85 definition", + summary: spec.summary, + functions: spec.functions, + note: spec.notes, + }, + }, + { type: "power_domain", params: { domain: "vcc" } }, + { + type: "internal_pulls", + params: { + pull_up: true, + pull_down: false, + note: "Software-selectable internal pull-up; no internal pull-down (ProtoPart has_internal_pullup / has_internal_pulldown).", + }, + }, + spec.resetLogic ? RESET_LOGIC_LEVELS_TRAIT : STD_LOGIC_LEVELS_TRAIT, + { + type: "protopart_node_position", + params: { + position: spec.nodePosition, + note: "Position of this pad in the ProtoPart 20-position node layout; the physical PDIP-8 pin number is the `pin` designator.", + }, + }, + ...(spec.traits ?? []), + ]; + + return { ...base, capabilities, parameters, traits }; +} + +// --------------------------------------------------------------------------- +// Port pins PB0-PB5 — ProtoPart electrical resources, in PDIP-8 package order +// --------------------------------------------------------------------------- + +const PORT_PIN_SPECS: Attiny85PinSpec[] = [ + { + pin: 1, name: "PB5", nodePosition: 1, + summary: "PCINT5 / !RESET / ADC0 / dW", + analogIn: true, + extraCaps: ["adc_in", "ext_reset", "debugwire"], + resetLogic: true, + functions: [ + { + name: "Pin Change Interrupt", direction: "sink", signal_class: "data", + description: "PCINT5 pin-change interrupt source. Only available when the RSTDISBL fuse repurposes the pin as PB5; latches on any logic-level transition on the pin.", + }, + { + name: "Reset", direction: "sink", signal_class: "data", + description: "Active-low external reset input (default function). Holds the AVR core in reset while driven low; minimum recognised pulse width is 2.5 us. Internal pull-up is enabled at power-up.", + }, + { + name: "ADC", direction: "sink", signal_class: "sense", + description: "ADC0 single-ended 10-bit analog input channel. Selectable only after the RSTDISBL fuse turns the pin into general-purpose PB5.", + }, + { + name: "Debug", direction: "bidirectional", signal_class: "data", + description: "debugWIRE single-wire on-chip debug interface, multiplexed on the RESET pin when the DWEN fuse is programmed. Replaces conventional ISP behaviour.", + }, + ], + traits: [ + { + type: "usage_restriction", + params: { + restriction: + "RESET is the default function. Programming the RSTDISBL fuse to free PB5 as GPIO/ADC0/PCINT5 permanently disables ISP programming — recovery requires a high-voltage serial programmer.", + exemption: + "Free as GPIO/ADC0/PCINT5 once RSTDISBL is programmed and the design accepts high-voltage-serial-only reprogramming.", + }, + }, + { + type: "high_voltage_tolerance", + params: { + note: "The high-voltage programming mode tolerates up to +13 V on this pin only (absolute max on all other pins is VCC + 0.5 V).", + max_voltage_V: 13, + }, + }, + ], + notes: + "RESET is the default function and uses a tighter threshold than regular GPIO: V_IH min = 0.9*VCC, V_IL max = 0.2*VCC (values shown at VCC=5 V). Programming the RSTDISBL fuse to free PB5 as GPIO/ADC0/PCINT5 permanently disables ISP programming — recovery requires a high-voltage serial programmer. The high-voltage programming mode tolerates up to +13 V on this pin only.", + }, + { + pin: 2, name: "PB3", nodePosition: 2, + summary: "PCINT3 / XTAL1 / CLKI / !OC1B / ADC3", + pwm: true, analogIn: true, + extraCaps: ["adc_in", "xtal1", "clki", "oc1b_n"], + functions: [ + { + name: "Pin Change Interrupt", direction: "sink", signal_class: "data", + description: "PCINT3 pin-change interrupt source. Fires on any logic-level transition on PB3.", + }, + { + name: "XTAL1", direction: "sink", signal_class: "clock", + description: "Crystal oscillator input. Pairs with XTAL2 on PB4 to drive an external 0.4-20 MHz crystal/resonator (or a 32.768 kHz watch crystal in low-frequency mode) when the CKSEL fuses select a crystal source (datasheet Tables 6-1 and 6-12). Pin is unavailable for other duties while the crystal oscillator is enabled.", + }, + { + name: "CLKI", direction: "sink", signal_class: "clock", + description: "External clock input. Routes a single-ended logic-level clock straight into the AVR core when the CKSEL fuses select the external-clock source. Mutually exclusive with XTAL1 crystal mode.", + }, + { + name: "!PWM", direction: "source", signal_class: "data", + description: "Timer1 compare-match output OC1B (inverted). Capable of high-speed PWM up to ~250 kHz at 8-bit resolution when fed from the internal 64 MHz PLL clock.", + }, + { + name: "ADC", direction: "sink", signal_class: "sense", + description: "ADC3 single-ended 10-bit analog input channel.", + }, + ], + notes: + "Standard GPIO thresholds at VCC=5 V: V_IH min = 0.6*VCC, V_IL max = 0.3*VCC; V_OH min = 4.3 V at 10 mA source, V_OL max = 0.6 V at 10 mA sink. When PB3 is committed to XTAL1/CLKI the pin cannot also act as a GPIO, PWM, or ADC channel — the crystal load capacitors occupy the net.", + }, + { + pin: 3, name: "PB4", nodePosition: 5, + summary: "PCINT4 / XTAL2 / CLKO / OC1B / ADC2", + pwm: true, analogIn: true, + extraCaps: ["adc_in", "xtal2", "clko", "oc1b"], + functions: [ + { + name: "Pin Change Interrupt", direction: "sink", signal_class: "data", + description: "PCINT4 pin-change interrupt source. Fires on any logic-level transition on PB4.", + }, + { + name: "XTAL2", direction: "source", signal_class: "clock", + description: "Crystal oscillator output. Pairs with XTAL1 on PB3 to drive an external crystal/resonator when the CKSEL fuses select a crystal source.", + }, + { + name: "CLKO", direction: "source", signal_class: "clock", + description: "Buffered system clock output. Enabled by programming the CKOUT fuse; outputs the divided system clock so downstream devices can be slaved to the AVR.", + }, + { + name: "PWM", direction: "source", signal_class: "data", + description: "Timer1 compare-match output OC1B. Capable of high-speed PWM up to ~250 kHz at 8-bit resolution when fed from the internal 64 MHz PLL clock.", + }, + { + name: "ADC", direction: "sink", signal_class: "sense", + description: "ADC2 single-ended 10-bit analog input channel.", + }, + ], + notes: + "Standard GPIO thresholds at VCC=5 V: V_IH min = 0.6*VCC, V_IL max = 0.3*VCC; V_OH min = 4.3 V at 10 mA source, V_OL max = 0.6 V at 10 mA sink. When PB4 is committed to XTAL2 the pin cannot also act as GPIO, PWM, or ADC.", + }, + { + pin: 5, name: "PB0", nodePosition: 11, + summary: "MOSI / DI / SDA / AIN0 / OC0A / !OC1A / AREF / PCINT0", + pwm: true, analogIn: true, i2cSda: true, spiMosi: true, + extraCaps: ["usi_di", "ain0", "aref", "oc0a", "oc1a_n"], + functions: [ + { + name: "MOSI", direction: "bidirectional", signal_class: "data", + description: "SPI master-out/slave-in data line used by AVR ISP programmers (slave role on this device, sinking writes; mastered out when USI is configured as SPI master).", + }, + { + name: "DI", direction: "sink", signal_class: "data", + description: "USI serial data input. Sampled in three-wire (SPI-like) and two-wire (I2C-like) USI modes.", + }, + { + name: "SDA", direction: "bidirectional", signal_class: "data", + description: "USI two-wire (I2C-compatible) data line. Open-drain when driven low; idle state must be pulled high externally.", + }, + { + name: "AIN0", direction: "sink", signal_class: "sense", + description: "Analog comparator positive input.", + }, + { + name: "PWM", direction: "source", signal_class: "data", + description: "Timer0 compare-match output OC0A — 8-bit PWM up to ~62 kHz at the typical 16 MHz system clock.", + }, + { + name: "!PWM", direction: "source", signal_class: "data", + description: "Timer1 compare-match output OC1A (inverted). High-speed PWM up to ~250 kHz when the timer is fed from the 64 MHz PLL clock.", + }, + { + name: "AREF", direction: "sink", signal_class: "sense", + description: "External ADC reference voltage input. Selected by the REFS bits in ADMUX; external reference range is 2.0 V to VCC for single-ended channels (datasheet Table 21-8).", + }, + { + name: "Pin Change Interrupt", direction: "sink", signal_class: "data", + description: "PCINT0 pin-change interrupt source. Fires on any logic-level transition on PB0.", + }, + ], + notes: + "Standard GPIO thresholds at VCC=5 V: V_IH min = 0.6*VCC, V_IL max = 0.3*VCC; V_OH min = 4.3 V at 10 mA source, V_OL max = 0.6 V at 10 mA sink. USI SDA on this pin is open-drain and needs an external pull-up (typically 4.7 kΩ at 100 kHz or 2.2 kΩ at 400 kHz). The pin also doubles as the AREF input and as the ISP MOSI line — isolate during programming if it is also driving a peripheral net.", + }, + { + pin: 6, name: "PB1", nodePosition: 12, + summary: "MISO / DO / AIN1 / OC0B / OC1A / PCINT1", + pwm: true, analogIn: true, spiMiso: true, + extraCaps: ["usi_do", "ain1", "oc0b", "oc1a"], + functions: [ + { + name: "MISO", direction: "bidirectional", signal_class: "data", + description: "SPI master-in/slave-out data line used by AVR ISP programmers (source role on this device when reading flash; sink when USI is configured as SPI master).", + }, + { + name: "DO", direction: "source", signal_class: "data", + description: "USI serial data output. Driven in three-wire (SPI-like) USI modes.", + }, + { + name: "AIN1", direction: "sink", signal_class: "sense", + description: "Analog comparator negative input.", + }, + { + name: "PWM", direction: "source", signal_class: "data", + description: "Timer0 compare-match output OC0B (8-bit, up to ~62 kHz) and Timer1 compare-match output OC1A — high-speed PWM up to ~250 kHz when Timer1 is fed from the 64 MHz PLL clock.", + }, + { + name: "Pin Change Interrupt", direction: "sink", signal_class: "data", + description: "PCINT1 pin-change interrupt source. Fires on any logic-level transition on PB1.", + }, + ], + traits: [ + { + type: "usage_note", + params: { + note: "Neither PB1 nor PB0 carries an ADC channel — ADC0-ADC3 live on PB5/PB2/PB4/PB3 (datasheet Table 17-4). Assign PB1 to digital/USI/PWM duties when possible and keep PB2/PB3/PB4 free for ADC.", + }, + }, + ], + notes: + "Standard GPIO thresholds at VCC=5 V: V_IH min = 0.6*VCC, V_IL max = 0.3*VCC; V_OH min = 4.3 V at 10 mA source, V_OL max = 0.6 V at 10 mA sink. Neither PB1 nor PB0 carries an ADC channel — ADC0-ADC3 live on PB5/PB2/PB4/PB3 (datasheet Table 17-4). Assign PB1 to digital/USI/PWM duties when possible and keep PB2/PB3/PB4 free for ADC.", + }, + { + pin: 7, name: "PB2", nodePosition: 14, + summary: "SCK / USCK / SCL / ADC1 / T0 / INT0 / PCINT2", + analogIn: true, i2cScl: true, spiSck: true, + extraCaps: ["adc_in", "usi_usck", "t0", "int0"], + functions: [ + { + name: "SCK", direction: "bidirectional", signal_class: "clock", + description: "SPI serial clock line. Sourced by the AVR ISP programmer during programming; sourced or sunk by USI depending on master/slave configuration.", + }, + { + name: "USCK", direction: "bidirectional", signal_class: "clock", + description: "USI serial clock. Driven when USI is configured as master; sampled when USI is slave.", + }, + { + name: "SCL", direction: "bidirectional", signal_class: "clock", + description: "USI two-wire (I2C-compatible) clock line. Open-drain when driven low; idle state must be pulled high externally.", + }, + { + name: "ADC", direction: "sink", signal_class: "sense", + description: "ADC1 single-ended 10-bit analog input channel.", + }, + { + name: "T0", direction: "sink", signal_class: "clock", + description: "Timer/Counter0 external clock input. Counts rising or falling edges per the CS02:0 bits.", + }, + { + name: "Interrupt", direction: "sink", signal_class: "data", + description: "INT0 dedicated external interrupt input. Triggers on low level or on rising/falling/any edge per the ISC01:0 bits — the only edge-configurable external interrupt on the device.", + }, + { + name: "Pin Change Interrupt", direction: "sink", signal_class: "data", + description: "PCINT2 pin-change interrupt source. Fires on any logic-level transition on PB2.", + }, + ], + notes: + "Standard GPIO thresholds at VCC=5 V: V_IH min = 0.6*VCC, V_IL max = 0.3*VCC; V_OH min = 4.3 V at 10 mA source, V_OL max = 0.6 V at 10 mA sink. USI SCL is open-drain and needs an external pull-up (typically 4.7 kΩ at 100 kHz or 2.2 kΩ at 400 kHz). INT0 — the only edge-configurable external interrupt on the device — is unique to this pin. PB2 also doubles as the ISP SCK line — isolate during programming if it is also driving a peripheral net.", + }, +]; + +const portPins: InterfaceDef[] = PORT_PIN_SPECS.map(attiny85PortPin); + +// --------------------------------------------------------------------------- +// Power pins — ProtoPart resources "vcc" (pin 8) and "gnd" (pin 4) +// --------------------------------------------------------------------------- + +const gndPin: InterfaceDef = { + ...Ground({ id: "pin_4", name: "GND", pin: 4, maxCurrentA: 0.2 }), + traits: [ + { type: "power_domain", params: { domain: "vcc" } }, + { + type: "current_rating_basis", + params: { + note: "Total continuous return current through the GND pin is limited to 200 mA (same absolute-max as the VCC pin) — an absolute maximum, not a recommended operating point.", + }, + }, + { + type: "net_shareable", + params: { net: "gnd", policy: "single_ground_instance_may_serve_all_members" }, + }, + { + type: "assembly_requirement", + params: { + note: "Single-pin ground — place the decoupling capacitor between this pin and the VCC pin with the shortest possible loop.", + source: "ProtoPart attiny85 definition, GND pin notes", + }, + }, + { + type: "protopart_node_position", + params: { + position: 8, + note: "Position of this pad in the ProtoPart 20-position node layout; the physical PDIP-8 pin number is the `pin` designator.", + }, + }, + ], +}; + +const vccPin: InterfaceDef = { + ...PowerIn({ id: "pin_8", name: "VCC", pin: 8, voltageV: VCC_RANGE, nominalV: 5 }), + capabilities: ["power_in", "vcc"], + traits: [ + { type: "power_domain", params: { domain: "vcc" } }, + { + type: "operating_conditions", + params: { + voltage_range_10MHz_V: VCC_RANGE, + voltage_range_20MHz_V: VCC_RANGE_20MHZ, + note: "2.7-5.5 V for operation up to 10 MHz; 4.5-5.5 V for the 20 MHz speed grade. At 3.3 V the maximum reliable clock is 10 MHz.", + }, + }, + { + type: "absolute_maximum", + params: { + supply_voltage_V: [-0.5, 6], + note: "Absolute-max supply voltage is 6.0 V — exceeding it damages the part. Total continuous current through the VCC pin is limited to 200 mA, which also caps the sum of per-pin source currents.", + }, + }, + { + type: "implied_passives", + params: { + purpose: "Supply decoupling", + components: [ + { kind: "capacitor", value: "100 nF ceramic", connection: "VCC (pin 8) to GND (pin 4), as close to the package as possible" }, + ], + source: "ProtoPart attiny85 definition, design rules", + }, + }, + { + type: "protopart_node_position", + params: { + position: 15, + note: "Position of this pad in the ProtoPart 20-position node layout; the physical PDIP-8 pin number is the `pin` designator.", + }, + }, + ], +}; + +/** All 8 physical PDIP-8 pins, in package order — schematic-honest. */ +const pins: InterfaceDef[] = [...portPins, gndPin, vccPin].sort( + (a, b) => Number(a.pin ?? 0) - Number(b.pin ?? 0), +); + +// --------------------------------------------------------------------------- +// Peripheral controllers — every routing on the ATtiny85 is pin-fixed +// --------------------------------------------------------------------------- + +function withTraits(iface: InterfaceDef, traits: TraitDef[]): InterfaceDef { + return { ...iface, traits: [...(iface.traits ?? []), ...traits] }; +} + +/** Attach traits to the interface with the given id inside a builder result. */ +function amend(ifaces: InterfaceDef[], id: string, traits: TraitDef[]): InterfaceDef[] { + return ifaces.map((i) => (i.id === id ? withTraits(i, traits) : i)); +} + +function composed(config: { + id: string; + name: string; + protocolType: string; + roles: string[]; + slots: SlotDef[]; + profiles?: InterfaceDef["profiles"]; + parameters?: Parameter[]; + maxInstances?: number; + defaultActive?: boolean; + traits?: TraitDef[]; + domain?: InterfaceDef["domain"]; +}): InterfaceDef { + return { + id: config.id, + name: config.name, + domain: config.domain ?? "electrical", + exposed: true, + default_active: config.defaultActive ?? false, + protocols: [{ type: config.protocolType, roles: config.roles }], + slots: config.slots, + ...(config.profiles ? { profiles: config.profiles } : {}), + ...(config.parameters ? { parameters: config.parameters } : {}), + ...(config.maxInstances !== undefined ? { max_instances: config.maxInstances } : {}), + ...(config.traits ? { traits: config.traits } : {}), + }; +} + +/** Shared USI-mode exclusivity — verbatim policy from the ProtoPart usage notes. */ +const USI_SHARED_TRAIT = (self: string, others: string): TraitDef => ({ + type: "co_requirement", + params: { + with: others, + condition: `${self} active`, + effect: + "The Universal Serial Interface (USI) is shared between SPI and TWI/I2C modes — only one protocol can be active at a time.", + source: "ProtoPart attiny85 definition, usage notes", + }, +}); + +// ADC — one 10-bit SAR converter, four fixed single-ended channels. +const adc = composed({ + id: "adc", + name: "ADC (10-bit SAR)", + protocolType: "analog", + roles: ["input"], + parameters: [resolutionBits(10)], + slots: [ + { id: "channel", required: true, count: 4, match: { protocol: "analog", role: "input", capability: "adc_in" } }, + ], + profiles: [ + { + id: "adc_channels", + label: "ADC0-ADC3 (fixed analog pins)", + bindings: { channel: ["pin_1", "pin_7", "pin_3", "pin_2"] }, + }, + ], + maxInstances: 1, + traits: [ + { + type: "channels", + params: { + count: 4, + mapping: "ADC0=PB5 (pin 1), ADC1=PB2 (pin 7), ADC2=PB4 (pin 3), ADC3=PB3 (pin 2)", + note: "Single-ended 10-bit channels.", + }, + }, + { + // The canonical ATtiny85 co-requirement: ADC0 lives on the RESET pin. + type: "co_requirement", + params: { + with: "pin_1, isp", + condition: "ADC0 in use", + effect: + "ADC0 is selectable only after the RSTDISBL fuse turns the pin into general-purpose PB5 — which permanently disables ISP programming (recovery requires a high-voltage serial programmer).", + source: "ProtoPart attiny85 definition, PB5 function list and warnings", + }, + }, + { + type: "reference_options", + params: { + note: "Reference selected by the REFS bits in ADMUX; the external AREF input (PB0, pin 5) accepts 2.0 V to VCC for single-ended channels (datasheet Table 21-8).", + external_reference_interface: "adc_reference", + }, + }, + ], +}); + +// External ADC reference input (AREF on PB0). +const adcReference = composed({ + id: "adc_reference", + name: "ADC Reference (AREF)", + protocolType: "analog", + roles: ["input"], + slots: [{ id: "aref", required: true, match: { protocol: "analog", role: "input", capability: "aref" } }], + profiles: [ + { id: "aref_pin", label: "AREF = PB0 (pin 5)", bindings: { aref: "pin_5" } }, + ], + maxInstances: 1, + traits: [ + { + type: "configuration_note", + params: { + note: "External ADC reference voltage input. Selected by the REFS bits in ADMUX; external reference range is 2.0 V to VCC for single-ended channels (datasheet Table 21-8).", + source: "ATtiny25/45/85 Datasheet (Atmel-2586), Table 21-8; ProtoPart attiny85 definition, PB0 function list", + }, + }, + { + type: "co_requirement", + params: { + with: "pin_5", + condition: "external AREF in use", + effect: "AREF shares PB0 with the USI data lines (MOSI/DI/SDA), the comparator AIN0 input, and the Timer0/Timer1 PWM outputs — committing it to AREF removes those functions.", + }, + }, + ], +}); + +// Analog comparator — fixed AIN0/AIN1 pair. +const comparator = composed({ + id: "comparator", + name: "Analog Comparator", + protocolType: "comparator", + roles: ["input"], + slots: [ + { id: "ain0", required: true, label: "AIN0 (positive)", match: { protocol: "analog", role: "input", capability: "ain0" } }, + { id: "ain1", required: true, label: "AIN1 (negative)", match: { protocol: "analog", role: "input", capability: "ain1" } }, + ], + profiles: [ + { + id: "comparator_pins", + label: "AIN0=PB0 (pin 5), AIN1=PB1 (pin 6)", + bindings: { ain0: "pin_5", ain1: "pin_6" }, + }, + ], + maxInstances: 1, + traits: [ + { + type: "channels", + params: { count: 1, mapping: "Positive input AIN0=PB0, negative input AIN1=PB1" }, + }, + ], +}); + +// Timer0 PWM — OC0A/OC0B compare-match outputs, 8-bit. +const timer0Pwm = composed({ + id: "timer0_pwm", + name: "Timer0 PWM (OC0A/OC0B)", + protocolType: "pwm", + roles: ["output"], + parameters: [resolutionBits(8)], + slots: [ + { id: "oc0a", required: false, match: { protocol: "pwm", role: "output", capability: "oc0a" } }, + { id: "oc0b", required: false, match: { protocol: "pwm", role: "output", capability: "oc0b" } }, + ], + profiles: [ + { + id: "timer0_outputs", + label: "OC0A=PB0 (pin 5), OC0B=PB1 (pin 6)", + bindings: { oc0a: "pin_5", oc0b: "pin_6" }, + }, + ], + maxInstances: 1, + traits: [ + { + type: "channels", + params: { + count: 2, + mapping: "OC0A=PB0, OC0B=PB1", + detail: "8-bit PWM up to ~62 kHz at the typical 16 MHz system clock.", + source: "ProtoPart attiny85 definition, PB0/PB1 function lists", + }, + }, + { + type: "instance_combinations", + params: { + instances_in_silicon: 1, + routing: "pin_fixed", + combination_space: "OC0A and OC0B are fixed to PB0 and PB1 — no alternative routing exists.", + }, + }, + ], +}); + +// Timer1 PWM — OC1A/!OC1A/OC1B/!OC1B, high-speed via the 64 MHz PLL. +const timer1Pwm = composed({ + id: "timer1_pwm", + name: "Timer1 High-Speed PWM (OC1A/OC1B + inverted)", + protocolType: "pwm", + roles: ["output"], + parameters: [resolutionBits(8)], + slots: [ + { id: "oc1a", required: false, match: { protocol: "pwm", role: "output", capability: "oc1a" } }, + { id: "oc1a_n", required: false, label: "!OC1A (inverted)", match: { protocol: "pwm", role: "output", capability: "oc1a_n" } }, + { id: "oc1b", required: false, match: { protocol: "pwm", role: "output", capability: "oc1b" } }, + { id: "oc1b_n", required: false, label: "!OC1B (inverted)", match: { protocol: "pwm", role: "output", capability: "oc1b_n" } }, + ], + profiles: [ + { + id: "timer1_outputs", + label: "OC1A=PB1 (pin 6), !OC1A=PB0 (pin 5), OC1B=PB4 (pin 3), !OC1B=PB3 (pin 2)", + bindings: { oc1a: "pin_6", oc1a_n: "pin_5", oc1b: "pin_3", oc1b_n: "pin_2" }, + }, + ], + maxInstances: 1, + traits: [ + { + type: "channels", + params: { + count: 4, + mapping: "OC1A=PB1, !OC1A=PB0, OC1B=PB4, !OC1B=PB3", + detail: "Compare-match outputs with true and inverted (!) polarities. High-speed PWM up to ~250 kHz at 8-bit resolution when fed from the internal 64 MHz PLL clock.", + source: "ProtoPart attiny85 definition, PB0/PB1/PB3/PB4 function lists", + }, + }, + { + type: "display_notation", + params: { note: "The ProtoPart definition models the inverted outputs as a separate '!PWM' interface; '!' marks the active-low/inverted compare-match outputs." }, + }, + { + type: "instance_combinations", + params: { + instances_in_silicon: 1, + routing: "pin_fixed", + combination_space: "Each output is fixed to its pad — no alternative routing exists.", + }, + }, + ], +}); + +// Timer/Counter0 external clock input (T0). +const timerT0 = composed({ + id: "timer", + name: "Timer/Counter0 External Clock (T0)", + protocolType: "timer", + roles: ["input"], + slots: [{ id: "t0", required: true, match: { capability: "t0" } }], + profiles: [ + { id: "t0_pin", label: "T0 = PB2 (pin 7)", bindings: { t0: "pin_7" } }, + ], + maxInstances: 1, + traits: [ + { + type: "configuration_note", + params: { + note: "Timer/Counter0 external clock input. Counts rising or falling edges per the CS02:0 bits.", + source: "ProtoPart attiny85 definition, PB2 function list", + }, + }, + ], +}); + +// Pin-change interrupts — every port pin is a PCINT source. +const pcint = composed({ + id: "pin_change_interrupt", + name: "Pin Change Interrupt (PCINT0-5)", + protocolType: "pin_change_interrupt", + roles: ["input"], + slots: [{ id: "channel", required: true, count: 6, match: { capability: "pcint" } }], + profiles: [ + { + id: "pcint_channels", + label: "PCINT0-5 (all port pins)", + bindings: { + channel: ["pin_5", "pin_6", "pin_7", "pin_2", "pin_3", "pin_1"], // PCINT0-5 = PB0-PB5 + }, + }, + ], + maxInstances: 1, + traits: [ + { + type: "channels", + params: { + count: 6, + mapping: "PCINT0=PB0, PCINT1=PB1, PCINT2=PB2, PCINT3=PB3, PCINT4=PB4, PCINT5=PB5", + note: "Fires on any logic-level transition. PCINT5 is only available when the RSTDISBL fuse repurposes the RESET pin as PB5.", + }, + }, + ], +}); + +// INT0 — the only edge-configurable external interrupt, fixed to PB2. +const int0 = composed({ + id: "interrupt", + name: "External Interrupt (INT0)", + protocolType: "interrupt", + roles: ["input"], + slots: [{ id: "int0", required: true, match: { capability: "int0" } }], + profiles: [ + { id: "int0_pin", label: "INT0 = PB2 (pin 7)", bindings: { int0: "pin_7" } }, + ], + maxInstances: 1, + traits: [ + { + type: "configuration_note", + params: { + note: "Triggers on low level or on rising/falling/any edge per the ISC01:0 bits — the only edge-configurable external interrupt on the device.", + source: "ProtoPart attiny85 definition, PB2 function list", + }, + }, + ], +}); + +// USI — the raw Universal Serial Interface (USCK/DO/DI), the hardware that +// backs both the SPI-like three-wire and I2C-like two-wire modes below. +const usi = composed({ + id: "usi", + name: "USI (Universal Serial Interface)", + protocolType: "usi", + roles: ["master", "slave", "clock_provider"], + slots: [ + { id: "usck", required: true, match: { capability: "usi_usck" } }, + { id: "do", required: true, match: { capability: "usi_do" } }, + { id: "di", required: true, match: { capability: "usi_di" } }, + ], + profiles: [ + { + id: "usi_pins", + label: "USCK=PB2 (pin 7), DO=PB1 (pin 6), DI=PB0 (pin 5)", + bindings: { usck: "pin_7", do: "pin_6", di: "pin_5" }, + }, + ], + maxInstances: 1, + traits: [ + { + type: "operating_modes", + params: { + modes: ["three-wire (SPI-like)", "two-wire (TWI/I2C-like)"], + note: "Only one protocol can be active at a time; the same three pins are also the AVR ISP programming lines.", + source: "ProtoPart attiny85 definition, usage notes", + }, + }, + { + type: "instance_combinations", + params: { + instances_in_silicon: 1, + routing: "pin_fixed", + combination_space: "USCK/DO/DI are fixed to PB2/PB1/PB0 — no alternative routing exists.", + }, + }, + ], +}); + +// I2C (USI two-wire mode) — open-drain SDA/SCL on PB0/PB2 with mandatory +// external pull-ups. Clock range per the ProtoPart pull-up guidance +// (4.7 kΩ at 100 kHz, 2.2 kΩ at 400 kHz). +const I2C_TRAITS: TraitDef[] = [ + { type: "display_notation", params: { latex: "I^{2}C" } }, + { + type: "implementation_note", + params: { + note: "I2C-compatible signalling is provided by the USI in two-wire mode, not by a dedicated TWI peripheral. SDA and SCL are open-drain when driven low; the idle state must be pulled high externally.", + source: "ProtoPart attiny85 definition, PB0/PB2 function lists", + }, + }, + { + type: "implied_passives", + params: { + purpose: "Open-drain bus pull-ups", + components: [ + { kind: "resistor", value: "typ. 4.7 kΩ at 100 kHz, 2.2 kΩ at 400 kHz", connection: "SDA (PB0) and SCL (PB2) to the bus supply" }, + ], + source: "ProtoPart attiny85 definition, design rules", + }, + }, + USI_SHARED_TRAIT("I2C (two-wire USI mode)", "usi, spi"), +]; + +const i2c = amend( + I2C({ + id: "i2c", + name: "I2C (USI two-wire mode)", + roles: ["master", "slave"], + clockFreqHz: [100_000, 400_000], + maxInstances: 1, + profiles: [ + { id: "i2c_usi_pins", label: "SDA=PB0 (pin 5), SCL=PB2 (pin 7)", sda: "pin_5", scl: "pin_7" }, + ], + }), + "i2c", + I2C_TRAITS, +); + +// SPI (USI three-wire mode / ISP wiring) — fixed to PB0/PB1/PB2. The USI has +// no hardware chip-select; the SPI builder's optional `ss` slot stays unbound. +const SPI_TRAITS: TraitDef[] = [ + { + type: "implementation_note", + params: { + note: "SPI-like signalling is provided by the USI in three-wire mode (DI/DO/USCK), and the same pins carry the AVR ISP programming bus (MOSI/MISO/SCK). There is no hardware slave-select; framing is handled in firmware.", + source: "ProtoPart attiny85 definition, PB0/PB1/PB2 function lists", + }, + }, + { + type: "protocol_roles_note", + params: { + note: "The ProtoPart definition lists roles master/slave/clock_provider for this bus; clock_provider reflects USCK being driven when USI is master.", + }, + }, + { + type: "instance_combinations", + params: { + instances_in_silicon: 1, + routing: "pin_fixed", + combination_space: "MOSI/MISO/SCK are fixed to PB0/PB1/PB2 — no alternative routing exists.", + }, + }, + USI_SHARED_TRAIT("SPI (three-wire USI mode)", "usi, i2c"), +]; + +const spi = amend( + SPI({ + id: "spi", + name: "SPI (USI three-wire mode)", + roles: ["master", "slave"], + maxInstances: 1, + profiles: [ + { id: "spi_usi_pins", label: "MOSI=PB0 (pin 5), MISO=PB1 (pin 6), SCK=PB2 (pin 7)", mosi: "pin_5", miso: "pin_6", sck: "pin_7" }, + ], + }), + "spi", + SPI_TRAITS, +); + +// External reset — the default function of PB5. +const reset = composed({ + id: "reset", + name: "External Reset (RESET, active-low)", + protocolType: "digital", + roles: ["input"], + defaultActive: true, + parameters: [ + { id: "min_reset_pulse", name: "Minimum recognised reset pulse width", unit: "µs", value: 2.5 }, + ], + slots: [{ id: "reset_n", required: true, match: { capability: "ext_reset" } }], + profiles: [ + { id: "reset_pin", label: "RESET = PB5 (pin 1)", default_active: true, bindings: { reset_n: "pin_1" } }, + ], + maxInstances: 1, + traits: [ + { + type: "reset_behavior", + params: { + polarity: "active_low", + note: "Holds the AVR core in reset while driven low; minimum recognised pulse width is 2.5 us. Internal pull-up is enabled at power-up.", + source: "ProtoPart attiny85 definition, PB5 function list", + }, + }, + { + type: "implied_passives", + params: { + purpose: "Reliable reset in noisy environments", + components: [ + { kind: "resistor", value: "10 kΩ", connection: "RESET (pin 1) to VCC" }, + ], + note: "Bring RESET out to a reset button or programming header. Do not leave RESET floating in noisy environments.", + source: "ProtoPart attiny85 definition, design rules", + }, + }, + { + type: "co_requirement", + params: { + with: "pin_1, isp", + condition: "PB5 reclaimed as GPIO (RSTDISBL fuse programmed)", + effect: "Disabling the reset function permanently disables conventional ISP programming; a high-voltage serial programmer is then required to recover the chip.", + source: "ProtoPart attiny85 definition, warnings", + }, + }, + ], +}); + +// debugWIRE — single-wire on-chip debug multiplexed on the RESET pin. +const debugWire = composed({ + id: "debugwire", + name: "debugWIRE On-Chip Debug", + protocolType: "debug", + roles: ["target"], + slots: [{ id: "dw", required: true, match: { capability: "debugwire" } }], + profiles: [ + { id: "dw_pin", label: "dW = RESET/PB5 (pin 1)", bindings: { dw: "pin_1" } }, + ], + maxInstances: 1, + traits: [ + { + type: "configuration_note", + params: { + note: "debugWIRE single-wire on-chip debug interface, multiplexed on the RESET pin when the DWEN fuse is programmed. Replaces conventional ISP behaviour.", + source: "ProtoPart attiny85 definition, PB5 function list", + }, + }, + ], +}); + +// AVR ISP — 6-pin in-system programming header (SPI on PB0/PB1/PB2 + RESET). +const isp = composed({ + id: "isp", + name: "AVR ISP Programming (6-pin)", + protocolType: "spi", + roles: ["device"], + slots: [ + { id: "mosi", required: true, match: { capability: "spi_mosi" } }, + { id: "miso", required: true, match: { capability: "spi_miso" } }, + { id: "sck", required: true, match: { capability: "spi_sck" } }, + { id: "reset_n", required: true, match: { capability: "ext_reset" } }, + { id: "vcc", required: true, match: { capability: "vcc" } }, + { id: "gnd", required: true, match: { capability: "ground" } }, + ], + profiles: [ + { + id: "isp_header", + label: "Standard 6-pin AVR ISP header", + bindings: { + mosi: "pin_5", // PB0 + miso: "pin_6", // PB1 + sck: "pin_7", // PB2 + reset_n: "pin_1", // RESET + vcc: "pin_8", + gnd: "pin_4", + }, + }, + ], + maxInstances: 1, + traits: [ + { + type: "configuration_note", + params: { + note: "For ISP programming, expose PB0 (MOSI), PB1 (MISO), PB2 (SCK), RESET, VCC, and GND on a standard 6-pin AVR ISP header.", + source: "ProtoPart attiny85 definition, design rules", + }, + }, + { + type: "co_requirement", + params: { + with: "usi, i2c, spi", + condition: "ISP header shares the USI pins", + effect: "ISP programming reuses the same three USI pins, so an ISP header on a board running USI peripherals needs to share or isolate those nets during programming.", + source: "ProtoPart attiny85 definition, usage notes", + }, + }, + { + type: "co_requirement", + params: { + with: "reset", + condition: "always", + effect: "Conventional ISP requires the RESET function on pin 1. Programming the RSTDISBL fuse permanently disables ISP; recovery requires a high-voltage serial programmer.", + source: "ProtoPart attiny85 definition, warnings", + }, + }, + ], +}); + +// Crystal oscillator — XTAL1/XTAL2 on PB3/PB4, selected by the CKSEL fuses. +const oscillators = composed({ + id: "oscillators", + name: "Crystal Oscillator (XTAL1/XTAL2)", + protocolType: "clock", + roles: ["input"], + // Crystal/resonator envelope: 32.768 kHz low-frequency mode plus the + // 0.4-20 MHz crystal/resonator modes (datasheet Tables 6-1/6-12). 0 Hz is + // not a valid crystal source; DC-20 MHz applies only to the CLKI external + // clock input (Table 21-3). + parameters: [clockFreqHz([32_768, 20_000_000])], + slots: [ + { id: "xtal1", required: true, match: { capability: "xtal1" } }, + { id: "xtal2", required: true, match: { capability: "xtal2" } }, + ], + profiles: [ + { + id: "xtal_pins", + label: "XTAL1=PB3 (pin 2), XTAL2=PB4 (pin 3)", + bindings: { xtal1: "pin_2", xtal2: "pin_3" }, + }, + ], + maxInstances: 1, + traits: [ + { + type: "co_requirement", + params: { + with: "pin_2, pin_3", + condition: "crystal/resonator fitted", + effect: "If using PB3/PB4 as a crystal oscillator (XTAL1/XTAL2), they cannot also drive PWM, ADC, or GPIO — the crystal load capacitors and physical crystal occupy the pins.", + source: "ProtoPart attiny85 definition, design rules", + }, + }, + { + type: "implied_passives", + params: { + purpose: "Crystal load", + components: [ + { kind: "capacitor", value: "per crystal vendor C_L spec", connection: "XTAL1 to GND" }, + { kind: "capacitor", value: "per crystal vendor C_L spec", connection: "XTAL2 to GND" }, + ], + source: "ProtoPart attiny85 definition, design rules ('the crystal load capacitors and physical crystal occupy the pins')", + }, + }, + { + type: "optionality", + params: { + note: "Optional — the internal 8 MHz RC oscillator is the default clock and suffices for most applications, but drifts up to ±10% over voltage and temperature; for UART or other timing-sensitive bit-banged protocols, calibrate OSCCAL or use an external crystal.", + source: "ProtoPart attiny85 definition, design rules / usage notes", + }, + }, + { + type: "configuration_note", + params: { + note: "Enabled when the CKSEL fuses select the low/high-frequency crystal source. Crystal/resonator operating modes cover 0.4-20 MHz (0.4-0.9 MHz for ceramic resonators only); a 32.768 kHz watch crystal uses the separate low-frequency oscillator mode (datasheet Tables 6-1 and 6-12).", + source: "ATtiny25/45/85 Datasheet (Atmel-2586), Tables 6-1/6-12; ProtoPart attiny85 definition, PB3 function list", + }, + }, + ], +}); + +// External clock input — CLKI on PB3, mutually exclusive with the crystal. +const clockInput = composed({ + id: "clock_input", + name: "External Clock Input (CLKI)", + protocolType: "clock", + roles: ["input"], + slots: [{ id: "clki", required: true, match: { capability: "clki" } }], + profiles: [ + { id: "clki_pin", label: "CLKI = PB3 (pin 2)", bindings: { clki: "pin_2" } }, + ], + maxInstances: 1, + traits: [ + { + type: "configuration_note", + params: { + note: "Routes a single-ended logic-level clock straight into the AVR core when the CKSEL fuses select the external-clock source.", + source: "ProtoPart attiny85 definition, PB3 function list", + }, + }, + { + type: "co_requirement", + params: { + with: "oscillators", + condition: "external clock selected", + effect: "Mutually exclusive with XTAL1 crystal mode.", + source: "ProtoPart attiny85 definition, PB3 function list", + }, + }, + ], +}); + +// Buffered system clock output — CLKO on PB4, enabled by the CKOUT fuse. +const clockOutput = composed({ + id: "clock_output", + name: "System Clock Output (CLKO)", + protocolType: "clock", + roles: ["output"], + slots: [{ id: "clko", required: true, match: { capability: "clko" } }], + profiles: [ + { id: "clko_pin", label: "CLKO = PB4 (pin 3)", bindings: { clko: "pin_3" } }, + ], + maxInstances: 1, + traits: [ + { + type: "configuration_note", + params: { + note: "Enabled by programming the CKOUT fuse; outputs the divided system clock so downstream devices can be slaved to the AVR.", + source: "ProtoPart attiny85 definition, PB4 function list", + }, + }, + ], +}); + +// Composed power input — mirrors the ProtoPart "power_input" interface +// (VCC + GND with max_connections 1 and matching voltage-domain constraint). +const powerInput = composed({ + id: "power_input", + name: "VCC / GND", + protocolType: "power", + roles: ["input"], + defaultActive: true, + slots: [ + { id: "vcc", required: true, match: { protocol: "power", role: "input", capability: "vcc" } }, + { id: "gnd", required: true, match: { protocol: "power", role: "ground", capability: "ground" } }, + ], + profiles: [ + { + id: "power_pins", + label: "VCC=pin 8, GND=pin 4", + default_active: true, + bindings: { vcc: "pin_8", gnd: "pin_4" }, + }, + ], + maxInstances: 1, + traits: [ + { + type: "connection_constraints", + params: { + max_connections: 1, + requires_matching_voltage_domain: true, + source: "ProtoPart attiny85 definition, electrical interface 'power_input' constraints", + }, + }, + { + type: "implied_passives", + params: { + purpose: "Supply decoupling", + components: [ + { kind: "capacitor", value: "100 nF", connection: "VCC (pin 8) to GND (pin 4), close to the package" }, + ], + source: "ProtoPart attiny85 definition, 'power_input' interface description", + }, + }, + { + type: "supply_note", + params: { + note: "Single-rail DC supply input. 2.7-5.5 V (10 MHz max) or 4.5-5.5 V (20 MHz max).", + }, + }, + ], +}); + +// --------------------------------------------------------------------------- +// Mechanical +// --------------------------------------------------------------------------- + +const footprintMount: InterfaceDef = { + id: "footprint_mounting", + name: "PDIP-8 Through-Hole Footprint", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["mounting_point"] }], + capabilities: ["pdip8", "through_hole"], +}; + +/** + * ProtoPart mechanical interface "dnc": 12 footprint-only mounting positions + * in the 20-position node layout. They have no internal bond to the PDIP-8 + * die and no physical package pin, so they are preserved here as a single + * mechanical interface rather than as leaf electrical pins. + */ +const dncMounting: InterfaceDef = { + id: "dnc", + name: "DNC Mounting Pins", + domain: "mechanical", + exposed: true, + default_active: false, + protocols: [{ type: "mechanical_connection", roles: ["mounting_point"] }], + capabilities: ["dnc_mounting", "through_hole"], + max_instances: 12, + traits: [ + { + type: "protopart_node_positions", + params: { + positions: [3, 4, 6, 7, 9, 10, 13, 16, 17, 18, 19, 20], + note: "Do not connect. Footprint mounting pins only; no internal bond to the PDIP-8 die. Footprint-only mounting pins in the ProtoPart 20-position node layout — they do not correspond to physical PDIP-8 package pins.", + source: "ProtoPart attiny85 definition, mechanical resources", + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const ATTINY85: ModuleDef = defineModule({ + id: "attiny85", + name: "Microchip ATtiny85 8-bit AVR Microcontroller", + version: "1.0.0", + manufacturer: "Microchip Technology (Atmel)", + part_number: "ATTINY85-20PU", + description: + "Microchip (formerly Atmel) ATtiny85 8-bit AVR RISC microcontroller IC with 8 KB flash, 512 B EEPROM, 512 B SRAM, six GPIO, 4-channel 10-bit ADC, Universal Serial Interface (USI) supporting SPI/I2C-like signalling, two 8-bit timer/counters with PWM, internal RC oscillator, and an internal PLL for 64 MHz high-speed PWM. Default variant: ATTINY85-20PU in 8-pin PDIP, rated to 20 MHz at 4.5-5.5 V, -40 to +85 C industrial temperature range. Programmed via 6-pin AVR ISP (SPI on PB0/PB1/PB2 plus RESET) or debugWIRE single-wire interface on the RESET pin.", + tags: [ + "attiny85", + "attiny", + "avr", + "microcontroller", + "mcu", + "8-bit", + "atmel", + "microchip", + "pdip-8", + "dip-8", + "usi", + "arduino-compatible", + ], + categories: ["microcontroller"], + + interfaces: [ + // All 8 physical PDIP-8 pins in package order — schematic-honest. + ...pins, + + // Power + powerInput, + + // Analog peripherals + adc, + adcReference, + comparator, + + // Timers / PWM + timer0Pwm, + timer1Pwm, + timerT0, + + // Interrupts + pcint, + int0, + + // Serial / bus controllers (all backed by the single USI) + usi, + ...i2c, + ...spi, + + // Reset / debug / programming + reset, + debugWire, + isp, + + // Clocks + oscillators, + clockInput, + clockOutput, + + // Mechanical + footprintMount, + dncMounting, + ], + + interfaceGroups: [ + { + id: "required_power_pins", + label: "Required Power Pins", + members: ["pin_8", "pin_4"], + policy: "all_of", + }, + { + id: "isp_header_pins", + label: "AVR ISP Header Pins (6-pin)", + members: ["pin_5", "pin_6", "pin_7", "pin_1", "pin_8", "pin_4"], + policy: "all_of", + }, + { + id: "usi_pins", + label: "USI Pins (shared by SPI, I2C, and ISP)", + members: ["pin_5", "pin_6", "pin_7"], + policy: "all_of", + }, + { + id: "crystal_pins", + label: "Crystal Oscillator Pins (XTAL1/XTAL2)", + members: ["pin_2", "pin_3"], + policy: "all_of", + }, + ], + + requirements: [ + { + type: "power", + description: + "Single-rail DC supply: 2.7-5.5 V for operation up to 10 MHz, 4.5-5.5 V for the 20 MHz speed grade (1.8-5.5 V only on ATTINY85V low-voltage variants). Absolute-max supply is 6.0 V. Total continuous current through VCC or GND is limited to 200 mA. Place a 100 nF decoupling capacitor between VCC (pin 8) and GND (pin 4) close to the package.", + voltage_V: [2.7, 5.5], + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + { + id: "vcc", + name: "Core/I-O supply", + nominal_voltage_V: 5, + voltage_range_V: VCC_RANGE, + max_current_mA: VCC_GND_CURRENT_MA, + }, + ], + metadata: { + supply_voltage_V: VCC_RANGE, + // ProtoPart records pin_count 20 for its node layout (8 electrical + // pads + 12 DNC mounting positions); the physical PDIP-8 package has + // 8 pins. + protopart_node_pin_count: 20, + package_pins: 8, + vcc_domain: { + isolation_type: "non_isolated", + ground_reference: "common", + description: + "Single VCC rail supplies both the AVR core and the I/O pad ring. ATTINY85-20PU is rated 2.7-5.5 V for operation up to 10 MHz and 4.5-5.5 V for operation up to 20 MHz. Total continuous current through the VCC or GND pin is limited to 200 mA. Recommended 0.1 uF decoupling cap close to pin 8.", + }, + architecture: "8-bit AVR RISC", + instruction_set: "AVR (120 instructions)", + flash_memory_KB: 8, + eeprom_memory_B: 512, + sram_memory_B: 512, + flash_endurance_cycles: 10_000, + eeprom_endurance_cycles: 100_000, + gpio_count: 6, + adc_channels: 4, + adc_resolution_bits: 10, + pwm_channels: 4, + timer_count: 2, + comparator_count: 1, + max_clock_speed_MHz: { + at_2p7V_to_5p5V: 10, + at_4p5V_to_5p5V: 20, + }, + internal_oscillators_MHz: [8, 0.128], + pll_peripheral_clock_MHz: 64, + logic_thresholds_V: { + v_ih_min_fraction_vcc: 0.6, + v_il_max_fraction_vcc: 0.3, + v_oh_min_V_at_5V_10mA_source: 4.3, + v_ol_max_V_at_5V_10mA_sink: 0.6, + }, + max_pin_current_mA: PIN_CURRENT_MA, + max_total_vcc_or_gnd_current_mA: VCC_GND_CURRENT_MA, + active_current_typ: { + at_1p8V_1MHz_mA: 0.3, + at_3V_4MHz_mA: 1.5, + at_5V_8MHz_mA: 5, + at_5V_20MHz_mA: 9, + }, + power_down_current_typ_uA: { + at_1p8V_wdt_off: 0.1, + at_5V_wdt_off: 0.5, + }, + sleep_modes: ["idle", "adc_noise_reduction", "power_down"], + absolute_maximum_ratings: { + supply_voltage_V: [-0.5, 6], + pin_voltage_V_non_reset: [-0.5, "VCC + 0.5"], + pin_voltage_V_reset: [-0.5, 13], + pin_continuous_current_mA: 40, + vcc_or_gnd_continuous_current_mA: 200, + operating_temperature_absmax_C: [-55, 125], + storage_temperature_C: [-65, 150], + junction_temperature_C: 150, + }, + programming_interfaces: [ + "spi_isp_6pin", + "debugwire", + "high_voltage_serial_programming", + ], + source: "ProtoPart attiny85 definition, electrical domain metadata (ATtiny25/45/85 Datasheet, Atmel-2586)", + }, + }, + { + domain: "mechanical", + dimensions_mm: { length: 9.43, width: 7.94, height: 4.32 }, + weight_g: 0.55, + metadata: { + package_type: "PDIP-8", + mounting_method: "through_hole", + field_serviceable: true, + requires_special_tools: false, + protopart_node_layout: + "The ProtoPart node places the 8 electrical pads at positions 1/2/5/8/11/12/14/15 of a 20-position layout; positions 3, 4, 6, 7, 9, 10, 13, 16-20 are DNC footprint-only mounting pins (see the `dnc` interface).", + }, + }, + { + domain: "thermal", + operating_temperature_C: [-40, 85], + metadata: { + requires_thermal_management: false, + thermal_monitoring_available: false, + }, + }, + ], + + traits: [ + { + type: "audit_note", + params: { + note: "The ProtoPart metadata.id reads 'ATTINY85-20SU' (the SOIC variant suffix) while part_number, description, and all purchase records say ATTINY85-20PU (PDIP-8). The PDIP-8 ATTINY85-20PU is taken as authoritative.", + }, + }, + { + type: "capability_absence", + params: { + capability: "uart", + note: "There is no hardware UART on the ATtiny85. UART communication must be bit-banged in firmware (e.g. via SoftwareSerial or Timer0 in CTC mode).", + source: "ProtoPart attiny85 definition, warnings", + }, + }, + { + type: "design_rules", + params: { + source: "ProtoPart attiny85 definition, design_rules (verbatim)", + rules: [ + "Place a 0.1 uF (100 nF) ceramic decoupling capacitor between VCC (pin 8) and GND (pin 4) as close to the package as possible.", + "If the RESET pin (PB5, pin 1) is left in its default reset role, pull it to VCC through a 10 kΩ resistor and bring it out to a reset button or programming header. Do not leave RESET floating in noisy environments.", + "For ISP programming, expose PB0 (MOSI), PB1 (MISO), PB2 (SCK), RESET, VCC, and GND on a standard 6-pin AVR ISP header.", + "When using USI as an I2C/TWI master or slave, add external pull-ups on SDA (PB0) and SCL (PB2): typically 4.7 kΩ at 100 kHz, 2.2 kΩ at 400 kHz.", + "Do not sink or source more than 40 mA on a single pin, and keep the sum of all currents through VCC or GND below 200 mA. Use an external transistor or driver for higher-current loads.", + "If using PB3/PB4 as a crystal oscillator (XTAL1/XTAL2), they cannot also drive PWM, ADC, or GPIO — the crystal load capacitors and physical crystal occupy the pins.", + "The internal 8 MHz RC oscillator drifts up to ±10% over voltage and temperature; for UART or other timing-sensitive bit-banged protocols, calibrate OSCCAL or use an external crystal.", + ], + }, + }, + { + type: "application_examples", + params: { + source: "ProtoPart attiny85 definition, application_examples (verbatim)", + examples: [ + "Compact embedded controller for sensor reading and small actuator control where 6 GPIO are sufficient.", + "Arduino-compatible projects programmed via Digispark-style USB bootloader or 6-pin ISP using ATTinyCore.", + "Low-power battery-operated nodes leveraging the power-down sleep mode and watchdog wake-up.", + "USI-driven I2C master for adding extra peripherals to a board that has no spare hardware I2C.", + "High-speed PWM generation (up to ~250 kHz) on Timer1 using the internal 64 MHz PLL clock source.", + ], + }, + }, + { + type: "usage_notes", + params: { + source: "ProtoPart attiny85 definition, usage_notes (verbatim)", + note: "The ATtiny85 is the largest member of the ATtiny25/45/85 family. Pin assignments are identical across the three; only flash and SRAM capacity differ. When used in a 4.5-5.5 V system with the internal 8 MHz RC oscillator the chip can be powered directly from the supply with only a decoupling capacitor; no external crystal is required for most applications. The Universal Serial Interface (USI) is shared between SPI and TWI/I2C modes — only one protocol can be active at a time. ISP programming reuses the same three USI pins, so an ISP header on a board running USI peripherals needs to share or isolate those nets during programming. The RESET pin can be reclaimed as PB5 by programming the RSTDISBL fuse, but this disables conventional ISP programming and you will then need a high-voltage serial programmer to recover the chip.", + }, + }, + { + type: "warnings", + params: { + source: "ProtoPart attiny85 definition, warnings (verbatim)", + warnings: [ + "The single VCC pin must stay between 2.7 V and 5.5 V for the -20PU industrial variant (1.8-5.5 V for ATTINY85V low-voltage variants). Exceeding 6.0 V will damage the device.", + "The 20 MHz speed grade only applies when VCC is between 4.5 V and 5.5 V. At 3.3 V the maximum reliable clock is 10 MHz.", + "There is no hardware UART on the ATtiny85. UART communication must be bit-banged in firmware (e.g. via SoftwareSerial or Timer0 in CTC mode).", + "Programming the RSTDISBL fuse to free PB5 as a GPIO permanently disables ISP programming; recovery requires a high-voltage serial programmer.", + "The internal RC oscillator drifts with temperature and supply voltage; precision-timed protocols may require an external crystal or careful OSCCAL trimming.", + ], + }, + }, + { + type: "terminology_policy", + params: { + note: "Signal and function names in `attiny85_pin_functions` traits are carried verbatim from the ProtoPart definition (datasheet notation, including '!' for active-low/inverted); canonical capability tags exist only where slot matching requires them.", + }, + }, + ], + + artifacts: [ + { + id: "art_datasheet", + name: "ATtiny25/45/85 Datasheet (Atmel-2586)", + type: "datasheet", + url: "https://ww1.microchip.com/downloads/en/devicedoc/atmel-2586-avr-8-bit-microcontroller-attiny25-attiny45-attiny85_datasheet.pdf", + }, + { + id: "art_product_page", + name: "ATtiny85 Microchip product page", + type: "documentation", + url: "https://www.microchip.com/en-us/product/attiny85", + }, + { + id: "art_product_image", + name: "ATTINY85-20PU product photo (DigiKey)", + type: "custom", + filePath: "./ProtoPart/protoparts/attiny85/artifacts/images/150_8P3_P_8.jpg", + mimeType: "image/jpeg", + tags: ["image", "product-photo"], + }, + { + id: "art_thumbnail", + name: "Microchip ATtiny85 8-bit AVR Microcontroller thumbnail", + type: "custom", + filePath: "./ProtoPart/protoparts/attiny85/thumbnail.png", + mimeType: "image/png", + tags: ["image", "thumbnail", "preview"], + }, + ], +}); diff --git a/library/parts/attiny85/artifacts/thumbnail.png b/library/parts/attiny85/artifacts/thumbnail.png new file mode 100644 index 0000000..e791db4 Binary files /dev/null and b/library/parts/attiny85/artifacts/thumbnail.png differ diff --git a/library/parts/bme280.ts b/library/parts/bme280.ts new file mode 100644 index 0000000..40b82b1 --- /dev/null +++ b/library/parts/bme280.ts @@ -0,0 +1,824 @@ +/** + * Bosch Sensortec BME280 — datasheet-honest part definition. + * + * Primary source: BME280 Datasheet BST-BME280-DS002 (Rev 1.24, Feb 2024) + * - Table 35 Pin description (pin numbers, names, functions — LGA-8) + * - Section 1.1 Supply ranges and current consumption (VDD 1.71-3.6 V, + * VDDIO 1.2-3.6 V, 3.6 µA typ @ 1 Hz, 0.1 µA sleep) + * - Section 6.2 I²C interface (Standard/Fast/High-speed, SDO address strap) + * - Section 6.3 SPI interface (mode '00'/'11' auto-selection, spi3w_en + * register bit; 4-/3-wire connection diagrams Figures 18/19 in §7.3-§7.4) + * - Section 7.2 Design-in guidance (CSB mode latch Figure 17, pull-ups, + * decoupling), Section 7.5 Package (2.5 × 2.5 × 0.93 mm LGA, metal lid) + * All values below are carried from the audited ProtoPart definition + * (protoparts/bme280/definition.json); nothing is added from convention. + * + * Architecture notes honoured by this file: + * - Pin-honest like a schematic: all 8 LGA pads are leaf interfaces, in + * package order, with ids "pin_N" and the datasheet pin name (Table 35) + * as the displayed name. + * - Every pin carries its verbatim function list (name, direction, + * signal class) in a `bme280_pin_functions` trait — display data is + * separated from the canonical capability tags slot matching needs. + * - Buses share pads: I²C, SPI 4-wire, and SPI 3-wire are composed + * interfaces whose slots bind the same physical pins; a one_of + * interface group models the power-on mode latch (one mode at a time). + * - Co-requirements (`co_requirement` traits): CSB tied to VDDIO latches + * I²C mode; SDO straps the I²C address (GND → 0x76, VDDIO → 0x77, + * never floating); SDO is hi-Z/DNC in SPI 3-wire mode. + * - Implied harness connections (`implied_passives` traits): ~4.7 kΩ + * pull-ups to VDDIO on SDA/SCL in I²C mode; 100 nF decoupling at VDD + * and VDDIO. + * - Shareability exemption (`net_shareable` trait): the two GND pads + * (pins 1 and 7) are one ground net; a single ground instance may + * serve both. + */ + +import type { + InterfaceDef, + ModuleDef, + TraitDef, +} from "../../src/types/index.js"; +import { + Ground, + I2C, + Pin, + PowerIn, + SPI, + clockFreqHz, + defineModule, +} from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Electrical constants — Datasheet Section 1.1 (via ProtoPart power_domains) +// --------------------------------------------------------------------------- + +/** VDD analog + digital core supply range. */ +const VDD_RANGE: [number, number] = [1.71, 3.6]; +/** VDDIO digital interface supply range (CSB, SDI/SDA, SCK/SCL, SDO). */ +const VDDIO_RANGE: [number, number] = [1.2, 3.6]; +/** I²C: Standard-mode 100 kHz up to High-speed mode 3.4 MHz (§6.2). */ +const I2C_CLOCK_RANGE: [number, number] = [100_000, 3_400_000]; +/** SPI (3- and 4-wire): up to 10 MHz (§6.3). */ +const SPI_CLOCK_RANGE: [number, number] = [0, 10_000_000]; + +/** §7.2: external pull-ups to VDDIO on SDA/SCL when the bus runs I²C. */ +const I2C_PULLUP_TRAIT: TraitDef = { + type: "implied_passives", + params: { + purpose: "I²C open-drain bus pull-ups", + components: [ + { + kind: "resistor", + value: "~4.7 kΩ typical (moderate bus capacitance)", + connection: "SDA (pin 3) and SCL (pin 4) to VDDIO", + }, + ], + source: "BME280 Datasheet §7.2", + }, +}; + +/** §7.2-§7.4: 100 nF decoupling close to each supply pin. */ +function decouplingTrait(pinName: string): TraitDef { + return { + type: "implied_passives", + params: { + purpose: "Supply decoupling", + components: [ + { kind: "capacitor", value: "100 nF", connection: `${pinName} to GND, close to the pin` }, + ], + source: "BME280 Datasheet §7.2-§7.4", + }, + }; +} + +/** The two GND pads sit on one common ground net. */ +const GND_NET_TRAIT: TraitDef = { + type: "net_shareable", + params: { + net: "gnd", + policy: "single_ground_instance_may_serve_all_members", + members: ["pin_1", "pin_7"], + }, +}; + +/** + * JSON constraint carried verbatim: bus logic levels must match the VDDIO + * domain of the host ("requires_matching_voltage_domain": true). + */ +const VDDIO_DOMAIN_MATCH_TRAIT: TraitDef = { + type: "voltage_domain_matching", + params: { + requires_matching_voltage_domain: true, + domain: "vddio", + note: "Logic thresholds scale with VDDIO (datasheet section 6.4, interface parameter specification: input levels specified as %VDDIO).", + }, +}; + +function withTraits(iface: InterfaceDef, traits: TraitDef[]): InterfaceDef { + return { ...iface, traits: [...(iface.traits ?? []), ...traits] }; +} + +/** Attach traits to the interface with the given id inside a builder result. */ +function amend(ifaces: InterfaceDef[], id: string, traits: TraitDef[]): InterfaceDef[] { + return ifaces.map((i) => (i.id === id ? withTraits(i, traits) : i)); +} + +// --------------------------------------------------------------------------- +// Physical pads — Datasheet Table 35 (p. 38), in package pin order +// --------------------------------------------------------------------------- + +const pin1Gnd: InterfaceDef = withTraits(Ground({ id: "pin_1", name: "GND", pin: 1 }), [ + { + type: "bme280_pin_functions", + params: { + source: "BME280 Datasheet Table 35, p. 38", + functions: [{ name: "GND", direction: "sink", signal_class: "ground" }], + description: "GND — ground reference.", + }, + }, + { type: "power_domain", params: { domain: "vdd" } }, + GND_NET_TRAIT, +]); + +const pin2Csb: InterfaceDef = withTraits( + Pin({ + id: "pin_2", + name: "CSB", + pin: 2, + voltageV: VDDIO_RANGE, + capabilities: { inputOnly: true, spiSs: true }, + }), + [ + { + type: "bme280_pin_functions", + params: { + source: "BME280 Datasheet Table 35, p. 38", + functions: [ + { name: "CSB", direction: "sink", signal_class: "data" }, + { name: "VDDIO", direction: "sink", signal_class: "power" }, + ], + description: + "CSB — chip-select input (SPI 4-wire and SPI 3-wire); must be tied directly to VDDIO to select I²C mode.", + }, + }, + { type: "power_domain", params: { domain: "vddio" } }, + { + type: "internal_pulls", + params: { + available: true, + pull_up: "internal pull-up to VDDIO", + note: "SPI: CSB is active-low chip-select with an internal pull-up to VDDIO.", + }, + }, + { + type: "co_requirement", + params: { + with: "i2c", + condition: "I²C mode", + effect: + "The pin must be hard-wired to VDDIO at power-on so the device latches I²C mode.", + source: "BME280 Datasheet §7.2, Figure 17", + }, + }, + ], +); + +const pin3Sdi: InterfaceDef = (() => { + const base = Pin({ + id: "pin_3", + name: "SDI", + pin: 3, + voltageV: VDDIO_RANGE, + capabilities: { i2cSda: true, spiMosi: true }, + }); + return { + ...base, + // spi_3w_sdio: half-duplex bidirectional data for the 3-wire bus slot. + capabilities: [...(base.capabilities ?? []), "spi_3w_sdio"], + traits: [ + { + type: "bme280_pin_functions", + params: { + source: "BME280 Datasheet Table 35, p. 38", + functions: [ + { name: "SDI", direction: "sink", signal_class: "data" }, + { name: "SDI/SDO", direction: "bidirectional", signal_class: "data" }, + { name: "SDA", direction: "bidirectional", signal_class: "data" }, + ], + description: + "SDI — serial data input in SPI 4-wire, bidirectional SDI/SDO half-duplex data in SPI 3-wire, SDA in I²C.", + note: "In SPI 4-wire SDI is sampled on the SCK rising edge. In SPI 3-wire the same pin carries the device's response after the address phase.", + }, + }, + { type: "power_domain", params: { domain: "vddio" } }, + I2C_PULLUP_TRAIT, + ], + } satisfies InterfaceDef; +})(); + +const pin4Sck: InterfaceDef = withTraits( + Pin({ + id: "pin_4", + name: "SCK", + pin: 4, + voltageV: VDDIO_RANGE, + capabilities: { inputOnly: true, i2cScl: true, spiSck: true }, + }), + [ + { + type: "bme280_pin_functions", + params: { + source: "BME280 Datasheet Table 35, p. 38", + functions: [ + { name: "SCK", direction: "sink", signal_class: "clock" }, + { name: "SCL", direction: "sink", signal_class: "clock" }, + ], + description: "SCK — serial clock input in SPI 4-wire and SPI 3-wire, SCL in I²C.", + }, + }, + { type: "power_domain", params: { domain: "vddio" } }, + I2C_PULLUP_TRAIT, + ], +); + +const pin5Sdo: InterfaceDef = withTraits( + Pin({ + id: "pin_5", + name: "SDO", + pin: 5, + voltageV: VDDIO_RANGE, + capabilities: { spiMiso: true }, + }), + [ + { + type: "bme280_pin_functions", + params: { + source: "BME280 Datasheet Table 35, p. 38", + functions: [ + { name: "SDO", direction: "source", signal_class: "data" }, + { name: "DNC", direction: "bidirectional", signal_class: "data" }, + { name: "GND", direction: "sink", signal_class: "ground" }, + { name: "VDDIO", direction: "sink", signal_class: "power" }, + ], + description: + "SDO — serial data output in SPI 4-wire; DNC (Do Not Connect) in SPI 3-wire; I²C address strap in I²C (tie to GND for 0x76, VDDIO for 0x77).", + note: "SPI 4-wire: SDO changes on the SCK falling edge (§6.3). SPI 3-wire: pin is held hi-Z, leave DNC. I²C: must not float.", + }, + }, + { type: "power_domain", params: { domain: "vddio" } }, + { + type: "co_requirement", + params: { + with: "i2c", + condition: "I²C mode", + effect: + "The pin selects the 7-bit address — tie to GND for default 0x76 or to VDDIO for 0x77; never leave floating.", + source: "BME280 Datasheet §6.2", + }, + }, + { + type: "co_requirement", + params: { + with: "spi_3wire", + condition: "SPI 3-wire mode", + effect: "The pin is held hi-Z and must be left DNC (Do Not Connect).", + source: "BME280 Datasheet §6.3", + }, + }, + ], +); + +const pin6Vddio: InterfaceDef = (() => { + const base = PowerIn({ + id: "pin_6", + name: "VDDIO", + pin: 6, + voltageV: VDDIO_RANGE, + nominalV: 1.8, + }); + return { + ...base, + capabilities: ["power_in", "vddio_supply"], + traits: [ + { + type: "bme280_pin_functions", + params: { + source: "BME280 Datasheet Table 35, p. 38", + functions: [{ name: "VDDIO", direction: "sink", signal_class: "power" }], + description: "VDDIO — digital / interface supply input (1.2 V to 3.6 V).", + }, + }, + { type: "power_domain", params: { domain: "vddio" } }, + decouplingTrait("VDDIO"), + ], + } satisfies InterfaceDef; +})(); + +const pin7Gnd: InterfaceDef = withTraits(Ground({ id: "pin_7", name: "GND", pin: 7 }), [ + { + type: "bme280_pin_functions", + params: { + source: "BME280 Datasheet Table 35, p. 38", + functions: [{ name: "GND", direction: "sink", signal_class: "ground" }], + description: "GND — ground reference.", + }, + }, + { type: "power_domain", params: { domain: "vdd" } }, + GND_NET_TRAIT, +]); + +const pin8Vdd: InterfaceDef = (() => { + const base = PowerIn({ + id: "pin_8", + name: "VDD", + pin: 8, + voltageV: VDD_RANGE, + nominalV: 3.3, + }); + return { + ...base, + capabilities: ["power_in", "vdd_supply"], + traits: [ + { + type: "bme280_pin_functions", + params: { + source: "BME280 Datasheet Table 35, p. 38", + functions: [{ name: "VDD", direction: "sink", signal_class: "power" }], + description: "VDD — analog and digital core supply input (1.71 V to 3.6 V).", + }, + }, + { type: "power_domain", params: { domain: "vdd" } }, + decouplingTrait("VDD"), + ], + } satisfies InterfaceDef; +})(); + +/** All 8 pads in physical package order — schematic-honest. */ +const pins: InterfaceDef[] = [ + pin1Gnd, + pin2Csb, + pin3Sdi, + pin4Sck, + pin5Sdo, + pin6Vddio, + pin7Gnd, + pin8Vdd, +]; + +// --------------------------------------------------------------------------- +// Serial buses — one mode at a time, latched by CSB at power-on +// --------------------------------------------------------------------------- + +// I²C — target (slave) up to 3.4 MHz on the shared SDI/SCK pads. +const i2c = amend( + I2C({ + id: "i2c", + name: "I²C", + roles: ["slave"], + clockFreqHz: I2C_CLOCK_RANGE, + sda: "pin_3", + scl: "pin_4", + maxInstances: 1, // JSON constraint: max_connections 1 + }), + "i2c", + [ + { type: "display_notation", params: { latex: "I^{2}C" } }, + { + type: "operating_modes", + params: { + modes: ["Standard-mode (100 kHz)", "Fast-mode (400 kHz)", "High-speed mode (up to 3.4 MHz)"], + source: "BME280 Datasheet §6.2", + }, + }, + { + type: "co_requirement", + params: { + with: "pin_2", + condition: "I²C mode", + effect: + "Configuration strap (board-level, not a bus signal): CSB must be tied directly to VDDIO to latch I²C mode at power-on.", + source: "BME280 Datasheet §7.2, Figure 17", + }, + }, + { + type: "co_requirement", + params: { + with: "pin_5", + condition: "I²C mode", + effect: + "Configuration strap: SDO selects the 7-bit address — tie to GND for 0x76, tie to VDDIO for 0x77, never leave floating.", + source: "BME280 Datasheet §6.2", + }, + }, + I2C_PULLUP_TRAIT, + VDDIO_DOMAIN_MATCH_TRAIT, + ], +); + +// SPI 4-wire — full-duplex target up to 10 MHz (Table 35 / Figure 18). +const spi4wire = amend( + SPI({ + id: "spi_4wire", + name: "SPI 4-wire", + roles: ["slave"], + clockFreqHz: SPI_CLOCK_RANGE, + mosi: "pin_3", // SDI: host-to-device serial data, sampled on SCK rising edge + miso: "pin_5", // SDO: device-to-host serial data, changes on SCK falling edge + sck: "pin_4", + ss: "pin_2", // CSB: active-low chip-select with internal pull-up to VDDIO + maxInstances: 1, // JSON constraint: max_connections 1 + }), + "spi_4wire", + [ + { + type: "spi_modes", + params: { + modes: ["mode '00' (CPOL=CPHA=0)", "mode '11' (CPOL=CPHA=1)"], + note: "Mode is auto-selected from the SCK level at the falling edge of CSB.", + source: "BME280 Datasheet §6.3 (4-wire connection diagram: §7.3 Figure 18)", + }, + }, + VDDIO_DOMAIN_MATCH_TRAIT, + ], +); + +// SPI 3-wire — half-duplex target on a single SDI/SDO data line; the SPI +// builder assumes separate MOSI/MISO, so this bus is hand-rolled. +const spi3wire: InterfaceDef = { + id: "spi_3wire", + name: "SPI 3-wire", + domain: "electrical", + exposed: true, + default_active: false, + protocols: [{ type: "spi", roles: ["slave"] }], + parameters: [clockFreqHz(SPI_CLOCK_RANGE)], + slots: [ + { id: "csb", required: true, match: { protocol: "spi", role: "select", capability: "spi_ss" } }, + { id: "sck", required: true, match: { protocol: "spi", role: "clock", capability: "spi_sck" } }, + { + id: "sdio", + label: "SDI/SDO (half-duplex bidirectional data)", + required: true, + match: { protocol: "spi", role: "data", capability: "spi_3w_sdio" }, + }, + ], + profiles: [ + { + id: "spi_3wire_pins", + label: "CSB/SCK/SDI-SDO (pins 2/4/3)", + bindings: { csb: "pin_2", sck: "pin_4", sdio: "pin_3" }, + }, + ], + max_instances: 1, // JSON constraint: max_connections 1 + traits: [ + { + type: "configuration_note", + params: { + note: "Enabled by setting spi3w_en=1 in register 0xF5 (config). Same mode '00' / mode '11' behavior as SPI 4-wire.", + source: "BME280 Datasheet §6.3 and §5.4.6 register 0xF5 (3-wire connection diagram: §7.4 Figure 19)", + }, + }, + { + type: "co_requirement", + params: { + with: "pin_5", + condition: "SPI 3-wire mode", + effect: "SDO (pin 5) is held hi-Z in this mode and must be left DNC (Do Not Connect).", + source: "BME280 Datasheet §6.3", + }, + }, + { + type: "operating_notes", + params: { + note: "Host drives SDI/SDO during the address phase; the device drives it during the data phase.", + source: "BME280 Datasheet §6.3.2 (SPI read: data is sent out on SDI in 3-wire mode)", + }, + }, + VDDIO_DOMAIN_MATCH_TRAIT, + ], +}; + +// --------------------------------------------------------------------------- +// Power interfaces — carried from the ProtoPart composed power interfaces +// --------------------------------------------------------------------------- + +const vddPowerIn: InterfaceDef = { + id: "vdd_power_in", + name: "VDD Power", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "power", roles: ["input"] }], + parameters: [{ id: "voltage", unit: "V", value: 3.3, range: VDD_RANGE }], + slots: [ + { id: "vdd", required: true, match: { protocol: "power", role: "input", capability: "vdd_supply" } }, + { id: "gnd", required: true, match: { protocol: "power", role: "ground", capability: "ground" } }, + ], + profiles: [ + { + id: "vdd_power_pins", + label: "VDD/GND (pins 8/1)", + default_active: true, + bindings: { vdd: "pin_8", gnd: "pin_1" }, + }, + ], + traits: [ + { + type: "supply_characteristics", + params: { + description: + "1.71 V to 3.6 V analog and digital core supply. Typical current is 3.6 uA at 1 Hz humidity+pressure+temperature, 0.1 uA in sleep mode.", + source: "BME280 Datasheet §1.1", + }, + }, + ], +}; + +const vddioPowerIn: InterfaceDef = { + id: "vddio_power_in", + name: "VDDIO Power", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "power", roles: ["input"] }], + parameters: [{ id: "voltage", unit: "V", value: 1.8, range: VDDIO_RANGE }], + slots: [ + { id: "vddio", required: true, match: { protocol: "power", role: "input", capability: "vddio_supply" } }, + { id: "gnd", required: true, match: { protocol: "power", role: "ground", capability: "ground" } }, + ], + profiles: [ + { + id: "vddio_power_pins", + label: "VDDIO/GND (pins 6/7)", + default_active: true, + bindings: { vddio: "pin_6", gnd: "pin_7" }, + }, + ], + traits: [ + { + type: "supply_characteristics", + params: { + description: + "1.2 V to 3.6 V digital interface supply for the I²C / SPI bus. Independent of VDD so host logic can run at 1.8 V while VDD is 3.3 V.", + source: "BME280 Datasheet §1.1", + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// Mechanical +// --------------------------------------------------------------------------- + +const pcbMount: InterfaceDef = { + id: "pcb_mount", + name: "PCB Surface Mount", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["mounting_point"] }], + capabilities: ["lga8_2p5x2p5_0p65mm", "surface_mount"], + traits: [ + { + type: "package_description", + params: { + note: "Surface-mount LGA solder attachment to PCB land pattern. Eight 0.65 mm pitch pads (0.35 x 0.35 mm each) on the bottom of the metal-lid package. 2.5 x 2.5 x 0.93 mm body. (Pitch corrected from 0.4 mm to the datasheet value 0.65 mm.)", + source: "BME280 Datasheet Section 7.5, Figure 20 (LGA PITCH 0.65, LGA SIZE 0.350x0.350)", + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const BME280: ModuleDef = defineModule({ + id: "bme280", + name: "Bosch BME280", + version: "1.1", + manufacturer: "Bosch Sensortec", + part_number: "BME280", + description: + "Bosch Sensortec BME280 is a bare LGA-8 combined digital humidity, barometric pressure, and temperature sensor. Tiny 2.5 x 2.5 x 0.93 mm package with I2C (up to 3.4 MHz) and SPI (3- and 4-wire, up to 10 MHz). Independent VDD (1.71-3.6 V) and VDDIO (1.2-3.6 V) supplies suit battery-powered and ultra-low-power designs at 3.6 uA running and 0.1 uA in sleep.", + tags: [ + "bme280", + "bosch", + "bosch-sensortec", + "environmental", + "humidity", + "pressure", + "temperature", + "barometer", + "i2c", + "spi", + "lga-8", + "low-power", + ], + categories: ["sensor", "sensor.environmental"], + + interfaces: [ + // All 8 physical pads in package order — schematic-honest. + ...pins, + + // Serial buses (shared pads; mode latched by CSB at power-on) + ...i2c, + ...spi4wire, + spi3wire, + + // Power + vddPowerIn, + vddioPowerIn, + + // Mechanical + pcbMount, + ], + + interfaceGroups: [ + { + id: "required_power_pins", + label: "Required Power Pins", + members: ["pin_1", "pin_6", "pin_7", "pin_8"], + policy: "all_of", + }, + { + id: "ground_common_net", + label: "Ground Pads (one common net)", + members: ["pin_1", "pin_7"], + policy: "all_of", + }, + { + // The device runs exactly one host interface, latched at power-on: + // CSB tied to VDDIO → I²C; CSB driven as chip-select → SPI (3- or + // 4-wire per the spi3w_en register bit). + id: "serial_interface_mode", + label: "Serial Interface Mode (I²C / SPI 4-wire / SPI 3-wire)", + members: ["i2c", "spi_4wire", "spi_3wire"], + policy: "one_of", + }, + ], + + requirements: [ + { + type: "power", + description: + "VDD analog and digital core supply: 1.71-3.6 V. Typical full-sensor current is 3.6 uA at 1 Hz output rate; 0.1 uA in sleep mode (§1.1). Do not exceed 4.25 V (absolute maximum).", + voltage_V: [1.71, 3.6], + current_mA: 1, + }, + { + type: "power", + description: + "VDDIO interface supply for CSB, SDI/SDA, SCK/SCL, and SDO: 1.2-3.6 V, independent of VDD. Do not exceed 4.25 V (absolute maximum).", + voltage_V: [1.2, 3.6], + current_mA: 1, + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + { + id: "vdd", + name: "VDD Analog Supply", + nominal_voltage_V: 3.3, + voltage_range_V: VDD_RANGE, + max_current_mA: 1, + }, + { + id: "vddio", + name: "VDDIO Interface Supply", + nominal_voltage_V: 1.8, + voltage_range_V: VDDIO_RANGE, + max_current_mA: 1, + }, + ], + metadata: { + pin_count: 8, + package_type: "LGA-8", + supply_voltage_V: [1.2, 3.6], + power_consumption_mW: 0.012, + max_operating_freq_Hz: 10_000_000, + power_domain_notes: { + vdd: "Analog and digital core supply. Typical full-sensor current is 3.6 uA at 1 Hz output rate; 0.1 uA in sleep mode. Datasheet section 1.1.", + vddio: + "Digital I/O supply for CSB, SDI/SDA, SCK/SCL, and SDO pins. Independent of VDD so the host can run logic at 1.8 V while VDD is 3.3 V. Datasheet section 1.1.", + }, + isolation_type: "non_isolated", + ground_reference: "common", + }, + }, + { + domain: "mechanical", + dimensions_mm: { length: 2.5, width: 2.5, height: 0.93 }, + metadata: { + package_type: "LGA-8", + pitch_mm: 0.65, + mounting_method: "surface_mount", + requires_special_tools: false, + field_serviceable: false, + lid: "metal lid (Datasheet section 7.5, Figure 20)", + }, + }, + { + domain: "thermal", + operating_temperature_C: [-40, 85], + metadata: { + requires_thermal_management: false, + thermal_monitoring_available: true, + cooling_method: "passive", + }, + }, + ], + + traits: [ + { + // ProtoPart design_rules — carried verbatim. + type: "design_rules", + params: { + rules: [ + "Tie CSB (pin 2) directly to VDDIO to select I2C mode; the device only enters I2C mode while CSB is high.", + "Do not leave SDO (pin 5) floating in I2C mode. Tie to GND for 7-bit address 0x76 or to VDDIO for 0x77.", + "Add external pull-up resistors to VDDIO on SDA (pin 3) and SCL (pin 4) when using I2C. A 4.7 kOhm pull-up is typical for moderate bus capacitance.", + "Place 100 nF decoupling capacitors close to VDD (pin 8) and VDDIO (pin 6).", + "Maximum I2C clock is 3.4 MHz (high-speed mode); maximum SPI clock is 10 MHz.", + "Do not exceed 4.25 V on VDD or VDDIO (datasheet absolute maximum ratings).", + "Avoid placing the sensor near localized heat sources; self-heating shifts the temperature reading and the derived humidity / altitude values.", + ], + }, + }, + { + type: "usage_notes", + params: { + note: "BME280 is a fine-pitch bare LGA IC, not a ready-to-wire breakout. Use it when the PCB can support the 2.5 mm LGA-8 footprint with two decoupling capacitors and external I2C pull-ups. For the simplest host integration, run it in I2C mode with CSB tied to VDDIO and SDO strapped to GND for address 0x76 (or VDDIO for 0x77). VDD and VDDIO can be driven from the same rail or from independent supplies (e.g., 3.3 V VDD + 1.8 V VDDIO).", + }, + }, + { + type: "application_examples", + params: { + examples: [ + "Indoor environmental monitoring (temperature, humidity, pressure)", + "Wearable / handheld altimeter and barometer", + "Indoor navigation with floor-change detection", + "HVAC control sensor node", + "Battery-powered IoT climate node where ultra-low-power sleep is critical", + "GPS dead-reckoning enhancement via barometric altitude", + ], + }, + }, + { + type: "compatibility_notes", + params: { + note: "Logic thresholds scale with VDDIO (datasheet section 6.4, interface parameter specification). Register-compatible with the BMP280 pressure sensor, so existing BMP280 drivers work with the BME280's pressure and temperature channels. The 7-bit I2C address must be 0x76 or 0x77; if two BME280s share a bus, strap SDO differently on each.", + }, + }, + { + type: "warnings", + params: { + warnings: [ + "Bare LGA-8 IC is not solderless-breadboard friendly. For prototyping, use a breakout board such as the Adafruit BME280 instead.", + "Do not exceed 4.25 V on VDD or VDDIO; absolute maximum ratings will damage the device.", + "The sensor is not waterproof. Long-term exposure to condensation can degrade humidity accuracy and may require reconditioning.", + "Self-heating from neighboring components or from VDD regulator dissipation distorts the temperature, humidity, and pressure outputs.", + ], + }, + }, + { + type: "terminology_policy", + params: { + note: "Signal and function names in `bme280_pin_functions` traits are datasheet-verbatim (Table 35) and intentionally NOT normalised to a curated whitelist; canonical capability tags exist only where slot matching requires them.", + }, + }, + ], + + artifacts: [ + { + id: "art_datasheet", + name: "BME280 Datasheet (BST-BME280-DS001-24, Rev 1.24, Feb 2024)", + type: "datasheet", + url: "https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf", + }, + { + id: "art_product_page", + name: "Bosch Sensortec BME280 Product Page", + type: "documentation", + url: "https://www.bosch-sensortec.com/en/products/environmental-sensors/humidity-sensors-bme280/", + }, + { + id: "art_product_image", + name: "BME280 product photo (DigiKey)", + type: "custom", + filePath: "./ProtoPart/protoparts/bme280/artifacts/images/MFG_BME280.jpg", + mimeType: "image/jpeg", + tags: ["image", "product-photo"], + }, + ], + + geometry: { + xScale: 1, + yScale: 1, + outline: { preset: "rectangle" }, + }, +}); diff --git a/library/parts/bme280/artifacts/thumbnail.png b/library/parts/bme280/artifacts/thumbnail.png new file mode 100644 index 0000000..049fac4 Binary files /dev/null and b/library/parts/bme280/artifacts/thumbnail.png differ diff --git a/library/parts/esp32-d0wdq6.ts b/library/parts/esp32-d0wdq6.ts new file mode 100644 index 0000000..a6153be --- /dev/null +++ b/library/parts/esp32-d0wdq6.ts @@ -0,0 +1,2228 @@ +/** + * Espressif ESP32-D0WDQ6 — datasheet-honest part definition. + * + * Primary source: ESP32 Series Datasheet v5.2 (2025.11) + * - Table 2-1 Pin Overview (pin numbers, names, functions — copied verbatim) + * - Table 2-5/2-6 Pin mapping between chip and flash/PSRAM + * - Table 3-1 Default configuration of strapping pins + * - Table 4-3/4-4 ADC characteristics and calibrated ranges + * - Table 4-6 Peripheral pin configurations (IO_MUX-fixed vs GPIO-matrix routable) + * - Table 5-1 Absolute maximum ratings (cumulative IO output current 1200 mA) + * - Table 5-2 Recommended power supply characteristics + * - Table 5-3 DC characteristics (I_OH per power domain, I_OL, 45 kΩ pulls) + * - Appendix A Notes on ESP32 pin lists (input-only pins, drive-strength options) + * Secondary sources are cited per-trait ("ESP32 TRM", "ESP32 Hardware Design + * Guidelines"); nothing below is carried over from convention or SDK defaults + * without attribution. + * + * Architecture notes honoured by this file: + * - Pin-honest like a schematic: all 49 physical pads (48 + exposed pad) are + * leaf interfaces, in package order, with ids "pin_N" and the datasheet + * pin name as the displayed name. + * - Every pin carries its verbatim datasheet function list (name, direction, + * routing) in an `esp32_pin_functions` trait — display data is separated + * from the canonical capability tags the matching engine needs. + * - Instances vs combinations: `max_instances` states how many controllers + * exist in silicon; slots + capability tags span the honest combination + * space (GPIO-matrix signals match any eligible pin via `matrix_in` / + * `matrix_out`); IO_MUX routings are captured as named profiles. + * - Co-requirements (`co_requirement` traits): ADC2 vs Wi-Fi, 40 MHz crystal + * for RF, MTDI strap vs VDD_SDIO voltage, 32K_XP/XN commitment, MTDO/GPIO5 + * strap vs SDIO-slave timing. + * - Implied harness connections (`implied_passives` traits): crystal load + * caps, BBPLL loop filter RC (datasheet-specified values), CHIP_PU reset + * RC, LNA π-matching network. + * - Shareability exemptions (`net_shareable` traits): the five analog supply + * pins are one decoupled net; a single supply instance may serve all of + * them (and GND is inherently shareable). + */ + +import type { + InterfaceDef, + ModuleDef, + SlotDef, + TraitDef, +} from "../../src/types/index.js"; +import type { Parameter } from "../../src/types/parameter.js"; +import { + Ground, + Pin, + PowerIn, + SPI, + UART, + I2C, + defineModule, + clockFreqHz, + resolutionBits, + voltageRangeV, +} from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Electrical constants — Datasheet Tables 5-1, 5-2, 5-3 +// --------------------------------------------------------------------------- + +/** Table 5-2: VDDA / VDD3P3 / VDD3P3_RTC / VDD_SDIO(3.3 V mode); min 2.3 V for chips with no in-package flash (D0WDQ6). */ +const VDD_33_RANGE: [number, number] = [2.3, 3.6]; +/** Table 5-2: VDD3P3_CPU may run down to 1.8 V. */ +const VDD_CPU_RANGE: [number, number] = [1.8, 3.6]; +/** Table 5-2 + note 1: VDD_SDIO is 1.8 V (internal LDO) or tracks VDD3P3_RTC. */ +const VDD_SDIO_RANGE: [number, number] = [1.8, 3.6]; + +/** Table 5-3: I_OH at maximum drive strength, VDD3P3_CPU / VDD3P3_RTC domains. */ +const IOH_RTC_CPU_MA = 40; +/** Table 5-3: I_OH typ, VDD_SDIO domain (see note 3 derating below). */ +const IOH_SDIO_MA = 20; +/** Table 5-3: I_OL at maximum drive strength, all domains. */ +const IOL_MA = 28; +/** Table 5-3: internal weak pull-up / pull-down resistance. */ +const RPU_RPD_KOHM = 45; + +/** Appendix A note 8: per-pin drive strength options; default setting is 2 (≈20 mA). */ +const DRIVE_STRENGTH_TRAIT: TraitDef = { + type: "drive_strength_options", + params: { + options_mA: [5, 10, 20, 40], + default_mA: 20, + source: "ESP32 Series Datasheet v5.2, Appendix A.1 note 8", + }, +}; + +type Esp32PowerDomain = "VDD3P3_RTC" | "VDD3P3_CPU" | "VDD_SDIO"; + +const DOMAIN_VOLTAGE: Record = { + VDD3P3_RTC: VDD_33_RANGE, + VDD3P3_CPU: VDD_CPU_RANGE, + VDD_SDIO: VDD_SDIO_RANGE, +}; + +const DOMAIN_SOURCE_MA: Record = { + VDD3P3_RTC: IOH_RTC_CPU_MA, + VDD3P3_CPU: IOH_RTC_CPU_MA, + VDD_SDIO: IOH_SDIO_MA, +}; + +// --------------------------------------------------------------------------- +// Datasheet-honest per-pin function metadata +// --------------------------------------------------------------------------- + +/** How a function reaches the pad (Datasheet Table 4-6 / TRM IO_MUX chapter). */ +type Esp32Via = "io_mux" | "rtc_mux" | "analog" | "gpio_matrix"; + +interface Esp32Function { + /** Verbatim signal name from Datasheet Table 2-1 / Table 4-6. */ + name: string; + /** Datasheet Type column: I = input, O = output, "I/O" = bidirectional. */ + direction: "I" | "O" | "I/O"; + via: Esp32Via; + /** + * Optional display override for notation the plain name cannot carry + * (e.g. active-low overlines on other parts). Unused on the ESP32 but part + * of the honest-display contract. + */ + display?: string; + note?: string; +} + +interface Esp32GpioSpec { + pin: number; + /** Datasheet pin name (Table 2-1) — used as the displayed interface name. */ + name: string; + gpio: number; + domain: Esp32PowerDomain; + /** GPIO34–39: no output driver, no internal pull resistors (Appendix A note 2). */ + inputOnly?: boolean; + functions: Esp32Function[]; + /** Capability tags for IO_MUX-fixed signals (slot matching). */ + fixedCaps?: string[]; + adc?: { converter: 1 | 2; channel: number }; + dac?: 1 | 2; + touch?: number; + rtcGpio?: number; + /** Table 3-1 strapping defaults + Section 3 boot parameter it controls. */ + strapping?: { defaultPull: "pull-up" | "pull-down"; bitValue: 0 | 1; controls: string }; + /** Table 2-6: allocated to the external SPI flash on a normally-booting design. */ + flashReserved?: boolean; + notes?: string; +} + +function unique(values: string[]): string[] { + return [...new Set(values)]; +} + +/** Build one schematic-honest GPIO pad from its datasheet row. */ +function esp32Gpio(spec: Esp32GpioSpec): InterfaceDef { + const outputCapable = !spec.inputOnly; + + const base = Pin({ + id: `pin_${spec.pin}`, + name: spec.name, + pin: spec.pin, + voltageV: DOMAIN_VOLTAGE[spec.domain], + capabilities: { + inputOnly: spec.inputOnly, + interrupt: true, + pwm: outputCapable, // LEDC/MCPWM: any output-capable GPIO via GPIO matrix (Table 4-6) + analogIn: spec.adc !== undefined, + analogOut: spec.dac !== undefined, + touch: spec.touch !== undefined, + // I2C0/1 route via GPIO matrix to any output-capable GPIO (Table 4-6) + i2cSda: outputCapable, + i2cScl: outputCapable, + // UART0/1/2 route via GPIO matrix to any GPIO; TX/RTS need an output driver + uartRx: true, + uartCts: true, + uartTx: outputCapable, + uartRts: outputCapable, + }, + }); + + const capabilities = unique([ + ...(base.capabilities ?? []), + `gpio${spec.gpio}`, + // GPIO-matrix routability (Table 4-6 "Any GPIO Pins" rows): the honest + // combination space for TWAI / RMT / PCNT / I2S / EMAC MDC-MDIO / etc. + "matrix_in", + ...(outputCapable ? ["matrix_out"] : []), + ...(spec.adc ? [`adc${spec.adc.converter}_in`] : []), + ...(spec.dac ? ["dac_out"] : []), + ...(spec.rtcGpio !== undefined ? ["rtc_gpio"] : []), + ...(spec.fixedCaps ?? []), + ]); + + const parameters: Parameter[] = [ + ...(base.parameters ?? []), + ...(outputCapable + ? [ + { id: "source_current", name: "I_OH (max drive strength)", unit: "mA", value: DOMAIN_SOURCE_MA[spec.domain] }, + { id: "sink_current", name: "I_OL (max drive strength)", unit: "mA", value: IOL_MA }, + ] + : []), + ]; + + const traits: TraitDef[] = [ + { + type: "esp32_pin_functions", + params: { + source: "ESP32 Series Datasheet v5.2, Table 2-1 / Table 4-6", + functions: spec.functions, + ...(spec.notes ? { note: spec.notes } : {}), + }, + }, + { + type: "power_domain", + params: { + domain: spec.domain, + ...(spec.domain === "VDD_SDIO" + ? { note: "Pad voltage tracks VDD_SDIO: 1.8 V or VDD3P3_RTC depending on the MTDI strap / eFuse / register setting." } + : {}), + }, + }, + spec.inputOnly + ? { + type: "internal_pulls", + params: { + available: false, + reason: "GPIO34-39 have no output driver and no internal pull-up/pull-down circuitry (Appendix A.1 note 2).", + }, + } + : { + type: "internal_pulls", + params: { available: true, pull_up_kOhm: RPU_RPD_KOHM, pull_down_kOhm: RPU_RPD_KOHM, programmable: true }, + }, + ...(outputCapable ? [DRIVE_STRENGTH_TRAIT] : []), + ...(spec.domain === "VDD_SDIO" && outputCapable + ? [ + { + type: "current_derating", + params: { + rule: "VDD_SDIO-domain source current falls from ~30 mA to ~10 mA per pin as the number of simultaneously sourcing pins in the domain increases.", + source: "ESP32 Series Datasheet v5.2, Table 5-3 note 3", + }, + }, + ] + : []), + ...(spec.strapping + ? [ + { + type: "boot_strapping", + params: { + default_pull: spec.strapping.defaultPull, + bit_value: spec.strapping.bitValue, + controls: spec.strapping.controls, + sampling: "Latched on the rising edge of CHIP_PU; hold ≥1 ms after CHIP_PU is high (Table 3-2).", + source: "ESP32 Series Datasheet v5.2, Section 3 / Table 3-1", + }, + }, + ] + : []), + ...(spec.flashReserved + ? [ + { + type: "usage_restriction", + params: { + restriction: "Allocated to the external SPI flash on any normally-booting design (Table 2-6); repurposing breaks default ROM boot.", + exemption: "Free when the design boots from a flash wired to an alternative mapping — the restriction is conditional, not absolute.", + }, + }, + ] + : []), + ]; + + return { ...base, capabilities, parameters, traits }; +} + +// --------------------------------------------------------------------------- +// GPIO pads — Datasheet Table 2-1, in package pin order +// --------------------------------------------------------------------------- + +const GPIO_SPECS: Esp32GpioSpec[] = [ + { + pin: 5, name: "SENSOR_VP", gpio: 36, domain: "VDD3P3_RTC", inputOnly: true, + adc: { converter: 1, channel: 0 }, rtcGpio: 0, + functions: [ + { name: "GPIO36", direction: "I", via: "io_mux" }, + { name: "ADC1_CH0", direction: "I", via: "analog" }, + { name: "RTC_GPIO0", direction: "I", via: "rtc_mux" }, + ], + }, + { + pin: 6, name: "SENSOR_CAPP", gpio: 37, domain: "VDD3P3_RTC", inputOnly: true, + adc: { converter: 1, channel: 1 }, rtcGpio: 1, + functions: [ + { name: "GPIO37", direction: "I", via: "io_mux" }, + { name: "ADC1_CH1", direction: "I", via: "analog" }, + { name: "RTC_GPIO1", direction: "I", via: "rtc_mux" }, + ], + }, + { + pin: 7, name: "SENSOR_CAPN", gpio: 38, domain: "VDD3P3_RTC", inputOnly: true, + adc: { converter: 1, channel: 2 }, rtcGpio: 2, + functions: [ + { name: "GPIO38", direction: "I", via: "io_mux" }, + { name: "ADC1_CH2", direction: "I", via: "analog" }, + { name: "RTC_GPIO2", direction: "I", via: "rtc_mux" }, + ], + }, + { + pin: 8, name: "SENSOR_VN", gpio: 39, domain: "VDD3P3_RTC", inputOnly: true, + adc: { converter: 1, channel: 3 }, rtcGpio: 3, + functions: [ + { name: "GPIO39", direction: "I", via: "io_mux" }, + { name: "ADC1_CH3", direction: "I", via: "analog" }, + { name: "RTC_GPIO3", direction: "I", via: "rtc_mux" }, + ], + }, + { + pin: 10, name: "VDET_1", gpio: 34, domain: "VDD3P3_RTC", inputOnly: true, + adc: { converter: 1, channel: 6 }, rtcGpio: 4, + functions: [ + { name: "GPIO34", direction: "I", via: "io_mux" }, + { name: "ADC1_CH6", direction: "I", via: "analog" }, + { name: "RTC_GPIO4", direction: "I", via: "rtc_mux" }, + ], + }, + { + pin: 11, name: "VDET_2", gpio: 35, domain: "VDD3P3_RTC", inputOnly: true, + adc: { converter: 1, channel: 7 }, rtcGpio: 5, + functions: [ + { name: "GPIO35", direction: "I", via: "io_mux" }, + { name: "ADC1_CH7", direction: "I", via: "analog" }, + { name: "RTC_GPIO5", direction: "I", via: "rtc_mux" }, + ], + }, + { + pin: 12, name: "32K_XP", gpio: 32, domain: "VDD3P3_RTC", + adc: { converter: 1, channel: 4 }, touch: 9, rtcGpio: 9, + fixedCaps: ["xtal_32k_p"], + functions: [ + { name: "GPIO32", direction: "I/O", via: "io_mux" }, + { name: "ADC1_CH4", direction: "I", via: "analog" }, + { name: "RTC_GPIO9", direction: "I/O", via: "rtc_mux" }, + { name: "TOUCH9", direction: "I", via: "analog" }, + { name: "32K_XP", direction: "I", via: "analog", note: "32.768 kHz crystal oscillator input" }, + ], + }, + { + pin: 13, name: "32K_XN", gpio: 33, domain: "VDD3P3_RTC", + adc: { converter: 1, channel: 5 }, touch: 8, rtcGpio: 8, + fixedCaps: ["xtal_32k_n"], + functions: [ + { name: "GPIO33", direction: "I/O", via: "io_mux" }, + { name: "ADC1_CH5", direction: "I", via: "analog" }, + { name: "RTC_GPIO8", direction: "I/O", via: "rtc_mux" }, + { name: "TOUCH8", direction: "I", via: "analog" }, + { name: "32K_XN", direction: "O", via: "analog", note: "32.768 kHz crystal oscillator output" }, + ], + }, + { + pin: 14, name: "GPIO25", gpio: 25, domain: "VDD3P3_RTC", + adc: { converter: 2, channel: 8 }, dac: 1, rtcGpio: 6, + fixedCaps: ["emac_rxd0"], + functions: [ + { name: "GPIO25", direction: "I/O", via: "io_mux" }, + { name: "ADC2_CH8", direction: "I", via: "analog" }, + { name: "RTC_GPIO6", direction: "I/O", via: "rtc_mux" }, + { name: "DAC_1", direction: "O", via: "analog" }, + { name: "EMAC_RXD0", direction: "I", via: "io_mux" }, + ], + }, + { + pin: 15, name: "GPIO26", gpio: 26, domain: "VDD3P3_RTC", + adc: { converter: 2, channel: 9 }, dac: 2, rtcGpio: 7, + fixedCaps: ["emac_rxd1"], + functions: [ + { name: "GPIO26", direction: "I/O", via: "io_mux" }, + { name: "ADC2_CH9", direction: "I", via: "analog" }, + { name: "RTC_GPIO7", direction: "I/O", via: "rtc_mux" }, + { name: "DAC_2", direction: "O", via: "analog" }, + { name: "EMAC_RXD1", direction: "I", via: "io_mux" }, + ], + }, + { + pin: 16, name: "GPIO27", gpio: 27, domain: "VDD3P3_RTC", + adc: { converter: 2, channel: 7 }, touch: 7, rtcGpio: 17, + fixedCaps: ["emac_rx_dv"], + functions: [ + { name: "GPIO27", direction: "I/O", via: "io_mux" }, + { name: "ADC2_CH7", direction: "I", via: "analog" }, + { name: "RTC_GPIO17", direction: "I/O", via: "rtc_mux" }, + { name: "TOUCH7", direction: "I", via: "analog" }, + { name: "EMAC_RX_DV", direction: "I", via: "io_mux", note: "CRS_DV in RMII mode" }, + ], + }, + { + pin: 17, name: "MTMS", gpio: 14, domain: "VDD3P3_RTC", + adc: { converter: 2, channel: 6 }, touch: 6, rtcGpio: 16, + fixedCaps: ["spi_sck", "hspi_clk", "sdio_host_clk", "hs2_clk", "sdio_slave_clk", "jtag_tms", "emac_txd2"], + functions: [ + { name: "GPIO14", direction: "I/O", via: "io_mux" }, + { name: "ADC2_CH6", direction: "I", via: "analog" }, + { name: "RTC_GPIO16", direction: "I/O", via: "rtc_mux" }, + { name: "TOUCH6", direction: "I", via: "analog" }, + { name: "EMAC_TXD2", direction: "O", via: "io_mux" }, + { name: "HSPICLK", direction: "I/O", via: "io_mux" }, + { name: "HS2_CLK", direction: "O", via: "io_mux", note: "SDIO host port 2 clock" }, + { name: "SD_CLK", direction: "I", via: "io_mux", note: "SDIO slave clock" }, + { name: "MTMS", direction: "I", via: "io_mux", note: "JTAG TMS" }, + ], + }, + { + pin: 18, name: "MTDI", gpio: 12, domain: "VDD3P3_RTC", + adc: { converter: 2, channel: 5 }, touch: 5, rtcGpio: 15, + strapping: { defaultPull: "pull-down", bitValue: 0, controls: "Internal LDO (VDD_SDIO) voltage: low = 3.3 V (default), high = 1.8 V" }, + fixedCaps: ["spi_miso", "hspi_q", "sdio_host_data2", "hs2_data2", "sdio_slave_data2", "jtag_tdi", "emac_txd3"], + functions: [ + { name: "GPIO12", direction: "I/O", via: "io_mux" }, + { name: "ADC2_CH5", direction: "I", via: "analog" }, + { name: "RTC_GPIO15", direction: "I/O", via: "rtc_mux" }, + { name: "TOUCH5", direction: "I", via: "analog" }, + { name: "EMAC_TXD3", direction: "O", via: "io_mux" }, + { name: "HSPIQ", direction: "I/O", via: "io_mux" }, + { name: "HS2_DATA2", direction: "I/O", via: "io_mux" }, + { name: "SD_DATA2", direction: "I/O", via: "io_mux" }, + { name: "MTDI", direction: "I", via: "io_mux", note: "JTAG TDI" }, + ], + }, + { + pin: 20, name: "MTCK", gpio: 13, domain: "VDD3P3_RTC", + adc: { converter: 2, channel: 4 }, touch: 4, rtcGpio: 14, + fixedCaps: ["spi_mosi", "hspi_d", "sdio_host_data3", "hs2_data3", "sdio_slave_data3", "jtag_tck", "emac_rx_er"], + functions: [ + { name: "GPIO13", direction: "I/O", via: "io_mux" }, + { name: "ADC2_CH4", direction: "I", via: "analog" }, + { name: "RTC_GPIO14", direction: "I/O", via: "rtc_mux" }, + { name: "TOUCH4", direction: "I", via: "analog" }, + { name: "EMAC_RX_ER", direction: "I", via: "io_mux" }, + { name: "HSPID", direction: "I/O", via: "io_mux" }, + { name: "HS2_DATA3", direction: "I/O", via: "io_mux" }, + { name: "SD_DATA3", direction: "I/O", via: "io_mux" }, + { name: "MTCK", direction: "I", via: "io_mux", note: "JTAG TCK" }, + ], + }, + { + pin: 21, name: "MTDO", gpio: 15, domain: "VDD3P3_RTC", + adc: { converter: 2, channel: 3 }, touch: 3, rtcGpio: 13, + strapping: { defaultPull: "pull-up", bitValue: 1, controls: "U0TXD ROM-boot log printing; together with GPIO5, timing of the SDIO slave" }, + fixedCaps: ["spi_ss", "hspi_cs0", "sdio_host_cmd", "hs2_cmd", "sdio_slave_cmd", "jtag_tdo", "emac_rxd3"], + functions: [ + { name: "GPIO15", direction: "I/O", via: "io_mux" }, + { name: "ADC2_CH3", direction: "I", via: "analog" }, + { name: "RTC_GPIO13", direction: "I/O", via: "rtc_mux" }, + { name: "TOUCH3", direction: "I", via: "analog" }, + { name: "EMAC_RXD3", direction: "I", via: "io_mux" }, + { name: "HSPICS0", direction: "I/O", via: "io_mux" }, + { name: "HS2_CMD", direction: "I/O", via: "io_mux" }, + { name: "SD_CMD", direction: "I/O", via: "io_mux" }, + { name: "MTDO", direction: "O", via: "io_mux", note: "JTAG TDO" }, + ], + }, + { + pin: 22, name: "GPIO2", gpio: 2, domain: "VDD3P3_RTC", + adc: { converter: 2, channel: 2 }, touch: 2, rtcGpio: 12, + strapping: { defaultPull: "pull-down", bitValue: 0, controls: "Chip boot mode (together with GPIO0): must be low or floating to enter serial download mode" }, + fixedCaps: ["hspi_wp", "sdio_host_data0", "hs2_data0", "sdio_slave_data0"], + functions: [ + { name: "GPIO2", direction: "I/O", via: "io_mux" }, + { name: "ADC2_CH2", direction: "I", via: "analog" }, + { name: "RTC_GPIO12", direction: "I/O", via: "rtc_mux" }, + { name: "TOUCH2", direction: "I", via: "analog" }, + { name: "HSPIWP", direction: "I/O", via: "io_mux" }, + { name: "HS2_DATA0", direction: "I/O", via: "io_mux" }, + { name: "SD_DATA0", direction: "I/O", via: "io_mux" }, + ], + }, + { + pin: 23, name: "GPIO0", gpio: 0, domain: "VDD3P3_RTC", + adc: { converter: 2, channel: 1 }, touch: 1, rtcGpio: 11, + strapping: { defaultPull: "pull-up", bitValue: 1, controls: "Chip boot mode: high = SPI flash boot (default), low = serial download mode" }, + fixedCaps: ["emac_tx_clk", "clk_out"], + functions: [ + { name: "GPIO0", direction: "I/O", via: "io_mux" }, + { name: "ADC2_CH1", direction: "I", via: "analog" }, + { name: "RTC_GPIO11", direction: "I/O", via: "rtc_mux" }, + { name: "TOUCH1", direction: "I", via: "analog" }, + { name: "EMAC_TX_CLK", direction: "I", via: "io_mux", note: "MII TX clock in; RMII 50 MHz REF_CLK in, or CLK_OUT with internal PLL" }, + { name: "CLK_OUT1", direction: "O", via: "io_mux" }, + ], + }, + { + pin: 24, name: "GPIO4", gpio: 4, domain: "VDD3P3_RTC", + adc: { converter: 2, channel: 0 }, touch: 0, rtcGpio: 10, + fixedCaps: ["hspi_hd", "sdio_host_data1", "hs2_data1", "sdio_slave_data1", "emac_tx_er"], + functions: [ + { name: "GPIO4", direction: "I/O", via: "io_mux" }, + { name: "ADC2_CH0", direction: "I", via: "analog" }, + { name: "RTC_GPIO10", direction: "I/O", via: "rtc_mux" }, + { name: "TOUCH0", direction: "I", via: "analog" }, + { name: "EMAC_TX_ER", direction: "O", via: "io_mux" }, + { name: "HSPIHD", direction: "I/O", via: "io_mux" }, + { name: "HS2_DATA1", direction: "I/O", via: "io_mux" }, + { name: "SD_DATA1", direction: "I/O", via: "io_mux" }, + ], + }, + { + // Datasheet v5.2 Table 2-1 groups pins 25-33 under the VDD_SDIO power + // domain — GPIO16/GPIO17 are NOT VDD3P3_CPU pins. + pin: 25, name: "GPIO16", gpio: 16, domain: "VDD_SDIO", + fixedCaps: ["sdio_host_data4", "hs1_data4", "emac_clk_out"], + notes: "Unavailable on modules with in-package/off-package PSRAM (used as PSRAM CE#).", + functions: [ + { name: "GPIO16", direction: "I/O", via: "io_mux" }, + { name: "HS1_DATA4", direction: "I/O", via: "io_mux" }, + { name: "U2RXD", direction: "I", via: "io_mux" }, + { name: "EMAC_CLK_OUT", direction: "O", via: "io_mux" }, + ], + }, + { + pin: 27, name: "GPIO17", gpio: 17, domain: "VDD_SDIO", + fixedCaps: ["sdio_host_data5", "hs1_data5", "emac_clk_out_180"], + notes: "Unavailable on modules with in-package/off-package PSRAM (PSRAM SCLK option).", + functions: [ + { name: "GPIO17", direction: "I/O", via: "io_mux" }, + { name: "HS1_DATA5", direction: "I/O", via: "io_mux" }, + { name: "U2TXD", direction: "O", via: "io_mux" }, + { name: "EMAC_CLK_OUT_180", direction: "O", via: "io_mux" }, + ], + }, + { + pin: 28, name: "SD_DATA_2", gpio: 9, domain: "VDD_SDIO", flashReserved: true, + fixedCaps: ["spi01_hd", "sdio_host_data2", "hs1_data2", "sdio_slave_data2"], + functions: [ + { name: "GPIO9", direction: "I/O", via: "io_mux" }, + { name: "HS1_DATA2", direction: "I/O", via: "io_mux" }, + { name: "U1RXD", direction: "I", via: "io_mux" }, + { name: "SD_DATA2", direction: "I/O", via: "io_mux" }, + { name: "SPIHD", direction: "I/O", via: "io_mux", note: "Flash IO3/HOLD# (Table 2-6)" }, + ], + }, + { + pin: 29, name: "SD_DATA_3", gpio: 10, domain: "VDD_SDIO", flashReserved: true, + fixedCaps: ["spi01_wp", "sdio_host_data3", "hs1_data3", "sdio_slave_data3"], + functions: [ + { name: "GPIO10", direction: "I/O", via: "io_mux" }, + { name: "HS1_DATA3", direction: "I/O", via: "io_mux" }, + { name: "U1TXD", direction: "O", via: "io_mux" }, + { name: "SD_DATA3", direction: "I/O", via: "io_mux" }, + { name: "SPIWP", direction: "I/O", via: "io_mux", note: "Flash IO2/WP# (Table 2-6)" }, + ], + }, + { + pin: 30, name: "SD_CMD", gpio: 11, domain: "VDD_SDIO", flashReserved: true, + fixedCaps: ["spi01_cs0", "sdio_host_cmd", "hs1_cmd", "sdio_slave_cmd"], + functions: [ + { name: "GPIO11", direction: "I/O", via: "io_mux" }, + { name: "HS1_CMD", direction: "I/O", via: "io_mux" }, + { name: "U1RTS", direction: "O", via: "io_mux" }, + { name: "SD_CMD", direction: "I/O", via: "io_mux" }, + { name: "SPICS0", direction: "I/O", via: "io_mux", note: "Flash CS# (Table 2-6)" }, + ], + }, + { + pin: 31, name: "SD_CLK", gpio: 6, domain: "VDD_SDIO", flashReserved: true, + fixedCaps: ["spi01_clk", "sdio_host_clk", "hs1_clk", "sdio_slave_clk"], + functions: [ + { name: "GPIO6", direction: "I/O", via: "io_mux" }, + { name: "HS1_CLK", direction: "O", via: "io_mux" }, + { name: "U1CTS", direction: "I", via: "io_mux" }, + { name: "SD_CLK", direction: "I", via: "io_mux" }, + { name: "SPICLK", direction: "I/O", via: "io_mux", note: "Flash CLK (Table 2-6)" }, + ], + }, + { + pin: 32, name: "SD_DATA_0", gpio: 7, domain: "VDD_SDIO", flashReserved: true, + fixedCaps: ["spi01_q", "sdio_host_data0", "hs1_data0", "sdio_slave_data0"], + functions: [ + { name: "GPIO7", direction: "I/O", via: "io_mux" }, + { name: "HS1_DATA0", direction: "I/O", via: "io_mux" }, + { name: "U2RTS", direction: "O", via: "io_mux" }, + { name: "SD_DATA0", direction: "I/O", via: "io_mux" }, + { name: "SPIQ", direction: "I/O", via: "io_mux", note: "Flash IO1/DO (Table 2-6)" }, + ], + }, + { + pin: 33, name: "SD_DATA_1", gpio: 8, domain: "VDD_SDIO", flashReserved: true, + fixedCaps: ["spi01_d", "sdio_host_data1", "hs1_data1", "sdio_slave_data1"], + functions: [ + { name: "GPIO8", direction: "I/O", via: "io_mux" }, + { name: "HS1_DATA1", direction: "I/O", via: "io_mux" }, + { name: "U2CTS", direction: "I", via: "io_mux" }, + { name: "SD_DATA1", direction: "I/O", via: "io_mux" }, + { name: "SPID", direction: "I/O", via: "io_mux", note: "Flash IO0/DI (Table 2-6)" }, + ], + }, + { + pin: 34, name: "GPIO5", gpio: 5, domain: "VDD3P3_CPU", + strapping: { defaultPull: "pull-up", bitValue: 1, controls: "Together with MTDO, timing of the SDIO slave" }, + fixedCaps: ["spi_ss", "vspi_cs0", "sdio_host_data6", "hs1_data6", "emac_rx_clk"], + functions: [ + { name: "GPIO5", direction: "I/O", via: "io_mux" }, + { name: "HS1_DATA6", direction: "I/O", via: "io_mux" }, + { name: "VSPICS0", direction: "I/O", via: "io_mux" }, + { name: "EMAC_RX_CLK", direction: "I", via: "io_mux", note: "MII only" }, + ], + }, + { + pin: 35, name: "GPIO18", gpio: 18, domain: "VDD3P3_CPU", + fixedCaps: ["spi_sck", "vspi_clk", "sdio_host_data7", "hs1_data7"], + functions: [ + { name: "GPIO18", direction: "I/O", via: "io_mux" }, + { name: "HS1_DATA7", direction: "I/O", via: "io_mux" }, + { name: "VSPICLK", direction: "I/O", via: "io_mux" }, + ], + }, + { + pin: 36, name: "GPIO23", gpio: 23, domain: "VDD3P3_CPU", + fixedCaps: ["spi_mosi", "vspi_d", "sdio_host_strobe", "hs1_strobe"], + functions: [ + { name: "GPIO23", direction: "I/O", via: "io_mux" }, + { name: "HS1_STROBE", direction: "I", via: "io_mux" }, + { name: "VSPID", direction: "I/O", via: "io_mux" }, + ], + }, + { + pin: 38, name: "GPIO19", gpio: 19, domain: "VDD3P3_CPU", + fixedCaps: ["spi_miso", "vspi_q", "emac_txd0"], + functions: [ + { name: "GPIO19", direction: "I/O", via: "io_mux" }, + { name: "U0CTS", direction: "I", via: "io_mux" }, + { name: "VSPIQ", direction: "I/O", via: "io_mux" }, + { name: "EMAC_TXD0", direction: "O", via: "io_mux" }, + ], + }, + { + pin: 39, name: "GPIO22", gpio: 22, domain: "VDD3P3_CPU", + fixedCaps: ["vspi_wp", "emac_txd1"], + functions: [ + { name: "GPIO22", direction: "I/O", via: "io_mux" }, + { name: "U0RTS", direction: "O", via: "io_mux" }, + { name: "VSPIWP", direction: "I/O", via: "io_mux" }, + { name: "EMAC_TXD1", direction: "O", via: "io_mux" }, + ], + }, + { + pin: 40, name: "U0RXD", gpio: 3, domain: "VDD3P3_CPU", + fixedCaps: ["clk_out"], + notes: "Default UART0 RX — used by the ROM bootloader for flashing.", + functions: [ + { name: "GPIO3", direction: "I/O", via: "io_mux" }, + { name: "U0RXD", direction: "I", via: "io_mux" }, + { name: "CLK_OUT2", direction: "O", via: "io_mux" }, + ], + }, + { + pin: 41, name: "U0TXD", gpio: 1, domain: "VDD3P3_CPU", + fixedCaps: ["clk_out", "emac_rxd2"], + notes: "Default UART0 TX — the ROM bootloader prints on this pin at reset unless silenced by the MTDO strap.", + functions: [ + { name: "GPIO1", direction: "I/O", via: "io_mux" }, + { name: "U0TXD", direction: "O", via: "io_mux" }, + { name: "CLK_OUT3", direction: "O", via: "io_mux" }, + { name: "EMAC_RXD2", direction: "I", via: "io_mux" }, + ], + }, + { + pin: 42, name: "GPIO21", gpio: 21, domain: "VDD3P3_CPU", + fixedCaps: ["vspi_hd", "emac_tx_en"], + functions: [ + { name: "GPIO21", direction: "I/O", via: "io_mux" }, + { name: "VSPIHD", direction: "I/O", via: "io_mux" }, + { name: "EMAC_TX_EN", direction: "O", via: "io_mux" }, + ], + }, +]; + +const gpioPads: InterfaceDef[] = GPIO_SPECS.map(esp32Gpio); + +// --------------------------------------------------------------------------- +// Power, RF, crystal, and analog service pads — Datasheet Table 2-1 +// --------------------------------------------------------------------------- + +/** The five analog supply pads (1, 3, 4, 43, 46) sit on one decoupled 3.3 V net. */ +function analogSupplyPin(pinNo: number, name: string): InterfaceDef { + const base = PowerIn({ + id: `pin_${pinNo}`, + name, + pin: pinNo, + voltageV: VDD_33_RANGE, + nominalV: 3.3, + }); + return { + ...base, + capabilities: ["power_in", "analog_supply"], + traits: [ + { type: "power_domain", params: { domain: "VDDA (analog power domain)" } }, + { + // Shareability exemption: one supply instance may legally serve all + // five pads — pin-usage exclusivity does not apply to a shared rail. + type: "net_shareable", + params: { + net: "esp32_analog_3v3", + policy: "single_supply_instance_may_serve_all_members", + members: ["pin_1", "pin_3", "pin_4", "pin_43", "pin_46"], + }, + }, + { + type: "implied_passives", + params: { + purpose: "Supply decoupling", + components: [{ kind: "capacitor", value: "100 nF", connection: "pin to GND, close to the pad" }], + source: "ESP32 Hardware Design Guidelines", + }, + }, + ], + }; +} + +const powerAndServicePads: InterfaceDef[] = [ + analogSupplyPin(1, "VDDA"), + + { + id: "pin_2", + name: "LNA_IN", + pin: 2, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "rf", roles: ["transceiver"] }], + capabilities: ["rf_2g4_antenna_feed"], + parameters: [ + // Table 5-6 note 2: Wi-Fi radio output impedance for the QFN 6*6 package. + { id: "impedance", name: "Radio output impedance (real part)", unit: "Ω", value: 30 }, + { id: "frequency", unit: "Hz", range: [2_400_000_000, 2_484_000_000] }, + ], + traits: [ + { + type: "rf_port", + params: { + description: "Low Noise Amplifier (LNA) input signal, Power Amplifier (PA) output signal — shared Wi-Fi/Bluetooth feed.", + output_impedance: "30 + j10 Ω (QFN 6*6 package)", + source: "ESP32 Series Datasheet v5.2, Table 2-2 / Table 5-6 note 2", + }, + }, + { + type: "implied_passives", + params: { + purpose: "Impedance matching from 30 + j10 Ω to a 50 Ω antenna", + components: [{ kind: "pi_network", value: "CLC π-matching network, values per board layout", connection: "LNA_IN to antenna" }], + source: "ESP32 Hardware Design Guidelines", + }, + }, + ], + bridgesTo: ["wifi_radio", "bluetooth_radio"], + }, + + analogSupplyPin(3, "VDD3P3"), + analogSupplyPin(4, "VDD3P3"), + + { + id: "pin_9", + name: "CHIP_PU", + pin: 9, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input"] }], + capabilities: ["chip_enable", "reset_input"], + parameters: [ + { id: "voltage", unit: "V", range: VDD_33_RANGE }, + // Table 5-3: V_IL_nRST — low level that shuts the chip down. + { id: "reset_low_threshold", name: "V_IL_nRST (max)", unit: "V", value: 0.6 }, + ], + traits: [ + { + type: "esp32_pin_functions", + params: { + source: "ESP32 Series Datasheet v5.2, Table 2-1 / 2-2", + functions: [ + { name: "CHIP_PU", direction: "I", via: "analog", note: "High: on, enables the chip. Low: off, the chip powers down. Do not leave floating." }, + ], + }, + }, + { type: "internal_pulls", params: { available: false, reason: "CHIP_PU has no internal pull — it must be driven or pulled externally." } }, + { + type: "power_up_timing", + params: { + t_STBL_min_us: 50, + t_RST_min_us: 50, + description: "Rails must be stable ≥50 µs before CHIP_PU rises; hold below V_IL_nRST ≥50 µs to reset.", + source: "ESP32 Series Datasheet v5.2, Table 2-4", + }, + }, + { + type: "implied_passives", + params: { + purpose: "Clean power-on reset", + components: [ + { kind: "resistor", value: "10 kΩ", connection: "CHIP_PU to VDD3P3_RTC" }, + { kind: "capacitor", value: "1 nF", connection: "CHIP_PU to GND" }, + ], + source: "ESP32 Hardware Design Guidelines", + }, + }, + ], + }, + + PowerIn({ id: "pin_19", name: "VDD3P3_RTC", pin: 19, voltageV: VDD_33_RANGE, nominalV: 3.3 }), + + { + id: "pin_26", + name: "VDD_SDIO", + pin: 26, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "power", roles: ["input", "output"] }], + capabilities: ["vdd_sdio"], + parameters: [{ id: "voltage", unit: "V", value: 3.3, range: VDD_SDIO_RANGE }], + traits: [ + { + type: "internal_regulator", + params: { + description: "Output of the internal SDIO-LDO: 1.8 V, or the same voltage as VDD3P3_RTC. May instead be driven by an external supply (the LDO disables automatically).", + source: "ESP32 Series Datasheet v5.2, Table 2-1 / Appendix A.1 note 3", + }, + }, + { + type: "co_requirement", + params: { + with: "pin_18", + condition: "MTDI strap sampled at reset", + effect: "MTDI low (default pull-down) selects 3.3 V; MTDI high selects 1.8 V. eFuse bits EFUSE_SDIO_FORCE / EFUSE_SDIO_TIEH override; software can reconfigure at runtime.", + source: "ESP32 Series Datasheet v5.2, Section 3", + }, + }, + ], + }, + + PowerIn({ id: "pin_37", name: "VDD3P3_CPU", pin: 37, voltageV: VDD_CPU_RANGE, nominalV: 3.3 }), + + analogSupplyPin(43, "VDDA"), + + { + id: "pin_44", + name: "XTAL_N", + pin: 44, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "clock", roles: ["output"] }], + capabilities: ["xtal_n"], + parameters: [{ id: "clock_freq", unit: "Hz", range: [2_000_000, 60_000_000] }], + traits: [ + { type: "esp32_pin_functions", params: { source: "Datasheet v5.2 Table 2-1/2-2", functions: [{ name: "XTAL_N", direction: "O", via: "analog", note: "External crystal output (differential clock negative)" }] } }, + ], + }, + { + id: "pin_45", + name: "XTAL_P", + pin: 45, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "clock", roles: ["input"] }], + capabilities: ["xtal_p"], + parameters: [{ id: "clock_freq", unit: "Hz", range: [2_000_000, 60_000_000] }], + traits: [ + { type: "esp32_pin_functions", params: { source: "Datasheet v5.2 Table 2-1/2-2", functions: [{ name: "XTAL_P", direction: "I", via: "analog", note: "External crystal input (differential clock positive)" }] } }, + ], + }, + + analogSupplyPin(46, "VDDA"), + + { + id: "pin_47", + name: "CAP2", + pin: 47, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "analog", roles: ["input"] }], + capabilities: ["bbpll_cap2"], + traits: [ + { + type: "implied_passives", + params: { + purpose: "BBPLL loop filter", + components: [ + { kind: "capacitor", value: "3.3 nF (10%)", connection: "CAP2 to CAP1 (in parallel with the resistor)" }, + { kind: "resistor", value: "20 kΩ", connection: "CAP2 to CAP1" }, + ], + source: "ESP32 Series Datasheet v5.2, Table 2-1 (verbatim: 'Connects to a 3.3 nF (10%) capacitor and 20 kΩ resistor in parallel to CAP1')", + }, + }, + ], + }, + { + id: "pin_48", + name: "CAP1", + pin: 48, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "analog", roles: ["input"] }], + capabilities: ["bbpll_cap1"], + traits: [ + { + type: "implied_passives", + params: { + purpose: "BBPLL loop filter", + components: [{ kind: "capacitor", value: "10 nF", connection: "CAP1 in series to GND" }], + source: "ESP32 Series Datasheet v5.2, Table 2-1 (verbatim: 'Connects to a 10 nF series capacitor to ground')", + }, + }, + ], + }, + + { + ...Ground({ id: "pin_49", name: "GND", pin: 49, maxCurrentA: 1.2 }), + traits: [ + { + type: "current_rating_basis", + params: { + note: "1.2 A is the absolute-maximum cumulative IO output current (Table 5-1), returned through the exposed pad — not a recommended operating point.", + }, + }, + { + type: "net_shareable", + params: { net: "gnd", policy: "single_ground_instance_may_serve_all_members" }, + }, + { + type: "assembly_requirement", + params: { + note: "The exposed pad is the only ground return and the primary heat path — solder to the PCB ground plane with a via array.", + source: "ESP32 Hardware Design Guidelines", + }, + }, + ], + }, +]; + +/** All 49 pads, sorted into physical package order — schematic-honest. */ +const pins: InterfaceDef[] = [...powerAndServicePads, ...gpioPads].sort( + (a, b) => Number(a.pin ?? 0) - Number(b.pin ?? 0), +); + +// --------------------------------------------------------------------------- +// Peripheral controllers +// --------------------------------------------------------------------------- + +function withTraits(iface: InterfaceDef, traits: TraitDef[]): InterfaceDef { + return { ...iface, traits: [...(iface.traits ?? []), ...traits] }; +} + +/** Attach traits to the interface with the given id inside a builder result. */ +function amend(ifaces: InterfaceDef[], id: string, traits: TraitDef[]): InterfaceDef[] { + return ifaces.map((i) => (i.id === id ? withTraits(i, traits) : i)); +} + +function composed(config: { + id: string; + name: string; + protocolType: string; + roles: string[]; + slots: SlotDef[]; + profiles?: InterfaceDef["profiles"]; + parameters?: Parameter[]; + maxInstances?: number; + defaultActive?: boolean; + traits?: TraitDef[]; + domain?: InterfaceDef["domain"]; +}): InterfaceDef { + return { + id: config.id, + name: config.name, + domain: config.domain ?? "electrical", + exposed: true, + default_active: config.defaultActive ?? false, + protocols: [{ type: config.protocolType, roles: config.roles }], + slots: config.slots, + ...(config.profiles ? { profiles: config.profiles } : {}), + ...(config.parameters ? { parameters: config.parameters } : {}), + ...(config.maxInstances !== undefined ? { max_instances: config.maxInstances } : {}), + ...(config.traits ? { traits: config.traits } : {}), + }; +} + +/** Table 4-4: calibrated effective measurement ranges per attenuation setting. */ +const ADC_ATTENUATION_TRAIT: TraitDef = { + type: "adc_attenuation_ranges", + params: { + source: "ESP32 Series Datasheet v5.2, Table 4-4 (after eFuse-Vref calibration)", + ranges: [ + { atten: 0, effective_range_mV: [100, 950], total_error_mV: 23 }, + { atten: 1, effective_range_mV: [100, 1250], total_error_mV: 30 }, + { atten: 2, effective_range_mV: [150, 1750], total_error_mV: 40 }, + { atten: 3, effective_range_mV: [150, 2450], total_error_mV: 60 }, + ], + note: "Above ~2450 mV (raw reading > 3000) at atten 3, accuracy degrades beyond the table (Table 4-3 note). ±6% chip-to-chip spread before calibration.", + }, +}; + +const adc1 = composed({ + id: "adc1", + name: "ADC1", + protocolType: "analog", + roles: ["input"], + parameters: [ + resolutionBits(12), + voltageRangeV(0.1, 2.45), // widest calibrated effective range (atten 3) + { id: "sample_rate", name: "Sampling rate (DIG controller)", unit: "Hz", value: 2_000_000 }, + { id: "sample_rate_rtc", name: "Sampling rate (RTC controller)", unit: "Hz", value: 200_000 }, + ], + slots: [ + { id: "channel", required: true, count: 8, match: { protocol: "analog", role: "input", capability: "adc1_in" } }, + ], + profiles: [ + { + id: "adc1_channels", + label: "ADC1_CH0-CH7 (fixed analog pads)", + bindings: { + channel: ["pin_5", "pin_6", "pin_7", "pin_8", "pin_12", "pin_13", "pin_10", "pin_11"], + }, + }, + ], + maxInstances: 1, + traits: [ + ADC_ATTENUATION_TRAIT, + { type: "channels", params: { count: 8, mapping: "CH0=SENSOR_VP, CH1=SENSOR_CAPP, CH2=SENSOR_CAPN, CH3=SENSOR_VN, CH4=32K_XP, CH5=32K_XN, CH6=VDET_1, CH7=VDET_2" } }, + { type: "low_power_capable", params: { note: "Usable by the ULP coprocessor during sleep (RTC controller, ≤200 ksps)." } }, + ], +}); + +const adc2 = composed({ + id: "adc2", + name: "ADC2", + protocolType: "analog", + roles: ["input"], + parameters: [ + resolutionBits(12), + voltageRangeV(0.1, 2.45), + { id: "sample_rate", name: "Sampling rate (DIG controller)", unit: "Hz", value: 2_000_000 }, + { id: "sample_rate_rtc", name: "Sampling rate (RTC controller)", unit: "Hz", value: 200_000 }, + ], + slots: [ + { id: "channel", required: true, count: 10, match: { protocol: "analog", role: "input", capability: "adc2_in" } }, + ], + profiles: [ + { + id: "adc2_channels", + label: "ADC2_CH0-CH9 (fixed analog pads)", + bindings: { + channel: ["pin_24", "pin_23", "pin_22", "pin_21", "pin_20", "pin_18", "pin_17", "pin_16", "pin_14", "pin_15"], + }, + }, + ], + maxInstances: 1, + traits: [ + ADC_ATTENUATION_TRAIT, + { type: "channels", params: { count: 10, mapping: "CH0=GPIO4, CH1=GPIO0, CH2=GPIO2, CH3=MTDO, CH4=MTCK, CH5=MTDI, CH6=MTMS, CH7=GPIO27, CH8=GPIO25, CH9=GPIO26" } }, + { + // The canonical co-requirement example: a second converter instance + // exists, but it is conditioned on the radio being idle. + type: "co_requirement", + params: { + with: "wifi_radio", + condition: "Wi-Fi active", + effect: "ADC2 is used by the Wi-Fi driver and cannot be read while Wi-Fi is active — schedule reads when the radio is idle, or use ADC1.", + source: "ESP32 TRM (On-Chip Sensors); Datasheet Table 4-3 specifies ADC characteristics with 'Wi-Fi & Bluetooth off'", + }, + }, + ], +}); + +const dac = composed({ + id: "dac", + name: "DAC", + protocolType: "analog", + roles: ["output"], + parameters: [resolutionBits(8), voltageRangeV(0, 3.3)], + slots: [ + { id: "channel", required: true, count: 2, match: { protocol: "analog", role: "output", capability: "dac_out" } }, + ], + profiles: [ + { id: "dac_channels", label: "DAC_1 (GPIO25) / DAC_2 (GPIO26)", bindings: { channel: ["pin_14", "pin_15"] } }, + ], + maxInstances: 1, + traits: [ + { type: "channels", params: { count: 2, mapping: "DAC_1=GPIO25, DAC_2=GPIO26", note: "Independent conversions; supply-referenced (VDD as reference)." } }, + ], +}); + +const touch = composed({ + id: "touch", + name: "Capacitive Touch Sensor", + protocolType: "capacitive_touch", + roles: ["input"], + slots: [{ id: "channel", required: true, count: 10, match: { capability: "touch" } }], + profiles: [ + { + id: "touch_channels", + label: "T0-T9 (fixed analog pads)", + bindings: { + channel: ["pin_24", "pin_23", "pin_22", "pin_21", "pin_20", "pin_18", "pin_17", "pin_16", "pin_13", "pin_12"], + }, + }, + ], + maxInstances: 1, + traits: [ + { type: "channels", params: { count: 10, mapping: "T0=GPIO4, T1=GPIO0, T2=GPIO2, T3=MTDO, T4=MTCK, T5=MTDI, T6=MTMS, T7=GPIO27, T8=32K_XN, T9=32K_XP" } }, + { + type: "qualification_note", + params: { + note: "The ESP32 touch sensor has not passed the Conducted Susceptibility (CS) test and thus has limited application scenarios.", + source: "ESP32 Series Datasheet v5.2, Section 4.10 note", + }, + }, + ], +}); + +const rtcGpio = composed({ + id: "rtc_gpio", + name: "RTC GPIO", + protocolType: "digital", + roles: ["input", "output"], + slots: [{ id: "channel", required: true, count: 18, match: { capability: "rtc_gpio" } }], + profiles: [ + { + id: "rtc_gpio_channels", + label: "RTC_GPIO0-17", + bindings: { + channel: [ + "pin_5", "pin_6", "pin_7", "pin_8", "pin_10", "pin_11", // RTC_GPIO0-5 + "pin_14", "pin_15", "pin_13", "pin_12", // RTC_GPIO6-9 + "pin_24", "pin_23", "pin_22", "pin_21", "pin_20", "pin_18", "pin_17", "pin_16", // RTC_GPIO10-17 + ], + }, + }, + ], + maxInstances: 1, + traits: [ + { type: "channels", params: { count: 18, note: "VDD3P3_RTC-domain pads that stay functional in Deep-sleep — e.g. as wake-up sources (Appendix A.1 note 5)." } }, + ], +}); + +// I²C — two controllers; pins route to any output-capable GPIO via the GPIO +// matrix (Table 4-6: "Any GPIO Pins"). No IO_MUX default exists — the +// GPIO21/GPIO22 pairing seen on dev boards is an SDK convention, not silicon. +const I2C_TRAITS: TraitDef[] = [ + { type: "display_notation", params: { latex: "I^{2}C" } }, + { + type: "instance_combinations", + params: { + instances_in_silicon: 2, + routing: "gpio_matrix", + combination_space: "SDA: any output-capable GPIO (open-drain needs a driver); SCL: any output-capable GPIO.", + note: "No IO_MUX default pins. GPIO21/GPIO22 is an ESP-IDF/Arduino convention only.", + }, + }, + { + type: "implied_passives", + params: { + purpose: "Open-drain bus pull-ups", + components: [{ kind: "resistor", value: "typ. 2.2-10 kΩ (bus-speed dependent)", connection: "SDA and SCL to the bus supply" }], + source: "I²C bus specification; datasheet notes speeds >400 kbit/s are 'constrained by SDA pull-up strength'", + }, + }, +]; + +const i2c0 = amend( + I2C({ id: "i2c0", instance: 0, name: "I²C0", roles: ["master", "slave"], clockFreqHz: [100_000, 5_000_000], maxInstances: 1 }), + "i2c0", + I2C_TRAITS, +); +const i2c1 = amend( + I2C({ id: "i2c1", instance: 1, name: "I²C1", roles: ["master", "slave"], clockFreqHz: [100_000, 5_000_000], maxInstances: 1 }), + "i2c1", + I2C_TRAITS, +); + +// SPI0/SPI1 — the flash bus. SPI0 is the cache controller, SPI1 the user +// master on the same pins; both are pinned to the SD_* pads via IO_MUX. +const qspiFlash = composed({ + id: "spi01_flash", + name: "SPI0/SPI1 Flash Bus", + protocolType: "spi", + roles: ["master"], + parameters: [clockFreqHz([0, 80_000_000])], + slots: [ + { id: "sck", required: true, match: { protocol: "spi", role: "clock", capability: "spi01_clk" } }, + { id: "cs", required: true, match: { protocol: "spi", role: "select", capability: "spi01_cs0" } }, + { id: "io0", required: true, match: { protocol: "spi", role: "data_out", capability: "spi01_d" } }, + { id: "io1", required: true, match: { protocol: "spi", role: "data_in", capability: "spi01_q" } }, + { id: "io2", required: false, match: { capability: "spi01_wp" } }, + { id: "io3", required: false, match: { capability: "spi01_hd" } }, + ], + profiles: [ + { + id: "spi01_flash_iomux", + label: "Off-package flash wiring (Table 2-6)", + default_active: true, + bindings: { + sck: "pin_31", // SPICLK -> flash CLK + cs: "pin_30", // SPICS0 -> flash CS# + io0: "pin_33", // SPID -> flash IO0/DI + io1: "pin_32", // SPIQ -> flash IO1/DO + io2: "pin_29", // SPIWP -> flash IO2/WP# + io3: "pin_28", // SPIHD -> flash IO3/HOLD# + }, + }, + ], + maxInstances: 1, + defaultActive: true, + traits: [ + { type: "spi_modes", params: { modes: ["Standard SPI", "Dual SPI", "Quad SPI"], note: "Connects external flash and SRAM (Section 4.10 'Parallel QSPI')." } }, + { + type: "instance_combinations", + params: { + instances_in_silicon: 1, + routing: "io_mux_fixed", + combination_space: "Exactly one pin set — the flash bus is not matrix-routable.", + }, + }, + ], +}); + +const SPI_USER_TRAITS = (busName: string, iomux: string): TraitDef[] => [ + { + type: "instance_combinations", + params: { + instances_in_silicon: 1, + routing: "io_mux_preferred", + combination_space: `IO_MUX pins (${iomux}) reach full speed; per the ESP32 TRM the ${busName} signals may also route through the GPIO matrix to other pins at reduced clock (≤40 MHz).`, + source: "Datasheet Section 4.8.2; ESP32 TRM, SPI Controller chapter", + }, + }, +]; + +const hspi = amend( + SPI({ + id: "hspi", + name: "HSPI (SPI2)", + roles: ["master", "slave"], + clockFreqHz: [0, 80_000_000], + maxInstances: 1, + profiles: [ + { id: "hspi_iomux", label: "HSPI IO_MUX (GPIO12-15)", mosi: "pin_20", miso: "pin_18", sck: "pin_17", ss: "pin_21" }, + ], + }), + "hspi", + SPI_USER_TRAITS("HSPI", "GPIO2, GPIO4, GPIO12-15"), +); + +const vspi = amend( + SPI({ + id: "vspi", + name: "VSPI (SPI3)", + roles: ["master", "slave"], + clockFreqHz: [0, 80_000_000], + maxInstances: 1, + profiles: [ + { id: "vspi_iomux", label: "VSPI IO_MUX (GPIO5, 18, 19, 23)", mosi: "pin_36", miso: "pin_38", sck: "pin_35", ss: "pin_34" }, + ], + }), + "vspi", + SPI_USER_TRAITS("VSPI", "GPIO5, GPIO18-19, GPIO21-23"), +); + +// UARTs — three controllers, any GPIO via the GPIO matrix; the profiles below +// are the IO_MUX (full-speed, default) routings from Table 2-1. +const UART_TRAITS = (n: number, iomuxNote: string): TraitDef[] => [ + { + type: "instance_combinations", + params: { + instances_in_silicon: 1, + routing: "gpio_matrix", + combination_space: "RX/CTS: any GPIO (input path); TX/RTS: any output-capable GPIO.", + iomux_default: iomuxNote, + source: "ESP32 Series Datasheet v5.2, Section 4.8.3 / Table 2-1", + }, + }, + ...(n === 1 + ? [{ + type: "co_requirement", + params: { + with: "spi01_flash", + condition: "UART1 on its IO_MUX pins (GPIO9/GPIO10)", + effect: "The UART1 IO_MUX pins are the flash SPIHD/SPIWP pads — on any flash-booting design UART1 must be matrix-routed to other pins.", + }, + } satisfies TraitDef] + : []), +]; + +const uart0 = amend( + UART({ + id: "uart0", + name: "UART0", + roles: ["host", "device"], + baudRate: [0, 5_000_000], + maxInstances: 1, + profiles: [ + { id: "uart0_iomux", label: "UART0 IO_MUX (GPIO1/3, RTS GPIO22, CTS GPIO19)", rx: "pin_40", tx: "pin_41", rts: "pin_39", cts: "pin_38" }, + ], + }), + "uart0", + [ + ...UART_TRAITS(0, "U0RXD=GPIO3 (pin 40), U0TXD=GPIO1 (pin 41), U0RTS=GPIO22, U0CTS=GPIO19"), + { type: "boot_console", params: { note: "UART0 is the ROM bootloader console: flashing uses U0RXD/U0TXD; boot log printing on U0TXD is controlled by the MTDO strap." } }, + ], +); + +const uart1 = amend( + UART({ + id: "uart1", + name: "UART1", + roles: ["host", "device"], + baudRate: [0, 5_000_000], + maxInstances: 1, + profiles: [ + { id: "uart1_iomux", label: "UART1 IO_MUX (GPIO9/10, RTS GPIO11, CTS GPIO6 — conflicts with flash)", rx: "pin_28", tx: "pin_29", rts: "pin_30", cts: "pin_31" }, + ], + }), + "uart1", + UART_TRAITS(1, "U1RXD=GPIO9 (pin 28), U1TXD=GPIO10 (pin 29), U1RTS=GPIO11, U1CTS=GPIO6"), +); + +const uart2 = amend( + UART({ + id: "uart2", + name: "UART2", + roles: ["host", "device"], + baudRate: [0, 5_000_000], + maxInstances: 1, + profiles: [ + { id: "uart2_iomux", label: "UART2 IO_MUX (GPIO16/17, RTS GPIO7, CTS GPIO8)", rx: "pin_25", tx: "pin_27", rts: "pin_32", cts: "pin_33" }, + ], + }), + "uart2", + UART_TRAITS(2, "U2RXD=GPIO16 (pin 25), U2TXD=GPIO17 (pin 27), U2RTS=GPIO7, U2CTS=GPIO8"), +); + +/** Matrix-routed peripheral helper: signals reach any eligible GPIO. */ +function matrixPeripheral(config: { + id: string; + name: string; + protocolType: string; + roles: string[]; + slots: SlotDef[]; + maxInstances: number; + parameters?: Parameter[]; + traits?: TraitDef[]; +}): InterfaceDef { + return composed({ ...config }); +} + +const i2sTraits = (n: number): TraitDef[] => [ + { + type: "instance_combinations", + params: { + instances_in_silicon: 2, + routing: "gpio_matrix", + combination_space: "All I2S signals route to any eligible GPIO via the GPIO matrix.", + source: "ESP32 Series Datasheet v5.2, Section 4.8.5", + }, + }, + { + type: "operating_modes", + params: { + modes: ["master", "slave", "full-duplex", "half-duplex", "PDM in/out", ...(n === 0 ? ["parallel LCD data output", "parallel camera data input"] : ["parallel LCD data output"])], + source: "ESP32 Series Datasheet v5.2, Section 4.8.5 / Table 4-6", + }, + }, +]; + +const i2s0 = matrixPeripheral({ + id: "i2s0", + name: "I²S0", + protocolType: "i2s", + roles: ["master", "slave"], + slots: [ + { id: "bck", required: false, match: { capability: "matrix_out" } }, + { id: "ws", required: false, match: { capability: "matrix_out" } }, + { id: "data_out", required: false, match: { capability: "matrix_out" } }, + { id: "data_in", required: false, match: { capability: "matrix_in" } }, + { id: "mclk", required: false, label: "I2S0_CLK", match: { capability: "clk_out" } }, + ], + maxInstances: 1, + traits: [...i2sTraits(0), { type: "display_notation", params: { latex: "I^{2}S0" } }], +}); + +const i2s1 = matrixPeripheral({ + id: "i2s1", + name: "I²S1", + protocolType: "i2s", + roles: ["master", "slave"], + slots: [ + { id: "bck", required: false, match: { capability: "matrix_out" } }, + { id: "ws", required: false, match: { capability: "matrix_out" } }, + { id: "data_out", required: false, match: { capability: "matrix_out" } }, + { id: "data_in", required: false, match: { capability: "matrix_in" } }, + ], + maxInstances: 1, + traits: [...i2sTraits(1), { type: "display_notation", params: { latex: "I^{2}S1" } }], +}); + +const ledc = matrixPeripheral({ + id: "ledc", + name: "LED PWM Controller (LEDC)", + protocolType: "pwm", + roles: ["output"], + parameters: [ + { id: "resolution", name: "Max duty-cycle resolution", unit: "dimensionless", value: 20 }, + ], + slots: [ + { id: "channel", required: false, count: 16, match: { protocol: "pwm", role: "output", capability: "pwm_out" } }, + ], + maxInstances: 1, + traits: [ + { + type: "channels", + params: { + count: 16, + detail: "Eight high-speed (ledc_hs_sig_out0-7, 80 MHz clock) + eight low-speed (ledc_ls_sig_out0-7) generators sharing eight 20-bit timers.", + source: "ESP32 Series Datasheet v5.2, Section 4.8.8 / Table 4-6", + }, + }, + { + type: "instance_combinations", + params: { instances_in_silicon: 1, routing: "gpio_matrix", combination_space: "Any output-capable GPIO per channel." }, + }, + ], +}); + +const mcpwmTraits: TraitDef[] = [ + { + type: "channels", + params: { + count: 6, + detail: "Three PWM operators generating waveform pairs (six outputs), three fault-detection inputs, three sync inputs, three 32-bit capture channels.", + source: "ESP32 Series Datasheet v5.2, Section 4.8.9", + }, + }, + { + type: "instance_combinations", + params: { instances_in_silicon: 2, routing: "gpio_matrix", combination_space: "Any output-capable GPIO for outputs; any GPIO for fault/sync/capture inputs." }, + }, +]; + +function mcpwm(n: 0 | 1): InterfaceDef { + return matrixPeripheral({ + id: `mcpwm${n}`, + name: `Motor Control PWM ${n} (MCPWM${n})`, + protocolType: "pwm", + roles: ["output"], + slots: [ + { id: "pwm_out", required: false, count: 6, match: { protocol: "pwm", role: "output", capability: "pwm_out" } }, + { id: "fault_in", required: false, count: 3, match: { capability: "matrix_in" } }, + { id: "sync_in", required: false, count: 3, match: { capability: "matrix_in" } }, + { id: "capture_in", required: false, count: 3, match: { capability: "matrix_in" } }, + ], + maxInstances: 1, + traits: mcpwmTraits, + }); +} + +const pcnt = matrixPeripheral({ + id: "pcnt", + name: "Pulse Counter (PCNT)", + protocolType: "pulse_counter", + roles: ["input"], + slots: [ + { id: "sig_in", required: false, count: 16, match: { capability: "matrix_in" } }, + { id: "ctrl_in", required: false, count: 16, match: { capability: "matrix_in" } }, + ], + maxInstances: 1, + traits: [ + { + type: "channels", + params: { + count: 8, + detail: "Eight units; each has a 16-bit signed counter and two channels (signal + control input each), with glitch filtering.", + source: "ESP32 Series Datasheet v5.2, Section 4.8.7 / Table 4-6", + }, + }, + { type: "instance_combinations", params: { instances_in_silicon: 8, routing: "gpio_matrix", combination_space: "Any GPIO per input." } }, + ], +}); + +const rmt = matrixPeripheral({ + id: "rmt", + name: "Remote Control Peripheral (RMT)", + protocolType: "rmt", + roles: ["input", "output"], + slots: [ + { id: "sig_in", required: false, count: 8, match: { capability: "matrix_in" } }, + { id: "sig_out", required: false, count: 8, match: { capability: "matrix_out" } }, + ], + maxInstances: 1, + traits: [ + { + type: "channels", + params: { count: 8, detail: "Eight channels, each with independent TX and RX for infrared or generic pulse trains.", source: "ESP32 Series Datasheet v5.2, Section 4.8.6" }, + }, + { type: "instance_combinations", params: { instances_in_silicon: 1, routing: "gpio_matrix", combination_space: "Any GPIO (in) / any output-capable GPIO (out) per channel." } }, + ], +}); + +const twai = matrixPeripheral({ + id: "twai", + name: "TWAI (CAN 2.0)", + protocolType: "twai", + roles: ["controller"], + parameters: [ + // Chip revisions v0.0/v1.0/v1.1 (this part): 25 kbit/s floor. Only the + // -V3 revision extends down to 12.5 kbit/s. + { id: "bit_rate", unit: "Hz", range: [25_000, 1_000_000] }, + ], + slots: [ + { id: "rx", required: true, match: { capability: "matrix_in" } }, + { id: "tx", required: true, match: { capability: "matrix_out" } }, + { id: "bus_off_on", required: false, match: { capability: "matrix_out" } }, + { id: "clkout", required: false, match: { capability: "matrix_out" } }, + ], + maxInstances: 1, + traits: [ + { + // The canonical instances-vs-combinations example: one controller in + // silicon, a huge matrix-routing combination space. + type: "instance_combinations", + params: { + instances_in_silicon: 1, + routing: "gpio_matrix", + combination_space: "RX: any of the 34 GPIOs; TX: any of the 28 output-capable GPIOs. Combinations are plentiful — simultaneous instances are exactly one.", + source: "ESP32 Series Datasheet v5.2, Section 4.8.12 / Table 4-6", + }, + }, + { + type: "protocol_compliance", + params: { standard: "ISO 11898-1 (CAN Specification 2.0)", frames: ["standard 11-bit", "extended 29-bit"], note: "Requires an external CAN transceiver to drive the bus." }, + }, + ], +}); + +const sdioHost = composed({ + id: "sdio_host", + name: "SD/SDIO/MMC Host Controller", + protocolType: "sdio", + roles: ["host"], + parameters: [clockFreqHz([0, 80_000_000])], + slots: [ + { id: "clk", required: true, match: { capability: "sdio_host_clk" } }, + { id: "cmd", required: true, match: { capability: "sdio_host_cmd" } }, + { id: "data0", required: true, match: { capability: "sdio_host_data0" } }, + { id: "data1", required: false, match: { capability: "sdio_host_data1" } }, + { id: "data2", required: false, match: { capability: "sdio_host_data2" } }, + { id: "data3", required: false, match: { capability: "sdio_host_data3" } }, + { id: "data4", required: false, match: { capability: "sdio_host_data4" } }, + { id: "data5", required: false, match: { capability: "sdio_host_data5" } }, + { id: "data6", required: false, match: { capability: "sdio_host_data6" } }, + { id: "data7", required: false, match: { capability: "sdio_host_data7" } }, + { id: "strobe", required: false, match: { capability: "sdio_host_strobe" } }, + ], + profiles: [ + { + id: "sdio_host_hs1", + label: "Port 1 (HS1_*): up to 8-bit + strobe", + bindings: { + clk: "pin_31", cmd: "pin_30", + data0: "pin_32", data1: "pin_33", data2: "pin_28", data3: "pin_29", + data4: "pin_25", data5: "pin_27", data6: "pin_34", data7: "pin_35", + strobe: "pin_36", + }, + }, + { + id: "sdio_host_hs2", + label: "Port 2 (HS2_*): up to 4-bit", + bindings: { + clk: "pin_17", cmd: "pin_21", + data0: "pin_22", data1: "pin_24", data2: "pin_18", data3: "pin_20", + }, + }, + ], + maxInstances: 2, + traits: [ + { + type: "protocol_compliance", + params: { + standards: ["SD Memory Card 3.0/3.01", "SDIO 3.0", "CE-ATA 1.1", "MMC 4.41", "eMMC 4.5/4.51"], + note: "Up to 80 MHz clock output; 1-/4-/8-bit bus modes; two SD/SDIO/MMC 4.41 cards in 4-bit mode; one SD card at 1.8 V.", + source: "ESP32 Series Datasheet v5.2, Section 4.8.10", + }, + }, + { + type: "instance_combinations", + params: { + instances_in_silicon: 1, + routing: "io_mux_fixed", + combination_space: "Two fixed IO_MUX port mappings (HS1_*, HS2_*); slots do not mix across ports.", + }, + }, + { + type: "co_requirement", + params: { + with: "spi01_flash", + condition: "Port 1 (HS1) in use", + effect: "HS1_CLK/CMD/DATA0-3 are the external-flash pads — port 1 conflicts with a flash-booting design.", + }, + }, + ], +}); + +const sdioSlave = composed({ + id: "sdio_slave", + name: "SDIO/SPI Slave Controller", + protocolType: "sdio", + roles: ["device"], + parameters: [clockFreqHz([0, 50_000_000])], + slots: [ + { id: "clk", required: true, match: { capability: "sdio_slave_clk" } }, + { id: "cmd", required: true, match: { capability: "sdio_slave_cmd" } }, + { id: "data0", required: false, match: { capability: "sdio_slave_data0" } }, + { id: "data1", required: false, match: { capability: "sdio_slave_data1" } }, + { id: "data2", required: false, match: { capability: "sdio_slave_data2" } }, + { id: "data3", required: false, match: { capability: "sdio_slave_data3" } }, + ], + profiles: [ + { + id: "sdio_slave_mt", + label: "SD_* on MTMS/MTDO/GPIO2/GPIO4/MTDI/MTCK", + bindings: { clk: "pin_17", cmd: "pin_21", data0: "pin_22", data1: "pin_24", data2: "pin_18", data3: "pin_20" }, + }, + { + id: "sdio_slave_sd", + label: "SD_* on GPIO6-11 (flash pads)", + bindings: { clk: "pin_31", cmd: "pin_30", data0: "pin_32", data1: "pin_33", data2: "pin_28", data3: "pin_29" }, + }, + ], + maxInstances: 1, + traits: [ + { + type: "protocol_compliance", + params: { + standards: ["SDIO Card Specification 2.0"], + note: "SPI, 1-bit SDIO and 4-bit SDIO transfer modes, 0-50 MHz; DMA to shared memory.", + source: "ESP32 Series Datasheet v5.2, Section 4.8.11", + }, + }, + { + type: "co_requirement", + params: { + with: "pin_21, pin_34", + condition: "SDIO slave in use", + effect: "The MTDO and GPIO5 straps set the SDIO-slave sampling/driving clock edge at boot (Section 3).", + }, + }, + ], +}); + +const jtag = composed({ + id: "jtag", + name: "JTAG", + protocolType: "jtag", + roles: ["target"], + slots: [ + { id: "tms", required: true, match: { capability: "jtag_tms" } }, + { id: "tck", required: true, match: { capability: "jtag_tck" } }, + { id: "tdi", required: true, match: { capability: "jtag_tdi" } }, + { id: "tdo", required: true, match: { capability: "jtag_tdo" } }, + ], + profiles: [ + { id: "jtag_iomux", label: "MTMS/MTCK/MTDI/MTDO", bindings: { tms: "pin_17", tck: "pin_20", tdi: "pin_18", tdo: "pin_21" } }, + ], + maxInstances: 1, + traits: [ + { type: "configuration_note", params: { note: "eFuse EFUSE_DISABLE_JTAG permanently disables JTAG (one-time programmable).", source: "ESP32 Series Datasheet v5.2, Section 3" } }, + ], +}); + +const ethernetMac = composed({ + id: "ethernet_mac", + name: "Ethernet MAC (MII/RMII)", + protocolType: "ethernet_mac", + roles: ["controller"], + slots: [ + // Common to both PHY interfaces (IO_MUX-fixed): + { id: "txd0", required: true, match: { capability: "emac_txd0" } }, + { id: "txd1", required: true, match: { capability: "emac_txd1" } }, + { id: "tx_en", required: true, match: { capability: "emac_tx_en" } }, + { id: "rxd0", required: true, match: { capability: "emac_rxd0" } }, + { id: "rxd1", required: true, match: { capability: "emac_rxd1" } }, + { id: "rx_dv", required: true, label: "RX_DV (MII) / CRS_DV (RMII)", match: { capability: "emac_rx_dv" } }, + { id: "tx_clk", required: false, label: "TX_CLK (MII) / REF_CLK (RMII)", match: { capability: "emac_tx_clk" } }, + // MII-only: + { id: "rx_clk", required: false, match: { capability: "emac_rx_clk" } }, + { id: "txd2", required: false, match: { capability: "emac_txd2" } }, + { id: "txd3", required: false, match: { capability: "emac_txd3" } }, + { id: "rxd2", required: false, match: { capability: "emac_rxd2" } }, + { id: "rxd3", required: false, match: { capability: "emac_rxd3" } }, + { id: "rx_er", required: false, match: { capability: "emac_rx_er" } }, + { id: "tx_er", required: false, match: { capability: "emac_tx_er" } }, + // Clock outputs / management (management routes via GPIO matrix): + { id: "clk_out", required: false, match: { capability: "emac_clk_out" } }, + { id: "clk_out_180", required: false, match: { capability: "emac_clk_out_180" } }, + { id: "mdc", required: false, match: { capability: "matrix_out" } }, + { id: "mdio", required: false, match: { capability: "matrix_out" } }, + ], + profiles: [ + { + id: "emac_mii", + label: "MII (17 signals)", + bindings: { + tx_clk: "pin_23", rx_clk: "pin_34", tx_en: "pin_42", + txd0: "pin_38", txd1: "pin_39", txd2: "pin_17", txd3: "pin_18", + rx_er: "pin_20", rx_dv: "pin_16", + rxd0: "pin_14", rxd1: "pin_15", rxd2: "pin_41", rxd3: "pin_21", + tx_er: "pin_24", + }, + }, + { + id: "emac_rmii", + label: "RMII (9 signals)", + bindings: { + tx_clk: "pin_23", // 50 MHz REF_CLK in, or CLK_OUT with internal PLL + tx_en: "pin_42", + txd0: "pin_38", txd1: "pin_39", + rx_dv: "pin_16", // CRS_DV + rxd0: "pin_14", rxd1: "pin_15", + }, + }, + ], + maxInstances: 1, + traits: [ + { + type: "protocol_compliance", + params: { + standard: "IEEE 802.3-2008 MAC", + features: ["MII and RMII PHY interfaces", "half/full duplex", "hardware PTP (IEEE 1588-2008)", "25 MHz/50 MHz clock output"], + source: "ESP32 Series Datasheet v5.2, Section 4.8.13", + }, + }, + { + type: "co_requirement", + params: { + with: "external PHY", + condition: "always", + effect: "The MAC has no PHY — an external MII (17-signal) or RMII (9-signal) PHY plus its clock scheme is required. MDC/MDIO/CRS/COL are slow signals routable to any GPIO via the matrix.", + source: "ESP32 Series Datasheet v5.2, Section 4.8.13 / Appendix A.1 note 11", + }, + }, + ], +}); + +const clockOutput = composed({ + id: "clock_output", + name: "Clock Output", + protocolType: "clock", + roles: ["output"], + slots: [{ id: "out", required: true, match: { protocol: "clock", role: "output", capability: "clk_out" } }], + profiles: [ + { id: "clk_out1", label: "CLK_OUT1 (GPIO0)", bindings: { out: "pin_23" } }, + { id: "clk_out2", label: "CLK_OUT2 (GPIO3)", bindings: { out: "pin_40" } }, + { id: "clk_out3", label: "CLK_OUT3 (GPIO1)", bindings: { out: "pin_41" } }, + ], + maxInstances: 3, + traits: [ + { type: "instance_combinations", params: { instances_in_silicon: 3, routing: "io_mux_fixed", combination_space: "CLK_OUT1/2/3 are IO_MUX-fixed to GPIO0/GPIO3/GPIO1." } }, + ], +}); + +const xtalMain = composed({ + id: "xtal_main", + name: "Main Crystal Oscillator", + protocolType: "clock", + roles: ["input"], + parameters: [clockFreqHz([2_000_000, 60_000_000])], + slots: [ + { id: "xp", required: true, match: { protocol: "clock", role: "input", capability: "xtal_p" } }, + { id: "xn", required: true, match: { protocol: "clock", role: "output", capability: "xtal_n" } }, + ], + profiles: [ + { id: "xtal_pins", label: "XTAL_P/XTAL_N (pins 45/44)", default_active: true, bindings: { xp: "pin_45", xn: "pin_44" } }, + ], + maxInstances: 1, + defaultActive: true, + traits: [ + { + type: "co_requirement", + params: { + with: "wifi_radio, bluetooth_radio", + condition: "RF in use", + effect: "The external crystal may be 2-60 MHz, but Wi-Fi/Bluetooth functionality requires exactly 40 MHz.", + source: "ESP32 Series Datasheet v5.2, Section 'Clocking' (External 2 MHz ~ 60 MHz crystal oscillator; 40 MHz only for Wi-Fi/Bluetooth functionality)", + }, + }, + { + type: "implied_passives", + params: { + purpose: "Crystal load", + components: [ + { kind: "capacitor", value: "per crystal vendor C_L spec", connection: "XTAL_P to GND" }, + { kind: "capacitor", value: "per crystal vendor C_L spec", connection: "XTAL_N to GND" }, + ], + source: "ESP32 Hardware Design Guidelines", + }, + }, + ], +}); + +const xtal32k = composed({ + id: "xtal_32k", + name: "32.768 kHz RTC Crystal Oscillator", + protocolType: "clock", + roles: ["input"], + parameters: [clockFreqHz(32_768)], + slots: [ + { id: "xp", required: true, match: { capability: "xtal_32k_p" } }, + { id: "xn", required: true, match: { capability: "xtal_32k_n" } }, + ], + profiles: [ + { id: "xtal_32k_pins", label: "32K_XP/32K_XN (GPIO32/GPIO33)", bindings: { xp: "pin_12", xn: "pin_13" } }, + ], + maxInstances: 1, + traits: [ + { + type: "co_requirement", + params: { + with: "pin_12, pin_13", + condition: "32 kHz crystal fitted", + effect: "Commits GPIO32 and GPIO33 entirely — their GPIO/ADC/TOUCH functions become unavailable.", + }, + }, + { + type: "implied_passives", + params: { + purpose: "Crystal load", + components: [ + { kind: "capacitor", value: "per crystal vendor C_L spec", connection: "32K_XP to GND" }, + { kind: "capacitor", value: "per crystal vendor C_L spec", connection: "32K_XN to GND" }, + ], + source: "ESP32 Hardware Design Guidelines", + }, + }, + { type: "optionality", params: { note: "Optional — the RTC slow clock can instead use the internal RC oscillator (with reduced timekeeping accuracy)." } }, + ], +}); + +const bbpllFilter = composed({ + id: "bbpll_loop_filter", + name: "BBPLL Loop Filter", + protocolType: "analog", + roles: ["input"], + slots: [ + { id: "cap1", required: true, match: { capability: "bbpll_cap1" } }, + { id: "cap2", required: true, match: { capability: "bbpll_cap2" } }, + ], + profiles: [ + { id: "bbpll_pins", label: "CAP1/CAP2 (pins 48/47)", default_active: true, bindings: { cap1: "pin_48", cap2: "pin_47" } }, + ], + maxInstances: 1, + defaultActive: true, +}); + +// --------------------------------------------------------------------------- +// Radios — Datasheet Sections 4.6/4.7, Tables 5-4 through 5-9 +// --------------------------------------------------------------------------- + +const wifiRadio: InterfaceDef = { + id: "wifi_radio", + name: "Wi-Fi Radio (802.11 b/g/n, 2.4 GHz)", + domain: "network", + exposed: true, + default_active: false, + protocols: [{ type: "wifi", roles: ["station", "access_point"] }], + capabilities: ["wifi_802_11_bgn", "wifi_2g4"], + parameters: [ + { id: "frequency", unit: "Hz", range: [2_400_000_000, 2_484_000_000] }, + { id: "max_data_rate", name: "Max data rate (802.11n)", unit: "bit/s", value: 150_000_000 }, + { id: "max_tx_power", name: "Max TX power (802.11b)", unit: "dBm", value: 20.5 }, + ], + traits: [ + { + type: "radio_characteristics", + params: { + modes: ["802.11b", "802.11g", "802.11n MCS0-7 HT20/HT40", "802.11n MCS32 (RX)"], + tx_current_peak_mA: { "11b_19_5dBm": 240, "11g_16dBm": 190, "11n_14dBm": 180 }, + rx_current_mA: [95, 100], + source: "ESP32 Series Datasheet v5.2, Section 4.6 / Table 5-4", + }, + }, + { + type: "co_requirement", + params: { + with: "adc2", + condition: "Wi-Fi active", + effect: "ADC2 unavailable while Wi-Fi runs (shared hardware).", + }, + }, + { + type: "co_requirement", + params: { with: "xtal_main", condition: "always", effect: "Requires a 40 MHz main crystal." }, + }, + ], + bridgesTo: ["pin_2"], +}; + +const bluetoothRadio: InterfaceDef = { + id: "bluetooth_radio", + name: "Bluetooth 4.2 BR/EDR + Bluetooth LE Radio", + domain: "network", + exposed: true, + default_active: false, + protocols: [{ type: "bluetooth", roles: ["peer"] }], + capabilities: ["bluetooth_classic", "bluetooth_le", "bluetooth_2g4"], + parameters: [ + { id: "frequency", unit: "Hz", range: [2_402_000_000, 2_480_000_000] }, + { id: "tx_power_range", name: "RF power control range", unit: "dBm", range: [-12, 9] }, + ], + traits: [ + { + type: "radio_characteristics", + params: { + compliance: "Bluetooth v4.2 BR/EDR and Bluetooth LE", + tx_power_classes: "Class-1, class-2 and class-3 without external PA; dynamic control range up to 21 dB", + rx_sensitivity_dBm: { BR: -90, BLE: -94 }, + tx_current_mA: 130, + rx_current_mA: [95, 100], + source: "ESP32 Series Datasheet v5.2, Sections 4.7/5.7, Tables 5-7 to 5-9", + }, + }, + { + type: "co_requirement", + params: { with: "xtal_main", condition: "always", effect: "Requires a 40 MHz main crystal." }, + }, + ], + bridgesTo: ["pin_2"], +}; + +// --------------------------------------------------------------------------- +// Mechanical / thermal +// --------------------------------------------------------------------------- + +const footprintMount: InterfaceDef = { + id: "footprint_mounting", + name: "QFN-48 6×6 mm Surface-Mount Footprint", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["mounting_point"] }], + capabilities: ["qfn48_6x6_0p4mm", "surface_mount"], +}; + +const thermalPad: InterfaceDef = { + id: "thermal_pad", + name: "Exposed Thermal Pad", + pin: 49, + domain: "thermal", + exposed: true, + default_active: true, + protocols: [{ type: "thermal_connection", roles: ["thermal_source"] }], + capabilities: ["heat_sink", "pcb_thermal_plane"], + traits: [ + { type: "assembly_requirement", params: { note: "Same physical pad as pin 49 (GND); primary heat path to the PCB ground plane." } }, + ], +}; + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const ESP32D0WDQ6: ModuleDef = defineModule({ + id: "esp32-d0wdq6", + name: "Espressif ESP32-D0WDQ6", + version: "2.0.0", + manufacturer: "Espressif Systems", + part_number: "ESP32-D0WDQ6", + description: + "Bare ESP32-D0WDQ6 SoC (chip revision v1.0/v1.1) in a QFN-48 6×6 mm package: dual-core Xtensa LX6 up to 240 MHz, 448 KB ROM, 520 KB SRAM, 2.4 GHz Wi-Fi 802.11 b/g/n and Bluetooth 4.2 BR/EDR + LE. No in-package flash — external SPI flash is required for normal boot. NRND (Not Recommended for New Designs) per Datasheet v5.2.", + tags: ["esp32", "soc", "microcontroller", "wifi", "bluetooth", "ble", "xtensa-lx6", "qfn-48", "iot", "nrnd"], + categories: ["microcontroller", "connectivity.wireless"], + + interfaces: [ + // All 49 physical pads in package order — schematic-honest. + ...pins, + + // Analog peripherals + adc1, + adc2, + dac, + touch, + rtcGpio, + + // Serial / bus controllers + ...i2c0, + ...i2c1, + qspiFlash, + ...hspi, + ...vspi, + ...uart0, + ...uart1, + ...uart2, + i2s0, + i2s1, + + // Timers / signal peripherals + ledc, + mcpwm(0), + mcpwm(1), + pcnt, + rmt, + twai, + + // Storage / debug / network fabric + sdioHost, + sdioSlave, + jtag, + ethernetMac, + clockOutput, + + // Clocks and analog service networks + xtalMain, + xtal32k, + bbpllFilter, + + // Radios + wifiRadio, + bluetoothRadio, + + // Mechanical / thermal + footprintMount, + thermalPad, + ], + + interfaceGroups: [ + { + id: "required_power_pins", + label: "Required Power Pins", + members: ["pin_1", "pin_3", "pin_4", "pin_19", "pin_26", "pin_37", "pin_43", "pin_46", "pin_49"], + policy: "all_of", + }, + { + id: "analog_supply_common_net", + label: "Analog Supply Pins (one decoupled net)", + members: ["pin_1", "pin_3", "pin_4", "pin_43", "pin_46"], + policy: "all_of", + }, + { + id: "boot_strapping_pins", + label: "Boot Strapping Pins (Table 3-1)", + members: ["pin_23", "pin_22", "pin_18", "pin_21", "pin_34"], + policy: "all_of", + }, + { + id: "external_flash_pins", + label: "External SPI Flash Pins (Table 2-6)", + members: ["pin_28", "pin_29", "pin_30", "pin_31", "pin_32", "pin_33"], + policy: "all_of", + }, + ], + + requirements: [ + { + type: "power", + description: + "External supply must deliver ≥0.5 A across the 3.3 V rails (Table 5-2); peak RF transmit draws 240 mA (802.11b, +19.5 dBm). VDDA/VDD3P3/VDD3P3_RTC: 2.3-3.6 V; VDD3P3_CPU: 1.8-3.6 V.", + voltage_V: [2.3, 3.6], + current_mA: 500, + }, + { + type: "interface", + description: + "No in-package flash: normal boot requires an external SPI flash on the SPI0/SPI1 flash bus (SPICS0/SPICLK/SPID/SPIQ, optionally SPIWP/SPIHD — Table 2-6).", + interface_protocol: "spi", + }, + { + type: "interface", + description: + "A main crystal (or oscillator) on XTAL_P/XTAL_N is required; 40 MHz is mandatory for Wi-Fi/Bluetooth operation.", + interface_protocol: "clock", + }, + { + type: "capability", + description: + "LNA_IN must reach a 2.4 GHz antenna through a π-matching network transforming the 30 + j10 Ω radio impedance (QFN 6×6) to the antenna's 50 Ω.", + capability: "rf_2g4_antenna_feed", + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + { id: "vdda_analog", name: "VDDA / VDD3P3 (analog + RF)", nominal_voltage_V: 3.3, voltage_range_V: VDD_33_RANGE, max_current_mA: 500 }, + { id: "vdd3p3_rtc", name: "VDD3P3_RTC (RTC + RTC-domain IO)", nominal_voltage_V: 3.3, voltage_range_V: VDD_33_RANGE }, + { id: "vdd3p3_cpu", name: "VDD3P3_CPU (CPU + digital IO)", nominal_voltage_V: 3.3, voltage_range_V: VDD_CPU_RANGE }, + { id: "vdd_sdio", name: "VDD_SDIO (internal SDIO-LDO output: 1.8 V or VDD3P3_RTC)", nominal_voltage_V: 3.3, voltage_range_V: VDD_SDIO_RANGE, regulation_type: "regulated" }, + { id: "gnd", name: "Ground (exposed pad)", nominal_voltage_V: 0, voltage_range_V: [0, 0], max_current_mA: 1200 }, + ], + metadata: { + pin_count: 49, + package_pins: "48 perimeter pins + exposed pad", + cpu: "Dual-core Xtensa LX6, up to 240 MHz", + rom_KB: 448, + sram_KB: 520, + current_consumption: { + tx_80211b_19_5dBm_mA: 240, + rx_wifi_mA: "95-100", + modem_sleep_240MHz_mA: "30-68", + light_sleep_mA: 0.8, + }, + absolute_max: { + input_voltage_V: [-0.3, 3.6], + cumulative_io_output_current_mA: 1200, + }, + dc_characteristics: { + V_IH: "0.75 × VDD", + V_IL: "0.25 × VDD", + I_OH_mA: { VDD3P3_CPU: 40, VDD3P3_RTC: 40, VDD_SDIO: 20 }, + I_OL_mA: 28, + pull_resistors_kOhm: 45, + }, + source: "ESP32 Series Datasheet v5.2, Tables 5-1 to 5-4", + }, + }, + { + domain: "mechanical", + dimensions_mm: { length: 6, width: 6, height: 0.85 }, + metadata: { + package_type: "QFN-48", + pitch_mm: 0.4, + mounting_method: "surface_mount", + pin_1_orientation: "anti-clockwise numbering from pin 1, top view", + }, + }, + { + domain: "thermal", + // Table 5-2 note 3: chips with no in-package flash/PSRAM (D0WDQ6). + operating_temperature_C: [-40, 125], + metadata: { + storage_temperature_C: [-40, 150], + primary_heat_path: "exposed_pad_to_pcb_ground_plane", + }, + }, + { + domain: "network", + metadata: { + wireless_standards: ["802.11b", "802.11g", "802.11n (HT20/HT40)", "Bluetooth 4.2 BR/EDR", "Bluetooth LE"], + frequency_bands_ghz: [2.4], + max_data_rate_mbps: 150, + }, + }, + ], + + traits: [ + { + type: "lifecycle_status", + params: { + status: "not_recommended_for_new_designs", + source: "ESP32 Series Datasheet v5.2 cover page: 'ESP32-D0WDQ6 - Not Recommended for New Designs (NRND)'", + }, + }, + { type: "chip_revision", params: { revisions: ["v1.0", "v1.1"], note: "The -V3 suffix part is the v3.0/v3.1 revision — a distinct orderable." } }, + { type: "requires_external_flash", params: { interfaceId: "spi01_flash", defaultProfile: "spi01_flash_iomux" } }, + { type: "wireless_soc", params: { radios: ["wifi_radio", "bluetooth_radio"], rfFeed: "pin_2" } }, + { + type: "terminology_policy", + params: { + note: "Signal and function names in `esp32_pin_functions` traits are datasheet-verbatim and intentionally NOT normalised to a curated whitelist; canonical capability tags exist only where slot matching requires them.", + }, + }, + ], + + artifacts: [ + { + id: "art_datasheet", + name: "ESP32 Series Datasheet v5.2", + type: "datasheet", + url: "https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf", + }, + { + id: "art_technical_reference", + name: "ESP32 Technical Reference Manual", + type: "datasheet", + url: "https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf", + }, + { + id: "art_hardware_design_guidelines", + name: "ESP32 Hardware Design Guidelines", + type: "documentation", + url: "https://www.espressif.com/sites/default/files/documentation/esp32_hardware_design_guidelines_en.pdf", + }, + { + id: "art_errata", + name: "ESP32 Series SoC Errata", + type: "documentation", + url: "https://www.espressif.com/sites/default/files/documentation/eco_and_workarounds_for_bugs_in_esp32_en.pdf", + }, + { + id: "art_product_page", + name: "Espressif ESP32 Product Page", + type: "documentation", + url: "https://www.espressif.com/en/products/socs/esp32", + }, + { + id: "art_snapeda", + name: "SnapEDA Symbol and Footprint", + type: "cad", + url: "https://www.snapeda.com/parts/ESP32-D0WDQ6/Espressif%20Systems/view-part/?ref=digikey", + }, + { + id: "art_ultralibrarian", + name: "Ultra Librarian CAD Models", + type: "cad", + url: "https://app.ultralibrarian.com/details/A7AAB95B-922A-11EA-B5D0-0AEBB021A1EA/Espressif-Systems/ESP32-D0WDQ6?ref=digikey", + }, + { + id: "art_chip_image", + name: "ESP32-D0WDQ6 Product Photo", + type: "custom", + filePath: "./ProtoPart/protoparts/esp32-d0wdq6/artifacts/images/ESP32-D0WDQ6_tilted.png", + mimeType: "image/png", + tags: ["image", "product-photo"], + }, + ], + + geometry: { + xScale: 1, + yScale: 1, + outline: { preset: "rectangle" }, + }, +}); diff --git a/library/parts/esp32-d0wdq6/artifacts/thumbnail.png b/library/parts/esp32-d0wdq6/artifacts/thumbnail.png new file mode 100644 index 0000000..8ee2fd8 Binary files /dev/null and b/library/parts/esp32-d0wdq6/artifacts/thumbnail.png differ diff --git a/library/parts/esp32-devkitc-v4.ts b/library/parts/esp32-devkitc-v4.ts new file mode 100644 index 0000000..935dce1 --- /dev/null +++ b/library/parts/esp32-devkitc-v4.ts @@ -0,0 +1,2060 @@ +/** + * Espressif ESP32-DevKitC V4 — board-level, schematic-honest part definition. + * + * Primary sources (as audited in the ProtoPart esp32-devkitc-v4 definition): + * - ESP32-DevKitC V4 User Guide (docs.espressif.com/projects/esp-dev-kits) + * — header pinout (J2/J3, 2x19 @ 2.54 mm), EN/BOOT buttons, power options + * - ESP32-DevKitC V4 schematic (esp32_devkitc_v4_sch.pdf) + * — AMS1117-3.3 LDO, CP2102N USB-to-UART bridge, C15 errata + * - ESP32-WROOM-32E / ESP32-WROOM-32UE datasheet + * — module pin functions, flash-reserved pads, PSRAM sub-variant limits + * + * This is a BOARD audit, not a chip audit. The board carries an + * ESP32-WROOM-32E module (which itself contains an ESP32 chip + 4 MB flash); + * module/chip internals are not re-modelled here — only what the board + * exposes: 38 header pins, the Micro-USB connector, the USB-UART bridge, the + * LDO, and the EN/BOOT push-buttons, exactly as the ProtoPart JSON defines + * them. + * + * Architecture notes honoured by this file: + * - Pin-honest like a schematic: all 38 header pins are leaf interfaces in + * header order (J2-1..J2-19, then J3-1..J3-19), with ids equal to the + * ProtoPart resource ids and the official silkscreen name as the + * displayed name. The Micro-USB receptacle is a 39th leaf interface. + * - Every pin carries its verbatim ProtoPart function list (name, + * direction, signal_class, description — including the generator's alias + * entries) in a `devkitc_pin_functions` trait; canonical capability tags + * exist only where slot matching needs them. + * - Composed interfaces mirror the ProtoPart `interfaces` list one-to-one + * (same ids, names, protocol types/roles, and max_instances), with slots + * derived from the `requires` lists and profiles binding the header-pin + * ids wherever the routing is fixed on this board. + * - Flash-reserved pins (D0-D3, CMD, CLK = GPIO6-11) carry + * `usage_restriction` traits — they are wired to the on-module SPI flash + * and must not be used as general I/O (board warning #2). + * - Strapping pins (GPIO0/2/5/12/15) carry `boot_strapping` traits; TX/RX + * carry the USB-UART sharing note; GPIO16/17 carry the PSRAM + * sub-variant restriction. + * - Power-input exclusivity (Micro-USB OR 5V pin OR 3V3 pin — never two at + * once) is modelled as a `one_of` interface group. + * - The seven ProtoPart board warnings are preserved verbatim as + * `board_warning` module traits; purchase info and the preview-artifact + * pointer (no OpenUHD home) are preserved as module traits too. + */ + +import type { + InterfaceDef, + ModuleDef, + SlotDef, + TraitDef, +} from "../../src/types/index.js"; +import type { Parameter } from "../../src/types/parameter.js"; +import { + Ground, + Pin, + SPI, + defineModule, +} from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Electrical constants — ProtoPart power_domains +// --------------------------------------------------------------------------- + +/** io_3v3 power domain: on-board AMS1117-3.3 LDO output. */ +const IO_3V3_RANGE: [number, number] = [3, 3.6]; +/** usb_5v power domain: Micro-USB VBUS or the 5V header pin. */ +const USB_5V_RANGE: [number, number] = [4.75, 5.25]; +/** io_3v3 domain max current (LDO budget), mA. */ +const IO_3V3_MAX_MA = 600; +/** usb_5v domain max current (USB host / external supply limit), mA. */ +const USB_5V_MAX_MA = 500; + +/** Source citation used by every per-pin functions trait. */ +const FUNCTIONS_SOURCE = + "ProtoPart esp32-devkitc-v4 definition (ESP32-DevKitC V4 user guide; ESP32-WROOM-32E datasheet)"; + +// --------------------------------------------------------------------------- +// Verbatim ProtoPart per-pin function metadata +// --------------------------------------------------------------------------- + +/** One entry of a ProtoPart resource `functions` list, carried verbatim. */ +interface DevkitFunction { + /** Verbatim ProtoPart function name (aliases included). */ + name: string; + direction: "sink" | "source" | "bidirectional"; + signal_class?: string; + /** Verbatim ProtoPart function description. */ + note?: string; +} + +// Shared pad-capability functions (identical text across pins in the JSON). +const FN_INPUT_ENABLE: DevkitFunction = { + name: "input_enable", direction: "sink", signal_class: "digital_pad", + note: "Pad supports input-enable mode.", +}; +const FN_IE_ALIAS: DevkitFunction = { + name: "IE", direction: "sink", signal_class: "digital_pad", + note: "IE alias. Pad supports input-enable mode.", +}; +const FN_INPUT_DISABLE: DevkitFunction = { + name: "input_disable", direction: "sink", signal_class: "digital_pad", + note: "Pad supports input-disable mode.", +}; +const FN_ID_ALIAS: DevkitFunction = { + name: "ID", direction: "sink", signal_class: "digital_pad", + note: "ID alias. Pad supports input-disable mode.", +}; +const FN_WEAK_PULLUP: DevkitFunction = { + name: "weak_pullup", direction: "bidirectional", signal_class: "digital_pad", + note: "Pad has an internal weak pull-up.", +}; +const FN_WPU_ALIAS: DevkitFunction = { + name: "WPU", direction: "bidirectional", signal_class: "digital_pad", + note: "WPU alias. Pad has an internal weak pull-up.", +}; +const FN_WEAK_PULLDOWN: DevkitFunction = { + name: "Weak Pull-Down Capability", direction: "bidirectional", signal_class: "digital_pad", + note: "Pad has an internal weak pull-down.", +}; +const FN_OPEN_DRAIN: DevkitFunction = { + name: "Open Drain Capability", direction: "source", signal_class: "digital_pad", + note: "Pad supports open-drain output mode.", +}; +const FN_SPI_FLASH: DevkitFunction = { + name: "spi_flash", direction: "bidirectional", + note: "Reserved on this board for on-module SPI flash.", +}; + +function fnGpioIo(gpio: number): DevkitFunction { + return { + name: "GPIO Input / Output", direction: "bidirectional", signal_class: "digital", + note: `GPIO${gpio} digital I/O pad.`, + }; +} + +function fnGpioInputOnly(gpio: number): DevkitFunction { + return { + name: "GPIO Input Only", direction: "sink", signal_class: "digital", + note: `GPIO${gpio} input-only digital pad.`, + }; +} + +function fnAdc(converter: 1 | 2, channel: number): DevkitFunction { + return { + name: `ADC${converter}`, direction: "sink", signal_class: "analog", + note: `ADC${converter}_CH${channel} ADC input.`, + }; +} + +function fnRtcGpio(rtc: number, gpio: number, inputOnly: boolean): DevkitFunction { + return { + name: "RTC GPIO", direction: inputOnly ? "sink" : "bidirectional", signal_class: "rtc", + note: `RTC GPIO${rtc} function on GPIO${gpio}.`, + }; +} + +/** The JSON lists each touch channel twice: lowercase + uppercase alias. */ +function fnTouch(n: number): DevkitFunction[] { + return [ + { name: `touch${n}`, direction: "sink", signal_class: "capacitive_touch", note: `Touch sensor channel ${n}.` }, + { name: `TOUCH${n}`, direction: "sink", signal_class: "capacitive_touch", note: `TOUCH${n} alias. Touch sensor channel ${n}.` }, + ]; +} + +// --------------------------------------------------------------------------- +// Header GPIO pin builder +// --------------------------------------------------------------------------- + +function unique(values: string[]): string[] { + return [...new Set(values)]; +} + +interface DevkitGpioSpec { + /** ProtoPart resource id (kept as the interface id). */ + id: string; + /** Silkscreen / ProtoPart display name. */ + name: string; + /** Header designator, e.g. "J2-13" — the board's pin, in header order. */ + pin: string; + /** ESP32 GPIO number behind this header pin. */ + gpio: number; + /** GPIO34-39 group: no output driver (ProtoPart "GPIO Input Only"). */ + inputOnly?: boolean; + /** Wired to the on-module SPI flash — not general I/O (warning #2). */ + flashReserved?: boolean; + adc?: { converter: 1 | 2; channel: number }; + dac?: 1 | 2; + touch?: number; + rtcGpio?: number; + /** Internal weak pull the ProtoPart functions declare for this pad. */ + pull?: "up" | "down"; + /** Pin-builder capability flags for bus signals this pad carries. */ + flags?: { + i2cSda?: boolean; i2cScl?: boolean; + spiMosi?: boolean; spiMiso?: boolean; spiSck?: boolean; spiSs?: boolean; + uartRx?: boolean; uartTx?: boolean; + }; + /** Extra canonical capability tags (jtag_*, spi_flash_*, straps, ...). */ + extraCaps?: string[]; + /** Verbatim ProtoPart resource functions. */ + functions: DevkitFunction[]; + /** Verbatim ProtoPart resource description. */ + description: string; + extraTraits?: TraitDef[]; + bridgesTo?: string[]; +} + +/** Build one schematic-honest header GPIO pin from its ProtoPart resource. */ +function devkitGpio(spec: DevkitGpioSpec): InterfaceDef { + const base = Pin({ + id: spec.id, + name: spec.name, + pin: spec.pin, + voltageV: IO_3V3_RANGE, // io_3v3 power domain (ProtoPart power_domain_id) + capabilities: { + inputOnly: spec.inputOnly, + analogIn: spec.adc !== undefined, + analogOut: spec.dac !== undefined, + touch: spec.touch !== undefined, + ...(spec.flags ?? {}), + }, + }); + + const capabilities = unique([ + ...(base.capabilities ?? []), + `gpio${spec.gpio}`, + // GPIO pool tags: input-only pins and flash-reserved pins are excluded + // from the general "GPIO Input / Output" pool, exactly as the ProtoPart + // functions are laid out. + ...(spec.inputOnly ? ["gpio_input_only"] : []), + ...(!spec.inputOnly && !spec.flashReserved ? ["gpio_io"] : []), + ...(spec.adc ? [`adc${spec.adc.converter}_in`] : []), + ...(spec.rtcGpio !== undefined ? ["rtc_gpio"] : []), + // Every GPIO pad in the ProtoPart source carries "Open Drain Capability" + // and an input-enable (IE) or input-disable (ID) function. + "open_drain", + "pad_input_gate", + ...(spec.pull === "up" ? ["weak_pullup"] : []), + ...(spec.pull === "down" ? ["weak_pulldown"] : []), + ...(spec.extraCaps ?? []), + ]); + + const traits: TraitDef[] = [ + { + type: "devkitc_pin_functions", + params: { + source: FUNCTIONS_SOURCE, + description: spec.description, + gpio: spec.gpio, + functions: spec.functions, + }, + }, + { type: "power_domain", params: { domain: "io_3v3" } }, + ...(spec.pull === "up" + ? [{ type: "internal_pulls", params: { pull_up: true, note: "Pad has an internal weak pull-up (ProtoPart weak_pullup/WPU function)." } }] + : []), + ...(spec.pull === "down" + ? [{ type: "internal_pulls", params: { pull_down: true, note: "Pad has an internal weak pull-down (ProtoPart Weak Pull-Down Capability function)." } }] + : []), + ...(spec.flashReserved + ? [ + { + type: "usage_restriction", + params: { + restriction: + "Wired to the on-module SPI flash on the ESP32-WROOM-32E and should not be used as general I/O.", + source: + "ProtoPart warning: 'GPIOs 6-11 (header pins D0, D1, D2, D3, CMD, CLK) are wired to the on-package SPI flash and MUST NOT be used as general I/O on the WROOM-32E variant.'", + }, + }, + ] + : []), + ...(spec.extraTraits ?? []), + ]; + + return { + ...base, + capabilities, + traits, + ...(spec.bridgesTo ? { bridgesTo: spec.bridgesTo } : {}), + }; +} + +// --------------------------------------------------------------------------- +// J2 header (19 pins) — ProtoPart resources in header order J2-1..J2-19 +// --------------------------------------------------------------------------- + +/** J2-1 — 3v3: bidirectional 3.3 V rail pin (LDO output or regulated input). */ +const pin3v3: InterfaceDef = { + id: "3v3", + name: "3v3", + pin: "J2-1", + domain: "electrical", + exposed: true, + default_active: true, + // ProtoPart functions: power_input (sink) + power_output (source) — the + // pin is honestly bidirectional, not a plain PowerIn. + protocols: [{ type: "power", roles: ["input", "output"] }], + capabilities: ["power_3v3"], + parameters: [ + { id: "voltage", unit: "V", value: 3.3, range: IO_3V3_RANGE }, + { id: "max_current", unit: "A", value: IO_3V3_MAX_MA / 1000 }, + ], + traits: [ + { + type: "devkitc_pin_functions", + params: { + source: FUNCTIONS_SOURCE, + description: + "3.3V header pin (J2 pin 1). Output of the on-board LDO when powered from USB/5V; can also be used as a regulated 3.3V input to bypass the LDO.", + functions: [ + { name: "power_input", direction: "sink", note: "Power input on this header pin." }, + { name: "power_output", direction: "source", note: "Power output on this header pin." }, + { name: "power_3v3", direction: "bidirectional", signal_class: "power", note: "3.3 V board rail on J2 pin 1." }, + { name: "3V3", direction: "bidirectional", signal_class: "power", note: "3V3 alias. 3.3 V board rail on J2 pin 1." }, + ] satisfies DevkitFunction[], + }, + }, + { type: "power_domain", params: { domain: "io_3v3" } }, + { + type: "internal_regulator", + params: { + description: + "On-board AMS1117-3.3 LDO output. Supplies the ESP32-WROOM-32E module and the 3V3 header pin.", + source: "ProtoPart power domain io_3v3", + }, + }, + ], +}; + +/** J2-2 — EN: CHIP_PU / active-low reset, tied to the EN push-button. */ +const pinEn: InterfaceDef = { + id: "en", + name: "EN", + pin: "J2-2", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input"] }], + capabilities: ["chip_pu", "reset_input"], + parameters: [{ id: "voltage", unit: "V", range: IO_3V3_RANGE }], + traits: [ + { + type: "devkitc_pin_functions", + params: { + source: FUNCTIONS_SOURCE, + description: + "CHIP_PU / EN active-low reset. Pull low to reset the ESP32. Also tied to the on-board EN push-button.", + functions: [ + { name: "reset", direction: "sink", note: "Active-low ESP32 reset input." }, + { name: "enable", direction: "sink", signal_class: "control", note: "ESP32 enable/reset input." }, + { name: "chip_pu", direction: "sink", signal_class: "control", note: "CHIP_PU input: high enables the chip; low powers it down." }, + { name: "EN", direction: "sink", signal_class: "control", note: "EN alias. ESP32 enable/reset input." }, + { name: "PU", direction: "sink", signal_class: "control", note: "PU alias. CHIP_PU input: high enables the chip; low powers it down." }, + ] satisfies DevkitFunction[], + }, + }, + { type: "power_domain", params: { domain: "io_3v3" } }, + { + type: "push_button", + params: { button: "EN", behavior: "Pressing the on-board EN button pulls this line low and resets the ESP32." }, + }, + ], +}; + +/** J2-14 GND, J3-1 GND, J3-7 GND: one shared board ground net. */ +function devkitGnd(id: string, name: string, pin: string): InterfaceDef { + return { + ...Ground({ id, name, pin }), + traits: [ + { + type: "devkitc_pin_functions", + params: { + source: FUNCTIONS_SOURCE, + functions: [ + { name: "ground", direction: "bidirectional", note: "Board ground reference." }, + { name: "GND", direction: "bidirectional", signal_class: "power", note: "GND alias. Board ground reference." }, + ] satisfies DevkitFunction[], + }, + }, + { type: "power_domain", params: { domain: "gnd" } }, + { + type: "net_shareable", + params: { + net: "gnd", + policy: "single_ground_instance_may_serve_all_members", + members: ["gnd", "gnd_j3_1", "gnd_j3_7"], + }, + }, + ], + }; +} + +/** J2-19 — 5V: bidirectional 5 V rail pin (board power input or VBUS out). */ +const pin5v: InterfaceDef = { + id: "5v", + name: "5V", + pin: "J2-19", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "power", roles: ["input", "output"] }], + capabilities: ["power_5v0"], + parameters: [ + { id: "voltage", unit: "V", value: 5, range: USB_5V_RANGE }, + { id: "max_current", unit: "A", value: USB_5V_MAX_MA / 1000 }, + ], + traits: [ + { + type: "devkitc_pin_functions", + params: { + source: FUNCTIONS_SOURCE, + description: + "5V header pin (J2 pin 19). Can be used as a 5V input (board power) OR as a 5V output when powered via Micro-USB.", + functions: [ + { name: "power_input", direction: "sink", note: "Power input on this header pin." }, + { name: "power_output", direction: "source", note: "Power output on this header pin." }, + { name: "power_5v0", direction: "bidirectional", signal_class: "power", note: "5 V board rail on J2 pin 19." }, + { name: "5V0", direction: "bidirectional", signal_class: "power", note: "5V0 alias. 5 V board rail on J2 pin 19." }, + ] satisfies DevkitFunction[], + }, + }, + { type: "power_domain", params: { domain: "usb_5v" } }, + ], +}; + +const j2Gpio: InterfaceDef[] = [ + devkitGpio({ + id: "vp", name: "VP", pin: "J2-3", gpio: 36, inputOnly: true, + adc: { converter: 1, channel: 0 }, rtcGpio: 0, + extraCaps: ["sensor_vp"], + description: "GPIO36 (VP / S_VP / SENSOR_VP). Input-only RTC GPIO; ADC1 channel 0.", + functions: [ + FN_INPUT_DISABLE, + { name: "sensor_vp", direction: "sink", signal_class: "analog", note: "SENSOR_VP analog input on GPIO36." }, + { name: "S_VP", direction: "sink", signal_class: "analog", note: "S_VP alias. SENSOR_VP analog input on GPIO36." }, + FN_ID_ALIAS, + fnGpioInputOnly(36), + fnAdc(1, 0), + fnRtcGpio(0, 36, true), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "vn", name: "VN", pin: "J2-4", gpio: 39, inputOnly: true, + adc: { converter: 1, channel: 3 }, rtcGpio: 3, + extraCaps: ["sensor_vn"], + description: "GPIO39 (VN / S_VN / SENSOR_VN). Input-only RTC GPIO; ADC1 channel 3.", + functions: [ + FN_INPUT_DISABLE, + { name: "sensor_vn", direction: "sink", signal_class: "analog", note: "SENSOR_VN analog input on GPIO39." }, + { name: "S_VN", direction: "sink", signal_class: "analog", note: "S_VN alias. SENSOR_VN analog input on GPIO39." }, + FN_ID_ALIAS, + fnGpioInputOnly(39), + fnAdc(1, 3), + fnRtcGpio(3, 39, true), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_34", name: "Pin 34", pin: "J2-5", gpio: 34, inputOnly: true, + adc: { converter: 1, channel: 6 }, rtcGpio: 4, + extraCaps: ["vdet"], + description: "GPIO34 (VDET_1). Input-only RTC GPIO; ADC1 channel 6.", + functions: [ + FN_INPUT_DISABLE, + FN_ID_ALIAS, + fnGpioInputOnly(34), + fnAdc(1, 6), + { name: "Voltage Detection", direction: "sink", signal_class: "analog", note: "VDET_1 analog input on GPIO34." }, + fnRtcGpio(4, 34, true), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_35", name: "Pin 35", pin: "J2-6", gpio: 35, inputOnly: true, + adc: { converter: 1, channel: 7 }, rtcGpio: 5, + extraCaps: ["vdet"], + description: "GPIO35 (VDET_2). Input-only RTC GPIO; ADC1 channel 7.", + functions: [ + FN_INPUT_DISABLE, + FN_ID_ALIAS, + fnGpioInputOnly(35), + fnAdc(1, 7), + { name: "Voltage Detection", direction: "sink", signal_class: "analog", note: "VDET_2 analog input on GPIO35." }, + fnRtcGpio(5, 35, true), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_32", name: "Pin 32", pin: "J2-7", gpio: 32, + adc: { converter: 1, channel: 4 }, touch: 9, rtcGpio: 9, + extraCaps: ["xtal_32k_p"], + description: "GPIO32. ADC1_CH4, TOUCH9, XTAL_32K_P (32 kHz crystal input).", + functions: [ + FN_INPUT_DISABLE, + { name: "xtal_32k_p", direction: "sink", signal_class: "clock", note: "32.768 kHz crystal oscillator input on GPIO32." }, + ...fnTouch(9), + { name: "32K_XP", direction: "sink", signal_class: "clock", note: "32K_XP alias. 32.768 kHz crystal oscillator input on GPIO32." }, + FN_ID_ALIAS, + fnGpioIo(32), + fnAdc(1, 4), + fnRtcGpio(9, 32, false), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_33", name: "Pin 33", pin: "J2-8", gpio: 33, + adc: { converter: 1, channel: 5 }, touch: 8, rtcGpio: 8, + extraCaps: ["xtal_32k_n"], + description: "GPIO33. ADC1_CH5, TOUCH8, XTAL_32K_N.", + functions: [ + FN_INPUT_DISABLE, + { name: "xtal_32k_n", direction: "source", signal_class: "clock", note: "32.768 kHz crystal oscillator output on GPIO33." }, + ...fnTouch(8), + { name: "32K_XN", direction: "source", signal_class: "clock", note: "32K_XN alias. 32.768 kHz crystal oscillator output on GPIO33." }, + FN_ID_ALIAS, + fnGpioIo(33), + fnAdc(1, 5), + fnRtcGpio(8, 33, false), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_25", name: "Pin 25", pin: "J2-9", gpio: 25, + adc: { converter: 2, channel: 8 }, dac: 1, rtcGpio: 6, + description: "GPIO25. ADC2_CH8 and DAC1.", + functions: [ + FN_INPUT_DISABLE, + FN_ID_ALIAS, + fnGpioIo(25), + fnAdc(2, 8), + { name: "DAC", direction: "source", signal_class: "analog", note: "DAC_1 output on GPIO25." }, + fnRtcGpio(6, 25, false), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_26", name: "Pin 26", pin: "J2-10", gpio: 26, + adc: { converter: 2, channel: 9 }, dac: 2, rtcGpio: 7, + description: "GPIO26. ADC2_CH9 and DAC2.", + functions: [ + FN_INPUT_DISABLE, + FN_ID_ALIAS, + fnGpioIo(26), + fnAdc(2, 9), + { name: "DAC", direction: "source", signal_class: "analog", note: "DAC_2 output on GPIO26." }, + fnRtcGpio(7, 26, false), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_27", name: "Pin 27", pin: "J2-11", gpio: 27, + adc: { converter: 2, channel: 7 }, touch: 7, rtcGpio: 17, + description: "GPIO27. ADC2_CH7, TOUCH7.", + functions: [ + FN_INPUT_DISABLE, + ...fnTouch(7), + FN_ID_ALIAS, + fnGpioIo(27), + fnAdc(2, 7), + fnRtcGpio(17, 27, false), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_14", name: "Pin 14", pin: "J2-12", gpio: 14, + adc: { converter: 2, channel: 6 }, touch: 6, rtcGpio: 16, pull: "up", + flags: { spiSck: true }, + extraCaps: ["jtag_tms"], + description: "GPIO14. ADC2_CH6, TOUCH6, MTMS (JTAG TMS), HSPI_CLK.", + functions: [ + { name: "spi_sck", direction: "source", note: "SPI clock signal on GPIO14." }, + FN_INPUT_ENABLE, + FN_WEAK_PULLUP, + { name: "hspi_sck", direction: "source", signal_class: "digital", note: "HSPI clock output on GPIO14." }, + { name: "jtag_tms", direction: "sink", signal_class: "digital", note: "JTAG MTMS / TMS on GPIO14." }, + ...fnTouch(6), + { name: "HSPI_SCK", direction: "source", signal_class: "digital", note: "HSPI_SCK alias. HSPI clock output on GPIO14." }, + { name: "MTMS", direction: "sink", signal_class: "digital", note: "MTMS alias. JTAG MTMS / TMS on GPIO14." }, + FN_IE_ALIAS, + FN_WPU_ALIAS, + fnGpioIo(14), + fnAdc(2, 6), + fnRtcGpio(16, 14, false), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_12", name: "Pin 12", pin: "J2-13", gpio: 12, + adc: { converter: 2, channel: 5 }, touch: 5, rtcGpio: 15, pull: "up", + flags: { spiMiso: true }, + extraCaps: ["jtag_tdi", "strap_vdd_flash"], + description: "GPIO12. ADC2_CH5, TOUCH5, MTDI (JTAG TDI), HSPI_Q. Strapping pin (controls flash voltage at boot).", + extraTraits: [ + { + type: "boot_strapping", + params: { + controls: "MTDI strap selects VDD_SDIO flash voltage at reset.", + // Audit note: per the ESP32 datasheet strapping-pin table, MTDI's + // default strap configuration is PULL-DOWN (VDD_SDIO = 3.3 V). The + // weak_pullup/WPU function on this pad is a programmable pad + // capability, not the boot default. + default_configuration: "Pull-down (bit value 0 → VDD_SDIO = 3.3 V) per the ESP32 series datasheet strapping-pins table.", + source: + "ProtoPart pin_12 vdd_flash_strap function; board warning: strapping pins pulled to conflicting levels during reset can prevent boot or change flash voltage.", + }, + }, + ], + functions: [ + { name: "spi_miso", direction: "sink", note: "SPI MISO signal on GPIO12." }, + FN_INPUT_ENABLE, + FN_WEAK_PULLUP, + { name: "vdd_flash_strap", direction: "sink", signal_class: "strapping", note: "MTDI strap selects VDD_SDIO flash voltage at reset." }, + { name: "hspi_miso", direction: "sink", signal_class: "digital", note: "HSPI MISO input on GPIO12." }, + { name: "jtag_tdi", direction: "sink", signal_class: "digital", note: "JTAG MTDI / TDI on GPIO12." }, + ...fnTouch(5), + { name: "HSPI_MISO", direction: "sink", signal_class: "digital", note: "HSPI_MISO alias. HSPI MISO input on GPIO12." }, + { name: "MTDI", direction: "sink", signal_class: "digital", note: "MTDI alias. JTAG MTDI / TDI on GPIO12." }, + { name: "VDD_FLASH", direction: "sink", signal_class: "strapping", note: "VDD_FLASH alias. MTDI strap selects VDD_SDIO flash voltage at reset." }, + FN_IE_ALIAS, + FN_WPU_ALIAS, + fnGpioIo(12), + fnAdc(2, 5), + fnRtcGpio(15, 12, false), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_13", name: "Pin 13", pin: "J2-15", gpio: 13, + adc: { converter: 2, channel: 4 }, touch: 4, rtcGpio: 14, pull: "down", + flags: { spiMosi: true }, + extraCaps: ["jtag_tck"], + description: "GPIO13. ADC2_CH4, TOUCH4, MTCK (JTAG TCK), HSPI_D.", + functions: [ + { name: "spi_mosi", direction: "source", note: "SPI MOSI signal on GPIO13." }, + FN_INPUT_ENABLE, + { name: "hspi_mosi", direction: "source", signal_class: "digital", note: "HSPI MOSI output on GPIO13." }, + { name: "jtag_tck", direction: "sink", signal_class: "digital", note: "JTAG MTCK / TCK on GPIO13." }, + ...fnTouch(4), + { name: "HSPI_MOSI", direction: "source", signal_class: "digital", note: "HSPI_MOSI alias. HSPI MOSI output on GPIO13." }, + { name: "MTCK", direction: "sink", signal_class: "digital", note: "MTCK alias. JTAG MTCK / TCK on GPIO13." }, + FN_IE_ALIAS, + fnGpioIo(13), + fnAdc(2, 4), + fnRtcGpio(14, 13, false), + FN_OPEN_DRAIN, + FN_WEAK_PULLDOWN, + ], + }), +]; + +/** Flash-bus header pins (D0-D3, CMD, CLK): reserved for the module flash. */ +function flashPin(config: { + id: string; name: string; pin: string; gpio: number; + /** Canonical capability tag, e.g. "spi_flash_d2". */ + cap: string; + /** Verbatim ProtoPart signal function + its alias entry. */ + signal: DevkitFunction; alias: DevkitFunction; + /** JSON function order differs between D2/D3/CMD and CLK/D0/D1. */ + signalFirst?: boolean; + description: string; +}): InterfaceDef { + return devkitGpio({ + id: config.id, name: config.name, pin: config.pin, gpio: config.gpio, + flashReserved: true, pull: "up", + extraCaps: [config.cap], + description: config.description, + functions: config.signalFirst + ? [FN_SPI_FLASH, config.signal, FN_INPUT_ENABLE, FN_WEAK_PULLUP, config.alias, FN_IE_ALIAS, FN_WPU_ALIAS, FN_OPEN_DRAIN] + : [FN_SPI_FLASH, FN_INPUT_ENABLE, FN_WEAK_PULLUP, config.signal, config.alias, FN_IE_ALIAS, FN_WPU_ALIAS, FN_OPEN_DRAIN], + }); +} + +const j2FlashPins: InterfaceDef[] = [ + flashPin({ + id: "d2", name: "D2", pin: "J2-16", gpio: 9, cap: "spi_flash_d2", + signal: { name: "spi_flash_d2", direction: "bidirectional", signal_class: "digital", note: "SPI flash data 2; reserved on this board." }, + alias: { name: "D2", direction: "bidirectional", signal_class: "digital", note: "D2 alias. SPI flash data 2; reserved on this board." }, + description: "D2 / SD_DATA2 / GPIO9 (J2-16). Wired to on-module SPI flash on the ESP32-WROOM-32E and should not be used as general I/O.", + }), + flashPin({ + id: "d3", name: "D3", pin: "J2-17", gpio: 10, cap: "spi_flash_d3", + signal: { name: "spi_flash_d3", direction: "bidirectional", signal_class: "digital", note: "SPI flash data 3; reserved on this board." }, + alias: { name: "D3", direction: "bidirectional", signal_class: "digital", note: "D3 alias. SPI flash data 3; reserved on this board." }, + description: "D3 / SD_DATA3 / GPIO10 (J2-17). Wired to on-module SPI flash on the ESP32-WROOM-32E and should not be used as general I/O.", + }), + flashPin({ + id: "cmd", name: "CMD", pin: "J2-18", gpio: 11, cap: "spi_flash_cmd", + signal: { name: "spi_flash_cmd", direction: "bidirectional", signal_class: "digital", note: "SPI flash command; reserved on this board." }, + alias: { name: "CMD", direction: "bidirectional", signal_class: "digital", note: "CMD alias. SPI flash command; reserved on this board." }, + description: "CMD / SD_CMD / GPIO11 (J2-18). Wired to on-module SPI flash on the ESP32-WROOM-32E and should not be used as general I/O.", + }), +]; + +// --------------------------------------------------------------------------- +// J3 header (19 pins) — ProtoPart resources in header order J3-1..J3-19 +// --------------------------------------------------------------------------- + +const j3Gpio: InterfaceDef[] = [ + devkitGpio({ + id: "pin_23", name: "Pin 23", pin: "J3-2", gpio: 23, + flags: { spiMosi: true }, + description: "GPIO23. Default VSPI_MOSI.", + functions: [ + { name: "spi_mosi", direction: "source", note: "SPI MOSI signal on GPIO23." }, + { name: "vspi_mosi", direction: "source", signal_class: "digital", note: "VSPI MOSI output on GPIO23." }, + { name: "wire_mosi", direction: "source", signal_class: "digital", note: "Arduino SPI MOSI alias on GPIO23." }, + FN_INPUT_ENABLE, + { name: "VSPI_MOSI", direction: "source", signal_class: "digital", note: "VSPI_MOSI alias. VSPI MOSI output on GPIO23." }, + { name: "WIRE_MOSI", direction: "source", signal_class: "digital", note: "WIRE_MOSI alias. Arduino SPI MOSI alias on GPIO23." }, + FN_IE_ALIAS, + fnGpioIo(23), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_22", name: "Pin 22", pin: "J3-3", gpio: 22, + flags: { i2cScl: true }, + description: "GPIO22. Default I2C0_SCL.", + functions: [ + { name: "i2c_scl", direction: "source", note: "I2C clock signal on GPIO22." }, + { name: "wire_scl", direction: "source", signal_class: "digital", note: "Arduino Wire SCL / I2C clock on GPIO22." }, + FN_INPUT_ENABLE, + { name: "WIRE_SCL", direction: "source", signal_class: "digital", note: "WIRE_SCL alias. Arduino Wire SCL / I2C clock on GPIO22." }, + FN_IE_ALIAS, + fnGpioIo(22), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "tx", name: "TX", pin: "J3-4", gpio: 1, pull: "up", + flags: { uartTx: true }, + description: "GPIO1 / U0TXD. Primary serial transmit; shared with the on-board USB-UART bridge. Strapping behavior: pulled up at boot.", + bridgesTo: ["micro_usb"], + extraTraits: [ + { + type: "shared_with_usb_bridge", + params: { + note: "TX (GPIO1) is shared with the on-board USB-to-UART bridge. To use UART0 with an external peer, unplug USB or tri-state the bridge by holding the bridge IC in reset.", + source: "ProtoPart board warning #7", + }, + }, + // Audit correction: GPIO1/U0TXD is NOT one of the five ESP32 strapping + // pins (GPIO0/2/5/12/15 per the ESP32 datasheet strapping-pin table); + // "pulled up at boot" is its IO_MUX reset default, not a strap. + { + type: "reset_default", + params: { + note: "Weak pull-up at reset (IO_MUX default for U0TXD). Not a strapping pin — the ESP32 strapping pins are GPIO0/2/5/12/15 only.", + source: "ESP32 series datasheet: IO_MUX pad list and strapping-pins table.", + }, + }, + ], + functions: [ + { name: "uart_tx", direction: "source", note: "UART transmit output on GPIO1." }, + { name: "u0txd", direction: "source", signal_class: "digital", note: "UART0 transmit output on GPIO1." }, + { name: "serial_tx", direction: "source", signal_class: "digital", note: "Primary serial TX alias on GPIO1." }, + FN_INPUT_ENABLE, + FN_WEAK_PULLUP, + { name: "U0TXD", direction: "source", signal_class: "digital", note: "U0TXD alias. UART0 transmit output on GPIO1." }, + { name: "SERIAL_TX", direction: "source", signal_class: "digital", note: "SERIAL_TX alias. Primary serial TX alias on GPIO1." }, + { name: "TX", direction: "source", note: "TX alias. UART transmit output on GPIO1." }, + FN_IE_ALIAS, + FN_WPU_ALIAS, + fnGpioIo(1), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "rx", name: "RX", pin: "J3-5", gpio: 3, pull: "up", + flags: { uartRx: true }, + description: "GPIO3 / U0RXD. Primary serial receive; shared with the on-board USB-UART bridge.", + bridgesTo: ["micro_usb"], + extraTraits: [ + { + type: "shared_with_usb_bridge", + params: { + note: "RX (GPIO3) is shared with the on-board USB-to-UART bridge. To use UART0 with an external peer, unplug USB or tri-state the bridge by holding the bridge IC in reset.", + source: "ProtoPart board warning #7", + }, + }, + ], + functions: [ + { name: "uart_rx", direction: "sink", note: "UART receive input on GPIO3." }, + { name: "u0rxd", direction: "sink", signal_class: "digital", note: "UART0 receive input on GPIO3." }, + { name: "serial_rx", direction: "sink", signal_class: "digital", note: "Primary serial RX alias on GPIO3." }, + FN_INPUT_ENABLE, + FN_WEAK_PULLUP, + { name: "U0RXD", direction: "sink", signal_class: "digital", note: "U0RXD alias. UART0 receive input on GPIO3." }, + { name: "SERIAL_RX", direction: "sink", signal_class: "digital", note: "SERIAL_RX alias. Primary serial RX alias on GPIO3." }, + { name: "RX", direction: "sink", note: "RX alias. UART receive input on GPIO3." }, + FN_IE_ALIAS, + FN_WPU_ALIAS, + fnGpioIo(3), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_21", name: "Pin 21", pin: "J3-6", gpio: 21, + flags: { i2cSda: true }, + description: "GPIO21. Default I2C0_SDA.", + functions: [ + { name: "i2c_sda", direction: "bidirectional", note: "I2C data signal on GPIO21." }, + { name: "wire_sda", direction: "bidirectional", signal_class: "digital", note: "Arduino Wire SDA / I2C data on GPIO21." }, + FN_INPUT_ENABLE, + { name: "WIRE_SDA", direction: "bidirectional", signal_class: "digital", note: "WIRE_SDA alias. Arduino Wire SDA / I2C data on GPIO21." }, + FN_IE_ALIAS, + fnGpioIo(21), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_19", name: "Pin 19", pin: "J3-8", gpio: 19, + flags: { spiMiso: true }, + description: "GPIO19. Default VSPI_MISO.", + functions: [ + { name: "spi_miso", direction: "sink", note: "SPI MISO signal on GPIO19." }, + { name: "vspi_miso", direction: "sink", signal_class: "digital", note: "VSPI MISO input on GPIO19." }, + FN_INPUT_ENABLE, + { name: "VSPI_MISO", direction: "sink", signal_class: "digital", note: "VSPI_MISO alias. VSPI MISO input on GPIO19." }, + FN_IE_ALIAS, + fnGpioIo(19), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_18", name: "Pin 18", pin: "J3-9", gpio: 18, + flags: { spiSck: true }, + description: "GPIO18. Default VSPI_SCK.", + functions: [ + { name: "spi_sck", direction: "source", note: "SPI clock signal on GPIO18." }, + { name: "vspi_sck", direction: "source", signal_class: "digital", note: "VSPI clock output on GPIO18." }, + FN_INPUT_ENABLE, + { name: "VSPI_SCK", direction: "source", signal_class: "digital", note: "VSPI_SCK alias. VSPI clock output on GPIO18." }, + FN_IE_ALIAS, + fnGpioIo(18), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_5", name: "Pin 5", pin: "J3-10", gpio: 5, pull: "up", + flags: { spiSs: true }, + extraCaps: ["sdio"], + description: "GPIO5. Default VSPI_CS0. Strapping pin (pulled up at boot).", + extraTraits: [ + { + type: "boot_strapping", + params: { + controls: "GPIO5 SDIO timing strap; pulled up at boot.", + source: "ProtoPart pin_5 sdio function and description; board warning: strapping pins pulled to conflicting levels during reset can prevent boot or change flash voltage.", + }, + }, + ], + functions: [ + { name: "spi_cs", direction: "source", note: "SPI chip-select signal on GPIO5." }, + { name: "vspi_ss", direction: "source", signal_class: "digital", note: "VSPI chip-select output on GPIO5." }, + { name: "sdio", direction: "bidirectional", signal_class: "digital", note: "GPIO5 SDIO timing strap / SDIO alternate function." }, + FN_INPUT_ENABLE, + FN_WEAK_PULLUP, + { name: "VSPI_SS", direction: "source", signal_class: "digital", note: "VSPI_SS alias. VSPI chip-select output on GPIO5." }, + { name: "SDIO", direction: "bidirectional", signal_class: "digital", note: "SDIO alias. GPIO5 SDIO timing strap / SDIO alternate function." }, + FN_IE_ALIAS, + FN_WPU_ALIAS, + fnGpioIo(5), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_17", name: "Pin 17", pin: "J3-11", gpio: 17, + flags: { uartTx: true }, + // Audit correction: 28 is the WROOM-32E MODULE pin number (datasheet pin + // definitions: IO17 = module pin 28); the ESP32 chip pin for GPIO17 is 27. + description: "GPIO17 / U2TXD (module pin 28). Fully usable on the default ESP32-WROOM-32E module. NOTE: on WROVER-E / WROVER-IE and on -R2 sub-variants of WROOM-32E (ESP32-D0WDR2-V3 die with embedded 2 MB PSRAM), GPIO17 is consumed by the in-package PSRAM and cannot be used as general I/O.", + extraTraits: [ + { + type: "variant_restriction", + params: { + note: "Not usable on WROVER-E / WROVER-IE variants or on -R2 (ESP32-D0WDR2-V3) sub-variants of WROOM-32E, where the in-package PSRAM consumes this pin.", + source: "ProtoPart board warning #3", + }, + }, + ], + functions: [ + { name: "uart_tx", direction: "source", note: "UART transmit output on GPIO17." }, + FN_INPUT_ENABLE, + FN_IE_ALIAS, + fnGpioIo(17), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_16", name: "Pin 16", pin: "J3-12", gpio: 16, + flags: { uartRx: true }, + // Audit correction: 27 is the WROOM-32E MODULE pin number (datasheet pin + // definitions: IO16 = module pin 27); the ESP32 chip pin for GPIO16 is 25. + description: "GPIO16 / U2RXD (module pin 27). Fully usable on the default ESP32-WROOM-32E module. NOTE: on WROVER-E / WROVER-IE and on -R2 sub-variants of WROOM-32E (ESP32-D0WDR2-V3 die with embedded 2 MB PSRAM), GPIO16 is consumed by the in-package PSRAM and cannot be used as general I/O.", + extraTraits: [ + { + type: "variant_restriction", + params: { + note: "Not usable on WROVER-E / WROVER-IE variants or on -R2 (ESP32-D0WDR2-V3) sub-variants of WROOM-32E, where the in-package PSRAM consumes this pin.", + source: "ProtoPart board warning #3", + }, + }, + ], + functions: [ + { name: "uart_rx", direction: "sink", note: "UART receive input on GPIO16." }, + FN_INPUT_ENABLE, + FN_IE_ALIAS, + fnGpioIo(16), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_4", name: "Pin 4", pin: "J3-13", gpio: 4, + adc: { converter: 2, channel: 0 }, touch: 0, rtcGpio: 10, pull: "down", + description: "GPIO4. ADC2_CH0, TOUCH0.", + functions: [ + ...fnTouch(0), + FN_INPUT_ENABLE, + FN_IE_ALIAS, + fnGpioIo(4), + fnAdc(2, 0), + fnRtcGpio(10, 4, false), + FN_OPEN_DRAIN, + FN_WEAK_PULLDOWN, + ], + }), + devkitGpio({ + id: "pin_0", name: "Pin 0", pin: "J3-14", gpio: 0, + adc: { converter: 2, channel: 1 }, touch: 1, rtcGpio: 11, pull: "up", + extraCaps: ["strap_boot"], + description: "GPIO0. ADC2_CH1, TOUCH1. BOOT strapping pin - hold LOW at reset to enter firmware download mode (used by the on-board BOOT push-button).", + extraTraits: [ + { + type: "boot_strapping", + params: { + controls: "GPIO0 boot strap; low at reset enters download mode.", + source: "ProtoPart pin_0 boot function; used by the on-board BOOT push-button.", + }, + }, + { + type: "push_button", + params: { button: "BOOT", behavior: "Pressing the on-board BOOT button pulls GPIO0 low; hold at reset to enter firmware download mode." }, + }, + { + type: "hardware_errata", + params: { + note: "Earlier ESP32-DevKitC V4 boards may have a 0402 cap (C15) near GPIO0 that causes spurious boot-into-download or distorts a clock signal output on GPIO0. Remove C15 if you hit either issue.", + source: "ProtoPart board warning #6", + }, + }, + ], + functions: [ + ...fnTouch(1), + { name: "boot", direction: "sink", signal_class: "strapping", note: "GPIO0 boot strap; low at reset enters download mode." }, + FN_INPUT_ENABLE, + FN_WEAK_PULLUP, + { name: "BOOT", direction: "sink", signal_class: "strapping", note: "BOOT alias. GPIO0 boot strap; low at reset enters download mode." }, + FN_IE_ALIAS, + FN_WPU_ALIAS, + fnGpioIo(0), + fnAdc(2, 1), + fnRtcGpio(11, 0, false), + FN_OPEN_DRAIN, + ], + }), + devkitGpio({ + id: "pin_2", name: "Pin 2", pin: "J3-15", gpio: 2, + adc: { converter: 2, channel: 2 }, touch: 2, rtcGpio: 12, pull: "down", + description: "GPIO2. ADC2_CH2, TOUCH2. Strapping pin (must be low or floating at boot).", + extraTraits: [ + { + type: "boot_strapping", + params: { + // Audit correction: the low/floating requirement applies only to + // entering the serial bootloader (GPIO0 low); GPIO2 is don't-care + // for normal SPI flash boot (ESP32 datasheet boot-mode strapping). + controls: "Boot-mode strap together with GPIO0: to enter the serial bootloader (GPIO0 low at reset), GPIO2 must also be low or floating; it is don't-care for normal SPI flash boot.", + source: "ProtoPart pin_2 description; board warning #4; ESP32 series datasheet strapping-pins boot-mode table.", + }, + }, + ], + functions: [ + ...fnTouch(2), + FN_INPUT_ENABLE, + FN_IE_ALIAS, + fnGpioIo(2), + fnAdc(2, 2), + fnRtcGpio(12, 2, false), + FN_OPEN_DRAIN, + FN_WEAK_PULLDOWN, + ], + }), + devkitGpio({ + id: "pin_15", name: "Pin 15", pin: "J3-16", gpio: 15, + adc: { converter: 2, channel: 3 }, touch: 3, rtcGpio: 13, pull: "up", + flags: { spiSs: true }, + extraCaps: ["jtag_tdo", "strap_boot_log"], + description: "GPIO15. ADC2_CH3, TOUCH3, MTDO (JTAG TDO). Strapping pin (controls boot-mode messages).", + extraTraits: [ + { + type: "boot_strapping", + params: { + controls: "MTDO strap controls U0TXD boot log output.", + source: "ProtoPart pin_15 boot_log function; board warning #4.", + }, + }, + ], + functions: [ + { name: "touch3", direction: "sink", signal_class: "capacitive_touch", note: "Touch sensor channel 3." }, + { name: "jtag_tdo", direction: "source", signal_class: "digital", note: "JTAG MTDO / TDO on GPIO15." }, + { name: "hspi_ss", direction: "source", signal_class: "digital", note: "HSPI chip-select output on GPIO15." }, + { name: "boot_log", direction: "source", signal_class: "strapping", note: "MTDO strap controls U0TXD boot log output." }, + FN_INPUT_ENABLE, + FN_WEAK_PULLUP, + { name: "TOUCH3", direction: "sink", signal_class: "capacitive_touch", note: "TOUCH3 alias. Touch sensor channel 3." }, + { name: "HSPI_SS", direction: "source", signal_class: "digital", note: "HSPI_SS alias. HSPI chip-select output on GPIO15." }, + { name: "MTDO", direction: "source", signal_class: "digital", note: "MTDO alias. JTAG MTDO / TDO on GPIO15." }, + { name: "LOG", direction: "source", signal_class: "strapping", note: "LOG alias. MTDO strap controls U0TXD boot log output." }, + FN_IE_ALIAS, + FN_WPU_ALIAS, + fnGpioIo(15), + fnAdc(2, 3), + fnRtcGpio(13, 15, false), + FN_OPEN_DRAIN, + ], + }), +]; + +const j3FlashPins: InterfaceDef[] = [ + flashPin({ + id: "d1", name: "D1", pin: "J3-17", gpio: 8, cap: "spi_flash_d1", signalFirst: true, + signal: { name: "spi_flash_d1", direction: "bidirectional", signal_class: "digital", note: "SPI flash data 1; reserved on this board." }, + alias: { name: "D1", direction: "bidirectional", signal_class: "digital", note: "D1 alias. SPI flash data 1; reserved on this board." }, + description: "D1 / SD_DATA1 / GPIO8 (J3-17). Wired to on-module SPI flash on the ESP32-WROOM-32E and should not be used as general I/O.", + }), + flashPin({ + id: "d0", name: "D0", pin: "J3-18", gpio: 7, cap: "spi_flash_d0", signalFirst: true, + signal: { name: "spi_flash_d0", direction: "bidirectional", signal_class: "digital", note: "SPI flash data 0; reserved on this board." }, + alias: { name: "D0", direction: "bidirectional", signal_class: "digital", note: "D0 alias. SPI flash data 0; reserved on this board." }, + description: "D0 / SD_DATA0 / GPIO7 (J3-18). Wired to on-module SPI flash on the ESP32-WROOM-32E and should not be used as general I/O.", + }), + flashPin({ + id: "clk", name: "CLK", pin: "J3-19", gpio: 6, cap: "spi_flash_sck", signalFirst: true, + signal: { name: "spi_flash_sck", direction: "source", signal_class: "digital", note: "SPI flash clock; reserved on this board." }, + alias: { name: "CLK", direction: "source", signal_class: "digital", note: "CLK alias. SPI flash clock; reserved on this board." }, + description: "CLK / SD_CLK / GPIO6 (J3-19). Wired to on-module SPI flash on the ESP32-WROOM-32E and should not be used as general I/O.", + }), +]; + +// --------------------------------------------------------------------------- +// Micro-USB connector +// --------------------------------------------------------------------------- + +const microUsb: InterfaceDef = { + id: "micro_usb", + name: "Micro-USB", + domain: "electrical", + exposed: true, + default_active: true, + // The connector both powers the board (VBUS -> usb_5v rail) and carries + // USB 2.0 FS data into the CP2102N USB-to-UART bridge. + protocols: [ + { type: "usb", roles: ["device"] }, + { type: "power", roles: ["input"] }, + ], + capabilities: ["micro_usb"], + parameters: [{ id: "voltage", unit: "V", value: 5, range: USB_5V_RANGE }], + traits: [ + { + type: "devkitc_pin_functions", + params: { + source: FUNCTIONS_SOURCE, + description: + "Micro-USB-B receptacle. Carries VBUS (5V) and USB 2.0 Full-Speed D+/D- routed through the on-board USB-to-UART bridge (CP2102N) to U0TXD/U0RXD on the ESP32.", + functions: [ + { name: "micro_usb", direction: "bidirectional", note: "Micro-USB power and USB-UART connector." }, + ] satisfies DevkitFunction[], + }, + }, + { type: "power_domain", params: { domain: "usb_5v" } }, + { + type: "usb_uart_bridge", + params: { + part: "CP2102N", + connects: "USB 2.0 Full-Speed D+/D- to U0TXD/U0RXD (header pins TX/RX, GPIO1/GPIO3)", + purpose: "Power and programming (firmware flashing / serial console).", + }, + }, + { type: "connector", params: { connector_type: "micro_usb" } }, + ], + bridgesTo: ["tx", "rx"], +}; + +/** + * All 38 header pins in header order (J2-1..19, J3-1..19) + Micro-USB. + * + * Audit: the full header map was VERIFIED pin-for-pin against the official + * ESP32-DevKitC V4 user guide pin-layout tables (docs.espressif.com): + * J2 1-19: 3V3, EN, VP, VN, IO34, IO35, IO32, IO33, IO25, IO26, IO27, + * IO14, IO12, GND, IO13, D2, D3, CMD, 5V + * J3 1-19: GND, IO23, IO22, TX, RX, IO21, GND, IO19, IO18, IO5, IO17, + * IO16, IO4, IO0, IO2, IO15, D1, D0, CLK + * Every designator below (including the ~24 mid-header ones the builder + * assigned from resource order) matches the official table. + */ +const pins: InterfaceDef[] = [ + // J2, pins 1-19 + pin3v3, + pinEn, + ...j2Gpio.slice(0, 11), // VP..Pin 12 (J2-3..J2-13) + devkitGnd("gnd", "GND", "J2-14"), + ...j2Gpio.slice(11, 12), // Pin 13 (J2-15) + ...j2FlashPins, // D2, D3, CMD (J2-16..J2-18) + pin5v, + // J3, pins 1-19 + devkitGnd("gnd_j3_1", "GND (J3-1)", "J3-1"), + ...j3Gpio.slice(0, 5), // Pin 23..Pin 21 (J3-2..J3-6) + devkitGnd("gnd_j3_7", "GND (J3-7)", "J3-7"), + ...j3Gpio.slice(5), // Pin 19..Pin 15 (J3-8..J3-16) + ...j3FlashPins, // D1, D0, CLK (J3-17..J3-19) + // Connector + microUsb, +]; + +// --------------------------------------------------------------------------- +// Composed interfaces — one-to-one with the ProtoPart `interfaces` list +// (same ids, names, protocol types/roles and max_instances). Pool-style +// interfaces (one function claimed per instance) use a single count-1 slot +// with max_instances equal to the pool size, exactly as the JSON counts them. +// --------------------------------------------------------------------------- + +function composed(config: { + id: string; + name: string; + protocolType: string; + roles: string[]; + slots: SlotDef[]; + profiles?: InterfaceDef["profiles"]; + parameters?: Parameter[]; + maxInstances?: number; + defaultActive?: boolean; + traits?: TraitDef[]; + domain?: InterfaceDef["domain"]; +}): InterfaceDef { + return { + id: config.id, + name: config.name, + domain: config.domain ?? "electrical", + exposed: true, + default_active: config.defaultActive ?? false, + protocols: [{ type: config.protocolType, roles: config.roles }], + slots: config.slots, + ...(config.profiles ? { profiles: config.profiles } : {}), + ...(config.parameters ? { parameters: config.parameters } : {}), + ...(config.maxInstances !== undefined ? { max_instances: config.maxInstances } : {}), + ...(config.traits ? { traits: config.traits } : {}), + }; +} + +// --- Power rails ------------------------------------------------------------ + +const power3v3 = composed({ + id: "power_3v3", + name: "Power 3.3V", + protocolType: "power", + roles: ["peer"], // ProtoPart role: peer (rail is input OR output) + slots: [{ id: "rail", required: true, match: { protocol: "power", capability: "power_3v3" } }], + profiles: [{ id: "power_3v3_rail", label: "3V3 (J2-1)", bindings: { rail: "3v3" } }], + maxInstances: 1, +}); + +const power5v = composed({ + id: "power_5v", + name: "Power 5V", + protocolType: "power", + roles: ["peer"], + slots: [{ id: "rail", required: true, match: { protocol: "power", capability: "power_5v0" } }], + profiles: [{ id: "power_5v_rail", label: "5V (J2-19)", bindings: { rail: "5v" } }], + maxInstances: 1, +}); + +const ground = composed({ + id: "ground", + name: "Ground", + protocolType: "power", + roles: ["peer"], + slots: [{ id: "pin", required: true, match: { protocol: "power", role: "ground", capability: "ground" } }], + maxInstances: 3, // three GND header pins: J2-14, J3-1, J3-7 + traits: [ + { type: "channels", params: { count: 3, mapping: "GND=J2-14, GND (J3-1)=J3-1, GND (J3-7)=J3-7" } }, + ], +}); + +// --- Reset / boot ----------------------------------------------------------- + +const chipEnableReset = composed({ + id: "chip_enable_reset", + name: "Chip Enable / Reset", + protocolType: "digital", + roles: ["input"], + slots: [{ id: "en", required: true, match: { protocol: "digital", role: "input", capability: "chip_pu" } }], + profiles: [{ id: "chip_enable_reset_en", label: "EN (J2-2)", bindings: { en: "en" } }], + maxInstances: 1, + traits: [ + { + type: "protopart_requires", + params: { + functions: ["EN", "PU"], + note: "The ProtoPart source requires EN and PU — both aliases of the same header pin; one physical pin satisfies both.", + }, + }, + ], +}); + +const bootConfiguration = composed({ + id: "boot_configuration", + name: "Boot Configuration", + protocolType: "digital", + roles: ["input"], + slots: [ + { id: "boot", required: true, label: "BOOT (GPIO0)", match: { protocol: "digital", role: "input", capability: "strap_boot" } }, + { id: "log", required: true, label: "LOG (GPIO15 / MTDO)", match: { protocol: "digital", role: "input", capability: "strap_boot_log" } }, + { id: "vdd_flash", required: true, label: "VDD_FLASH (GPIO12 / MTDI)", match: { protocol: "digital", role: "input", capability: "strap_vdd_flash" } }, + ], + profiles: [ + { + id: "boot_configuration_straps", + label: "BOOT=Pin 0, LOG=Pin 15, VDD_FLASH=Pin 12", + bindings: { boot: "pin_0", log: "pin_15", vdd_flash: "pin_12" }, + }, + ], + maxInstances: 3, // ProtoPart max_instances: 3 + traits: [ + { + type: "boot_strapping", + params: { + note: "GPIOs 0, 2, 5, 12, and 15 are strapping pins - pulling them to conflicting levels during reset can prevent boot or change flash voltage.", + source: "ProtoPart board warning #4", + }, + }, + ], +}); + +// --- GPIO pools ------------------------------------------------------------- + +const gpioInputOnly = composed({ + id: "gpio_input_only", + name: "GPIO Input Only", + protocolType: "digital", + roles: ["input"], + slots: [{ id: "pin", required: true, match: { protocol: "digital", role: "input", capability: "gpio_input_only" } }], + maxInstances: 4, + traits: [ + { type: "channels", params: { count: 4, mapping: "VP=GPIO36, VN=GPIO39, Pin 34=GPIO34, Pin 35=GPIO35" } }, + ], +}); + +const gpioInputOutput = composed({ + id: "gpio_input_output", + name: "GPIO Input / Output", + protocolType: "digital", + roles: ["peer"], + slots: [{ id: "pin", required: true, match: { protocol: "digital", capability: "gpio_io" } }], + maxInstances: 22, + traits: [ + { + type: "channels", + params: { + count: 22, + note: "All bidirectional header GPIOs. Excludes the four input-only pins (GPIO34-39 group) and the six flash-reserved pins (D0-D3, CMD, CLK), which the ProtoPart source does not give a GPIO Input / Output function.", + }, + }, + ], +}); + +// --- Analog ----------------------------------------------------------------- + +const adc1 = composed({ + id: "adc1", + name: "ADC1", + protocolType: "analog", + roles: ["input"], + slots: [{ id: "channel", required: true, match: { protocol: "analog", role: "input", capability: "adc1_in" } }], + maxInstances: 6, // six ADC1 channels reach the headers + traits: [ + { + type: "channels", + params: { + count: 6, + mapping: "CH0=VP (GPIO36), CH3=VN (GPIO39), CH4=Pin 32, CH5=Pin 33, CH6=Pin 34, CH7=Pin 35", + }, + }, + ], +}); + +const adc2 = composed({ + id: "adc2", + name: "ADC2", + protocolType: "analog", + roles: ["input"], + slots: [{ id: "channel", required: true, match: { protocol: "analog", role: "input", capability: "adc2_in" } }], + maxInstances: 10, + traits: [ + { + type: "channels", + params: { + count: 10, + mapping: "CH0=Pin 4, CH1=Pin 0, CH2=Pin 2, CH3=Pin 15, CH4=Pin 13, CH5=Pin 12, CH6=Pin 14, CH7=Pin 27, CH8=Pin 25, CH9=Pin 26", + }, + }, + ], +}); + +const dac = composed({ + id: "dac", + name: "DAC", + protocolType: "analog", + roles: ["output"], + slots: [{ id: "channel", required: true, match: { protocol: "analog", role: "output", capability: "analog_out" } }], + maxInstances: 2, + traits: [ + { type: "channels", params: { count: 2, mapping: "DAC_1=Pin 25 (GPIO25), DAC_2=Pin 26 (GPIO26)" } }, + ], +}); + +const hallSensor = composed({ + id: "hall_sensor", + name: "Hall Sensor", + protocolType: "analog", + roles: ["input"], + slots: [ + { id: "svp", required: true, label: "S_VP", match: { protocol: "analog", role: "input", capability: "sensor_vp" } }, + { id: "svn", required: true, label: "S_VN", match: { protocol: "analog", role: "input", capability: "sensor_vn" } }, + ], + profiles: [ + { id: "hall_sensor_vp_vn", label: "S_VP=VP (J2-3), S_VN=VN (J2-4)", bindings: { svp: "vp", svn: "vn" } }, + ], + maxInstances: 2, // ProtoPart max_instances: 2 (one S_VP/S_VN pair exists on the board) +}); + +const voltageDetection = composed({ + id: "voltage_detection", + name: "Voltage Detection", + protocolType: "analog", + roles: ["input"], + slots: [{ id: "channel", required: true, match: { protocol: "analog", role: "input", capability: "vdet" } }], + maxInstances: 2, + traits: [ + { type: "channels", params: { count: 2, mapping: "VDET_1=Pin 34 (GPIO34), VDET_2=Pin 35 (GPIO35)" } }, + ], +}); + +// --- RTC / touch / oscillator ----------------------------------------------- + +const rtcGpio = composed({ + id: "rtc_gpio", + name: "RTC GPIO", + protocolType: "digital", + roles: ["peer"], + slots: [{ id: "channel", required: true, match: { protocol: "digital", capability: "rtc_gpio" } }], + maxInstances: 16, + traits: [ + { + type: "channels", + params: { + count: 16, + mapping: + "RTC_GPIO0=VP, RTC_GPIO3=VN, RTC_GPIO4=Pin 34, RTC_GPIO5=Pin 35, RTC_GPIO6=Pin 25, RTC_GPIO7=Pin 26, RTC_GPIO8=Pin 33, RTC_GPIO9=Pin 32, RTC_GPIO10=Pin 4, RTC_GPIO11=Pin 0, RTC_GPIO12=Pin 2, RTC_GPIO13=Pin 15, RTC_GPIO14=Pin 13, RTC_GPIO15=Pin 12, RTC_GPIO16=Pin 14, RTC_GPIO17=Pin 27", + }, + }, + ], +}); + +const touchSensor = composed({ + id: "touch_sensor", + name: "Touch Sensor", + protocolType: "custom", // ProtoPart protocol type: custom + roles: ["input"], + slots: [{ id: "channel", required: true, match: { capability: "touch" } }], + maxInstances: 10, + traits: [ + { + type: "channels", + params: { + count: 10, + mapping: "T0=Pin 4, T1=Pin 0, T2=Pin 2, T3=Pin 15, T4=Pin 13, T5=Pin 12, T6=Pin 14, T7=Pin 27, T8=Pin 33, T9=Pin 32", + note: "The ProtoPart source enumerates the ten channel functions TOUCH0-TOUCH9 individually; each instance claims one channel.", + }, + }, + ], +}); + +const rtcCrystalOscillator = composed({ + id: "rtc_crystal_oscillator", + name: "RTC Crystal Oscillator", + protocolType: "oscillators", // ProtoPart protocol type + roles: ["peer"], + slots: [ + { id: "xp", required: true, label: "32K_XP", match: { capability: "xtal_32k_p" } }, + { id: "xn", required: true, label: "32K_XN", match: { capability: "xtal_32k_n" } }, + ], + profiles: [ + { id: "rtc_crystal_pins", label: "32K_XP=Pin 32, 32K_XN=Pin 33", bindings: { xp: "pin_32", xn: "pin_33" } }, + ], + maxInstances: 2, // ProtoPart max_instances: 2 (one XP/XN pair exists on the board) +}); + +// --- Serial buses ----------------------------------------------------------- + +// UART is hand-rolled (not the UART builder): the ProtoPart role is "peer", +// which the builder's host/device role set cannot express, and the source +// gives no baud-rate data to carry. +const uartTraits = (aliasNote?: string): TraitDef[] => [ + { + type: "co_requirement", + params: { + with: "micro_usb", + condition: "UART0 profile (TX/RX header pins) with USB connected", + effect: + "TX (GPIO1) and RX (GPIO3) are shared with the on-board USB-to-UART bridge. To use UART0 with an external peer, unplug USB or tri-state the bridge by holding the bridge IC in reset.", + source: "ProtoPart board warning #7", + }, + }, + ...(aliasNote ? [{ type: "alias_note", params: { note: aliasNote } }] : []), +]; + +const uart = composed({ + id: "uart", + name: "UART", + protocolType: "uart", + roles: ["peer"], + slots: [ + { id: "tx", required: true, match: { protocol: "uart", role: "transmitter", capability: "uart_tx" } }, + { id: "rx", required: true, match: { protocol: "uart", role: "receiver", capability: "uart_rx" } }, + ], + profiles: [ + { id: "uart0", label: "UART0: TX (GPIO1), RX (GPIO3) — shared with USB bridge", bindings: { tx: "tx", rx: "rx" } }, + // Second RX/TX pair on the headers (ProtoPart max_instances: 2): the + // pin_16/pin_17 resources are described as GPIO16 / U2RXD and + // GPIO17 / U2TXD and carry uart_rx / uart_tx functions. + // Audit: VERIFIED against the ESP32-WROOM-32E datasheet pin definitions — + // U2RXD is a listed function of GPIO16 (module pin 27) and U2TXD of + // GPIO17 (module pin 28), so the UART2 profile is datasheet-honest. + { id: "uart2", label: "UART2: Pin 17 (GPIO17/U2TXD), Pin 16 (GPIO16/U2RXD)", bindings: { tx: "pin_17", rx: "pin_16" } }, + ], + maxInstances: 2, + traits: uartTraits(), +}); + +const arduinoSerial = composed({ + id: "arduino_serial", + name: "Arduino Serial", + protocolType: "uart", + roles: ["peer"], + slots: [ + { id: "tx", required: true, match: { protocol: "uart", role: "transmitter", capability: "uart_tx" } }, + { id: "rx", required: true, match: { protocol: "uart", role: "receiver", capability: "uart_rx" } }, + ], + profiles: [ + { id: "arduino_serial_default", label: "Serial: TX (GPIO1), RX (GPIO3)", bindings: { tx: "tx", rx: "rx" } }, + ], + maxInstances: 2, // ProtoPart max_instances: 2 + traits: uartTraits( + "Arduino-core alias of the primary serial port (Serial), carried as its own interface because the ProtoPart source lists it separately.", + ), +}); + +// SPI builder fits: ProtoPart role "master" is a builder role. No clock +// frequency is given in the source, so none is emitted. +const hspi = SPI({ + id: "hspi", + name: "High-Speed SPI", + roles: ["master"], + maxInstances: 4, // ProtoPart max_instances: 4 + profiles: [ + { + id: "hspi_default", + label: "HSPI: SCK=Pin 14, MISO=Pin 12, MOSI=Pin 13, SS=Pin 15", + mosi: "pin_13", miso: "pin_12", sck: "pin_14", ss: "pin_15", + }, + ], +}); + +const vspi = SPI({ + id: "vspi", + name: "Virtual SPI", + roles: ["master"], + maxInstances: 4, // ProtoPart max_instances: 4 + profiles: [ + { + id: "vspi_default", + label: "VSPI: SCK=Pin 18, MISO=Pin 19, MOSI=Pin 23, SS=Pin 5", + mosi: "pin_23", miso: "pin_19", sck: "pin_18", ss: "pin_5", + }, + ], +}); + +// I2C is hand-rolled (not the I2C builder): the builder always emits a +// default 400 kHz clock parameter, and the ProtoPart source gives no bus +// clock to carry — hand-rolling avoids fabricating one. +const i2cWire = composed({ + id: "i2c_wire", + name: "I2C / Wire", + protocolType: "i2c", + roles: ["master"], + slots: [ + { id: "sda", required: true, match: { protocol: "i2c", role: "data", capability: "i2c_sda" } }, + { id: "scl", required: true, match: { protocol: "i2c", role: "clock", capability: "i2c_scl" } }, + ], + profiles: [ + { id: "i2c_wire_default", label: "Wire: SDA=Pin 21 (GPIO21), SCL=Pin 22 (GPIO22)", bindings: { sda: "pin_21", scl: "pin_22" } }, + ], + maxInstances: 2, // ProtoPart max_instances: 2 +}); + +const externalFlashMemorySpi = composed({ + id: "external_flash_memory_spi", + name: "External Flash Memory SPI", + protocolType: "spi", + roles: ["master"], + slots: [ + { id: "clk", required: true, match: { capability: "spi_flash_sck" } }, + { id: "cmd", required: true, match: { capability: "spi_flash_cmd" } }, + { id: "d0", required: true, match: { capability: "spi_flash_d0" } }, + { id: "d1", required: true, match: { capability: "spi_flash_d1" } }, + { id: "d2", required: true, match: { capability: "spi_flash_d2" } }, + { id: "d3", required: true, match: { capability: "spi_flash_d3" } }, + ], + profiles: [ + { + id: "flash_bus_pins", + label: "CLK (J3-19), CMD (J2-18), D0 (J3-18), D1 (J3-17), D2 (J2-16), D3 (J2-17)", + bindings: { clk: "clk", cmd: "cmd", d0: "d0", d1: "d1", d2: "d2", d3: "d3" }, + }, + ], + maxInstances: 6, // ProtoPart max_instances: 6 + traits: [ + { + type: "usage_restriction", + params: { + restriction: + "This bus is already committed to the SPI flash inside the ESP32-WROOM-32E module — the header pins expose it but it must not be repurposed as general I/O.", + source: "ProtoPart board warning #2", + }, + }, + ], +}); + +const jtag = composed({ + id: "jtag", + name: "JTAG", + protocolType: "jtag", + roles: ["target"], + slots: [ + { id: "tdi", required: true, match: { capability: "jtag_tdi" } }, + { id: "tdo", required: true, match: { capability: "jtag_tdo" } }, + { id: "tck", required: true, match: { capability: "jtag_tck" } }, + { id: "tms", required: true, match: { capability: "jtag_tms" } }, + ], + profiles: [ + { + id: "jtag_mt_pins", + label: "MTDI=Pin 12, MTDO=Pin 15, MTCK=Pin 13, MTMS=Pin 14", + bindings: { tdi: "pin_12", tdo: "pin_15", tck: "pin_13", tms: "pin_14" }, + }, + ], + maxInstances: 4, // ProtoPart max_instances: 4 +}); + +const sdio = composed({ + id: "sdio", + name: "SDIO", + protocolType: "sdio", + roles: ["peer"], + slots: [{ id: "sdio", required: true, match: { capability: "sdio" } }], + profiles: [{ id: "sdio_gpio5", label: "SDIO strap / alternate function on Pin 5 (GPIO5)", bindings: { sdio: "pin_5" } }], + maxInstances: 1, +}); + +// --- Pad-capability pools (carried verbatim from the ProtoPart source) ------ + +const openDrainCapability = composed({ + id: "open_drain_capability", + name: "Open Drain Capability", + protocolType: "digital", + roles: ["peer"], + slots: [{ id: "pad", required: true, match: { protocol: "digital", capability: "open_drain" } }], + // Mirrors the ProtoPart pool: all 32 header GPIO pads carry an "Open Drain + // Capability" function in the source. Datasheet caveat: GPIO34-39 (VP, VN, + // Pin 34, Pin 35) are input-only with no output driver, so open-drain + // OUTPUT is not physically available on those four pads. + maxInstances: 32, +}); + +const inputEnableCapability = composed({ + id: "input_enable_capability", + name: "Input Enable Capability", + protocolType: "digital", + roles: ["peer"], + slots: [{ id: "pad", required: true, match: { protocol: "digital", capability: "pad_input_gate" } }], + maxInstances: 32, + traits: [ + { + type: "protopart_requires", + params: { + functions: ["IE", "ID"], + note: "The ProtoPart source lists input-enable (IE) on bidirectional pads and input-disable (ID) on input-only pads; both are carried under the shared pad_input_gate capability tag.", + }, + }, + ], +}); + +const weakPullUpCapability = composed({ + id: "weak_pull_up_capability", + name: "Weak Pull-Up Capability", + protocolType: "digital", + roles: ["peer"], + slots: [{ id: "pad", required: true, match: { protocol: "digital", capability: "weak_pullup" } }], + maxInstances: 13, // pads the ProtoPart source marks WPU +}); + +const weakPullDownCapability = composed({ + id: "weak_pull_down_capability", + name: "Weak Pull-Down Capability", + protocolType: "digital", + roles: ["peer"], + slots: [{ id: "pad", required: true, match: { protocol: "digital", capability: "weak_pulldown" } }], + maxInstances: 3, // Pin 13, Pin 2, Pin 4 +}); + +// --------------------------------------------------------------------------- +// Network domain — radios inside the ESP32-WROOM-32E module +// --------------------------------------------------------------------------- + +const wifiRadio: InterfaceDef = { + id: "wifi_radio", + name: "2.4 GHz Wi-Fi radio (PCB trace antenna)", + domain: "network", + exposed: true, + default_active: true, + protocols: [{ type: "rf", roles: ["transceiver"] }], + capabilities: ["wifi_rf"], + traits: [ + { + type: "antenna", + params: { + description: + "Integrated 802.11 b/g/n radio inside the ESP32-WROOM-32E module. PCB trace antenna on the module.", + }, + }, + ], +}; + +const bluetoothRadio: InterfaceDef = { + id: "bluetooth_radio", + name: "Bluetooth 4.2 dual-mode radio", + domain: "network", + exposed: true, + default_active: true, + protocols: [{ type: "rf", roles: ["transceiver"] }], + capabilities: ["bluetooth_rf"], + traits: [ + { + type: "antenna", + params: { + description: + "Integrated Bluetooth 4.2 BR/EDR + BLE radio inside the ESP32-WROOM-32E module. Shares the PCB trace antenna with Wi-Fi.", + }, + }, + ], +}; + +const wifiClient = composed({ + id: "wifi_client", + name: "Wi-Fi", + domain: "network", + protocolType: "wifi", + roles: ["client", "access_point", "peer"], // ProtoPart roles, verbatim + slots: [{ id: "rf", required: true, match: { capability: "wifi_rf" } }], + profiles: [{ id: "wifi_client_radio", label: "On-module Wi-Fi radio", bindings: { rf: "wifi_radio" } }], + traits: [ + { + type: "radio_characteristics", + params: { + description: "2.4 GHz 802.11 b/g/n. Supports station, soft-AP, and Wi-Fi Direct / ESP-NOW peer roles.", + }, + }, + ], +}); + +const bluetoothPeer = composed({ + id: "bluetooth_peer", + name: "Bluetooth (Classic + BLE)", + domain: "network", + protocolType: "bluetooth", + roles: ["peer"], + slots: [{ id: "rf", required: true, match: { capability: "bluetooth_rf" } }], + profiles: [{ id: "bluetooth_peer_radio", label: "On-module Bluetooth radio", bindings: { rf: "bluetooth_radio" } }], + traits: [ + { + type: "radio_characteristics", + params: { + description: "Bluetooth 4.2 dual-mode: Classic BR/EDR plus Bluetooth Low Energy.", + }, + }, + ], +}); + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const ESP32_DEVKITC_V4: ModuleDef = defineModule({ + id: "esp32-devkitc-v4", + name: "Espressif ESP32-DevKitC V4", + version: "1.0.0", + manufacturer: "Espressif Systems", + part_number: "ESP32-DevKitC-V4", + description: + "Espressif's reference ESP32 development board. Hosts an ESP32-WROOM-32E module (Xtensa LX6 dual-core 240 MHz, 4 MB flash, 2.4 GHz Wi-Fi + Bluetooth 4.2 dual-mode) on a 54.4 x 27.9 mm breakout PCB. Exposes 38 pins on two 19-pin 2.54 mm headers (J2/J3), a Micro-USB port wired to a USB-to-UART bridge (CP2102N) for power and programming, and EN (reset) + BOOT (download) push-buttons. Breadboard-friendly footprint.", + tags: ["esp32", "esp32-wroom-32e", "dev-board", "wifi", "bluetooth", "ble", "micro-usb", "xtensa", "dual-core"], + categories: ["microcontroller.development_board", "connectivity.wireless"], + + interfaces: [ + // All 38 header pins in header order (J2-1..19, J3-1..19) + Micro-USB. + ...pins, + + // Power rails + power3v3, + power5v, + ground, + + // Reset / boot + chipEnableReset, + bootConfiguration, + + // GPIO pools + gpioInputOnly, + gpioInputOutput, + + // Analog + adc1, + adc2, + dac, + hallSensor, + voltageDetection, + + // RTC / touch / oscillator + rtcGpio, + touchSensor, + rtcCrystalOscillator, + + // Serial buses + uart, + arduinoSerial, + ...hspi, + ...vspi, + i2cWire, + externalFlashMemorySpi, + jtag, + sdio, + + // Pad-capability pools + openDrainCapability, + inputEnableCapability, + weakPullUpCapability, + weakPullDownCapability, + + // Network (radios inside the WROOM-32E module) + wifiRadio, + bluetoothRadio, + wifiClient, + bluetoothPeer, + ], + + interfaceGroups: [ + { + id: "j2_header", + label: "J2 Header (19 pins, 2.54 mm)", + members: [ + "3v3", "en", "vp", "vn", "pin_34", "pin_35", "pin_32", "pin_33", + "pin_25", "pin_26", "pin_27", "pin_14", "pin_12", "gnd", "pin_13", + "d2", "d3", "cmd", "5v", + ], + policy: "all_of", + }, + { + id: "j3_header", + label: "J3 Header (19 pins, 2.54 mm)", + members: [ + "gnd_j3_1", "pin_23", "pin_22", "tx", "rx", "pin_21", "gnd_j3_7", + "pin_19", "pin_18", "pin_5", "pin_17", "pin_16", "pin_4", "pin_0", + "pin_2", "pin_15", "d1", "d0", "clk", + ], + policy: "all_of", + }, + { + id: "flash_reserved_pins", + label: "Flash-Reserved Pins (GPIO6-11 — not general I/O)", + members: ["d2", "d3", "cmd", "clk", "d0", "d1"], + policy: "all_of", + }, + { + id: "strapping_pins", + label: "Strapping Pins (GPIO0/2/5/12/15)", + members: ["pin_0", "pin_2", "pin_5", "pin_12", "pin_15"], + policy: "all_of", + }, + { + // Board warning #5: power via Micro-USB OR the 5V header pin OR the + // 3V3 header pin — never two of these simultaneously. + id: "power_source", + label: "Power Source (exactly one)", + members: ["micro_usb", "5v", "3v3"], + policy: "one_of", + }, + ], + + requirements: [ + { + type: "power", + description: + "Power via exactly one of: Micro-USB VBUS (5 V), the 5V header pin (4.75-5.25 V, up to 500 mA from the USB host or external supply), or a regulated 3.3 V supply on the 3V3 header pin (3.0-3.6 V, bypassing the on-board AMS1117-3.3 LDO). Never power from two of these simultaneously, or the board / supply can be damaged.", + voltage_V: [3, 5.25], // ProtoPart supply_voltage_V + current_mA: 500, + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + { id: "usb_5v", name: "5V input rail", nominal_voltage_V: 5, voltage_range_V: USB_5V_RANGE, max_current_mA: USB_5V_MAX_MA }, + { id: "io_3v3", name: "3.3V logic rail", nominal_voltage_V: 3.3, voltage_range_V: IO_3V3_RANGE, max_current_mA: IO_3V3_MAX_MA, regulation_type: "regulated" }, + { id: "gnd", name: "Ground", nominal_voltage_V: 0, voltage_range_V: [0, 0], max_current_mA: 1000 }, + ], + metadata: { + pin_count: 38, + supply_voltage_V: [3, 5.25], + supports_usb: true, + package_type: "PCB module / 2x 19-pin 2.54mm headers", + // PowerDomainDef has no description field — the ProtoPart power + // domain descriptions are preserved here verbatim. + power_domain_notes: { + usb_5v: "5V rail sourced from Micro-USB VBUS or the 5V header pin. Current limited by USB host or external supply.", + io_3v3: "On-board AMS1117-3.3 LDO output. Supplies the ESP32-WROOM-32E module and the 3V3 header pin.", + gnd: "Common board ground.", + }, + }, + }, + { + domain: "network", + metadata: { + network_protocols: ["wifi", "bluetooth"], + wireless_standards: ["802.11 b/g/n (2.4 GHz)", "Bluetooth 4.2 BR/EDR", "Bluetooth Low Energy 4.2"], + frequency_bands_ghz: [2.4], + }, + }, + { + domain: "thermal", + operating_temperature_C: [-40, 85], + }, + { + domain: "mechanical", + dimensions_mm: { length: 54.4, width: 27.9, height: 13 }, + }, + ], + + traits: [ + { + type: "hosted_module", + params: { + module: "ESP32-WROOM-32E", + note: "The board carries an ESP32-WROOM-32E module (ESP32 chip + 4 MB SPI flash + PCB trace antenna); module internals are not decomposed here — this definition models only what the board exposes.", + }, + }, + { + type: "usb_uart_bridge", + params: { + part: "CP2102N", + connects: "Micro-USB D+/D- to U0TXD/U0RXD (header pins TX/RX, GPIO1/GPIO3)", + purpose: "Power and programming.", + }, + }, + { + type: "voltage_regulator", + params: { + part: "AMS1117-3.3", + output: "io_3v3 rail (3.3 V nominal, 600 mA max)", + source: "ProtoPart power domain io_3v3", + }, + }, + { + type: "push_buttons", + params: { + buttons: [ + { name: "EN", function: "Reset — pulls EN/CHIP_PU (J2-2) low." }, + { name: "BOOT", function: "Download mode — pulls GPIO0 (J3-14) low; hold at reset to enter firmware download mode." }, + ], + }, + }, + + // ProtoPart board warnings, verbatim. + { + type: "board_warning", + params: { + topic: "logic_level", + warning: "All GPIO signals are 3.3V logic. ESP32 GPIOs are NOT 5V-tolerant - driving them with 5V can damage the module.", + }, + }, + { + type: "board_warning", + params: { + topic: "flash_reserved_pins", + warning: "GPIOs 6-11 (header pins D0, D1, D2, D3, CMD, CLK) are wired to the on-package SPI flash and MUST NOT be used as general I/O on the WROOM-32E variant.", + }, + }, + { + type: "board_warning", + params: { + topic: "psram_variants", + warning: "GPIOs 16 and 17 are usable on the default ESP32-WROOM-32E module (no PSRAM). They are NOT usable on WROVER-E / WROVER-IE variants or on -R2 (ESP32-D0WDR2-V3) sub-variants of WROOM-32E, where the in-package PSRAM consumes both pins.", + }, + }, + { + type: "board_warning", + params: { + topic: "strapping_pins", + warning: "GPIOs 0, 2, 5, 12, and 15 are strapping pins - pulling them to conflicting levels during reset can prevent boot or change flash voltage.", + }, + }, + { + type: "board_warning", + params: { + topic: "power_input_exclusivity", + warning: "Power the board via Micro-USB OR the 5V header pin OR the 3V3 header pin - never two of these simultaneously, or the board / supply can be damaged.", + }, + }, + { + type: "board_warning", + params: { + topic: "gpio0_c15_errata", + warning: "Earlier ESP32-DevKitC V4 boards may have a 0402 cap (C15) near GPIO0 that causes spurious boot-into-download or distorts a clock signal output on GPIO0. Remove C15 if you hit either issue.", + }, + }, + { + type: "board_warning", + params: { + topic: "usb_uart_sharing", + warning: "TX (GPIO1) and RX (GPIO3) are shared with the on-board USB-to-UART bridge. To use UART0 with an external peer, unplug USB or tri-state the bridge by holding the bridge IC in reset.", + }, + }, + + // ProtoPart fields with no OpenUHD home, preserved verbatim. + { type: "preview_artifact", params: { artifactId: "art_thumbnail" } }, + { + type: "purchase_info", + params: { + vendors: [ + { + vendor: "Amazon", + vendorPartId: "B09MQJWQN2", + title: "ESP32-DevKitC-32E Development Board (Espressif)", + link: "https://www.amazon.com/dp/B09MQJWQN2?tag=protoboard01-20", + isAffiliate: true, + currentPriceUSD: "11.00", + currency: "USD", + availabilityStatus: "in_stock", + productStatus: "active", + priceTimestamp: "2026-05-16T21:20:00Z", + region: "US", + }, + { + vendor: "DigiKey", + vendorPartId: "1965-ESP32-DEVKITC-32E-ND", + title: "ESP32-DEVKITC-32E Espressif ESP32-WROOM-32E DevKitC V4", + link: "https://www.digikey.com/en/products/detail/espressif-systems/ESP32-DEVKITC-32E/12091810", + isAffiliate: false, + currentPriceUSD: "10.00", + currency: "USD", + availabilityStatus: "in_stock", + productStatus: "active", + stockQuantityAvailable: 393, + priceTimestamp: "2026-05-16T21:20:00Z", + region: "US", + }, + { + vendor: "Espressif", + title: "ESP32-DevKitC overview (manufacturer product page)", + link: "https://www.espressif.com/en/products/devkits/esp32-devkitc/overview", + isAffiliate: false, + availabilityStatus: "unknown", + priceTimestamp: "2026-05-16T21:20:00Z", + notes: "Manufacturer product page; not a direct-purchase storefront for retail customers.", + }, + ], + }, + }, + ], + + artifacts: [ + { + id: "art_user_guide", + name: "ESP32-DevKitC V4 user guide (Espressif)", + type: "documentation", // ProtoPart type "link" + url: "https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32/esp32-devkitc/user_guide.html", + }, + { + id: "art_schematic", + name: "ESP32-DevKitC V4 schematic (PDF)", + type: "datasheet", + url: "https://dl.espressif.com/dl/schematics/esp32_devkitc_v4_sch.pdf", + }, + { + id: "art_pcb_layout", + name: "ESP32-DevKitC V4 PCB layout (PDF)", + type: "datasheet", + url: "https://dl.espressif.com/dl/schematics/esp32_devkitc_v4_pcb_layout.pdf", + }, + { + id: "art_dimensions", + name: "ESP32-DevKitC V4 dimensions (PDF)", + type: "datasheet", + url: "https://dl.espressif.com/dl/schematics/esp32_devkitc_v4_dimensions.pdf", + }, + { + id: "art_module_datasheet", + name: "ESP32-WROOM-32E datasheet (PDF)", + type: "datasheet", + url: "https://www.espressif.com/sites/default/files/documentation/esp32-wroom-32e_esp32-wroom-32ue_datasheet_en.pdf", + }, + { + id: "art_product_image", + name: "ESP32-DEVKITC-32E product photo", + type: "custom", // ProtoPart type "image" + filePath: "./ProtoPart/protoparts/esp32-devkitc-v4/artifacts/images/ESP32-DEVKITC-32E.jpg", + mimeType: "image/jpeg", + tags: ["image", "product-photo"], + }, + { + id: "art_product_page", + name: "Espressif product page", + type: "documentation", // ProtoPart type "link" + url: "https://www.espressif.com/en/products/devkits/esp32-devkitc/overview", + }, + { + id: "art_thumbnail", + name: "Espressif ESP32-DevKitC V4 thumbnail", + type: "custom", // ProtoPart type "image" + filePath: "./ProtoPart/protoparts/esp32-devkitc-v4/thumbnail.png", + mimeType: "image/png", + tags: ["image", "thumbnail"], + }, + ], + + // The ProtoPart source has node_geometry: null — no geometry is carried + // over rather than fabricating one. +}); diff --git a/library/parts/esp32-devkitc-v4/artifacts/thumbnail.png b/library/parts/esp32-devkitc-v4/artifacts/thumbnail.png new file mode 100644 index 0000000..5326211 Binary files /dev/null and b/library/parts/esp32-devkitc-v4/artifacts/thumbnail.png differ diff --git a/library/parts/frc-pneumatic-piston.ts b/library/parts/frc-pneumatic-piston.ts new file mode 100644 index 0000000..2d15a99 --- /dev/null +++ b/library/parts/frc-pneumatic-piston.ts @@ -0,0 +1,289 @@ +/** + * FRC Pneumatic Piston (generic double-acting cylinder) — source-honest part + * definition. + * + * Primary source: ProtoPart definition `frc-pneumatic-piston` + * (ProtoPart/protoparts/frc-pneumatic-piston/definition.json, + * schema 1.4.0, part version 0.1.0) + * - domains[pneumatic].resources Port A (Extend) / Port B (Retract): + * quick-connect fittings, 8.3 bar working = max pressure, one + * fluid-class sink function each (AIR_IN_A / AIR_IN_B) + * - domains[pneumatic].interfaces Air Inlet A (Extend) / Air Inlet B + * (Retract): pneumatic sink, one function each, max_instances 1 + * - domains[pneumatic].metadata working_medium: compressed_air + * Secondary source: WPILib Control System Hardware — Pneumatics + * (the definition's datasheet_url; generic FRC pneumatics reference). + * + * Architecture notes honoured by this file: + * - Port-honest like a plumbing diagram: both physical air ports are leaf + * interfaces carrying the source definition's resource ids and names; + * each verbatim function record (name, direction, signal_class) lives in + * a `piston_port_functions` trait, mirroring the pin-function convention. + * - The two ProtoPart pneumatic interfaces map to composed interfaces with + * one required slot each and a fixed default profile — a cylinder port + * has exactly one plumbing combination, so the combination space is 1. + * - Double-acting semantics: extend/retract roles are `actuation_role` + * traits; both inlets form an `all_of` interface group because a + * double-acting cylinder is actuated by pressurising one port while the + * other vents. + * - No fabrication: the source definition is generic. Bore, stroke, rod + * thread, port thread spec, flow rating, and mounting interfaces + * (rod end / body) are NOT specified there and are deliberately absent + * here — see the `generic_part` trait. + */ + +import type { + InterfaceDef, + ModuleDef, + TraitDef, +} from "../../src/types/index.js"; +import { defineModule } from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Pneumatic constants — definition.json domains[pneumatic].resources +// --------------------------------------------------------------------------- + +/** + * Both ports: working_pressure_bar, kept verbatim from the source definition + * (which rates working = max = 8.3 bar). Audit note: 8.3 bar ≈ 120 psi is the + * FRC STORED-pressure ceiling (compressor cutoff, FRC R806 / WPILib + * pneumatics docs) — read it as a pressure RATING. Actual FRC working + * pressure at a cylinder is capped at 60 psi (~4.1 bar) by the primary + * regulator (FRC R807); the source's "working" label overstates the FRC + * operating pressure. + */ +const WORKING_PRESSURE_BAR = 8.3; +/** Both ports: max_pressure_bar — the source rates working = max. */ +const MAX_PRESSURE_BAR = 8.3; + +const SOURCE = "ProtoPart frc-pneumatic-piston definition.json (schema 1.4.0)"; + +// --------------------------------------------------------------------------- +// Physical air ports — domains[pneumatic].resources, port-honest +// --------------------------------------------------------------------------- + +interface PistonPortSpec { + /** ProtoPart resource id — kept verbatim as the leaf interface id. */ + id: string; + /** ProtoPart resource name — used as the displayed interface name. */ + name: string; + /** Verbatim function name from the resource's function list. */ + functionName: string; + /** Capability tag for slot matching by the composed air-inlet interface. */ + capability: string; +} + +/** Build one plumbing-honest air port from its source resource record. */ +function pistonPort(spec: PistonPortSpec): InterfaceDef { + return { + id: spec.id, + name: spec.name, + domain: "pneumatic", + exposed: true, + default_active: true, + // Resource function direction "sink": the port consumes supplied air. + protocols: [{ type: "pneumatic", roles: ["sink"] }], + capabilities: [spec.capability, "quick_connect"], + parameters: [ + { id: "working_pressure", name: "Working pressure", unit: "bar", value: WORKING_PRESSURE_BAR }, + { id: "max_pressure", name: "Max pressure", unit: "bar", value: MAX_PRESSURE_BAR }, + ], + traits: [ + { + type: "piston_port_functions", + params: { + source: SOURCE, + functions: [{ name: spec.functionName, direction: "sink", signal_class: "fluid" }], + }, + }, + { + type: "connector", + params: { connector_type: "quick_connect", source: SOURCE }, + }, + ], + }; +} + +const portA = pistonPort({ + id: "port-a-res", + name: "Port A (Extend)", + functionName: "AIR_IN_A", + capability: "air_in_a", +}); + +const portB = pistonPort({ + id: "port-b-res", + name: "Port B (Retract)", + functionName: "AIR_IN_B", + capability: "air_in_b", +}); + +// --------------------------------------------------------------------------- +// Air inlets — domains[pneumatic].interfaces +// --------------------------------------------------------------------------- + +interface AirInletSpec { + /** ProtoPart interface id — kept verbatim. */ + id: string; + name: string; + /** ProtoPart interface description (verbatim actuation semantics). */ + description: string; + action: "extend" | "retract"; + portId: string; + portCapability: string; +} + +/** Build one composed air inlet from its source interface record. */ +function airInlet(spec: AirInletSpec): InterfaceDef { + const traits: TraitDef[] = [ + { + type: "actuation_role", + params: { action: spec.action, description: spec.description, source: SOURCE }, + }, + ]; + return { + id: spec.id, + name: spec.name, + domain: "pneumatic", + exposed: true, + default_active: true, + protocols: [{ type: "pneumatic", roles: ["sink"] }], + slots: [ + { + id: "supply", + required: true, + match: { protocol: "pneumatic", role: "sink", capability: spec.portCapability }, + }, + ], + profiles: [ + { + id: `${spec.id.replace(/-/g, "_")}_port`, + label: spec.name, + default_active: true, + bindings: { supply: spec.portId }, + }, + ], + max_instances: 1, // definition.json: max_instances 1 per inlet + traits, + }; +} + +const airInA = airInlet({ + id: "air-in-a", + name: "Air Inlet A (Extend)", + description: "Air supply to extend piston", + action: "extend", + portId: "port-a-res", + portCapability: "air_in_a", +}); + +const airInB = airInlet({ + id: "air-in-b", + name: "Air Inlet B (Retract)", + description: "Air supply to retract piston", + action: "retract", + portId: "port-b-res", + portCapability: "air_in_b", +}); + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const FRC_PNEUMATIC_PISTON: ModuleDef = defineModule({ + id: "frc-pneumatic-piston", + name: "Pneumatic Piston", + version: "0.1.0", + manufacturer: "Various (Bimba, SMC)", + part_number: "Pneumatic Cylinder", + description: + "Generic double-acting pneumatic piston/cylinder for FRC. Provides linear motion when air pressure is applied.", + tags: ["FRC", "pneumatic", "piston", "cylinder", "actuator"], + categories: ["actuator.linear_actuator", "robotics.frc"], + + interfaces: [ + // Physical air ports — plumbing-honest. + portA, + portB, + + // Composed air inlets (the ProtoPart pneumatic interfaces). + airInA, + airInB, + ], + + interfaceGroups: [ + { + // "Double-acting" (module description): pressurising one inlet while + // the other vents produces motion — both must be plumbed. + id: "double_acting_inlets", + label: "Double-Acting Air Inlets (extend + retract)", + members: ["air-in-a", "air-in-b"], + policy: "all_of", + }, + ], + + requirements: [ + { + type: "interface", + description: + "Double-acting: each port needs a compressed-air supply path (working medium: compressed_air) rated to the 8.3 bar max port pressure. In FRC this is a solenoid valve on the working-pressure circuit, downstream of the primary regulator (working pressure capped at 60 psi / ~4.1 bar per FRC R807; 8.3 bar ≈ 120 psi is the stored-pressure ceiling, FRC R806).", + interface_protocol: "pneumatic", + }, + ], + + domains: [ + { + domain: "pneumatic", + metadata: { + working_medium: "compressed_air", // domains[pneumatic].metadata, verbatim + working_pressure_bar: WORKING_PRESSURE_BAR, + max_pressure_bar: MAX_PRESSURE_BAR, + source: SOURCE, + }, + }, + ], + + traits: [ + { + // Honest-gap record: what the generic source definition does NOT specify. + type: "generic_part", + params: { + note: "Generic catalogue placeholder (part_number 'Pneumatic Cylinder', manufacturer 'Various'). Bore, stroke, rod thread, port thread spec, flow rating, and mechanical mounting interfaces (rod end / body) are not specified by the source definition and are intentionally not modelled.", + source: SOURCE, + }, + }, + { + type: "protopart_provenance", + params: { protopart_id: "frc-pneumatic-piston", schema_version: "1.4.0", part_type: "actuator" }, + }, + { + // definition.json previewArtifactId — no ModuleDef field exists for it. + type: "preview_artifact", + params: { artifactId: "art_thumbnail" }, + }, + ], + + artifacts: [ + { + // metadata.datasheet_url — a docs page, not a true datasheet. + id: "art_wpilib_pneumatics", + name: "WPILib Control System Hardware — Pneumatics", + type: "documentation", + url: "https://docs.wpilib.org/en/stable/docs/controls-overviews/control-system-hardware.html#pneumatics", + }, + { + id: "art_thumbnail", + name: "Thumbnail", + type: "custom", // source type "image" has no ArtifactType equivalent + filePath: "./ProtoPart/protoparts/frc-pneumatic-piston/thumbnail.png", + mimeType: "image/png", + tags: ["image", "thumbnail"], + }, + ], + + geometry: { + xScale: 1.5, + yScale: 0.5, + outline: { preset: "rounded_rectangle" }, + }, +}); diff --git a/library/parts/frc-pneumatic-piston/artifacts/thumbnail.png b/library/parts/frc-pneumatic-piston/artifacts/thumbnail.png new file mode 100644 index 0000000..93e8240 Binary files /dev/null and b/library/parts/frc-pneumatic-piston/artifacts/thumbnail.png differ diff --git a/library/parts/hcsr04-ultrasonic-sensor.ts b/library/parts/hcsr04-ultrasonic-sensor.ts new file mode 100644 index 0000000..3b22294 --- /dev/null +++ b/library/parts/hcsr04-ultrasonic-sensor.ts @@ -0,0 +1,371 @@ +/** + * HC-SR04 Ultrasonic Sonar Distance Sensor (+ bundled 2 × 10 kΩ echo divider) + * — ProtoPart-honest part definition. + * + * Primary source: ProtoPart `HCSR04-Ultrasonic-Sensor` definition.json + * (schema 1.4.0, part version 0.1.0) — the audited source of truth for + * every electrical value below. Its own datasheet reference: + * HC-SR04 datasheet (SparkFun mirror), + * https://cdn.sparkfun.com/datasheets/Sensors/Proximity/HCSR04.pdf + * + * Architecture notes honoured by this file: + * - Pin-honest like a schematic: the 4 header pins (VCC, Trig, Echo, GND) + * are leaf interfaces with ids "pin_N" in header order and the ProtoPart + * resource name as the displayed name. The ProtoPart JSON carries no + * physical pin designators; the 1-4 numbering follows the part's header + * order (VCC, Trig, Echo, GND). + * - Every pin carries its verbatim ProtoPart function list (name, + * signal_class, description) in an `hcsr04_pin_functions` trait — + * display data is separated from the canonical capability tags the + * matching engine needs (`power_in`, `ground`, `hcsr04_trig`, + * `hcsr04_echo`). + * - Implied harness connections (`implied_passives` trait): this specific + * ProtoPart bundles a 2 × 10 kΩ resistor divider on the Echo pin so the + * 5 V echo pulse is safe for 3.3 V hosts — the divider is part of the + * audited definition, mirroring how the ESP32 reference models external + * RC networks. + * - Source-discrepancy resolution (audited): the ProtoPart metadata + * description says the divider lowers the echo to 2.5 V; the echo + * power-domain and resource descriptions say 3 V. Adjudicated to 2.5 V — + * 2 × 10 kΩ halves 5 V, and the Adafruit product page (PID 3942) states + * the divider converts the 5 V level "to a safe 2.5V". The 3 V wording is + * still carried verbatim in the quoted source strings. + * - No fabricated interfaces: the ProtoPart JSON specifies no trigger pulse + * width, echo pulse-width-to-distance timing, measuring range, resolution, + * or supply/quiescent current. No interfaces or parameters were invented + * for them; datasheet-verified timing facts (Elecfreaks HC-SR04 datasheet, + * SparkFun mirror) are recorded only in the `data_gap` trait note. + */ + +import type { + InterfaceDef, + ModuleDef, + TraitDef, +} from "../../src/types/index.js"; +import { + Ground, + Pin, + PowerIn, + defineModule, + voltageV, +} from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Electrical constants — ProtoPart electrical domain (power_domains) +// --------------------------------------------------------------------------- + +/** Shared citation string for values lifted verbatim from the ProtoPart JSON. */ +const SRC = + "ProtoPart HCSR04-Ultrasonic-Sensor definition.json (schema 1.4.0, v0.1.0)"; + +/** VCC power domain: "Main power supply must not exceed 5.0V." */ +const VCC_NOMINAL_V = 5; +/** Trig power domain: "trigger pin, works with 3V-5V inputs". */ +const TRIG_INPUT_RANGE_V: [number, number] = [3, 5]; +/** + * Echo power domain nominal: post-divider level. Adjudicated to 2.5 V — a + * 2 × 10 kΩ divider halves the 5 V echo pulse (5 V × 10k/(10k+10k) = 2.5 V), + * and the Adafruit product page (PID 3942) states the divider converts "the + * 5V logic level to a safe 2.5V". The ProtoPart echo-domain text said 3 V; + * that figure is preserved verbatim in descriptions but is not used here. + */ +const ECHO_DIVIDED_NOMINAL_V = 2.5; + +// --------------------------------------------------------------------------- +// ProtoPart-honest per-pin function metadata +// --------------------------------------------------------------------------- + +/** Signal classes as they appear in the ProtoPart resource functions. */ +type Hcsr04SignalClass = "power" | "control" | "data" | "ground"; + +interface Hcsr04Function { + /** Verbatim function name from the ProtoPart resource. */ + name: string; + /** Verbatim `signal_class` from the ProtoPart resource. */ + signal_class: Hcsr04SignalClass; + /** + * Verbatim description. The ProtoPart functions carry no per-function + * description; the owning resource's description is used. + */ + description: string; +} + +/** Wrap a pin's verbatim ProtoPart function list as a display trait. */ +function hcsr04PinFunctions(functions: Hcsr04Function[]): TraitDef { + return { + type: "hcsr04_pin_functions", + params: { source: `${SRC}, electrical resources`, functions }, + }; +} + +// --------------------------------------------------------------------------- +// Header pins — 4 leaf interfaces in header order (VCC, Trig, Echo, GND) +// --------------------------------------------------------------------------- + +const vccPin: InterfaceDef = { + ...PowerIn({ id: "pin_1", name: "VCC", pin: 1, voltageV: VCC_NOMINAL_V }), + capabilities: ["power_in"], + traits: [ + hcsr04PinFunctions([ + { name: "VCC", signal_class: "power", description: "Main power supply must not exceed 5.0V." }, + ]), + { type: "power_domain", params: { domain: "vcc" } }, + { + type: "supply_limit", + params: { + max_voltage_V: 5.0, + note: "Main power supply must not exceed 5.0V.", + source: `${SRC}, VCC power domain`, + }, + }, + ], +}; + +const trigPin: InterfaceDef = { + // Input-only from the sensor's side: the host drives Trig, the sensor + // never does — the Pin builder drops the output/bidirectional roles. + ...Pin({ + id: "pin_2", + name: "Trig", + pin: 2, + voltageV: TRIG_INPUT_RANGE_V, + capabilities: { inputOnly: true }, + }), + capabilities: ["digital_io", "hcsr04_trig"], + traits: [ + hcsr04PinFunctions([ + { name: "Trig", signal_class: "control", description: "trigger pin, works with 3V-5V inputs" }, + ]), + { type: "power_domain", params: { domain: "trig" } }, + { + type: "logic_compatibility", + params: { + note: "Works with 3V-5V inputs — the trigger input accepts 3.3 V logic directly, no level shifting required.", + source: `${SRC}, Trig power domain`, + }, + }, + ], +}; + +const echoPin: InterfaceDef = { + id: "pin_3", + name: "Echo", + pin: 3, + domain: "electrical", + exposed: true, + default_active: true, + // Output-only from the sensor's side (hand-rolled: the Pin builder has no + // output-only form) — the host samples the echo pulse on this line. + protocols: [{ type: "digital", roles: ["output"] }], + capabilities: ["digital_io", "hcsr04_echo"], + // Post-divider nominal level as this ProtoPart ships it: 2.5 V (see the + // implied_passives trait — the 2.5 V vs 3 V source discrepancy is resolved + // in favour of 2.5 V per divider arithmetic and the Adafruit product page). + parameters: [voltageV(ECHO_DIVIDED_NOMINAL_V)], + traits: [ + hcsr04PinFunctions([ + { + name: "Echo", + signal_class: "data", + description: + "Echo pin outputs a 5V pulse, however this is lowered to 3V by the 10kOhm resistor divider for ONLY this device", + }, + ]), + { type: "power_domain", params: { domain: "echo" } }, + { + // The bundled divider — part of this audited definition, not a board + // suggestion. Topology is the standard divider arrangement implied by + // "10kOhm resistor divider" with "2 x 10K resistors"; the ProtoPart + // JSON does not draw the network explicitly. + type: "implied_passives", + params: { + purpose: "Echo-pin level shifting for 3.3 V hosts — bundled with this specific part", + components: [ + { kind: "resistor", value: "10 kΩ", connection: "Echo (sensor output) in series to the host signal node" }, + { kind: "resistor", value: "10 kΩ", connection: "host signal node to GND" }, + ], + behavior: + "Echo pin outputs a 5V pulse, however this is lowered to 3V by the 10kOhm resistor divider for ONLY this device", + source: `${SRC}, echo power domain / resource description`, + discrepancy_note: + "RESOLVED: the divided echo level is 2.5 V, not 3 V. A 2 x 10 kOhm divider halves the 5 V pulse (5 V x 10k/(10k+10k) = 2.5 V), and the Adafruit product page for this exact bundle (PID 3942) says the two 10K resistors 'convert the 5V logic level to a safe 2.5V'. The ProtoPart metadata's 2.5 V figure is correct; the echo power-domain/resource '3V' wording (preserved verbatim above) is arithmetically wrong.", + }, + }, + ], +}; + +const gndPin: InterfaceDef = { + ...Ground({ id: "pin_4", name: "GND", pin: 4 }), + traits: [ + hcsr04PinFunctions([ + { name: "GND", signal_class: "ground", description: "Ground pin" }, + ]), + { + type: "net_shareable", + params: { net: "gnd", policy: "single_ground_instance_may_serve_all_members" }, + }, + ], +}; + +/** All 4 header pins, in header order — schematic-honest. */ +const pins: InterfaceDef[] = [vccPin, trigPin, echoPin, gndPin]; + +// --------------------------------------------------------------------------- +// Composed sensor interface — ProtoPart electrical interface +// "ultrasonic_sensor" (requires VCC, GND, Trig, Echo — one of each) +// --------------------------------------------------------------------------- + +const ultrasonicSensor: InterfaceDef = { + id: "ultrasonic_sensor", + name: "Ultrasonic Sensor", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "ultrasonic_sensor", roles: ["sensor"] }], + slots: [ + { id: "vcc", required: true, match: { protocol: "power", role: "input", capability: "power_in" } }, + { id: "gnd", required: true, match: { protocol: "power", role: "ground", capability: "ground" } }, + { id: "trig", required: true, match: { protocol: "digital", role: "input", capability: "hcsr04_trig" } }, + { id: "echo", required: true, match: { protocol: "digital", role: "output", capability: "hcsr04_echo" } }, + ], + profiles: [ + { + id: "ultrasonic_sensor_header", + label: "4-pin header (VCC / Trig / Echo / GND)", + default_active: true, + bindings: { vcc: "pin_1", gnd: "pin_4", trig: "pin_2", echo: "pin_3" }, + }, + ], + max_instances: 1, + traits: [ + { + type: "operating_principle", + params: { + note: "Ultrasonic Sensor, 4-pin, 2-wire with a 10kOhm resistor divider on the echo pin to lower the echo pin voltage to 2.5V", + source: `${SRC}, metadata description (verbatim)`, + }, + }, + { + type: "data_gap", + params: { + note: "The ProtoPart source specifies no trigger pulse width, echo pulse-width timing, measuring range, or update rate — trigger/echo timing semantics remain unmodelled as interfaces/parameters. Datasheet-verified timing facts (Elecfreaks HC-SR04 datasheet, SparkFun mirror, cited below): trigger requires a >= 10 uS TTL high pulse; the module then emits an 8-cycle 40 kHz ultrasonic burst; Echo goes high for a duration proportional to range (uS / 58 = cm, uS / 148 = inch, or range = high-level time x 340 m/s / 2); measuring range 2 cm to 400 cm; recommended measurement cycle over 60 ms to prevent trigger/echo overlap.", + datasheet: "https://cdn.sparkfun.com/datasheets/Sensors/Proximity/HCSR04.pdf", + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const HCSR04_ULTRASONIC_SENSOR: ModuleDef = defineModule({ + id: "hcsr04-ultrasonic-sensor", + name: "HC-SR04 Ultrasonic Sonar Distance Sensor + 2 x 10K resistors", + version: "1.0.0", // audited definition; ProtoPart source version 0.1.0 + manufacturer: "Adafruit", + part_number: "HCSR04", + description: + "Ultrasonic Sensor, 4-pin, 2-wire with a 10kOhm resistor divider on the echo pin to lower the echo pin voltage to 2.5V", + // ProtoPart tag list, deduplicated ("sensor" appeared twice in the source). + tags: ["ultrasonic_sensor", "sensor", "ultrasonic", "sonar", "distance", "adafruit", "breakout"], + categories: ["sensor.distance"], + + interfaces: [ + // All 4 header pins in header order — schematic-honest. + ...pins, + + // The composed sensor function. + ultrasonicSensor, + ], + + interfaceGroups: [ + { + id: "sensor_header", + label: "Sensor Header Pins (all required)", + members: ["pin_1", "pin_2", "pin_3", "pin_4"], + policy: "all_of", + }, + ], + + requirements: [ + { + type: "power", + description: + "Main power supply must not exceed 5.0V (ProtoPart VCC power domain). The source JSON specifies no supply-current figure.", + voltage_V: 5, + }, + ], + + domains: [ + { + domain: "electrical", + // The ProtoPart JSON models Trig and Echo signal levels as + // power_domains (a schema quirk carried over faithfully). Descriptions + // and the isolation/ground fields have no PowerDomainDef home and are + // preserved in metadata below. + power_domains: [ + { id: "vcc", name: "VCC", nominal_voltage_V: VCC_NOMINAL_V }, + { id: "trig", name: "Trig", voltage_range_V: TRIG_INPUT_RANGE_V }, + { id: "echo", name: "Echo", nominal_voltage_V: ECHO_DIVIDED_NOMINAL_V }, + ], + metadata: { + pin_count: 4, + pin_order: ["VCC", "Trig", "Echo", "GND"], + power_domain_descriptions: { + vcc: "Main power supply must not exceed 5.0V.", + trig: "trigger pin, works with 3V-5V inputs", + echo: "Echo pin outputs a 5V pulse, however this is lowered to 3V by the 10kOhm resistor divider for ONLY this device", + }, + // ProtoPart per-domain fields with no PowerDomainDef equivalent: + isolation_type: "non_isolated", + ground_reference: "common", + // ProtoPart Trig domain declared its 3-5 V range in + // `nominal_voltage_V` (as an array); mapped to voltage_range_V above. + trig_nominal_field_note: + "Source JSON expressed the Trig domain's 3-5 V window in nominal_voltage_V; carried as voltage_range_V.", + source: SRC, + }, + }, + ], + + traits: [ + { + // "+ 2 x 10K resistors" from the ProtoPart name: the divider is a + // bundled component of this specific part, not an optional add-on. + type: "bundled_components", + params: { + components: [{ kind: "resistor", value: "10 kΩ", quantity: 2 }], + purpose: + "Echo-pin resistor divider to lower the 5 V echo pulse for 3.3 V hosts (see implied_passives on pin_3).", + scope: "for ONLY this device — the bare HC-SR04 module does not include the divider", + source: `${SRC}, metadata name / echo power domain`, + }, + }, + ], + + artifacts: [ + { + id: "art_datasheet", + name: "HC-SR04 Datasheet (SparkFun mirror)", + type: "datasheet", + url: "https://cdn.sparkfun.com/datasheets/Sensors/Proximity/HCSR04.pdf", + }, + { + id: "art_thumbnail", + name: "HC-SR04 Thumbnail", + type: "custom", + filePath: "./ProtoPart/protoparts/HCSR04-Ultrasonic-Sensor/thumbnail.png", + mimeType: "image/png", + tags: ["image", "thumbnail"], + }, + ], + + // Presentational default (the ProtoPart source carries no node_geometry), + // matching the ESP32 reference convention. + geometry: { + xScale: 1, + yScale: 1, + outline: { preset: "rectangle" }, + }, +}); diff --git a/library/parts/hcsr04-ultrasonic-sensor/artifacts/thumbnail.png b/library/parts/hcsr04-ultrasonic-sensor/artifacts/thumbnail.png new file mode 100644 index 0000000..58cc191 Binary files /dev/null and b/library/parts/hcsr04-ultrasonic-sensor/artifacts/thumbnail.png differ diff --git a/library/parts/icm-42688-p.ts b/library/parts/icm-42688-p.ts new file mode 100644 index 0000000..60042e5 --- /dev/null +++ b/library/parts/icm-42688-p.ts @@ -0,0 +1,1164 @@ +/** + * TDK InvenSense ICM-42688-P — datasheet-honest part definition. + * + * Primary source: ProtoPart audited definition + * (ProtoPart/protoparts/icm-42688-p/definition.json), itself sourced from: + * - ICM-42688-P Datasheet DS-000347 rev 1.6 — pin assignment table (pin + * numbers, names, functions — copied verbatim), host-interface speeds + * (I3C ≤12.5 MHz, I2C ≤1 MHz, SPI ≤24 MHz), supply ranges (1.71–3.6 V), + * Table 3 (Section 3.3.1) low-noise-mode 6-axis current (0.88 mA), logic + * thresholds (V_IH ≥ 0.7·VDDIO, V_IL ≤ 0.3·VDDIO), absolute-max supply + * -0.5 V to +4 V (Table 9). + * - TDK application schematic / Table 11 BOM — decoupling (10 nF at VDDIO, + * 100 nF + 2.2 µF at VDD). + * - AN-000173 — APEX motion functions (pedometer, tap, tilt, + * wake-on-motion, significant motion detection). + * Nothing below is carried over from convention or SDK defaults without + * attribution; values absent from the ProtoPart JSON are omitted, not guessed. + * + * Architecture notes honoured by this file: + * - Pin-honest like a schematic: all 14 physical LGA pads are leaf + * interfaces, in package order, with ids "pin_N" and the datasheet pin + * name as the displayed name (including the five RESV pads). + * - Every pin carries its verbatim ProtoPart/datasheet function list + * (name, direction, signal class) in an `icm42688p_pin_functions` trait — + * display data is separated from the canonical capability tags the + * matching engine needs. + * - Instances vs combinations: the four host buses (I3C / I2C / SPI 4-wire / + * SPI 3-wire) are composed interfaces multiplexed onto the same AP_* pads + * (pins 1, 12, 13, 14) — mode-exclusive, one host protocol per board + * design (`bus_mode_exclusivity` traits + a one_of interface group). + * - Co-requirements (`co_requirement` traits): AP_CS tied to VDDIO in + * I2C/I3C mode; AP_AD0 strap selects slave address 0x68/0x69; INT vs + * INT1 aggregate mapping on pin 4; INT2/FSYNC/CLKIN single-function + * rule on pin 9. + * - Implied harness connections (`implied_passives` traits): supply + * decoupling at pins 5 and 8, I2C pull-ups to VDDIO on AP_SDA/AP_SCL. + * - Usage restrictions (`usage_restriction` traits): pin 7 RESV must be + * tied to GND; pins 2/3/10/11 RESV are no-connect-or-GND. + */ + +import type { + InterfaceDef, + ModuleDef, + ProtocolDef, + SlotDef, + TraitDef, +} from "../../src/types/index.js"; +import type { Parameter } from "../../src/types/parameter.js"; +import { + Ground, + PowerIn, + SPI, + I2C, + defineModule, + clockFreqHz, + voltageRangeV, +} from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Electrical constants — Datasheet DS-000347 rev 1.6 via ProtoPart definition +// --------------------------------------------------------------------------- + +/** VDD core/sensor supply: 1.71–3.6 V, typical 1.8 V. */ +const VDD_RANGE: [number, number] = [1.71, 3.6]; +/** VDDIO I/O supply (host interface, INT1, INT2/FSYNC/CLKIN pins): 1.71–3.6 V. */ +const VDDIO_RANGE: [number, number] = [1.71, 3.6]; +/** Typical supply voltage for both rails. */ +const SUPPLY_NOMINAL_V = 1.8; + +/** Host-interface maximum clock rates (ProtoPart electrical metadata). */ +const I2C_MAX_HZ = 1_000_000; +const I3C_MAX_HZ = 12_500_000; +const SPI_MAX_HZ = 24_000_000; + +// --------------------------------------------------------------------------- +// Datasheet-honest per-pin function metadata +// --------------------------------------------------------------------------- + +interface Icm42688pFunction { + /** Verbatim function name from the ProtoPart definition / datasheet pin table. */ + name: string; + /** + * ProtoPart direction convention: "source" = driven by the ICM-42688-P, + * "sink" = received by it. Absent for pure reserved pads. + */ + direction?: "source" | "sink" | "bidirectional"; + signal_class?: string; + description?: string; +} + +/** Verbatim function-list trait — the honest-display contract per pin. */ +function pinFunctions(functions: Icm42688pFunction[], note?: string): TraitDef { + return { + type: "icm42688p_pin_functions", + params: { + source: + "ICM-42688-P Datasheet DS-000347 rev 1.6, pin assignment table (via ProtoPart definition)", + functions, + ...(note ? { note } : {}), + }, + }; +} + +function powerDomain(domain: "VDD" | "VDDIO"): TraitDef { + return { + type: "power_domain", + params: { + domain, + description: + domain === "VDD" + ? "Analog and digital core supply, 1.71 V to 3.6 V (typical 1.8 V)." + : "Digital I/O supply for the host interface, INT1, and INT2/FSYNC/CLKIN pins. 1.71 V to 3.6 V.", + }, + }; +} + +/** AP_AD0 slave-address strap — shared by pin 1 and the strap interface. */ +const ADDRESS_STRAP_TRAIT: TraitDef = { + type: "address_strapping", + params: { + strap: "AP_AD0", + low: "7-bit I2C/I3C slave address 0x68 (AP_AD0 pulled to GND)", + high: "7-bit I2C/I3C slave address 0x69 (AP_AD0 pulled to VDDIO)", + note: "The host interface supports only 7-bit slave addresses 0x68 and 0x69.", + source: + "ICM-42688-P Datasheet DS-000347 rev 1.6, pin 1 description / ProtoPart compatibility notes", + }, +}; + +/** INT drive options — datasheet INT_CONFIG register, verbatim from ProtoPart. */ +const INT_DRIVE_TRAIT: TraitDef = { + type: "drive_configuration", + params: { + options: ["push-pull", "open-drain"], + polarity: "programmable", + note: "Drive type and polarity programmable via INT_CONFIG register.", + source: "ICM-42688-P Datasheet DS-000347 rev 1.6 (via ProtoPart pin 4 notes)", + }, +}; + +// --------------------------------------------------------------------------- +// Leaf pins — LGA-14, in package pin order (ProtoPart electrical resources) +// --------------------------------------------------------------------------- + +interface IcmPinSpec { + pin: number; + /** Datasheet pin name — used as the displayed interface name. */ + name: string; + domain: "VDD" | "VDDIO"; + protocols: ProtocolDef[]; + /** Canonical capability tags for slot matching. */ + capabilities?: string[]; + functions: Icm42688pFunction[]; + notes?: string; + extraTraits?: TraitDef[]; + /** VDDIO-referenced logic pins carry the supply voltage range; RESV pads do not. */ + withVoltageParam?: boolean; + defaultActive?: boolean; +} + +/** Build one schematic-honest LGA pad from its ProtoPart resource row. */ +function icmPin(spec: IcmPinSpec): InterfaceDef { + return { + id: `pin_${spec.pin}`, + name: spec.name, + pin: spec.pin, + domain: "electrical", + exposed: true, + default_active: spec.defaultActive ?? true, + protocols: spec.protocols, + ...(spec.capabilities ? { capabilities: spec.capabilities } : {}), + ...(spec.withVoltageParam ?? true + ? { parameters: [voltageRangeV(VDDIO_RANGE[0], VDDIO_RANGE[1])] } + : {}), + traits: [ + pinFunctions(spec.functions, spec.notes), + powerDomain(spec.domain), + ...(spec.extraTraits ?? []), + ], + }; +} + +/** Pins 2, 3, 10, 11: reserved, "No Connect or Connect to GND". */ +function resvPin(pinNo: number): InterfaceDef { + return icmPin({ + pin: pinNo, + name: "RESV", + domain: "VDDIO", + protocols: [], + capabilities: ["resv"], + withVoltageParam: false, + defaultActive: false, + functions: [{ name: "RESV", description: "Reserved pin; no connect or connect to GND." }], + notes: "TDK datasheet pinout table: 'No Connect or Connect to GND'.", + extraTraits: [ + { + type: "usage_restriction", + params: { + restriction: "Reserved pin — leave unconnected or tie to GND. No signal function.", + source: "TDK datasheet pinout table: 'No Connect or Connect to GND' (via ProtoPart design rules)", + }, + }, + ], + }); +} + +const pins: InterfaceDef[] = [ + // Pin 1 — AP_SDO / AP_AD0 + icmPin({ + pin: 1, + name: "AP_SDO / AP_AD0", + domain: "VDDIO", + protocols: [{ type: "digital", roles: ["input", "output"] }], + capabilities: ["spi_miso", "i2c_addr_strap"], + functions: [ + { name: "MISO", direction: "source", signal_class: "data", description: "AP_SDO: SPI serial data output in 4-wire mode." }, + { name: "Addr", direction: "sink", signal_class: "data", description: "AP_AD0: I3C/I2C slave address LSB strap." }, + ], + notes: "Pull to GND for 7-bit I2C/I3C slave address 0x68, or to VDDIO for 0x69.", + extraTraits: [ADDRESS_STRAP_TRAIT], + }), + + // Pins 2, 3 — RESV + resvPin(2), + resvPin(3), + + // Pin 4 — INT1 / INT + icmPin({ + pin: 4, + name: "INT1 / INT", + domain: "VDDIO", + protocols: [ + { type: "digital", roles: ["output"] }, + { type: "interrupt", roles: ["output"] }, + ], + capabilities: ["int1", "int_aggregate"], + functions: [ + { name: "INT1", direction: "source", signal_class: "data", description: "Interrupt 1 output." }, + { name: "INT", direction: "source", signal_class: "data", description: "All interrupts mapped to pin 4." }, + ], + notes: "Drive type and polarity programmable via INT_CONFIG register.", + extraTraits: [INT_DRIVE_TRAIT], + }), + + // Pin 5 — VDDIO + { + ...PowerIn({ id: "pin_5", name: "VDDIO", pin: 5, voltageV: VDDIO_RANGE, nominalV: SUPPLY_NOMINAL_V }), + capabilities: ["power_in", "vddio"], + traits: [ + pinFunctions( + [{ name: "VDDIO", direction: "sink", signal_class: "power", description: "I/O power supply voltage." }], + "Place a 10 nF decoupling capacitor close to this pin per TDK application schematic (DS-000347 rev 1.6, Table 11: VDDIO bypass C3, X7R 10 nF).", + ), + powerDomain("VDDIO"), + { + type: "implied_passives", + params: { + purpose: "Supply decoupling", + components: [{ kind: "capacitor", value: "10 nF", connection: "VDDIO (pin 5) to GND, close to the pin" }], + source: "TDK application schematic, DS-000347 rev 1.6 Table 11 (VDDIO bypass C3, X7R 10 nF)", + }, + }, + ], + }, + + // Pin 6 — GND + { + ...Ground({ id: "pin_6", name: "GND", pin: 6 }), + traits: [ + pinFunctions([{ name: "GND", direction: "sink", signal_class: "ground" }]), + powerDomain("VDD"), + { + type: "net_shareable", + params: { net: "gnd", policy: "single_ground_instance_may_serve_all_members" }, + }, + ], + }, + + // Pin 7 — RESV (must be tied to GND) + { + id: "pin_7", + name: "RESV", + pin: 7, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "power", roles: ["ground"] }], + capabilities: ["ground", "resv"], + traits: [ + pinFunctions( + [ + { name: "RESV", description: "Reserved pin; connect to GND." }, + { name: "GND", direction: "sink", signal_class: "ground" }, + ], + "TDK datasheet pinout table requires this pin to be tied to GND.", + ), + powerDomain("VDD"), + { + type: "usage_restriction", + params: { + restriction: "Pin 7 (RESV) must be tied to GND.", + consequence: "Floating it can cause unspecified behavior.", + source: "TDK datasheet pinout table (via ProtoPart design rules and warnings)", + }, + }, + ], + }, + + // Pin 8 — VDD + { + ...PowerIn({ id: "pin_8", name: "VDD", pin: 8, voltageV: VDD_RANGE, nominalV: SUPPLY_NOMINAL_V }), + capabilities: ["power_in", "vdd"], + traits: [ + pinFunctions( + [{ name: "VDD", direction: "sink", signal_class: "power", description: "Power supply voltage." }], + "Place a 100 nF (C1) plus 2.2 uF (C2) decoupling capacitor close to this pin per TDK application schematic (DS-000347 rev 1.6, Table 11: VDD bypass, X7R).", + ), + powerDomain("VDD"), + { + type: "implied_passives", + params: { + purpose: "Supply decoupling", + components: [ + { kind: "capacitor", value: "100 nF", connection: "VDD (pin 8) to GND, close to the pin" }, + { kind: "capacitor", value: "2.2 µF (bulk)", connection: "VDD rail to GND" }, + ], + source: "TDK application schematic, DS-000347 rev 1.6 Table 11 (VDD bypass C1 0.1 µF + C2 2.2 µF, X7R)", + }, + }, + ], + }, + + // Pin 9 — INT2 / FSYNC / CLKIN + icmPin({ + pin: 9, + name: "INT2 / FSYNC / CLKIN", + domain: "VDDIO", + protocols: [ + { type: "digital", roles: ["input", "output"] }, + { type: "interrupt", roles: ["output"] }, + { type: "clock", roles: ["input"] }, + ], + capabilities: ["int2", "fsync", "clkin"], + functions: [ + { name: "INT2", direction: "source", signal_class: "data", description: "Interrupt 2 output." }, + { name: "FSYNC", direction: "sink", signal_class: "data", description: "Frame sync input." }, + { name: "CLKIN", direction: "sink", signal_class: "clock", description: "External clock input." }, + ], + notes: "If FSYNC and CLKIN are not used, configure this pin as INT2 output, or tie to GND when unused per datasheet.", + extraTraits: [ + { + type: "mode_exclusive", + params: { + modes: ["INT2", "FSYNC", "CLKIN"], + rule: "Pin 9 (INT2/FSYNC/CLKIN) provides exactly one function at a time; if unused, tie to GND.", + source: "ProtoPart design rules (ICM-42688-P datasheet DS-000347 rev 1.6)", + }, + }, + ], + }), + + // Pins 10, 11 — RESV + resvPin(10), + resvPin(11), + + // Pin 12 — AP_CS + icmPin({ + pin: 12, + name: "AP_CS", + domain: "VDDIO", + protocols: [{ type: "digital", roles: ["input"] }], + capabilities: ["spi_ss"], + functions: [ + { name: "CS", direction: "sink", signal_class: "data", description: "AP_CS: SPI chip select." }, + { name: "VDDIO", direction: "sink", signal_class: "power", description: "Tie AP_CS to VDDIO when using the I3C or I2C host interface." }, + ], + notes: "Connect to VDDIO when using I3C or I2C host interface (per datasheet pinout description).", + extraTraits: [ + { + type: "co_requirement", + params: { + with: "i2c_slave, i3c_slave", + condition: "I3C or I2C host interface selected", + effect: "Tie pin 12 (AP_CS) to VDDIO; pin 12 is SPI chip select only when SPI is selected.", + source: "ProtoPart design rules (ICM-42688-P datasheet pinout description)", + }, + }, + ], + }), + + // Pin 13 — AP_SCL / AP_SCLK + icmPin({ + pin: 13, + name: "AP_SCL / AP_SCLK", + domain: "VDDIO", + protocols: [{ type: "digital", roles: ["input"] }], + capabilities: ["i2c_scl", "spi_sck"], + functions: [ + { name: "SCL", direction: "sink", signal_class: "clock", description: "AP_SCL: I3C/I2C serial clock." }, + { name: "SCLK", direction: "sink", signal_class: "clock", description: "AP_SCLK: SPI serial clock." }, + ], + }), + + // Pin 14 — AP_SDA / AP_SDIO / AP_SDI + icmPin({ + pin: 14, + name: "AP_SDA / AP_SDIO / AP_SDI", + domain: "VDDIO", + protocols: [{ type: "digital", roles: ["input", "output", "bidirectional"] }], + capabilities: ["i2c_sda", "spi_mosi", "spi_sdio"], + functions: [ + { name: "SDA", direction: "bidirectional", signal_class: "data", description: "AP_SDA: I3C/I2C serial data." }, + { name: "MOSI/MISO", direction: "bidirectional", signal_class: "data", description: "AP_SDIO: SPI serial data I/O in 3-wire mode." }, + { name: "MOSI", direction: "sink", signal_class: "data", description: "AP_SDI: SPI serial data input in 4-wire mode." }, + ], + notes: "External pull-up resistor to VDDIO required when used as I2C SDA.", + }), +]; + +// --------------------------------------------------------------------------- +// Composed interfaces — host buses, strap, interrupts, FSYNC/CLKIN, power +// --------------------------------------------------------------------------- + +function withTraits(iface: InterfaceDef, traits: TraitDef[]): InterfaceDef { + return { ...iface, traits: [...(iface.traits ?? []), ...traits] }; +} + +/** Attach traits to the interface with the given id inside a builder result. */ +function amend(ifaces: InterfaceDef[], id: string, traits: TraitDef[]): InterfaceDef[] { + return ifaces.map((i) => (i.id === id ? withTraits(i, traits) : i)); +} + +function composed(config: { + id: string; + name: string; + protocolType: string; + roles: string[]; + slots: SlotDef[]; + profiles?: InterfaceDef["profiles"]; + parameters?: Parameter[]; + maxInstances?: number; + defaultActive?: boolean; + traits?: TraitDef[]; + domain?: InterfaceDef["domain"]; +}): InterfaceDef { + return { + id: config.id, + name: config.name, + domain: config.domain ?? "electrical", + exposed: true, + default_active: config.defaultActive ?? false, + protocols: [{ type: config.protocolType, roles: config.roles }], + slots: config.slots, + ...(config.profiles ? { profiles: config.profiles } : {}), + ...(config.parameters ? { parameters: config.parameters } : {}), + ...(config.maxInstances !== undefined ? { max_instances: config.maxInstances } : {}), + ...(config.traits ? { traits: config.traits } : {}), + }; +} + +const HOST_BUSES = ["i2c_slave", "i3c_slave", "spi_4wire_slave", "spi_3wire_slave"] as const; + +/** + * The four host buses share the AP_* pads (pins 1, 12, 13, 14) — exactly one + * host protocol may be wired per board design (ProtoPart compatibility notes). + */ +function busModeExclusivity(self: string): TraitDef { + return { + type: "bus_mode_exclusivity", + params: { + exclusive_with: HOST_BUSES.filter((id) => id !== self), + note: "The same physical pins (AP_SCL, AP_SDA, AP_CS, AP_SDO) are multiplexed across I3C, I2C, and SPI; pick one host protocol per board design.", + source: "ProtoPart compatibility notes (ICM-42688-P datasheet DS-000347 rev 1.6)", + }, + }; +} + +/** ProtoPart interface constraint `requires_matching_voltage_domain: true`. */ +const VOLTAGE_DOMAIN_TRAIT: TraitDef = { + type: "voltage_domain_matching", + params: { + requires_matching_voltage_domain: true, + note: "Logic thresholds scale with VDDIO (V_IH >= 0.7 * VDDIO, V_IL <= 0.3 * VDDIO).", + source: "ProtoPart interface constraints / compatibility notes", + }, +}; + +/** AP_AD0 address selection applies to both I2C and I3C target modes. */ +const ADDR_CO_REQUIREMENT: TraitDef = { + type: "co_requirement", + params: { + with: "ap_ad0_strap", + condition: "I2C or I3C host interface selected", + effect: "7-bit slave address is 0x68 when AP_AD0 is pulled low, 0x69 when pulled high.", + source: "ProtoPart i2c_slave description (ICM-42688-P datasheet DS-000347 rev 1.6)", + }, +}; + +// I²C — fast-mode-plus target, up to 1 MHz on AP_SCL/AP_SDA (pins 13/14). +const i2cSlave = amend( + I2C({ + id: "i2c_slave", + name: "Host I2C", + roles: ["slave"], + clockFreqHz: [0, I2C_MAX_HZ], + maxInstances: 1, + profiles: [ + { id: "i2c_pins", label: "AP_SCL/AP_SDA (pins 13/14)", sda: "pin_14", scl: "pin_13" }, + ], + }), + "i2c_slave", + [ + { type: "display_notation", params: { latex: "I^{2}C" } }, + { + type: "protocol_compliance", + params: { + standard: "I2C fast-mode-plus", + max_clock_hz: I2C_MAX_HZ, + note: "I2C target interface, fast-mode-plus up to 1 MHz.", + source: "ProtoPart i2c_slave description", + }, + }, + busModeExclusivity("i2c_slave"), + ADDR_CO_REQUIREMENT, + { + type: "co_requirement", + params: { + with: "pin_12", + condition: "I2C host interface selected", + effect: "Pin 12 (AP_CS) must be tied to VDDIO for I2C mode.", + source: "ProtoPart i2c_slave description", + }, + }, + { + type: "implied_passives", + params: { + purpose: "I2C open-drain bus pull-ups", + components: [ + { + kind: "resistor", + value: "required; value not specified in source (choose per bus speed/capacitance)", + connection: "AP_SDA (pin 14) and AP_SCL (pin 13) to VDDIO", + }, + ], + source: "ProtoPart design rules: 'External pull-up resistors to VDDIO are required on AP_SDA and AP_SCL when using I2C.'", + }, + }, + VOLTAGE_DOMAIN_TRAIT, + ], +); + +// I3C — MIPI I3C SDR target, up to 12.5 MHz SCL on the same AP_SCL/AP_SDA pads. +// Hand-composed: the ProtoPart schema (v1.5.0) has no canonical I3C protocol +// type and modelled it as `custom`; normalised here to protocol type "i3c". +const i3cSlave = composed({ + id: "i3c_slave", + name: "Host I3C", + protocolType: "i3c", + roles: ["slave"], + parameters: [clockFreqHz([0, I3C_MAX_HZ])], + slots: [ + { id: "scl", required: true, match: { capability: "i2c_scl" } }, + { id: "sda", required: true, match: { capability: "i2c_sda" } }, + ], + profiles: [ + { id: "i3c_pins", label: "AP_SCL/AP_SDA (pins 13/14)", bindings: { scl: "pin_13", sda: "pin_14" } }, + ], + maxInstances: 1, + traits: [ + { + type: "protocol_compliance", + params: { + standard: "MIPI I3C SDR", + max_clock_hz: I3C_MAX_HZ, + note: "MIPI I3C SDR target interface, up to 12.5 MHz SCL.", + protopart_protocol_type: "custom (no canonical I3C type in ProtoPart schema v1.5.0; normalised to 'i3c' here)", + source: "ProtoPart i3c_slave description", + }, + }, + busModeExclusivity("i3c_slave"), + ADDR_CO_REQUIREMENT, + { + type: "co_requirement", + params: { + with: "pin_12", + condition: "I3C host interface selected", + effect: "AP_CS (pin 12) must be tied to VDDIO when using I3C.", + source: "ProtoPart i3c_slave description", + }, + }, + VOLTAGE_DOMAIN_TRAIT, + ], +}); + +// SPI 4-wire — target up to 24 MHz, mode 0/3, on AP_CS/AP_SCLK/AP_SDI/AP_SDO. +const spi4WireSlave = amend( + SPI({ + id: "spi_4wire_slave", + name: "Host SPI 4-Wire", + roles: ["slave"], + clockFreqHz: [0, SPI_MAX_HZ], + maxInstances: 1, + profiles: [ + { + id: "spi_4wire_pins", + label: "AP_CS/AP_SCLK/AP_SDI/AP_SDO (pins 12/13/14/1)", + mosi: "pin_14", + miso: "pin_1", + sck: "pin_13", + ss: "pin_12", + }, + ], + }), + "spi_4wire_slave", + [ + { + type: "spi_modes", + params: { + modes: ["Mode 0", "Mode 3"], + note: "SPI 4-wire target interface up to 24 MHz, mode 0/3.", + source: "ProtoPart spi_4wire_slave description", + }, + }, + busModeExclusivity("spi_4wire_slave"), + VOLTAGE_DOMAIN_TRAIT, + ], +); + +// SPI 3-wire — a single bidirectional data line (AP_SDIO) replaces MOSI/MISO; +// hand-composed because the standard SPI builder cannot express a shared +// data slot. Pin 1 (AP_SDO/MISO) is unused in this mode. +const spi3WireSlave = composed({ + id: "spi_3wire_slave", + name: "Host SPI 3-Wire", + protocolType: "spi", + roles: ["slave"], + parameters: [clockFreqHz([0, SPI_MAX_HZ])], + slots: [ + { id: "ss", required: true, match: { protocol: "spi", role: "select", capability: "spi_ss" } }, + { id: "sck", required: true, match: { protocol: "spi", role: "clock", capability: "spi_sck" } }, + { id: "sdio", required: true, label: "AP_SDIO (bidirectional data)", match: { capability: "spi_sdio" } }, + ], + profiles: [ + { + id: "spi_3wire_pins", + label: "AP_CS/AP_SCLK/AP_SDIO (pins 12/13/14)", + bindings: { ss: "pin_12", sck: "pin_13", sdio: "pin_14" }, + }, + ], + maxInstances: 1, + traits: [ + { + type: "protocol_compliance", + params: { + note: "SPI 3-wire target interface up to 24 MHz.", + source: "ProtoPart spi_3wire_slave description", + }, + }, + { + type: "configuration_note", + params: { + note: "Pin 1 AP_SDO/MISO is unused in this mode.", + source: "ProtoPart spi_3wire_slave description", + }, + }, + busModeExclusivity("spi_3wire_slave"), + VOLTAGE_DOMAIN_TRAIT, + ], +}); + +// AP_AD0 register-address strap — a static configuration input on pin 1. +const apAd0Strap = composed({ + id: "ap_ad0_strap", + name: "AP_AD0 Address Strap", + protocolType: "digital", + roles: ["input"], + slots: [{ id: "ad0", required: true, match: { capability: "i2c_addr_strap" } }], + profiles: [{ id: "ap_ad0_pin", label: "AP_AD0 (pin 1)", bindings: { ad0: "pin_1" } }], + maxInstances: 1, + traits: [ + ADDRESS_STRAP_TRAIT, + { + type: "co_requirement", + params: { + with: "spi_4wire_slave", + condition: "SPI 4-wire host interface selected", + effect: "Pin 1 serves as AP_SDO (MISO) in 4-wire SPI mode — the address strap is meaningful only in I2C/I3C mode.", + source: "ProtoPart pin 1 function list", + }, + }, + ], +}); + +// Interrupt outputs — pin 4 carries either the named INT1 or the aggregate INT. +const int1Output = composed({ + id: "int1_output", + name: "Interrupt 1 Output", + protocolType: "digital", + roles: ["output"], + slots: [{ id: "int1", required: true, match: { capability: "int1" } }], + profiles: [{ id: "int1_pin", label: "INT1 (pin 4)", bindings: { int1: "pin_4" } }], + maxInstances: 1, + traits: [ + { + type: "interrupt_sources", + params: { + note: "Programmable interrupt output for data ready, FIFO, motion, and APEX events. Push-pull or open-drain, polarity selectable.", + source: "ProtoPart int1_output description", + }, + }, + INT_DRIVE_TRAIT, + ], +}); + +const intOutput = composed({ + id: "int_output", + name: "Aggregate Interrupt Output", + protocolType: "digital", + roles: ["output"], + slots: [{ id: "int", required: true, match: { capability: "int_aggregate" } }], + profiles: [{ id: "int_pin", label: "INT (pin 4, all interrupts)", bindings: { int: "pin_4" } }], + maxInstances: 1, + traits: [ + { + type: "co_requirement", + params: { + with: "int1_output", + condition: "all interrupts mapped to INT on pin 4", + effect: "Aggregate interrupt output on pin 4 when all interrupts are mapped to INT. This is the same physical pin as INT1 and is mutually exclusive with using pin 4 as a named INT1-only connection.", + source: "ProtoPart int_output description", + }, + }, + INT_DRIVE_TRAIT, + ], +}); + +// Pin-9 alternates — INT2, FSYNC, CLKIN: exactly one function at a time. +const PIN9_MODE_TRAIT: TraitDef = { + type: "co_requirement", + params: { + with: "pin_9", + condition: "pin 9 mode selection", + effect: "Pin 9 (INT2/FSYNC/CLKIN) provides exactly one function at a time; if unused, tie to GND.", + source: "ProtoPart design rules", + }, +}; + +const int2Output = composed({ + id: "int2_output", + name: "Interrupt 2 Output", + protocolType: "digital", + roles: ["output"], + slots: [{ id: "int2", required: true, match: { capability: "int2" } }], + profiles: [{ id: "int2_pin", label: "INT2 (pin 9)", bindings: { int2: "pin_9" } }], + maxInstances: 1, + traits: [ + { + type: "interrupt_sources", + params: { + note: "Programmable interrupt output on pin 9 when not configured as FSYNC or CLKIN. Push-pull or open-drain, polarity selectable.", + source: "ProtoPart int2_output description", + }, + }, + INT_DRIVE_TRAIT, + PIN9_MODE_TRAIT, + ], +}); + +const fsyncIn = composed({ + id: "fsync_in", + name: "Frame Sync Input", + protocolType: "digital", + roles: ["input"], + slots: [{ id: "fsync", required: true, match: { capability: "fsync" } }], + profiles: [{ id: "fsync_pin", label: "FSYNC (pin 9)", bindings: { fsync: "pin_9" } }], + maxInstances: 1, + traits: [ + { + type: "optionality", + params: { + note: "Optional external frame-sync input on pin 9. Used to timestamp gyro/accel samples against an external strobe (for example a camera VSYNC).", + source: "ProtoPart fsync_in description", + }, + }, + PIN9_MODE_TRAIT, + ], +}); + +const clkin = composed({ + id: "clkin", + name: "External Reference Clock", + protocolType: "clock", + roles: ["input"], + // ProtoPart description: "32 kHz nominal" — stated as-is, no tighter spec given. + parameters: [{ id: "clock_freq", name: "Nominal CLKIN frequency", unit: "Hz", value: 32_000 }], + slots: [{ id: "clkin", required: true, match: { capability: "clkin" } }], + profiles: [{ id: "clkin_pin", label: "CLKIN (pin 9)", bindings: { clkin: "pin_9" } }], + maxInstances: 1, + traits: [ + { + type: "optionality", + params: { + note: "Optional external reference clock input on pin 9 (32 kHz nominal) used to drive the internal sampling clock and reduce ODR temperature drift / device-to-device variation.", + source: "ProtoPart clkin description", + }, + }, + PIN9_MODE_TRAIT, + ], +}); + +// Power interfaces — the ProtoPart definition composes each rail with its +// ground return (VDD+GND, VDDIO+GND), mirrored here as composed interfaces. +const POWER_UP_TRAIT: TraitDef = { + type: "power_up_requirement", + params: { + note: "VDD and VDDIO must ramp monotonically (10%-90% ramp time 0.01-3 ms per datasheet A.C. characteristics); absolute-max VDD/VDDIO is -0.5 V to +4 V (DS-000347 rev 1.6, Table 9). Both supplies must be present for proper operation.", + source: "ProtoPart warnings / design rules (ICM-42688-P datasheet DS-000347 rev 1.6)", + }, +}; + +const vddPowerIn = composed({ + id: "vdd_power_in", + name: "VDD Power", + protocolType: "power", + roles: ["input"], + defaultActive: true, + parameters: [voltageRangeV(VDD_RANGE[0], VDD_RANGE[1], SUPPLY_NOMINAL_V)], + slots: [ + { id: "vdd", required: true, match: { protocol: "power", role: "input", capability: "vdd" } }, + { id: "gnd", required: true, match: { protocol: "power", role: "ground", capability: "ground" } }, + ], + profiles: [ + { id: "vdd_pins", label: "VDD/GND (pins 8/6)", default_active: true, bindings: { vdd: "pin_8", gnd: "pin_6" } }, + ], + maxInstances: 1, + traits: [ + { + type: "supply_description", + params: { + note: "1.71 V to 3.6 V core and sensor supply (typical 1.8 V). Datasheet Table 3 (Section 3.3.1, D.C. Electrical Characteristics) lists low-noise-mode 6-axis current consumption of 0.88 mA at typical conditions.", + source: "ProtoPart vdd power domain / vdd_power_in description", + }, + }, + POWER_UP_TRAIT, + ], +}); + +const vddioPowerIn = composed({ + id: "vddio_power_in", + name: "VDDIO Power", + protocolType: "power", + roles: ["input"], + defaultActive: true, + parameters: [voltageRangeV(VDDIO_RANGE[0], VDDIO_RANGE[1], SUPPLY_NOMINAL_V)], + slots: [ + { id: "vddio", required: true, match: { protocol: "power", role: "input", capability: "vddio" } }, + { id: "gnd", required: true, match: { protocol: "power", role: "ground", capability: "ground" } }, + ], + profiles: [ + { id: "vddio_pins", label: "VDDIO/GND (pins 5/6)", default_active: true, bindings: { vddio: "pin_5", gnd: "pin_6" } }, + ], + maxInstances: 1, + traits: [ + { + type: "supply_description", + params: { + note: "1.71 V to 3.6 V digital I/O supply (typical 1.8 V) for the host interface, INT1, and INT2/FSYNC/CLKIN pins.", + source: "ProtoPart vddio power domain / vddio_power_in description", + }, + }, + POWER_UP_TRAIT, + ], +}); + +// --------------------------------------------------------------------------- +// Mechanical +// --------------------------------------------------------------------------- + +const pcbMount: InterfaceDef = { + id: "pcb_mount", + name: "PCB Surface Mount", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["mounting_point"] }], + capabilities: ["lga14_2p5x3p0mm", "surface_mount"], + traits: [ + { + type: "assembly_requirement", + params: { + note: "Surface-mount LGA-14 solder attachment to PCB land pattern. Package body 2.5 mm x 3.0 mm x 0.91 mm.", + source: "ProtoPart pcb_mount description", + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const ICM_42688_P: ModuleDef = defineModule({ + id: "icm-42688-p", + name: "TDK InvenSense ICM-42688-P", + version: "1.0.0", + manufacturer: "TDK InvenSense", + part_number: "ICM-42688-P", + description: + "TDK InvenSense ICM-42688-P is a high-precision 6-axis MEMS MotionTracking IMU combining a 3-axis gyroscope and 3-axis accelerometer with selectable gyro full-scale ranges from +/-15.625 dps to +/-2000 dps and accelerometer ranges from +/-2 g to +/-16 g. It supports host interfaces of I3C (up to 12.5 MHz), I2C (up to 1 MHz), and SPI (up to 24 MHz), a 2 KB FIFO, programmable INT1 and INT2 (with FSYNC / CLKIN multiplexed on INT2), and APEX motion functions including pedometer, tap, tilt, wake-on-motion, and significant motion detection.", + tags: [ + "icm-42688-p", + "icm42688", + "tdk", + "invensense", + "imu", + "accelerometer", + "gyroscope", + "6-axis", + "i2c", + "spi", + "i3c", + "fifo", + "interrupts", + "fsync", + "clkin", + "apex", + "motion-tracking", + ], + categories: ["sensor.motion"], + + interfaces: [ + // All 14 physical LGA pads in package order — schematic-honest. + ...pins, + + // Host buses (mode-exclusive on the shared AP_* pads) + ...i2cSlave, + i3cSlave, + ...spi4WireSlave, + spi3WireSlave, + + // Configuration strap + apAd0Strap, + + // Interrupts / sync / clock + int1Output, + intOutput, + int2Output, + fsyncIn, + clkin, + + // Power + vddPowerIn, + vddioPowerIn, + + // Mechanical + pcbMount, + ], + + interfaceGroups: [ + { + id: "required_power_pins", + label: "Required Power / Ground Connections (incl. pin 7 RESV tied to GND)", + members: ["pin_5", "pin_6", "pin_7", "pin_8"], + policy: "all_of", + }, + { + id: "host_interface", + label: "Host Interface (one protocol per design — shared AP_* pads)", + members: ["i2c_slave", "i3c_slave", "spi_4wire_slave", "spi_3wire_slave"], + policy: "one_of", + }, + { + id: "pin_4_interrupt_mode", + label: "Pin 4: named INT1 vs aggregate INT", + members: ["int1_output", "int_output"], + policy: "one_of", + }, + { + id: "pin_9_mode", + label: "Pin 9: INT2 / FSYNC / CLKIN (exactly one function)", + members: ["int2_output", "fsync_in", "clkin"], + policy: "one_of", + }, + ], + + requirements: [ + { + type: "power", + description: + "Both VDD (pin 8) and VDDIO (pin 5) must be present within 1.71-3.6 V (typical 1.8 V) for proper operation. Low-noise-mode 6-axis current consumption is 0.88 mA (datasheet Table 3, Section 3.3.1); total power ~1.584 mW at 1.8 V.", + voltage_V: [1.71, 3.6], + current_mA: 1, + }, + { + type: "interface", + description: + "Host must attach through exactly one of the multiplexed AP_* host interfaces: I3C (up to 12.5 MHz), I2C (up to 1 MHz), or SPI (up to 24 MHz, 3- or 4-wire). AP_CS must be tied to VDDIO in I2C/I3C mode.", + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + { + id: "vdd", + name: "VDD Core/Sensor Supply", + nominal_voltage_V: 1.8, + voltage_range_V: VDD_RANGE, + max_current_mA: 1, + }, + { + id: "vddio", + name: "VDDIO I/O Supply", + nominal_voltage_V: 1.8, + voltage_range_V: VDDIO_RANGE, + max_current_mA: 1, + }, + ], + metadata: { + package_type: "LGA-14", + pin_count: 14, + supply_voltage_V: [1.71, 3.6], + power_consumption_mW: 1.584, + i2c_addresses_7bit: ["0x68", "0x69"], + i2c_max_frequency_hz: I2C_MAX_HZ, + i3c_max_frequency_hz: I3C_MAX_HZ, + spi_max_frequency_hz: SPI_MAX_HZ, + accelerometer_ranges_g: [2, 4, 8, 16], + gyroscope_ranges_dps: [15.625, 31.25, 62.5, 125, 250, 500, 1000, 2000], + accelerometer_odr_hz: [1.5625, 32000], + gyroscope_odr_hz: [12.5, 32000], + fifo_size_bytes: 2048, + gyroscope_noise_density_mdps_rt_hz: 2.8, + accelerometer_noise_density_ug_rt_hz: 70, + low_noise_6axis_current_mA: 0.88, + esd_hbm_kV: 2, + esd_cdm_V: 500, + logic_levels: { vih_min: "0.7*VDDIO", vil_max: "0.3*VDDIO" }, + absolute_max_supply_V: 4, + power_domain_notes: { + vdd: "Analog and digital core supply, 1.71 V to 3.6 V (typical 1.8 V). Datasheet Table 3 (Section 3.3.1, D.C. Electrical Characteristics) lists low-noise-mode 6-axis current consumption of 0.88 mA at typical conditions.", + vddio: "Digital I/O supply for the host interface, INT1, and INT2/FSYNC/CLKIN pins. 1.71 V to 3.6 V.", + }, + source: "ICM-42688-P Datasheet DS-000347 rev 1.6, via ProtoPart definition (electrical metadata)", + }, + }, + { + domain: "mechanical", + dimensions_mm: { length: 3, width: 2.5, height: 0.91 }, + metadata: { + package_type: "LGA-14", + mounting_method: "surface_mount", + requires_special_tools: false, + field_serviceable: false, + }, + }, + { + domain: "thermal", + operating_temperature_C: [-40, 85], + metadata: { + requires_thermal_management: false, + }, + }, + ], + + traits: [ + { + type: "apex_motion_functions", + params: { + functions: ["pedometer", "tap", "tilt", "wake-on-motion", "significant motion detection"], + source: "ProtoPart description; AN-000173 ICM-426xx APEX Motion Functions", + }, + }, + { + type: "assembly_requirement", + params: { + note: "This is a fine-pitch surface-mount LGA IC and is not suitable for solderless breadboard use without a breakout board.", + source: "ProtoPart warnings", + }, + }, + { + type: "configuration_dependence", + params: { + note: "Current, noise, and timing specs depend on selected power mode, ODR, filters, and VDDIO level.", + source: "ProtoPart warnings", + }, + }, + { + type: "usage_notes", + params: { + note: "ICM-42688-P is a bare LGA-14 IC, not a ready-to-wire breakout. Use it when the PCB can support the TDK LGA-14 footprint, local decoupling, and a 1.71 V to 3.6 V logic-level host. The simplest configuration is I2C with AP_CS tied to VDDIO and AP_AD0 strapped for address selection. For lowest noise and tightest ODR accuracy, drive a 32 kHz CLKIN on pin 9 and use SPI at up to 24 MHz.", + source: "ProtoPart usage notes", + }, + }, + { + type: "compatibility_notes", + params: { + note: "Logic thresholds scale with VDDIO (VIH >= 0.7 * VDDIO, VIL <= 0.3 * VDDIO). The host interface supports only 7-bit slave addresses 0x68 and 0x69. The same physical pins (AP_SCL, AP_SDA, AP_CS, AP_SDO) are multiplexed across I3C, I2C, and SPI; pick one host protocol per board design. Unlike the Bosch BMI270, the ICM-42688-P does NOT expose an AUX I2C master for an external magnetometer.", + source: "ProtoPart compatibility notes", + }, + }, + { + type: "application_examples", + params: { + examples: [ + "AR/VR/XR head and controller motion tracking.", + "Robotics pose, vibration, and gesture sensing.", + "High-performance IoT motion analytics.", + "Industrial and consumer inertial measurement where CLKIN-based timing accuracy is required.", + ], + source: "ProtoPart application examples", + }, + }, + { + type: "terminology_policy", + params: { + note: "Signal and function names in `icm42688p_pin_functions` traits are ProtoPart/datasheet-verbatim and intentionally NOT normalised to a curated whitelist; canonical capability tags exist only where slot matching requires them.", + }, + }, + ], + + artifacts: [ + { + id: "art_datasheet", + name: "ICM-42688-P Datasheet (DS-000347 rev 1.6)", + type: "datasheet", + url: "https://product.tdk.com/system/files/dam/doc/product/sensor/mortion-inertial/imu/data_sheet/ds-000347-icm-42688-p-v1.6.pdf", + }, + { + id: "art_product_page", + name: "TDK InvenSense ICM-42688-P Product Page", + type: "documentation", + url: "https://invensense.tdk.com/products/motion-tracking/6-axis/icm-42688-p/", + }, + { + id: "art_apex_appnote", + name: "AN-000173 ICM-426xx APEX Motion Functions", + type: "documentation", + url: "https://invensense.tdk.com/download-resource/an-000173", + }, + { + id: "art_distributor_search", + name: "TDK InvenSense distributor search (ICM-42688-P)", + type: "documentation", + url: "https://dilp.netcomponents.com/tdkcorp_result.html?mode=1&partnumber1=icm-42688-p&partnumber2=dk-42688-p&pq=Search&ref=/cgi-bin/tdkcorp.asp", + tags: ["purchasing"], + }, + { + id: "art_product_image", + name: "ICM-42688-P product photo (DigiKey)", + type: "custom", + filePath: "./ProtoPart/protoparts/icm-42688-p/artifacts/images/1428_-14LGA-_97-2_5x3_-_-14.jpg", + mimeType: "image/jpeg", + tags: ["image", "product-photo"], + }, + ], + + geometry: { + xScale: 1, + yScale: 1, + outline: { preset: "rectangle" }, + }, +}); diff --git a/library/parts/icm-42688-p/artifacts/thumbnail.png b/library/parts/icm-42688-p/artifacts/thumbnail.png new file mode 100644 index 0000000..05d8374 Binary files /dev/null and b/library/parts/icm-42688-p/artifacts/thumbnail.png differ diff --git a/library/parts/index.ts b/library/parts/index.ts new file mode 100644 index 0000000..7dd314f --- /dev/null +++ b/library/parts/index.ts @@ -0,0 +1,23 @@ +/** + * Canonical parts library — one file per part, each exporting a single + * datasheet-honest ModuleDef (see docs/showcase-plan.md § 2.3). + */ + +export { ADAFRUIT_938_128X64_OLED } from "./adafruit-938-128x64-oled.js"; +export { ADAFRUIT_FEATHER_NRF52_BLUEFRUIT_LE_3406 } from "./adafruit-feather-nrf52-bluefruit-le-3406.js"; +export { ARDUINO_NANO } from "./arduino-nano.js"; +export { ATTINY85 } from "./attiny85.js"; +export { BME280 } from "./bme280.js"; +export { ESP32D0WDQ6 } from "./esp32-d0wdq6.js"; +export { ESP32_DEVKITC_V4 } from "./esp32-devkitc-v4.js"; +export { FRC_PNEUMATIC_PISTON } from "./frc-pneumatic-piston.js"; +export { HCSR04_ULTRASONIC_SENSOR } from "./hcsr04-ultrasonic-sensor.js"; +export { ICM_42688_P } from "./icm-42688-p.js"; +export { L298N_MOTOR_DRIVER } from "./l298n-motor-driver.js"; +export { PJRC_TEENSY_4_1 } from "./pjrc-teensy-4-1.js"; +export { REV_21_1652_NEO_VORTEX } from "./rev-21-1652-neo-vortex-brushless-motor.js"; +export { RP2040 } from "./rp2040.js"; +export { SEEED_XIAO_ESP32C3 } from "./seeed-xiao-esp32c3.js"; +export { STEPPERONLINE_17HS19_2004S1 } from "./stepperonline-17hs19-2004s1.js"; +export { TOWERPRO_SG90 } from "./towerpro-sg90.js"; +export { VL53L0X } from "./vl53l0x.js"; diff --git a/library/parts/l298n-motor-driver.ts b/library/parts/l298n-motor-driver.ts new file mode 100644 index 0000000..8112bd8 --- /dev/null +++ b/library/parts/l298n-motor-driver.ts @@ -0,0 +1,986 @@ +/** + * L298N Dual H-Bridge Motor Driver Module — audit-honest part definition. + * + * Primary source: ProtoPart audited definition + * ProtoPart/protoparts/l298n-motor-driver/definition.json (schema 1.4.0, part v0.3.0) + * whose electrical values trace to the ST L298N datasheet (mirror: + * components101.com L298N-Motor-Driver-Datasheet.pdf). Everything below is + * carried over from that JSON — power-domain ranges (Vs 5-35 V board-dependent, + * logic 4.5-5.5 V @ ~36 mA), per-output 2 A continuous rating, the 78M05 + * regulator's 100 mA / 5% / 100 mV delivery envelope, the ~2.3 V input-HIGH + * threshold, the ~2 V Darlington drop, and the module's design rules, + * validation requirements, usage notes, and warnings (all preserved verbatim + * as traits). Nothing is invented beyond that JSON. + * + * Architecture notes honoured by this file: + * - Pin-honest like a schematic: all 13 board terminals defined by the + * ProtoPart resources are leaf interfaces with their official screw + * terminal / header designators as `pin` strings (the JSON assigns no + * numeric pin positions, so the silk-screen names are the designators). + * - Instances vs combinations: the two H-bridge channels are composed + * interfaces. Direction control is one interface per channel (the JSON + * defines `motor_channel_a_control` / `motor_channel_b_control` + * separately); the motor output is a single interface with two profiles + * (OUT1+OUT2, OUT3+OUT4) and `max_instances: 2`, mirroring the JSON's + * one `dc_motor_output` interface with `max_connections: 2`. + * - Co-requirements (`co_requirement` traits): the onboard 5 V regulator + * jumper vs. Vs ≤ ~12 V (regulator-output mode) and its inverse + * (external-5 V-input mode); the ENA/ENB enable jumpers vs. PWM speed + * control. Jumpers are not JSON resources, so they are modelled as + * traits, not pins. + * - Shareability: GND is a single common net for motor and logic returns + * (`net_shareable`); the JSON's `shareable_with` marking on the EN + * pwm_input requirement is preserved as a `slot_shareability` trait. + * - The mutually exclusive roles of the 5V terminal (regulated output vs. + * logic input) are expressed as two composed power interfaces over the + * same `vss_5v` pin, grouped `one_of`. + */ + +import type { + InterfaceDef, + ModuleDef, + TraitDef, +} from "../../src/types/index.js"; +import { + Ground, + Pin, + PowerIn, + PowerOut, + defineModule, + maxCurrentA, + voltageRangeV, +} from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Electrical constants — ProtoPart electrical domain (power_domains, resources) +// --------------------------------------------------------------------------- + +/** power_domains.vmotor: board-dependent motor supply range, typically 5-35 V. */ +const VS_RANGE: [number, number] = [5, 35]; +/** power_domains.vmotor: nominal motor supply voltage. */ +const VS_NOMINAL_V = 12; +/** power_domains.vmotor: max_current_mA 4000 (~2 A per channel, two channels). */ +const VS_MAX_CURRENT_A = 4; + +/** power_domains.logic5v: 5 V TTL logic supply range. */ +const LOGIC_RANGE: [number, number] = [4.5, 5.5]; +/** power_domains.logic5v: logic supply consumption (~36 mA). */ +const LOGIC_MAX_CURRENT_A = 0.036; + +/** resources.out1-out4 current_rating.source.max_continuous_mA. */ +const OUT_MAX_CONTINUOUS_A = 2; + +/** design_rules: "Logic input HIGH threshold is ~2.3 V ...". */ +const V_IH_APPROX_V = 2.3; + +/** design_rules / warnings: "~2 V or more drop across the bipolar outputs under load". */ +const OUTPUT_DROP_TRAIT: TraitDef = { + type: "voltage_drop", + params: { + drop_V_approx: 2, + note: "Expect ~2 V or more drop across the bipolar (Darlington) outputs under load, so size Vs accordingly.", + source: "ProtoPart usage_notes / warnings (L298N datasheet, bipolar output stage)", + }, +}; + +/** design_rules[6], verbatim — shared by every logic-level control pin. */ +const LOGIC_LEVEL_TRAIT: TraitDef = { + type: "logic_levels", + params: { + v_ih_approx_V: V_IH_APPROX_V, + rule: "Logic input HIGH threshold is ~2.3 V, so 3.3 V control signals are typically acceptable, but the logic supply should still be 5 V.", + source: "ProtoPart design_rules (L298N datasheet TTL input levels)", + }, +}; + +function powerDomainTrait(domain: "vmotor" | "logic5v"): TraitDef { + return { + type: "power_domain", + params: { + domain, + isolation_type: "non_isolated", + ground_reference: "common", + }, + }; +} + +function connectorTrait(kind: "screw_terminal" | "pin_header"): TraitDef { + return { type: "connector", params: { connector_type: kind } }; +} + +/** Verbatim per-terminal function description from the ProtoPart resource. */ +function pinFunctionTrait(description: string): TraitDef { + return { + type: "l298n_pin_function", + params: { + description, + source: "ProtoPart definition.json, electrical resources", + }, + }; +} + +// --------------------------------------------------------------------------- +// Power terminals — screw terminal block (resources: vs_in, vss_5v, gnd) +// --------------------------------------------------------------------------- + +const vsIn: InterfaceDef = { + ...PowerIn({ + id: "vs_in", + name: "VS (Motor Supply)", + pin: "VS", + voltageV: VS_RANGE, + nominalV: VS_NOMINAL_V, + maxCurrentA: VS_MAX_CURRENT_A, + }), + capabilities: ["power_in", "motor_supply_in"], + traits: [ + powerDomainTrait("vmotor"), + connectorTrait("screw_terminal"), + pinFunctionTrait( + "Motor supply input (Vs/Vcc2) for external power (e.g., battery or DC supply 5–35 V)", + ), + { + type: "data_note", + params: { + note: "RESOLVED (datasheet audit 2026-07-09): the ST L298 datasheet specifies VS operative condition VIH+2.5 V (~4.8 V) min to 46 V max, 50 V absolute maximum — so neither of the previously conflicting quotes (4.5-36 V resource text vs 5-35 V domain) is an IC datasheet limit. 5-35 V is retained as the module-level operating range (board-dependent derating well inside the IC envelope); the former 4.5-36 V resource quote was corrected to match.", + source: "ST L298 datasheet, ABSOLUTE MAXIMUM RATINGS (VS Power Supply 50 V) and ELECTRICAL CHARACTERISTICS (VS Supply Voltage, pin 4: VIH+2.5 min / 46 V max, operative condition)", + }, + }, + ], + // Vs feeds the H-bridge output stage directly, and the onboard 78M05 + // regulator derives the 5 V rail from it when the regulator jumper is fitted + // (metadata.description: "onboard 5 V regulator (activated via jumper)"). + bridgesTo: ["out1", "out2", "out3", "out4", "vss_5v"], +}; + +/** + * The 5V terminal is dual-role: regulated output (regulator jumper installed, + * Vs modest) or logic input (jumper removed / Vs high). Hand-rolled because + * the PowerIn/PowerOut builders are single-role; the two composed interfaces + * `logic_5v_in` / `logic_5v_out` below expose each role separately. + */ +const vss5v: InterfaceDef = { + id: "vss_5v", + name: "5V (Vss Logic)", + pin: "5V", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "power", roles: ["input", "output"] }], + capabilities: ["logic_5v"], + parameters: [{ id: "voltage", unit: "V", value: 5, range: LOGIC_RANGE }], + traits: [ + powerDomainTrait("logic5v"), + connectorTrait("screw_terminal"), + pinFunctionTrait( + "5 V logic pin. Acts as a regulated 5 V output when the onboard regulator jumper is installed and Vs is modest (typically ≤12 V); otherwise use as a 5 V logic input (jumper removed).", + ), + { + type: "internal_regulator", + params: { + regulator: "78M05", + description: + "Onboard 5 V regulator (activated via jumper) that can power the logic circuitry and, within limits, an external controller.", + source: "ProtoPart metadata.description / design_rules ('78M05 regulator')", + }, + }, + ], +}; + +const gnd: InterfaceDef = { + ...Ground({ id: "gnd", name: "GND", pin: "GND" }), + traits: [ + powerDomainTrait("vmotor"), + connectorTrait("screw_terminal"), + pinFunctionTrait("Ground connection (common ground for motor and logic)"), + { + // Shareability exemption: one ground net legally serves the motor + // supply return, the logic return, and the controller's ground. + type: "net_shareable", + params: { + net: "gnd", + policy: "single_ground_instance_may_serve_all_members", + rule: "Always connect grounds: the motor power source ground, control logic ground, and module ground must be common.", + source: "ProtoPart design_rules", + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// Control header — IN1-IN4 direction inputs, ENA/ENB enable/PWM inputs +// --------------------------------------------------------------------------- + +/** Direction input pin (IN1-IN4): input-only 5 V TTL logic, pin header. */ +function directionInput(config: { + id: string; + name: string; + description: string; +}): InterfaceDef { + const base = Pin({ + id: config.id, + name: config.name, + pin: config.name, + voltageV: 5, + capabilities: { inputOnly: true }, + }); + return { + ...base, + capabilities: [...(base.capabilities ?? []), "digital_in"], + traits: [ + powerDomainTrait("logic5v"), + connectorTrait("pin_header"), + pinFunctionTrait(config.description), + LOGIC_LEVEL_TRAIT, + ], + }; +} + +const in1 = directionInput({ + id: "in1", + name: "IN1", + description: "IN1 - Logic input for Motor A (controls OUT1, one direction)", +}); +const in2 = directionInput({ + id: "in2", + name: "IN2", + description: "IN2 - Logic input for Motor A (controls OUT2, opposite direction)", +}); +const in3 = directionInput({ + id: "in3", + name: "IN3", + description: "IN3 - Logic input for Motor B (controls OUT3, one direction)", +}); +const in4 = directionInput({ + id: "in4", + name: "IN4", + description: "IN4 - Logic input for Motor B (controls OUT4, opposite direction)", +}); + +/** + * Enable/PWM input (ENA/ENB). Hand-rolled: the PWM builder models PWM + * *outputs*, whereas these pins are PWM sinks (JSON function `pwm_input`, + * direction sink). Each is jumper-able HIGH on many modules. + */ +function enableInput(config: { + id: string; + name: string; + motor: "A" | "B"; + description: string; +}): InterfaceDef { + return { + id: config.id, + name: config.name, + pin: config.name, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [ + { type: "pwm", roles: ["input"] }, + { type: "digital", roles: ["input"] }, + ], + capabilities: ["pwm_in", "digital_in"], + parameters: [{ id: "voltage", unit: "V", value: 5 }], + traits: [ + powerDomainTrait("logic5v"), + connectorTrait("pin_header"), + pinFunctionTrait(config.description), + LOGIC_LEVEL_TRAIT, + { + type: "enable_jumper", + params: { + default: "jumper installed — pin tied HIGH, channel enabled", + note: `The module includes enable jumpers that tie ENA/ENB HIGH by default: with jumpers installed, each channel is enabled (full-speed if IN pins demand it); remove a jumper to drive that EN pin from a PWM-capable MCU output.`, + source: "ProtoPart metadata.has_enable_jumpers / usage_notes", + }, + }, + { + type: "co_requirement", + params: { + with: `motor_channel_${config.motor.toLowerCase()}_control`, + condition: "PWM speed control desired", + effect: `PWM speed control of Motor ${config.motor} requires this pin's onboard enable jumper to be removed; with the jumper fitted the pin is held HIGH (full speed / enabled only).`, + source: "ProtoPart design_rules / usage_notes", + }, + }, + ], + }; +} + +const enA = enableInput({ + id: "ena", + name: "ENA", + motor: "A", + description: "ENA - Enable/PWM input for Motor A (jumper-able HIGH on many modules)", +}); +const enB = enableInput({ + id: "enb", + name: "ENB", + motor: "B", + description: "ENB - Enable/PWM input for Motor B (jumper-able HIGH on many modules)", +}); + +// --------------------------------------------------------------------------- +// Motor output terminals — OUT1-OUT4 screw terminals, 2 A continuous each +// --------------------------------------------------------------------------- + +function motorOutput(config: { + id: string; + name: string; + description: string; +}): InterfaceDef { + const base = PowerOut({ + id: config.id, + name: config.name, + pin: config.name, + voltageV: VS_RANGE, // output stage swings on the vmotor domain (minus the Darlington drop) + maxCurrentA: OUT_MAX_CONTINUOUS_A, + }); + return { + ...base, + capabilities: ["power_out", "motor_out"], + traits: [ + powerDomainTrait("vmotor"), + connectorTrait("screw_terminal"), + pinFunctionTrait(config.description), + OUTPUT_DROP_TRAIT, + ], + }; +} + +const out1 = motorOutput({ + id: "out1", + name: "OUT1", + description: "Motor A Output 1 (connect one terminal of Motor A here)", +}); +const out2 = motorOutput({ + id: "out2", + name: "OUT2", + description: "Motor A Output 2 (connect the other terminal of Motor A here)", +}); +const out3 = motorOutput({ + id: "out3", + name: "OUT3", + description: "Motor B Output 1 (connect one terminal of Motor B here)", +}); +const out4 = motorOutput({ + id: "out4", + name: "OUT4", + description: "Motor B Output 2 (connect the other terminal of Motor B here)", +}); + +// --------------------------------------------------------------------------- +// Composed power interfaces — JSON electrical `interfaces` +// --------------------------------------------------------------------------- + +const motorPowerIn: InterfaceDef = { + id: "motor_power_in", + name: "Motor Power Input (Vs + GND)", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "power", roles: ["input"] }], + parameters: [ + voltageRangeV(VS_RANGE[0], VS_RANGE[1], VS_NOMINAL_V), + maxCurrentA(VS_MAX_CURRENT_A), + ], + slots: [ + { id: "vs", required: true, match: { protocol: "power", role: "input", capability: "motor_supply_in" } }, + { id: "gnd", required: true, match: { capability: "ground" } }, + ], + profiles: [ + { + id: "motor_power_terminals", + label: "VS + GND screw terminals", + default_active: true, + bindings: { vs: "vs_in", gnd: "gnd" }, + }, + ], + max_instances: 1, + traits: [ + { + type: "source_description", + params: { description: "Motor power supply input interface (connect Vs/Vcc2 and GND)" }, + }, + ], +}; + +const logic5vIn: InterfaceDef = { + id: "logic_5v_in", + name: "Logic 5V Input (Vss)", + domain: "electrical", + exposed: true, + default_active: false, // one_of with logic_5v_out — the regulator jumper state selects the mode + protocols: [{ type: "power", roles: ["input"] }], + parameters: [ + voltageRangeV(LOGIC_RANGE[0], LOGIC_RANGE[1], 5), + maxCurrentA(LOGIC_MAX_CURRENT_A), + ], + slots: [ + { id: "vss", required: true, match: { protocol: "power", role: "input", capability: "logic_5v" } }, + { id: "gnd", required: true, match: { capability: "ground" } }, + ], + profiles: [ + { + id: "logic_5v_in_terminals", + label: "5V + GND screw terminals", + bindings: { vss: "vss_5v", gnd: "gnd" }, + }, + ], + max_instances: 1, // JSON constraints.max_connections: 1 + traits: [ + { + type: "source_description", + params: { + description: + "5 V logic input interface (Vss). Used when the onboard regulator jumper is removed or Vs is high.", + }, + }, + { + type: "connection_constraints", + params: { max_connections: 1, requires_matching_voltage_domain: false }, + }, + { + type: "co_requirement", + params: { + with: "vs_in", + condition: "Vs above ~12 V", + effect: + "If motor supply voltage exceeds ~12 V, remove the onboard regulator jumper and supply 5 V to the logic input pin externally to avoid overheating the regulator.", + source: "ProtoPart design_rules", + }, + }, + ], +}; + +const logic5vOut: InterfaceDef = { + id: "logic_5v_out", + name: "Logic 5V Output (regulated)", + domain: "electrical", + exposed: true, + default_active: false, // one_of with logic_5v_in — the regulator jumper state selects the mode + protocols: [{ type: "power", roles: ["output"] }], + parameters: [ + // JSON power_delivery: max 5 V / 100 mA, 5% regulation, 100 mV ripple. + { id: "voltage", unit: "V", value: 5, tolerance: { type: "percent", value: 5 } }, + maxCurrentA(0.1), + { id: "ripple_voltage", name: "Ripple voltage (max)", unit: "V", value: 0.1 }, + ], + slots: [ + { id: "vss", required: true, match: { protocol: "power", role: "output", capability: "logic_5v" } }, + { id: "gnd", required: true, match: { capability: "ground" } }, + ], + profiles: [ + { + id: "logic_5v_out_terminals", + label: "5V + GND screw terminals", + bindings: { vss: "vss_5v", gnd: "gnd" }, + }, + ], + max_instances: 1, // JSON constraints.max_connections: 1 + traits: [ + { + type: "source_description", + params: { + description: + "5 V regulated power output interface (jumper enabled, Vs typically ≤12 V). Can power a microcontroller lightly (≈100 mA max).", + }, + }, + { + type: "connection_constraints", + params: { max_connections: 1, requires_matching_voltage_domain: false }, + }, + { + type: "co_requirement", + params: { + with: "vs_in", + condition: "onboard regulator jumper installed", + effect: + "Available only with the regulator jumper fitted and Vs modest (typically ≤12 V). Do not draw more than ~100 mA; excessive current can overheat the 78M05 regulator.", + source: "ProtoPart design_rules / warnings", + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// H-bridge channels — direction control (per channel) + motor output (paired) +// --------------------------------------------------------------------------- + +function motorChannelControl(config: { + id: string; + motor: "A" | "B"; + inHigh: string; // slot -> first direction pin id + inLow: string; // slot -> second direction pin id + en: string; + description: string; +}): InterfaceDef { + return { + id: config.id, + name: `Motor ${config.motor} Control (${config.inHigh.toUpperCase()}/${config.inLow.toUpperCase()} + ${config.en.toUpperCase()})`, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input"] }], + slots: [ + // JSON requires: digital_input ×2 + pwm_input ×1 (shareable_with digital_input). + { id: "in_a", required: true, match: { protocol: "digital", role: "input", capability: "digital_in" } }, + { id: "in_b", required: true, match: { protocol: "digital", role: "input", capability: "digital_in" } }, + { id: "en", required: true, match: { protocol: "pwm", role: "input", capability: "pwm_in" } }, + ], + profiles: [ + { + id: `channel_${config.motor.toLowerCase()}_pins`, + label: `${config.inHigh.toUpperCase()}/${config.inLow.toUpperCase()} + ${config.en.toUpperCase()} header pins`, + default_active: true, + bindings: { in_a: config.inHigh, in_b: config.inLow, en: config.en }, + }, + ], + max_instances: 1, + traits: [ + { type: "source_description", params: { description: config.description } }, + { + type: "slot_shareability", + params: { + slot: "en", + shareable_with: ["digital_input"], + note: "The JSON marks the pwm_input requirement shareable with digital_input: the EN pin may be tied HIGH (onboard jumper) or driven as a plain digital enable instead of PWM.", + source: "ProtoPart electrical interfaces (requires[].shareable_with)", + }, + }, + { + type: "usage_rule", + params: { + rule: "Use ENA and ENB for PWM speed control: you can tie them HIGH for full speed or feed a PWM signal for variable speed. Ensure they are enabled (HIGH) for the motor to run.", + source: "ProtoPart design_rules", + }, + }, + ], + bridgesTo: ["dc_motor_output"], + }; +} + +const motorChannelAControl = motorChannelControl({ + id: "motor_channel_a_control", + motor: "A", + inHigh: "in1", + inLow: "in2", + en: "ena", + description: "Control for Motor A: IN1/IN2 for direction, ENA for PWM/enable.", +}); + +const motorChannelBControl = motorChannelControl({ + id: "motor_channel_b_control", + motor: "B", + inHigh: "in3", + inLow: "in4", + en: "enb", + description: "Control for Motor B: IN3/IN4 for direction, ENB for PWM/enable.", +}); + +const dcMotorOutput: InterfaceDef = { + id: "dc_motor_output", + name: "DC Motor Output (OUT pair)", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "power", roles: ["output"] }], + parameters: [ + voltageRangeV(VS_RANGE[0], VS_RANGE[1]), + maxCurrentA(OUT_MAX_CONTINUOUS_A), + ], + slots: [ + { id: "phase_a", required: true, match: { protocol: "power", role: "output", capability: "motor_out" } }, + { id: "phase_b", required: true, match: { protocol: "power", role: "output", capability: "motor_out" } }, + ], + profiles: [ + { + id: "output_a", + label: "Motor A (OUT1 + OUT2)", + default_active: true, + bindings: { phase_a: "out1", phase_b: "out2" }, + }, + { + id: "output_b", + label: "Motor B (OUT3 + OUT4)", + default_active: true, + bindings: { phase_a: "out3", phase_b: "out4" }, + }, + ], + max_instances: 2, // JSON constraints.max_connections: 2 — one motor per output pair + traits: [ + { + type: "source_description", + params: { + description: + "DC motor connection interface (each motor uses a pair: OUT1+OUT2 or OUT3+OUT4, 2 A/channel DC continuous).", + }, + }, + { + type: "connection_constraints", + params: { max_connections: 2, requires_matching_voltage_domain: true }, + }, + { + type: "data_note", + params: { + note: "RESOLVED (datasheet audit 2026-07-09): the ST L298 datasheet rates IO at 2 A DC per channel (repetitive peak 2.5 A, non-repetitive peak 3 A, total DC current up to 4 A). The former ~600 mA/channel figure does not appear in the L298 datasheet (likely confusion with the L293D) and was corrected to 2 A, matching the OUT1-OUT4 resource ratings (2000 mA max continuous per output).", + source: "ST L298 datasheet, ABSOLUTE MAXIMUM RATINGS (IO Peak Output Current, each channel) and front-page 'TOTAL DC CURRENT UP TO 4 A'", + }, + }, + { + type: "usage_rule", + params: { + rule: "For each motor, use the designated pair of outputs: connect one motor to OUT1 & OUT2 (Motor A) and the other motor to OUT3 & OUT4 (Motor B). The motor should be the only load between each output pair.", + source: "ProtoPart design_rules / validation_requirements", + }, + }, + { + type: "stepper_mode", + params: { + note: "Capable of driving two DC motors or one stepper motor (using both H-bridges together). Using both channels for a stepper increases dissipation; monitor temperature under load.", + source: "ProtoPart metadata.description / application_examples / warnings", + }, + }, + OUTPUT_DROP_TRAIT, + ], +}; + +// --------------------------------------------------------------------------- +// Mechanical — four M3/#4 corner mounting holes (mechanical domain) +// --------------------------------------------------------------------------- + +function mountingHole(n: 1 | 2 | 3 | 4): InterfaceDef { + return { + id: `mount${n}`, + name: `Mounting Hole ${n}`, + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "threaded_connection", roles: ["mounting_point"] }], + capabilities: ["mounting_hole"], + parameters: [ + { id: "hole_diameter", unit: "mm", value: 3 }, + { id: "max_force", unit: "N", value: 100 }, + ], + traits: [ + { + type: "fastener", + params: { + connector_type: "through_hole", + thread_spec: "M3 / #4", + description: "Corner mounting hole", + }, + }, + ], + }; +} + +const moduleMounting: InterfaceDef = { + id: "module_mounting", + name: "PCB Mounting", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "threaded_connection", roles: ["mounting_point"] }], + slots: [ + { id: "hole", required: true, count: 4, match: { capability: "mounting_hole" } }, + ], + profiles: [ + { + id: "corner_holes", + label: "Four M3/#4 corner holes", + default_active: true, + bindings: { hole: ["mount1", "mount2", "mount3", "mount4"] }, + }, + ], + max_instances: 1, + traits: [ + { + type: "source_description", + params: { + description: "PCB module mounting interface (four M3/#4 screws at corners)", + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const L298N_MOTOR_DRIVER: ModuleDef = defineModule({ + id: "l298n-motor-driver", + name: "L298N Dual H-Bridge Motor Driver Module", + version: "0.3.0", + manufacturer: "STMicroelectronics", + part_number: "L298N", + description: + "Dual-channel H-bridge motor driver module based on the L298N IC. Capable of driving two DC motors (or one stepper motor) with up to ~2 A per channel and motor supply voltages from ~5 V up to ~35 V (board-dependent). Includes an onboard 5 V regulator (activated via jumper) that can power the logic circuitry and, within limits, an external controller. Provides directional control and speed (PWM) control for motors, with built-in clamp diodes and a heat sink for handling power dissipation. Widely used in robotics and motor control projects.", + tags: [ + "motor", + "driver", + "H-bridge", + "L298N", + "DC motor", + "stepper motor", + "dual motor driver", + "robotics", + ], + categories: ["motor_driver", "actuator.motor_controller"], + + interfaces: [ + // Power screw terminals — board designators VS / 5V / GND. + vsIn, + vss5v, + gnd, + + // Control header — IN1-IN4 direction, ENA/ENB enable/PWM. + in1, + in2, + in3, + in4, + enA, + enB, + + // Motor output screw terminals — OUT1-OUT4. + out1, + out2, + out3, + out4, + + // Composed power interfaces + motorPowerIn, + logic5vIn, + logic5vOut, + + // H-bridge channels + motorChannelAControl, + motorChannelBControl, + dcMotorOutput, + + // Mechanical + mountingHole(1), + mountingHole(2), + mountingHole(3), + mountingHole(4), + moduleMounting, + ], + + interfaceGroups: [ + { + id: "required_power_terminals", + label: "Required Power Terminals", + members: ["vs_in", "gnd"], + policy: "all_of", + }, + { + // The 5V terminal is either the regulator's output or an external logic + // input, selected by the onboard regulator jumper — never both. + id: "logic_5v_modes", + label: "5V Terminal Mode (regulator jumper state)", + members: ["logic_5v_in", "logic_5v_out"], + policy: "one_of", + }, + { + id: "control_header", + label: "Control Header (ENA / IN1-IN4 / ENB)", + members: ["ena", "in1", "in2", "in3", "in4", "enb"], + policy: "any_of", + }, + { + id: "motor_a_output_pair", + label: "Motor A Output Pair (OUT1 + OUT2)", + members: ["out1", "out2"], + policy: "all_of", + }, + { + id: "motor_b_output_pair", + label: "Motor B Output Pair (OUT3 + OUT4)", + members: ["out3", "out4"], + policy: "all_of", + }, + ], + + requirements: [ + { + type: "power", + description: + "Motor supply (Vs) within 5-35 V (board-dependent; nominal 12 V), correctly connected to the Vs input terminal; up to ~2 A per channel (~4 A total). Validate that motor stall currents do not exceed ~2 A per channel.", + voltage_V: VS_RANGE, + current_mA: 4000, + }, + { + type: "power", + description: + "5 V TTL logic supply (~36 mA): either the onboard regulator (jumper fitted, Vs typically ≤12 V) or an external regulated 5 V on the Vss pin (jumper removed / Vs above ~12 V). All grounds (module, power source, microcontroller) must be common.", + voltage_V: [4.5, 5.5], + current_mA: 36, + }, + { + type: "interface", + description: + "A controller providing 0-5 V logic direction signals (IN1-IN4) and, for speed control, a PWM source on ENA/ENB (3.3 V logic typically accepted — input HIGH threshold ~2.3 V).", + interface_protocol: "digital", + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + { + id: "vmotor", + name: "Motor Supply (Vs)", + nominal_voltage_V: 12, + voltage_range_V: VS_RANGE, + max_current_mA: 4000, + }, + { + id: "logic5v", + name: "Logic 5V (Vss)", + nominal_voltage_V: 5, + voltage_range_V: LOGIC_RANGE, + max_current_mA: 36, + }, + ], + metadata: { + supply_voltage_V: VS_RANGE, + power_consumption_mW: 180, + // ProtoPart states pin_count 15 while 13 terminals are defined as + // resources (the count likely includes onboard jumper positions, + // which the JSON does not model as resources). + pin_count: 15, + modeled_terminals: 13, + logic_input_high_threshold_V: V_IH_APPROX_V, + isolation: "non_isolated, common ground reference (both power domains)", + source: "ProtoPart definition.json, electrical domain", + }, + }, + { + domain: "mechanical", + dimensions_mm: { length: 43, width: 43, height: 27 }, + weight_g: 26, + metadata: { + package_type: "PCB Module", + mounting_method: "four M3/#4 screws at corners (3 mm through-holes, 100 N max each)", + }, + }, + { + domain: "thermal", + operating_temperature_C: [-20, 85], + metadata: { + thermal_design_power_W: 5, + requires_thermal_management: true, + thermal_monitoring_available: false, + cooling_method: "passive", + note: "Built-in heat sink. Provide adequate heat sinking/airflow if running motors near 2 A continuously — the L298N dissipates significant heat (Darlington drop ~2 V+).", + }, + }, + ], + + traits: [ + // Signal flow: the motor supply is switched onto the output pairs by the + // H-bridges (fixture-convention bridge, consistent with vs_in.bridgesTo). + { type: "can_bridge", params: { from: ["motor_power_in"], to: ["dc_motor_output"] } }, + { + type: "provides_power", + params: { + interfaceId: "logic_5v_out", + voltage: { id: "voltage", unit: "V", value: 5 }, + maxCurrent: { id: "max_current", unit: "A", value: 0.1 }, + }, + }, + // metadata.has_enable_jumpers — no structural OpenUHD home; per-pin + // `enable_jumper` traits on ENA/ENB carry the behaviour. + { type: "has_enable_jumpers", params: { value: true } }, + { + type: "design_rules", + params: { + source: "ProtoPart definition.json, design_rules (verbatim)", + rules: [ + "If motor supply voltage exceeds ~12 V, remove the onboard regulator jumper and supply 5 V to the logic input pin externally to avoid overheating the regulator.", + "Do not draw more than ~100 mA from the module's 5 V output; excessive current can overheat the 78M05 regulator.", + "Provide adequate heat sinking/airflow if running motors near 2 A continuously. The L298N dissipates significant heat (Darlington drop ~2 V+).", + "Always connect grounds: the motor power source ground, control logic ground, and module ground must be common.", + "For each motor, use the designated pair of outputs: connect one motor to OUT1 & OUT2 (Motor A) and the other motor to OUT3 & OUT4 (Motor B).", + "Use ENA and ENB for PWM speed control: you can tie them HIGH for full speed or feed a PWM signal for variable speed. Ensure they are enabled (HIGH) for the motor to run.", + "Logic input HIGH threshold is ~2.3 V, so 3.3 V control signals are typically acceptable, but the logic supply should still be 5 V.", + ], + }, + }, + { + type: "validation_requirements", + params: { + source: "ProtoPart definition.json, validation_requirements (verbatim)", + requirements: [ + "Verify motor supply (Vs) is within 5–35 V range and correctly connected to the Vs input terminal.", + "If the 5 V regulator jumper is used, ensure motor supply does not exceed ~12 V. If motor supply is above 12 V, ensure jumper is removed and a stable 5 V is provided to the logic input pin.", + "Confirm that all grounds (module, power source, microcontroller) are connected together (common ground).", + "Validate that motor stall currents do not exceed ~2 A per channel (and consider using fuses or current limiting if motors can draw more).", + "Check that each control input (IN1–IN4, ENA, ENB) from the microcontroller is properly assigned and within 0–5 V logic levels, and that at least one of ENA/ENB is enabled if you expect the motor to run.", + "Ensure that no two outputs are shorted directly together or to supply/ground (other than through a motor); the motor should be the only load between each output pair.", + ], + }, + }, + { + type: "usage_notes", + params: { + source: "ProtoPart definition.json, usage_notes (verbatim)", + note: "This L298N motor driver module allows you to control two DC motors (or one stepper motor). Set IN1/IN2 (Motor A) and IN3/IN4 (Motor B) for direction; provide PWM on ENA/ENB for speed. The module includes enable jumpers that tie ENA/ENB HIGH by default: with jumpers installed, each channel is enabled (full-speed if IN pins demand it); remove a jumper to drive that EN pin from a PWM-capable MCU output. If the onboard 5 V regulator jumper is fitted and Vs is modest (typically ≤12 V), the 5 V pin can supply light external loads (~100 mA). For Vs above ~12 V or higher 5 V loads, remove the jumper and supply a regulated 5 V to the Vss pin. Expect ~2 V or more drop across the bipolar outputs under load, so size Vs accordingly.", + }, + }, + { + type: "compatibility_notes", + params: { + source: "ProtoPart definition.json, compatibility_notes (verbatim)", + note: "The L298N module works with any microcontroller that can provide 5 V logic signals (TTL). It is directly compatible with Arduino Uno and other 5 V logic boards. It can also be controlled by 3.3 V logic (e.g., Raspberry Pi, ESP32) because 3.3 V is typically recognized as HIGH by the L298N (~2.3 V threshold), but you still need to provide a 5 V supply to the module's logic. Note this driver is less efficient than modern MOSFET drivers (TB6612FNG, DRV8833).", + }, + }, + { + type: "application_examples", + params: { + source: "ProtoPart definition.json, application_examples (verbatim)", + examples: [ + "Arduino-based 2WD robot car (driving two DC gear motors for left/right wheels)", + "Controlling a small bipolar stepper motor (using both H-bridges together)", + "DIY RC tank or rover (two motor channels for track drive)", + "Automating a curtain or conveyor using DC motors with forward/reverse control", + "Educational projects and prototyping where a simple motor driver is needed for DC motors or solenoids", + ], + }, + }, + { + type: "warnings", + params: { + source: "ProtoPart definition.json, warnings (verbatim)", + warnings: [ + "The onboard regulator can overheat if you draw too much current from the 5 V pin or if Vs is high; use it only for light loads or provide an external 5 V.", + "During operation, the L298N chip and heat sink can become very hot. Avoid touching the heat sink and provide ventilation if possible.", + "A significant voltage drop (~2 V or more) occurs across the driver at high currents due to bipolar transistor outputs.", + "Using both channels (e.g., for a stepper) increases dissipation; monitor temperature under load.", + "Double-check wiring: ensure Vs is not connected to the 5 V logic pin and that grounds are common.", + ], + }, + }, + ], + + artifacts: [ + { + id: "art_datasheet", + name: "L298N Motor Driver Datasheet (components101 mirror)", + type: "datasheet", + url: "https://components101.com/sites/default/files/component_datasheet/L298N-Motor-Driver-Datasheet.pdf", + }, + { + // ProtoPart artifact type "image" is not an OpenUHD ArtifactType — + // mapped to "custom" with an image tag; it is the ProtoPart preview + // artifact (previewArtifactId: art_thumbnail). + id: "art_thumbnail", + name: "Thumbnail", + type: "custom", + filePath: "./ProtoPart/protoparts/l298n-motor-driver/thumbnail.png", + mimeType: "image/png", + tags: ["image", "thumbnail", "preview"], + }, + ], +}); diff --git a/library/parts/l298n-motor-driver/artifacts/thumbnail.png b/library/parts/l298n-motor-driver/artifacts/thumbnail.png new file mode 100644 index 0000000..bd8003e Binary files /dev/null and b/library/parts/l298n-motor-driver/artifacts/thumbnail.png differ diff --git a/library/parts/pjrc-teensy-4-1.ts b/library/parts/pjrc-teensy-4-1.ts new file mode 100644 index 0000000..afb0ea7 --- /dev/null +++ b/library/parts/pjrc-teensy-4-1.ts @@ -0,0 +1,1077 @@ +/** + * PJRC Teensy 4.1 (TEENSY41) — source-honest part definition. + * + * Primary source: ProtoPart definition `pjrc-teensy-4-1` (schema 1.4.0, + * version 0.4.0) — the audited source of truth for this file: + * - electrical power_domains: USB 5V, VIN (Raw Input), Regulated 3.3V + * - electrical resources: USB connector, VIN, two 3.3 V output pins, two + * GND pins, and header pins 0-23 with their per-pin function lists + * - electrical interfaces: 3.3 V power output, digital pins, analog input, + * I2C master, SPI master, PWM, Digital Audio 1/2, S/PDIF, MQS, serial + * ports, CAN bus, USB device (480 MHz) + * - design_rules / warnings / validation_requirements / usage_notes / + * application_examples / compatibility_notes (carried as traits) + * Secondary source: PJRC datasheets page (https://www.pjrc.com/teensy/datasheets.html) + * as cited by the ProtoPart metadata. Nothing below is carried over from + * convention, Arduino-core defaults, or the bare i.MX RT1062 datasheet + * without being present in the ProtoPart definition. + * + * Architecture notes honoured by this file: + * - Pin-honest like a schematic: every edge pin the ProtoPart source + * defines is a leaf interface with its silkscreen designator — the USB + * connector, VIN, two 3.3 V pins, two GND pins, and header pins 0-23. + * The source does NOT define pins 24-41, VBAT/ON-OFF/PROGRAM, the + * micro-SD slot, USB host pads, or the Ethernet PHY pads; they are + * intentionally absent here (see the `source_coverage` module trait). + * - Every pin carries its verbatim ProtoPart function list in a + * `teensy_pin_functions` trait — display data is separated from the + * canonical capability tags the matching engine needs. + * - Instances vs combinations: the ProtoPart source declares each + * peripheral interface against function names, not pin pairings. + * Where the source pins a function to exactly one pin (SPI, Digital + * Audio 2, S/PDIF, MQS, USB) the binding is a named profile; where + * several pins carry the function (serial RX/TX, SDA/SCL, CAN_TX/RX, + * OUT1) the combination space is left open via capability-tag slots + * and documented in `instance_combinations` traits. + * - Co-requirements (`co_requirement` traits): USB and VIN power sources + * are mutually exclusive (ProtoPart design rule). + * - Implied harness connections (`implied_passives` traits): external + * I2C pull-up resistors; VIN supply decoupling (both ProtoPart design + * rules). + * - Shareability exemptions (`net_shareable` traits): the two 3.3 V + * output pins are one regulator net; the two GND pins are one ground + * net — a single supply/ground instance may serve all members. + */ + +import type { + InterfaceDef, + ModuleDef, + SlotDef, + TraitDef, +} from "../../src/types/index.js"; +import type { Parameter } from "../../src/types/parameter.js"; +import { + Ground, + Pin, + PowerIn, + PowerOut, + SPI, + UART, + defineModule, + maxCurrentA, + maxFrequencyHz, + voltageV, +} from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Electrical constants — ProtoPart power_domains + design_rules/warnings +// --------------------------------------------------------------------------- + +/** ProtoPart power domain "usb_5v": USB-supplied power with built-in protection. */ +const USB_5V_RANGE: [number, number] = [4.5, 5.5]; +/** + * VIN input range per PJRC's official Teensy 4.1 pinout card (card11a_rev4, + * https://www.pjrc.com/teensy/pinout.html): "Vin (3.6 to 5.5 volts)". + * NOTE: the ProtoPart power domain said [3.6, 5] and its own design rule + * "VIN must be 4.5-6V when using external power" — both disagreed with each + * other and with PJRC; adjudicated to the official figure (see the + * `source_discrepancy` trait on vin, which preserves both source values). + */ +const VIN_RANGE: [number, number] = [3.6, 5.5]; +/** + * 3.3 V rail per PJRC's official pinout card: "3.3V (250 mA max)"; product + * page (https://www.pjrc.com/store/teensy41.html): "The recommended maximum + * for external 3.3V usage is 250mA". The ProtoPart source's 1000 mA figure + * is not supported by PJRC and was corrected (see the thermal_note trait on + * the 3.3 V output pins, which preserves the source's 1 A wording). + */ +const RAIL_3V3_NOMINAL_V = 3.3; +const RAIL_3V3_MAX_A = 0.25; + +/** ProtoPart design rule "Maximum 15mA per I/O pin". */ +const PIN_MAX_MA = 15; + +/** + * ProtoPart warning: "Maximum current per pin is around 4-6mA for continuous + * draw" — carried as the continuous-drive guidance parameter on every pin. + */ +const PIN_DRIVE_GUIDANCE: Parameter = { + id: "drive_current", + name: "Continuous drive current (source guidance)", + unit: "mA", + range: [4, 6], +}; + +/** ProtoPart design rules: 3.3 V only, NOT 5 V tolerant, never exceed 3.6 V. */ +const IO_VOLTAGE_TRAIT: TraitDef = { + type: "io_voltage_limits", + params: { + logic_level_V: 3.3, + absolute_max_input_V: 3.6, + five_volt_tolerant: false, + note: "All I/O pins are 3.3V only and NOT 5V tolerant. Never apply more than 3.3V to any I/O pin; do not exceed 3.6V on any I/O pin.", + source: "ProtoPart pjrc-teensy-4-1 design_rules / warnings", + }, +}; + +/** ProtoPart design rule + warnings on per-pin and cumulative current. */ +const IO_CURRENT_TRAIT: TraitDef = { + type: "io_current_limits", + params: { + per_pin_max_mA: PIN_MAX_MA, + continuous_guidance_mA: [4, 6], + pjrc_recommended_max_output_mA: 4, + pjrc_note: "PJRC's official spec (https://www.pjrc.com/store/teensy41.html): 'The recommended maximum output current is 4mA'. PJRC publishes no 15 mA per-pin figure; the source's 15 mA design rule is retained unverified.", + cumulative_note: "Maximum current for all I/O is limited (source does not quantify the cumulative figure).", + load_note: "If you need to drive an LED or other load, use a transistor or MOSFET.", + source: "ProtoPart pjrc-teensy-4-1 design_rules / warnings", + }, +}; + +/** ProtoPart design rule: USB and VIN power sources are mutually exclusive. */ +function powerSourceExclusivity(other: string): TraitDef { + return { + type: "co_requirement", + params: { + with: other, + condition: "both power sources connected", + effect: "USB and VIN power sources are mutually exclusive — power the board from exactly one of them.", + source: "ProtoPart pjrc-teensy-4-1 design_rules", + }, + }; +} + +// --------------------------------------------------------------------------- +// Source-verbatim per-pin function metadata +// --------------------------------------------------------------------------- + +/** + * Verbatim function-name strings from the ProtoPart electrical resources. + * The source lists names only (no direction/routing columns), so the trait + * carries the strings exactly as written. + */ +interface TeensyPinSpec { + /** Silkscreen / Arduino pin number (ProtoPart resource id "pin_"). */ + pin: number; + /** Verbatim ProtoPart function names, in source order. */ + functions: string[]; + /** Editorial note about the source data itself, when warranted. */ + sourceNote?: string; + /** + * PJRC-doc adjudication: expose the digital capability when the official + * pinout documentation confirms it despite an omission in the ProtoPart + * function list (the verbatim `functions` array is never altered). + */ + digitalPerPjrc?: boolean; +} + +function unique(values: string[]): string[] { + return [...new Set(values)]; +} + +/** + * Map a verbatim ProtoPart function name to an extra canonical capability + * tag (beyond what the Pin builder emits). Names that are pure display data + * (CTX/CRX, which no ProtoPart interface requires) map to nothing and live + * only in the `teensy_pin_functions` trait. + */ +const EXTRA_CAPS: Record = { + CAN_RX: "can_rx", + CAN_TX: "can_tx", + OUT1: "audio1_out", + IN1: "audio1_in", + LRCLK1: "audio1_lrclk", + BCLK1: "audio1_bclk", + MCLK1: "audio1_mclk", + OUT2: "audio2_out", + IN2: "audio2_in", + LRCLK2: "audio2_lrclk", + BCLK2: "audio2_bclk", + MQSL: "mqs_l", + MQSR: "mqs_r", + "S/PDIF_Out": "spdif_out", + "S/PDIF_In": "spdif_in", +}; + +/** Build one schematic-honest header pin from its ProtoPart resource row. */ +function teensyPin(spec: TeensyPinSpec): InterfaceDef { + const fns = new Set(spec.functions); + + const base = Pin({ + id: `pin_${spec.pin}`, + name: `Pin ${spec.pin}`, + pin: spec.pin, + voltageV: 3.3, + capabilities: { + // Capability flags derive strictly from the source's function list. + digital: fns.has("Digital_pin") || spec.digitalPerPjrc === true, + pwm: fns.has("PWM"), + analogIn: fns.has("Analog_read_pin"), + uartRx: fns.has("Serial_RX"), + uartTx: fns.has("Serial_TX"), + i2cSda: fns.has("SDA"), + i2cScl: fns.has("SCL"), + spiMosi: fns.has("MOSI"), + spiMiso: fns.has("MISO"), + spiSck: fns.has("SCK"), + spiSs: fns.has("CS"), + }, + }); + + const capabilities = unique([ + ...(base.capabilities ?? []), + ...spec.functions.flatMap((fn) => { + const cap = EXTRA_CAPS[fn]; + return cap !== undefined ? [cap] : []; + }), + ]); + + const parameters: Parameter[] = [...(base.parameters ?? []), PIN_DRIVE_GUIDANCE]; + + const traits: TraitDef[] = [ + { + type: "teensy_pin_functions", + params: { + source: "ProtoPart pjrc-teensy-4-1 definition, electrical resources (verbatim function names)", + functions: spec.functions, + ...(spec.sourceNote ? { note: spec.sourceNote } : {}), + }, + }, + IO_VOLTAGE_TRAIT, + IO_CURRENT_TRAIT, + ]; + + return { ...base, capabilities, parameters, traits }; +} + +// --------------------------------------------------------------------------- +// Header pins 0-23 — ProtoPart electrical resources, in board pin order. +// Function lists are verbatim from the source (including CTX/CRX on pins +// 11/13, which no source interface consumes). +// --------------------------------------------------------------------------- + +const PIN_SPECS: TeensyPinSpec[] = [ + { pin: 0, functions: ["PWM", "CAN_RX", "Serial_RX", "Digital_pin"] }, + { pin: 1, functions: ["PWM", "Serial_TX", "CAN_TX", "Digital_pin"] }, + { pin: 2, functions: ["PWM", "Digital_pin", "OUT2"] }, + { pin: 3, functions: ["LRCLK2", "PWM", "Digital_pin"] }, + { pin: 4, functions: ["BCLK2", "PWM", "Digital_pin"] }, + { pin: 5, functions: ["IN2", "PWM", "Digital_pin"] }, + { pin: 6, functions: ["OUT1", "PWM", "Digital_pin"] }, + { pin: 7, functions: ["OUT1", "PWM", "Serial_RX", "Digital_pin"] }, + { pin: 8, functions: ["IN1", "PWM", "Serial_TX", "Digital_pin"] }, + { pin: 9, functions: ["OUT1", "PWM", "Digital_pin"] }, + { + pin: 10, + functions: ["MQSR", "PWM", "CS"], + digitalPerPjrc: true, + sourceNote: + "The source function list omits Digital_pin on this pin (unlike every neighbouring pin); the verbatim list is carried as-is. Adjudicated against PJRC's official Teensy 4.1 pinout card (card11a_rev4, https://www.pjrc.com/teensy/pinout.html): pin 10 is a digital I/O pin ('All digital pins have Interrupt capability'; product page lists 55 digital input/output pins) — the omission is a source error, so the digital_io capability IS exposed here.", + }, + { pin: 11, functions: ["MOSI", "PWM", "CTX", "Digital_pin"] }, + { pin: 12, functions: ["MISO", "PWM", "MQSL", "Digital_pin"] }, + { pin: 13, functions: ["SCK", "PWM", "CRX", "Digital_pin"] }, + { pin: 14, functions: ["Serial_TX", "PWM", "Analog_read_pin", "S/PDIF_Out", "Digital_pin"] }, + { pin: 15, functions: ["Serial_RX", "PWM", "Analog_read_pin", "S/PDIF_In", "Digital_pin"] }, + { pin: 16, functions: ["Serial_RX", "SCL", "Analog_read_pin", "Digital_pin"] }, + { pin: 17, functions: ["Serial_TX", "SDA", "Analog_read_pin", "Digital_pin"] }, + { pin: 18, functions: ["SDA", "Analog_read_pin", "PWM", "Digital_pin"] }, + { pin: 19, functions: ["SCL", "Analog_read_pin", "PWM", "Digital_pin"] }, + { pin: 20, functions: ["LRCLK1", "Analog_read_pin", "Digital_pin", "Serial_TX"] }, + { pin: 21, functions: ["BCLK1", "Analog_read_pin", "Digital_pin", "Serial_RX"] }, + { pin: 22, functions: ["CAN_TX", "PWM", "Analog_read_pin", "Digital_pin"] }, + { pin: 23, functions: ["CAN_RX", "Analog_read_pin", "PWM", "Digital_pin", "MCLK1"] }, +]; + +const headerPins: InterfaceDef[] = PIN_SPECS.map(teensyPin); + +// --------------------------------------------------------------------------- +// Power and connector pins — ProtoPart electrical resources +// --------------------------------------------------------------------------- + +const usbConnector: InterfaceDef = { + ...PowerIn({ + id: "usb_5v", + name: "USB Connector (5V in)", + pin: "USB", + voltageV: USB_5V_RANGE, + nominalV: 5, + maxCurrentA: 0.5, + }), + capabilities: ["power_in", "usb_connector"], + traits: [ + { + type: "power_domain", + params: { + domain: "usb_5v", + description: "USB-supplied power with built-in protection.", + regulation_type: "regulated", + }, + }, + powerSourceExclusivity("vin"), + ], +}; + +const vin: InterfaceDef = { + ...PowerIn({ + id: "vin", + name: "VIN (Raw Input)", + pin: "VIN", + voltageV: VIN_RANGE, + nominalV: 5, + maxCurrentA: 0.5, + }), + capabilities: ["power_in", "vin_input"], + traits: [ + { + type: "power_domain", + params: { + domain: "vin", + description: "External power input through VIN pin.", + regulation_type: "unregulated", + }, + }, + { + type: "source_discrepancy", + params: { + field: "VIN voltage range", + power_domain_value_V: [3.6, 5], + design_rule_value: "VIN must be 4.5-6V when using external power", + note: "The ProtoPart power domain ([3.6, 5] V) and its own design rule disagree; both figures are preserved verbatim.", + resolution: + "Adjudicated against PJRC's official Teensy 4.1 pinout card (card11a_rev4, https://www.pjrc.com/teensy/pinout.html), which prints 'Vin (3.6 to 5.5 volts)'. Neither source figure matches: the power domain had the correct 3.6 V floor but a low 5 V ceiling, and the design rule's 4.5-6 V is wrong on both ends (6 V exceeds PJRC's documented maximum). The structured parameter now carries PJRC's 3.6-5.5 V.", + }, + }, + { + type: "implied_passives", + params: { + purpose: "VIN supply stability", + components: [ + { kind: "capacitor", value: "sufficient decoupling (source does not quantify)", connection: "VIN to GND" }, + ], + source: "ProtoPart design rules: 'Provide stable 5V supply to VIN with sufficient decoupling' / 'Ensure proper decoupling capacitors for stable operation'", + }, + }, + powerSourceExclusivity("usb_5v"), + ], +}; + +/** The two 3.3 V output pins sit on one regulated net (250 mA max per PJRC). */ +function rail3v3Pin(id: string, ordinal: number): InterfaceDef { + const base = PowerOut({ + id, + name: `3.3V Output ${ordinal}`, + pin: "3.3V", + voltageV: RAIL_3V3_NOMINAL_V, + maxCurrentA: RAIL_3V3_MAX_A, + }); + return { + ...base, + capabilities: ["power_out", "power_3v3_out"], + traits: [ + { + type: "power_domain", + params: { + domain: "regulated_3v3", + description: "3.3V output for CPU and low-power external devices.", + regulation_type: "regulated", + }, + }, + { + // Shareability exemption: one supply consumer net may legally span + // both pads — they are the same regulator output. + type: "net_shareable", + params: { + net: "teensy41_regulated_3v3", + policy: "single_supply_instance_may_serve_all_members", + members: ["3v3_out_1", "3v3_out_2"], + }, + }, + { + type: "thermal_note", + params: { + note: "The 3.3V regulator can supply up to 1A but may dissipate heat at high currents.", + source: "ProtoPart pjrc-teensy-4-1 warnings", + correction: + "The source's 1 A figure is not supported by PJRC. Official pinout card (card11a_rev4): '3.3V (250 mA max)'; product page (https://www.pjrc.com/store/teensy41.html): 'The recommended maximum for external 3.3V usage is 250mA'. The structured current limit on this pin carries 250 mA.", + }, + }, + ], + }; +} + +function gndPin(id: string, ordinal: number): InterfaceDef { + return { + ...Ground({ id, name: `Ground ${ordinal}`, pin: "GND" }), + traits: [ + { + type: "net_shareable", + params: { net: "gnd", policy: "single_ground_instance_may_serve_all_members", members: ["gnd1", "gnd2"] }, + }, + { + type: "power_domain", + params: { + domain: "regulated_3v3", + note: "The ProtoPart source associates both ground pins with the regulated_3v3 power domain (common ground reference).", + }, + }, + ], + }; +} + +const powerAndServicePins: InterfaceDef[] = [ + usbConnector, + vin, + rail3v3Pin("3v3_out_1", 1), + rail3v3Pin("3v3_out_2", 2), + gndPin("gnd1", 1), + gndPin("gnd2", 2), +]; + +/** All edge pins the source defines: power/connector pins, then pins 0-23. */ +const pins: InterfaceDef[] = [...powerAndServicePins, ...headerPins]; + +// --------------------------------------------------------------------------- +// Composed interfaces — ProtoPart electrical `interfaces`, one for one. +// Slot capability tags are canonical; the source's raw protocol type/role +// strings are preserved in `source_protocol` traits wherever they were +// normalised (see the module-level `terminology_policy` trait). +// --------------------------------------------------------------------------- + +function composed(config: { + id: string; + name: string; + protocolType: string; + roles: string[]; + slots: SlotDef[]; + profiles?: InterfaceDef["profiles"]; + parameters?: Parameter[]; + maxInstances?: number; + defaultActive?: boolean; + traits?: TraitDef[]; + domain?: InterfaceDef["domain"]; +}): InterfaceDef { + return { + id: config.id, + name: config.name, + domain: config.domain ?? "electrical", + exposed: true, + default_active: config.defaultActive ?? false, + protocols: [{ type: config.protocolType, roles: config.roles }], + slots: config.slots, + ...(config.profiles ? { profiles: config.profiles } : {}), + ...(config.parameters ? { parameters: config.parameters } : {}), + ...(config.maxInstances !== undefined ? { max_instances: config.maxInstances } : {}), + ...(config.traits ? { traits: config.traits } : {}), + }; +} + +function withTraits(iface: InterfaceDef, traits: TraitDef[]): InterfaceDef { + return { ...iface, traits: [...(iface.traits ?? []), ...traits] }; +} + +/** Attach traits to the interface with the given id inside a builder result. */ +function amend(ifaces: InterfaceDef[], id: string, traits: TraitDef[]): InterfaceDef[] { + return ifaces.map((i) => (i.id === id ? withTraits(i, traits) : i)); +} + +/** Preserve the source's raw protocol descriptor where it was normalised. */ +function sourceProtocol(type: string, role: string): TraitDef { + return { type: "source_protocol", params: { raw: { type, role } } }; +} + +// 3.3 V power output — source requires one 3v3_power_output pin + one ground. +const powerOutput3v3 = composed({ + id: "3v3_power_output", + name: "3.3V Power Output", + protocolType: "power", + roles: ["output"], + parameters: [voltageV(RAIL_3V3_NOMINAL_V), maxCurrentA(RAIL_3V3_MAX_A)], + slots: [ + { id: "rail", required: true, match: { protocol: "power", role: "output", capability: "power_3v3_out" } }, + { id: "gnd", required: true, match: { protocol: "power", role: "ground", capability: "ground" } }, + ], + traits: [ + { + type: "instance_combinations", + params: { + combination_space: "Rail: 3v3_out_1 or 3v3_out_2 (one regulator net); ground: gnd1 or gnd2 (one ground net). The source specifies no fixed pairing.", + }, + }, + ], +}); + +// Digital pins — any header pin whose source function list includes Digital_pin. +const digitalPins = composed({ + id: "digital_pins", + name: "Digital Pins (Read/Write)", + protocolType: "digital", + roles: ["input", "output", "bidirectional"], + slots: [ + { id: "pin", required: true, match: { protocol: "digital", capability: "digital_io" } }, + ], + traits: [ + sourceProtocol("digital", "transmitter"), + { + type: "instance_combinations", + params: { + combination_space: "Any header pin 0-23. The ProtoPart function list omitted Digital_pin on pin 10; adjudicated as a source error against PJRC's official pinout card ('All digital pins have Interrupt capability') — see the teensy_pin_functions note on pin_10.", + }, + }, + ], +}); + +// Analog inputs — pins whose source function list includes Analog_read_pin. +const analogInput = composed({ + id: "analog_input", + name: "Analog Pins (Read)", + protocolType: "analog", + roles: ["input"], + slots: [ + { id: "channel", required: true, match: { protocol: "analog", role: "input", capability: "analog_in" } }, + ], + traits: [ + sourceProtocol("analog", "receiver"), + { + type: "channels", + params: { + count: 10, + mapping: "Pins 14-23 carry the Analog_read_pin function.", + note: "The source assigns no A0-A9 channel names and no ADC resolution/reference figures.", + }, + }, + { + type: "co_requirement", + params: { + with: "3v3_power_output", + condition: "analog measurements in use", + effect: "Verify analog reference voltage (ProtoPart validation requirement).", + }, + }, + ], +}); + +// I2C master — SDA/SCL route to two candidate pins each; the source does not +// pair them into controllers, so the combination space stays open. +const i2cMaster = composed({ + id: "i2c_master", + name: "I2C Master (SDA/SCL)", + protocolType: "i2c", + roles: ["master"], + slots: [ + { id: "sda", required: true, match: { protocol: "i2c", role: "data", capability: "i2c_sda" } }, + { id: "scl", required: true, match: { protocol: "i2c", role: "clock", capability: "i2c_scl" } }, + ], + traits: [ + { + type: "instance_combinations", + params: { + combination_space: "SDA: pin 17 or pin 18; SCL: pin 16 or pin 19. The source declares function eligibility only — no controller pairings and no bus clock figure.", + }, + }, + { + type: "implied_passives", + params: { + purpose: "Open-drain bus pull-ups", + components: [ + { kind: "resistor", value: "value per bus (source does not quantify)", connection: "SDA and SCL to the bus supply" }, + ], + source: "ProtoPart design rules: 'Use external pull-up resistors for I2C communication'", + }, + }, + ], +}); + +// SPI master — the only peripheral the source pins to exactly one pin per +// signal: MOSI=11, MISO=12, SCK=13, CS=10. +const spiMaster = amend( + SPI({ + id: "spi_master_miso", // ProtoPart interface id, kept verbatim + name: "SPI Master (MISO/MOSI/SCK/CS)", + roles: ["master"], + profiles: [ + { + id: "spi_main_pins", + label: "MOSI=11 / MISO=12 / SCK=13 / CS=10", + mosi: "pin_11", + miso: "pin_12", + sck: "pin_13", + ss: "pin_10", + }, + ], + }), + "spi_master_miso", + [ + { + type: "source_discrepancy", + params: { + field: "spi_master_miso protocol/requires", + raw_protocol: { type: "spi", role: "slave" }, + raw_requires: ["MISO x1", "MISO x1", "SCK x1", "CS x1"], + note: "The source names and describes this interface as 'SPI Master (MISO/MOSI/SCK/CS)' but records protocol role 'slave' and lists MISO twice (once presumably intended as MOSI). Roles here follow the interface's own name/description; the raw fields are preserved verbatim for audit.", + resolution: + "Adjudicated against PJRC's official Teensy 4.1 pinout card (card11a_rev4, https://www.pjrc.com/teensy/pinout.html): pins 10=CS, 11=MOSI, 12=MISO, 13=SCK form the board's main SPI port, with distinct MOSI and MISO signals — the board acts as SPI controller (master) on this port. The 'slave' role and duplicated MISO are source transcription errors; the 'master' role and the MOSI=11/MISO=12/SCK=13/CS=10 profile used here match the official card.", + }, + }, + { + type: "signal_integrity", + params: { + note: "Keep signal wires short for high-speed interfaces.", + source: "ProtoPart pjrc-teensy-4-1 design_rules", + }, + }, + ], +); + +// PWM — one slot, any pin whose source function list includes PWM. +const pwmPins = composed({ + id: "pwm_pins", + name: "PWM Pins", + protocolType: "pwm", + roles: ["output"], + slots: [ + { id: "channel", required: true, match: { protocol: "pwm", role: "output", capability: "pwm_out" } }, + ], + traits: [ + sourceProtocol("pwm", "transmitter"), + { + type: "channels", + params: { + count: 20, + mapping: "Pins 0-15, 18, 19, 22 and 23 carry the PWM function (pins 16, 17, 20, 21 do not).", + note: "The source assigns no PWM frequency or resolution figures.", + }, + }, + ], +}); + +// Digital Audio 1 — OUT1/IN1/LRCLK1/BCLK1. OUT1 has three candidate pins, so +// no unique profile exists; MCLK1 (pin 23) is a pin function the source +// interface does not require. +const digitalAudio1 = composed({ + id: "digital_audio", + name: "Digital Audio 1", + protocolType: "digital_audio", + roles: ["transmitter"], + slots: [ + { id: "out", required: true, label: "OUT1", match: { capability: "audio1_out" } }, + { id: "in", required: true, label: "IN1", match: { capability: "audio1_in" } }, + { id: "lrclk", required: true, label: "LRCLK1", match: { capability: "audio1_lrclk" } }, + { id: "bclk", required: true, label: "BCLK1", match: { capability: "audio1_bclk" } }, + ], + traits: [ + { + type: "instance_combinations", + params: { + combination_space: "OUT1: pin 6, 7 or 9; IN1: pin 8; LRCLK1: pin 20; BCLK1: pin 21.", + note: "MCLK1 is available on pin 23 per the source pin-function list, but the source's Digital Audio 1 interface does not require it — it is left as an unconsumed capability (audio1_mclk).", + }, + }, + ], +}); + +// Digital Audio 2 — OUT2/IN2/LRCLK2/BCLK2, each on exactly one pin. +const digitalAudio2 = composed({ + id: "digital_audio_2", + name: "Digital Audio 2", + protocolType: "digital_audio", + roles: ["transmitter"], + slots: [ + { id: "out", required: true, label: "OUT2", match: { capability: "audio2_out" } }, + { id: "in", required: true, label: "IN2", match: { capability: "audio2_in" } }, + { id: "lrclk", required: true, label: "LRCLK2", match: { capability: "audio2_lrclk" } }, + { id: "bclk", required: true, label: "BCLK2", match: { capability: "audio2_bclk" } }, + ], + profiles: [ + { + id: "digital_audio_2_pins", + label: "OUT2=2 / LRCLK2=3 / BCLK2=4 / IN2=5", + bindings: { out: "pin_2", in: "pin_5", lrclk: "pin_3", bclk: "pin_4" }, + }, + ], +}); + +// S/PDIF — digital audio over single coax, out on pin 14, in on pin 15. +const spdif = composed({ + id: "digital_audio_over_single_coax", + name: "Digital Audio Over Single Coax", + protocolType: "digital_audio_over_single_coax", + roles: ["transmitter"], + slots: [ + { id: "out", required: true, label: "S/PDIF_Out", match: { capability: "spdif_out" } }, + { id: "in", required: true, label: "S/PDIF_In", match: { capability: "spdif_in" } }, + ], + profiles: [ + { + id: "spdif_pins", + label: "S/PDIF Out=14 / In=15", + bindings: { out: "pin_14", in: "pin_15" }, + }, + ], +}); + +// Medium Quality Sound — MQSL on pin 12, MQSR on pin 10. +const mqs = composed({ + id: "Medium_Quality_Sound_Output", // ProtoPart interface id, kept verbatim + name: "Medium Quality Sound", + protocolType: "medium_quality_sound_output", + roles: ["transmitter"], + slots: [ + { id: "left", required: true, label: "MQSL", match: { capability: "mqs_l" } }, + { id: "right", required: true, label: "MQSR", match: { capability: "mqs_r" } }, + ], + profiles: [ + { + id: "mqs_pins", + label: "MQSL=12 / MQSR=10", + bindings: { left: "pin_12", right: "pin_10" }, + }, + ], +}); + +// Serial ports — one RX + one TX per port; the source declares eligibility +// per function name only (no Serial1..Serial8 pairings, no baud figures). +const serialPorts = amend( + UART({ + id: "Serial_ports", // ProtoPart interface id, kept verbatim + name: "Serial Ports", + }), + "Serial_ports", + [ + sourceProtocol("serial_ports", "transmitter"), + { + type: "instance_combinations", + params: { + combination_space: "Serial_RX: pins 0, 7, 15, 16, 21; Serial_TX: pins 1, 8, 14, 17, 20. The source declares no fixed RX/TX pairings and no baud-rate figures.", + }, + }, + ], +); + +// CAN bus — TX/RX eligibility only; the source declares no controller pairings. +const canBus = composed({ + id: "can_bus", + name: "CAN Bus", + protocolType: "can", + roles: ["transceiver"], + slots: [ + { id: "tx", required: true, label: "CAN_TX", match: { capability: "can_tx" } }, + { id: "rx", required: true, label: "CAN_RX", match: { capability: "can_rx" } }, + ], + traits: [ + { + type: "instance_combinations", + params: { + combination_space: "CAN_TX: pin 1 or 22; CAN_RX: pin 0 or 23. The source declares no controller pairings and no bit-rate figures.", + note: "Pins 11 and 13 additionally list verbatim 'CTX'/'CRX' functions that no source interface requires; they are preserved as display data only (see teensy_pin_functions).", + }, + }, + ], +}); + +// USB device — 480 Mbit/s (PJRC spec: "USB device 480 Mbit/sec speed"; the +// ProtoPart source wrote "480 MHz" — same 480e6 signaling figure, kept as +// the maxFrequencyHz parameter), max one connection, on the USB connector. +const usbDevice = composed({ + id: "usb_device", + name: "Usb Device", + protocolType: "usb", + roles: ["device"], + parameters: [maxFrequencyHz(480_000_000)], + slots: [ + { id: "connector", required: true, match: { capability: "usb_connector" } }, + ], + profiles: [ + { id: "usb_device_connector", label: "On-board USB connector", bindings: { connector: "usb_5v" } }, + ], + maxInstances: 1, // constraints.max_connections: 1 + traits: [ + { + type: "source_constraints", + params: { + exclusive: false, + max_connections: 1, + requires_matching_voltage_domain: false, + source: "ProtoPart pjrc-teensy-4-1 usb_device interface constraints", + }, + }, + { + type: "programming_path", + params: { + note: "Connect via USB and ensure board is recognized; run a blink test on the built-in LED.", + source: "ProtoPart pjrc-teensy-4-1 validation_requirements", + }, + }, + ], +}); + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const PJRC_TEENSY_4_1: ModuleDef = defineModule({ + id: "pjrc-teensy-4-1", + name: "PJRC Teensy 4.1", + version: "1.0.0", + manufacturer: "PJRC", + part_number: "TEENSY41", + description: + "PJRC Teensy 4.1 high-performance microcontroller development board based on NXP i.MX RT1062 ARM Cortex-M7. Ultra-fast 600MHz processor with extensive I/O capabilities.", + tags: ["mcu", "board", "3v3", "usb", "arm", "cortex-m7", "teensy", "microcontroller", "high-performance", "600mhz"], + categories: ["microcontroller.teensy"], + + interfaces: [ + // All edge pins the ProtoPart source defines, in source order: + // USB / VIN / 3.3V x2 / GND x2, then header pins 0-23. + ...pins, + + // Power delivery + powerOutput3v3, + + // General-purpose I/O + digitalPins, + analogInput, + pwmPins, + + // Serial / bus controllers + i2cMaster, + ...spiMaster, + ...serialPorts, + canBus, + + // Audio + digitalAudio1, + digitalAudio2, + spdif, + mqs, + + // Connectivity + usbDevice, + ], + + interfaceGroups: [ + { + id: "power_sources", + label: "Power Sources (mutually exclusive per design rule)", + members: ["usb_5v", "vin"], + policy: "one_of", + }, + { + id: "regulated_3v3_common_net", + label: "3.3V Output Pins (one regulator net)", + members: ["3v3_out_1", "3v3_out_2"], + policy: "all_of", + }, + { + id: "ground_common_net", + label: "Ground Pins (one ground net)", + members: ["gnd1", "gnd2"], + policy: "all_of", + }, + ], + + requirements: [ + { + type: "power", + description: + "Power from exactly one source: USB (4.5-5.5 V, ≤500 mA, regulated with built-in protection) or VIN (3.6-5.5 V per PJRC's official pinout card 'Vin (3.6 to 5.5 volts)'; the ProtoPart power domain said 3.6-5 V and its design rule 4.5-6 V — see the source_discrepancy trait on vin). Provide stable 5V supply to VIN with sufficient decoupling.", + voltage_V: 5, + current_mA: 500, + }, + { + type: "interface", + description: + "Programming and validation path: connect via USB and ensure the board is recognized (Teensyduino, PlatformIO, or a direct ARM toolchain per the source usage notes).", + interface_protocol: "usb", + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + { id: "usb_5v", name: "USB 5V", nominal_voltage_V: 5, voltage_range_V: USB_5V_RANGE, max_current_mA: 500, regulation_type: "regulated" }, + { id: "vin", name: "VIN (Raw Input)", nominal_voltage_V: 5, voltage_range_V: VIN_RANGE, max_current_mA: 500, regulation_type: "unregulated" }, + { id: "regulated_3v3", name: "Regulated 3.3V", nominal_voltage_V: 3.3, max_current_mA: 250, regulation_type: "regulated" }, + ], + metadata: { + // ProtoPart power-domain fields with no PowerDomainDef home: + power_domain_details: { + usb_5v: { + voltage_tolerance_percent: 10, + isolation_type: "non_isolated", + ground_reference: "common", + efficiency_percent: 90, + voltage_ripple_mV: 50, + compatible_domains: [], + description: "USB-supplied power with built-in protection", + }, + vin: { + voltage_tolerance_percent: 10, + isolation_type: "non_isolated", + ground_reference: "common", + efficiency_percent: 85, + voltage_ripple_mV: 100, + compatible_domains: [], + description: "External power input through VIN pin", + }, + regulated_3v3: { + voltage_tolerance_percent: 6, + isolation_type: "non_isolated", + ground_reference: "common", + efficiency_percent: 85, + voltage_ripple_mV: 5, + compatible_domains: ["usb_5v", "vin"], + description: "3.3V output for CPU and low-power external devices", + }, + }, + modeled_edge_pins: 30, + header_io_pins: 24, + io_voltage: "3.3 V only — NOT 5 V tolerant; never exceed 3.6 V on any I/O pin", + per_pin_current_mA: { design_rule_max: PIN_MAX_MA, continuous_guidance: [4, 6], pjrc_recommended_max_output: 4 }, + cpu: "NXP i.MX RT1062 ARM Cortex-M7, 600 MHz (dynamic clock scaling; can be overclocked beyond 600 MHz per source usage notes)", + source: "ProtoPart pjrc-teensy-4-1 definition (schema 1.4.0, version 0.4.0)", + }, + }, + { + domain: "mechanical", + dimensions_mm: { length: 61, width: 18, height: 5 }, + weight_g: 5, + metadata: { + package_type: "PCB Module", + mounting_method: "breadboard", + enclosure_type: "open_pcb", + assembly_time_min: 2, + field_serviceable: true, + mount_holes: [], + note: "The ProtoPart source defines no mechanical resources or interfaces (no mounting interface is modeled). Form factor does not match Arduino shield layouts, but adapter boards exist (source compatibility notes).", + }, + }, + { + domain: "thermal", + operating_temperature_C: [-40, 85], + metadata: { + thermal_design_power_W: 1, + requires_thermal_management: false, + thermal_monitoring_available: false, + cooling_method: "passive", + note: "The high-speed MCU can warm up under load; the 3.3V regulator may dissipate heat at high currents (source warnings).", + }, + }, + ], + + traits: [ + { + type: "terminology_policy", + params: { + note: "Interface ids and display names are ProtoPart-verbatim (including 'Serial_ports', 'Medium_Quality_Sound_Output', and 'spi_master_miso'). Protocol types/roles were normalised to the canonical builder vocabulary only where a canonical equivalent exists (serial_ports→uart; transmitter/receiver→output/input for digital, analog and pwm); every normalised raw descriptor is preserved in a source_protocol or source_discrepancy trait. Pin function names in teensy_pin_functions traits are source-verbatim and NOT normalised.", + }, + }, + { + type: "source_coverage", + params: { + note: "The ProtoPart source defines only the USB connector, VIN, two 3.3V pins, two GND pins, and header pins 0-23. It does not define pins 24-41, VBAT/ON-OFF/PROGRAM pins, the micro-SD slot (mentioned only in the source usage notes), the USB host pads, the Ethernet PHY pads, or the PSRAM/flash expansion pads — those board features are therefore not modeled here.", + }, + }, + { + type: "design_rules", + params: { + source: "ProtoPart pjrc-teensy-4-1 design_rules (verbatim)", + rules: [ + "Maximum 15mA per I/O pin", + "Maximum current for all I/O is limited", + "VIN must be 4.5-6V when using external power", + "USB and VIN power sources are mutually exclusive", + "Do not exceed 3.6V on any I/O pin", + "Use external pull-up resistors for I2C communication", + "Ensure proper decoupling capacitors for stable operation", + "All I/O pins are 3.3V only and NOT 5V tolerant", + "Provide stable 5V supply to VIN with sufficient decoupling", + "Keep signal wires short for high-speed interfaces", + ], + }, + }, + { + type: "validation_requirements", + params: { + source: "ProtoPart pjrc-teensy-4-1 validation_requirements (verbatim)", + requirements: [ + "Check power supply compatibility", + "Verify I/O voltage levels", + "Validate current limits", + "Check communication protocol compatibility", + "Ensure proper grounding", + "Verify analog reference voltage", + "Connect via USB and ensure board is recognized", + "Run a blink test on the built-in LED", + "Test each used interface with appropriate examples", + ], + }, + }, + { + type: "warnings", + params: { + source: "ProtoPart pjrc-teensy-4-1 warnings (verbatim)", + warnings: [ + "Never apply more than 3.3V to any I/O pin", + "I/O pins are not 5V tolerant", + "Maximum current per pin is around 4-6mA for continuous draw", + "If you need to drive an LED or other load, use a transistor or MOSFET", + "The high-speed MCU can warm up under load", + "The 3.3V regulator can supply up to 1A but may dissipate heat at high currents", + "Be cautious of the small components and the SD card slot", + ], + }, + }, + { + type: "usage_notes", + params: { + source: "ProtoPart pjrc-teensy-4-1 usage_notes (verbatim)", + note: "Requires the Teensyduino extension or compatible support to program using the Arduino IDE. The board can also be used with PlatformIO or direct ARM toolchains. The MCU supports dynamic clock scaling. The Teensy 4.1 can be overclocked beyond 600MHz for more performance. The built-in SD card slot allows for convenient data logging.", + }, + }, + { + type: "compatibility_notes", + params: { + source: "ProtoPart pjrc-teensy-4-1 compatibility_notes (verbatim)", + note: "Software: Most Arduino sketches will compile and run, but direct hardware register manipulation must be adapted to the NXP i.MX RT1062's registers. Many Arduino libraries have Teensy 4 support. Hardware: The form factor doesn't match Arduino shield layouts, but adapter boards exist. Most 3.3V sensors and modules can connect directly.", + }, + }, + { + type: "application_examples", + params: { + source: "ProtoPart pjrc-teensy-4-1 application_examples (verbatim)", + examples: [ + "Advanced robotics and drones with high-speed control loops", + "Digital audio workstations or synthesizers", + "LED stage lighting or art installations", + "Scientific instrumentation and data acquisition", + "IoT edge devices requiring significant processing", + ], + }, + }, + ], + + artifacts: [ + { + id: "art_datasheet", + name: "PJRC Teensy Datasheets", + type: "datasheet", + url: "https://www.pjrc.com/teensy/datasheets.html", + }, + { + id: "art_thumbnail", + name: "Teensy 4.1 Thumbnail", + type: "custom", + filePath: "./ProtoPart/protoparts/pjrc-teensy-4-1/thumbnail.png", + mimeType: "image/png", + tags: ["image", "thumbnail"], + }, + ], + + geometry: { + xScale: 1, + yScale: 1, + outline: { preset: "rectangle" }, + }, +}); diff --git a/library/parts/pjrc-teensy-4-1/artifacts/thumbnail.png b/library/parts/pjrc-teensy-4-1/artifacts/thumbnail.png new file mode 100644 index 0000000..01dfb99 Binary files /dev/null and b/library/parts/pjrc-teensy-4-1/artifacts/thumbnail.png differ diff --git a/library/parts/rev-21-1652-neo-vortex-brushless-motor.ts b/library/parts/rev-21-1652-neo-vortex-brushless-motor.ts new file mode 100644 index 0000000..d378644 --- /dev/null +++ b/library/parts/rev-21-1652-neo-vortex-brushless-motor.ts @@ -0,0 +1,536 @@ +/** + * REV Robotics NEO Vortex Brushless Motor (REV-21-1652) — ProtoPart-honest + * part definition. + * + * Primary source: ProtoPart definition + * ProtoPart/protoparts/rev-21-1652-neo-vortex-brushless-motor/definition.json + * (schema 1.4.0, part version 0.2.0) — the audited source of truth. + * - metadata: "High-power sensored brushless motor with dock interface and + * 1/2 in hex through-bore rotor." + * - electrical domain: three phase dock contacts + one JST-PH 6 encoder + * connector; one power domain ("Sensor 5V": 5 V nominal, 4.5-5.5 V, + * 200 mA max). + * - mechanical domain: 4x threaded mounting holes and the 1/2 in hex + * through-bore rotor output. + * Secondary source: REV Robotics product page + * https://www.revrobotics.com/rev-21-1652/ (the ProtoPart `datasheet_url`). + * + * Architecture notes honoured by this file: + * - Contact-honest like a schematic: every ProtoPart electrical/mechanical + * `resource` is a leaf interface carrying its verbatim description in a + * `vortex_contact_functions` trait; every ProtoPart `interface` is a + * composed interface whose slots bind those leaves through a fixed + * default profile (there is exactly one physical binding for each). + * - The three phase contacts are one indivisible set: the dock interface + * requires all of phase_a/phase_b/phase_c, mirrored by an all_of + * interface group. A sensored controller additionally consumes the + * 6-pin sensor bundle — captured as the `controller_docking_set` group + * and a `sensored_motor` trait, wording taken from the JSON only. + * - ProtoPart `constraints` (max_connections, requires_connector_type) + * map to `max_instances` plus `connection_constraint` traits; + * `connector_type` on each resource is preserved on its leaf trait. + * - Performance data (absent from the audited JSON) has been verified + * against REV Robotics official sources and added as a module-level + * `motor_performance` trait: 12 V nominal, 565 Kv, 6784 RPM free speed, + * 3.6 A free running current, 211 A stall current, 3.6 Nm stall torque, + * 640 W peak power, 375 W typical output at 40 A. Sources: + * https://www.revrobotics.com/rev-21-1652/ and + * https://docs.revrobotics.com/brushless/neo/vortex. The source JSON + * still carries no phase-contact electrical ratings, no mass, no body + * dimensions, and no thermal domain — those remain unmodelled. + * - Former source-internal discrepancy RESOLVED against REV docs: the two + * texts describe two different features, both real. "#10-32 threaded + * mounting hole (2 in bolt circle)" is the motor MOUNTING pattern (REV + * specs: "#10-32 threaded holes on a 2in bolt circle"). "4x M3 x 25 mm + * socket head cap screws" are the DOCKING hardware that secure a + * SPARK Flex or NEO Vortex Solo Adapter to the motor (REV: "M3 SHCS x + * 25 mm"; ideal torque 11.5 ±0.9 in-lb / 1.3 ±0.1 Nm per + * https://docs.revrobotics.com/brushless/neo/vortex/solo-adapter). + * The face_mount interface now carries the #10-32 pattern; the M3 + * hardware is recorded as docking metadata. + * - The JSON's own `warnings` (protopart-whitelist gaps for `phase_c` and + * `encoder_index`) are preserved as a module-level trait. + */ + +import type { + InterfaceDef, + ModuleDef, + TraitDef, +} from "../../src/types/index.js"; +import { defineModule } from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Source citation shared by the verbatim-description traits +// --------------------------------------------------------------------------- + +const PROTOPART_SOURCE = + "ProtoPart rev-21-1652-neo-vortex-brushless-motor definition.json (schema 1.4.0, v0.2.0)"; + +/** + * Verbatim ProtoPart resource metadata for one leaf contact — the + * electromechanical analogue of the gold standard's `esp32_pin_functions` + * trait: display/source data separated from the canonical capability tags + * the matching engine needs. + */ +function contactFunctions(functions: string[], description: string): TraitDef { + return { + type: "vortex_contact_functions", + params: { + source: PROTOPART_SOURCE, + functions: functions.map((name) => ({ name })), + description, + }, + }; +} + +// --------------------------------------------------------------------------- +// Electrical leaf contacts — ProtoPart electrical `resources` +// --------------------------------------------------------------------------- +// No power/signal builder fits here: the JSON declares no voltage, current, +// or signal-class data for these contacts (only `connector_type` and a +// custom/peer protocol on the composed interfaces), so the leaves are +// hand-rolled with exactly what the source states. + +/** One motor phase power contact in the dock interface. */ +function phaseContact(letter: "a" | "b" | "c"): InterfaceDef { + const upper = letter.toUpperCase(); + return { + id: `phase_${letter}`, + name: `Phase ${upper} dock contact`, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "custom", roles: ["peer"] }], + capabilities: [`phase_${letter}`], + traits: [ + contactFunctions( + [`phase_${letter}`], + `Motor phase ${upper} power contact in the dock interface.`, + ), + { type: "connector", params: { connector_type: "dock_contact" } }, + ], + // The rotor is the motor's mechanical output; phase power entering the + // dock leaves the part as shaft motion at the 1/2 in hex through-bore. + bridgesTo: ["rotor_hex_bore"], + }; +} + +const phaseContacts: InterfaceDef[] = [ + phaseContact("a"), + phaseContact("b"), + phaseContact("c"), +]; + +/** The 6-pin JST-PH sensor connector — ProtoPart resource `jst_encoder_connector`. */ +const jstEncoderConnector: InterfaceDef = { + id: "jst_encoder_connector", + name: "JST-PH 6 encoder connector", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "custom", roles: ["peer"] }], + capabilities: ["encoder_connector"], + traits: [ + contactFunctions(["encoder_connector"], "JST-PH 6 encoder connector."), + { type: "connector", params: { connector_type: "jst_ph_6" } }, + { + // "Sensor 5V" is the only power domain the ProtoPart electrical domain + // declares; the composed encoder interface's description lists 5V/GND + // among the six positions of this connector. + type: "power_domain", + params: { + domain: "sensor_5v", + note: "'Sensor 5V' power domain (5 V nominal, 4.5-5.5 V, 200 mA max) per the ProtoPart electrical domain; the connector carries 5V/GND alongside the sensor signals.", + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// Electrical composed interfaces — ProtoPart electrical `interfaces` +// --------------------------------------------------------------------------- + +/** + * ProtoPart `bldc_phase_dock`: requires phase_a + phase_b + phase_c (count 1 + * each); constraints max_connections: 1, requires_connector_type: "dock_contact". + */ +const bldcPhaseDock: InterfaceDef = { + id: "bldc_phase_dock", + name: "Brushless phase dock", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "custom", roles: ["peer"] }], + slots: [ + { id: "phase_a", required: true, match: { capability: "phase_a" } }, + { id: "phase_b", required: true, match: { capability: "phase_b" } }, + { id: "phase_c", required: true, match: { capability: "phase_c" } }, + ], + profiles: [ + { + id: "dock_contacts", + label: "Phase A/B/C dock contacts (fixed)", + default_active: true, + bindings: { phase_a: "phase_a", phase_b: "phase_b", phase_c: "phase_c" }, + }, + ], + max_instances: 1, // ProtoPart constraints.max_connections: 1 + traits: [ + { + type: "interface_description", + params: { + source: PROTOPART_SOURCE, + description: + "3-phase dock interface to a compatible motor controller (e.g., docked SPARK Flex).", + }, + }, + { + type: "connection_constraint", + params: { + max_connections: 1, + requires_connector_type: "dock_contact", + source: `${PROTOPART_SOURCE}, interface bldc_phase_dock constraints`, + }, + }, + { + // All three phases are one indivisible connection: the interface's + // `requires` list demands each of phase_a/phase_b/phase_c at count 1. + type: "indivisible_set", + params: { + members: ["phase_a", "phase_b", "phase_c"], + note: "A motor controller must take all three phase contacts together — the dock interface requires one of each.", + }, + }, + ], + bridgesTo: ["rotor_hex_bore"], +}; + +/** + * ProtoPart `encoder_connector`: requires the encoder_connector function + * (count 1); constraints max_connections: 1, requires_connector_type: "jst_ph_6". + */ +const encoderConnector: InterfaceDef = { + id: "encoder_connector", + name: "JST-PH 6 encoder connector", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "custom", roles: ["peer"] }], + slots: [ + { id: "connector", required: true, match: { capability: "encoder_connector" } }, + ], + profiles: [ + { + id: "jst_ph_6", + label: "JST-PH 6 connector (fixed)", + default_active: true, + bindings: { connector: "jst_encoder_connector" }, + }, + ], + max_instances: 1, // ProtoPart constraints.max_connections: 1 + traits: [ + { + type: "interface_description", + params: { + source: PROTOPART_SOURCE, + description: + "6-pin JST-PH sensor connector (via Solo Adapter): 5V/GND, quadrature A/B, index/C, temperature analog.", + }, + }, + { + type: "connection_constraint", + params: { + max_connections: 1, + requires_connector_type: "jst_ph_6", + source: `${PROTOPART_SOURCE}, interface encoder_connector constraints`, + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// Mechanical leaves — ProtoPart mechanical `resources` +// --------------------------------------------------------------------------- + +/** + * ProtoPart resource `mounting_holes_x4`. + * NOTE (audit, resolved): the description below is CONFIRMED against REV + * specs — "#10-32 threaded holes on a 2in bolt circle" is the motor's + * mounting pattern (https://www.revrobotics.com/rev-21-1652/). The + * "4x M3 x 25 mm socket head cap screws" formerly attributed to face_mount + * are the separate docking hardware for a SPARK Flex / Solo Adapter. + */ +const mountingHoles: InterfaceDef = { + id: "mounting_holes_x4", + name: "Mounting holes x4", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["mounting_point"] }], + capabilities: ["mounting_holes_x4"], + traits: [ + contactFunctions( + ["mounting_holes_x4"], + "#10-32 threaded mounting hole (2 in bolt circle).", + ), + { type: "connector", params: { connector_type: "threaded_hole" } }, + ], +}; + +/** ProtoPart resource `hex_bore` — the rotor output. */ +const hexBore: InterfaceDef = { + id: "hex_bore", + name: "1/2 in hex rotor bore", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["custom"] }], + capabilities: ["mechanical_drive"], + traits: [ + contactFunctions(["mechanical_drive"], "1/2 in hex through-bore output interface."), + { type: "connector", params: { connector_type: "custom" } }, + ], +}; + +// --------------------------------------------------------------------------- +// Mechanical composed interfaces — ProtoPart mechanical `interfaces` +// --------------------------------------------------------------------------- + +/** ProtoPart `face_mount`: requires mounting_holes_x4 (count 1); max_instances 1. */ +const faceMount: InterfaceDef = { + id: "face_mount", + name: "Face mount", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["mounting_point"] }], + slots: [ + { id: "holes", required: true, match: { capability: "mounting_holes_x4" } }, + ], + profiles: [ + { + id: "face_mount_holes", + label: "4x mounting holes (fixed)", + default_active: true, + bindings: { holes: "mounting_holes_x4" }, + }, + ], + max_instances: 1, + traits: [ + { + type: "assembly_requirement", + params: { + source: + "REV NEO Vortex specifications (https://www.revrobotics.com/rev-21-1652/); docking hardware per https://docs.revrobotics.com/brushless/neo/vortex/solo-adapter", + note: "Mounting pattern: 4x #10-32 threaded holes on a 2 in bolt circle (motor mounting). The 4x M3 x 25 mm socket head cap screws are NOT mounting hardware — they are the docking screws that secure a SPARK Flex or NEO Vortex Solo Adapter to the motor (ideal torque 11.5 ±0.9 in-lb / 1.3 ±0.1 Nm; do not run the motor without them installed).", + }, + }, + ], +}; + +/** ProtoPart `rotor_hex_bore`: requires mechanical_drive (count 1); max_instances 1. */ +const rotorHexBore: InterfaceDef = { + id: "rotor_hex_bore", + name: "1/2 in hex rotor bore", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["custom"] }], + slots: [ + { id: "bore", required: true, match: { capability: "mechanical_drive" } }, + ], + profiles: [ + { + id: "rotor_bore", + label: "1/2 in hex through-bore (fixed)", + default_active: true, + bindings: { bore: "hex_bore" }, + }, + ], + max_instances: 1, + traits: [ + { + type: "interface_description", + params: { + source: PROTOPART_SOURCE, + description: "Output rotor interface: 1/2 in hex through-bore.", + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const REV_21_1652_NEO_VORTEX: ModuleDef = defineModule({ + id: "rev-21-1652-neo-vortex-brushless-motor", + name: "NEO Vortex Brushless Motor", + version: "0.2.0", + manufacturer: "REV Robotics", + part_number: "REV-21-1652", + description: + "High-power sensored brushless motor with dock interface and 1/2 in hex through-bore rotor. Three-phase dock contacts mate a compatible motor controller (e.g., docked SPARK Flex); a 6-pin JST-PH connector (via Solo Adapter) carries 5V/GND, quadrature A/B, index/C, and temperature analog.", + tags: ["brushless", "bldc", "sensored", "frc", "rev-ion", "motor"], + categories: ["motor"], // ProtoPart metadata.type: "motor" (no taxonomy field in source) + + interfaces: [ + // Electrical leaf contacts — ProtoPart resources, in source order. + ...phaseContacts, + jstEncoderConnector, + + // Electrical composed interfaces. + bldcPhaseDock, + encoderConnector, + + // Mechanical leaves — ProtoPart resources. + mountingHoles, + hexBore, + + // Mechanical composed interfaces. + faceMount, + rotorHexBore, + ], + + interfaceGroups: [ + { + id: "bldc_phase_contacts", + label: "Phase Dock Contacts (one indivisible 3-phase set)", + members: ["phase_a", "phase_b", "phase_c"], + policy: "all_of", + }, + { + // A sensored controller consumes the phase set and the sensor bundle + // together: the part is described as a sensored brushless motor, and + // the encoder connector is its commutation/telemetry feedback path. + id: "controller_docking_set", + label: "Motor Controller Connections (phases + sensor feedback)", + members: ["bldc_phase_dock", "encoder_connector"], + policy: "all_of", + }, + ], + + requirements: [ + { + type: "interface", + description: + "Drive requires a compatible 3-phase motor controller on the dock interface (e.g., docked SPARK Flex) taking all three phase contacts together.", + interface_protocol: "custom", + }, + { + type: "power", + description: + "Sensor 5V supply for the encoder connector: 5 V nominal (4.5-5.5 V), 200 mA max, per the ProtoPart 'Sensor 5V' power domain.", + voltage_V: [4.5, 5.5], + current_mA: 200, + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + { + id: "sensor_5v", + name: "Sensor 5V", + nominal_voltage_V: 5, + voltage_range_V: [4.5, 5.5], + max_current_mA: 200, + }, + ], + metadata: { + // Faithfulness note: the ProtoPart source declares no phase + // voltage/current ratings. kV, torque, and power figures were + // absent from the source too, but have been verified against REV + // official specs and added as the module-level `motor_performance` + // trait below. + connector_types: { + phase_contacts: "dock_contact", + encoder: "jst_ph_6", + }, + source: PROTOPART_SOURCE, + }, + }, + { + domain: "mechanical", + // No dimensions_mm / weight_g: the source JSON carries neither. + metadata: { + output_interface: "1/2 in hex through-bore rotor", + mounting_pattern: + "#10-32 threaded mounting holes on a 2 in bolt circle (confirmed: REV NEO Vortex specs, https://www.revrobotics.com/rev-21-1652/)", + docking_hardware: + "4x M3 x 25 mm socket head cap screws through counterbored clearance holes, securing a docked SPARK Flex or NEO Vortex Solo Adapter (NOT motor mounting); ideal torque 11.5 ±0.9 in-lb (1.3 ±0.1 Nm) per https://docs.revrobotics.com/brushless/neo/vortex/solo-adapter", + source: PROTOPART_SOURCE, + }, + }, + ], + + traits: [ + { + // Performance figures verified against REV Robotics official sources + // (product page specs + docs.revrobotics.com NEO Vortex page). Not + // present in the audited ProtoPart JSON — added during datasheet audit. + type: "motor_performance", + params: { + nominal_voltage_V: 12, + motor_kv_rpm_per_V: 565, + free_speed_rpm: 6784, + free_running_current_A: 3.6, + stall_current_A: 211, + stall_torque_Nm: 3.6, + peak_output_power_W: 640, + typical_output_power_at_40A_W: 375, + source: + "REV Robotics NEO Vortex specifications: https://www.revrobotics.com/rev-21-1652/ and https://docs.revrobotics.com/brushless/neo/vortex", + }, + }, + { + type: "sensored_motor", + params: { + note: "Sensored brushless motor (per metadata description). Sensor path is the 6-pin JST-PH connector — verbatim: '6-pin JST-PH sensor connector (via Solo Adapter): 5V/GND, quadrature A/B, index/C, temperature analog.'", + source: PROTOPART_SOURCE, + }, + }, + { + // The source JSON's own `warnings` array, carried verbatim. + type: "protopart_warnings", + params: { + warnings: [ + "Whitelist additions needed: functions phase_c and encoder_index are not present in protopart-whitelist.json; add them to make this part canonical.", + ], + source: PROTOPART_SOURCE, + }, + }, + { + type: "terminology_policy", + params: { + note: "Function names (phase_a/phase_b/phase_c, encoder_connector, mounting_holes_x4, mechanical_drive) are ProtoPart-verbatim and intentionally NOT normalised: the source itself flags phase_c and encoder_index as absent from the protopart whitelist. Capability tags mirror those function names for slot matching only.", + }, + }, + ], + + artifacts: [ + { + id: "art_product_page", + name: "REV Robotics NEO Vortex Product Page (REV-21-1652)", + type: "documentation", + url: "https://www.revrobotics.com/rev-21-1652/", // ProtoPart metadata.datasheet_url + }, + { + id: "art_thumbnail", + name: "NEO Vortex Thumbnail", + type: "custom", + filePath: "./ProtoPart/protoparts/rev-21-1652-neo-vortex-brushless-motor/thumbnail.png", + mimeType: "image/png", + tags: ["image", "thumbnail"], + }, + ], + + // The source JSON declares no node_geometry; the neutral default matches + // the gold-standard tail structure. + geometry: { + xScale: 1, + yScale: 1, + outline: { preset: "rectangle" }, + }, +}); diff --git a/library/parts/rev-21-1652-neo-vortex-brushless-motor/artifacts/thumbnail.png b/library/parts/rev-21-1652-neo-vortex-brushless-motor/artifacts/thumbnail.png new file mode 100644 index 0000000..c75282a Binary files /dev/null and b/library/parts/rev-21-1652-neo-vortex-brushless-motor/artifacts/thumbnail.png differ diff --git a/library/parts/rp2040.ts b/library/parts/rp2040.ts new file mode 100644 index 0000000..a5fe9a1 --- /dev/null +++ b/library/parts/rp2040.ts @@ -0,0 +1,2151 @@ +/** + * Raspberry Pi RP2040 — datasheet-honest part definition. + * + * Audited from: C:\Software\ProtoPart\protoparts\rp2040\definition.json (v2.0, + * schema 1.5.0). Primary sources cited by that definition: + * - RP2040 Datasheet (https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf) + * Table 1 Pin descriptions (pin numbers, names — copied verbatim) + * Table 2 GPIO function table (F1 SPI, F2 UART, F3 I2C, F4 PWM, F5 SIO, + * F6 PIO0, F7 PIO1, F8 CLOCK, F9 USB) — the fixed function-select + * routing that replaces a full GPIO matrix on this part + * Table 342 PADS_BANK0:SWCLK reset state (PUE=1/PDE=0) + * Table 619 TESTEN reset state (internal pull-down) + * Table 634 Recommended supply voltages (IOVDD, DVDD, USB_VDD, ADC_AVDD) + * §1.4.2 Pin specifications (single ground via exposed pad, USB routing) + * §2.16 Crystal oscillator (XOSC, Pierce topology, 12 MHz for USB boot) + * §2.9.5 ADC supply sensitivity (performance compromised below 2.97 V) + * §3.6.2 PIO example: WS2812 LEDs + * §4.2-§4.5 UART (PL011), I2C (DW_apb_i2c), SPI (PL022), PWM + * - Hardware Design with RP2040 (decoupling, QSPI flash, BOOTSEL wiring) + * + * This models the BARE RP2040 CHIP (QFN-56 7×7 mm), not a Pico board: no + * onboard flash, no USB connector, no BOOTSEL button (board-level BOOTSEL is a + * pushbutton on QSPI_SS_N, pin 56 — there is no dedicated USB_BOOT pin). + * + * Architecture notes honoured by this file: + * - Pin-honest like a schematic: all 57 physical pads (56 perimeter + exposed + * ground pad) are leaf interfaces, in package order, ids "pin_N" (and + * "pad_gnd" for the exposed pad, keeping the ProtoPart resource id), with + * the datasheet pin name as the displayed name. + * - Every GPIO carries its verbatim function-select list (name, direction, + * signal class, FUNCSEL slot) in an `rp2040_pin_functions` trait — display + * data is separated from the canonical capability tags the matching engine + * needs. + * - Instances vs combinations: `max_instances` states how many controllers + * (or state machines / mux slots) exist in silicon; slots + capability tags + * span the honest combination space. The RP2040 has no ESP32-style GPIO + * matrix — every peripheral function is FUNCSEL-fixed to specific pins, so + * controller-specific tags (spi0_rx, uart1_tx, i2c0_sda, pwm3_a, + * clock_gpin0, usb_vbus_det, ...) are emitted per pin from the JSON's own + * function data, and profiles enumerate every silicon-valid pin group. + * SIO/PIO0/PIO1 reach every bank-0 GPIO — those tags are the matrix + * analogue here. + * - Co-requirements (`co_requirement` traits): external QSPI flash for XIP + * boot (the chip cannot run user code without it), 12 MHz crystal for the + * USB bootloader, VREG_VOUT→DVDD external routing, USB_VDD supplied even + * when USB is unused, ADC_AVDD as ADC reference/clamp rail. + * - Implied harness connections (`implied_passives` traits): crystal load + * caps, USB_DP/DM 27 Ω series terminations, ADC_AVDD ferrite-bead filter, + * 100 nF per supply pin + 1 µF bulk per rail (all from the ProtoPart + * design_rules). + * - Shareability exemptions (`net_shareable` traits): the six IOVDD pins are + * one rail, the two DVDD pins are one rail, and the exposed pad is the + * single external ground connection. + */ + +import type { + InterfaceDef, + ModuleDef, + SlotDef, + TraitDef, +} from "../../src/types/index.js"; +import type { Parameter } from "../../src/types/parameter.js"; +import { + Ground, + Pin, + PowerIn, + PowerOut, + SPI, + UART, + I2C, + defineModule, + clockFreqHz, + resolutionBits, +} from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Electrical constants — ProtoPart power_domains (Datasheet Table 634 / §1.4.2) +// --------------------------------------------------------------------------- + +/** IOVDD digital I/O ring: 1.8 V or 3.3 V (typical 3.3 V). Sets logic level for all GPIOs and the QSPI interface. */ +const IOVDD_RANGE: [number, number] = [1.8, 3.3]; +/** DVDD digital core: 1.05-1.16 V (typ 1.1 V) per Table 634. */ +const DVDD_RANGE: [number, number] = [1.05, 1.16]; +/** VREG_VIN input to the on-chip core LDO. */ +const VREG_VIN_RANGE: [number, number] = [1.8, 3.3]; +/** USB_VDD dedicated USB 1.1 PHY supply: 3.135-3.63 V (typ 3.3 V) per Table 634. */ +const USB_VDD_RANGE: [number, number] = [3.135, 3.63]; +/** ADC_AVDD analog supply/reference: 1.62-3.63 V (typ 3.3 V) per Table 634; performance compromised below 2.97 V (§2.9.5). */ +const ADC_AVDD_RANGE: [number, number] = [1.62, 3.63]; + +/** ProtoPart design rule 2: per-supply-pin decoupling. */ +const SUPPLY_DECOUPLING_TRAIT: TraitDef = { + type: "implied_passives", + params: { + purpose: "Supply decoupling", + components: [ + { kind: "capacitor", value: "100 nF ceramic", connection: "pin to GND, close to the pin" }, + { kind: "capacitor", value: "1 µF bulk", connection: "one per rail group" }, + ], + source: "ProtoPart design_rules / Hardware Design with RP2040", + }, +}; + +// --------------------------------------------------------------------------- +// Datasheet-honest per-pin function metadata +// --------------------------------------------------------------------------- + +/** + * One FUNCSEL entry, carried verbatim from the ProtoPart per-pin function + * lists. `direction` uses the ProtoPart vocabulary (source/sink/bidirectional) + * rather than the datasheet's I/O column, because that is what the audited + * JSON records. `funcsel` is the Datasheet Table 2 column: the ProtoPart + * definition cites F1 (SPI), F2 (UART), F4 (PWM), F8 (CLOCK) and F9 (USB) + * explicitly; F3/F5/F6/F7 complete the same table row. + */ +interface Rp2040Function { + name: string; + direction: "source" | "sink" | "bidirectional"; + signal_class: "data" | "clock" | "sense" | "power" | "ground"; + funcsel?: string; + note?: string; +} + +interface Rp2040GpioSpec { + /** QFN-56 package pin number (Datasheet Table 1). */ + pin: number; + /** Bank-0 GPIO number — used as the displayed interface name (GPIOn). */ + gpio: number; + /** F1: fixed SPI controller signal on this pin. */ + spi: string; + /** F2: fixed UART controller signal on this pin. */ + uart: string; + /** F3: fixed I2C controller signal on this pin. */ + i2c: string; + /** F4: fixed PWM slice/channel on this pin (PWM_). */ + pwm: string; + /** F9: fixed USB VBUS-management signal on this pin. */ + usb: string; + /** F8: CLOCK_GPINn / CLOCK_GPOUTn where present (GPIO20-25 only). */ + clock?: string; + /** ADC channel number for GPIO26-29 (pins 38-41). */ + adc?: number; +} + +function unique(values: string[]): string[] { + return [...new Set(values)]; +} + +/** Verbatim function list for one GPIO pad, mirroring the ProtoPart entry. */ +function gpioFunctions(spec: Rp2040GpioSpec): Rp2040Function[] { + const spiDir: "source" | "sink" = spec.spi.endsWith("_RX") ? "sink" : "source"; + const uartDir: "source" | "sink" = + spec.uart.endsWith("_RX") || spec.uart.endsWith("CTS") ? "sink" : "source"; + const usbDir: "source" | "sink" = spec.usb.endsWith("_EN") ? "source" : "sink"; + + const fns: Rp2040Function[] = [ + { name: "digital_io", direction: "bidirectional", signal_class: "data" }, + ]; + if (spec.adc !== undefined) { + fns.push({ + name: "analog_input", + direction: "sink", + signal_class: "sense", + note: `ADC${spec.adc}`, + }); + } + fns.push( + { + name: spec.spi, + direction: spiDir, + signal_class: spec.spi.endsWith("_SCK") ? "clock" : "data", + funcsel: "F1", + }, + { name: spec.uart, direction: uartDir, signal_class: "data", funcsel: "F2" }, + { + name: spec.i2c, + direction: "bidirectional", + signal_class: spec.i2c.endsWith("_SCL") ? "clock" : "data", + funcsel: "F3", + }, + { name: spec.pwm, direction: "source", signal_class: "data", funcsel: "F4" }, + { name: "SIO", direction: "bidirectional", signal_class: "data", funcsel: "F5" }, + { name: "PIO0", direction: "bidirectional", signal_class: "data", funcsel: "F6" }, + { name: "PIO1", direction: "bidirectional", signal_class: "data", funcsel: "F7" }, + ); + if (spec.clock !== undefined) { + fns.push({ + name: spec.clock, + direction: spec.clock.includes("GPIN") ? "sink" : "source", + signal_class: "clock", + funcsel: "F8", + }); + } + fns.push({ + name: spec.usb, + direction: usbDir, + signal_class: usbDir === "sink" ? "sense" : "data", + funcsel: "F9", + }); + return fns; +} + +/** Build one schematic-honest GPIO pad from its function-select row. */ +function rp2040Gpio(spec: Rp2040GpioSpec): InterfaceDef { + const base = Pin({ + id: `pin_${spec.pin}`, + name: `GPIO${spec.gpio}`, + pin: spec.pin, + voltageV: IOVDD_RANGE, + capabilities: { + // Every bank-0 GPIO carries a fixed PWM channel via F4 (JSON per-pin + // `pwm_out` function) — generic tag for inter-module PWM matching. + pwm: true, + analogIn: spec.adc !== undefined, + // Generic bus tags reflect the ONE fixed controller signal this pin has + // (the F1/F2/F3 columns), never a routable superset. + i2cSda: spec.i2c.endsWith("_SDA"), + i2cScl: spec.i2c.endsWith("_SCL"), + spiMosi: spec.spi.endsWith("_TX"), // datasheet TX = controller data out + spiMiso: spec.spi.endsWith("_RX"), // datasheet RX = controller data in + spiSck: spec.spi.endsWith("_SCK"), + spiSs: spec.spi.endsWith("_CSn"), + uartTx: spec.uart.endsWith("_TX"), + uartRx: spec.uart.endsWith("_RX"), + uartRts: spec.uart.endsWith("RTS"), + uartCts: spec.uart.endsWith("CTS"), + }, + }); + + const capabilities = unique([ + ...(base.capabilities ?? []), + `gpio${spec.gpio}`, + // Controller-specific FUNCSEL tags — the honest combination space. These + // are what the composed controllers' traits/profiles enumerate; the + // routing is fixed, not matrix-style. + spec.spi.toLowerCase(), + spec.uart.toLowerCase(), + spec.i2c.toLowerCase(), + spec.pwm.toLowerCase(), + spec.usb.toLowerCase(), + // SIO/PIO reach every bank-0 GPIO (F5/F6/F7) — the matrix analogue. + "sio", + "pio0", + "pio1", + ...(spec.clock !== undefined + ? [spec.clock.toLowerCase(), spec.clock.includes("GPIN") ? "clock_gpin" : "clock_gpout"] + : []), + ...(spec.adc !== undefined ? [`adc_ch${spec.adc}`] : []), + ]); + + const traits: TraitDef[] = [ + { + type: "rp2040_pin_functions", + params: { + source: + "RP2040 Datasheet §1.4.3 Table 2 (GPIO function table) — carried verbatim from the ProtoPart definition", + functions: gpioFunctions(spec), + }, + }, + { + type: "power_domain", + params: { + domain: "IOVDD", + note: "Logic level tracks IOVDD: 3.3 V typical, 1.8 V supported. All GPIO are NOT 5 V tolerant — apply level shifting for 5 V signals (ProtoPart warnings).", + }, + }, + ...(spec.adc !== undefined + ? [ + { + type: "analog_input_clamp", + params: { + note: "The voltage on the ADC analogue inputs must not exceed IOVDD — voltages greater than IOVDD leak through the pad ESD protection diodes (absolute maximum pin voltage IOVDD + 0.5 V, Table 622).", + source: "RP2040 Datasheet §2.9.5 note / Table 622 — corrects the ProtoPart warning's 'clamped to ADC_AVDD + 0.3 V'", + }, + } satisfies TraitDef, + ] + : []), + ]; + + return { ...base, capabilities, traits }; +} + +// --------------------------------------------------------------------------- +// Bank-0 GPIO pads — ProtoPart pins 2-9, 11-18, 27-32, 34-41 (Datasheet +// Table 1 / Table 2), in package pin order. Each row is the pin's fixed +// FUNCSEL assignment, transcribed from the per-pin function lists. +// --------------------------------------------------------------------------- + +const GPIO_SPECS: Rp2040GpioSpec[] = [ + { pin: 2, gpio: 0, spi: "SPI0_RX", uart: "UART0_TX", i2c: "I2C0_SDA", pwm: "PWM0_A", usb: "USB_OVCUR_DET" }, + { pin: 3, gpio: 1, spi: "SPI0_CSn", uart: "UART0_RX", i2c: "I2C0_SCL", pwm: "PWM0_B", usb: "USB_VBUS_DET" }, + { pin: 4, gpio: 2, spi: "SPI0_SCK", uart: "UART0_CTS", i2c: "I2C1_SDA", pwm: "PWM1_A", usb: "USB_VBUS_EN" }, + { pin: 5, gpio: 3, spi: "SPI0_TX", uart: "UART0_RTS", i2c: "I2C1_SCL", pwm: "PWM1_B", usb: "USB_OVCUR_DET" }, + { pin: 6, gpio: 4, spi: "SPI0_RX", uart: "UART1_TX", i2c: "I2C0_SDA", pwm: "PWM2_A", usb: "USB_VBUS_DET" }, + { pin: 7, gpio: 5, spi: "SPI0_CSn", uart: "UART1_RX", i2c: "I2C0_SCL", pwm: "PWM2_B", usb: "USB_VBUS_EN" }, + { pin: 8, gpio: 6, spi: "SPI0_SCK", uart: "UART1_CTS", i2c: "I2C1_SDA", pwm: "PWM3_A", usb: "USB_OVCUR_DET" }, + { pin: 9, gpio: 7, spi: "SPI0_TX", uart: "UART1_RTS", i2c: "I2C1_SCL", pwm: "PWM3_B", usb: "USB_VBUS_DET" }, + { pin: 11, gpio: 8, spi: "SPI1_RX", uart: "UART1_TX", i2c: "I2C0_SDA", pwm: "PWM4_A", usb: "USB_VBUS_EN" }, + { pin: 12, gpio: 9, spi: "SPI1_CSn", uart: "UART1_RX", i2c: "I2C0_SCL", pwm: "PWM4_B", usb: "USB_OVCUR_DET" }, + { pin: 13, gpio: 10, spi: "SPI1_SCK", uart: "UART1_CTS", i2c: "I2C1_SDA", pwm: "PWM5_A", usb: "USB_VBUS_DET" }, + { pin: 14, gpio: 11, spi: "SPI1_TX", uart: "UART1_RTS", i2c: "I2C1_SCL", pwm: "PWM5_B", usb: "USB_VBUS_EN" }, + { pin: 15, gpio: 12, spi: "SPI1_RX", uart: "UART0_TX", i2c: "I2C0_SDA", pwm: "PWM6_A", usb: "USB_OVCUR_DET" }, + { pin: 16, gpio: 13, spi: "SPI1_CSn", uart: "UART0_RX", i2c: "I2C0_SCL", pwm: "PWM6_B", usb: "USB_VBUS_DET" }, + { pin: 17, gpio: 14, spi: "SPI1_SCK", uart: "UART0_CTS", i2c: "I2C1_SDA", pwm: "PWM7_A", usb: "USB_VBUS_EN" }, + { pin: 18, gpio: 15, spi: "SPI1_TX", uart: "UART0_RTS", i2c: "I2C1_SCL", pwm: "PWM7_B", usb: "USB_OVCUR_DET" }, + { pin: 27, gpio: 16, spi: "SPI0_RX", uart: "UART0_TX", i2c: "I2C0_SDA", pwm: "PWM0_A", usb: "USB_VBUS_DET" }, + { pin: 28, gpio: 17, spi: "SPI0_CSn", uart: "UART0_RX", i2c: "I2C0_SCL", pwm: "PWM0_B", usb: "USB_VBUS_EN" }, + { pin: 29, gpio: 18, spi: "SPI0_SCK", uart: "UART0_CTS", i2c: "I2C1_SDA", pwm: "PWM1_A", usb: "USB_OVCUR_DET" }, + { pin: 30, gpio: 19, spi: "SPI0_TX", uart: "UART0_RTS", i2c: "I2C1_SCL", pwm: "PWM1_B", usb: "USB_VBUS_DET" }, + { pin: 31, gpio: 20, spi: "SPI0_RX", uart: "UART1_TX", i2c: "I2C0_SDA", pwm: "PWM2_A", usb: "USB_VBUS_EN", clock: "CLOCK_GPIN0" }, + { pin: 32, gpio: 21, spi: "SPI0_CSn", uart: "UART1_RX", i2c: "I2C0_SCL", pwm: "PWM2_B", usb: "USB_OVCUR_DET", clock: "CLOCK_GPOUT0" }, + { pin: 34, gpio: 22, spi: "SPI0_SCK", uart: "UART1_CTS", i2c: "I2C1_SDA", pwm: "PWM3_A", usb: "USB_VBUS_DET", clock: "CLOCK_GPIN1" }, + { pin: 35, gpio: 23, spi: "SPI0_TX", uart: "UART1_RTS", i2c: "I2C1_SCL", pwm: "PWM3_B", usb: "USB_VBUS_EN", clock: "CLOCK_GPOUT1" }, + { pin: 36, gpio: 24, spi: "SPI1_RX", uart: "UART1_TX", i2c: "I2C0_SDA", pwm: "PWM4_A", usb: "USB_OVCUR_DET", clock: "CLOCK_GPOUT2" }, + { pin: 37, gpio: 25, spi: "SPI1_CSn", uart: "UART1_RX", i2c: "I2C0_SCL", pwm: "PWM4_B", usb: "USB_VBUS_DET", clock: "CLOCK_GPOUT3" }, + { pin: 38, gpio: 26, spi: "SPI1_SCK", uart: "UART1_CTS", i2c: "I2C1_SDA", pwm: "PWM5_A", usb: "USB_VBUS_EN", adc: 0 }, + { pin: 39, gpio: 27, spi: "SPI1_TX", uart: "UART1_RTS", i2c: "I2C1_SCL", pwm: "PWM5_B", usb: "USB_OVCUR_DET", adc: 1 }, + { pin: 40, gpio: 28, spi: "SPI1_RX", uart: "UART0_TX", i2c: "I2C0_SDA", pwm: "PWM6_A", usb: "USB_VBUS_DET", adc: 2 }, + { pin: 41, gpio: 29, spi: "SPI1_CSn", uart: "UART0_RX", i2c: "I2C0_SCL", pwm: "PWM6_B", usb: "USB_VBUS_EN", adc: 3 }, +]; + +const gpioPads: InterfaceDef[] = GPIO_SPECS.map(rp2040Gpio); + +// --------------------------------------------------------------------------- +// QSPI pads (bank 1) — pins 51-56, dedicated to the external XIP flash. +// Each also has a software-controlled GPIO (digital_io) function per the +// ProtoPart definition. +// --------------------------------------------------------------------------- + +function qspiPad(config: { + pin: number; + name: string; + cap: string; + direction: "source" | "bidirectional"; + signalClass: "data" | "clock"; + note: string; + extraTraits?: TraitDef[]; +}): InterfaceDef { + return { + id: `pin_${config.pin}`, + name: config.name, + pin: config.pin, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input", "output", "bidirectional"] }], + capabilities: ["digital_io", config.cap], + parameters: [{ id: "voltage", unit: "V", range: IOVDD_RANGE }], + traits: [ + { + type: "rp2040_pin_functions", + params: { + source: "RP2040 Datasheet Table 1 — carried verbatim from the ProtoPart definition", + functions: [ + { + name: config.name, + direction: config.direction, + signal_class: config.signalClass, + note: config.note, + }, + { name: "digital_io", direction: "bidirectional", signal_class: "data" }, + ] satisfies Rp2040Function[], + }, + }, + { type: "power_domain", params: { domain: "IOVDD" } }, + { + type: "usage_restriction", + params: { + restriction: + "Dedicated to the external XIP flash on any booting design — the chip cannot run user code without it.", + exemption: + "Usable as a software-controlled GPIO when not required for flash access (ProtoPart pin descriptions).", + }, + }, + ...(config.extraTraits ?? []), + ], + }; +} + +const qspiPads: InterfaceDef[] = [ + qspiPad({ + pin: 51, name: "QSPI_SD3", cap: "qspi_sd3", direction: "bidirectional", signalClass: "data", + note: "External XIP flash data line 3.", + }), + qspiPad({ + pin: 52, name: "QSPI_SCLK", cap: "qspi_sclk", direction: "source", signalClass: "clock", + note: "External XIP flash clock output.", + }), + qspiPad({ + pin: 53, name: "QSPI_SD0", cap: "qspi_sd0", direction: "bidirectional", signalClass: "data", + note: "External XIP flash data line 0 (MOSI in single-bit SPI mode).", + }), + qspiPad({ + pin: 54, name: "QSPI_SD2", cap: "qspi_sd2", direction: "bidirectional", signalClass: "data", + note: "External XIP flash data line 2.", + }), + qspiPad({ + pin: 55, name: "QSPI_SD1", cap: "qspi_sd1", direction: "bidirectional", signalClass: "data", + note: "External XIP flash data line 1 (MISO in single-bit SPI mode).", + }), + qspiPad({ + pin: 56, name: "QSPI_SS_N", cap: "qspi_ss_n", direction: "source", signalClass: "data", + note: "External XIP flash chip-select (active low).", + extraTraits: [ + { type: "display_notation", params: { active_low: true, display: "QSPI_SS̅" } }, + { + type: "boot_strapping", + params: { + controls: + "USB BOOTSEL UF2 mode: pulling this pin low during reset forces the boot ROM into UF2 mass-storage mode — the standard board-level BOOTSEL mechanism (there is no dedicated USB_BOOT pin on the bare chip).", + recommended_wiring: + "Route through a momentary pushbutton to GND to support firmware loading via the USB BOOTSEL UF2 path (ProtoPart design_rules).", + source: "ProtoPart pin 56 description / design_rules / warnings", + }, + }, + ], + }), +]; + +// --------------------------------------------------------------------------- +// Power, clock, debug, and service pads — ProtoPart pins 1, 10, 19-26, 33, +// 42-50 and the exposed ground pad +// --------------------------------------------------------------------------- + +/** The six IOVDD pads (1, 10, 22, 33, 42, 49) sit on one 1.8-3.3 V rail. */ +function iovddPin(pinNo: number, ordinal: number): InterfaceDef { + const base = PowerIn({ + id: `pin_${pinNo}`, + name: "IOVDD", + pin: pinNo, + voltageV: IOVDD_RANGE, + nominalV: 3.3, + }); + return { + ...base, + capabilities: ["power_in", "iovdd"], + traits: [ + { + type: "power_domain", + params: { + domain: "IOVDD", + note: `Digital I/O ring supply (${ordinal} of 6). Sets logic level for all 30 GPIOs and the QSPI flash interface.`, + }, + }, + { + // Shareability exemption: one supply instance may legally serve all + // six pads — mixing rails on IOVDD pins is not supported. + type: "net_shareable", + params: { + net: "rp2040_iovdd", + policy: "single_supply_instance_may_serve_all_members", + members: ["pin_1", "pin_10", "pin_22", "pin_33", "pin_42", "pin_49"], + source: "ProtoPart design_rules: 'Tie all six IOVDD pins to the same supply rail (1.8 V or 3.3 V).'", + }, + }, + SUPPLY_DECOUPLING_TRAIT, + ], + }; +} + +/** The two DVDD pads (23, 50) sit on one 1.1 V core rail. */ +function dvddPin(pinNo: number, ordinal: number): InterfaceDef { + const base = PowerIn({ + id: `pin_${pinNo}`, + name: "DVDD", + pin: pinNo, + voltageV: DVDD_RANGE, + nominalV: 1.1, + }); + return { + ...base, + capabilities: ["power_in", "dvdd"], + traits: [ + { + type: "power_domain", + params: { + domain: "DVDD", + note: `1.1 V digital core supply (${ordinal} of 2). Keep within 1.05-1.16 V per Datasheet Table 634 (use 1.15 V if running clk_sys at 200 MHz).`, + }, + }, + { + type: "net_shareable", + params: { + net: "rp2040_dvdd", + policy: "single_supply_instance_may_serve_all_members", + members: ["pin_23", "pin_50"], + }, + }, + { + type: "co_requirement", + params: { + with: "pin_45", + condition: "always", + effect: + "DVDD must not be left floating: supply from VREG_VOUT (typical — route pin 45 externally to both DVDD pins with low impedance) or from an external 1.1 V regulator.", + source: "ProtoPart design_rules / warnings", + }, + }, + SUPPLY_DECOUPLING_TRAIT, + ], + }; +} + +const powerAndServicePads: InterfaceDef[] = [ + iovddPin(1, 1), + iovddPin(10, 2), + + { + id: "pin_19", + name: "TESTEN", + pin: 19, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input"] }], + capabilities: ["testen", "factory_test"], + parameters: [{ id: "voltage", unit: "V", range: IOVDD_RANGE }], + traits: [ + { + type: "rp2040_pin_functions", + params: { + source: "RP2040 Datasheet Table 1 / Table 619 — carried verbatim from the ProtoPart definition", + functions: [ + { + name: "digital_input", + direction: "sink", + signal_class: "data", + note: "TESTEN — factory test enable (IOVDD bank).", + }, + ] satisfies Rp2040Function[], + }, + }, + { + type: "internal_pulls", + params: { + available: true, + pull_down_at_reset: true, + source: "RP2040 Datasheet Table 619 (internal pull-down at reset)", + }, + }, + { + type: "usage_restriction", + params: { + restriction: + "Must be tied externally to GND in production use — a floating TESTEN can place the chip in factory test mode and prevent normal boot. Not a ground source.", + source: "ProtoPart pin 19 description / design_rules / warnings", + }, + }, + ], + }, + + { + id: "pin_20", + name: "XIN", + pin: 20, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "clock", roles: ["input"] }], + capabilities: ["xtal_in"], + parameters: [ + // ProtoPart pin 20: "USB bootloader requires 12 MHz." + { id: "clock_freq", unit: "Hz", value: 12_000_000 }, + ], + traits: [ + { + type: "rp2040_pin_functions", + params: { + source: "RP2040 Datasheet Table 1 / §2.16 — carried verbatim from the ProtoPart definition", + functions: [ + { + name: "XIN", + direction: "sink", + signal_class: "clock", + note: "Crystal oscillator input. Connect a 12 MHz crystal between XIN/XOUT with load capacitors, or drive XIN with a single-ended CMOS clock (XOUT disconnected).", + }, + { name: "digital_input", direction: "sink", signal_class: "clock" }, + ] satisfies Rp2040Function[], + }, + }, + { type: "power_domain", params: { domain: "IOVDD" } }, + ], + }, + { + id: "pin_21", + name: "XOUT", + pin: 21, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "clock", roles: ["output"] }], + capabilities: ["xtal_out"], + traits: [ + { + type: "rp2040_pin_functions", + params: { + source: "RP2040 Datasheet Table 1 / §2.16 — carried verbatim from the ProtoPart definition", + functions: [ + { + name: "XOUT", + direction: "source", + signal_class: "clock", + note: "Crystal oscillator output. Connect to the 12 MHz crystal opposite XIN; leave open when XIN is driven by an external clock.", + }, + { name: "digital_output", direction: "source", signal_class: "clock" }, + ] satisfies Rp2040Function[], + }, + }, + { type: "power_domain", params: { domain: "IOVDD" } }, + ], + }, + + iovddPin(22, 3), + dvddPin(23, 1), + + { + id: "pin_24", + name: "SWCLK", + pin: 24, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input"] }], + capabilities: ["swd_clk"], + parameters: [{ id: "voltage", unit: "V", range: IOVDD_RANGE }], + traits: [ + { + type: "rp2040_pin_functions", + params: { + source: "RP2040 Datasheet Table 1 / Table 342 — carried verbatim from the ProtoPart definition", + functions: [ + { + name: "SWCLK", + direction: "sink", + signal_class: "clock", + note: "Serial Wire Debug clock input (Digital In FT, IOVDD bank). Multi-drop SWD bus access to both M0+ cores; also used to download code.", + }, + { name: "digital_input", direction: "sink", signal_class: "clock" }, + ] satisfies Rp2040Function[], + }, + }, + { + type: "internal_pulls", + params: { + available: true, + pull_up_at_reset: true, + source: "RP2040 Datasheet Table 342, PADS_BANK0:SWCLK (PUE=1/PDE=0)", + }, + }, + { type: "power_domain", params: { domain: "IOVDD" } }, + ], + }, + { + id: "pin_25", + name: "SWDIO", + pin: 25, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input", "output", "bidirectional"] }], + capabilities: ["swd_io"], + parameters: [{ id: "voltage", unit: "V", range: IOVDD_RANGE }], + traits: [ + { + type: "rp2040_pin_functions", + params: { + source: "RP2040 Datasheet Table 1 — carried verbatim from the ProtoPart definition", + functions: [ + { + name: "SWDIO", + direction: "bidirectional", + signal_class: "data", + note: "Serial Wire Debug bidirectional data. Pair with SWCLK for SWD debug to both cores.", + }, + { name: "digital_io", direction: "bidirectional", signal_class: "data" }, + ] satisfies Rp2040Function[], + }, + }, + { + type: "internal_pulls", + params: { available: true, pull_up_at_reset: true, source: "ProtoPart pin 25 description ('has internal pull-up')" }, + }, + { type: "power_domain", params: { domain: "IOVDD" } }, + ], + }, + { + id: "pin_26", + name: "RUN", + pin: 26, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input"] }], + capabilities: ["run_reset", "reset_input"], + parameters: [{ id: "voltage", unit: "V", range: IOVDD_RANGE }], + traits: [ + { + type: "rp2040_pin_functions", + params: { + source: "RP2040 Datasheet Table 1 — carried verbatim from the ProtoPart definition", + functions: [ + { + name: "RESET", + direction: "sink", + signal_class: "data", + note: "Global asynchronous reset. Reset when driven low, run when driven high.", + }, + ] satisfies Rp2040Function[], + }, + }, + { + type: "internal_pulls", + params: { + available: true, + pull_up_at_reset: true, + note: "Has internal pull-up; if no external reset is required, tie directly to IOVDD.", + source: "ProtoPart pin 26 description", + }, + }, + { type: "power_domain", params: { domain: "IOVDD" } }, + ], + }, + + iovddPin(33, 4), + iovddPin(42, 5), + + { + ...PowerIn({ + id: "pin_43", + name: "ADC_AVDD", + pin: 43, + voltageV: ADC_AVDD_RANGE, + nominalV: 3.3, + }), + capabilities: ["power_in", "adc_avdd", "analog_supply"], + traits: [ + { + type: "power_domain", + params: { + domain: "ADC_AVDD", + note: "Analog supply / reference for the 12-bit ADC. 1.62-3.63 V (typ 3.3 V) per Table 634; ADC performance is compromised below 2.97 V (§2.9.5).", + }, + }, + { + type: "implied_passives", + params: { + purpose: "ADC supply filtering for best ENOB", + components: [ + { kind: "ferrite_bead", value: "from the main 3V3 rail", connection: "3V3 to ADC_AVDD" }, + { kind: "capacitor", value: "1 µF", connection: "ADC_AVDD to GND" }, + { kind: "capacitor", value: "100 nF", connection: "ADC_AVDD to GND" }, + ], + source: "ProtoPart design_rules / adc_avdd power-domain description", + }, + }, + ], + }, + + { + ...PowerIn({ + id: "pin_44", + name: "VREG_VIN", + pin: 44, + voltageV: VREG_VIN_RANGE, + nominalV: 3.3, + }), + capabilities: ["power_in", "vreg_vin"], + traits: [ + { + type: "power_domain", + params: { + domain: "VREG_VIN", + note: "Power input for the internal core voltage regulator (nominal 1.8-3.3 V per Datasheet §1.4.2).", + }, + }, + SUPPLY_DECOUPLING_TRAIT, + ], + bridgesTo: ["pin_45"], + }, + { + ...PowerOut({ + id: "pin_45", + name: "VREG_VOUT", + pin: 45, + voltageV: 1.1, + maxCurrentA: 0.1, + }), + capabilities: ["power_out", "vreg_vout"], + traits: [ + { + type: "internal_regulator", + params: { + description: + "Output of the internal core voltage regulator: nominal 1.1 V at up to 100 mA (Datasheet §1.4.2). Externally route to both DVDD pins (23, 50).", + source: "ProtoPart pin 45 description / ldo interface", + }, + }, + { + type: "co_requirement", + params: { + with: "pin_23, pin_50", + condition: "internal core LDO in use (typical)", + effect: + "Keep VREG_VOUT decoupling close to pin 45 and route to both DVDD pins with low impedance.", + source: "ProtoPart design_rules", + }, + }, + ], + bridgesTo: ["pin_23", "pin_50"], + }, + + { + id: "pin_46", + name: "USB_DM", + pin: 46, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "usb", roles: ["data_minus"] }], + capabilities: ["usb_dm"], + traits: [ + { + type: "rp2040_pin_functions", + params: { + source: "RP2040 Datasheet Table 1 / §1.4.2 — carried verbatim from the ProtoPart definition", + functions: [ + { + name: "usb_dm", + direction: "bidirectional", + signal_class: "data", + note: "USB 1.1 differential data minus.", + }, + ] satisfies Rp2040Function[], + }, + }, + { type: "power_domain", params: { domain: "USB_VDD" } }, + { + type: "internal_pulls", + params: { + available: true, + note: "USB bus pull-ups/pull-downs are provided internally.", + source: "ProtoPart pin 46 description", + }, + }, + { + type: "implied_passives", + params: { + purpose: "USB series termination", + components: [{ kind: "resistor", value: "27 Ω", connection: "in series with USB_DM" }], + source: "ProtoPart pin 46 description / design_rules", + }, + }, + { + type: "layout_requirement", + params: { + note: "Route USB_DP/USB_DM as a 90 Ω differential pair (Hardware Design with RP2040 §2.4.1; the 27 Ω series terminations are from Datasheet §1.4.2).", + source: "ProtoPart design_rules", + }, + }, + ], + }, + { + id: "pin_47", + name: "USB_DP", + pin: 47, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "usb", roles: ["data_plus"] }], + capabilities: ["usb_dp"], + traits: [ + { + type: "rp2040_pin_functions", + params: { + source: "RP2040 Datasheet Table 1 / §1.4.2 — carried verbatim from the ProtoPart definition", + functions: [ + { + name: "usb_dp", + direction: "bidirectional", + signal_class: "data", + note: "USB 1.1 differential data plus.", + }, + ] satisfies Rp2040Function[], + }, + }, + { type: "power_domain", params: { domain: "USB_VDD" } }, + { + type: "internal_pulls", + params: { + available: true, + note: "USB bus pull-ups/pull-downs are provided internally.", + source: "ProtoPart pin 47 description", + }, + }, + { + type: "implied_passives", + params: { + purpose: "USB series termination", + components: [{ kind: "resistor", value: "27 Ω", connection: "in series with USB_DP" }], + source: "ProtoPart pin 47 description / design_rules", + }, + }, + { + type: "layout_requirement", + params: { + note: "Route USB_DP/USB_DM as a 90 Ω differential pair (Hardware Design with RP2040 §2.4.1; the 27 Ω series terminations are from Datasheet §1.4.2).", + source: "ProtoPart design_rules", + }, + }, + ], + }, + + { + ...PowerIn({ + id: "pin_48", + name: "USB_VDD", + pin: 48, + voltageV: USB_VDD_RANGE, + nominalV: 3.3, + }), + capabilities: ["power_in", "usb_vdd"], + traits: [ + { + type: "power_domain", + params: { + domain: "USB_VDD", + note: "Dedicated 3.3 V supply for the integrated USB 1.1 PHY. Decouple separately from IOVDD; supply from the same 3.3 V rail with its own decoupling.", + }, + }, + { + type: "co_requirement", + params: { + with: "usb_device, usb_host", + condition: "always (even if USB is unused)", + effect: + "USB_VDD must be supplied even if USB is unused; otherwise the USB PHY draws leakage current that may keep the chip from entering low-power states cleanly.", + source: "ProtoPart warnings", + }, + }, + SUPPLY_DECOUPLING_TRAIT, + ], + }, + + iovddPin(49, 6), + dvddPin(50, 2), + + { + ...Ground({ id: "pad_gnd", name: "GND (Exposed Pad)", pin: 57, maxCurrentA: 1.0 }), + traits: [ + { + type: "assembly_requirement", + params: { + note: "Exposed thermal pad on the QFN-56 underside — the single external ground connection (Datasheet §1.4.2), bonded to all internal ground pads on the die. Solder to the PCB ground plane and stitch with thermal vias for heat dissipation and ground reference integrity; leaving it floating violates the ground reference, increases noise, and degrades thermal performance.", + source: "ProtoPart pad_gnd description / design_rules / warnings", + }, + }, + { + type: "net_shareable", + params: { net: "gnd", policy: "single_ground_instance_may_serve_all_members" }, + }, + { + type: "pin_designator_note", + params: { + note: "The ProtoPart resource id is 'pad_gnd' (no perimeter pin number); 57 is the CAD-footprint designator for the exposed pad.", + }, + }, + ], + }, +]; + +/** All 57 pads, sorted into physical package order — schematic-honest. */ +const pins: InterfaceDef[] = [...powerAndServicePads, ...gpioPads, ...qspiPads].sort( + (a, b) => Number(a.pin ?? 0) - Number(b.pin ?? 0), +); + +// --------------------------------------------------------------------------- +// Peripheral controllers +// --------------------------------------------------------------------------- + +function withTraits(iface: InterfaceDef, traits: TraitDef[]): InterfaceDef { + return { ...iface, traits: [...(iface.traits ?? []), ...traits] }; +} + +/** Attach traits to the interface with the given id inside a builder result. */ +function amend(ifaces: InterfaceDef[], id: string, traits: TraitDef[]): InterfaceDef[] { + return ifaces.map((i) => (i.id === id ? withTraits(i, traits) : i)); +} + +function composed(config: { + id: string; + name: string; + protocolType: string; + roles: string[]; + slots: SlotDef[]; + profiles?: InterfaceDef["profiles"]; + parameters?: Parameter[]; + maxInstances?: number; + defaultActive?: boolean; + traits?: TraitDef[]; + domain?: InterfaceDef["domain"]; +}): InterfaceDef { + return { + id: config.id, + name: config.name, + domain: config.domain ?? "electrical", + exposed: true, + default_active: config.defaultActive ?? false, + protocols: [{ type: config.protocolType, roles: config.roles }], + slots: config.slots, + ...(config.profiles ? { profiles: config.profiles } : {}), + ...(config.parameters ? { parameters: config.parameters } : {}), + ...(config.maxInstances !== undefined ? { max_instances: config.maxInstances } : {}), + ...(config.traits ? { traits: config.traits } : {}), + }; +} + +// ADC — 4 user channels on GPIO26-29 (ProtoPart `adc_in` interface). +const adc = composed({ + id: "adc_in", + name: "ADC", + protocolType: "analog", + roles: ["input"], + parameters: [resolutionBits(12)], + slots: [ + { id: "channel", required: true, count: 4, match: { protocol: "analog", role: "input", capability: "analog_in" } }, + ], + profiles: [ + { + id: "adc_channels", + label: "ADC0-ADC3 (GPIO26-29, pins 38-41)", + bindings: { channel: ["pin_38", "pin_39", "pin_40", "pin_41"] }, + }, + ], + maxInstances: 1, + traits: [ + { + type: "channels", + params: { + count: 4, + mapping: "ADC0=GPIO26 (pin 38), ADC1=GPIO27 (pin 39), ADC2=GPIO28 (pin 40), ADC3=GPIO29 (pin 41)", + note: "12-bit successive-approximation ADC; an internal temperature sensor occupies a further non-pinned input (ProtoPart usage_notes).", + }, + }, + { + type: "performance_note", + params: { + note: "Effective ENOB ~9 bits without filtering; ADC performance is compromised below ADC_AVDD = 2.97 V (Datasheet §2.9.5).", + source: "ProtoPart usage_notes / adc_avdd power-domain description", + }, + }, + { + type: "co_requirement", + params: { + with: "pin_43", + condition: "always", + effect: + "ADC_AVDD is the analog supply and conversion reference; the voltage on ADC inputs must not exceed IOVDD (leakage through ESD protection diodes above IOVDD — Datasheet §2.9.5 note). Filter from the main 3V3 with a ferrite bead + 1 µF + 100 nF for best ENOB.", + source: "ProtoPart warnings / design_rules (input limit corrected to the datasheet's IOVDD bound, §2.9.5)", + }, + }, + ], +}); + +// SPI — two PL022 controllers; each signal is FUNCSEL-fixed (F1) to specific +// pin groups (no matrix routing). Datasheet names TX/RX/SCK/CSn map onto the +// builder's mosi/miso/sck/ss slots. +const SPI_TRAITS = (n: 0 | 1, groups: string, partial?: string): TraitDef[] => [ + { + type: "controller_ip", + params: { ip: "Arm PL022 SSP", source: "RP2040 Datasheet §4.4; ProtoPart spi interface description" }, + }, + { + type: "signal_naming", + params: { + note: `Datasheet signals are SPI${n}_TX / SPI${n}_RX / SPI${n}_SCK / SPI${n}_CSn; TX binds the mosi slot, RX the miso slot, CSn the ss slot.`, + }, + }, + { + type: "instance_combinations", + params: { + instances_in_silicon: 1, + routing: "gpio_funcsel_fixed", + combination_space: groups, + ...(partial ? { partial_groups: partial } : {}), + source: "RP2040 Datasheet §1.4.3 Table 2 (F1 column); ProtoPart per-pin function lists", + }, + }, + { + type: "protopart_constraints", + params: { requires_matching_voltage_domain: true, max_lane_rate_mbps: 62 }, + }, +]; + +const spi0 = amend( + SPI({ + id: "spi_0", + name: "SPI 0", + roles: ["master"], // ProtoPart models the PL022 interfaces in master role + clockFreqHz: [0, 62_500_000], // "Up to ~62.5 Mbps (sysclk/2)" + maxInstances: 1, + profiles: [ + { id: "spi0_gpio0", label: "SPI0 on GPIO0-3 (pins 2-5)", miso: "pin_2", ss: "pin_3", sck: "pin_4", mosi: "pin_5" }, + { id: "spi0_gpio4", label: "SPI0 on GPIO4-7 (pins 6-9)", miso: "pin_6", ss: "pin_7", sck: "pin_8", mosi: "pin_9" }, + { id: "spi0_gpio16", label: "SPI0 on GPIO16-19 (pins 27-30)", miso: "pin_27", ss: "pin_28", sck: "pin_29", mosi: "pin_30" }, + { id: "spi0_gpio20", label: "SPI0 on GPIO20-23 (pins 31-35)", miso: "pin_31", ss: "pin_32", sck: "pin_34", mosi: "pin_35" }, + ], + }), + "spi_0", + SPI_TRAITS(0, "Four fixed pin groups: GPIO0-3, GPIO4-7, GPIO16-19, GPIO20-23. Signals do not mix across groups arbitrarily — each GPIO carries exactly one fixed SPI0 signal."), +); + +const spi1 = amend( + SPI({ + id: "spi_1", + name: "SPI 1", + roles: ["master"], + clockFreqHz: [0, 62_500_000], + maxInstances: 1, + profiles: [ + { id: "spi1_gpio8", label: "SPI1 on GPIO8-11 (pins 11-14)", miso: "pin_11", ss: "pin_12", sck: "pin_13", mosi: "pin_14" }, + { id: "spi1_gpio12", label: "SPI1 on GPIO12-15 (pins 15-18)", miso: "pin_15", ss: "pin_16", sck: "pin_17", mosi: "pin_18" }, + { id: "spi1_gpio24", label: "SPI1 on GPIO24-27 (pins 36-39)", miso: "pin_36", ss: "pin_37", sck: "pin_38", mosi: "pin_39" }, + ], + }), + "spi_1", + SPI_TRAITS( + 1, + "Three complete fixed pin groups: GPIO8-11, GPIO12-15, GPIO24-27.", + "GPIO28/GPIO29 (pins 40/41) carry only SPI1_RX / SPI1_CSn — not a complete bus by themselves.", + ), +); + +// UART — two PL011 controllers; TX/RX on F2 with optional CTS/RTS on the +// adjacent F2 pins of the same group. +const UART_TRAITS = (n: 0 | 1, groups: string, partial?: string): TraitDef[] => [ + { + type: "controller_ip", + params: { ip: "Arm PL011", source: "RP2040 Datasheet §4.2; ProtoPart uart interface description" }, + }, + { + type: "instance_combinations", + params: { + instances_in_silicon: 1, + routing: "gpio_funcsel_fixed", + combination_space: groups, + ...(partial ? { partial_groups: partial } : {}), + source: "RP2040 Datasheet §1.4.3 Table 2 (F2 column); ProtoPart per-pin function lists", + }, + }, + { + type: "flow_control", + params: { + note: `Optional hardware flow control via UART${n}_CTS / UART${n}_RTS on the corresponding F2 pins.`, + source: "ProtoPart uart interface description", + }, + }, +]; + +const uart0 = amend( + UART({ + id: "uart_0", + name: "UART 0", + maxInstances: 1, + profiles: [ + { id: "uart0_gpio0", label: "UART0 on GPIO0-3 (pins 2-5)", tx: "pin_2", rx: "pin_3", cts: "pin_4", rts: "pin_5" }, + { id: "uart0_gpio12", label: "UART0 on GPIO12-15 (pins 15-18)", tx: "pin_15", rx: "pin_16", cts: "pin_17", rts: "pin_18" }, + { id: "uart0_gpio16", label: "UART0 on GPIO16-19 (pins 27-30)", tx: "pin_27", rx: "pin_28", cts: "pin_29", rts: "pin_30" }, + { id: "uart0_gpio28", label: "UART0 on GPIO28/29 (pins 40/41, no flow control)", tx: "pin_40", rx: "pin_41" }, + ], + }), + "uart_0", + UART_TRAITS( + 0, + "Four fixed TX/RX groups: GPIO0/1, GPIO12/13, GPIO16/17, GPIO28/29 (CTS/RTS on GPIO2/3, GPIO14/15, GPIO18/19).", + "The GPIO28/29 group has no CTS/RTS pins.", + ), +); + +const uart1 = amend( + UART({ + id: "uart_1", + name: "UART 1", + maxInstances: 1, + profiles: [ + { id: "uart1_gpio4", label: "UART1 on GPIO4-7 (pins 6-9)", tx: "pin_6", rx: "pin_7", cts: "pin_8", rts: "pin_9" }, + { id: "uart1_gpio8", label: "UART1 on GPIO8-11 (pins 11-14)", tx: "pin_11", rx: "pin_12", cts: "pin_13", rts: "pin_14" }, + { id: "uart1_gpio20", label: "UART1 on GPIO20-23 (pins 31-35)", tx: "pin_31", rx: "pin_32", cts: "pin_34", rts: "pin_35" }, + { id: "uart1_gpio24", label: "UART1 on GPIO24-27 (pins 36-39)", tx: "pin_36", rx: "pin_37", cts: "pin_38", rts: "pin_39" }, + ], + }), + "uart_1", + UART_TRAITS(1, "Four fixed TX/RX groups: GPIO4/5, GPIO8/9, GPIO20/21, GPIO24/25 (CTS/RTS on GPIO6/7, GPIO10/11, GPIO22/23, GPIO26/27)."), +); + +// I2C — two DW_apb_i2c controllers. The ProtoPart definition models master +// and slave as separate interfaces per controller; they are merged here into +// one interface per controller with both roles (mirroring the gold-standard +// convention). SDA sits on even GPIOs, SCL on odd, alternating controllers +// every two GPIOs (F3). +const I2C_TRAITS = (n: 0 | 1, pairs: string): TraitDef[] => [ + { type: "display_notation", params: { latex: "I^{2}C" } }, + { + type: "controller_ip", + params: { + ip: "Synopsys DW_apb_i2c", + modes: ["standard-mode", "fast-mode", "fast-mode plus"], + source: "RP2040 Datasheet §4.3; ProtoPart i2c interface descriptions", + }, + }, + { + type: "instance_combinations", + params: { + instances_in_silicon: 1, + routing: "gpio_funcsel_fixed", + combination_space: pairs, + source: "RP2040 Datasheet §1.4.3 Table 2 (F3 column); ProtoPart per-pin function lists", + }, + }, + { + type: "role_note", + params: { + note: `The ProtoPart definition models I2C ${n} master and slave as separate interfaces (i2c_${n}_master / i2c_${n}_slave); both are roles of the same DW_apb_i2c controller.`, + }, + }, + { type: "protopart_constraints", params: { requires_matching_voltage_domain: true } }, + { + type: "implied_passives", + params: { + purpose: "Open-drain bus pull-ups", + components: [{ kind: "resistor", value: "bus-speed dependent", connection: "SDA and SCL to the bus supply" }], + source: "I²C bus specification (open-drain bus) — not stated in the ProtoPart definition", + }, + }, +]; + +const i2c0 = amend( + I2C({ + id: "i2c_0", + name: "I2C 0", + roles: ["master", "slave"], + clockFreqHz: [100_000, 1_000_000], // standard / fast / fast-mode plus (mode names per ProtoPart; rates are the I2C-spec definitions of those modes) + maxInstances: 1, + profiles: [ + { id: "i2c0_gpio0", label: "I2C0 on GPIO0/1 (pins 2/3)", sda: "pin_2", scl: "pin_3" }, + { id: "i2c0_gpio4", label: "I2C0 on GPIO4/5 (pins 6/7)", sda: "pin_6", scl: "pin_7" }, + { id: "i2c0_gpio8", label: "I2C0 on GPIO8/9 (pins 11/12)", sda: "pin_11", scl: "pin_12" }, + { id: "i2c0_gpio12", label: "I2C0 on GPIO12/13 (pins 15/16)", sda: "pin_15", scl: "pin_16" }, + { id: "i2c0_gpio16", label: "I2C0 on GPIO16/17 (pins 27/28)", sda: "pin_27", scl: "pin_28" }, + { id: "i2c0_gpio20", label: "I2C0 on GPIO20/21 (pins 31/32)", sda: "pin_31", scl: "pin_32" }, + { id: "i2c0_gpio24", label: "I2C0 on GPIO24/25 (pins 36/37)", sda: "pin_36", scl: "pin_37" }, + { id: "i2c0_gpio28", label: "I2C0 on GPIO28/29 (pins 40/41)", sda: "pin_40", scl: "pin_41" }, + ], + }), + "i2c_0", + I2C_TRAITS(0, "Eight fixed SDA/SCL pairs: GPIO0/1, 4/5, 8/9, 12/13, 16/17, 20/21, 24/25, 28/29."), +); + +const i2c1 = amend( + I2C({ + id: "i2c_1", + name: "I2C 1", + roles: ["master", "slave"], + clockFreqHz: [100_000, 1_000_000], + maxInstances: 1, + profiles: [ + { id: "i2c1_gpio2", label: "I2C1 on GPIO2/3 (pins 4/5)", sda: "pin_4", scl: "pin_5" }, + { id: "i2c1_gpio6", label: "I2C1 on GPIO6/7 (pins 8/9)", sda: "pin_8", scl: "pin_9" }, + { id: "i2c1_gpio10", label: "I2C1 on GPIO10/11 (pins 13/14)", sda: "pin_13", scl: "pin_14" }, + { id: "i2c1_gpio14", label: "I2C1 on GPIO14/15 (pins 17/18)", sda: "pin_17", scl: "pin_18" }, + { id: "i2c1_gpio18", label: "I2C1 on GPIO18/19 (pins 29/30)", sda: "pin_29", scl: "pin_30" }, + { id: "i2c1_gpio22", label: "I2C1 on GPIO22/23 (pins 34/35)", sda: "pin_34", scl: "pin_35" }, + { id: "i2c1_gpio26", label: "I2C1 on GPIO26/27 (pins 38/39)", sda: "pin_38", scl: "pin_39" }, + ], + }), + "i2c_1", + I2C_TRAITS(1, "Seven fixed SDA/SCL pairs: GPIO2/3, 6/7, 10/11, 14/15, 18/19, 22/23, 26/27."), +); + +// PWM — one block: 8 slices × 2 channels (A/B) = 16 channels, every bank-0 +// GPIO carrying exactly one fixed channel via F4. +const PWM_CHANNEL_TAGS = [ + "pwm0_a", "pwm0_b", "pwm1_a", "pwm1_b", "pwm2_a", "pwm2_b", "pwm3_a", "pwm3_b", + "pwm4_a", "pwm4_b", "pwm5_a", "pwm5_b", "pwm6_a", "pwm6_b", "pwm7_a", "pwm7_b", +]; + +const pwm = composed({ + id: "pwm", + name: "PWM", + protocolType: "pwm", + roles: ["output"], + slots: PWM_CHANNEL_TAGS.map((tag): SlotDef => ({ + id: tag, + required: false, + match: { protocol: "pwm", role: "output", capability: tag }, + })), + maxInstances: 1, + traits: [ + { + type: "channels", + params: { + count: 16, + detail: + "16 channels arranged as 8 slices × 2 channels (A/B). Each slice has an independent 16-bit counter with an 8.4 fractional clock divider (8 integer + 4 fractional bits, Datasheet §4.5.1 / DIV register); channels A and B share the counter but have independent compare registers.", + mapping: + "Slice = floor(GPIO/2) mod 8; channel A on even GPIOs, B on odd. GPIO16-29 repeat slices 0-6, so PWM0_A appears on GPIO0 and GPIO16, etc.; PWM7_A/B appear only on GPIO14/15.", + source: "ProtoPart pwm interface description / RP2040 Datasheet §4.5", + }, + }, + { + type: "measurement_input", + params: { + note: "The B channel can also serve as a frequency / duty-cycle measurement input.", + source: "ProtoPart pwm interface description", + }, + }, + { + type: "instance_combinations", + params: { + instances_in_silicon: 1, + routing: "gpio_funcsel_fixed", + combination_space: + "Any bank-0 GPIO via F4; the per-pin PWMn_A/PWMn_B capability tag identifies which slice/channel that pin carries.", + }, + }, + { + type: "application_note", + params: { + note: "8.4 fractional clock dividers (Datasheet §4.5.1) easily cover 22-25 kHz / 12-bit duty applications.", + source: "ProtoPart usage_notes (divider width corrected to the datasheet's 8.4 fractional divider)", + }, + }, + ], +}); + +// PIO — two programmable-I/O blocks, four state machines each (eight user +// state machines total). Any bank-0 GPIO is reachable — the RP2040's +// matrix-analogue combination space. +function pio(n: 0 | 1): InterfaceDef { + return composed({ + id: `pio${n}`, + name: `PIO${n} (Programmable I/O Block ${n})`, + protocolType: "pio", + roles: ["input", "output"], + slots: [ + { id: "gpio", required: false, count: 30, match: { capability: `pio${n}` } }, + ], + maxInstances: 4, // state machines in this block + traits: [ + { + type: "channels", + params: { + count: 4, + detail: "Four user-programmable I/O state machines per block; eight total across PIO0/PIO1.", + source: "ProtoPart metadata description / usage_notes", + }, + }, + { + type: "instance_combinations", + params: { + instances_in_silicon: 4, + routing: "gpio_funcsel_fixed", + combination_space: `Any bank-0 GPIO (GPIO0-29) via funcsel F${6 + n}; QSPI-bank pads are not reachable.`, + }, + }, + { + type: "application_note", + params: { + note: "Especially good for protocols not natively supported: I2S, SK6812 / WS2812 LED data, parallel pixel buses (CSI/DPI capture), software USB host.", + source: "ProtoPart usage_notes / application_examples", + }, + }, + ], + }); +} + +// WS2812 / NeoPixel driver — a PIO application the ProtoPart definition +// promotes to a first-class interface (datasheet §3.6.2). +const ws2812 = composed({ + id: "ws2812", + name: "NeoPixel / WS2812B LED Control", + protocolType: "digital", + roles: ["transmitter"], + parameters: [ + { id: "bit_rate", name: "NRZ wire bit rate", unit: "Hz", value: 800_000 }, + ], + slots: [ + // The `pio0` tag spans exactly the 30 bank-0 GPIOs — the honest pin space + // (the program can run on either PIO block; QSPI pads are unreachable). + { id: "data", required: true, match: { protocol: "digital", role: "output", capability: "pio0" } }, + ], + maxInstances: 8, // bounded by the eight PIO state machines + traits: [ + { + type: "implemented_by", + params: { + note: "WS2812 / NeoPixel-family LED data line driver implemented via a PIO state machine (Datasheet §3.6.2 'WS2812 LEDs'); bit timing is held in the PIO program. One bank-0 GPIO drives a daisy-chain of LEDs using the canonical ~800 kbit/s NRZ wire protocol.", + consumes: "one PIO state machine per instance", + source: "ProtoPart ws2812 interface description", + }, + }, + { + type: "instance_combinations", + params: { + instances_in_silicon: 8, + routing: "gpio_funcsel_fixed", + combination_space: "Any GPIO0-29 can serve as the data pin.", + }, + }, + ], +}); + +// QSPI flash bus — the XIP boot path. Dedicated bank-1 pads; mandatory for +// running user code. +const qspiFlash = composed({ + id: "qspi_flash", + name: "Quad-SPI (XIP Flash Bus)", + protocolType: "spi", + roles: ["master"], + parameters: [ + { id: "max_data_rate", name: "Max data rate (QSPI quad-SDR)", unit: "bit/s", value: 133_000_000 }, + ], + slots: [ + { id: "sclk", required: true, match: { protocol: "spi", role: "clock", capability: "qspi_sclk" } }, + { id: "ss_n", required: true, match: { protocol: "spi", role: "select", capability: "qspi_ss_n" } }, + { id: "sd0", required: true, match: { capability: "qspi_sd0" } }, + { id: "sd1", required: true, match: { capability: "qspi_sd1" } }, + { id: "sd2", required: true, match: { capability: "qspi_sd2" } }, + { id: "sd3", required: true, match: { capability: "qspi_sd3" } }, + ], + profiles: [ + { + id: "qspi_flash_fixed", + label: "Dedicated QSPI pads (pins 51-56)", + default_active: true, + bindings: { + sclk: "pin_52", + ss_n: "pin_56", + sd0: "pin_53", + sd1: "pin_55", + sd2: "pin_54", + sd3: "pin_51", + }, + }, + ], + maxInstances: 1, + defaultActive: true, + traits: [ + { + type: "instance_combinations", + params: { + instances_in_silicon: 1, + routing: "dedicated_pads", + combination_space: "Exactly one pin set — the QSPI flash bus is not muxed onto bank-0 GPIOs.", + }, + }, + { + type: "co_requirement", + params: { + with: "external QSPI flash", + condition: "always", + effect: + "External QSPI flash is mandatory — the RP2040 has no internal flash and cannot boot or run user code without it (typically 2-16 MB). Use a flash compatible with the chip's XIP boot ROM (typically Winbond W25Q-series).", + source: "ProtoPart qspi_flash description / design_rules / warnings", + }, + }, + { type: "protopart_constraints", params: { requires_matching_voltage_domain: true } }, + ], +}); + +// USB 1.1 PHY — one PHY, two mutually exclusive modes. The ProtoPart +// definition models device and host as separate interfaces; the +// `usb_phy_mode` interface group below enforces one-of. +function usbMode(mode: "device" | "host", traits: TraitDef[]): InterfaceDef { + return composed({ + id: `usb_${mode}`, + name: mode === "device" ? "USB 1.1 Device" : "USB 1.1 Host", + protocolType: "usb", + roles: [mode], + slots: [ + { id: "dp", required: true, match: { protocol: "usb", role: "data_plus", capability: "usb_dp" } }, + { id: "dm", required: true, match: { protocol: "usb", role: "data_minus", capability: "usb_dm" } }, + ], + profiles: [ + { + id: `usb_${mode}_pins`, + label: "USB_DP / USB_DM (pins 47/46)", + bindings: { dp: "pin_47", dm: "pin_46" }, + }, + ], + maxInstances: 1, + traits: [ + { + type: "co_requirement", + params: { + with: "pin_48", + condition: "always", + effect: "The USB PHY is powered from the dedicated USB_VDD rail (3.135-3.63 V).", + }, + }, + { + type: "co_requirement", + params: { + with: mode === "device" ? "usb_host" : "usb_device", + condition: `usb_${mode === "device" ? "host" : "device"} active`, + effect: "One PHY: device and host modes are mutually exclusive (see the usb_phy_mode interface group).", + }, + }, + ...traits, + ], + }); +} + +const usbDevice = usbMode("device", [ + { + type: "boot_rom_role", + params: { + note: "In device mode used for the BOOTSEL UF2 mass-storage interface and user firmware CDC/HID.", + source: "ProtoPart usb_device description", + }, + }, +]); + +const usbHost = usbMode("host", [ + { + type: "operating_modes", + params: { + modes: ["full-speed", "low-speed"], + note: "USB 1.1 host mode supports Full Speed and Low Speed devices (software stack required).", + source: "RP2040 Datasheet Table 1 / §1.2 ('Full Speed device and Full/Low Speed host') — corrects the ProtoPart usb_host description's 'full-speed only'", + }, + }, +]); + +// USB VBUS management — F9 mux signals, each available on ten fixed GPIOs. +function usbMgmt(config: { + id: string; + name: string; + cap: string; + role: "input" | "output"; + gpios: string; + description: string; +}): InterfaceDef { + return composed({ + id: config.id, + name: config.name, + protocolType: "digital", + roles: [config.role], + slots: [{ id: "signal", required: true, match: { capability: config.cap } }], + maxInstances: 1, + traits: [ + { + type: "instance_combinations", + params: { + instances_in_silicon: 1, + routing: "gpio_funcsel_fixed", + combination_space: config.gpios, + source: "RP2040 Datasheet §1.4.3 Table 2 (F9 column); ProtoPart interface description", + }, + }, + { type: "description", params: { note: config.description } }, + ], + }); +} + +const usbVbusDetect = usbMgmt({ + id: "usb_vbus_detect", + name: "USB VBUS Detect", + cap: "usb_vbus_det", + role: "input", + gpios: "Available on GPIO1, 4, 7, 10, 13, 16, 19, 22, 25, 28 per Datasheet Table 2.", + description: + "USB VBUS detection input via GPIO F9 mux (USB_VBUS_DET). Read this signal to know when the USB host is providing bus power.", +}); + +const usbVbusEnable = usbMgmt({ + id: "usb_vbus_enable", + name: "USB VBUS Enable", + cap: "usb_vbus_en", + role: "output", + gpios: "Available on GPIO2, 5, 8, 11, 14, 17, 20, 23, 26, 29 per Datasheet Table 2.", + description: + "USB VBUS enable output via GPIO F9 mux (USB_VBUS_EN). Drives an external USB host power switch (e.g. TPS2051) in host mode.", +}); + +const usbOvercurrentDetect = usbMgmt({ + id: "usb_overcurrent_detect", + name: "USB Over-current Detect", + cap: "usb_ovcur_det", + role: "input", + gpios: "Available on GPIO0, 3, 6, 9, 12, 15, 18, 21, 24, 27 per Datasheet Table 2.", + description: + "USB over-current detection input via GPIO F9 mux (USB_OVCUR_DET). Reads the active-low overcurrent flag from an external USB power switch in host mode.", +}); + +// General-purpose clock I/O — F8 mux: two GPIN slots, four GPOUT slots, each +// fixed to one GPIO. (The ProtoPart definition types these as digital +// interfaces; they are modelled here with the clock protocol, matching the +// gold-standard convention for clock pins.) +const clockGpInput = composed({ + id: "clock_gp_input", + name: "Clock GP Input", + protocolType: "clock", + roles: ["input"], + slots: [{ id: "in", required: true, match: { capability: "clock_gpin" } }], + profiles: [ + { id: "clock_gpin0", label: "CLOCK_GPIN0 (GPIO20, pin 31)", bindings: { in: "pin_31" } }, + { id: "clock_gpin1", label: "CLOCK_GPIN1 (GPIO22, pin 34)", bindings: { in: "pin_34" } }, + ], + maxInstances: 2, + traits: [ + { + type: "instance_combinations", + params: { + instances_in_silicon: 2, + routing: "gpio_funcsel_fixed", + combination_space: "CLOCK_GPIN0 on GPIO20 (pin 31), CLOCK_GPIN1 on GPIO22 (pin 34) via F8 — no other pins.", + source: "RP2040 Datasheet §1.4.3 Table 3 / §2.15.2; ProtoPart clock_gp_input description", + }, + }, + { + type: "description", + params: { + note: "Routes an external clock signal into the chip's clocks block as an alternative reference (e.g. to drive clk_ref or feed the frequency counter).", + }, + }, + ], +}); + +const clockGpOutput = composed({ + id: "clock_gp_output", + name: "Clock GP Output", + protocolType: "clock", + roles: ["output"], + slots: [{ id: "out", required: true, match: { capability: "clock_gpout" } }], + profiles: [ + { id: "clock_gpout0", label: "CLOCK_GPOUT0 (GPIO21, pin 32)", bindings: { out: "pin_32" } }, + { id: "clock_gpout1", label: "CLOCK_GPOUT1 (GPIO23, pin 35)", bindings: { out: "pin_35" } }, + { id: "clock_gpout2", label: "CLOCK_GPOUT2 (GPIO24, pin 36)", bindings: { out: "pin_36" } }, + { id: "clock_gpout3", label: "CLOCK_GPOUT3 (GPIO25, pin 37)", bindings: { out: "pin_37" } }, + ], + maxInstances: 4, + traits: [ + { + type: "instance_combinations", + params: { + instances_in_silicon: 4, + routing: "gpio_funcsel_fixed", + combination_space: + "CLOCK_GPOUT0-3 fixed to GPIO21 (pin 32), GPIO23 (pin 35), GPIO24 (pin 36), GPIO25 (pin 37) via F8.", + source: "RP2040 Datasheet §1.4.3 Table 3 / §2.15.2; ProtoPart clock_gp_output description", + }, + }, + { + type: "description", + params: { + note: "Drives any selected internal clock (PLL outputs, clk_sys, etc.) onto a GPIO with optional integer divide, for use by other parts.", + }, + }, + ], +}); + +// Crystal oscillator — Pierce XOSC on the dedicated XIN/XOUT pads. +const crystalOscillator = composed({ + id: "crystal_oscillator", + name: "External Crystal Oscillator (XOSC)", + protocolType: "clock", + roles: ["input"], + parameters: [clockFreqHz(12_000_000)], // the USB bootloader requires 12 MHz + slots: [ + { id: "xin", required: true, match: { protocol: "clock", role: "input", capability: "xtal_in" } }, + // Optional: left open when XIN is driven by a single-ended CMOS clock. + { id: "xout", required: false, match: { protocol: "clock", role: "output", capability: "xtal_out" } }, + ], + profiles: [ + { + id: "xosc_pins", + label: "XIN/XOUT (pins 20/21)", + default_active: true, + bindings: { xin: "pin_20", xout: "pin_21" }, + }, + ], + maxInstances: 1, + defaultActive: true, + traits: [ + { + type: "oscillator_topology", + params: { + note: "Pierce-type crystal oscillator (XOSC, Datasheet §2.16). XIN and XOUT form the inverter terminals of an on-chip Pierce circuit driving an external crystal. Drives clk_ref / the PLLs.", + source: "ProtoPart crystal_oscillator description", + }, + }, + { + type: "implied_passives", + params: { + purpose: "Crystal load", + components: [ + { kind: "capacitor", value: "per crystal vendor C_L spec", connection: "XIN to GND" }, + { kind: "capacitor", value: "per crystal vendor C_L spec", connection: "XOUT to GND" }, + ], + source: "ProtoPart crystal_oscillator description ('external crystal with two load capacitors to ground')", + }, + }, + { + type: "co_requirement", + params: { + with: "usb_device, usb_host", + condition: "USB bootloader / USB in use", + effect: + "The USB bootloader requires a 12 MHz crystal (or a 12 MHz CMOS clock driven into XIN with XOUT left open, per §1.4.2 / §2.16.2).", + source: "ProtoPart pin 20 / crystal_oscillator descriptions", + }, + }, + { + type: "alternative_drive", + params: { + note: "XIN may instead be driven by a single-ended CMOS clock with XOUT disconnected.", + }, + }, + ], +}); + +// SWD — dedicated two-wire debug port to both Cortex-M0+ cores. +const swd = composed({ + id: "swd", + name: "SWD (Serial Wire Debug)", + protocolType: "swd", + roles: ["target"], + slots: [ + { id: "swclk", required: true, match: { capability: "swd_clk" } }, + { id: "swdio", required: true, match: { capability: "swd_io" } }, + ], + profiles: [ + { id: "swd_pins", label: "SWCLK/SWDIO (pins 24/25)", bindings: { swclk: "pin_24", swdio: "pin_25" } }, + ], + maxInstances: 1, + traits: [ + { + type: "description", + params: { + note: "Serial Wire Debug interface to both Cortex-M0+ cores (Datasheet §2.3.4). Dedicated multi-drop SWD bus on SWCLK (pin 24) and SWDIO (pin 25), each with internal pull-up at reset. Used for debugging and to download code over the bus.", + source: "ProtoPart swd interface description", + }, + }, + ], +}); + +// RUN reset — the ProtoPart `reset_input` interface. +const resetInput = composed({ + id: "reset_input", + name: "RUN Reset", + protocolType: "digital", + roles: ["input"], + slots: [{ id: "run", required: true, match: { capability: "run_reset" } }], + profiles: [ + { id: "run_pin", label: "RUN (pin 26)", default_active: true, bindings: { run: "pin_26" } }, + ], + maxInstances: 1, + defaultActive: true, + traits: [ + { + type: "description", + params: { + note: "Active-low RUN pin (pin 26). Drive low to hold the RP2040 in reset; has internal pull-up so it can be left unconnected or tied to IOVDD for autonomous power-on reset.", + source: "ProtoPart reset_input description", + }, + }, + ], +}); + +// --------------------------------------------------------------------------- +// Mechanical / thermal +// --------------------------------------------------------------------------- + +const footprintMount: InterfaceDef = { + id: "footprint_mounting", + name: "QFN-56 7×7 mm Surface-Mount Footprint", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["mounting_point"] }], + capabilities: ["qfn56_7x7_0p4mm", "surface_mount"], + traits: [ + { + type: "assembly_requirement", + params: { + note: "Reflow solder to the QFN-56 land pattern. The center exposed pad must be soldered to the PCB ground plane and stitched with thermal vias for heat dissipation and electrical ground continuity.", + source: "ProtoPart footprint_mounting description", + }, + }, + ], +}; + +const thermalPad: InterfaceDef = { + id: "thermal_pad", + name: "Exposed Thermal Pad", + pin: 57, + domain: "thermal", + exposed: true, + default_active: true, + protocols: [{ type: "thermal_connection", roles: ["thermal_source"] }], + capabilities: ["heat_sink", "pcb_thermal_plane"], + traits: [ + { + type: "assembly_requirement", + params: { + note: "Same physical pad as pad_gnd (the single external ground connection); primary heat path to the PCB ground plane via thermal vias.", + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const RP2040: ModuleDef = defineModule({ + id: "rp2040", + name: "Raspberry Pi RP2040", + version: "2.0.0", + manufacturer: "Raspberry Pi", + part_number: "RP2040", + description: + "Bare-chip dual-core Arm Cortex-M0+ microcontroller in QFN-56 7×7 mm package. 264 KB SRAM with NO internal flash (external QSPI flash required for code execution via XIP). 30 multi-function GPIOs (GPIO0..GPIO29), 2x UART, 2x I2C, 2x SPI, 16 PWM channels (8 slices), 4-channel 12-bit ADC, USB 1.1 host/device PHY, and 8 user-programmable I/O state machines (PIO) across two PIO blocks. Flexible system clock up to 133 MHz default.", + tags: [ + "rp2040", + "raspberry-pi", + "microcontroller", + "arm-cortex-m0", + "dual-core", + "qfn-56", + "pio", + "usb", + "bare-chip", + ], + categories: ["microcontroller.raspberry_pi"], + + interfaces: [ + // All 57 physical pads in package order — schematic-honest. + ...pins, + + // Analog + adc, + + // Serial / bus controllers + ...spi0, + ...spi1, + ...uart0, + ...uart1, + ...i2c0, + ...i2c1, + qspiFlash, + + // Timers / programmable I/O + pwm, + pio(0), + pio(1), + ws2812, + + // USB fabric + usbDevice, + usbHost, + usbVbusDetect, + usbVbusEnable, + usbOvercurrentDetect, + + // Clocks / debug / reset + clockGpInput, + clockGpOutput, + crystalOscillator, + swd, + resetInput, + + // Mechanical / thermal + footprintMount, + thermalPad, + ], + + interfaceGroups: [ + { + id: "required_power_pins", + label: "Required Power Pins", + // IOVDD ×6, DVDD ×2, USB_VDD (must be supplied even if USB is unused — + // ProtoPart warnings), and the exposed ground pad. VREG_VIN (pin 44) is + // required only when the internal core LDO feeds DVDD; ADC_AVDD (pin 43) + // is the ADC supply — the ProtoPart definition does not state either as + // unconditionally required, so they are deliberately not listed here. + members: [ + "pin_1", "pin_10", "pin_22", "pin_23", "pin_33", "pin_42", + "pin_48", "pin_49", "pin_50", "pad_gnd", + ], + policy: "all_of", + }, + { + id: "iovdd_common_net", + label: "IOVDD Pins (one 1.8-3.3 V rail)", + members: ["pin_1", "pin_10", "pin_22", "pin_33", "pin_42", "pin_49"], + policy: "all_of", + }, + { + id: "dvdd_common_net", + label: "DVDD Pins (one 1.1 V core rail)", + members: ["pin_23", "pin_50"], + policy: "all_of", + }, + { + id: "qspi_flash_pins", + label: "Dedicated QSPI Flash Pins", + members: ["pin_51", "pin_52", "pin_53", "pin_54", "pin_55", "pin_56"], + policy: "all_of", + }, + { + id: "usb_phy_mode", + label: "USB PHY Mode (one PHY: device or host)", + members: ["usb_device", "usb_host"], + policy: "one_of", + }, + ], + + requirements: [ + { + type: "power", + description: + "IOVDD digital I/O ring: six pins (1, 10, 22, 33, 42, 49) tied to one 1.8-3.3 V rail (typical 3.3 V) — mixing rails is not supported. Sets the logic level for all GPIOs and the QSPI interface.", + voltage_V: [1.8, 3.3], + current_mA: 50, + }, + { + type: "power", + description: + "DVDD digital core: 1.05-1.16 V (typ 1.1 V, Table 634) on pins 23 and 50 — from VREG_VOUT (typical; VREG_VIN accepts 1.8-3.3 V) or an external 1.1 V regulator. Must not be left floating.", + voltage_V: [1.05, 1.16], + current_mA: 100, + }, + { + type: "power", + description: + "USB_VDD: dedicated 3.135-3.63 V (typ 3.3 V) supply for the USB 1.1 PHY (pin 48). Must be supplied even if USB is unused.", + voltage_V: [3.135, 3.63], + current_mA: 30, + }, + { + type: "interface", + description: + "No internal flash: the chip cannot boot or run user code without an external QSPI flash (typically 2-16 MB, Winbond W25Q-series compatible with the XIP boot ROM) on the dedicated QSPI pads (pins 51-56).", + interface_protocol: "spi", + }, + { + type: "interface", + description: + "A 12 MHz crystal between XIN/XOUT (or a 12 MHz CMOS clock into XIN with XOUT open) is required for the USB bootloader.", + interface_protocol: "clock", + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + { id: "iovdd", name: "IOVDD Digital I/O Supply", nominal_voltage_V: 3.3, voltage_range_V: IOVDD_RANGE, max_current_mA: 50 }, + { id: "dvdd", name: "DVDD Digital Core Supply", nominal_voltage_V: 1.1, voltage_range_V: DVDD_RANGE, max_current_mA: 100 }, + { id: "vreg_vin", name: "Core LDO Input", nominal_voltage_V: 3.3, voltage_range_V: VREG_VIN_RANGE, max_current_mA: 100, regulation_type: "regulated" }, + { id: "usb_vdd", name: "USB PHY Supply", nominal_voltage_V: 3.3, voltage_range_V: USB_VDD_RANGE, max_current_mA: 30 }, + { id: "adc_avdd", name: "ADC Analog Supply", nominal_voltage_V: 3.3, voltage_range_V: ADC_AVDD_RANGE, max_current_mA: 5 }, + { id: "gnd", name: "Ground (exposed pad)", nominal_voltage_V: 0, voltage_range_V: [0, 0], max_current_mA: 1000 }, + ], + metadata: { + pin_count: 56, + package_pins: "56 perimeter pins + exposed ground pad", + cpu: "Dual-core Arm Cortex-M0+, up to 133 MHz default system clock", + sram_KB: 264, + internal_flash: "none — external QSPI flash required (XIP)", + supply_voltage_V: [1.8, 3.3], + power_consumption_mW: 132, + max_operating_freq_Hz: 133_000_000, + supports_usb: true, + supports_hot_plug: false, + // Verbatim ProtoPart power-domain descriptions with no PowerDomainDef home: + power_domain_details: { + iovdd: + "Digital I/O ring supply (six pins: 1, 10, 22, 33, 42, 49). Sets logic level for all 30 GPIOs and QSPI flash interface. Typical 3.3 V; 1.8 V supported for low-voltage I/O. Non-isolated, common ground reference.", + dvdd: + "1.1 V digital core supply (pins 23 and 50). Normally driven by the on-chip core LDO from VREG_VOUT, but can be supplied externally. Per datasheet Table 634, DVDD must be 1.05-1.16 V (typ 1.1 V).", + vreg_vin: + "VREG_VIN input to the on-chip core voltage regulator (1.8-3.3 V). VREG_VOUT delivers ~1.1 V to DVDD.", + usb_vdd: + "Dedicated 3.3 V supply for the USB 1.1 PHY (USB_VDD pin 48). Per datasheet Table 634: 3.135-3.63 V (typ 3.3 V). Decouple separately from IOVDD.", + adc_avdd: + "Analog supply / reference for the 12-bit ADC (ADC_AVDD pin 43). Per datasheet Table 634: 1.62-3.63 V (typ 3.3 V); ADC performance is compromised below 2.97 V (datasheet §2.9.5). Filter from main 3V3 with ferrite bead to maximize ENOB.", + gnd: + "Common ground via the QFN-56 exposed thermal pad (center). Single external ground connection per datasheet §1.4.2; bonded to a number of internal ground pads on the RP2040 die.", + }, + source: "ProtoPart electrical domain (power_domains + metadata); RP2040 Datasheet Table 634", + }, + }, + { + domain: "mechanical", + dimensions_mm: { length: 7, width: 7, height: 0.9 }, + metadata: { + package_type: "QFN-56 (7x7) EP", + pitch_mm: 0.4, + exposed_pad_mm: "3.0-3.2 (nominal 3.1) square (Datasheet §5.1 D2/E2)", + max_height_mm: 0.9, // Datasheet Table §5.1: A max 0.900 mm + mounting_method: "surface_mount", + enclosure_type: "ic_package", + }, + }, + { + domain: "thermal", + operating_temperature_C: [-40, 85], + metadata: { + thermal_design_power_W: 0.13, + requires_thermal_management: false, + thermal_monitoring_available: true, // internal temperature sensor on the ADC + cooling_method: "passive", + primary_heat_path: "exposed_pad_to_pcb_ground_plane", + }, + }, + ], + + traits: [ + { type: "requires_external_flash", params: { interfaceId: "qspi_flash", defaultProfile: "qspi_flash_fixed" } }, + { + type: "bare_chip_notice", + params: { + note: "This is the bare RP2040 chip, not a Pico board. There is no dedicated USB_BOOT pin — the board-level BOOTSEL button on the Pi Pico is wired to QSPI_SS_N (pin 56). Bare-chip BOOTSEL entry is via a pushbutton on QSPI_CS.", + source: "ProtoPart usage_notes / warnings", + }, + }, + { + type: "compatibility_notes", + params: { + note: "Pin-compatible only with itself (RP2040). The RP2350 successor uses a different package (QFN-60) and is NOT a drop-in replacement. Most RP2040 reference layouts (Pico, Pico W, Adafruit Feather RP2040) are good starting points for new designs.", + }, + }, + { + type: "design_rules", + params: { + source: "ProtoPart definition design_rules (verbatim)", + rules: [ + "Tie all six IOVDD pins (1, 10, 22, 33, 42, 49) to the same supply rail (1.8 V or 3.3 V). Mixing rails on IOVDD pins is not supported.", + "Bypass each supply pin with a 100 nF ceramic close to the pin. Place a 1 uF bulk cap on each rail group.", + "DVDD (pins 23, 50) must be supplied either by VREG_VOUT (typical) or an external 1.1 V regulator. Keep DVDD within 1.05-1.16 V per datasheet Table 634 (use 1.15 V if running clk_sys at 200 MHz).", + "Connect the QFN-56 center pad to the PCB ground plane with multiple thermal vias for both heat dissipation and ground reference integrity. This pad is the single external ground connection on the bare chip.", + "External QSPI flash is mandatory — the RP2040 has no internal flash. Use a flash compatible with the chip's XIP boot ROM (typically Winbond W25Q-series).", + "Keep VREG_VOUT decoupling close to pin 45 and route to both DVDD pins (23, 50) with low impedance.", + "Filter ADC_AVDD (pin 43) with a ferrite bead from 3V3 plus 1 uF + 100 nF for best ENOB.", + "USB_VDD (pin 48) must come from the same 3.3 V rail with its own decoupling; route USB_DP/USB_DM (pins 47/46) as a 90 ohm differential pair (Hardware Design with RP2040 §2.4.1) with 27 ohm series termination resistors per datasheet §1.4.2.", + "Tie TESTEN (pin 19) to GND in production designs; floating TESTEN may cause incorrect boot behavior.", + "To support firmware loading via the USB BOOTSEL UF2 path at the board level, route QSPI_SS_N (pin 56) through a momentary pushbutton-to-GND. Pulling QSPI_CS low during reset forces the boot ROM into UF2 mass-storage mode.", + ], + }, + }, + { + type: "warnings", + params: { + source: "ProtoPart definition warnings (verbatim)", + warnings: [ + "No internal flash. The chip cannot boot or run user code without an external QSPI flash (typically 2-16 MB).", + "All GPIO are 3.3 V (or 1.8 V) digital — NOT 5 V tolerant. Apply level shifting for 5 V signals.", + "The voltage on ADC inputs must not exceed IOVDD — voltages above IOVDD leak through the ESD protection diodes (Datasheet §2.9.5 note; absolute maximum pin voltage IOVDD + 0.5 V per Table 622). [Corrected from the ProtoPart 'clamped to ADC_AVDD + 0.3 V' wording.]", + "DVDD pins (23, 50) must not be left floating. Connect to VREG_VOUT (typical) or an external 1.1 V supply.", + "The exposed center pad must be soldered to ground — it is the single external GND connection. Leaving it floating violates ground reference, increases noise, and degrades thermal performance.", + "USB_VDD must be supplied even if USB is unused; otherwise the USB PHY draws leakage current that may keep the chip from entering low-power states cleanly.", + "TESTEN (pin 19) must be tied to GND. A floating TESTEN can place the chip in factory test mode and prevent normal boot.", + "There is no dedicated USB_BOOT pin on the bare RP2040 chip. The board-level BOOTSEL button on the Pi Pico is wired to QSPI_CS (pin 56), not to a separate USB_BOOT pin.", + ], + }, + }, + { + type: "usage_notes", + params: { + source: "ProtoPart definition usage_notes (verbatim)", + note: "Bare-chip dual-core Cortex-M0+ at 133 MHz with 264 KB SRAM. Requires external QSPI flash for user code (no internal flash). Strong PIO subsystem (8 user state machines) makes it especially good for protocols not natively supported (I2S, SK6812 / WS2812 LED data, parallel pixel buses, software USB host). 16 PWM channels (8 slices x A/B) with 8.4 fractional clock dividers (Datasheet §4.5.1) — easily covers 22-25 kHz / 12-bit duty applications. 4-channel 12-bit ADC plus internal temperature sensor; effective ENOB ~9 bits without filtering. USB 1.1 PHY supports both device (BOOTSEL UF2 + user CDC/HID) and host modes. SWD debug via SWCLK (pin 24) / SWDIO (pin 25). Bare-chip BOOTSEL entry is via a pushbutton on QSPI_CS (pin 56), not a dedicated USB_BOOT pin — that pin only exists on board-level products like the Pi Pico.", + }, + }, + { + type: "application_examples", + params: { + source: "ProtoPart definition application_examples (verbatim)", + examples: [ + "Audio effects co-processor with PIO-based I2S receive and bass-band FFT envelope tracking", + "Multi-channel SK6812 / WS2812 LED ring driver via PIO", + "Cost-sensitive USB MIDI / HID device", + "Closed-loop coil drivers (e.g. 22-25 kHz / 12-bit duty PWM into MOSFET gates)", + "Educational and hobbyist Pico-class boards", + "Industrial sensor hubs with parallel CSI / DPI capture via PIO", + "I2C peripheral acting as command relay between a host MCU/BT SoC and on-board effects engine", + ], + }, + }, + { + type: "terminology_policy", + params: { + note: "Signal and function names in `rp2040_pin_functions` traits are ProtoPart/datasheet-verbatim and intentionally NOT normalised to a curated whitelist; canonical capability tags exist only where slot matching requires them. The ProtoPart per-pin generic entries (pwm_out, CLOCK_GPIN, CLOCK_GPOUT, SIO, PIO0, PIO1) are represented as capability tags plus the F4/F5/F6/F7/F8 rows of each pin's function list.", + }, + }, + ], + + artifacts: [ + { + id: "art_datasheet", + name: "RP2040 Datasheet", + type: "datasheet", + url: "https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf", + }, + { + id: "art_hardware_design", + name: "Hardware Design with RP2040", + type: "datasheet", + url: "https://datasheets.raspberrypi.com/rp2040/hardware-design-with-rp2040.pdf", + }, + { + id: "art_product_page", + name: "Raspberry Pi RP2040 product page", + type: "documentation", + url: "https://www.raspberrypi.com/products/rp2040/", + }, + { + id: "art_mfg_image", + name: "RP2040 manufacturer photo (DigiKey)", + type: "custom", + filePath: "./ProtoPart/protoparts/rp2040/artifacts/images/MFG_RP2040.jpg", + mimeType: "image/jpeg", + tags: ["image", "product-photo"], + }, + { + id: "art_snapeda", + name: "SnapEDA part page", + type: "cad", + url: "https://www.snapeda.com/parts/SC0914(13)/Raspberry%20Pi/view-part/?ref=digikey", + }, + { + id: "art_ultralibrarian", + name: "UltraLibrarian part page", + type: "cad", + url: "https://app.ultralibrarian.com/details/A6F27E67-2E9C-11ED-B159-0A34D6323D74/Raspberry-Pi/SC0914-13-?ref=digikey", + }, + { + id: "art_digikey_forum_compare", + name: "DigiKey forum: SC0914-7 vs SC0914-13", + type: "documentation", + url: "https://forum.digikey.com/t/sc0914-7-vs-sc0914-13-raspberry-pi-comparison/19824", + }, + ], + + geometry: { + xScale: 1, + yScale: 1, + outline: { preset: "rectangle" }, + }, +}); diff --git a/library/parts/rp2040/artifacts/thumbnail.png b/library/parts/rp2040/artifacts/thumbnail.png new file mode 100644 index 0000000..1530213 Binary files /dev/null and b/library/parts/rp2040/artifacts/thumbnail.png differ diff --git a/library/parts/seeed-xiao-esp32c3.ts b/library/parts/seeed-xiao-esp32c3.ts new file mode 100644 index 0000000..8b04d85 --- /dev/null +++ b/library/parts/seeed-xiao-esp32c3.ts @@ -0,0 +1,827 @@ +/** + * Seeed Studio XIAO ESP32C3 — source-honest part definition. + * + * Primary source: ProtoPart `seeed-xiao-esp32c3` definition v0.2.0 (schema + * 1.4.0) — the audited source of truth for this file. Its underlying vendor + * reference is the Seeed Studio product page for SKU 113991054: + * https://www.seeedstudio.com/Seeed-XIAO-ESP32C3-p-5431.html + * Nothing below is carried over from ESP32-C3 SoC datasheets, SDK defaults, + * or general XIAO-family knowledge unless the ProtoPart JSON states it; gaps + * in the JSON are left unrepresented rather than filled in. + * + * Architecture notes honoured by this file: + * - Pin-honest like a schematic: all 14 edge pads (D0-D10, 5V, 3V3, GND) + * plus the USB-C receptacle and the U.FL antenna connector are leaf + * interfaces. The JSON provides no numeric pad indices, so `pin` carries + * the official silkscreen designator ("D0" … "D10", "5V", "3V3", "GND") + * and the array preserves the JSON's resource order. + * - Every pad carries its verbatim JSON function list (name, direction, + * shareable_with) in a `xiao_pad_functions` trait — display data is + * separated from the canonical capability tags slot matching needs. + * - The JSON's composed `interfaces` (power in/out, USB device, GPIO, ADC, + * I2C master, SPI master, UART) map onto slot-composed InterfaceDefs + * whose profiles bind the pad ids the JSON's `requires` functions live + * on; `max_instances` reflects how many pads can satisfy the interface. + * - Co-requirements: D7 is both the default UART RX pad and the modeled + * SPI SS pad (JSON warning) — captured as `co_requirement` traits on + * both controllers and a trait on the pad itself. + * - Shareability: the USB-C connector's four functions are mutually + * shareable (one physical connector serves power + data + ground); the + * U.FL feed is shared between the Wi-Fi and Bluetooth radios. + * - The JSON's four `warnings` are distributed to the interfaces they + * constrain (3.3 V logic restriction, D6/D7 contention, strapping-pin + * advisory, USB 5 V budget caveat) instead of being dropped. + */ + +import type { + InterfaceDef, + ModuleDef, + SlotDef, + TraitDef, +} from "../../src/types/index.js"; +import type { Parameter } from "../../src/types/parameter.js"; +import { + Ground, + Pin, + PowerOut, + SPI, + defineModule, + maxCurrentA, + voltageRangeV, +} from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Electrical constants — ProtoPart definition, electrical power_domains +// --------------------------------------------------------------------------- + +/** Citation string used by every trait that carries JSON-verbatim data. */ +const SOURCE = + "ProtoPart seeed-xiao-esp32c3 definition v0.2.0 (Seeed Studio XIAO ESP32C3, SKU 113991054)"; + +/** power_domains.usb_5v: USB / 5V rail. */ +const USB_5V_RANGE: [number, number] = [4.75, 5.25]; +/** power_domains.usb_5v.max_current_mA — see the USB2.0 budget caveat below. */ +const USB_5V_MAX_A = 0.5; + +/** power_domains.regulated_3v3 (3.3 V regulator output) and io_3v3 (I/O reference) share this range. */ +const RANGE_3V3: [number, number] = [3.0, 3.6]; +/** power_domains.regulated_3v3.max_current_mA. */ +const REG_3V3_MAX_A = 0.7; + +/** power_domains.gnd.max_current_mA. */ +const GND_MAX_A = 2.0; + +// --------------------------------------------------------------------------- +// Source-honest per-pad function metadata +// --------------------------------------------------------------------------- + +/** Verbatim function entry from the JSON's electrical `resources[].functions`. */ +interface XiaoFunction { + name: string; + /** JSON direction column, carried verbatim. */ + direction: "input" | "output" | "bidirectional"; + /** Functions this one may share its physical connection with (JSON `shareable_with`). */ + shareable_with?: string[]; +} + +/** Wrap a pad's verbatim JSON function list as a display trait. */ +function padFunctionsTrait(connectorType: string, functions: XiaoFunction[]): TraitDef { + return { + type: "xiao_pad_functions", + params: { + source: `${SOURCE}, electrical resources`, + connector_type: connectorType, + functions, + }, + }; +} + +interface XiaoIoPadSpec { + id: string; + /** Official silkscreen designator — used as the `pin` field. */ + silkscreen: string; + /** Verbatim JSON resource name — used as the displayed interface name. */ + name: string; + analogIn?: boolean; + i2cSda?: boolean; + i2cScl?: boolean; + uartTx?: boolean; + uartRx?: boolean; + spiSs?: boolean; + spiSck?: boolean; + spiMiso?: boolean; + spiMosi?: boolean; + functions: XiaoFunction[]; + traits?: TraitDef[]; +} + +/** Build one schematic-honest 3.3 V I/O edge pad from its JSON resource row. */ +function xiaoIoPad(spec: XiaoIoPadSpec): InterfaceDef { + const base = Pin({ + id: spec.id, + name: spec.name, + pin: spec.silkscreen, + voltageV: RANGE_3V3, // io_3v3 domain — 3.3 V logic only (see logic_level_restriction) + capabilities: { + analogIn: spec.analogIn, + i2cSda: spec.i2cSda, + i2cScl: spec.i2cScl, + uartTx: spec.uartTx, + uartRx: spec.uartRx, + spiSs: spec.spiSs, + spiSck: spec.spiSck, + spiMiso: spec.spiMiso, + spiMosi: spec.spiMosi, + }, + }); + + return { + ...base, + traits: [ + padFunctionsTrait("pin", spec.functions), + { type: "power_domain", params: { domain: "io_3v3" } }, + ...(spec.traits ?? []), + ], + }; +} + +// --------------------------------------------------------------------------- +// I/O edge pads — JSON electrical resources d0..d10, in JSON order +// --------------------------------------------------------------------------- + +const DIGITAL_IO_FN: XiaoFunction = { name: "digital_io", direction: "bidirectional" }; +const ANALOG_IN_FN: XiaoFunction = { name: "analog_input", direction: "input" }; + +const IO_PAD_SPECS: XiaoIoPadSpec[] = [ + { id: "d0", silkscreen: "D0", name: "D0 / A0", analogIn: true, functions: [DIGITAL_IO_FN, ANALOG_IN_FN] }, + { id: "d1", silkscreen: "D1", name: "D1 / A1", analogIn: true, functions: [DIGITAL_IO_FN, ANALOG_IN_FN] }, + { id: "d2", silkscreen: "D2", name: "D2 / A2", analogIn: true, functions: [DIGITAL_IO_FN, ANALOG_IN_FN] }, + { id: "d3", silkscreen: "D3", name: "D3 / A3", analogIn: true, functions: [DIGITAL_IO_FN, ANALOG_IN_FN] }, + { + id: "d4", silkscreen: "D4", name: "D4 / SDA", i2cSda: true, + functions: [DIGITAL_IO_FN, { name: "i2c_sda", direction: "bidirectional" }], + }, + { + id: "d5", silkscreen: "D5", name: "D5 / SCL", i2cScl: true, + functions: [DIGITAL_IO_FN, { name: "i2c_scl", direction: "bidirectional" }], + }, + { + id: "d6", silkscreen: "D6", name: "D6 / TX", uartTx: true, + functions: [DIGITAL_IO_FN, { name: "uart_tx", direction: "output" }], + }, + { + id: "d7", silkscreen: "D7", name: "D7 / RX (also usable as SPI SS)", uartRx: true, spiSs: true, + functions: [ + DIGITAL_IO_FN, + { name: "uart_rx", direction: "input" }, + { name: "spi_ss", direction: "output" }, + ], + traits: [ + { + type: "co_requirement", + params: { + with: "uart, spi_master", + condition: "UART and SPI (with SS) required simultaneously", + effect: + "D6/D7 are the default UART TX/RX pins; D7 is also modeled as SPI SS, so UART and SPI-SS may contend if both are required simultaneously.", + source: `${SOURCE}, warnings`, + }, + }, + ], + }, + { + id: "d8", silkscreen: "D8", name: "D8 / SCK", spiSck: true, + functions: [DIGITAL_IO_FN, { name: "spi_sck", direction: "output" }], + }, + { + id: "d9", silkscreen: "D9", name: "D9 / MISO", spiMiso: true, + functions: [DIGITAL_IO_FN, { name: "spi_miso", direction: "input" }], + }, + { + id: "d10", silkscreen: "D10", name: "D10 / MOSI", spiMosi: true, + functions: [DIGITAL_IO_FN, { name: "spi_mosi", direction: "output" }], + }, +]; + +const ioPads: InterfaceDef[] = IO_PAD_SPECS.map(xiaoIoPad); + +// --------------------------------------------------------------------------- +// Connector and power pads — JSON electrical resources, in JSON order +// --------------------------------------------------------------------------- + +/** + * USB-C receptacle. The JSON models it as one resource carrying four + * mutually-shareable functions (power_input, usb_dp, usb_dm, ground) — one + * physical connector legally serves the 5 V input, the data pair, and the + * ground return at once, so all four capability tags live on this single + * leaf and the composed interfaces below bind their slots back to it. + */ +const usbC: InterfaceDef = { + id: "usb_c", + name: "USB-C receptacle", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [ + { type: "usb", roles: ["device"] }, + { type: "power", roles: ["input"] }, + ], + capabilities: ["usb_dp", "usb_dm", "power_5v_in", "ground"], + parameters: [voltageRangeV(USB_5V_RANGE[0], USB_5V_RANGE[1], 5), maxCurrentA(USB_5V_MAX_A)], + traits: [ + padFunctionsTrait("usb_c", [ + { name: "power_input", direction: "input", shareable_with: ["usb_dp", "usb_dm", "ground"] }, + { name: "usb_dp", direction: "bidirectional", shareable_with: ["power_input", "usb_dm", "ground"] }, + { name: "usb_dm", direction: "bidirectional", shareable_with: ["power_input", "usb_dp", "ground"] }, + { name: "ground", direction: "bidirectional", shareable_with: ["power_input", "usb_dp", "usb_dm"] }, + ]), + { type: "power_domain", params: { domain: "usb_5v" } }, + { + // Shareability exemption: the connector's functions are mutually + // shareable — power, data, and ground instances may all use this pad. + type: "net_shareable", + params: { + net: "usb_c_connector", + policy: "all_connector_functions_share_one_physical_connector", + members: ["power_input", "usb_dp", "usb_dm", "ground"], + }, + }, + ], +}; + +/** + * 5V edge pad. The JSON gives it both power_input and power_output functions + * (mutually shareable): it is the USB/5V-rail node exposed at the board edge, + * usable to feed the board or to tap the rail. + */ +const pin5v: InterfaceDef = { + id: "pin_5v", + name: "5V pin", + pin: "5V", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "power", roles: ["input", "output"] }], + capabilities: ["power_5v_in", "power_5v_out"], + parameters: [voltageRangeV(USB_5V_RANGE[0], USB_5V_RANGE[1], 5), maxCurrentA(USB_5V_MAX_A)], + traits: [ + padFunctionsTrait("pin", [ + { name: "power_input", direction: "input", shareable_with: ["power_output"] }, + { name: "power_output", direction: "output", shareable_with: ["power_input"] }, + ]), + { type: "power_domain", params: { domain: "usb_5v" } }, + { + type: "supply_budget", + params: { + note: "USB 5V available current depends on the upstream USB source and any onboard protection; treat 500mA as a USB2.0 default-budget, not a guaranteed board output capability.", + source: `${SOURCE}, warnings`, + }, + }, + ], +}; + +/** 3V3 edge pad — regulator output only (the JSON models no power_input here). */ +const pin3v3: InterfaceDef = { + ...PowerOut({ + id: "pin_3v3", + name: "3V3 pin", + pin: "3V3", + voltageV: RANGE_3V3, + nominalV: 3.3, + maxCurrentA: REG_3V3_MAX_A, + }), + capabilities: ["power_3v3_out"], + traits: [ + padFunctionsTrait("pin", [{ name: "power_output", direction: "output" }]), + { type: "power_domain", params: { domain: "regulated_3v3" } }, + ], +}; + +/** GND edge pad — common return for every domain (inherently shareable). */ +const gndPin: InterfaceDef = { + ...Ground({ id: "gnd_pin", name: "GND pin", pin: "GND", maxCurrentA: GND_MAX_A }), + traits: [ + padFunctionsTrait("pin", [{ name: "ground", direction: "bidirectional" }]), + { type: "power_domain", params: { domain: "gnd" } }, + { + type: "net_shareable", + params: { net: "gnd", policy: "single_ground_instance_may_serve_all_members" }, + }, + ], +}; + +/** + * All edge pads plus the USB-C receptacle, in the JSON's resource order. + * The JSON assigns no numeric pad indices — `pin` fields carry silkscreen + * designators instead of package numbers. + */ +const pads: InterfaceDef[] = [usbC, pin5v, pin3v3, gndPin, ...ioPads]; + +// --------------------------------------------------------------------------- +// Composed electrical interfaces — JSON electrical `interfaces` (requires → +// slots; the functions they name → capability tags on the pads above) +// --------------------------------------------------------------------------- + +function composed(config: { + id: string; + name: string; + protocolType: string; + roles: string[]; + slots: SlotDef[]; + profiles?: InterfaceDef["profiles"]; + parameters?: Parameter[]; + capabilities?: string[]; + maxInstances?: number; + defaultActive?: boolean; + traits?: TraitDef[]; + domain?: InterfaceDef["domain"]; +}): InterfaceDef { + return { + id: config.id, + name: config.name, + domain: config.domain ?? "electrical", + exposed: true, + default_active: config.defaultActive ?? false, + protocols: [{ type: config.protocolType, roles: config.roles }], + slots: config.slots, + ...(config.profiles ? { profiles: config.profiles } : {}), + ...(config.parameters ? { parameters: config.parameters } : {}), + ...(config.capabilities ? { capabilities: config.capabilities } : {}), + ...(config.maxInstances !== undefined ? { max_instances: config.maxInstances } : {}), + ...(config.traits ? { traits: config.traits } : {}), + }; +} + +function withTraits(iface: InterfaceDef, traits: TraitDef[]): InterfaceDef { + return { ...iface, traits: [...(iface.traits ?? []), ...traits] }; +} + +/** Attach traits to the interface with the given id inside a builder result. */ +function amend(ifaces: InterfaceDef[], id: string, traits: TraitDef[]): InterfaceDef[] { + return ifaces.map((i) => (i.id === id ? withTraits(i, traits) : i)); +} + +/** + * 5V power in — JSON requires one power_input + one ground. Both the USB-C + * receptacle and the 5V pad expose power_input on the usb_5v domain, so both + * routings are modeled as profiles. + */ +const powerInUsb = composed({ + id: "power_in_usb", + name: "5V power in", + protocolType: "power", + roles: ["input"], + parameters: [voltageRangeV(USB_5V_RANGE[0], USB_5V_RANGE[1], 5), maxCurrentA(USB_5V_MAX_A)], + slots: [ + { id: "vin", required: true, match: { protocol: "power", role: "input", capability: "power_5v_in" } }, + { id: "gnd", required: true, match: { capability: "ground" } }, + ], + profiles: [ + { + id: "power_in_via_usb_c", + label: "USB-C VBUS + connector ground", + bindings: { vin: "usb_c", gnd: "usb_c" }, + }, + { + id: "power_in_via_5v_pad", + label: "5V edge pad + GND edge pad", + bindings: { vin: "pin_5v", gnd: "gnd_pin" }, + }, + ], + maxInstances: 1, + traits: [ + { + type: "supply_budget", + params: { + note: "USB 5V available current depends on the upstream USB source and any onboard protection; treat 500mA as a USB2.0 default-budget, not a guaranteed board output capability.", + source: `${SOURCE}, warnings`, + }, + }, + ], +}); + +/** 3.3V power out — JSON requires one power_output + one ground. */ +const powerOut3v3 = composed({ + id: "power_out_3v3", + name: "3.3V power out", + protocolType: "power", + roles: ["output"], + parameters: [voltageRangeV(RANGE_3V3[0], RANGE_3V3[1], 3.3), maxCurrentA(REG_3V3_MAX_A)], + slots: [ + { id: "vout", required: true, match: { protocol: "power", role: "output", capability: "power_3v3_out" } }, + { id: "gnd", required: true, match: { capability: "ground" } }, + ], + profiles: [ + { + id: "power_out_3v3_pads", + label: "3V3 edge pad + GND edge pad", + bindings: { vout: "pin_3v3", gnd: "gnd_pin" }, + }, + ], + maxInstances: 1, +}); + +/** USB device/data — JSON requires usb_dp + usb_dm; both live on the receptacle. */ +const usbDevice = composed({ + id: "usb_device", + name: "USB device/data", + protocolType: "usb", + roles: ["device"], + slots: [ + { id: "dp", required: true, match: { capability: "usb_dp" } }, + { id: "dm", required: true, match: { capability: "usb_dm" } }, + ], + profiles: [ + { id: "usb_device_usb_c", label: "USB-C receptacle D+/D-", bindings: { dp: "usb_c", dm: "usb_c" } }, + ], + maxInstances: 1, +}); + +/** + * GPIO (single pin) — JSON protocol digital/peer, requiring one digital_io + * function. Eleven pads (D0-D10) carry digital_io, so up to eleven + * simultaneous instances exist; any pad satisfies the slot. + */ +const gpio = composed({ + id: "gpio", + name: "GPIO (single pin)", + protocolType: "digital", + roles: ["peer"], // JSON-verbatim role + slots: [ + { id: "io", required: true, match: { protocol: "digital", capability: "digital_io" } }, + ], + maxInstances: 11, + traits: [ + { + type: "instance_combinations", + params: { + combination_space: "Any of the 11 digital-capable edge pads (D0-D10); one GPIO instance per pad.", + source: `${SOURCE}, electrical resources`, + }, + }, + ], +}); + +/** + * ADC (single channel) — JSON requires one analog_input function; only + * D0-D3 (A0-A3) carry it, so at most four simultaneous channels exist. + * The JSON states no resolution, sample rate, or measurement range — + * none are invented here. + */ +const adcIn = composed({ + id: "adc_in", + name: "ADC (single channel)", + protocolType: "analog", + roles: ["input"], + slots: [ + { id: "channel", required: true, match: { protocol: "analog", role: "input", capability: "analog_in" } }, + ], + maxInstances: 4, + traits: [ + { + type: "channels", + params: { count: 4, mapping: "A0=D0, A1=D1, A2=D2, A3=D3", source: `${SOURCE}, electrical resources` }, + }, + ], +}); + +/** + * I2C master — hand-rolled rather than built with the I2C() builder because + * the builder always emits a clock_freq parameter (defaulting to 400 kHz + * Fast-mode) and the JSON states no I2C clock speed; slot shapes mirror the + * builder's canonical ones. JSON requires i2c_sda + i2c_scl, fixed on D4/D5. + */ +const i2cMaster = composed({ + id: "i2c_master", + name: "I2C master", + protocolType: "i2c", + roles: ["master"], + slots: [ + { id: "sda", required: true, match: { protocol: "i2c", role: "data", capability: "i2c_sda" } }, + { id: "scl", required: true, match: { protocol: "i2c", role: "clock", capability: "i2c_scl" } }, + ], + profiles: [ + { id: "i2c_master_default", label: "SDA=D4, SCL=D5", bindings: { sda: "d4", scl: "d5" } }, + ], + maxInstances: 1, + traits: [{ type: "display_notation", params: { latex: "I^{2}C" } }], +}); + +/** + * SPI master — JSON requires spi_sck + spi_mosi + spi_miso + spi_ss, on + * D8/D10/D9/D7 respectively. No clock frequency is stated in the JSON so + * none is emitted (the SPI builder only adds parameters when provided). + */ +const spiMaster = amend( + SPI({ + id: "spi_master", + name: "SPI master", + roles: ["master"], + mosi: "d10", + miso: "d9", + sck: "d8", + ss: "d7", + maxInstances: 1, + }), + "spi_master", + [ + { + type: "co_requirement", + params: { + with: "uart", + condition: "SPI SS bound to D7 while the UART is also required", + effect: + "D7 doubles as the default UART RX pin — SPI-SS and UART may contend if both are required simultaneously.", + source: `${SOURCE}, warnings`, + }, + }, + ], +); + +/** + * UART — hand-rolled rather than built with UART() so the JSON-verbatim + * protocol role "peer" is preserved (the builder only offers host/device); + * slot shapes mirror the builder's canonical ones. JSON requires uart_tx + + * uart_rx, fixed on D6/D7. + */ +const uart = composed({ + id: "uart", + name: "UART", + protocolType: "uart", + roles: ["peer"], // JSON-verbatim role + slots: [ + { id: "rx", required: true, match: { protocol: "uart", role: "receiver", capability: "uart_rx" } }, + { id: "tx", required: true, match: { protocol: "uart", role: "transmitter", capability: "uart_tx" } }, + ], + profiles: [ + { id: "uart_default", label: "TX=D6, RX=D7 (board default)", bindings: { rx: "d7", tx: "d6" } }, + ], + maxInstances: 1, + traits: [ + { + type: "default_routing", + params: { + note: "D6/D7 are the default UART TX/RX pins.", + source: `${SOURCE}, warnings`, + }, + }, + { + type: "co_requirement", + params: { + with: "spi_master", + condition: "SPI SS on D7 in use", + effect: + "D7 is also modeled as SPI SS, so UART and SPI-SS may contend if both are required simultaneously.", + source: `${SOURCE}, warnings`, + }, + }, + ], +}); + +// --------------------------------------------------------------------------- +// Network domain — U.FL antenna feed and radios (JSON network domain) +// --------------------------------------------------------------------------- + +/** + * U.FL external antenna connector — a physical wireless port (JSON + * `resource_type: physical_port`, connector `u.fl`) whose two functions + * (wifi_antenna, bluetooth_antenna) are mutually shareable: one antenna + * feed serves both radios. + */ +const uflAntenna: InterfaceDef = { + id: "ufl_antenna", + name: "U.FL external antenna connector", + domain: "network", + exposed: true, + default_active: true, + protocols: [{ type: "rf", roles: ["transceiver"] }], + capabilities: ["wifi_antenna", "bluetooth_antenna"], + traits: [ + { + type: "rf_port", + params: { + port_type: "wireless", + connector: "u.fl", + description: "External U.FL antenna connector — the board's only modeled antenna feed.", + source: `${SOURCE}, network resources`, + }, + }, + { + type: "net_shareable", + params: { + net: "xiao_antenna_feed", + policy: "single_antenna_serves_both_radios", + members: ["wifi_antenna", "bluetooth_antenna"], + }, + }, + ], + bridgesTo: ["wifi_client", "bluetooth_peer"], +}; + +/** Wi-Fi (client) — JSON requires one wifi_antenna function. */ +const wifiClient = composed({ + id: "wifi_client", + name: "Wi-Fi (client)", + domain: "network", + protocolType: "wifi", + roles: ["client"], + capabilities: ["wifi_802_11_bgn", "wifi_2g4"], + slots: [{ id: "antenna", required: true, match: { capability: "wifi_antenna" } }], + profiles: [ + { id: "wifi_ufl", label: "U.FL external antenna", bindings: { antenna: "ufl_antenna" } }, + ], + maxInstances: 1, + traits: [ + { + type: "wireless_standard", + params: { standard: "802.11 b/g/n (2.4GHz)", source: `${SOURCE}, network wireless_capabilities` }, + }, + ], +}); + +/** Bluetooth LE — JSON requires one bluetooth_antenna function. */ +const bluetoothPeer = composed({ + id: "bluetooth_peer", + name: "Bluetooth LE", + domain: "network", + protocolType: "bluetooth", + roles: ["peer"], + capabilities: ["bluetooth_le", "bluetooth_2g4"], + slots: [{ id: "antenna", required: true, match: { capability: "bluetooth_antenna" } }], + profiles: [ + { id: "bluetooth_ufl", label: "U.FL external antenna", bindings: { antenna: "ufl_antenna" } }, + ], + maxInstances: 1, + traits: [ + { + type: "wireless_standard", + params: { standard: "Bluetooth 5 (LE)", source: `${SOURCE}, network wireless_capabilities` }, + }, + ], +}); + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const SEEED_XIAO_ESP32C3: ModuleDef = defineModule({ + id: "seeed-xiao-esp32c3", + name: "Seeed Studio XIAO ESP32C3", + version: "0.2.0", + manufacturer: "Seeed Studio", + part_number: "113991054", + description: + "Thumb-sized ESP32-C3 (RISC-V) dev board in the XIAO form factor with USB-C, 3.3V I/O, Wi-Fi + BLE, and an external U.FL antenna connector.", + tags: ["xiao", "esp32-c3", "risc-v", "wifi", "ble", "usb-c"], + categories: ["microcontroller.seeed_mcu", "microcontroller.esp32", "connectivity.wireless"], + + interfaces: [ + // Edge pads + connectors in the JSON's resource order — schematic-honest. + ...pads, + + // Composed electrical interfaces (JSON electrical `interfaces`) + powerInUsb, + powerOut3v3, + usbDevice, + gpio, + adcIn, + i2cMaster, + ...spiMaster, + uart, + + // Network domain: antenna feed and radios + uflAntenna, + wifiClient, + bluetoothPeer, + ], + + interfaceGroups: [ + { + id: "power_entry", + label: "5V Power Entry (USB-C receptacle or 5V edge pad)", + members: ["usb_c", "pin_5v"], + policy: "any_of", + }, + { + id: "analog_capable_pads", + label: "Analog-Capable Pads (A0-A3)", + members: ["d0", "d1", "d2", "d3"], + policy: "any_of", + }, + ], + + requirements: [ + { + type: "power", + description: + "5 V supply on the usb_5v domain, via the USB-C receptacle or the 5V edge pad (4.75-5.25 V). Treat the 500 mA figure as a USB2.0 default-budget dependent on the upstream source, not a guaranteed capability.", + voltage_V: [4.75, 5.25], + current_mA: 500, + }, + { + type: "capability", + description: + "Wi-Fi/BLE operation requires an external 2.4 GHz antenna attached to the U.FL connector — the JSON models no onboard antenna.", + capability: "wifi_antenna", + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + { id: "usb_5v", name: "USB / 5V rail", nominal_voltage_V: 5, voltage_range_V: USB_5V_RANGE, max_current_mA: 500 }, + { + id: "regulated_3v3", + name: "3.3V regulator output", + nominal_voltage_V: 3.3, + voltage_range_V: RANGE_3V3, + max_current_mA: 700, + regulation_type: "regulated", + }, + { id: "io_3v3", name: "3.3V digital/analog I/O reference", nominal_voltage_V: 3.3, voltage_range_V: RANGE_3V3, max_current_mA: 20 }, + { id: "gnd", name: "Ground", nominal_voltage_V: 0, voltage_range_V: [0, 0], max_current_mA: 2000 }, + ], + metadata: { + edge_pads: 14, + edge_pad_names: ["D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "D10", "5V", "3V3", "GND"], + connectors: ["USB-C receptacle", "U.FL antenna connector"], + pad_designators: "silkscreen names — the audited JSON assigns no numeric pad indices", + io_3v3_current_note: + "max_current_mA 20 is the io_3v3 domain figure from the JSON; it does not state whether this is per-pad or aggregate.", + source: SOURCE, + }, + }, + { + domain: "network", + metadata: { + wireless_standards: ["802.11 b/g/n (2.4GHz)", "Bluetooth 5 (LE)"], + antenna: "external via U.FL connector", + source: `${SOURCE}, network wireless_capabilities`, + }, + }, + { + domain: "thermal", + operating_temperature_C: [-40, 85], + }, + { + domain: "mechanical", + // The JSON states length and width only — no height. + dimensions_mm: { length: 21, width: 17.8 }, + metadata: { + form_factor: "Seeed Studio XIAO", + mounting: "14 edge pads (connector_type: pin) + USB-C receptacle", + source: SOURCE, + }, + }, + ], + + traits: [ + { + type: "logic_level_restriction", + params: { + note: "All GPIO/ADC are 3.3V-domain; do not apply 5V logic directly.", + source: `${SOURCE}, warnings`, + }, + }, + { + type: "boot_strapping_advisory", + params: { + note: "Some pins are strapping/boot sensitive (board docs recommend avoiding external pulls on certain pins during reset/boot).", + limitation: "The audited JSON does not enumerate which pads are strapping-sensitive — verify against board docs before adding external pulls.", + source: `${SOURCE}, warnings`, + }, + }, + { type: "wireless_soc", params: { radios: ["wifi_client", "bluetooth_peer"], rfFeed: "ufl_antenna" } }, + { + type: "terminology_policy", + params: { + note: "Function names in `xiao_pad_functions` traits are JSON-verbatim and intentionally NOT normalised to a curated whitelist; canonical capability tags exist only where slot matching requires them.", + }, + }, + ], + + artifacts: [ + { + id: "art_product_page", + name: "Seeed Studio XIAO ESP32C3 Product Page", + type: "documentation", + url: "https://www.seeedstudio.com/Seeed-XIAO-ESP32C3-p-5431.html", + description: "Vendor product page — the datasheet_url recorded in the audited ProtoPart definition.", + }, + { + id: "art_thumbnail", + name: "Seeed Studio XIAO ESP32C3 Thumbnail", + type: "custom", + filePath: "./ProtoPart/protoparts/seeed-xiao-esp32c3/thumbnail.png", + mimeType: "image/png", + tags: ["image", "thumbnail"], + }, + ], + + geometry: { + xScale: 1, + yScale: 1, + outline: { preset: "rectangle" }, + }, +}); diff --git a/library/parts/seeed-xiao-esp32c3/artifacts/thumbnail.png b/library/parts/seeed-xiao-esp32c3/artifacts/thumbnail.png new file mode 100644 index 0000000..20d78be Binary files /dev/null and b/library/parts/seeed-xiao-esp32c3/artifacts/thumbnail.png differ diff --git a/library/parts/stepperonline-17hs19-2004s1.ts b/library/parts/stepperonline-17hs19-2004s1.ts new file mode 100644 index 0000000..de1df0c --- /dev/null +++ b/library/parts/stepperonline-17hs19-2004s1.ts @@ -0,0 +1,586 @@ +/** + * STEPPERONLINE 17HS19-2004S1 — datasheet-honest part definition. + * + * Primary source: STEPPERONLINE 17HS19-2004S1 product page (datasheet) + * https://www.omc-stepperonline.com/nema-17-bipolar-59ncm-84oz-in-2a-42x48mm-4-wires-w-1m-cable-connector-17hs19-2004s1 + * audited via the ProtoPart definition (protoparts/stepperonline-17hs19-2004s1, + * schema 1.4.0): NEMA 17 bipolar stepper, 59 N·cm holding torque, 1.8° step, + * 2.0 A/phase, 4-wire with 1 m cable and 4-pin 2.54 mm female connector. + * Nothing below is carried over from stepper-catalogue convention without + * attribution to that audited source. + * + * Architecture notes honoured by this file: + * - Pin-honest like a schematic: the four motor leads are leaf interfaces. + * The source defines connector positions only by wire colour + * (A+=Black, A-=Green, B+=Red, B-=Blue), so the colour codes are the + * `pin` designators — no invented connector pin numbers. + * - Phase pairing: each winding (A+/A-, B+/B-) is an `all_of` interface + * group — a driver must connect each winding as a pair, never a single + * lead. The composed `bipolar_stepper_phases` interface spans all four + * leads with one default lead-colour profile (max_connections: 1). + * - Current-driven load: the phase coils are a floating power domain; + * the 2.8 V "rated voltage" is I×R at 2.0 A and 1.4 Ω. A chopper + * (current-limiting) driver is required — the coils must never be + * driven directly from a voltage source (source design rule). + * - Mechanical-domain content is carried faithfully: NEMA 17 front + * mounting face (31±0.2 mm square hole pattern, 4×M3 tapped), Ø5 mm × 24 mm D-cut + * output shaft (15 mm flat), 0.59 N·m holding torque, 1.8° step angle, + * 42×42×48 mm frame, 390 g — as leaf + composed mechanical interfaces + * mirroring the source's resources/interfaces split. + * - Source design_rules / validation_requirements / usage_notes / + * warnings / compatibility_notes are preserved verbatim as module + * traits, and additionally woven into the interfaces they govern. + */ + +import type { + InterfaceDef, + ModuleDef, + TraitDef, +} from "../../src/types/index.js"; +import type { Parameter } from "../../src/types/parameter.js"; +import { + defineModule, + maxCurrentA, + voltageRangeV, +} from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Electrical constants — ProtoPart `phase_coils` power domain + lead ratings +// --------------------------------------------------------------------------- + +/** Rated coil voltage: "2.8 V is I×R at 2.0 A and 1.4 Ω" (power-domain description / usage notes). */ +const COIL_NOMINAL_V = 2.8; +/** Phase-coil domain withstand range (power_domains.phase_coils.voltage_range_V). */ +const COIL_RANGE_V: [number, number] = [0, 24]; +/** Rated continuous phase current: lead current_rating 2000 mA source and sink; design rule "Limit phase current to ≤2.0 A." */ +const RATED_PHASE_CURRENT_A = 2.0; +/** Coil resistance per phase: "Verify coil resistance ≈1.4 Ω/phase at 25 °C" (validation requirement). */ +const COIL_RESISTANCE_OHM = 1.4; +/** Holding torque: 59 N·cm; "Confirm holding torque near 0.59 N·m at 2-phase energize" (validation requirement). */ +const HOLDING_TORQUE_NM = 0.59; +/** Full step angle (part description: "1.8° step"). */ +const STEP_ANGLE_DEG = 1.8; + +/** Lead termination — every lead is a flying wire in the 1 m cable (resource connector_type: "wire"). */ +const LEAD_TERMINATION_TRAIT: TraitDef = { + type: "termination", + params: { + connector_type: "wire", + cable: "1 m cable terminating in a 4-pin 2.54 mm female connector", + note: "Verify mating header (source compatibility notes).", + source: "ProtoPart stepperonline-17hs19-2004s1: electrical resources / compatibility_notes", + }, +}; + +// --------------------------------------------------------------------------- +// Phase leads — schematic-honest leaf interfaces, one per wire +// --------------------------------------------------------------------------- + +interface StepperLeadSpec { + /** Interface id, verbatim from the ProtoPart resource id. */ + id: string; + /** Displayed name, verbatim ("Lead BLK (A+)", ...). */ + name: string; + /** Wire colour code — the only connector-position designator the source defines. */ + colour: "BLK" | "GRN" | "RED" | "BLU"; + /** Verbatim function name (PHASE_A, PHASE_A_BAR, PHASE_B, PHASE_B_BAR). */ + functionName: string; + /** Verbatim function description ("Phase A positive", ...). */ + functionDescription: string; + /** Canonical capability tag for slot matching. */ + capability: string; + winding: "A" | "B"; +} + +/** Build one motor lead from its audited resource row. */ +function stepperLead(spec: StepperLeadSpec): InterfaceDef { + const parameters: Parameter[] = [ + // Floating, current-driven winding: 2.8 V nominal (I×R), 0-24 V domain range. + voltageRangeV(COIL_RANGE_V[0], COIL_RANGE_V[1], COIL_NOMINAL_V), + maxCurrentA(RATED_PHASE_CURRENT_A), + ]; + + return { + id: spec.id, + name: spec.name, + pin: spec.colour, + domain: "electrical", + exposed: true, + default_active: true, + // Source function: direction "bidirectional", signal_class "power" — + // winding current alternates under the driver's H-bridge. + protocols: [{ type: "power", roles: ["bidirectional"] }], + capabilities: [spec.capability], + parameters, + traits: [ + { + type: "stepper_phase_function", + params: { + function: spec.functionName, + description: spec.functionDescription, + direction: "bidirectional", + signal_class: "power", + source: "ProtoPart stepperonline-17hs19-2004s1, electrical resources", + }, + }, + { + type: "power_domain", + params: { + domain: "phase_coils", + isolation_type: "non_isolated", + ground_reference: "floating", + note: "Per-phase DC winding. Current-driven; 2.8 V is I×R at 2.0 A and 1.4 Ω.", + }, + }, + { + type: "current_rating", + params: { + source_max_continuous_mA: 2000, + sink_max_continuous_mA: 2000, + source: "ProtoPart lead current_rating", + }, + }, + { + // Phase pairing is physical: one winding, two leads. A driver must + // land both leads of the winding — a single lead is meaningless. + type: "winding_pair", + params: { + winding: `Phase ${spec.winding}`, + pair: + spec.winding === "A" + ? ["lead_black", "lead_green"] + : ["lead_red", "lead_blue"], + }, + }, + LEAD_TERMINATION_TRAIT, + ], + }; +} + +const phaseLeads: InterfaceDef[] = [ + stepperLead({ + id: "lead_black", name: "Lead BLK (A+)", colour: "BLK", + functionName: "PHASE_A", functionDescription: "Phase A positive", + capability: "phase_a", winding: "A", + }), + stepperLead({ + id: "lead_green", name: "Lead GRN (A-)", colour: "GRN", + functionName: "PHASE_A_BAR", functionDescription: "Phase A negative", + capability: "phase_a_bar", winding: "A", + }), + stepperLead({ + id: "lead_red", name: "Lead RED (B+)", colour: "RED", + functionName: "PHASE_B", functionDescription: "Phase B positive", + capability: "phase_b", winding: "B", + }), + stepperLead({ + id: "lead_blue", name: "Lead BLU (B-)", colour: "BLU", + functionName: "PHASE_B_BAR", functionDescription: "Phase B negative", + capability: "phase_b_bar", winding: "B", + }), +]; + +// --------------------------------------------------------------------------- +// Composed drive interface — the connection a bipolar stepper driver makes +// --------------------------------------------------------------------------- + +const bipolarStepperPhases: InterfaceDef = { + id: "bipolar_stepper_phases", + name: "Bipolar stepper, 2-phase", + domain: "electrical", + exposed: true, + default_active: true, + // Verbatim protocol from the audited definition: type + // "bipolar_stepper_phases", role "motor" — pairs with a driver-side + // interface presenting the driver role of the same protocol. + protocols: [{ type: "bipolar_stepper_phases", roles: ["motor"] }], + max_instances: 1, // constraints.max_connections: 1 + parameters: [ + { id: "rated_phase_current", name: "Rated phase current (continuous)", unit: "A", value: RATED_PHASE_CURRENT_A }, + { id: "coil_resistance", name: "Coil resistance per phase (≈, 25 °C)", unit: "Ω", value: COIL_RESISTANCE_OHM }, + { id: "rated_coil_voltage", name: "Rated coil voltage (I×R)", unit: "V", value: COIL_NOMINAL_V }, + // Datasheet: "INDUCTANCE/PHASE(mH)@1KHz 3.00±20%" (17HS19-2004S1 Full Datasheet, + // omc-stepperonline.com/download/17HS19-2004S1.pdf) — added during datasheet audit. + { id: "coil_inductance", name: "Coil inductance per phase (±20%, 1 kHz)", unit: "mH", value: 3.0 }, + // Electrical-domain figure: power_consumption_mW = 11200 (both phases at rating). + { id: "power_consumption", name: "Power consumption", unit: "W", value: 11.2 }, + ], + slots: [ + { id: "phase_a", label: "PHASE_A (A+)", required: true, match: { protocol: "power", role: "bidirectional", capability: "phase_a" } }, + { id: "phase_a_bar", label: "PHASE_A_BAR (A-)", required: true, match: { protocol: "power", role: "bidirectional", capability: "phase_a_bar" } }, + { id: "phase_b", label: "PHASE_B (B+)", required: true, match: { protocol: "power", role: "bidirectional", capability: "phase_b" } }, + { id: "phase_b_bar", label: "PHASE_B_BAR (B-)", required: true, match: { protocol: "power", role: "bidirectional", capability: "phase_b_bar" } }, + ], + profiles: [ + { + id: "stepper_leads", + label: "4-wire leads: A+=Black, A-=Green, B+=Red, B-=Blue", + default_active: true, + bindings: { + phase_a: "lead_black", + phase_a_bar: "lead_green", + phase_b: "lead_red", + phase_b_bar: "lead_blue", + }, + }, + ], + traits: [ + { + type: "wiring_sequence", + params: { + mapping: "A+=Black, A-=Green, B+=Red, B-=Blue", + note: "Connect to a bipolar stepper driver.", + source: "ProtoPart interface description / design rule 'Observe wiring sequence'", + }, + }, + { + type: "drive_requirement", + params: { + driver: "Chopper (current-limiting) bipolar stepper driver", + rule: "Use a chopper driver; do not drive coils directly from a voltage source. Limit phase current to ≤2.0 A.", + typical_bus: "Rated coil voltage is 2.8 V (I×R). Typical driver bus 12-36 V with current limiting.", + source: "ProtoPart design_rules / usage_notes", + }, + }, + { + type: "connection_constraint", + params: { + max_connections: 1, + requires_connector_type: "wire", + source: "ProtoPart bipolar_stepper_phases constraints", + }, + }, + { + type: "duty_warning", + params: { + warning: "Prolonged stall at rated current can overheat the motor.", + source: "ProtoPart warnings", + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// Mechanical — NEMA 17 mounting face and D-cut output shaft +// --------------------------------------------------------------------------- + +/** Leaf: the physical front mounting face (ProtoPart mechanical resource `front_face_mount`). */ +const frontFaceMount: InterfaceDef = { + id: "front_face_mount", + name: "Front face mount", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical", roles: ["bidirectional"] }], + capabilities: ["mechanical_mount"], + parameters: [ + // Datasheet dimension drawing: 31±0.2 mm is the hole-to-hole spacing of the + // square NEMA 17 pattern (two "31±0.2" dims), not a bolt-circle diameter. + // Id kept stable for consumers; corrected during datasheet audit + // (17HS19-2004S1_Dimension-2.svg, omc-stepperonline.com). + { id: "bolt_circle_diameter", name: "Mounting hole spacing (square pattern)", unit: "mm", value: 31, tolerance: { type: "absolute", value: 0.2 } }, + // mount_holes: front, diam 3 mm (M3 nominal). + { id: "mount_hole_diameter", name: "Mount hole diameter", unit: "mm", value: 3 }, + ], + traits: [ + { + type: "mechanical_function", + params: { + function: "MECHANICAL_MOUNT", + // Datasheet drawing: "4-M3 DEPTH 4.5MIN" — tapped holes in the front face, + // not clearance holes (corrected during datasheet audit). + description: "NEMA 17 4×M3 tapped (4.5 mm min depth)", + direction: "bidirectional", + signal_class: "mechanical_drive", + source: "ProtoPart mechanical resources", + }, + }, + // Datasheet drawing: "4-M3 DEPTH 4.5MIN" (tapped, blind) on a 31±0.2 mm square. + { type: "hole_pattern", params: { count: 4, thread: "M3 tapped, 4.5 mm min depth", location: "front" } }, + { type: "termination", params: { connector_type: "through_hole" } }, + ], +}; + +/** Leaf: the physical output shaft (ProtoPart mechanical resource `output_shaft`). */ +const outputShaft: InterfaceDef = { + id: "output_shaft", + name: "D-cut output shaft", + domain: "mechanical", + exposed: true, + default_active: true, + // Source function SHAFT_OUTPUT: direction "source" — torque flows out. + protocols: [{ type: "mechanical", roles: ["source"] }], + capabilities: ["shaft_output"], + parameters: [ + // "Ø5 mm shaft, 24 mm length, D-flat 15 mm." + { id: "shaft_diameter", name: "Shaft diameter", unit: "mm", value: 5 }, + { id: "shaft_length", name: "Shaft length", unit: "mm", value: 24 }, + { id: "d_flat_length", name: "D-flat length", unit: "mm", value: 15 }, + ], + traits: [ + { + type: "mechanical_function", + params: { + function: "SHAFT_OUTPUT", + description: "5 mm D-shaft", + direction: "source", + signal_class: "mechanical_drive", + source: "ProtoPart mechanical resources", + }, + }, + { type: "termination", params: { connector_type: "custom" } }, + ], +}; + +/** Composed: the NEMA 17 front mount a bracket/frame connects to. */ +const mountingInterface: InterfaceDef = { + id: "mounting_interface", + name: "NEMA 17 front mount", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["mounting_point"] }], + max_instances: 1, + slots: [ + { id: "mount", required: true, match: { protocol: "mechanical", role: "bidirectional", capability: "mechanical_mount" } }, + ], + profiles: [ + { + id: "front_face", + label: "Front face (31±0.2 mm square hole pattern, 4×M3 tapped)", + default_active: true, + bindings: { mount: "front_face_mount" }, + }, + ], + traits: [ + { type: "assembly_requirement", params: { note: "Use four M3 screws.", source: "ProtoPart mounting_interface description" } }, + ], +}; + +/** Composed: the torque output a coupler/gear connects to. */ +const shaftInterface: InterfaceDef = { + id: "shaft_interface", + name: "Shaft output", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["custom"] }], + max_instances: 1, + parameters: [ + { id: "holding_torque", name: "Holding torque (2-phase energize)", unit: "Nm", value: HOLDING_TORQUE_NM }, + { id: "step_angle", name: "Full step angle", unit: "deg", value: STEP_ANGLE_DEG }, + ], + slots: [ + { id: "shaft", required: true, match: { protocol: "mechanical", role: "source", capability: "shaft_output" } }, + ], + profiles: [ + { + id: "d_cut_shaft", + label: "Ø5 mm × 24 mm D-cut shaft (15 mm flat)", + default_active: true, + bindings: { shaft: "output_shaft" }, + }, + ], + traits: [ + { + type: "coupling_requirement", + params: { + note: "Couple with 5 mm bore coupler or gear.", + source: "ProtoPart shaft_interface description", + }, + }, + { + type: "load_limits", + params: { + warning: "Avoid excessive axial/radial loads; use proper couplers.", + validation: "Check runout and coupler alignment to avoid bearing load.", + source: "ProtoPart warnings / validation_requirements", + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const STEPPERONLINE_17HS19_2004S1: ModuleDef = defineModule({ + id: "stepperonline-17hs19-2004s1", + name: "STEPPERONLINE 17HS19-2004S1", + version: "1.0.0", + manufacturer: "STEPPERONLINE", + part_number: "17HS19-2004S1", + description: + "NEMA 17 bipolar stepper motor, 59 N·cm holding torque, 1.8° step, 2.0 A/phase, 4-wire with 1 m cable and 4-pin connector.", + tags: ["nema17", "stepper", "bipolar", "59Ncm", "2A", "48mm", "4-wire", "1m-cable"], + categories: ["actuator.motor.stepper"], + + interfaces: [ + // The four motor leads — schematic-honest, colour-designated. + ...phaseLeads, + + // The drive connection a bipolar stepper driver makes (all four leads). + bipolarStepperPhases, + + // Mechanical: mounting face and output shaft (leaf + composed). + frontFaceMount, + outputShaft, + mountingInterface, + shaftInterface, + ], + + interfaceGroups: [ + { + id: "phase_a_winding", + label: "Phase A winding (A+/A- — connect as a pair)", + members: ["lead_black", "lead_green"], + policy: "all_of", + }, + { + id: "phase_b_winding", + label: "Phase B winding (B+/B- — connect as a pair)", + members: ["lead_red", "lead_blue"], + policy: "all_of", + }, + { + id: "required_motor_leads", + label: "Required Motor Leads (4-wire bipolar)", + members: ["lead_black", "lead_green", "lead_red", "lead_blue"], + policy: "all_of", + }, + ], + + requirements: [ + { + type: "interface", + description: + "Connect to a bipolar stepper driver. Use a chopper (current-limiting) driver; do not drive the coils directly from a voltage source. Rated coil voltage is 2.8 V (I×R); typical driver bus 12-36 V with current limiting.", + interface_protocol: "bipolar_stepper_phases", + }, + { + type: "power", + description: + "Driver must limit phase current to ≤2.0 A continuous per winding (design rule). Phase-coil domain withstand range 0-24 V; the coils are floating and non-isolated.", + current_mA: 2000, + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + { + id: "phase_coils", + name: "Stepper Phase Coils", + nominal_voltage_V: COIL_NOMINAL_V, + voltage_range_V: COIL_RANGE_V, + }, + ], + metadata: { + pin_count: 4, + power_consumption_mW: 11200, + package_type: "Motor with 1 m cable and 4-pin connector", + phase_coils_domain: { + isolation_type: "non_isolated", + ground_reference: "floating", + description: "Per-phase DC winding. Current-driven; 2.8 V is I×R at 2.0 A and 1.4 Ω.", + }, + source: "ProtoPart stepperonline-17hs19-2004s1, electrical domain", + }, + }, + { + domain: "mechanical", + dimensions_mm: { length: 48, width: 42, height: 42 }, + weight_g: 390, + metadata: { + package_type: "NEMA 17 frame", + mount_holes: [{ location: "front", diam_mm: 3 }], + source: "ProtoPart stepperonline-17hs19-2004s1, mechanical domain", + }, + }, + ], + + traits: [ + { + type: "design_rules", + params: { + rules: [ + "Limit phase current to ≤2.0 A.", + "Use a chopper driver; do not drive coils directly from a voltage source.", + "Observe wiring sequence: A+=Black, A-=Green, B+=Red, B-=Blue.", + ], + source: "ProtoPart design_rules (verbatim)", + }, + }, + { + type: "validation_requirements", + params: { + checks: [ + "Verify coil resistance ≈1.4 Ω/phase at 25 °C.", + "Confirm holding torque near 0.59 N·m at 2-phase energize.", + "Check runout and coupler alignment to avoid bearing load.", + ], + source: "ProtoPart validation_requirements (verbatim)", + }, + }, + { + type: "usage_notes", + params: { + note: "Rated coil voltage is 2.8 V (I×R). Typical driver bus 12-36 V with current limiting.", + source: "ProtoPart usage_notes (verbatim)", + }, + }, + { + type: "compatibility_notes", + params: { + note: "Bipolar 4-wire. 1 m cable terminates in 4-pin 2.54 mm female connector; verify mating header.", + source: "ProtoPart compatibility_notes (verbatim)", + }, + }, + { + type: "warnings", + params: { + warnings: [ + "Prolonged stall at rated current can overheat the motor.", + "Avoid excessive axial/radial loads; use proper couplers.", + ], + source: "ProtoPart warnings (verbatim)", + }, + }, + { + type: "application_examples", + params: { + examples: ["3D printer axes", "Small CNC stages", "Robotics actuators"], + source: "ProtoPart application_examples (verbatim)", + }, + }, + { + // ProtoPart previewArtifactId — preserved so the preview selection survives the audit. + type: "preview_artifact", + params: { artifactId: "art_thumbnail" }, + }, + ], + + artifacts: [ + { + id: "art_datasheet", + name: "STEPPERONLINE 17HS19-2004S1 Product Page (datasheet)", + type: "datasheet", + url: "https://www.omc-stepperonline.com/nema-17-bipolar-59ncm-84oz-in-2a-42x48mm-4-wires-w-1m-cable-connector-17hs19-2004s1", + }, + { + id: "art_thumbnail", + name: "Thumbnail", + type: "custom", + filePath: "./ProtoPart/protoparts/stepperonline-17hs19-2004s1/thumbnail.png", + mimeType: "image/png", + tags: ["image", "thumbnail"], + }, + ], + + geometry: { + xScale: 1, + yScale: 1, + outline: { preset: "rectangle" }, + }, +}); diff --git a/library/parts/stepperonline-17hs19-2004s1/artifacts/thumbnail.png b/library/parts/stepperonline-17hs19-2004s1/artifacts/thumbnail.png new file mode 100644 index 0000000..986aa7f Binary files /dev/null and b/library/parts/stepperonline-17hs19-2004s1/artifacts/thumbnail.png differ diff --git a/library/parts/towerpro-sg90.ts b/library/parts/towerpro-sg90.ts new file mode 100644 index 0000000..09fbdee --- /dev/null +++ b/library/parts/towerpro-sg90.ts @@ -0,0 +1,556 @@ +/** + * Tower Pro SG90 — audit-honest part definition. + * + * Primary source: ProtoPart `towerpro-sg90` definition v0.2.0 (schema 1.4.0), + * itself citing the Tower Pro SG90 product page + * (https://towerpro.com.tw/product/sg90-analog/): + * - electrical domain power domain "servo_5v" (4.8-6.0 V, ~650 mA stall), + * three lead resources (V+/GND/Signal with wire colours), and the + * `rc_servo_3wire` PWM control interface (pulse/frame constraints) + * - mechanical domain output spline + mounting flange resources, horn and + * mounting interfaces, 23 × 12.2 × 29 mm, 9 g + * - design_rules / validation_requirements / usage_notes / warnings / + * compatibility_notes — preserved verbatim as traits below + * Nothing below is carried over from servo folklore or SDK defaults; every + * value traces to a field of that definition. + * + * Architecture notes honoured by this file: + * - Pin-honest like a schematic: the integral 3-wire lead (S/JR connector) + * is three leaf interfaces with the ProtoPart resource ids and the + * standard wire colours as displayed names. The source gives no + * connector position numbers, so no `pin` designators are fabricated. + * - The PWM control contract (1.0-2.0 ms ↔ approx 0-180°, 0.5-2.5 ms + * extended, 20 ms frame / ~50 Hz) is carried as canonical parameters + * plus a `pwm_control_contract` trait on `rc_servo_3wire`. + * - The servo is a PWM *device* (signal sink) — the PWM() builder models + * PWM sources (roles ["output"]), so the control interface is + * hand-rolled with protocol role "device" per the ProtoPart JSON; only + * its canonical parameter helpers are reused. + * - Stall-current supply demands live in module `requirements` (dedicated + * 4.8-6.0 V rail, ≥650 mA budget, tied grounds). + * - Mechanical content (stall torque, travel, spline, flange holes, + * dimensions, mass, gear material) is carried into the mechanical + * domain, leaf interfaces, and traits — none of it is invented. + * - Shareability: GND is inherently shareable and must be common with the + * PWM source's ground (`net_shareable` trait). + */ + +import type { + InterfaceDef, + ModuleDef, + TraitDef, +} from "../../src/types/index.js"; +import { + Ground, + PowerIn, + defineModule, + clockFreqHz, + voltageRangeV, +} from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Source citation + electrical constants — ProtoPart electrical domain +// --------------------------------------------------------------------------- + +const SOURCE = + "ProtoPart towerpro-sg90 definition v0.2.0 (Tower Pro SG90 product page: https://towerpro.com.tw/product/sg90-analog/)"; + +/** Power domain "servo_5v": supply range for logic and motor. */ +const SUPPLY_RANGE_V: [number, number] = [4.8, 6]; +const SUPPLY_NOMINAL_V = 5; +/** Power domain "servo_5v": stall current up to ~650 mA at 6 V. */ +const STALL_CURRENT_A = 0.65; + +/** Signal lead `voltage_tolerance_V`. */ +const SIGNAL_TOLERANCE_V: [number, number] = [3, 6]; +/** Signal lead `logic_levels_V.low_max`. */ +const V_IL_MAX = 0.8; +/** Signal lead `logic_levels_V.high_min`. */ +const V_IH_MIN = 2.3; + +/** rc_servo_3wire constraints: nominal pulse width, 1.0-2.0 ms → approx 0-180°. */ +const PULSE_NOMINAL_US: [number, number] = [1000, 2000]; +/** rc_servo_3wire constraints: extended pulse widths accepted, unit-dependent. */ +const PULSE_EXTENDED_US: [number, number] = [500, 2500]; +/** rc_servo_3wire constraints: frame_period_ms. */ +const FRAME_PERIOD_MS = 20; +/** "1-2 ms pulse at ~50 Hz controls angle" (RC_PWM function) — 20 ms frame. */ +const FRAME_RATE_HZ = 50; + +/** Every lead resource declares `connector_type: "3-pin-servo"`. */ +function servoLeadConnector(wireColors: string[]): TraitDef { + return { + type: "connector", + params: { + connector_type: "3-pin-servo", + wire_colors: wireColors, + note: "Integral lead terminated in a 3-pin S/JR-style servo connector (electrical domain metadata: 'Micro servo with 3-pin S/JR connector'). The source gives no position numbering.", + }, + }; +} + +/** Verbatim ProtoPart resource function list — display data, mirrored from the JSON. */ +function sg90Functions( + functions: Array<{ name: string; direction: string; signal_class: string; description: string }>, +): TraitDef { + return { type: "sg90_functions", params: { source: SOURCE, functions } }; +} + +// --------------------------------------------------------------------------- +// The 3-wire lead — ProtoPart electrical resources, one leaf per wire +// --------------------------------------------------------------------------- + +/** Resource `lead_red_vin`: POWER_IN, sink, max continuous 650 mA. */ +const leadVin: InterfaceDef = { + ...PowerIn({ + id: "lead_red_vin", + name: "V+ (Red)", + voltageV: SUPPLY_RANGE_V, + nominalV: SUPPLY_NOMINAL_V, + maxCurrentA: STALL_CURRENT_A, + }), + capabilities: ["power_in"], + traits: [ + sg90Functions([ + { name: "POWER_IN", direction: "sink", signal_class: "power", description: "Supply 4.8–6.0 V" }, + ]), + { + type: "power_domain", + params: { + domain: "servo_5v", + description: "Primary supply for servo logic and motor. Stall current up to ~650 mA at 6 V.", + }, + }, + servoLeadConnector(["red"]), + { + type: "current_rating_basis", + params: { + note: "650 mA is the stall-current ceiling at 6 V, not a typical draw — validation expects no-load current <60 mA at 6 V and stall current <650 mA at 6 V.", + source: SOURCE, + }, + }, + ], +}; + +/** Resource `lead_brown_gnd`: GROUND, 0 V return. */ +const leadGnd: InterfaceDef = { + ...Ground({ id: "lead_brown_gnd", name: "GND (Brown/Black)" }), + traits: [ + sg90Functions([ + { name: "GROUND", direction: "sink", signal_class: "power", description: "0 V return" }, + ]), + { + type: "power_domain", + params: { + domain: "servo_5v", + note: "ground_reference: system_ground; isolation_type: non_isolated.", + }, + }, + servoLeadConnector(["brown", "black"]), + { + // Shareability: ground is one net, and it must be common with the PWM + // source even when the servo runs from its own supply. + type: "net_shareable", + params: { + net: "gnd", + policy: "single_ground_instance_may_serve_all_members", + note: "'Power servos from a separate 5–6 V supply; tie grounds.' / 'ensure common ground.'", + }, + }, + ], +}; + +/** Resource `lead_orange_sig`: RC_PWM input, 3-6 V tolerant, TTL-ish thresholds. */ +const leadSignal: InterfaceDef = { + id: "lead_orange_sig", + name: "Signal (Orange/Yellow/White)", + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "pwm", roles: ["input"] }], + capabilities: ["rc_pwm_in"], + parameters: [ + voltageRangeV(SIGNAL_TOLERANCE_V[0], SIGNAL_TOLERANCE_V[1]), // voltage_tolerance_V + { id: "v_il_max", name: "Logic low, max (V_IL)", unit: "V", value: V_IL_MAX }, + { id: "v_ih_min", name: "Logic high, min (V_IH)", unit: "V", value: V_IH_MIN }, + ], + traits: [ + sg90Functions([ + { name: "RC_PWM", direction: "input", signal_class: "pwm", description: "1–2 ms pulse at ~50 Hz controls angle" }, + ]), + { type: "power_domain", params: { domain: "servo_5v" } }, + servoLeadConnector(["orange", "yellow", "white"]), + { + type: "logic_compatibility", + params: { + note: "Accepts 3.3 V or 5 V logic PWM in most cases; ensure common ground. Some clones require 5 V-high.", + source: SOURCE, + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// RC servo control — ProtoPart electrical interface `rc_servo_3wire` +// --------------------------------------------------------------------------- + +// The servo consumes PWM (protocol role "device" in the source JSON); the +// PWM() builder emits source-side units (roles ["output"]) and so does not +// fit — hand-rolled, with the canonical parameter helpers reused. +const rcServoControl: InterfaceDef = { + id: "rc_servo_3wire", + name: "RC servo PWM control", + domain: "electrical", + exposed: true, + default_active: true, // the 3-wire lead is integral — always present + protocols: [{ type: "pwm", roles: ["device"] }], + slots: [ + { id: "power", required: true, match: { protocol: "power", role: "input", capability: "power_in" } }, + { id: "ground", required: true, match: { protocol: "power", role: "ground", capability: "ground" } }, + { id: "signal", required: true, match: { protocol: "pwm", role: "input", capability: "rc_pwm_in" } }, + ], + profiles: [ + { + id: "rc_servo_3wire_lead", + label: "Integral 3-wire lead (S/JR connector)", + default_active: true, + bindings: { power: "lead_red_vin", ground: "lead_brown_gnd", signal: "lead_orange_sig" }, + }, + ], + parameters: [ + clockFreqHz(FRAME_RATE_HZ), // "~50 Hz" per the RC_PWM function; 20 ms frame + { id: "frame_period", name: "PWM frame period", unit: "ms", value: FRAME_PERIOD_MS }, + { id: "pulse_width", name: "Nominal pulse width", unit: "µs", range: PULSE_NOMINAL_US }, + { id: "pulse_width_extended", name: "Extended pulse width (unit-dependent)", unit: "µs", range: PULSE_EXTENDED_US }, + ], + max_instances: 1, // one physical lead + traits: [ + { + type: "pwm_control_contract", + params: { + pulse_width_us: PULSE_NOMINAL_US, + mapping: "Standard 3-wire RC servo interface. 1.0–2.0 ms → approx 0–180°.", + pulse_extension_us: PULSE_EXTENDED_US, + extension_note: "Accepts extended pulse widths 0.5–2.5 ms depending on unit.", + frame_period_ms: FRAME_PERIOD_MS, + neutral_check: "Verify neutral at 1500 µs pulse within ±10° (validation requirement).", + source: SOURCE, + }, + }, + { + type: "calibration_note", + params: { + note: "Generate 50 Hz PWM with 1–2 ms pulses for nominal 0–180°; calibrate endpoints to avoid stall. Sweep 1000–2000 µs and confirm no mechanical binding.", + source: SOURCE, + }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// Mechanical — ProtoPart mechanical resources and interfaces +// --------------------------------------------------------------------------- + +/** Resource `servo_output_spline`: SERVO_OUTPUT, mechanical_drive source. */ +const outputSpline: InterfaceDef = { + id: "servo_output_spline", + name: "Output spline", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_drive", roles: ["source"] }], + capabilities: ["servo_output"], + parameters: [ + // metadata.description: "~1.2–1.8 kg·cm stall torque depending on source" + { id: "stall_torque", name: "Stall torque (source-dependent)", unit: "kg·cm", range: [1.2, 1.8] }, + // metadata.description: "~180° travel" + { id: "travel", name: "Rotational travel (approx.)", unit: "°", value: 180 }, + ], + traits: [ + sg90Functions([ + { name: "SERVO_OUTPUT", direction: "source", signal_class: "mechanical_drive", description: "Rotational output shaft for horn" }, + ]), + { + type: "connector", + params: { + connector_type: "spline", + note: "Accepts included plastic horns; micro spline (often ~21T, varies by vendor).", + }, + }, + { + type: "gear_train", + params: { + material: "plastic", + note: "Plastic gears. Avoid shock loads. Do not backdrive at power-off. Hold torque depends on supply voltage.", + source: SOURCE, + }, + }, + ], +}; + +/** Resource `mounting_flange`: MECHANICAL_MOUNT, bidirectional — both ears are one resource. */ +const mountingFlange: InterfaceDef = { + id: "mounting_flange", + name: "Mount flange", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_mount", roles: ["bidirectional"] }], + capabilities: ["mechanical_mount"], + traits: [ + sg90Functions([ + { name: "MECHANICAL_MOUNT", direction: "bidirectional", signal_class: "mechanical_mount", description: "Two mounting ears with holes" }, + ]), + { + type: "connector", + params: { + connector_type: "through_hole", + note: "Two Ø2.2 mm self-tapping screw holes on ears.", + }, + }, + ], +}; + +/** ProtoPart mechanical interface `horn_interface`. */ +const hornInterface: InterfaceDef = { + id: "horn_interface", + name: "Horn attachment", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["shaft"] }], + slots: [ + { id: "output", required: true, match: { capability: "servo_output" } }, + ], + profiles: [ + { id: "horn_spline", label: "Output spline", default_active: true, bindings: { output: "servo_output_spline" } }, + ], + max_instances: 1, + traits: [ + { + type: "assembly_requirement", + params: { note: "Attach horns with included M2 screw. Ensure travel limits are respected.", source: SOURCE }, + }, + ], +}; + +/** ProtoPart mechanical interface `mounting_interface` (requires MECHANICAL_MOUNT ×2). */ +const mountingInterface: InterfaceDef = { + id: "mounting_interface", + name: "Servo mounting ears", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["mounting_point"] }], + slots: [ + // The source requires the MECHANICAL_MOUNT function twice (two ears). + { id: "ear", required: true, count: 2, match: { capability: "mechanical_mount" } }, + ], + profiles: [ + { + id: "mounting_ears", + label: "Flange ears (both holes)", + default_active: true, + // The ProtoPart models both ears as the single `mounting_flange` + // resource — one leaf carries both Ø2.2 mm holes. + bindings: { ear: "mounting_flange" }, + }, + ], + max_instances: 1, + traits: [ + { + type: "assembly_requirement", + params: { note: "Use included self-tapping screws. Do not overtighten.", source: SOURCE }, + }, + ], +}; + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const TOWERPRO_SG90: ModuleDef = defineModule({ + id: "towerpro-sg90", + name: "Tower Pro SG90", + version: "0.2.0", + manufacturer: "Tower Pro", + part_number: "SG90", + description: + "9 g analog micro servo for RC and robotics. ~180° travel, 4.8–6.0 V supply, ~1.2–1.8 kg·cm stall torque depending on source.", + tags: ["sg90", "micro-servo", "rc-servo", "9g", "pwm"], + categories: ["actuator.motor.servo", "robotics.rc"], + + interfaces: [ + // The 3-wire lead — schematic-honest leaves (source declares no + // connector position numbers, so none are shown). + leadVin, + leadGnd, + leadSignal, + + // Control + rcServoControl, + + // Mechanical + outputSpline, + mountingFlange, + hornInterface, + mountingInterface, + ], + + interfaceGroups: [ + { + id: "servo_lead_3pin", + label: "3-wire servo lead (S/JR connector)", + members: ["lead_red_vin", "lead_brown_gnd", "lead_orange_sig"], + policy: "all_of", + }, + ], + + requirements: [ + { + type: "power", + description: + "Supply 4.8–6.0 V on a dedicated rail; avoid powering from an MCU 5 V pin. Budget peak (stall) current ≥650 mA per servo, and tie the servo-supply ground to the PWM source's ground.", + voltage_V: SUPPLY_RANGE_V, + current_mA: 650, + }, + { + type: "interface", + description: + "Control requires a ~50 Hz PWM source delivering 1–2 ms pulses for nominal 0–180°; calibrate endpoints to avoid stall. 3.3 V or 5 V logic accepted in most cases (some clones require 5 V-high).", + interface_protocol: "pwm", + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + { + id: "servo_5v", + name: "Servo 5V Rail", + nominal_voltage_V: SUPPLY_NOMINAL_V, + voltage_range_V: SUPPLY_RANGE_V, + max_current_mA: 650, + regulation_type: "unregulated", + }, + ], + metadata: { + pin_count: 3, + package_type: "Micro servo with 3-pin S/JR connector", + supply_voltage_V: SUPPLY_RANGE_V, + power_consumption_mW: 300, + // PowerDomainDef has no home for these source fields: + power_domain_detail: { + isolation_type: "non_isolated", + ground_reference: "system_ground", + description: "Primary supply for servo logic and motor. Stall current up to ~650 mA at 6 V.", + }, + source: SOURCE, + }, + }, + { + domain: "mechanical", + dimensions_mm: { length: 23, width: 12.2, height: 29 }, + weight_g: 9, + metadata: { + package_type: "9g micro servo", + mounting_method: "Flange ears with two screws", + source: SOURCE, + }, + }, + ], + + traits: [ + { + // metadata.description figures — the only torque/travel data the + // source carries (no transit-speed spec exists in the definition). + type: "servo_characteristics", + params: { + drive_type: "analog", + stall_torque_kg_cm: [1.2, 1.8], + stall_torque_note: "depending on source", + travel_deg_approx: 180, + source: SOURCE, + }, + }, + { + type: "design_rules", + params: { + rules: [ + "Supply 4.8–6.0 V on dedicated rail; avoid powering from MCU 5V pin.", + "Budget peak current ≥650 mA per servo.", + "Generate 50 Hz PWM with 1–2 ms pulses for nominal 0–180°; calibrate endpoints to avoid stall.", + ], + source: SOURCE, + }, + }, + { + type: "validation_requirements", + params: { + checks: [ + "Verify neutral at 1500 µs pulse within ±10°.", + "Sweep 1000–2000 µs and confirm no mechanical binding.", + "Measure no-load current <60 mA at 6 V and stall current <650 mA at 6 V.", + ], + source: SOURCE, + }, + }, + { + type: "usage_notes", + params: { + note: "Plastic gears. Avoid shock loads. Do not backdrive at power-off. Hold torque depends on supply voltage.", + source: SOURCE, + }, + }, + { + type: "compatibility_notes", + params: { + note: "Accepts 3.3 V or 5 V logic PWM in most cases; ensure common ground. Some clones require 5 V-high.", + source: SOURCE, + }, + }, + { + type: "warnings", + params: { + warnings: [ + "Do not exceed travel limits. Stall can overheat motor.", + "Power servos from a separate 5–6 V supply; tie grounds.", + "Avoid continuous stall or hammering; plastic gear wear will accelerate.", + ], + source: SOURCE, + }, + }, + { + type: "application_examples", + params: { + examples: ["RC airplane control surface", "Lightweight gimbal tilt", "Small robotics gripper"], + source: SOURCE, + }, + }, + ], + + artifacts: [ + { + id: "art_datasheet", + name: "Tower Pro SG90 Product Page", + type: "datasheet", + url: "https://towerpro.com.tw/product/sg90-analog/", + }, + { + id: "art_thumbnail", + name: "Thumbnail", + type: "custom", + filePath: "./ProtoPart/protoparts/towerpro-sg90/thumbnail.png", + mimeType: "image/png", + description: "ProtoPart preview artifact (previewArtifactId).", + tags: ["image", "thumbnail", "preview"], + }, + ], + + geometry: { + xScale: 1, + yScale: 1, + outline: { preset: "rectangle" }, + }, +}); diff --git a/library/parts/towerpro-sg90/artifacts/thumbnail.png b/library/parts/towerpro-sg90/artifacts/thumbnail.png new file mode 100644 index 0000000..6857932 Binary files /dev/null and b/library/parts/towerpro-sg90/artifacts/thumbnail.png differ diff --git a/library/parts/vl53l0x.ts b/library/parts/vl53l0x.ts new file mode 100644 index 0000000..8b17802 --- /dev/null +++ b/library/parts/vl53l0x.ts @@ -0,0 +1,721 @@ +/** + * ST VL53L0X — datasheet-honest part definition. + * + * Primary source: ProtoPart audited definition `vl53l0x` (schema 1.4.0, + * v1.0), itself audited from the ST VL53L0X datasheet + * (https://www.st.com/resource/en/datasheet/vl53l0x.pdf): + * - Electrical domain: single `avdd_main` power domain (2.8 V nominal, + * 2.6-3.5 V) feeding both AVDD and AVDDVCSEL; 12-pad Optical LGA-12 + * pinout; I2C target (400 kHz max, default 7-bit address 0x29); + * XSHUT hardware-standby input; GPIO1 open-drain interrupt output. + * - Design rules: shared AVDD/AVDDVCSEL supply, 100 nF + 4.7 uF + * decoupling, 1.5-2 kOhm I2C pull-ups (2.8 V @ 400 kHz, once per bus), + * XSHUT must always be driven, DNC pin 8 floating / GPIO1 unconnected + * when unused. + * - Warnings: Class 1 laser (IEC 60825-1:2014), reflectance-dependent + * range, XSHUT-low disables I2C. + * Nothing below is carried over from convention or SDK defaults; values not + * present in the ProtoPart JSON (e.g. supply current, per-pin logic levels) + * are omitted rather than invented. + * + * Architecture notes honoured by this file: + * - Pin-honest like a schematic: all 12 physical pads are leaf interfaces, + * in package order, with ids "pin_N" and the datasheet pad name as the + * displayed name. Pad 8 (DNC) is modelled explicitly even though the + * ProtoPart resources list omits it — the design rules document it + * ("Leave DNC pin 8 floating"), and a schematic-honest part shows every + * pad. + * - Every pad carries its ProtoPart function list (name, direction, + * signal class) in a `vl53l0x_pin_functions` trait — display data is + * separated from the canonical capability tags the matching engine needs. + * - Buses are separate composed interfaces: the I2C target binds SDA/SCL + * through slots + a default profile; XSHUT and GPIO1 get `gpio_control` + * / `gpio_interrupt` composed interfaces mirroring the ProtoPart + * `interfaces` block. + * - Co-requirements (`co_requirement` traits): XSHUT low forces hardware + * standby and disables I2C. + * - Implied harness connections (`implied_passives` traits): supply + * decoupling (100 nF + 4.7 uF), I2C bus pull-ups (1.5-2 kOhm), the + * conditional XSHUT pull-up, the conditional GPIO1 pull-up. + * - Shareability exemptions (`net_shareable` traits): AVDD and AVDDVCSEL + * sit on one supply net (a single supply instance may serve both); + * AVSSVCSEL plus all GND pads are one ground net; SDA/SCL are multi-drop + * bus lines (the ProtoPart requires-block marks them shareable with each + * other). + */ + +import type { + InterfaceDef, + ModuleDef, + SlotDef, + TraitDef, +} from "../../src/types/index.js"; +import type { Parameter } from "../../src/types/parameter.js"; +import { + Ground, + I2C, + PowerIn, + defineModule, +} from "../../src/protocols/index.js"; + +// --------------------------------------------------------------------------- +// Electrical constants — ProtoPart electrical domain +// --------------------------------------------------------------------------- + +/** `avdd_main` power domain: single external supply feeding AVDD and AVDDVCSEL. */ +const AVDD_RANGE: [number, number] = [2.6, 3.5]; +const AVDD_NOMINAL_V = 2.8; +/** `timing.max_i2c_freq_hz` / electrical metadata `i2c_max_frequency_hz`. */ +const I2C_MAX_FREQ_HZ = 400_000; +/** Electrical metadata `i2c_address_7bit` (0x29; 8-bit write form 0x52). */ +const I2C_ADDRESS_7BIT = 0x29; + +const SOURCE = "ST VL53L0X datasheet, via ProtoPart audited definition vl53l0x v1.0"; +const DESIGN_RULES_SOURCE = `${SOURCE} (design_rules)`; + +// --------------------------------------------------------------------------- +// Datasheet-honest per-pin function metadata +// --------------------------------------------------------------------------- + +interface Vl53l0xFunction { + /** Verbatim function name from the ProtoPart resource entry. */ + name: string; + /** ProtoPart `direction` field, carried verbatim. */ + direction: "input" | "output" | "bidirectional" | "sink" | "source"; + /** ProtoPart `signal_class` field, carried verbatim. */ + signal_class: "power" | "ground" | "data" | "clock"; +} + +/** Preserve a pad's ProtoPart function list + description as display data. */ +function pinFunctions(functions: Vl53l0xFunction[], description: string): TraitDef { + return { + type: "vl53l0x_pin_functions", + params: { source: SOURCE, functions, description }, + }; +} + +// --------------------------------------------------------------------------- +// Power and ground pads — Optical LGA-12, in package pin order +// --------------------------------------------------------------------------- + +/** AVSSVCSEL + all GND pads sit on the main ground net (design rule 1). */ +const GROUND_NET_MEMBERS = ["pin_2", "pin_3", "pin_4", "pin_6", "pin_12"]; + +function groundPin(pinNo: number, name: string, description: string): InterfaceDef { + return { + ...Ground({ id: `pin_${pinNo}`, name, pin: pinNo }), + traits: [ + pinFunctions([{ name: "ground", direction: "sink", signal_class: "ground" }], description), + // The ProtoPart resource assigns ground pads to the avdd_main domain + // (ground_reference: "common"). + { type: "power_domain", params: { domain: "avdd_main" } }, + { + type: "net_shareable", + params: { + net: "gnd", + policy: "single_ground_instance_may_serve_all_members", + members: GROUND_NET_MEMBERS, + note: "Connect AVSSVCSEL plus all GND pins to the main ground.", + source: DESIGN_RULES_SOURCE, + }, + }, + ], + }; +} + +/** AVDDVCSEL (pin 1) and AVDD (pin 11) share one decoupled 2.6-3.5 V net. */ +function supplyPin(pinNo: number, name: string, description: string, decouplingConnection: string): InterfaceDef { + const base = PowerIn({ + id: `pin_${pinNo}`, + name, + pin: pinNo, + voltageV: AVDD_RANGE, + nominalV: AVDD_NOMINAL_V, + }); + return { + ...base, + traits: [ + pinFunctions([{ name: "power_in", direction: "sink", signal_class: "power" }], description), + { type: "power_domain", params: { domain: "avdd_main" } }, + { + // Shareability exemption: one supply instance may legally serve both + // supply pads — the datasheet mandates a single shared rail. + type: "net_shareable", + params: { + net: "vl53l0x_avdd", + policy: "single_supply_instance_may_serve_all_members", + members: ["pin_1", "pin_11"], + note: "Power AVDD and AVDDVCSEL from the same 2.6 V to 3.5 V supply.", + source: DESIGN_RULES_SOURCE, + }, + }, + { + type: "implied_passives", + params: { + purpose: "Supply decoupling", + components: [ + { kind: "capacitor", value: "100 nF", connection: decouplingConnection }, + { kind: "capacitor", value: "4.7 uF", connection: decouplingConnection }, + ], + source: `${DESIGN_RULES_SOURCE}: 'Place 100 nF and 4.7 uF decoupling capacitors as close as possible to the AVDDVCSEL/AVSSVCSEL and AVDD pins.'`, + }, + }, + ], + }; +} + +// --------------------------------------------------------------------------- +// Signal and service pads +// --------------------------------------------------------------------------- + +const xshutPad: InterfaceDef = { + id: "pin_5", + name: "XSHUT", + pin: 5, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input"] }], + capabilities: ["shutdown_ctrl"], + traits: [ + pinFunctions( + [{ name: "shutdown_ctrl", direction: "input", signal_class: "data" }], + "Active-low hardware shutdown input.", + ), + { + type: "drive_requirement", + params: { + rule: "Drive XSHUT at all times to avoid leakage current.", + source: DESIGN_RULES_SOURCE, + }, + }, + { + type: "implied_passives", + params: { + purpose: "Guaranteed XSHUT level while the host is in reset", + components: [ + { kind: "resistor", value: "10 kOhm (recommended)", connection: "XSHUT to the main supply" }, + ], + condition: "Add a pull-up if the host state during reset is not guaranteed.", + source: `${DESIGN_RULES_SOURCE}; value per ST VL53L0X datasheet DS11555 Rev 2 Figure 3 note: 'XSHUT and GPIO1 pull up recommended values are 10k Ohms'`, + }, + }, + { + type: "co_requirement", + params: { + with: "i2c_target", + condition: "XSHUT held low", + effect: "Holding XSHUT low forces hardware standby and disables I2C communication.", + source: `${SOURCE} (warnings / shutdown_in interface)`, + }, + }, + ], +}; + +const gpio1Pad: InterfaceDef = { + id: "pin_7", + name: "GPIO1", + pin: 7, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["output"] }], + capabilities: ["interrupt_out"], + traits: [ + pinFunctions( + [{ name: "interrupt", direction: "output", signal_class: "data" }], + "Open-drain interrupt output.", + ), + { + type: "output_driver", + params: { + drive: "open_drain", + note: "GPIO1 is open-drain and requires a pull-up if used.", + source: `${SOURCE} (compatibility_notes)`, + }, + }, + { + type: "implied_passives", + params: { + purpose: "Open-drain interrupt line pull-up", + components: [ + { kind: "resistor", value: "10 kOhm (recommended)", connection: "GPIO1 to the bus/IO supply" }, + ], + condition: "Only when interrupt signaling is used.", + source: `${SOURCE} (compatibility_notes); value per ST VL53L0X datasheet DS11555 Rev 2 Figure 3 note: 'XSHUT and GPIO1 pull up recommended values are 10k Ohms'`, + }, + }, + { + type: "optionality", + params: { + note: "Leave GPIO1 unconnected if interrupt signaling is not used.", + source: DESIGN_RULES_SOURCE, + }, + }, + ], +}; + +// Pad 8 is absent from the ProtoPart resources list; the design rules name +// it DNC and require it floating, so it is modelled as an unconnectable pad. +const dncPad: InterfaceDef = { + id: "pin_8", + name: "DNC", + pin: 8, + domain: "electrical", + exposed: false, + default_active: false, + protocols: [], + capabilities: ["do_not_connect"], + traits: [ + { + type: "usage_restriction", + params: { + restriction: "Leave DNC pin 8 floating.", + source: DESIGN_RULES_SOURCE, + }, + }, + ], +}; + +const sdaPad: InterfaceDef = { + id: "pin_9", + name: "SDA", + pin: 9, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input", "output", "bidirectional"] }], + capabilities: ["i2c_sda"], + traits: [ + pinFunctions([{ name: "i2c_sda", direction: "bidirectional", signal_class: "data" }], "I2C serial data."), + ], +}; + +// SCL is an input on this target device (the ProtoPart function direction is +// "input") — no output roles are claimed. +const sclPad: InterfaceDef = { + id: "pin_10", + name: "SCL", + pin: 10, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input"] }], + capabilities: ["i2c_scl"], + traits: [ + pinFunctions([{ name: "i2c_scl", direction: "input", signal_class: "clock" }], "I2C serial clock input."), + ], +}; + +/** All 12 pads in physical package order — schematic-honest. */ +const pins: InterfaceDef[] = [ + supplyPin(1, "AVDDVCSEL", "VCSEL supply pin connected to the main supply.", "close to the AVDDVCSEL/AVSSVCSEL pins"), + groundPin(2, "AVSSVCSEL", "VCSEL ground return."), + groundPin(3, "GND", "Main ground."), + groundPin(4, "GND2", "Ground."), + xshutPad, + groundPin(6, "GND3", "Ground."), + gpio1Pad, + dncPad, + sdaPad, + sclPad, + supplyPin(11, "AVDD", "Main supply input.", "close to the AVDD pin"), + groundPin(12, "GND4", "Ground."), +]; + +// --------------------------------------------------------------------------- +// Composed interfaces — ProtoPart electrical `interfaces` block +// --------------------------------------------------------------------------- + +function withTraits(iface: InterfaceDef, traits: TraitDef[]): InterfaceDef { + return { ...iface, traits: [...(iface.traits ?? []), ...traits] }; +} + +/** Attach traits to the interface with the given id inside a builder result. */ +function amend(ifaces: InterfaceDef[], id: string, traits: TraitDef[]): InterfaceDef[] { + return ifaces.map((i) => (i.id === id ? withTraits(i, traits) : i)); +} + +function composed(config: { + id: string; + name: string; + protocolType: string; + roles: string[]; + slots: SlotDef[]; + profiles?: InterfaceDef["profiles"]; + parameters?: Parameter[]; + maxInstances?: number; + defaultActive?: boolean; + traits?: TraitDef[]; + domain?: InterfaceDef["domain"]; +}): InterfaceDef { + return { + id: config.id, + name: config.name, + domain: config.domain ?? "electrical", + exposed: true, + default_active: config.defaultActive ?? false, + protocols: [{ type: config.protocolType, roles: config.roles }], + slots: config.slots, + ...(config.profiles ? { profiles: config.profiles } : {}), + ...(config.parameters ? { parameters: config.parameters } : {}), + ...(config.maxInstances !== undefined ? { max_instances: config.maxInstances } : {}), + ...(config.traits ? { traits: config.traits } : {}), + }; +} + +// I2C target — ST terminology is "target"; the OpenUHD I2C role vocabulary +// expresses that as "slave". SDA/SCL are fixed pads, so the single profile is +// the honest combination space. +const I2C_TARGET_TRAITS: TraitDef[] = [ + { + type: "i2c_addressing", + params: { + default_address_7bit: "0x29", + default_address_8bit_write: "0x52", + programmable: true, + note: + "Programmable I2C address; default 7-bit address 0x29. ST documents the default as 0x52 in 8-bit form, which corresponds to 7-bit address 0x29 used by most host SDKs.", + source: `${SOURCE} (i2c_target interface / compatibility_notes)`, + }, + }, + { + type: "implied_passives", + params: { + purpose: "Open-drain bus pull-ups", + components: [ + { + kind: "resistor", + value: "1.5-2 kOhm (ST recommendation for 2.8 V operation at 400 kHz)", + connection: "SDA and SCL to the bus supply — fit only once per bus", + }, + ], + source: DESIGN_RULES_SOURCE, + }, + }, + { + // The ProtoPart requires-block marks i2c_sda and i2c_scl as shareable + // with each other: one shared bus harness may carry both functions, and + // the bus itself is multi-drop. + type: "net_shareable", + params: { + net: "i2c_bus", + policy: "multi_drop_bus_shared_across_targets", + note: + "Multi-sensor arrays on one bus are typically managed by holding devices in hardware standby through XSHUT, then assigning unique addresses during initialization.", + source: `${SOURCE} (requires.shareable_with / usage_notes)`, + }, + }, + { + type: "co_requirement", + params: { + with: "pin_5", + condition: "XSHUT held low", + effect: "I2C is unavailable while XSHUT is held low (hardware standby).", + source: `${SOURCE} (shutdown_in interface / warnings)`, + }, + }, +]; + +const i2cTarget = amend( + I2C({ + id: "i2c_target", + name: "I2C target interface", + roles: ["slave"], + clockFreqHz: I2C_MAX_FREQ_HZ, + address: I2C_ADDRESS_7BIT, + sda: "pin_9", + scl: "pin_10", + maxInstances: 1, + defaultActive: true, + }), + "i2c_target", + I2C_TARGET_TRAITS, +); + +const interruptOut = composed({ + id: "interrupt_out", + name: "Interrupt output", + protocolType: "gpio_interrupt", + roles: ["source"], + slots: [{ id: "int", required: true, match: { capability: "interrupt_out" } }], + profiles: [ + { id: "interrupt_out_gpio1", label: "GPIO1 (pin 7)", bindings: { int: "pin_7" } }, + ], + maxInstances: 1, + defaultActive: false, // optional — leave GPIO1 unconnected when unused + traits: [ + { + type: "output_driver", + params: { + drive: "open_drain", + note: "GPIO1 open-drain interrupt output — requires a pull-up if used.", + source: `${SOURCE} (interrupt_out interface / compatibility_notes)`, + }, + }, + { + type: "optionality", + params: { + note: "Leave GPIO1 unconnected if interrupt signaling is not used.", + source: DESIGN_RULES_SOURCE, + }, + }, + ], +}); + +const shutdownIn = composed({ + id: "shutdown_in", + name: "Hardware standby control", + protocolType: "gpio_control", + roles: ["sink"], + slots: [{ id: "xshut", required: true, match: { capability: "shutdown_ctrl" } }], + profiles: [ + { id: "shutdown_in_xshut", label: "XSHUT (pin 5)", default_active: true, bindings: { xshut: "pin_5" } }, + ], + maxInstances: 1, + defaultActive: true, // XSHUT must be driven at all times (design rule) + traits: [ + { + type: "co_requirement", + params: { + with: "i2c_target", + condition: "XSHUT driven low", + effect: "Drive XSHUT low for hardware standby; I2C is unavailable while held low.", + source: `${SOURCE} (shutdown_in interface)`, + }, + }, + { + type: "multi_sensor_usage", + params: { + note: + "Multi-sensor arrays on one bus are typically managed by holding devices in hardware standby through XSHUT, then assigning unique addresses during initialization.", + source: `${SOURCE} (usage_notes)`, + }, + }, + ], +}); + +// --------------------------------------------------------------------------- +// Mechanical +// --------------------------------------------------------------------------- + +const footprintMount: InterfaceDef = { + id: "footprint_mounting", + name: "Optical LGA-12 4.4×2.4 mm Surface-Mount Footprint", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["mounting_point"] }], + capabilities: ["lga12_optical", "surface_mount"], +}; + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export const VL53L0X: ModuleDef = defineModule({ + id: "vl53l0x", + name: "ST VL53L0X Time-of-Flight Ranging Sensor", + version: "1.0.0", + manufacturer: "STMicroelectronics", + part_number: "VL53L0X", + description: + "Single-zone 940 nm laser time-of-flight ranging sensor with embedded SPAD array, programmable I2C target interface, XSHUT hardware standby input, and GPIO1 interrupt output.", + tags: ["vl53l0x", "tof", "distance", "ranging", "i2c", "0x29", "xshut", "gpio1", "laser", "stmicroelectronics"], + categories: ["sensor.distance"], + + interfaces: [ + // All 12 physical pads in package order — schematic-honest. + ...pins, + + // Bus / control interfaces (ProtoPart electrical `interfaces` block) + ...i2cTarget, + interruptOut, + shutdownIn, + + // Mechanical + footprintMount, + ], + + interfaceGroups: [ + { + id: "required_power_pins", + label: "Required Power Pins", + members: ["pin_1", "pin_2", "pin_3", "pin_4", "pin_6", "pin_11", "pin_12"], + policy: "all_of", + }, + { + id: "avdd_common_net", + label: "Supply Pins (one shared 2.6-3.5 V net)", + members: ["pin_1", "pin_11"], + policy: "all_of", + }, + { + id: "i2c_bus_pins", + label: "I2C Bus Pins", + members: ["pin_9", "pin_10"], + policy: "all_of", + }, + ], + + requirements: [ + { + type: "power", + description: + "Power AVDD and AVDDVCSEL from the same 2.6 V to 3.5 V supply (2.8 V nominal) and connect AVSSVCSEL plus all GND pins to the main ground. The ProtoPart definition specifies no supply-current figure.", + voltage_V: AVDD_RANGE, + }, + { + type: "interface", + description: + "A host I2C controller (up to 400 kHz) with external pull-ups fitted only once per bus; ST recommends 1.5-2 kOhm pull-ups for 2.8 V operation at 400 kHz.", + interface_protocol: "i2c", + }, + { + type: "interface", + description: + "XSHUT must be driven at all times to avoid leakage current; add a pull-up if the host state during reset is not guaranteed.", + interface_protocol: "gpio_control", + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + { + id: "avdd_main", + name: "Main supply", + nominal_voltage_V: AVDD_NOMINAL_V, + voltage_range_V: AVDD_RANGE, + }, + ], + metadata: { + pin_count: 12, + supply_voltage_V: AVDD_RANGE, + package_type: "Optical LGA-12", + i2c_address_7bit: "0x29", + i2c_address_8bit_write: "0x52", + i2c_max_frequency_hz: I2C_MAX_FREQ_HZ, + field_of_view_deg: 25, + laser_wavelength_nm: 940, + maximum_ranging_distance_cm: 200, + typical_profile_distance_cm: 120, + // PowerDomainDef has no home for these ProtoPart power-domain fields: + power_domain_details: { + avdd_main: { + isolation_type: "non_isolated", + ground_reference: "common", + description: "Single external supply feeding both AVDD and AVDDVCSEL.", + }, + }, + source: SOURCE, + }, + }, + { + domain: "mechanical", + dimensions_mm: { length: 4.4, width: 2.4, height: 1 }, + metadata: { + package_type: "Optical LGA-12", + mounting_method: "surface_mount", + }, + }, + { + domain: "thermal", + operating_temperature_C: [-20, 70], + metadata: { + requires_thermal_management: false, + }, + }, + ], + + traits: [ + { + type: "laser_safety", + params: { + classification: "Class 1 laser device compliant with IEC 60825-1:2014", + restriction: "Do not modify the optics or aperture geometry.", + source: `${SOURCE} (warnings)`, + }, + }, + { + type: "ranging_performance", + params: { + maximum_ranging_distance_cm: 200, + typical_profile_distance_cm: 120, + field_of_view_deg: 25, + laser_wavelength_nm: 940, + caveat: + "The advertised 2 m maximum range requires favorable target reflectance and indoor conditions; darker targets and outdoor IR reduce practical range significantly. Maximum usable range depends strongly on target reflectance and ambient infrared conditions.", + source: `${SOURCE} (electrical metadata / warnings / compatibility_notes)`, + }, + }, + { + type: "usage_notes", + params: { + note: + "The VL53L0X is a PCB-integration sensor module rather than a ready-to-wire breakout. It exposes a programmable I2C target interface plus XSHUT and GPIO1 for system control. Multi-sensor arrays on one bus are typically managed by holding devices in hardware standby through XSHUT, then assigning unique addresses during initialization.", + source: SOURCE, + }, + }, + { + type: "application_examples", + params: { + examples: [ + "Wall tracking and collision avoidance for robotics.", + "Access control and presence detection.", + "Liquid level and inventory sensing.", + ], + source: SOURCE, + }, + }, + { + // Verbatim design-rule block, preserved for auditability; each rule is + // also distributed onto the pad/bus it constrains as a structured trait. + type: "design_rules", + params: { + rules: [ + "Power AVDD and AVDDVCSEL from the same 2.6 V to 3.5 V supply and connect AVSSVCSEL plus all GND pins to the main ground.", + "Place 100 nF and 4.7 uF decoupling capacitors as close as possible to the AVDDVCSEL/AVSSVCSEL and AVDD pins.", + "Use external I2C pull-ups only once per bus; ST recommends 1.5 kOhm to 2 kOhm pull-ups for 2.8 V operation at 400 kHz.", + "Drive XSHUT at all times to avoid leakage current; add a pull-up if the host state during reset is not guaranteed.", + "Leave DNC pin 8 floating and leave GPIO1 unconnected if interrupt signaling is not used.", + ], + source: SOURCE, + }, + }, + { + type: "preview_artifact", + params: { artifactId: "art_thumbnail" }, + }, + ], + + artifacts: [ + { + id: "art_datasheet", + name: "ST VL53L0X Datasheet", + type: "datasheet", + url: "https://www.st.com/resource/en/datasheet/vl53l0x.pdf", + }, + { + id: "art_product_page", + name: "ST VL53L0X Product Page", + type: "documentation", + url: "https://www.st.com/en/imaging-and-photonics-solutions/vl53l0x.html", + }, + { + id: "art_thumbnail", + name: "Thumbnail", + type: "custom", + filePath: "./ProtoPart/protoparts/vl53l0x/thumbnail.png", + mimeType: "image/png", + tags: ["image", "thumbnail"], + }, + ], + + geometry: { + xScale: 1, + yScale: 1, + outline: { preset: "rectangle" }, + }, +}); diff --git a/library/parts/vl53l0x/artifacts/thumbnail.png b/library/parts/vl53l0x/artifacts/thumbnail.png new file mode 100644 index 0000000..0004a70 Binary files /dev/null and b/library/parts/vl53l0x/artifacts/thumbnail.png differ diff --git a/package-lock.json b/package-lock.json index b0625a6..f50096b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/node": "^25.6.0", + "tsx": "^4.23.1", "typescript": "^5.7.0", "vitest": "^3.1.0" } @@ -1406,6 +1407,509 @@ "node": ">=14.0.0" } }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/package.json b/package.json index cc7ad69..475e753 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "files": [ "dist", "src", + "library", "docs/**/*.md", "LICENSE", "NOTICE", @@ -25,6 +26,8 @@ ], "scripts": { "build": "tsc -p tsconfig.build.json", + "build:library": "tsx scripts/build-library.ts", + "prepack": "npm run build && npm run build:library", "test": "vitest run", "test:watch": "vitest", "type-check": "tsc --noEmit" @@ -54,6 +57,7 @@ "homepage": "https://github.com/Delta-Robotics-Inc/uhd#readme", "devDependencies": { "@types/node": "^25.6.0", + "tsx": "^4.23.1", "typescript": "^5.7.0", "vitest": "^3.1.0" } diff --git a/scripts/build-library.ts b/scripts/build-library.ts new file mode 100644 index 0000000..dc92122 --- /dev/null +++ b/scripts/build-library.ts @@ -0,0 +1,157 @@ +/** + * Library manifest build step (docs/showcase-plan.md § 2.3). + * + * Compiles the TS part definitions in library/parts/ into: + * - library/dist/index.json search manifest (id, name, taxonomy, + * protocols, domains, thumbnail) + * - library/dist/parts/.json full ModuleDef, one file per part + * - library/dist/thumbnails/.png self-hosted thumbnails, copied from + * library/parts//artifacts/thumbnail.png + * + * The website statically imports these — no database needed at this scale. + * + * Run: npm run build:library (npx tsx scripts/build-library.ts) + */ + +import { copyFileSync, existsSync, mkdirSync, writeFileSync, rmSync } from "fs"; +import { fileURLToPath } from "url"; +import { join } from "path"; + +import type { ModuleDef } from "../src/types/index.js"; +import * as parts from "../library/parts/index.js"; + +/** One search-manifest row per part — the fields /parts and /validate filter on. */ +interface ManifestEntry { + id: string; + name: string; + description?: string; + version?: string; + manufacturer?: string; + part_number?: string; + /** tags + categories from the ModuleDef, merged and deduped */ + taxonomy: string[]; + /** unique protocol types across exposed interfaces (e.g. "i2c", "power") */ + protocols: string[]; + /** unique domain kinds across interfaces + domain metadata */ + domains: string[]; + /** URL/path of the first image-like artifact, if any */ + thumbnail?: string; +} + +function uniqueSorted(values: string[]): string[] { + return [...new Set(values)].sort(); +} + +function collectTaxonomy(def: ModuleDef): string[] { + return uniqueSorted([...(def.tags ?? []), ...(def.categories ?? [])]); +} + +function collectProtocols(def: ModuleDef): string[] { + const types = def.interfaces + .filter((iface) => iface.exposed) + .flatMap((iface) => iface.protocols.map((p) => p.type)); + return uniqueSorted(types); +} + +function collectDomains(def: ModuleDef): string[] { + const fromInterfaces = def.interfaces.map((iface) => iface.domain); + const fromMetadata = (def.domains ?? []).map((d) => d.domain); + return uniqueSorted([...fromInterfaces, ...fromMetadata]); +} + +/** + * Self-hosted thumbnail (docs/showcase-plan.md § 2.3 artifacts): a part may + * ship library/parts//artifacts/thumbnail.png; it is copied into + * library/dist/thumbnails/.png and referenced dist-relative. Falls back + * to an externally-hosted image artifact URL on the ModuleDef. + */ +function findThumbnail(def: ModuleDef): string | undefined { + if (existsSync(hostedThumbnailSource(def.id))) { + return `thumbnails/${def.id}.png`; + } + const artifact = (def.artifacts ?? []).find( + (a) => + a.url !== undefined && + (a.tags?.includes("thumbnail") || a.mimeType?.startsWith("image/")), + ); + return artifact?.url; +} + +function hostedThumbnailSource(id: string): string { + return join(partsSrcDir, id, "artifacts", "thumbnail.png"); +} + +function toManifestEntry(def: ModuleDef): ManifestEntry { + return { + id: def.id, + name: def.name, + description: def.description, + version: def.version, + manufacturer: def.manufacturer, + part_number: def.part_number, + taxonomy: collectTaxonomy(def), + protocols: collectProtocols(def), + domains: collectDomains(def), + thumbnail: findThumbnail(def), + }; +} + +function assertUniqueIds(defs: ModuleDef[]): void { + const seen = new Map(); + for (const def of defs) { + seen.set(def.id, (seen.get(def.id) ?? 0) + 1); + } + const duplicates = [...seen.entries()].filter(([, count]) => count > 1); + if (duplicates.length > 0) { + const ids = duplicates.map(([id]) => id).join(", "); + throw new Error(`Duplicate part ids in library/parts: ${ids}`); + } +} + +const distDir = fileURLToPath(new URL("../library/dist", import.meta.url)); +const partsDir = join(distDir, "parts"); +const thumbsDir = join(distDir, "thumbnails"); +const partsSrcDir = fileURLToPath(new URL("../library/parts", import.meta.url)); + +const defs = Object.values(parts) as ModuleDef[]; +if (defs.length === 0) { + throw new Error("No parts exported from library/parts/index.ts"); +} +assertUniqueIds(defs); + +rmSync(distDir, { recursive: true, force: true }); +mkdirSync(partsDir, { recursive: true }); + +const manifest = defs + .map(toManifestEntry) + .sort((a, b) => a.id.localeCompare(b.id)); + +writeFileSync( + join(distDir, "index.json"), + JSON.stringify(manifest, null, 2) + "\n", +); +for (const def of defs) { + writeFileSync( + join(partsDir, `${def.id}.json`), + JSON.stringify(def, null, 2) + "\n", + ); +} + +mkdirSync(thumbsDir, { recursive: true }); +let thumbnailCount = 0; +for (const def of defs) { + const source = hostedThumbnailSource(def.id); + if (existsSync(source)) { + copyFileSync(source, join(thumbsDir, `${def.id}.png`)); + thumbnailCount += 1; + } +} + +console.log( + `library/dist: wrote index.json (${manifest.length} parts) + ${defs.length} part files + ${thumbnailCount} thumbnails`, +); +for (const entry of manifest) { + console.log( + ` ${entry.id} [${entry.domains.join(", ")}] protocols: ${entry.protocols.join(", ") || "—"}`, + ); +} diff --git a/src/index.ts b/src/index.ts index 8bce318..dd16e85 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,9 @@ // Types export * from "./types/index.js"; +// Protocol interface builders (one file per protocol) + defineModule +export * from "./protocols/index.js"; + // Matching export { areRolesCompatible, getCompatibleRoles } from "./matching/roles.js"; export { matchProtocols } from "./matching/protocol-match.js"; diff --git a/src/protocols/analog.ts b/src/protocols/analog.ts new file mode 100644 index 0000000..96f5fb9 --- /dev/null +++ b/src/protocols/analog.ts @@ -0,0 +1,105 @@ +import type { InterfaceDef, ProtocolDef } from "../types/interface.js"; +import type { Parameter } from "../types/parameter.js"; +import { voltageV, voltageRangeV, resolutionBits } from "./params.js"; +import { signalPin, type SignalSpec } from "./signal.js"; + +/** + * Analog converter interface builders (ADC inputs, DAC outputs). + * + * Channels may be declared inline (pin number, display name, V/mA/Hz, + * channel number) or reference already-declared pin interface ids. + * Returns generated channel pins + the converter — spread into the + * module's interfaces. Use `instance` for parts with multiple converters + * (ADC1, ADC2, ...), which also prefixes default channel names + * (ADC1_CH0, ADC2_CH3, ...). + */ +export type AnalogChannel = string | (SignalSpec & { channel?: number }); + +export interface AnalogConfig { + /** Interface id. Defaults to "" (e.g. "adc1"). */ + id?: string; + /** Instance number for multi-converter parts; also prefixes default channel names. */ + instance?: number; + name?: string; + /** Converter resolution in bits. */ + resolutionBits?: number; + /** Measurable input range (ADC) or output range (DAC). */ + rangeV?: [number, number]; + voltageV?: number; + /** Channels: inline specs (pin number, name, params) or existing pin interface ids. */ + channels?: AnalogChannel[]; + exposed?: boolean; + defaultActive?: boolean; +} + +function analog( + config: AnalogConfig, + kind: "adc" | "dac", + role: "input" | "output", + capability: string, +): InterfaceDef[] { + const instance = config.instance; + const id = config.id ?? `${kind}${instance ?? 0}`; + const label = `${kind.toUpperCase()}${instance !== undefined ? instance : ""}`; + const channelProto: ProtocolDef[] = [{ type: "analog", roles: [role] }]; + + const generated: InterfaceDef[] = []; + const channelIds: string[] = []; + + (config.channels ?? []).forEach((channel, index) => { + if (typeof channel === "string") { + channelIds.push(channel); + return; + } + const n = channel.channel ?? index; + const pin = signalPin({ + id: `${id}_ch${n}`, + defaultName: `${label}_CH${n}`, + capability, + protocols: channelProto, + spec: channel, + }); + generated.push(pin); + channelIds.push(pin.id); + }); + + const parameters: Parameter[] = []; + if (config.resolutionBits !== undefined) parameters.push(resolutionBits(config.resolutionBits)); + if (config.rangeV) parameters.push(voltageRangeV(config.rangeV[0], config.rangeV[1])); + else if (config.voltageV !== undefined) parameters.push(voltageV(config.voltageV)); + + const converter: InterfaceDef = { + id, + name: config.name ?? label, + domain: "electrical", + exposed: config.exposed ?? true, + default_active: config.defaultActive ?? false, + protocols: [{ type: "analog", roles: [role] }], + ...(parameters.length > 0 ? { parameters } : {}), + ...(channelIds.length > 0 + ? { + slots: [ + { + id: "channel", + required: true, + count: channelIds.length, + match: { protocol: "analog", role, capability }, + }, + ], + profiles: [{ id: `${id}_channels`, bindings: { channel: channelIds } }], + } + : {}), + }; + + return [...generated, converter]; +} + +/** An ADC peripheral: analog input converter with optional channel pins. */ +export function ADC(config: AnalogConfig): InterfaceDef[] { + return analog(config, "adc", "input", "analog_in"); +} + +/** A DAC peripheral: analog output converter with optional channel pins. */ +export function DAC(config: AnalogConfig): InterfaceDef[] { + return analog(config, "dac", "output", "analog_out"); +} diff --git a/src/protocols/define-module.ts b/src/protocols/define-module.ts new file mode 100644 index 0000000..78faec0 --- /dev/null +++ b/src/protocols/define-module.ts @@ -0,0 +1,51 @@ +import type { ModuleDef } from "../types/module.js"; +import { validateProfile } from "../binding/profile.js"; + +/** + * Validating constructor for part definitions. + * + * Wrap every part file's ModuleDef in this call. TypeScript checks the + * shape at compile time; this adds the referential checks the type system + * cannot express: + * - interface IDs are unique + * - every declared profile passes validateProfile (bindings exist, + * required slots are bound, bound pins carry the slot's capability) + * - interfaceGroups members reference existing interfaces + * + * Throws an Error listing every problem found, so a bad part fails at + * import time rather than after it reaches the database. + */ +export function defineModule(def: ModuleDef): ModuleDef { + const problems: string[] = []; + const interfaceIds = new Set(); + + for (const iface of def.interfaces) { + if (interfaceIds.has(iface.id)) { + problems.push(`duplicate interface id "${iface.id}"`); + } + interfaceIds.add(iface.id); + } + + for (const iface of def.interfaces) { + for (const profile of iface.profiles ?? []) { + const result = validateProfile(def, iface.id, profile); + for (const error of result.errors) { + problems.push(`interface "${iface.id}" profile "${profile.id}": ${error}`); + } + } + } + + for (const group of def.interfaceGroups ?? []) { + for (const member of group.members) { + if (!interfaceIds.has(member)) { + problems.push(`interface group "${group.id}" references missing interface "${member}"`); + } + } + } + + if (problems.length > 0) { + throw new Error(`Invalid module "${def.id}":\n - ${problems.join("\n - ")}`); + } + + return def; +} diff --git a/src/protocols/gpio.ts b/src/protocols/gpio.ts new file mode 100644 index 0000000..31b4b1b --- /dev/null +++ b/src/protocols/gpio.ts @@ -0,0 +1,34 @@ +import type { InterfaceDef } from "../types/interface.js"; +import { Pin, type PinConfig } from "./pin.js"; + +/** + * GPIO builders. + * + * `GPIO` is the per-pin form — identical to `Pin` but named for part files + * that think in terms of "a GPIO" rather than "a physical pin". + * + * `GPIOBank` builds many pins that share electrical characteristics in one + * call (voltage/drive current applied to each), which keeps MCU part files + * short when 30+ pins differ only by id and capability flags. + */ +export function GPIO(config: PinConfig): InterfaceDef { + return Pin(config); +} + +export interface GPIOBankConfig { + /** Electrical defaults applied to every pin in the bank. */ + voltageV?: number | [number, number]; + driveCurrentmA?: number; + /** Per-pin configs; each may override the bank defaults. */ + pins: PinConfig[]; +} + +export function GPIOBank(config: GPIOBankConfig): InterfaceDef[] { + return config.pins.map((pin) => + Pin({ + voltageV: config.voltageV, + driveCurrentmA: config.driveCurrentmA, + ...pin, + }), + ); +} diff --git a/src/protocols/i2c.ts b/src/protocols/i2c.ts new file mode 100644 index 0000000..35d4b8a --- /dev/null +++ b/src/protocols/i2c.ts @@ -0,0 +1,117 @@ +import type { InterfaceDef } from "../types/interface.js"; +import type { Parameter } from "../types/parameter.js"; +import { clockFreqHz, voltageV } from "./params.js"; +import { resolveSignal, DIGITAL_BIDIR, type SignalRef } from "./signal.js"; + +/** + * I2C bus interface builder. + * + * Returns the bus InterfaceDef plus any pin InterfaceDefs generated from + * inline signal specs — spread the result into the module's interfaces: + * + * ...I2C({ + * instance: 1, // -> id "i2c1", names SDA1/SCL1 + * roles: ["master"], + * clockFreqHz: 400_000, + * sda: { pin: 42, name: "SDA1", voltageV: [1.8, 3.6], maxCurrentmA: 40 }, + * scl: { pin: 39, name: "SCL1", voltageV: [1.8, 3.6], maxCurrentmA: 40 }, + * }) + * + * A signal may instead be a string referencing an already-declared pin + * interface id (the MCU/GPIO-matrix style). Either way the default + * profile is wired automatically. Declare a second controller by calling + * I2C() again with a different instance number. + */ +export type I2CRole = "master" | "slave"; + +export interface I2CProfile { + id: string; + label?: string; + /** Pin interface ID bound to the SDA slot. */ + sda: string; + /** Pin interface ID bound to the SCL slot. */ + scl: string; + defaultActive?: boolean; +} + +export interface I2CConfig { + /** Interface id. Defaults to "i2c" (e.g. "i2c0"). */ + id?: string; + /** Instance number for multi-controller parts; also suffixes default signal names. */ + instance?: number; + name?: string; + /** Roles this controller can take (default ["master"]). */ + roles?: I2CRole[]; + /** Max SCL clock in Hz (default 400 kHz Fast-mode). */ + clockFreqHz?: number | [number, number]; + /** Bus logic voltage. */ + voltageV?: number; + /** 7-bit device address, for slave-only devices such as sensors. */ + address?: number; + /** SDA signal: inline spec (pin number, name, V/mA/Hz) or existing pin interface id. */ + sda?: SignalRef; + /** SCL signal: inline spec or existing pin interface id. */ + scl?: SignalRef; + /** Extra named pin routings beyond the auto-generated default. */ + profiles?: I2CProfile[]; + /** Max simultaneous instances (profiles) of this controller. */ + maxInstances?: number; + exposed?: boolean; + defaultActive?: boolean; +} + +export function I2C(config: I2CConfig): InterfaceDef[] { + const instance = config.instance; + const id = config.id ?? `i2c${instance ?? 0}`; + const sfx = instance !== undefined ? String(instance) : ""; + + const generated: InterfaceDef[] = []; + const profiles = [...(config.profiles ?? [])]; + + if (config.sda !== undefined && config.scl !== undefined) { + const sdaId = resolveSignal( + config.sda, + { id: `${id}_sda`, defaultName: `SDA${sfx}`, capability: "i2c_sda", protocols: DIGITAL_BIDIR }, + generated, + ); + const sclId = resolveSignal( + config.scl, + { id: `${id}_scl`, defaultName: `SCL${sfx}`, capability: "i2c_scl", protocols: DIGITAL_BIDIR }, + generated, + ); + profiles.unshift({ id: `${id}_default`, label: config.name ?? id.toUpperCase(), sda: sdaId, scl: sclId }); + } + + const parameters: Parameter[] = [clockFreqHz(config.clockFreqHz ?? 400_000)]; + if (config.voltageV !== undefined) parameters.push(voltageV(config.voltageV)); + if (config.address !== undefined) { + parameters.push({ id: "i2c_address", unit: "dimensionless", value: config.address }); + } + + const bus: InterfaceDef = { + id, + name: config.name ?? `I2C${sfx}`, + domain: "electrical", + exposed: config.exposed ?? true, + default_active: config.defaultActive ?? false, + protocols: [{ type: "i2c", roles: config.roles ?? ["master"] }], + parameters, + slots: [ + { id: "sda", required: true, match: { protocol: "i2c", role: "data", capability: "i2c_sda" } }, + { id: "scl", required: true, match: { protocol: "i2c", role: "clock", capability: "i2c_scl" } }, + ], + ...(profiles.length > 0 + ? { + profiles: profiles.map((p) => ({ + id: p.id, + label: p.label, + bindings: { sda: p.sda, scl: p.scl }, + ...(p.defaultActive !== undefined ? { default_active: p.defaultActive } : {}), + })), + } + : {}), + ...(config.maxInstances !== undefined ? { max_instances: config.maxInstances } : {}), + }; + + return [...generated, bus]; +} diff --git a/src/protocols/index.ts b/src/protocols/index.ts new file mode 100644 index 0000000..4196dce --- /dev/null +++ b/src/protocols/index.ts @@ -0,0 +1,20 @@ +/** + * Standardized protocol interface builders. + * + * One file per protocol. Part definition files import these builders and + * supply part-specific parameters (pins, voltages, currents, frequencies); + * the builders emit InterfaceDef objects with the canonical protocol types, + * roles, capability tags, and parameter IDs that the matching engine + * (matching/roles.ts) and slot binding (binding/) understand. + */ +export * from "./params.js"; +export * from "./signal.js"; +export * from "./pin.js"; +export * from "./gpio.js"; +export * from "./power.js"; +export * from "./i2c.js"; +export * from "./spi.js"; +export * from "./uart.js"; +export * from "./pwm.js"; +export * from "./analog.js"; +export * from "./define-module.js"; diff --git a/src/protocols/params.ts b/src/protocols/params.ts new file mode 100644 index 0000000..070e40a --- /dev/null +++ b/src/protocols/params.ts @@ -0,0 +1,60 @@ +import type { Parameter } from "../types/parameter.js"; + +/** + * Canonical parameter builders. + * + * Every protocol builder emits parameters through these helpers so that + * parameter IDs and units stay identical across all part definitions + * ("voltage" is always volts, "clock_freq" is always Hz, etc.). + * The matching/constraint engines key off these IDs. + */ + +/** Fixed operating voltage, optionally with an allowed range. */ +export function voltageV(value: number, range?: [number, number]): Parameter { + return range + ? { id: "voltage", unit: "V", value, range } + : { id: "voltage", unit: "V", value }; +} + +/** Voltage specified only as a min/max range (no single nominal value). */ +export function voltageRangeV(min: number, max: number, nominal?: number): Parameter { + return nominal !== undefined + ? { id: "voltage", unit: "V", value: nominal, range: [min, max] } + : { id: "voltage", unit: "V", range: [min, max] }; +} + +/** Maximum continuous supply current in amps (power interfaces). */ +export function maxCurrentA(value: number): Parameter { + return { id: "max_current", unit: "A", value }; +} + +/** Per-pin drive/sink current in milliamps (signal pins). */ +export function driveCurrentmA(value: number): Parameter { + return { id: "drive_current", unit: "mA", value }; +} + +/** Bus clock frequency in Hz — a fixed value or a supported range. */ +export function clockFreqHz(value: number | [number, number]): Parameter { + return Array.isArray(value) + ? { id: "clock_freq", unit: "Hz", range: value } + : { id: "clock_freq", unit: "Hz", value }; +} + +/** UART baud rate — a fixed value or a supported range. */ +export function baudRate(value: number | [number, number]): Parameter { + return Array.isArray(value) + ? { id: "baud_rate", unit: "Hz", range: value } + : { id: "baud_rate", unit: "Hz", value }; +} + +/** Maximum signal frequency a pin supports, in Hz — fixed or [min, max]. */ +export function maxFrequencyHz(value: number | [number, number]): Parameter { + return Array.isArray(value) + ? { id: "max_frequency", unit: "Hz", range: value } + : { id: "max_frequency", unit: "Hz", value }; +} + +/** Converter resolution in bits (ADC/DAC/PWM). */ +export function resolutionBits(value: number): Parameter { + return { id: "resolution", unit: "dimensionless", value }; +} diff --git a/src/protocols/pin.ts b/src/protocols/pin.ts new file mode 100644 index 0000000..157ce53 --- /dev/null +++ b/src/protocols/pin.ts @@ -0,0 +1,116 @@ +import type { InterfaceDef, ProtocolDef } from "../types/interface.js"; +import type { Parameter } from "../types/parameter.js"; +import { voltageV, voltageRangeV, driveCurrentmA } from "./params.js"; + +/** + * Physical pin builder. + * + * A pin is a leaf electrical interface. Composed bus interfaces (I2C, SPI, + * UART, ...) never own pins directly — they declare slots that bind to pin + * IDs, and the capability tags set here are what slot matching checks. + * + * Capability tag names are canonical and must match SlotDef.match.capability + * values used by the bus builders in this folder. + */ +export interface PinCapabilities { + /** General-purpose digital I/O (default true). */ + digital?: boolean; + /** Pin is input-only (e.g. ESP32 GPIO34-39): drops output/bidirectional roles. */ + inputOnly?: boolean; + pwm?: boolean; + interrupt?: boolean; + analogIn?: boolean; + analogOut?: boolean; + touch?: boolean; + i2cSda?: boolean; + i2cScl?: boolean; + spiMosi?: boolean; + spiMiso?: boolean; + spiSck?: boolean; + spiSs?: boolean; + uartRx?: boolean; + uartTx?: boolean; + uartRts?: boolean; + uartCts?: boolean; +} + +export interface PinConfig { + id: string; + /** Human label, e.g. "GPIO21". */ + name?: string; + /** Physical package pin/pad designator (e.g. 42 or "A7"). */ + pin?: number | string; + /** Logic-level voltage: a nominal value or [min, max] range. */ + voltageV?: number | [number, number]; + /** Max continuous source/sink current in mA. */ + driveCurrentmA?: number; + capabilities?: PinCapabilities; + exposed?: boolean; + defaultActive?: boolean; +} + +export function Pin(config: PinConfig): InterfaceDef { + const caps = config.capabilities ?? {}; + const protocols: ProtocolDef[] = []; + const capabilities: string[] = []; + + const digital = caps.digital ?? true; + if (digital) { + protocols.push({ + type: "digital", + roles: caps.inputOnly ? ["input"] : ["input", "output", "bidirectional"], + }); + capabilities.push("digital_io"); + } + if (caps.pwm) { + protocols.push({ type: "pwm", roles: ["output"] }); + capabilities.push("pwm_out"); + } + if (caps.interrupt) { + protocols.push({ type: "interrupt", roles: ["input"] }); + capabilities.push("interrupt"); + } + if (caps.analogIn) { + protocols.push({ type: "analog", roles: ["input"] }); + capabilities.push("analog_in"); + } + if (caps.analogOut) { + protocols.push({ type: "analog", roles: ["output"] }); + capabilities.push("analog_out"); + } + if (caps.touch) capabilities.push("touch"); + if (caps.i2cSda) capabilities.push("i2c_sda"); + if (caps.i2cScl) capabilities.push("i2c_scl"); + if (caps.spiMosi) capabilities.push("spi_mosi"); + if (caps.spiMiso) capabilities.push("spi_miso"); + if (caps.spiSck) capabilities.push("spi_sck"); + if (caps.spiSs) capabilities.push("spi_ss"); + if (caps.uartRx) capabilities.push("uart_rx"); + if (caps.uartTx) capabilities.push("uart_tx"); + if (caps.uartRts) capabilities.push("uart_rts"); + if (caps.uartCts) capabilities.push("uart_cts"); + + const parameters: Parameter[] = []; + if (config.voltageV !== undefined) { + parameters.push( + Array.isArray(config.voltageV) + ? voltageRangeV(config.voltageV[0], config.voltageV[1]) + : voltageV(config.voltageV), + ); + } + if (config.driveCurrentmA !== undefined) { + parameters.push(driveCurrentmA(config.driveCurrentmA)); + } + + return { + id: config.id, + name: config.name, + ...(config.pin !== undefined ? { pin: config.pin } : {}), + domain: "electrical", + exposed: config.exposed ?? true, + default_active: config.defaultActive ?? true, + protocols, + capabilities, + ...(parameters.length > 0 ? { parameters } : {}), + }; +} diff --git a/src/protocols/power.ts b/src/protocols/power.ts new file mode 100644 index 0000000..6d4e2a2 --- /dev/null +++ b/src/protocols/power.ts @@ -0,0 +1,78 @@ +import type { InterfaceDef } from "../types/interface.js"; +import type { Parameter } from "../types/parameter.js"; +import { voltageV, voltageRangeV, maxCurrentA } from "./params.js"; + +/** + * Power interface builders: supply inputs, regulated outputs, and ground. + * Compatibility is role-based ("input" pairs with "output", see matching/roles.ts). + */ +export interface PowerConfig { + id: string; + name?: string; + /** Physical package pin/pad designator, when the rail maps to a single pin. */ + pin?: number | string; + /** Nominal voltage or [min, max] acceptable range. */ + voltageV: number | [number, number]; + /** Nominal value when voltageV is a range. */ + nominalV?: number; + /** Max continuous current in amps. */ + maxCurrentA?: number; + exposed?: boolean; + defaultActive?: boolean; +} + +function power(config: PowerConfig, role: "input" | "output"): InterfaceDef { + const parameters: Parameter[] = [ + Array.isArray(config.voltageV) + ? voltageRangeV(config.voltageV[0], config.voltageV[1], config.nominalV) + : voltageV(config.voltageV), + ]; + if (config.maxCurrentA !== undefined) parameters.push(maxCurrentA(config.maxCurrentA)); + + return { + id: config.id, + name: config.name, + ...(config.pin !== undefined ? { pin: config.pin } : {}), + domain: "electrical", + exposed: config.exposed ?? true, + default_active: config.defaultActive ?? true, + protocols: [{ type: "power", roles: [role] }], + parameters, + }; +} + +/** A supply rail this part consumes (e.g. VDD input pins). */ +export function PowerIn(config: PowerConfig): InterfaceDef { + return power(config, "input"); +} + +/** A supply rail this part provides (e.g. a regulator output). */ +export function PowerOut(config: PowerConfig): InterfaceDef { + return power(config, "output"); +} + +export interface GroundConfig { + id?: string; + name?: string; + /** Physical package pin/pad designator, when ground maps to a single pin. */ + pin?: number | string; + /** Max cumulative return current in amps, if the datasheet specifies one. */ + maxCurrentA?: number; +} + +/** Common ground return. */ +export function Ground(config: GroundConfig = {}): InterfaceDef { + return { + id: config.id ?? "gnd", + name: config.name ?? "Ground", + ...(config.pin !== undefined ? { pin: config.pin } : {}), + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "power", roles: ["ground"] }], + capabilities: ["ground"], + ...(config.maxCurrentA !== undefined + ? { parameters: [maxCurrentA(config.maxCurrentA)] } + : {}), + }; +} diff --git a/src/protocols/pwm.ts b/src/protocols/pwm.ts new file mode 100644 index 0000000..dae70ba --- /dev/null +++ b/src/protocols/pwm.ts @@ -0,0 +1,94 @@ +import type { InterfaceDef, ProtocolDef } from "../types/interface.js"; +import type { Parameter } from "../types/parameter.js"; +import { clockFreqHz, resolutionBits, voltageV } from "./params.js"; +import { signalPin, type SignalSpec } from "./signal.js"; + +/** + * PWM output interface builder. + * + * For dedicated PWM peripherals (LEDC/MCPWM units, servo/LED driver + * channels). Channels may be declared inline (pin number, display name, + * V/mA/Hz, channel number) or reference already-declared pin interface + * ids. Returns generated channel pins + the unit — spread into the + * module's interfaces. Use `instance` for parts with multiple PWM units. + * Plain MCU pins that merely support PWM should instead set `pwm: true` + * on the Pin builder. + */ +export type PWMChannel = string | (SignalSpec & { channel?: number }); + +const PWM_PROTO: ProtocolDef[] = [{ type: "pwm", roles: ["output"] }]; + +export interface PWMConfig { + /** Interface id. Defaults to "pwm" (e.g. "pwm0"). */ + id?: string; + /** Instance number for multi-unit parts; also prefixes default channel names. */ + instance?: number; + name?: string; + /** Output frequency in Hz — fixed or [min, max] supported range. */ + freqHz?: number | [number, number]; + /** Duty-cycle resolution in bits. */ + resolutionBits?: number; + voltageV?: number; + /** Channels: inline specs (pin number, name, params) or existing pin interface ids. */ + channels?: PWMChannel[]; + maxInstances?: number; + exposed?: boolean; + defaultActive?: boolean; +} + +export function PWM(config: PWMConfig): InterfaceDef[] { + const instance = config.instance; + const id = config.id ?? `pwm${instance ?? 0}`; + const label = `PWM${instance !== undefined ? instance : ""}`; + + const generated: InterfaceDef[] = []; + const channelIds: string[] = []; + + (config.channels ?? []).forEach((channel, index) => { + if (typeof channel === "string") { + channelIds.push(channel); + return; + } + const n = channel.channel ?? index; + const pin = signalPin({ + id: `${id}_ch${n}`, + defaultName: `${label}_CH${n}`, + capability: "pwm_out", + protocols: PWM_PROTO, + spec: channel, + }); + generated.push(pin); + channelIds.push(pin.id); + }); + + const parameters: Parameter[] = []; + if (config.freqHz !== undefined) parameters.push(clockFreqHz(config.freqHz)); + if (config.resolutionBits !== undefined) parameters.push(resolutionBits(config.resolutionBits)); + if (config.voltageV !== undefined) parameters.push(voltageV(config.voltageV)); + + const unit: InterfaceDef = { + id, + name: config.name ?? label, + domain: "electrical", + exposed: config.exposed ?? true, + default_active: config.defaultActive ?? false, + protocols: [{ type: "pwm", roles: ["output"] }], + ...(parameters.length > 0 ? { parameters } : {}), + ...(channelIds.length > 0 + ? { + slots: [ + { + id: "channel", + required: true, + count: channelIds.length, + match: { protocol: "pwm", role: "output", capability: "pwm_out" }, + }, + ], + profiles: [{ id: `${id}_channels`, bindings: { channel: channelIds } }], + } + : {}), + ...(config.maxInstances !== undefined ? { max_instances: config.maxInstances } : {}), + }; + + return [...generated, unit]; +} diff --git a/src/protocols/signal.ts b/src/protocols/signal.ts new file mode 100644 index 0000000..207acd0 --- /dev/null +++ b/src/protocols/signal.ts @@ -0,0 +1,89 @@ +import type { InterfaceDef, ProtocolDef } from "../types/interface.js"; +import type { Parameter } from "../types/parameter.js"; +import { voltageV, voltageRangeV, driveCurrentmA, maxFrequencyHz } from "./params.js"; + +/** + * Inline signal declaration for bus builders. + * + * Lets a part file describe a bus signal (SDA, SCL, MOSI, TX, an ADC + * channel, ...) directly inside the protocol call: physical pin number, + * display name, and electrical parameters. The builder turns each spec + * into a leaf pin InterfaceDef carrying the signal's canonical capability + * tag and auto-binds it into the bus's default profile. + */ +export interface SignalSpec { + /** Physical package pin/pad designator (e.g. 42 or "A7"). */ + pin: number | string; + /** Display name, e.g. "SDA1" or "GPIO21 / SDA". Defaults to the signal role + instance. */ + name?: string; + /** Interface id override. Defaults to `_`. */ + id?: string; + /** Logic-level voltage: nominal or [min, max]. */ + voltageV?: number | [number, number]; + /** Max continuous source/sink current in mA. */ + maxCurrentmA?: number; + /** Max signal frequency in Hz — fixed or [min, max]. */ + maxFrequencyHz?: number | [number, number]; +} + +/** A bus signal is either a reference to an existing pin interface id, or an inline spec. */ +export type SignalRef = string | SignalSpec; + +export interface SignalPinOptions { + /** Generated interface id (used when spec.id is absent). */ + id: string; + /** Display name fallback (used when spec.name is absent), e.g. "SDA1". */ + defaultName: string; + /** Canonical capability tag for slot matching, e.g. "i2c_sda". */ + capability: string; + /** Protocols the generated pin speaks, e.g. digital bidirectional. */ + protocols: ProtocolDef[]; + spec: SignalSpec; +} + +/** Build a leaf pin InterfaceDef from an inline signal spec. */ +export function signalPin(opts: SignalPinOptions): InterfaceDef { + const { spec } = opts; + const parameters: Parameter[] = []; + if (spec.voltageV !== undefined) { + parameters.push( + Array.isArray(spec.voltageV) + ? voltageRangeV(spec.voltageV[0], spec.voltageV[1]) + : voltageV(spec.voltageV), + ); + } + if (spec.maxCurrentmA !== undefined) parameters.push(driveCurrentmA(spec.maxCurrentmA)); + if (spec.maxFrequencyHz !== undefined) parameters.push(maxFrequencyHz(spec.maxFrequencyHz)); + + return { + id: spec.id ?? opts.id, + name: spec.name ?? opts.defaultName, + pin: spec.pin, + domain: "electrical", + exposed: true, + default_active: true, + protocols: opts.protocols, + capabilities: [opts.capability], + ...(parameters.length > 0 ? { parameters } : {}), + }; +} + +/** + * Resolve one signal of a bus: returns the pin interface id to bind, and + * appends a generated pin InterfaceDef when the signal was declared inline. + */ +export function resolveSignal( + ref: SignalRef, + opts: Omit, + generated: InterfaceDef[], +): string { + if (typeof ref === "string") return ref; + const pin = signalPin({ ...opts, spec: ref }); + generated.push(pin); + return pin.id; +} + +/** Bidirectional digital pin protocol — the default for generated bus signal pins. */ +export const DIGITAL_BIDIR: ProtocolDef[] = [ + { type: "digital", roles: ["input", "output", "bidirectional"] }, +]; diff --git a/src/protocols/spi.ts b/src/protocols/spi.ts new file mode 100644 index 0000000..45e20c0 --- /dev/null +++ b/src/protocols/spi.ts @@ -0,0 +1,132 @@ +import type { InterfaceDef } from "../types/interface.js"; +import type { Parameter } from "../types/parameter.js"; +import { clockFreqHz, voltageV } from "./params.js"; +import { resolveSignal, DIGITAL_BIDIR, type SignalRef } from "./signal.js"; + +/** + * SPI bus interface builder. + * + * Signals may be declared inline (pin number, display name, V/mA/Hz) or + * reference already-declared pin interface ids. Returns generated pins + + * the bus — spread into the module's interfaces. Use `instance` for parts + * with multiple SPI controllers (SPI1, SPI2, ...). + */ +export type SPIRole = "master" | "slave"; + +export interface SPIProfile { + id: string; + label?: string; + mosi: string; + miso: string; + sck: string; + /** One or more chip-select pin interface IDs. */ + ss?: string | string[]; + defaultActive?: boolean; +} + +export interface SPIConfig { + /** Interface id. Defaults to "spi" (e.g. "spi0"). */ + id?: string; + /** Instance number for multi-controller parts; also suffixes default signal names. */ + instance?: number; + name?: string; + /** Roles this controller can take (default ["master"]). */ + roles?: SPIRole[]; + /** Clock frequency in Hz — fixed or [min, max] supported range. */ + clockFreqHz?: number | [number, number]; + voltageV?: number; + /** MOSI signal: inline spec or existing pin interface id. */ + mosi?: SignalRef; + /** MISO signal: inline spec or existing pin interface id. */ + miso?: SignalRef; + /** SCK signal: inline spec or existing pin interface id. */ + sck?: SignalRef; + /** Chip select: inline spec or existing pin interface id (optional). */ + ss?: SignalRef; + /** Extra named pin routings beyond the auto-generated default. */ + profiles?: SPIProfile[]; + maxInstances?: number; + exposed?: boolean; + defaultActive?: boolean; +} + +export function SPI(config: SPIConfig): InterfaceDef[] { + const instance = config.instance; + const id = config.id ?? `spi${instance ?? 0}`; + const sfx = instance !== undefined ? String(instance) : ""; + + const generated: InterfaceDef[] = []; + const profiles = [...(config.profiles ?? [])]; + + if (config.mosi !== undefined && config.miso !== undefined && config.sck !== undefined) { + const mosiId = resolveSignal( + config.mosi, + { id: `${id}_mosi`, defaultName: `MOSI${sfx}`, capability: "spi_mosi", protocols: DIGITAL_BIDIR }, + generated, + ); + const misoId = resolveSignal( + config.miso, + { id: `${id}_miso`, defaultName: `MISO${sfx}`, capability: "spi_miso", protocols: DIGITAL_BIDIR }, + generated, + ); + const sckId = resolveSignal( + config.sck, + { id: `${id}_sck`, defaultName: `SCK${sfx}`, capability: "spi_sck", protocols: DIGITAL_BIDIR }, + generated, + ); + const ssId = + config.ss !== undefined + ? resolveSignal( + config.ss, + { id: `${id}_ss`, defaultName: `SS${sfx}`, capability: "spi_ss", protocols: DIGITAL_BIDIR }, + generated, + ) + : undefined; + profiles.unshift({ + id: `${id}_default`, + label: config.name ?? id.toUpperCase(), + mosi: mosiId, + miso: misoId, + sck: sckId, + ...(ssId !== undefined ? { ss: ssId } : {}), + }); + } + + const parameters: Parameter[] = []; + if (config.clockFreqHz !== undefined) parameters.push(clockFreqHz(config.clockFreqHz)); + if (config.voltageV !== undefined) parameters.push(voltageV(config.voltageV)); + + const bus: InterfaceDef = { + id, + name: config.name ?? `SPI${sfx}`, + domain: "electrical", + exposed: config.exposed ?? true, + default_active: config.defaultActive ?? false, + protocols: [{ type: "spi", roles: config.roles ?? ["master"] }], + ...(parameters.length > 0 ? { parameters } : {}), + slots: [ + { id: "mosi", required: true, match: { protocol: "spi", role: "data_out", capability: "spi_mosi" } }, + { id: "miso", required: true, match: { protocol: "spi", role: "data_in", capability: "spi_miso" } }, + { id: "sck", required: true, match: { protocol: "spi", role: "clock", capability: "spi_sck" } }, + { id: "ss", required: false, match: { protocol: "spi", role: "select", capability: "spi_ss" } }, + ], + ...(profiles.length > 0 + ? { + profiles: profiles.map((p) => ({ + id: p.id, + label: p.label, + bindings: { + mosi: p.mosi, + miso: p.miso, + sck: p.sck, + ...(p.ss !== undefined ? { ss: p.ss } : {}), + }, + ...(p.defaultActive !== undefined ? { default_active: p.defaultActive } : {}), + })), + } + : {}), + ...(config.maxInstances !== undefined ? { max_instances: config.maxInstances } : {}), + }; + + return [...generated, bus]; +} diff --git a/src/protocols/uart.ts b/src/protocols/uart.ts new file mode 100644 index 0000000..6e44db5 --- /dev/null +++ b/src/protocols/uart.ts @@ -0,0 +1,147 @@ +import type { InterfaceDef, ProtocolDef } from "../types/interface.js"; +import type { Parameter } from "../types/parameter.js"; +import { baudRate, voltageV } from "./params.js"; +import { resolveSignal, type SignalRef } from "./signal.js"; + +/** + * UART/serial interface builder. + * + * Signals may be declared inline (pin number, display name, V/mA/Hz) or + * reference already-declared pin interface ids. Returns generated pins + + * the port — spread into the module's interfaces. Use `instance` for + * parts with multiple ports (UART0, UART1, ...), which also suffixes + * default signal names (TX1/RX1). + */ +export type UARTRole = "host" | "device"; + +const RX_PROTO: ProtocolDef[] = [{ type: "uart", roles: ["receiver"] }]; +const TX_PROTO: ProtocolDef[] = [{ type: "uart", roles: ["transmitter"] }]; + +export interface UARTProfile { + id: string; + label?: string; + rx: string; + tx: string; + rts?: string; + cts?: string; + defaultActive?: boolean; +} + +export interface UARTConfig { + /** Interface id. Defaults to "uart" (e.g. "uart0"). */ + id?: string; + /** Instance number for multi-port parts; also suffixes default signal names. */ + instance?: number; + name?: string; + /** Roles this port can take (default ["host", "device"]). */ + roles?: UARTRole[]; + /** Baud rate in Hz — fixed or [min, max] supported range. */ + baudRate?: number | [number, number]; + voltageV?: number; + /** RX signal: inline spec or existing pin interface id. */ + rx?: SignalRef; + /** TX signal: inline spec or existing pin interface id. */ + tx?: SignalRef; + /** RTS flow control: inline spec or existing pin interface id (optional). */ + rts?: SignalRef; + /** CTS flow control: inline spec or existing pin interface id (optional). */ + cts?: SignalRef; + /** Extra named pin routings beyond the auto-generated default. */ + profiles?: UARTProfile[]; + maxInstances?: number; + exposed?: boolean; + defaultActive?: boolean; +} + +export function UART(config: UARTConfig): InterfaceDef[] { + const instance = config.instance; + const id = config.id ?? `uart${instance ?? 0}`; + const sfx = instance !== undefined ? String(instance) : ""; + + const generated: InterfaceDef[] = []; + const profiles = [...(config.profiles ?? [])]; + const hasFlowControl = + config.rts !== undefined || config.cts !== undefined || + profiles.some((p) => p.rts !== undefined || p.cts !== undefined); + + if (config.rx !== undefined && config.tx !== undefined) { + const rxId = resolveSignal( + config.rx, + { id: `${id}_rx`, defaultName: `RX${sfx}`, capability: "uart_rx", protocols: RX_PROTO }, + generated, + ); + const txId = resolveSignal( + config.tx, + { id: `${id}_tx`, defaultName: `TX${sfx}`, capability: "uart_tx", protocols: TX_PROTO }, + generated, + ); + const rtsId = + config.rts !== undefined + ? resolveSignal( + config.rts, + { id: `${id}_rts`, defaultName: `RTS${sfx}`, capability: "uart_rts", protocols: TX_PROTO }, + generated, + ) + : undefined; + const ctsId = + config.cts !== undefined + ? resolveSignal( + config.cts, + { id: `${id}_cts`, defaultName: `CTS${sfx}`, capability: "uart_cts", protocols: RX_PROTO }, + generated, + ) + : undefined; + profiles.unshift({ + id: `${id}_default`, + label: config.name ?? id.toUpperCase(), + rx: rxId, + tx: txId, + ...(rtsId !== undefined ? { rts: rtsId } : {}), + ...(ctsId !== undefined ? { cts: ctsId } : {}), + }); + } + + const parameters: Parameter[] = []; + if (config.baudRate !== undefined) parameters.push(baudRate(config.baudRate)); + if (config.voltageV !== undefined) parameters.push(voltageV(config.voltageV)); + + const slots = [ + { id: "rx", required: true, match: { protocol: "uart", role: "receiver", capability: "uart_rx" } }, + { id: "tx", required: true, match: { protocol: "uart", role: "transmitter", capability: "uart_tx" } }, + ]; + if (hasFlowControl) { + slots.push( + { id: "rts", required: false, match: { protocol: "uart", role: "transmitter", capability: "uart_rts" } }, + { id: "cts", required: false, match: { protocol: "uart", role: "receiver", capability: "uart_cts" } }, + ); + } + + const port: InterfaceDef = { + id, + name: config.name ?? `UART${sfx}`, + domain: "electrical", + exposed: config.exposed ?? true, + default_active: config.defaultActive ?? false, + protocols: [{ type: "uart", roles: config.roles ?? ["host", "device"] }], + ...(parameters.length > 0 ? { parameters } : {}), + slots, + ...(profiles.length > 0 + ? { + profiles: profiles.map((p) => ({ + id: p.id, + label: p.label, + bindings: { + rx: p.rx, + tx: p.tx, + ...(p.rts !== undefined ? { rts: p.rts } : {}), + ...(p.cts !== undefined ? { cts: p.cts } : {}), + }, + ...(p.defaultActive !== undefined ? { default_active: p.defaultActive } : {}), + })), + } + : {}), + ...(config.maxInstances !== undefined ? { max_instances: config.maxInstances } : {}), + }; + + return [...generated, port]; +} diff --git a/src/types/interface.ts b/src/types/interface.ts index 756f161..7f2b01d 100644 --- a/src/types/interface.ts +++ b/src/types/interface.ts @@ -40,6 +40,9 @@ export interface InterfaceDef { domain: DomainKind; exposed: boolean; + /** Physical package pin/pad designator (e.g. 42 or "A7"), for leaf pin interfaces */ + pin?: number | string; + /** What this interface speaks — used for ALL inter-module matching */ protocols: ProtocolDef[]; diff --git a/test/esp32-d0wdq6.test.ts b/test/esp32-d0wdq6.test.ts new file mode 100644 index 0000000..3226a24 --- /dev/null +++ b/test/esp32-d0wdq6.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from "vitest"; +import { applyProfile } from "../src/binding/profile.js"; +import { getClaimedInterfaces } from "../src/binding/claims.js"; +import { instantiateModule } from "../src/instance/instantiate.js"; +import { resolveAllInterfaces } from "../src/instance/resolve.js"; +import { ESP32D0WDQ6 } from "./fixtures/esp32-d0wdq6.js"; + +function iface(id: string) { + const found = ESP32D0WDQ6.interfaces.find((candidate) => candidate.id === id); + if (!found) throw new Error(`Missing interface ${id}`); + return found; +} + +describe("ESP32-D0WDQ6 OpenUHD definition", () => { + it("defines identity, domains, and physical package pins", () => { + expect(ESP32D0WDQ6.id).toBe("esp32-d0wdq6"); + expect(ESP32D0WDQ6.manufacturer).toBe("Espressif Systems"); + expect(ESP32D0WDQ6.part_number).toBe("ESP32-D0WDQ6"); + expect(ESP32D0WDQ6.domains?.map((domain) => domain.domain)).toEqual([ + "electrical", + "mechanical", + "thermal", + "network", + ]); + + const physicalPads = new Set( + ESP32D0WDQ6.interfaces + .map((interfaceDef) => interfaceDef.pin) + .filter((pin) => pin !== undefined), + ); + expect(physicalPads.size).toBe(49); + }); + + it("models input-only GPIO34-GPIO39 without output roles", () => { + const sensorVp = iface("sensor_vp"); + + expect(sensorVp.protocols.find((protocol) => protocol.type === "digital")?.roles).toEqual(["input"]); + expect(sensorVp.capabilities).toContain("analog_in"); + expect(sensorVp.capabilities).toContain("adc1_ch0"); + expect(sensorVp.capabilities).not.toContain("pwm_out"); + }); + + it("validates default I2C, SPI, UART, ADC, and JTAG profiles", () => { + expect(applyProfile(ESP32D0WDQ6, "i2c", "i2c0_default")?.validation.valid).toBe(true); + expect(applyProfile(ESP32D0WDQ6, "spi", "hspi_default")?.validation.valid).toBe(true); + expect(applyProfile(ESP32D0WDQ6, "spi", "vspi_default")?.validation.valid).toBe(true); + expect(applyProfile(ESP32D0WDQ6, "uart", "uart0_default")?.validation.valid).toBe(true); + expect(applyProfile(ESP32D0WDQ6, "adc1", "adc1_channels")?.validation.valid).toBe(true); + expect(applyProfile(ESP32D0WDQ6, "adc2", "adc2_channels")?.validation.valid).toBe(true); + expect(applyProfile(ESP32D0WDQ6, "jtag", "jtag_default")?.validation.valid).toBe(true); + }); + + it("activates and claims the external SPI flash pads by default", () => { + const instance = instantiateModule(ESP32D0WDQ6); + const claimed = getClaimedInterfaces(instance.interfaceStates); + + expect(instance.interfaceStates.qspi_flash.instances.spi0_flash_default.active).toBe(true); + expect(claimed.has("sd_clk")).toBe(true); + expect(claimed.has("sd_cmd")).toBe(true); + expect(claimed.has("sd_data_0")).toBe(true); + expect(claimed.has("sd_data_1")).toBe(true); + expect(claimed.has("sd_data_2")).toBe(true); + expect(claimed.has("sd_data_3")).toBe(true); + }); + + it("keeps optional buses inactive until a profile is selected", () => { + const instance = instantiateModule(ESP32D0WDQ6); + const resolved = resolveAllInterfaces(ESP32D0WDQ6, instance); + + expect(resolved.find((entry) => entry.interfaceDef.id === "i2c")?.active).toBe(false); + expect(resolved.find((entry) => entry.interfaceDef.id === "spi")?.active).toBe(false); + expect(resolved.find((entry) => entry.interfaceDef.id === "uart")?.active).toBe(false); + expect(resolved.find((entry) => entry.interfaceDef.id === "qspi_flash")?.active).toBe(true); + }); + + it("bridges the wireless radios to the shared LNA_IN RF feed", () => { + expect(iface("wifi_radio").bridgesTo).toContain("lna_in"); + expect(iface("bluetooth_radio").bridgesTo).toContain("lna_in"); + expect(iface("lna_in").bridgesTo).toEqual(["wifi_radio", "bluetooth_radio"]); + }); +}); + diff --git a/test/fixtures/esp32-d0wdq6.ts b/test/fixtures/esp32-d0wdq6.ts new file mode 100644 index 0000000..7491514 --- /dev/null +++ b/test/fixtures/esp32-d0wdq6.ts @@ -0,0 +1,1048 @@ +import type { InterfaceDef, ModuleDef, SlotDef } from "../../src/types/index.js"; +import { + ADC, + DAC, + Ground, + I2C, + Pin, + PowerIn, + SPI, + UART, + defineModule, + type PinConfig, +} from "../../src/protocols/index.js"; + +type Esp32PinConfig = PinConfig & { + functions: string[]; + extraCapabilities?: string[]; + sourceCurrentmA?: number; + sinkCurrentmA?: number; + strapping?: boolean; + externalFlashPad?: boolean; +}; + +const VDD_RTC: [number, number] = [2.3, 3.6]; +const VDD_IO: [number, number] = [1.8, 3.6]; +const GPIO_DRIVE_MA = 28; + +const MATRIX_IN = ["twai_rx", "rmt_in"]; +const MATRIX_OUT = ["twai_tx", "twai_clkout", "rmt_out", "clk_out"]; +const MATRIX_BIDIR = [...MATRIX_IN, ...MATRIX_OUT]; + +function unique(values: string[]): string[] { + return [...new Set(values)]; +} + +function addTraits( + iface: InterfaceDef, + capabilities: string[], + params: Record, +): InterfaceDef { + return { + ...iface, + capabilities: unique([...(iface.capabilities ?? []), ...capabilities]), + traits: [ + ...(iface.traits ?? []), + { type: "esp32_pin_functions", params }, + ], + }; +} + +function esp32Pin(config: Esp32PinConfig): InterfaceDef { + const { + functions, + extraCapabilities = [], + sourceCurrentmA, + sinkCurrentmA, + strapping, + externalFlashPad, + ...pinConfig + } = config; + + const capabilities = [...extraCapabilities]; + if (strapping) capabilities.push("strapping_pin"); + if (externalFlashPad) capabilities.push("external_flash_pad"); + + return addTraits(Pin(pinConfig), capabilities, { + functions, + ...(sourceCurrentmA !== undefined || sinkCurrentmA !== undefined + ? { current_mA: { source: sourceCurrentmA, sink: sinkCurrentmA } } + : {}), + ...(strapping ? { boot_strapping_pin: true } : {}), + ...(externalFlashPad ? { normally_reserved_for_external_flash: true } : {}), + }); +} + +function digitalInput( + id: string, + name: string, + pin: number, + capabilities: string[], +): InterfaceDef { + return { + id, + name, + pin, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "digital", roles: ["input"] }], + capabilities: ["digital_in", ...capabilities], + parameters: [{ id: "voltage", unit: "V", range: VDD_RTC }], + }; +} + +function composed( + id: string, + name: string, + protocolType: string, + roles: string[], + slots: SlotDef[], + profiles?: InterfaceDef["profiles"], + maxInstances?: number, + defaultActive = false, + domain: InterfaceDef["domain"] = "electrical", +): InterfaceDef { + return { + id, + name, + domain, + exposed: true, + default_active: defaultActive, + protocols: [{ type: protocolType, roles }], + slots, + ...(profiles ? { profiles } : {}), + ...(maxInstances !== undefined ? { max_instances: maxInstances } : {}), + }; +} + +const powerPins: InterfaceDef[] = [ + PowerIn({ id: "vdda_1", name: "VDDA Analog Supply 1", pin: 1, voltageV: VDD_RTC, nominalV: 3.3, maxCurrentA: 0.5 }), + PowerIn({ id: "vdd3p3_3", name: "VDD3P3 RF PA Supply 1", pin: 3, voltageV: VDD_RTC, nominalV: 3.3, maxCurrentA: 0.5 }), + PowerIn({ id: "vdd3p3_4", name: "VDD3P3 RF PA Supply 2", pin: 4, voltageV: VDD_RTC, nominalV: 3.3, maxCurrentA: 0.5 }), + PowerIn({ id: "vdd3p3_rtc", name: "VDD3P3 RTC IO Supply", pin: 19, voltageV: VDD_RTC, nominalV: 3.3, maxCurrentA: 0.04 }), + { + id: "vdd_sdio", + name: "VDD_SDIO Supply", + pin: 26, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "power", roles: ["input", "output"] }], + parameters: [ + { id: "voltage", unit: "V", value: 3.3, range: VDD_IO }, + { id: "max_current", unit: "A", value: 0.04 }, + ], + capabilities: ["vdd_sdio"], + }, + PowerIn({ id: "vdd3p3_cpu", name: "VDD3P3 CPU IO Supply", pin: 37, voltageV: VDD_IO, nominalV: 3.3, maxCurrentA: 0.04 }), + PowerIn({ id: "vdda_43", name: "VDDA Analog Supply 2", pin: 43, voltageV: VDD_RTC, nominalV: 3.3, maxCurrentA: 0.5 }), + PowerIn({ id: "vdda_46", name: "VDDA Analog Supply 3", pin: 46, voltageV: VDD_RTC, nominalV: 3.3, maxCurrentA: 0.5 }), + Ground({ id: "gnd", name: "Exposed Ground / Thermal Pad", pin: 49, maxCurrentA: 1.2 }), +]; + +const specialPins: InterfaceDef[] = [ + { + id: "lna_in", + name: "LNA_IN 2.4 GHz RF Feed", + pin: 2, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "rf", roles: ["transceiver"] }], + capabilities: ["rf_2g4_antenna_feed"], + parameters: [ + { id: "impedance", unit: "ohm", value: 50 }, + { id: "frequency", unit: "Hz", range: [2_400_000_000, 2_500_000_000] }, + ], + bridgesTo: ["wifi_radio", "bluetooth_radio"], + }, + digitalInput("chip_pu", "CHIP_PU Enable", 9, ["chip_enable", "reset_input"]), + { + id: "xtal_n", + name: "XTAL_N Crystal Output", + pin: 44, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "clock", roles: ["output"] }], + capabilities: ["crystal_out"], + parameters: [{ id: "clock_freq", unit: "Hz", value: 40_000_000 }], + }, + { + id: "xtal_p", + name: "XTAL_P Crystal Input", + pin: 45, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "clock", roles: ["input"] }], + capabilities: ["crystal_in"], + parameters: [{ id: "clock_freq", unit: "Hz", value: 40_000_000 }], + }, + { + id: "cap2", + name: "CAP2 Bias Network Pin", + pin: 47, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "analog", roles: ["input"] }], + capabilities: ["bias_capacitor_pin"], + }, + { + id: "cap1", + name: "CAP1 Bias Capacitor Pin", + pin: 48, + domain: "electrical", + exposed: true, + default_active: true, + protocols: [{ type: "analog", roles: ["input"] }], + capabilities: ["bias_capacitor_pin"], + }, +]; + +const gpioPins: InterfaceDef[] = [ + esp32Pin({ + id: "sensor_vp", + name: "GPIO36 / SENSOR_VP / ADC1_CH0", + pin: 5, + voltageV: VDD_RTC, + capabilities: { inputOnly: true, analogIn: true, interrupt: true }, + functions: ["GPIO36", "ADC1_CH0", "RTC_GPIO0", "SENSOR_VP"], + extraCapabilities: ["gpio36", "adc1_ch0", "rtc_gpio0", ...MATRIX_IN], + }), + esp32Pin({ + id: "sensor_capp", + name: "GPIO37 / SENSOR_CAPP / ADC1_CH1", + pin: 6, + voltageV: VDD_RTC, + capabilities: { inputOnly: true, analogIn: true, interrupt: true }, + functions: ["GPIO37", "ADC1_CH1", "RTC_GPIO1", "SENSOR_CAPP"], + extraCapabilities: ["gpio37", "adc1_ch1", "rtc_gpio1", ...MATRIX_IN], + }), + esp32Pin({ + id: "sensor_capn", + name: "GPIO38 / SENSOR_CAPN / ADC1_CH2", + pin: 7, + voltageV: VDD_RTC, + capabilities: { inputOnly: true, analogIn: true, interrupt: true }, + functions: ["GPIO38", "ADC1_CH2", "RTC_GPIO2", "SENSOR_CAPN"], + extraCapabilities: ["gpio38", "adc1_ch2", "rtc_gpio2", ...MATRIX_IN], + }), + esp32Pin({ + id: "sensor_vn", + name: "GPIO39 / SENSOR_VN / ADC1_CH3", + pin: 8, + voltageV: VDD_RTC, + capabilities: { inputOnly: true, analogIn: true, interrupt: true }, + functions: ["GPIO39", "ADC1_CH3", "RTC_GPIO3", "SENSOR_VN"], + extraCapabilities: ["gpio39", "adc1_ch3", "rtc_gpio3", ...MATRIX_IN], + }), + esp32Pin({ + id: "vdet_1", + name: "GPIO34 / VDET_1 / ADC1_CH6", + pin: 10, + voltageV: VDD_RTC, + capabilities: { inputOnly: true, analogIn: true, interrupt: true }, + functions: ["GPIO34", "ADC1_CH6", "RTC_GPIO4", "VDET_1"], + extraCapabilities: ["gpio34", "adc1_ch6", "rtc_gpio4", ...MATRIX_IN], + }), + esp32Pin({ + id: "vdet_2", + name: "GPIO35 / VDET_2 / ADC1_CH7", + pin: 11, + voltageV: VDD_RTC, + capabilities: { inputOnly: true, analogIn: true, interrupt: true }, + functions: ["GPIO35", "ADC1_CH7", "RTC_GPIO5", "VDET_2"], + extraCapabilities: ["gpio35", "adc1_ch7", "rtc_gpio5", ...MATRIX_IN], + }), + esp32Pin({ + id: "32k_xp", + name: "GPIO32 / ADC1_CH4 / TOUCH9 / 32K_XP", + pin: 12, + voltageV: VDD_RTC, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, analogIn: true, touch: true, i2cSda: true, i2cScl: true }, + functions: ["GPIO32", "ADC1_CH4", "RTC_GPIO9", "TOUCH9", "32K_XP"], + extraCapabilities: ["gpio32", "adc1_ch4", "rtc_gpio9", "touch9", "xtal_32k_xp", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), + esp32Pin({ + id: "32k_xn", + name: "GPIO33 / ADC1_CH5 / TOUCH8 / 32K_XN", + pin: 13, + voltageV: VDD_RTC, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, analogIn: true, touch: true, i2cSda: true, i2cScl: true }, + functions: ["GPIO33", "ADC1_CH5", "RTC_GPIO8", "TOUCH8", "32K_XN"], + extraCapabilities: ["gpio33", "adc1_ch5", "rtc_gpio8", "touch8", "xtal_32k_xn", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), + esp32Pin({ + id: "gpio25", + name: "GPIO25 / ADC2_CH8 / DAC_1 / EMAC_RXD0", + pin: 14, + voltageV: VDD_RTC, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, analogIn: true, analogOut: true, i2cSda: true, i2cScl: true }, + functions: ["GPIO25", "ADC2_CH8", "RTC_GPIO6", "DAC_1", "EMAC_RXD0"], + extraCapabilities: ["gpio25", "adc2_ch8", "rtc_gpio6", "dac1", "emac_rxd0", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), + esp32Pin({ + id: "gpio26", + name: "GPIO26 / ADC2_CH9 / DAC_2 / EMAC_RXD1", + pin: 15, + voltageV: VDD_RTC, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, analogIn: true, analogOut: true, i2cSda: true, i2cScl: true }, + functions: ["GPIO26", "ADC2_CH9", "RTC_GPIO7", "DAC_2", "EMAC_RXD1"], + extraCapabilities: ["gpio26", "adc2_ch9", "rtc_gpio7", "dac2", "emac_rxd1", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), + esp32Pin({ + id: "gpio27", + name: "GPIO27 / ADC2_CH7 / TOUCH7 / EMAC_RX_DV", + pin: 16, + voltageV: VDD_RTC, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, analogIn: true, touch: true, i2cSda: true, i2cScl: true }, + functions: ["GPIO27", "ADC2_CH7", "RTC_GPIO17", "TOUCH7", "EMAC_RX_DV"], + extraCapabilities: ["gpio27", "adc2_ch7", "rtc_gpio17", "touch7", "emac_rx_dv", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), + esp32Pin({ + id: "mtms", + name: "GPIO14 / MTMS / HSPICLK / ADC2_CH6", + pin: 17, + voltageV: VDD_RTC, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, analogIn: true, touch: true, i2cSda: true, i2cScl: true, spiSck: true }, + functions: ["GPIO14", "ADC2_CH6", "RTC_GPIO16", "TOUCH6", "EMAC_TXD2", "HSPICLK", "HS2_CLK", "SD_CLK", "MTMS"], + extraCapabilities: ["gpio14", "adc2_ch6", "rtc_gpio16", "touch6", "emac_txd2", "sdio_clk", "jtag_tms", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), + esp32Pin({ + id: "mtdi", + name: "GPIO12 / MTDI / HSPIQ / ADC2_CH5", + pin: 18, + voltageV: VDD_RTC, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, analogIn: true, touch: true, i2cSda: true, i2cScl: true, spiMiso: true }, + functions: ["GPIO12", "ADC2_CH5", "RTC_GPIO15", "TOUCH5", "EMAC_TXD3", "HSPIQ", "HS2_DATA2", "SD_DATA2", "MTDI"], + extraCapabilities: ["gpio12", "adc2_ch5", "rtc_gpio15", "touch5", "emac_txd3", "sdio_data2", "jtag_tdi", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + strapping: true, + }), + esp32Pin({ + id: "mtck", + name: "GPIO13 / MTCK / HSPID / ADC2_CH4", + pin: 20, + voltageV: VDD_RTC, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, analogIn: true, touch: true, i2cSda: true, i2cScl: true, spiMosi: true }, + functions: ["GPIO13", "ADC2_CH4", "RTC_GPIO14", "TOUCH4", "EMAC_RX_ER", "HSPID", "HS2_DATA3", "SD_DATA3", "MTCK"], + extraCapabilities: ["gpio13", "adc2_ch4", "rtc_gpio14", "touch4", "emac_rx_er", "sdio_data3", "jtag_tck", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), + esp32Pin({ + id: "mtdo", + name: "GPIO15 / MTDO / HSPICS0 / ADC2_CH3", + pin: 21, + voltageV: VDD_RTC, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, analogIn: true, touch: true, i2cSda: true, i2cScl: true, spiSs: true }, + functions: ["GPIO15", "ADC2_CH3", "RTC_GPIO13", "TOUCH3", "EMAC_RXD3", "HSPICS0", "HS2_CMD", "SD_CMD", "MTDO"], + extraCapabilities: ["gpio15", "adc2_ch3", "rtc_gpio13", "touch3", "emac_rxd3", "sdio_cmd", "jtag_tdo", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + strapping: true, + }), + esp32Pin({ + id: "gpio2", + name: "GPIO2 / ADC2_CH2 / TOUCH2 / HSPIWP", + pin: 22, + voltageV: VDD_RTC, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, analogIn: true, touch: true, i2cSda: true, i2cScl: true }, + functions: ["GPIO2", "ADC2_CH2", "RTC_GPIO12", "TOUCH2", "HSPIWP", "HS2_DATA0", "SD_DATA0"], + extraCapabilities: ["gpio2", "adc2_ch2", "rtc_gpio12", "touch2", "spi_wp", "sdio_data0", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + strapping: true, + }), + esp32Pin({ + id: "gpio0", + name: "GPIO0 / ADC2_CH1 / TOUCH1 / EMAC_TX_CLK", + pin: 23, + voltageV: VDD_RTC, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, analogIn: true, touch: true, i2cSda: true, i2cScl: true }, + functions: ["GPIO0", "ADC2_CH1", "RTC_GPIO11", "TOUCH1", "EMAC_TX_CLK", "CLK_OUT1"], + extraCapabilities: ["gpio0", "adc2_ch1", "rtc_gpio11", "touch1", "emac_tx_clk", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + strapping: true, + }), + esp32Pin({ + id: "gpio4", + name: "GPIO4 / ADC2_CH0 / TOUCH0 / HSPIHD", + pin: 24, + voltageV: VDD_RTC, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, analogIn: true, touch: true, i2cSda: true, i2cScl: true }, + functions: ["GPIO4", "ADC2_CH0", "RTC_GPIO10", "TOUCH0", "EMAC_TX_ER", "HSPIHD", "HS2_DATA1", "SD_DATA1"], + extraCapabilities: ["gpio4", "adc2_ch0", "rtc_gpio10", "touch0", "spi_hd", "emac_tx_er", "sdio_data1", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), + esp32Pin({ + id: "gpio16", + name: "GPIO16 / U2RXD / HS1_DATA4", + pin: 25, + voltageV: VDD_IO, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, i2cSda: true, i2cScl: true, uartRx: true }, + functions: ["GPIO16", "HS1_DATA4", "U2RXD", "EMAC_CLK_OUT"], + extraCapabilities: ["gpio16", "sdio_data4", "emac_clk_out", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), + esp32Pin({ + id: "gpio17", + name: "GPIO17 / U2TXD / HS1_DATA5", + pin: 27, + voltageV: VDD_IO, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, i2cSda: true, i2cScl: true, uartTx: true }, + functions: ["GPIO17", "HS1_DATA5", "U2TXD", "EMAC_CLK_OUT_180"], + extraCapabilities: ["gpio17", "sdio_data5", "emac_clk_out_180", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), + esp32Pin({ + id: "sd_data_2", + name: "GPIO9 / SD_DATA2 / SPIHD / U1RXD", + pin: 28, + voltageV: VDD_IO, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, i2cSda: true, i2cScl: true, uartRx: true }, + functions: ["GPIO9", "HS1_DATA2", "U1RXD", "SD_DATA2", "SPIHD"], + extraCapabilities: ["gpio9", "sdio_data2", "spi_hd", ...MATRIX_BIDIR], + sourceCurrentmA: 30, + sinkCurrentmA: 28, + externalFlashPad: true, + }), + esp32Pin({ + id: "sd_data_3", + name: "GPIO10 / SD_DATA3 / SPIWP / U1TXD", + pin: 29, + voltageV: VDD_IO, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, i2cSda: true, i2cScl: true, uartTx: true }, + functions: ["GPIO10", "HS1_DATA3", "U1TXD", "SD_DATA3", "SPIWP"], + extraCapabilities: ["gpio10", "sdio_data3", "spi_wp", ...MATRIX_BIDIR], + sourceCurrentmA: 30, + sinkCurrentmA: 28, + externalFlashPad: true, + }), + esp32Pin({ + id: "sd_cmd", + name: "GPIO11 / SD_CMD / SPICS0 / U1RTS", + pin: 30, + voltageV: VDD_IO, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, i2cSda: true, i2cScl: true, spiSs: true, uartRts: true }, + functions: ["GPIO11", "HS1_CMD", "U1RTS", "SD_CMD", "SPICS0"], + extraCapabilities: ["gpio11", "sdio_cmd", ...MATRIX_BIDIR], + sourceCurrentmA: 30, + sinkCurrentmA: 28, + externalFlashPad: true, + }), + esp32Pin({ + id: "sd_clk", + name: "GPIO6 / SD_CLK / SPICLK / U1CTS", + pin: 31, + voltageV: VDD_IO, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, i2cSda: true, i2cScl: true, spiSck: true, uartCts: true }, + functions: ["GPIO6", "HS1_CLK", "U1CTS", "SD_CLK", "SPICLK"], + extraCapabilities: ["gpio6", "sdio_clk", ...MATRIX_BIDIR], + sourceCurrentmA: 30, + sinkCurrentmA: 28, + externalFlashPad: true, + }), + esp32Pin({ + id: "sd_data_0", + name: "GPIO7 / SD_DATA0 / SPIQ / U2RTS", + pin: 32, + voltageV: VDD_IO, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, i2cSda: true, i2cScl: true, spiMiso: true, uartRts: true }, + functions: ["GPIO7", "HS1_DATA0", "U2RTS", "SD_DATA0", "SPIQ"], + extraCapabilities: ["gpio7", "sdio_data0", ...MATRIX_BIDIR], + sourceCurrentmA: 30, + sinkCurrentmA: 28, + externalFlashPad: true, + }), + esp32Pin({ + id: "sd_data_1", + name: "GPIO8 / SD_DATA1 / SPID / U2CTS", + pin: 33, + voltageV: VDD_IO, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, i2cSda: true, i2cScl: true, spiMosi: true, uartCts: true }, + functions: ["GPIO8", "HS1_DATA1", "U2CTS", "SD_DATA1", "SPID"], + extraCapabilities: ["gpio8", "sdio_data1", ...MATRIX_BIDIR], + sourceCurrentmA: 30, + sinkCurrentmA: 28, + externalFlashPad: true, + }), + esp32Pin({ + id: "gpio5", + name: "GPIO5 / VSPICS0 / EMAC_RX_CLK", + pin: 34, + voltageV: VDD_IO, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, i2cSda: true, i2cScl: true, spiSs: true }, + functions: ["GPIO5", "HS1_DATA6", "VSPICS0", "EMAC_RX_CLK"], + extraCapabilities: ["gpio5", "sdio_data6", "emac_rx_clk", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + strapping: true, + }), + esp32Pin({ + id: "gpio18", + name: "GPIO18 / VSPICLK / HS1_DATA7", + pin: 35, + voltageV: VDD_IO, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, i2cSda: true, i2cScl: true, spiSck: true }, + functions: ["GPIO18", "HS1_DATA7", "VSPICLK"], + extraCapabilities: ["gpio18", "sdio_data7", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), + esp32Pin({ + id: "gpio23", + name: "GPIO23 / VSPID / HS1_STROBE", + pin: 36, + voltageV: VDD_IO, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, i2cSda: true, i2cScl: true, spiMosi: true }, + functions: ["GPIO23", "HS1_STROBE", "VSPID"], + extraCapabilities: ["gpio23", "sdio_strobe", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), + esp32Pin({ + id: "gpio19", + name: "GPIO19 / U0CTS / VSPIQ / EMAC_TXD0", + pin: 38, + voltageV: VDD_IO, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, i2cSda: true, i2cScl: true, spiMiso: true, uartCts: true }, + functions: ["GPIO19", "U0CTS", "VSPIQ", "EMAC_TXD0"], + extraCapabilities: ["gpio19", "emac_txd0", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), + esp32Pin({ + id: "gpio22", + name: "GPIO22 / U0RTS / VSPIWP / EMAC_TXD1", + pin: 39, + voltageV: VDD_IO, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, i2cSda: true, i2cScl: true, uartRts: true }, + functions: ["GPIO22", "U0RTS", "VSPIWP", "EMAC_TXD1"], + extraCapabilities: ["gpio22", "spi_wp", "emac_txd1", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), + esp32Pin({ + id: "u0rxd", + name: "GPIO3 / U0RXD / CLK_OUT2", + pin: 40, + voltageV: VDD_IO, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, i2cSda: true, i2cScl: true, uartRx: true }, + functions: ["GPIO3", "U0RXD", "CLK_OUT2"], + extraCapabilities: ["gpio3", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), + esp32Pin({ + id: "u0txd", + name: "GPIO1 / U0TXD / CLK_OUT3 / EMAC_RXD2", + pin: 41, + voltageV: VDD_IO, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, i2cSda: true, i2cScl: true, uartTx: true }, + functions: ["GPIO1", "U0TXD", "CLK_OUT3", "EMAC_RXD2"], + extraCapabilities: ["gpio1", "emac_rxd2", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), + esp32Pin({ + id: "gpio21", + name: "GPIO21 / VSPIHD / EMAC_TX_EN", + pin: 42, + voltageV: VDD_IO, + driveCurrentmA: GPIO_DRIVE_MA, + capabilities: { pwm: true, interrupt: true, i2cSda: true, i2cScl: true }, + functions: ["GPIO21", "VSPIHD", "EMAC_TX_EN"], + extraCapabilities: ["gpio21", "spi_hd", "emac_tx_en", ...MATRIX_BIDIR], + sourceCurrentmA: 40, + sinkCurrentmA: 28, + }), +]; + +const qspiFlash = composed( + "qspi_flash", + "SPI0/1 External Flash Interface", + "spi", + ["master"], + [ + { id: "sck", required: true, match: { protocol: "spi", role: "clock", capability: "spi_sck" } }, + { id: "cs", required: true, match: { protocol: "spi", role: "select", capability: "spi_ss" } }, + { id: "io0", required: true, match: { protocol: "spi", role: "data_out", capability: "spi_mosi" } }, + { id: "io1", required: true, match: { protocol: "spi", role: "data_in", capability: "spi_miso" } }, + { id: "io2", required: true, match: { capability: "spi_wp" } }, + { id: "io3", required: true, match: { capability: "spi_hd" } }, + ], + [ + { + id: "spi0_flash_default", + label: "SPI0/1 Flash (GPIO6-GPIO11)", + default_active: true, + bindings: { + sck: "sd_clk", + cs: "sd_cmd", + io0: "sd_data_1", + io1: "sd_data_0", + io2: "sd_data_3", + io3: "sd_data_2", + }, + }, + ], + 1, + true, +); + +const ledcPwm = composed( + "ledc_pwm", + "LEDC PWM Generator", + "pwm", + ["output"], + [{ id: "channel", required: true, match: { protocol: "pwm", role: "output", capability: "pwm_out" } }], + undefined, + 16, +); + +const twai = composed( + "twai", + "TWAI CAN 2.0 Controller", + "twai", + ["controller"], + [ + { id: "rx", required: true, match: { capability: "twai_rx" } }, + { id: "tx", required: true, match: { capability: "twai_tx" } }, + ], + undefined, + 1, +); + +const touch = composed( + "touch", + "Capacitive Touch Sensors", + "capacitive_touch", + ["input"], + [{ id: "channel", required: true, count: 10, match: { capability: "touch" } }], + [ + { + id: "touch_channels", + bindings: { + channel: ["gpio4", "gpio0", "gpio2", "mtdo", "mtck", "mtdi", "mtms", "gpio27", "32k_xn", "32k_xp"], + }, + }, + ], + 10, +); + +const jtag = composed( + "jtag", + "JTAG Debug Port", + "jtag", + ["target"], + [ + { id: "tms", required: true, match: { capability: "jtag_tms" } }, + { id: "tck", required: true, match: { capability: "jtag_tck" } }, + { id: "tdi", required: true, match: { capability: "jtag_tdi" } }, + { id: "tdo", required: true, match: { capability: "jtag_tdo" } }, + ], + [{ id: "jtag_default", bindings: { tms: "mtms", tck: "mtck", tdi: "mtdi", tdo: "mtdo" } }], + 1, +); + +const sdio = composed( + "sdio", + "SDIO / SDMMC Host", + "sdio", + ["host", "device"], + [ + { id: "clk", required: true, match: { capability: "sdio_clk" } }, + { id: "cmd", required: true, match: { capability: "sdio_cmd" } }, + { id: "data0", required: true, match: { capability: "sdio_data0" } }, + { id: "data1", required: true, match: { capability: "sdio_data1" } }, + { id: "data2", required: false, match: { capability: "sdio_data2" } }, + { id: "data3", required: false, match: { capability: "sdio_data3" } }, + { id: "data4", required: false, match: { capability: "sdio_data4" } }, + { id: "data5", required: false, match: { capability: "sdio_data5" } }, + { id: "data6", required: false, match: { capability: "sdio_data6" } }, + { id: "data7", required: false, match: { capability: "sdio_data7" } }, + { id: "strobe", required: false, match: { capability: "sdio_strobe" } }, + ], + [ + { + id: "sdio_slot1_8bit", + label: "SDIO Slot 1 (GPIO6-GPIO11, GPIO16-GPIO18, GPIO23)", + bindings: { + clk: "sd_clk", + cmd: "sd_cmd", + data0: "sd_data_0", + data1: "sd_data_1", + data2: "sd_data_2", + data3: "sd_data_3", + data4: "gpio16", + data5: "gpio17", + data6: "gpio5", + data7: "gpio18", + strobe: "gpio23", + }, + }, + { + id: "sdio_slot2_4bit", + label: "SDIO Slot 2 (GPIO2, GPIO4, GPIO12-GPIO15)", + bindings: { + clk: "mtms", + cmd: "mtdo", + data0: "gpio2", + data1: "gpio4", + data2: "mtdi", + data3: "mtck", + }, + }, + ], + 2, +); + +const ethernetMac = composed( + "ethernet_mac", + "Ethernet MAC MII/RMII Signals", + "ethernet_mac", + ["controller"], + [ + { id: "txd0", required: true, match: { capability: "emac_txd0" } }, + { id: "txd1", required: true, match: { capability: "emac_txd1" } }, + { id: "tx_en", required: true, match: { capability: "emac_tx_en" } }, + { id: "rxd0", required: true, match: { capability: "emac_rxd0" } }, + { id: "rxd1", required: true, match: { capability: "emac_rxd1" } }, + { id: "rx_dv", required: false, match: { capability: "emac_rx_dv" } }, + { id: "rx_clk", required: false, match: { capability: "emac_rx_clk" } }, + { id: "tx_clk", required: false, match: { capability: "emac_tx_clk" } }, + { id: "clk_out", required: false, match: { capability: "emac_clk_out" } }, + ], + [ + { + id: "emac_default", + bindings: { + txd0: "gpio19", + txd1: "gpio22", + tx_en: "gpio21", + rxd0: "gpio25", + rxd1: "gpio26", + rx_dv: "gpio27", + rx_clk: "gpio5", + tx_clk: "gpio0", + clk_out: "gpio16", + }, + }, + ], + 1, +); + +const wifiRadio: InterfaceDef = { + id: "wifi_radio", + name: "Wi-Fi 2.4 GHz Radio", + domain: "network", + exposed: true, + default_active: false, + protocols: [{ type: "wifi", roles: ["station", "access_point"] }], + capabilities: ["wifi_802_11_bgn", "wifi_2g4"], + parameters: [ + { id: "frequency", unit: "Hz", range: [2_400_000_000, 2_500_000_000] }, + { id: "max_bandwidth", unit: "dimensionless", value: 150 }, + ], + bridgesTo: ["lna_in"], +}; + +const bluetoothRadio: InterfaceDef = { + id: "bluetooth_radio", + name: "Bluetooth 4.2 BR/EDR + BLE Radio", + domain: "network", + exposed: true, + default_active: false, + protocols: [{ type: "bluetooth", roles: ["peer"] }], + capabilities: ["bluetooth_classic", "bluetooth_le", "bluetooth_2g4"], + parameters: [{ id: "frequency", unit: "Hz", range: [2_400_000_000, 2_500_000_000] }], + bridgesTo: ["lna_in"], +}; + +const footprintMount: InterfaceDef = { + id: "footprint_mounting", + name: "QFN-48 6x6 mm Surface-Mount Footprint", + domain: "mechanical", + exposed: true, + default_active: true, + protocols: [{ type: "mechanical_connection", roles: ["mounting_point"] }], + capabilities: ["qfn48_6x6_0p4mm", "surface_mount"], +}; + +const thermalPad: InterfaceDef = { + id: "thermal_pad", + name: "Exposed Thermal Pad", + pin: 49, + domain: "thermal", + exposed: true, + default_active: true, + protocols: [{ type: "thermal_connection", roles: ["thermal_source"] }], + capabilities: ["heat_sink", "pcb_thermal_plane"], +}; + +export const ESP32D0WDQ6: ModuleDef = defineModule({ + id: "esp32-d0wdq6", + name: "Espressif ESP32-D0WDQ6", + version: "1.0.0", + manufacturer: "Espressif Systems", + part_number: "ESP32-D0WDQ6", + description: + "Bare ESP32-D0WDQ6 SoC in QFN-48 6x6 mm package with dual-core Xtensa LX6, 2.4 GHz Wi-Fi, Bluetooth 4.2 BR/EDR + BLE, and external SPI flash requirement.", + tags: ["esp32", "soc", "microcontroller", "wifi", "bluetooth", "ble", "xtensa-lx6", "qfn-48", "iot"], + categories: ["microcontroller", "connectivity.wireless"], + + interfaces: [ + ...powerPins, + ...specialPins, + ...gpioPins, + + ...ADC({ + id: "adc1", + name: "ADC1 SAR Converter", + instance: 1, + resolutionBits: 12, + rangeV: [0, 3.3], + channels: ["sensor_vp", "sensor_capp", "sensor_capn", "sensor_vn", "32k_xp", "32k_xn", "vdet_1", "vdet_2"], + }), + ...ADC({ + id: "adc2", + name: "ADC2 SAR Converter", + instance: 2, + resolutionBits: 12, + rangeV: [0, 3.3], + channels: ["gpio4", "gpio0", "gpio2", "mtdo", "mtck", "mtdi", "mtms", "gpio27", "gpio25", "gpio26"], + }), + ...DAC({ + id: "dac", + name: "8-bit DAC Outputs", + resolutionBits: 8, + rangeV: [0, 3.3], + channels: ["gpio25", "gpio26"], + }), + ...I2C({ + id: "i2c", + name: "I2C Controllers", + roles: ["master", "slave"], + clockFreqHz: [100_000, 400_000], + voltageV: 3.3, + maxInstances: 2, + profiles: [ + { id: "i2c0_default", label: "I2C0 Default (GPIO21/GPIO22)", sda: "gpio21", scl: "gpio22" }, + ], + }), + qspiFlash, + ...SPI({ + id: "spi", + name: "HSPI / VSPI User SPI Controllers", + roles: ["master", "slave"], + clockFreqHz: [100_000, 80_000_000], + voltageV: 3.3, + maxInstances: 2, + profiles: [ + { id: "hspi_default", label: "HSPI IO_MUX", mosi: "mtck", miso: "mtdi", sck: "mtms", ss: "mtdo" }, + { id: "vspi_default", label: "VSPI IO_MUX", mosi: "gpio23", miso: "gpio19", sck: "gpio18", ss: "gpio5" }, + ], + }), + ...UART({ + id: "uart", + name: "UART Controllers", + roles: ["host", "device"], + baudRate: [300, 5_000_000], + voltageV: 3.3, + maxInstances: 3, + profiles: [ + { id: "uart0_default", label: "UART0", rx: "u0rxd", tx: "u0txd", rts: "gpio22", cts: "gpio19" }, + { id: "uart1_default", label: "UART1", rx: "sd_data_2", tx: "sd_data_3", rts: "sd_cmd", cts: "sd_clk" }, + { id: "uart2_default", label: "UART2", rx: "gpio16", tx: "gpio17", rts: "sd_data_0", cts: "sd_data_1" }, + ], + }), + ledcPwm, + twai, + touch, + jtag, + sdio, + ethernetMac, + wifiRadio, + bluetoothRadio, + footprintMount, + thermalPad, + ], + + interfaceGroups: [ + { + id: "required_power_pins", + label: "Required Power Pins", + members: ["vdda_1", "vdd3p3_3", "vdd3p3_4", "vdd3p3_rtc", "vdd_sdio", "vdd3p3_cpu", "vdda_43", "vdda_46", "gnd"], + policy: "all_of", + }, + { + id: "boot_strapping_pins", + label: "Boot Strapping Pins", + members: ["gpio0", "gpio2", "gpio5", "mtdi", "mtdo"], + policy: "all_of", + }, + ], + + requirements: [ + { + type: "power", + description: "Primary ESP32 supply rails must be within the ESP32 operating range and capable of Wi-Fi/BT transmit current bursts.", + voltage_V: [2.3, 3.6], + current_mA: 500, + }, + { + type: "interface", + description: "ESP32-D0WDQ6 has no in-package flash; normal boot requires external SPI flash on the SPI0/1 flash interface.", + interface_protocol: "spi", + }, + { + type: "capability", + description: "LNA_IN must connect to a matched 50 ohm 2.4 GHz antenna network.", + capability: "rf_2g4_antenna_feed", + }, + ], + + domains: [ + { + domain: "electrical", + power_domains: [ + { id: "vdda_analog", name: "VDDA / VDD3P3 Analog and RF", nominal_voltage_V: 3.3, voltage_range_V: VDD_RTC, max_current_mA: 500 }, + { id: "io_3v3", name: "VDD_SDIO / VDD3P3_CPU IO", nominal_voltage_V: 3.3, voltage_range_V: VDD_IO, max_current_mA: 40 }, + { id: "gnd", name: "Common Ground", nominal_voltage_V: 0, voltage_range_V: [0, 0], max_current_mA: 1200 }, + ], + metadata: { + pin_count: 49, + max_operating_freq_Hz: 240_000_000, + typical_power_mW: 792, + package_type: "QFN-48", + }, + }, + { + domain: "mechanical", + dimensions_mm: { length: 6, width: 6, height: 0.85 }, + metadata: { + package_type: "QFN-48", + pitch_mm: 0.4, + mounting_method: "surface_mount", + }, + }, + { + domain: "thermal", + operating_temperature_C: [-40, 125], + metadata: { + thermal_design_power_W: 0.79, + primary_heat_path: "exposed_pad_to_pcb_ground_plane", + }, + }, + { + domain: "network", + metadata: { + wireless_standards: ["802.11b", "802.11g", "802.11n", "Bluetooth 4.2 BR/EDR", "Bluetooth LE"], + frequency_bands_ghz: [2.4], + max_bandwidth_mbps: 150, + }, + }, + ], + + traits: [ + { type: "lifecycle_status", params: { status: "not_recommended_for_new_designs", source_definition: "ProtoPart esp32-d0wdq6" } }, + { type: "requires_external_flash", params: { interfaceId: "qspi_flash", defaultProfile: "spi0_flash_default" } }, + { type: "wireless_soc", params: { radios: ["wifi_radio", "bluetooth_radio"], rfFeed: "lna_in" } }, + ], + + artifacts: [ + { + id: "art_datasheet", + name: "ESP32 Series Datasheet v5.2", + type: "datasheet", + url: "https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf", + }, + { + id: "art_technical_reference", + name: "ESP32 Technical Reference Manual", + type: "datasheet", + url: "https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf", + }, + { + id: "art_product_page", + name: "Espressif ESP32 Product Page", + type: "documentation", + url: "https://www.espressif.com/en/products/socs/esp32", + }, + { + id: "art_snapeda", + name: "SnapEDA Symbol and Footprint", + type: "cad", + url: "https://www.snapeda.com/parts/ESP32-D0WDQ6/Espressif%20Systems/view-part/?ref=digikey", + }, + { + id: "art_ultralibrarian", + name: "Ultra Librarian CAD Models", + type: "cad", + url: "https://app.ultralibrarian.com/details/A7AAB95B-922A-11EA-B5D0-0AEBB021A1EA/Espressif-Systems/ESP32-D0WDQ6?ref=digikey", + }, + { + id: "art_chip_image", + name: "ESP32-D0WDQ6 Product Photo", + type: "custom", + filePath: "../ProtoPart/protoparts/esp32-d0wdq6/artifacts/images/ESP32-D0WDQ6_tilted.png", + mimeType: "image/png", + tags: ["image", "product-photo"], + }, + ], + + geometry: { + xScale: 1, + yScale: 1, + outline: { preset: "rectangle" }, + }, +}); + diff --git a/tsconfig.json b/tsconfig.json index 69d3163..84ba9d0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,6 @@ }, "skipLibCheck": true }, - "include": ["src/**/*.ts", "test/**/*.ts"], + "include": ["src/**/*.ts", "test/**/*.ts", "library/**/*.ts", "scripts/**/*.ts"], "exclude": ["node_modules", "dist"] }