9.4 KiB
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.
LEDs
kaleidoscope::driver::led::Base
Keyscanner
kaleidoscope::driver::keyscanner::Base
Helpers
kaleidoscope::device::ATMega32U4Keyboard
kaleidoscope::driver::keyscanner::AVR
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::state_t KeyScanner::state_ = {};
// 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.