基于STM32设计的电竞选手训练辅助系统设计

举报
DS小龙哥 发表于 2025/12/25 15:15:31 2025/12/25
【摘要】 项目开发背景随着电子竞技行业的迅猛发展,职业选手的训练需求日益增长,操作精准度和反应速度成为决定比赛胜负的关键因素。传统训练方法多依赖教练的主观观察和选手的自我感觉,缺乏客观数据支持,难以系统化地优化操作细节。因此,开发一种能够量化分析选手表现的智能辅助系统,对于提升训练效率和竞技水平具有重要意义。当前电竞训练中,手部动作轨迹和按键频率等关键参数往往无法被实时捕捉和分析,导致选手难以识别操...

项目开发背景

随着电子竞技行业的迅猛发展,职业选手的训练需求日益增长,操作精准度和反应速度成为决定比赛胜负的关键因素。传统训练方法多依赖教练的主观观察和选手的自我感觉,缺乏客观数据支持,难以系统化地优化操作细节。因此,开发一种能够量化分析选手表现的智能辅助系统,对于提升训练效率和竞技水平具有重要意义。

当前电竞训练中,手部动作轨迹和按键频率等关键参数往往无法被实时捕捉和分析,导致选手难以识别操作中的延迟或连贯性问题。本系统通过集成高精度传感器和云平台技术,旨在弥补这一空白,为选手提供科学的训练数据反馈,帮助其快速调整操作习惯,缩短训练周期。

该系统基于STM32微控制器和多种硬件模块,实现了动作捕捉、数据采集和云端交互的一体化设计。通过结合华为云平台和QT上位机,不仅能够生成可视化报告,还能进行历史数据对比,为选手和教练提供个性化的优化建议,推动电竞训练向数据驱动和智能化方向发展。

本项目的开发顺应了电竞产业技术升级的趋势,通过创新应用嵌入式系统和物联网技术,有望为职业选手和业余爱好者提供一套高效、可靠的训练工具,促进整个行业的科学化进程。

设计实现的功能

(1)使用JY901九轴姿态传感器实时采集选手手部动作轨迹。
(2)使用机械键盘矩阵扫描电路实时采集按键频率。
(3)通过STM32F103C8T6主控分析操作延迟与动作连贯性,并生成训练报告。
(4)在0.96寸OLED显示屏上显示实时数据反馈。
(5)通过ESP8266-01S Wi-Fi模块将训练数据及操作热区分布图上传至华为云平台。
(6)QT上位机从云平台获取数据,提供操作优化建议与历史数据对比分析。

项目硬件模块组成

(1)STM32F103C8T6最小系统核心板(主控)
(2)JY901九轴姿态传感器(手部动作捕捉)
(3)机械键盘矩阵扫描电路(按键频率采集)
(4)0.96寸OLED显示屏(实时数据反馈)
(5)ESP8266-01S Wi-Fi模块(华为云通信)
(6)洞洞板焊接传感器接口电路,杜邦线连接各模块

设计意义

本设计基于STM32的电竞选手训练辅助系统,通过集成手部动作捕捉与按键频率采集功能,能够实时监控选手的操作细节,从而为训练提供客观数据支持。系统能够帮助选手和教练精准识别操作中的延迟与动作连贯性问题,替代传统主观评价方式,提升训练的科学性与针对性,有效缩短选手技能提升周期。

系统将训练数据及操作热区分布上传至华为云平台,实现了数据的远程存储与共享,便于长期跟踪和分析选手表现趋势。结合QT上位机提供的优化建议与历史数据对比功能,教练可以快速制定个性化训练方案,同时为团队战术调整提供可靠依据,增强电竞训练的数据驱动决策能力。

硬件设计采用STM32主控与多种传感器模块的紧凑组合,通过洞洞板焊接和杜邦线连接,确保了系统的灵活性与低成本实现。这种模块化结构不仅便于维护和扩展,还体现了在资源受限环境下开发实用电竞辅助工具的可行性,对推广普及科学训练方法具有实际意义。

设计思路

该系统设计以STM32F103C8T6最小系统核心板作为主控制器,负责协调各个硬件模块的工作流程,实现电竞选手训练数据的实时采集、处理与传输。系统通过集成JY901九轴姿态传感器和机械键盘矩阵扫描电路,分别捕捉手部动作轨迹和按键频率,确保数据采集的准确性和实时性。OLED显示屏用于即时显示关键指标,如操作延迟和连贯性评分,为选手提供现场反馈。整个硬件系统通过洞洞板焊接接口电路,并使用杜邦线连接各模块,确保结构紧凑且易于调试。

手部动作轨迹采集由JY901传感器完成,该传感器通过I2C或UART接口与STM32通信,实时输出加速度、角速度和磁力计数据。STM32对原始数据进行滤波和坐标转换,计算出选手手部的三维运动轨迹,并记录动作的时间戳。按键频率采集通过机械键盘矩阵扫描电路实现,STM32周期扫描键盘矩阵,检测按键按下和释放事件,统计单位时间内的操作次数和间隔,从而评估按键节奏和响应速度。

数据处理与分析在STM32内部完成,系统结合手部动作轨迹和按键频率数据,计算操作延迟(如按键响应时间)和动作连贯性(如轨迹平滑度)。通过预设算法,STM32生成训练报告,包括关键性能指标和改进建议,报告内容通过OLED显示屏实时显示,并暂存于内部存储器中。这一过程确保了训练反馈的即时性和客观性。

数据上传功能通过ESP8266-01S Wi-Fi模块实现,STM32将训练报告和操作热区分布图(基于动作轨迹生成)封装为数据包,通过AT指令集与模块通信,连接至华为云平台。上传过程采用MQTT或HTTP协议,确保数据安全传输至云端存储,便于后续远程访问和分析。

QT上位机作为系统的辅助工具,从华为云平台获取历史训练数据,进行可视化处理。它提供操作优化建议,例如针对延迟过高或动作不连贯的专项训练方案,并通过图表对比历史数据,帮助选手追踪进步趋势。上位机界面设计直观,支持数据导出和报告生成,增强了系统的整体实用性和用户体验。

框架图

+-------------------+       +-----------------------+
|   JY901传感器     |<----->|                       |
| (手部动作捕捉)    |       |                       |
+-------------------+       |                       |
                            |   STM32F103C8T6       |
+-------------------+       |   (主控)             |
| 键盘矩阵扫描电路  |<----->|   -数据采集          |
| (按键频率采集)    |       |   -延迟分析          |
+-------------------+       |   -报告生成          |
                            |                       |
+-------------------+       |                       |
|   OLED显示屏      |<----->|                       |
| (实时数据反馈)    |       |                       |
+-------------------+       +-----------------------+
                                    |
                                    | (UART通信)
                                    v
                            +-------------------+
                            |  ESP8266-01S      |
                            | (Wi-Fi模块)       |
                            +-------------------+
                                    |
                                    | (Wi-Fi传输)
                                    v
                            +-------------------+
                            |   华为云平台       |
                            | (数据存储与处理)  |
                            +-------------------+
                                    |
                                    | (API接口)
                                    v
                            +-------------------+
                            |   QT上位机        |
                            | (优化建议与分析)  |
                            +-------------------+

系统总体设计

本系统以STM32F103C8T6最小系统核心板作为主控制器,负责协调整个电竞选手训练辅助系统的运行。系统通过JY901九轴姿态传感器实时捕捉选手手部动作轨迹,结合机械键盘矩阵扫描电路采集按键频率数据,从而实现对操作行为的全面监控。这些硬件模块通过洞洞板焊接的接口电路和杜邦线连接,确保信号稳定传输,同时0.96寸OLED显示屏用于实时显示关键数据,如当前操作状态和初步分析结果,方便选手在训练中即时调整。

数据采集后,STM32主控对操作延迟和动作连贯性进行分析,通过内置算法计算响应时间和动作流畅度,生成详细的训练报告。报告内容包括操作效率评估和潜在问题点,为后续优化提供依据。同时,系统利用ESP8266-01S Wi-Fi模块将训练数据及操作热区分布图上传至华为云平台,实现远程存储和管理,确保数据安全可靠。

上传至华为云的数据可通过QT上位机进行进一步处理,该上位机提供操作优化建议和历史数据对比分析功能。选手和教练可以通过上位机界面查看趋势图表和热区分布,识别操作弱点并制定针对性训练计划。整个系统设计注重实用性和可扩展性,各模块协同工作,从数据采集到云端交互形成闭环,有效提升电竞选手的训练效率。

系统功能总结

功能模块 功能描述 实现硬件/组件
手部动作采集 实时采集选手操作时的手部动作轨迹 JY901九轴姿态传感器
按键频率采集 实时采集选手的按键频率 机械键盘矩阵扫描电路
数据处理与分析 分析操作延迟与动作连贯性,生成训练报告 STM32F103C8T6核心板
实时数据反馈 显示实时采集的数据和分析结果 0.96寸OLED显示屏
云数据上传 将训练数据及操作热区分布图上传至华为云平台 ESP8266-01S Wi-Fi模块
上位机分析与建议 QT上位机提供操作优化建议与历史数据对比分析 通过Wi-Fi模块与云平台通信,QT软件
系统连接与集成 焊接传感器接口电路,连接各模块 洞洞板、杜邦线

设计的各个功能模块描述

主控模块基于STM32F103C8T6最小系统核心板,作为系统的控制中心,负责初始化和管理各外设模块。它实时采集JY901传感器和键盘矩阵的数据,进行数据处理与分析,包括计算操作延迟、评估动作连贯性,并生成训练报告。同时,主控模块协调OLED显示屏的实时反馈和ESP8266模块的云平台通信,确保系统整体运行稳定。

手部动作捕捉模块采用JY901九轴姿态传感器,通过I2C或UART接口与STM32连接,实时采集选手手部的加速度、角速度和磁力计数据。这些数据用于重构手部运动轨迹,捕捉细微动作变化,为分析操作连贯性和延迟提供原始输入。

按键频率采集模块通过机械键盘矩阵扫描电路实现,STM32利用GPIO引脚配置矩阵扫描,检测按键事件并记录按键时间和频率。该模块能够准确捕捉选手的操作节奏和速度,为训练报告中的按键分析提供基础数据。

实时数据反馈模块使用0.96寸OLED显示屏,通过I2C或SPI接口与STM32通信,动态显示关键指标如按键频率、动作参数或简单统计信息。这使得选手能够即时查看训练状态,辅助调整操作策略。

云通信模块依托ESP8266-01S Wi-Fi模块,通过UART与STM32连接,配置为STA模式接入无线网络。该模块负责将处理后的训练数据、操作热区分布图等通过MQTT或HTTP协议上传至华为云平台,实现数据的远程存储和共享。

接口电路模块在洞洞板上焊接各传感器和模块的接口电路,使用杜邦线灵活连接STM32、JY901、键盘矩阵、OLED和ESP8266等组件。这种设计确保了信号传输的稳定性,并便于系统的组装与调试。

上位机代码设计

#include <QApplication>
#include <QMainWindow>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QWidget>
#include <QLabel>
#include <QPushButton>
#include <QTextEdit>
#include <QChartView>
#include <QLineSeries>
#include <QValueAxis>
#include <QScatterSeries>
#include <QTimer>
#include <QTcpSocket>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDateTime>
#include <QFile>
#include <QMessageBox>
#include <QSplitter>
#include <QTabWidget>

QT_CHARTS_USE_NAMESPACE

class TrainingData {
public:
    double delay;
    double continuity;
    QVector<QPointF> trajectory;
    QVector<int> keyFrequency;
    QDateTime timestamp;
};

class DataAnalyzer {
public:
    static double calculateAverageDelay(const QVector<TrainingData>& data) {
        if(data.isEmpty()) return 0;
        double sum = 0;
        for(const auto& d : data) {
            sum += d.delay;
        }
        return sum / data.size();
    }
    
    static double calculateConsistency(const QVector<TrainingData>& data) {
        if(data.size() < 2) return 0;
        double sum = 0;
        for(int i = 1; i < data.size(); i++) {
            sum += qAbs(data[i].delay - data[i-1].delay);
        }
        return 1.0 / (1.0 + sum / (data.size() - 1));
    }
};

class CloudManager : public QObject {
    Q_OBJECT
private:
    QTcpSocket *socket;
    QString cloudUrl = "192.168.1.100"; // 华为云IP
    int cloudPort = 8080;
    
public:
    CloudManager(QObject *parent = nullptr) : QObject(parent) {
        socket = new QTcpSocket(this);
    }
    
    void uploadData(const TrainingData& data) {
        QJsonObject json;
        json["timestamp"] = data.timestamp.toString(Qt::ISODate);
        json["delay"] = data.delay;
        json["continuity"] = data.continuity;
        
        QJsonArray trajArray;
        for(const auto& point : data.trajectory) {
            QJsonObject pointObj;
            pointObj["x"] = point.x();
            pointObj["y"] = point.y();
            trajArray.append(pointObj);
        }
        json["trajectory"] = trajArray;
        
        QJsonArray keyArray;
        for(int freq : data.keyFrequency) {
            keyArray.append(freq);
        }
        json["key_frequency"] = keyArray;
        
        QJsonDocument doc(json);
        QByteArray dataToSend = doc.toJson();
        
        socket->connectToHost(cloudUrl, cloudPort);
        if(socket->waitForConnected(3000)) {
            socket->write(dataToSend);
            socket->waitForBytesWritten(1000);
        }
        socket->disconnectFromHost();
    }
};

class MainWindow : public QMainWindow {
    Q_OBJECT
    
private:
    QVector<TrainingData> historicalData;
    CloudManager *cloudManager;
    
    // UI组件
    QTabWidget *tabWidget;
    QWidget *realTimeTab;
    QWidget *analysisTab;
    QWidget *historyTab;
    
    // 实时数据显示
    QLabel *delayLabel;
    QLabel *continuityLabel;
    QTextEdit *suggestionText;
    QChartView *trajectoryChartView;
    QChartView *heatMapChartView;
    
    // 历史数据分析
    QChartView *trendChartView;
    QTextEdit *comparisonText;
    
    QTimer *dataTimer;
    
public:
    MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) {
        cloudManager = new CloudManager(this);
        setupUI();
        setupCharts();
        setupConnections();
        
        dataTimer = new QTimer(this);
        dataTimer->start(1000); // 1秒更新一次
    }
    
private:
    void setupUI() {
        setWindowTitle("电竞选手训练辅助系统");
        setMinimumSize(1200, 800);
        
        tabWidget = new QTabWidget(this);
        
        // 实时数据标签页
        realTimeTab = createRealTimeTab();
        analysisTab = createAnalysisTab();
        historyTab = createHistoryTab();
        
        tabWidget->addTab(realTimeTab, "实时数据");
        tabWidget->addTab(analysisTab, "数据分析");
        tabWidget->addTab(historyTab, "历史对比");
        
        setCentralWidget(tabWidget);
    }
    
    QWidget* createRealTimeTab() {
        QWidget *tab = new QWidget();
        QVBoxLayout *layout = new QVBoxLayout(tab);
        
        // 顶部状态显示
        QHBoxLayout *statusLayout = new QHBoxLayout();
        delayLabel = new QLabel("操作延迟: -- ms");
        continuityLabel = new QLabel("动作连贯性: --");
        statusLayout->addWidget(delayLabel);
        statusLayout->addWidget(continuityLabel);
        statusLayout->addStretch();
        
        // 图表区域
        QHBoxLayout *chartLayout = new QHBoxLayout();
        trajectoryChartView = new QChartView();
        trajectoryChartView->setMinimumSize(400, 300);
        heatMapChartView = new QChartView();
        heatMapChartView->setMinimumSize(400, 300);
        chartLayout->addWidget(trajectoryChartView);
        chartLayout->addWidget(heatMapChartView);
        
        // 建议区域
        suggestionText = new QTextEdit();
        suggestionText->setPlaceholderText("优化建议将显示在这里...");
        
        layout->addLayout(statusLayout);
        layout->addLayout(chartLayout);
        layout->addWidget(suggestionText);
        
        return tab;
    }
    
    QWidget* createAnalysisTab() {
        QWidget *tab = new QWidget();
        QVBoxLayout *layout = new QVBoxLayout(tab);
        
        trendChartView = new QChartView();
        trendChartView->setMinimumSize(800, 400);
        
        QPushButton *generateReportBtn = new QPushButton("生成训练报告");
        connect(generateReportBtn, &QPushButton::clicked, this, &MainWindow::generateTrainingReport);
        
        layout->addWidget(trendChartView);
        layout->addWidget(generateReportBtn);
        
        return tab;
    }
    
    QWidget* createHistoryTab() {
        QWidget *tab = new QWidget();
        QVBoxLayout *layout = new QVBoxLayout(tab);
        
        comparisonText = new QTextEdit();
        comparisonText->setPlaceholderText("历史数据对比分析将显示在这里...");
        
        QPushButton *compareBtn = new QPushButton("对比分析");
        connect(compareBtn, &QPushButton::clicked, this, &MainWindow::performComparison);
        
        layout->addWidget(comparisonText);
        layout->addWidget(compareBtn);
        
        return tab;
    }
    
    void setupCharts() {
        setupTrajectoryChart();
        setupHeatMapChart();
        setupTrendChart();
    }
    
    void setupTrajectoryChart() {
        QChart *chart = new QChart();
        chart->setTitle("手部运动轨迹");
        
        QLineSeries *series = new QLineSeries();
        series->setName("实时轨迹");
        
        chart->addSeries(series);
        chart->createDefaultAxes();
        chart->axes(Qt::Horizontal).first()->setRange(-100, 100);
        chart->axes(Qt::Vertical).first()->setRange(-100, 100);
        
        trajectoryChartView->setChart(chart);
    }
    
    void setupHeatMapChart() {
        QChart *chart = new QChart();
        chart->setTitle("操作热区分布");
        
        QScatterSeries *series = new QScatterSeries();
        series->setName("热区分布");
        series->setMarkerSize(10.0);
        
        chart->addSeries(series);
        chart->createDefaultAxes();
        chart->axes(Qt::Horizontal).first()->setRange(0, 10);
        chart->axes(Qt::Vertical).first()->setRange(0, 10);
        
        heatMapChartView->setChart(chart);
    }
    
    void setupTrendChart() {
        QChart *chart = new QChart();
        chart->setTitle("训练趋势分析");
        
        QLineSeries *delaySeries = new QLineSeries();
        delaySeries->setName("操作延迟");
        
        QLineSeries *continuitySeries = new QLineSeries();
        continuitySeries->setName("动作连贯性");
        
        chart->addSeries(delaySeries);
        chart->addSeries(continuitySeries);
        chart->createDefaultAxes();
        
        trendChartView->setChart(chart);
    }
    
    void setupConnections() {
        connect(dataTimer, &QTimer::timeout, this, &MainWindow::updateRealTimeData);
    }
    
private slots:
    void updateRealTimeData() {
        // 模拟从STM32接收数据
        TrainingData newData;
        newData.delay = 50 + (qrand() % 50); // 50-100ms延迟
        newData.continuity = 0.7 + (qrand() % 30) / 100.0; // 0.7-1.0连贯性
        
        // 生成模拟轨迹数据
        for(int i = 0; i < 20; i++) {
            newData.trajectory.append(QPointF(
                (qrand() % 200) - 100,
                (qrand() % 200) - 100
            ));
        }
        
        // 生成模拟按键频率
        for(int i = 0; i < 10; i++) {
            newData.keyFrequency.append(qrand() % 100);
        }
        
        newData.timestamp = QDateTime::currentDateTime();
        
        // 更新显示
        delayLabel->setText(QString("操作延迟: %1 ms").arg(newData.delay));
        continuityLabel->setText(QString("动作连贯性: %1").arg(newData.continuity, 0, 'f', 2));
        
        updateTrajectoryChart(newData);
        updateHeatMapChart(newData);
        updateSuggestions(newData);
        
        // 保存并上传数据
        historicalData.append(newData);
        cloudManager->uploadData(newData);
    }
    
    void updateTrajectoryChart(const TrainingData& data) {
        QChart *chart = trajectoryChartView->chart();
        QLineSeries *series = qobject_cast<QLineSeries*>(chart->series().first());
        
        series->clear();
        for(const auto& point : data.trajectory) {
            series->append(point);
        }
        
        chart->update();
    }
    
    void updateHeatMapChart(const TrainingData& data) {
        QChart *chart = heatMapChartView->chart();
        QScatterSeries *series = qobject_cast<QScatterSeries*>(chart->series().first());
        
        series->clear();
        // 模拟热区数据
        for(int i = 0; i < 10; i++) {
            series->append(qrand() % 10, qrand() % 10);
        }
        
        chart->update();
    }
    
    void updateSuggestions(const TrainingData& data) {
        QString suggestions;
        
        if(data.delay > 80) {
            suggestions += "? 操作延迟较高,建议练习反应速度训练\n";
        }
        if(data.continuity < 0.8) {
            suggestions += "? 动作连贯性有待提高,建议进行连贯性练习\n";
        }
        if(data.keyFrequency.size() > 0 && data.keyFrequency[0] < 30) {
            suggestions += "? 按键频率较低,建议提高操作频率\n";
        }
        
        if(suggestions.isEmpty()) {
            suggestions = "? 当前表现良好,继续保持!";
        }
        
        suggestionText->setText(suggestions);
    }
    
    void generateTrainingReport() {
        if(historicalData.isEmpty()) {
            QMessageBox::warning(this, "警告", "没有训练数据可生成报告");
            return;
        }
        
        QString report = QString("电竞选手训练报告\n"
                                "生成时间: %1\n"
                                "训练次数: %2\n"
                                "平均操作延迟: %3 ms\n"
                                "平均连贯性: %4\n"
                                "训练一致性: %5\n\n")
                                .arg(QDateTime::currentDateTime().toString())
                                .arg(historicalData.size())
                                .arg(DataAnalyzer::calculateAverageDelay(historicalData))
                                .arg(DataAnalyzer::calculateAverageDelay(historicalData) / 100.0, 0, 'f', 2)
                                .arg(DataAnalyzer::calculateConsistency(historicalData), 0, 'f', 2);
        
        QMessageBox::information(this, "训练报告", report);
    }
    
    void performComparison() {
        if(historicalData.size() < 2) {
            comparisonText->setText("需要至少2次训练数据才能进行对比分析");
            return;
        }
        
        TrainingData latest = historicalData.last();
        TrainingData previous = historicalData[historicalData.size()-2];
        
        QString comparison = QString("最近两次训练对比分析:\n\n"
                                    "操作延迟: %1 ms → %2 ms (%3%)\n"
                                    "动作连贯性: %4 → %5 (%6%)\n\n")
                                    .arg(previous.delay)
                                    .arg(latest.delay)
                                    .arg(((latest.delay - previous.delay) / previous.delay) * 100, 0, 'f', 1)
                                    .arg(previous.continuity, 0, 'f', 2)
                                    .arg(latest.continuity, 0, 'f', 2)
                                    .arg(((latest.continuity - previous.continuity) / previous.continuity) * 100, 0, 'f', 1);
        
        if(latest.delay < previous.delay && latest.continuity > previous.continuity) {
            comparison += "分析结果: 训练效果显著提升!";
        } else {
            comparison += "分析结果: 需要继续努力训练";
        }
        
        comparisonText->setText(comparison);
    }
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    MainWindow window;
    window.show();
    
    return app.exec();
}

#include "main.moc"

模块代码设计

#include "stm32f10x.h"

// OLED显示定义
#define OLED_ADDRESS 0x78
#define OLED_CMD 0x00
#define OLED_DATA 0x40

// JY901传感器数据结构
typedef struct {
    float accel[3];
    float gyro[3];
    float angle[3];
} JY901_Data;

// 键盘矩阵定义
#define ROWS 4
#define COLS 4
uint8_t key_matrix[ROWS][COLS] = {0};
uint8_t key_frequency[ROWS][COLS] = {0};

// 系统状态变量
JY901_Data hand_data;
uint32_t operation_count = 0;
uint32_t total_delay = 0;

// 延时函数
void Delay_ms(uint32_t n) {
    for(uint32_t i=0; i<n; i++)
        for(uint32_t j=0; j<7200; j++);
}

// GPIO初始化
void GPIO_Init(void) {
    // 使能时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN;
    
    // USART1 TX(PA9) RX(PA10) - JY901
    GPIOA->CRH &= ~(GPIO_CRH_CNF9 | GPIO_CRH_MODE9 | GPIO_CRH_CNF10 | GPIO_CRH_MODE10);
    GPIOA->CRH |= GPIO_CRH_CNF9_1 | GPIO_CRH_MODE9_0 | GPIO_CRH_CNF10_0;
    
    // USART2 TX(PA2) RX(PA3) - ESP8266
    GPIOA->CRL &= ~(GPIO_CRL_CNF2 | GPIO_CRL_MODE2 | GPIO_CRL_CNF3 | GPIO_CRL_MODE3);
    GPIOA->CRL |= GPIO_CRL_CNF2_1 | GPIO_CRL_MODE2_0 | GPIO_CRL_CNF3_0;
    
    // I2C1 SCL(PB6) SDA(PB7) - OLED
    GPIOB->CRL &= ~(GPIO_CRL_CNF6 | GPIO_CRL_MODE6 | GPIO_CRL_CNF7 | GPIO_CRL_MODE7);
    GPIOB->CRL |= GPIO_CRL_CNF6_1 | GPIO_CRL_MODE6_0 | GPIO_CRL_CNF7_1 | GPIO_CRL_MODE7_0;
    
    // 键盘矩阵行输出(PC0-PC3)
    GPIOC->CRL &= ~(GPIO_CRL_CNF0 | GPIO_CRL_MODE0 | GPIO_CRL_CNF1 | GPIO_CRL_MODE1 |
                   GPIO_CRL_CNF2 | GPIO_CRL_MODE2 | GPIO_CRL_CNF3 | GPIO_CRL_MODE3);
    GPIOC->CRL |= GPIO_CRL_MODE0_0 | GPIO_CRL_MODE1_0 | GPIO_CRL_MODE2_0 | GPIO_CRL_MODE3_0;
    
    // 键盘矩阵列输入(PC4-PC7) 上拉输入
    GPIOC->CRL &= ~(GPIO_CRL_CNF4 | GPIO_CRL_MODE4 | GPIO_CRL_CNF5 | GPIO_CRL_MODE5 |
                   GPIO_CRL_CNF6 | GPIO_CRL_MODE6 | GPIO_CRL_CNF7 | GPIO_CRL_MODE7);
    GPIOC->CRL |= GPIO_CRL_CNF4_1 | GPIO_CRL_CNF5_1 | GPIO_CRL_CNF6_1 | GPIO_CRL_CNF7_1;
    GPIOC->ODR |= GPIO_ODR_ODR4 | GPIO_ODR_ODR5 | GPIO_ODR_ODR6 | GPIO_ODR_ODR7;
}

// USART1初始化 - JY901
void USART1_Init(void) {
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    
    USART1->BRR = 72000000 / 9600;
    USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
    USART1->CR1 |= USART_CR1_RXNEIE;
    
    NVIC_EnableIRQ(USART1_IRQn);
}

// USART2初始化 - ESP8266
void USART2_Init(void) {
    RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
    
    USART2->BRR = 36000000 / 115200;
    USART2->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
}

// I2C1初始化 - OLED
void I2C1_Init(void) {
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    
    I2C1->CR1 &= ~I2C_CR1_PE;
    I2C1->CR2 = 36;
    I2C1->CCR = 180;
    I2C1->TRISE = 37;
    I2C1->CR1 |= I2C_CR1_PE;
}

// I2C起始信号
void I2C_Start(void) {
    I2C1->CR1 |= I2C_CR1_START;
    while(!(I2C1->SR1 & I2C_SR1_SB));
    I2C1->SR1;
}

// I2C停止信号
void I2C_Stop(void) {
    I2C1->CR1 |= I2C_CR1_STOP;
    while(I2C1->CR1 & I2C_CR1_STOP);
}

// I2C发送地址
void I2C_Address(uint8_t addr) {
    I2C1->DR = addr;
    while(!(I2C1->SR1 & I2C_SR1_ADDR));
    I2C1->SR1;
    I2C1->SR2;
}

// I2C发送数据
void I2C_Write(uint8_t data) {
    I2C1->DR = data;
    while(!(I2C1->SR1 & I2C_SR1_TXE));
}

// OLED初始化
void OLED_Init(void) {
    I2C_Start();
    I2C_Address(OLED_ADDRESS);
    I2C_Write(OLED_CMD);
    
    // 初始化命令序列
    uint8_t init_cmd[] = {
        0xAE, 0x20, 0x10, 0xB0, 0xC8, 0x00, 0x10, 0x40, 0x81, 0xFF,
        0xA1, 0xA6, 0xA8, 0x3F, 0xA4, 0xD3, 0x00, 0xD5, 0xF0, 0xD9,
        0x22, 0xDA, 0x12, 0xDB, 0x20, 0x8D, 0x14, 0xAF
    };
    
    for(int i=0; i<sizeof(init_cmd); i++) {
        I2C_Write(init_cmd[i]);
    }
    I2C_Stop();
}

// OLED显示字符串
void OLED_ShowString(uint8_t x, uint8_t y, char *str) {
    I2C_Start();
    I2C_Address(OLED_ADDRESS);
    I2C_Write(OLED_CMD);
    I2C_Write(0xB0 + y);
    I2C_Write(((x & 0xF0) >> 4) | 0x10);
    I2C_Write((x & 0x0F) | 0x01);
    I2C_Stop();
    
    I2C_Start();
    I2C_Address(OLED_ADDRESS);
    I2C_Write(OLED_DATA);
    
    while(*str) {
        I2C_Write(*str++);
    }
    I2C_Stop();
}

// JY901数据解析
void JY901_ParseData(uint8_t *data) {
    if(data[0] == 0x55 && data[1] == 0x51) {
        // 加速度数据
        hand_data.accel[0] = (int16_t)(data[3]<<8 | data[2]) / 32768.0 * 16;
        hand_data.accel[1] = (int16_t)(data[5]<<8 | data[4]) / 32768.0 * 16;
        hand_data.accel[2] = (int16_t)(data[7]<<8 | data[6]) / 32768.0 * 16;
    }
    else if(data[0] == 0x55 && data[1] == 0x52) {
        // 角速度数据
        hand_data.gyro[0] = (int16_t)(data[3]<<8 | data[2]) / 32768.0 * 2000;
        hand_data.gyro[1] = (int16_t)(data[5]<<8 | data[4]) / 32768.0 * 2000;
        hand_data.gyro[2] = (int16_t)(data[7]<<8 | data[6]) / 32768.0 * 2000;
    }
    else if(data[0] == 0x55 && data[1] == 0x53) {
        // 角度数据
        hand_data.angle[0] = (int16_t)(data[3]<<8 | data[2]) / 32768.0 * 180;
        hand_data.angle[1] = (int16_t)(data[5]<<8 | data[4]) / 32768.0 * 180;
        hand_data.angle[2] = (int16_t)(data[7]<<8 | data[6]) / 32768.0 * 180;
    }
}

// 键盘矩阵扫描
void Keyboard_Scan(void) {
    static uint32_t last_scan = 0;
    if(SystemCoreClock - last_scan < 10000) return;
    last_scan = SystemCoreClock;
    
    for(int row=0; row<ROWS; row++) {
        // 设置当前行为低电平
        GPIOC->BRR = (1 << row);
        Delay_ms(1);
        
        for(int col=0; col<COLS; col++) {
            if(!(GPIOC->IDR & (1 << (col+4)))) {
                if(key_matrix[row][col] == 0) {
                    key_matrix[row][col] = 1;
                    key_frequency[row][col]++;
                    operation_count++;
                    
                    // 记录操作延迟
                    total_delay += SystemCoreClock - last_scan;
                }
            } else {
                key_matrix[row][col] = 0;
            }
        }
        
        // 恢复当前行为高电平
        GPIOC->BSRR = (1 << row);
    }
}

// ESP8266发送数据到华为云
void ESP8266_SendToCloud(void) {
    char buffer[256];
    
    // 构建JSON数据
    sprintf(buffer, "{\"accel\":[%.2f,%.2f,%.2f],\"gyro\":[%.2f,%.2f,%.2f],\"ops\":%lu,\"delay\":%lu}",
            hand_data.accel[0], hand_data.accel[1], hand_data.accel[2],
            hand_data.gyro[0], hand_data.gyro[1], hand_data.gyro[2],
            operation_count, total_delay);
    
    // 发送AT指令
    USART2_SendString("AT+CIPSTART=\"TCP\",\"192.168.1.100\",1883\r\n");
    Delay_ms(1000);
    USART2_SendString("AT+CIPSEND=");
    USART2_SendNumber(strlen(buffer));
    USART2_SendString("\r\n");
    Delay_ms(100);
    USART2_SendString(buffer);
    USART2_SendString("\r\n");
}

// USART2发送字符串
void USART2_SendString(char *str) {
    while(*str) {
        while(!(USART2->SR & USART_SR_TXE));
        USART2->DR = *str++;
    }
}

// USART2发送数字
void USART2_SendNumber(uint32_t num) {
    char buffer[10];
    sprintf(buffer, "%lu", num);
    USART2_SendString(buffer);
}

// USART1中断服务函数 - JY901数据接收
void USART1_IRQHandler(void) {
    static uint8_t rx_buffer[11];
    static uint8_t rx_index = 0;
    
    if(USART1->SR & USART_SR_RXNE) {
        uint8_t data = USART1->DR;
        
        if(rx_index == 0 && data == 0x55) {
            rx_buffer[rx_index++] = data;
        }
        else if(rx_index > 0) {
            rx_buffer[rx_index++] = data;
            if(rx_index >= 11) {
                JY901_ParseData(rx_buffer);
                rx_index = 0;
            }
        }
    }
}

// 系统初始化
void System_Init(void) {
    SystemInit();
    GPIO_Init();
    USART1_Init();
    USART2_Init();
    I2C1_Init();
    OLED_Init();
    
    // 显示初始信息
    OLED_ShowString(0, 0, "E-Sports Trainer");
    OLED_ShowString(0, 2, "Initializing...");
    Delay_ms(2000);
}

// 主函数
int main(void) {
    System_Init();
    
    char display_buffer[32];
    uint32_t last_upload = 0;
    
    while(1) {
        // 键盘扫描
        Keyboard_Scan();
        
        // 更新OLED显示
        sprintf(display_buffer, "Ops:%lu Delay:%lu", operation_count, total_delay);
        OLED_ShowString(0, 0, display_buffer);
        
        sprintf(display_buffer, "Acc:%.1f,%.1f,%.1f", hand_data.accel[0], hand_data.accel[1], hand_data.accel[2]);
        OLED_ShowString(0, 2, display_buffer);
        
        sprintf(display_buffer, "Gyro:%.1f,%.1f,%.1f", hand_data.gyro[0], hand_data.gyro[1], hand_data.gyro[2]);
        OLED_ShowString(0, 4, display_buffer);
        
        // 每5秒上传数据到云平台
        if(SystemCoreClock - last_upload > 5000000) {
            ESP8266_SendToCloud();
            last_upload = SystemCoreClock;
        }
        
        Delay_ms(100);
    }
}

项目核心代码

#include "stm32f10x.h"
#include "jy901.h"
#include "keyboard.h"
#include "oled.h"
#include "esp8266.h"
#include "cloud.h"
#include <stdio.h>
#include <string.h>

// 系统状态定义
typedef enum {
    SYSTEM_IDLE = 0,
    SYSTEM_TRAINING,
    SYSTEM_UPLOADING,
    SYSTEM_ERROR
} SystemState;

// 训练数据结构
typedef struct {
    float hand_position[3];      // 手部位置
    float hand_rotation[3];      // 手部旋转
    uint32_t key_press_count;    // 按键次数
    uint32_t key_sequence[32];   // 按键序列
    uint32_t timestamp;          // 时间戳
} TrainingData;

// 全局变量
volatile SystemState system_state = SYSTEM_IDLE;
TrainingData current_data;
uint32_t training_start_time = 0;
uint8_t data_buffer[512];

// 系统时钟初始化
void SystemClock_Config(void) {
    // 开启HSE
    RCC->CR |= RCC_CR_HSEON;
    while(!(RCC->CR & RCC_CR_HSERDY));
    
    // 配置PLL 8MHz * 9 = 72MHz
    RCC->CFGR |= RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9;
    RCC->CR |= RCC_CR_PLLON;
    while(!(RCC->CR & RCC_CR_PLLRDY));
    
    // 配置Flash延迟
    FLASH->ACR |= FLASH_ACR_LATENCY_2 | FLASH_ACR_PRFTBE;
    
    // 切换系统时钟到PLL
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
    
    // 配置AHB、APB2、APB1预分频器
    RCC->CFGR |= RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE2_DIV1 | RCC_CFGR_PPRE1_DIV2;
}

// GPIO初始化
void GPIO_Config(void) {
    // 开启GPIO时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN;
    
    // 配置LED指示灯 PC13
    GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
    GPIOC->CRH |= GPIO_CRH_MODE13_0;  // 输出模式,最大速度2MHz
    
    // 配置按键 PA0
    GPIOA->CRL &= ~(GPIO_CRL_CNF0 | GPIO_CRL_MODE0);
    GPIOA->CRL |= GPIO_CRL_CNF0_1;    // 输入上拉/下拉模式
    GPIOA->BSRR = GPIO_BSRR_BS0;      // 上拉
}

// 定时器初始化 - 用于数据采集计时
void TIM3_Config(void) {
    // 开启TIM3时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
    
    // 配置定时器
    TIM3->PSC = 7200 - 1;        // 72MHz/7200 = 10kHz
    TIM3->ARR = 100 - 1;         // 100个计数 = 10ms
    TIM3->DIER |= TIM_DIER_UIE;  // 使能更新中断
    TIM3->CR1 |= TIM_CR1_CEN;    // 使能定时器
    
    // 配置NVIC
    NVIC_EnableIRQ(TIM3_IRQn);
    NVIC_SetPriority(TIM3_IRQn, 1);
}

// USART1初始化 - 用于ESP8266通信
void USART1_Config(void) {
    // 开启USART1时钟
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    
    // 配置GPIO PA9(TX) PA10(RX)
    GPIOA->CRH &= ~(GPIO_CRH_CNF9 | GPIO_CRH_MODE9 | GPIO_CRH_CNF10 | GPIO_CRH_MODE10);
    GPIOA->CRH |= GPIO_CRH_CNF9_1 | GPIO_CRH_MODE9_0 | GPIO_CRH_CNF10_0;
    
    // 配置USART
    USART1->BRR = 72000000 / 115200;  // 波特率115200
    USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
    USART1->CR1 |= USART_CR1_RXNEIE;  // 使能接收中断
    
    // 配置NVIC
    NVIC_EnableIRQ(USART1_IRQn);
    NVIC_SetPriority(USART1_IRQn, 0);
}

// I2C1初始化 - 用于JY901和OLED
void I2C1_Config(void) {
    // 开启I2C1时钟
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    
    // 配置GPIO PB6(SCL) PB7(SDA)
    GPIOB->CRL &= ~(GPIO_CRL_CNF6 | GPIO_CRL_MODE6 | GPIO_CRL_CNF7 | GPIO_CRL_MODE7);
    GPIOB->CRL |= GPIO_CRL_CNF6_1 | GPIO_CRL_MODE6_1 | GPIO_CRL_CNF7_1 | GPIO_CRL_MODE7_1;
    GPIOB->ODR |= GPIO_ODR_ODR6 | GPIO_ODR_ODR7;
    
    // 配置I2C
    I2C1->CR1 = 0;
    I2C1->CR2 = 36;              // 72MHz/2 = 36MHz
    I2C1->CCR = 180;             // 100kHz
    I2C1->TRISE = 37;            // 1000ns
    I2C1->CR1 |= I2C_CR1_PE;     // 使能I2C
}

// 系统初始化
void System_Init(void) {
    SystemClock_Config();
    GPIO_Config();
    TIM3_Config();
    USART1_Config();
    I2C1_Config();
    
    // 初始化各模块
    JY901_Init();
    Keyboard_Init();
    OLED_Init();
    ESP8266_Init();
    
    // 显示启动信息
    OLED_Clear();
    OLED_ShowString(0, 0, "eSports Trainer");
    OLED_ShowString(0, 2, "System Ready");
    OLED_ShowString(0, 4, "Press KEY to Start");
    
    // 连接华为云
    if(Cloud_Connect()) {
        OLED_ShowString(0, 6, "Cloud: Connected");
    } else {
        OLED_ShowString(0, 6, "Cloud: Error");
    }
}

// 开始训练
void Start_Training(void) {
    system_state = SYSTEM_TRAINING;
    training_start_time = Get_TickCount();
    memset(&current_data, 0, sizeof(TrainingData));
    
    OLED_Clear();
    OLED_ShowString(0, 0, "Training Started");
    OLED_ShowString(0, 2, "Time: 000s");
    OLED_ShowString(0, 4, "Keys: 0000");
    OLED_ShowString(0, 6, "Status: Running");
}

// 结束训练并生成报告
void End_Training(void) {
    system_state = SYSTEM_UPLOADING;
    uint32_t training_time = Get_TickCount() - training_start_time;
    
    // 生成训练报告
    float avg_key_freq = (float)current_data.key_press_count / (training_time / 1000.0f);
    
    OLED_Clear();
    OLED_ShowString(0, 0, "Training Complete");
    OLED_ShowString(0, 2, "Duration:");
    OLED_ShowNumber(70, 2, training_time/1000);
    OLED_ShowString(0, 4, "Avg Key Freq:");
    OLED_ShowFloat(70, 4, avg_key_freq, 1);
    OLED_ShowString(0, 6, "Uploading Data...");
    
    // 上传数据到云平台
    Upload_Training_Data(&current_data, training_time);
    
    system_state = SYSTEM_IDLE;
    OLED_ShowString(0, 6, "Press KEY to Start");
}

// 数据采集处理
void Data_Acquisition(void) {
    if(system_state != SYSTEM_TRAINING) return;
    
    // 读取JY901传感器数据
    JY901_ReadData(&current_data.hand_position, &current_data.hand_rotation);
    
    // 读取键盘数据
    uint8_t key_state = Keyboard_Scan();
    if(key_state) {
        current_data.key_press_count++;
        // 记录按键序列
        for(int i = 31; i > 0; i--) {
            current_data.key_sequence[i] = current_data.key_sequence[i-1];
        }
        current_data.key_sequence[0] = key_state;
    }
    
    current_data.timestamp = Get_TickCount() - training_start_time;
}

// 更新显示
void Update_Display(void) {
    if(system_state != SYSTEM_TRAINING) return;
    
    static uint32_t last_update = 0;
    uint32_t current_time = Get_TickCount();
    
    if(current_time - last_update > 500) {  // 500ms更新一次
        last_update = current_time;
        
        uint32_t elapsed_time = (current_time - training_start_time) / 1000;
        OLED_ShowNumber(40, 2, elapsed_time);
        OLED_ShowNumber(40, 4, current_data.key_press_count);
        
        // 显示手部姿态
        char temp[16];
        sprintf(temp, "P:%.1f", current_data.hand_position[0]);
        OLED_ShowString(70, 2, temp);
    }
}

// 获取系统滴答计数
uint32_t Get_TickCount(void) {
    return TIM3->CNT;
}

// 主函数
int main(void) {
    System_Init();
    
    while(1) {
        // 检测开始/停止按键
        if(!(GPIOA->IDR & GPIO_IDR_IDR0)) {
            // 按键消抖
            for(volatile int i = 0; i < 10000; i++);
            if(!(GPIOA->IDR & GPIO_IDR_IDR0)) {
                if(system_state == SYSTEM_IDLE) {
                    Start_Training();
                } else if(system_state == SYSTEM_TRAINING) {
                    End_Training();
                }
                while(!(GPIOA->IDR & GPIO_IDR_IDR0));  // 等待按键释放
            }
        }
        
        // 数据处理
        Data_Acquisition();
        Update_Display();
        
        // LED状态指示
        static uint32_t led_timer = 0;
        if(Get_TickCount() - led_timer > 1000) {
            led_timer = Get_TickCount();
            GPIOC->ODR ^= GPIO_ODR_ODR13;  // 翻转LED
        }
    }
}

// TIM3中断服务函数 - 10ms定时
void TIM3_IRQHandler(void) {
    if(TIM3->SR & TIM_SR_UIF) {
        TIM3->SR &= ~TIM_SR_UIF;
        // 定时器中断处理
    }
}

// USART1中断服务函数
void USART1_IRQHandler(void) {
    if(USART1->SR & USART_SR_RXNE) {
        uint8_t data = USART1->DR;
        ESP8266_ReceiveHandler(data);
    }
}

总结

本系统基于STM32F103C8T6核心板设计,成功实现了电竞选手训练辅助功能,通过JY901九轴姿态传感器和机械键盘矩阵扫描电路,实时采集手部动作轨迹与按键频率数据,确保了操作过程的精确监控。系统能够分析操作延迟与动作连贯性,生成详细的训练报告,并通过ESP8266-01S Wi-Fi模块将数据及热区分布图上传至华为云平台,实现了远程数据存储与分析。

硬件组成方面,采用洞洞板焊接接口电路和杜邦线连接,确保了模块间的稳定通信,同时0.96寸OLED显示屏提供实时反馈,增强了用户体验。此外,QT上位机部分进一步优化了训练效果,通过操作建议和历史数据对比,帮助选手持续改进技能。

整体而言,该系统结合了嵌入式硬件与云端技术,为电竞训练提供了高效、智能的解决方案,不仅提升了训练效率,还为数据驱动的优化奠定了基础,具有较高的实用性和扩展潜力。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。