diff --git a/README.md b/README.md index 8a6b7d3..feff3c0 100644 --- a/README.md +++ b/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 diff --git a/layout.scm b/layout.scm index b958faa..1129c5c 100644 --- a/layout.scm +++ b/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,81 +12,100 @@ (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 + (vector key-q key-w key-e key-r key-t key-backtick + key-y key-u key-i key-o key-p - key-a key-s key-d key-f key-g key-backslash - key-h key-j key-k key-l key-semicolon + key-a key-s key-d key-f key-g key-backslash + key-h key-j key-k key-l key-semicolon - key-z key-x key-c key-v key-b mod-ctrl - key-n key-m key-comma key-period key-slash + key-z key-x key-c key-v key-b mod-ctrl + key-n key-m key-comma key-period key-slash - key-esc key-tab mod-super mod-shift key-backspace mod-alt - key-space fn key-quote key-left-bracket key-enter)) + key-esc key-tab mod-super mod-shift key-backspace mod-alt + 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 + (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 - key-dash key-equal (sft key-3) (sft key-dash) (sft key-equal) mod-ctrl - (sft key-8) key-1 key-2 key-3 (sft key-right-bracket) + key-dash key-equal (sft key-3) (sft key-dash) (sft key-equal) mod-ctrl + (sft key-8) key-1 key-2 key-3 (sft key-right-bracket) - (set-layer 2) key-insert mod-super mod-shift key-backspace mod-alt - key-space fn key-e key-0 key-right-bracket)) + (set-layer 2) key-insert mod-super mod-shift key-backspace mod-alt + key-space fn key-e key-0 key-right-bracket)) (define l2-layer - (vector key-insert key-home key-up key-end key-page-up 0 - key-up key-f7 key-f8 key-f9 key-f10 + (vector key-insert key-home key-up key-end key-page-up 0 + key-up key-f7 key-f8 key-f9 key-f10 - key-delete key-left key-down key-right key-page-down 0 - key-down key-f4 key-f5 key-f6 key-f11 + key-delete key-left key-down key-right key-page-down 0 + key-down key-f4 key-f5 key-f6 key-f11 - (set-layer 0) key-vol-up 0 0 reset mod-ctrl - (set-layer 4) key-f1 key-f2 key-f3 key-f12 + (set-layer 0) key-vol-up 0 0 reset mod-ctrl + (set-layer 4) key-f1 key-f2 key-f3 key-f12 - 0 key-vol-down mod-super mod-shift key-backspace mod-alt - key-space (set-layer 0) key-printscreen key-scroll-lock key-pause)) + 0 key-vol-down mod-super mod-shift key-backspace mod-alt + key-space (set-layer 0) key-printscreen key-scroll-lock key-pause)) (define hard-dvorak-layer - (vector key-quote key-comma key-period key-p key-y key-backslash - key-f key-g key-c key-r key-l + (vector key-quote key-comma key-period key-p key-y key-backslash + key-f key-g key-c key-r key-l - key-a key-o key-e key-u key-i key-backtick - key-d key-h key-t key-n key-s + key-a key-o key-e key-u key-i key-backtick + key-d key-h key-t key-n key-s - key-semicolon key-q key-j key-k key-x mod-ctrl - key-b key-m key-w key-v key-z + key-semicolon key-q key-j key-k key-x mod-ctrl + key-b key-m key-w key-v key-z - key-esc key-tab mod-super mod-shift key-backspace mod-alt - key-space fn key-quote key-left-bracket key-enter)) + key-esc key-tab mod-super mod-shift key-backspace mod-alt + key-space fn key-quote key-left-bracket key-enter)) (define hard-dvorak-fn-layer - (vector (sft key-1) (sft key-2) key-up (sft key-4) (sft key-5) (sft key-6) - key-page-up key-7 key-8 key-9 (sft key-backspace) + (vector (sft key-1) (sft key-2) key-up (sft key-4) (sft key-5) (sft key-6) + key-page-up key-7 key-8 key-9 (sft key-backspace) - (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) + (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)) + (set-layer 2) key-insert mod-super mod-shift key-backspace mod-alt + key-space fn key-e key-0 key-right-bracket)) (set! layers (vector base-layer fn-layer l2-layer hard-dvorak-layer hard-dvorak-fn-layer)) diff --git a/menelaus.scm b/menelaus.scm index d48e991..a05b59a 100644 --- a/menelaus.scm +++ b/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)))))