|
Wireless Keyboard Receiver in Practice: NRF24L01+ Dual-Mode Design, Low-Cost Custom Input Device

Wireless Keyboard Receiver in Practice: NRF24L01+ Dual-Mode Design, Low-Cost Custom Input Device

Want to make your own wireless keyboard receiver? Those wireless keyboards costing just tens of yuan on the market actually use NRF24L01+ modules at their core. Today we’ll disassemble this black box and build a dual-mode wireless keyboard receiver for less than 50 yuan - it can work as both a regular keyboard and a macro keyboard.

The inspiration for this project came from the mess of cables in my workshop. Every time I switch devices, I have to unplug and replug USB, which is annoying. So I decided to make my own, with customizable key mappings - it’s just awesome.

What Do You Need to Prepare?

ItemModel/SpecificationPrice
Main ControllerArduino Pro Micro (ATmega32U4)¥25
Wireless ModuleNRF24L01+ 2.4GHz¥8
Receiver ModuleNRF24L01+ (with PCB antenna)¥10
Key SwitchesMechanical keyboard switches ×6¥15
PCB Prototype Board5×7cm¥5
USB CableMicro USB¥5
Jumper WiresMale-to-male/Female-to-female¥5
Total¥73

Note: For the receiver, it’s recommended to buy the version with PCB antenna for more stable signal. The transmitter can use a regular antenna since the distance is short.

Step 1: Hardware Connection

Let’s first look at the transmitter (keyboard side) wiring. The SPI pins of ATmega32U4 are fixed, don’t connect them wrong:

NRF24L01+    →    Arduino Pro Micro
---------------------------------------
VCC          →    3.3V  (Never connect to 5V, it will burn!)
GND          →    GND
CE           →    D9
CSN          →    D10
SCK          →    D15
MOSI         →    D16
MISO         →    D14
IRQ          →    Not connected (optional)

⚠️ Pitfall Warning: NRF24L01+ is a 3.3V device. Although Pro Micro has 3.3V output, the current is limited. If the power supply is unstable, the module will repeatedly restart. It’s recommended to add a 10μF capacitor between VCC and GND.

The receiver wiring is similar, except the CE and CSN pins can be changed:

NRF24L01+    →    Arduino Pro Micro (Receiver)
--------------------------------------------
VCC          →    3.3V
GND          →    GND
CE           →    D8
CSN          →    D9
SCK          →    D15
MOSI         →    D16
MISO         →    D14

Step 2: Install Dependency Libraries

Open Arduino IDE and install the following libraries:

# Search and install via library manager, or use CLI
arduino-cli lib install "RF24"
arduino-cli lib install "HID-Project"

Or in the IDE:

  1. Click “Tools” → “Manage Libraries…” in the menu bar

  2. Search for RF24 in the library manager and click install

  3. Similarly search for and install the HID-Project library

Principle Explanation: The RF24 library handles NRF24L01+ wireless communication, while HID-Project makes Arduino emulate a USB keyboard. ATmega32U4 natively supports USB HID, which is why it’s more suitable for this project than Uno/Nano.

Step 3: Transmitter Code (Keyboard Side)

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <HID-Project.h>

// Pin definitions
#define CE_PIN  9
#define CSN_PIN 10

RF24 radio(CE_PIN, CSN_PIN);

// Communication pipe addresses
const uint64_t pipes[2] = {0xF0F0F0F0E7LL, 0xF0F0F0F0D2LL};

// Key mapping (customizable)
struct KeyMapping {
  uint8_t pin;
  HIDKeys key;
  bool lastState;
};

KeyMapping keys[] = {
  {2, KEY_F1, HIGH},   // F1 macro key
  {3, KEY_F2, HIGH},   // F2 macro key
  {4, KEY_F3, HIGH},   // F3 macro key
  {5, KEY_F4, HIGH},   // F4 macro key
  {6, KEY_F5, HIGH},   // F5 macro key
  {7, KEY_F6, HIGH},   // F6 macro key
};

void setup() {
  Serial.begin(115200);

  // Initialize key pins
  for (int i = 0; i < sizeof(keys)/sizeof(keys[0]); i++) {
    pinMode(keys[i].pin, INPUT_PULLUP);
  }

  // Initialize NRF24L01+
  radio.begin();
  radio.setPALevel(RF24_PA_HIGH);
  radio.setDataRate(RF24_2MBPS);
  radio.openWritingPipe(pipes[1]);
  radio.openReadingPipe(1, pipes[0]);
  radio.stopListening();

  Serial.println("Transmitter ready");
}

void loop() {
  for (int i = 0; i < sizeof(keys)/sizeof(keys[0]); i++) {
    bool currentState = digitalRead(keys[i].pin);
    
    if (currentState == LOW && keys[i].lastState == HIGH) {
      // Key pressed
      uint8_t packet[2] = {0x01, (uint8_t)keys[i].key};
      radio.write(&packet, sizeof(packet));
      Serial.print("Key pressed: ");
      Serial.println(keys[i].key);
      delay(50);  // Debounce
    }
    
    keys[i].lastState = currentState;
  }
}

Step 4: Receiver Code

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <HID-Project.h>

// Pin definitions
#define CE_PIN  8
#define CSN_PIN 9

RF24 radio(CE_PIN, CSN_PIN);

const uint64_t pipes[2] = {0xF0F0F0F0E7LL, 0xF0F0F0F0D2LL};

// Mode switching: 0=regular keyboard, 1=macro keyboard
uint8_t mode = 0;
#define MODE_SWITCH_PIN A0

void setup() {
  Serial.begin(115200);
  pinMode(MODE_SWITCH_PIN, INPUT_PULLUP);

  // Initialize NRF24L01+
  radio.begin();
  radio.setPALevel(RF24_PA_HIGH);
  radio.setDataRate(RF24_2MBPS);
  radio.openWritingPipe(pipes[1]);
  radio.openReadingPipe(1, pipes[0]);
  radio.startListening();

  // Initialize USB HID
  BootKeyboard.begin();

  Serial.println("Receiver ready, waiting for connection...");
}

void loop() {
  // Check mode switch
  if (digitalRead(MODE_SWITCH_PIN) == LOW) {
    mode = 1 - mode;
    delay(300);  // Debounce
    Serial.print("Switched to mode: ");
    Serial.println(mode ? "Macro Keyboard" : "Regular Keyboard");
  }

  // Receive wireless data
  if (radio.available()) {
    uint8_t packet[2];
    radio.read(&packet, sizeof(packet));

    if (packet[0] == 0x01) {
      // Key event
      HIDKeys key = (HIDKeys)packet[1];

      if (mode == 0) {
        // Regular mode: direct forwarding
        BootKeyboard.press(key);
        delay(50);
        BootKeyboard.release(key);
      } else {
        // Macro mode: execute preset macro
        executeMacro(key);
      }

      Serial.print("Received key: ");
      Serial.println(key);
    }
  }
}

// Macro definition example
void executeMacro(HIDKeys key) {
  switch (key) {
    case KEY_F1:
      // Ctrl+C copy
      BootKeyboard.press(KEY_LEFT_CTRL);
      BootKeyboard.press('c');
      delay(50);
      BootKeyboard.releaseAll();
      break;

    case KEY_F2:
      // Ctrl+V paste
      BootKeyboard.press(KEY_LEFT_CTRL);
      BootKeyboard.press('v');
      delay(50);
      BootKeyboard.releaseAll();
      break;

    case KEY_F3:
      // Alt+Tab switch window
      BootKeyboard.press(KEY_LEFT_ALT);
      BootKeyboard.press(KEY_TAB);
      delay(50);
      BootKeyboard.releaseAll();
      break;

    case KEY_F4:
      // Win+D show desktop
      BootKeyboard.press(KEY_LEFT_GUI);
      BootKeyboard.press('d');
      delay(50);
      BootKeyboard.releaseAll();
      break;

    default:
      BootKeyboard.press(key);
      delay(50);
      BootKeyboard.release(key);
  }
}

Step 5: Testing and Verification

After flashing the code, test according to the following steps:

  1. Connect the transmitter Pro Micro to the computer via USB, flash the transmitter code

  2. Connect the receiver Pro Micro to the computer, flash the receiver code

  3. Press the keys on the transmitter, observe if the receiver correctly outputs the corresponding keyboard events. I tested in my workshop, with the transmitter on the workbench and the receiver plugged into the HTPC in the living room. It worked stably even through two walls.

Common Problem Troubleshooting

Problem 1: Receiver doesn’t receive data

  • Cause: Pipe address mismatch or insufficient power supply

  • Solution: Check if the pipes[] array is consistent on both ends; use a multimeter to measure if VCC is stable at 3.3V; add a 10μF capacitor

Problem 2: Computer doesn’t recognize USB keyboard

  • Cause: ATmega32U4 firmware issue or poor quality USB cable

  • Solution: Try a USB cable that can transmit data (some can only charge); reflash the Bootloader

Problem 3: Noticeable key delay

  • Cause: Wireless rate set too low or debounce time too long

  • Solution: Change setDataRate to RF24_2MBPS; reduce debounce time from 50ms to 20ms

Problem 4: Macro keys execute multiple times

  • Cause: Key debounce logic issue

  • Solution: Ensure lastState state machine is updated correctly; add 100ms delay after macro execution

Advanced Usage

This project is just a beginning. You can:

  1. Add a small OLED screen to display current mode, battery level, and other information

  2. Add a lithium battery and charging module to make a truly wireless portable version

  3. Use ESP32-C3 to replace Pro Micro, supporting WiFi/Bluetooth dual-mode communication

  4. Add a rotary encoder to implement knob control (adjusting volume, scrolling pages, etc.)

  5. Design a custom PCB enclosure and use 3D printing to create a complete keyboard form factor

Summary

With a cost of 70 yuan, we made a dual-mode wireless keyboard receiver. The core is the combination of NRF24L01+ module + ATmega32U4. The greatest value of this project is not saving money, but being completely controllable - you can add whatever features you want without having to rely on manufacturers.

I’m already considering the next generation design: using ESP32-C3 as the main controller, supporting WiFi and Bluetooth dual-mode, and able to update firmware via OTA. I’ll share it with everyone then.

Hope this blog post is helpful to you!


Related Resources: