| 本帖最后由 豆爸 于 2025-10-6 16:19 编辑 
 
 一、简介 
 本项目基于 ESP32 开发板,通过 ESP-NOW 无线通信协议构建了一套 “摇杆指令发送 - 接收 - 显示” 系统。发送端利用 ADC 模块采集摇杆的 X、Y 轴模拟信号,转化为 “上 / 下 / 左 / 右 / 停止”5 种控制指令;接收端通过 ESP-NOW 接收指令后,在 LCD 屏幕上实时显示指令内容,同时在串口输出 sender MAC 地址与指令信息,实现了低延迟、近距离的无线指令交互。 该系统展示了低功耗无线通信技术在物联网设备控制中的应用,适用于遥控小车、智能家居控制等多种场景。 
 二、硬件  
 1、硬件清单 
 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= 12LCD屏幕:SPl=2,sck=18,mosi=23,cs=5,dc=4,res=19,bl=NoneSD卡: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) 
 (2)接收端(ESP32 、ST7735 LCD) 
 2、烧录固件 
 (1)Firebeetle 2 ESP32-C5 
 打开ESP系列芯片烧录工具,按下面步骤进行操作: 
 选择正确的芯片类型,以Firebeetle 2 ESP32-C5 ver ECO1,选择esp32c5。选择“使用内置MicroPython固件”。点击“开始烧录”按钮。“确认”对话框中,点击“是”按钮,进入烧录程序。当出现“固件烧录完成”,即完成了固件烧录,如下图所示。
 
 (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编程掌机。
			
			
			复制代码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[1])
                        y_analog = int(parts[2])
                        
                        # 死区处理 - 如果摇杆接近中心位置,则停止
                        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 格式的黑色)。附件:程序、固件、工具
 |