2026-11-13 22:30:14 +01:00
|
|
|
// Main PCB of the 2026 hexapod robot — a Teensy 3.6 carrier board.
|
|
|
|
|
//
|
|
|
|
|
// Dimensions and placements are all derived from the EasyEDA export
|
|
|
|
|
// (PCB_test_2026-11-13.json), cross-checked against the EasyEDA renders
|
|
|
|
|
// (top.png, bottom.png) and photos of the populated board.
|
|
|
|
|
//
|
|
|
|
|
// Every component is placed at its actual pad reference point (not an
|
|
|
|
|
// eyeballed position), with its real-world body dimensions.
|
|
|
|
|
//
|
|
|
|
|
// Coordinate frame: X right, Y up, Z up, origin = board bottom-left.
|
|
|
|
|
// Board top surface at z = PCB_THICK, bottom surface at z = 0.
|
|
|
|
|
// EasyEDA uses Y-down with the base unit = 10 mil (0.254 mm). Conversion:
|
|
|
|
|
// x_mm = (x_eda - 3980.315) * 0.254
|
|
|
|
|
// y_mm = (3330.7087 - y_eda) * 0.254
|
|
|
|
|
|
|
|
|
|
/* [Board] */
|
|
|
|
|
BOARD_W = 46.0;
|
|
|
|
|
BOARD_H = 89.0;
|
|
|
|
|
PCB_THICK = 1.6;
|
|
|
|
|
CORNER_R = 1.0;
|
|
|
|
|
|
|
|
|
|
/* [Mounting holes] */
|
|
|
|
|
HOLE_DIA = 4.0; // M3 clearance
|
2026-11-14 12:14:32 +01:00
|
|
|
// Exact EasyEDA HOLE positions; the right/bottom insets are 5.22/5.04 mm,
|
|
|
|
|
// not 5, so the grid is 35.78 x 78.96 mm (the body's PCB bosses use it).
|
|
|
|
|
HOLES = [[5.00, 5.04], [40.78, 5.04], [5.00, 84.00], [40.78, 84.00]];
|
|
|
|
|
HOLE_GRID_CENTER = [22.89, 44.52];
|
2026-11-13 22:30:14 +01:00
|
|
|
|
|
|
|
|
/* [Rendering] */
|
|
|
|
|
SHOW_COMPONENTS = true;
|
|
|
|
|
SHOW_LABELS = true;
|
|
|
|
|
TEENSY_ALPHA = 0.4;
|
|
|
|
|
EXPLODE = 0;
|
|
|
|
|
|
|
|
|
|
/* [Hidden] */
|
|
|
|
|
$fa = 2; $fs = 0.3;
|
|
|
|
|
|
|
|
|
|
C_PCB = "#1a1a1a";
|
|
|
|
|
C_TEENSY = "#1e5a2a";
|
|
|
|
|
C_MODULE = "#1f4fa0";
|
|
|
|
|
C_HEADER = "#111";
|
|
|
|
|
C_METAL = "#b0b0b0";
|
|
|
|
|
C_PLASTIC = "#222";
|
|
|
|
|
C_LED_ON = "#55ff55";
|
|
|
|
|
C_LABEL = "white";
|
|
|
|
|
|
|
|
|
|
// ---------- board ----------------------------------------------------
|
|
|
|
|
|
|
|
|
|
module board_outline_2d() {
|
|
|
|
|
if (CORNER_R > 0)
|
|
|
|
|
hull() for (p = [[CORNER_R, CORNER_R],
|
|
|
|
|
[BOARD_W - CORNER_R, CORNER_R],
|
|
|
|
|
[CORNER_R, BOARD_H - CORNER_R],
|
|
|
|
|
[BOARD_W - CORNER_R, BOARD_H - CORNER_R]])
|
|
|
|
|
translate(p) circle(r = CORNER_R);
|
|
|
|
|
else
|
|
|
|
|
square([BOARD_W, BOARD_H]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module mounting_holes_2d() {
|
2026-11-14 12:14:32 +01:00
|
|
|
for (h = HOLES) translate(h) circle(d = HOLE_DIA);
|
2026-11-13 22:30:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module pcb() {
|
|
|
|
|
color(C_PCB)
|
|
|
|
|
linear_extrude(PCB_THICK)
|
|
|
|
|
difference() { board_outline_2d(); mounting_holes_2d(); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- helpers --------------------------------------------------
|
|
|
|
|
|
|
|
|
|
module mount_top(cx, cy, rot = 0) {
|
|
|
|
|
translate([cx, cy, PCB_THICK + EXPLODE]) rotate([0, 0, rot]) children();
|
|
|
|
|
}
|
|
|
|
|
module mount_bottom(cx, cy, rot = 0) {
|
|
|
|
|
translate([cx, cy, -EXPLODE])
|
|
|
|
|
mirror([0, 0, 1])
|
|
|
|
|
rotate([0, 0, rot])
|
|
|
|
|
children();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module centered_cube(sx, sy, sz) {
|
|
|
|
|
translate([-sx/2, -sy/2, 0]) cube([sx, sy, sz]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module label(txt, size = 2.4, rot = 0) {
|
|
|
|
|
if (SHOW_LABELS)
|
|
|
|
|
color(C_LABEL)
|
|
|
|
|
rotate([0, 0, rot])
|
|
|
|
|
linear_extrude(0.2)
|
|
|
|
|
text(txt, size = size, halign = "center",
|
|
|
|
|
valign = "center", font = "Liberation Sans");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generic 1xN pin-header strip, pins along Y, strip centered on local origin.
|
|
|
|
|
// pitch defaults to 2.54 mm (standard); pass 2.0 for JST-PH.
|
|
|
|
|
module header_1xN(n, h = 8.5, pitch = 2.54) {
|
|
|
|
|
L = n * pitch;
|
|
|
|
|
color(C_HEADER) translate([-pitch/2, -L/2 + pitch/2 - pitch/2, 0])
|
|
|
|
|
cube([pitch, L, h]);
|
|
|
|
|
color(C_METAL)
|
|
|
|
|
for (i = [0:n-1])
|
|
|
|
|
translate([0, (i - (n-1)/2) * pitch, h])
|
|
|
|
|
cylinder(d = 0.65, h = 2.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generic MxN pin-header, M rows along X (2.54 pitch) x N pins along Y.
|
|
|
|
|
module header_MxN(m, n, h = 8.5, pitch = 2.54) {
|
|
|
|
|
L = n * pitch; W = m * pitch;
|
|
|
|
|
color(C_HEADER)
|
|
|
|
|
translate([-W/2, -L/2, 0]) cube([W, L, h]);
|
|
|
|
|
color(C_METAL)
|
|
|
|
|
for (i = [0:m-1], j = [0:n-1])
|
|
|
|
|
translate([(i - (m-1)/2) * pitch, (j - (n-1)/2) * pitch, h])
|
|
|
|
|
cylinder(d = 0.65, h = 2.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- components -----------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// Every component module below is written so its LOCAL origin (0,0,0)
|
|
|
|
|
// matches a well-defined reference on the board — usually the geometric
|
|
|
|
|
// centre of its pad group (= the position the EasyEDA LIB extractor
|
|
|
|
|
// returned). All geometry is built upward from z = 0.
|
|
|
|
|
|
|
|
|
|
// Teensy 3.6: local origin = centre of the 2x24 pin pattern.
|
|
|
|
|
// Pin bbox: 15.24 mm (X, two rows at 0.6") x 58.42 mm (Y, 24 pins).
|
|
|
|
|
// Body is 17.8 x 61 x 1.6 mm, slightly wider than the pin pitch.
|
|
|
|
|
// Micro-USB jack at +Y end; microSD slot at -Y end.
|
|
|
|
|
module teensy_3_6() {
|
|
|
|
|
HDR_H = 11.0;
|
|
|
|
|
ROW_SPACING = 15.24; // 0.6 inch
|
|
|
|
|
PIN_SPAN = 24 * 2.54;
|
|
|
|
|
BODY_L = 61.0; BODY_W = 17.8; BODY_T = 1.6;
|
|
|
|
|
|
|
|
|
|
for (dx = [-ROW_SPACING/2, ROW_SPACING/2])
|
|
|
|
|
translate([dx, 0, 0]) header_1xN(24, HDR_H);
|
|
|
|
|
|
|
|
|
|
translate([0, 0, HDR_H]) {
|
|
|
|
|
color(C_TEENSY, TEENSY_ALPHA)
|
|
|
|
|
centered_cube(BODY_W, BODY_L, BODY_T);
|
|
|
|
|
// MK66FX1M0 chip, towards +Y end
|
|
|
|
|
color("#222")
|
|
|
|
|
translate([0, 10, BODY_T]) centered_cube(10, 10, 1.3);
|
|
|
|
|
// micro-USB jack, protrudes past +Y edge
|
|
|
|
|
color(C_METAL)
|
|
|
|
|
translate([0, BODY_L/2 - 1, BODY_T])
|
|
|
|
|
centered_cube(7.5, 5.5, 2.6);
|
|
|
|
|
// microSD slot at -Y end
|
|
|
|
|
color(C_METAL)
|
|
|
|
|
translate([0, -BODY_L/2 + 7, BODY_T])
|
|
|
|
|
centered_cube(12, 14, 1.5);
|
|
|
|
|
if (SHOW_LABELS)
|
|
|
|
|
translate([0, -4, BODY_T + 1.5]) label("Teensy 3.6", 2.6);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// nRF24L01+ with PCB antenna. Local origin = centre of the 2x4 header
|
|
|
|
|
// (pads span 7.62 x 2.54 mm). Module body is 15.5 x 29 x 1.2 mm; the
|
|
|
|
|
// header sits near one short edge, so the body extends -Y from origin
|
|
|
|
|
// and the PCB antenna end hangs off that side of the board.
|
|
|
|
|
module nrf24() {
|
|
|
|
|
HDR_H = 7.5;
|
|
|
|
|
BODY_L = 29.0; BODY_W = 15.5; BODY_T = 1.2;
|
|
|
|
|
HDR_FROM_EDGE = 3.2; // from body +Y edge to header centre
|
|
|
|
|
body_y = -(BODY_L/2 - HDR_FROM_EDGE);
|
|
|
|
|
|
|
|
|
|
header_MxN(2, 4, HDR_H);
|
|
|
|
|
translate([0, body_y, HDR_H]) {
|
|
|
|
|
color(C_MODULE) centered_cube(BODY_W, BODY_L, BODY_T);
|
|
|
|
|
// RF shield (can) over the chip area, nearer the header end
|
|
|
|
|
color(C_METAL)
|
|
|
|
|
translate([0, BODY_L/2 - 7.5, BODY_T])
|
|
|
|
|
centered_cube(BODY_W - 2, 10.0, 1.8);
|
|
|
|
|
// Cylindrical crystal next to the shield
|
|
|
|
|
color(C_METAL)
|
|
|
|
|
translate([-BODY_W/4, BODY_L/2 - 15, BODY_T])
|
|
|
|
|
cube([3.2, 8, 2.5]);
|
|
|
|
|
if (SHOW_LABELS)
|
|
|
|
|
translate([0, BODY_L/2 - 20, BODY_T + 2.2]) label("nRF24", 2.2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GY-521 (MPU6050) breakout. Local origin = centre of the 1x8 header
|
|
|
|
|
// (pads span 0 x 17.78 mm, 2.54 pitch along Y). The board extends +X
|
|
|
|
|
// from the header; body is 15.6 x 21.2 x 1.6 mm.
|
|
|
|
|
module gy521() {
|
|
|
|
|
HDR_H = 8.0;
|
|
|
|
|
BODY_L = 21.2; BODY_W = 15.6; BODY_T = 1.6;
|
|
|
|
|
|
|
|
|
|
header_1xN(8, HDR_H);
|
|
|
|
|
translate([BODY_W/2, 0, HDR_H]) {
|
|
|
|
|
color(C_MODULE) centered_cube(BODY_W, BODY_L, BODY_T);
|
|
|
|
|
// MPU6050 QFN near the board centre
|
|
|
|
|
color("#222")
|
|
|
|
|
translate([0, 0, BODY_T]) centered_cube(4, 4, 1.1);
|
|
|
|
|
if (SHOW_LABELS)
|
|
|
|
|
translate([0, BODY_L/2 - 3, BODY_T + 1.5]) label("MPU", 2.4);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 7805 in TO-220. Local origin = centre of the three TO-252 pads on the
|
|
|
|
|
// EasyEDA board (the physical build has a through-hole 7805 bent flat
|
|
|
|
|
// into those pads). Pads span ~4.57 x 7.29 mm.
|
|
|
|
|
module ic_7805() {
|
|
|
|
|
BODY_L = 10.0; BODY_W = 4.7; BODY_H = 4.5;
|
|
|
|
|
// body laid flat along +Y (matching the pad bbox orientation)
|
|
|
|
|
translate([0, 0, 0]) {
|
|
|
|
|
color(C_PLASTIC)
|
|
|
|
|
translate([-BODY_L/2, -BODY_W/2, 0]) cube([BODY_L, BODY_W, BODY_H]);
|
|
|
|
|
// metal tab
|
|
|
|
|
color(C_METAL)
|
|
|
|
|
translate([-BODY_L/2, -BODY_W/2, BODY_H - 1.2])
|
|
|
|
|
cube([BODY_L, BODY_W, 1.2]);
|
|
|
|
|
// 3 leads out of +Y side
|
|
|
|
|
color(C_METAL)
|
|
|
|
|
for (dx = [-2.54, 0, 2.54])
|
|
|
|
|
translate([dx - 0.3, BODY_W/2, 0])
|
|
|
|
|
cube([0.6, 3, 0.6]);
|
|
|
|
|
if (SHOW_LABELS)
|
|
|
|
|
translate([0, 0, BODY_H + 0.5]) label("7805", 2.0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Through-hole zener, lying between two pads along X (pad span 3.28 mm).
|
|
|
|
|
module zener() {
|
|
|
|
|
color("#444") rotate([0, 90, 0]) cylinder(d = 2.0, h = 3.0, center = true);
|
|
|
|
|
// cathode band
|
|
|
|
|
color("#eee") translate([-0.9, 0, 0])
|
|
|
|
|
rotate([0, 90, 0]) cylinder(d = 2.05, h = 0.3, center = true);
|
|
|
|
|
if (SHOW_LABELS)
|
|
|
|
|
translate([0, 3.5, 0]) label("ZENER", 1.4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2-pin JST-XH battery connector (silkscreen JP7). Pads 2 mm apart in X.
|
|
|
|
|
module jst_jp7() {
|
|
|
|
|
BODY_W = 7.6; BODY_D = 4.0; BODY_H = 7.0;
|
|
|
|
|
color(C_PLASTIC)
|
|
|
|
|
translate([-BODY_W/2, -BODY_D/2 + 1.0, 0])
|
|
|
|
|
cube([BODY_W, BODY_D, BODY_H]);
|
|
|
|
|
color(C_METAL)
|
|
|
|
|
translate([-3, -BODY_D/2 + 1.0 - 0.5, BODY_H - 1.5])
|
|
|
|
|
cube([6, 0.5, 1.0]);
|
|
|
|
|
if (SHOW_LABELS) translate([0, 0, BODY_H + 0.5]) label("JP7", 1.8);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5-pin JST-PH 2 mm servo header (JP1..JP6). Pads 2 mm pitch along Y,
|
|
|
|
|
// total span 8 mm. Local origin = pad-group centre.
|
|
|
|
|
module servo_jst5(label_txt) {
|
|
|
|
|
PITCH = 2.0;
|
|
|
|
|
header_1xN(5, 8.5, pitch = PITCH);
|
|
|
|
|
// JST-PH housing body (sits above the pins when soldered male-up)
|
|
|
|
|
color(C_PLASTIC)
|
|
|
|
|
translate([-1.8, -5.0, 0]) cube([3.6, 10.0, 7.0]);
|
|
|
|
|
if (SHOW_LABELS)
|
|
|
|
|
translate([3.5, 0, 4]) label(label_txt, 1.8, 90);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1206 SMD LED and 2010 SMD resistor, both on the TOP layer.
|
|
|
|
|
module led_1206() {
|
|
|
|
|
color(C_LED_ON) translate([-1.6, -0.8, 0]) cube([3.2, 1.6, 0.9]);
|
|
|
|
|
if (SHOW_LABELS)
|
|
|
|
|
translate([0, -2.5, 1.0]) label("PWR", 1.6);
|
|
|
|
|
}
|
|
|
|
|
module r_2010(txt = "510") {
|
|
|
|
|
color("#eee") translate([-2.5, -1.25, 0]) cube([5.0, 2.5, 0.6]);
|
|
|
|
|
if (SHOW_LABELS)
|
|
|
|
|
translate([0, 0, 0.7]) label(txt, 1.4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// "Hexapod v1" silkscreen text, at the position of the top-silk TEXT
|
|
|
|
|
// entry in the EasyEDA file.
|
|
|
|
|
module title_silk() {
|
|
|
|
|
if (SHOW_LABELS)
|
|
|
|
|
translate([6, 80.9, PCB_THICK + 0.02])
|
|
|
|
|
color("white")
|
|
|
|
|
linear_extrude(0.1)
|
|
|
|
|
text("Hexapod v1", size = 2.8, halign = "left",
|
|
|
|
|
valign = "center", font = "Liberation Sans");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- assembly -------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// Placements use the pad-reference centres extracted from the EasyEDA
|
|
|
|
|
// file (see scratchpad/libs.py output).
|
|
|
|
|
|
|
|
|
|
module components() {
|
|
|
|
|
// --- TOP side ----
|
|
|
|
|
mount_top(22.62, 44.79) teensy_3_6();
|
|
|
|
|
mount_top(33.00, 83.00) led_1206();
|
|
|
|
|
mount_top(32.55, 78.72) r_2010("510");
|
|
|
|
|
title_silk();
|
|
|
|
|
|
|
|
|
|
// --- BOTTOM side ----
|
|
|
|
|
mount_bottom(28.00, 8.00) nrf24(); // header centre; body hangs off -Y edge
|
|
|
|
|
mount_bottom(19.22, 44.73) gy521(); // header centre; body extends +X
|
|
|
|
|
mount_bottom(23.00, 79.00) ic_7805();
|
|
|
|
|
mount_bottom(22.00, 67.00) zener();
|
|
|
|
|
mount_bottom(15.00, 9.00) jst_jp7();
|
|
|
|
|
|
|
|
|
|
// Servo headers, silkscreen numbering per bottom.png:
|
|
|
|
|
// left column (x=9) top-to-bottom = JP1, JP2, JP3
|
|
|
|
|
// right column (x=38) top-to-bottom = JP4, JP5, JP6
|
|
|
|
|
mount_bottom( 9.00, 69.00) servo_jst5("JP1");
|
|
|
|
|
mount_bottom( 9.00, 43.00) servo_jst5("JP2");
|
|
|
|
|
mount_bottom( 9.00, 20.00) servo_jst5("JP3");
|
|
|
|
|
mount_bottom(38.00, 69.00) servo_jst5("JP4");
|
|
|
|
|
mount_bottom(38.00, 44.00) servo_jst5("JP5");
|
|
|
|
|
mount_bottom(38.00, 20.00) servo_jst5("JP6");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module main_pcb() {
|
|
|
|
|
pcb();
|
|
|
|
|
if (SHOW_COMPONENTS) components();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main_pcb();
|