22浏览
查看: 22|回复: 0

[ESP8266/ESP32] ESP32-C5系列连载04-老黄的金箍棒项目终于不用烂尾了

[复制链接]
本帖最后由 漂移菌 于 2025-10-23 12:52 编辑

书接上文,中间利用python代码的修改,将ESP32-C5上链接的MPU6050 的数据采集并通过串口传给了树莓派5, 通过树莓派5 采集并将数据写入对应的文件。
下面让我来为你详细讲解一下如何在树莓派上用 Arduino-cli 构建 ESP32-C5 + MPU6050 获取设备姿态信息的完整流程。
这里我会尝试使用 MPU6050 内置的 DMP(Digital Motion Processor) 功能,直接输出四元数,再通过官方库函数转换为欧拉角(Yaw、Pitch、Roll),这种方式精度高且计算量小。

硬件连接主要去看上一篇帖子哈。
在树莓派上我部署了arduino-cli的环境,编译非常丝滑,也不用鼠标点点点,如果还不记得怎么用。可以尝试参考我下面的方法:

  1. # 安装 arduino-cli
  2. curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
  3. # 添加到环境变量
  4. echo 'export PATH=$PATH:$HOME/bin' >> ~/.bashrc
  5. source ~/.bashrc
  6. # 初始化配置
  7. arduino-cli config init
  8. arduino-cli core update-index
  9. # 安装 ESP32 开发板支持
  10. arduino-cli core install esp32:esp32
复制代码
在config init 部分,需要修改一下你的配置哦。不然使用的编译器和编译环境可能认不出你的esp32-C5


  1. pi@rpi16g:~/max7219_test $ cat ~/.arduino15/arduino-cli.yaml
  2. board_manager:
  3.     additional_urls: ['https://github.com/espressif/arduino-esp32/releases/download/3.3.0-alpha1/package_esp32_dev_index_cn.json']
  4. network:
  5.     connection_timeout: 1800s
复制代码
然后记得安装一下下面两个库:

[color=rgba(0, 0, 0, 0.9)]
  • I2Cdevlib(用于 I2C 通信)
  • MPU6050(用于 MPU6050 控制)
步骤如下:
  1. cd ~/Arduino/libraries
  2. git clone https://github.com/jrowberg/i2cdevlib
  3. mv i2cdevlib/Arduino/I2Cdev .
  4. mv i2cdevlib/Arduino/MPU6050 .
复制代码
然后创建一个Arduino Sketch 项目
  1. arduino-cli sketch new esp32c5-mpu6050
  2. cd ~/esp32c5-mpu6050
复制代码
创建主文件 esp32c5-mpu6050.ino,内容如下:
  1. #include <Arduino.h>
  2. #include "I2Cdev.h"
  3. #include "MPU6050_6Axis_MotionApps20.h"
  4. #include "Wire.h"
  5. #define INTERRUPT_PIN  2
  6. #define SDA_PIN 9
  7. #define SCL_PIN 10
  8. #define COB_PIN 15
  9. MPU6050 mpu;
  10. volatile bool mpuInterrupt = false;
  11. bool dmpReady = false;
  12. uint8_t mpuIntStatus;
  13. uint8_t devStatus;
  14. uint16_t packetSize;
  15. uint8_t fifoBuffer[64];
  16. Quaternion q;
  17. VectorFloat gravity;
  18. float ypr[3];  // [yaw, pitch, roll]
  19. void IRAM_ATTR dmpDataReady() {
  20.         mpuInterrupt = true;
  21. }
  22. void setup() {
  23.         Serial.begin(9600);
  24.         Wire.begin(SDA_PIN, SCL_PIN);
  25.         Wire.setClock(100000);
  26.         mpu.initialize();
  27.         if (!mpu.testConnection()) {
  28.         Serial.println("MPU6050连接失败!");
  29.                 while (1);
  30.         }
  31.         devStatus = mpu.dmpInitialize();
  32.         if (devStatus != 0) {
  33.                 Serial.printf("DMP初始化失败,状态码: %d\n", devStatus);
  34.                 while (1);
  35.         }
  36.         mpu.setXGyroOffset(220);
  37.         mpu.setYGyroOffset(76);
  38.         mpu.setZGyroOffset(-85);
  39.         mpu.setZAccelOffset(1788);
  40.         mpu.CalibrateAccel(6);
  41.         mpu.CalibrateGyro(6);
  42.         mpu.setDMPEnabled(true);
  43.         pinMode(INTERRUPT_PIN, INPUT);
  44.         attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), dmpDataReady, RISING);
  45.         mpuIntStatus = mpu.getIntStatus();
  46.         dmpReady = true;
  47.         packetSize = mpu.dmpGetFIFOPacketSize();
  48.         pinMode(COB_PIN, OUTPUT);
  49.         digitalWrite(COB_PIN, HIGH);
  50.         Serial.println("ESP32-C5 + MPU6050 + COB Light 初始化完成!");
  51.         digitalWrite(COB_PIN, LOW);
  52.         delay(3);
  53.         digitalWrite(COB_PIN, HIGH);
  54. }
  55. void loop() {
  56.         if (!dmpReady) return;
  57.         uint16_t fifoCount = mpu.getFIFOCount();
  58.         if (!mpuInterrupt && fifoCount < packetSize) return;
  59.         mpuInterrupt = false;
  60.         mpuIntStatus = mpu.getIntStatus();
  61.         fifoCount = mpu.getFIFOCount();
  62.         if((mpuIntStatus & 0x10) || fifoCount == 1024){
  63.                 mpu.resetFIFO();
  64.                 Serial.println("DAMN, FIFO特喵的溢出了!");
  65.                 return;
  66.         }
  67.         if (mpuIntStatus & 0x02) {
  68.                 while (fifoCount < packetSize) fifoCount = mpu.getFIFOCount();
  69.                 mpu.getFIFOBytes(fifoBuffer, packetSize);
  70.                 fifoCount -= packetSize;
  71.                 mpu.dmpGetQuaternion(&q, fifoBuffer);
  72.                 mpu.dmpGetGravity(&gravity, &q);
  73.                 mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);
  74.             /*
  75.                 Serial.print("YAW: ");
  76.                 Serial.print(ypr[0] * 180 /M_PI);
  77.                 Serial.print("\tPITCH: ");
  78.                 Serial.print(ypr[1] * 180 /M_PI);
  79.                 if ((ypr[1] * 180 /M_PI) > 50.0){
  80.                    digitalWrite(COB_PIN, LOW);}
  81.                 else {
  82.                    digitalWrite(COB_PIN, HIGH);
  83.                 }
  84.                 Serial.print("\tROLL: ");
  85.                 Serial.println(ypr[2] * 180 /M_PI);
  86.                 */
  87.                 Serial.printf("IMU:%.2f,%.2f,%.2f\n",(ypr[0]*180/M_PI),(ypr[1]*180/M_PI),(ypr[2]*180/M_PI));
  88.         }
  89. }
复制代码

编译并上传代码到esp32-C5:
  1. # 编译
  2. arduino-cli compile --fqbn esp32:esp32:esp32:CDCOnBoot=cdc  esp32c5-mpu6050
  3. # 上传(替换为你的串口)
  4. arduino-cli upload -p /dev/ttyACM0--fqbn esp32:esp32:esp32:CDCOnBoot=cdc  esp32c5-mpu6050
复制代码
打开串口监视器看看是否有数据输出:
  1. arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200
复制代码

如果一切正确,可以看到类似下面的输出:
  1. IMU:  -12.34, 45.33, -3.21  
复制代码


到这里,ESP32-C5 的数据采集并通过串口输出就好了,接下来我们到树莓派上去搭建数据采集的环境。
在树莓派上把「yaw/pitch/roll 序列」变成「画圈 / 画×」的 AI 分类器,整套流程可以塞进 4 步就能完成。
1. 数据采集和打标签(要在树莓派主机上完成)
操作思路: ESP32 ---》 串口 ---》 树莓派USB串口读取
2. 写个简单的采集脚本, 每类动作记录1条 .csv 文件,然后全部记录下来后再机器学习。
采集脚本如下:
  1. pi@rpi16g:~/esp32_TinyML_MPU6050 $ cat collect_data.py
  2. # 功能:通过MPU6050采集数据集,目前采集4组,circle是划圆,cross是划十字,左右就是向左砍和向右砍
  3. # 目标;帮老黄的金箍棒灯效采集数据
  4. # Editor: 漂移菌
  5. # 串口数据来自接入的ESP32
  6. import serial
  7. import time
  8. import csv
  9. import os
  10. GESTURES = ["circle", "cross", "left", "right"]
  11. SAMPLES_PER_GESTURE = 30 # 每种手势采集30条
  12. SAMPLE_DURATION = 2.0    # 每条样本2秒
  13. SAMPLE_RATE = 50         # 50HZ
  14. TOTAL_READS = int(SAMPLE_RATE * SAMPLE_DURATION)
  15. ser = serial.Serial("/dev/ttyUSB0", 9600, timeout=1)
  16. time.sleep(2)
  17. def collect_gesture(gesture_name, index):
  18.     filename = f"{gesture_name}/{gesture_name}_{index:03d}.csv"
  19.     print(f"准备采集: {filename}, 请在2秒内完成动作...")
  20.     time.sleep(2)
  21.     data = []
  22.     try:
  23.         while len(data) < TOTAL_READS:
  24.             line = ser.readline().decode('utf-8', errors='ignore').strip()
  25.             if line:
  26.                 print(line)
  27.                 parts = line.split(',')
  28.                 if len(parts) == 3:
  29.                     try:
  30.                         yaw, pitch, roll = map(float, parts)
  31.                         data.append([yaw, pitch, roll])
  32.                         print(f"✅\r已采集: {len(data)}条数据", end='\n')
  33.                     except ValueError:
  34.                         print("跳过坏数据:", line)
  35.         print(f"✅ 采集完成:{filename}, 共{len(data)}条数据")
  36.     except KeyboardInterrupt:
  37.         print(f"✅ 手动结束采集:{filename}, 共{len(data)}条数据")
  38.     with open(filename, 'w', newline='') as f:
  39.         writer = csv.writer(f)
  40.         writer.writerow(["yaw", "pitch", "roll"])
  41.         writer.writerows(data)
  42. if __name__ == "__main__":
  43.     for gesture in GESTURES:
  44.         os.makedirs(gesture, exist_ok=True)
  45.         for i in range(1, SAMPLES_PER_GESTURE+1):
  46.             input(f"\n按下回车开始采集{gesture}第{i}条...")
  47.             collect_gesture(gesture, i)
复制代码
然后根据提示执行,分别进行画圆圈,打×, 左挥,右砍, 采集到足够的数据会在当前目录下看到类似这样的几个文件夹:
  1. pi@rpi16g:~/esp32_TinyML_MPU6050 $ tree data
  2. data
  3. ├── circle
  4. │   ├── circle_001.csv
  5. │   ├── circle_002.csv
  6. │   ├── circle_003.csv
  7. │   ├── circle_004.csv
  8. │   ├── circle_005.csv
  9. │   ├── circle_006.csv
  10. │   ├── circle_007.csv
  11. │   ├── circle_008.csv
  12. │   ├── circle_009.csv
  13. │   ├── circle_010.csv
  14. │   ├── circle_011.csv
  15. │   ├── circle_012.csv
  16. │   ├── circle_013.csv
  17. │   ├── circle_014.csv
  18. │   ├── circle_015.csv
  19. │   ├── circle_016.csv
  20. │   ├── circle_017.csv
  21. │   ├── circle_018.csv
  22. │   ├── circle_019.csv
  23. │   ├── circle_020.csv
  24. │   ├── circle_021.csv
  25. │   ├── circle_022.csv
  26. │   ├── circle_023.csv
  27. │   ├── circle_024.csv
  28. │   ├── circle_025.csv
  29. │   ├── circle_026.csv
  30. │   ├── circle_027.csv
  31. │   ├── circle_028.csv
  32. │   ├── circle_029.csv
  33. │   └── circle_030.csv
  34. ├── cross
  35. │   ├── cross_001.csv
  36. │   ├── cross_002.csv
  37. │   ├── cross_003.csv
  38. │   ├── cross_004.csv
  39. │   ├── cross_005.csv
  40. │   ├── cross_006.csv
  41. │   ├── cross_007.csv
  42. │   ├── cross_008.csv
  43. │   ├── cross_009.csv
  44. │   ├── cross_010.csv
  45. │   ├── cross_011.csv
  46. │   ├── cross_012.csv
  47. │   ├── cross_013.csv
  48. │   ├── cross_014.csv
  49. │   ├── cross_015.csv
  50. │   ├── cross_016.csv
  51. │   ├── cross_017.csv
  52. │   ├── cross_018.csv
  53. │   ├── cross_019.csv
  54. │   ├── cross_020.csv
  55. │   ├── cross_021.csv
  56. │   ├── cross_022.csv
  57. │   ├── cross_023.csv
  58. │   ├── cross_024.csv
  59. │   ├── cross_025.csv
  60. │   ├── cross_026.csv
  61. │   ├── cross_027.csv
  62. │   ├── cross_028.csv
  63. │   ├── cross_029.csv
  64. │   └── cross_030.csv
  65. ├── left
  66. │   ├── left_001.csv
  67. │   ├── left_002.csv
  68. │   ├── left_003.csv
  69. │   ├── left_004.csv
  70. │   ├── left_005.csv
  71. │   ├── left_006.csv
  72. │   ├── left_007.csv
  73. │   ├── left_008.csv
  74. │   ├── left_009.csv
  75. │   ├── left_010.csv
  76. │   ├── left_011.csv
  77. │   ├── left_012.csv
  78. │   ├── left_013.csv
  79. │   ├── left_014.csv
  80. │   ├── left_015.csv
  81. │   ├── left_016.csv
  82. │   ├── left_017.csv
  83. │   ├── left_018.csv
  84. │   ├── left_019.csv
  85. │   ├── left_020.csv
  86. │   ├── left_021.csv
  87. │   ├── left_022.csv
  88. │   ├── left_023.csv
  89. │   ├── left_024.csv
  90. │   ├── left_025.csv
  91. │   ├── left_026.csv
  92. │   ├── left_027.csv
  93. │   ├── left_028.csv
  94. │   ├── left_029.csv
  95. │   └── left_030.csv
  96. └── right
  97.     ├── right_001.csv
  98.     ├── right_002.csv
  99.     ├── right_003.csv
  100.     ├── right_004.csv
  101.     ├── right_005.csv
  102.     ├── right_006.csv
  103.     ├── right_007.csv
  104.     ├── right_008.csv
  105.     ├── right_009.csv
  106.     ├── right_010.csv
  107.     ├── right_011.csv
  108.     ├── right_012.csv
  109.     ├── right_013.csv
  110.     ├── right_014.csv
  111.     ├── right_015.csv
  112.     ├── right_016.csv
  113.     ├── right_017.csv
  114.     ├── right_018.csv
  115.     ├── right_019.csv
  116.     ├── right_020.csv
  117.     ├── right_021.csv
  118.     ├── right_022.csv
  119.     ├── right_023.csv
  120.     ├── right_024.csv
  121.     ├── right_025.csv
  122.     ├── right_026.csv
  123.     ├── right_027.csv
  124.     ├── right_028.csv
  125.     ├── right_029.csv
  126.     └── right_030.csv
  127. 5 directories, 120 files
复制代码
五个分类目录和120个文件,这些数据信息单独如下:
ESP32-C5系列连载04-老黄的金箍棒项目终于不用烂尾了图1ESP32-C5系列连载04-老黄的金箍棒项目终于不用烂尾了图2
然后就可以编写代码来训练了。
目前你已经有:
  • ✅ ESP32 + MPU6050 采集的 yaw/pitch/roll 数据(CSV 格式)
  • ✅ 树莓派5(性能足够)
目标就是在树莓派5 本地训练一个TinyML级别的模型了。哈哈

用于识别:
  • 画圈(circle)
  • 打叉(cross)
  • 左挥(left)
  • 右挥(right)
最终的solution如下:
✅ 方案:用 scikit-learn 训练轻量模型 → 导出为 C数组 → ESP32 上推理

[td]
步骤
工具
说明
1. 数据加载
pandas
读取 CSV(yaw/pitch/roll)
2. 特征提取
scipy / scikit-learn
提取时域特征(均值、方差、能量、峰值)
3. 标签编码
sklearn.preprocessing
四类:circle/cross/left/right
4. 模型训练
RandomForestClassifier 或 SVM
轻量、快速、适合树莓派
5. 模型导出
micromlgen 或 sklearn-porter
导出为 C 代码
6. ESP32 推理
C 语言
直接运行模型,控制 RGB


树莓派先要安装依赖:
  1. sudo apt update
  2. sudo apt install python3-pip
  3. pip3 install pandas scikit-learn scipy micromlgen
复制代码
然后就可以编写一个训练脚本,例如我这里是train_local.py
  1. import os
  2. import pandas as pd
  3. import numpy as np
  4. from sklearn.ensemble import RandomForestClassifier
  5. from sklearn.model_selection import train_test_split
  6. from sklearn.metrics import classification_report
  7. from micromlgen import port  # 转换成c代码
  8. import pickle
  9. # 0. 先做个函数提取特征
  10. def extract_features(df):
  11.     return [
  12.             df['yaw'].mean(), df['yaw'].std(), df['yaw'].max(), df['yaw'].min(),
  13.             df['pitch'].mean(), df['pitch'].std(), df['pitch'].max(), df['pitch'].min(),
  14.             df['roll'].mean(), df['roll'].std(), df['roll'].max(), df['roll'].min(),]
  15. # 1. 尝试加载所有的CSV文件.
  16. def load_data(folder):
  17.     X, y = [], []
  18.     label = os.path.basename(folder)
  19.     for file in os.listdir(folder):
  20.         if file.endswith('.csv'):
  21.             df = pd.read_csv(os.path.join(folder, file))
  22.             # 提取时域特征
  23.             features = extract_features(df)
  24.             X.append(features)
  25.             y.append(label)
  26.     return X, y
  27. # 2. 加载所有类别
  28. X_all, y_all = [], []
  29. for gesture in ['circle', 'cross', 'left', 'right']:
  30.     X, y = load_data(f'data/{gesture}')
  31.     X_all.extend(X)
  32.     y_all.extend(y)
  33. X_all = np.array(X_all)
  34. print(f"X_all.shape = {X_all.shape}")
  35. # 3. 训练模型
  36. X_train, X_test, y_train, y_test = train_test_split(X_all, y_all, test_size=0.2)
  37. clf = RandomForestClassifier(n_estimators=30, max_depth=10)
  38. clf.fit(X_train, y_train)
  39. with open("my_esp32-gesture-RFmodel.pkl", 'wb') as f:
  40.     pickle.dump(clf, f)
  41. # 4. 评估模型
  42. print(classification_report(y_test, clf.predict(X_test)))
  43. print('-'* 50)
  44. print(f"训练集准确率: {clf.score(X_all, y_all):.2f}")
  45. # 5. 导出一个C头文件,后面烧录到esp32
  46. with open('model.h', 'w') as f:
  47.     f.write(port(clf))
  48. print(f"已经生成model.h头文件")
复制代码

这时候执行这个脚本就会很快训练出一个非常精准的模型,然后还会得到一个model.h的头文件,这个需要放到arduino项目里
然后继续下面的步骤:
  • 把 model.h 放到 Arduino 项目里
  • 每次采集 2 秒数据(100 条),提取同样的 12 个特征
  • 调用 predict(features) → 返回类别索引
  • 根据类别控制 RGB 灯
然后安装到金箍棒里面,我目前还在打印和调试,未完,待续吧~更新比较慢。但是不会断。哈哈

那个训练的过程我发布到B站了,链接如下:

原谅我没有语音解说哈~

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2025 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail