ATREUS


firmware tutorial, part 2

In this series, we step through the process of incrementally building up a keyboard firmware using Microscheme, a subset of the Scheme programming language targeting microcontrollers in the AVR family, such as those used by the Arduino, the A-Star Micro found in the Atreus keyboard kit, and the controller on the Keyboardio Atreus.

At the conclusion of our previous installment, we were left with a keyboard firmware that could send keycodes, but only for a single key at a time on a single row of the keyboard.


step three: matrix scan

This step improves on the last by doing a full matrix scan, so every key in every row will be registered. Recall that the limited number of pins means we can't just wire each key into its own pin. Instead each key switch has one leg connected to each other key in the column, and all of these are wired into an input pin. The other leg is wired together with those of the same row into an output pin. Pressing a key creates an electrical connection between the output pin of that switch's row and the input pin for that switch's column. This arrangement is referred to as the keyboard's matrix.

Matrix diagram

All the controller can read is the state of a column, from the pins along the bottom in the diagram. If all the rows (the pins on the left) were active at once, we would have no way to distinguish between different keys in a column, say Q, A, Z, and Esc. Luckily we can turn the rows on and off individually, so by quickly cycling through the rows and reading all the columns in each cycle, the state of each individual switch in the matrix can be determined. Even though we're stepping through one row at a time, the controller is able to cycle through them quickly enough to give the illusion that they're all being scanned at once[1]. Let's look at some code that does this:

(define (loop)
  (call-c-func "usb_send" 0 (scan-row 0 0) 0 0 0 0 0)
  (loop))

(define (scan-row row pressed)
  (if (< row row-count)
      (begin
          (for-each-vector high row-pins)
          (low (vector-ref row-pins row)) ; activate the row
          (let ((pressed (scan-column row 0 pressed)))
            (scan-row (+ row 1) pressed)))
      pressed))

(define (scan-column row col pressed)
  (if (< col col-count)
      (let ((pressed (if (low? (vector-ref column-pins col))
                         (vector-ref layout (+ col (* row col-count)))
                         pressed)))
        (scan-column row (+ col 1) pressed))
      pressed))

Since we need a bit more state on the stack, we're not using fold anymore and opting instead for a more scheme-traditional recursive approach. The scan-row function takes a row number and the pressed key, or zero for no key. If it's done all the rows it returns the pressed keycode, but otherwise it activates the current row, then it begins scanning each column in the row with scan-column. Then it recurses to the next row.

The scan-column function follows the same recursive pattern, but for each column it uses low? to see if the key switch at the given row/column has been pressed, (remember from the last article, because of the way pullup resistors are used pressed keys read as low and unpressed as high) and if so it looks it up it the layout vector. This vector contains each row after another, so the offset is calculated by adding the current column number to the row number times the column count.

step four: six-key rollover

At this point we are still only capable of sending a single non-modifier keycode at a time. To remedy this we will add support for a thing called 6KRO, or 6-key rollover[2]. This means that up to six non-modifier keys can be pressed at a time and each will register with the OS. We'll also add support for modifiers (ctrl, alt, shift, etc) in this step.

Since the code is getting a bit longer, we'll now include just the snippets with relevant changes rather than inlining the whole thing, but the whole code for this section can be viewed independently.

The scan-row and scan-column functions remain unchanged except that the need to represent up to six keycodes at once forces us to move from pressed being a single number to being a list. To do this we've moved the keycode lookup from scan-column to its own function in record:

(define (scan-column row col pressed)
  (if (< col col-count)
      (let ((pressed (if (low? (vector-ref column-pins col))
                         (record row col pressed)
                         pressed)))
        (scan-column row (+ col 1) pressed))
      pressed))

(define (record row col pressed)
  (let ((keycode (vector-ref layout (+ col (* row col-count)))))
    (if (modifier? keycode)
        (cons (bit-or keycode (car pressed)) (cdr pressed))
        (cons (car pressed) (cons keycode (cdr pressed))))))

The first element of the list is an integer representing the modifier state. When a modifier is detected pressed, it's ORed together with that modifier's keycode. For example, ctrl has a keycode of 1 and alt has a keycode of 4. The modifier value starts at zero, but after ctrl is scanned it becomes (bit-or 0 1), which is 1. Then when the scan gets to alt, it becomes (bit-or 1 4), which is 5.

Subsequent list elements represent regular keys and are consed onto the pressed list as they are scanned.

(define (loop)
  (free! (let ((pressed (fold scan-row 0 rows)))
           (call-c-func "usb_send" 0 pressed 0 0 0 0 0)))
  (loop))

(define (usb-send modifiers key1 key2 key3 key4 key5 key6)
  (call-c-func "usb_send" modifiers key1 key2 key3 key4 key5 key6))

(define (pad pressed) ; usb_send needs six arguments
  (if (< (length pressed) 7)
      (pad (cons (car pressed) (cons 0 (cdr pressed))))
      pressed))

This list gets passed back to the loop function once the whole matrix has been scanned. We use the apply function to use that list as the arguments to usb-send, which uses the FFI to call the underlying C function. (We cannot use apply on the FFI call directly because it is a special form implemented in the compiler and not a proper function.) But at this point, we have sent the host OS both the state of any active modifiers plus all the keycodes for the active keys, within the USB spec's 6-key limit.


that's all for now...

We've reached the end of this installment, and the firmware is getting pretty close to usable. If it were a conventional keyboard we could almost be done here, but the Atreus is such a small board that it requires layers for it to be practical. Without layers it can only type letters and a handful of punctuation characters; digits and most of the punctuation characters are absent. In the next installment we'll see how to expose them on additional layers. We'll also need to write some debouncing code to deal with a particular quirk of electrical contacts.

There's no full write-up for the completed firmware yet, but the code has all been written and is very thoroughly commented, so if you are curious, dig in!


[1] This same matrix technique is reversed in LED matrix signs where you have a limited number of output pins rather than a limited number of inputs.

[2] Note that the Atreus is capable in hardware of unlimited N-key rollover; it's just that the USB standard only allows each frame to contain a record of no more than 6 non-modifier keys. With some fancier tricks (which are used by the normal firmware on the Atreus) you can work around that problem, but that is beyond the scope of this article.

2015-03-07