每个男孩心里大概都住着一个操控坦克、机甲或赛车的梦。但打开购物软件,看到动辄上千元的航模遥控器,再摸摸钱包,“指挥官”的距离是不是有点远?
如果换一种思路:用一块 ESP32-S2,加上几个摇杆和按键,自己就能攒出一个支持 16 个通道、延迟低于 10ms、空旷地通信距离可达 100 米的 开源遥控器——而且所有原理图和代码都开源——你会不会觉得,这个梦其实伸手就能够到?
那么,这个 16 通道的“坦克指挥官”是如何从原理图变成实物的?我们一起来拆解。
完整项目资料可访问: https://www.eetree.cn/project/detail/5482
01 项目介绍
本项目设计并实现了一款基于 ESP32-S2-WROVER-N4R2 模块的 RC 遥控器,专用于坦克模型或其它多通道遥控设备。该遥控器支持最多 16 个控制通道,采用 ESP-NOW 无线通信协议,具备低延迟、高响应、多输入源等特点。硬件上集成了摇杆、电位器、按键等多种输入方式,并预留了 SPI/I2C 扩展接口,便于后续功能升级。项目基于 PlatformIO 平台开发,代码结构清晰,易于维护与二次开发。
1.1 效果展示
完整操作视频可参考项目页面,实际硬件与数据效果见 3.2 节实测部分。
1.2 硬件介绍
本遥控器硬件核心为 ESP32-S2-WROVER-N4R2 模组,主要外围电路包括:
- 2 路摇杆输入:每路摇杆提供 X、Y 两个模拟轴,共实现 4 通道模拟输入。
- 2 路电位器输入:独立线性电位器,用于调节额外模拟量(如油门微调、舵量比例)。
- 2 路摇杆按键:按压摇杆触发,可作为数字开关。
- 8 路独立按键:用于模式切换、功能触发等。
- 供电系统:默认支持 1S 锂电池(3.7V~4.2V);若更换 LDO 芯片(如从 3.3V LDO 换为宽压型),可支持 2S 电池(7.4V)。
- 预留接口:
- SPI 2.4G 模拟接口(可外接 NRF24L01 等)
- SPI 显示模块接口
- I2C OLED 接口(如 SSD1306)
1.3 功能概览
| 功能项 |
说明 |
| 通道数 |
最大支持 16 通道数据发送,当前硬件提供 6 个模拟通道 + 10 个数字通道(可配置为开关通道) |
| 无线协议 |
ESP-NOW(点对点或广播,低功耗、低延迟) |
| 输入设备 |
2×摇杆(4 轴)、2×电位器、2×摇杆按键、8×独立按键 |
| 状态指示 |
(可选)通过预留 I2C 接口连接 OLED 显示通道值及连接状态 |
| 开发平台 |
PlatformIO + Arduino 框架 |
| 供电方式 |
1S 锂电池(未集成则外部充电) |
1.4 设计思路
- 模块化输入采集:每个输入设备(摇杆、电位器、按键)独立读取,软件中统一转换为 0~255 或 0~100% 的标准化数值。
- 通道映射:物理输入可灵活映射到 16 个逻辑通道中,例如摇杆左 X→通道 1,左 Y→通道 2,电位器 1→通道 5 等。
- 低延迟通信:ESP-NOW 无需连接路由器,直接 MAC 层通信,延迟 <10ms,适合实时遥控。
- 扩展性:预留 SPI/I2C 接口,未来可增加显示屏、2.4G 备份链路、舵机反馈等。
02 功能实现
2.1 硬件设计
硬件框图:

2.2 软件流程图

2.3 软件代码实现
以下为核心代码片段(基于 PlatformIO + Arduino 框架)。
2.3.1 数据结构定义
定义控制数据包结构体(与接收端约定一致):
structData_Package {
uint8_t leftJoystickX; // 左摇杆 X
uint8_t leftJoystickY; // 左摇杆 Y
uint8_t rightJoystickX; // 右摇杆 X
uint8_t rightJoystickY; // 右摇杆 Y
uint8_t leftJoystickBtn:1; // 左摇杆按键
uint8_t rightJoystickBtn:1; // 右摇杆按键
uint8_t leftSwitch1:1; // 左按键开关1
uint8_t leftSwitch2:1; // 左按键开关2 和 左按键开关1 反相
uint8_t rightSwitch1:1; // 右按键开关1
uint8_t rightSwitch2:1; // 右按键开关2 和 右按键开关1 反相
uint8_t leftPotentiometer; // 左电位器
uint8_t rightPotentiometer; // 右电位器
};
2.3.2 初始化 ESP-NOW
// 初始化 ESP-NOW
Serial.println("Initializing ESP-NOW...");
delay(100);
WiFi.mode(WIFI_STA);
Serial.println("InitESPNow");
// Init ESPNow with a fallback logic
InitESPNow();
// 设置发送数据回调函数
esp_now_register_send_cb(OnDataSent);
2.3.3 模拟输入读取(ADC)
int potValues[NUMBER_OF_POT_SAMPLES];
int potValues2[NUMBER_OF_POT_SAMPLES];
int potValues3[NUMBER_OF_POT_SAMPLES];
int potValues4[NUMBER_OF_POT_SAMPLES];
// 连续采样 NUMBER_OF_POT_SAMPLES 个数据,间隔 DELAY_BETWEEN_SAMPLES 毫秒。这里不应该这样做,虽然没有什么大问题,但会使的采样慢,导致整个运行效率过低 TODO 待优化
for (int i = 0 ; i < NUMBER_OF_POT_SAMPLES ; i++) {
potValues[i] = analogRead(LEFT_JOYSTICK_X_PIN);
potValues2[i] = analogRead(LEFT_JOYSTICK_Y_PIN);
potValues3[i] = analogRead(RIGHT_JOYSTICK_X_PIN);
potValues4[i] = analogRead(RIGHT_JOYSTICK_Y_PIN);
delay(DELAY_BETWEEN_SAMPLES);
}
// 计算 左 摇杆 X 轴采样均值。
int potValue = 0;
for (int i = 0 ; i < NUMBER_OF_POT_SAMPLES ; i++) {
potValue += potValues[i];
}
potValue = potValue / NUMBER_OF_POT_SAMPLES;
LX_read = map(potValue, 0, 4095, 0, 255);
// 计算 左 摇杆 Y 轴采样均值。
int potValue2 = 0;
for (int i = 0 ; i < NUMBER_OF_POT_SAMPLES ; i++) {
potValue2 += potValues2[i];
}
potValue2 = potValue2 / NUMBER_OF_POT_SAMPLES;
LY_read = map(potValue2, 0, 4095, 255, 0);
// 计算 右 摇杆 X 轴采样均值。
int potValue3 = 0;
for (int i = 0 ; i < NUMBER_OF_POT_SAMPLES ; i++) {
potValue3 += potValues3[i];
}
potValue3 = potValue3 / NUMBER_OF_POT_SAMPLES;
RX_read = map(potValue3, 0, 4095, 255, 0);
// 计算 右 摇杆 Y 轴采样均值。
int potValue4 = 0;
for (int i = 0 ; i < NUMBER_OF_POT_SAMPLES ; i++) {
potValue4 += potValues4[i];
}
potValue4 = potValue4 / NUMBER_OF_POT_SAMPLES;
RY_read = map(potValue4, 0, 4095, 255, 0);
2.3.4 通道映射与发送
data.leftJoystickX = LX_to_send;
data.leftJoystickY = LY_to_send;
data.rightJoystickX = RX_to_send;
data.rightJoystickY = RY_to_send;
data.leftJoystickBtn = !digitalRead(LEFT_JOYSTICK_BUTTON_PIN);
data.rightJoystickBtn = !digitalRead(RIGHT_JOYSTICK_BUTTON_PIN);
data.upBtnLeft = !digitalRead(LEFT_UP_BUTTON_PIN);
data.downBtnLeft = !digitalRead(LEFT_DOWN_BUTTON_PIN);
data.leftBtnLeft = !digitalRead(LEFT_LEFT_BUTTON_PIN);
data.rightBtnLeft = !digitalRead(LEFT_RIGHT_BUTTON_PIN);
data.upBtnRight = !digitalRead(RIGHT_UP_BUTTON_PIN);
data.downBtnRight = !digitalRead(RIGHT_DOWN_BUTTON_PIN);
data.leftBtnRight = !digitalRead(RIGHT_LEFT_BUTTON_PIN);
data.rightBtnRight = !digitalRead(RIGHT_RIGHT_BUTTON_PIN);
data.leftPotentiometer = 255 - map(lp, 120, 4095, 0, 255);
data.rightPotentiometer = map(rp, 120, 4095, 0, 255);
// 发送数据 send data
voidsendData(){ //uint8_t data
esp_now_send(broadcastAddress, (uint8_t *) &data, sizeof(data));
}
03 功能展示
3.1 硬件展示
原理图:

PCB 3D 预览:

PCB 实物:

3.2 实测效果
- 模拟通道响应:摇杆和电位器线性度良好,ADC 12 位分辨率,经映射后输出 0~255 连续变化。
- 数字按键:8 个独立按键及摇杆按键无抖动误触发(软件已做简单去抖)。
- 无线通信:ESP-NOW 在开阔地有效距离约 100 米,数据丢包率低于 1%(每 1000 包重传小于 10 次)。
- 通道数验证:通过串口监视器打印发送的 16 个通道值,全部可正确接收并解析。
测试环境使用一块 Seeed Studio XIAO ESP32C3 开发板作为接收机,接收遥控器发送的 ESP-NOW 数据包并通过串口发送至上位机,上位机以图形方式展示各通道的数值。

总结
本项目成功基于 ESP32-S2 设计并实现了一款功能完备、扩展性强的 RC 遥控器。6 个模拟输入 + 10 个数字输入,满足大多数坦克、机器人、航模控制需求。ESP-NOW 协议在保证低延迟的同时,具备较好的抗干扰能力。通道映射、数据比例均可通过修改代码灵活调整,便于适配不同接收机。
这个项目的价值,不只是一块能用的 PCB。它展示了如何用 ESP32-S2 这颗高性价比芯片,配合 ESP-NOW 协议,搭建一套低延迟、多通道、扩展性强的遥控系统。它既可以是一把坦克遥控器,也可以是机械臂控制器、机器人主控、甚至自定义输入设备。
当你在串口监视器里看到自己推动摇杆时数据流畅地跳动,当你按下按键模型真的动起来时,你会明白:从零到一搭建控制链路的感觉,远胜于买一台成品。
如果你对这种开源实战项目感兴趣,也欢迎来云栈社区继续交流。