~technomancy/menelaus

382d557dda2bd1cc986cad3be851cba861840154 — Phil Hagelberg 8 months ago 625fb3d
Add a ton of comments.
3 files changed, 222 insertions(+), 113 deletions(-)

M README.md
M layout.scm
M menelaus.scm
M README.md => README.md +28 -11
@@ 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

M layout.scm => layout.scm +65 -46
@@ 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))

M menelaus.scm => menelaus.scm +129 -56
@@ 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")
(include "layout.scm")

;; What are the rows and columns we care about?
(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
;; 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))

;; 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)))
;; 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))

(include "layout.scm")
;; 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

;;;;;;;;;;;;;;;;;;; utils
;;;;;;;;;;;;;;;;;;; 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

(define (lookup key-pos)
  (let ((layout (or momentary-layer current-layer)))
    (vector-ref layout key-pos)))
;; 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))

;; which keys caused the keycodes/modifiers to be down?
;; 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)))

;; 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)))))