The thing I’m sharing today is a very early and very small slice of the project that is likely going to consume the next few years of my life. This is a parametric generator for M.2 expansion card mockups in OpenSCAD.

I can’t imagine this is useful to many people, but the beauty of open source software and hardware is that it doesn’t have to be. Even if only one person happens upon this years down the road and it saves them a few hours or gives them the reference point they needed, it was worth sharing. Sharing knowledge isn’t zero-sum; it’s exponential (though maybe not in this specific instance).

```
// M.2 Mockup Generator
// by Nirav Patel <https://eclecti.cc>
//
// To the extent possible under law, Nirav Patel has waived all copyright
// and related or neighboring rights to M.2 Mockup Generator.
//
// An easy to use parametric M.2 mockup generator.
// See https://en.wikipedia.org/wiki/M.2 for more information about the
// M.2 (formerly NGFF) standard.
edge_width = 19.85;
edge_length = 4;
edge_r = 0.5;
hole_r = 1.75;
key_r = 0.6;
key_length = 3.5;
pcb_thick = 0.8;
bottom_edge_keepout = 5.2;
top_gnd_pad_r = 2.75;
bottom_gnd_pad_r = 3;
bottom_gnd_pad_offset = 1;
bevel = 0.3;
bevel_angle = 20;
$fn=50;
module key_cutout() {
union() {
translate([0, key_length - key_r]) circle(r = key_r);
translate([-key_r, -1]) square([key_r*2,key_length - key_r +1]);
}
}
function get_key(key_type="mm") =
key_table[search([key_type],key_table,1,0)[0]][1];
key_table = [["", 100], ["A", 6.625], ["B", 5.625], ["C", 4.625], ["D", 3.625], ["E", 2.625], ["F", 1.625], ["G", -1.125], ["H", -2.125], ["J", -3.125], ["K", -4.125], ["L", -5.125], ["M", -6.125]];
module m2_2d(width=22, length=30, key="M", key2="") {
// Start with a 2d version
difference() {
square([width, length]);
// Left cutout
translate([(width-edge_width)/2-width, -edge_r]) square([width, edge_length]);
translate([(width-edge_width)/2-width-edge_r, 0]) square([width, edge_length]);
translate([(width-edge_width)/2-edge_r, edge_length-edge_r]) circle(r=edge_r);
// Right cutout
translate([width-(width-edge_width)/2, -edge_r]) square([width, edge_length]);
translate([width-(width-edge_width)/2+edge_r, 0]) square([width, edge_length]);
translate([width-(width-edge_width)/2+edge_r, edge_length-edge_r]) circle(r=edge_r);
// Hole cutout
translate([width/2, length]) circle(r = hole_r);
// Key cutouts
translate([width/2 + get_key(key), 0])
key_cutout();
translate([width/2 + get_key(key2), 0])
key_cutout();
}
}
module m2_text(width=22, length=30, height="", key="", key2="") {
if (key2 != "") {
string = str(width, length, "-", height, "-", key, "-", key2);
linear_extrude(height = 1)
text(string, size = 2.5);
} else {
string = str(width, length, "-", height, "-", key);
linear_extrude(height = 1)
text(string, size = 2.5);
}
}
module m2(width=22, length=30, height="", key="", key2="") {
union() {
difference() {
linear_extrude(height = pcb_thick)
m2_2d(width, length, key, key2);
// The card edge bevel
translate([0, bevel, 0]) rotate([180-bevel_angle, 0, 0]) cube([width, 10, 10]);
translate([0, bevel, pcb_thick]) rotate([90+bevel_angle, 0, 0]) cube([width, 10, 10]);
}
difference() {
// The various component keepout zones and embossed text
if (height == "S1") {
difference() {
translate([0, edge_length, pcb_thick]) cube([width, length-edge_length, 1.2]);
translate([1, edge_length+1, pcb_thick+1.2-0.5]) m2_text(width, length, height, key, key2);
}
} else if (height == "S2") {
difference() {
translate([0, edge_length, pcb_thick]) cube([width, length-edge_length, 1.35]);
translate([1, edge_length+1, pcb_thick+1.35-0.5]) m2_text(width, length, height, key, key2);
}
} else if (height == "S3") {
difference() {
translate([0, edge_length, pcb_thick]) cube([width, length-edge_length, 1.5]);
translate([1, edge_length+1, pcb_thick+1.5-0.5]) m2_text(width, length, height, key, key2);
}
} else if (height == "D1") {
difference() {
translate([0, edge_length, pcb_thick]) cube([width, length-edge_length, 1.2]);
translate([1, edge_length+1, pcb_thick+1.2-0.5]) m2_text(width, length, height, key, key2);
}
translate([0, bottom_edge_keepout, -1.35]) cube([width, length-bottom_edge_keepout, 1.35]);
} else if (height == "D2") {
difference() {
translate([0, edge_length, pcb_thick]) cube([width, length-edge_length, 1.35]);
translate([1, edge_length+1, pcb_thick+1.35-0.5]) m2_text(width, length, height, key, key2);
}
translate([0, bottom_edge_keepout, -1.35]) cube([width, length-bottom_edge_keepout, 1.35]);
} else if (height == "D3") {
difference() {
translate([0, edge_length, pcb_thick]) cube([width, length-edge_length, 1.5]);
translate([1, edge_length+1, pcb_thick+1.5-0.5]) m2_text(width, length, height, key, key2);
}
translate([0, bottom_edge_keepout, -1.35]) cube([width, length-bottom_edge_keepout, 1.35]);
} else if (height == "D4") {
difference() {
translate([0, edge_length, pcb_thick]) cube([width, length-edge_length, 1.5]);
translate([1, edge_length+1, pcb_thick+1.5-0.5]) m2_text(width, length, height, key, key2);
}
translate([0, bottom_edge_keepout, -0.7]) cube([width, length-bottom_edge_keepout, 0.7]);
} else if (height == "D5") {
difference() {
translate([0, edge_length, pcb_thick]) cube([width, length-edge_length, 1.5]);
translate([1, edge_length+1, pcb_thick+1.5-0.5]) m2_text(width, length, height, key, key2);
}
translate([0, bottom_edge_keepout, -1.5]) cube([width, length-bottom_edge_keepout, 1.5]);
} else {
translate([1, edge_length+1, pcb_thick-0.5]) m2_text(width, length, height, key, key2);
}
// The ground pad keepouts
translate([width/2, length, pcb_thick]) cylinder(r = top_gnd_pad_r, h = 10);
translate([width/2, length-bottom_gnd_pad_offset, -10]) {
cylinder(r = bottom_gnd_pad_r, h = 10);
translate([-bottom_gnd_pad_r, 0, 0]) cube([bottom_gnd_pad_r*2, bottom_gnd_pad_offset, 10]);
}
}
}
}
m2(width=22, length=30, height="S3", key="A", key2="E");
```

This is also up on GitHub as a Gist.