diff --git a/Makefile b/Makefile index 09608e0..dcc905c 100644 --- a/Makefile +++ b/Makefile @@ -32,4 +32,12 @@ usb_keyboard.s: usb_keyboard.h usb_keyboard.c avr-gcc -std=gnu99 -S -D F_CPU=$(F_CPU)UL -mmcu=$(MCU) -c \ -o usb_keyboard.s usb_keyboard.c -.PHONY: build upload test clean count +udev: /etc/udev/rules.d/a-star.rules + +/etc/udev/rules.d/a-star.rules: + echo "SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"1ffb\", \ + ATTRS{idProduct}==\"0101\", ENV{ID_MM_DEVICE_IGNORE}=\"1\"" > $@ + echo "SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"1ffb\", \ + ATTRS{idProduct}==\"2300\", ENV{ID_MM_DEVICE_IGNORE}=\"1\"" >> $@ + +.PHONY: build upload test clean count udev diff --git a/README.md b/README.md index feff3c0..2614d3a 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,12 @@ 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. +press with a pin to the bottom of the board. On linux-based systems +you can monitor for the bootloader activation using `sudo dmesg --follow`. + +Some linux-based systems will need a udev rule to grant permissions to +the USB device for uploading firmware. If you get permission denied on +`/dev/ttyACM0` or whatever it is, try running `sudo make udev`. ## Known bugs @@ -72,7 +77,8 @@ into Racket and simulates the GPIO functions with a test harness: Copyright © 2014-2020 Phil Hagelberg and contributors -Released under the [GNU GPL version 3](https://www.gnu.org/licenses/gpl.html). +Released under the [GNU GPL version 3](https://www.gnu.org/licenses/gpl.html) +or any later version. Uses [PJRC USB Keyboard library](http://www.pjrc.com/teensy/usb_keyboard.html) which is Copyright © 2009 PJRC.COM, LLC and released under the MIT/X11 license. diff --git a/keycodes.scm b/keycodes.scm index 164d923..835d541 100644 --- a/keycodes.scm +++ b/keycodes.scm @@ -97,7 +97,7 @@ (define (combo modifier keycode) (list (car modifier) keycode)) (define (uncombo keycode) (and (= 2 (length keycode)) (car (cdr keycode)))) -;; we're treating these a little differently; they are not literal USB values +;; We're treating these a little differently; they are not literal USB values. (define mod-ctrl (modify 1)) (define mod-shift (modify 2)) (define mod-alt (modify 3)) diff --git a/layout.scm b/layout.scm index 1129c5c..ec227f5 100644 --- a/layout.scm +++ b/layout.scm @@ -21,7 +21,7 @@ ;; (sft key-5) key-backtick) -;;;; layers +;;;; 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 @@ -53,6 +53,7 @@ key-n key-m key-comma key-period key-slash key-esc key-tab mod-super mod-shift key-backspace mod-alt + ;; fn takes us to fn-layer below while it is held down key-space fn key-quote key-left-bracket key-enter)) (define fn-layer @@ -65,6 +66,7 @@ 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 takes us to l2-layer below; doesn't need to be held (set-layer 2) key-insert mod-super mod-shift key-backspace mod-alt key-space fn key-e key-0 key-right-bracket)) @@ -75,10 +77,13 @@ 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 + ;; the B key enters the bootloader + 0 key-vol-up 0 0 reset mod-ctrl + ;; the N key switches to hardware dvorak mode (set-layer 4) key-f1 key-f2 key-f3 key-f12 0 key-vol-down mod-super mod-shift key-backspace mod-alt + ;; tapping the fn key brings us back to the base layer key-space (set-layer 0) key-printscreen key-scroll-lock key-pause)) (define hard-dvorak-layer @@ -109,3 +114,4 @@ (set! layers (vector base-layer fn-layer l2-layer hard-dvorak-layer hard-dvorak-fn-layer)) +(set! current-layer (vector-ref layers 0)) diff --git a/menelaus.scm b/menelaus.scm index a05b59a..c0bf8b2 100644 --- a/menelaus.scm +++ b/menelaus.scm @@ -1,4 +1,18 @@ -;;; menelaus.scm - a USB keyboard firmware for the Atreus. +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 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 @@ -7,9 +21,11 @@ ;; 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. +;; 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. + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (include "keycodes.scm") (include "layout.scm") @@ -33,7 +49,7 @@ ;; isn't yet part of Microscheme: ;; https://github.com/ryansuchocki/microscheme/issues/32 -;;;;;;;;;;;;;;;;;;; Utility +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Utility functions (define (find-aux v x n max) (let ((y (vector-ref v n))) @@ -61,14 +77,19 @@ ;; 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 +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Matrix Scan -;; A scan is defined as a list containing the key positions which are currently +;; 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)))) @@ -106,7 +127,10 @@ (scan-matrix (scan-column scan (car rows-left) columns) (cdr rows-left))))) -;;;;;;;;;;;;;;;;;;; Debouncing +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 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. @@ -114,6 +138,8 @@ ;; 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) @@ -127,7 +153,11 @@ (define (debounce-matrix) (debounce-matrix-aux (list) debounce-passes)) -;;;;;;;;;;;;;;;;;;; Press and release tracking +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 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. @@ -145,6 +175,8 @@ ;; 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)) @@ -182,11 +214,45 @@ (for-each remove-last-down (cdr p/r)) p/r)) -;;;;;;;;;;;;;;;;;;; Generating Keycodes +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Layout lookup -;; 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. +;; 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 (vector 0 0 0 0)) @@ -254,18 +320,14 @@ (release-modifier modifier-slot key 0) #f))))) -;;;;;;;;;;;;;;;;;;; SHOWTIME +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Send USB -;; Prepare the GPIO pins and initialize the USB connection. -(define (init) - (set! current-layer (vector-ref layers 0)) - (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 +;; This phase is responsible for the initialization, the main loop, and +;; actually sending the USB frame to the host once it has been calculated. - (call-c-func "usb_init") - (pause 200)) +;; 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) @@ -283,10 +345,24 @@ ;; 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))))) (loop)) -(init) +;; 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)