Look, I know I have a lot of unfinished posts. But this thing is pretty interesting to me. First, I have always wanted to do some LISP or Scheme. Second, I am really into keyboard and ergo shit since last year. Third, the keyboard I'm trying to fuck up is akin to my juvenile love or something along that line. Fourth, believe it or not, KiCad's pcb file format and component footprints seem really like S-Expr which is really neat.
kicad_mod
overviewTo give you an example, the following snippet is from ai03's repo:
(module SOT143B (layer F.Cu) (tedit 5C42C5FB)
Which means this module is named SOT143B
and will be placed at the front layer,
(attr smd)
is a footprint for an SMD component,
(fp_text reference REF** (at 0 2.45) (layer F.SilkS) (effects (font (size 1 1) (thickness 0.15))))
(fp_text value SOT143B (at 0 -2.3) (layer F.Fab) (effects (font (size 1 1) (thickness 0.15))))
will have REF**
with 0.15
mm thickness printed on the front layer silk,
(fp_line (start 0.55 -1.45) (end 0.55 -0.65) (layer F.Fab) (width 0.15))
; snip
(fp_line (start -1.45 -0.65) (end 1.45 -0.65) (layer F.SilkS) (width 0.15))
will have some lines on the front layer, especially as fabrication and silks, according
to the start
and end
expression,
(pad 3 smd rect (at 0.95 -1) (size 0.6 0.7) (layers F.Cu F.Paste F.Mask))
(pad 2 smd rect (at 0.95 1) (size 0.6 0.7) (layers F.Cu F.Paste F.Mask))
(pad 4 smd rect (at -0.95 -1) (size 0.6 0.7) (layers F.Cu F.Paste F.Mask))
(pad 1 smd rect (at -0.75 1) (size 1 0.7) (layers F.Cu F.Paste F.Mask))
)
and have 4 smd pads shaped as rectangles on front cuprum layer. As you can see, however you look at it, that's an S-Expr!
Well, whatever, let's get weird.
I am going to write some short and simple explanation on a modified atreus codebase by replacing Pololu's A-Star with common Promicro, swapping MX-Alps switch style footprint with MX-Alps-Choc switch style footprint.
; atreus.rkt
#lang racket
Well, because Racket has so many things, the line above tells that I am specifically using racket dialect.
(define cols 12)
(define rows 4)
(define x-offset 20)
(define y-offset 20)
(define spacing 19)
(define angle 10)
(define column-offsets '(8 5 0 6 11 65 65 11 6 0 5 8))
There are some definitions here:
cols
is the amount of columns this keyboard has.rows
is the amount of rows this keyboard has.x-offset
is the distance between the T
and Y
keys on QWERTY layout.y-offset
is the distance between the same thing, I guess.spacing
is the size of a single keycap.
You know, common switch spacing of common mechanical keyboards is 19mm.
But if you want to use Choc style switch, I think it should be around 18mm.angle
is the angle the keyboard should be slanted by.
The higher you set this, the more slanted it becomes and I think the more it will
be comfortable to you, to some extent.column-offsets
is a list of how much should a column be pushed down.
The higher the value set, the lower it will be.
For example, the first value is 8
so it will be pushed 8mm down.
While the sixth value, 65
, the key will be pushed 65mm down and that is the
thumb button.Now, the basic definitions have been set, now I will define a module for the switch footprint:
(define (switch-module x y rotation label net-pos net-neg)
First, I defined what a switch-module
needs:
x
is the x coordinate where a switch should be placed at.y
is the y coordinate where a switch should be placed at.rotation
is how much a switch should be rotated.label
, what should we label it.net-pos
and net-neg
are where should this switch be connected at.
Why? well, because electronics work by harnessing the movements of electrons
from a place to another.
In this case, one of the legs should be connected to the micro controller and
the other leg should be connected to the negative polar of a diode.The snippet below is an incomplete version of a switch footprint by keeb.io the sake of briefness.
`(module Keebio-Parts:MX-Alps-Choc-1U-NoLED (layer Front) (tedit 4FD81CDD) (tstamp 543EF801)
o The line above is a macro (you can see the tilde at the start of the line) that defines the start of a footprint library and its "properties", if you permit me to use that word. But, the first line of that definition also contains some information:
Keebio-Parts:MX-Alps-Choc-1U-NoLED
.(layer)
expression.(tedit)
expression.(tstamp)
expression. (at ,x ,y ,rotation)
This is where we should put the footprint. As you can see above, each of token that is started with a comma, will be replaced by the provided parameter which has the same name.
(path /543DB910)
(fp_text reference ,label (at 0 3.302 ,rotation) (layer F.SilkS) (effects (font (size 1.524 1.778) (thickness 0.254))))
(fp_line (start -9.525 9.525) (end -9.525 -9.525) (layer Dwgs.User) (width 0.15))
(fp_line (start 5 -7) (end 7 -7) (layer Dwgs.User) (width 0.15))
(pad "" np_thru_hole circle (at -5.22 4.2 ,(+ 48.1 rotation)) (size 1.2 1.2) (drill 1.2) (layers *.Cu *.Mask))
(pad "" np_thru_hole circle (at -5.08 0 ,(+ 48.1 rotation)) (size 1.75 1.75) (drill 1.75) (layers *.Cu *.Mask))
(pad 1 thru_hole circle (at 5 -3.8 ,rotation) (size 2 2) (drill 1.2) (layers *.Cu *.Mask) ,net-pos)
(pad 2 thru_hole oval (at -3.81 -2.54 ,(+ 48.1 rotation)) (size 4.211556 2.25) (drill 1.47 (offset 0.980778 0)) (layers *.Cu *.Mask) ,net-neg)))
list of s-exp above should be clear based on what I've written at the first snippet.
(define (diode-module x y rotation label net-pos net-neg)
;skipped for brevity.
)
diode-module
's definition should pretty much the same with switch-module
above.
The interesting part is the next one, microcontroller-module
.
(define microcontroller-module
Module definition without parameter, because I decided to hard wire it.
`(module PROMICRO (layer Front) (tedit 4FDC31C8) (tstamp 543EF800)
(at 134 45 270)
(path /543EEB02)
(fp_text reference Promicro (at 10 0) (layer F.SilkS) (effects (font (size 1 1) (thickness 0.15))))
(fp_line (start -15.24 7.62) (end 15.9 7.62) (layer F.SilkS) (width 0.381))
(fp_line (start 15.9 7.62) (end 15.9 -7.62) (layer F.SilkS) (width 0.381))
(fp_line (start 15.9 -7.62) (end -15.24 -7.62) (layer F.SilkS) (width 0.381))
S-expr-s above just define where at the front layer it should be placed, what should be drawn on it. Not really interesting, to be quite honest.
(pad B5 thru_hole circle (at 13.97 -6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) (net 4 N-row-3))
(pad B4 thru_hole circle (at 11.43 -6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) (net 3 N-row-2))
(pad E6 thru_hole circle (at 8.89 -6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) (net 15 N-col-10))
(pad D7 thru_hole circle (at 6.35 -6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) (net 11 N-col-6))
(pad C6 thru_hole circle (at 3.81 -6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask))
(pad D4 thru_hole circle (at 1.27 -6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) (net 13 N-col-8))
(pad D0 thru_hole circle (at -1.27 -6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) (net 12 N-col-7))
(pad D1 thru_hole circle (at -3.81 -6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) (net 14 N-col-9))
(pad GND thru_hole circle (at -6.35 -6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask))
(pad GND thru_hole circle (at -8.89 -6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask))
(pad RX1 thru_hole circle (at -11.43 -6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask))
(pad TX0 thru_hole rect (at -13.97 -6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask))
Now this one is interesting enough.
S-expr above define the left side of a promicro and which net
, or "cable connection" if you will,
each pin should be attached at.
For example, N-row-3
(third row of the keyboard) should be connected to pin B5
(bottom left).
But, when there's no s-expr that states which net
it should receive, then there's no connection incoming
to that legs.
For example, TX0
doesn't get connected to any switches or diodes.
(pad B6 thru_hole circle (at 13.97 6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) (net 2 N-row-1))
(pad B2 thru_hole circle (at 11.43 6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) (net 1 N-row-0))
(pad B3 thru_hole circle (at 8.89 6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) (net 10 N-col-5))
(pad B1 thru_hole circle (at 6.35 6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) (net 6 N-col-1))
(pad F7 thru_hole circle (at 3.81 6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) (net 5 N-col-0))
(pad F6 thru_hole circle (at 1.27 6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) (net 9 N-col-4))
(pad F5 thru_hole circle (at -1.27 6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) (net 8 N-col-3))
(pad F4 thru_hole circle (at -3.81 6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask) (net 7 N-col-2))
(pad VCC thru_hole circle (at -6.35 6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask))
(pad RST thru_hole circle (at -8.89 6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask))
(pad GND thru_hole circle (at -11.43 6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask))
(pad RAW thru_hole circle (at -13.97 6.35 270) (size 1.7526 1.7526) (drill 1.0922) (layers *.Cu *.SilkS *.Mask)))
)
Same goes with this section. But the difference is it defines the right side of the promicro.
The next part is where the net
s are defined.
(define nets
`((net 0 "")
(net 1 N-row-0)
(net 2 N-row-1)
(net 3 N-row-2)
(net 4 N-row-3)
net
takes two params.
The first one defines the number of the net and the second one as the
name of the net.
The part above defines the number and the name of the net
s for atreus'
first to fourth row.
(net 5 N-col-0)
(net 6 N-col-1)
(net 7 N-col-2)
(net 8 N-col-3)
(net 9 N-col-4)
(net 10 N-col-5)
(net 11 N-col-6)
(net 12 N-col-7)
(net 13 N-col-8)
(net 14 N-col-9)
(net 15 N-col-10)
While the snippet above defines the net
numbers and names for atreus'
columns.
Why does it define them 11 times, though?
Because on the left side, atreus has 5 columns, the right side 5 columns,
and the thumb buttons are counted as 1 column.
,@(for/list ([s (in-range 42)])
(list 'net (+ 16 s) (string->symbol (format "N-diode-~s" s))))))
The snippet above contains another macro and a list comprehension.
For the list comprehension, for/list
, the code thread through 1 to 42
and then, create a list which contains net
, s + 16
because it already
has 16 nets from the previous parts, and a formatted string N-diode-s
.
(define (net-class nets)
Just a net-class
definition that asks for one params.
Though the param in question should be a list.
(append '(net_class Default "This is the default net class."
(clearance 0.254)
(trace_width 0.2032)
(via_dia 0.889)
(via_drill 0.635)
(uvia_dia 0.508)
(uvia_drill 0.127))
The snippet above is the first part of a function where the nets
parameter
will be appended to the net_class
.
(for/list ([n nets])
(list 'add_net (last n)))))
Now, for/list
will iterate each and every element of nets
will be
used to create a list of list that contains add_net
and the last part of the
nets
elements, so it will result (add_net N-col-4)
, for example.
The next part of the code is defining where a should be placed at, how should it
be placed, etc, when somebody give some values of row
and col
.
(define (switch row col)
The code above is just function definition that takes two parameter, row
and col
.
(let* ([left? (< col 6)]
In the body of the function, there are some value definitions.
First one is whether the switch in question should be placed on the left side or not.
If the col
value is less than 6
, then it means the switch should be on the left
side of the keyboard.
[rotation (if left? -10 10)]
The next value defines how much a switch should be rotated.
Switches on the left should be rotated for -10
degrees (or 350
degrees) while
switches on the right should be rotated 10
degrees.
If somebody wants to change this value, they should also change the value of angle
above.
[x (* (+ 1 col) spacing)]
This x
value defines where should a switch be put at in X coordinate in cartesian
coordinate system.
As you can see, it calculated the value by incrementing the col
value and then
multiply it by 19
.
[y (+ (list-ref column-offsets col) (* spacing row))]
This y
value defines where a switch should be put at in Y coordinate in cartesian
coordinate system.
y
value is calculated by accessing the col
-th value of column-offsets
and then
adding it to the spacing
multiplied by row
.
So, the for col
equals to 1
and row
equals to 1
, y
value should be 5 + 19
or 24
.
[hypotenuse (sqrt (+ (* x x) (* y y)))]
It's just a standard hypotenuse
function.
[Θ (atan (/ y x))]
theta
function... I don't know perhaps wikipedia's arctangle knows...
[Θ′ (- Θ (degrees->radians rotation))]
I don't know how do you call it.
Can I say it as theta-prime
?
Anyway, this value just the difference between theta
and radian value of rotation
.
[x′ (+ (if left? x-offset 5) (* hypotenuse (cos Θ′)))]
This x-prime
is where the switch should be actually placed at after rotating it
by rotation
value.
[y′ (+ (if left? y-offset (+ y-offset 42.885)) (* hypotenuse (sin Θ′)))]
Same goes with y-prime
, where should a switch should be placed at after it is being
rotated by rotation
value by using some math and trigonometry.
You wouldn't believe on how much trig and math actually being used when you're
doing something like this, right?
Yeah yeah, I can hear your laments over it.
Students, you should pay attention to your classes.
You cannot use what you don't have.
[label (format "SW~a:~a" col row)]
This value is just what should the fab put on them.
In this case, it should conform the format SWcol:row
.
[diode (+ row (* col 4))]
In keyboard, one of the switch's legs should be connected to a diode.
This one, the value definition above, defines which diode it should connect to.
The value is calculated by multiplying col
by four and then adding it row
.
;; if we try to number nets linearly, kicad segfaults; woo!
;; so we re-use the nets we skipped with the missing col 5/6 diodes
[diode (cond [(> diode 44) (- diode 20)]
[(> diode 41) (- diode 21)]
[true diode])]
Just what the comment said.
[net-col (if left? col (- col 1))]
If the switch is on the right side, just decrement the column.
[diode-net `(net ,(+ 16 diode)
,(string->symbol (format "N-diode-~s" diode)))]
Just like what I've said previously, one of the legs of any switch should be connected
to the diode.
That's why, the net
for the diode should be defined here.
But why the first parameter of net
should be added by 16
you said?
Well, because the first 16
nets
were used for column and row net
s.
[column-net `(net ,(+ net-col 5)
,(string->symbol (format "N-col-~s" net-col)))]
Now, in common mechanical keyboard diy project, every switch in a row should be
connected by a single wire, or in this case, a net.
Therefore, one of the legs should have a net
connection to one of the column net
s.
Again, I heard you ask why the net-col
added by 5
.
Well, because the first 5
nets
were used for row net
s.
;; rotate middle keys additional 90° after calculating position
[rotation (cond [(= 5 col) 80]
[(= 6 col) 280]
[true rotation])])
This one redefines value when col
value is either 5
or 6
.
When the col
equals to 5
, or the left thumb, rotate the switch by 80
degrees.
When the col
equals to 6
, or the right thumb, rotate the switch by 280
degrees.
This way, your keycaps won't be broken because of some differences between x
and y
axis of the switch's stem.
You know, nerds are annoying when it comes to this kind of differences.
You will be surprised on how much people raged on the net because of Kaihua's Box
switches' stem's x and y axis has different thickness and crack nerds'
waste of money overpriced plastics expensive keycaps.
(switch-module x′ y′ rotation label
(if left? diode-net column-net)
(if left? column-net diode-net))))
Now, this is where the footprint library being used.
The next one is where diode placement being defined.
Almost most identical to switch
function, though.
(define (diode row col)
(let* ([left? (< col 6)]
[rotation (if left? -10 10)]
[x (* (+ 1 col) spacing)]
[y (+ (list-ref column-offsets col) (* spacing row))]
[hypotenuse (sqrt (+ (* x x) (* y y)))]
[Θ (atan (/ y x))]
[Θ′ (- Θ (degrees->radians rotation))]
[x′ (+ (if left? x-offset 5) (* hypotenuse (cos Θ′))
(if left? 9 -9))]
[y′ (+ (if left? y-offset (+ y-offset 42.885))
(* hypotenuse (sin Θ′)))]
[label (format "D~a:~a" col row)]
[diode (+ row (* col 4))]
;; if we try to number nets linearly, kicad segfaults; woo!
;; so we re-use the nets we skipped with the missing col 5/6 diodes
[diode (cond [(> diode 44) (- diode 20)]
[(> diode 41) (- diode 21)]
[true diode])]
See, practically the same as switch
explanation.
[net-row (cond [(= col 5) 2]
[(= col 6) 3]
[true row])])
The function above defines that the left thumb button should be considered as the third row while the right thumb button should be considered as the fourth row.
(diode-module x′ y′ rotation label
`(net ,(+ 16 diode)
,(string->symbol (format "N-diode-~s" diode)))
`(net ,(+ net-row 1)
,(string->symbol (format "N-row-~s" net-row))))))
When actually using diode-module
to use the library footprint, positive pad
should be connected to diode net
s while the negative pad should be connected
to the row net
s.
As for why the diode added by 16
is because, like I've written before, there
are 12
column net
s and 4
row net
s.
But why it only increments net-row
by 1
, not some other?
Well, because, you know, something something starts at 0
something something or 1
.
The next part is where the switch
es and diode
s are being joined together.
(define switches+diodes
Just a value definiton because it does not accept any parameters.
(for/list ([col (in-range cols)]
#:when true
[row (if (or (= 5 col) (= 6 col))
'(0) (in-range rows))])
A list comprehension using 0
- cols
(12
) and 0
- rows
(4
).
Except when col
equals to 5
or 6
, just use 0
.
Why? Thumb button is just one for each finger, right?
You remember that, right?
(list (switch row col) (diode row col))))
After that list comprehension, create a pair of switch
and diode
using the
result of that list comprehension.
(define edge-cuts
(for/list [(s '([31 22] [84 22] [141 30] [127 30] [185 22] [237 22] [250 95] [161 112] [107 112] [18 95]))
(e '([84 22] [127 30] [185 22] [141 30] [237 22] [250 95] [161 112] [107 112] [18 95] [31 22]))]
`(gr_line (start ,@s) (end ,@e) (angle 90) (layer Edge.Cuts) (width 0.3))))
Now, edge cut is where the pcb ends. Somehow I feel so pedantic when I explain something that is so clear.
Anyway, this value definition uses, again, a list comprehension using a list named s
which is a mnemonic for start
(surprise!) and e
which is a mnemonic for end
.
Those lists are used to create (gr_line)
though I'm quite sure those values are
created by manually put that in kicad.
(because that's what i do)
(define board
(apply append nets
(list (net-class nets))
(list microcontroller-module)
edge-cuts
switches+diodes))
Now, the board's definition is basically appending nets
, edge-cuts
, switches+diodes
,
and (net-class nets)
and microcontroller-module
that have been transformed into
a list with a single element.
(define (write-placement filename)
Function definition above, write-placement
, defines the main function where
(when (file-exists? filename) (delete-file filename))
when a file with filename
as its name exists, delete the file in question.
(call-with-output-file filename
save the body's output to the filename,
(λ (op)
by using a anonymous function op
(display (call-with-input-file "header.rktd" (curry read-string 9999)) op)
to display the content of header.rktd
(I'll talk about it later) by buffering
the content in memory,
;; kicad has this terrible bug where it's whitespace-sensitive here =(
(display "\n" op)
(for ([f board]) (pretty-print f op 1))
then put the content of board
list,
(display (call-with-input-file "traces.rktd" (curry read-string 999999)) op)
followed by storing the content of traces.rktd
,
(display ")" op))))
and finally put a close bracket at the end of the file.
I want to talk about header.rktd
and traces.rktd
first.
header.rktd
is where you put names, information, and, to put simply, some decorations
to the pcb.
To create that file, to be quite honest, I don't know.
But you can see an example of header.rktd
here.traces.rktd
is where you put the net routes.
Some people use autorouter like FreeRouter.org or something else, while some other
people route them manually.
Personally, I manually route first and then followed by letting autorouter does
its job, and then clean some weird traces.
For an example, check traces.rktd
here.
I get that by using egrep "\(via \(at|\(segment \(start" atreus.kicad_pcb > traces.rktd
.(write-placement "atreus.kicad_pcb")
Then, the last part is where the pcb file being generated.
Check the result by using pcbnew atreus.kicad_pcb
.
I genuinely enjoyed the process modifying this codebase and learning Racket. After a while, you won't really mind the braces. That's it, basically. I have nothing to say about Racket and KiCad's pcb file format other than, "it's really nice experience to learn about it."
For my usage, I think I will use choc switches and chase the low-profile as the target. Though that means I won't be using any case.
And I will update this post when I print the pcb and have them on my hands.