I have drawn (and built) some (unnecessarily complex) keyboards. All of them use overpowered small computer (ATMega32U4) so I can use existing firmware to make them programmable. It's really nice thing to have a programmable keyboard, you know. It's also nice to understand decently what goes on in your peripheral.
Now, the leading project, or at least in my circle, is QMK which was written in C. I'm not the brightest tool in the shed, but the maintainers of that project make it quite easy, for me, to add new keyboards (which I did for some useless boards of mine). Though it's another story to really understand the inner working of QMK because it is built on top of an operating system. Well, to be absolutely honest, I feel overwhelmed when I have to learn something if I also have to learn an operating system it's built on top of.
So, I decided to look for some other alternatives:
So, considering my situation, I think it's better for me to pick Kaleidoscope.
Ackshually, I'm not sure if the correct term is porting
but whatever.
The keyboard that I choose is: Squiggle.
Specifically the minimalist
version which "only" uses a single promicro to power
it (doesn't even use ioexpander).
Based on what I have done in the past, what I need to provide if I want to make a keyboard supported by a keyboard firmware project are:
So, the matrix approximately looks like this:
c0 c1 c2 c3 c4 c5 c6 c7 c8 c9
X - X - X - X - X -------- X - X - X - X - X r0
| | | | | | | | | |
X - X - X - X - X -------- X - X - X - X - X r1
| | | | | | | | | |
X - X - X - X - X -------- X - X - X - X - X r2
| | | |
X - X -------- X - X r3
While the promicro's pins that it uses:
+---|==|---+
|o o|
|o o|
|o o|
|o o|
|o o| r0
|o /\ o| r1
c0 |o / \ o| r2
c1 |o / \ o| r3
c2 |o \ / o| c5
c3 |o \ / o| c6
c4 |o \/ o| c7
c9 |o o| c8
+----------+
I head to readthedocs and then read what I need to install to develop something with it.
All of them can be installed via pacman
, package manager of my distro.
Yes, I use arch.
In short, I followed this page to setup the toolchain I needed, followed by opening Arduino, and then try to compile some other keyboards' firmwares.
Everything seems nice and dandy, and then I set to explore it.
See, there are some directories in ~/Arduino/hardware/keyboardio
.
avr
build-tools
toolchain
virtual
The source files of the project lie in the avr
dir.
The contents of directories other than avr
aren't really relevant for this post.
In avr
dir, there are some directories and files:
bootloaders
: which contain source files for atreus's bootloader (teensy
variant, i think) and caterina (promicro's default bootloader, i think).build-tools
: which is a symlink to the previous build-tools
.libraries
: where the source files of kaleidoscope reside.variants
: where pin definition of supported keyboards located at.boards.txt
: contains the information needed when you flash something.platform.txt
: standard arduino thingy, no worries.In libraries
, there are:
Kaleidoscope
is the real (I swear this time is the last one) location of
the main source file of this firmware reside at.Kaleidoscope-HIDAdaptor-KeyboardioHID
I think it "wraps" KeyboardioHID
to
be used by Kaleidoscope
as a namespace.KeyboardioHID
contains the source files for HID over USB functionality.KeyboardioScanner
contains the source file for something related to pin scanning.Model01-Firmware
I think it's the default firmware of your KeyboardIO Model01,
if you have one, that is.Now, for the main directory, there are some dirs and files:
bin
directory that contains utilities.docs
directory that contains the source for the main doc site.etc
directory contains files that you'd put in /etc/
.examples
directory contains example sketches.extras
not sure why it's there.src
is a proper name for directory where you put your source files on.test
because it's cool to test stuff.CODE_OF_CONDUCT.md
is a good way to cover your back, legally, when you are
active in a 9-5 environment.
I'm not joking.CONTRIBUTING.md
is a good way to cover your back, legally, when some randos
on the net give you something.
I'm not joking either.library.properties
is a standard arduino thingy.LICENSE
: Stallman-sama approves.Makefile
is the wisest format of build directive in software world.README.md
no.
I'd better be banging my head on the table for an hour than reading it.In src
directory, there are:
kaleidoscope
kaleidoscope_internal
include
their respective plugins or keyboard
"definition".I won't be touching kaleidoscope_internal
if I just want to "port" a keyboard.
So, I will see what's inside of kaleidoscope
dir.
device
seems to be what I'm looking for.driver
contains source file of drivers that are needed to make your microcontroller
useable as a keyboard.plugin
neat stuff you will likely use.util
not sure what it is..cpp
and .h
files.Because I've found what I'm looking for, I will see what is the content of that dir.
avr
that contains pin and port definition.dygma
, ez
, kbdfans
, keyboardio
, olkb
, softhruf
, technomancy
are
vendor directories that contains their respective keyboards.virtual
not sure.In the past, I used a hand-wired atreus so I will use it as an example from the existing device definitions.
There are two files:
Atreus.h
Atreus.cpp
The content of the header file is the following:
#pragma once
#ifdef ARDUINO_AVR_ATREUS
#include <Arduino.h>
#include "kaleidoscope/driver/bootloader/avr/HalfKay.h"
#include "kaleidoscope/driver/bootloader/avr/Caterina.h"
#include "kaleidoscope/device/ATmega32U4Keyboard.h"
namespace kaleidoscope {
namespace device {
namespace technomancy {
struct AtreusProps : kaleidoscope::device::ATmega32U4KeyboardProps {
struct KeyScannerProps : public kaleidoscope::driver::keyscanner::ATmegaProps {
static constexpr uint8_t matrix_rows = 4;
static constexpr uint8_t matrix_columns = 12;
typedef MatrixAddr<matrix_rows, matrix_columns> KeyAddr;
#ifndef KALEIDOSCOPE_VIRTUAL_BUILD
#ifdef KALEIDOSCOPE_HARDWARE_ATREUS_PINOUT_ASTAR
static constexpr uint8_t matrix_row_pins[matrix_rows] = {PIN_D0, PIN_D1, PIN_D3, PIN_D2};
static constexpr uint8_t matrix_col_pins[matrix_columns] = {PIN_D7, PIN_C6, PIN_B5, PIN_B4, PIN_E6, PIN_D4, PIN_B6, PIN_F6, PIN_F7, PIN_D6, PIN_B7};
#endif
#ifdef KALEIDOSCOPE_HARDWARE_ATREUS_PINOUT_ASTAR_DOWN
/* omitted */
#endif
#endif // ifndef KALEIDOSCOPE_VIRTUAL_BUILD
};
typedef kaleidoscope::driver::keyscanner::ATmega<KeyScannerProps> KeyScanner;
#ifdef KALEIDOSCOPE_HARDWARE_ATREUS_PINOUT_LEGACY_TEENSY2
typedef kaleidoscope::driver::bootloader::avr::HalfKay BootLoader;
#else
typedef kaleidoscope::driver::bootloader::avr::Caterina BootLoader;
#endif
static constexpr const char *short_name = "atreus";
};
#ifndef KALEIDOSCOPE_VIRTUAL_BUILD
class Atreus: public kaleidoscope::device::ATmega32U4Keyboard<AtreusProps> {};
#else // ifndef KALEIDOSCOPE_VIRTUAL_BUILD
class Atreus;
#endif // ifndef KALEIDOSCOPE_VIRTUAL_BUILD
#define PER_KEY_DATA(dflt, \
/* omitted */ \
) \
\
/* omitted */
#define PER_KEY_DATA_STACKED(dflt, \
/* omitted */ \
) \
/* omitted */
}
}
EXPORT_DEVICE(kaleidoscope::device::technomancy::Atreus)
}
#endif
Which basically defines:
kaleidoscope::device::technomancy
.AtreusProps
) that contains:
KeyScannerProps
which in turn defines:
matrix_rows
: the amount of rows a device has.matrix_columns
: the amount of columns a device has.matrix_row_pins
: the pin used by each row.matrix_col_pins
: the pin used by each column.KeyScanner
which basically the previous point.BootLoader
whether it uses Caterina
or HalfKay
bootloader.PER_KEY_DATA
which represents how the keys are arranged if both sides of the
halves are next to each other.PER_KEY_DATA_STACKED
which represent how the keys are arranged if both sides
of the halves are located on top of the other one.Now, for the cpp
file:
#ifndef KALEIDOSCOPE_VIRTUAL_BUILD
#ifdef ARDUINO_AVR_ATREUS
#include "kaleidoscope/Runtime.h"
#include "kaleidoscope/driver/keyscanner/Base_Impl.h"
using KeyScannerProps = typename kaleidoscope::device::technomancy::AtreusProps::KeyScannerProps;
using KeyScanner = typename kaleidoscope::device::technomancy::AtreusProps::KeyScanner;
namespace kaleidoscope {
namespace device {
namespace technomancy {
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];
template<> uint16_t KeyScanner::previousKeyState_[KeyScannerProps::matrix_rows] = {};
template<> uint16_t KeyScanner::keyState_[KeyScannerProps::matrix_rows] = {};
template<> uint16_t KeyScanner::masks_[KeyScannerProps::matrix_rows] = {};
template<> uint8_t KeyScanner::debounce_matrix_[KeyScannerProps::matrix_rows][KeyScannerProps::matrix_columns] = {};
ISR(TIMER1_OVF_vect) {
Runtime.device().keyScanner().do_scan_ = true;
}
} // namespace technomancy
} // namespace device
} // namespace kaleidoscope
#endif
#endif // ifndef KALEIDOSCOPE_VIRTUAL_BUILD
Which I think pretty clear.
From the previous sections, to support a new device, there are some files that need to be changed or created. In this case:
~/Arduino/hardware/keyboardio/avr/boards.txt
needs to include information for Squiggle.~/Arduino/hardware/keyboardio/avr/libraries/Kaleidoscope/src/Kaleidoscope-Hardware-Brick-Squiggle.h
needs to be created.~/Arduino/hardware/keyboardio/avr/libraries/Kaleidoscope/src/kaleidoscope/device/brick/
needs to be created.~/Arduino/hardware/keyboardio/avr/libraries/Kaleidoscope/src/kaleidoscope/device/brick/Squiggle.h
needs to be created.~/Arduino/hardware/keyboardio/avr/libraries/Kaleidoscope/src/kaleidoscope/device/brick/Squiggle.cpp
needs to be created.For boards.txt
's addition, I added the following snippet.
squiggle.name=Squiggle # Should be self explanatory.
squiggle.vid.0=0x6969 # Nice.
squiggle.pid.0=0x4200 # Blaze It!
squiggle.upload.maximum_data_size=2560 # Not sure, but everything seems use it.
squiggle.build.extra_flags={build.usb_flags} '-DKALEIDOSCOPE_HARDWARE_H="Kaleidoscope-Hardware-Brick-Squiggle.h"'
# Based on other entries, the format should be Kaleidoscope-Hardware-Vendor-Board.h
squiggle.upload.tool=avrdude # avrdude is pretty nice.
squiggle.upload.protocol=avr109 # promicro uses this, i think.
squiggle.upload.maximum_size=28672 # usually 32kb. but other boards use this.
squiggle.upload.speed=57600
squiggle.upload.disable_flushing=true
squiggle.upload.wait_for_upload_port=true
squiggle.build.mcu=atmega32u4 # promicro uses it.
squiggle.build.f_cpu=16000000L # i use 16KHz variant.
squiggle.build.vid=0x6969 # should be the same as squiggle.vid
squiggle.build.pid=0x4200 # should be the same as squiggle.pid
squiggle.build.usb_product="Squiggle"
squiggle.build.usb_manufacturer="Brick"
squiggle.build.board=AVR_SQUIGGLE
squiggle.build.core=arduino:arduino
While ~/Arduino/hardware/keyboardio/avr/libraries/Kaleidoscope/src/Kaleidoscope-Hardware-Brick-Squiggle.h
,
I just need to create a header file that contains the following lines
#pragma once
#include "kaleidoscope/device/brick/Squiggle.h"
As for Squiggle.h
and Squiggle.cpp
, basically I just copied Atreus.h
and Atreus.cpp
with some modifications:
#pragma once
#ifdef ARDUINO_AVR_SQUIGGLE
#include <Arduino.h>
#include "kaleidoscope/driver/bootloader/avr/Caterina.h"
#include "kaleidoscope/device/ATmega32U4Keyboard.h"
namespace kaleidoscope {
namespace device {
namespace brick {
struct SquiggleProps : kaleidoscope::device::ATmega32U4KeyboardProps {
struct KeyScannerProps : public kaleidoscope::driver::keyscanner::ATmegaProps {
static constexpr uint8_t matrix_rows = 4;
static constexpr uint8_t matrix_columns = 10;
typedef MatrixAddr<matrix_rows, matrix_columns> KeyAddr;
#ifndef KALEIDOSCOPE_VIRTUAL_BUILD
static constexpr uint8_t matrix_row_pins[matrix_rows] = {PIN_F4, PIN_F5, PIN_F6, PIN_F7};
static constexpr uint8_t matrix_col_pins[matrix_columns] = {PIN_D4, PIN_C6, PIN_D7, PIN_E6, PIN_B4, PIN_B1, PIN_B3, PIN_B2, PIN_B6, PIN_B5};
#endif // ifndef KALEIDOSCOPE_VIRTUAL_BUILD
};
typedef kaleidoscope::driver::keyscanner::ATmega<KeyScannerProps> KeyScanner;
typedef kaleidoscope::driver::bootloader::avr::Caterina BootLoader;
static constexpr const char *short_name = "squiggle";
};
#ifndef KALEIDOSCOPE_VIRTUAL_BUILD
class Squiggle: public kaleidoscope::device::ATmega32U4Keyboard<SquiggleProps> {};
#else // ifndef KALEIDOSCOPE_VIRTUAL_BUILD
class Squiggle;
#endif // ifndef KALEIDOSCOPE_VIRTUAL_BUILD
#define PER_KEY_DATA(dflt, \
R0C0, R0C1, R0C2, R0C3, R0C4, R0C5, R0C6, R0C7, R0C8, R0C9, \
R1C0, R1C1, R1C2, R1C3, R1C4, R1C5, R1C6, R1C7, R1C8, R1C9, \
R2C0, R2C1, R2C2, R2C3, R2C4, R2C5, R2C6, R2C7, R2C8, R2C9, \
R3C3, R3C4, R3C5, R3C6 \
) \
\
R0C0, R0C1, R0C2, R0C3, R0C4, R0C5, R0C6, R0C7, R0C8, R0C9, \
R1C0, R1C1, R1C2, R1C3, R1C4, R1C5, R1C6, R1C7, R1C8, R1C9, \
R2C0, R2C1, R2C2, R2C3, R2C4, R2C5, R2C6, R2C7, R2C8, R2C9, \
XXX, XXX, XXX, R3C3, R3C4, R3C5, R3C6, XXX, XXX, XXX
#define PER_KEY_DATA_STACKED(dflt, \
R0C0, R0C1, R0C2, R0C3, R0C4, \
R1C0, R1C1, R1C2, R1C3, R1C4, \
R2C0, R2C1, R2C2, R2C3, R2C4, \
R3C3, R3C4, \
\
R0C5, R0C6, R0C7, R0C8, R0C9, \
R1C5, R1C6, R1C7, R1C8, R1C9, \
R2C5, R2C6, R2C7, R2C8, R2C9, \
R3C5, R3C6 \
) \
R0C0, R0C1, R0C2, R0C3, R0C4, R0C5, R0C6, R0C7, R0C8, R0C9, \
R1C0, R1C1, R1C2, R1C3, R1C4, R1C5, R1C6, R1C7, R1C8, R1C9, \
R2C0, R2C1, R2C2, R2C3, R2C4, R2C5, R2C6, R2C7, R2C8, R2C9, \
XXX, XXX, XXX, R3C3, R3C4, R3C5, R3C6, XXX, XXX, XXX
}
}
EXPORT_DEVICE(kaleidoscope::device::brick::Squiggle)
}
#endif
Which basically I copied the following information:
kaleidoscope::device::brick
.SquiggleProps
) that contains:
KeyScannerProps
which in turn defines:
matrix_rows
: 4matrix_columns
: 10matrix_row_pins
: PIN_F4, PIN_F5, PIN_F6, PIN_F7matrix_col_pins
: PIN_D4, PIN_C6, PIN_D7, PIN_E6, PIN_B4, PIN_B1, PIN_B3, PIN_B2, PIN_B6, PIN_B5KeyScanner
which basically the previous point.BootLoader
which uses Caterina
.PER_KEY_DATA
PER_KEY_DATA_STACKED
And for Squiggle.cpp
, I use the following snippet
#ifndef KALEIDOSCOPE_VIRTUAL_BUILD
#ifdef ARDUINO_AVR_SQUIGGLE
#include "kaleidoscope/Runtime.h"
#include "kaleidoscope/driver/keyscanner/Base_Impl.h"
using KeyScannerProps = typename kaleidoscope::device::brick::SquiggleProps::KeyScannerProps;
using KeyScanner = typename kaleidoscope::device::brick::SquiggleProps::KeyScanner;
namespace kaleidoscope {
namespace device {
namespace brick {
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];
template<> uint16_t KeyScanner::previousKeyState_[KeyScannerProps::matrix_rows] = {};
template<> uint16_t KeyScanner::keyState_[KeyScannerProps::matrix_rows] = {};
template<> uint16_t KeyScanner::masks_[KeyScannerProps::matrix_rows] = {};
template<> uint8_t KeyScanner::debounce_matrix_[KeyScannerProps::matrix_rows][KeyScannerProps::matrix_columns] = {};
ISR(TIMER1_OVF_vect) {
Runtime.device().keyScanner().do_scan_ = true;
}
} // namespace brick
} // namespace device
} // namespace kaleidoscope
#endif
#endif // ifndef KALEIDOSCOPE_VIRTUAL_BUILD
And that's it. Basically that's all I need to do to port it.
The following snippet is my keymap from QMK that I've brought over to Kaleidoscope.
#include "Kaleidoscope.h"
#include "Kaleidoscope-Macros.h"
#include "Kaleidoscope-Qukeys.h"
#define MO(n) ShiftToLayer(n)
#define TG(n) LockLayer(n)
enum {
DVORAK,
RAISE,
LOWER
};
enum {
RESET
};
#define Key_Exclamation LSHIFT(Key_1)
#define Key_At LSHIFT(Key_2)
#define Key_Hash LSHIFT(Key_3)
#define Key_Dollar LSHIFT(Key_4)
#define Key_And LSHIFT(Key_7)
#define Key_Star LSHIFT(Key_8)
#define Key_Plus LSHIFT(Key_Equals)
#define Key_Question LSHIFT(Key_Slash)
#define Key_Underscore LSHIFT(Key_Minus)
#define Sft_A SFT_T(A)
#define Sft_S SFT_T(S)
#define Ctl_O CTL_T(O)
#define Ctl_N CTL_T(N)
#define Alt_E ALT_T(E)
#define Alt_T ALT_T(T)
#define Gui_I GUI_T(I)
#define Gui_D GUI_T(D)
#define Rse_Space LT(1, Space)
#define Lwr_Backspace LT(2, Backspace)
#define Sft_Escape SFT_T(Escape)
#define Alt_Enter ALT_T(Enter)
#define Mcr_Reset M(RESET)
/* *INDENT-OFF* */
KEYMAPS(
[DVORAK] = KEYMAP_STACKED
(
Key_Semicolon ,Key_Comma ,Key_Period ,Key_P ,Key_Y
,Sft_A ,Ctl_O ,Alt_E ,Key_U ,Gui_I
,Key_Quote ,Key_Q ,Key_J ,Key_K ,Key_X
,Lwr_Backspace ,Sft_Escape
,Key_F ,Key_G ,Key_C ,Key_R ,Key_L
,Gui_D ,Key_H ,Alt_T ,Ctl_N ,Sft_S
,Key_B ,Key_M ,Key_W ,Key_V ,Key_Z
,Alt_Enter ,Rse_Space
),
[RAISE] = KEYMAP_STACKED
(
Key_Exclamation ,Key_At ,Key_UpArrow ,Key_LeftCurlyBracket ,Key_RightCurlyBracket
,Key_Hash ,Key_LeftArrow ,Key_DownArrow ,Key_RightArrow ,Key_Dollar
,Key_LeftBracket ,Key_RightBracket ,Key_LeftParen ,Key_RightParen ,Key_And
,___ ,___
,Key_Backslash ,Key_7 ,Key_8 ,Key_9 ,Key_Star
,Key_Equals ,Key_4 ,Key_5 ,Key_6 ,Key_0
,Key_Backtick ,Key_1 ,Key_2 ,Key_3 ,Key_Plus
,___ ,___
),
[LOWER] = KEYMAP_STACKED
(
Key_Escape ,Key_Question ,Key_Underscore ,Key_F1 ,Key_F2
,Key_LeftShift ,Key_Tab ,Key_PageUp ,Key_F5 ,Key_F6
,Mcr_Reset ,Key_ScrollLock ,Key_PageDown ,Key_F9 ,Key_F10
,___ ,___
,Key_F3 ,Key_F4 ,Key_Minus ,Key_Slash ,Mcr_Reset
,Key_F7 ,Key_F8 ,Key_Home ,Key_LeftAlt ,Key_Enter
,Key_F11 ,Key_F12 ,Key_End ,Key_Insert ,Key_Slash
,___ ,___
)
)
/* *INDENT-ON* */
KALEIDOSCOPE_INIT_PLUGINS(Macros, Qukeys, MagicCombo);
const macro_t *macroAction(uint8_t macroIndex, uint8_t keyState) {
switch (macroIndex) {
case RESET:
Kaleidoscope.rebootBootloader();
break;
default:
break;
}
return MACRO_NONE;
}
void setup() {
Kaleidoscope.setup();
}
void loop() {
Kaleidoscope.loop();
}
I still cannot emulate QMK's combo behaviour to Kaleidoscope.