macr0

The assembled macr0 revision 2.

Macr0 is a 4-button macro pad which identifies as a USB keyboard.

This project is effectively a handful of proofs of concept. I wanted to test a few things I haven't done before with the intention to scale-up for a future project (that project: jank).

The goals I had in mind for macr0 included:

  1. HID compliant USB peripheral using an ATmega32U4 microcontroller.
  2. Keys will be matrixed (even though the mcu has plenty of IO for only 4 keys ).
  3. Keys will be mechanical Gateron switches - these are pin-compatible Cherry MX clones.
  4. Keys will all be LED back-lit using a CAT4104 constant-current LED driver (even though there are only 4 LEDs and they could be easily driven directly by the mcu).
  5. Back-light illumination will be adjustable.
  6. Connectivity (and power) via a USC-C connector, configured for USB 2.0.

KiCad for schematic and PCB design, JLCPCB for fabrication. VSCode with the PlatformIO IDE extension for developng the firmware. Hardware (schematic and PCB design) and firmware are all open and published in a Gitlab repository.

The firmware was programmed in C and draws from Dean Camera's LUFA libray.


Revision 1

For the most part, everything worked out. The only goal I didn't quite hit was in regards to the LED backlighting. I used a CAT4101 LED but I didn't size the LEDs correctly. Using 4 LEDs total, I used two channels of the CAT4104 with two LEDs per channel. With VCC at 5V and the CAT4104 requiring 0.4V headroom, that leaves 4.6V per channel or 2.3V per LED. The LEDs I used are rated at 3.5V. As a result, I still have backlighting but at a lower brightness and with some instability (flickering).

I have been using the device as a media controller with the keys configured as play/pause, stop, previous and next.

The wood enclosure is made from spotted gum and was intended to match another project - volcon. A photo in the gallery below shows both devices together.


Revision 2

The assembled macr0 revision 2, showing the LED pulse effect.

Having learned a few lessons and proved concepts for a larger-scale project, I decided to revise macr0 since I've found it quite useful. Changes to be incorporated::

Since this revision won't require the keyscan functionality, I'll record the keyscan code below for future reference (i.e. the mentioned larger-scale project) since it will no longer be reflected in the main gitlab branch.

Update: revision 2 works great. I get full brightness out of the LEDs (although I set the default to quite dim). A button underneath the unit cycles through different LED modes - off, various brighness levels and a pulse effect with three different speeds. The PCB is also a nicer fit to the wood enclosure.


Configuration

With Revision 2 the keys can be configured to take any of the following actions:

The key actions are set when the AVR is flashed, so configuration is done with the source code. The only file that needs to be edited is the "keymap.c" file. Following are some examples of how the keys can be configured.

Example 1

All four keys are configured for media control - toggle (play/pause) stop, previous and next. It is this configuration I actually settled on for daily use.

// keymap.c - Configure keys with this file.

#include "keymap.h"

const uint8_t KEY_PIN_ARRAY[] = {KEY_1, KEY_2, KEY_3, KEY_4}; // Set keys as regular keystrokes or media keys.
const uint8_t MACRO_PIN_ARRAY[] = {};                         // Set keys as macros.

// Map the regular keystrokes.
const char KEY_MAP[] PROGMEM = {
    HID_MEDIACONTROLLER_SC_TOGGLE,   // Key 1
    HID_MEDIACONTROLLER_SC_STOP,     // Key 2
    HID_MEDIACONTROLLER_SC_PREVIOUS, // Key 3
    HID_MEDIACONTROLLER_SC_NEXT      // Key 4
};

// Map the macro strings.
const char MACRO_MAP[][MAX_MACRO_CHARS] PROGMEM = {};

Example 2

In this example the top two keys (1 and 2) are for media control (toggle and stop) and the bottom two keys (3 and 4) are macros that will type two different sentences.

// keymap.c - Configure keys with this file.

#include "keymap.h"

const uint8_t KEY_PIN_ARRAY[] = {KEY_1, KEY_2};   // Set keys as regular keystrokes or media keys.
const uint8_t MACRO_PIN_ARRAY[] = {KEY_3, KEY_4}; // Set keys as macros.

// Map the regular keystrokes.
const char KEY_MAP[] PROGMEM = {
    HID_MEDIACONTROLLER_SC_TOGGLE, // Key 1
    HID_MEDIACONTROLLER_SC_STOP    // Key 2
};

// Map the macro strings.
const char MACRO_MAP[][MAX_MACRO_CHARS] PROGMEM = {
    {"The quick brown fox jumped over the lazy dog.\n"},                         // Key 3
    {"How much wood would a woodchuck chuck if a woodchuck could chuck wood?\n"} // Key 4
};

Example 3

For this final example all keys act as regular keyboard keystrokes. Key 1 is a modifier (left shift) and the remaining keys are A, B and C. This configuration therefore give macr0 the ability to type 'A', 'B', 'C', 'a', 'b' and 'c'.

// keymap.c - Configure keys with this file.

#include "keymap.h"

const uint8_t KEY_PIN_ARRAY[] = {KEY_1, KEY_2, KEY_3, KEY_4}; // Set keys as regular keystrokes or media keys.
const uint8_t MACRO_PIN_ARRAY[] = {};                         // Set keys as macros.

// Map the regular keystrokes.
const char KEY_MAP[] PROGMEM = {
    HID_KEYBOAD_SC_LEFT_SHIFT, // Key 1
    HID_KEYBOAD_SC_A,          // Key 2
    HID_KEYBOAD_SC_B,          // Key 3
    HID_KEYBOAD_SC_C           // Key 4
};

// Map the macro strings.
const char MACRO_MAP[][MAX_MACRO_CHARS] PROGMEM = {};



Key Scanning Code Snippets (superseded by revision 2)

The following code snippets became redundant and were removed from revision 2. I've kept them here as reference for future use in a planned project that will use this matrixing keyscan functionality.

// The key map array.
const char KEYMAP[NUM_ROWS][NUM_COLS] PROGMEM = {
    // Column 1                       Column 2
    {HID_MEDIACONTROLLER_SC_TOGGLE,   HID_MEDIACONTROLLER_SC_STOP}, // Row 1
    {HID_MEDIACONTROLLER_SC_PREVIOUS, HID_MEDIACONTROLLER_SC_NEXT}  // Row 2
};

// Initialise the gpio for scanning rows and columns.
void keyscan_init(void)
{
    // Set rows as outputs and initialise all as high (disabled).
    KEYS_DDR |= ((1 << ROW_1) | (1 << ROW_2));
    KEYS_PORT |= ((1 << ROW_1) | (1 << ROW_2));

    // Set columns as inputs and enable pull-ups.
    KEYS_DDR &= ~((1 << COL_1) | (1 << COL_2));
    KEYS_PORT |= ((1 << COL_1) | (1 << COL_2));
}

// Parse the detected key and update the appropriate part of the report struct.
void handle_key(char key, keyscan_report_t *keyscan_report)
{
    // Media key scan values start at 0xF0, after the last keyboard modifier key scan.
    if(key > HID_KEYBOARD_SC_RIGHT_GUI)
    {
        // Convert the media key to a value from 0 to 10.
        key -= HID_MEDIACONTROLLER_SC_PLAY;

        // Shift a bit to the corresponding bit within the media_keys integer.
        keyscan_report->media_keys |= (1 << key);
    }

    // Modifier keys scan values start at 0xE0, after the last keyboard modifier key scan.
    else if(key > HID_KEYBOARD_SC_APPLICATION)
    {
        // Convert the media key to a value from 0 to 7.
        key -= HID_KEYBOARD_SC_LEFT_CONTROL;

        // Shift a bit to the corresponding bit within the modifier integer.
        keyscan_report->modifier |= (1 << key);
    }

    // Regular keys scan values range from 0x00 to 0x65.
    else if(key > HID_KEYBOARD_SC_RESERVED)
    {
        // Skip array elements that already have a keyscan value written.
        uint8_t i = 0;
        while((keyscan_report->keys[i]) && (i < MAX_KEYS)) i++;

        // Only register the key if the maximum number of simultaneous keys is not reached.
        if(i < MAX_KEYS) keyscan_report->keys[i] = key;
    }
}

// Row-by-row scan through each column and determine which keys are currently pressed.
void create_keyscan_report(keyscan_report_t *keyscan_report)
{
    // Start with a blank keyscan report.
    memset(keyscan_report, 0, sizeof(keyscan_report_t));

    // Define the pins to scan through.
    uint8_t row_array[2] = {ROW_1, ROW_2};
    uint8_t col_array[2] = {COL_1, COL_2};

    // Loop through for each row.
    for(uint8_t r = 0; r < sizeof(row_array); r++)
    {
        // Set low current row (enable check).
        KEYS_PORT &= ~(1 << row_array[r]);

        // Wait until row is set low before continuing, otherwise column checks can be missed.
        while(!(~KEYS_PINS & (1 << row_array[r]))) {}

        // Loop through for each column in the current row.
        for(uint8_t c = 0; c < sizeof(col_array); c++)
        {
            // If the button in the current row and column is pressed, handle it.
            if(~KEYS_PINS & (1 << col_array[c]))
            {
                handle_key(pgm_read_byte(&KEYMAP[r][c]), keyscan_report);
            }
        }

        // Set high current row (disable check).
        KEYS_PORT |= (1 << row_array[r]);
    }
}


Gallery