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

FireBeetle 2 ESP32-C5 电压采集上传QT波形上位机 简单

头像 卓尔 2025.10.14 14 0

        拿到这个FireBeetle 2 ESP32-C5开发板后,很激动,非常精美,先测试一下ADC和串口功能。

        所以这篇文章设计一个简单的任务:实现模拟电压信号的采集并通过串口将采集到的电压数据以特定格式发送给PC端的QT上位机软件,从而完成电压波形的实时显示。

        ESP32-C5芯片内置了12位ADC,支持多通道模拟输入,采样精度高,且具有多档输入衰减可选,便于适配不同电压量程的传感器信号。具体到本方案,选用ADC1的通道4,即GPIO5作为采样引脚,该引脚对应FireBeetle 2开发板上的模拟输入接口,方便用户接入外部电压信号。

软件方面,首先需要对ADC进行初始化设置,包括配置采样分辨率和输入衰减等级。

       ADC默认分辨率是12位,允许的采样值范围是0到4095。初始化完成后,在主循环中通过调用analogRead(5)读取GPIO5引脚的ADC采样值,采样值是一个数字量,需要通过计算转化为对应的实际电压。转换公式为采样值乘以最大量程电压3.6伏特,再除以最大采样值4095,这样即可得到电压的近似值。为了保证数据传输的规范和上位机解析的简便,程序将电压转换值以字符串形式通过串口输出,格式统一为“Temp:xx.xx\r\n”,其中“xx.xx”表示电压值,保留两位小数,方便程序识别和处理。单片机程序如下:

代码
void setup() {
  Serial.begin(115200);
  while (!Serial) {
    delay(10);
  }
  Serial.println("ESP32C5 ADC1_4(GPIO5)采集并格式化发送示例");
  
  analogReadResolution(12);       // 12位分辨率
  analogSetAttenuation(ADC_11db); // 11dB衰减,测量范围约3.6V
}

void loop() {
  int adcValue = analogRead(5);
  float voltage = adcValue * (3.6f / 4095.0f);

  // 发送格式 Temp:xx\r\n,电压保留两位小数
  Serial.print("Temp:");
  Serial.print(voltage, 2);
  Serial.print("\r\n");

  delay(1000);
}

        在串口通信方面,ESP32程序中采用标准串口初始化,设置波特率为115200,保证数据传输的稳定性和速率匹配。串口数据输出时,使用Serial.print函数逐段发送固定前缀“Temp:”,随后输出电压数值,再加上回车换行符\r\n,形成一条完整的数据帧。这样设计的通信协议简单易用,便于QT上位机端准确读取每次采样数据,实现波形的连续刷新。采样频率设定为1秒一次,这样既能减轻数据通信压力,也能满足多数普通波形监测需求。如果对实时性有更高要求,采样间隔可以缩短,但需要同步调整上位机的接收处理能力,先用串口调试助手测试一下串口通讯格式。

image.png

      QT上位机设计流程如下:使用QMainWindow作为主窗口,结合布局管理器(如QHBoxLayout、QVBoxLayout)组织控件。本例中,主窗口分为左侧控制面板(串口选择、波特率、数据显示)和右侧波形图,通过QSplitter实现可拖动的分隔布局。控件如QComboBox(端口选择)、QPushButton(开关串口)、QLabel(电压显示)等按功能分组,垂直布局确保紧凑。动画背景(AnimatedBackground)通过QWidget的自定义绘制增强视觉效果。

      此外,PyQt支持通过QSS(Qt样式表)定制界面外观。本例将科技风格改为温馨色调,使用桃橙(#FFDAB9)和浅粉红(#FFB6C1)渐变背景,搭配深色文字(#333333)以提升可读性。按钮、输入框等控件的圆角边框和悬停效果通过QSS实现,确保界面统一且美观。pyqtgraph的波形图背景设为浅色(#FFF5EE),曲线采用粉色(#FF69B4),与主题呼应。

      核心功能主要分为串口通信、数据解析和波形更新。串口通信使用pyserial库,通过QTimer定时读取数据,格式为“Temp:xx\r\n”,解析后提取电压值。数据显示在QLabel中,实时更新。波形绘制借助pyqtgraph,设置时间轴(X)和电压轴(Y),通过动态数组(time_data、voltage_data)存储数据,限制最大点数(500)以优化性能。异常处理(如串口错误、数据解析失败)通过QMessageBox提示用户。

      事件驱动机制也是设计重点。按钮点击(如“打开串口”)通过信号-槽机制连接到toggle_serial方法,切换串口状态并更新按钮文本。QTimer的timeout信号触发read_serial_data,处理串口数据流。用户输入(如端口选择)通过QComboBox的currentText获取,确保交互实时响应。窗口关闭时,closeEvent确保串口正确关闭,避免资源泄露。PyQt完整代码如下:

代码
import sys
import serial
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtCore import QTimer, Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QPushButton, QComboBox, QLabel, QTextEdit, \
    QSplitter, QHBoxLayout, QWidget, QMessageBox
from PyQt5.QtGui import QColor, QPainter
import pyqtgraph as pg
from pyqtgraph import PlotWidget
from serial.tools import list_ports

# ------------------- 温馨风格样式表 -------------------
STYLE_SHEET = """
QMainWindow {
    background-color: transparent;
    color: #333333;
}
QPushButton {
    background-color: #FFDAB9;
    color: #333333;
    border: 1px solid #FFB6C1;
    border-radius: 10px;
    padding: 5px;
}
QPushButton:hover {
    background-color: #FFB6C1;
}
QComboBox {
    background-color: #FFDAB9;
    border: 1px solid #FFB6C1;
    border-radius: 10px;
    color: #333333;
    padding: 2px;
}
QTextEdit {
    background-color: #FFDAB9;
    border: 1px solid #FFB6C1;
    border-radius: 10px;
    color: #333333;
}
QLabel {
    color: #333333;
    font-size: 16px;
    font-weight: bold;
}
"""

# ------------------- 动画背景组件 -------------------
class AnimatedBackground(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAttribute(Qt.WA_StyledBackground, True)
        self.color_position = 0
        self.timer = QTimer()
        self.timer.timeout.connect(self.update_background)
        self.timer.start(50)

    def update_background(self):
        self.color_position += 1
        if self.color_position > 100:
            self.color_position = 0
        self.update()

    def paintEvent(self, event):
        painter = QPainter(self)
        gradient = QtGui.QLinearGradient(0, 0, self.width(), self.height())
        gradient.setColorAt(0, QColor(255, 218, 185))  # 桃橙
        gradient.setColorAt(self.color_position / 100.0, QColor(255, 182, 193))  # 浅粉红
        gradient.setColorAt(1, QColor(255, 218, 185))
        painter.fillRect(self.rect(), gradient)

# ------------------- 主窗口 -------------------
class VoltageMainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.serial_port = None
        self.max_points = 500
        self.time_data = []
        self.voltage_data = []
        self.init_ui()
        self.init_serial()

    def init_ui(self):
        self.setWindowTitle('电压模拟测量上位机')
        self.setGeometry(100, 100, 1200, 600)

        # 添加动画背景
        animated_background = AnimatedBackground(self)
        self.setCentralWidget(animated_background)

        # 主水平布局
        main_layout = QHBoxLayout(animated_background)

        # 控制面板布局(左侧)
        control_layout = QVBoxLayout()
        control_layout.setAlignment(Qt.AlignTop)

        # 串口选择下拉菜单
        self.port_combobox = QComboBox()
        self.port_combobox.addItems([port.device for port in list_ports.comports()])
        control_layout.addWidget(self.port_combobox)

        # 波特率选择下拉菜单
        self.baudrate_combobox = QComboBox()
        self.baudrate_combobox.addItems(['9600', '19200', '38400', '57600', '115200'])
        self.baudrate_combobox.setCurrentText('115200')
        control_layout.addWidget(self.baudrate_combobox)

        # 打开/关闭串口按钮
        self.serial_button = QPushButton('打开串口')
        self.serial_button.clicked.connect(self.toggle_serial)
        control_layout.addWidget(self.serial_button)

        # 数据展示框
        self.received_data_display = QTextEdit()
        self.received_data_display.setReadOnly(True)
        control_layout.addWidget(self.received_data_display)

        # 电压显示
        self.voltage_display = QLabel("当前电压:-- V")
        control_layout.addWidget(self.voltage_display)

        # 图表布局(右侧)
        plot_layout = QVBoxLayout()
        self.plot = self.create_plot_widget()
        plot_layout.addWidget(self.plot)

        # 将控制面板和图表放入 QSplitter
        splitter = QSplitter(Qt.Horizontal)
        control_widget = QWidget()
        control_widget.setLayout(control_layout)
        plot_widget = QWidget()
        plot_widget.setLayout(plot_layout)

        splitter.addWidget(control_widget)
        splitter.addWidget(plot_widget)
        splitter.setSizes([self.width() // 5, 4 * self.width() // 5])

        main_layout.addWidget(splitter)

    def create_plot_widget(self):
        plot = PlotWidget(self)
        plot.setBackground('#FFF5EE')
        plot.setTitle("电压波形", color="#333333", size="14pt")
        plot.setLabel('left', "电压 (V)", **{'color': '#333333', 'font-size': '12pt'})
        plot.setLabel('bottom', '时间 (s)', **{'color': '#333333', 'font-size': '12pt'})
        plot.showGrid(x=True, y=True, alpha=0.3)
        plot.setYRange(0, 5)  # 假设电压范围0-5V,可根据需要调整
        self.voltage_curve = plot.plot(pen=pg.mkPen('#FF69B4', width=2))  # 粉色曲线,温馨调
        return plot

    def init_serial(self):
        self.serial_timer = QTimer()
        self.serial_timer.timeout.connect(self.read_serial_data)
        self.time_counter = 0

    def toggle_serial(self):
        if self.serial_port is None or not self.serial_port.is_open:
            try:
                port = self.port_combobox.currentText()
                baudrate = int(self.baudrate_combobox.currentText())
                self.serial_port = serial.Serial(port, baudrate, timeout=1)
                self.serial_button.setText("关闭串口")
                self.received_data_display.append(f"已连接到 {port},波特率 {baudrate}")
                self.serial_timer.start(10)
            except Exception as e:
                QMessageBox.critical(self, "错误", f"无法连接串口: {str(e)}")
        else:
            self.serial_port.close()
            self.serial_button.setText("打开串口")
            self.received_data_display.append("已断开串口连接")
            self.serial_timer.stop()

    def read_serial_data(self):
        if self.serial_port and self.serial_port.is_open:
            try:
                if self.serial_port.in_waiting > 0:
                    data = self.serial_port.readline().decode('utf-8').strip()
                    self.received_data_display.append(f"接收: {data}")
                    self.parse_serial_data(data)
            except Exception as e:
                self.received_data_display.append(f"读取错误: {str(e)}")

    def parse_serial_data(self, data):
        try:
            # 格式: Temp:xx
            if data.startswith("Temp:"):
                voltage = float(data.split(':')[1])
                # 更新显示
                self.voltage_display.setText(f"当前电压:{voltage:.2f} V")

                # 更新波形
                self.time_counter += 0.1
                self.time_data.append(self.time_counter)
                self.voltage_data.append(voltage)
                if len(self.time_data) > self.max_points:
                    self.time_data.pop(0)
                    self.voltage_data.pop(0)
                self.voltage_curve.setData(self.time_data, self.voltage_data)
        except Exception as e:
            self.received_data_display.append(f"解析错误: {str(e)}")

    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)
    app.setStyleSheet(STYLE_SHEET)
    mainWin = VoltageMainWindow()
    mainWin.show()
    sys.exit(app.exec_())

image.png

整体的硬件连接图如下:

image.png

评论

user-avatar