Macro Pad Firmware guide!

This documentation provides an overview of the updated example firmware file (main.c). It covers the setup, key scanning, encoder handling, display updates, and the implementation of modifier keys and multi-key support.

The code is located at CircuitReeRUG/macro_pad. Specifically in src/main.c.

Note: The example firmware is absolute trash, but it's a good starting point for your own custom firmware.

1. Included Libraries and Headers

The firmware begins by including necessary hardware and software libraries:

 
#include <hardware/i2c.h> // pico-sdk
#include <hardware/pio.h> // pico-sdk
#include <pico/multicore.h> // pico-sdk
#include <pico/stdlib.h> // pico-sdk
#include "config.h" // Our custom configuration, you are encouraged to use it
#include "encoder.h" // Encoder lib
#include "hardware/timer.h" // pico-sdk
#include "hid_helpers.h" // HID helper functions
#include "mhid.h" // TinyUSB HID handler
#include "ssd1306.h" // OLED display lib

These headers provide functionalities for I2C communication, Programmable I/O (PIO), multi-core processing, standard input/output, encoder handling, HID (Human Interface Device) interactions, and controlling the SSD1306 OLED display.

2. Global Variables and Structures

A global instance of the OLED display is declared:

static ssd1306_t oled_display;

The firmware introduces a new Key structure to represent keys in the keymap:

typedef struct Key {
    bool type;  // true for modifier key, false for regular keys
    union {
        uint8_t mod_key;    // Modifier key (e.g., Ctrl, Alt, Shift)
        uint8_t keys[6];    // Up to 6 regular keys
    };
} Key;

A macro is defined for easy access to keys in the keymap:

#define query(key) keymap[key / MATRIX_COLS][key % MATRIX_COLS]

3. I2C Setup

The setup_i2c() function initializes the I2C communication necessary for the OLED display on pin 17 (SDA) and pin 18 (SCL):

static void setup_i2c() {
    i2c_init(I2C_INSTANCE(1), 400 * 1000);
    gpio_set_function(SCREEN_SDA, GPIO_FUNC_I2C);
    gpio_set_function(SCREEN_SCL, GPIO_FUNC_I2C);
    gpio_pull_up(SCREEN_SDA);
    gpio_pull_up(SCREEN_SCL);
}

4. Keyboard GPIO Setup

The setup_kb_gpio() function configures the GPIO pins for the keyboard matrix (rows and columns):

static void setup_kb_gpio() {
    for (uint8_t i = 0; i < MATRIX_COLS; i++) {
        gpio_init(col_pins[i]);
        gpio_set_dir(col_pins[i], GPIO_IN);
        gpio_pull_up(col_pins[i]);
    }
    for (uint8_t j = 0; j < MATRIX_ROWS; j++) {
        gpio_init(row_pins[j]);
        gpio_set_dir(row_pins[j], GPIO_OUT);
        gpio_put(row_pins[j], 1);
    }
}

5. Updated Keymap Definition

The keymap supports modifier keys and combinations of multiple keys:


// extended keymap (multiple keys per button - up to 6)
static const Key keymap[MATRIX_ROWS][MATRIX_COLS] = {
    {
        {true, .mod_key = KEYBOARD_MODIFIER_LEFTALT}, // Alt key  (ROW 1, COL 3)
        {true, .mod_key = KEYBOARD_MODIFIER_LEFTCTRL}, // Ctrl key (ROW 1, COL 2)
        {true, .mod_key = KEYBOARD_MODIFIER_LEFTGUI}, // Windows key (ROW 1, COL 1)
    },
    {
        {false, .keys = {HID_KEY_M, HID_KEY_N, HID_KEY_O, HID_KEY_P, HID_KEY_Q, HID_KEY_R}}, // MNOPQR (R2:C3)
        {false, .keys = {HID_KEY_G, HID_KEY_H, HID_KEY_I, HID_KEY_J, HID_KEY_K, HID_KEY_L}}, // GHIJKL (R2:C2)
        {false, .keys = {HID_KEY_A, HID_KEY_B, HID_KEY_C, HID_KEY_D, HID_KEY_E, HID_KEY_F}}, // ABCDEF (R2:C1)
    },
    {
        {false, .keys = {HID_KEY_5, HID_KEY_6, HID_KEY_7, HID_KEY_8, HID_KEY_9, HID_KEY_0}}, // 567890 (R3:C3)
        {false, .keys = {HID_KEY_Y, HID_KEY_Z, HID_KEY_1, HID_KEY_2, HID_KEY_3, HID_KEY_4}}, // YZ1234 (R3:C2)
        {false, .keys = {HID_KEY_S, HID_KEY_T, HID_KEY_U, HID_KEY_V, HID_KEY_W, HID_KEY_X}}, // STUVWX (R3:C1)
    },
    {
        {true, .mod_key = KEYBOARD_MODIFIER_LEFTSHIFT}, // Shift key (R4:C3)
        {false, .keys = {HID_KEY_ENTER, HID_KEY_SPACE, HID_KEY_BACKSPACE, HID_KEY_TAB, HID_KEY_END, HID_KEY_DELETE}}, // Enter, Space, Backspace, Tab, End, Delete (R4:C2)
        {true, .mod_key = KEYBOARD_MODIFIER_LEFTSHIFT}, // Shift key (R4:C1)
    },
};

// basic keymap (1 key per button)
static const Key keymap[MATRIX_ROWS][MATRIX_COLS] = {
    {
        {true, .mod_key = KEYBOARD_MODIFIER_LEFTALT}, // Alt key  (ROW 1, COL 3)
        {true, .mod_key = KEYBOARD_MODIFIER_LEFTCTRL}, // Ctrl key (ROW 1, COL 2)
        {true, .mod_key = KEYBOARD_MODIFIER_LEFTGUI}, // Windows key (ROW 1, COL 1)
    },
    {
        {false, .keys = {HID_KEY_C}}, // C (R2:C3)
        {false, .keys = {HID_KEY_B}}, // B (R2:C2)
        {false, .keys = {HID_KEY_A}}, // A (R2:C1)
    },
    {
        {false, .keys = {HID_KEY_F}}, // F (R3:C3)
        {false, .keys = {HID_KEY_E}}, // E (R3:C2)
        {false, .keys = {HID_KEY_D}}, // D (R3:C1)
    },
    {
        {true, .mod_key = KEYBOARD_MODIFIER_LEFTSHIFT}, // Shift key (R4:C3)
        {false, .keys = {HID_KEY_G}}, // G (R4:C2)
        {true, .mod_key = KEYBOARD_MODIFIER_LEFTSHIFT}, // Shift key (R4:C1)
    },
};

Note: The keys are defined from right to left, if you want "ABC" to be pressed, you should define it as "CBA".

6. Scanning the Keyboard Matrix

The scan_matrix() function detects key presses by scanning the keyboard matrix:

static inline void scan_matrix(int8_t *keys, int8_t *cnt) {
    for (uint8_t i = 0; i < MATRIX_ROWS; i++) {
        gpio_put(row_pins[i], 0);
        sleep_us(1);
        for (uint8_t j = 0; j < MATRIX_COLS; j++) {
            if (gpio_get(col_pins[j]) == 0 && *cnt < 2) {
                keys[*cnt] = (int8_t)(i * MATRIX_COLS + j);
                (*cnt)++;
            }
        }
        gpio_put(row_pins[i], 1);
    }
}

7. Parsing Key Presses

The parse_keys() function interprets the scanned keys and creates a HID report:

static inline hid_report parse_keys(int8_t keys[2], int8_t cnt) {
    hid_report report = {.valid = true, .mod_key = 0, .data = {0}};

    for (int8_t i = 0; i < cnt; i++) {
        Key key_entry = query(keys[i]);
        if (key_entry.type) {
            // Modifier key
            report.mod_key |= key_entry.mod_key;
        } else {
            // Regular keys
            memcpy(report.data, key_entry.keys, sizeof(key_entry.keys));
        }
    }

    return report;
}

8. Debouncing Key Presses

We debounce key presses using the get_key().

static inline hid_report get_key() {
    int8_t keys[2] = {0};
    hid_report report = {.valid = false};
    uint32_t start = to_ms_since_boot(get_absolute_time());

    while (to_ms_since_boot(get_absolute_time()) - start < DEBOUNCE_DELAY) {
        int8_t current_keys[2] = {0};
        int8_t cnt = 0;
        scan_matrix(current_keys, &cnt);
        if (memcmp(keys, current_keys, sizeof(keys)) != 0 && cnt > 0) {
            memcpy(keys, current_keys, sizeof(keys));
            report = parse_keys(keys, cnt);
            start = to_ms_since_boot(get_absolute_time());
        }
    }

    return report;
}

This function continuously scans for key presses within a defined debounce delay. If the detected key changes during this period, it resets the timer, ensuring that only stable key presses are registered.

Note: The get_key() function is called to get the key pressed.

9. Updating the OLED Display

static inline void set_dpy(int8_t keycode) {
    if (keycode == -1) return;
    char ch = keycode_to_char((char)keycode, false);
    if (ch) {
        ssd1306_clear(&oled_display);
        char str[2] = {ch, '\0'};
        ssd1306_draw_string(&oled_display, 0, 0, 7, str);
        ssd1306_show(&oled_display);
    }
}

This function translates the keycode to a character and displays it on the OLED screen. If the keycode is invalid (-1), it does nothing.

10. Handling the Encoder

static inline int32_t get_enc() {
    int8_t diff = get_enc_pos_diff();
    if (diff > 0) {
        return encoder.increment;  // Increment action
    } else if (diff < 0) {
        return encoder.decrement;  // Decrement action
    } else if (get_enc_btn_state()) {
        return encoder.button;     // Button press action
    }
    return -1;
}

11. Main Function

The main() function initializes the board, I2C, OLED display, keyboard GPIO, and encoder. It then enters an infinite loop to handle HID tasks and run the HID handler.

int main() {
    board_init();
    tud_init(BOARD_TUD_RHPORT);

    if (board_init_after_tusb) {
        board_init_after_tusb();
    }

    // Display setup
    setup_i2c();
    ssd1306_init(&oled_display, 128, 64, 0x3C, I2C_INSTANCE(1));
    ssd1306_clear(&oled_display);
    ssd1306_show(&oled_display);

    // Keyboard and encoder setup
    setup_kb_gpio();
    setup_enc();

    while (1) {
        tud_task();
        run_hid(.get_key = get_key, .set_dpy = set_dpy, .get_enc = get_enc);
    }
}

Note: The run_hid() function is called to handle HID tasks (whenever an event happens, send it to the host). Function pointers are passed.

12. Implementing Modifier Keys

With the updated keymap and scanning functions, implementing modifier keys is straightforward.

A. Keymap with Modifiers

Modifier keys are defined in the keymap with the type field set to true and the mod_key field specifying the modifier:

{
    {true, .mod_key = KEYBOARD_MODIFIER_LEFTSHIFT},
    // ...
}

B. Handling Modifiers in parse_keys()

The parse_keys() function combines multiple modifier keys if pressed simultaneously:

if (key_entry.type) {
    // Modifier key
    report.mod_key |= key_entry.mod_key;
}

C. Sending HID Reports

The HID report structure now includes both the modifier keys and up to six regular keys, allowing for combinations like Shift + A.

13. Customization