TerslerThe little junk rig that could

Source files / shroud · raw

p100_shroud.scad

// Dual Tesla P100 intake shroud for one 120 mm fan (e.g. Arctic S12038-4K)
// Board: ASUS Z10PA-U8, cards in PCIE6 + PCIE4 (2 slots apart).
//
// The shroud slides over the tail ends of both cards (the end opposite
// the slot bracket). The fan blows into the fins, and the air leaves
// through the bracket vents.
//
// Print with the sleeve opening on the bed (fan flange on top): then the
// gap blocker and the tabs grow from the bed, and no supports are needed.
//
// Coordinates: z = airflow axis, z = 0 is the fan face.
//              x = stacking direction (across the cards), y = card height.
//
// MEASURE YOUR CARDS AND OVERWRITE THE DEFAULTS BELOW.
// The defaults are nominal values, not measured.

/* [Cards: measure these] */
card_h      = 111.2;  // tail face: lowest to highest point of the card (PCB edge to top)
card_t      = 37.0;   // tail face: thickness of one card's cooler shroud
slot_pitch  = 40.64;  // centre-to-centre spacing of the two cards (2 slots x 20.32)
n_cards     = 2;
clearance   = 0.6;    // gap per side in the sleeve (add 1-2 mm foam tape to seal)

/* [Shroud] */
wall        = 2.0;    // wall thickness (PETG/ASA recommended, not PLA)
trans_len   = 60;     // length of the taper from the fan to the sleeve
sleeve_len  = 18;     // how far the shroud slides over the card tails
x_off       = 0;      // shift the card opening relative to the fan centre
y_off       = 0;

/* [Streamlining] */
// The taper morphs smoothly from the round fan opening to a rectangle with
// slightly rounded corners. An S-curve (smoothstep) along the length means
// it starts and ends tangentially: no sharp kinks where flow could separate.
loft_steps  = 24;     // more = smoother
r_end       = 3;      // corner radius of the taper at the sleeve
smooth      = true;   // false = straight (linear) taper

/* [Card stops] */
// Four small corner stops instead of a full lip: the card ends rest on them,
// while the fin face stays almost completely open. 45° bevel on the fan side.
stops       = true;
stop_size   = 6;      // edge length of a stop
stop_t      = 1.5;    // thickness of the stop face

/* [Fan] */
fan_size    = 120;
fan_hole_sp = 106;    // measured on the actual fan, centre to centre (nominal 120 mm spec: 105)
fan_hole_d  = 4.6;    // M4 clearance (fan holes are 4.5 mm; printed holes shrink ~0.1-0.2 mm)
                      // M4 x 45 through-bolts + nuts: 4 mm flange + 38 mm fan
fan_open_d  = 116;    // round inlet opening (slightly smaller than the fan)
flange_t    = 4;

/* [Gap blocker] */
// Closes the air gap between the two coolers. Without it a lot of air
// takes this low-resistance path instead of going through the fins.
// The tapered nose splits the flow towards the two fin faces smoothly.
gap_block       = true;
gap_block_clear = 0.3;  // play per side in the gap
nose_len        = 20;   // length of the tapered nose (0 = flat face)
nose_edge       = 0.6;  // thickness of the leading edge (printable minimum)

/* [Splitter] */
// Full dividing wall in the taper. Usually not needed: the fan hub sits
// in front of the gap anyway, so the airflow splits symmetrically.
// Only enable it if the two cards run more than ~5 °C apart.
splitter        = false;
splitter_t      = 1.6;
splitter_start  = 30;   // distance from the fan face; further away = quieter

/* [Mounting] */
ears        = true;   // two tabs on the underside for zip ties / a support strut
ear_hole_d  = 4.5;

$fn = 96;
e = 0.01;

// ---- derived dimensions ----
stack_x = slot_pitch * (n_cards - 1) + card_t;  // width of the stacked tail face
in_x  = stack_x + 2 * clearance;
in_y  = card_h  + 2 * clearance;
out_x = in_x + 2 * wall;
out_y = in_y + 2 * wall;
z_t0  = flange_t;               // taper start
z_t1  = flange_t + trans_len;   // taper end = sleeve start
z_end = z_t1 + sleeve_len;
gap   = slot_pitch - card_t;    // air gap between the two coolers
bt    = gap - 2 * gap_block_clear;  // gap blocker thickness

// ---- smooth loft ----
function lerp(a, b, s) = a + (b - a) * s;
function ease(t) = smooth ? t * t * (3 - 2 * t) : t;   // smoothstep

// cross section at loft fraction s: rounded rectangle, s = 0 is a circle
module section(s, grow = 0) {
    w = lerp(fan_open_d, in_x, s);
    h = lerp(fan_open_d, in_y, s);
    r = lerp(fan_open_d / 2, r_end, s);
    // hull of four corner circles: stays valid when it degenerates to a circle
    translate([lerp(0, x_off, s), lerp(0, y_off, s)])
        hull() for (sx = [-1, 1], sy = [-1, 1])
            translate([sx * max(w/2 - r, 0), sy * max(h/2 - r, 0)]) circle(r = r + grow);
}

module slice(t, grow = 0) {
    translate([0, 0, lerp(z_t0, z_t1, t) - e/2])
        linear_extrude(e) section(ease(t), grow);
}

module loft(grow = 0) {
    for (i = [0 : loft_steps - 1])
        hull() {
            slice(i / loft_steps, grow);
            slice((i + 1) / loft_steps, grow);
        }
}

module outer_body() {
    translate([-fan_size/2, -fan_size/2, 0]) cube([fan_size, fan_size, flange_t]);   // flange
    loft(wall);                                                                      // taper
    translate([x_off - out_x/2, y_off - out_y/2, z_t1 - e]) cube([out_x, out_y, sleeve_len + e]); // sleeve
}

module inner_air() {
    translate([0, 0, -e]) cylinder(d = fan_open_d, h = flange_t + 2*e);   // fan inlet
    loft(0);                                                               // taper
    translate([x_off - in_x/2, y_off - in_y/2, z_t1 - e]) cube([in_x, in_y, sleeve_len + 2*e]); // sleeve
}

module fan_holes() {
    for (sx = [-1, 1], sy = [-1, 1])
        translate([sx * fan_hole_sp/2, sy * fan_hole_sp/2, -e])
            cylinder(d = fan_hole_d, h = flange_t + 2*e);
}

// x centre of the gap between card i and card i+1
function gap_x(i) = x_off - stack_x/2 + i * slot_pitch - gap/2;

module splitters() {
    for (i = [1 : n_cards - 1])
        intersection() {
            inner_air();
            translate([gap_x(i) - splitter_t/2, -fan_size, splitter_start])
                cube([splitter_t, 2*fan_size, z_t1 - splitter_start]);
        }
}

// bar that fills the gap between the coolers, with a tapered nose towards the fan
module gap_blockers() {
    if (bt > 0.8)
        for (i = [1 : n_cards - 1]) {
            xc = gap_x(i);
            // bar inside the sleeve
            translate([xc - bt/2, y_off - in_y/2 - 0.5, z_t1]) cube([bt, in_y + 1, sleeve_len]);
            // nose: wedge from bar thickness to a thin leading edge
            if (nose_len > 0)
                intersection() {
                    inner_air();
                    hull() {
                        translate([xc - bt/2, -fan_size, z_t1]) cube([bt, 2*fan_size, e]);
                        translate([xc - nose_edge/2, -fan_size, z_t1 - nose_len]) cube([nose_edge, 2*fan_size, e]);
                    }
                }
        }
}

// corner stops: flat face towards the cards, 45° bevel towards the fan
module corner_stops() {
    intersection() {
        outer_body();
        for (sx = [-1, 1], sy = [-1, 1]) {
            cx = x_off + sx * in_x/2;
            cy = y_off + sy * in_y/2;
            hull() {
                // flat block in the corner (cards rest on its lower face)
                translate([cx - (sx > 0 ? stop_size : 0.5), cy - (sy > 0 ? stop_size : 0.5), z_t1])
                    cube([stop_size + 0.5, stop_size + 0.5, stop_t]);
                // tip of the bevel, back in the corner, towards the fan
                translate([cx + sx * 0.5 - (sx > 0 ? e : 0), cy + sy * 0.5 - (sy > 0 ? e : 0), z_t1 - stop_size])
                    cube([e, e, e]);
            }
        }
    }
}

module ears_mod() {
    for (sx = [-1, 1])
        difference() {
            translate([x_off + sx * (out_x/2 - 6) - 6, y_off - out_y/2 - 10, z_end - 12]) cube([12, 10 + e, 12]);
            translate([x_off + sx * (out_x/2 - 6), y_off - out_y/2 - 5, z_end - 13]) cylinder(d = ear_hole_d, h = 14);
        }
}

module shroud() {
    difference() {
        union() {
            outer_body();
            if (ears) ears_mod();
        }
        inner_air();
        fan_holes();
    }
    if (stops) corner_stops();
    if (splitter && n_cards > 1) splitters();
    if (gap_block && n_cards > 1) gap_blockers();
}

/* [Preview] */
cutaway = false;   // true = show a cross-section (for checking, not for printing)

if (cutaway)
    difference() { shroud(); translate([-150, 0, -10]) cube([300, 150, 200]); }
else
    shroud();

// Print info in the console
echo(str("Stack tail face (nominal): ", stack_x, " x ", card_h, " mm; cooler gap: ", gap, " mm; blocker: ", bt, " mm"));
echo(str("Total length: ", z_end, " mm (+ fan ", 38, " mm)"));