Controller board: ESP32 + 2x StepStick carrier (v0.4), FluidNC config
- KiCad project generated by pcb/tools (design, route, schematic, fab outputs). - v0.4 fixes the v0.3 review findings: low-dropout 3.3 V regulator, OR'd 5 V supply, polyfuse, USB ESD protection, input clamps, ceramic decoupling at the drivers, A4988/DRV8825 jumper, ground stitching. DRC/ERC clean. - v0.3 (as ordered) kept in pcb/archive/v0.3. - FluidNC config.yaml for the board; rho hard limit on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c498882bbb
commit
1400cc8afd
46 files changed
+226470
No files matched your search
+115
@@ -0,0 +1,115 @@
|
||||
# SandTable Controller v0.4 (custom PCB)
|
||||
|
||||

|
||||
|
||||
A 2-layer, 80 × 63 mm board that replaces the Arduino Uno + CNC shield in the sand table. It uses the **same 4 mounting holes as an Uno**, so it fits the deck standoffs.
|
||||
|
||||
**Status:** v0.4 is designed and checked in KiCad, with order files ready for PCBWay. It has not been built yet. v0.3 boards have been ordered; their files are kept in [`archive/v0.3/`](archive/v0.3/), and "Using a v0.3 board" below lists what to watch for on them.
|
||||
|
||||
- **MCU:** ESP32-WROOM-32E (Wi-Fi), running FluidNC. USB-C programming through a CH340C with auto-reset, so no buttons need pressing. ESD protection on the USB data lines.
|
||||
- **Motors:** 2× DRV8825 StepStick sockets, each with a 100 µF bulk capacitor plus a 10 µF ceramic right at its power pins. The board fixes the drivers at 1/32 microstepping and keeps them awake: M0–M2, RESET and SLEEP are all tied to 3.3 V. A4988 drivers also fit (at 1/16) after bridging solder jumper JP1.
|
||||
- **Power:** 12 V barrel jack → 2.5 A resettable fuse → reverse-polarity diode → RECOM R-78E 5 V 1 A switching regulator → AP7361C 3.3 V low-dropout regulator. USB 5 V and regulator 5 V are diode-ORed, so USB alone powers the logic for flashing. The motors need the 12 V supply.
|
||||
- **Connectors:** JST-XH to match the motor cable plugs:
|
||||
- 2× motor (pin order matches the Arduino CNC shield: each coil on an adjacent pair)
|
||||
- home switch
|
||||
- hall sensor (5 V supply)
|
||||
- WS2812B LED data (buffered to 5 V)
|
||||
- 4-pin expansion header
|
||||
- **Inputs:** the home and hall inputs have a pull-up to 3.3 V, an RC filter and a clamp diode to 3.3 V, so a 5 V sensor or module can't overdrive the ESP32.
|
||||
- **Copper:** ground pour on both layers, stitched with about 350 vias. The ESP32 antenna sits over a copper-free area at the board edge.
|
||||
|
||||
## Changes in v0.4
|
||||
From a design review of v0.3. None of these stop a v0.3 board from working; they add margin and make the board safe for other people's parts.
|
||||
|
||||
| Area | v0.3 | v0.4 |
|
||||
|---|---|---|
|
||||
| 3.3 V regulator | AMS1117: needs about 4.4 V in, but only gets about 4.5 V after the OR diode, so the rail could sag during Wi-Fi bursts | AP7361C-33E: 0.09 V dropout at 300 mA, stable with ceramic capacitors. **Different pinout** (1 IN, 2 GND/tab, 3 OUT); never swap in a 1117-type part |
|
||||
| 5 V regulator | R-78E5.0-0.5 (0.5 A): not enough for the ESP32 plus an LED strip | R-78E5.0-1.0 (1 A, same footprint). The OR diode D2 is now a 3 A SS34 |
|
||||
| Driver supply | 100 µF capacitors 19 mm from the VMOT pins | Plus a 10 µF 50 V ceramic across VMOT and GND at each socket (C16, C17). They sit between the socket strips, under the module |
|
||||
| Driver pin 2 (FAULT on DRV8825, VDD on A4988) | Tied to 3.3 V: safe only on modules with a series resistor on FAULT | Solder jumper JP1, open by default. Bridge it only for A4988 modules |
|
||||
| Home / hall inputs | A 5 V sensor module with its own pull-up could push about 4 V into IO34 / IO35 | BAT54 clamp diodes to 3.3 V (D6, D7), after the 1 k series resistors |
|
||||
| 12 V input | No fuse | Bourns MF-MSMF250/16X-2 resettable fuse (2.5 A hold, 5 A trip) |
|
||||
| USB | No ESD protection | USBLC6-2SC6 on D+ / D− |
|
||||
| Motor traces | One at 0.3 mm, the rest 0.5 mm | All 0.5 mm (about 1.5 A each); the router may not narrow motor or 12 V traces |
|
||||
| Ground | 29 ground vias, 3–4 under the ESP32 | About 350 stitching vias, about 100 under the ESP32 |
|
||||
| Motor plug pin order | Coil pairs on pins 1–2 and 3–4 | Same pairs, in the driver's own order (as on the CNC shield). An axis may now turn the other way than on v0.3: see "Firmware" below |
|
||||
|
||||
## Checks (KiCad 9.0.7, `kicad-cli`)
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| ERC (schematic) | 0 violations |
|
||||
| DRC (board, all severities) against PCBWay's standard limits: 6/6 mil trace/space, 0.3 mm hole, 0.15 mm ring | 0 violations, 0 unconnected |
|
||||
| Schematic ↔ PCB parity | 0 differences |
|
||||
| Vias within 0.2 mm of a pad, holes inside a pad (checked from the board file) | 0 |
|
||||
| Pinouts of every active part checked against its datasheet or KiCad's library symbol | ESP32, CH340C, AP7361C, 74AHCT1G125, R-78E, USBLC6, MMBT3904, USB-C, driver sockets |
|
||||
| Fit in the sand table (v3 shell and deck) | Board corners 3 mm from the wall, clear of the deck bosses; jack and USB next to the cable notch |
|
||||
|
||||
## Ordering from PCBWay
|
||||
Everything for the order is in [`pcbway-order/`](pcbway-order/). [`ORDER.md`](pcbway-order/ORDER.md) there lists the exact settings to enter, which file goes in which upload box, and a note to paste for PCBWay.
|
||||
|
||||
| File | What |
|
||||
|---|---|
|
||||
| `1-gerbers-…zip` | Board files: Gerbers plus separate plated and non-plated drill files |
|
||||
| `2-bom-…csv` | Parts list for turnkey assembly, with manufacturer part numbers |
|
||||
| `3-centroid-…csv` | Part positions in mm, origin at the board's bottom-left corner |
|
||||
| `assembly-top-…pdf`, `schematic-…pdf` | For PCBWay's engineers if they ask |
|
||||
|
||||
The DRV8825 modules are not on the order; you plug in your own. The back of the board has PCBWay's "WayWayWay" marker, so their order number prints there instead of on the front.
|
||||
|
||||
## Before first power-up
|
||||
1. **Driver type.** DRV8825: leave JP1 (under the theta driver socket, marked "A4988 ONLY") open. A4988: bridge JP1 with a blob of solder, or the A4988 has no logic supply.
|
||||
2. **Driver direction.** Line up the module's VMOT, EN and DIR pins with the VMOT / EN / DIR labels on the board. A driver plugged in backwards is destroyed.
|
||||
3. **Driver current.** On a DRV8825, current limit = 2 × Vref (with the usual 0.1 Ω sense resistors).
|
||||
- Start at about 70 % of the motor's rated current. For example, a 1.0 A motor needs Vref ≈ 0.35 V.
|
||||
- Measure Vref between the trim pot and GND with the 12 V supply connected.
|
||||
- Never plug or unplug a motor while the power is on.
|
||||
4. **Home switch wiring.** Connect the switch's COM to HOME pin 3 (GND) and its NO contact to pin 2.
|
||||
5. **Firmware.**
|
||||
- Flash FluidNC over USB with the FluidNC web installer.
|
||||
- Upload `fluidnc/config.yaml` as `config.yaml`.
|
||||
- In the config, X is theta in degrees and Y is rho in mm. `$H` homes rho.
|
||||
- If an axis turns the wrong way, add `:low` to its direction pin.
|
||||
|
||||
## Using a v0.3 board
|
||||
The v0.3 boards work; the v0.4 changes add margin. On a v0.3 board:
|
||||
- **Random resets or Wi-Fi dropouts**, mostly on USB power alone: the AMS1117 is short of headroom. Run from the 12 V supply, or replace U3 with a low-dropout regulator that has the **same pinout as an AMS1117** (pin 1 GND, pin 2 OUT and tab, pin 3 IN). ST's LDL1117S33R is sold as one; check its datasheet's pin table before buying. The AP7361C used on v0.4 has a different pinout and does not fit a v0.3 board.
|
||||
- **DRV8825 clones:** check that the module has a resistor in series with its FAULT pin, as Pololu's does. If not, clip or bend that pin (FAULT, between A2 and the last GND on the motor-pin row) so it doesn't plug in; a DRV8825 doesn't need it.
|
||||
- **LED strip:** the 5 V regulator gives 0.5 A in total, which leaves room for only 2–3 LEDs at full white.
|
||||
- **Hall sensor:** use a bare open-collector sensor (A3144 style), not a breakout module with its own pull-up to 5 V.
|
||||
- **Motor traces:** fine up to about 1.2 A per motor.
|
||||
|
||||
## ESP32 pin map
|
||||
|
||||
| Function | GPIO |
|
||||
|---|---|
|
||||
| Theta (spin) STEP / DIR | IO18 / IO19 |
|
||||
| Rho (arm) STEP / DIR | IO27 / IO14 |
|
||||
| Driver enable (shared, low = on, pulled up so the drivers are off at boot) | IO13 |
|
||||
| Rho home switch | IO34 |
|
||||
| Theta hall sensor | IO35 |
|
||||
| LED strip data (through a 74AHCT1G125 buffer) | IO33 |
|
||||
| Status LED | IO2 |
|
||||
| Expansion header | IO32, IO25 |
|
||||
| Free, not connected | IO16, IO17 |
|
||||
|
||||
## Files
|
||||
|
||||
| Path | What |
|
||||
|---|---|
|
||||
| `kicad/` | KiCad 9 project: schematic, board, and project-local symbol and footprint tables |
|
||||
| `sandtable-controller-schematic.pdf` | Schematic |
|
||||
| `pcbway-order/` | Everything to upload to PCBWay, plus `ORDER.md` with the settings |
|
||||
| `renders/` | 3D renders |
|
||||
| `sandtable-controller.step` | 3D model |
|
||||
| `fluidnc/config.yaml` | FluidNC machine config (a starting point, not yet run on hardware) |
|
||||
| `archive/v0.3/` | The v0.3 board as ordered: KiCad files, order files, schematic and the generator's `design.py` |
|
||||
| `tools/` | Python generator: `design.py` (parts, placement, nets, sourcing) → `route.py` (router, ground pour, stitching vias, board file), `sch.py` (schematic), `fab.py` (PCBWay order folder, renders, copy into this folder). `place_new.py` finds free spots for parts added in a revision |
|
||||
|
||||
To regenerate everything, run `python3 tools/route.py && python3 tools/sch.py && python3 tools/fab.py`. The KiCad files are built in `~/kicad/sandtable-controller` because the KiCad snap can't read inside `~/snap/`.
|
||||
|
||||
## Notes
|
||||
- **5 V budget:** the 5 V regulator supplies 1 A. The ESP32 takes up to about 0.4 A during Wi-Fi bursts, which leaves about 0.6 A for an LED strip: roughly 10 WS2812B LEDs at full white, or more at lower brightness. On USB power alone, keep the strip off; USB gives 0.5 A in total. Use a separate 5 V supply for longer strips.
|
||||
- **Input voltage:** 12 V. The fuse is rated 16 V, so don't use a higher supply.
|
||||
- **Noise:** DRV8825s are louder than TMC2209s; expect some motor whine. TMC2209 modules in standalone mode might work in these sockets, but their pin layouts vary by brand, so check before trying one.
|
||||
- **LED strip:** FluidNC doesn't drive the WS2812B header. It's there for other firmware or an add-on later.
|
||||
@@ -0,0 +1,98 @@
|
||||
# SandTable Controller v0.3 (custom PCB)
|
||||
|
||||

|
||||
|
||||
A 2-layer, 80 × 63 mm board that replaces the Arduino Uno + CNC shield in the sand table. It uses the **same 4 mounting holes as an Uno**, so it fits the deck standoffs.
|
||||
|
||||
**Status: designed and checked in KiCad, with order files ready for PCBWay. It has not been built or tested yet.**
|
||||
|
||||
- **MCU:** ESP32-WROOM-32E (Wi-Fi), running FluidNC. USB-C programming through a CH340C with auto-reset, so no buttons need pressing.
|
||||
- **Motors:** 2× DRV8825 StepStick sockets, with 100 µF bulk capacitors. The board fixes the drivers at 1/32 microstepping and keeps them awake: M0–M2, RESET and SLEEP are all tied to 3.3 V. A4988 drivers also fit and run at 1/16.
|
||||
- **Power:** 12 V barrel jack with a reverse-polarity diode → RECOM R-78E 5 V switching regulator → AMS1117 3.3 V. USB 5 V and regulator 5 V are diode-ORed, so USB alone powers the logic for flashing. The motors need the 12 V supply.
|
||||
- **Connectors:** JST-XH to match the motor cable plugs:
|
||||
- 2× motor
|
||||
- home switch
|
||||
- hall sensor (5 V)
|
||||
- WS2812B LED data (buffered to 5 V)
|
||||
- 4-pin expansion header
|
||||
- **Copper:** ground pour on both layers. The ESP32 antenna sits over a copper-free area at the board edge.
|
||||
|
||||
## Changes in v0.3
|
||||
- **No holes in solder pads.** PCBWay's review of v0.2 found vias inside pads, which they would only build reliably by filling them with resin, for extra cost.
|
||||
- The router now keeps every via at least 0.2 mm from every pad. In v0.2, 26 vias sat in or overlapped pads of small parts.
|
||||
- The ESP32's 12 ground-pad holes are replaced by one solid copper pad, with the same 9 solder windows.
|
||||
- PCBWay can now build it as drawn, with no via filling.
|
||||
- **Parts list:** exact part numbers for the 4-pin header (Sullins PRPC004SAAN-RC), the driver sockets (Sullins PPTC081LFBN-RC) and the 100 µF capacitors (Panasonic EEE-FK1V101GP), as agreed with PCBWay.
|
||||
|
||||
## Changes in v0.2
|
||||
- **Drivers:** DRV8825 instead of TMC2209 in UART mode. The serial line and its resistor are removed, so IO16/IO17 are now free.
|
||||
- **Holes:** the smallest hole is now 0.3 mm. The ESP32 footprint's 0.2 mm ground-pad vias were enlarged, because PCBWay charges extra for holes under 0.3 mm.
|
||||
- **Order files:** a PCBWay order folder (Gerbers, parts list, placement file, assembly drawing, order sheet) replaces the JLCPCB files.
|
||||
- **Silkscreen:** VMOT / EN / DIR are printed beside each driver socket.
|
||||
|
||||
## Checks (KiCad 9.0.7, `kicad-cli`)
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| ERC (schematic) | 0 violations |
|
||||
| DRC (board, all severities) against PCBWay's standard limits: 6/6 mil trace/space, 0.3 mm hole, 0.15 mm ring | 0 violations, 0 unconnected |
|
||||
| Schematic ↔ PCB parity | 0 differences |
|
||||
| Vias within 0.2 mm of a pad, holes inside a pad (checked from the board file) | 0 |
|
||||
|
||||
## Ordering from PCBWay
|
||||
Everything for the order is in [`pcbway-order/`](pcbway-order/). [`ORDER.md`](pcbway-order/ORDER.md) there lists the exact settings to enter, which file goes in which upload box, and a note to paste for PCBWay.
|
||||
|
||||
| File | What |
|
||||
|---|---|
|
||||
| `1-gerbers-…zip` | Board files: Gerbers plus separate plated and non-plated drill files |
|
||||
| `2-bom-…csv` | Parts list for turnkey assembly, with manufacturer part numbers |
|
||||
| `3-centroid-…csv` | Part positions in mm, origin at the board's bottom-left corner |
|
||||
| `assembly-top-…pdf`, `schematic-…pdf` | For PCBWay's engineers if they ask |
|
||||
|
||||
The DRV8825 modules are not on the order; you plug in your own. The back of the board has PCBWay's "WayWayWay" marker, so their order number prints there instead of on the front.
|
||||
|
||||
## Before first power-up
|
||||
1. **Driver direction.** Line up the module's VMOT, EN and DIR pins with the VMOT / EN / DIR labels on the board. A driver plugged in backwards is destroyed.
|
||||
2. **Driver current.** On a DRV8825, current limit = 2 × Vref (with the usual 0.1 Ω sense resistors).
|
||||
- Start at about 70 % of the motor's rated current. For example, a 1.0 A motor needs Vref ≈ 0.35 V.
|
||||
- Measure Vref between the trim pot and GND with the 12 V supply connected.
|
||||
- Never plug or unplug a motor while the power is on.
|
||||
3. **Home switch wiring.** Connect the switch's COM to HOME pin 3 (GND) and its NO contact to pin 2.
|
||||
4. **Firmware.**
|
||||
- Flash FluidNC over USB with the FluidNC web installer.
|
||||
- Upload `fluidnc/config.yaml` as `config.yaml`.
|
||||
- In the config, X is theta in degrees and Y is rho in mm. `$H` homes rho.
|
||||
- If an axis turns the wrong way, add `:low` to its direction pin.
|
||||
|
||||
## ESP32 pin map
|
||||
|
||||
| Function | GPIO |
|
||||
|---|---|
|
||||
| Theta (spin) STEP / DIR | IO18 / IO19 |
|
||||
| Rho (arm) STEP / DIR | IO27 / IO14 |
|
||||
| Driver enable (shared, low = on, pulled up so the drivers are off at boot) | IO13 |
|
||||
| Rho home switch | IO34 |
|
||||
| Theta hall sensor | IO35 |
|
||||
| LED strip data (through a 74AHCT1G125 buffer) | IO33 |
|
||||
| Status LED | IO2 |
|
||||
| Expansion header | IO32, IO25 |
|
||||
| Free, not connected | IO16, IO17 |
|
||||
|
||||
## Files
|
||||
|
||||
| Path | What |
|
||||
|---|---|
|
||||
| `kicad/` | KiCad 9 project: schematic, board, and project-local symbol and footprint tables |
|
||||
| `sandtable-controller-schematic.pdf` | Schematic |
|
||||
| `pcbway-order/` | Everything to upload to PCBWay, plus `ORDER.md` with the settings |
|
||||
| `renders/` | 3D renders |
|
||||
| `sandtable-controller.step` | 3D model. The copy placed in `sand-table-v2.solidcraft` is v0.1, which has the same outline and holes. |
|
||||
| `fluidnc/config.yaml` | FluidNC machine config (a starting point, not yet run on hardware) |
|
||||
| `tools/` | Python generator: `design.py` (parts, placement, nets, sourcing) → `route.py` (router, ground pour, board file), `sch.py` (schematic), `fab.py` (PCBWay order folder, renders, copy into this folder) |
|
||||
|
||||
To regenerate everything, run `python3 tools/route.py && python3 tools/sch.py && python3 tools/fab.py`. The KiCad files are built in `~/kicad/sandtable-controller` because the KiCad snap can't read inside `~/snap/`.
|
||||
|
||||
## Notes
|
||||
- **5 V budget:** the 5 V regulator supplies 0.5 A. That covers the ESP32 plus a short LED strip (about 10 LEDs at full white). Use a separate 5 V supply for longer strips.
|
||||
- **Noise:** DRV8825s are louder than TMC2209s; expect some motor whine. TMC2209 modules in standalone mode might work in these sockets, but their pin layouts vary by brand, so check before trying one.
|
||||
- **LED strip:** FluidNC doesn't drive the WS2812B header. It's there for other firmware or an add-on later.
|
||||
@@ -0,0 +1,183 @@
|
||||
"""SandTable Controller (version in VERSION): single source of truth for parts, placement and nets.
|
||||
|
||||
Board coordinates are mm, y down, origin at the board's top-left corner.
|
||||
x = 0 is the edge that faces the table wall (power + USB); x = 80 faces the
|
||||
table centre (motor/sensor connectors). The 4 mounting holes reuse the
|
||||
Arduino Uno hole pattern so the board drops onto the existing deck standoffs.
|
||||
"""
|
||||
|
||||
VERSION = '0.3'
|
||||
BOARD_W = 80.0
|
||||
BOARD_H = 63.34
|
||||
ORIGIN = (100.0, 100.0) # where board (0,0) lands in KiCad coordinates
|
||||
|
||||
# ---------------------------------------------------------------- parts
|
||||
# ref: (symbol lib_id, footprint lib_id, value, x, y, rot, lcsc, description)
|
||||
PARTS = {
|
||||
'U1': ('RF_Module:ESP32-WROOM-32E', 'RF_Module:ESP32-WROOM-32E', 'ESP32-WROOM-32E-N8', 43.0, 50.59, 180, '', 'Wi-Fi MCU module (FluidNC)'),
|
||||
'U2': ('Interface_USB:CH340C', 'Package_SO:SOIC-16_3.9x9.9mm_P1.27mm', 'CH340C', 22.0, 47.0, 0, 'C84681', 'USB-UART bridge'),
|
||||
'U3': ('Regulator_Linear:AMS1117-3.3', 'Package_TO_SOT_SMD:SOT-223-3_TabPin2', 'AMS1117-3.3', 66.0, 41.5, 0, 'C6186', '5V -> 3.3V LDO'),
|
||||
'U4': ('74xGxx:74AHCT1G125', 'Package_TO_SOT_SMD:SOT-23-5', '74AHCT1G125', 65.5, 30.0, 0, '', '3.3V -> 5V LED data buffer'),
|
||||
'PS1': ('Converter_DCDC:R-785.0-0.5', 'Converter_DCDC:Converter_DCDC_RECOM_R-78E-0.5_THT', 'R-78E5.0-0.5', 18.0, 35.0, 0, '', '12V -> 5V switching regulator'),
|
||||
'A1': ('Driver_Motor:Pololu_Breakout_A4988', 'Module:Pololu_Breakout-16_15.2x20.3mm', 'DRV8825 StepStick (theta/spin)', 38.1, 1.9, 270, '', 'Stepper driver socket (DRV8825, 1/32 step)'),
|
||||
'A2': ('Driver_Motor:Pololu_Breakout_A4988', 'Module:Pololu_Breakout-16_15.2x20.3mm', 'DRV8825 StepStick (rho/arm)', 59.1, 1.9, 270, '', 'Stepper driver socket (DRV8825, 1/32 step)'),
|
||||
'J1': ('Connector:Barrel_Jack', 'Connector_BarrelJack:BarrelJack_CUI_PJ-063AH_Horizontal', '12V DC 5.5x2.1', 12.3, 25.0, 270, '', 'Power input'),
|
||||
'J2': ('Connector:USB_C_Receptacle_USB2.0_16P', 'Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12', 'USB-C', 3.4, 44.0, 270, 'C165948', 'Programming / serial'),
|
||||
'J3': ('Connector_Generic:Conn_01x04', 'Connector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical', 'SPIN MOTOR', 4.5, 3.4, 270, 'C158012', 'Theta motor (XH-4, matches motor cable)'),
|
||||
'J4': ('Connector_Generic:Conn_01x04', 'Connector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical', 'ARM MOTOR', 76.5, 3.4, 270, 'C158012', 'Rho motor via slip ring'),
|
||||
'J5': ('Connector_Generic:Conn_01x03', 'Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical', 'HOME SW', 76.5, 18.5, 270, 'C144394', 'Rho home switch (via slip ring)'),
|
||||
'J6': ('Connector_Generic:Conn_01x03', 'Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical', 'HALL', 76.5, 29.6, 270, 'C144394', 'Optional theta hall sensor (5V)'),
|
||||
'J7': ('Connector_Generic:Conn_01x03', 'Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical', 'LED STRIP', 76.5, 40.7, 270, 'C144394', 'WS2812B strip (5V, small strips only)'),
|
||||
'J8': ('Connector_Generic:Conn_01x04', 'Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical', 'EXPANSION', 77.5, 52.0, 0, '', '3V3 / IO32 / IO25 / GND'),
|
||||
'D1': ('Device:D_Schottky', 'Diode_SMD:D_SMA', 'SS34', 16.0, 22.0, 270, 'C8678', 'Reverse-polarity protection'),
|
||||
'D2': ('Device:D_Schottky', 'Diode_SMD:D_SOD-123', 'B5819W', 29.0, 35.0, 0, 'C8598', '5V OR-ing (regulator)'),
|
||||
'D3': ('Device:D_Schottky', 'Diode_SMD:D_SOD-123', 'B5819W', 13.0, 39.8, 0, 'C8598', '5V OR-ing (USB)'),
|
||||
'D4': ('Device:LED', 'LED_SMD:LED_0603_1608Metric', 'GREEN', 34.0, 30.0, 0, 'C72043', 'Power LED'),
|
||||
'D5': ('Device:LED', 'LED_SMD:LED_0603_1608Metric', 'BLUE', 38.0, 30.0, 0, 'C72041', 'Status LED (IO2)'),
|
||||
'Q1': ('Transistor_BJT:MMBT3904', 'Package_TO_SOT_SMD:SOT-23', 'MMBT3904', 28.0, 47.2, 0, 'C20526', 'Auto-reset (EN)'),
|
||||
'Q2': ('Transistor_BJT:MMBT3904', 'Package_TO_SOT_SMD:SOT-23', 'MMBT3904', 28.0, 51.2, 0, 'C20526', 'Auto-reset (IO0)'),
|
||||
'SW1': ('Switch:SW_Push', 'Button_Switch_SMD:SW_SPST_B3U-1000P', 'RESET', 27.5, 55.0, 0, 'C231329', 'EN / reset button'),
|
||||
'SW2': ('Switch:SW_Push', 'Button_Switch_SMD:SW_SPST_B3U-1000P', 'BOOT', 22.0, 55.0, 0, 'C231329', 'IO0 / boot button'),
|
||||
'C1': ('Device:C_Polarized', 'Capacitor_SMD:CP_Elec_8x10', '100uF 25V', 24.0, 21.0, 0, '', 'Theta driver VMOT bulk'),
|
||||
'C2': ('Device:C_Polarized', 'Capacitor_SMD:CP_Elec_8x10', '100uF 25V', 45.5, 21.0, 0, '', 'Rho driver VMOT bulk'),
|
||||
'C3': ('Device:C', 'Capacitor_SMD:C_1206_3216Metric', '10uF 25V', 20.5, 39.6, 0, 'C13585', '12V input decoupling'),
|
||||
'C4': ('Device:C', 'Capacitor_SMD:C_1206_3216Metric', '10uF 16V', 26.0, 39.6, 0, 'C13585', 'Regulator output'),
|
||||
'C5': ('Device:C', 'Capacitor_SMD:C_0805_2012Metric', '10uF', 59.6, 44.0, 90, 'C15850', 'LDO input'),
|
||||
'C6': ('Device:C', 'Capacitor_SMD:C_0805_2012Metric', '22uF', 59.6, 39.5, 90, 'C45783', 'LDO output'),
|
||||
'C7': ('Device:C', 'Capacitor_SMD:C_0603_1608Metric', '100nF', 27.0, 43.0, 90, 'C14663', 'CH340C decoupling'),
|
||||
'C8': ('Device:C', 'Capacitor_SMD:C_0603_1608Metric', '1uF', 54.6, 52.7, 0, 'C15849', 'EN reset delay'),
|
||||
'C9': ('Device:C', 'Capacitor_SMD:C_0805_2012Metric', '10uF', 57.5, 54.6, 90, 'C15850', 'ESP32 bulk decoupling'),
|
||||
'C10': ('Device:C', 'Capacitor_SMD:C_0603_1608Metric', '100nF', 55.0, 55.0, 90, 'C14663', 'ESP32 decoupling'),
|
||||
'C13': ('Device:C', 'Capacitor_SMD:C_0603_1608Metric', '100nF', 61.0, 18.5, 0, 'C14663', 'Home switch filter'),
|
||||
'C14': ('Device:C', 'Capacitor_SMD:C_0603_1608Metric', '100nF', 61.0, 20.7, 0, 'C14663', 'Hall filter'),
|
||||
'C15': ('Device:C', 'Capacitor_SMD:C_0603_1608Metric', '100nF', 61.0, 30.0, 90, 'C14663', 'Buffer decoupling'),
|
||||
'R1': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '5.1k', 11.5, 42.75, 0, 'C23186', 'USB-C CC1 pull-down'),
|
||||
'R2': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '5.1k', 11.5, 45.75, 0, 'C23186', 'USB-C CC2 pull-down'),
|
||||
'R3': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '10k', 31.0, 45.0, 90, 'C25804', 'Q1 base'),
|
||||
'R4': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '10k', 31.0, 49.5, 90, 'C25804', 'Q2 base'),
|
||||
'R5': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '10k', 54.6, 51.0, 0, 'C25804', 'EN pull-up'),
|
||||
'R6': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '10k', 31.0, 37.5, 0, 'C25804', 'IO0 pull-up'),
|
||||
'R8': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '10k', 47.0, 31.0, 0, 'C25804', 'Driver enable pull-up (off at boot)'),
|
||||
'R9': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '10k', 54.0, 18.5, 0, 'C25804', 'Home switch pull-up'),
|
||||
'R10': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '1k', 57.5, 18.5, 0, 'C21190', 'Home switch series'),
|
||||
'R11': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '10k', 54.0, 20.7, 0, 'C25804', 'Hall pull-up'),
|
||||
'R12': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '1k', 57.5, 20.7, 0, 'C21190', 'Hall series'),
|
||||
'R13': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '330', 70.0, 30.0, 90, 'C23138', 'LED data series'),
|
||||
'R14': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '1k', 34.0, 32.5, 0, 'C21190', 'Power LED'),
|
||||
'R15': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '1k', 38.0, 32.5, 0, 'C21190', 'Status LED'),
|
||||
'H1': ('Mechanical:MountingHole', 'MountingHole:MountingHole_3.2mm_M3', 'M3', 15.24, 7.54, 0, '', 'Uno hole'),
|
||||
'H2': ('Mechanical:MountingHole', 'MountingHole:MountingHole_3.2mm_M3', 'M3', 13.97, 55.8, 0, '', 'Uno hole'),
|
||||
'H3': ('Mechanical:MountingHole', 'MountingHole:MountingHole_3.2mm_M3', 'M3', 66.04, 22.78, 0, '', 'Uno hole'),
|
||||
'H4': ('Mechanical:MountingHole', 'MountingHole:MountingHole_3.2mm_M3', 'M3', 66.04, 50.72, 0, '', 'Uno hole'),
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- nets
|
||||
NETS = {
|
||||
'GND': ['J1.2', 'J2.A1', 'J2.A12', 'J2.B1', 'J2.B12', 'J2.S1', 'C1.2', 'C2.2', 'C3.2', 'C4.2', 'C5.2',
|
||||
'C6.2', 'C7.2', 'C8.2', 'C9.2', 'C10.2', 'C13.2', 'C14.2', 'C15.2', 'PS1.2', 'U3.1', 'U2.1',
|
||||
'U1.1', 'U1.15', 'U1.38', 'U1.39', 'A1.1', 'A1.7', 'A2.1', 'A2.7',
|
||||
'J5.3', 'J6.3', 'J7.3', 'J8.4', 'SW1.2', 'SW2.2', 'R1.2', 'R2.2', 'D4.1', 'D5.1', 'U4.1', 'U4.3'],
|
||||
'VIN_RAW': ['J1.1', 'D1.2'],
|
||||
'+12V': ['D1.1', 'C1.1', 'C2.1', 'C3.1', 'PS1.1', 'A1.8', 'A2.8'],
|
||||
'+5V_REG': ['PS1.3', 'C4.1', 'D2.2'],
|
||||
'VBUS': ['J2.A4', 'J2.A9', 'J2.B4', 'J2.B9', 'D3.2'],
|
||||
'+5V': ['D2.1', 'D3.1', 'U3.3', 'C5.1', 'U4.5', 'C15.1', 'J6.1', 'J7.1'],
|
||||
'+3V3': ['U3.2', 'C6.1', 'U1.2', 'C9.1', 'C10.1', 'U2.16', 'U2.4', 'C7.1', 'R5.1', 'R6.1', 'R8.1',
|
||||
'R9.1', 'R11.1', 'R14.1', 'J5.1', 'J8.1',
|
||||
# DRV8825 sockets: VDD/FAULT, M0-M2 high (1/32 step), RESET + SLEEP high (awake)
|
||||
'A1.2', 'A1.10', 'A1.11', 'A1.12', 'A1.13', 'A1.14',
|
||||
'A2.2', 'A2.10', 'A2.11', 'A2.12', 'A2.13', 'A2.14'],
|
||||
'USB_DP': ['J2.A6', 'J2.B6', 'U2.5'],
|
||||
'USB_DM': ['J2.A7', 'J2.B7', 'U2.6'],
|
||||
'CC1': ['J2.A5', 'R1.1'],
|
||||
'CC2': ['J2.B5', 'R2.1'],
|
||||
'U0TXD': ['U1.35', 'U2.3'],
|
||||
'U0RXD': ['U1.34', 'U2.2'],
|
||||
'DTR': ['U2.13', 'R3.1', 'Q2.2'],
|
||||
'RTS': ['U2.14', 'R4.1', 'Q1.2'],
|
||||
'Q1B': ['R3.2', 'Q1.1'],
|
||||
'Q2B': ['R4.2', 'Q2.1'],
|
||||
'EN': ['Q1.3', 'U1.3', 'R5.2', 'C8.1', 'SW1.1'],
|
||||
'IO0': ['Q2.3', 'U1.25', 'R6.2', 'SW2.1'],
|
||||
'DRV_EN': ['U1.16', 'R8.2', 'A1.9', 'A2.9'],
|
||||
'X_STEP': ['U1.30', 'A1.15'],
|
||||
'X_DIR': ['U1.31', 'A1.16'],
|
||||
'Y_STEP': ['U1.12', 'A2.15'],
|
||||
'Y_DIR': ['U1.13', 'A2.16'],
|
||||
'X_1A': ['A1.4', 'J3.1'], 'X_1B': ['A1.3', 'J3.2'], 'X_2A': ['A1.5', 'J3.3'], 'X_2B': ['A1.6', 'J3.4'],
|
||||
'Y_1A': ['A2.4', 'J4.1'], 'Y_1B': ['A2.3', 'J4.2'], 'Y_2A': ['A2.5', 'J4.3'], 'Y_2B': ['A2.6', 'J4.4'],
|
||||
'HOME_RAW': ['J5.2', 'R9.2', 'R10.1'],
|
||||
'HOME': ['R10.2', 'C13.1', 'U1.6'],
|
||||
'HALL_RAW': ['J6.2', 'R11.2', 'R12.1'],
|
||||
'HALL': ['R12.2', 'C14.1', 'U1.7'],
|
||||
'LED_DATA': ['U1.9', 'U4.2'],
|
||||
'LED_BUF': ['U4.4', 'R13.1'],
|
||||
'LED_DOUT': ['R13.2', 'J7.2'],
|
||||
'EXP_IO32': ['U1.8', 'J8.2'],
|
||||
'EXP_IO25': ['U1.10', 'J8.3'],
|
||||
'STAT': ['U1.24', 'R15.1'],
|
||||
'STAT_A': ['R15.2', 'D5.2'],
|
||||
'PWR_A': ['R14.2', 'D4.2'],
|
||||
}
|
||||
|
||||
# Pins intentionally left unconnected (get no-connect flags in the schematic)
|
||||
NO_CONNECT = ['U1.4', 'U1.5', 'U1.14', 'U1.17', 'U1.18', 'U1.19', 'U1.20', 'U1.21', 'U1.22', 'U1.23',
|
||||
'U1.11', 'U1.26', 'U1.29', 'U1.32', 'U1.33', 'U1.36', 'U1.37',
|
||||
'U2.7', 'U2.8', 'U2.9', 'U2.10', 'U2.11', 'U2.12', 'U2.15',
|
||||
'U1.27', 'U1.28', 'J2.A8', 'J2.B8']
|
||||
|
||||
# ---------------------------------------------------------------- rules
|
||||
# net class -> (track width, clearance)
|
||||
NETCLASS = {
|
||||
'Default': (0.2, 0.16),
|
||||
'Power': (0.8, 0.2),
|
||||
'Motor': (0.6, 0.2),
|
||||
'Rail': (0.5, 0.2),
|
||||
}
|
||||
NET_TO_CLASS = {}
|
||||
for n in ('GND', 'VIN_RAW', '+12V'):
|
||||
NET_TO_CLASS[n] = 'Power'
|
||||
for n in ('X_1A', 'X_1B', 'X_2A', 'X_2B', 'Y_1A', 'Y_1B', 'Y_2A', 'Y_2B'):
|
||||
NET_TO_CLASS[n] = 'Motor'
|
||||
for n in ('+5V', '+5V_REG', 'VBUS', '+3V3'):
|
||||
NET_TO_CLASS[n] = 'Rail'
|
||||
|
||||
VIA = (0.6, 0.3) # diameter, drill
|
||||
|
||||
# PCBWay standard process: 6/6 mil track/space, 0.3 mm min hole, 0.15 mm annular ring.
|
||||
# Library footprints with smaller holes (the ESP32's thermal vias are 0.2 mm) are enlarged to fit.
|
||||
MIN_DRILL = 0.3
|
||||
MIN_RING = 0.2
|
||||
|
||||
# ---------------------------------------------------------------- sourcing (PCBWay BOM)
|
||||
# (refs, manufacturer, manufacturer part number, description, notes). Blank MPN = any brand to the description.
|
||||
SOURCING = [
|
||||
(['U1'], 'Espressif', 'ESP32-WROOM-32E-N8', 'Wi-Fi + Bluetooth module, 8 MB flash, PCB antenna', ''),
|
||||
(['U2'], 'WCH', 'CH340C', 'USB to UART bridge, SOIC-16', ''),
|
||||
(['U3'], 'Advanced Monolithic Systems', 'AMS1117-3.3', 'LDO regulator 3.3 V 1 A, SOT-223', 'Any AMS1117-3.3 equivalent ok'),
|
||||
(['U4'], 'Texas Instruments', 'SN74AHCT1G125DBVR', 'Single buffer, TTL-level input, SOT-23-5', ''),
|
||||
(['PS1'], 'RECOM', 'R-78E5.0-0.5', 'Switching regulator 5 V 0.5 A, SIP-3', ''),
|
||||
(['J1'], 'Same Sky (CUI Devices)', 'PJ-063AH', 'DC barrel jack, 2.0 mm pin, fits 5.5 x 2.1 mm plugs', ''),
|
||||
(['J2'], 'Korean Hroparts Elec', 'TYPE-C-31-M-12', 'USB-C receptacle, USB 2.0, 16 pin', ''),
|
||||
(['J3', 'J4'], 'JST', 'B4B-XH-A(LF)(SN)', 'XH header, 4 pin, 2.5 mm pitch, vertical', ''),
|
||||
(['J5', 'J6', 'J7'], 'JST', 'B3B-XH-A(LF)(SN)', 'XH header, 3 pin, 2.5 mm pitch, vertical', ''),
|
||||
(['J8'], 'Sullins', 'PRPC004SAAN-RC', 'Male pin header 1 x 4, 2.54 mm pitch, vertical', ''),
|
||||
(['A1', 'A2'], 'Sullins', 'PPTC081LFBN-RC', 'Female pin header 1 x 8, 2.54 mm pitch, 8.5 mm tall', '2 headers per driver (4 total). Sockets only: the DRV8825 modules are supplied by me'),
|
||||
(['D1'], '', 'SS34', 'Schottky diode 40 V 3 A, SMA', 'Any brand'),
|
||||
(['D2', 'D3'], '', 'B5819W', 'Schottky diode 40 V 1 A, SOD-123', 'Any brand'),
|
||||
(['D4'], '', '', 'LED green, 0603', 'Any brand'),
|
||||
(['D5'], '', '', 'LED blue, 0603', 'Any brand'),
|
||||
(['Q1', 'Q2'], 'onsemi', 'MMBT3904LT1G', 'NPN transistor 40 V 200 mA, SOT-23', 'Any MMBT3904 ok'),
|
||||
(['SW1', 'SW2'], 'Omron', 'B3U-1000P', 'Tactile switch, SMD, 3 x 2.5 mm', ''),
|
||||
(['C1', 'C2'], 'Panasonic', 'EEE-FK1V101GP', 'Aluminium electrolytic capacitor 100 uF 35 V, SMD 8 x 10.2 mm', ''),
|
||||
(['C3', 'C4'], '', '', 'Ceramic capacitor 10 uF 25 V X5R, 1206', 'Any brand'),
|
||||
(['C5', 'C9'], '', '', 'Ceramic capacitor 10 uF 16 V X5R, 0805', 'Any brand'),
|
||||
(['C6'], '', '', 'Ceramic capacitor 22 uF 10 V X5R, 0805', 'Any brand'),
|
||||
(['C7', 'C10', 'C13', 'C14', 'C15'], '', '', 'Ceramic capacitor 100 nF 50 V X7R, 0603', 'Any brand'),
|
||||
(['C8'], '', '', 'Ceramic capacitor 1 uF 16 V X7R, 0603', 'Any brand'),
|
||||
(['R1', 'R2'], '', '', 'Resistor 5.1 k 1%, 0603', 'Any brand'),
|
||||
(['R3', 'R4', 'R5', 'R6', 'R8', 'R9', 'R11'], '', '', 'Resistor 10 k 1%, 0603', 'Any brand'),
|
||||
(['R10', 'R12', 'R14', 'R15'], '', '', 'Resistor 1 k 1%, 0603', 'Any brand'),
|
||||
(['R13'], '', '', 'Resistor 330 1%, 0603', 'Any brand'),
|
||||
]
|
||||
SOURCING_QTY = {('A1', 'A2'): 4} # BOM lines whose piece count differs from the designator count
|
||||
@@ -0,0 +1,18 @@
|
||||
(fp_lib_table
|
||||
(version 7)
|
||||
(lib (name "Button_Switch_SMD")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Button_Switch_SMD.pretty")(options "")(descr ""))
|
||||
(lib (name "Capacitor_SMD")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Capacitor_SMD.pretty")(options "")(descr ""))
|
||||
(lib (name "Connector_BarrelJack")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Connector_BarrelJack.pretty")(options "")(descr ""))
|
||||
(lib (name "Connector_JST")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Connector_JST.pretty")(options "")(descr ""))
|
||||
(lib (name "Connector_PinHeader_2.54mm")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Connector_PinHeader_2.54mm.pretty")(options "")(descr ""))
|
||||
(lib (name "Connector_USB")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Connector_USB.pretty")(options "")(descr ""))
|
||||
(lib (name "Converter_DCDC")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Converter_DCDC.pretty")(options "")(descr ""))
|
||||
(lib (name "Diode_SMD")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Diode_SMD.pretty")(options "")(descr ""))
|
||||
(lib (name "LED_SMD")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/LED_SMD.pretty")(options "")(descr ""))
|
||||
(lib (name "Module")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Module.pretty")(options "")(descr ""))
|
||||
(lib (name "MountingHole")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/MountingHole.pretty")(options "")(descr ""))
|
||||
(lib (name "Package_SO")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Package_SO.pretty")(options "")(descr ""))
|
||||
(lib (name "Package_TO_SOT_SMD")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Package_TO_SOT_SMD.pretty")(options "")(descr ""))
|
||||
(lib (name "RF_Module")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/RF_Module.pretty")(options "")(descr ""))
|
||||
(lib (name "Resistor_SMD")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Resistor_SMD.pretty")(options "")(descr ""))
|
||||
)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,722 @@
|
||||
{
|
||||
"board": {
|
||||
"3dviewports": [],
|
||||
"design_settings": {
|
||||
"defaults": {
|
||||
"apply_defaults_to_fp_fields": false,
|
||||
"apply_defaults_to_fp_shapes": false,
|
||||
"apply_defaults_to_fp_text": false,
|
||||
"board_outline_line_width": 0.05,
|
||||
"copper_line_width": 0.2,
|
||||
"copper_text_italic": false,
|
||||
"copper_text_size_h": 1.5,
|
||||
"copper_text_size_v": 1.5,
|
||||
"copper_text_thickness": 0.3,
|
||||
"copper_text_upright": false,
|
||||
"courtyard_line_width": 0.05,
|
||||
"dimension_precision": 4,
|
||||
"dimension_units": 3,
|
||||
"dimensions": {
|
||||
"arrow_length": 1270000,
|
||||
"extension_offset": 500000,
|
||||
"keep_text_aligned": true,
|
||||
"suppress_zeroes": false,
|
||||
"text_position": 0,
|
||||
"units_format": 1
|
||||
},
|
||||
"fab_line_width": 0.1,
|
||||
"fab_text_italic": false,
|
||||
"fab_text_size_h": 1.0,
|
||||
"fab_text_size_v": 1.0,
|
||||
"fab_text_thickness": 0.15,
|
||||
"fab_text_upright": false,
|
||||
"other_line_width": 0.1,
|
||||
"other_text_italic": false,
|
||||
"other_text_size_h": 1.0,
|
||||
"other_text_size_v": 1.0,
|
||||
"other_text_thickness": 0.15,
|
||||
"other_text_upright": false,
|
||||
"pads": {
|
||||
"drill": 0.0,
|
||||
"height": 8.0,
|
||||
"width": 2.54
|
||||
},
|
||||
"silk_line_width": 0.12,
|
||||
"silk_text_italic": false,
|
||||
"silk_text_size_h": 1.0,
|
||||
"silk_text_size_v": 1.0,
|
||||
"silk_text_thickness": 0.15,
|
||||
"silk_text_upright": false,
|
||||
"zones": {
|
||||
"min_clearance": 0.508
|
||||
}
|
||||
},
|
||||
"diff_pair_dimensions": [],
|
||||
"drc_exclusions": [],
|
||||
"meta": {
|
||||
"filename": "board_design_settings.json",
|
||||
"version": 2
|
||||
},
|
||||
"rule_severities": {
|
||||
"annular_width": "error",
|
||||
"clearance": "error",
|
||||
"connection_width": "warning",
|
||||
"copper_edge_clearance": "error",
|
||||
"copper_sliver": "warning",
|
||||
"courtyards_overlap": "error",
|
||||
"creepage": "error",
|
||||
"diff_pair_gap_out_of_range": "error",
|
||||
"diff_pair_uncoupled_length_too_long": "error",
|
||||
"drill_out_of_range": "error",
|
||||
"duplicate_footprints": "warning",
|
||||
"extra_footprint": "warning",
|
||||
"footprint": "error",
|
||||
"footprint_filters_mismatch": "ignore",
|
||||
"footprint_symbol_mismatch": "warning",
|
||||
"footprint_type_mismatch": "ignore",
|
||||
"hole_clearance": "error",
|
||||
"hole_near_hole": "error",
|
||||
"hole_to_hole": "error",
|
||||
"holes_co_located": "warning",
|
||||
"invalid_outline": "error",
|
||||
"isolated_copper": "warning",
|
||||
"item_on_disabled_layer": "error",
|
||||
"items_not_allowed": "error",
|
||||
"length_out_of_range": "error",
|
||||
"lib_footprint_issues": "ignore",
|
||||
"lib_footprint_mismatch": "ignore",
|
||||
"malformed_courtyard": "error",
|
||||
"microvia_drill_out_of_range": "error",
|
||||
"mirrored_text_on_front_layer": "warning",
|
||||
"missing_courtyard": "ignore",
|
||||
"missing_footprint": "warning",
|
||||
"net_conflict": "warning",
|
||||
"nonmirrored_text_on_back_layer": "warning",
|
||||
"npth_inside_courtyard": "ignore",
|
||||
"padstack": "warning",
|
||||
"pth_inside_courtyard": "ignore",
|
||||
"shorting_items": "error",
|
||||
"silk_edge_clearance": "ignore",
|
||||
"silk_over_copper": "ignore",
|
||||
"silk_overlap": "ignore",
|
||||
"skew_out_of_range": "error",
|
||||
"solder_mask_bridge": "error",
|
||||
"starved_thermal": "error",
|
||||
"text_height": "ignore",
|
||||
"text_thickness": "warning",
|
||||
"through_hole_pad_without_hole": "error",
|
||||
"too_many_vias": "error",
|
||||
"track_angle": "error",
|
||||
"track_dangling": "warning",
|
||||
"track_segment_length": "error",
|
||||
"track_width": "error",
|
||||
"tracks_crossing": "error",
|
||||
"unconnected_items": "error",
|
||||
"unresolved_variable": "error",
|
||||
"via_dangling": "warning",
|
||||
"zones_intersect": "error"
|
||||
},
|
||||
"rules": {
|
||||
"max_error": 0.005,
|
||||
"min_clearance": 0.16,
|
||||
"min_connection": 0.0,
|
||||
"min_copper_edge_clearance": 0.3,
|
||||
"min_groove_width": 0.0,
|
||||
"min_hole_clearance": 0.2,
|
||||
"min_hole_to_hole": 0.25,
|
||||
"min_microvia_diameter": 0.2,
|
||||
"min_microvia_drill": 0.1,
|
||||
"min_resolved_spokes": 2,
|
||||
"min_silk_clearance": 0.0,
|
||||
"min_text_height": 0.8,
|
||||
"min_text_thickness": 0.08,
|
||||
"min_through_hole_diameter": 0.3,
|
||||
"min_track_width": 0.16,
|
||||
"min_via_annular_width": 0.15,
|
||||
"min_via_diameter": 0.6,
|
||||
"solder_mask_to_copper_clearance": 0.0,
|
||||
"use_height_for_length_calcs": true
|
||||
},
|
||||
"teardrop_options": [
|
||||
{
|
||||
"td_onpthpad": true,
|
||||
"td_onroundshapesonly": false,
|
||||
"td_onsmdpad": true,
|
||||
"td_ontrackend": false,
|
||||
"td_onvia": true
|
||||
}
|
||||
],
|
||||
"teardrop_parameters": [
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_round_shape",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
},
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_rect_shape",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
},
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_track_end",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
}
|
||||
],
|
||||
"track_widths": [],
|
||||
"tuning_pattern_settings": {
|
||||
"diff_pair_defaults": {
|
||||
"corner_radius_percentage": 80,
|
||||
"corner_style": 1,
|
||||
"max_amplitude": 1.0,
|
||||
"min_amplitude": 0.2,
|
||||
"single_sided": false,
|
||||
"spacing": 1.0
|
||||
},
|
||||
"diff_pair_skew_defaults": {
|
||||
"corner_radius_percentage": 80,
|
||||
"corner_style": 1,
|
||||
"max_amplitude": 1.0,
|
||||
"min_amplitude": 0.2,
|
||||
"single_sided": false,
|
||||
"spacing": 0.6
|
||||
},
|
||||
"single_track_defaults": {
|
||||
"corner_radius_percentage": 80,
|
||||
"corner_style": 1,
|
||||
"max_amplitude": 1.0,
|
||||
"min_amplitude": 0.2,
|
||||
"single_sided": false,
|
||||
"spacing": 0.6
|
||||
}
|
||||
},
|
||||
"via_dimensions": [],
|
||||
"zones_allow_external_fillets": false
|
||||
},
|
||||
"ipc2581": {
|
||||
"dist": "",
|
||||
"distpn": "",
|
||||
"internal_id": "",
|
||||
"mfg": "",
|
||||
"mpn": ""
|
||||
},
|
||||
"layer_pairs": [],
|
||||
"layer_presets": [],
|
||||
"viewports": []
|
||||
},
|
||||
"cvpcb": {
|
||||
"equivalence_files": []
|
||||
},
|
||||
"erc": {
|
||||
"erc_exclusions": [],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"pin_map": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
],
|
||||
"rule_severities": {
|
||||
"bus_definition_conflict": "error",
|
||||
"bus_entry_needed": "error",
|
||||
"bus_to_bus_conflict": "error",
|
||||
"bus_to_net_conflict": "error",
|
||||
"conflicting_netclasses": "error",
|
||||
"different_unit_footprint": "error",
|
||||
"different_unit_net": "error",
|
||||
"duplicate_reference": "error",
|
||||
"duplicate_sheet_names": "error",
|
||||
"endpoint_off_grid": "warning",
|
||||
"extra_units": "error",
|
||||
"footprint_filter": "ignore",
|
||||
"footprint_link_issues": "warning",
|
||||
"four_way_junction": "ignore",
|
||||
"global_label_dangling": "warning",
|
||||
"hier_label_mismatch": "error",
|
||||
"label_dangling": "error",
|
||||
"label_multiple_wires": "warning",
|
||||
"lib_symbol_issues": "warning",
|
||||
"lib_symbol_mismatch": "warning",
|
||||
"missing_bidi_pin": "warning",
|
||||
"missing_input_pin": "warning",
|
||||
"missing_power_pin": "error",
|
||||
"missing_unit": "warning",
|
||||
"multiple_net_names": "warning",
|
||||
"net_not_bus_member": "warning",
|
||||
"no_connect_connected": "warning",
|
||||
"no_connect_dangling": "warning",
|
||||
"pin_not_connected": "error",
|
||||
"pin_not_driven": "error",
|
||||
"pin_to_pin": "warning",
|
||||
"power_pin_not_driven": "error",
|
||||
"same_local_global_label": "warning",
|
||||
"similar_label_and_power": "warning",
|
||||
"similar_labels": "warning",
|
||||
"similar_power": "warning",
|
||||
"simulation_model_issue": "ignore",
|
||||
"single_global_label": "ignore",
|
||||
"unannotated": "error",
|
||||
"unconnected_wire_endpoint": "warning",
|
||||
"unit_value_mismatch": "error",
|
||||
"unresolved_variable": "error",
|
||||
"wire_dangling": "error"
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"pinned_footprint_libs": [],
|
||||
"pinned_symbol_libs": []
|
||||
},
|
||||
"meta": {
|
||||
"filename": "sandtable-controller.kicad_pro",
|
||||
"version": 3
|
||||
},
|
||||
"net_settings": {
|
||||
"classes": [
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.16,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.2,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "Default",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"priority": 2147483647,
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.2,
|
||||
"via_diameter": 0.6,
|
||||
"via_drill": 0.3,
|
||||
"wire_width": 6
|
||||
},
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.2,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.2,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "Power",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"priority": 0,
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.8,
|
||||
"via_diameter": 0.6,
|
||||
"via_drill": 0.3,
|
||||
"wire_width": 6
|
||||
},
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.2,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.2,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "Motor",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"priority": 0,
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.6,
|
||||
"via_diameter": 0.6,
|
||||
"via_drill": 0.3,
|
||||
"wire_width": 6
|
||||
},
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.2,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.2,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "Rail",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"priority": 0,
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.5,
|
||||
"via_diameter": 0.6,
|
||||
"via_drill": 0.3,
|
||||
"wire_width": 6
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"version": 4
|
||||
},
|
||||
"net_colors": null,
|
||||
"netclass_assignments": null,
|
||||
"netclass_patterns": [
|
||||
{
|
||||
"netclass": "Power",
|
||||
"pattern": "/GND"
|
||||
},
|
||||
{
|
||||
"netclass": "Power",
|
||||
"pattern": "/VIN_RAW"
|
||||
},
|
||||
{
|
||||
"netclass": "Power",
|
||||
"pattern": "/+12V"
|
||||
},
|
||||
{
|
||||
"netclass": "Motor",
|
||||
"pattern": "/X_1A"
|
||||
},
|
||||
{
|
||||
"netclass": "Motor",
|
||||
"pattern": "/X_1B"
|
||||
},
|
||||
{
|
||||
"netclass": "Motor",
|
||||
"pattern": "/X_2A"
|
||||
},
|
||||
{
|
||||
"netclass": "Motor",
|
||||
"pattern": "/X_2B"
|
||||
},
|
||||
{
|
||||
"netclass": "Motor",
|
||||
"pattern": "/Y_1A"
|
||||
},
|
||||
{
|
||||
"netclass": "Motor",
|
||||
"pattern": "/Y_1B"
|
||||
},
|
||||
{
|
||||
"netclass": "Motor",
|
||||
"pattern": "/Y_2A"
|
||||
},
|
||||
{
|
||||
"netclass": "Motor",
|
||||
"pattern": "/Y_2B"
|
||||
},
|
||||
{
|
||||
"netclass": "Rail",
|
||||
"pattern": "/+5V"
|
||||
},
|
||||
{
|
||||
"netclass": "Rail",
|
||||
"pattern": "/+5V_REG"
|
||||
},
|
||||
{
|
||||
"netclass": "Rail",
|
||||
"pattern": "/VBUS"
|
||||
},
|
||||
{
|
||||
"netclass": "Rail",
|
||||
"pattern": "/+3V3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pcbnew": {
|
||||
"last_paths": {
|
||||
"gencad": "",
|
||||
"idf": "",
|
||||
"netlist": "",
|
||||
"plot": "",
|
||||
"pos_files": "",
|
||||
"specctra_dsn": "",
|
||||
"step": "",
|
||||
"svg": "",
|
||||
"vrml": ""
|
||||
},
|
||||
"page_layout_descr_file": ""
|
||||
},
|
||||
"schematic": {
|
||||
"annotate_start_num": 0,
|
||||
"bom_export_filename": "${PROJECTNAME}.csv",
|
||||
"bom_fmt_presets": [],
|
||||
"bom_fmt_settings": {
|
||||
"field_delimiter": ",",
|
||||
"keep_line_breaks": false,
|
||||
"keep_tabs": false,
|
||||
"name": "CSV",
|
||||
"ref_delimiter": ",",
|
||||
"ref_range_delimiter": "",
|
||||
"string_delimiter": "\""
|
||||
},
|
||||
"bom_presets": [],
|
||||
"bom_settings": {
|
||||
"exclude_dnp": false,
|
||||
"fields_ordered": [
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Reference",
|
||||
"name": "Reference",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Value",
|
||||
"name": "Value",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Datasheet",
|
||||
"name": "Datasheet",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Footprint",
|
||||
"name": "Footprint",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Qty",
|
||||
"name": "${QUANTITY}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "DNP",
|
||||
"name": "${DNP}",
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"filter_string": "",
|
||||
"group_symbols": true,
|
||||
"include_excluded_from_bom": false,
|
||||
"name": "Grouped By Value",
|
||||
"sort_asc": true,
|
||||
"sort_field": "Reference"
|
||||
},
|
||||
"connection_grid_size": 50.0,
|
||||
"drawing": {
|
||||
"dashed_lines_dash_length_ratio": 12.0,
|
||||
"dashed_lines_gap_length_ratio": 3.0,
|
||||
"default_line_thickness": 6.0,
|
||||
"default_text_size": 50.0,
|
||||
"field_names": [],
|
||||
"intersheets_ref_own_page": false,
|
||||
"intersheets_ref_prefix": "",
|
||||
"intersheets_ref_short": false,
|
||||
"intersheets_ref_show": false,
|
||||
"intersheets_ref_suffix": "",
|
||||
"junction_size_choice": 3,
|
||||
"label_size_ratio": 0.25,
|
||||
"operating_point_overlay_i_precision": 3,
|
||||
"operating_point_overlay_i_range": "~A",
|
||||
"operating_point_overlay_v_precision": 3,
|
||||
"operating_point_overlay_v_range": "~V",
|
||||
"overbar_offset_ratio": 1.23,
|
||||
"pin_symbol_size": 0.0,
|
||||
"text_offset_ratio": 0.08
|
||||
},
|
||||
"legacy_lib_dir": "",
|
||||
"legacy_lib_list": [],
|
||||
"meta": {
|
||||
"version": 1
|
||||
},
|
||||
"net_format_name": "",
|
||||
"page_layout_descr_file": "",
|
||||
"plot_directory": "",
|
||||
"space_save_all_events": true,
|
||||
"spice_current_sheet_as_root": false,
|
||||
"spice_external_command": "spice \"%I\"",
|
||||
"spice_model_current_sheet_as_root": true,
|
||||
"spice_save_all_currents": false,
|
||||
"spice_save_all_dissipations": false,
|
||||
"spice_save_all_voltages": false,
|
||||
"subpart_first_id": 65,
|
||||
"subpart_id_separator": 0
|
||||
},
|
||||
"sheets": [
|
||||
[
|
||||
"d585c3d2-c803-5c74-8a05-da481956c6b2",
|
||||
"Root"
|
||||
]
|
||||
],
|
||||
"text_variables": {}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,753 @@
|
||||
(kicad_symbol_lib
|
||||
(version 20241209)
|
||||
(generator "sandtable-gen")
|
||||
(generator_version "9.0")
|
||||
(symbol "CH340C_3V3"
|
||||
(exclude_from_sim no)
|
||||
(in_bom yes)
|
||||
(on_board yes)
|
||||
(property "Reference" "U"
|
||||
(at -5.08 13.97 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify right)
|
||||
)
|
||||
)
|
||||
(property "Value" "CH340C"
|
||||
(at 1.27 13.97 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify left)
|
||||
)
|
||||
)
|
||||
(property "Footprint" "Package_SO:SOIC-16_3.9x9.9mm_P1.27mm"
|
||||
(at -18.542 30.226 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify left)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "Datasheet" "https://datasheet.lcsc.com/szlcsc/Jiangsu-Qin-Heng-CH340C_C84681.pdf"
|
||||
(at -6.604 33.274 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "Description" "USB serial converter, crystal-less, UART, SOIC-16"
|
||||
(at -1.524 36.068 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "ki_keywords" "USB UART Serial Converter Interface"
|
||||
(at 0 0 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "ki_fp_filters" "SOIC*3.9x9.9mm*P1.27mm*"
|
||||
(at 0 0 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(symbol "CH340C_3V3_0_1"
|
||||
(rectangle
|
||||
(start -7.62 12.7)
|
||||
(end 7.62 -12.7)
|
||||
(stroke
|
||||
(width 0.254)
|
||||
(type default)
|
||||
)
|
||||
(fill
|
||||
(type background)
|
||||
)
|
||||
)
|
||||
)
|
||||
(symbol "CH340C_3V3_1_1"
|
||||
(pin input line
|
||||
(at -10.16 7.62 0)
|
||||
(length 2.54)
|
||||
(name "R232"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "15"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin bidirectional line
|
||||
(at -10.16 2.54 0)
|
||||
(length 2.54)
|
||||
(name "UD+"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "5"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin bidirectional line
|
||||
(at -10.16 0 0)
|
||||
(length 2.54)
|
||||
(name "UD-"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "6"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin no_connect line
|
||||
(at -7.62 -5.08 0)
|
||||
(length 2.54)
|
||||
(hide yes)
|
||||
(name "NC"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "7"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin no_connect line
|
||||
(at -7.62 -7.62 0)
|
||||
(length 2.54)
|
||||
(hide yes)
|
||||
(name "NC"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "8"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin power_in line
|
||||
(at -2.54 15.24 270)
|
||||
(length 2.54)
|
||||
(name "VCC"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "16"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin passive line
|
||||
(at 0 15.24 270)
|
||||
(length 2.54)
|
||||
(name "V3"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "4"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin power_in line
|
||||
(at 0 -15.24 90)
|
||||
(length 2.54)
|
||||
(name "GND"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "1"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin output line
|
||||
(at 10.16 10.16 180)
|
||||
(length 2.54)
|
||||
(name "TXD"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "2"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at 10.16 7.62 180)
|
||||
(length 2.54)
|
||||
(name "RXD"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "3"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at 10.16 2.54 180)
|
||||
(length 2.54)
|
||||
(name "~{CTS}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "9"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at 10.16 0 180)
|
||||
(length 2.54)
|
||||
(name "~{DSR}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "10"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at 10.16 -2.54 180)
|
||||
(length 2.54)
|
||||
(name "~{RI}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "11"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at 10.16 -5.08 180)
|
||||
(length 2.54)
|
||||
(name "~{DCD}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "12"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin output line
|
||||
(at 10.16 -7.62 180)
|
||||
(length 2.54)
|
||||
(name "~{DTR}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "13"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin output line
|
||||
(at 10.16 -10.16 180)
|
||||
(length 2.54)
|
||||
(name "~{RTS}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "14"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(embedded_fonts no)
|
||||
)
|
||||
(symbol "DRV8825_StepStick"
|
||||
(exclude_from_sim no)
|
||||
(in_bom yes)
|
||||
(on_board yes)
|
||||
(property "Reference" "A"
|
||||
(at -2.54 19.05 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify right)
|
||||
)
|
||||
)
|
||||
(property "Value" "Pololu_Breakout_A4988"
|
||||
(at -2.54 16.51 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify right)
|
||||
)
|
||||
)
|
||||
(property "Footprint" "Module:Pololu_Breakout-16_15.2x20.3mm"
|
||||
(at 6.985 -19.05 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify left)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "Datasheet" "https://www.pololu.com/product/2980/pictures"
|
||||
(at 2.54 -7.62 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "Description" "Pololu Breakout Board, Stepper Driver A4988"
|
||||
(at 0 0 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "ki_keywords" "Pololu Breakout Board Stepper Driver A4988"
|
||||
(at 0 0 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "ki_fp_filters" "Pololu*Breakout*15.2x20.3mm*"
|
||||
(at 0 0 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(symbol "DRV8825_StepStick_0_1"
|
||||
(rectangle
|
||||
(start 10.16 -17.78)
|
||||
(end -7.62 15.24)
|
||||
(stroke
|
||||
(width 0.254)
|
||||
(type default)
|
||||
)
|
||||
(fill
|
||||
(type background)
|
||||
)
|
||||
)
|
||||
)
|
||||
(symbol "DRV8825_StepStick_1_1"
|
||||
(pin input line
|
||||
(at -10.16 10.16 0)
|
||||
(length 2.54)
|
||||
(name "~{RST}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "13"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at -10.16 7.62 0)
|
||||
(length 2.54)
|
||||
(name "~{SLP}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "14"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at -10.16 2.54 0)
|
||||
(length 2.54)
|
||||
(name "~{EN}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "9"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at -10.16 0 0)
|
||||
(length 2.54)
|
||||
(name "STEP"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "15"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at -10.16 -2.54 0)
|
||||
(length 2.54)
|
||||
(name "DIR"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "16"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at -10.16 -7.62 0)
|
||||
(length 2.54)
|
||||
(name "M0"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "10"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at -10.16 -10.16 0)
|
||||
(length 2.54)
|
||||
(name "M1"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "11"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at -10.16 -12.7 0)
|
||||
(length 2.54)
|
||||
(name "M2"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "12"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin passive line
|
||||
(at 0 17.78 270)
|
||||
(length 2.54)
|
||||
(name "VDD/~{FAULT}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "2"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin power_in line
|
||||
(at 0 -20.32 90)
|
||||
(length 2.54)
|
||||
(name "GND"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "1"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin power_in line
|
||||
(at 5.08 17.78 270)
|
||||
(length 2.54)
|
||||
(name "VMOT"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "8"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin power_in line
|
||||
(at 5.08 -20.32 90)
|
||||
(length 2.54)
|
||||
(name "GND"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "7"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin output line
|
||||
(at 12.7 2.54 180)
|
||||
(length 2.54)
|
||||
(name "A2"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "3"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin output line
|
||||
(at 12.7 0 180)
|
||||
(length 2.54)
|
||||
(name "A1"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "4"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin output line
|
||||
(at 12.7 -2.54 180)
|
||||
(length 2.54)
|
||||
(name "B1"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "5"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin output line
|
||||
(at 12.7 -5.08 180)
|
||||
(length 2.54)
|
||||
(name "B2"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "6"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(embedded_fonts no)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
(sym_lib_table
|
||||
(version 7)
|
||||
(lib (name "SandTable")(type "KiCad")(uri "${KIPRJMOD}/sandtable.kicad_sym")(options "")(descr "SandTable custom symbols"))
|
||||
(lib (name "74xGxx")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/74xGxx.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Connector")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Connector.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Connector_Generic")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Connector_Generic.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Converter_DCDC")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Converter_DCDC.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Device")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Device.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Driver_Motor")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Driver_Motor.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Interface_USB")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Interface_USB.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Mechanical")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Mechanical.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "RF_Module")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/RF_Module.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Regulator_Linear")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Regulator_Linear.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Switch")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Switch.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Transistor_BJT")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Transistor_BJT.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "power")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/power.kicad_sym")(options "")(descr ""))
|
||||
)
|
||||
Binary file not shown.
@@ -0,0 +1,28 @@
|
||||
Item #,Designator,Qty,Manufacturer,Mfg Part #,Description / Value,Package / Footprint,Type,Notes
|
||||
1,U1,1,Espressif,ESP32-WROOM-32E-N8,"Wi-Fi + Bluetooth module, 8 MB flash, PCB antenna",ESP32-WROOM-32E,SMD,
|
||||
2,U2,1,WCH,CH340C,"USB to UART bridge, SOIC-16",SOIC-16_3.9x9.9mm_P1.27mm,SMD,
|
||||
3,U3,1,Advanced Monolithic Systems,AMS1117-3.3,"LDO regulator 3.3 V 1 A, SOT-223",SOT-223-3_TabPin2,SMD,Any AMS1117-3.3 equivalent ok
|
||||
4,U4,1,Texas Instruments,SN74AHCT1G125DBVR,"Single buffer, TTL-level input, SOT-23-5",SOT-23-5,SMD,
|
||||
5,PS1,1,RECOM,R-78E5.0-0.5,"Switching regulator 5 V 0.5 A, SIP-3",Converter_DCDC_RECOM_R-78E-0.5_THT,Thru-Hole,
|
||||
6,J1,1,Same Sky (CUI Devices),PJ-063AH,"DC barrel jack, 2.0 mm pin, fits 5.5 x 2.1 mm plugs",BarrelJack_CUI_PJ-063AH_Horizontal,Thru-Hole,
|
||||
7,J2,1,Korean Hroparts Elec,TYPE-C-31-M-12,"USB-C receptacle, USB 2.0, 16 pin",USB_C_Receptacle_HRO_TYPE-C-31-M-12,SMD,
|
||||
8,"J3,J4",2,JST,B4B-XH-A(LF)(SN),"XH header, 4 pin, 2.5 mm pitch, vertical",JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical,Thru-Hole,
|
||||
9,"J5,J6,J7",3,JST,B3B-XH-A(LF)(SN),"XH header, 3 pin, 2.5 mm pitch, vertical",JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical,Thru-Hole,
|
||||
10,J8,1,Sullins,PRPC004SAAN-RC,"Male pin header 1 x 4, 2.54 mm pitch, vertical",PinHeader_1x04_P2.54mm_Vertical,Thru-Hole,
|
||||
11,"A1,A2",4,Sullins,PPTC081LFBN-RC,"Female pin header 1 x 8, 2.54 mm pitch, 8.5 mm tall",Pololu_Breakout-16_15.2x20.3mm,Thru-Hole,2 headers per driver (4 total). Sockets only: the DRV8825 modules are supplied by me
|
||||
12,D1,1,,SS34,"Schottky diode 40 V 3 A, SMA",D_SMA,SMD,Any brand
|
||||
13,"D2,D3",2,,B5819W,"Schottky diode 40 V 1 A, SOD-123",D_SOD-123,SMD,Any brand
|
||||
14,D4,1,,,"LED green, 0603",LED_0603_1608Metric,SMD,Any brand
|
||||
15,D5,1,,,"LED blue, 0603",LED_0603_1608Metric,SMD,Any brand
|
||||
16,"Q1,Q2",2,onsemi,MMBT3904LT1G,"NPN transistor 40 V 200 mA, SOT-23",SOT-23,SMD,Any MMBT3904 ok
|
||||
17,"SW1,SW2",2,Omron,B3U-1000P,"Tactile switch, SMD, 3 x 2.5 mm",SW_SPST_B3U-1000P,SMD,
|
||||
18,"C1,C2",2,Panasonic,EEE-FK1V101GP,"Aluminium electrolytic capacitor 100 uF 35 V, SMD 8 x 10.2 mm",CP_Elec_8x10,SMD,
|
||||
19,"C3,C4",2,,,"Ceramic capacitor 10 uF 25 V X5R, 1206",C_1206_3216Metric,SMD,Any brand
|
||||
20,"C5,C9",2,,,"Ceramic capacitor 10 uF 16 V X5R, 0805",C_0805_2012Metric,SMD,Any brand
|
||||
21,C6,1,,,"Ceramic capacitor 22 uF 10 V X5R, 0805",C_0805_2012Metric,SMD,Any brand
|
||||
22,"C7,C10,C13,C14,C15",5,,,"Ceramic capacitor 100 nF 50 V X7R, 0603",C_0603_1608Metric,SMD,Any brand
|
||||
23,C8,1,,,"Ceramic capacitor 1 uF 16 V X7R, 0603",C_0603_1608Metric,SMD,Any brand
|
||||
24,"R1,R2",2,,,"Resistor 5.1 k 1%, 0603",R_0603_1608Metric,SMD,Any brand
|
||||
25,"R3,R4,R5,R6,R8,R9,R11",7,,,"Resistor 10 k 1%, 0603",R_0603_1608Metric,SMD,Any brand
|
||||
26,"R10,R12,R14,R15",4,,,"Resistor 1 k 1%, 0603",R_0603_1608Metric,SMD,Any brand
|
||||
27,R13,1,,,"Resistor 330 1%, 0603",R_0603_1608Metric,SMD,Any brand
|
||||
|
@@ -0,0 +1,52 @@
|
||||
Designator,Mid X (mm),Mid Y (mm),Layer,Rotation,Value,Footprint
|
||||
A1,38.100,61.440,Top,270,DRV8825 StepStick (theta/spin),Pololu_Breakout-16_15.2x20.3mm
|
||||
A2,59.100,61.440,Top,270,DRV8825 StepStick (rho/arm),Pololu_Breakout-16_15.2x20.3mm
|
||||
C1,24.000,42.340,Top,0,100uF 25V,CP_Elec_8x10
|
||||
C2,45.500,42.340,Top,0,100uF 25V,CP_Elec_8x10
|
||||
C3,20.500,23.740,Top,0,10uF 25V,C_1206_3216Metric
|
||||
C4,26.000,23.740,Top,0,10uF 16V,C_1206_3216Metric
|
||||
C5,59.600,19.340,Top,90,10uF,C_0805_2012Metric
|
||||
C6,59.600,23.840,Top,90,22uF,C_0805_2012Metric
|
||||
C7,27.000,20.340,Top,90,100nF,C_0603_1608Metric
|
||||
C8,54.600,10.640,Top,0,1uF,C_0603_1608Metric
|
||||
C9,57.500,8.740,Top,90,10uF,C_0805_2012Metric
|
||||
C10,55.000,8.340,Top,90,100nF,C_0603_1608Metric
|
||||
C13,61.000,44.840,Top,0,100nF,C_0603_1608Metric
|
||||
C14,61.000,42.640,Top,0,100nF,C_0603_1608Metric
|
||||
C15,61.000,33.340,Top,90,100nF,C_0603_1608Metric
|
||||
D1,16.000,41.340,Top,270,SS34,D_SMA
|
||||
D2,29.000,28.340,Top,0,B5819W,D_SOD-123
|
||||
D3,13.000,23.540,Top,0,B5819W,D_SOD-123
|
||||
D4,34.000,33.340,Top,0,GREEN,LED_0603_1608Metric
|
||||
D5,38.000,33.340,Top,0,BLUE,LED_0603_1608Metric
|
||||
J1,12.300,38.340,Top,270,12V DC 5.5x2.1,BarrelJack_CUI_PJ-063AH_Horizontal
|
||||
J2,3.400,19.340,Top,270,USB-C,USB_C_Receptacle_HRO_TYPE-C-31-M-12
|
||||
J3,4.500,59.940,Top,270,SPIN MOTOR,JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical
|
||||
J4,76.500,59.940,Top,270,ARM MOTOR,JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical
|
||||
J5,76.500,44.840,Top,270,HOME SW,JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical
|
||||
J6,76.500,33.740,Top,270,HALL,JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical
|
||||
J7,76.500,22.640,Top,270,LED STRIP,JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical
|
||||
J8,77.500,11.340,Top,0,EXPANSION,PinHeader_1x04_P2.54mm_Vertical
|
||||
PS1,18.000,28.340,Top,0,R-78E5.0-0.5,Converter_DCDC_RECOM_R-78E-0.5_THT
|
||||
Q1,28.000,16.140,Top,0,MMBT3904,SOT-23
|
||||
Q2,28.000,12.140,Top,0,MMBT3904,SOT-23
|
||||
R1,11.500,20.590,Top,0,5.1k,R_0603_1608Metric
|
||||
R2,11.500,17.590,Top,0,5.1k,R_0603_1608Metric
|
||||
R3,31.000,18.340,Top,90,10k,R_0603_1608Metric
|
||||
R4,31.000,13.840,Top,90,10k,R_0603_1608Metric
|
||||
R5,54.600,12.340,Top,0,10k,R_0603_1608Metric
|
||||
R6,31.000,25.840,Top,0,10k,R_0603_1608Metric
|
||||
R8,47.000,32.340,Top,0,10k,R_0603_1608Metric
|
||||
R9,54.000,44.840,Top,0,10k,R_0603_1608Metric
|
||||
R10,57.500,44.840,Top,0,1k,R_0603_1608Metric
|
||||
R11,54.000,42.640,Top,0,10k,R_0603_1608Metric
|
||||
R12,57.500,42.640,Top,0,1k,R_0603_1608Metric
|
||||
R13,70.000,33.340,Top,90,330,R_0603_1608Metric
|
||||
R14,34.000,30.840,Top,0,1k,R_0603_1608Metric
|
||||
R15,38.000,30.840,Top,0,1k,R_0603_1608Metric
|
||||
SW1,27.500,8.340,Top,0,RESET,SW_SPST_B3U-1000P
|
||||
SW2,22.000,8.340,Top,0,BOOT,SW_SPST_B3U-1000P
|
||||
U1,43.000,12.750,Top,180,ESP32-WROOM-32E-N8,ESP32-WROOM-32E
|
||||
U2,22.000,16.340,Top,0,CH340C,SOIC-16_3.9x9.9mm_P1.27mm
|
||||
U3,66.000,21.840,Top,0,AMS1117-3.3,SOT-223-3_TabPin2
|
||||
U4,65.500,33.340,Top,0,74AHCT1G125,SOT-23-5
|
||||
|
@@ -0,0 +1,51 @@
|
||||
# PCBWay order: SandTable Controller v0.3
|
||||
|
||||
## Files
|
||||
|
||||
| File | Where it goes |
|
||||
|---|---|
|
||||
| `1-gerbers-sandtable-controller-v0.3.zip` | PCB order: **Upload Gerber file** |
|
||||
| `2-bom-sandtable-controller-v0.3.csv` | Assembly order: **BOM file** |
|
||||
| `3-centroid-sandtable-controller-v0.3.csv` | Assembly order: **Centroid / pick-and-place file** |
|
||||
| `assembly-top-v0.3.pdf` | Attach if PCBWay asks for an assembly drawing |
|
||||
| `schematic-v0.3.pdf` | Attach if PCBWay asks for a schematic |
|
||||
|
||||
## PCB settings
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| Board type | Single pieces |
|
||||
| Size | 80 x 63.34 mm |
|
||||
| Quantity | 5 (the minimum) |
|
||||
| Layers | 2 |
|
||||
| Material | FR-4, TG130-140 |
|
||||
| Thickness | 1.6 mm |
|
||||
| Min track / spacing | 6/6 mil |
|
||||
| Min hole size | 0.3 mm |
|
||||
| Solder mask / silkscreen | any colour / white |
|
||||
| Surface finish | HASL lead-free (or ENIG) |
|
||||
| Via process | Tenting vias |
|
||||
| Finished copper | 1 oz |
|
||||
| Product No. | Specify a location (the back of the board has a "WayWayWay" marker for it) |
|
||||
|
||||
## Assembly settings (PCB Assembly quote)
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| Service | Turnkey (PCBWay buys the parts) |
|
||||
| Assembly side | Top side only |
|
||||
| Quantity | 1 or 2 (fewer than the 5 bare boards is fine) |
|
||||
| Unique parts | 27 |
|
||||
| SMD parts | 41 |
|
||||
| BGA / QFP parts | 0 |
|
||||
| Through-hole parts | 12 (10 designators; each driver socket is 2 headers) |
|
||||
| Accept alternatives | Yes, for lines marked "Any brand" |
|
||||
|
||||
**Note for PCBWay** (paste into the comments box):
|
||||
|
||||
> Please fit the female header sockets for A1 and A2 only. Do not supply the stepper driver modules; I will plug in my own. BOM lines marked "Any brand" may use any equivalent part to the description. Centroid coordinates are in mm, with the origin at the board's bottom-left corner.
|
||||
|
||||
## When PCBWay sends their review
|
||||
- Check the polarity of C1, C2 (electrolytic), D1-D3 (diodes) and D4, D5 (LEDs) against the + and bar marks in `assembly-top-v0.3.pdf`.
|
||||
- Check that the ESP32 (U1) antenna end sits at the board edge, over the copper-free area.
|
||||
- Check that pin 1 of U2, U3 and U4 is at the dot or triangle on the silkscreen.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,77 @@
|
||||
# FluidNC config for the SandTable Controller v0.3 (ESP32 + 2x DRV8825 at 1/32 step).
|
||||
# A starting point: not yet run on real hardware.
|
||||
#
|
||||
# Units
|
||||
# X = theta (arm angle) in DEGREES
|
||||
# 200 steps x 32 microsteps x (80T hub / 20T motor pulley) = 25600 steps per arm turn = 71.111 per degree
|
||||
# Y = rho (ball distance from the centre) in MM
|
||||
# 200 steps x 32 microsteps / (20T GT2 pulley = 40 mm per turn) = 160 steps per mm
|
||||
#
|
||||
# If an axis turns the wrong way, add :low to its direction_pin (for example gpio.19:low)
|
||||
# or flip one coil's plug.
|
||||
|
||||
board: SandTable Controller v0.3
|
||||
name: Sand Table
|
||||
|
||||
stepping:
|
||||
engine: RMT
|
||||
idle_ms: 255 # 255 = never switch the motors off, so the arm can't drift
|
||||
pulse_us: 4 # DRV8825 needs at least 1.9 us
|
||||
dir_delay_us: 2 # DRV8825 needs at least 0.65 us
|
||||
disable_delay_us: 0
|
||||
|
||||
axes:
|
||||
shared_stepper_disable_pin: gpio.13 # high = drivers off (a pull-up keeps them off during boot)
|
||||
|
||||
x: # theta, degrees
|
||||
steps_per_mm: 71.111
|
||||
max_rate_mm_per_min: 3600 # 10 arm turns per minute
|
||||
acceleration_mm_per_sec2: 120
|
||||
max_travel_mm: 360 # only sets how far $HX searches; theta is unlimited
|
||||
soft_limits: false
|
||||
homing:
|
||||
cycle: 0 # not part of $H. With a hall sensor fitted, home theta by hand with $HX
|
||||
positive_direction: true
|
||||
mpos_mm: 0
|
||||
feed_mm_per_min: 120
|
||||
seek_mm_per_min: 720
|
||||
settle_ms: 250
|
||||
seek_scaler: 1.1
|
||||
feed_scaler: 1.1
|
||||
motor0:
|
||||
limit_neg_pin: NO_PIN
|
||||
limit_pos_pin: NO_PIN
|
||||
limit_all_pin: gpio.35:low # HALL connector (active low, pulled up on the board)
|
||||
hard_limits: false
|
||||
pulloff_mm: 5
|
||||
stepstick:
|
||||
step_pin: gpio.18
|
||||
direction_pin: gpio.19
|
||||
disable_pin: NO_PIN
|
||||
|
||||
y: # rho, mm
|
||||
steps_per_mm: 160
|
||||
max_rate_mm_per_min: 1500
|
||||
acceleration_mm_per_sec2: 100
|
||||
max_travel_mm: 135
|
||||
soft_limits: false
|
||||
homing:
|
||||
cycle: 1 # $H homes rho
|
||||
positive_direction: false # the home switch is at the centre end of the arm
|
||||
mpos_mm: 3 # switch trips near rho 2 mm, then backs off 1 mm. Tune so Y0 is dead centre
|
||||
feed_mm_per_min: 100
|
||||
seek_mm_per_min: 600
|
||||
settle_ms: 250
|
||||
seek_scaler: 1.1
|
||||
feed_scaler: 1.1
|
||||
motor0:
|
||||
limit_neg_pin: gpio.34:low # HOME connector: switch COM to GND (pin 3), NO to signal (pin 2)
|
||||
limit_pos_pin: NO_PIN
|
||||
limit_all_pin: NO_PIN
|
||||
hard_limits: true # patterns stay 1 mm past the switch (rho_min_mm = 3), so a press
|
||||
# while drawing means lost steps: FluidNC stops at once by itself
|
||||
pulloff_mm: 1
|
||||
stepstick:
|
||||
step_pin: gpio.27
|
||||
direction_pin: gpio.14
|
||||
disable_pin: NO_PIN
|
||||
@@ -0,0 +1,20 @@
|
||||
(fp_lib_table
|
||||
(version 7)
|
||||
(lib (name "Button_Switch_SMD")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Button_Switch_SMD.pretty")(options "")(descr ""))
|
||||
(lib (name "Capacitor_SMD")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Capacitor_SMD.pretty")(options "")(descr ""))
|
||||
(lib (name "Connector_BarrelJack")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Connector_BarrelJack.pretty")(options "")(descr ""))
|
||||
(lib (name "Connector_JST")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Connector_JST.pretty")(options "")(descr ""))
|
||||
(lib (name "Connector_PinHeader_2.54mm")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Connector_PinHeader_2.54mm.pretty")(options "")(descr ""))
|
||||
(lib (name "Connector_USB")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Connector_USB.pretty")(options "")(descr ""))
|
||||
(lib (name "Converter_DCDC")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Converter_DCDC.pretty")(options "")(descr ""))
|
||||
(lib (name "Diode_SMD")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Diode_SMD.pretty")(options "")(descr ""))
|
||||
(lib (name "Fuse")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Fuse.pretty")(options "")(descr ""))
|
||||
(lib (name "Jumper")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Jumper.pretty")(options "")(descr ""))
|
||||
(lib (name "LED_SMD")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/LED_SMD.pretty")(options "")(descr ""))
|
||||
(lib (name "Module")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Module.pretty")(options "")(descr ""))
|
||||
(lib (name "MountingHole")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/MountingHole.pretty")(options "")(descr ""))
|
||||
(lib (name "Package_SO")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Package_SO.pretty")(options "")(descr ""))
|
||||
(lib (name "Package_TO_SOT_SMD")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Package_TO_SOT_SMD.pretty")(options "")(descr ""))
|
||||
(lib (name "RF_Module")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/RF_Module.pretty")(options "")(descr ""))
|
||||
(lib (name "Resistor_SMD")(type "KiCad")(uri "${KICAD9_FOOTPRINT_DIR}/Resistor_SMD.pretty")(options "")(descr ""))
|
||||
)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,726 @@
|
||||
{
|
||||
"board": {
|
||||
"3dviewports": [],
|
||||
"design_settings": {
|
||||
"defaults": {
|
||||
"apply_defaults_to_fp_fields": false,
|
||||
"apply_defaults_to_fp_shapes": false,
|
||||
"apply_defaults_to_fp_text": false,
|
||||
"board_outline_line_width": 0.05,
|
||||
"copper_line_width": 0.2,
|
||||
"copper_text_italic": false,
|
||||
"copper_text_size_h": 1.5,
|
||||
"copper_text_size_v": 1.5,
|
||||
"copper_text_thickness": 0.3,
|
||||
"copper_text_upright": false,
|
||||
"courtyard_line_width": 0.05,
|
||||
"dimension_precision": 4,
|
||||
"dimension_units": 3,
|
||||
"dimensions": {
|
||||
"arrow_length": 1270000,
|
||||
"extension_offset": 500000,
|
||||
"keep_text_aligned": true,
|
||||
"suppress_zeroes": false,
|
||||
"text_position": 0,
|
||||
"units_format": 1
|
||||
},
|
||||
"fab_line_width": 0.1,
|
||||
"fab_text_italic": false,
|
||||
"fab_text_size_h": 1.0,
|
||||
"fab_text_size_v": 1.0,
|
||||
"fab_text_thickness": 0.15,
|
||||
"fab_text_upright": false,
|
||||
"other_line_width": 0.1,
|
||||
"other_text_italic": false,
|
||||
"other_text_size_h": 1.0,
|
||||
"other_text_size_v": 1.0,
|
||||
"other_text_thickness": 0.15,
|
||||
"other_text_upright": false,
|
||||
"pads": {
|
||||
"drill": 0.0,
|
||||
"height": 8.0,
|
||||
"width": 2.54
|
||||
},
|
||||
"silk_line_width": 0.12,
|
||||
"silk_text_italic": false,
|
||||
"silk_text_size_h": 1.0,
|
||||
"silk_text_size_v": 1.0,
|
||||
"silk_text_thickness": 0.15,
|
||||
"silk_text_upright": false,
|
||||
"zones": {
|
||||
"min_clearance": 0.508
|
||||
}
|
||||
},
|
||||
"diff_pair_dimensions": [],
|
||||
"drc_exclusions": [],
|
||||
"meta": {
|
||||
"filename": "board_design_settings.json",
|
||||
"version": 2
|
||||
},
|
||||
"rule_severities": {
|
||||
"annular_width": "error",
|
||||
"clearance": "error",
|
||||
"connection_width": "warning",
|
||||
"copper_edge_clearance": "error",
|
||||
"copper_sliver": "warning",
|
||||
"courtyards_overlap": "error",
|
||||
"creepage": "error",
|
||||
"diff_pair_gap_out_of_range": "error",
|
||||
"diff_pair_uncoupled_length_too_long": "error",
|
||||
"drill_out_of_range": "error",
|
||||
"duplicate_footprints": "warning",
|
||||
"extra_footprint": "warning",
|
||||
"footprint": "error",
|
||||
"footprint_filters_mismatch": "ignore",
|
||||
"footprint_symbol_mismatch": "warning",
|
||||
"footprint_type_mismatch": "ignore",
|
||||
"hole_clearance": "error",
|
||||
"hole_near_hole": "error",
|
||||
"hole_to_hole": "error",
|
||||
"holes_co_located": "warning",
|
||||
"invalid_outline": "error",
|
||||
"isolated_copper": "warning",
|
||||
"item_on_disabled_layer": "error",
|
||||
"items_not_allowed": "error",
|
||||
"length_out_of_range": "error",
|
||||
"lib_footprint_issues": "ignore",
|
||||
"lib_footprint_mismatch": "ignore",
|
||||
"malformed_courtyard": "error",
|
||||
"microvia_drill_out_of_range": "error",
|
||||
"mirrored_text_on_front_layer": "warning",
|
||||
"missing_courtyard": "ignore",
|
||||
"missing_footprint": "warning",
|
||||
"net_conflict": "warning",
|
||||
"nonmirrored_text_on_back_layer": "warning",
|
||||
"npth_inside_courtyard": "ignore",
|
||||
"padstack": "warning",
|
||||
"pth_inside_courtyard": "ignore",
|
||||
"shorting_items": "error",
|
||||
"silk_edge_clearance": "ignore",
|
||||
"silk_over_copper": "ignore",
|
||||
"silk_overlap": "ignore",
|
||||
"skew_out_of_range": "error",
|
||||
"solder_mask_bridge": "error",
|
||||
"starved_thermal": "error",
|
||||
"text_height": "ignore",
|
||||
"text_thickness": "warning",
|
||||
"through_hole_pad_without_hole": "error",
|
||||
"too_many_vias": "error",
|
||||
"track_angle": "error",
|
||||
"track_dangling": "warning",
|
||||
"track_segment_length": "error",
|
||||
"track_width": "error",
|
||||
"tracks_crossing": "error",
|
||||
"unconnected_items": "error",
|
||||
"unresolved_variable": "error",
|
||||
"via_dangling": "warning",
|
||||
"zones_intersect": "error"
|
||||
},
|
||||
"rules": {
|
||||
"max_error": 0.005,
|
||||
"min_clearance": 0.16,
|
||||
"min_connection": 0.0,
|
||||
"min_copper_edge_clearance": 0.3,
|
||||
"min_groove_width": 0.0,
|
||||
"min_hole_clearance": 0.2,
|
||||
"min_hole_to_hole": 0.25,
|
||||
"min_microvia_diameter": 0.2,
|
||||
"min_microvia_drill": 0.1,
|
||||
"min_resolved_spokes": 2,
|
||||
"min_silk_clearance": 0.0,
|
||||
"min_text_height": 0.8,
|
||||
"min_text_thickness": 0.08,
|
||||
"min_through_hole_diameter": 0.3,
|
||||
"min_track_width": 0.16,
|
||||
"min_via_annular_width": 0.15,
|
||||
"min_via_diameter": 0.6,
|
||||
"solder_mask_to_copper_clearance": 0.0,
|
||||
"use_height_for_length_calcs": true
|
||||
},
|
||||
"teardrop_options": [
|
||||
{
|
||||
"td_onpthpad": true,
|
||||
"td_onroundshapesonly": false,
|
||||
"td_onsmdpad": true,
|
||||
"td_ontrackend": false,
|
||||
"td_onvia": true
|
||||
}
|
||||
],
|
||||
"teardrop_parameters": [
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_round_shape",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
},
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_rect_shape",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
},
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_track_end",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
}
|
||||
],
|
||||
"track_widths": [],
|
||||
"tuning_pattern_settings": {
|
||||
"diff_pair_defaults": {
|
||||
"corner_radius_percentage": 80,
|
||||
"corner_style": 1,
|
||||
"max_amplitude": 1.0,
|
||||
"min_amplitude": 0.2,
|
||||
"single_sided": false,
|
||||
"spacing": 1.0
|
||||
},
|
||||
"diff_pair_skew_defaults": {
|
||||
"corner_radius_percentage": 80,
|
||||
"corner_style": 1,
|
||||
"max_amplitude": 1.0,
|
||||
"min_amplitude": 0.2,
|
||||
"single_sided": false,
|
||||
"spacing": 0.6
|
||||
},
|
||||
"single_track_defaults": {
|
||||
"corner_radius_percentage": 80,
|
||||
"corner_style": 1,
|
||||
"max_amplitude": 1.0,
|
||||
"min_amplitude": 0.2,
|
||||
"single_sided": false,
|
||||
"spacing": 0.6
|
||||
}
|
||||
},
|
||||
"via_dimensions": [],
|
||||
"zones_allow_external_fillets": false
|
||||
},
|
||||
"ipc2581": {
|
||||
"dist": "",
|
||||
"distpn": "",
|
||||
"internal_id": "",
|
||||
"mfg": "",
|
||||
"mpn": ""
|
||||
},
|
||||
"layer_pairs": [],
|
||||
"layer_presets": [],
|
||||
"viewports": []
|
||||
},
|
||||
"cvpcb": {
|
||||
"equivalence_files": []
|
||||
},
|
||||
"erc": {
|
||||
"erc_exclusions": [],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"pin_map": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
],
|
||||
"rule_severities": {
|
||||
"bus_definition_conflict": "error",
|
||||
"bus_entry_needed": "error",
|
||||
"bus_to_bus_conflict": "error",
|
||||
"bus_to_net_conflict": "error",
|
||||
"conflicting_netclasses": "error",
|
||||
"different_unit_footprint": "error",
|
||||
"different_unit_net": "error",
|
||||
"duplicate_reference": "error",
|
||||
"duplicate_sheet_names": "error",
|
||||
"endpoint_off_grid": "warning",
|
||||
"extra_units": "error",
|
||||
"footprint_filter": "ignore",
|
||||
"footprint_link_issues": "warning",
|
||||
"four_way_junction": "ignore",
|
||||
"global_label_dangling": "warning",
|
||||
"hier_label_mismatch": "error",
|
||||
"label_dangling": "error",
|
||||
"label_multiple_wires": "warning",
|
||||
"lib_symbol_issues": "warning",
|
||||
"lib_symbol_mismatch": "warning",
|
||||
"missing_bidi_pin": "warning",
|
||||
"missing_input_pin": "warning",
|
||||
"missing_power_pin": "error",
|
||||
"missing_unit": "warning",
|
||||
"multiple_net_names": "warning",
|
||||
"net_not_bus_member": "warning",
|
||||
"no_connect_connected": "warning",
|
||||
"no_connect_dangling": "warning",
|
||||
"pin_not_connected": "error",
|
||||
"pin_not_driven": "error",
|
||||
"pin_to_pin": "warning",
|
||||
"power_pin_not_driven": "error",
|
||||
"same_local_global_label": "warning",
|
||||
"similar_label_and_power": "warning",
|
||||
"similar_labels": "warning",
|
||||
"similar_power": "warning",
|
||||
"simulation_model_issue": "ignore",
|
||||
"single_global_label": "ignore",
|
||||
"unannotated": "error",
|
||||
"unconnected_wire_endpoint": "warning",
|
||||
"unit_value_mismatch": "error",
|
||||
"unresolved_variable": "error",
|
||||
"wire_dangling": "error"
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"pinned_footprint_libs": [],
|
||||
"pinned_symbol_libs": []
|
||||
},
|
||||
"meta": {
|
||||
"filename": "sandtable-controller.kicad_pro",
|
||||
"version": 3
|
||||
},
|
||||
"net_settings": {
|
||||
"classes": [
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.16,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.2,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "Default",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"priority": 2147483647,
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.2,
|
||||
"via_diameter": 0.6,
|
||||
"via_drill": 0.3,
|
||||
"wire_width": 6
|
||||
},
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.2,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.2,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "Power",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"priority": 0,
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.8,
|
||||
"via_diameter": 0.6,
|
||||
"via_drill": 0.3,
|
||||
"wire_width": 6
|
||||
},
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.2,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.2,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "Motor",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"priority": 0,
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.5,
|
||||
"via_diameter": 0.6,
|
||||
"via_drill": 0.3,
|
||||
"wire_width": 6
|
||||
},
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.2,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.2,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "Rail",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"priority": 0,
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.5,
|
||||
"via_diameter": 0.6,
|
||||
"via_drill": 0.3,
|
||||
"wire_width": 6
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"version": 4
|
||||
},
|
||||
"net_colors": null,
|
||||
"netclass_assignments": null,
|
||||
"netclass_patterns": [
|
||||
{
|
||||
"netclass": "Power",
|
||||
"pattern": "/GND"
|
||||
},
|
||||
{
|
||||
"netclass": "Power",
|
||||
"pattern": "/VIN_RAW"
|
||||
},
|
||||
{
|
||||
"netclass": "Power",
|
||||
"pattern": "/VIN_FUSED"
|
||||
},
|
||||
{
|
||||
"netclass": "Power",
|
||||
"pattern": "/+12V"
|
||||
},
|
||||
{
|
||||
"netclass": "Motor",
|
||||
"pattern": "/X_1A"
|
||||
},
|
||||
{
|
||||
"netclass": "Motor",
|
||||
"pattern": "/X_1B"
|
||||
},
|
||||
{
|
||||
"netclass": "Motor",
|
||||
"pattern": "/X_2A"
|
||||
},
|
||||
{
|
||||
"netclass": "Motor",
|
||||
"pattern": "/X_2B"
|
||||
},
|
||||
{
|
||||
"netclass": "Motor",
|
||||
"pattern": "/Y_1A"
|
||||
},
|
||||
{
|
||||
"netclass": "Motor",
|
||||
"pattern": "/Y_1B"
|
||||
},
|
||||
{
|
||||
"netclass": "Motor",
|
||||
"pattern": "/Y_2A"
|
||||
},
|
||||
{
|
||||
"netclass": "Motor",
|
||||
"pattern": "/Y_2B"
|
||||
},
|
||||
{
|
||||
"netclass": "Rail",
|
||||
"pattern": "/+5V"
|
||||
},
|
||||
{
|
||||
"netclass": "Rail",
|
||||
"pattern": "/+5V_REG"
|
||||
},
|
||||
{
|
||||
"netclass": "Rail",
|
||||
"pattern": "/VBUS"
|
||||
},
|
||||
{
|
||||
"netclass": "Rail",
|
||||
"pattern": "/+3V3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pcbnew": {
|
||||
"last_paths": {
|
||||
"gencad": "",
|
||||
"idf": "",
|
||||
"netlist": "",
|
||||
"plot": "",
|
||||
"pos_files": "",
|
||||
"specctra_dsn": "",
|
||||
"step": "",
|
||||
"svg": "",
|
||||
"vrml": ""
|
||||
},
|
||||
"page_layout_descr_file": ""
|
||||
},
|
||||
"schematic": {
|
||||
"annotate_start_num": 0,
|
||||
"bom_export_filename": "${PROJECTNAME}.csv",
|
||||
"bom_fmt_presets": [],
|
||||
"bom_fmt_settings": {
|
||||
"field_delimiter": ",",
|
||||
"keep_line_breaks": false,
|
||||
"keep_tabs": false,
|
||||
"name": "CSV",
|
||||
"ref_delimiter": ",",
|
||||
"ref_range_delimiter": "",
|
||||
"string_delimiter": "\""
|
||||
},
|
||||
"bom_presets": [],
|
||||
"bom_settings": {
|
||||
"exclude_dnp": false,
|
||||
"fields_ordered": [
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Reference",
|
||||
"name": "Reference",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Value",
|
||||
"name": "Value",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Datasheet",
|
||||
"name": "Datasheet",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Footprint",
|
||||
"name": "Footprint",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Qty",
|
||||
"name": "${QUANTITY}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "DNP",
|
||||
"name": "${DNP}",
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"filter_string": "",
|
||||
"group_symbols": true,
|
||||
"include_excluded_from_bom": false,
|
||||
"name": "Grouped By Value",
|
||||
"sort_asc": true,
|
||||
"sort_field": "Reference"
|
||||
},
|
||||
"connection_grid_size": 50.0,
|
||||
"drawing": {
|
||||
"dashed_lines_dash_length_ratio": 12.0,
|
||||
"dashed_lines_gap_length_ratio": 3.0,
|
||||
"default_line_thickness": 6.0,
|
||||
"default_text_size": 50.0,
|
||||
"field_names": [],
|
||||
"intersheets_ref_own_page": false,
|
||||
"intersheets_ref_prefix": "",
|
||||
"intersheets_ref_short": false,
|
||||
"intersheets_ref_show": false,
|
||||
"intersheets_ref_suffix": "",
|
||||
"junction_size_choice": 3,
|
||||
"label_size_ratio": 0.25,
|
||||
"operating_point_overlay_i_precision": 3,
|
||||
"operating_point_overlay_i_range": "~A",
|
||||
"operating_point_overlay_v_precision": 3,
|
||||
"operating_point_overlay_v_range": "~V",
|
||||
"overbar_offset_ratio": 1.23,
|
||||
"pin_symbol_size": 0.0,
|
||||
"text_offset_ratio": 0.08
|
||||
},
|
||||
"legacy_lib_dir": "",
|
||||
"legacy_lib_list": [],
|
||||
"meta": {
|
||||
"version": 1
|
||||
},
|
||||
"net_format_name": "",
|
||||
"page_layout_descr_file": "",
|
||||
"plot_directory": "",
|
||||
"space_save_all_events": true,
|
||||
"spice_current_sheet_as_root": false,
|
||||
"spice_external_command": "spice \"%I\"",
|
||||
"spice_model_current_sheet_as_root": true,
|
||||
"spice_save_all_currents": false,
|
||||
"spice_save_all_dissipations": false,
|
||||
"spice_save_all_voltages": false,
|
||||
"subpart_first_id": 65,
|
||||
"subpart_id_separator": 0
|
||||
},
|
||||
"sheets": [
|
||||
[
|
||||
"d585c3d2-c803-5c74-8a05-da481956c6b2",
|
||||
"Root"
|
||||
]
|
||||
],
|
||||
"text_variables": {}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,753 @@
|
||||
(kicad_symbol_lib
|
||||
(version 20241209)
|
||||
(generator "sandtable-gen")
|
||||
(generator_version "9.0")
|
||||
(symbol "CH340C_3V3"
|
||||
(exclude_from_sim no)
|
||||
(in_bom yes)
|
||||
(on_board yes)
|
||||
(property "Reference" "U"
|
||||
(at -5.08 13.97 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify right)
|
||||
)
|
||||
)
|
||||
(property "Value" "CH340C"
|
||||
(at 1.27 13.97 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify left)
|
||||
)
|
||||
)
|
||||
(property "Footprint" "Package_SO:SOIC-16_3.9x9.9mm_P1.27mm"
|
||||
(at -18.542 30.226 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify left)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "Datasheet" "https://datasheet.lcsc.com/szlcsc/Jiangsu-Qin-Heng-CH340C_C84681.pdf"
|
||||
(at -6.604 33.274 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "Description" "USB serial converter, crystal-less, UART, SOIC-16"
|
||||
(at -1.524 36.068 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "ki_keywords" "USB UART Serial Converter Interface"
|
||||
(at 0 0 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "ki_fp_filters" "SOIC*3.9x9.9mm*P1.27mm*"
|
||||
(at 0 0 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(symbol "CH340C_3V3_0_1"
|
||||
(rectangle
|
||||
(start -7.62 12.7)
|
||||
(end 7.62 -12.7)
|
||||
(stroke
|
||||
(width 0.254)
|
||||
(type default)
|
||||
)
|
||||
(fill
|
||||
(type background)
|
||||
)
|
||||
)
|
||||
)
|
||||
(symbol "CH340C_3V3_1_1"
|
||||
(pin input line
|
||||
(at -10.16 7.62 0)
|
||||
(length 2.54)
|
||||
(name "R232"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "15"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin bidirectional line
|
||||
(at -10.16 2.54 0)
|
||||
(length 2.54)
|
||||
(name "UD+"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "5"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin bidirectional line
|
||||
(at -10.16 0 0)
|
||||
(length 2.54)
|
||||
(name "UD-"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "6"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin no_connect line
|
||||
(at -7.62 -5.08 0)
|
||||
(length 2.54)
|
||||
(hide yes)
|
||||
(name "NC"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "7"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin no_connect line
|
||||
(at -7.62 -7.62 0)
|
||||
(length 2.54)
|
||||
(hide yes)
|
||||
(name "NC"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "8"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin power_in line
|
||||
(at -2.54 15.24 270)
|
||||
(length 2.54)
|
||||
(name "VCC"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "16"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin passive line
|
||||
(at 0 15.24 270)
|
||||
(length 2.54)
|
||||
(name "V3"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "4"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin power_in line
|
||||
(at 0 -15.24 90)
|
||||
(length 2.54)
|
||||
(name "GND"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "1"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin output line
|
||||
(at 10.16 10.16 180)
|
||||
(length 2.54)
|
||||
(name "TXD"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "2"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at 10.16 7.62 180)
|
||||
(length 2.54)
|
||||
(name "RXD"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "3"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at 10.16 2.54 180)
|
||||
(length 2.54)
|
||||
(name "~{CTS}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "9"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at 10.16 0 180)
|
||||
(length 2.54)
|
||||
(name "~{DSR}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "10"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at 10.16 -2.54 180)
|
||||
(length 2.54)
|
||||
(name "~{RI}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "11"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at 10.16 -5.08 180)
|
||||
(length 2.54)
|
||||
(name "~{DCD}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "12"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin output line
|
||||
(at 10.16 -7.62 180)
|
||||
(length 2.54)
|
||||
(name "~{DTR}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "13"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin output line
|
||||
(at 10.16 -10.16 180)
|
||||
(length 2.54)
|
||||
(name "~{RTS}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "14"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(embedded_fonts no)
|
||||
)
|
||||
(symbol "DRV8825_StepStick"
|
||||
(exclude_from_sim no)
|
||||
(in_bom yes)
|
||||
(on_board yes)
|
||||
(property "Reference" "A"
|
||||
(at -2.54 19.05 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify right)
|
||||
)
|
||||
)
|
||||
(property "Value" "Pololu_Breakout_A4988"
|
||||
(at -2.54 16.51 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify right)
|
||||
)
|
||||
)
|
||||
(property "Footprint" "Module:Pololu_Breakout-16_15.2x20.3mm"
|
||||
(at 6.985 -19.05 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify left)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "Datasheet" "https://www.pololu.com/product/2980/pictures"
|
||||
(at 2.54 -7.62 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "Description" "Pololu Breakout Board, Stepper Driver A4988"
|
||||
(at 0 0 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "ki_keywords" "Pololu Breakout Board Stepper Driver A4988"
|
||||
(at 0 0 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "ki_fp_filters" "Pololu*Breakout*15.2x20.3mm*"
|
||||
(at 0 0 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(symbol "DRV8825_StepStick_0_1"
|
||||
(rectangle
|
||||
(start 10.16 -17.78)
|
||||
(end -7.62 15.24)
|
||||
(stroke
|
||||
(width 0.254)
|
||||
(type default)
|
||||
)
|
||||
(fill
|
||||
(type background)
|
||||
)
|
||||
)
|
||||
)
|
||||
(symbol "DRV8825_StepStick_1_1"
|
||||
(pin input line
|
||||
(at -10.16 10.16 0)
|
||||
(length 2.54)
|
||||
(name "~{RST}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "13"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at -10.16 7.62 0)
|
||||
(length 2.54)
|
||||
(name "~{SLP}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "14"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at -10.16 2.54 0)
|
||||
(length 2.54)
|
||||
(name "~{EN}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "9"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at -10.16 0 0)
|
||||
(length 2.54)
|
||||
(name "STEP"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "15"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at -10.16 -2.54 0)
|
||||
(length 2.54)
|
||||
(name "DIR"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "16"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at -10.16 -7.62 0)
|
||||
(length 2.54)
|
||||
(name "M0"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "10"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at -10.16 -10.16 0)
|
||||
(length 2.54)
|
||||
(name "M1"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "11"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin input line
|
||||
(at -10.16 -12.7 0)
|
||||
(length 2.54)
|
||||
(name "M2"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "12"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin passive line
|
||||
(at 0 17.78 270)
|
||||
(length 2.54)
|
||||
(name "VDD/~{FAULT}"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "2"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin power_in line
|
||||
(at 0 -20.32 90)
|
||||
(length 2.54)
|
||||
(name "GND"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "1"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin power_in line
|
||||
(at 5.08 17.78 270)
|
||||
(length 2.54)
|
||||
(name "VMOT"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "8"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin power_in line
|
||||
(at 5.08 -20.32 90)
|
||||
(length 2.54)
|
||||
(name "GND"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "7"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin output line
|
||||
(at 12.7 2.54 180)
|
||||
(length 2.54)
|
||||
(name "A2"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "3"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin output line
|
||||
(at 12.7 0 180)
|
||||
(length 2.54)
|
||||
(name "A1"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "4"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin output line
|
||||
(at 12.7 -2.54 180)
|
||||
(length 2.54)
|
||||
(name "B1"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "5"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pin output line
|
||||
(at 12.7 -5.08 180)
|
||||
(length 2.54)
|
||||
(name "B2"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(number "6"
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(embedded_fonts no)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
(sym_lib_table
|
||||
(version 7)
|
||||
(lib (name "SandTable")(type "KiCad")(uri "${KIPRJMOD}/sandtable.kicad_sym")(options "")(descr "SandTable custom symbols"))
|
||||
(lib (name "74xGxx")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/74xGxx.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Connector")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Connector.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Connector_Generic")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Connector_Generic.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Converter_DCDC")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Converter_DCDC.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Device")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Device.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Driver_Motor")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Driver_Motor.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Interface_USB")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Interface_USB.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Jumper")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Jumper.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Mechanical")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Mechanical.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Power_Protection")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Power_Protection.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "RF_Module")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/RF_Module.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Regulator_Linear")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Regulator_Linear.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Switch")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Switch.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "Transistor_BJT")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/Transistor_BJT.kicad_sym")(options "")(descr ""))
|
||||
(lib (name "power")(type "KiCad")(uri "${KICAD9_SYMBOL_DIR}/power.kicad_sym")(options "")(descr ""))
|
||||
)
|
||||
Binary file not shown.
@@ -0,0 +1,32 @@
|
||||
Item #,Designator,Qty,Manufacturer,Mfg Part #,Description / Value,Package / Footprint,Type,Notes
|
||||
1,U1,1,Espressif,ESP32-WROOM-32E-N8,"Wi-Fi + Bluetooth module, 8 MB flash, PCB antenna",ESP32-WROOM-32E,SMD,
|
||||
2,U2,1,WCH,CH340C,"USB to UART bridge, SOIC-16",SOIC-16_3.9x9.9mm_P1.27mm,SMD,
|
||||
3,U3,1,Diodes Incorporated,AP7361C-33E-13,"LDO regulator 3.3 V 1 A, low dropout, SOT-223 (pin 1 IN, 2 GND/tab, 3 OUT)",SOT-223-3_TabPin2,SMD,"Not an AMS1117: the pinout differs, no substitutes"
|
||||
4,U5,1,STMicroelectronics,USBLC6-2SC6,"USB ESD protection, 2 lines, SOT-23-6",SOT-23-6,SMD,
|
||||
5,U4,1,Texas Instruments,SN74AHCT1G125DBVR,"Single buffer, TTL-level input, SOT-23-5",SOT-23-5,SMD,
|
||||
6,PS1,1,RECOM,R-78E5.0-1.0,"Switching regulator 5 V 1 A, SIP-3",Converter_DCDC_RECOM_R-78E-0.5_THT,Thru-Hole,
|
||||
7,F1,1,Bourns,MF-MSMF250/16X-2,"PTC resettable fuse, 2.5 A hold, 5 A trip, 16 V, 1812",Fuse_1812_4532Metric,SMD,
|
||||
8,J1,1,Same Sky (CUI Devices),PJ-063AH,"DC barrel jack, 2.0 mm pin, fits 5.5 x 2.1 mm plugs",BarrelJack_CUI_PJ-063AH_Horizontal,Thru-Hole,
|
||||
9,J2,1,Korean Hroparts Elec,TYPE-C-31-M-12,"USB-C receptacle, USB 2.0, 16 pin",USB_C_Receptacle_HRO_TYPE-C-31-M-12,SMD,
|
||||
10,"J3,J4",2,JST,B4B-XH-A(LF)(SN),"XH header, 4 pin, 2.5 mm pitch, vertical",JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical,Thru-Hole,
|
||||
11,"J5,J6,J7",3,JST,B3B-XH-A(LF)(SN),"XH header, 3 pin, 2.5 mm pitch, vertical",JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical,Thru-Hole,
|
||||
12,J8,1,Sullins,PRPC004SAAN-RC,"Male pin header 1 x 4, 2.54 mm pitch, vertical",PinHeader_1x04_P2.54mm_Vertical,Thru-Hole,
|
||||
13,"A1,A2",4,Sullins,PPTC081LFBN-RC,"Female pin header 1 x 8, 2.54 mm pitch, 8.5 mm tall",Pololu_Breakout-16_15.2x20.3mm,Thru-Hole,2 headers per driver (4 total). Sockets only: the DRV8825 modules are supplied by me
|
||||
14,"D1,D2",2,,SS34,"Schottky diode 40 V 3 A, SMA",D_SMA,SMD,Any brand
|
||||
15,D3,1,,B5819W,"Schottky diode 40 V 1 A, SOD-123",D_SOD-123,SMD,Any brand
|
||||
16,"D6,D7",2,,BAT54WS,"Schottky diode 30 V 0.2 A, SOD-323",D_SOD-323,SMD,Any brand
|
||||
17,D4,1,,,"LED green, 0603",LED_0603_1608Metric,SMD,Any brand
|
||||
18,D5,1,,,"LED blue, 0603",LED_0603_1608Metric,SMD,Any brand
|
||||
19,"Q1,Q2",2,onsemi,MMBT3904LT1G,"NPN transistor 40 V 200 mA, SOT-23",SOT-23,SMD,Any MMBT3904 ok
|
||||
20,"SW1,SW2",2,Omron,B3U-1000P,"Tactile switch, SMD, 3 x 2.5 mm",SW_SPST_B3U-1000P,SMD,
|
||||
21,"C1,C2",2,Panasonic,EEE-FK1V101GP,"Aluminium electrolytic capacitor 100 uF 35 V, SMD 8 x 10.2 mm",CP_Elec_8x10,SMD,
|
||||
22,"C3,C4",2,,,"Ceramic capacitor 10 uF 25 V X5R, 1206",C_1206_3216Metric,SMD,Any brand
|
||||
23,"C16,C17",2,,,"Ceramic capacitor 10 uF 50 V X7R, 1206",C_1206_3216Metric,SMD,Any brand
|
||||
24,"C5,C9",2,,,"Ceramic capacitor 10 uF 16 V X5R, 0805",C_0805_2012Metric,SMD,Any brand
|
||||
25,C6,1,,,"Ceramic capacitor 22 uF 10 V X5R, 0805",C_0805_2012Metric,SMD,Any brand
|
||||
26,"C7,C10,C13,C14,C15",5,,,"Ceramic capacitor 100 nF 50 V X7R, 0603",C_0603_1608Metric,SMD,Any brand
|
||||
27,C8,1,,,"Ceramic capacitor 1 uF 16 V X7R, 0603",C_0603_1608Metric,SMD,Any brand
|
||||
28,"R1,R2",2,,,"Resistor 5.1 k 1%, 0603",R_0603_1608Metric,SMD,Any brand
|
||||
29,"R3,R4,R5,R6,R8,R9,R11",7,,,"Resistor 10 k 1%, 0603",R_0603_1608Metric,SMD,Any brand
|
||||
30,"R10,R12,R14,R15",4,,,"Resistor 1 k 1%, 0603",R_0603_1608Metric,SMD,Any brand
|
||||
31,R13,1,,,"Resistor 330 1%, 0603",R_0603_1608Metric,SMD,Any brand
|
||||
|
@@ -0,0 +1,58 @@
|
||||
Designator,Mid X (mm),Mid Y (mm),Layer,Rotation,Value,Footprint
|
||||
A1,38.100,61.440,Top,270,DRV8825 StepStick (theta/spin),Pololu_Breakout-16_15.2x20.3mm
|
||||
A2,59.100,61.440,Top,270,DRV8825 StepStick (rho/arm),Pololu_Breakout-16_15.2x20.3mm
|
||||
C1,24.000,42.340,Top,0,100uF 25V,CP_Elec_8x10
|
||||
C2,45.500,42.340,Top,0,100uF 25V,CP_Elec_8x10
|
||||
C3,20.500,23.740,Top,0,10uF 25V,C_1206_3216Metric
|
||||
C4,26.000,23.740,Top,0,10uF 16V,C_1206_3216Metric
|
||||
C5,59.600,19.340,Top,90,10uF,C_0805_2012Metric
|
||||
C6,59.600,23.840,Top,90,22uF,C_0805_2012Metric
|
||||
C7,27.000,20.340,Top,90,100nF,C_0603_1608Metric
|
||||
C8,54.600,10.640,Top,0,1uF,C_0603_1608Metric
|
||||
C9,57.500,8.740,Top,90,10uF,C_0805_2012Metric
|
||||
C10,55.000,8.340,Top,90,100nF,C_0603_1608Metric
|
||||
C13,61.000,44.840,Top,0,100nF,C_0603_1608Metric
|
||||
C14,61.000,42.640,Top,0,100nF,C_0603_1608Metric
|
||||
C15,61.000,33.340,Top,90,100nF,C_0603_1608Metric
|
||||
C16,21.800,57.940,Top,0,10uF 50V,C_1206_3216Metric
|
||||
C17,42.800,57.940,Top,0,10uF 50V,C_1206_3216Metric
|
||||
D1,16.000,41.340,Top,90,SS34,D_SMA
|
||||
D2,28.870,30.340,Top,90,SS34,D_SMA
|
||||
D3,13.000,23.540,Top,0,B5819W,D_SOD-123
|
||||
D4,34.000,33.340,Top,0,GREEN,LED_0603_1608Metric
|
||||
D5,38.000,33.340,Top,0,BLUE,LED_0603_1608Metric
|
||||
D6,64.500,45.340,Top,0,BAT54WS,D_SOD-323
|
||||
D7,64.500,47.490,Top,0,BAT54WS,D_SOD-323
|
||||
F1,12.600,47.140,Top,0,2.5A PTC,Fuse_1812_4532Metric
|
||||
J1,12.300,38.340,Top,270,12V DC 5.5x2.1,BarrelJack_CUI_PJ-063AH_Horizontal
|
||||
J2,3.400,19.340,Top,270,USB-C,USB_C_Receptacle_HRO_TYPE-C-31-M-12
|
||||
J3,4.500,59.940,Top,270,SPIN MOTOR,JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical
|
||||
J4,76.500,59.940,Top,270,ARM MOTOR,JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical
|
||||
J5,76.500,44.840,Top,270,HOME SW,JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical
|
||||
J6,76.500,33.740,Top,270,HALL,JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical
|
||||
J7,76.500,22.640,Top,270,LED STRIP,JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical
|
||||
J8,77.500,11.340,Top,0,EXPANSION,PinHeader_1x04_P2.54mm_Vertical
|
||||
PS1,18.000,28.340,Top,0,R-78E5.0-1.0,Converter_DCDC_RECOM_R-78E-0.5_THT
|
||||
Q1,28.000,16.140,Top,0,MMBT3904,SOT-23
|
||||
Q2,28.000,12.140,Top,0,MMBT3904,SOT-23
|
||||
R1,11.500,20.590,Top,0,5.1k,R_0603_1608Metric
|
||||
R2,11.500,17.590,Top,0,5.1k,R_0603_1608Metric
|
||||
R3,31.000,18.340,Top,90,10k,R_0603_1608Metric
|
||||
R4,31.000,13.840,Top,90,10k,R_0603_1608Metric
|
||||
R5,54.600,12.340,Top,0,10k,R_0603_1608Metric
|
||||
R6,31.000,25.840,Top,0,10k,R_0603_1608Metric
|
||||
R8,47.000,32.340,Top,0,10k,R_0603_1608Metric
|
||||
R9,54.000,44.840,Top,0,10k,R_0603_1608Metric
|
||||
R10,57.500,44.840,Top,0,1k,R_0603_1608Metric
|
||||
R11,54.000,42.640,Top,0,10k,R_0603_1608Metric
|
||||
R12,57.500,42.640,Top,0,1k,R_0603_1608Metric
|
||||
R13,70.000,33.340,Top,90,330,R_0603_1608Metric
|
||||
R14,34.000,30.840,Top,0,1k,R_0603_1608Metric
|
||||
R15,38.000,30.840,Top,0,1k,R_0603_1608Metric
|
||||
SW1,27.500,8.340,Top,0,RESET,SW_SPST_B3U-1000P
|
||||
SW2,22.000,8.340,Top,0,BOOT,SW_SPST_B3U-1000P
|
||||
U1,43.000,12.750,Top,180,ESP32-WROOM-32E-N8,ESP32-WROOM-32E
|
||||
U2,22.000,16.340,Top,0,CH340C,SOIC-16_3.9x9.9mm_P1.27mm
|
||||
U3,66.000,21.840,Top,0,AP7361C-33E,SOT-223-3_TabPin2
|
||||
U4,65.500,33.340,Top,0,74AHCT1G125,SOT-23-5
|
||||
U5,13.000,13.840,Top,0,USBLC6-2SC6,SOT-23-6
|
||||
|
@@ -0,0 +1,53 @@
|
||||
# PCBWay order: SandTable Controller v0.4
|
||||
|
||||
## Files
|
||||
|
||||
| File | Where it goes |
|
||||
|---|---|
|
||||
| `1-gerbers-sandtable-controller-v0.4.zip` | PCB order: **Upload Gerber file** |
|
||||
| `2-bom-sandtable-controller-v0.4.csv` | Assembly order: **BOM file** |
|
||||
| `3-centroid-sandtable-controller-v0.4.csv` | Assembly order: **Centroid / pick-and-place file** |
|
||||
| `assembly-top-v0.4.pdf` | Attach if PCBWay asks for an assembly drawing |
|
||||
| `schematic-v0.4.pdf` | Attach if PCBWay asks for a schematic |
|
||||
|
||||
## PCB settings
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| Board type | Single pieces |
|
||||
| Size | 80 x 63.34 mm |
|
||||
| Quantity | 5 (the minimum) |
|
||||
| Layers | 2 |
|
||||
| Material | FR-4, TG130-140 |
|
||||
| Thickness | 1.6 mm |
|
||||
| Min track / spacing | 6/6 mil |
|
||||
| Min hole size | 0.3 mm |
|
||||
| Solder mask / silkscreen | any colour / white |
|
||||
| Surface finish | HASL lead-free (or ENIG) |
|
||||
| Via process | Tenting vias |
|
||||
| Finished copper | 1 oz |
|
||||
| Product No. | Specify a location (the back of the board has a "WayWayWay" marker for it) |
|
||||
|
||||
## Assembly settings (PCB Assembly quote)
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| Service | Turnkey (PCBWay buys the parts) |
|
||||
| Assembly side | Top side only |
|
||||
| Quantity | 1 or 2 (fewer than the 5 bare boards is fine) |
|
||||
| Unique parts | 31 |
|
||||
| SMD parts | 47 |
|
||||
| BGA / QFP parts | 0 |
|
||||
| Through-hole parts | 12 (10 designators; each driver socket is 2 headers) |
|
||||
| Accept alternatives | Yes, for lines marked "Any brand" |
|
||||
|
||||
**Note for PCBWay** (paste into the comments box):
|
||||
|
||||
> Please fit the female header sockets for A1 and A2 only. Do not supply the stepper driver modules; I will plug in my own. BOM lines marked "Any brand" may use any equivalent part to the description. U3 (AP7361C-33E) has a different pinout from AMS1117-type regulators, so please do not substitute it. Leave solder jumper JP1 open. C16 and C17 sit between the driver socket strips, under where the modules plug in; that is intended. Centroid coordinates are in mm, with the origin at the board's bottom-left corner.
|
||||
|
||||
## When PCBWay sends their review
|
||||
- Check the polarity of C1, C2 (electrolytic), D1-D3, D6, D7 (diodes) and D4, D5 (LEDs) against the + and bar marks in `assembly-top-v0.4.pdf`.
|
||||
- Check that the ESP32 (U1) antenna end sits at the board edge, over the copper-free area.
|
||||
- Check that pin 1 of U2, U3, U4 and U5 is at the dot or triangle on the silkscreen.
|
||||
- U3 is an AP7361C-33E, not an AMS1117: its pinout is different (pin 1 IN, pin 2 GND/tab, pin 3 OUT), so it must not be swapped for a 1117-type regulator.
|
||||
- JP1 is a solder jumper (bare pads), not a part: leave it open.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 827 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 765 KiB |
Binary file not shown.
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,357 @@
|
||||
"""Board model: placed footprints, absolute pad geometry, KiCad file writers."""
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import uuid as uuidlib
|
||||
|
||||
import design as D
|
||||
from kilib import load_footprint, fp_pads, fp_courtyard
|
||||
from sexpr import QStr, dumps, find, findall
|
||||
|
||||
OX, OY = D.ORIGIN
|
||||
|
||||
# Driver sockets: only the two female header strips sit on the board. The plugged-in module is
|
||||
# about 8.5 mm up, so low parts (the VMOT ceramics) may go between the strips.
|
||||
POLOLU = 'Module:Pololu_Breakout-16_15.2x20.3mm'
|
||||
POLOLU_COURT = [(-1.53, -1.52, 1.53, 19.3), (11.17, -1.52, 14.21, 19.3)]
|
||||
|
||||
|
||||
def uid(*parts):
|
||||
"""Deterministic UUID so regenerated files diff cleanly."""
|
||||
return str(uuidlib.uuid5(uuidlib.NAMESPACE_URL, 'sandtable-pcb/' + '/'.join(map(str, parts))))
|
||||
|
||||
|
||||
def rot_pt(x, y, deg):
|
||||
"""KiCad footprint transform (y down, positive = CCW on screen)."""
|
||||
t = math.radians(deg)
|
||||
c, s = round(math.cos(t), 12), round(math.sin(t), 12)
|
||||
return (x * c + y * s, -x * s + y * c)
|
||||
|
||||
|
||||
class Pad:
|
||||
__slots__ = ('ref', 'num', 'net', 'x', 'y', 'hx', 'hy', 'shape', 'layers', 'drill', 'kind')
|
||||
|
||||
def dist(self, px, py):
|
||||
"""Distance from point to pad copper (0 inside)."""
|
||||
dx = abs(px - self.x)
|
||||
dy = abs(py - self.y)
|
||||
if self.shape == 'circle':
|
||||
return max(0.0, math.hypot(dx, dy) - self.hx)
|
||||
if self.shape == 'oval':
|
||||
if self.hx >= self.hy:
|
||||
r = self.hy
|
||||
ex = max(0.0, dx - (self.hx - r))
|
||||
return max(0.0, math.hypot(ex, dy) - r)
|
||||
r = self.hx
|
||||
ey = max(0.0, dy - (self.hy - r))
|
||||
return max(0.0, math.hypot(dx, ey) - r)
|
||||
ex = max(0.0, dx - self.hx)
|
||||
ey = max(0.0, dy - self.hy)
|
||||
return math.hypot(ex, ey)
|
||||
|
||||
|
||||
class Part:
|
||||
def __init__(self, ref, spec):
|
||||
self.ref = ref
|
||||
(self.sym, self.fp_id, self.value, self.x, self.y, self.rot,
|
||||
self.lcsc, self.descr) = spec
|
||||
self.fp = load_footprint(self.fp_id, min_drill=D.MIN_DRILL, min_ring=D.MIN_RING, solid_thermal_pads=True)
|
||||
self.raw_pads = fp_pads(self.fp)
|
||||
self.pin_net = {}
|
||||
|
||||
def court_rects(self):
|
||||
"""Courtyard as axis-aligned rects in board coords."""
|
||||
if self.fp_id == 'RF_Module:ESP32-WROOM-32E':
|
||||
local = [(-9.75, -6.31, 9.75, 13.54), (-24.25, -28.0, 24.25, -6.31)]
|
||||
elif self.fp_id == POLOLU:
|
||||
local = POLOLU_COURT
|
||||
else:
|
||||
c = fp_courtyard(self.fp)
|
||||
local = [c] if c else []
|
||||
out = []
|
||||
for (x0, y0, x1, y1) in local:
|
||||
pts = [rot_pt(x, y, self.rot) for x, y in ((x0, y0), (x1, y0), (x0, y1), (x1, y1))]
|
||||
xs = [p[0] + self.x for p in pts]
|
||||
ys = [p[1] + self.y for p in pts]
|
||||
out.append((min(xs), min(ys), max(xs), max(ys)))
|
||||
return out
|
||||
|
||||
def pads(self):
|
||||
out = []
|
||||
for p in self.raw_pads:
|
||||
if p['kind'] == 'connect':
|
||||
continue
|
||||
px, py = rot_pt(p['x'], p['y'], self.rot)
|
||||
ang = (p['rot'] + self.rot) % 180
|
||||
w, h = p['w'], p['h']
|
||||
if abs(ang - 90) < 1:
|
||||
w, h = h, w
|
||||
pad = Pad()
|
||||
pad.ref = self.ref
|
||||
pad.num = p['num']
|
||||
pad.net = self.pin_net.get(p['num'])
|
||||
pad.x = self.x + px
|
||||
pad.y = self.y + py
|
||||
pad.hx, pad.hy = w / 2, h / 2
|
||||
shape = p['shape']
|
||||
pad.shape = 'circle' if shape == 'circle' else ('oval' if shape == 'oval' else 'rect')
|
||||
if pad.shape == 'circle':
|
||||
pad.hx = pad.hy = w / 2
|
||||
pad.kind = p['kind']
|
||||
if p['kind'] == 'np_thru_hole':
|
||||
pad.layers = ()
|
||||
elif any(l in ('*.Cu', 'F&B.Cu') for l in p['layers']):
|
||||
pad.layers = ('F.Cu', 'B.Cu')
|
||||
else:
|
||||
pad.layers = tuple(l for l in p['layers'] if l in ('F.Cu', 'B.Cu'))
|
||||
pad.drill = p['drill']
|
||||
out.append(pad)
|
||||
return out
|
||||
|
||||
|
||||
def build():
|
||||
parts = {ref: Part(ref, spec) for ref, spec in D.PARTS.items()}
|
||||
for net, pins in D.NETS.items():
|
||||
for pin in pins:
|
||||
ref, num = pin.split('.', 1)
|
||||
if ref not in parts:
|
||||
raise KeyError(f'net {net}: unknown part {ref}')
|
||||
nums = {p['num'] for p in parts[ref].raw_pads}
|
||||
if num not in nums:
|
||||
raise KeyError(f'net {net}: {ref} has no pad {num}')
|
||||
if num in parts[ref].pin_net:
|
||||
raise ValueError(f'{pin} in two nets')
|
||||
parts[ref].pin_net[num] = net
|
||||
return parts
|
||||
|
||||
|
||||
def check_placement(parts):
|
||||
problems = []
|
||||
rects = []
|
||||
for p in parts.values():
|
||||
for r in p.court_rects():
|
||||
rects.append((p.ref, r))
|
||||
for i in range(len(rects)):
|
||||
for j in range(i + 1, len(rects)):
|
||||
a, ra = rects[i]
|
||||
b, rb = rects[j]
|
||||
if a == b:
|
||||
continue
|
||||
ox = min(ra[2], rb[2]) - max(ra[0], rb[0])
|
||||
oy = min(ra[3], rb[3]) - max(ra[1], rb[1])
|
||||
if ox > 0.001 and oy > 0.001:
|
||||
problems.append(f'courtyard overlap {a} / {b}: {ox:.2f} x {oy:.2f}')
|
||||
for p in parts.values():
|
||||
for pad in p.pads():
|
||||
if pad.layers and (pad.x - pad.hx < 0.3 or pad.y - pad.hy < 0.3 or
|
||||
pad.x + pad.hx > D.BOARD_W - 0.3 or pad.y + pad.hy > D.BOARD_H - 0.3):
|
||||
problems.append(f'pad near/outside edge {p.ref}.{pad.num} at ({pad.x:.2f},{pad.y:.2f})')
|
||||
return problems
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- writers
|
||||
def pcb_name(net):
|
||||
"""KiCad names nets from local labels with the sheet path prefix."""
|
||||
return net if net.startswith('unconnected-') else '/' + net
|
||||
|
||||
|
||||
def nc_nets():
|
||||
"""Single-pad nets KiCad creates for unconnected symbol pins: (ref, pad) -> name."""
|
||||
import sch
|
||||
from kilib import sym_pins
|
||||
connected = {p for pins in D.NETS.values() for p in pins}
|
||||
out = {}
|
||||
for ref, spec in D.PARTS.items():
|
||||
full, sym, _ = sch.symbol_def(spec[0])
|
||||
for pin in sym_pins(sym):
|
||||
key = f'{ref}.{pin["num"]}'
|
||||
if key not in connected:
|
||||
out[(ref, pin['num'])] = f'unconnected-({ref}-{pin["name"]}-Pad{pin["num"]})'
|
||||
return out
|
||||
|
||||
|
||||
def net_list():
|
||||
names = sorted(D.NETS.keys(), key=lambda n: (n != 'GND', n))
|
||||
ids = {n: i + 1 for i, n in enumerate(names)}
|
||||
for name in sorted(set(nc_nets().values())):
|
||||
ids[name] = len(ids) + 1
|
||||
return ids
|
||||
|
||||
|
||||
NC_NETS = {}
|
||||
|
||||
|
||||
def footprint_node(part, net_ids, sheet_file):
|
||||
fp = copy.deepcopy(part.fp)
|
||||
keep = [x for x in fp[2:] if not (isinstance(x, list) and x and x[0] in
|
||||
('version', 'generator', 'generator_version', 'layer', 'zone'))]
|
||||
if part.fp_id == POLOLU:
|
||||
def on_crtyd(x):
|
||||
lay = find(x, 'layer') if isinstance(x, list) else None
|
||||
return lay is not None and str(lay[1]) == 'F.CrtYd'
|
||||
keep = [x for x in keep if not (isinstance(x, list) and x and x[0] in ('fp_line', 'fp_rect') and on_crtyd(x))]
|
||||
for i, (x0, y0, x1, y1) in enumerate(POLOLU_COURT):
|
||||
keep.append(['fp_rect', ['start', fmt(x0), fmt(y0)], ['end', fmt(x1), fmt(y1)],
|
||||
['stroke', ['width', '0.05'], ['type', 'solid']], ['fill', 'no'],
|
||||
['layer', QStr('F.CrtYd')], ['uuid', QStr(uid('crtyd', part.ref, i))]])
|
||||
node = ['footprint', QStr(part.fp_id), ['layer', QStr('F.Cu')], ['uuid', QStr(uid('fp', part.ref))],
|
||||
['at', fmt(part.x + OX), fmt(part.y + OY)] + ([fmt(part.rot)] if part.rot else [])]
|
||||
for x in keep:
|
||||
if isinstance(x, list) and x and x[0] == 'property':
|
||||
name = str(x[1])
|
||||
if name == 'Reference':
|
||||
x[2] = QStr(part.ref)
|
||||
elif name == 'Value':
|
||||
x[2] = QStr(part.value)
|
||||
small = part.fp_id.split(':')[0] in ('Resistor_SMD', 'LED_SMD', 'Button_Switch_SMD') or \
|
||||
(part.fp_id.startswith('Capacitor_SMD:C_')) or part.fp_id.endswith(('SOT-23', 'SOT-23-5', 'D_SOD-123'))
|
||||
if name == 'Reference' and small:
|
||||
eff = find(x, 'effects')
|
||||
fnt = find(eff, 'font') if eff else None
|
||||
if fnt is not None:
|
||||
sz, th = find(fnt, 'size'), find(fnt, 'thickness')
|
||||
if sz is not None:
|
||||
sz[1:] = ['0.7', '0.7']
|
||||
if th is not None:
|
||||
th[1] = '0.1'
|
||||
at = find(x, 'at')
|
||||
if at is not None and name == 'Reference' and part.ref in getattr(D, 'REF_POS', {}) and not part.rot:
|
||||
at[1], at[2] = fmt(D.REF_POS[part.ref][0]), fmt(D.REF_POS[part.ref][1])
|
||||
if at is not None:
|
||||
# absolute text angle: keep text readable (0 or 90)
|
||||
while len(at) < 4:
|
||||
at.append('0')
|
||||
at[3] = '0' if name in ('Reference', 'Value') else fmt(part.rot)
|
||||
if find(x, 'uuid') is None:
|
||||
x.insert(3, ['uuid', QStr(uid('prop', part.ref, name))])
|
||||
if isinstance(x, list) and x and x[0] == 'pad':
|
||||
at = find(x, 'at')
|
||||
local = float(at[3]) if len(at) > 3 else 0.0
|
||||
ang = (local + part.rot) % 360
|
||||
del at[3:]
|
||||
if ang:
|
||||
at.append(fmt(ang))
|
||||
num = str(x[1])
|
||||
net = part.pin_net.get(num) or NC_NETS.get((part.ref, num))
|
||||
if net and x[2] != 'np_thru_hole':
|
||||
x.append(['net', str(net_ids[net]), QStr(pcb_name(net))])
|
||||
x.append(['uuid', QStr(uid('pad', part.ref, num, len(x)))])
|
||||
node.append(x)
|
||||
# extra properties
|
||||
node.insert(5, ['property', QStr('LCSC'), QStr(part.lcsc), ['at', '0', '0', '0'], ['layer', QStr('F.Fab')],
|
||||
['hide', 'yes'], ['uuid', QStr(uid('prop', part.ref, 'LCSC'))],
|
||||
['effects', ['font', ['size', '1', '1'], ['thickness', '0.15']]]])
|
||||
node.append(['path', QStr('/' + uid('sym', part.ref))])
|
||||
node.append(['sheetname', QStr('/')])
|
||||
node.append(['sheetfile', QStr(sheet_file)])
|
||||
return node
|
||||
|
||||
|
||||
def fmt(v):
|
||||
s = f'{float(v):.4f}'.rstrip('0').rstrip('.')
|
||||
return '0' if s in ('-0', '') else s
|
||||
|
||||
|
||||
HEADER_LAYERS = ['layers',
|
||||
['0', QStr('F.Cu'), 'signal'], ['2', QStr('B.Cu'), 'signal'],
|
||||
['9', QStr('F.Adhes'), 'user', QStr('F.Adhesive')], ['11', QStr('B.Adhes'), 'user', QStr('B.Adhesive')],
|
||||
['13', QStr('F.Paste'), 'user'], ['15', QStr('B.Paste'), 'user'],
|
||||
['5', QStr('F.SilkS'), 'user', QStr('F.Silkscreen')], ['7', QStr('B.SilkS'), 'user', QStr('B.Silkscreen')],
|
||||
['1', QStr('F.Mask'), 'user'], ['3', QStr('B.Mask'), 'user'],
|
||||
['17', QStr('Dwgs.User'), 'user', QStr('User.Drawings')], ['19', QStr('Cmts.User'), 'user', QStr('User.Comments')],
|
||||
['21', QStr('Eco1.User'), 'user', QStr('User.Eco1')], ['23', QStr('Eco2.User'), 'user', QStr('User.Eco2')],
|
||||
['25', QStr('Edge.Cuts'), 'user'], ['27', QStr('Margin'), 'user'],
|
||||
['31', QStr('F.CrtYd'), 'user', QStr('F.Courtyard')], ['29', QStr('B.CrtYd'), 'user', QStr('B.Courtyard')],
|
||||
['35', QStr('F.Fab'), 'user'], ['33', QStr('B.Fab'), 'user']]
|
||||
|
||||
|
||||
def text_node(txt, x, y, layer, size=1.2, angle=0, bold=False, justify=None):
|
||||
eff = ['effects', ['font', ['size', fmt(size), fmt(size)], ['thickness', fmt(size * 0.15)]] + ([['bold', 'yes']] if bold else [])]
|
||||
if justify:
|
||||
eff.append(['justify'] + justify)
|
||||
if layer.startswith('B.'):
|
||||
eff.append(['justify', 'mirror'])
|
||||
return ['gr_text', QStr(txt), ['at', fmt(x + OX), fmt(y + OY), fmt(angle)], ['layer', QStr(layer)],
|
||||
['uuid', QStr(uid('txt', txt, x, y, layer))], eff]
|
||||
|
||||
|
||||
def write_pcb(path, parts, tracks=(), vias=(), zones=(), texts=(), sheet_file='sandtable-controller.kicad_sch'):
|
||||
NC_NETS.clear()
|
||||
NC_NETS.update(nc_nets())
|
||||
net_ids = net_list()
|
||||
root = ['kicad_pcb', ['version', '20241229'], ['generator', QStr('pcbnew')], ['generator_version', QStr('9.0')],
|
||||
['general', ['thickness', '1.6'], ['legacy_teardrops', 'no']], ['paper', QStr('A4')],
|
||||
['title_block', ['title', QStr('SandTable Controller')], ['date', QStr('2026-10-22')], ['rev', QStr(D.VERSION)],
|
||||
['company', QStr('lazypugs')], ['comment', '1', QStr('ESP32 + 2x DRV8825 StepStick, drop-in for the Uno bay')]],
|
||||
HEADER_LAYERS,
|
||||
['setup', ['pad_to_mask_clearance', '0'], ['allow_soldermask_bridges_in_footprints', 'no'],
|
||||
['tenting', 'front', 'back'], ['aux_axis_origin', fmt(OX), fmt(OY + D.BOARD_H)],
|
||||
['pcbplotparams', ['layerselection', '0x00000000_00000000_55555555_5755f5ff'],
|
||||
['plot_on_all_layers_selection', '0x00000000_00000000_00000000_00000000'],
|
||||
['disableapertmacros', 'no'], ['usegerberextensions', 'no'], ['usegerberattributes', 'yes'],
|
||||
['usegerberadvancedattributes', 'yes'], ['creategerberjobfile', 'yes'], ['svgprecision', '4'],
|
||||
['plotframeref', 'no'], ['mode', '1'], ['useauxorigin', 'no'], ['outputformat', '1'],
|
||||
['mirror', 'no'], ['drillshape', '1'], ['scaleselection', '1'], ['outputdirectory', QStr('')]]],
|
||||
['net', '0', QStr('')]]
|
||||
for name, i in sorted(net_ids.items(), key=lambda kv: kv[1]):
|
||||
root.append(['net', str(i), QStr(pcb_name(name))])
|
||||
for ref in sorted(parts):
|
||||
root.append(footprint_node(parts[ref], net_ids, sheet_file))
|
||||
# board outline
|
||||
root.append(['gr_rect', ['start', fmt(OX), fmt(OY)], ['end', fmt(OX + D.BOARD_W), fmt(OY + D.BOARD_H)],
|
||||
['stroke', ['width', '0.05'], ['type', 'default']], ['fill', 'no'], ['layer', QStr('Edge.Cuts')],
|
||||
['uuid', QStr(uid('edge'))]])
|
||||
root.extend(texts)
|
||||
for (x1, y1, x2, y2, w, layer, net) in tracks:
|
||||
root.append(['segment', ['start', fmt(x1 + OX), fmt(y1 + OY)], ['end', fmt(x2 + OX), fmt(y2 + OY)],
|
||||
['width', fmt(w)], ['layer', QStr(layer)], ['net', str(net_ids[net])],
|
||||
['uuid', QStr(uid('seg', x1, y1, x2, y2, layer, net))]])
|
||||
for (x, y, net) in vias:
|
||||
root.append(['via', ['at', fmt(x + OX), fmt(y + OY)], ['size', fmt(D.VIA[0])], ['drill', fmt(D.VIA[1])],
|
||||
['layers', QStr('F.Cu'), QStr('B.Cu')], ['net', str(net_ids[net])],
|
||||
['uuid', QStr(uid('via', x, y, net))]])
|
||||
root.extend(zones)
|
||||
with open(path, 'w') as f:
|
||||
f.write(dumps(root) + '\n')
|
||||
return net_ids
|
||||
|
||||
|
||||
def keepout_zone(name, x0, y0, x1, y1):
|
||||
return ['zone', ['net', '0'], ['net_name', QStr('')], ['layers', QStr('F.Cu'), QStr('B.Cu')],
|
||||
['uuid', QStr(uid('zone', name))], ['name', QStr(name)], ['hatch', 'edge', '0.5'],
|
||||
['connect_pads', ['clearance', '0']], ['min_thickness', '0.25'], ['filled_areas_thickness', 'no'],
|
||||
['keepout', ['tracks', 'not_allowed'], ['vias', 'not_allowed'], ['pads', 'not_allowed'],
|
||||
['copperpour', 'not_allowed'], ['footprints', 'allowed']],
|
||||
['placement', ['enabled', 'no'], ['sheetname', QStr('')]],
|
||||
['fill', ['thermal_gap', '0.5'], ['thermal_bridge_width', '0.5']],
|
||||
['polygon', ['pts'] + [['xy', fmt(x + OX), fmt(y + OY)] for x, y in
|
||||
((x0, y0), (x1, y0), (x1, y1), (x0, y1))]]]
|
||||
|
||||
|
||||
def write_project(path, name):
|
||||
tmpl = '/snap/kicad/current/usr/share/kicad/template/API_Series-500/API_Series-500.kicad_pro'
|
||||
with open(tmpl) as f:
|
||||
pro = json.load(f)
|
||||
pro['meta']['filename'] = name + '.kicad_pro'
|
||||
base = pro['net_settings']['classes'][0]
|
||||
classes = []
|
||||
for cname, (w, clr) in D.NETCLASS.items():
|
||||
c = dict(base)
|
||||
c.update(name=cname, track_width=w, clearance=clr, via_diameter=D.VIA[0], via_drill=D.VIA[1],
|
||||
priority=2147483647 if cname == 'Default' else 0)
|
||||
classes.append(c)
|
||||
pro['net_settings']['classes'] = classes
|
||||
pro['net_settings']['netclass_patterns'] = [{'netclass': c, 'pattern': pcb_name(n)} for n, c in D.NET_TO_CLASS.items()]
|
||||
rules = pro['board']['design_settings']['rules']
|
||||
# PCBWay standard process limits
|
||||
rules.update(min_clearance=0.16, min_track_width=0.16, min_via_diameter=0.6, min_through_hole_diameter=D.MIN_DRILL,
|
||||
min_copper_edge_clearance=0.3, min_hole_clearance=0.2, min_hole_to_hole=0.25,
|
||||
min_via_annular_width=0.15, min_silk_clearance=0.0)
|
||||
sev = pro['board']['design_settings']['rule_severities']
|
||||
sev.update({'silk_over_copper': 'ignore', 'silk_overlap': 'ignore', 'silk_edge_clearance': 'ignore',
|
||||
'lib_footprint_issues': 'ignore', 'lib_footprint_mismatch': 'ignore', 'text_height': 'ignore'})
|
||||
pro.pop('boards', None)
|
||||
pro['sheets'] = [[uid('rootsheet'), 'Root']]
|
||||
with open(path, 'w') as f:
|
||||
json.dump(pro, f, indent=2)
|
||||
@@ -0,0 +1,203 @@
|
||||
"""SandTable Controller (version in VERSION): single source of truth for parts, placement and nets.
|
||||
|
||||
Board coordinates are mm, y down, origin at the board's top-left corner.
|
||||
x = 0 is the edge that faces the table wall (power + USB); x = 80 faces the
|
||||
table centre (motor/sensor connectors). The 4 mounting holes reuse the
|
||||
Arduino Uno hole pattern so the board drops onto the existing deck standoffs.
|
||||
"""
|
||||
|
||||
VERSION = '0.4'
|
||||
BOARD_W = 80.0
|
||||
BOARD_H = 63.34
|
||||
ORIGIN = (100.0, 100.0) # where board (0,0) lands in KiCad coordinates
|
||||
|
||||
# ---------------------------------------------------------------- parts
|
||||
# ref: (symbol lib_id, footprint lib_id, value, x, y, rot, lcsc, description)
|
||||
PARTS = {
|
||||
'U1': ('RF_Module:ESP32-WROOM-32E', 'RF_Module:ESP32-WROOM-32E', 'ESP32-WROOM-32E-N8', 43.0, 50.59, 180, '', 'Wi-Fi MCU module (FluidNC)'),
|
||||
'U2': ('Interface_USB:CH340C', 'Package_SO:SOIC-16_3.9x9.9mm_P1.27mm', 'CH340C', 22.0, 47.0, 0, 'C84681', 'USB-UART bridge'),
|
||||
'U3': ('Regulator_Linear:AP7361C-33E', 'Package_TO_SOT_SMD:SOT-223-3_TabPin2', 'AP7361C-33E', 66.0, 41.5, 0, '', '5V -> 3.3V LDO, 0.36 V dropout at 1 A (pin 1 IN, 2 GND/tab, 3 OUT)'),
|
||||
'U4': ('74xGxx:74AHCT1G125', 'Package_TO_SOT_SMD:SOT-23-5', '74AHCT1G125', 65.5, 30.0, 0, '', '3.3V -> 5V LED data buffer'),
|
||||
'PS1': ('Converter_DCDC:R-78E5.0-1.0', 'Converter_DCDC:Converter_DCDC_RECOM_R-78E-0.5_THT', 'R-78E5.0-1.0', 18.0, 35.0, 0, '', '12V -> 5V 1A switching regulator'),
|
||||
'A1': ('Driver_Motor:Pololu_Breakout_A4988', 'Module:Pololu_Breakout-16_15.2x20.3mm', 'DRV8825 StepStick (theta/spin)', 38.1, 1.9, 270, '', 'Stepper driver socket (DRV8825, 1/32 step)'),
|
||||
'A2': ('Driver_Motor:Pololu_Breakout_A4988', 'Module:Pololu_Breakout-16_15.2x20.3mm', 'DRV8825 StepStick (rho/arm)', 59.1, 1.9, 270, '', 'Stepper driver socket (DRV8825, 1/32 step)'),
|
||||
'J1': ('Connector:Barrel_Jack', 'Connector_BarrelJack:BarrelJack_CUI_PJ-063AH_Horizontal', '12V DC 5.5x2.1', 12.3, 25.0, 270, '', 'Power input'),
|
||||
'J2': ('Connector:USB_C_Receptacle_USB2.0_16P', 'Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12', 'USB-C', 3.4, 44.0, 270, 'C165948', 'Programming / serial'),
|
||||
'J3': ('Connector_Generic:Conn_01x04', 'Connector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical', 'SPIN MOTOR', 4.5, 3.4, 270, 'C158012', 'Theta motor (XH-4, matches motor cable)'),
|
||||
'J4': ('Connector_Generic:Conn_01x04', 'Connector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical', 'ARM MOTOR', 76.5, 3.4, 270, 'C158012', 'Rho motor via slip ring'),
|
||||
'J5': ('Connector_Generic:Conn_01x03', 'Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical', 'HOME SW', 76.5, 18.5, 270, 'C144394', 'Rho home switch (via slip ring)'),
|
||||
'J6': ('Connector_Generic:Conn_01x03', 'Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical', 'HALL', 76.5, 29.6, 270, 'C144394', 'Optional theta hall sensor (5V)'),
|
||||
'J7': ('Connector_Generic:Conn_01x03', 'Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical', 'LED STRIP', 76.5, 40.7, 270, 'C144394', 'WS2812B strip (5V, small strips only)'),
|
||||
'J8': ('Connector_Generic:Conn_01x04', 'Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical', 'EXPANSION', 77.5, 52.0, 0, '', '3V3 / IO32 / IO25 / GND'),
|
||||
'D1': ('Device:D_Schottky', 'Diode_SMD:D_SMA', 'SS34', 16.0, 22.0, 90, 'C8678', 'Reverse-polarity protection'),
|
||||
'D2': ('Device:D_Schottky', 'Diode_SMD:D_SMA', 'SS34', 28.87, 33.0, 90, 'C8678', '5V OR-ing (regulator, 1 A)'),
|
||||
'D3': ('Device:D_Schottky', 'Diode_SMD:D_SOD-123', 'B5819W', 13.0, 39.8, 0, 'C8598', '5V OR-ing (USB)'),
|
||||
'D4': ('Device:LED', 'LED_SMD:LED_0603_1608Metric', 'GREEN', 34.0, 30.0, 0, 'C72043', 'Power LED'),
|
||||
'D5': ('Device:LED', 'LED_SMD:LED_0603_1608Metric', 'BLUE', 38.0, 30.0, 0, 'C72041', 'Status LED (IO2)'),
|
||||
'Q1': ('Transistor_BJT:MMBT3904', 'Package_TO_SOT_SMD:SOT-23', 'MMBT3904', 28.0, 47.2, 0, 'C20526', 'Auto-reset (EN)'),
|
||||
'Q2': ('Transistor_BJT:MMBT3904', 'Package_TO_SOT_SMD:SOT-23', 'MMBT3904', 28.0, 51.2, 0, 'C20526', 'Auto-reset (IO0)'),
|
||||
'SW1': ('Switch:SW_Push', 'Button_Switch_SMD:SW_SPST_B3U-1000P', 'RESET', 27.5, 55.0, 0, 'C231329', 'EN / reset button'),
|
||||
'SW2': ('Switch:SW_Push', 'Button_Switch_SMD:SW_SPST_B3U-1000P', 'BOOT', 22.0, 55.0, 0, 'C231329', 'IO0 / boot button'),
|
||||
'C1': ('Device:C_Polarized', 'Capacitor_SMD:CP_Elec_8x10', '100uF 25V', 24.0, 21.0, 0, '', 'Theta driver VMOT bulk'),
|
||||
'C2': ('Device:C_Polarized', 'Capacitor_SMD:CP_Elec_8x10', '100uF 25V', 45.5, 21.0, 0, '', 'Rho driver VMOT bulk'),
|
||||
'C3': ('Device:C', 'Capacitor_SMD:C_1206_3216Metric', '10uF 25V', 20.5, 39.6, 0, 'C13585', '12V input decoupling'),
|
||||
'C4': ('Device:C', 'Capacitor_SMD:C_1206_3216Metric', '10uF 16V', 26.0, 39.6, 0, 'C13585', 'Regulator output'),
|
||||
'C5': ('Device:C', 'Capacitor_SMD:C_0805_2012Metric', '10uF', 59.6, 44.0, 90, 'C15850', 'LDO input'),
|
||||
'C6': ('Device:C', 'Capacitor_SMD:C_0805_2012Metric', '22uF', 59.6, 39.5, 90, 'C45783', 'LDO output'),
|
||||
'C7': ('Device:C', 'Capacitor_SMD:C_0603_1608Metric', '100nF', 27.0, 43.0, 90, 'C14663', 'CH340C decoupling'),
|
||||
'C8': ('Device:C', 'Capacitor_SMD:C_0603_1608Metric', '1uF', 54.6, 52.7, 0, 'C15849', 'EN reset delay'),
|
||||
'C9': ('Device:C', 'Capacitor_SMD:C_0805_2012Metric', '10uF', 57.5, 54.6, 90, 'C15850', 'ESP32 bulk decoupling'),
|
||||
'C10': ('Device:C', 'Capacitor_SMD:C_0603_1608Metric', '100nF', 55.0, 55.0, 90, 'C14663', 'ESP32 decoupling'),
|
||||
'C13': ('Device:C', 'Capacitor_SMD:C_0603_1608Metric', '100nF', 61.0, 18.5, 0, 'C14663', 'Home switch filter'),
|
||||
'C14': ('Device:C', 'Capacitor_SMD:C_0603_1608Metric', '100nF', 61.0, 20.7, 0, 'C14663', 'Hall filter'),
|
||||
'C15': ('Device:C', 'Capacitor_SMD:C_0603_1608Metric', '100nF', 61.0, 30.0, 90, 'C14663', 'Buffer decoupling'),
|
||||
'R1': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '5.1k', 11.5, 42.75, 0, 'C23186', 'USB-C CC1 pull-down'),
|
||||
'R2': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '5.1k', 11.5, 45.75, 0, 'C23186', 'USB-C CC2 pull-down'),
|
||||
'R3': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '10k', 31.0, 45.0, 90, 'C25804', 'Q1 base'),
|
||||
'R4': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '10k', 31.0, 49.5, 90, 'C25804', 'Q2 base'),
|
||||
'R5': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '10k', 54.6, 51.0, 0, 'C25804', 'EN pull-up'),
|
||||
'R6': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '10k', 31.0, 37.5, 0, 'C25804', 'IO0 pull-up'),
|
||||
'R8': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '10k', 47.0, 31.0, 0, 'C25804', 'Driver enable pull-up (off at boot)'),
|
||||
'R9': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '10k', 54.0, 18.5, 0, 'C25804', 'Home switch pull-up'),
|
||||
'R10': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '1k', 57.5, 18.5, 0, 'C21190', 'Home switch series'),
|
||||
'R11': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '10k', 54.0, 20.7, 0, 'C25804', 'Hall pull-up'),
|
||||
'R12': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '1k', 57.5, 20.7, 0, 'C21190', 'Hall series'),
|
||||
'R13': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '330', 70.0, 30.0, 90, 'C23138', 'LED data series'),
|
||||
'R14': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '1k', 34.0, 32.5, 0, 'C21190', 'Power LED'),
|
||||
'R15': ('Device:R', 'Resistor_SMD:R_0603_1608Metric', '1k', 38.0, 32.5, 0, 'C21190', 'Status LED'),
|
||||
# v0.4 additions (positions set by tools/place_new.py)
|
||||
'C16': ('Device:C', 'Capacitor_SMD:C_1206_3216Metric', '10uF 50V', 21.8, 5.4, 0, '', 'Theta driver VMOT ceramic, at the socket pins'),
|
||||
'C17': ('Device:C', 'Capacitor_SMD:C_1206_3216Metric', '10uF 50V', 42.8, 5.4, 0, '', 'Rho driver VMOT ceramic, at the socket pins'),
|
||||
'F1': ('Device:Polyfuse', 'Fuse:Fuse_1812_4532Metric', '2.5A PTC', 12.6, 16.2, 0, '', '12V input resettable fuse'),
|
||||
'U5': ('Power_Protection:USBLC6-2SC6', 'Package_TO_SOT_SMD:SOT-23-6', 'USBLC6-2SC6', 13.0, 49.5, 0, '', 'USB data ESD protection'),
|
||||
'D6': ('Device:D_Schottky', 'Diode_SMD:D_SOD-323', 'BAT54WS', 64.5, 18.0, 0, '', 'Home input clamp to 3.3V (5V-safe)'),
|
||||
'D7': ('Device:D_Schottky', 'Diode_SMD:D_SOD-323', 'BAT54WS', 64.5, 15.85, 0, '', 'Hall input clamp to 3.3V (5V-safe)'),
|
||||
'JP1': ('Jumper:SolderJumper_2_Open', 'Jumper:SolderJumper-2_P1.3mm_Open_Pad1.0x1.5mm', 'A4988', 36.0, 8.0, 0, '', 'Driver pin 2 to 3.3V: bridge only for A4988 (VDD); leave open for DRV8825 (FAULT)'),
|
||||
'H1': ('Mechanical:MountingHole', 'MountingHole:MountingHole_3.2mm_M3', 'M3', 15.24, 7.54, 0, '', 'Uno hole'),
|
||||
'H2': ('Mechanical:MountingHole', 'MountingHole:MountingHole_3.2mm_M3', 'M3', 13.97, 55.8, 0, '', 'Uno hole'),
|
||||
'H3': ('Mechanical:MountingHole', 'MountingHole:MountingHole_3.2mm_M3', 'M3', 66.04, 22.78, 0, '', 'Uno hole'),
|
||||
'H4': ('Mechanical:MountingHole', 'MountingHole:MountingHole_3.2mm_M3', 'M3', 66.04, 50.72, 0, '', 'Uno hole'),
|
||||
}
|
||||
|
||||
# Silkscreen reference positions that would otherwise collide (offset from the part origin, mm)
|
||||
REF_POS = {'C16': (3.6, 0.0), 'C17': (3.6, 0.0), 'D6': (2.9, 0.0), 'D7': (2.9, 0.0), 'SW1': (0.0, 2.7), 'H3': (0.0, 4.4)}
|
||||
|
||||
# ---------------------------------------------------------------- nets
|
||||
NETS = {
|
||||
'GND': ['J1.2', 'J2.A1', 'J2.A12', 'J2.B1', 'J2.B12', 'J2.S1', 'C1.2', 'C2.2', 'C3.2', 'C4.2', 'C5.2',
|
||||
'C6.2', 'C7.2', 'C8.2', 'C9.2', 'C10.2', 'C13.2', 'C14.2', 'C15.2', 'PS1.2', 'U3.2', 'U2.1',
|
||||
'C16.2', 'C17.2', 'U5.2',
|
||||
'U1.1', 'U1.15', 'U1.38', 'U1.39', 'A1.1', 'A1.7', 'A2.1', 'A2.7',
|
||||
'J5.3', 'J6.3', 'J7.3', 'J8.4', 'SW1.2', 'SW2.2', 'R1.2', 'R2.2', 'D4.1', 'D5.1', 'U4.1', 'U4.3'],
|
||||
'VIN_RAW': ['J1.1', 'F1.1'],
|
||||
'VIN_FUSED': ['F1.2', 'D1.2'],
|
||||
'+12V': ['D1.1', 'C1.1', 'C2.1', 'C3.1', 'PS1.1', 'A1.8', 'A2.8', 'C16.1', 'C17.1'],
|
||||
'+5V_REG': ['PS1.3', 'C4.1', 'D2.2'],
|
||||
'VBUS': ['J2.A4', 'J2.A9', 'J2.B4', 'J2.B9', 'D3.2', 'U5.5'],
|
||||
'+5V': ['D2.1', 'D3.1', 'U3.1', 'C5.1', 'U4.5', 'C15.1', 'J6.1', 'J7.1'],
|
||||
'+3V3': ['U3.3', 'C6.1', 'U1.2', 'C9.1', 'C10.1', 'U2.16', 'U2.4', 'C7.1', 'R5.1', 'R6.1', 'R8.1',
|
||||
'R9.1', 'R11.1', 'R14.1', 'J5.1', 'J8.1', 'JP1.1', 'D6.1', 'D7.1',
|
||||
# DRV8825 sockets: M0-M2 high (1/32 step), RESET + SLEEP high (awake)
|
||||
'A1.10', 'A1.11', 'A1.12', 'A1.13', 'A1.14',
|
||||
'A2.10', 'A2.11', 'A2.12', 'A2.13', 'A2.14'],
|
||||
# socket pin 2 is FAULT on a DRV8825 and VDD on an A4988: only bridged to 3.3V for A4988 modules
|
||||
'DRV_VDD': ['A1.2', 'A2.2', 'JP1.2'],
|
||||
'USB_DP': ['J2.A6', 'J2.B6', 'U5.1', 'U5.6', 'U2.5'],
|
||||
'USB_DM': ['J2.A7', 'J2.B7', 'U5.3', 'U5.4', 'U2.6'],
|
||||
'CC1': ['J2.A5', 'R1.1'],
|
||||
'CC2': ['J2.B5', 'R2.1'],
|
||||
'U0TXD': ['U1.35', 'U2.3'],
|
||||
'U0RXD': ['U1.34', 'U2.2'],
|
||||
'DTR': ['U2.13', 'R3.1', 'Q2.2'],
|
||||
'RTS': ['U2.14', 'R4.1', 'Q1.2'],
|
||||
'Q1B': ['R3.2', 'Q1.1'],
|
||||
'Q2B': ['R4.2', 'Q2.1'],
|
||||
'EN': ['Q1.3', 'U1.3', 'R5.2', 'C8.1', 'SW1.1'],
|
||||
'IO0': ['Q2.3', 'U1.25', 'R6.2', 'SW2.1'],
|
||||
'DRV_EN': ['U1.16', 'R8.2', 'A1.9', 'A2.9'],
|
||||
'X_STEP': ['U1.30', 'A1.15'],
|
||||
'X_DIR': ['U1.31', 'A1.16'],
|
||||
'Y_STEP': ['U1.12', 'A2.15'],
|
||||
'Y_DIR': ['U1.13', 'A2.16'],
|
||||
# motor plugs follow the driver's own pin order (as on the Arduino CNC shield): each coil on an adjacent pair
|
||||
'X_2B': ['A1.6', 'J3.1'], 'X_2A': ['A1.5', 'J3.2'], 'X_1A': ['A1.4', 'J3.3'], 'X_1B': ['A1.3', 'J3.4'],
|
||||
'Y_1B': ['A2.3', 'J4.1'], 'Y_1A': ['A2.4', 'J4.2'], 'Y_2A': ['A2.5', 'J4.3'], 'Y_2B': ['A2.6', 'J4.4'],
|
||||
'HOME_RAW': ['J5.2', 'R9.2', 'R10.1'],
|
||||
'HOME': ['R10.2', 'C13.1', 'D6.2', 'U1.6'],
|
||||
'HALL_RAW': ['J6.2', 'R11.2', 'R12.1'],
|
||||
'HALL': ['R12.2', 'C14.1', 'D7.2', 'U1.7'],
|
||||
'LED_DATA': ['U1.9', 'U4.2'],
|
||||
'LED_BUF': ['U4.4', 'R13.1'],
|
||||
'LED_DOUT': ['R13.2', 'J7.2'],
|
||||
'EXP_IO32': ['U1.8', 'J8.2'],
|
||||
'EXP_IO25': ['U1.10', 'J8.3'],
|
||||
'STAT': ['U1.24', 'R15.1'],
|
||||
'STAT_A': ['R15.2', 'D5.2'],
|
||||
'PWR_A': ['R14.2', 'D4.2'],
|
||||
}
|
||||
|
||||
# Pins intentionally left unconnected (get no-connect flags in the schematic)
|
||||
NO_CONNECT = ['U1.4', 'U1.5', 'U1.14', 'U1.17', 'U1.18', 'U1.19', 'U1.20', 'U1.21', 'U1.22', 'U1.23',
|
||||
'U1.11', 'U1.26', 'U1.29', 'U1.32', 'U1.33', 'U1.36', 'U1.37',
|
||||
'U2.7', 'U2.8', 'U2.9', 'U2.10', 'U2.11', 'U2.12', 'U2.15',
|
||||
'U1.27', 'U1.28', 'J2.A8', 'J2.B8']
|
||||
|
||||
# ---------------------------------------------------------------- rules
|
||||
# net class -> (track width, clearance)
|
||||
NETCLASS = {
|
||||
'Default': (0.2, 0.16),
|
||||
'Power': (0.8, 0.2),
|
||||
'Motor': (0.5, 0.2),
|
||||
'Rail': (0.5, 0.2),
|
||||
}
|
||||
NET_TO_CLASS = {}
|
||||
for n in ('GND', 'VIN_RAW', 'VIN_FUSED', '+12V'):
|
||||
NET_TO_CLASS[n] = 'Power'
|
||||
for n in ('X_1A', 'X_1B', 'X_2A', 'X_2B', 'Y_1A', 'Y_1B', 'Y_2A', 'Y_2B'):
|
||||
NET_TO_CLASS[n] = 'Motor'
|
||||
for n in ('+5V', '+5V_REG', 'VBUS', '+3V3'):
|
||||
NET_TO_CLASS[n] = 'Rail'
|
||||
|
||||
VIA = (0.6, 0.3) # diameter, drill
|
||||
|
||||
# PCBWay standard process: 6/6 mil track/space, 0.3 mm min hole, 0.15 mm annular ring.
|
||||
# Library footprints with smaller holes (the ESP32's thermal vias are 0.2 mm) are enlarged to fit.
|
||||
MIN_DRILL = 0.3
|
||||
MIN_RING = 0.2
|
||||
|
||||
# ---------------------------------------------------------------- sourcing (PCBWay BOM)
|
||||
# (refs, manufacturer, manufacturer part number, description, notes). Blank MPN = any brand to the description.
|
||||
SOURCING = [
|
||||
(['U1'], 'Espressif', 'ESP32-WROOM-32E-N8', 'Wi-Fi + Bluetooth module, 8 MB flash, PCB antenna', ''),
|
||||
(['U2'], 'WCH', 'CH340C', 'USB to UART bridge, SOIC-16', ''),
|
||||
(['U3'], 'Diodes Incorporated', 'AP7361C-33E-13', 'LDO regulator 3.3 V 1 A, low dropout, SOT-223 (pin 1 IN, 2 GND/tab, 3 OUT)', 'Not an AMS1117: the pinout differs, no substitutes'),
|
||||
(['U5'], 'STMicroelectronics', 'USBLC6-2SC6', 'USB ESD protection, 2 lines, SOT-23-6', ''),
|
||||
(['U4'], 'Texas Instruments', 'SN74AHCT1G125DBVR', 'Single buffer, TTL-level input, SOT-23-5', ''),
|
||||
(['PS1'], 'RECOM', 'R-78E5.0-1.0', 'Switching regulator 5 V 1 A, SIP-3', ''),
|
||||
(['F1'], 'Bourns', 'MF-MSMF250/16X-2', 'PTC resettable fuse, 2.5 A hold, 5 A trip, 16 V, 1812', ''),
|
||||
(['J1'], 'Same Sky (CUI Devices)', 'PJ-063AH', 'DC barrel jack, 2.0 mm pin, fits 5.5 x 2.1 mm plugs', ''),
|
||||
(['J2'], 'Korean Hroparts Elec', 'TYPE-C-31-M-12', 'USB-C receptacle, USB 2.0, 16 pin', ''),
|
||||
(['J3', 'J4'], 'JST', 'B4B-XH-A(LF)(SN)', 'XH header, 4 pin, 2.5 mm pitch, vertical', ''),
|
||||
(['J5', 'J6', 'J7'], 'JST', 'B3B-XH-A(LF)(SN)', 'XH header, 3 pin, 2.5 mm pitch, vertical', ''),
|
||||
(['J8'], 'Sullins', 'PRPC004SAAN-RC', 'Male pin header 1 x 4, 2.54 mm pitch, vertical', ''),
|
||||
(['A1', 'A2'], 'Sullins', 'PPTC081LFBN-RC', 'Female pin header 1 x 8, 2.54 mm pitch, 8.5 mm tall', '2 headers per driver (4 total). Sockets only: the DRV8825 modules are supplied by me'),
|
||||
(['D1', 'D2'], '', 'SS34', 'Schottky diode 40 V 3 A, SMA', 'Any brand'),
|
||||
(['D3'], '', 'B5819W', 'Schottky diode 40 V 1 A, SOD-123', 'Any brand'),
|
||||
(['D6', 'D7'], '', 'BAT54WS', 'Schottky diode 30 V 0.2 A, SOD-323', 'Any brand'),
|
||||
(['D4'], '', '', 'LED green, 0603', 'Any brand'),
|
||||
(['D5'], '', '', 'LED blue, 0603', 'Any brand'),
|
||||
(['Q1', 'Q2'], 'onsemi', 'MMBT3904LT1G', 'NPN transistor 40 V 200 mA, SOT-23', 'Any MMBT3904 ok'),
|
||||
(['SW1', 'SW2'], 'Omron', 'B3U-1000P', 'Tactile switch, SMD, 3 x 2.5 mm', ''),
|
||||
(['C1', 'C2'], 'Panasonic', 'EEE-FK1V101GP', 'Aluminium electrolytic capacitor 100 uF 35 V, SMD 8 x 10.2 mm', ''),
|
||||
(['C3', 'C4'], '', '', 'Ceramic capacitor 10 uF 25 V X5R, 1206', 'Any brand'),
|
||||
(['C16', 'C17'], '', '', 'Ceramic capacitor 10 uF 50 V X7R, 1206', 'Any brand'),
|
||||
(['C5', 'C9'], '', '', 'Ceramic capacitor 10 uF 16 V X5R, 0805', 'Any brand'),
|
||||
(['C6'], '', '', 'Ceramic capacitor 22 uF 10 V X5R, 0805', 'Any brand'),
|
||||
(['C7', 'C10', 'C13', 'C14', 'C15'], '', '', 'Ceramic capacitor 100 nF 50 V X7R, 0603', 'Any brand'),
|
||||
(['C8'], '', '', 'Ceramic capacitor 1 uF 16 V X7R, 0603', 'Any brand'),
|
||||
(['R1', 'R2'], '', '', 'Resistor 5.1 k 1%, 0603', 'Any brand'),
|
||||
(['R3', 'R4', 'R5', 'R6', 'R8', 'R9', 'R11'], '', '', 'Resistor 10 k 1%, 0603', 'Any brand'),
|
||||
(['R10', 'R12', 'R14', 'R15'], '', '', 'Resistor 1 k 1%, 0603', 'Any brand'),
|
||||
(['R13'], '', '', 'Resistor 330 1%, 0603', 'Any brand'),
|
||||
]
|
||||
SOURCING_QTY = {('A1', 'A2'): 4} # BOM lines whose piece count differs from the designator count
|
||||
@@ -0,0 +1,219 @@
|
||||
"""Build the PCBWay order files and copy the KiCad project + outputs into the repo.
|
||||
|
||||
Run after route.py and sch.py. Writes to ~/kicad/sandtable-controller/fab (the KiCad snap can't
|
||||
read inside ~/snap), then copies the results to ../ (the repo's pcb/ folder).
|
||||
"""
|
||||
import csv
|
||||
import glob
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
import design as D
|
||||
|
||||
WORK = os.path.expanduser('~/kicad/sandtable-controller')
|
||||
NAME = 'sandtable-controller'
|
||||
PCB = os.path.join(WORK, NAME + '.kicad_pcb')
|
||||
SCH = os.path.join(WORK, NAME + '.kicad_sch')
|
||||
FAB = os.path.join(WORK, 'fab')
|
||||
GERB = os.path.join(FAB, 'gerbers')
|
||||
REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
CLI = 'kicad.kicad-cli'
|
||||
|
||||
|
||||
def cli(*args):
|
||||
r = subprocess.run([CLI, *args], capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
sys.exit(f'kicad-cli {" ".join(args[:3])} failed:\n{r.stdout}\n{r.stderr}')
|
||||
|
||||
|
||||
|
||||
|
||||
def bom(pcb_text):
|
||||
"""PCBWay BOM straight from design.py (one line per sourcing group)."""
|
||||
parts = {r: p for r, p in D.PARTS.items() if not r.startswith(('H', 'JP'))} # holes, solder jumpers
|
||||
covered = [r for refs, *_ in D.SOURCING for r in refs]
|
||||
dup = sorted({r for r in covered if covered.count(r) > 1})
|
||||
missing = sorted(set(parts) - set(covered))
|
||||
extra = sorted(set(covered) - set(parts))
|
||||
if dup or missing or extra:
|
||||
sys.exit(f'SOURCING mismatch: duplicate {dup}, missing {missing}, unknown {extra}')
|
||||
rows = []
|
||||
for i, (refs, mfr, mpn, desc, notes) in enumerate(D.SOURCING, 1):
|
||||
fps = {parts[r][1].split(':')[1] for r in refs}
|
||||
tht = any(pcb_text.count(f'(footprint "{parts[r][1]}"') and _is_tht(pcb_text, r) for r in refs)
|
||||
qty = D.SOURCING_QTY.get(tuple(refs), len(refs))
|
||||
rows.append([i, ','.join(refs), qty, mfr, mpn, desc, ' / '.join(sorted(fps)),
|
||||
'Thru-Hole' if tht else 'SMD', notes])
|
||||
return rows
|
||||
|
||||
|
||||
def _is_tht(pcb_text, ref):
|
||||
# find the footprint block that carries this reference and read its (attr ...)
|
||||
key = f'(property "Reference" "{ref}"'
|
||||
i = pcb_text.find(key)
|
||||
start = pcb_text.rfind('(footprint ', 0, i)
|
||||
j = pcb_text.find('(attr ', start, i + 4000)
|
||||
return pcb_text[j:j + 30].startswith('(attr through_hole')
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(GERB, exist_ok=True)
|
||||
for f in glob.glob(os.path.join(GERB, '*')):
|
||||
os.remove(f)
|
||||
# Gerbers + drill, all relative to the board's bottom-left corner
|
||||
cli('pcb', 'export', 'gerbers', '-o', GERB + '/', '-l',
|
||||
'F.Cu,B.Cu,F.Paste,F.SilkS,B.SilkS,F.Mask,B.Mask,Edge.Cuts', '--use-drill-file-origin', PCB)
|
||||
cli('pcb', 'export', 'drill', '-o', GERB + '/', '--format', 'excellon', '--excellon-units', 'mm',
|
||||
'--excellon-separate-th', '--drill-origin', 'plot', PCB)
|
||||
zpath = os.path.join(FAB, NAME + '-gerbers.zip')
|
||||
with zipfile.ZipFile(zpath, 'w', zipfile.ZIP_DEFLATED) as z:
|
||||
for f in sorted(os.listdir(GERB)):
|
||||
z.write(os.path.join(GERB, f), f)
|
||||
print('gerbers:', ', '.join(sorted(os.listdir(GERB))))
|
||||
|
||||
# Centroid (pick and place)
|
||||
raw = os.path.join(FAB, 'pos-raw.csv')
|
||||
cli('pcb', 'export', 'pos', '-o', raw, '--format', 'csv', '--units', 'mm', '--side', 'both',
|
||||
'--use-drill-file-origin', PCB)
|
||||
pos = list(csv.DictReader(open(raw)))
|
||||
os.remove(raw)
|
||||
with open(os.path.join(FAB, 'centroid-pcbway.csv'), 'w', newline='') as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(['Designator', 'Mid X (mm)', 'Mid Y (mm)', 'Layer', 'Rotation', 'Value', 'Footprint'])
|
||||
for r in sorted(pos, key=lambda r: (r['Ref'][0], int(''.join(c for c in r['Ref'] if c.isdigit())))):
|
||||
w.writerow([r['Ref'], f"{float(r['PosX']):.3f}", f"{float(r['PosY']):.3f}",
|
||||
'Top' if r['Side'] == 'top' else 'Bottom', f"{float(r['Rot']) % 360:g}", r['Val'], r['Package']])
|
||||
print('centroid:', len(pos), 'parts')
|
||||
|
||||
# BOM
|
||||
pcb_text = open(PCB).read()
|
||||
rows = bom(pcb_text)
|
||||
with open(os.path.join(FAB, 'bom-pcbway.csv'), 'w', newline='') as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(['Item #', 'Designator', 'Qty', 'Manufacturer', 'Mfg Part #', 'Description / Value',
|
||||
'Package / Footprint', 'Type', 'Notes'])
|
||||
w.writerows(rows)
|
||||
print('bom:', len(rows), 'lines,', sum(r[2] for r in rows), 'pieces')
|
||||
|
||||
# 3D model, renders, schematic PDF
|
||||
cli('pcb', 'export', 'step', '-f', '-o', os.path.join(FAB, NAME + '.step'), '--subst-models', PCB)
|
||||
cli('pcb', 'render', '-o', os.path.join(FAB, 'render-iso.png'), '--width', '1800', '--height', '1200',
|
||||
'--quality', 'high', '--perspective', '--floor', '--rotate', '310,0,325', '--zoom', '0.9', PCB)
|
||||
for side in ('top', 'bottom'):
|
||||
cli('pcb', 'render', '-o', os.path.join(FAB, f'render-{side}.png'), '--side', side,
|
||||
'--width', '1800', '--height', '1400', '--quality', 'high', PCB)
|
||||
cli('sch', 'export', 'pdf', '-o', os.path.join(WORK, NAME + '-schematic.pdf'), SCH)
|
||||
|
||||
# Assembly drawing (part outlines, references, polarity marks)
|
||||
assembly_pdf(os.path.join(FAB, 'assembly-top.pdf'))
|
||||
|
||||
# Copy into the repo: KiCad project, the PCBWay order folder, renders
|
||||
kdir = os.path.join(REPO, 'kicad')
|
||||
odir = os.path.join(REPO, 'pcbway-order')
|
||||
rdir = os.path.join(REPO, 'renders')
|
||||
for d in (kdir, odir, rdir):
|
||||
os.makedirs(d, exist_ok=True)
|
||||
for f in glob.glob(os.path.join(odir, '*')):
|
||||
os.remove(f)
|
||||
for f in (NAME + '.kicad_pro', NAME + '.kicad_pcb', NAME + '.kicad_sch', 'sym-lib-table', 'fp-lib-table',
|
||||
'sandtable.kicad_sym'):
|
||||
shutil.copy2(os.path.join(WORK, f), kdir)
|
||||
tag = f'{NAME}-v{D.VERSION}'
|
||||
shutil.copy2(zpath, os.path.join(odir, f'1-gerbers-{tag}.zip'))
|
||||
shutil.copy2(os.path.join(FAB, 'bom-pcbway.csv'), os.path.join(odir, f'2-bom-{tag}.csv'))
|
||||
shutil.copy2(os.path.join(FAB, 'centroid-pcbway.csv'), os.path.join(odir, f'3-centroid-{tag}.csv'))
|
||||
shutil.copy2(os.path.join(FAB, 'assembly-top.pdf'), os.path.join(odir, f'assembly-top-v{D.VERSION}.pdf'))
|
||||
shutil.copy2(os.path.join(WORK, NAME + '-schematic.pdf'), os.path.join(odir, f'schematic-v{D.VERSION}.pdf'))
|
||||
write_order_sheet(os.path.join(odir, 'ORDER.md'), tag, rows, pcb_text)
|
||||
for f in ('render-iso.png', 'render-top.png', 'render-bottom.png'):
|
||||
shutil.copy2(os.path.join(FAB, f), rdir)
|
||||
shutil.copy2(os.path.join(FAB, NAME + '.step'), REPO)
|
||||
shutil.copy2(os.path.join(WORK, NAME + '-schematic.pdf'), REPO)
|
||||
print('order folder:', ', '.join(sorted(os.listdir(odir))))
|
||||
|
||||
|
||||
def assembly_pdf(out):
|
||||
"""KiCad plots on an A4 sheet with the board small in one corner: crop to the board and fit to A4."""
|
||||
full, crop = out + '.full.pdf', out + '.crop.pdf'
|
||||
cli('pcb', 'export', 'pdf', '-o', full, '--mode-single', '-l', 'F.Fab,F.SilkS,Edge.Cuts',
|
||||
'--ev', '--black-and-white', PCB)
|
||||
mm, page_h, m = 72 / 25.4, 210.0, 5.0 # A4 landscape sheet, 5 mm margin
|
||||
ox, oy = D.ORIGIN
|
||||
box = [(ox - m) * mm, (page_h - (oy + D.BOARD_H + m)) * mm, (ox + D.BOARD_W + m) * mm, (page_h - (oy - m)) * mm]
|
||||
gs = ['gs', '-q', '-sDEVICE=pdfwrite']
|
||||
subprocess.run(gs + ['-o', crop, '-c', '[/CropBox [' + ' '.join(f'{v:.1f}' for v in box) + '] /PAGES pdfmark',
|
||||
'-f', full], check=True)
|
||||
subprocess.run(gs + ['-o', out, '-dUseCropBox', '-dFIXEDMEDIA', '-dPDFFitPage',
|
||||
'-dDEVICEWIDTHPOINTS=842', '-dDEVICEHEIGHTPOINTS=595', crop], check=True)
|
||||
os.remove(full)
|
||||
os.remove(crop)
|
||||
|
||||
|
||||
def write_order_sheet(path, tag, rows, pcb_text):
|
||||
smd = pcb_text.count('(attr smd')
|
||||
tht_pieces = sum(r[2] for r in rows if r[7] == 'Thru-Hole')
|
||||
tht_parts = sum(len(r[1].split(',')) for r in rows if r[7] == 'Thru-Hole')
|
||||
text = f"""# PCBWay order: SandTable Controller v{D.VERSION}
|
||||
|
||||
## Files
|
||||
|
||||
| File | Where it goes |
|
||||
|---|---|
|
||||
| `1-gerbers-{tag}.zip` | PCB order: **Upload Gerber file** |
|
||||
| `2-bom-{tag}.csv` | Assembly order: **BOM file** |
|
||||
| `3-centroid-{tag}.csv` | Assembly order: **Centroid / pick-and-place file** |
|
||||
| `assembly-top-v{D.VERSION}.pdf` | Attach if PCBWay asks for an assembly drawing |
|
||||
| `schematic-v{D.VERSION}.pdf` | Attach if PCBWay asks for a schematic |
|
||||
|
||||
## PCB settings
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| Board type | Single pieces |
|
||||
| Size | {D.BOARD_W:g} x {D.BOARD_H:g} mm |
|
||||
| Quantity | 5 (the minimum) |
|
||||
| Layers | 2 |
|
||||
| Material | FR-4, TG130-140 |
|
||||
| Thickness | 1.6 mm |
|
||||
| Min track / spacing | 6/6 mil |
|
||||
| Min hole size | 0.3 mm |
|
||||
| Solder mask / silkscreen | any colour / white |
|
||||
| Surface finish | HASL lead-free (or ENIG) |
|
||||
| Via process | Tenting vias |
|
||||
| Finished copper | 1 oz |
|
||||
| Product No. | Specify a location (the back of the board has a "WayWayWay" marker for it) |
|
||||
|
||||
## Assembly settings (PCB Assembly quote)
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| Service | Turnkey (PCBWay buys the parts) |
|
||||
| Assembly side | Top side only |
|
||||
| Quantity | 1 or 2 (fewer than the 5 bare boards is fine) |
|
||||
| Unique parts | {len(rows)} |
|
||||
| SMD parts | {smd} |
|
||||
| BGA / QFP parts | 0 |
|
||||
| Through-hole parts | {tht_pieces} ({tht_parts} designators; each driver socket is 2 headers) |
|
||||
| Accept alternatives | Yes, for lines marked "Any brand" |
|
||||
|
||||
**Note for PCBWay** (paste into the comments box):
|
||||
|
||||
> Please fit the female header sockets for A1 and A2 only. Do not supply the stepper driver modules; I will plug in my own. BOM lines marked "Any brand" may use any equivalent part to the description. U3 (AP7361C-33E) has a different pinout from AMS1117-type regulators, so please do not substitute it. Leave solder jumper JP1 open. C16 and C17 sit between the driver socket strips, under where the modules plug in; that is intended. Centroid coordinates are in mm, with the origin at the board's bottom-left corner.
|
||||
|
||||
## When PCBWay sends their review
|
||||
- Check the polarity of C1, C2 (electrolytic), D1-D3, D6, D7 (diodes) and D4, D5 (LEDs) against the + and bar marks in `assembly-top-v{D.VERSION}.pdf`.
|
||||
- Check that the ESP32 (U1) antenna end sits at the board edge, over the copper-free area.
|
||||
- Check that pin 1 of U2, U3, U4 and U5 is at the dot or triangle on the silkscreen.
|
||||
- U3 is an AP7361C-33E, not an AMS1117: its pinout is different (pin 1 IN, pin 2 GND/tab, pin 3 OUT), so it must not be swapped for a 1117-type regulator.
|
||||
- JP1 is a solder jumper (bare pads), not a part: leave it open.
|
||||
"""
|
||||
with open(path, 'w') as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Load footprints and symbols from the installed KiCad libraries."""
|
||||
import math
|
||||
import os
|
||||
from sexpr import parse, find, findall, QStr
|
||||
|
||||
KICAD = os.environ.get('KICAD_SHARE', '/snap/kicad/current/usr/share/kicad')
|
||||
FP_DIR = os.path.join(KICAD, 'footprints')
|
||||
SYM_DIR = os.path.join(KICAD, 'symbols')
|
||||
|
||||
|
||||
def load_footprint(lib_id, min_drill=None, min_ring=0.2, solid_thermal_pads=False):
|
||||
lib, name = lib_id.split(':', 1)
|
||||
path = os.path.join(FP_DIR, lib + '.pretty', name + '.kicad_mod')
|
||||
with open(path) as f:
|
||||
fp = parse(f.read())
|
||||
if solid_thermal_pads:
|
||||
_solidify_thermal_pads(fp)
|
||||
if min_drill:
|
||||
_enforce_min_drill(fp, min_drill, min_ring)
|
||||
return fp
|
||||
|
||||
|
||||
def _solidify_thermal_pads(fp):
|
||||
"""Remove plated holes that sit inside a cluster of same-numbered SMD pads (thermal vias in an
|
||||
exposed pad) and add one copper-only pad over the cluster, so the copper stays continuous under
|
||||
the solder-mask/paste windows but no hole is left in a pad. Fabs flag holes in pads (solder
|
||||
wicks into them) and charge for resin filling."""
|
||||
import copy
|
||||
pads = findall(fp, 'pad')
|
||||
by_num = {}
|
||||
for p in pads:
|
||||
by_num.setdefault(str(p[1]), []).append(p)
|
||||
for num, group in by_num.items():
|
||||
smd = [p for p in group if str(p[2]) == 'smd']
|
||||
tht = [p for p in group if str(p[2]) == 'thru_hole']
|
||||
if len(smd) < 2 or not tht:
|
||||
continue
|
||||
boxes = []
|
||||
for p in smd:
|
||||
at, size = find(p, 'at'), find(p, 'size')
|
||||
x, y, w, h = float(at[1]), float(at[2]), float(size[1]), float(size[2])
|
||||
boxes.append((x - w / 2, y - h / 2, x + w / 2, y + h / 2))
|
||||
x0, y0 = min(b[0] for b in boxes), min(b[1] for b in boxes)
|
||||
x1, y1 = max(b[2] for b in boxes), max(b[3] for b in boxes)
|
||||
inside = [p for p in tht if x0 - 0.5 <= float(find(p, 'at')[1]) <= x1 + 0.5
|
||||
and y0 - 0.5 <= float(find(p, 'at')[2]) <= y1 + 0.5]
|
||||
if not inside:
|
||||
continue
|
||||
for p in inside:
|
||||
fp.remove(p)
|
||||
solid = copy.deepcopy(smd[0])
|
||||
solid[:] = [v for v in solid if not (isinstance(v, list) and v and v[0] in ('uuid', 'roundrect_rratio'))]
|
||||
solid[3] = 'rect'
|
||||
at, size, layers = find(solid, 'at'), find(solid, 'size'), find(solid, 'layers')
|
||||
at[1:] = [f'{(x0 + x1) / 2:g}', f'{(y0 + y1) / 2:g}']
|
||||
size[1:] = [f'{x1 - x0:g}', f'{y1 - y0:g}']
|
||||
layers[1:] = [QStr('F.Cu')]
|
||||
fp.insert(fp.index(smd[0]), solid)
|
||||
|
||||
|
||||
def _enforce_min_drill(fp, min_drill, min_ring):
|
||||
"""Grow round plated holes below min_drill (and their copper ring) so the board fits the fab's process."""
|
||||
for p in findall(fp, 'pad'):
|
||||
if str(p[2]) != 'thru_hole':
|
||||
continue
|
||||
drill = find(p, 'drill')
|
||||
if not drill or len(drill) < 2 or isinstance(drill[1], list) or str(drill[1]) == 'oval':
|
||||
continue
|
||||
if float(drill[1]) >= min_drill - 1e-9:
|
||||
continue
|
||||
drill[1] = f'{min_drill:g}'
|
||||
size = find(p, 'size')
|
||||
need = min_drill + 2 * min_ring
|
||||
for i in (1, 2):
|
||||
if float(size[i]) < need:
|
||||
size[i] = f'{need:g}'
|
||||
|
||||
|
||||
def fp_pads(fp):
|
||||
"""Pads as dicts in footprint-local coords (KiCad: y down)."""
|
||||
pads = []
|
||||
for p in findall(fp, 'pad'):
|
||||
num = str(p[1])
|
||||
kind = p[2] # smd / thru_hole / np_thru_hole / connect
|
||||
shape = p[3]
|
||||
at = find(p, 'at')
|
||||
x, y = float(at[1]), float(at[2])
|
||||
rot = float(at[3]) if len(at) > 3 else 0.0
|
||||
size = find(p, 'size')
|
||||
w, h = float(size[1]), float(size[2])
|
||||
layers = [str(l) for l in find(p, 'layers')[1:]]
|
||||
drill = find(p, 'drill')
|
||||
d = None
|
||||
if drill:
|
||||
vals = [v for v in drill[1:] if not isinstance(v, list)]
|
||||
if vals and vals[0] == 'oval':
|
||||
d = (float(vals[1]), float(vals[2]) if len(vals) > 2 else float(vals[1]))
|
||||
elif vals:
|
||||
d = (float(vals[0]), float(vals[0]))
|
||||
pads.append(dict(num=num, kind=kind, shape=shape, x=x, y=y, rot=rot,
|
||||
w=w, h=h, layers=layers, drill=d, node=p))
|
||||
return pads
|
||||
|
||||
|
||||
def fp_courtyard(fp):
|
||||
"""Bounding box (x0,y0,x1,y1) of the front/back courtyard lines/rects."""
|
||||
xs, ys = [], []
|
||||
for kind in ('fp_line', 'fp_rect', 'fp_circle', 'fp_arc', 'fp_poly'):
|
||||
for g in findall(fp, kind):
|
||||
layer = find(g, 'layer')
|
||||
if not layer or 'CrtYd' not in str(layer[1]):
|
||||
continue
|
||||
for key in ('start', 'end', 'mid', 'center'):
|
||||
pt = find(g, key)
|
||||
if pt:
|
||||
xs.append(float(pt[1])); ys.append(float(pt[2]))
|
||||
pts = find(g, 'pts')
|
||||
if pts:
|
||||
for xy in findall(pts, 'xy'):
|
||||
xs.append(float(xy[1])); ys.append(float(xy[2]))
|
||||
if kind == 'fp_circle':
|
||||
c = find(g, 'center'); e = find(g, 'end')
|
||||
r = math.hypot(float(e[1]) - float(c[1]), float(e[2]) - float(c[2]))
|
||||
xs += [float(c[1]) - r, float(c[1]) + r]
|
||||
ys += [float(c[2]) - r, float(c[2]) + r]
|
||||
if not xs:
|
||||
return None
|
||||
return (min(xs), min(ys), max(xs), max(ys))
|
||||
|
||||
|
||||
_symlib_cache = {}
|
||||
|
||||
|
||||
def load_symbol(lib_id):
|
||||
"""Return the symbol definition list (with 'extends' resolved)."""
|
||||
lib, name = lib_id.split(':', 1)
|
||||
if lib not in _symlib_cache:
|
||||
with open(os.path.join(SYM_DIR, lib + '.kicad_sym')) as f:
|
||||
_symlib_cache[lib] = parse(f.read())
|
||||
root = _symlib_cache[lib]
|
||||
syms = {str(s[1]): s for s in findall(root, 'symbol')}
|
||||
s = syms[name]
|
||||
ext = find(s, 'extends')
|
||||
if ext:
|
||||
base = load_symbol(lib + ':' + str(ext[1]))
|
||||
# copy base, take name + properties from derived
|
||||
import copy
|
||||
merged = copy.deepcopy(base)
|
||||
merged[1] = QStr(name)
|
||||
# rename sub-units
|
||||
for sub in findall(merged, 'symbol'):
|
||||
sub[1] = QStr(str(sub[1]).replace(str(ext[1]), name, 1))
|
||||
# properties: inherited from the base, overridden/extended by the derived symbol
|
||||
base_props = findall(merged, 'property')
|
||||
merged = [x for x in merged if not (isinstance(x, list) and x and x[0] == 'property')]
|
||||
derived = {str(p[1]): p for p in findall(s, 'property')}
|
||||
props = []
|
||||
for p in base_props:
|
||||
props.append(derived.pop(str(p[1]), p))
|
||||
props += list(derived.values())
|
||||
# insert properties after the symbol-level flags
|
||||
idx = 2
|
||||
while idx < len(merged) and isinstance(merged[idx], list) and merged[idx][0] in ('pin_names', 'pin_numbers', 'exclude_from_sim', 'in_bom', 'on_board'):
|
||||
idx += 1
|
||||
merged = merged[:idx] + props + merged[idx:]
|
||||
return merged
|
||||
return s
|
||||
|
||||
|
||||
def sym_pins(sym):
|
||||
"""Pins (number, name, type, x, y, angle, length) in symbol coords (y up)."""
|
||||
pins = []
|
||||
for unit in findall(sym, 'symbol'):
|
||||
for p in findall(unit, 'pin'):
|
||||
at = find(p, 'at')
|
||||
length = float(find(p, 'length')[1])
|
||||
name = find(p, 'name')
|
||||
number = find(p, 'number')
|
||||
pins.append(dict(type=str(p[1]), style=str(p[2]),
|
||||
x=float(at[1]), y=float(at[2]), angle=float(at[3]) if len(at) > 3 else 0.0,
|
||||
length=length, name=str(name[1]), num=str(number[1]),
|
||||
hidden=('hide' in [str(v) for v in p if not isinstance(v, list)]),
|
||||
unit=str(unit[1])))
|
||||
return pins
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Build the board, report placement problems and write an unrouted PCB for a look."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
import design as D
|
||||
from board import build, check_placement, write_pcb, write_project, keepout_zone
|
||||
|
||||
OUT = os.path.expanduser('~/kicad/sandtable-controller')
|
||||
|
||||
parts = build()
|
||||
probs = check_placement(parts)
|
||||
print(f'{len(parts)} parts, {sum(len(v) for v in D.NETS.values())} net pins, {len(D.NETS)} nets')
|
||||
for p in probs:
|
||||
print(' !', p)
|
||||
if not probs:
|
||||
print(' placement OK')
|
||||
zones = [keepout_zone('ANTENNA', 18.75, 56.9, 67.25, D.BOARD_H)]
|
||||
write_pcb(os.path.join(OUT, 'sandtable-controller.kicad_pcb'), parts, zones=zones)
|
||||
write_project(os.path.join(OUT, 'sandtable-controller.kicad_pro'), 'sandtable-controller')
|
||||
print('wrote unrouted board')
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Find free spots for parts added in a revision: a spiral search around each part's
|
||||
preferred position (from design.py), trying 4 rotations, keeping courtyards apart,
|
||||
inside the board and out of the antenna keep-out. Prints the positions to paste into design.py."""
|
||||
import math
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
import design as D
|
||||
from board import build
|
||||
|
||||
GAP = 0.25 # min gap between courtyards, mm
|
||||
KEEPOUT = (18.75, 56.9, 67.25, D.BOARD_H) # ESP32 antenna
|
||||
EDGE = 0.6
|
||||
|
||||
|
||||
def overlaps(a, b, gap=GAP):
|
||||
return min(a[2], b[2]) - max(a[0], b[0]) > -gap and min(a[3], b[3]) - max(a[1], b[1]) > -gap
|
||||
|
||||
|
||||
def legal(part, fixed):
|
||||
for r in part.court_rects():
|
||||
if r[0] < EDGE or r[1] < EDGE or r[2] > D.BOARD_W - EDGE or r[3] > D.BOARD_H - EDGE:
|
||||
return False
|
||||
if overlaps(r, KEEPOUT, 0):
|
||||
return False
|
||||
if any(overlaps(r, f) for f in fixed):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def place(refs, parts):
|
||||
fixed = [r for ref, p in parts.items() if ref not in refs for r in p.court_rects()]
|
||||
out = {}
|
||||
for ref in refs:
|
||||
p = parts[ref]
|
||||
x0, y0, rot0 = p.x, p.y, p.rot
|
||||
best = None
|
||||
for step in range(0, 80): # radius in 0.25 mm steps, up to 20 mm
|
||||
rad = step * 0.25
|
||||
n = max(1, int(2 * math.pi * rad / 0.25))
|
||||
for i in range(n):
|
||||
a = 2 * math.pi * i / n
|
||||
x, y = round(x0 + rad * math.cos(a), 2), round(y0 + rad * math.sin(a), 2)
|
||||
for rot in (rot0, (rot0 + 90) % 360, (rot0 + 180) % 360, (rot0 + 270) % 360):
|
||||
p.x, p.y, p.rot = x, y, rot
|
||||
if legal(p, fixed):
|
||||
best = (x, y, rot, rad)
|
||||
break
|
||||
if best:
|
||||
break
|
||||
if best:
|
||||
break
|
||||
if not best:
|
||||
print(f'{ref}: no spot within 20 mm')
|
||||
p.x, p.y, p.rot = x0, y0, rot0
|
||||
continue
|
||||
p.x, p.y, p.rot = best[:3]
|
||||
fixed += p.court_rects()
|
||||
out[ref] = best
|
||||
print(f'{ref}: ({best[0]}, {best[1]}) rot {best[2]} moved {best[3]:.2f} mm from preferred')
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parts = build()
|
||||
place(sys.argv[1:], parts)
|
||||
@@ -0,0 +1,213 @@
|
||||
"""Ground pour: raster fill on the router grid -> fractured polygons for KiCad.
|
||||
|
||||
Cells are squares of side G centred on grid points. A cell may be filled when no
|
||||
foreign copper is within the pour clearance + half the cell diagonal; that radius
|
||||
map already exists in the router (radius for a 0.2 mm track = 0.38 mm).
|
||||
"""
|
||||
from collections import deque
|
||||
|
||||
import router as R
|
||||
|
||||
|
||||
def fill_mask(rt, net, layer, r_map, open_cells=2):
|
||||
k = rt.nid(net)
|
||||
nx, ny = rt.nx, rt.ny
|
||||
m = rt.maps[r_map][layer]
|
||||
mask = bytearray(nx * ny)
|
||||
for i in range(nx * ny):
|
||||
v = m[i]
|
||||
if v == -1 or v == k:
|
||||
mask[i] = 1
|
||||
# morphological opening (erode then dilate, 4-neighbourhood) removes slivers
|
||||
for _ in range(open_cells):
|
||||
mask = _erode(mask, nx, ny)
|
||||
for _ in range(open_cells):
|
||||
mask = _dilate(mask, nx, ny, limit=m, k=k)
|
||||
return mask
|
||||
|
||||
|
||||
def _erode(mask, nx, ny):
|
||||
out = bytearray(len(mask))
|
||||
for iy in range(1, ny - 1):
|
||||
b = iy * nx
|
||||
for ix in range(1, nx - 1):
|
||||
i = b + ix
|
||||
if mask[i] and mask[i - 1] and mask[i + 1] and mask[i - nx] and mask[i + nx]:
|
||||
out[i] = 1
|
||||
return out
|
||||
|
||||
|
||||
def _dilate(mask, nx, ny, limit, k):
|
||||
out = bytearray(mask)
|
||||
for iy in range(1, ny - 1):
|
||||
b = iy * nx
|
||||
for ix in range(1, nx - 1):
|
||||
i = b + ix
|
||||
if not mask[i] and (mask[i - 1] or mask[i + 1] or mask[i - nx] or mask[i + nx]):
|
||||
v = limit[i]
|
||||
if v == -1 or v == k:
|
||||
out[i] = 1
|
||||
return out
|
||||
|
||||
|
||||
def keep_connected(mask, nx, ny, anchors):
|
||||
"""Keep only 4-connected components that contain an anchor cell index."""
|
||||
keep = bytearray(len(mask))
|
||||
q = deque(i for i in anchors if mask[i])
|
||||
for i in q:
|
||||
keep[i] = 1
|
||||
while q:
|
||||
i = q.popleft()
|
||||
for j in (i - 1, i + 1, i - nx, i + nx):
|
||||
if 0 <= j < len(mask) and mask[j] and not keep[j]:
|
||||
keep[j] = 1
|
||||
q.append(j)
|
||||
return keep
|
||||
|
||||
|
||||
def contours(mask, nx, ny):
|
||||
"""Boundary loops of the union of filled cells, in corner coordinates (cx, cy)
|
||||
where cell (ix, iy) spans corners (ix..ix+1, iy..iy+1). Filled area on the left."""
|
||||
edges = {}
|
||||
def add(a, b):
|
||||
edges.setdefault(a, []).append(b)
|
||||
for iy in range(ny):
|
||||
b = iy * nx
|
||||
for ix in range(nx):
|
||||
if not mask[b + ix]:
|
||||
continue
|
||||
# neighbours (outside = empty)
|
||||
up = iy > 0 and mask[b + ix - nx]
|
||||
dn = iy < ny - 1 and mask[b + ix + nx]
|
||||
lf = ix > 0 and mask[b + ix - 1]
|
||||
rt_ = ix < nx - 1 and mask[b + ix + 1]
|
||||
# traverse cell boundary counter-clockwise in screen coords (y down): filled on left
|
||||
if not up:
|
||||
add((ix + 1, iy), (ix, iy))
|
||||
if not lf:
|
||||
add((ix, iy), (ix, iy + 1))
|
||||
if not dn:
|
||||
add((ix, iy + 1), (ix + 1, iy + 1))
|
||||
if not rt_:
|
||||
add((ix + 1, iy + 1), (ix + 1, iy))
|
||||
loops = []
|
||||
while edges:
|
||||
start = next(iter(edges))
|
||||
loop = [start]
|
||||
cur = start
|
||||
prev_dir = None
|
||||
while True:
|
||||
outs = edges[cur]
|
||||
if len(outs) == 1:
|
||||
nxt = outs.pop()
|
||||
else:
|
||||
# pinch point: prefer the turn that keeps the filled region on the left (turn left first)
|
||||
best = None
|
||||
for cand in outs:
|
||||
d = (cand[0] - cur[0], cand[1] - cur[1])
|
||||
if prev_dir is None:
|
||||
best = cand
|
||||
break
|
||||
# left turn in y-down coords: (dx,dy) -> (dy,-dx)
|
||||
if d == (prev_dir[1], -prev_dir[0]):
|
||||
best = cand
|
||||
break
|
||||
if best is None:
|
||||
best = outs[0]
|
||||
outs.remove(best)
|
||||
nxt = best
|
||||
if not outs:
|
||||
del edges[cur]
|
||||
prev_dir = (nxt[0] - cur[0], nxt[1] - cur[1])
|
||||
cur = nxt
|
||||
if cur == start:
|
||||
break
|
||||
loop.append(cur)
|
||||
loops.append(_simplify(loop))
|
||||
return loops
|
||||
|
||||
|
||||
def _simplify(loop):
|
||||
out = []
|
||||
n = len(loop)
|
||||
for i in range(n):
|
||||
a, b, c = loop[i - 1], loop[i], loop[(i + 1) % n]
|
||||
if (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]) != 0:
|
||||
out.append(b)
|
||||
return out
|
||||
|
||||
|
||||
def area2(loop):
|
||||
s = 0
|
||||
for i in range(len(loop)):
|
||||
x1, y1 = loop[i - 1]
|
||||
x2, y2 = loop[i]
|
||||
s += x1 * y2 - x2 * y1
|
||||
return s
|
||||
|
||||
|
||||
def point_in(loop, x, y):
|
||||
inside = False
|
||||
n = len(loop)
|
||||
for i in range(n):
|
||||
x1, y1 = loop[i - 1]
|
||||
x2, y2 = loop[i]
|
||||
if (y1 > y) != (y2 > y):
|
||||
xi = x1 + (y - y1) * (x2 - x1) / (y2 - y1)
|
||||
if xi > x:
|
||||
inside = not inside
|
||||
return inside
|
||||
|
||||
|
||||
def fracture(outer, holes):
|
||||
"""Merge holes into the outer ring with zero-width bridges (KiCad's fractured form).
|
||||
Rectilinear integer loops. Holes are bridged rightwards to the nearest vertical edge."""
|
||||
ring = list(outer)
|
||||
for hole in sorted(holes, key=lambda h: -max(p[0] for p in h)):
|
||||
xh = max(p[0] for p in hole)
|
||||
vi = min((i for i in range(len(hole)) if hole[i][0] == xh), key=lambda i: hole[i][1])
|
||||
V = hole[vi]
|
||||
best = None
|
||||
n = len(ring)
|
||||
for i in range(n):
|
||||
A, B = ring[i], ring[(i + 1) % n]
|
||||
if A[0] == B[0] and A[0] > V[0] and min(A[1], B[1]) <= V[1] <= max(A[1], B[1]):
|
||||
if best is None or A[0] < best[0]:
|
||||
best = (A[0], i)
|
||||
if best is None:
|
||||
continue
|
||||
x, i = best
|
||||
P = (x, V[1])
|
||||
seq = hole[vi:] + hole[:vi]
|
||||
ring = ring[:i + 1] + [P] + seq + [V, P] + ring[i + 1:]
|
||||
out = []
|
||||
for p in ring:
|
||||
if not out or out[-1] != p:
|
||||
out.append(p)
|
||||
if len(out) > 1 and out[0] == out[-1]:
|
||||
out.pop()
|
||||
return out
|
||||
|
||||
|
||||
def polygons(mask, nx, ny):
|
||||
loops = contours(mask, nx, ny)
|
||||
outers, holes = [], []
|
||||
for lp in loops:
|
||||
if len(lp) < 4:
|
||||
continue
|
||||
(outers if area2(lp) < 0 else holes).append(lp)
|
||||
result = []
|
||||
owner = {}
|
||||
for h in holes:
|
||||
x, y = h[0][0] + 1e-3, h[0][1] + 1e-3
|
||||
cands = [o for o in outers if point_in(o, x, y)]
|
||||
if cands:
|
||||
owner[id(h)] = min(cands, key=lambda o: abs(area2(o)))
|
||||
for o in outers:
|
||||
hs = [h for h in holes if owner.get(id(h)) is o]
|
||||
result.append(fracture(o, hs))
|
||||
return result
|
||||
|
||||
|
||||
def to_mm(poly, G):
|
||||
return [((cx - 0.5) * G, (cy - 0.5) * G) for (cx, cy) in poly]
|
||||
@@ -0,0 +1,349 @@
|
||||
"""Place + route the SandTable controller and write the KiCad board."""
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
import design as D
|
||||
from board import build, check_placement, write_pcb, write_project, keepout_zone, text_node
|
||||
import router as R
|
||||
|
||||
OUT = os.path.expanduser('~/kicad/sandtable-controller')
|
||||
|
||||
# routing width per net
|
||||
WIDTH = {}
|
||||
for n in D.NETS:
|
||||
WIDTH[n] = 0.2
|
||||
for n in ('VIN_RAW', 'VIN_FUSED', '+12V'):
|
||||
WIDTH[n] = 0.8
|
||||
WIDTH['GND'] = 0.5
|
||||
for n in ('X_1A', 'X_1B', 'X_2A', 'X_2B', 'Y_1A', 'Y_1B', 'Y_2A', 'Y_2B'):
|
||||
WIDTH[n] = 0.5
|
||||
for n in ('VBUS', '+3V3'):
|
||||
WIDTH[n] = 0.4
|
||||
for n in ('+5V', '+5V_REG'): # up to 1 A from the 5 V regulator (LED strip)
|
||||
WIDTH[n] = 0.6
|
||||
FALLBACK = [0.3, 0.2]
|
||||
# nets that must keep their full width: fail loudly instead of necking down
|
||||
NO_FALLBACK = {'VIN_RAW', 'VIN_FUSED', '+12V', 'X_1A', 'X_1B', 'X_2A', 'X_2B', 'Y_1A', 'Y_1B', 'Y_2A', 'Y_2B'}
|
||||
VIA_PAD_GAP = 0.2 # min copper gap between a via and any pad (keeps holes out of solder pads)
|
||||
|
||||
|
||||
def radius(w):
|
||||
return round(w / 2 + R.CLEAR + R.MARGIN, 3)
|
||||
|
||||
|
||||
def via_radius(w):
|
||||
return max(radius(w), round(R.VIA_D / 2 + R.CLEAR + R.MARGIN, 3))
|
||||
|
||||
|
||||
# Hand-routed USB-C escape (pads are on a 0.5 mm pitch). Board coords.
|
||||
USB_X = 7.445
|
||||
PREROUTE = {
|
||||
'USB_DP': dict(tracks=[(USB_X, 43.75, 9.0, 43.75, 0.2, 0), (USB_X, 44.75, 9.0, 44.75, 0.2, 0),
|
||||
(9.0, 43.75, 9.0, 44.75, 0.2, 0)], ports=[(9.0, 44.25, (0,))]),
|
||||
'USB_DM': dict(tracks=[(USB_X, 43.25, 6.15, 43.25, 0.2, 0), (USB_X, 44.25, 6.15, 44.25, 0.2, 0),
|
||||
(6.15, 43.25, 6.15, 44.25, 0.2, 1)],
|
||||
vias=[(6.15, 43.25), (6.15, 44.25)], ports=[(6.15, 43.75, (1,))]),
|
||||
'CC1': dict(tracks=[(USB_X, 42.75, 10.675, 42.75, 0.2, 0)]),
|
||||
'CC2': dict(tracks=[(USB_X, 45.75, 10.675, 45.75, 0.2, 0)]),
|
||||
'GND': dict(tracks=[(USB_X, 40.75, 9.3, 40.75, 0.4, 0), (USB_X, 47.25, 9.3, 47.25, 0.4, 0),
|
||||
# driver VMOT ceramics (C16, C17): straight down from the socket's GND pin 7
|
||||
(22.86, 1.9, 22.86, 5.4, 0.5, 0), (43.86, 1.9, 43.86, 5.4, 0.5, 0),
|
||||
# boxed-in GND pads get their own via: U5 pin 2 (between the USB lines), C13/C14
|
||||
(11.863, 49.5, 10.55, 49.5, 0.3, 0),
|
||||
(61.775, 18.5, 61.775, 19.6, 0.3, 0), (61.775, 19.6, 61.775, 20.7, 0.3, 0)],
|
||||
vias=[(10.55, 49.5), (61.775, 19.6)],
|
||||
ports=[(9.3, 40.75, (0,)), (9.3, 47.25, (0,))]),
|
||||
# ... and from the socket's VMOT pin 8
|
||||
'+12V': dict(tracks=[(20.32, 1.9, 20.32, 5.4, 0.8, 0), (41.32, 1.9, 41.32, 5.4, 0.8, 0)]),
|
||||
'VBUS': dict(tracks=[(USB_X, 41.55, 9.3, 41.55, 0.4, 0), (USB_X, 46.45, 9.3, 46.45, 0.4, 0)],
|
||||
ports=[(9.3, 41.55, (0,)), (9.3, 46.45, (0,))]),
|
||||
}
|
||||
|
||||
ORDER_FIRST = ['USB_DP', 'USB_DM', 'CC1', 'CC2', 'VBUS', 'VIN_RAW',
|
||||
# motor coils first, outermost lane first (J3.1 / J3.4 / J4.1 / J4.4), then the 12 V trunk
|
||||
'X_2B', 'X_1B', 'X_2A', 'X_1A', 'Y_1B', 'Y_2B', 'Y_1A', 'Y_2A', '+12V',
|
||||
'STAT', # short straight run under the LEDs; boxed in if left late
|
||||
'+5V_REG', '+5V', '+3V3']
|
||||
ORDER_LAST = ['GND']
|
||||
|
||||
|
||||
def net_order(parts):
|
||||
pads_by_net = {}
|
||||
for p in parts.values():
|
||||
for pad in p.pads():
|
||||
if pad.net:
|
||||
pads_by_net.setdefault(pad.net, []).append(pad)
|
||||
rest = [n for n in D.NETS if n not in ORDER_FIRST and n not in ORDER_LAST]
|
||||
|
||||
def span(n):
|
||||
xs = [p.x for p in pads_by_net[n]]
|
||||
ys = [p.y for p in pads_by_net[n]]
|
||||
return (max(xs) - min(xs)) + (max(ys) - min(ys))
|
||||
rest.sort(key=span)
|
||||
return ORDER_FIRST + rest + ORDER_LAST, pads_by_net
|
||||
|
||||
|
||||
def seg_cells(x1, y1, x2, y2, L):
|
||||
n = max(1, int(math.hypot(x2 - x1, y2 - y1) / (R.G / 2)))
|
||||
out = set()
|
||||
for i in range(n + 1):
|
||||
t = i / n
|
||||
ix, iy = int(round((x1 + t * (x2 - x1)) / R.G)), int(round((y1 + t * (y2 - y1)) / R.G))
|
||||
out.add((L, ix, iy))
|
||||
return out
|
||||
|
||||
|
||||
def route_all(parts, order, pads_by_net, verbose=True):
|
||||
widths = set(WIDTH.values()) | set(FALLBACK) | {0.4}
|
||||
radii = {radius(w) for w in widths} | {via_radius(w) for w in widths}
|
||||
rt = R.Router(D.BOARD_W, D.BOARD_H, radii)
|
||||
rt.block_edges()
|
||||
rt.block_rect(18.75, 56.9, 67.25, D.BOARD_H) # ESP32 antenna keep-out
|
||||
for p in parts.values():
|
||||
for pad in p.pads():
|
||||
rt.add_pad(pad)
|
||||
rt.keep_vias_off_pad(pad, VIA_PAD_GAP)
|
||||
pre_components = {} # net -> list of cell sets (each one electrically connected)
|
||||
for net, pr in PREROUTE.items():
|
||||
own = rt.net_cells.setdefault(net, set())
|
||||
items = [] # (cells, endpoints)
|
||||
for (x, y) in pr.get('vias', []):
|
||||
rt.add_via(x, y, net)
|
||||
ix, iy = rt.cell(x, y)
|
||||
items.append(({(0, ix, iy), (1, ix, iy)}, {(round(x, 3), round(y, 3))}))
|
||||
for (x1, y1, x2, y2, w, L) in pr['tracks']:
|
||||
rt.add_track(x1, y1, x2, y2, w, L, net)
|
||||
items.append((seg_cells(x1, y1, x2, y2, L), {(round(x1, 3), round(y1, 3)), (round(x2, 3), round(y2, 3))}))
|
||||
# union items sharing an endpoint
|
||||
comps = []
|
||||
for cells, ends in items:
|
||||
merged = [c for c in comps if c[1] & ends]
|
||||
for c in merged:
|
||||
comps.remove(c)
|
||||
cells = cells | c[0]
|
||||
ends = ends | c[1]
|
||||
comps.append((cells, ends))
|
||||
ports = pr.get('ports')
|
||||
pc = []
|
||||
for c in comps:
|
||||
own |= c[0]
|
||||
if ports:
|
||||
pcells = set()
|
||||
for (px, py, Ls) in ports:
|
||||
ix, iy = rt.cell(px, py)
|
||||
if any((L, ix, iy) in c[0] for L in (0, 1)) or any(abs(px - e[0]) < 1e-6 and abs(py - e[1]) < 1e-6 for e in c[1]):
|
||||
for L in Ls:
|
||||
pcells.add((L, ix, iy))
|
||||
pc.append((c[0], pcells if pcells else None))
|
||||
else:
|
||||
pc.append((c[0], None))
|
||||
pre_components[net] = pc
|
||||
|
||||
failures = []
|
||||
stats = {}
|
||||
t0 = time.time()
|
||||
for net in order:
|
||||
pads = pads_by_net[net]
|
||||
w0 = WIDTH[net]
|
||||
# terminals: pre-routed components (with the pads they touch) and the remaining pads
|
||||
terms = []
|
||||
used = set()
|
||||
for comp, port_cells in pre_components.get(net, []):
|
||||
cells = set(comp)
|
||||
pts = []
|
||||
for p in pads:
|
||||
pc = rt.pad_cells(p)
|
||||
if pc & comp:
|
||||
cells |= pc
|
||||
used.add(id(p))
|
||||
pts.append((p.x, p.y))
|
||||
if port_cells:
|
||||
cells = set(port_cells)
|
||||
c = next(iter(port_cells))
|
||||
pts = [(c[1] * R.G, c[2] * R.G)]
|
||||
if not pts:
|
||||
c = next(iter(comp))
|
||||
pts = [(c[1] * R.G, c[2] * R.G)]
|
||||
terms.append((cells, pts[0][0], pts[0][1], f'pre:{net}'))
|
||||
for p in pads:
|
||||
if id(p) not in used:
|
||||
terms.append((rt.pad_cells(p), p.x, p.y, f'{p.ref}.{p.num}'))
|
||||
first = min(terms, key=lambda t: (t[1] + t[2]))
|
||||
tree = set(first[0])
|
||||
remaining = [t for t in terms if t is not first]
|
||||
nseg = nvia = 0
|
||||
while remaining:
|
||||
sample = list(tree)[::max(1, len(tree) // 400)]
|
||||
|
||||
def dist_to_tree(t):
|
||||
return min(abs(t[1] - c[1] * R.G) + abs(t[2] - c[2] * R.G) for c in sample)
|
||||
t = min(remaining, key=dist_to_tree)
|
||||
targets = t[0]
|
||||
remaining.remove(t)
|
||||
if targets & tree: # already connected (pads touching / overlapping)
|
||||
tree |= targets
|
||||
continue
|
||||
path = None
|
||||
tries = [w0] if net in NO_FALLBACK else [w0] + [fw for fw in FALLBACK if fw < w0]
|
||||
for w in tries:
|
||||
path = rt.search(net, radius(w), via_radius(w), tree, targets, t[1], t[2])
|
||||
if path:
|
||||
break
|
||||
if not path:
|
||||
failures.append((net, t[3]))
|
||||
continue
|
||||
a, b = rt.commit_path(net, path, w)
|
||||
nseg += a; nvia += b
|
||||
tree |= set(path) | targets
|
||||
rt.net_cells[net] |= targets
|
||||
stats[net] = (nseg, nvia)
|
||||
if verbose:
|
||||
print(f' {net:10s} pads={len(pads):2d} segs={nseg:3d} vias={nvia:2d} t={time.time() - t0:6.1f}s', flush=True)
|
||||
return rt, failures
|
||||
|
||||
|
||||
def stitch_vias(rt, pitch=4.0, dense=((33.0, 37.0, 53.5, 56.5, 2.0),)):
|
||||
"""GND stitching vias: one per pitch x pitch block wherever a via fits and the ground pour
|
||||
reaches it (connected) on both layers. Denser blocks under the ESP32 module."""
|
||||
import pour
|
||||
r_map = radius(0.2)
|
||||
k = rt.nid('GND')
|
||||
nx = rt.nx
|
||||
keep = {}
|
||||
for L in (0, 1):
|
||||
mask = pour.fill_mask(rt, 'GND', L, r_map, open_cells=2)
|
||||
anchors = [iy * nx + ix for (cl, ix, iy) in rt.net_cells.get('GND', ()) if cl == L]
|
||||
anchors += [rt.cell(x, y)[1] * nx + rt.cell(x, y)[0] for (x, y, n) in rt.vias if n == 'GND']
|
||||
keep[L] = pour.keep_connected(mask, nx, rt.ny, anchors)
|
||||
mv = rt.maps[via_radius(0.2)]
|
||||
gnd_vias = [(x, y) for (x, y, n) in rt.vias if n == 'GND']
|
||||
|
||||
def ok(ix, iy, min_gap):
|
||||
if ix < 1 or iy < 1 or ix >= nx - 1 or iy >= rt.ny - 1:
|
||||
return False
|
||||
i = iy * nx + ix
|
||||
if rt.no_via[i] or not (keep[0][i] and keep[1][i]):
|
||||
return False
|
||||
if any(mv[L][i] not in (-1, k) for L in (0, 1)):
|
||||
return False
|
||||
x, y = rt.xy(ix, iy)
|
||||
return all(math.hypot(x - vx, y - vy) >= min_gap for vx, vy in gnd_vias)
|
||||
|
||||
def blocks(x0, y0, x1, y1, p):
|
||||
s = int(round(p / R.G))
|
||||
for by in range(int(y0 / R.G), int(y1 / R.G), s):
|
||||
for bx in range(int(x0 / R.G), int(x1 / R.G), s):
|
||||
yield bx, by, s
|
||||
|
||||
added = 0
|
||||
passes = [(0.0, 0.0, D.BOARD_W, D.BOARD_H, pitch)] + list(dense)
|
||||
for (x0, y0, x1, y1, p) in passes:
|
||||
for bx, by, s in blocks(x0, y0, x1, y1, p):
|
||||
cx, cy = bx + s // 2, by + s // 2
|
||||
cands = sorted(((ix, iy) for iy in range(by, by + s) for ix in range(bx, bx + s)),
|
||||
key=lambda c: (c[0] - cx) ** 2 + (c[1] - cy) ** 2)
|
||||
for ix, iy in cands:
|
||||
if ok(ix, iy, p * 0.6):
|
||||
x, y = rt.xy(ix, iy)
|
||||
rt.add_via(round(x, 3), round(y, 3), 'GND')
|
||||
gnd_vias.append((x, y))
|
||||
added += 1
|
||||
break
|
||||
print(f' stitching vias: {added}')
|
||||
return added
|
||||
|
||||
|
||||
def gnd_pours(rt, parts):
|
||||
"""GND copper pour on both layers, filled by pour.py (clearance 0.3 mm)."""
|
||||
import pour
|
||||
from board import uid, fmt, net_list
|
||||
from sexpr import QStr
|
||||
OX, OY = D.ORIGIN
|
||||
k = rt.nid('GND')
|
||||
r_map = radius(0.2) # 0.38 mm: 0.3 pour clearance + half cell diagonal
|
||||
net_ids = net_list()
|
||||
zones = []
|
||||
for L, lname in ((1, 'B.Cu'), (0, 'F.Cu')):
|
||||
mask = pour.fill_mask(rt, 'GND', L, r_map, open_cells=2)
|
||||
# anchors: GND copper cells on this layer (pads, tracks, vias)
|
||||
anchors = []
|
||||
for (cl, ix, iy) in rt.net_cells.get('GND', ()):
|
||||
if cl == L:
|
||||
anchors.append(iy * rt.nx + ix)
|
||||
for (x, y, n) in rt.vias:
|
||||
if n == 'GND':
|
||||
ix, iy = rt.cell(x, y)
|
||||
anchors.append(iy * rt.nx + ix)
|
||||
keep = pour.keep_connected(mask, rt.nx, rt.ny, anchors)
|
||||
polys = pour.polygons(keep, rt.nx, rt.ny)
|
||||
filled = []
|
||||
for poly in polys:
|
||||
pts = pour.to_mm(poly, R.G)
|
||||
filled.append(['filled_polygon', ['layer', QStr(lname)],
|
||||
['pts'] + [['xy', fmt(x + OX), fmt(y + OY)] for x, y in pts]])
|
||||
cells = sum(keep)
|
||||
print(f' GND pour {lname}: {len(polys)} islands, {cells * R.G * R.G:.0f} mm2')
|
||||
inset = 0.25
|
||||
outline = [(inset, inset), (D.BOARD_W - inset, inset), (D.BOARD_W - inset, D.BOARD_H - inset), (inset, D.BOARD_H - inset)]
|
||||
zones.append(['zone', ['net', str(net_ids['GND'])], ['net_name', QStr('/GND')], ['layer', QStr(lname)],
|
||||
['uuid', QStr(uid('gndzone', lname))], ['name', QStr('GND_' + lname[0])],
|
||||
['hatch', 'edge', '0.5'], ['connect_pads', 'yes', ['clearance', '0.3']],
|
||||
['min_thickness', '0.25'], ['filled_areas_thickness', 'no'],
|
||||
['fill', 'yes', ['thermal_gap', '0.5'], ['thermal_bridge_width', '0.5']],
|
||||
['polygon', ['pts'] + [['xy', fmt(x + OX), fmt(y + OY)] for x, y in outline]]] + filled)
|
||||
return zones
|
||||
|
||||
|
||||
def silk_labels():
|
||||
T = []
|
||||
T.append(text_node('SANDTABLE', 53.8, 27.6, 'F.SilkS', size=1.1, bold=True))
|
||||
T.append(text_node(f'CTRL v{D.VERSION}', 53.8, 29.6, 'F.SilkS', size=1.0))
|
||||
for txt, x, y, ang in (('SPIN', 4.5, 15.2, 0), ('12V', 2.4, 17.6, 0), ('USB', 2.2, 37.3, 0),
|
||||
('ARM', 71.0, 7.15, 90), ('HOME', 71.0, 21.0, 90), ('HALL', 71.4, 33.6, 90),
|
||||
('LED', 71.0, 43.2, 90), ('EXP', 74.6, 55.8, 90)):
|
||||
T.append(text_node(txt, x, y, 'F.SilkS', size=0.9, angle=ang))
|
||||
# driver orientation: match the pin names printed on the DRV8825 module
|
||||
for x1 in (38.1, 59.1): # A1 / A2 pin-1 x; pins step 2.54 mm to the left
|
||||
T.append(text_node('VMOT', x1 - 7 * 2.54, 3.5, 'F.SilkS', size=0.8))
|
||||
T.append(text_node('EN', x1 - 7 * 2.54, 13.0, 'F.SilkS', size=0.8))
|
||||
T.append(text_node('DIR', x1, 13.0, 'F.SilkS', size=0.8))
|
||||
# back side: name plate
|
||||
T.append(text_node(f'SANDTABLE CONTROLLER v{D.VERSION}', 40.0, 30.0, 'B.SilkS', size=1.6, bold=True))
|
||||
T.append(text_node('ESP32 + 2x DRV8825 | 12V in | FluidNC', 40.0, 33.0, 'B.SilkS', size=1.0))
|
||||
T.append(text_node('lazypugs 2026', 40.0, 35.5, 'B.SilkS', size=1.0))
|
||||
# JP1 (under the theta driver): bridge only for A4988 modules
|
||||
jx, jy = D.PARTS['JP1'][3], D.PARTS['JP1'][4]
|
||||
T.append(text_node('A4988 ONLY', jx, jy + 1.9, 'F.SilkS', size=0.7))
|
||||
T.append(text_node('WayWayWay', 40.0, 38.0, 'B.SilkS', size=1.0)) # PCBWay prints its order number here
|
||||
return T
|
||||
|
||||
|
||||
def main():
|
||||
parts = build()
|
||||
probs = check_placement(parts)
|
||||
for p in probs:
|
||||
print(' !', p)
|
||||
order, pads_by_net = net_order(parts)
|
||||
rt, failures = route_all(parts, order, pads_by_net)
|
||||
print(f'tracks={len(rt.tracks)} vias={len(rt.vias)} failures={len(failures)}')
|
||||
crowded = [(x, y, pd.ref, pd.num) for (x, y, n) in rt.vias for p in parts.values() for pd in p.pads()
|
||||
if pd.layers and pd.kind != 'np_thru_hole' and pd.dist(x, y) < R.VIA_D / 2 + VIA_PAD_GAP - 1e-6]
|
||||
print(f'vias closer than {VIA_PAD_GAP} mm to a pad: {len(crowded)}', crowded[:6])
|
||||
for f in failures:
|
||||
print(' FAILED', f)
|
||||
stitch_vias(rt)
|
||||
tracks = [(x1, y1, x2, y2, w, L, n) for (x1, y1, x2, y2, w, L, n) in rt.tracks]
|
||||
zones = [keepout_zone('ANTENNA', 18.75, 56.9, 67.25, D.BOARD_H)]
|
||||
zones += gnd_pours(rt, parts)
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
write_pcb(os.path.join(OUT, 'sandtable-controller.kicad_pcb'), parts, tracks=tracks, vias=rt.vias,
|
||||
zones=zones, texts=silk_labels())
|
||||
write_project(os.path.join(OUT, 'sandtable-controller.kicad_pro'), 'sandtable-controller')
|
||||
return failures
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,289 @@
|
||||
"""A small two-layer grid router (A* with turn penalties, vias, per-net widths).
|
||||
|
||||
Obstacle model: for every distinct "inflation radius" R (track half-width +
|
||||
clearance + discretisation margin) we keep, per layer, an owner map:
|
||||
-1 = free, k >= 0 = only copper of net k is within R, -2 = blocked for everyone.
|
||||
A cell is usable by net k iff its owner is -1 or k.
|
||||
"""
|
||||
import heapq
|
||||
import math
|
||||
from array import array
|
||||
|
||||
G = 0.1 # grid pitch, mm
|
||||
CLEAR = 0.2 # copper-copper clearance used by the router (DRC default is 0.15)
|
||||
EDGE = 0.35 # copper-to-board-edge clearance
|
||||
MARGIN = 0.08 # discretisation safety margin
|
||||
VIA_D = 0.6
|
||||
COST_STEP = 10
|
||||
COST_DIAG = 14
|
||||
COST_VIA = 90
|
||||
COST_TURN = 4
|
||||
COST_BCU = 1 # tiny extra per step on the bottom layer (keeps B.Cu freer for GND)
|
||||
|
||||
DIRS = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)]
|
||||
|
||||
|
||||
def seg_dist(px, py, x1, y1, x2, y2):
|
||||
dx, dy = x2 - x1, y2 - y1
|
||||
L2 = dx * dx + dy * dy
|
||||
if L2 == 0:
|
||||
return math.hypot(px - x1, py - y1)
|
||||
t = max(0.0, min(1.0, ((px - x1) * dx + (py - y1) * dy) / L2))
|
||||
return math.hypot(px - (x1 + t * dx), py - (y1 + t * dy))
|
||||
|
||||
|
||||
class Router:
|
||||
def __init__(self, width, height, radii):
|
||||
self.W, self.H = width, height
|
||||
self.nx = int(round(width / G)) + 1
|
||||
self.ny = int(round(height / G)) + 1
|
||||
self.radii = sorted(set(round(r, 3) for r in radii))
|
||||
n = self.nx * self.ny
|
||||
self.maps = {r: [array('h', [-1]) * n, array('h', [-1]) * n] for r in self.radii}
|
||||
self.net_index = {}
|
||||
self.objects = [] # (kind, data, net) for re-rasterising
|
||||
self.tracks = [] # (x1,y1,x2,y2,w,layer,net)
|
||||
self.vias = [] # (x,y,net)
|
||||
self.net_cells = {} # net -> set of (layer, ix, iy) owned copper cells (tree)
|
||||
self.no_via = bytearray(n) # 1 = a via centred here would touch or crowd a pad (any net)
|
||||
|
||||
# ------------------------------------------------------------ helpers
|
||||
def nid(self, net):
|
||||
if net is None:
|
||||
return -2
|
||||
if net not in self.net_index:
|
||||
self.net_index[net] = len(self.net_index)
|
||||
return self.net_index[net]
|
||||
|
||||
def cell(self, x, y):
|
||||
return int(round(x / G)), int(round(y / G))
|
||||
|
||||
def xy(self, ix, iy):
|
||||
return ix * G, iy * G
|
||||
|
||||
def _mark(self, layer_ids, net, x0, y0, x1, y1, inside):
|
||||
"""inside(px,py,R) -> True if cell centre is within R of the object."""
|
||||
k = self.nid(net)
|
||||
for r in self.radii:
|
||||
ix0 = max(0, int((x0 - r) / G) - 1)
|
||||
iy0 = max(0, int((y0 - r) / G) - 1)
|
||||
ix1 = min(self.nx - 1, int((x1 + r) / G) + 1)
|
||||
iy1 = min(self.ny - 1, int((y1 + r) / G) + 1)
|
||||
for L in layer_ids:
|
||||
m = self.maps[r][L]
|
||||
for iy in range(iy0, iy1 + 1):
|
||||
py = iy * G
|
||||
base = iy * self.nx
|
||||
for ix in range(ix0, ix1 + 1):
|
||||
idx = base + ix
|
||||
v = m[idx]
|
||||
if v == -2 or v == k:
|
||||
continue
|
||||
if inside(ix * G, py, r):
|
||||
m[idx] = k if v == -1 else -2
|
||||
|
||||
def add_pad(self, pad):
|
||||
layers = [0 if l == 'F.Cu' else 1 for l in pad.layers]
|
||||
if pad.kind == 'np_thru_hole':
|
||||
layers = [0, 1]
|
||||
hole = pad.drill[0] / 2 if pad.drill else pad.hx
|
||||
cx, cy = pad.x, pad.y
|
||||
self._mark(layers, None, cx - hole, cy - hole, cx + hole, cy + hole,
|
||||
lambda px, py, r: math.hypot(px - cx, py - cy) < hole + r)
|
||||
return
|
||||
net = pad.net
|
||||
self._mark(layers, net, pad.x - pad.hx, pad.y - pad.hy, pad.x + pad.hx, pad.y + pad.hy,
|
||||
lambda px, py, r: pad.dist(px, py) < r)
|
||||
# drilled THT pads also block the other layer for different nets via the copper ring (both layers)
|
||||
|
||||
def keep_vias_off_pad(self, pad, gap):
|
||||
"""Forbid via centres within (via radius + gap) of this pad's copper, whatever its net.
|
||||
A via in or touching a pad lets solder drain into the hole during assembly."""
|
||||
if pad.kind == 'np_thru_hole' or not pad.layers:
|
||||
return
|
||||
r = VIA_D / 2 + gap
|
||||
ix0, iy0 = max(0, int((pad.x - pad.hx - r) / G) - 1), max(0, int((pad.y - pad.hy - r) / G) - 1)
|
||||
ix1, iy1 = min(self.nx - 1, int((pad.x + pad.hx + r) / G) + 1), min(self.ny - 1, int((pad.y + pad.hy + r) / G) + 1)
|
||||
for iy in range(iy0, iy1 + 1):
|
||||
for ix in range(ix0, ix1 + 1):
|
||||
if pad.dist(ix * G, iy * G) < r:
|
||||
self.no_via[iy * self.nx + ix] = 1
|
||||
|
||||
def add_track(self, x1, y1, x2, y2, w, layer, net):
|
||||
hw = w / 2
|
||||
self._mark([layer], net, min(x1, x2) - hw, min(y1, y2) - hw, max(x1, x2) + hw, max(y1, y2) + hw,
|
||||
lambda px, py, r: seg_dist(px, py, x1, y1, x2, y2) < hw + r)
|
||||
self.tracks.append((x1, y1, x2, y2, w, 'F.Cu' if layer == 0 else 'B.Cu', net))
|
||||
|
||||
def add_via(self, x, y, net):
|
||||
rv = VIA_D / 2
|
||||
self._mark([0, 1], net, x - rv, y - rv, x + rv, y + rv,
|
||||
lambda px, py, r: math.hypot(px - x, py - y) < rv + r)
|
||||
self.vias.append((x, y, net))
|
||||
|
||||
def block_edges(self):
|
||||
for r in self.radii:
|
||||
lim = EDGE + r - CLEAR # r already includes clearance; edge rule replaces it
|
||||
n = int(math.ceil(lim / G)) # cells with index < n (or > max-n) are too close
|
||||
for L in (0, 1):
|
||||
m = self.maps[r][L]
|
||||
for iy in range(self.ny):
|
||||
base = iy * self.nx
|
||||
if iy < n or iy > self.ny - 1 - n:
|
||||
for ix in range(self.nx):
|
||||
m[base + ix] = -2
|
||||
else:
|
||||
for ix in list(range(n)) + list(range(self.nx - n, self.nx)):
|
||||
m[base + ix] = -2
|
||||
|
||||
def block_rect(self, x0, y0, x1, y1):
|
||||
"""Keep-out: nothing may enter (tracks are inflated by their radius)."""
|
||||
for r in self.radii:
|
||||
for L in (0, 1):
|
||||
m = self.maps[r][L]
|
||||
hw = r - CLEAR - MARGIN # track half width
|
||||
for iy in range(max(0, int((y0 - hw) / G)), min(self.ny, int((y1 + hw) / G) + 2)):
|
||||
for ix in range(max(0, int((x0 - hw) / G)), min(self.nx, int((x1 + hw) / G) + 2)):
|
||||
px, py = ix * G, iy * G
|
||||
if x0 - hw - CLEAR <= px <= x1 + hw + CLEAR and y0 - hw - CLEAR <= py <= y1 + hw + CLEAR:
|
||||
m[iy * self.nx + ix] = -2
|
||||
|
||||
# ------------------------------------------------------------ search
|
||||
def pad_cells(self, pad):
|
||||
layers = [0 if l == 'F.Cu' else 1 for l in pad.layers]
|
||||
cells = set()
|
||||
ix0, iy0 = int((pad.x - pad.hx) / G), int((pad.y - pad.hy) / G)
|
||||
ix1, iy1 = int((pad.x + pad.hx) / G) + 1, int((pad.y + pad.hy) / G) + 1
|
||||
for iy in range(iy0, iy1 + 1):
|
||||
for ix in range(ix0, ix1 + 1):
|
||||
if pad.dist(ix * G, iy * G) <= 1e-9:
|
||||
for L in layers:
|
||||
cells.add((L, ix, iy))
|
||||
if not cells:
|
||||
ix, iy = self.cell(pad.x, pad.y)
|
||||
for L in layers:
|
||||
cells.add((L, ix, iy))
|
||||
return cells
|
||||
|
||||
def search(self, net, r_track, r_via, sources, targets, tx, ty, max_expand=1500000):
|
||||
k = self.nid(net)
|
||||
nx = self.nx
|
||||
mt = self.maps[r_track]
|
||||
mv = self.maps[r_via]
|
||||
own = self.net_cells.setdefault(net, set())
|
||||
tset = targets
|
||||
txi, tyi = tx / G, ty / G
|
||||
|
||||
def free(L, ix, iy):
|
||||
if ix < 0 or iy < 0 or ix >= nx or iy >= self.ny:
|
||||
return False
|
||||
if (L, ix, iy) in tset:
|
||||
return True
|
||||
v = mt[L][iy * nx + ix]
|
||||
return v == -1 or v == k
|
||||
|
||||
def via_ok(ix, iy):
|
||||
if self.no_via[iy * nx + ix]:
|
||||
return False
|
||||
for L in (0, 1):
|
||||
v = mv[L][iy * nx + ix]
|
||||
if not (v == -1 or v == k):
|
||||
return False
|
||||
return True
|
||||
|
||||
def h(ix, iy):
|
||||
dx, dy = abs(ix - txi), abs(iy - tyi)
|
||||
return int(COST_STEP * (max(dx, dy) - min(dx, dy)) + COST_DIAG * min(dx, dy))
|
||||
|
||||
openh = []
|
||||
g = {}
|
||||
came = {}
|
||||
for (L, ix, iy) in sources:
|
||||
s = (L, ix, iy, 8)
|
||||
g[s] = 0
|
||||
heapq.heappush(openh, (h(ix, iy), 0, s))
|
||||
expanded = 0
|
||||
while openh:
|
||||
f, gc, s = heapq.heappop(openh)
|
||||
if g.get(s, 1 << 60) < gc:
|
||||
continue
|
||||
L, ix, iy, d = s
|
||||
if (L, ix, iy) in tset:
|
||||
path = [(L, ix, iy)]
|
||||
while s in came:
|
||||
s = came[s]
|
||||
path.append(s[:3])
|
||||
path.reverse()
|
||||
return path
|
||||
expanded += 1
|
||||
if expanded > max_expand:
|
||||
return None
|
||||
for nd, (dx, dy) in enumerate(DIRS):
|
||||
jx, jy = ix + dx, iy + dy
|
||||
if not free(L, jx, jy):
|
||||
continue
|
||||
if dx and dy:
|
||||
# don't cut corners between two blocked orthogonal neighbours
|
||||
if not (free(L, ix + dx, iy) or free(L, ix, iy + dy)):
|
||||
continue
|
||||
step = COST_DIAG
|
||||
else:
|
||||
step = COST_STEP
|
||||
if d != 8 and nd != d:
|
||||
step += COST_TURN * (1 if (nd - d) % 8 in (1, 7) else 3)
|
||||
if L == 1:
|
||||
step += COST_BCU
|
||||
ns = (L, jx, jy, nd)
|
||||
ng = gc + step
|
||||
if ng < g.get(ns, 1 << 60):
|
||||
g[ns] = ng
|
||||
came[ns] = s
|
||||
heapq.heappush(openh, (ng + h(jx, jy), ng, ns))
|
||||
# via
|
||||
if via_ok(ix, iy):
|
||||
ns = (1 - L, ix, iy, 8)
|
||||
ng = gc + COST_VIA
|
||||
if ng < g.get(ns, 1 << 60) and free(1 - L, ix, iy):
|
||||
g[ns] = ng
|
||||
came[ns] = s
|
||||
heapq.heappush(openh, (ng + h(ix, iy), ng, ns))
|
||||
return None
|
||||
|
||||
def commit_path(self, net, path, w, start_xy=None, end_xy=None):
|
||||
"""Turn a cell path into merged segments + vias, add them as obstacles."""
|
||||
own = self.net_cells.setdefault(net, set())
|
||||
pts = [] # (layer, x, y)
|
||||
for (L, ix, iy) in path:
|
||||
pts.append((L, ix * G, iy * G))
|
||||
own.add((L, ix, iy))
|
||||
segs = []
|
||||
vias = []
|
||||
run = [pts[0]]
|
||||
for p in pts[1:]:
|
||||
if p[0] != run[-1][0]:
|
||||
vias.append((p[1], p[2]))
|
||||
if len(run) > 1:
|
||||
segs.append(run)
|
||||
run = [p]
|
||||
else:
|
||||
run.append(p)
|
||||
if len(run) > 1:
|
||||
segs.append(run)
|
||||
out = []
|
||||
for run in segs:
|
||||
# merge collinear points
|
||||
simp = [run[0]]
|
||||
for i in range(1, len(run) - 1):
|
||||
a, b, c = simp[-1], run[i], run[i + 1]
|
||||
d1 = (round((b[1] - a[1]) / G), round((b[2] - a[2]) / G))
|
||||
d2 = (round((c[1] - b[1]) / G), round((c[2] - b[2]) / G))
|
||||
if d1[0] * d2[1] - d1[1] * d2[0] != 0 or d1[0] * d2[0] + d1[1] * d2[1] < 0:
|
||||
simp.append(b)
|
||||
simp.append(run[-1])
|
||||
for a, b in zip(simp, simp[1:]):
|
||||
out.append((a[1], a[2], b[1], b[2], w, a[0], net))
|
||||
for (x, y) in vias:
|
||||
self.add_via(x, y, net)
|
||||
for (x1, y1, x2, y2, ww, L, n) in out:
|
||||
self.add_track(x1, y1, x2, y2, ww, L, n)
|
||||
return len(out), len(vias)
|
||||
@@ -0,0 +1,271 @@
|
||||
"""Generate the KiCad schematic from design.py (label-style: every pin gets a stub + net label)."""
|
||||
import copy
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
import design as D
|
||||
from board import uid, fmt
|
||||
from kilib import load_symbol, sym_pins
|
||||
from sexpr import QStr, dumps, find, findall
|
||||
|
||||
OUT = os.path.expanduser('~/kicad/sandtable-controller')
|
||||
NAME = 'sandtable-controller'
|
||||
ROOT = uid('rootsheet')
|
||||
STUB = 2.54
|
||||
LIBNICK = 'SandTable'
|
||||
|
||||
# custom symbols kept in the project library
|
||||
CUSTOM = {
|
||||
'Driver_Motor:Pololu_Breakout_A4988': ('DRV8825_StepStick', {
|
||||
'2': ('VDD/~{FAULT}', 'passive'), '3': ('A2', None), '4': ('A1', None), '5': ('B1', None), '6': ('B2', None),
|
||||
'9': ('~{EN}', None), '10': ('M0', None), '11': ('M1', None), '12': ('M2', None),
|
||||
'13': ('~{RST}', None), '14': ('~{SLP}', None)}),
|
||||
'Interface_USB:CH340C': ('CH340C_3V3', {'4': ('V3', 'passive')}),
|
||||
}
|
||||
|
||||
GROUPS = [
|
||||
('POWER INPUT 12V -> 5V', (12.7, 25.4, 287.02, 104.14), ['J1', 'F1', 'D1', 'C3', 'PS1', 'C4', 'D2', '#FLG1', '#FLG2', '#FLG3']),
|
||||
('3.3V', (294.64, 25.4, 398.78, 104.14), ['U3', 'C5', 'C6']),
|
||||
('SENSORS / LED / EXPANSION', (406.4, 25.4, 579.12, 238.76), ['J5', 'R9', 'R10', 'C13', 'D6', 'J6', 'R11', 'R12', 'C14', 'D7', 'U4', 'C15', 'R13', 'J7', 'J8']),
|
||||
('USB-C + USB-SERIAL + AUTO-RESET', (12.7, 116.84, 190.5, 264.16), ['J2', 'U5', 'R1', 'R2', 'D3', 'U2', 'C7', 'Q1', 'R3', 'Q2', 'R4', '#FLG4']),
|
||||
('ESP32', (198.12, 116.84, 398.78, 264.16), ['U1', 'C9', 'C10', 'R5', 'C8', 'SW1', 'R6', 'SW2', 'R8', 'R14', 'D4', 'R15', 'D5']),
|
||||
('STEPPER DRIVERS (DRV8825, 1/32 step: M0-M2, RST, SLP tied high; JP1 bridged only for A4988)', (12.7, 276.86, 398.78, 406.4), ['A1', 'C1', 'C16', 'J3', 'A2', 'C2', 'C17', 'J4', 'JP1']),
|
||||
('MOUNTING (Arduino Uno pattern)', (406.4, 251.46, 579.12, 299.72), ['H1', 'H2', 'H3', 'H4']),
|
||||
]
|
||||
FLAGS = {'#FLG1': 'GND', '#FLG2': '+12V', '#FLG3': '+5V', '#FLG4': 'VBUS'}
|
||||
|
||||
|
||||
def snap(v, g=2.54):
|
||||
return round(v / g) * g
|
||||
|
||||
|
||||
def symbol_def(lib_id):
|
||||
"""(lib_id used in the schematic, symbol definition list)."""
|
||||
sym = copy.deepcopy(load_symbol(lib_id))
|
||||
lib_name = str(sym[1])
|
||||
if lib_id in CUSTOM:
|
||||
new, renames = CUSTOM[lib_id]
|
||||
for unit in findall(sym, 'symbol'):
|
||||
unit[1] = QStr(str(unit[1]).replace(lib_name, new, 1))
|
||||
for p in findall(unit, 'pin'):
|
||||
num = str(find(p, 'number')[1])
|
||||
if num in renames:
|
||||
nm, typ = renames[num]
|
||||
find(p, 'name')[1] = QStr(nm)
|
||||
if typ:
|
||||
p[1] = typ
|
||||
full = f'{LIBNICK}:{new}'
|
||||
sym[1] = QStr(full)
|
||||
return full, sym, new
|
||||
sym[1] = QStr(lib_id)
|
||||
return lib_id, sym, None
|
||||
|
||||
|
||||
def bbox(sym):
|
||||
xs, ys = [], []
|
||||
for unit in findall(sym, 'symbol'):
|
||||
for g in unit:
|
||||
if not isinstance(g, list):
|
||||
continue
|
||||
if g[0] == 'rectangle':
|
||||
s, e = find(g, 'start'), find(g, 'end')
|
||||
xs += [float(s[1]), float(e[1])]; ys += [float(s[2]), float(e[2])]
|
||||
elif g[0] in ('polyline',):
|
||||
for xy in findall(find(g, 'pts'), 'xy'):
|
||||
xs.append(float(xy[1])); ys.append(float(xy[2]))
|
||||
elif g[0] == 'circle':
|
||||
c = find(g, 'center'); r = float(find(g, 'radius')[1])
|
||||
xs += [float(c[1]) - r, float(c[1]) + r]; ys += [float(c[2]) - r, float(c[2]) + r]
|
||||
for p in sym_pins(sym):
|
||||
xs.append(p['x']); ys.append(p['y'])
|
||||
if not xs:
|
||||
return (-2.54, -2.54, 2.54, 2.54)
|
||||
return (min(xs), min(ys), max(xs), max(ys))
|
||||
|
||||
|
||||
def pin_net_map():
|
||||
m = {}
|
||||
for net, pins in D.NETS.items():
|
||||
for p in pins:
|
||||
m[p] = net
|
||||
return m
|
||||
|
||||
|
||||
def outward(angle):
|
||||
"""Unit vector (schematic coords, y down) pointing away from the symbol body."""
|
||||
a = int(round(angle)) % 360
|
||||
return {0: (-1, 0), 180: (1, 0), 90: (0, 1), 270: (0, -1)}[a]
|
||||
|
||||
|
||||
def label_angle(o):
|
||||
return {(1, 0): 0, (-1, 0): 180, (0, -1): 90, (0, 1): 270}[o]
|
||||
|
||||
|
||||
def font(size=1.27, hide=False, justify=None, bold=False):
|
||||
f = ['font', ['size', fmt(size), fmt(size)]] + ([['bold', 'yes']] if bold else [])
|
||||
e = ['effects', f]
|
||||
if justify:
|
||||
e.append(['justify'] + justify)
|
||||
if hide:
|
||||
e.append(['hide', 'yes'])
|
||||
return e
|
||||
|
||||
|
||||
def main():
|
||||
pin_net = pin_net_map()
|
||||
nc = set(D.NO_CONNECT)
|
||||
items = [] # schematic body items
|
||||
lib_syms = {}
|
||||
custom_defs = {}
|
||||
instances = {} # ref -> (lib_id, sym, value, footprint, lcsc, descr)
|
||||
for ref, (sym_id, fp_id, value, *_rest) in D.PARTS.items():
|
||||
lcsc, descr = _rest[-2], _rest[-1]
|
||||
full, sym, custom = symbol_def(sym_id)
|
||||
lib_syms[full] = sym
|
||||
if custom:
|
||||
custom_defs[custom] = sym
|
||||
instances[ref] = (full, sym, value, fp_id, lcsc, descr)
|
||||
flag_full, flag_sym, _ = symbol_def('power:PWR_FLAG')
|
||||
lib_syms[flag_full] = flag_sym
|
||||
for f in FLAGS:
|
||||
instances[f] = (flag_full, flag_sym, 'PWR_FLAG', '', '', '')
|
||||
|
||||
# sanity: every pin of every part is either in a net or no-connect
|
||||
missing = []
|
||||
for ref, (full, sym, *_r) in instances.items():
|
||||
if ref.startswith('#'):
|
||||
continue
|
||||
for p in sym_pins(sym):
|
||||
key = f'{ref}.{p["num"]}'
|
||||
if key not in pin_net and key not in nc:
|
||||
missing.append(key)
|
||||
if missing:
|
||||
print('UNASSIGNED PINS:', missing)
|
||||
|
||||
texts = []
|
||||
for title, (x0, y0, x1, y1), refs in GROUPS:
|
||||
texts.append(['text', QStr(title), ['exclude_from_sim', 'no'], ['at', fmt(x0), fmt(y0 - 3.81), '0'],
|
||||
font(2.0, bold=True, justify=['left', 'bottom']), ['uuid', QStr(uid('grp', title))]])
|
||||
texts.append(['rectangle', ['start', fmt(x0 - 2.54), fmt(y0 - 2.54)], ['end', fmt(x1), fmt(y1)],
|
||||
['stroke', ['width', '0.2'], ['type', 'dash']], ['fill', ['type', 'none']],
|
||||
['uuid', QStr(uid('grprect', title))]])
|
||||
# pack symbols left->right, wrap
|
||||
cx, cy, row_h = x0 + 2.54, y0 + 2.54, 0
|
||||
for ref in refs:
|
||||
full, sym, value, fp_id, lcsc, descr = instances[ref]
|
||||
bx0, by0, bx1, by1 = bbox(sym)
|
||||
pins = sym_pins(sym)
|
||||
lab = max([len(pin_net.get(f'{ref}.{p["num"]}', '') or ('X' if ref.startswith('#') else '')) for p in pins] + [0])
|
||||
lab_w = STUB + 1.0 * 1.27 * lab + 1.27
|
||||
left = any(outward(p['angle'])[0] < 0 for p in pins)
|
||||
right = any(outward(p['angle'])[0] > 0 for p in pins)
|
||||
up = any(outward(p['angle'])[1] < 0 for p in pins)
|
||||
dn = any(outward(p['angle'])[1] > 0 for p in pins)
|
||||
w = (bx1 - bx0) + (lab_w if left else 2.54) + (lab_w if right else 2.54) + 5.08
|
||||
h = (by1 - by0) + (lab_w if up else 5.08) + (lab_w if dn else 5.08) + 2.54
|
||||
if cx + w > x1 and cx > x0 + 2.54:
|
||||
cx = x0 + 2.54
|
||||
cy += row_h
|
||||
row_h = 0
|
||||
# symbol origin so that its bbox sits inside the slot
|
||||
ox = snap(cx + (lab_w if left else 2.54) - bx0, 1.27)
|
||||
oy = snap(cy + (lab_w if up else 5.08) + by1, 1.27)
|
||||
instances[ref] = (full, sym, value, fp_id, lcsc, descr, ox, oy, (by1, by0))
|
||||
cx += w
|
||||
row_h = max(row_h, h)
|
||||
if cy + row_h > y1 + 0.1:
|
||||
print(f' ! group "{title}" overflows its box by {cy + row_h - y1:.1f} mm')
|
||||
|
||||
sym_nodes = []
|
||||
flag_count = 0
|
||||
for ref, inst in instances.items():
|
||||
full, sym, value, fp_id, lcsc, descr, ox, oy, (top, bot) = inst
|
||||
is_flag = ref.startswith('#')
|
||||
node = ['symbol', ['lib_id', QStr(full)], ['at', fmt(ox), fmt(oy), '0'], ['unit', '1'],
|
||||
['exclude_from_sim', 'no'], ['in_bom', 'no' if (is_flag or ref.startswith(('H', 'JP'))) else 'yes'],
|
||||
['on_board', 'no' if is_flag else 'yes'], ['dnp', 'no'],
|
||||
['uuid', QStr(uid('sym', ref))]]
|
||||
props = {str(p[1]): p for p in findall(sym, 'property')}
|
||||
ref_y = oy - top - 1.27
|
||||
val_y = oy - bot + 2.54
|
||||
node.append(['property', QStr('Reference'), QStr(ref), ['at', fmt(ox), fmt(ref_y), '0'],
|
||||
font(hide=is_flag)])
|
||||
node.append(['property', QStr('Value'), QStr(value), ['at', fmt(ox), fmt(val_y), '0'], font(hide=is_flag)])
|
||||
node.append(['property', QStr('Footprint'), QStr(fp_id), ['at', fmt(ox), fmt(oy), '0'], font(hide=True)])
|
||||
ds = props.get('Datasheet')
|
||||
node.append(['property', QStr('Datasheet'), QStr(str(ds[2]) if ds else ''), ['at', fmt(ox), fmt(oy), '0'], font(hide=True)])
|
||||
node.append(['property', QStr('Description'), QStr(descr), ['at', fmt(ox), fmt(oy), '0'], font(hide=True)])
|
||||
if lcsc:
|
||||
node.append(['property', QStr('LCSC'), QStr(lcsc), ['at', fmt(ox), fmt(oy), '0'], font(hide=True)])
|
||||
pins = sym_pins(sym)
|
||||
seen = set()
|
||||
for p in pins:
|
||||
if p['num'] in seen:
|
||||
continue
|
||||
seen.add(p['num'])
|
||||
node.append(['pin', QStr(p['num']), ['uuid', QStr(uid('pin', ref, p['num']))]])
|
||||
node.append(['instances', ['project', QStr(NAME), ['path', QStr('/' + ROOT), ['reference', QStr(ref)], ['unit', '1']]]])
|
||||
sym_nodes.append(node)
|
||||
# wires + labels / no-connects
|
||||
done = set()
|
||||
for p in pins:
|
||||
key = f'{ref}.{p["num"]}'
|
||||
px, py = ox + p['x'], oy - p['y']
|
||||
if (round(px, 3), round(py, 3)) in done:
|
||||
continue
|
||||
done.add((round(px, 3), round(py, 3)))
|
||||
net = FLAGS[ref] if is_flag else pin_net.get(key)
|
||||
if net is None:
|
||||
if key in nc or p['type'] == 'no_connect':
|
||||
items.append(['no_connect', ['at', fmt(px), fmt(py)], ['uuid', QStr(uid('nc', key, px, py))]])
|
||||
continue
|
||||
o = outward(p['angle'])
|
||||
qx, qy = px + STUB * o[0], py + STUB * o[1]
|
||||
items.append(['wire', ['pts', ['xy', fmt(px), fmt(py)], ['xy', fmt(qx), fmt(qy)]],
|
||||
['stroke', ['width', '0'], ['type', 'default']], ['uuid', QStr(uid('w', key, px, py))]])
|
||||
ang = label_angle(o)
|
||||
just = ['left', 'bottom'] if ang in (0, 90) else ['right', 'bottom']
|
||||
items.append(['label', QStr(net), ['at', fmt(qx), fmt(qy), str(ang)], font(justify=just),
|
||||
['uuid', QStr(uid('lbl', key, px, py))]])
|
||||
|
||||
root = ['kicad_sch', ['version', '20250114'], ['generator', QStr('eeschema')], ['generator_version', QStr('9.0')],
|
||||
['uuid', QStr(ROOT)], ['paper', QStr('A2')],
|
||||
['title_block', ['title', QStr('SandTable Controller')], ['date', QStr('2026-10-22')], ['rev', QStr(D.VERSION)],
|
||||
['company', QStr('lazypugs')],
|
||||
['comment', '1', QStr('ESP32-WROOM-32E (FluidNC) + 2x DRV8825 StepStick, 12V in')],
|
||||
['comment', '2', QStr('Drop-in for the Arduino+CNC shield bay in the sand table (Uno hole pattern)')]],
|
||||
['lib_symbols'] + list(lib_syms.values())]
|
||||
root += texts + items + sym_nodes
|
||||
root.append(['sheet_instances', ['path', QStr('/'), ['page', QStr('1')]]])
|
||||
root.append(['embedded_fonts', 'no'])
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
with open(os.path.join(OUT, NAME + '.kicad_sch'), 'w') as f:
|
||||
f.write(dumps(root) + '\n')
|
||||
# project symbol library with the custom symbols
|
||||
lib = ['kicad_symbol_lib', ['version', '20241209'], ['generator', QStr('sandtable-gen')], ['generator_version', QStr('9.0')]]
|
||||
for name, s in custom_defs.items():
|
||||
s2 = copy.deepcopy(s)
|
||||
s2[1] = QStr(name)
|
||||
lib.append(s2)
|
||||
with open(os.path.join(OUT, 'sandtable.kicad_sym'), 'w') as f:
|
||||
f.write(dumps(lib) + '\n')
|
||||
symlibs = sorted({sid.split(':')[0] for sid in [v[0] for v in D.PARTS.values()] + ['power:PWR_FLAG']})
|
||||
with open(os.path.join(OUT, 'sym-lib-table'), 'w') as f:
|
||||
f.write('(sym_lib_table\n (version 7)\n')
|
||||
f.write(' (lib (name "SandTable")(type "KiCad")(uri "${KIPRJMOD}/sandtable.kicad_sym")(options "")(descr "SandTable custom symbols"))\n')
|
||||
for lib in symlibs:
|
||||
f.write(f' (lib (name "{lib}")(type "KiCad")(uri "${{KICAD9_SYMBOL_DIR}}/{lib}.kicad_sym")(options "")(descr ""))\n')
|
||||
f.write(')\n')
|
||||
fplibs = sorted({v[1].split(':')[0] for v in D.PARTS.values()})
|
||||
with open(os.path.join(OUT, 'fp-lib-table'), 'w') as f:
|
||||
f.write('(fp_lib_table\n (version 7)\n')
|
||||
for lib in fplibs:
|
||||
f.write(f' (lib (name "{lib}")(type "KiCad")(uri "${{KICAD9_FOOTPRINT_DIR}}/{lib}.pretty")(options "")(descr ""))\n')
|
||||
f.write(')\n')
|
||||
print('wrote schematic,', len(sym_nodes), 'symbols,', sum(1 for i in items if i[0] == 'label'), 'labels,',
|
||||
sum(1 for i in items if i[0] == 'no_connect'), 'no-connects')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Tiny S-expression reader/writer for KiCad files.
|
||||
|
||||
Lists are Python lists; atoms are str. Quoted strings are kept as QStr so they
|
||||
round-trip with quotes. Numbers stay as plain str atoms (KiCad cares about text).
|
||||
"""
|
||||
|
||||
|
||||
class QStr(str):
|
||||
"""A string that was quoted in the source (and must be written quoted)."""
|
||||
|
||||
|
||||
def parse(text):
|
||||
pos = 0
|
||||
n = len(text)
|
||||
stack = [[]]
|
||||
while pos < n:
|
||||
c = text[pos]
|
||||
if c in ' \t\r\n':
|
||||
pos += 1
|
||||
elif c == '(':
|
||||
stack.append([])
|
||||
pos += 1
|
||||
elif c == ')':
|
||||
done = stack.pop()
|
||||
stack[-1].append(done)
|
||||
pos += 1
|
||||
elif c == '"':
|
||||
pos += 1
|
||||
buf = []
|
||||
while True:
|
||||
ch = text[pos]
|
||||
if ch == '\\':
|
||||
buf.append(text[pos:pos + 2])
|
||||
pos += 2
|
||||
continue
|
||||
if ch == '"':
|
||||
pos += 1
|
||||
break
|
||||
buf.append(ch)
|
||||
pos += 1
|
||||
raw = ''.join(buf)
|
||||
# keep escapes as-is (we re-emit them unchanged)
|
||||
stack[-1].append(QStr(raw))
|
||||
else:
|
||||
start = pos
|
||||
while pos < n and text[pos] not in ' \t\r\n()"':
|
||||
pos += 1
|
||||
stack[-1].append(text[start:pos])
|
||||
assert len(stack) == 1
|
||||
return stack[0][0] if len(stack[0]) == 1 else stack[0]
|
||||
|
||||
|
||||
def dumps(node, indent=0):
|
||||
"""Serialize back to KiCad-style text (one sub-list per line when nested)."""
|
||||
if isinstance(node, QStr):
|
||||
return '"' + str(node) + '"'
|
||||
if isinstance(node, str):
|
||||
return node
|
||||
if isinstance(node, (int, float)):
|
||||
return fmt(node)
|
||||
# list
|
||||
simple = all(not isinstance(x, list) for x in node)
|
||||
if simple:
|
||||
return '(' + ' '.join(dumps(x) for x in node) + ')'
|
||||
pad = ' ' * (indent + 1)
|
||||
parts = []
|
||||
head = []
|
||||
i = 0
|
||||
while i < len(node) and not isinstance(node[i], list):
|
||||
head.append(dumps(node[i]))
|
||||
i += 1
|
||||
out = '(' + ' '.join(head)
|
||||
for x in node[i:]:
|
||||
if isinstance(x, list):
|
||||
out += '\n' + pad + dumps(x, indent + 1)
|
||||
else:
|
||||
out += ' ' + dumps(x)
|
||||
out += '\n' + ' ' * indent + ')'
|
||||
return out
|
||||
|
||||
|
||||
def fmt(v):
|
||||
if isinstance(v, str):
|
||||
return v
|
||||
s = f'{v:.4f}'.rstrip('0').rstrip('.')
|
||||
if s in ('-0', ''):
|
||||
s = '0'
|
||||
return s
|
||||
|
||||
|
||||
def find(node, key):
|
||||
"""First child list whose head is key."""
|
||||
for x in node:
|
||||
if isinstance(x, list) and x and x[0] == key:
|
||||
return x
|
||||
return None
|
||||
|
||||
|
||||
def findall(node, key):
|
||||
return [x for x in node if isinstance(x, list) and x and x[0] == key]
|
||||
Reference in new issue
Block a user