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(¤tStatus, 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 软件安装
- 下载并安装Qt Creator(推荐Qt 5.15+)
- 克隆项目代码:
git clone https://github.com/your-repo/BluetoothCarController.git - 打开CMakeLists.txt或.pro文件
- 编译运行
6.2 硬件配置
- 将HC-05蓝牙模块配置为从机模式,波特率115200
- 烧录STM32下位机程序
- 连接电机和传感器
- 上电测试
6.3 操作步骤
- 打开Qt上位机软件
- 选择正确的串口号和波特率
- 点击"连接"按钮
- 使用方向按钮控制小车移动
- 观察状态监控面板的数据
七、故障排除
| 问题 | 解决方法 |
|---|---|
| 无法连接蓝牙 | 检查HC-05是否配对,确认波特率设置 |
| 小车不动作 | 检查电机驱动接线,确认STM32程序正常运行 |
| 控制延迟大 | 降低数据刷新频率,优化通信协议 |
| 电池显示异常 | 校准电压检测电路,检查分压电阻 |