无线键盘接收器实战:NRF24L01+ 双模设计,低成本自定义输入设备

无线键盘接收器实战:NRF24L01+ 双模设计,低成本自定义输入设备

想不想自己做一个无线键盘接收器?市面上那些几十块的无线键盘,其实核心就是 NRF24L01+ 模块。今天我们来拆解这个黑盒子,用不到 50 元的成本,做一个支持双模切换的无线键盘接收器——既能当普通键盘用,也能当宏键盘使。

这个项目的灵感来自于我工作室里那堆乱七八糟的线缆。每次切换设备都要拔插 USB,烦得要死。索性自己做一个,还能自定义按键映射,简直不要太爽。

需要准备什么?

物品型号/规格价格
主控板Arduino Pro Micro (ATmega32U4)¥25
无线模块NRF24L01+ 2.4GHz¥8
接收端模块NRF24L01+ (带 PCB 天线)¥10
按键开关机械键盘轴体×6¥15
PCB 洞洞板5×7cm¥5
USB 数据线Micro USB¥5
杜邦线公对公/母对母¥5
*总计*¥73

注意: 接收端建议买带 PCB 天线的版本,信号更稳定。发射端用普通天线就行,反正离得近。

步骤 1:硬件连接

先来看发射端(键盘侧)的接线。ATmega32U4 的 SPI 引脚是固定的,别接错了:

NRF24L01+    →    Arduino Pro Micro
---------------------------------------
VCC          →    3.3V  (千万别接 5V,会烧!)
GND          →    GND
CE           →    D9
CSN          →    D10
SCK          →    D15
MOSI         →    D16
MISO         →    D14
IRQ          →    不接(可选)

⚠️ 踩坑提醒: NRF24L01+ 是 3.3V 器件,Pro Micro 虽然有 3.3V 输出,但电流有限。如果供电不稳,模块会反复重启。建议加个 10μF 电容在 VCC 和 GND 之间。

接收端接线类似,只是 CE 和 CSN 引脚可以换:

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

步骤 2:安装依赖库

打开 Arduino IDE,安装以下库:

# 通过库管理器搜索安装,或用 CLI
arduino-cli lib install "RF24"
arduino-cli lib install "HID-Project"

或者在 IDE 里:

  1. 点击菜单栏「工具」→「管理库…」

  2. 在库管理器中搜索 RF24,点击安装

  3. 同样搜索并安装 HID-Project

原理解析: RF24 库负责 NRF24L01+ 的无线通信,HID-Project 让 Arduino 模拟成 USB 键盘。ATmega32U4 原生支持 USB HID,这是它比 Uno/Nano 更适合这个项目的原因。

步骤 3:发射端代码(键盘侧)

#include 
#include 
#include 
#include 

// 引脚定义
#define CE_PIN  9
#define CSN_PIN 10

RF24 radio(CE_PIN, CSN_PIN);

// 通信管道地址
const uint64_t pipes[2] = {0xF0F0F0F0E7LL, 0xF0F0F0F0D2LL};

// 按键映射(自定义)
struct KeyMapping {
  uint8_t pin;
  HIDKeys key;
  bool lastState;
};

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

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

  // 初始化按键引脚
  for (int i = 0; i 
#include 
#include 
#include 

// 引脚定义
#define CE_PIN  8
#define CSN_PIN 9

RF24 radio(CE_PIN, CSN_PIN);

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

// 模式切换:0=普通键盘,1=宏键盘
uint8_t mode = 0;
#define MODE_SWITCH_PIN A0

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

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

  // 初始化 USB HID
  BootKeyboard.begin();

  Serial.println("接收端就绪,等待连接...");
}

void loop() {
  // 检查模式切换
  if (digitalRead(MODE_SWITCH_PIN) == LOW) {
    mode = 1 - mode;
    delay(300);  // 防抖
    Serial.print("切换到模式:");
    Serial.println(mode ? "宏键盘" : "普通键盘");
  }

  // 接收无线数据
  if (radio.available()) {
    uint8_t packet[2];
    radio.read(&packet, sizeof(packet));

    if (packet[0] == 0x01) {
      // 按键事件
      HIDKeys key = (HIDKeys)packet[1];

      if (mode == 0) {
        // 普通模式:直接转发
        BootKeyboard.press(key);
        delay(50);
        BootKeyboard.release(key);
      } else {
        // 宏模式:执行预设宏
        executeMacro(key);
      }

      Serial.print("收到按键:");
      Serial.println(key);
    }
  }
}

// 宏定义示例
void executeMacro(HIDKeys key) {
  switch (key) {
    case KEY_F1:
      // Ctrl+C 复制
      BootKeyboard.press(KEY_LEFT_CTRL);
      BootKeyboard.press('c');
      delay(50);
      BootKeyboard.releaseAll();
      break;

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

    case KEY_F3:
      // Alt+Tab 切换窗口
      BootKeyboard.press(KEY_LEFT_ALT);
      BootKeyboard.press(KEY_TAB);
      delay(50);
      BootKeyboard.releaseAll();
      break;

    case KEY_F4:
      // Win+D 显示桌面
      BootKeyboard.press(KEY_LEFT_GUI);
      BootKeyboard.press('d');
      delay(50);
      BootKeyboard.releaseAll();
      break;

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

步骤 5:测试验证

烧录代码后,按以下步骤测试:

  1. 将发射端 Pro Micro 通过 USB 连接电脑,烧录发射端代码

  2. 将接收端 Pro Micro 连接电脑,烧录接收端代码

  3. 按下发射端的按键,观察接收端是否正常输出对应的键盘事件 我在工作室测试,发射端放在工作台上,接收端插在客厅的 HTPC 上,隔了两堵墙还能稳定使用。

常见问题排查

问题 1: 接收端收不到数据

  • 原因: 管道地址不匹配或供电不足

  • 解决: 检查 pipes[] 数组在两端是否一致;用万用表测 VCC 是否稳定在 3.3V;加 10μF 电容

问题 2: 电脑识别不到 USB 键盘

  • 原因: ATmega32U4 固件问题或 USB 线质量问题

  • 解决: 换一根能传数据的 USB 线(有些只能充电);重新烧录 Bootloader

问题 3: 按键延迟明显

  • 原因: 无线速率设置过低或消抖时间太长

  • 解决:setDataRate 改为 RF24_2MBPS;消抖时间从 50ms 降到 20ms

问题 4: 宏按键执行多次

  • 原因: 按键消抖逻辑有问题

  • 解决: 确保 lastState 状态机正确更新;在宏执行后加 100ms 延时

进阶玩法

这个项目只是个开始,你可以:

  1. 增加 OLED 小屏幕,显示当前模式、电量等信息

  2. 加入锂电池和充电模块,做成真正无线的可移植版本

  3. 使用 ESP32-C3 替换 Pro Micro,支持 WiFi/蓝牙双模通信

  4. 增加旋转编码器,实现旋钮控制(调音量、滚动页面等)

  5. 设计定制 PCB 外壳,用 3D 打印做成完整键盘形态

总结

用 70 块钱的成本,我们做了一个支持双模切换的无线键盘接收器。核心就是 NRF24L01+ 模块 + ATmega32U4 的组合。这个项目最大的价值不在于省钱,而在于完全可控——你想加什么功能就加什么功能,不用看厂商脸色。

我已经在考虑下一代设计了:用 ESP32-C3 做主控,支持 WiFi 和蓝牙双模,还能通过 OTA 更新固件。到时候再和大家分享。

希望这篇博客文章对您有所帮助!


相关资源:


NRF24L01 高级应用

1. 多通道通信

实现多个键盘同时连接到一个接收器:

// 多通道接收器代码
#include <RF24.h>
#include <HID-Project.h>

RF24 radio(7, 8);  // CE, CSN

// 定义多个键盘的管道地址
const uint64_t keyboard_pipes[] = {
    0xF0F0F0F0E1LL,  // 键盘1
    0xF0F0F0F0E2LL,  // 键盘2
    0xF0F0F0F0E3LL   // 键盘3
};

struct KeyboardData {
    uint8_t device_id;
    uint8_t modifier;
    uint8_t key;
    bool pressed;
};

void setup() {
    Serial.begin(115200);
    radio.begin();
    radio.setChannel(100);
    radio.setDataRate(RF24_2MBPS);
    radio.setPALevel(RF24_PA_LOW);
    
    Keyboard.begin();
}

void loop() {
    // 轮询所有管道
    for (int i = 0; i < 3; i++) {
        radio.openReadingPipe(1, keyboard_pipes[i]);
        radio.startListening();
        
        if (radio.available()) {
            KeyboardData data;
            radio.read(&data, sizeof(data));
            
            // 处理按键
            if (data.pressed) {
                if (data.modifier) {
                    Keyboard.press(data.modifier);
                }
                Keyboard.press(data.key);
            } else {
                Keyboard.release(data.key);
                if (data.modifier) {
                    Keyboard.release(data.modifier);
                }
            }
        }
    }
}

2. 低功耗设计

优化发射端功耗:

// 低功耗键盘发射端
#include <RF24.h>
#include <avr/sleep.h>

RF24 radio(9, 10);
const int interrupt_pin = 2;  // 按键中断引脚

volatile bool key_pressed = false;

void wake_up() {
    key_pressed = true;
}

void setup() {
    pinMode(interrupt_pin, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(interrupt_pin), wake_up, FALLING);
    
    radio.begin();
    radio.setChannel(100);
    radio.setDataRate(RF24_2MBPS);
    radio.setPALevel(RF24_PA_MIN);  // 最小功率
    radio.setAutoAck(true);
    
    // 关闭不必要的模块
    radio.powerDown();
}

void loop() {
    if (key_pressed) {
        key_pressed = false;
        
        // 唤醒射频模块
        radio.powerUp();
        delay(2);  // 等待稳定
        
        // 发送按键数据
        uint8_t key = read_keypad();
        radio.openWritingPipe(0xF0F0F0F0E1LL);
        radio.write(&key, sizeof(key));
        
        // 关闭射频模块
        radio.powerDown();
    }
    
    // 进入睡眠模式
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    sleep_enable();
    sleep_mode();
    sleep_disable();
}

功耗对比

模式平均电流电池寿命(CR2032)
持续工作15mA15小时
每秒唤醒0.1mA100天
按键唤醒0.01mA3年

3. 数据加密

实现简单的数据加密:

// XOR 加密
void encrypt_data(uint8_t *data, int len, uint8_t key) {
    for (int i = 0; i < len; i++) {
        data[i] ^= key;
    }
}

// 发送加密数据
void send_encrypted_key(uint8_t key_code) {
    uint8_t encrypted = key_code ^ 0xA5;  // 简单 XOR 加密
    radio.write(&encrypted, sizeof(encrypted));
}

// 接收解密数据
void receive_and_decrypt() {
    uint8_t encrypted;
    radio.read(&encrypted, sizeof(encrypted));
    uint8_t decrypted = encrypted ^ 0xA5;
    
    // 处理解密后的按键
    Keyboard.press(decrypted);
}

4. 自动重连机制

实现断线自动重连:

unsigned long last_receive_time = 0;
const unsigned long TIMEOUT = 5000;  // 5秒超时

void check_connection() {
    if (millis() - last_receive_time > TIMEOUT) {
        // 连接断开,尝试重连
        Serial.println("Connection lost, reconnecting...");
        
        // 重新初始化射频模块
        radio.begin();
        radio.setChannel(100);
        radio.setDataRate(RF24_2MBPS);
        
        // 等待连接恢复
        while (!radio.available()) {
            delay(100);
        }
        
        Serial.println("Reconnected!");
        last_receive_time = millis();
    }
}

void loop() {
    if (radio.available()) {
        last_receive_time = millis();
        // 处理数据...
    }
    
    check_connection();
}

5. OTA 固件更新

通过无线方式更新固件:

// OTA 更新接收器
#include <RF24.h>
#include <avr/boot.h>

RF24 radio(7, 8);

#define FIRMWARE_PACKET_SIZE 32
#define FIRMWARE_START_ADDR 0x7000  // Bootloader 区域

void handle_firmware_update() {
    uint8_t packet[FIRMWARE_PACKET_SIZE];
    uint16_t address = FIRMWARE_START_ADDR;
    
    while (true) {
        if (radio.available()) {
            radio.read(packet, sizeof(packet));
            
            // 检查是否是结束包
            if (packet[0] == 0xFF && packet[1] == 0xFF) {
                // 固件更新完成,重启
                asm volatile ("jmp 0x0000");
            }
            
            // 写入 Flash
            boot_page_fill(address, *(uint16_t*)&packet[2]);
            address += 2;
            
            // 每页写满后写入
            if ((address % SPM_PAGESIZE) == 0) {
                boot_page_write(address - SPM_PAGESIZE);
                boot_spm_busy_wait();
            }
        }
    }
}

故障排查指南

常见问题及解决方案

问题1:无法接收到数据

可能原因:

  • 管道地址不匹配
  • 信道设置不一致
  • 数据速率不同
  • 距离过远

解决方案:

// 检查配置
Serial.print("Channel: ");
Serial.println(radio.getChannel());

Serial.print("Data rate: ");
Serial.println(radio.getDataRate());

// 测试连接
if (radio.write(&test_data, sizeof(test_data))) {
    Serial.println("Transmission successful");
} else {
    Serial.println("Transmission failed");
}

问题2:按键响应延迟

优化方法:

// 1. 减少延迟
radio.setRetries(5, 10);  // 减少重试次数

// 2. 提高数据速率
radio.setDataRate(RF24_2MBPS);

// 3. 优化轮询间隔
#define POLL_INTERVAL 5  // 5ms 轮询一次

问题3:按键重复或丢失

解决方案:

// 添加按键去抖
unsigned long last_key_time = 0;
const unsigned long DEBOUNCE_TIME = 50;

void handle_key(uint8_t key) {
    if (millis() - last_key_time > DEBOUNCE_TIME) {
        process_key(key);
        last_key_time = millis();
    }
}

问题4:功耗过高

优化策略:

// 1. 使用睡眠模式
set_sleep_mode(SLEEP_MODE_PWR_DOWN);

// 2. 关闭不必要的模块
radio.powerDown();
ADC->CTRLA &= ~ADC_ENABLE_bm;

// 3. 降低射频功率
radio.setPALevel(RF24_PA_MIN);

扩展项目

项目1:无线鼠标

使用相同的 NRF24L01 模块制作无线鼠标:

// 鼠标数据格式
struct MouseData {
    int8_t dx;      // X轴移动
    int8_t dy;      // Y轴移动
    uint8_t buttons; // 按键状态
};

// 使用 ADXL345 加速度计
#include <Wire.h>
#include <ADXL345.h>

ADXL345 adxl;

void read_mouse_movement() {
    double x, y, z;
    adxl.readAccel(&x, &y, &z);
    
    MouseData data;
    data.dx = (int8_t)(x * 10);
    data.dy = (int8_t)(y * 10);
    data.buttons = read_buttons();
    
    radio.write(&data, sizeof(data));
}

项目2:多媒体控制键盘

添加多媒体控制功能:

// 多媒体按键
#define KEY_PLAY_PAUSE  0xB3
#define KEY_NEXT        0xB5
#define KEY_PREVIOUS    0xB6
#define KEY_VOLUME_UP   0x80
#define KEY_VOLUME_DOWN 0x81

void handle_multimedia_key(uint8_t key) {
    switch(key) {
        case 0x01:
            Consumer.write(MEDIA_PLAY_PAUSE);
            break;
        case 0x02:
            Consumer.write(MEDIA_NEXT);
            break;
        case 0x03:
            Consumer.write(MEDIA_VOLUME_UP);
            break;
        // ...
    }
}

项目3:游戏手柄

制作无线游戏手柄:

// 游戏手柄数据
struct GamepadData {
    int16_t left_x;
    int16_t left_y;
    int16_t right_x;
    int16_t right_y;
    uint16_t buttons;
    uint8_t triggers[2];
};

// 使用摇杆和按钮
void read_gamepad() {
    GamepadData data;
    
    data.left_x = analogRead(A0) - 512;
    data.left_y = analogRead(A1) - 512;
    data.right_x = analogRead(A2) - 512;
    data.right_y = analogRead(A3) - 512;
    
    data.buttons = read_buttons();
    data.triggers[0] = analogRead(A4) >> 2;
    data.triggers[1] = analogRead(A5) >> 2;
    
    radio.write(&data, sizeof(data));
}

总结

通过这个项目,我们成功制作了一个功能完整的无线键盘接收器。核心要点:

  1. 硬件选择:NRF24L01+ 模块 + ATmega32U4 是性价比最高的组合
  2. 软件开发:使用 RF24 库和 HID-Project 库简化开发
  3. 性能优化:通过低功耗设计和数据加密提升产品品质
  4. 故障排查:建立系统化的问题诊断流程
  5. 扩展应用:可以扩展到鼠标、游戏手柄等外设

成本分析

  • NRF24L01+ 模块:15元
  • ATmega32U4 开发板:35元
  • 其他元件:20元
  • 总计:70元

相比市售的无线键盘接收器(100-200元),我们节省了 30-65% 的成本,而且获得了完全的可控性和可定制性。

下一步计划

  • 升级到 ESP32-C3,支持 WiFi 和蓝牙
  • 添加 OTA 固件更新功能
  • 开发配套的配置软件
  • 实现多设备同时连接

希望这个项目能激发你的创造力,动手制作属于自己的无线外设!


相关资源: