I want to build a portable version of this with bluetooth, battery, and low-profile switches.
Ergonomic, self-fitted, just the right size to fit on my Framework Laptop with little stand-offs so that it can rest on the clamshell without hitting any keys, just the right size to fit in to a laptop travel bag. i like the idea of keeping a folding split design like the rev0 has but made well enough to travel safely, but if I could build something less than 5mm thick it doesn't matter quite so much, does it?
Here's a low-profile choc PCB I could have printed, and it might even work:
Rev 1.1 Photos
Rev 1.0 Photos
data/202405/15T195735.970398/dca6bce9f88bc034a9883c0366144918f3757669e693fb259e62e99a8b29e592.png
DONE Learn to design a PCB
ruiqimao/keyboard-pcb-guide seems like a good place to start learning how to design this thing and use KiCad, but ultimately I decided to short-cut right to using ergogen once the controls in pbcnew were familiar enough; ergogen doesn't output a schematic (could one be generated, probably???) so I skipped the ruiqimao guide as soon as I felt comfortable with those UIs in kicad, knew how to add footprint libraries, etc.
so we'll need kicad installed, and I don't really need the 3D modeling capabilities, so I'll insert the minimal installation of it in to My NixOS configuration:
{ pkgs, ... }:
{
home.packages = with pkgs; [ kicad-small ];
}NEXT get kicad 3d models for the PCB , could be useful maybe for making a case....
INPROGRESS Use Ergogen to generate a PCB
Ergogen will place the footprints and generate the rat's nest by running the generate_board script =nix run .#generate_board=. The diode footprints provide both through-hole and SMD options on both sides of the board, which is nice, but it makes it a bit of a pain to route when the time comes, very fiddly work.
My Ergogen Atreoid YAML
Ergogen lets you define a columnar-ergonomic keyboard layout by defining stagger relative to each finger, and can output DXF files, STLs, KiCAD schematics/netlists... I only use it to generate what I call the "finger point cloud", basically the center-of-mass for each key which is then placed on to a 3d printed baseplate using a crappy python script I wrote.
here's a start from measuring my semi-curled finger lengths:
units:
# Parameters
row_spacing: 1cy
usb_cutout_x: 0
usb_cutout_y: 72
usb_cutout_r: 0
# Constants
choc_cap_x: 17.5
choc_cap_y: 16.5
choc_plate_thickness: 1.2
mx_plate_thickness: 1.5
apm_height: 33
outline_slop: 2.5
pcb_thickness: 1.6
case_thickness: 1.5
case_height: 10The matrix is wired up thus: careful study of this with the docs alongside is probably good. First you set up the points, all the places you want to place a keyboard key, and how those keys should be routed in the rat's nest:
points:
key: # each key across all zones will have these properties
bind: 5
width: choc_cap_x
height: choc_cap_y
zones:
matrix:
rows:
bottom.row_net: R3
home.row_net: R2
top.row_net: R1
num.row_net: R0
columns:
pinkycluster:
key:
stagger: 18
column_net: C0
mirror.column_net: C13
rows:
bottom.skip: true
num.skip: true
top.skip: false
pinky:
key:
stagger: 9
column_net: C1
mirror.column_net: C12
rows.bottom: $unset
ring.key:
stagger: -8
column_net: C2
mirror.column_net: C11
middle.key:
stagger: 8
column_net: C3
mirror.column_net: C10
index.key:
stagger: -14
column_net: C4
mirror.column_net: C9
inner.key:
bind: [44.5,10] # this is the "root" of the rest of the keys
stagger: -4
column_net: C5
mirror.column_net: C8
thumb:
key:
stagger: -6
column_net: C6
mirror.column_net: C7
rows:
bottom.key.bind: 10
num: $unset
rotate: -15
mirror:
ref: matrix_thumb_home
distance: 50After that you can define outlines and components for the hardware like switches, buttons, MCUs, etc below in Ergogen Hardware Details.
NEXT document this better
some day i'll document this better.
Hey Smell This, spend some time reverse engineering this config in the unofficial ergogen webui
NEXT how does this yaml thingy work
NEXT how i measured my fingers
NEXT philosophy of the layout and the layer system
NEXT document how i made the ratsnest
Ergogen Hardware Details
: add a bit about the footprint placement stuff and how it works in ergogen
These two footprints are the hotswap sockets for the keyswitches along with a set of thru-hole and surface mount diode locations
pcbs.monticello-drive.footprints.key:
what: choc
where: true
params:
from: "{{colrow}}"
to: "{{column_net}}"
hotswap: true
pcbs.monticello-drive.footprints.diodes:
what: diode
where: true
adjust:
rotate: 180
shift: [0, -8]
params:
from: "{{colrow}}"
to: "{{row_net}}"There is a nice!nano v2 footprint module in my fork of ergogen, it's basically an arduino pro micro but uses the nRF GPIO names and has the three extra pins that we use to route the display i2c.
The pin->net definitions here are duplicated between the zmk and ergogen configuration, I guess, we'll have to de-duplicate this some day.
I tried also to position it between the point of the highest keys, the middle fingers' top row, but I still have to do some fiddly bullshit to line it all up. Maybe I can make this positioning nicer when I generate outlines.
pcbs.monticello-drive.footprints.nice_nano_v2:
what: nice_nano_v2
params:
orientation: up
pad_type: thru_hole
P0_06: C0
P0_08: C1
P0_17: C2
P0_20: C3
P0_22: C4
P0_24: C5
P1_00: C6
P0_10: C7
P1_11: C8
P1_13: C9
P1_15: C10
P0_02: C11
P0_29: C12
P0_31: C13
P0_11: R0
P1_04: R1
P1_06: R2
P0_09: R3
P1_02: SDA
P1_07: SCL
adjust.rotate: -90
adjust.shift: [0, -apm_height/2 + 15 + outline_slop]
where.aggregate.parts:
- ref: matrix_middle_num
- ref: mirror_matrix_middle_num: add an image showing the routes from MCU to columns and rows and display; maybe just write out a schematic some day...
The keyboard has a 2-prong JST or solder-point for hooking up a battery close enough to the mcu and OLED that you could stash it underneath or nearby, along with a small PCM12-compatible slider switch between the battery and the MCU's battery input.
pcbs.monticello-drive.footprints.battery_jst:
what: jstph
params:
pos: BATTERY_P
neg: GND
where.aggregate.parts:
- ref: matrix_thumb_home
- ref: mirror_matrix_thumb_home
adjust.shift: [0, 3U]
pcbs.monticello-drive.footprints.battery_sw:
what: slider
params:
from: BATTERY_P
to: BATIN
adjust.shift: [1U, 17] # one mm lower than top of nice!nano
where.aggregate.parts:
- ref: matrix_middle_num
- ref: mirror_matrix_middle_numIt has a 4 pin through-hole for an OLED display, you'll want to socket this thing, probably, though it may make your board thicker than you want:
pcbs.monticello-drive.footprints.oled_jumper:
what: oled
params:
SDA: SDA
SCL: SCL
# adjust.rotate: -90
adjust.shift: [-20, 0] # weird label is included in calc
where.aggregate.parts:
- ref: matrix_inner_num
- ref: mirror_matrix_inner_numIt has a small momentary button routed to the bootloader reset functionality. This is only really necessary for the first time you flash it since the keymap has a bootloader combo on CTRL layer's top pinkycluster key, and you could also use a jumper to ground RST, but idk it's comforting in a way and doesn't cost much.
pcbs.monticello-drive.footprints.bootloader_rst:
what: button
params:
from: GND
to: RST
side: F
adjust.shift: [1U, 10]
where.aggregate.parts:
- ref: matrix_middle_num
- ref: mirror_matrix_middle_numBoard Outline
holy magic numbers, batman! this was iterated on in the Ergogen web ui, you'll end up changing it if you change any of the point settings.
There are the main outlines (from AAA to BBB) where we make a sort of rounded bun by overlapping two circles then cut the main body with a third circle.
(BBB to CCC) This is filleted to have rounded corners, and a cutout is added for the USB-C connector of a flush-fit MCU
(CCC to DDD) demarcate the footprints of the choc keyswitches
(DDD to EEE) provides an outline that is used in the top case to add an extrusion to make it easy to hit the board reset button.
(EEE to FFF) define case cutouts for the hardware on the board
the insets (FFF to GGG) are used to create the bottom case that "wraps" around the bottom of the board.
(GGG to end) define the top case as a bunch of those hardware outline definitions and a "key exclusion area" extended from the points of the keyboard.
pcbs.monticello-drive.outlines.main:
outline: combined
outlines:
# ===================AAA===========================
_bottom_arch_circle:
- what: circle
radius: 490
where:
aggregate.parts:
- ref: matrix_middle_bottom
- ref: mirror_matrix_middle_bottom
shift: [0, -415]
_top_arch_circle:
- what: circle
radius: 197
where:
aggregate.parts:
- ref: matrix_middle_bottom
- ref: mirror_matrix_middle_bottom
shift: [0, 140]
_main_body_circle:
- what: circle
radius: 147
where:
aggregate.parts:
- ref: matrix_middle_bottom
- ref: mirror_matrix_middle_bottom
shift: [0, 0]
_main: [
_top_arch_circle,
~_bottom_arch_circle,
~_main_body_circle
]
# ================== BBB ====================
_fillet:
- what: outline
name: _main
fillet: 6
_usb_c_cutout:
- what: rectangle
size: [9.28, 6.67]
where: &usbanchor
aggregate.parts:
- ref: matrix_middle_bottom
- ref: mirror_matrix_middle_bottom
shift: [ usb_cutout_x, usb_cutout_y ]
rotate: usb_cutout_r
combined: [
_fillet,
-_usb_c_cutout
]
# ================== CCC ====================
_switch_cutouts:
- what: rectangle
where: true
asym: source
size: 14 # Plate cutouts are 14mm * 14mm for both MX and Choc
bound: false
switch_plate:
[ combined, -_switch_cutouts]
# ================== DDD ====================
_power_reset_button:
- what: circle
radius: 1.5
adjust.shift: [1U, 10]
where.aggregate.parts:
- ref: matrix_middle_num
- ref: mirror_matrix_middle_num
# ================== EEE ====================
_usb_c_cutout_case:
- what: outline
name: _usb_c_cutout
expand: 2
_oled_cutout:
- adjust.shift: [0, -7]
where.aggregate.parts:
- ref: matrix_inner_num
- ref: mirror_matrix_inner_num
size: [ 40, 14 ]
what: rectangle
_promicro:
- adjust.rotate: -90
adjust.shift: [0, -apm_height/2 + 15 + outline_slop]
where.aggregate.parts:
- ref: matrix_middle_num
- ref: mirror_matrix_middle_num
what: rectangle
size: [40,19]
_power_switch:
- what: rectangle
size: [8, 6]
adjust.shift: [1U, 17] # one mm lower than top of nice!nano
where.aggregate.parts:
- ref: matrix_middle_num
- ref: mirror_matrix_middle_num
_power_switch_outer_case:
- what: outline
name: _power_switch
expand: 2
_combined_case_expand:
- what: outline
name: _fillet
expand: 2
expanded_case:
[ _combined_case_expand]
# ================== FFF ====================
inset:
- what: outline
name: _fillet
expand: -2
inset_bottom:
- what: outline
name: _fillet
expand: -4
# ================== GGG ====================
_key_exclusion:
- what: rectangle
where: true
asym: source
size: 19
bound: false
case_top:
[ combined, -_key_exclusion, -_oled_cutout, -_promicro, -_power_switch, -_power_reset_button]Many of those outlines are used by these case definitions (and of course the PCB below) but this case is basically designed to wrap around the sides of the board, and then there is an in-set cover on the top, all of this is held together with mounting putty until i decide to do something better, perhaps glued-in magnets or something. The problem is that the case footprints don't fit on my home printer so it has to be done in multiple parts and glued together.
cases:
bottom_outer_case:
- name: expanded_case
extrude: case_height + 2 # dark laughter magic number
- name: combined
extrude: case_height - case_thickness
shift: [0, 0, case_thickness]
operation: subtract
- name: _usb_c_cutout_case
extrude: case_height
shift: [0,2,case_thickness]
operation: subtract
- name: _power_switch_outer_case
extrude: case_height
shift: [0,2,pcb_thickness+case_thickness]
operation: subtract
- name: inset_bottom
extrude: case_height - pcb_thickness - case_thickness
operation: subtract
top_inner_case:
- name: case_top
extrude: case_height - pcb_thickness
- name: inset
extrude: case_height - case_thickness - pcb_thickness
operation: subtract
- name: _power_reset_button
extrude: case_height - 1.4 - 0.45 - case_thickness
shift: [0,0, 1.4 + 0.25 ]Sorry about the magic numbers...
BoM for Revision 1.1
These are one-time purchases that you can re-use between board revisions if you're careful, but maybe also keep a couple around in case you get a wild hair up your ass to make a new revision of the board:
| 1 | 25.00 | BoardSource Nice!Nano v2 | Nice!Nano v2 (gl finding these in stock) |
| 1 | 14.00 | BoardSource SuperMini NRF52840 | n!n2 alternative, maybe |
| 1 | 55 | Boardsource Choc keycaps set | |
| 5 | 6 | Boardsource Choc1 compatible 10x | i like the browns |
| 1 | 6 | BoardSource 128x32 OLED | |
| 1 | 5.50 | BoardSource 110mAh battery | or bring your own like i did |
I tried that SuperMini as an nice!nano v2 alternative for rev 1.1 and it's basically great, cheaper, and more available, except I think the 3 extra "inner" pins are in a different location, and the behavior of the bright blue LEDs on the board is much more annoying.
Soldering a board revision will consume these, consider buying a couple extra of each to build up a supply of spare parts even though some things ship 2x, pcbway MoQ is 5x. You'll never regret owning 72 extra through hole diodes!
| Q | C | ID | Desc |
|---|---|---|---|
| 2 | 8.40 | BoardSource Mill-Max Socket Set | pin & socket set for APM/etc doesn't break out SDA/SCL |
| 1 | 1.00 | BoardSource PTS526 | 2x 5.2mm reset momentary buttons |
| 5 | 1.50 | BoardSource CPG135001S30 | Choc hot swap sockets 10 pack |
| 5 | 0.50 | BoardSource TH diodes 10x pack | Diodes; SMD or through-hole both work, get em wherever |
| 1 | 2 | Boardsource SMD slide switch |
you might want to build this with SMD diodes, it's easier to do it with thru-holes but you could make a Monticello Drive that doesn't have any of the diode thru-hole or back SMD pads and slap a big silkscreen on it to end up with a pretty slick looking little device. for now, especially while i'm prototyping, i'm sticking to thru-hole diodes.
INPROGRESS Building the Monticello Drive
Having iterated on a base ergogen.yaml matrix to something you enjoy typing on using Revision Zero to prototype the keyboard, you now want a portable reliable keyboard.
You've added the hardware components to the board and generated the outlines following this document. Now it's time to open up kicad and route a PCB and have 5 PCBs manufactured of a prototype. Make sure when you order on PCBway you put those files in to version control and add a git tag or something. You'll have to just experiment with the routing and get good at it, it takes a while to get the hang of it and figure out how to design the board well. The repo has mine, you'll like to use it as inspiration in the [rev1.1] tag.
I'll have to do a better job explaining this some day!
With a keyboard built, you can flash the Rev1 Firmware to it.
NEXT upstream or literize the customizations and footprints i added to ergogen clone
there is a nice!nano compatible module and a nix flake in there at least. when prototyping, the nice_nano_v2 module needed to be replaced with a promicro.
DONE Revision One.1
need to think about "softer" revisions, and tweaks i should make to the layout itself, not just the board process and feature set! write a report of 1.0
switch back to through-hole mcu connection, gonna use those 8.5mm headers until i'm confident in a build
all the other rev1_1 tagged tasks below of course
DONE flip the OLED connector around
woops
DONE add a slider switch for the power
DONE figure out why the battery wireup doesn't work
It works when I use the small BATIN leads that I can't attach to headers, but not the header-able RAW pin... annoying.
i think this might have just been my flush-solder job, possibly
DONE switch footprint to choc hotswap sockets
NEXT fit the trackpoint module in to all of this.
i could make those cuts on the rev2 board so that there is a plane that the module plate could be soldered to
or it could be a part of the 3d printed model case so that it could be raised up to a more comfortable height without making internal cuts
i was thinking about it and
the board is two parts
one is the little sensor itself with four larger pins with a flush connector
one is the control board that spits out ps/2 and a bunch of other shit. we only need like one pin and the reset/init circuit, and like vcc/gnd, i think. i have this written down somewhere or can find it again in that cursed discord.
the sensor board needs to be mounted to the case or the pcb; if it's pcb mounted it ends up being screwed until it's flush tight in to some thru-holes w/ requisite tolerance for the screws, then those are filed off
the connection between the sensor and the processor is a bit complicated because these connectors are all under-side mounted w/o edge leads or through-holes;
i could leave it in its default package and carefully measure out the location to put down a zif connector or solder mess for the four pins on this zif that i need to route to somewhere on the mcu. i hope i only need one data pin for the ps/2 data, cause that's all I have!
DONE add outlines to the YAML file
DONE figure out why the silk screens for the nice nano etc weren't on the final boards, add them back, silkscreen something cool on here
WAITING get ergogen to generate schematics.
WAITING convert to choc v2
maybe idk; sockets while we're at it
WAITING kind of enamored by these PG1316 key switches I saw on reddit, something to consider in future sub-revisions...
waiting on a group buy or something, it's kind of the only reason I check reddit any more lol.
WAITING produce AxiDraw work from the SVG output of the PCB and schematic
Rev1 Firmware
The shield module is in ./zmk-config and should be detangled below. It's basically the same as the rev0's, but the matrix is laid out a bit differently compared to how i set it up in rev0, basically just in the pin layout for the columns are mirrored or flipped in some places and also scl/sda for the oleds are flipped. like i said, basically the same.
The metadata file for the keyboard shield makes sure that zmk knows that it needs to build the keyboard.
file_format: "1"
id: monticello_drive_rev1
name: r_1.0
type: shield
url: https://arcology.garden/my/atreoid
requires: [pro_mciro]
exposes:
- i2c_oled
features:
- keys
- displaysome magic bullshit lol, configuring the OLED and basic stuff no one should need to touch:
config SHIELD_MONTICELLO_DRIVE_REV1
def_bool $(shields_list_contains,monticello_drive_rev1)if SHIELD_MONTICELLO_DRIVE_REV1
config ZMK_KEYBOARD_NAME
default "Monty Drive r1.1"
if ZMK_DISPLAY
config ZMK_DISPLAY
select LV_USE_CONT
select LV_FONT_MONTSERRAT_26
select LV_FONT_MONTSERRAT_16
select LV_USE_LABEL
select LV_USE_IMG
config I2C
default y
config SSD1306
default y
config SSD1306_REVERSE_MODE
bool
default n
endif # ZMK_DISPLAY
if LVGL
config LV_Z_VDB_SIZE
default 64
config LV_DPI_DEF
int
default 148
config LV_Z_BITS_PER_PIXEL
default 1
choice LV_COLOR_DEPTH
default LV_COLOR_DEPTH_1
endchoice
endif # LVGL
endifThe keyboard is configured in a fairly straightforward, if baroque manner using something like the Linux kernel build process. these devicetree files we're working with are a format that is easy to use to merge multiple trees of configuration, to reach in to the device tree to re-configure things, etc and then generate code based on that tree... i think? it's kind of like how NixOS constructs its config attrset merging a bunch of files together to construct an entire configuration and then build a derivation recursively out to each dependency in the attrset until a root file system or activation profile is provided.
Usually you just need to configure the pin mapping in the matrix you've wired up. I also define a transformation matrix so that the left hand and right hand are defined on their own rows for legibility.
This makes the error messages coming out of the gen_defines.py script that zmk uses to generate C files from these device trees much worse than they are otherwise, but it allows for much more legible documents and keymap configuration, and the errors you get if you make a syntax error in the keymap file are already a pain to parse as it is!
#include <dt-bindings/zmk/matrix_transform.h>
/ {
chosen {
zephyr,display = &oled;
zmk,kscan = &kscan0;
zmk,matrix-transform = &atreoid_scan;
zmk,battery = &vbatt;
};
kscan0: kscan_0 {
compatible = "zmk,kscan-gpio-matrix";
diode-direction = "col2row";
wakeup-source;
col-gpios
= <&pro_micro 1 GPIO_ACTIVE_HIGH>
, <&pro_micro 0 GPIO_ACTIVE_HIGH>
, <&pro_micro 2 GPIO_ACTIVE_HIGH>
, <&pro_micro 3 GPIO_ACTIVE_HIGH>
, <&pro_micro 4 GPIO_ACTIVE_HIGH>
, <&pro_micro 5 GPIO_ACTIVE_HIGH>
, <&pro_micro 6 GPIO_ACTIVE_HIGH>
, <&pro_micro 16 GPIO_ACTIVE_HIGH>
, <&pro_micro 14 GPIO_ACTIVE_HIGH>
, <&pro_micro 15 GPIO_ACTIVE_HIGH>
, <&pro_micro 18 GPIO_ACTIVE_HIGH>
, <&pro_micro 19 GPIO_ACTIVE_HIGH>
, <&pro_micro 20 GPIO_ACTIVE_HIGH>
, <&pro_micro 21 GPIO_ACTIVE_HIGH>
;
row-gpios
= <&pro_micro 7 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>
, <&pro_micro 8 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>
, <&pro_micro 9 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>
, <&pro_micro 10 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>
;
};
atreoid_scan: matrix_transform {
compatible = "zmk,matrix-transform";
rows = <4>;
columns = <14>;
map = <
// left hand
RC(1,0) RC(0,1) RC(0,2) RC(0,3) RC(0,4) RC(0,5)
RC(2,1) RC(1,1) RC(1,2) RC(1,3) RC(1,4) RC(1,5) RC(1,6)
RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(2,5) RC(2,6)
RC(3,2) RC(3,3) RC(3,4) RC(3,5) RC(3,6)
// right hand
RC(0,8) RC(0,9) RC(0,10) RC(0,11) RC(0,12) RC(1,13)
RC(1,7) RC(1,8) RC(1,9) RC(1,10) RC(1,11) RC(1,12) RC(2,13)
RC(2,7) RC(2,8) RC(2,9) RC(2,10) RC(2,11) RC(2,12)
RC(3,7) RC(3,8) RC(3,9) RC(3,10) RC(3,11)
>;
};
vbatt: vbatt {
compatible = "zmk,battery-nrf-vddh";
};
};The shield definition also includes some magic to configure the OLED display with the i2c running on non-standard pins; I use two of three in the middle of the PCB rather than the standard arduino i2c pins, those are in the matrix array.
&pro_micro_i2c {
status = "okay";
oled: ssd1306@3c {
compatible = "solomon,ssd1306fb";
reg = <0x3c>;
width = <128>;
height = <32>;
segment-offset = <0>;
page-offset = <0>;
display-offset = <0>;
multiplex-ratio = <31>;
segment-remap;
com-invdir;
com-sequential;
inversion-on;
prechargep = <0x22>;
};
};
// remember that when I build a PCB version of this that i had to swap SDA and SCL on the board!
&pinctrl {
i2c0_default: i2c0_default {
group1 {
psels =
<NRF_PSEL(TWIM_SDA, 1, 2)>,
<NRF_PSEL(TWIM_SCL, 1, 7)>;
};
};
i2c0_sleep: i2c0_sleep {
group1 {
psels =
<NRF_PSEL(TWIM_SDA, 1, 2)>,
<NRF_PSEL(TWIM_SCL, 1, 7)>;
low-power-enable;
};
};
};NEXT 3D printed board case
ergogen can output simple cases based on the DXF outlines; the ergogen.yaml has some outlines and cases. generating the STLs for print is kind of a pain, i owe it to myself and anyone else to document the process i went through to generate the cases i defined in the Outline documentation above.
ergogen will generate jscad files
run an old fork of openjscad to generate the STL (this should fold in to the ergogen cli)
figure out how to fit it in to your printer
using openscad to modify it with BOSL2 jointers etc may require you to go in to tinkercad or something to make the STL valid enough for openscad/CGAL to work with. this is a pain and probably could be alleviated by more careful case design to generate valid solids, or the jscad driver could be improved. idk. tinkercad is free but requires an account and it doesn't export the model with its prior origin.



