Add a ton of comments.
This commit is contained in:
parent
625fb3dc1b
commit
382d557dda
3 changed files with 230 additions and 121 deletions
39
README.md
39
README.md
|
@ -8,16 +8,20 @@ See [this article about how it works](https://atreus.technomancy.us/firmware).
|
|||
|
||||
## Features
|
||||
|
||||
* 6KRO (6 simultaneous keys, plus modifiers)
|
||||
* 6KRO (6 simultaneous keys, plus 4 modifiers)
|
||||
* Software debouncing
|
||||
* Multiple layers, momentary and sticky (limited only by memory)
|
||||
* Combo keys (a single keystroke can send a modifier and a non-modifier)
|
||||
* Bind arbitrary Scheme functions to a key
|
||||
* Bind arbitrary Microscheme functions to a key
|
||||
* ~300 lines of code
|
||||
|
||||
## Usage
|
||||
|
||||
This requires [avrdude](https://www.nongnu.org/avrdude/) for uploading
|
||||
Install [microscheme](https://github.com/ryansuchocki/microscheme/)
|
||||
from source; place `microscheme` executable on your `$PATH`. Version
|
||||
823c5d9 from February 2020 is known to work.
|
||||
|
||||
Requires [avrdude](https://www.nongnu.org/avrdude/) for uploading
|
||||
to the controller on the keyboard; install with your package manager
|
||||
of choice.
|
||||
|
||||
|
@ -27,14 +31,32 @@ bootloader of the microcontroller (on Mac OS X sometimes it is
|
|||
|
||||
$ make upload USB=/dev/ttyACM0
|
||||
|
||||
By default you get the "multidvorak" layout, but you can also build a
|
||||
qwerty layout:
|
||||
Once you run that, put the device in bootloader mode; sometimes this
|
||||
can be invoked by a key combo and sometimes a hard reset is
|
||||
necessary. On the A-star Micro used in the Atreus kits, this is done
|
||||
by shorting GND and RST twice in under a second, which causes the
|
||||
onboard LED to pulse. The Keyboardio Atreus has a reset button you can
|
||||
press with a pin to the bottom of the board.
|
||||
|
||||
## Known bugs
|
||||
|
||||
The reset function in the firmware has no effect; hard-reset must be
|
||||
used to flash a new firmware once this is uploaded.
|
||||
|
||||
## Layout
|
||||
|
||||
By default you get the "multidvorak" layout which is designed to send
|
||||
the right keycodes with the assumption that the OS is set to use
|
||||
Dvorak, but it also includes layers for "hard Dvorak". But you can
|
||||
also build a qwerty layout:
|
||||
|
||||
$ cp qwerty.scm layout.scm
|
||||
$ make upload USB=/dev/ttyACM0
|
||||
|
||||
Or edit `layout.scm` to your liking; you can see a list of available
|
||||
keycodes in `keycodes.scm`.
|
||||
keycodes in `keycodes.scm`. The default layout works for 42-key Atreus
|
||||
kits and the 44-key Keyboardio Atreus, but you will have to uncomment
|
||||
a few things for the full 44-key support.
|
||||
|
||||
## Development
|
||||
|
||||
|
@ -46,11 +68,6 @@ into Racket and simulates the GPIO functions with a test harness:
|
|||
racket test.rkt
|
||||
..........................
|
||||
|
||||
## Known bugs
|
||||
|
||||
The reset function has no effect; hard-reset (shorting the RST and GND
|
||||
pins with a wire) must be used to flash the firmware.
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2014-2020 Phil Hagelberg and contributors
|
||||
|
|
39
layout.scm
39
layout.scm
|
@ -1,8 +1,8 @@
|
|||
;;; this is the multidvorak layout
|
||||
;;; This is the multidvorak layout.
|
||||
|
||||
;; it will work for the 44-key Atreus 2 or the 42-key Atreus 1.
|
||||
;; It will work for the 44-key Atreus 2 or the 42-key Atreus 1.
|
||||
|
||||
;; we have to declare this up front and set it later because of circularity
|
||||
;; We have to declare this up front and set it later because of circularity.
|
||||
(define layers #f)
|
||||
(define current-layer #f)
|
||||
(define momentary-layer #f)
|
||||
|
@ -12,17 +12,36 @@
|
|||
(define (set-layer n)
|
||||
(lambda (_) (set! current-layer (vector-ref layers n))))
|
||||
|
||||
;; this will reset the board but fails to enter the bootloader for some reason
|
||||
;; This will reset the board but fails to enter the bootloader for some reason.
|
||||
(define (reset _) (call-c-func "reset"))
|
||||
|
||||
;; on the Atreus 1, we need to expose backtick on the fn layer, but on
|
||||
;; the Atreus 2 it has its own key, so we put percent there instead
|
||||
;; On the Atreus 1, we need to expose backtick on the fn layer, but on
|
||||
;; the Atreus 2 it has its own key, so we put percent there instead.
|
||||
(define backtick-or-percent
|
||||
;; (sft key-5)
|
||||
key-backtick)
|
||||
|
||||
;;;; layers
|
||||
|
||||
;; NB: the middle keys (ctrl and alt on the 42-key, also ~ and \ on the 44-key
|
||||
;; variant) are physically in two separate columns, but electrically they are
|
||||
;; both wired in to the same middle column.
|
||||
|
||||
;;; physical location:
|
||||
|
||||
;; ~ \
|
||||
;; ctrl alt
|
||||
|
||||
;;; electrical arrangement:
|
||||
|
||||
;; ~
|
||||
;; \
|
||||
;; ctrl
|
||||
;; alt
|
||||
|
||||
;; This is why it looks like the top two rows should have 10 columns and the
|
||||
;; bottom should have 12; in reality there are electrically 11 columns.
|
||||
|
||||
(define base-layer
|
||||
(vector key-q key-w key-e key-r key-t key-backtick
|
||||
key-y key-u key-i key-o key-p
|
||||
|
@ -37,8 +56,8 @@
|
|||
key-space fn key-quote key-left-bracket key-enter))
|
||||
|
||||
(define fn-layer
|
||||
(vector (sft key-1) (sft key-2) key-up (sft key-4) backtick-or-percent (sft key-6)
|
||||
key-page-up key-7 key-8 key-9 key-backspace
|
||||
(vector (sft key-1) (sft key-2) key-up (sft key-4) backtick-or-percent
|
||||
(sft key-6) key-page-up key-7 key-8 key-9 key-backspace
|
||||
|
||||
(sft key-9) key-left key-down key-right (sft key-0) (sft key-7)
|
||||
key-page-down key-4 key-5 key-6 key-backslash
|
||||
|
@ -82,8 +101,8 @@
|
|||
(sft key-3) key-left key-down key-right (sft key-4) 0
|
||||
key-page-down key-4 key-5 key-6 (sft key-equal)
|
||||
|
||||
key-left-bracket key-right-bracket (sft key-9) (sft key-0) (sft key-7) mod-ctrl
|
||||
key-backtick key-1 key-2 key-3 key-backslash
|
||||
key-left-bracket key-right-bracket (sft key-9) (sft key-0) (sft key-7)
|
||||
mod-ctrl key-backtick key-1 key-2 key-3 key-backslash
|
||||
|
||||
(set-layer 2) key-insert mod-super mod-shift key-backspace mod-alt
|
||||
key-space fn key-e key-0 key-right-bracket))
|
||||
|
|
201
menelaus.scm
201
menelaus.scm
|
@ -1,20 +1,39 @@
|
|||
;;; menelaus.scm - a USB keyboard firmware for the Atreus.
|
||||
|
||||
;; Note that there are a few unusual style choices made here because
|
||||
;; it is written in a shared subset of Microscheme and Racket so that it
|
||||
;; can be tested on a PC without uploading it to a device for every change.
|
||||
|
||||
;; For one example, we use `and' where `when' would be more idiomatic. We
|
||||
;; are also missing the `cond' form.
|
||||
|
||||
;; In general when you see an -aux function, it is an internal function which
|
||||
;; recursively steps thru a vector/list with the initial arguments calculated
|
||||
;; by its non-aux equivalent.
|
||||
|
||||
(include "keycodes.scm")
|
||||
|
||||
(define rows (list 0 1 2 3))
|
||||
(define row-pins (vector 3 2 1 0))
|
||||
(define columns (list 0 1 2 3 4 5 6 7 8 9 10))
|
||||
(define column-pins (vector 6 5 9 8 7 4 10 19 18 12 11))
|
||||
|
||||
(define max-keys 10) ; single USB frame can only send 6 keycodes plus modifiers
|
||||
|
||||
;; pcbdown flip, comment out for normal
|
||||
;; (begin (set! mod-alt (modify 1))
|
||||
;; (set! mod-ctrl (modify 3))
|
||||
;; (set! column-pins (vector 11 12 18 19 10 4 7 8 9 5 6)))
|
||||
|
||||
(include "layout.scm")
|
||||
|
||||
;;;;;;;;;;;;;;;;;;; utils
|
||||
;; What are the rows and columns we care about?
|
||||
(define rows (list 0 1 2 3))
|
||||
(define columns (list 0 1 2 3 4 5 6 7 8 9 10))
|
||||
|
||||
;; Which GPIO pins are responsible for each row or column?
|
||||
(define row-pins (vector 3 2 1 0))
|
||||
(define column-pins (vector 6 5 9 8 7 4 10 19 18 12 11))
|
||||
|
||||
;; If you have a kit where the PCB is installed upside-down, uncomment this:
|
||||
;; (set! column-pins (vector 11 12 18 19 10 4 7 8 9 5 6))
|
||||
;; ;; Upside-down PCB makes the columns backwards but also trades ctrl and alt;
|
||||
;; ;; this hack only works for layouts where ctrl and alt are in standard place.
|
||||
;; (set! mod-alt (modify 1))
|
||||
;; (set! mod-ctrl (modify 3))
|
||||
|
||||
;; The above should be handled by a compile-time environment variable but that
|
||||
;; isn't yet part of Microscheme:
|
||||
;; https://github.com/ryansuchocki/microscheme/issues/32
|
||||
|
||||
;;;;;;;;;;;;;;;;;;; Utility
|
||||
|
||||
(define (find-aux v x n max)
|
||||
(let ((y (vector-ref v n)))
|
||||
|
@ -23,31 +42,62 @@
|
|||
(and (< n max)
|
||||
(find-aux v x (+ n 1) max)))))
|
||||
|
||||
;; Return the index for x in vector v.
|
||||
(define (find v x)
|
||||
(find-aux v x 0 (- (vector-length v) 1)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;; matrix
|
||||
(define (remove-aux v lst checked all?)
|
||||
(if (null? lst)
|
||||
(reverse checked)
|
||||
(if (equal? v (car lst))
|
||||
(if all?
|
||||
(remove-aux v (cdr lst) checked all?)
|
||||
(reverse (append (cdr lst) checked)))
|
||||
(remove-aux v (cdr lst) (cons (car lst) checked) all?))))
|
||||
|
||||
;; Return a copy of lst with the first element equal to v removed.
|
||||
(define (remove v lst) (remove-aux v lst (list) #f))
|
||||
|
||||
;; Return a copy of lst with all elements equal to v removed.
|
||||
(define (remove-all v lst) (remove-aux v lst (list) #t))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;; The Matrix
|
||||
|
||||
;; A scan is defined as a list containing the key positions which are currently
|
||||
;; pressed for a given pass thru the key matrix. We specifically do not attempt
|
||||
;; to look up what the keys are mapped to yet; we have to do that later on after
|
||||
;; identifying presses and releases, otherwise we run into layer-switching bugs.
|
||||
;; Each element in the list is an integer representation of the key in question.
|
||||
|
||||
;; Which key in a layout vector is represented by the given row and column?
|
||||
(define (offset-for row col)
|
||||
(+ col (* row (length columns))))
|
||||
|
||||
;; Update scan to include the key for the given row/col if it's pressed.
|
||||
(define (scan-key scan row col)
|
||||
(if (and (< (length scan) max-keys)
|
||||
(if (and (< (length scan) 10) ; one USB frame can only send 6 keycodes + mods
|
||||
;; pullup resistors mean a closed circuit is low rather than high
|
||||
(low? (vector-ref column-pins col)))
|
||||
(cons (offset-for row col) scan)
|
||||
scan))
|
||||
|
||||
;; Step thru every column for a row and ensure it gets scanned.
|
||||
(define (scan-column scan row columns-left)
|
||||
(if (null? columns-left)
|
||||
scan
|
||||
(scan-column (scan-key scan row (car columns-left))
|
||||
row (cdr columns-left))))
|
||||
|
||||
;; Scanning a single column tells us that the key for that column in the active
|
||||
;; row has been pressed, because the key creates a circuit between the active
|
||||
;; row's output pin and that column's input pin, causing the output pin's low
|
||||
;; voltage to overcome the input pin's pullup resistor.
|
||||
(define (activate-row row)
|
||||
(for-each-vector high row-pins)
|
||||
(low (vector-ref row-pins row)))
|
||||
|
||||
;; For each row, ensure that only its pin is activated, then check every column
|
||||
;; in that row, consing onto the scan list.
|
||||
(define (scan-matrix scan rows-left)
|
||||
(if (null? rows-left)
|
||||
scan
|
||||
|
@ -56,7 +106,13 @@
|
|||
(scan-matrix (scan-column scan (car rows-left) columns)
|
||||
(cdr rows-left)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;; debouncing
|
||||
;;;;;;;;;;;;;;;;;;; Debouncing
|
||||
|
||||
;; Electrical contacts do not switch cleanly from high to low voltage; there is
|
||||
;; a short period of "bounce" while the signal settles into its new position.
|
||||
;; In order to counteract this effect, we scan the whole matrix several times,
|
||||
;; only considering the data we get trustworthy if we get the same value three
|
||||
;; times in a row.
|
||||
|
||||
(define debounce-passes 3)
|
||||
|
||||
|
@ -71,19 +127,34 @@
|
|||
(define (debounce-matrix)
|
||||
(debounce-matrix-aux (list) debounce-passes))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;; press and release tracking
|
||||
;;;;;;;;;;;;;;;;;;; Press and release tracking
|
||||
|
||||
;; If we didn't have layers, we'd be done now. But since we have layers, we
|
||||
;; can't assume a 1:1 mapping between keys pressed and keycodes we should send.
|
||||
;; If you press key 0 on layer 0 where it's bound to Q and then switch to layer
|
||||
;; one where it's bound to ! then the layer switch shouldn't cause ! to be sent;
|
||||
;; you should have to release and press key 0 again to trigger that.
|
||||
|
||||
;; Fun fact: my original firmware written in C worked around this by just adding
|
||||
;; a delay to the activation of the layer, which was cringeworthy but kinda
|
||||
;; sorta worked; better than you would expect anyway:
|
||||
|
||||
;; https://github.com/technomancy/atreus-firmware/issues/12
|
||||
;; https://github.com/technomancy/atreus-firmware/issues/49
|
||||
|
||||
;; Because of this, it's necessary to track press and release on the level of
|
||||
;; physical keys and only map it to keycodes when a new press is detected.
|
||||
|
||||
;; Which physical keys were pressed during the last scan?
|
||||
(define last-keys-down (vector #f #f #f #f #f #f #f #f #f #f))
|
||||
|
||||
;; Find an empty slot in last-keys-down to save off the given key in.
|
||||
(define (add-last-down-aux key n)
|
||||
(if (not (vector-ref last-keys-down n))
|
||||
(vector-set! last-keys-down n key)
|
||||
(if (< n 9)
|
||||
(add-last-down-aux key (+ n 1))
|
||||
;; microscheme does not have a `when' form, so for compatibility with
|
||||
;; racket, we must always include an else branch.
|
||||
#f)))
|
||||
(and (< n 9) (add-last-down-aux key (+ n 1)))))
|
||||
|
||||
;; Remove the given key from the vector of presses from last pass.
|
||||
(define (remove-last-down-aux key n)
|
||||
(if (equal? key (vector-ref last-keys-down n))
|
||||
(vector-set! last-keys-down n #f)
|
||||
|
@ -92,19 +163,6 @@
|
|||
(define (add-last-down key) (add-last-down-aux key 0))
|
||||
(define (remove-last-down key) (remove-last-down-aux key 0))
|
||||
|
||||
(define (remove-aux v lst checked all?)
|
||||
;; also missing the cond form
|
||||
(if (null? lst)
|
||||
(reverse checked)
|
||||
(if (equal? v (car lst))
|
||||
(if all?
|
||||
(remove-aux v (cdr lst) checked all?)
|
||||
(reverse (append (cdr lst) checked)))
|
||||
(remove-aux v (cdr lst) (cons (car lst) checked) all?))))
|
||||
|
||||
(define (remove v lst) (remove-aux v lst (list) #f))
|
||||
(define (remove-all v lst) (remove-aux v lst (list) #t))
|
||||
|
||||
(define (press/release-aux press release keys-scanned)
|
||||
(if (null? keys-scanned)
|
||||
(cons press release)
|
||||
|
@ -113,6 +171,8 @@
|
|||
(press/release-aux press (remove key release) (cdr keys-scanned))
|
||||
(press/release-aux (cons key press) release (cdr keys-scanned))))))
|
||||
|
||||
;; Takes a list of keys from a scan and returns a cons where the car is a list
|
||||
;; of keys just pressed and the cdr is a list of keys just released.
|
||||
(define (press/release-for keys-scanned)
|
||||
(let ((p/r (press/release-aux (list)
|
||||
(remove-all #f (vector->list last-keys-down))
|
||||
|
@ -122,39 +182,42 @@
|
|||
(for-each remove-last-down (cdr p/r))
|
||||
p/r))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;; using press/release data to generate keycodes
|
||||
;;;;;;;;;;;;;;;;;;; Generating Keycodes
|
||||
|
||||
;; Given keys that have been pressed, turn those into keycodes for our USB
|
||||
;; frame. Given keys that are released, update the press/release tracking
|
||||
;; data to reflect them.
|
||||
|
||||
;; Vectors to store keycodes for the USB frame we are preparing to send.
|
||||
(define modifiers (vector 0 0 0 0))
|
||||
(define keycodes-down (vector 0 0 0 0 0 0))
|
||||
|
||||
;; For each element of the keycodes-down or modifiers vector, which physical
|
||||
;; key caused it to be pressed?
|
||||
(define keys-for-modifiers (vector #f #f #f #f))
|
||||
(define keys-for-frame (vector #f #f #f #f #f #f))
|
||||
|
||||
;; Given a physical key index, what keycode does it map to in the layout?
|
||||
(define (lookup key-pos)
|
||||
(let ((layout (or momentary-layer current-layer)))
|
||||
(vector-ref layout key-pos)))
|
||||
|
||||
(define modifiers (vector 0 0 0 0))
|
||||
(define keycodes-down (vector 0 0 0 0 0 0))
|
||||
|
||||
;; which keys caused the keycodes/modifiers to be down?
|
||||
(define keys-for-modifiers (vector #f #f #f #f))
|
||||
(define keys-for-frame (vector #f #f #f #f #f #f))
|
||||
|
||||
;; Record that a given key resulted in a specific modifier press.
|
||||
(define (press-modifier keycode key)
|
||||
(vector-set! modifiers (- keycode 1) 1)
|
||||
(vector-set! keys-for-modifiers (- keycode 1) key))
|
||||
|
||||
(define (release-modifier keycode key n)
|
||||
(if (= (or (vector-ref keys-for-modifiers n) (- 0 1)) key)
|
||||
(begin
|
||||
(vector-set! modifiers n 0)
|
||||
(vector-set! keys-for-modifiers n #f))
|
||||
(if (< n 3)
|
||||
(release-modifier keycode key (+ n 1))
|
||||
#f)))
|
||||
|
||||
;; Record that a given key resulted in a specific non-modifier press.
|
||||
(define (press-normal-key keycode key)
|
||||
(let ((slot (find keycodes-down 0)))
|
||||
(and slot (vector-set! keycodes-down slot keycode))
|
||||
(and slot (vector-set! keys-for-frame slot key))))
|
||||
|
||||
;; Record a key press in the modifiers/keycodes-down vectors for the layout.
|
||||
(define (press-key key)
|
||||
(let ((keycode (lookup key)))
|
||||
;; Sometimes "keycodes" are procedures; in that case we call them with
|
||||
;; true when the key is pressed and false when it's released.
|
||||
(if (procedure? keycode)
|
||||
(keycode #t)
|
||||
(if (modifier? keycode)
|
||||
|
@ -164,6 +227,15 @@
|
|||
#f))
|
||||
(press-normal-key keycode key)))))
|
||||
|
||||
;; Record that a given key being released resulted in a modifier release.
|
||||
(define (release-modifier keycode key n)
|
||||
(if (= (or (vector-ref keys-for-modifiers n) (- 0 1)) key)
|
||||
(begin
|
||||
(vector-set! modifiers n 0)
|
||||
(vector-set! keys-for-modifiers n #f))
|
||||
(and (< n 3) (release-modifier keycode key (+ n 1)))))
|
||||
|
||||
;; Record a key release, clearing it out of the press tracking data.
|
||||
(define (release-key key)
|
||||
;; lookup here looks it up in the current layer, even if it was pressed in
|
||||
;; the momentary layer. these need to be consistent across layers or tracked
|
||||
|
@ -182,15 +254,9 @@
|
|||
(release-modifier modifier-slot key 0)
|
||||
#f)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;; showtime
|
||||
|
||||
(define (set-usb-frame press/release)
|
||||
(let ((press (car press/release))
|
||||
(release (cdr press/release)))
|
||||
(for-each press-key press)
|
||||
(for-each release-key release)
|
||||
keycodes-down))
|
||||
;;;;;;;;;;;;;;;;;;; SHOWTIME
|
||||
|
||||
;; Prepare the GPIO pins and initialize the USB connection.
|
||||
(define (init)
|
||||
(set! current-layer (vector-ref layers 0))
|
||||
(for-each-vector output row-pins)
|
||||
|
@ -201,15 +267,22 @@
|
|||
(call-c-func "usb_init")
|
||||
(pause 200))
|
||||
|
||||
;; Take press/release data and set USB keycodes and modifiers.
|
||||
(define (set-usb-frame press/release)
|
||||
(let ((press (car press/release))
|
||||
(release (cdr press/release)))
|
||||
(for-each press-key press)
|
||||
(for-each release-key release)))
|
||||
|
||||
;; Actually send the USB frame.
|
||||
(define (usb-send m k0 k1 k2 k3 k4 k5)
|
||||
;; call-c-func is a special form and cannot be applied
|
||||
(let ((mods (+ (vector-ref m 0) (* (vector-ref m 1) 2)))) ; + isn't variadic
|
||||
(let ((mods (+ mods (+ (* (vector-ref m 2) 4) (* (vector-ref m 3) 8)))))
|
||||
;; call-c-func is a special form and cannot be applied
|
||||
(call-c-func "usb_send" mods k0 k1 k2 k3 k4 k5))))
|
||||
|
||||
;; Scan the matrix, determine the appropriate keycodes, and send them.
|
||||
(define (loop)
|
||||
;; scanning the matrix tells us only which physical keys were pressed and
|
||||
;; how many; it doesn't tell us which keycodes to send yet.
|
||||
(free! (let ((keys-scanned (debounce-matrix)))
|
||||
(set-usb-frame (press/release-for keys-scanned))
|
||||
(apply usb-send (cons modifiers (vector->list keycodes-down)))))
|
||||
|
|
Loading…
Reference in a new issue