FireBeetle 2 ESP32-C5基于ESPNOW的无线通信
本帖最后由 豆爸 于 2025-10-3 08:26 编辑一、简介
本项目基于 ESP32 开发板,通过 ESP-NOW 无线通信协议构建了一套 “摇杆指令发送 - 接收 - 显示” 系统。发送端利用 ADC 模块采集摇杆的 X、Y 轴模拟信号,转化为 “上 / 下 / 左 / 右 / 停止”5 种控制指令;接收端通过 ESP-NOW 接收指令后,在 LCD 屏幕上实时显示指令内容,同时在串口输出 sender MAC 地址与指令信息,实现了低延迟、近距离的无线指令交互。该系统展示了低功耗无线通信技术在物联网设备控制中的应用,适用于遥控小车、智能家居控制等多种场景。
二、硬件
1、硬件清单
硬件名称数量功能说明链接
FireBeetle 2 ESP32-C5 开发板1系统核心控制板,支持 ESP-NOW 通信-
Gravity: JoyStick 摇杆1输入设备,提供 X/Y 轴模拟控制信号https://www.dfrobot.com.cn/goods-117.html
Gravity: I2C OLED-2864 显示屏1显示设备,支持 I2C 通信的 OLED 屏幕https://www.dfrobot.com.cn/goods-1374.html
ESP32编程掌机(ST7735 LCD 显示屏)1显示设备,160×128 分辨率 ST7735 驱动 LCD-
Type-C&Micro二合一USB线1用于程序下载与供电https://www.dfrobot.com.cn/goods-2843.html
2、硬件简介
(1)FireBeetle 2 ESP32-C5 开发板
FireBeetle 2 ESP32-C5 是一款搭载乐鑫 ESP32-C5 模组的低功耗 IoT 开发板,面向智能家居和广泛物联网场景,集高性能计算、多协议支持与智能电源管理于一体,为各种部署需求提供高可靠性、高灵活性与长续航的解决方案。
当前试用赠送的Firebeetle 2 ESP32-C5开发板板载ESP32-C5模组为ECO1 版本,ECO1版本的ESP32-C5模组是基于 ESP32-C5 revision v0.1 版本芯片。
(2)Gravity: JoyStick 摇杆
DFRobot的JoyStick摇杆采用原装优质金属PS2摇杆电位器制作,具有(X,Y)2轴模拟输出,(Z)1路按钮数字输出。
(3)Gravity: I2C OLED-2864 显示屏
Gravity OLED-2864 显示屏是一款无需背景光源,自发光式的显示模块。模块采用蓝色背景,显示尺寸控制在0.96英寸,采用OLED专用驱动芯片SSD1306控制。模块采用Gravity I2C通用接口。
(4)ESP32编程掌机
ESP32编程掌机搭载了EESP32-D0WD MCU,并配备1.8寸ST7735 LCD屏幕、6个按键、蜂鸣器、光感器、电机驱动、锂电池,同时支持外接传感器扩展。
[*]按键:上=2,下=13,左=27,右=35,A =34,B= 12
[*]LCD屏幕:SPl=2,sck=18,mosi=23,cs=5,dc=4,res=19,bl=None
[*]SD卡:SPl=2,sck=18,mosi=23,miso=19,cs=22
[*]蜂鸣器:14,光照:36,热敏电阻:39,
[*]I2C:SCL=15,SDA=21
[*]端口1:33,端口2:32,端口3:26,端口4:25,端口5:UART,端口6:I2C
(5)Type-C&Micro二合一USB线
Type-C&Micro二合一USB线 ,采用扁平线不易打结,加上硅胶扎带方便收纳,同时两个接口都可以上传程序,一根线解决多种板子使用问题。
三、软件
1、开发环境:Thonny IDE
Thonny是一款专为Python初学者设计的集成开发环境(IDE),提供简洁直观的界面和易用特性,支持代码即时语法高亮显示、内置教程及逐步指导等功能,帮助用户专注于编程学习。
下载地址:https://github.com/thonny/thonny/releases/download/v4.1.7/thonny-4.1.7.exe
2、固件
(1)FireBeetle 2 ESP32-C5ESP32 MicroPython 固件
(2)游戏掌机MicroPython 固件
ESP32_GENERIC-SPIRAM-20250911-v1.26.1.bin
3、依赖库
(1)显示驱动库:st7735_buf.py(ST7735 LCD 屏幕底层驱动)
(2)显示工具库:easydisplay.py(简化 LCD 文本显示的封装库)
(3)系统内置库:network(网络配置)、espnow(ESP-NOW 通信)、machine(硬件引脚控制)、ubinascii(MAC 地址格式转换)三、制作过程1、硬件接线
(1)发送端接线(Gravity: JoyStick 摇杆、 FireBeetle 2 ESP32-C5)
VCC3.3V摇杆电源(不可接 5V,避免烧毁电位器)
GNDGND接地
X 轴(VRx)GPIO2X 轴模拟信号输入(接 ADC 引脚)
Y 轴(VRy)GPIO3Y 轴模拟信号输入(接 ADC 引脚)
SW(按键)不接线本项目暂不使用摇杆按键功能
(2)接收端(ESP32 、ST7735 LCD)
ST7735 LCD ESP32功能说明
VCC3.3V屏幕电源
GNDGND接地
SCK(时钟)GPIO18SPI 通信时钟线
SDA(数据)GPIO23SPI 通信数据线(MOSI)
CS(片选)GPIO5屏幕片选信号(低电平有效)
DC(数据 / 命令)GPIO4区分 SPI 传输的是命令还是数据
RES(复位)GPIO19屏幕复位信号(低电平复位)
BL(背光)不接线本项目暂不控制背光亮度
2、烧录固件
(1)Firebeetle 2 ESP32-C5
打开ESP系列芯片烧录工具,按下面步骤进行操作:
[*]选择正确的芯片类型,以Firebeetle 2 ESP32-C5 ver ECO1,选择esp32c5。
[*]选择“使用内置MicroPython固件”。
[*]点击“开始烧录”按钮。
[*]“确认”对话框中,点击“是”按钮,进入烧录程序。
[*]当出现“固件烧录完成”,即完成了固件烧录,如下图所示。
https://mc.dfrobot.com.cn/data/attachment/forum/202509/23/145756mzpg4vzpqopgnzzn.png
(2)ESP32编程掌机
打开ESP系列芯片烧录工具,按下面步骤进行操作:
[*]选择正确的芯片类型,这里选择esp32。
[*]选择“使用外置MicroPython固件”。
[*]点击“浏览”按钮,选择固件文件,这里选择ESP32_GENERIC-SPIRAM-20250911-v1.26.1.bin。
[*]输入正确的地址,这里使用0x1000。
[*]点击“开始烧录”按钮。
[*]“确认”对话框中,点击“是”按钮,进入烧录程序。
[*]当出现“固件烧录完成”,即完成了固件烧录。
3、依赖库上传
打开Thonny,选择“运行”->“配置解释器”,然后按下图所示进行设置。
选择菜单“视图”->“文件”,打开文件视图。在文件视图中找到要上传的库文件,右键弹出菜单选择“上传到/”,分别将ssd1306.pys上传到 FireBeetle 2 ESP32-C5,将t7735_buf.py和easydisplay.py上传到ESP32编程掌机。
4、编写程序
新建文件,复制 “发送端代码”,修改peer_mac为接收端的实际 MAC 地址,保存为main.py并上传到发送端 FireBeetle 2 ESP32-C5。新建文件,复制 “接收端代码”,保存为main.py并上传到ESP32编程掌机。
(1)发送端代码(main.py)
import network
import espnow
import time
import machine
import ubinascii
from machine import I2C, Pin
from ssd1306 import SSD1306_I2C
# ----------------------------
# 常量定义 - 集中管理配置参数
# ----------------------------
# ADC引脚配置 (连接摇杆)
ADC_X_PIN = 3
ADC_Y_PIN = 2
# 摇杆校准参数 (根据实际硬件调整)
JOYSTICK_CALIB = {
'x_min': 1978,
'x_center': 3634,
'x_max': 4095,
'y_min': 1974,
'y_center': 3600,
'y_max': 4095,
'threshold': 50# 死区阈值
}
# OLED屏幕配置
OLED_WIDTH = 128
OLED_HEIGHT = 64
OLED_I2C_SCL = 10
OLED_I2C_SDA = 9
# ESP-NOW配置
TARGET_MAC = b'\xac\x67\xb2\x44\xba\x8c'# 目标设备MAC地址
LOOP_DELAY = 0.1# 主循环延迟(秒)
# 方向指令常量
DIRECTIONS = {
'stop': 'Stop',
'forward': 'Forward',
'backward': 'Backward',
'left': 'Left',
'right': 'Right'
}
# ----------------------------
# 硬件初始化函数
# ----------------------------
def init_adc():
"""初始化ADC引脚用于读取摇杆值"""
adc_x = machine.ADC(machine.Pin(ADC_X_PIN))
adc_y = machine.ADC(machine.Pin(ADC_Y_PIN))
# 设置衰减以支持0-3.3V范围
adc_x.atten(machine.ADC.ATTN_11DB)
adc_y.atten(machine.ADC.ATTN_11DB)
return adc_x, adc_y
def init_oled():
"""初始化OLED屏幕"""
i2c = I2C(0, scl=Pin(OLED_I2C_SCL), sda=Pin(OLED_I2C_SDA), freq=400000)
oled = SSD1306_I2C(OLED_WIDTH, OLED_HEIGHT, i2c)
oled.fill(0)# 清屏
return oled
def init_espnow():
"""初始化ESP-NOW通信"""
# 初始化WiFi为STA模式
sta = network.WLAN(network.STA_IF)
sta.active(True)
# 显示本机MAC地址
mac_bytes = sta.config('mac')
mac_str = ubinascii.hexlify(mac_bytes, ':').decode()
print(f"本机MAC地址: {mac_str}")
sta.disconnect()# 不需要连接到AP
# 初始化ESP-NOW
esp_now = espnow.ESPNow()
esp_now.active(True)
# 添加目标设备
esp_now.add_peer(TARGET_MAC)
target_mac_str = ubinascii.hexlify(TARGET_MAC, ':').decode()
print(f"已添加目标设备MAC: {target_mac_str}")
return esp_now
# ----------------------------
# 功能函数
# ----------------------------
def read_joystick_values(adc_x, adc_y):
"""读取摇杆原始ADC值"""
return adc_x.read(), adc_y.read()
def calculate_direction(x, y):
"""根据摇杆位置计算方向指令"""
x_deviation = x - JOYSTICK_CALIB['x_center']
y_deviation = y - JOYSTICK_CALIB['y_center']
# 判断是否在死区内(停止状态)
if abs(x_deviation) < JOYSTICK_CALIB['threshold'] and \
abs(y_deviation) < JOYSTICK_CALIB['threshold']:
return DIRECTIONS['stop']
# 判断X/Y方向优先级
if abs(x_deviation) > abs(y_deviation):
return DIRECTIONS['right'] if x_deviation > 0 else DIRECTIONS['left']
else:
return DIRECTIONS['forward'] if y_deviation > 0 else DIRECTIONS['backward']
def calculate_analog_values(x, y):
"""
计算摇杆的模拟量值(-127~+127)
返回: (x_analog, y_analog)
"""
x_deviation = x - JOYSTICK_CALIB['x_center']
y_deviation = y - JOYSTICK_CALIB['y_center']
# 如果在死区内,返回0
if abs(x_deviation) < JOYSTICK_CALIB['threshold'] and \
abs(y_deviation) < JOYSTICK_CALIB['threshold']:
return 0, 0
# 计算X轴模拟量
if x_deviation > 0:
# 右方向:从中心到最大值映射到0~127
x_range = JOYSTICK_CALIB['x_max'] - JOYSTICK_CALIB['x_center']
x_analog = int((x_deviation / x_range) * 127)
else:
# 左方向:从最小值到中心映射到-127~0
x_range = JOYSTICK_CALIB['x_center'] - JOYSTICK_CALIB['x_min']
x_analog = int((x_deviation / x_range) * 127)
# 计算Y轴模拟量
if y_deviation > 0:
# 前方向:从中心到最大值映射到0~127
y_range = JOYSTICK_CALIB['y_max'] - JOYSTICK_CALIB['y_center']
y_analog = int((y_deviation / y_range) * 127)
else:
# 后方向:从最小值到中心映射到-127~0
y_range = JOYSTICK_CALIB['y_center'] - JOYSTICK_CALIB['y_min']
y_analog = int((y_deviation / y_range) * 127)
# 限制在-127~127范围内
x_analog = max(-127, min(127, x_analog))
y_analog = max(-127, min(127, y_analog))
return x_analog, y_analog
def send_analog_values(esp_now, x_analog, y_analog):
"""
发送模拟量值到目标设备
格式: "ANALOG:X:Y" 例如: "ANALOG:100:-50"
"""
analog_msg = f"ANALOG:{x_analog}:{y_analog}"
return esp_now.send(TARGET_MAC, analog_msg, True)
def get_centered_position(text, font_width=8, font_height=8):
"""计算文字在OLED屏幕上的居中位置"""
text_width = len(text) * font_width
x = (OLED_WIDTH - text_width) // 2
y = (OLED_HEIGHT - font_height) // 2 + 8# 垂直居中偏上一点
return x, y
# ----------------------------
# 主程序
# ----------------------------
def main():
# 初始化硬件
adc_x, adc_y = init_adc()
#oled = init_oled()
esp_now = init_espnow()
# 显示目标设备信息
target_mac_str = ubinascii.hexlify(TARGET_MAC, ':').decode()
target_mac_str_no_colon = ubinascii.hexlify(TARGET_MAC).decode()
oled.text(f"To:{target_mac_str_no_colon}", 0, 0)
oled.show()
last_direction = None
last_x_analog = 0
last_y_analog = 0
analog_mode = False# 模拟量模式开关
print("控制器启动,开始监控摇杆...")
try:
while True:
# 读取并计算方向
x, y = read_joystick_values(adc_x, adc_y)
current_direction = calculate_direction(x, y)
x_analog, y_analog = calculate_analog_values(x, y)
# 模拟量模式
if x_analog != last_x_analog or y_analog != last_y_analog:
send_result = send_analog_values(esp_now, x_analog, y_analog)
if send_result:
print(f"发送模拟量: X={x_analog:4d}, Y={y_analog:4d}")
else:
print(f"发送失败: X={x_analog:4d}, Y={y_analog:4d}")
# 更新OLED显示
oled.fill_rect(0, 16, OLED_WIDTH, OLED_HEIGHT-16, 0)
oled.text("Analog Mode", 0, 16)
oled.text(f"X:{x_analog:4d}", 0, 32)
oled.text(f"Y:{y_analog:4d}", 0, 48)
oled.show()
last_x_analog = x_analog
last_y_analog = y_analog
time.sleep(LOOP_DELAY)
except KeyboardInterrupt:
print("程序被用户终止")
finally:
# 清理资源
oled.fill(0)
oled.text("Stopped", 40, 32)
oled.show()
esp_now.send(TARGET_MAC, DIRECTIONS['stop'], True)
print("已发送停止指令,程序退出")
if __name__ == "__main__":
main()
(2)接收端代码(main.py)
# 导入必要库:网络、ESP-NOW通信、硬件控制及LCD驱动
import network
import espnow
import time
from time import sleep_ms
from machine import SPI, Pin
from driver import st7735_buf# ST7735 LCD底层驱动
from driver import drivers # 电机、光线传感器、温度传感器驱动
from lib.easydisplay import EasyDisplay# 简化LCD显示操作
import ubinascii
# ----------------------------------
# 1. 定义“计算居中坐标”的函数
# ----------------------------------
def get_center_pos(text, font_width=8, font_height=8):
"""
计算文字居中时的起始坐标 (x, y)
text: 要显示的文字(如 "forward")
font_width: 字体宽度(默认 8 像素)
font_height: 字体高度(默认 8 像素)
"""
# 计算文字总宽度(字符数 × 字体宽度)
text_total_width = len(text) * font_width
# X 轴:屏幕水平中心 - 文字总宽度的一半
x = (160 - text_total_width) // 2
# Y 轴:屏幕垂直中心 - 字体高度的一半
y = (128 - font_height) // 2 + 8
return x, y
# 初始化
spi = SPI(2, baudrate=20000000, polarity=0, phase=0, sck=Pin(18), mosi=Pin(23))
dp = st7735_buf.ST7735(width=160, height=128, spi=spi, cs=5, dc=4, res=19, rotate=1, bl=None,invert=False, rgb=True)
ed = EasyDisplay(display=dp, font="/font/text_lite_16px_2312.v3.bmf", show=True, color=0xFFFF, clear=True,color_type="RGB565")
hd = drivers.HardwareDrivers() # 创建硬件驱动实例
# 初始化ESP-NOW通信
# 初始化sta
sta = network.WLAN(network.STA_IF)
sta.active(True)
# 获取MAC地址(以字节形式)
mac_bytes = sta.config('mac')
# 将字节转换为人类可读的十六进制字符串
mac_str = ubinascii.hexlify(mac_bytes, ':').decode()
print(f"MAC地址: {mac_str}")
sta.disconnect()
ed.text(mac_str, 10, 10)
e = espnow.ESPNow()
e.active(True)# 启用ESP-NOW
def receive_messages():
"""接收ESP-NOW消息,处理后在LCD和串口显示"""
while True:
try:
host, msg = e.recv(0)# 非阻塞接收消息
if msg:
# 解码消息
command = msg.decode('utf-8').strip() if isinstance(msg, bytes) else str(msg).strip()
# 格式化发送端MAC
sender_mac = ':'.join(['%02x' % b for b in host])
print(f"来自 {sender_mac} 的指令: {command}")
# LCD显示指令
# ed.fill(0x0000)# 清屏(黑色)
dir_x, dir_y = get_center_pos(command)
ed.text(command, dir_x, dir_y)
# 首先判断是方向指令还是模拟量指令
if command.startswith("ANALOG:"):
# 模拟量指令 - 解析X和Y值
try:
parts = command.split(":")
x_analog = int(parts)
y_analog = int(parts)
# 死区处理 - 如果摇杆接近中心位置,则停止
if abs(x_analog) < 10 and abs(y_analog) < 10:
hd.motor_stop("ALL")
print("车辆停止")
else:
# 基础速度计算(基于Y轴)
base_speed = abs(y_analog) * 2# 转换为0-255范围
# 转向系数(基于X轴)
turn_factor = x_analog / 127.0# -1.0 到 +1.0
if y_analog > 10:# 前进
# 差速转向:一个轮子快,一个轮子慢
left_speed = int(base_speed * (1 - turn_factor))
right_speed = int(base_speed * (1 + turn_factor))
# 限制速度在0-255范围内
left_speed = max(0, min(255, left_speed))
right_speed = max(0, min(255, right_speed))
hd.motor_run(1, "CW", right_speed)# 右轮
hd.motor_run(2, "CW", left_speed) # 左轮
print(f"前进 - 左轮: {left_speed}, 右轮: {right_speed}")
elif y_analog < -10:# 后退
# 差速转向:一个轮子快,一个轮子慢
left_speed = int(base_speed * (1 + turn_factor))
right_speed = int(base_speed * (1 - turn_factor))
# 限制速度在0-255范围内
left_speed = max(0, min(255, left_speed))
right_speed = max(0, min(255, right_speed))
hd.motor_run(1, "CCW", right_speed)# 右轮
hd.motor_run(2, "CCW", left_speed) # 左轮
print(f"后退 - 左轮: {left_speed}, 右轮: {right_speed}")
else:# 原地转向(只有X轴输入)
turn_speed = abs(x_analog) * 2
if x_analog > 10:# 原地右转
hd.motor_run(1, "CCW", min(turn_speed, 255))
hd.motor_run(2, "CW", min(turn_speed, 255))
print(f"原地右转 - 速度: {turn_speed}")
elif x_analog < -10:# 原地左转
hd.motor_run(1, "CW", min(turn_speed, 255))
hd.motor_run(2, "CCW", min(turn_speed, 255))
print(f"原地左转 - 速度: {turn_speed}")
except (ValueError, IndexError):
print(f"模拟量指令解析错误: {command}")
else:
# 方向指令 - 原有的逻辑保持不变
if command == "Forward":
hd.motor_run(1, "CW", 255)
hd.motor_run(2, "CW", 255)
print("车辆前进")
elif command == "Backward":
hd.motor_run(1, "CCW", 255)
hd.motor_run(2, "CCW", 255)
print("车辆后退")
elif command == "Left":
hd.motor_run(1, "CW", 255)
hd.motor_run(2, "CCW", 255)
print("车辆左转")
elif command == "Right":
hd.motor_run(1, "CCW", 255)
hd.motor_run(2, "CW", 255)
print("车辆右转")
elif command == "Stop":
hd.motor_stop("ALL")
print("车辆停止")
sleep_ms(10)
except Exception as ex:
print(f"接收错误: {ex}")
sleep_ms(100)
def main():
"""主函数:初始化系统并启动消息接收"""
try:
# ed.fill(0x0000)
receive_messages()# 启动接收循环
except KeyboardInterrupt:
print("程序被中断")
except Exception as ex:
print(f"错误: {ex}")
finally:
# 清理资源
e.active(False)
sta.active(False)
ed.fill(0x0000)
ed.text("已停止", 50, 60)
if __name__ == "__main__":
main()
5、系统调试
(1)分别给发送端、接收端上电,打开 Thonny 的 “串口监视器”(波特率默认 115200)。
(2)发送端串口会打印自身 MAC 地址和 “准备发送指令” 提示;接收端会打印自身 MAC 地址和 “等待指令” 提示,LCD 屏幕显示 “ESPNow 接收端”“状态:运行中”。
(3)FireBeetle 2 ESP32-C5(发送端)拨动摇杆,发送ESPNOW指令,OLED屏幕显示“Forward/Backward/Left/Right”,串口打印 “发送指令:Forward/Backward/Left/Right/Stop”。
(4)ESP32掌机(接收端)串口打印 “来自 [发送端 MAC] 的指令:Forward/Backward/Left/Right/Stop”,且 LCD 屏幕实时显示“Forward/Backward/Left/Right/Stop”。
四、技术原理
1、ESP-NOW 通信
ESP-NOW 是一种由乐鑫公司定义的无连接 Wi-Fi 通信协议。在 ESP-NOW 中,应用程序数据被封装在各个供应商的动作帧中,然后在无连接的情况下,从一个 Wi-Fi 设备传输到另一个 Wi-Fi 设备。其特点是低延迟(毫秒级)、低功耗、无需 IP 地址,适合物联网设备间的短数据传输(如指令、传感器数据)。
(1)配对逻辑:发送端需先将接收端的 MAC 地址添加为 “peer(对等设备)”,才能向其发送数据;接收端无需主动添加,可被动接收已配对设备的消息。
(2)数据传输:本项目中发送端通过e.send(peer_mac, dir, True)发送指令(True表示等待接收端确认),接收端通过e.recv(0)非阻塞接收消息(0表示不等待,立即返回)。
2、ADC 摇杆信号采集
ESP32 的 ADC 模块(模拟 - 数字转换器)可将摇杆的模拟电压信号(0~3.3V)转换为 0~4095 的数字值(12 位精度)。
(1)信号校准:通过X_CENTER, Y_CENTER = 3634, 3600设置摇杆 “中立位置” 的基准值(需根据实际摇杆校准,避免漂移)。
(2)阈值过滤:通过THRESHOLD = 50设置 “死区阈值”,当摇杆偏移量小于 50 时,判定为 “stop”,避免轻微晃动导致误触发。
(3)方向判断:比较 X 轴(xd)和 Y 轴(yd)的偏移量绝对值,绝对值大的轴为 “有效方向”,再根据正负判断具体方向(如 xd>0 则为 “right”)。
3、LCD 屏幕显示
通过EasyDisplay封装库简化 ST7735 屏幕控制。
(1)初始化:先通过st7735_buf.ST7735配置 SPI 引脚、屏幕分辨率、旋转方向等底层参数,再通过EasyDisplay设置字体、默认颜色、清屏模式。
(2)文本显示:调用ed.text(内容, x坐标, y坐标)在指定位置显示文本,ed.fill(0x0000)实现清屏(0x0000 为 RGB565 格式的黑色)。
附件:程序、固件、工具
先占个坑
页:
[1]