基于Qt的串口上位机控制蓝牙小车程序

Qt串口上位机控制蓝牙小车解决方案,包含通信协议、UI设计、代码实现和硬件对接。

一、系统架构设计

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    Qt上位机软件                           │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐       │
│  │  串口配置   │  │  连接管理   │  │  状态显示   │       │
│  │  • 端口选择 │  │  • 连接/断开│  │  • 连接状态 │       │
│  │  • 波特率   │  │  • 自动重连 │  │  • 电池电量 │       │
│  │  • 数据位   │  │  • 心跳检测 │  │  • 信号强度 │       │
│  └─────────────┘  └─────────────┘  └─────────────┘       │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐       │
│  │  运动控制   │  │  模式切换   │  │  参数配置   │       │
│  │  • 方向控制 │  │  • 手动模式 │  │  • 速度调节 │       │
│  │  • 速度滑块 │  │  • 自动模式 │  │  • 转向灵敏度│       │
│  │  • 急停按钮 │  │  • 循迹模式 │  │  • PID参数  │       │
│  └─────────────┘  └─────────────┘  └─────────────┘       │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐       │
│  │  数据监控   │  │  日志记录   │  │  固件升级   │       │
│  │  • 实时曲线 │  │  • 操作日志 │  │  • 在线升级 │       │
│  │  • 传感器数据│  │  • 错误日志 │  │  • 版本管理 │       │
│  │  • 调试信息 │  │  • 导出功能 │  │  • 备份恢复 │       │
│  └─────────────┘  └─────────────┘  └─────────────┘       │
└─────────────────────────────────────────────────────────────┘
                           │ 串口通信 (UART)
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                    HC-05/HC-06 蓝牙模块                    │
└─────────────────────────────────────────────────────────────┘
                           │ 串口透传
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                     STM32/ESP32 主控                        │
├─────────────────────────────────────────────────────────────┤
│  • 电机驱动 (L298N/TB6612)                                 │
│  • 编码器反馈 (速度闭环)                                    │
│  • 超声波避障 (HC-SR04)                                    │
│  • 红外循迹 (TCRT5000)                                     │
│  • 舵机云台 (SG90)                                         │
│  • 电池管理 (电压监测)                                     │
└─────────────────────────────────────────────────────────────┘

二、通信协议设计

2.1 数据帧格式

cpp 复制代码
// 数据帧结构
typedef struct {
    uint8_t header;      // 帧头 0xAA
    uint8_t length;      // 数据长度
    uint8_t command;     // 命令字
    uint8_t data[32];    // 数据载荷
    uint8_t checksum;    // 校验和
    uint8_t footer;     // 帧尾 0x55
} DataFrame;

// 命令字定义
enum CommandType {
    CMD_HEARTBEAT = 0x01,      // 心跳包
    CMD_MOVE_FORWARD = 0x10,   // 前进
    CMD_MOVE_BACKWARD = 0x11,  // 后退
    CMD_TURN_LEFT = 0x12,      // 左转
    CMD_TURN_RIGHT = 0x13,     // 右转
    CMD_STOP = 0x14,           // 停止
    CMD_SET_SPEED = 0x20,      // 设置速度
    CMD_GET_STATUS = 0x30,     // 获取状态
    CMD_SET_MODE = 0x40,       // 设置模式
    CMD_FIRMWARE_UPDATE = 0x50 // 固件升级
};

2.2 控制指令详解

cpp 复制代码
// 运动控制指令
#pragma pack(push, 1)
typedef struct {
    uint8_t direction;    // 方向: 0=停止, 1=前进, 2=后退, 3=左转, 4=右转
    uint8_t speed_left;   // 左轮速度 (0-255)
    uint8_t speed_right;  // 右轮速度 (0-255)
    uint8_t acceleration; // 加速度 (0-100)
} MotionControl;

// 状态反馈
typedef struct {
    uint8_t battery_level; // 电池电量 (0-100%)
    uint16_t voltage;      // 电池电压 (mV)
    int16_t left_speed;    // 左轮实际速度 (rpm)
    int16_t right_speed;   // 右轮实际速度 (rpm)
    uint8_t obstacle_distance; // 障碍物距离 (cm)
    uint8_t temperature;   // 温度 (℃)
    uint8_t mode;          // 当前模式
} CarStatus;
#pragma pack(pop)

三、Qt项目实现

3.1 项目文件结构

复制代码
BluetoothCarController/
├── CMakeLists.txt
├── src/
│   ├── main.cpp
│   ├── MainWindow.h
│   ├── MainWindow.cpp
│   ├── SerialPortManager.h
│   ├── SerialPortManager.cpp
│   ├── ProtocolParser.h
│   ├── ProtocolParser.cpp
│   ├── CarController.h
│   ├── CarController.cpp
│   ├── DataLogger.h
│   └── DataLogger.cpp
├── ui/
│   ├── MainWindow.ui
│   ├── ControlPanel.ui
│   └── StatusMonitor.ui
├── resources/
│   ├── icons/
│   ├── styles/
│   └── config/
└── docs/

3.2 串口管理类 (SerialPortManager.h)

cpp 复制代码
#ifndef SERIALPORTMANAGER_H
#define SERIALPORTMANAGER_H

#include <QObject>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QTimer>
#include <QByteArray>

class SerialPortManager : public QObject
{
    Q_OBJECT

public:
    explicit SerialPortManager(QObject *parent = nullptr);
    ~SerialPortManager();

    // 串口操作
    bool openPort(const QString &portName, int baudRate);
    void closePort();
    bool isOpen() const;
    
    // 发送数据
    bool sendData(const QByteArray &data);
    bool sendCommand(uint8_t command, const QByteArray &payload = QByteArray());
    
    // 获取可用串口
    static QStringList getAvailablePorts();
    static QStringList getBaudRates();

signals:
    void portOpened(bool success);
    void portClosed();
    void dataReceived(const QByteArray &data);
    void connectionLost();
    void errorOccurred(const QString &error);

private slots:
    void onReadyRead();
    void onErrorOccurred(QSerialPort::SerialPortError error);
    void onHeartbeatTimeout();

private:
    QSerialPort *serialPort;
    QTimer *heartbeatTimer;
    bool connected;
    
    // 协议相关
    QByteArray receiveBuffer;
    static const uint8_t FRAME_HEADER = 0xAA;
    static const uint8_t FRAME_FOOTER = 0x55;
    
    uint8_t calculateChecksum(const QByteArray &data);
    bool parseDataFrame(const QByteArray &frame);
};

#endif // SERIALPORTMANAGER_H

3.3 串口管理类实现 (SerialPortManager.cpp)

cpp 复制代码
#include "SerialPortManager.h"
#include <QDebug>

SerialPortManager::SerialPortManager(QObject *parent)
    : QObject(parent), serialPort(nullptr), connected(false)
{
    serialPort = new QSerialPort(this);
    heartbeatTimer = new QTimer(this);
    
    connect(serialPort, &QSerialPort::readyRead, this, &SerialPortManager::onReadyRead);
    connect(serialPort, &QSerialPort::errorOccurred, this, &SerialPortManager::onErrorOccurred);
    connect(heartbeatTimer, &QTimer::timeout, this, &SerialPortManager::onHeartbeatTimeout);
    
    heartbeatTimer->setInterval(1000); // 1秒心跳
}

SerialPortManager::~SerialPortManager()
{
    closePort();
}

bool SerialPortManager::openPort(const QString &portName, int baudRate)
{
    serialPort->setPortName(portName);
    serialPort->setBaudRate(baudRate);
    serialPort->setDataBits(QSerialPort::Data8);
    serialPort->setParity(QSerialPort::NoParity);
    serialPort->setStopBits(QSerialPort::OneStop);
    serialPort->setFlowControl(QSerialPort::NoFlowControl);
    
    if (serialPort->open(QIODevice::ReadWrite)) {
        connected = true;
        heartbeatTimer->start();
        emit portOpened(true);
        qDebug() << "串口打开成功:" << portName;
        return true;
    } else {
        emit portOpened(false);
        qDebug() << "串口打开失败:" << serialPort->errorString();
        return false;
    }
}

void SerialPortManager::closePort()
{
    if (serialPort->isOpen()) {
        heartbeatTimer->stop();
        serialPort->close();
        connected = false;
        emit portClosed();
        qDebug() << "串口已关闭";
    }
}

bool SerialPortManager::sendCommand(uint8_t command, const QByteArray &payload)
{
    if (!connected) return false;
    
    QByteArray frame;
    frame.append(FRAME_HEADER);
    frame.append(static_cast<char>(payload.size() + 1)); // 长度包含命令字
    frame.append(static_cast<char>(command));
    frame.append(payload);
    
    // 计算校验和
    uint8_t checksum = calculateChecksum(frame.mid(1)); // 从长度字节开始
    frame.append(static_cast<char>(checksum));
    frame.append(FRAME_FOOTER);
    
    return sendData(frame);
}

bool SerialPortManager::sendData(const QByteArray &data)
{
    if (!serialPort->isOpen()) return false;
    
    qint64 bytesWritten = serialPort->write(data);
    return bytesWritten == data.size();
}

void SerialPortManager::onReadyRead()
{
    receiveBuffer.append(serialPort->readAll());
    
    // 解析数据帧
    while (receiveBuffer.size() >= 4) {
        int headerIndex = receiveBuffer.indexOf(FRAME_HEADER);
        if (headerIndex == -1) {
            receiveBuffer.clear();
            return;
        }
        
        if (headerIndex > 0) {
            receiveBuffer.remove(0, headerIndex);
        }
        
        if (receiveBuffer.size() < 4) return;
        
        uint8_t length = static_cast<uint8_t>(receiveBuffer[1]);
        int frameSize = 4 + length; // 头+长+数据+校验+尾
        
        if (receiveBuffer.size() < frameSize) return;
        
        QByteArray frame = receiveBuffer.mid(0, frameSize);
        if (parseDataFrame(frame)) {
            receiveBuffer.remove(0, frameSize);
        } else {
            receiveBuffer.remove(0, 1); // 移除错误的头
        }
    }
}

bool SerialPortManager::parseDataFrame(const QByteArray &frame)
{
    if (frame.size() < 4) return false;
    if (static_cast<uint8_t>(frame[0]) != FRAME_HEADER) return false;
    if (static_cast<uint8_t>(frame[frame.size()-1]) != FRAME_FOOTER) return false;
    
    uint8_t length = static_cast<uint8_t>(frame[1]);
    uint8_t command = static_cast<uint8_t>(frame[2]);
    
    if (length < 1) return false;
    
    QByteArray payload = frame.mid(3, length - 1);
    uint8_t receivedChecksum = static_cast<uint8_t>(frame[3 + length - 1]);
    
    // 验证校验和
    QByteArray dataForChecksum = frame.mid(1, 2 + payload.size());
    uint8_t calculatedChecksum = calculateChecksum(dataForChecksum);
    
    if (receivedChecksum != calculatedChecksum) {
        qDebug() << "校验和错误!";
        return false;
    }
    
    // 处理有效数据帧
    emit dataReceived(payload);
    return true;
}

uint8_t SerialPortManager::calculateChecksum(const QByteArray &data)
{
    uint8_t checksum = 0;
    for (int i = 0; i < data.size(); ++i) {
        checksum += static_cast<uint8_t>(data[i]);
    }
    return checksum;
}

QStringList SerialPortManager::getAvailablePorts()
{
    QStringList ports;
    const auto infos = QSerialPortInfo::availablePorts();
    for (const QSerialPortInfo &info : infos) {
        ports << info.portName();
    }
    return ports;
}

void SerialPortManager::onHeartbeatTimeout()
{
    if (connected) {
        sendCommand(CMD_HEARTBEAT);
    }
}

3.4 小车控制类 (CarController.h)

cpp 复制代码
#ifndef CARCONTROLLER_H
#define CARCONTROLLER_H

#include <QObject>
#include <QTimer>
#include "SerialPortManager.h"

class CarController : public QObject
{
    Q_OBJECT

public:
    explicit CarController(QObject *parent = nullptr);
    
    // 运动控制
    void moveForward(int speed = 100);
    void moveBackward(int speed = 100);
    void turnLeft(int speed = 100);
    void turnRight(int speed = 100);
    void stop();
    void emergencyStop();
    
    // 速度控制
    void setSpeed(int leftSpeed, int rightSpeed);
    void setAcceleration(int accel);
    
    // 模式切换
    enum CarMode {
        MODE_MANUAL = 0,
        MODE_AUTO = 1,
        MODE_TRACKING = 2,
        MODE_OBSTACLE_AVOID = 3
    };
    void setMode(CarMode mode);
    
    // 状态查询
    void requestStatus();
    CarStatus getCurrentStatus() const { return currentStatus; }

signals:
    void statusUpdated(const CarStatus &status);
    void connectionStatusChanged(bool connected);
    void errorOccurred(const QString &error);

private slots:
    void onDataReceived(const QByteArray &data);
    void onConnectionLost();

private:
    SerialPortManager *serialManager;
    CarStatus currentStatus;
    QTimer *statusTimer;
    
    void parseStatusData(const QByteArray &data);
};

#endif // CARCONTROLLER_H

3.5 小车控制类实现 (CarController.cpp)

cpp 复制代码
#include "CarController.h"
#include <QDebug>

CarController::CarController(QObject *parent)
    : QObject(parent)
{
    serialManager = new SerialPortManager(this);
    statusTimer = new QTimer(this);
    
    connect(serialManager, &SerialPortManager::dataReceived, 
            this, &CarController::onDataReceived);
    connect(serialManager, &SerialPortManager::connectionLost,
            this, &CarController::onConnectionLost);
    connect(statusTimer, &QTimer::timeout, this, &CarController::requestStatus);
    
    statusTimer->setInterval(1000); // 每秒请求一次状态
}

void CarController::moveForward(int speed)
{
    MotionControl motion;
    motion.direction = 1; // 前进
    motion.speed_left = static_cast<uint8_t>(speed);
    motion.speed_right = static_cast<uint8_t>(speed);
    motion.acceleration = 50;
    
    QByteArray payload(reinterpret_cast<const char*>(&motion), sizeof(MotionControl));
    serialManager->sendCommand(CMD_MOVE_FORWARD, payload);
}

void CarController::turnLeft(int speed)
{
    MotionControl motion;
    motion.direction = 3; // 左转
    motion.speed_left = static_cast<uint8_t>(speed * 0.5);  // 左轮慢
    motion.speed_right = static_cast<uint8_t>(speed);       // 右轮快
    motion.acceleration = 50;
    
    QByteArray payload(reinterpret_cast<const char*>(&motion), sizeof(MotionControl));
    serialManager->sendCommand(CMD_TURN_LEFT, payload);
}

void CarController::stop()
{
    serialManager->sendCommand(CMD_STOP);
}

void CarController::emergencyStop()
{
    stop();
    // 可以添加额外的紧急处理
    qDebug() << "紧急制动!";
}

void CarController::setSpeed(int leftSpeed, int rightSpeed)
{
    MotionControl motion;
    motion.direction = 0; // 停止方向,仅设置速度
    motion.speed_left = static_cast<uint8_t>(leftSpeed);
    motion.speed_right = static_cast<uint8_t>(rightSpeed);
    motion.acceleration = 100;
    
    QByteArray payload(reinterpret_cast<const char*>(&motion), sizeof(MotionControl));
    serialManager->sendCommand(CMD_SET_SPEED, payload);
}

void CarController::setMode(CarMode mode)
{
    uint8_t modeValue = static_cast<uint8_t>(mode);
    QByteArray payload;
    payload.append(static_cast<char>(modeValue));
    serialManager->sendCommand(CMD_SET_MODE, payload);
}

void CarController::requestStatus()
{
    serialManager->sendCommand(CMD_GET_STATUS);
}

void CarController::onDataReceived(const QByteArray &data)
{
    if (data.size() >= static_cast<int>(sizeof(CarStatus))) {
        parseStatusData(data);
        emit statusUpdated(currentStatus);
    }
}

void CarController::parseStatusData(const QByteArray &data)
{
    if (data.size() >= static_cast<int>(sizeof(CarStatus))) {
        memcpy(&currentStatus, data.constData(), sizeof(CarStatus));
        
        qDebug() << "收到状态更新:";
        qDebug() << "  电池电量:" << currentStatus.battery_level << "%";
        qDebug() << "  电池电压:" << currentStatus.voltage << "mV";
        qDebug() << "  左轮速度:" << currentStatus.left_speed << "rpm";
        qDebug() << "  右轮速度:" << currentStatus.right_speed << "rpm";
        qDebug() << "  障碍物距离:" << currentStatus.obstacle_distance << "cm";
    }
}

3.6 主窗口UI设计 (MainWindow.ui XML)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>1000</width>
    <height>700</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>蓝牙小车控制系统</string>
  </property>
  
  <!-- 中央控件 -->
  <widget class="QWidget" name="centralwidget">
   <layout class="QHBoxLayout" name="horizontalLayout">
    
    <!-- 左侧控制面板 -->
    <widget class="QGroupBox" name="controlPanel">
     <property name="title">
      <string>运动控制</string>
     </property>
     <layout class="QVBoxLayout" name="verticalLayout">
      
      <!-- 方向控制按钮 -->
      <widget class="QGridLayout" name="directionLayout">
       <item row="0" column="1">
        <widget class="QPushButton" name="btnForward">
         <property name="text">
          <string>↑ 前进</string>
         </property>
         <property name="minimumSize">
          <size>
           <width>80</width>
           <height>60</height>
          </size>
         </property>
        </widget>
       </item>
       <item row="1" column="0">
        <widget class="QPushButton" name="btnLeft">
         <property name="text">
          <string>← 左转</string>
         </property>
         <property name="minimumSize">
          <size>
           <width>80</width>
           <height>60</height>
          </size>
         </property>
        </widget>
       </item>
       <item row="1" column="1">
        <widget class="QPushButton" name="btnStop">
         <property name="text">
          <string>■ 停止</string>
         </property>
         <property name="minimumSize">
          <size>
           <width>80</width>
           <height>60</height>
          </size>
         </property>
         <property name="styleSheet">
          <string>QPushButton { background-color: red; color: white; }</string>
         </property>
        </widget>
       </item>
       <item row="1" column="2">
        <widget class="QPushButton" name="btnRight">
         <property name="text">
          <string>右转 →</string>
         </property>
         <property name="minimumSize">
          <size>
           <width>80</width>
           <height>60</height>
          </size>
         </property>
        </widget>
       </item>
       <item row="2" column="1">
        <widget class="QPushButton" name="btnBackward">
         <property name="text">
          <string>↓ 后退</string>
         </property>
         <property name="minimumSize">
          <size>
           <width>80</width>
           <height>60</height>
          </size>
         </property>
        </widget>
       </item>
      </widget>
      
      <!-- 速度控制 -->
      <widget class="QGroupBox" name="speedGroup">
       <property name="title">
        <string>速度控制</string>
       </property>
       <layout class="QVBoxLayout" name="speedLayout">
        <widget class="QLabel" name="lblSpeed">
         <property name="text">
          <string>速度: 100%</string>
         </property>
        </widget>
        <widget class="QSlider" name="speedSlider">
         <property name="orientation">
          <enum>Qt::Horizontal</enum>
         </property>
         <property name="minimum">
          <number>0</number>
         </property>
         <property name="maximum">
          <number>255</number>
         </property>
         <property name="value">
          <number>100</number>
         </property>
        </widget>
       </layout>
      </widget>
      
      <!-- 模式选择 -->
      <widget class="QGroupBox" name="modeGroup">
       <property name="title">
        <string>工作模式</string>
       </property>
       <layout class="QVBoxLayout" name="modeLayout">
        <widget class="QRadioButton" name="rbManual">
         <property name="text">
          <string>手动控制</string>
         </property>
         <property name="checked">
          <bool>true</bool>
         </property>
        </widget>
        <widget class="QRadioButton" name="rbAuto">
         <property name="text">
          <string>自动避障</string>
         </property>
        </widget>
        <widget class="QRadioButton" name="rbTracking">
         <property name="text">
          <string>循迹模式</string>
         </property>
        </widget>
       </layout>
      </widget>
      
     </layout>
    </widget>
    
    <!-- 右侧状态面板 -->
    <widget class="QGroupBox" name="statusPanel">
     <property name="title">
      <string>状态监控</string>
     </property>
     <layout class="QVBoxLayout" name="statusLayout">
      
      <!-- 连接状态 -->
      <widget class="QGroupBox" name="connectionGroup">
       <property name="title">
        <string>连接状态</string>
       </property>
       <layout class="QVBoxLayout" name="connLayout">
        <widget class="QLabel" name="lblConnectionStatus">
         <property name="text">
          <string>未连接</string>
         </property>
         <property name="styleSheet">
          <string>QLabel { color: red; font-weight: bold; }</string>
         </property>
        </widget>
        <widget class="QPushButton" name="btnConnect">
         <property name="text">
          <string>连接</string>
         </property>
        </widget>
       </layout>
      </widget>
      
      <!-- 电池状态 -->
      <widget class="QGroupBox" name="batteryGroup">
       <property name="title">
        <string>电池状态</string>
       </property>
       <layout class="QVBoxLayout" name="batteryLayout">
        <widget class="QProgressBar" name="batteryProgress">
         <property name="value">
          <number>85</number>
         </property>
        </widget>
        <widget class="QLabel" name="lblBatteryVoltage">
         <property name="text">
          <string>电压: 7.4V</string>
         </property>
        </widget>
       </layout>
      </widget>
      
      <!-- 实时数据显示 -->
      <widget class="QGroupBox" name="realTimeGroup">
       <property name="title">
        <string>实时数据</string>
       </property>
       <layout class="QFormLayout" name="rtLayout">
        <item row="0" column="0">
         <widget class="QLabel" name="label_1">
          <property name="text">
           <string>左轮速度:</string>
          </property>
         </widget>
        </item>
        <item row="0" column="1">
         <widget class="QLabel" name="lblLeftSpeed">
          <property name="text">
           <string>0 rpm</string>
          </property>
         </widget>
        </item>
        <item row="1" column="0">
         <widget class="QLabel" name="label_2">
          <property name="text">
           <string>右轮速度:</string>
          </property>
         </widget>
        </item>
        <item row="1" column="1">
         <widget class="QLabel" name="lblRightSpeed">
          <property name="text">
           <string>0 rpm</string>
          </property>
         </widget>
        </item>
        <item row="2" column="0">
         <widget class="QLabel" name="label_3">
          <property name="text">
           <string>障碍物距离:</string>
          </property>
         </widget>
        </item>
        <item row="2" column="1">
         <widget class="QLabel" name="lblObstacleDist">
          <property name="text">
           <string>-- cm</string>
          </property>
         </widget>
        </item>
       </layout>
      </widget>
      
     </layout>
    </widget>
    
   </layout>
  </widget>
  
  <!-- 菜单栏 -->
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>1000</width>
     <height>25</height>
    </rect>
   </property>
   <widget class="QMenu" name="menuFile">
    <property name="title">
     <string>文件</string>
    </property>
    <addaction name="actionSaveConfig"/>
    <addaction name="actionLoadConfig"/>
    <addaction name="separator"/>
    <addaction name="actionExit"/>
   </widget>
   <widget class="QMenu" name="menuTools">
    <property name="title">
     <string>工具</string>
    </property>
    <addaction name="actionFirmwareUpdate"/>
    <addaction name="actionCalibration"/>
   </widget>
   <addaction name="menuFile"/>
   <addaction name="menuTools"/>
  </widget>
  
  <!-- 状态栏 -->
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
</ui>

3.7 主窗口实现 (MainWindow.cpp)

cpp 复制代码
#include "MainWindow.h"
#include "ui_MainWindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
    , carController(nullptr)
{
    ui->setupUi(this);
    
    // 初始化串口管理器
    serialManager = new SerialPortManager(this);
    carController = new CarController(this);
    carController->setSerialManager(serialManager);
    
    // 初始化UI
    initUI();
    initConnections();
    
    // 更新可用串口列表
    updateSerialPorts();
}

void MainWindow::initUI()
{
    // 设置窗口图标和样式
    setWindowIcon(QIcon(":/icons/car_icon.png"));
    
    // 初始化串口配置下拉框
    ui->comboBoxPort->addItems(SerialPortManager::getAvailablePorts());
    ui->comboBoxBaudRate->addItems({"9600", "19200", "38400", "57600", "115200"});
    ui->comboBoxBaudRate->setCurrentText("115200");
    
    // 设置速度滑块
    ui->speedSlider->setRange(0, 255);
    ui->speedSlider->setValue(100);
    
    // 初始状态
    updateConnectionStatus(false);
}

void MainWindow::initConnections()
{
    // 连接按钮信号
    connect(ui->btnConnect, &QPushButton::clicked, this, &MainWindow::onConnectClicked);
    connect(ui->btnForward, &QPushButton::pressed, this, &MainWindow::onForwardPressed);
    connect(ui->btnBackward, &QPushButton::pressed, this, &MainWindow::onBackwardPressed);
    connect(ui->btnLeft, &QPushButton::pressed, this, &MainWindow::onLeftPressed);
    connect(ui->btnRight, &QPushButton::pressed, this, &MainWindow::onRightPressed);
    connect(ui->btnStop, &QPushButton::clicked, this, &MainWindow::onStopClicked);
    
    // 速度滑块
    connect(ui->speedSlider, &QSlider::valueChanged, this, &MainWindow::onSpeedChanged);
    
    // 模式切换
    connect(ui->rbManual, &QRadioButton::toggled, this, &MainWindow::onModeChanged);
    connect(ui->rbAuto, &QRadioButton::toggled, this, &MainWindow::onModeChanged);
    connect(ui->rbTracking, &QRadioButton::toggled, this, &MainWindow::onModeChanged);
    
    // 控制器信号
    connect(carController, &CarController::statusUpdated, 
            this, &MainWindow::onStatusUpdated);
    connect(carController, &CarController::connectionStatusChanged,
            this, &MainWindow::updateConnectionStatus);
}

void MainWindow::onConnectClicked()
{
    if (serialManager->isOpen()) {
        serialManager->closePort();
        ui->btnConnect->setText("连接");
    } else {
        QString portName = ui->comboBoxPort->currentText();
        int baudRate = ui->comboBoxBaudRate->currentText().toInt();
        
        if (serialManager->openPort(portName, baudRate)) {
            ui->btnConnect->setText("断开");
            carController->requestStatus();
        }
    }
}

void MainWindow::onForwardPressed()
{
    int speed = ui->speedSlider->value();
    carController->moveForward(speed);
}

void MainWindow::onSpeedChanged(int value)
{
    ui->lblSpeed->setText(QString("速度: %1%").arg(value * 100 / 255));
}

void MainWindow::onStatusUpdated(const CarStatus &status)
{
    // 更新电池状态
    ui->batteryProgress->setValue(status.battery_level);
    ui->lblBatteryVoltage->setText(QString("电压: %1V").arg(status.voltage / 1000.0));
    
    // 更新速度显示
    ui->lblLeftSpeed->setText(QString("%1 rpm").arg(status.left_speed));
    ui->lblRightSpeed->setText(QString("%1 rpm").arg(status.right_speed));
    
    // 更新障碍物距离
    if (status.obstacle_distance < 255) {
        ui->lblObstacleDist->setText(QString("%1 cm").arg(status.obstacle_distance));
    } else {
        ui->lblObstacleDist->setText("-- cm");
    }
    
    // 更新连接状态
    updateConnectionStatus(true);
}

void MainWindow::updateConnectionStatus(bool connected)
{
    if (connected) {
        ui->lblConnectionStatus->setText("已连接");
        ui->lblConnectionStatus->setStyleSheet("QLabel { color: green; font-weight: bold; }");
    } else {
        ui->lblConnectionStatus->setText("未连接");
        ui->lblConnectionStatus->setStyleSheet("QLabel { color: red; font-weight: bold; }");
    }
}

四、STM32下位机代码

4.1 蓝牙接收处理 (bluetooth.c)

c 复制代码
#include "bluetooth.h"
#include "motor_control.h"

#define RX_BUFFER_SIZE 128
uint8_t rx_buffer[RX_BUFFER_SIZE];
uint8_t rx_index = 0;

void Bluetooth_Init(void)
{
    // 初始化USART1用于蓝牙通信
    USART_InitTypeDef USART_InitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
    
    // 配置PA9(TX)和PA10(RX)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    USART_InitStructure.USART_BaudRate = 115200;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    
    USART_Init(USART1, &USART_InitStructure);
    USART_Cmd(USART1, ENABLE);
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
}

void USART1_IRQHandler(void)
{
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
        uint8_t data = USART_ReceiveData(USART1);
        
        if (rx_index < RX_BUFFER_SIZE) {
            rx_buffer[rx_index++] = data;
        }
        
        // 解析数据帧
        if (data == 0x55) { // 帧尾
            ParseDataFrame(rx_buffer, rx_index);
            rx_index = 0;
            memset(rx_buffer, 0, RX_BUFFER_SIZE);
        }
    }
}

void ParseDataFrame(uint8_t *data, uint16_t length)
{
    if (length < 4) return;
    if (data[0] != 0xAA) return; // 检查帧头
    
    uint8_t command = data[2];
    uint8_t *payload = &data[3];
    
    switch (command) {
        case CMD_MOVE_FORWARD:
            HandleMotionControl(payload);
            break;
        case CMD_STOP:
            Motor_Stop();
            break;
        case CMD_GET_STATUS:
            SendStatusResponse();
            break;
        // ... 其他命令处理
    }
}

参考代码 基于Qt的串口上位机控制蓝牙小车程序 www.youwenfan.com/contentcsv/122865.html

五、硬件连接说明

5.1 硬件清单

模块 型号 数量 备注
主控板 STM32F103C8T6 1 72MHz, 64KB Flash
蓝牙模块 HC-05 1 蓝牙2.0,串口透传
电机驱动 L298N 1 双路直流电机驱动
直流电机 12V减速电机 2 带编码器
电源 18650锂电池 2 7.4V 2200mAh
稳压模块 LM2596 1 5V输出给STM32
超声波 HC-SR04 1 避障传感器
循迹模块 TCRT5000 2 红外循迹

5.2 接线图

复制代码
STM32F103C8T6    HC-05蓝牙模块
PA9  (TX1)  -----> RX
PA10 (RX1)  -----> TX
3.3V       -----> VCC
GND        -----> GND

STM32F103C8T6    L298N电机驱动
PB6  (PWM)  -----> ENA
PB7  (PWM)  -----> ENB
PB8        -----> IN1
PB9        -----> IN2
PB10       -----> IN3
PB11       -----> IN4
5V         -----> 5V
GND        -----> GND

六、使用说明

6.1 软件安装

  1. 下载并安装Qt Creator(推荐Qt 5.15+)
  2. 克隆项目代码:git clone https://github.com/your-repo/BluetoothCarController.git
  3. 打开CMakeLists.txt或.pro文件
  4. 编译运行

6.2 硬件配置

  1. 将HC-05蓝牙模块配置为从机模式,波特率115200
  2. 烧录STM32下位机程序
  3. 连接电机和传感器
  4. 上电测试

6.3 操作步骤

  1. 打开Qt上位机软件
  2. 选择正确的串口号和波特率
  3. 点击"连接"按钮
  4. 使用方向按钮控制小车移动
  5. 观察状态监控面板的数据

七、故障排除

问题 解决方法
无法连接蓝牙 检查HC-05是否配对,确认波特率设置
小车不动作 检查电机驱动接线,确认STM32程序正常运行
控制延迟大 降低数据刷新频率,优化通信协议
电池显示异常 校准电压检测电路,检查分压电阻
相关推荐
冰暮流星1 小时前
javascript之this关键字
开发语言·前端·javascript
百度Geek说1 小时前
CodingAgent 的原始森林困境:一张地图能解决什么?
开发语言·javascript·ecmascript·coding agent
sunny.day1 小时前
js原型与原型链
开发语言·javascript·原型模式·js原型链
weixin_523185321 小时前
Java内存模型详解:栈、堆、方法区、本地方法栈与程序计数器
java·开发语言
换个昵称都难1 小时前
WebRTC QoS 实战:从原理到弱网优化
开发语言·php·webrtc
luoyayun3612 小时前
Qt/QML 音频频谱图与频谱瀑布图实现:从 PCM 到频域可视化
qt·音视频·频谱图·频谱瀑布图
爱吃生蚝的于勒2 小时前
QT开发第三章——常用控件
linux·服务器·开发语言·前端·javascript·c++·qt
未若君雅裁2 小时前
工厂模式详解:简单工厂、工厂方法与抽象工厂
java·开发语言
我命由我123452 小时前
由 ImageView 获取到的 Drawable 对象,它的 intrinsicWidth、intrinsicWidth 与实际图片的尺寸
java·开发语言·java-ee·android studio·android jetpack·android-studio·android runtime