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
- Define additional keymaps and logic to switch between them using modifier keys.
- Create complex macros that perform multiple actions.
- Display more information on the OLED.
- Have fun! Build your unique macro pad functionality!