Kaleidoscope Device API internals

This document is aimed at people interested in working on adding new devices - or improving support for existing ones - to Kaleidoscope. The APIs detailed here are a little bit more complex than most of the APIs our plugins provide. Nevertheless, we hope they’re still reasonably easy to use, and this document is an attempt to explain some of the more intricate parts of it.

Overview

The core idea of the APIs is that to build up a device, we compose various components together, by describing their properties, and using fairly generic, templated helper classes with the properties as template parameters.

This way, we can assemble together a device with a given MCU, which uses a particular Bootloader, some kind of Storage, perhaps some LEDs, and it will more than likely have a key scanner component too.

The base and helper classes provide a lot of the functionality themselves, so for a device built up from components already supported by Kaleidoscope, the amount of custom code one has to write will be minimal.

Component details

Device

A Device is the topmost level component, it is the interface the rest of Kaleidoscope will work with. The kaleidoscope::device::Base class is the ancestor of all devices, everything derives from this. Devices that use an ATMega32U4 MCU we also have the kaleidoscope::device::ATMega32U4Keyboard class, which sets up some of the components that is common to all ATMega32U4-based devices (such as the MCU and the Storage).

As hinted at above, a device - or rather, it’s Props - describe the components used for the device, such as the MCU, the Bootloader, the Storage driver, LEDs, and the key scanner. If any of that is unneeded, there’s no need to specify them in Props - the defaults are all no-ops.

All devices must also come with a Props struct, deriving from kaleidoscope::device::BaseProps.

As an example, the most basic device we can have, that does nothing, would look like this:

class ExampleDevice : public kaleidoscope::device::Base<kaleidoscope::device::BaseProps> {};

That’s not very useful, though. More often than not, we want to override at least some of the properties. In some cases, even override some of the pre-defined methods of the device. See the base class for an up-to-date list of methods and interfaces it provides. The most often changed methods are likely to be setup() and the constructor, and enableHardwareTestMode() if the device implements a hardware test mode. The rest are wrappers around the various components described by the Props.

In other words, the majority of customisation is in the Props, and in what components the device ends up using.

MCU

The heart of any device will be the main controller unit, or MCU for short. The kaleidoscope::driver::mcu::Base class is the ancestor of our MCU drivers, including mcu::ATMega32U4.

The core firmware will use the detachFromHost() and attachToHost() methods of the MCU driver, along with setup(), but the driver - like any other driver - is free to have other methods, to be used by individual devices.

For example, the ATMega32U4 driver implements a disableJTAG() and a disableClockDivision() method, which some of our devices use in their constructors.

Unlike some other components, the MCU component has no properties.

Bootloader

Another important component of a device is a bootloader. The bootloader is the thing that allows us to re-program the keyboard without additional hardware (aptly called a programmer). As such, the base class has a single method, rebootBootloader(), which our bootloader components implement.

Kaleidoscope currently supports Catalina, HalfKay, and FLIP bootloaders. Please consult them for more information. In many cases, setting up the bootloader in the device props is all one needs to do.

Like the MCU component, the bootloader does not use Props, either.

Storage

Not nearly as essential for a device is the Storage component. Storage is for persistent storage of configuration data, such as key maps, colormaps, feature toggles, and so on. It’s not a required component, but a recommended one nevertheless. This storage component is what allows apps like Chrysalis to configure some aspects of the keyboard without having to flash new firmware.

The Storage API resembles the Arduino EEPROM API very closely. In fact, our AVREEPROM class is but a thin wrapper around that!

The Storage component does use Props, one that describes the length - or size - of it. We provide an ATMega32U4EEPROMProps helper, which is preconfigured for the 1k EEPROM size of the ATMega32U4.

Putting it all together

To put things into perspective, and show a simple example, we’ll build an imaginary mini keypad: ATMega32U4 with Caterina as bootloader, no LEDs, and four keys only.

ImaginaryKeypad.h

#pragma once

#ifdef ARDUINO_AVR_IMAGINARY_KEYPAD

#include <Arduino.h>
#include "kaleidoscope/driver/keyscanner/AVR.h"
#include "kaleidoscope/driver/bootloader/avr/Caterina.h"
#include "kaleidoscope/device/ATMega32U4Keyboard.h"

namespace kaleidoscope {
namespace device {
namespace imaginary {

struct KeypadProps : kaleidoscope::device::ATmega32U4KeyboardProps {
  struct KeyScannerProps : public kaleidoscope::driver::keyscanner::ATmegaProps {
    static constexpr uint8_t matrix_rows = 2;
    static constexpr uint8_t matrix_columns = 2;
    typedef MatrixAddr<matrix_rows, matrix_columns> KeyAddr;
    static constexpr uint8_t matrix_row_pins[matrix_rows] = {PIN_D0, PIN_D1};
    static constexpr uint8_t matrix_col_pins[matrix_columns] = {PIN_C0, PIN_C1};
  };

  typedef kaleidoscope::driver::keyscanner::ATmega<KeyScannerProps> KeyScanner;
  typedef kaleidoscope::driver::bootloader::avr::Caterina BootLoader;
  static constexpr const char *short_name = "imaginary-keypad";
};

class Keypad: public kaleidoscope::device::ATmega32U4Keyboard<KeypadProps> {};

#define PER_KEY_DATA(dflt, \
  R0C0, R0C1,              \
  R1C0, R1C1               \
)                          \
  R0C0, R0C1, R1C0, R1C1

}
}

EXPORT_DEVICE(kaleidoscope::device::imaginary::Keypad);

}
#endif

ImaginaryKeypad.cpp

#ifdef ARDUINO_AVR_IMAGINARY_KEYPAD

#include <Kaleidoscope.h>

// Here, we set up aliases to the device's KeyScanner and KeyScannerProps in the
// global namespace within the scope of this file. We'll use these aliases to
// simplify some template initialization code below.
using KeyScannerProps = typename kaleidoscope::device::imaginary::KeypadProps::KeyScannerProps;
using KeyScanner = typename kaleidoscope::device::imaginary::KeypadProps::KeyScanner;

namespace kaleidoscope {
namespace device {
namespace imaginary {

// `KeyScannerProps` here refers to the alias set up above. We do not need to
// prefix the `matrix_rows` and `matrix_columns` names within the array
// declaration, because those are resolved within the context of the class, so
// the `matrix_rows` in `KeyScannerProps::matrix_row_pins[matrix_rows]` gets
// resolved as `KeyScannerProps::matrix_rows`.
const uint8_t KeyScannerProps::matrix_rows;
const uint8_t KeyScannerProps::matrix_columns;
constexpr uint8_t KeyScannerProps::matrix_row_pins[matrix_rows];
constexpr uint8_t KeyScannerProps::matrix_col_pins[matrix_columns];

// `KeyScanner` here refers to the alias set up above, just like in the
// `KeyScannerProps` case above.
template<> KeyScanner::row_state_t KeyScanner::matrix_state_[KeyScannerProps::matrix_rows] = {};

// We set up the TIMER1 interrupt vector here. Due to dependency reasons, this
// cannot be in a header-only driver, and must be placed here.
//
// Timer1 is responsible for setting a property on the KeyScanner, which will
// tell it to do a scan. We use this to make sure that scans happen at roughly
// the intervals we want. We do the scan outside of the interrupt scope for
// practical reasons: guarding every codepath against interrupts that can be
// reached from the scan is far too tedious, for very little gain.
ISR(TIMER1_OVF_vect) {
  Runtime.device().keyScanner().do_scan_ = true;
}

}
}
}
#endif

That’s it.