基于STM32设计的电竞选手训练辅助系统设计
项目开发背景
随着电子竞技行业的迅猛发展,职业选手的训练需求日益增长,操作精准度和反应速度成为决定比赛胜负的关键因素。传统训练方法多依赖教练的主观观察和选手的自我感觉,缺乏客观数据支持,难以系统化地优化操作细节。因此,开发一种能够量化分析选手表现的智能辅助系统,对于提升训练效率和竞技水平具有重要意义。
当前电竞训练中,手部动作轨迹和按键频率等关键参数往往无法被实时捕捉和分析,导致选手难以识别操作中的延迟或连贯性问题。本系统通过集成高精度传感器和云平台技术,旨在弥补这一空白,为选手提供科学的训练数据反馈,帮助其快速调整操作习惯,缩短训练周期。
该系统基于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(¤t_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(¤t_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(¤t_data.hand_position, ¤t_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上位机部分进一步优化了训练效果,通过操作建议和历史数据对比,帮助选手持续改进技能。
整体而言,该系统结合了嵌入式硬件与云端技术,为电竞训练提供了高效、智能的解决方案,不仅提升了训练效率,还为数据驱动的优化奠定了基础,具有较高的实用性和扩展潜力。
- 点赞
- 收藏
- 关注作者
评论(0)