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 concepts. I wanted to test a few things I haven't done before with the intention to scale-up for a future project.

The goals I had in mind for this project included:

  1. HID compliant USB peripheral using an ATmega32U4 microcontroller.
  2. Keys will be matrixed (even though the mcu has plenty of spare IO for only 4 buttons).
  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) will be via a USC-C connector, configured for USB 2.0.

KiCad for PCB design and 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.

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.

The assembled macr0 revision 2, bottom view.

Revision 2

Having learned a few lessons and prooved 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 code bellow for future reference (i.e. the mentioned larger-scale project) since it will no longer be reflected in the main gitlab branch.

Revision 2 works great. I get full brightness out of the LEDs (although I set the default to quite dim). The PCB is also a nicer fit to the wood enclosure.

Key Scanning Code Snippets

// The key map array.
    // Column 1                       Column 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.
        // Convert the media key to a value from 0 to 10.

        // 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.
        // Convert the media key to a value from 0 to 7.

        // 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]);