DIY Custom RP2040 Macropad With OLED and Encoder (QMK Firmware)

by np_vishwakarma in Circuits > Raspberry Pi

578 Views, 0 Favorites, 0 Comments

DIY Custom RP2040 Macropad With OLED and Encoder (QMK Firmware)

A Coustom Macropad (1).png
banner.png
20260611_172351.jpg
Top View (2).png

Apna Dost is a custom RP2040-powered macropad built to streamline everyday workflows with programmable keys, a rotary encoder, OLED feedback, and a fully open-source design.


Meet 'Apna Dost'—a custom 4-input macro pad engineered to turbocharge your daily desktop productivity, media control, and multitasking.

Powered by a Waveshare RP2040-Zero running custom QMK firmware, it packs mechanical switches, a 128x32 OLED, and a clicky rotary encoder into a clean layout.

  1. Dynamic OLED Display: Automatically renders custom 512-byte raw bitmaps for instant profile and layer feedback.
  2. Dual-Purpose Encoder: Handles smooth system volume adjustments by default, but shifts into a micro-step color wheel controller when needed.
  3. Engineered for Long Grinds: Built for 10+ hours of daily use, the onboard RGB is hardware-capped at 65% brightness to eliminate heat and component stress while delivering rich, smooth color transitions.


Supplies

20260411_123748.jpg
20260611_135311.jpg

These were the components used in this project:

  1. Waveshare RP2040-Zero
  2. OLED 128×32
  3. Mechanical Switches (otmeu blue switch)
  4. Rotary Encoder (EC11)
  5. Keycaps
  6. Encoder cap
  7. Diodes (1N4148)
  8. PCB / Perfboard
  9. Type-c USB Cable

Circuit Schematic and Hardware Design

2D_PCB1_2026-06-09.png
2D_PCB1_2026-06-09 (1).png
view.png

Hardware Architecture:

To build 'Apna Dost', I first mapped out the circuit schematic. While I assembled and hand-wired this version cleanly on a standard prototype perfboard, I have also fully designed a custom printed circuit board layout. I am sharing both the schematic and the open-source Gerber files if you want to order a professionally printed board!

Open source files and downloads:

All design files are open-sourced. Download them from the links below:

Gerber Files (Ready for Manufacturing) Download Gerber ZIP

Downloads

Hardware Assembly: Matrix, OLED, Encoder and Dual LED Mod

20260411_135841.jpg
20260411_135855.jpg
20260411_154340.jpg
20260611_134342.jpg
20260411_163438.jpg

1.The keyboard Matrix (2X2)

We have mapped our 4 physical inputs (3 keys + 1 encoder click) into a simple matrix using the following pins:

  1. Rows: GP10, GP11
  2. Columns: GP12, GP13

Each mechanical switch has a 1N4148 diode soldered to its pin to ensure clean signal isolation and to prevent from ghosting.

2. OLED Display Connection (I2C Bus)

The 128x32 SSD1306 OLED requires stable I2C communication. We are using the hardware I2C1 bus on the RP2040, which provides excellent stability for rendering raw bitmaps without performance drops:

  1. SDA (Data): Connected to GP6
  2. SCL (Clock): Connected to GP7
  3. VCC & GND: Connected to the board’s 5V/3.3V and Ground pins respectively.

3.Rotary Encoder Settings

The EC11 rotary encoder handles our precise rotary inputs and is connected natively to:

  1. Encoder Pin A: GP14
  2. Encoder Pin B: GP15

4.RGB LED (LED Expandability)

  1. My Setup (Internal + External Chain): Instead of leaving the internal LED uselessly hidden inside the case, I did a custom hardware hack. I traced the output line of the onboard smart LED on GP16 and carefully soldered an external addressable LED to it in series (Daisy-chain). This gives me both the onboard status light and an extra external glow off the exact same data pin!
  2. What you can do: If you don't want to perform this advanced soldering hack on the tiny onboard components, you can simply use GP16 for the single internal LED, or route a fresh wire to GP27 to run a completely separate external addressable LED strip. If you copy my dual-LED setup, just make sure to set your firmware count to RGBLIGHT_LED_COUNT 2.

QMK Firmware Initialization and Configuration

flowchat.png

Now that our hardware circuit is ready, it's time to configure the brain of our macropad using QMK Firmware. Inside your QMK directory (qmk_firmware/keyboards/), create a new folder named "apna_dost" (Change this to match your setup).

Inside this folder, we need to define our hardware features and layout mapping using two main configuration files.

1. Enabling Features (rules.mk)

Create a file named rules.mk. This file tells QMK exactly which hardware drivers to load.

RGBLIGHT_ENABLE = yes
WS2812_DRIVER = vendor
ENCODER_ENABLE = yes
EXTRAKEY_ENABLE = yes
OLED_ENABLE = yes
OLED_DRIVER = ssd1306
I2C_DRIVER = vendor # Uses native RP2040 hardware I2C drivers
LTO_ENABLE = yes # Optimizes firmware size

2. Defining the Layout Matrix (keyboard.json)

Next, create a file named keyboard.json. This file maps our physical 2x2 matrix keys, encoder pins, and hardware details so QMK can translate pin state changes into actual keystrokes

{
"keyboard_name": "apna_dost",
"manufacturer": "ApnaDost",
"processor": "RP2040",
"bootloader": "rp2040",
"diode_direction": "COL2ROW",
"matrix_pins": {
"rows": ["GP10", "GP11"],
"cols": ["GP12", "GP13"]
},
"encoder": {
"rotary": [
{
"pin_a": "GP14",
"pin_b": "GP15"
}
]
},
"layouts": {
"LAYOUT": {
"layout": [
{ "matrix": [0,0], "x": 0, "y": 0 },
{ "matrix": [0,1], "x": 1, "y": 0 },
{ "matrix": [1,0], "x": 0, "y": 1 },
{ "matrix": [1,1], "x": 1, "y": 1 }
]
}
},
"features": {
"extrakey": true,
"encoder": true,
"rgblight": true,
"oled": true
}
}

Writing the Core Logic

With the firmware initialized, we now need to write the actual logic that controls our dual-purpose encoder, locks our RGB brightness to safeguard the hardware

1. Configuration and Safety Limits (config.h)

Create a file named config.h in your project folder. In this file, we safely cap the brightness of our chained LEDs to 65% (165 out of 255) so that the RP2040's internal regulator stays perfectly cool. We also change the default Hue Step from 10 to 2 to unlock a massive variety of smooth color transitions.

#pragma once

// OLED Hardware Pins (GP6 = SDA, GP7 = SCL)
#define I2C_DRIVER I2CD1
#define I2C1_SDA_PIN GP6
#define I2C1_SCL_PIN GP7

// OLED display settings
#define OLED_DRIVER_ENABLE
#define OLED_MAX_COLUMNS 128
#define OLED_MAX_ROWS 32
#define OLED_TIMEOUT 30000 //Adjust according to you

// RGB and Encoder settings
#define WS2812_DI_PIN GP16
#define RGBLIGHT_SLEEP
#define RGBLIGHT_LED_COUNT 2
#define RGBLIGHT_LIMIT_VAL 165 // 65% Brightness Limit (255 * 0.65 = 165)
#define ENCODER_RESOLUTION 4

// Hue change
#define RGBLIGHT_HUE_STEP 2 // Default 10 make less to increase more

// USB settings
#define VENDOR_ID 0xFEED
#define PRODUCT_ID 0x6060
#define DEVICE_VER 0x0001
#define MANUFACTURER "ApnaDost"
#define PRODUCT "apna_dost"

#define RP2040_BOOTLOADER_DOUBLE_TAP_RESET
#define RP2040_BOOTLOADER_DOUBLE_TAP_RESET_TIMEOUT 500U

2. The Master Layout and Macros (keymap.c)

Next, create a file named keymap.c. This script maps our macro actions (like opening YouTube via Windows Run command) and contains the intelligent encoder logic. By default, the encoder fine-tunes the system volume, but when you press the encoder to mute the PC, it seamlessly shifts into a precise color wheel adjuster!

#include QMK_KEYBOARD_H
#include <stdio.h>

static uint8_t volume_level = 50;
static bool is_muted = false;

enum custom_keycodes {
YT = SAFE_RANGE
};

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
[0] = LAYOUT(
LALT(KC_TAB), // Key 1: App Switcher
YT, // Key 2: Custom YouTube Macro
LGUI(LSFT(KC_S)), // Key 3: Windows Screenshot Tool
KC_MUTE // Key 4: Encoder Tap (Mute/Unmute)
)
};

// Custom Macros Handling
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
if (!record->event.pressed) return true;

switch (keycode) {
case YT:
tap_code16(LGUI(KC_R));
wait_ms(200);
SEND_STRING("https://youtube.com");
tap_code(KC_ENTER);
return false;

case KC_MUTE:
is_muted = !is_muted;
return true;
}
return true;
}

// Intelligent Encoder Control
bool encoder_update_user(uint8_t index, bool clockwise) {
if (is_muted) {
// If system is muted, encoder adjusts the RGB color shades
if (clockwise) rgblight_increase_hue();
else rgblight_decrease_hue();
return false;
}

// Normal Mode: Adjusts Windows Master Volume
if (clockwise) {
tap_code(KC_VOLU);
if (volume_level <= 95) volume_level += 5;
} else {
tap_code(KC_VOLD);
if (volume_level >= 5) volume_level -= 5;
}
return false;
}

// OLED Bitmap Rendering
#ifdef OLED_ENABLE
oled_rotation_t oled_init_user(oled_rotation_t rotation) {
return OLED_ROTATION_0;
}

bool oled_task_user(void) {
oled_set_cursor(0, 0);

// Your custom 512-byte raw bitmap array (128x32px)
static const char image[] PROGMEM = {
// Paste your 512 bytes logo hex here......
};

oled_write_raw_P(image, sizeof(image));
return false;
}
#endif

Use this hex code to run some quick logo tests.

{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x04, 0x04, 0x04, 0x04, 0x1c, 0x0c, 0x0c, 0x04, 0x0c, 0xfc, 0xfc, 0x24, 0x04, 0x04, 0xfc, 0x7e,
0x1e, 0x04, 0x04, 0x04, 0x04, 0x04, 0xfc, 0xfc, 0x8c, 0xc4, 0x64, 0x24, 0xf4, 0xf4, 0x24, 0x04,
0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x1c, 0x0c, 0x0c, 0x04,
0x0c, 0xfc, 0xfc, 0x24, 0x04, 0x04, 0xfc, 0x7e, 0x1e, 0x06, 0x04, 0x04, 0x04, 0x04, 0xfc, 0xfc,
0x8c, 0xc4, 0x64, 0x24, 0xf4, 0xf4, 0x24, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x10, 0x38, 0x7c, 0xc8, 0x8c, 0x0c, 0x06, 0x06, 0x03, 0x01, 0x00, 0x00, 0xf0, 0xff, 0x0f, 0x00,
0x00, 0x38, 0x78, 0x4c, 0xe4, 0x7e, 0x3f, 0x01, 0x00, 0x00, 0xc0, 0xfc, 0x7f, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0c,
0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x38, 0x7c, 0xc8, 0x8c, 0x0c, 0x06, 0x06,
0x03, 0x01, 0x00, 0x00, 0xf0, 0xff, 0x1f, 0x00, 0x00, 0x38, 0x78, 0x4c, 0xe4, 0x7e, 0x3f, 0x01,
0x00, 0x00, 0xc0, 0xfc, 0x7f, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x07, 0x0e, 0x0c, 0x00, 0x00, 0x00, 0x0f, 0x1f, 0x03, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x07, 0x0e, 0x0c,
0x00, 0x00, 0x00, 0x0e, 0x1f, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0c, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00

};

Compiling & Flashing the Firmware

20260428_223052.jpg

With our configuration files (rules.mk, keyboard.json, config.h, and keymap.c) fully written and saved inside the qmk_firmware/keyboards/apna_dost/ directory, it is time to build the binary and flash it onto our microcontroller.

1. Setting Up the Compiler Environment

Open your QMK MSYS terminal (on Windows) or your native Terminal (on Linux/macOS) and run the setup command to ensure your environment is fully up to date:

Bash

qmk setup

2. Compiling the Project

To compile your custom source code into a flashable firmware file, execute the following command:

Bash

qmk compile -kb apna_dost -km default
  1. QMK will now parse your matrix pins, build the dynamic OLED drivers, apply the 65% hardware brightness cap, and pack everything into a high-performance binary file.
  2. Once successful, it will generate a file named apna_dost_default.uf2 inside your main QMK directory.

3. Flashing the RP2040-Zero MCU

  1. Unplug your macropad from your PC.
  2. Press and hold the physical BOOT button on the Waveshare RP2040-Zero controller.
  3. While holding the BOOT button, plug the USB Type-C cable back into your PC, then release the button.
  4. Your computer will instantly recognize the controller as a standard removable flash drive named RPI-RP2.
  5. Simply drag and drop (or copy-paste) the compiled apna_dost_default.uf2 file directly into that drive.

The drive will automatically disconnect and reboot in a fraction of a second. Your custom mechanical macro pad is now fully operational!

Final Testing and Working Demonstration

20260611_172308.jpg

Now that the firmware is flashed, your macropad is fully alive! In this final step, we will verify all functions and check the desktop integration.

What to Test:


1. Core Key Functions

  1. Key 1 — Toggle between open applications (Alt + Tab)
  2. Key 2 — Opens YouTube
  3. Key 3 — Launches the Windows Snipping Tool instantly
  4. Key 4 — Your custom function (whatever you assigned)

2. Rotary Encoder

  1. Rotate clockwise → Volume Up
  2. Rotate counter-clockwise → Volume Down
  3. Press the encoder switch → Mute / Unmute

3. RGB LED (Dual LED Hack)

  1. Tap the encoder button to mute your system audio. Now, rotate the encoder again; instead of changing volume, it will cycles seamlessly through incredibly fine color gradients at a perfectly safe, comfortable 65% brightness level.

The complete project (Gerber files, schematic, firmware, source code, and keymap) is fully open-source.

📂 GitHub Repository:

https://github.com/NP-Vishwakarma/apna_dost

Feel free to modify the keymap, add new layers, improve OLED animations, or redesign the PCB for your own needs.

If you face any issues or have questions regarding the matrix layout, soldering, or QMK configuration, drop a comment below. I'll be happy to help!

If you build your own version, I'd love to see it in the comments.

Made with ❤️ for productivity and learning.

Happy Building! ✌️