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:
ckochandClaude Opus 5.5 committed 2026-11-01 13:40:23 -05:00
1 parent c498882bbb
commit 1400cc8afd
46 files changed
+226470

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
# SandTable Controller v0.4 (custom PCB)
![render](renders/render-iso.png)
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.
+98
View File
@@ -0,0 +1,98 @@
# SandTable Controller v0.3 (custom PCB)
![render](renders/render-iso.png)
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.
+183
View File
@@ -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
+18
View File
@@ -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
+753
View File
@@ -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)
)
)
+17
View File
@@ -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 ""))
)
@@ -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
1 Item # Designator Qty Manufacturer Mfg Part # Description / Value Package / Footprint Type Notes
2 1 U1 1 Espressif ESP32-WROOM-32E-N8 Wi-Fi + Bluetooth module, 8 MB flash, PCB antenna ESP32-WROOM-32E SMD
3 2 U2 1 WCH CH340C USB to UART bridge, SOIC-16 SOIC-16_3.9x9.9mm_P1.27mm SMD
4 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
5 4 U4 1 Texas Instruments SN74AHCT1G125DBVR Single buffer, TTL-level input, SOT-23-5 SOT-23-5 SMD
6 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
7 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
8 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
9 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
10 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
11 10 J8 1 Sullins PRPC004SAAN-RC Male pin header 1 x 4, 2.54 mm pitch, vertical PinHeader_1x04_P2.54mm_Vertical Thru-Hole
12 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
13 12 D1 1 SS34 Schottky diode 40 V 3 A, SMA D_SMA SMD Any brand
14 13 D2,D3 2 B5819W Schottky diode 40 V 1 A, SOD-123 D_SOD-123 SMD Any brand
15 14 D4 1 LED green, 0603 LED_0603_1608Metric SMD Any brand
16 15 D5 1 LED blue, 0603 LED_0603_1608Metric SMD Any brand
17 16 Q1,Q2 2 onsemi MMBT3904LT1G NPN transistor 40 V 200 mA, SOT-23 SOT-23 SMD Any MMBT3904 ok
18 17 SW1,SW2 2 Omron B3U-1000P Tactile switch, SMD, 3 x 2.5 mm SW_SPST_B3U-1000P SMD
19 18 C1,C2 2 Panasonic EEE-FK1V101GP Aluminium electrolytic capacitor 100 uF 35 V, SMD 8 x 10.2 mm CP_Elec_8x10 SMD
20 19 C3,C4 2 Ceramic capacitor 10 uF 25 V X5R, 1206 C_1206_3216Metric SMD Any brand
21 20 C5,C9 2 Ceramic capacitor 10 uF 16 V X5R, 0805 C_0805_2012Metric SMD Any brand
22 21 C6 1 Ceramic capacitor 22 uF 10 V X5R, 0805 C_0805_2012Metric SMD Any brand
23 22 C7,C10,C13,C14,C15 5 Ceramic capacitor 100 nF 50 V X7R, 0603 C_0603_1608Metric SMD Any brand
24 23 C8 1 Ceramic capacitor 1 uF 16 V X7R, 0603 C_0603_1608Metric SMD Any brand
25 24 R1,R2 2 Resistor 5.1 k 1%, 0603 R_0603_1608Metric SMD Any brand
26 25 R3,R4,R5,R6,R8,R9,R11 7 Resistor 10 k 1%, 0603 R_0603_1608Metric SMD Any brand
27 26 R10,R12,R14,R15 4 Resistor 1 k 1%, 0603 R_0603_1608Metric SMD Any brand
28 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
1 Designator Mid X (mm) Mid Y (mm) Layer Rotation Value Footprint
2 A1 38.100 61.440 Top 270 DRV8825 StepStick (theta/spin) Pololu_Breakout-16_15.2x20.3mm
3 A2 59.100 61.440 Top 270 DRV8825 StepStick (rho/arm) Pololu_Breakout-16_15.2x20.3mm
4 C1 24.000 42.340 Top 0 100uF 25V CP_Elec_8x10
5 C2 45.500 42.340 Top 0 100uF 25V CP_Elec_8x10
6 C3 20.500 23.740 Top 0 10uF 25V C_1206_3216Metric
7 C4 26.000 23.740 Top 0 10uF 16V C_1206_3216Metric
8 C5 59.600 19.340 Top 90 10uF C_0805_2012Metric
9 C6 59.600 23.840 Top 90 22uF C_0805_2012Metric
10 C7 27.000 20.340 Top 90 100nF C_0603_1608Metric
11 C8 54.600 10.640 Top 0 1uF C_0603_1608Metric
12 C9 57.500 8.740 Top 90 10uF C_0805_2012Metric
13 C10 55.000 8.340 Top 90 100nF C_0603_1608Metric
14 C13 61.000 44.840 Top 0 100nF C_0603_1608Metric
15 C14 61.000 42.640 Top 0 100nF C_0603_1608Metric
16 C15 61.000 33.340 Top 90 100nF C_0603_1608Metric
17 D1 16.000 41.340 Top 270 SS34 D_SMA
18 D2 29.000 28.340 Top 0 B5819W D_SOD-123
19 D3 13.000 23.540 Top 0 B5819W D_SOD-123
20 D4 34.000 33.340 Top 0 GREEN LED_0603_1608Metric
21 D5 38.000 33.340 Top 0 BLUE LED_0603_1608Metric
22 J1 12.300 38.340 Top 270 12V DC 5.5x2.1 BarrelJack_CUI_PJ-063AH_Horizontal
23 J2 3.400 19.340 Top 270 USB-C USB_C_Receptacle_HRO_TYPE-C-31-M-12
24 J3 4.500 59.940 Top 270 SPIN MOTOR JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical
25 J4 76.500 59.940 Top 270 ARM MOTOR JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical
26 J5 76.500 44.840 Top 270 HOME SW JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical
27 J6 76.500 33.740 Top 270 HALL JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical
28 J7 76.500 22.640 Top 270 LED STRIP JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical
29 J8 77.500 11.340 Top 0 EXPANSION PinHeader_1x04_P2.54mm_Vertical
30 PS1 18.000 28.340 Top 0 R-78E5.0-0.5 Converter_DCDC_RECOM_R-78E-0.5_THT
31 Q1 28.000 16.140 Top 0 MMBT3904 SOT-23
32 Q2 28.000 12.140 Top 0 MMBT3904 SOT-23
33 R1 11.500 20.590 Top 0 5.1k R_0603_1608Metric
34 R2 11.500 17.590 Top 0 5.1k R_0603_1608Metric
35 R3 31.000 18.340 Top 90 10k R_0603_1608Metric
36 R4 31.000 13.840 Top 90 10k R_0603_1608Metric
37 R5 54.600 12.340 Top 0 10k R_0603_1608Metric
38 R6 31.000 25.840 Top 0 10k R_0603_1608Metric
39 R8 47.000 32.340 Top 0 10k R_0603_1608Metric
40 R9 54.000 44.840 Top 0 10k R_0603_1608Metric
41 R10 57.500 44.840 Top 0 1k R_0603_1608Metric
42 R11 54.000 42.640 Top 0 10k R_0603_1608Metric
43 R12 57.500 42.640 Top 0 1k R_0603_1608Metric
44 R13 70.000 33.340 Top 90 330 R_0603_1608Metric
45 R14 34.000 30.840 Top 0 1k R_0603_1608Metric
46 R15 38.000 30.840 Top 0 1k R_0603_1608Metric
47 SW1 27.500 8.340 Top 0 RESET SW_SPST_B3U-1000P
48 SW2 22.000 8.340 Top 0 BOOT SW_SPST_B3U-1000P
49 U1 43.000 12.750 Top 180 ESP32-WROOM-32E-N8 ESP32-WROOM-32E
50 U2 22.000 16.340 Top 0 CH340C SOIC-16_3.9x9.9mm_P1.27mm
51 U3 66.000 21.840 Top 0 AMS1117-3.3 SOT-223-3_TabPin2
52 U4 65.500 33.340 Top 0 74AHCT1G125 SOT-23-5
+51
View File
@@ -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
+77
View File
@@ -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
+20
View File
@@ -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
+726
View File
@@ -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
+753
View File
@@ -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)
)
)
+19
View File
@@ -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 ""))
)
@@ -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
1 Item # Designator Qty Manufacturer Mfg Part # Description / Value Package / Footprint Type Notes
2 1 U1 1 Espressif ESP32-WROOM-32E-N8 Wi-Fi + Bluetooth module, 8 MB flash, PCB antenna ESP32-WROOM-32E SMD
3 2 U2 1 WCH CH340C USB to UART bridge, SOIC-16 SOIC-16_3.9x9.9mm_P1.27mm SMD
4 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
5 4 U5 1 STMicroelectronics USBLC6-2SC6 USB ESD protection, 2 lines, SOT-23-6 SOT-23-6 SMD
6 5 U4 1 Texas Instruments SN74AHCT1G125DBVR Single buffer, TTL-level input, SOT-23-5 SOT-23-5 SMD
7 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
8 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
9 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
10 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
11 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
12 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
13 12 J8 1 Sullins PRPC004SAAN-RC Male pin header 1 x 4, 2.54 mm pitch, vertical PinHeader_1x04_P2.54mm_Vertical Thru-Hole
14 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
15 14 D1,D2 2 SS34 Schottky diode 40 V 3 A, SMA D_SMA SMD Any brand
16 15 D3 1 B5819W Schottky diode 40 V 1 A, SOD-123 D_SOD-123 SMD Any brand
17 16 D6,D7 2 BAT54WS Schottky diode 30 V 0.2 A, SOD-323 D_SOD-323 SMD Any brand
18 17 D4 1 LED green, 0603 LED_0603_1608Metric SMD Any brand
19 18 D5 1 LED blue, 0603 LED_0603_1608Metric SMD Any brand
20 19 Q1,Q2 2 onsemi MMBT3904LT1G NPN transistor 40 V 200 mA, SOT-23 SOT-23 SMD Any MMBT3904 ok
21 20 SW1,SW2 2 Omron B3U-1000P Tactile switch, SMD, 3 x 2.5 mm SW_SPST_B3U-1000P SMD
22 21 C1,C2 2 Panasonic EEE-FK1V101GP Aluminium electrolytic capacitor 100 uF 35 V, SMD 8 x 10.2 mm CP_Elec_8x10 SMD
23 22 C3,C4 2 Ceramic capacitor 10 uF 25 V X5R, 1206 C_1206_3216Metric SMD Any brand
24 23 C16,C17 2 Ceramic capacitor 10 uF 50 V X7R, 1206 C_1206_3216Metric SMD Any brand
25 24 C5,C9 2 Ceramic capacitor 10 uF 16 V X5R, 0805 C_0805_2012Metric SMD Any brand
26 25 C6 1 Ceramic capacitor 22 uF 10 V X5R, 0805 C_0805_2012Metric SMD Any brand
27 26 C7,C10,C13,C14,C15 5 Ceramic capacitor 100 nF 50 V X7R, 0603 C_0603_1608Metric SMD Any brand
28 27 C8 1 Ceramic capacitor 1 uF 16 V X7R, 0603 C_0603_1608Metric SMD Any brand
29 28 R1,R2 2 Resistor 5.1 k 1%, 0603 R_0603_1608Metric SMD Any brand
30 29 R3,R4,R5,R6,R8,R9,R11 7 Resistor 10 k 1%, 0603 R_0603_1608Metric SMD Any brand
31 30 R10,R12,R14,R15 4 Resistor 1 k 1%, 0603 R_0603_1608Metric SMD Any brand
32 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
1 Designator Mid X (mm) Mid Y (mm) Layer Rotation Value Footprint
2 A1 38.100 61.440 Top 270 DRV8825 StepStick (theta/spin) Pololu_Breakout-16_15.2x20.3mm
3 A2 59.100 61.440 Top 270 DRV8825 StepStick (rho/arm) Pololu_Breakout-16_15.2x20.3mm
4 C1 24.000 42.340 Top 0 100uF 25V CP_Elec_8x10
5 C2 45.500 42.340 Top 0 100uF 25V CP_Elec_8x10
6 C3 20.500 23.740 Top 0 10uF 25V C_1206_3216Metric
7 C4 26.000 23.740 Top 0 10uF 16V C_1206_3216Metric
8 C5 59.600 19.340 Top 90 10uF C_0805_2012Metric
9 C6 59.600 23.840 Top 90 22uF C_0805_2012Metric
10 C7 27.000 20.340 Top 90 100nF C_0603_1608Metric
11 C8 54.600 10.640 Top 0 1uF C_0603_1608Metric
12 C9 57.500 8.740 Top 90 10uF C_0805_2012Metric
13 C10 55.000 8.340 Top 90 100nF C_0603_1608Metric
14 C13 61.000 44.840 Top 0 100nF C_0603_1608Metric
15 C14 61.000 42.640 Top 0 100nF C_0603_1608Metric
16 C15 61.000 33.340 Top 90 100nF C_0603_1608Metric
17 C16 21.800 57.940 Top 0 10uF 50V C_1206_3216Metric
18 C17 42.800 57.940 Top 0 10uF 50V C_1206_3216Metric
19 D1 16.000 41.340 Top 90 SS34 D_SMA
20 D2 28.870 30.340 Top 90 SS34 D_SMA
21 D3 13.000 23.540 Top 0 B5819W D_SOD-123
22 D4 34.000 33.340 Top 0 GREEN LED_0603_1608Metric
23 D5 38.000 33.340 Top 0 BLUE LED_0603_1608Metric
24 D6 64.500 45.340 Top 0 BAT54WS D_SOD-323
25 D7 64.500 47.490 Top 0 BAT54WS D_SOD-323
26 F1 12.600 47.140 Top 0 2.5A PTC Fuse_1812_4532Metric
27 J1 12.300 38.340 Top 270 12V DC 5.5x2.1 BarrelJack_CUI_PJ-063AH_Horizontal
28 J2 3.400 19.340 Top 270 USB-C USB_C_Receptacle_HRO_TYPE-C-31-M-12
29 J3 4.500 59.940 Top 270 SPIN MOTOR JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical
30 J4 76.500 59.940 Top 270 ARM MOTOR JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical
31 J5 76.500 44.840 Top 270 HOME SW JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical
32 J6 76.500 33.740 Top 270 HALL JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical
33 J7 76.500 22.640 Top 270 LED STRIP JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical
34 J8 77.500 11.340 Top 0 EXPANSION PinHeader_1x04_P2.54mm_Vertical
35 PS1 18.000 28.340 Top 0 R-78E5.0-1.0 Converter_DCDC_RECOM_R-78E-0.5_THT
36 Q1 28.000 16.140 Top 0 MMBT3904 SOT-23
37 Q2 28.000 12.140 Top 0 MMBT3904 SOT-23
38 R1 11.500 20.590 Top 0 5.1k R_0603_1608Metric
39 R2 11.500 17.590 Top 0 5.1k R_0603_1608Metric
40 R3 31.000 18.340 Top 90 10k R_0603_1608Metric
41 R4 31.000 13.840 Top 90 10k R_0603_1608Metric
42 R5 54.600 12.340 Top 0 10k R_0603_1608Metric
43 R6 31.000 25.840 Top 0 10k R_0603_1608Metric
44 R8 47.000 32.340 Top 0 10k R_0603_1608Metric
45 R9 54.000 44.840 Top 0 10k R_0603_1608Metric
46 R10 57.500 44.840 Top 0 1k R_0603_1608Metric
47 R11 54.000 42.640 Top 0 10k R_0603_1608Metric
48 R12 57.500 42.640 Top 0 1k R_0603_1608Metric
49 R13 70.000 33.340 Top 90 330 R_0603_1608Metric
50 R14 34.000 30.840 Top 0 1k R_0603_1608Metric
51 R15 38.000 30.840 Top 0 1k R_0603_1608Metric
52 SW1 27.500 8.340 Top 0 RESET SW_SPST_B3U-1000P
53 SW2 22.000 8.340 Top 0 BOOT SW_SPST_B3U-1000P
54 U1 43.000 12.750 Top 180 ESP32-WROOM-32E-N8 ESP32-WROOM-32E
55 U2 22.000 16.340 Top 0 CH340C SOIC-16_3.9x9.9mm_P1.27mm
56 U3 66.000 21.840 Top 0 AP7361C-33E SOT-223-3_TabPin2
57 U4 65.500 33.340 Top 0 74AHCT1G125 SOT-23-5
58 U5 13.000 13.840 Top 0 USBLC6-2SC6 SOT-23-6
+53
View File
@@ -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
+357
View File
@@ -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)
+203
View File
@@ -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
+219
View File
@@ -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()
+184
View File
@@ -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
+21
View File
@@ -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')
+67
View File
@@ -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)
+213
View File
@@ -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]
+349
View File
@@ -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()
+289
View File
@@ -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)
+271
View File
@@ -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()
+100
View File
@@ -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]