menelaus/menelaus.scm

371 lines
15 KiB
Scheme

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; menelaus.scm
;; a USB keyboard firmware for the Atreus.
;; Copyright © 2014-2020 Phil Hagelberg
;; Released under the GNU General Public License version 3 or any later
;; version.
;; The point of a keyboard firmware is to translate physical key presses on
;; switches into USB keycodes that get sent to the host. This process takes
;; several phases:
;; Matrix scan -> Debounce -> Track press/release -> Layout lookup -> Send USB
;; Each phase is described in more detail below.
;; 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.
;; 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. The -aux function is never called directly.
;; This file should be loaded by your main layout file; see qwerty.scm
;; for an example. It assumes that keycodes, layouts, and pinout have already
;; been defined. In particular, it needs these definitions:
;; * everything in keycodes.scm
;; * momentary-layer: either false (when not active) or a vector of keycodes
;; * current-layer: a vector of keycodes for when momentary-layer is inactive
;; The keycodes in these vectors can be numbers, lists, or procedures;
;; for details see the "Layout lookup" section below.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Utility functions
(define (find-aux v x n max)
(let ((y (vector-ref v n)))
(if (and y (= x y))
n
(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)))
(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))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Matrix Scan
;; This phase is responsible for determining the current state of the key
;; matrix; that is, which keys are reading as down for a given instant.
;; It returns a scan 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) 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
(begin
(activate-row (car rows-left))
(scan-matrix (scan-column scan (car rows-left) columns)
(cdr rows-left)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Debounce
;; This phase is responsible for filtering out spurious keypresses detected
;; by the matrix scan due to physical properties of switching logic.
;; 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)
(define (debounce-matrix-aux last-scan passes-left)
(if (< 0 passes-left)
(let ((this-scan (scan-matrix (list) rows)))
(if (equal? this-scan last-scan)
(debounce-matrix-aux this-scan (- passes-left 1))
(debounce-matrix-aux this-scan debounce-passes)))
last-scan))
(define (debounce-matrix)
(debounce-matrix-aux (list) debounce-passes))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Track press/release
;; This phase is responsible for comparing the current state of the keys to
;; the previous pass and interpreting which keys are newly pressed and which are
;; newly released.
;; 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)
(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)
(and (< n 9) (remove-last-down-aux key (+ n 1)))))
(define (add-last-down key) (add-last-down-aux key 0))
(define (remove-last-down key) (remove-last-down-aux key 0))
(define (press/release-aux press release keys-scanned)
(if (null? keys-scanned)
(cons press release)
(let ((key (car keys-scanned)))
(if (member key release)
(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))
keys-scanned)))
;; save off press/release into last-keys-down for next cycle
(for-each add-last-down (car p/r))
(for-each remove-last-down (cdr p/r))
p/r))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Layout lookup
;; This phase is responsible for taking keys that have been pressed and turning
;; those into keycodes for our USB frame, and also for taking the keys that
;; have been released and removing from the USB frame and press/release
;; tracking data.
;; In order to release keys consistently across layer changes, it's necessary
;; to store which physical keys are responsible for which keycodes being sent.
;; A key being released means that we stop sending the keycode that was bound
;; to that key when it was pressed, not the keycode bound to that key in the
;; current layer!
;; Data is stored in two vectors: modifiers and keycodes-down. Modifiers is of
;; length 4 because there are only 4 modifiers; (we ignore that left-shift
;; and right-shift can be distinguished). The keycodes-down vector is of length
;; 6 because that is defined in the USB standard as the number of non-modifier
;; keycodes that a single USB frame can represent.
;; If you have more than ten fingers, I'm sorry; try a different firmware.
;; The layout is defined in layout.scm as a vector of layer vectors. Each layer
;; vector is simply a vector of elements, arranged one row after another
;; corresponding to the physical keys.
;; Most of these elements are integers; these correspond to normal USB keycodes
;; as defined in keycodes.scm.
;; Some elements are lists; these indicate modifier keys. A list of length 1 is
;; simply a single modifier key, while a list of length 2 is a modifier plus a
;; non-modifier simultaneously. This is how we can define a ! key despite there
;; being no USB keycode for the ! character; it is defined as a combo of shift
;; and 1.
;; Finally some elements are Scheme procedures (aka functions). These procedures
;; get called with #t when they are first pressed and with #f when released.
;; These are mostly used for layer switching but could be used for anything.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Vectors to store keycodes for the USB frame we are preparing to send.
(define modifiers 0)
(define sticky-modifiers 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 #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)))
;; Record that a given key resulted in a specific modifier press.
(define (press-modifier keycode key)
(set! modifiers (| modifiers keycode))
(vector-set! keys-for-modifiers (call-c-func "mathlog2" keycode) key))
(define (press-sticky-modifier keycode)
(set! sticky-modifiers (| sticky-modifiers keycode)))
(define (release-sticky-modifier keycode)
(set! sticky-modifiers (^ sticky-modifiers keycode)))
;; 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))))
(define (press-key-aux keycode 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)
(if (uncombo keycode)
(if (= (uncombo keycode) sticky-modifier)
(press-sticky-modifier (unmodify keycode))
(begin
(press-modifier (unmodify keycode) key)
(press-key-aux (uncombo keycode) key)))
(press-modifier (unmodify keycode) key))
(press-normal-key keycode key))))
;; Record a key press in the modifiers/keycodes-down vectors for the layout.
(define (press-key key)
(let ((keycode (lookup key)))
(press-key-aux keycode key)))
;; Record that a given key being released resulted in a modifier release.
(define (release-modifier modifier key n)
(if (= (or (vector-ref keys-for-modifiers n) (- 0 1)) key)
(begin
(set! modifiers (^ modifiers modifier))
(vector-set! keys-for-modifiers n #f))
(and (< n 7) (release-modifier modifier 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
;; in a similar manner as keys-for-frame.
(let ((keycode (lookup key)))
(if (procedure? keycode)
(keycode #f)
(let ((slot (find keys-for-frame key))
(modifier-slot (find keys-for-modifiers key)))
(if slot
(begin
(vector-set! keycodes-down slot 0)
(vector-set! keys-for-frame slot #f))
#f)
(if modifier-slot
(release-modifier (call-c-func "mathpow" 2 modifier-slot) key 0)
#f)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Send USB
;; This phase is responsible for the initialization, the main loop, and
;; actually sending the USB frame to the host once it has been calculated.
;; Not much left to do here; just tying up loose ends bringing it all together.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 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
(call-c-func "usb_send" (| m sticky-modifiers) k0 k1 k2 k3 k4 k5))
;; Scan the matrix, determine the appropriate keycodes, and send them.
(define (loop)
;; Microscheme doesn't have garbage collection; it has you preallocate
;; everything you can, and then run your code that might allocate more memory
;; inside this `free!' macro. When you enter this macro, the heap pointer gets
;; saved, and when you leave, it gets set back to the point it was previously,
;; effectively garbage-collecting any allocations which happened inside the
;; macro in one fell swoop. Primitive, but effective.
(free! (let ((keys-scanned (debounce-matrix)))
(set-usb-frame (press/release-for keys-scanned))
(apply usb-send (cons modifiers (vector->list keycodes-down)))
(if (> (vector-ref keycodes-down 0) 0) (set! sticky-modifiers 0) #f)))
(loop))
;; Prepare the GPIO pins.
(for-each-vector output row-pins)
(for-each-vector high row-pins)
(for-each-vector input column-pins)
(for-each-vector high column-pins) ; activate pullup resistors
;; Initialize the USB connection and go!
(call-c-func "usb_init")
(pause 200)
(loop)