回到首页 返回首页
回到顶部 回到顶部
返回上一页 返回上一页

FireBeetle 2 ESP32-C5 舵机控制与简易动画模拟 简单

头像 卓尔 2025.10.15 11 0

本项目实现了一个舵机控制系统,包含以下功能:

  • 硬件:ESP32C5 开发板控制舵机(信号引脚接 GPIO2)。
  • 软件
    • ESP32C5 固件:通过串口接收角度指令(0-180°),控制舵机旋转。
    • PyQt5 上位机:提供图形化界面,包含串口选择、角度滑块控制、状态显示和舵机动画模拟。
  • 通信:PC 与 ESP32C5 通过 USB 串口通信,波特率 115200,发送格式为 角度值\n(如 90\n)。
  • 硬件图如下:
  • bfed8cde5ab8113845c50e3b974d362c.jpg

所需硬件

  • ESP32C5 开发板(支持 ESP-IDF 3.x API)。
  • SG90 或类似舵机(工作电压 4.8-6V,脉宽 0.5ms-2.5ms)。
  • USB 数据线(用于串口通信和烧录固件)。
  • 外部电源(推荐 5V 电源模块,舵机供电)。
  • 杜邦线若干。
  • 电脑(运行 PyQt5 上位机,Windows/Linux/macOS 均可)。

接线说明

舵机引脚

ESP32C5 引脚

说明

信号线(橙色)

GPIO2

PWM 信号控制舵机角度

电源线(红色)

5V

ESP32C5 的 5V 引脚

地线(棕色)

GND

连接 ESP32C5 和外部电源的 GND

ESP32C5端完整代码如下:

代码
#define SERVO_PIN 2       // 舵机信号引脚
#define PWM_FREQ 50       // 50Hz (20ms周期)
#define PWM_RES 10        // 10位分辨率 (0-1023)

void setup() {
  Serial.begin(115200);
  while (!Serial);
  Serial.println("ESP32C5 Servo Control via Serial (Core 3.x API)");
  Serial.println("Enter angle (0-180 degrees):");

  // 新API (3.x):绑定引脚、频率、分辨率(自动分配通道,返回bool)
  if (!ledcAttach(SERVO_PIN, PWM_FREQ, PWM_RES)) {
    Serial.println("Error: Failed to attach PWM to pin");
    while (true); // 停止程序,如果绑定失败
  }
  
  // 初始化舵机到0度
  setServoAngle(0);
}

void loop() {
  // 检查串口是否有数据
  if (Serial.available() > 0) {
    String input = Serial.readStringUntil('\n'); // 读取一行输入
    input.trim(); // 去除首尾空格
    int angle = input.toInt(); // 转换为整数

    // 验证角度范围
    if (angle >= 0 && angle <= 180) {
      setServoAngle(angle);
      Serial.print("Servo set to ");
      Serial.print(angle);
      Serial.println(" degrees");
    } else {
      Serial.println("Error: Angle must be between 0 and 180");
    }
  }
}

// 设置舵机角度
void setServoAngle(int angle) {
  // 舵机PWM脉宽:0.5ms (0度) ~ 2.5ms (180度),周期20ms (50Hz)
  // 计算占空比:(pulse_width_ms / 20ms) * (2^res - 1)
  // 0.5ms -> 25, 2.5ms -> 128 (10位分辨率)
  int duty = map(angle, 0, 180, 25, 128); // 映射角度到PWM占空比值
  ledcWrite(SERVO_PIN, duty); // 3.x中使用ledcWrite(pin, duty)直接写占空比(通道自动)
}

       基于Arduino框架,通过串口接收PC发送的角度指令(0-180°)来驱动连接在GPIO2的舵机,采用50Hz PWM信号(20ms周期,10位分辨率)。代码在setup中初始化串口(波特率115200)并使用ESP32 Core 3.x API的ledcAttach配置PWM,初始设置舵机角度为0°。在loop中,程序循环读取串口输入,解析为整数角度,验证范围后通过setServoAngle函数映射角度到PWM占空比(0.5ms-2.5ms对应25-128),并使用ledcWrite输出信号控制舵机,同时通过串口反馈执行状态。

      Python端代码则通过PyQt5构建界面:分为左侧控制区(串口选择、连接按钮、角度滑块、状态显示)和右侧动画区(模拟舵机主体和臂的旋转)。通过pyserial实现与ESP32C5的串口通信(波特率115200,发送格式为角度值\n),支持动态更新串口列表、连接/断开串口、实时发送角度指令。滑块控制角度(0-180°)时,界面同步更新角度显示并通过QTransform实现舵机臂顺时针旋转动画,关闭窗口时自动断开串口,完整Python代码如下:

代码
import sys
import serial
import serial.tools.list_ports
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
                             QSlider, QLabel, QPushButton, QComboBox, QGraphicsView,
                             QGraphicsScene, QGraphicsRectItem)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QTransform, QPen
import qdarkstyle  # 深色主题(需安装:pip install qdarkstyle)


class ServoControlUI(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("ESP32C5 舵机控制")
        self.setGeometry(100, 100, 800, 600)

        # 初始化串口
        self.serial_port = None
        self.baudrate = 115200  # 与ESP32C5代码匹配

        # 设置机器人风格(深色主题)
        self.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
        self.init_ui()

    def init_ui(self):
        # 主窗口布局
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        main_layout = QHBoxLayout(central_widget)

        # 左侧:串口控制 + 滑块
        left_widget = QWidget()
        left_layout = QVBoxLayout(left_widget)
        left_layout.setAlignment(Qt.AlignTop)

        # 标题
        title_label = QLabel("舵机控制系统")
        title_label.setFont(QFont("微软雅黑", 16, QFont.Bold))  # 中文字体
        title_label.setAlignment(Qt.AlignCenter)
        left_layout.addWidget(title_label)

        # 串口选择
        serial_layout = QHBoxLayout()
        self.port_combo = QComboBox()
        self.port_combo.setStyleSheet("QComboBox { padding: 5px; border-radius: 5px; }")
        serial_layout.addWidget(QLabel("串口:"))
        serial_layout.addWidget(self.port_combo)

        self.connect_button = QPushButton("连接")
        self.connect_button.setStyleSheet("""
            QPushButton { background-color: #2ecc71; color: white; padding: 8px; border-radius: 5px; font-weight: bold; }
            QPushButton:hover { background-color: #27ae60; }
            QPushButton:disabled { background-color: #7f8c8d; }
            QPushButton:pressed { background-color: #1e8449; }
        """)
        self.connect_button.clicked.connect(self.toggle_serial)
        serial_layout.addWidget(self.connect_button)

        # 初始化端口列表(在 connect_button 定义后再调用)
        self.update_ports()
        left_layout.addLayout(serial_layout)

        # 滑块控制
        self.slider = QSlider(Qt.Horizontal)
        self.slider.setMinimum(0)
        self.slider.setMaximum(180)
        self.slider.setValue(0)
        self.slider.setTickPosition(QSlider.TicksBelow)
        self.slider.setTickInterval(10)
        self.slider.setStyleSheet("""
            QSlider::groove:horizontal { 
                height: 8px; 
                background: #34495e; 
                border-radius: 4px; 
            }
            QSlider::handle:horizontal { 
                background: #e74c3c; 
                width: 20px; 
                height: 20px; 
                border-radius: 10px; 
            }
        """)
        self.slider.valueChanged.connect(self.update_servo)
        left_layout.addWidget(QLabel("舵机角度控制 (0-180°):"))
        left_layout.addWidget(self.slider)

        # 角度显示
        self.angle_label = QLabel("角度: 0°")
        self.angle_label.setFont(QFont("微软雅黑", 12, QFont.Bold))
        left_layout.addWidget(self.angle_label)

        # 状态显示
        self.status_label = QLabel("状态: 未连接")
        self.status_label.setFont(QFont("微软雅黑", 10))
        self.status_label.setStyleSheet("color: #e74c3c;")  # 红色状态提示
        left_layout.addWidget(self.status_label)

        # 添加伸缩项
        left_layout.addStretch()

        main_layout.addWidget(left_widget)

        # 右侧:零件动画区域
        animation_widget = QWidget()
        animation_layout = QVBoxLayout(animation_widget)

        anim_title = QLabel("舵机动画模拟")
        anim_title.setFont(QFont("微软雅黑", 14, QFont.Bold))
        anim_title.setAlignment(Qt.AlignCenter)
        animation_layout.addWidget(anim_title)

        self.scene = QGraphicsScene()
        self.view = QGraphicsView(self.scene)
        self.view.setMinimumSize(400, 400)
        self.view.setStyleSheet("border: 2px solid #34495e; background: #2c3e50; border-radius: 5px;")

        # 绘制舵机主体(矩形模拟圆形)
        servo_body = QGraphicsRectItem(-30, -30, 60, 60)
        servo_body.setBrush(Qt.gray)
        pen = QPen(Qt.NoPen)
        servo_body.setPen(pen)
        self.scene.addItem(servo_body)

        # 绘制舵机臂(矩形表示)
        self.servo_arm = QGraphicsRectItem(-5, -60, 10, 120)  # 长条形臂
        self.servo_arm.setBrush(Qt.red)
        arm_pen = QPen(Qt.black)
        self.servo_arm.setPen(arm_pen)
        self.servo_arm.setTransformOriginPoint(0, 60)  # 设置旋转中心在臂底部(匹配主体)
        self.scene.addItem(self.servo_arm)

        # 添加中心点指示
        center_point = QGraphicsRectItem(-2, 58, 4, 4)
        center_point.setBrush(Qt.yellow)
        self.scene.addItem(center_point)

        animation_layout.addWidget(self.view)
        main_layout.addWidget(animation_widget)

    def update_ports(self):
        """更新可用串口列表"""
        self.port_combo.clear()
        ports = serial.tools.list_ports.comports()
        for port in ports:
            port_info = f"{port.device} - {port.description}"
            self.port_combo.addItem(port_info, port.device)  # 存储实际端口
        if not ports:
            self.port_combo.addItem("无可用串口")
            self.connect_button.setEnabled(False)
        else:
            self.connect_button.setEnabled(True)

    def toggle_serial(self):
        """连接/断开串口"""
        if self.serial_port and self.serial_port.is_open:
            # 断开连接
            self.serial_port.close()
            self.serial_port = None
            self.connect_button.setText("连接")
            self.status_label.setText("状态: 已断开连接")
            self.status_label.setStyleSheet("color: #e74c3c;")
            self.slider.setEnabled(False)
        else:
            # 尝试连接
            try:
                port_data = self.port_combo.currentData()
                if port_data is None:
                    port = self.port_combo.currentText()
                    if "无可用串口" in port:
                        self.status_label.setText("状态: 无可用串口")
                        return
                else:
                    port = port_data

                self.serial_port = serial.Serial(port, self.baudrate, timeout=1)
                self.connect_button.setText("断开")
                self.status_label.setText("状态: 已连接")
                self.status_label.setStyleSheet("color: #2ecc71;")
                self.slider.setEnabled(True)
                # 初始化舵机到0度
                self.send_angle(0)
            except serial.SerialException as e:
                self.status_label.setText(f"状态: 连接失败 - {str(e)}")
                self.status_label.setStyleSheet("color: #e74c3c;")

    def send_angle(self, angle):
        """发送角度到ESP32C5"""
        if self.serial_port and self.serial_port.is_open:
            try:
                # 发送格式:纯数字 + 换行符(如"90\n")
                self.serial_port.write(f"{angle}\n".encode())
                print(f"发送角度: {angle}°")  # 调试信息
            except serial.SerialException as e:
                self.status_label.setText(f"状态: 串口错误 - {str(e)}")
                self.status_label.setStyleSheet("color: #e74c3c;")
                self.serial_port.close()
                self.serial_port = None
                self.connect_button.setText("连接")
                self.slider.setEnabled(False)

    def update_servo(self):
        """更新舵机角度和动画"""
        angle = self.slider.value()
        self.angle_label.setText(f"角度: {angle}°")
        # 更新动画:旋转舵机臂(取反角度以改变旋转方向)
        transform = QTransform()
        transform.rotate(-angle)  # 改为顺时针旋转
        self.servo_arm.setTransform(transform)
        self.scene.update()
        # 发送到ESP32C5
        self.send_angle(angle)

    def closeEvent(self, event):
        """关闭窗口时断开串口"""
        if self.serial_port and self.serial_port.is_open:
            self.serial_port.close()
        event.accept()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = ServoControlUI()
    window.show()
    sys.exit(app.exec_())

界面样式如下图所示:

2df0034f7e10fdabe8df311cc0b92f25.png

完整演示视频如下:

评论

user-avatar