嵌入式开发 无线键盘接收器实战: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 里:
-
点击菜单栏「工具」→「管理库…」
-
在库管理器中搜索
RF24,点击安装 -
同样搜索并安装
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:测试验证
烧录代码后,按以下步骤测试:
-
将发射端 Pro Micro 通过 USB 连接电脑,烧录发射端代码
-
将接收端 Pro Micro 连接电脑,烧录接收端代码
-
按下发射端的按键,观察接收端是否正常输出对应的键盘事件 我在工作室测试,发射端放在工作台上,接收端插在客厅的 HTPC 上,隔了两堵墙还能稳定使用。
常见问题排查
问题 1: 接收端收不到数据
-
原因: 管道地址不匹配或供电不足
-
解决: 检查
pipes[]数组在两端是否一致;用万用表测 VCC 是否稳定在 3.3V;加 10μF 电容
问题 2: 电脑识别不到 USB 键盘
-
原因: ATmega32U4 固件问题或 USB 线质量问题
-
解决: 换一根能传数据的 USB 线(有些只能充电);重新烧录 Bootloader
问题 3: 按键延迟明显
-
原因: 无线速率设置过低或消抖时间太长
-
解决: 将
setDataRate改为RF24_2MBPS;消抖时间从 50ms 降到 20ms
问题 4: 宏按键执行多次
-
原因: 按键消抖逻辑有问题
-
解决: 确保
lastState状态机正确更新;在宏执行后加 100ms 延时
进阶玩法
这个项目只是个开始,你可以:
-
增加 OLED 小屏幕,显示当前模式、电量等信息
-
加入锂电池和充电模块,做成真正无线的可移植版本
-
使用 ESP32-C3 替换 Pro Micro,支持 WiFi/蓝牙双模通信
-
增加旋转编码器,实现旋钮控制(调音量、滚动页面等)
-
设计定制 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) |
|---|---|---|
| 持续工作 | 15mA | 15小时 |
| 每秒唤醒 | 0.1mA | 100天 |
| 按键唤醒 | 0.01mA | 3年 |
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));
}
总结
通过这个项目,我们成功制作了一个功能完整的无线键盘接收器。核心要点:
- 硬件选择:NRF24L01+ 模块 + ATmega32U4 是性价比最高的组合
- 软件开发:使用 RF24 库和 HID-Project 库简化开发
- 性能优化:通过低功耗设计和数据加密提升产品品质
- 故障排查:建立系统化的问题诊断流程
- 扩展应用:可以扩展到鼠标、游戏手柄等外设
成本分析:
- NRF24L01+ 模块:15元
- ATmega32U4 开发板:35元
- 其他元件:20元
- 总计:70元
相比市售的无线键盘接收器(100-200元),我们节省了 30-65% 的成本,而且获得了完全的可控性和可定制性。
下一步计划:
- 升级到 ESP32-C3,支持 WiFi 和蓝牙
- 添加 OTA 固件更新功能
- 开发配套的配置软件
- 实现多设备同时连接
希望这个项目能激发你的创造力,动手制作属于自己的无线外设!
相关资源: