【创意智造组】当创客遇上概率论:用FOC电机和二哈2打造智能色子概率统计装置

2026-04-0697
精华项目
AI 快速预览详细 收起
该项目名为“智能色子概率统计装置”,通过高精度FOC电机驱动盒子摇动骰子,利用二哈2 HuskyLens视觉传感器识别点数,并通过MQTT协议实现电机控制与视觉识别的联动。最终在行空板K10屏幕上动态显示统计结果。整个项目融合了电机控制、物联网通信、嵌入式AI、数据处理等多个创客技能点。适用于中学及以上的STEM教育场景,让学生在互动中理解概率论。

将机械运动、机器视觉与数学概率融合,用数据验证大数定律

【项目起源】

       概率统计是数学中一个既抽象又贴近生活的分支。课堂上我们学习“掷骰子”每个面出现的概率约为1/6,但真正动手掷上几千次并记录结果,对学生来说既枯燥又耗时。如果能做一个“自动掷骰子、自动识别点数、自动统计”的装置,就能在课堂上实时展示概率分布的形成过程,让数学“活”起来。

       基于这个想法,我设计了这套系统:用高精度FOC电机驱动盒子“摇骰子”,利用二哈2 HuskyLens视觉传感器识别点数,通过MQTT实现电机控制与视觉识别的联动,最终在行空板K10屏幕上动态显示统计结果。整个项目融合了“电机控制、物联网通信、嵌入式AI、数据处理”等多个创客技能点。

 

8ac9e1d5ccd1e39a515e42bdaf70b036_webwxgetmsgimg_&MsgID=4342473461287596877&skey=%40crypt_b90cd10e_03d46cecd9f852bd3967999190690e47.jpg

【效果演示】

 

       电机启动 → 盒子快速正反转 → 骰子在盒内翻滚 → 电机停止
       MQTT消息触发 → 二哈2拍照 → 识别点数 → 行空板屏幕更新统计数据
       重复N次后,屏幕上的数据逐渐逼近理论概率

 

 

 

【硬件清单】

部件型号/规格作用
主控1ESP32 (任意开发板)控制FOC电机,发送MQTT消息
主控2行空板K10接收MQTT消息,控制二哈2,显示统计
电机2208或2804 BLDC电机带动盒子产生随机翻滚
电机驱动SimpleFOC驱动器驱动无刷电机(使用升压模块供电12V)
磁编码器AS5600 (I2C)电机驱动自带检测电机角度(用于FOC控制)
视觉传感器二哈2 HuskyLens V2识别色子点数
通信Wi-Fi网络 + MQTT服务器设备间消息传递
其他透明盒子、色子、12V电源结构件与供电

image.png

【制作过程】

       1. 机械结构:让盒子“摇”起来

       为了保证每次停止后骰子位置随机,盒子的运动必须足够剧烈。我采用了一个透明亚克力盒子,将电机轴与盒子底部固定。电机通过正反转、急停等方式使骰子在盒内无规律碰撞。盒子内壁放入纸筒,让盒内部底面实现圆形,有利于骰子滚动,同时避免骰子撞击声过大且保护骰子。

“关键点”:电机与盒子连接要绝对紧固,否则高速转动下会产生松动。

image.png

2. 电路连接

       “ESP32 与 SimpleFOC 驱动板” 
       PWM 引脚:5 (A), 13 (B), 10 (C) 
       使能引脚:2 
       编码器 I2C:GPIO21(SDA), GPIO22(SCL)

image.png

(可参考与Arduino uno接线图)

       “行空板K10 与 二哈2” 
       二哈2通过 I2C 连接至行空板K10(默认 SDA/SCL)

       行空板通过 Wi-Fi 接入同一局域网

       “MQTT 服务器” 
       我在局域网的一台笔记本上运行了 DFRobot 的 “SIoT 2.0” 服务,当然也可以用任何公共 MQTT Broker。

       3. 电机控制逻辑(ESP32端)

       我们使用 “SimpleFOC” 库实现扭矩模式下的突然启停,并通过状态机实现:“正转0.5秒 → 反转0.5秒 → 重复3次 → 停止5秒 → 循环”。在停止阶段,ESP32 通过 MQTT 发布 `"MOTOR_STOPPED"` 消息到主题 `siot/motor`。

       关键代码片段:


// 进入停止状态后,发送一次MQTT消息
if (!mqttStopSent && (now - stateStartTime >= 500)) {
sendMotorStatus("MOTOR_STOPPED");
mqttStopSent = true;
}
 

 

       4. 视觉识别与统计(行空板K10端)

       行空板K10订阅 `siot/motor` 主题,一旦收到 `"MOTOR_STOPPED"`,立即设置拍照标志。在主循环中执行拍照并识别。

       “训练二哈2识别骰子点数”:
       1. 收集各种角度、光照下的骰子图像(每个点数至少50张)。

image.png
       2. 使用 “Mind+ 2.0” 的目标检测工具标注图片(框选骰子并标记数字)。

屏幕截图 2026-04-06 185110.png屏幕截图 2026-04-06 185022.png屏幕截图 2026-04-06 184847.png
       3. 在线训练模型并导出。
       4. 将模型通过二哈2的U盘模式烧录到传感器中。
       5. 在代码中调用 `huskylens.switchAlgorithm(129)` 切换到自定义模型。

识别结果通过 `huskylens.getCachedCenterResult()` 获取,统计每个点数出现的次数,并实时刷新屏幕显示。

image.png
STEM教育 · 智能色子概率统计装置:FOC电机与二哈2结合_image_1.webp
image.png

       5. MQTT通信测试

       启动 SIoT 服务后,在行空板K10的串口监视器观察订阅成功信息。用 MQTT 客户端发送 `"MOTOR_STOPPED"` 到 `siot/motor` 主题,验证能否触发拍照。确认无误后,将 ESP32 与行空板同时上电,系统即自动运行。

 

 

【数学概率融合:从实验中验证大数定律】

       我们让装置连续运行75次,记录每个点数出现的频率。下面是一次实验的部分数据:

image.png

       可以看到,随着实验次数增加,频率越来越接近理论概率 1/6 ≈ 0.1667。这直观地展示了“大数定律”:当试验次数足够多时,事件发生的频率趋近于其概率。

       课堂上可以让学生分别运行100次、200次、500次,观察柱状图的变化,从而理解“概率是频率的极限”这一核心思想。

4529b394b5a157f13b395720d20f5e56_webwxgetmsgimg_&MsgID=3954199621270867268&skey=%40crypt_b90cd10e_03d46cecd9f852bd3967999190690e47.jpg

【代码与模型获取】

       “ESP32 完整代码”:复制到 Arduino IDE 编译上传。

 

代码
#include <Arduino.h>
#include <Wire.h>
#include <SimpleFOC.h>
#include <WiFi.h>
#include <PubSubClient.h>

// ==================== 网络配置 ====================
const char* ssid = "sxs";
const char* password = "smj080823";
const char* mqtt_server = "192.168.31.11";
const int mqtt_port = 1883;
const char* mqtt_user = "siot";
const char* mqtt_password = "dfrobot";

// ==================== MQTT 主题 ====================
const char* control_topic = "siot/camera";          // 接收拍照指令(可选保留)
const char* status_topic = "siot/motor";     // 发送电机状态的主题

WiFiClient espClient;
PubSubClient client(espClient);

// ==================== 电机控制部分 ====================
MagneticSensorI2C sensor = MagneticSensorI2C(AS5600_I2C);
BLDCMotor motor = BLDCMotor(7);
BLDCDriver3PWM driver = BLDCDriver3PWM(5, 13, 10, 2);

float target_torque = 0.0f;
const float FORWARD_VOLTAGE = 4.0f;
const float REVERSE_VOLTAGE = -4.0f;
const float STOP_VOLTAGE = 0.0f;

const unsigned long DIRECTION_DURATION = 500;
const unsigned long STOP_DURATION = 5000;

enum ShakeStateType {
  SHAKE_FORWARD,
  SHAKE_REVERSE,
  SHAKE_STOP
};

ShakeStateType shakeState = SHAKE_FORWARD;
unsigned long stateStartTime = 0;
int shakeCounter = 0;
const int TOTAL_SHAKES = 3;

// 标志:防止在 STOP 状态重复发送 MQTT 消息
bool mqttStopSent = false;


// ==================== MQTT 回调函数(接收指令) ====================
void callback(char* topic, byte* payload, unsigned int length) {
  String message;
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  Serial.print("Message arrived on topic: ");
  Serial.print(topic);
  Serial.print(". Message: ");
  Serial.println(message);

 
}

// ==================== 重连 MQTT 服务器 ====================
void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    if (client.connect("ESP32_Camera_Controller", mqtt_user, mqtt_password)) {
      Serial.println("connected");
      client.subscribe(control_topic);
      Serial.print("Subscribed to topic: ");
      Serial.println(control_topic);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

// ==================== 发送 MQTT 状态消息 ====================
void sendMotorStatus(const char* status) {
  if (client.connected()) {
    client.publish(status_topic, status);
    Serial.print("MQTT published: ");
    Serial.print(status_topic);
    Serial.print(" -> ");
    Serial.println(status);
  } else {
    Serial.println("MQTT not connected, cannot send status");
  }
}

// ==================== 初始化 ====================
void setup() {
  Serial.begin(115200);
  pinMode(HUSKYLENS_A_BUTTON_PIN, OUTPUT);
  digitalWrite(HUSKYLENS_A_BUTTON_PIN, HIGH);

  // 连接WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected");

  // 配置 MQTT
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);

  // 初始化电机
  sensor.init();
  motor.linkSensor(&sensor);
  driver.voltage_power_supply = 12;
  driver.voltage_limit = 6;
  driver.init();
  motor.linkDriver(&driver);
  motor.phase_resistance = 2.9;
  motor.torque_controller = TorqueControlType::voltage;
  motor.controller = MotionControlType::torque;
  motor.foc_modulation = FOCModulationType::SpaceVectorPWM;
  motor.init();
  motor.initFOC();

  shakeState = SHAKE_FORWARD;
  stateStartTime = millis();
  target_torque = FORWARD_VOLTAGE;
  motor.move(target_torque);
  shakeCounter = 0;
  mqttStopSent = false;
}

void loop() {
  // 维护 MQTT 连接
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  // 电机 FOC 更新
  motor.loopFOC();
  motor.move(target_torque);

  // ========== 电机状态机 ==========
  unsigned long now = millis();
  switch (shakeState) {
    case SHAKE_FORWARD:
      if (now - stateStartTime >= DIRECTION_DURATION) {
        shakeState = SHAKE_REVERSE;
        stateStartTime = now;
        target_torque = REVERSE_VOLTAGE;
        Serial.println("Forward done -> Reverse");
      }
      break;

    case SHAKE_REVERSE:
      if (now - stateStartTime >= DIRECTION_DURATION) {
        shakeCounter++;
        if (shakeCounter >= TOTAL_SHAKES) {
          shakeState = SHAKE_STOP;
          stateStartTime = now;
          target_torque = STOP_VOLTAGE;
          mqttStopSent = false;          // 重置发送标志
          Serial.println("All shakes done -> Stop 5s");
        } else {
          shakeState = SHAKE_FORWARD;
          stateStartTime = now;
          target_torque = FORWARD_VOLTAGE;
          Serial.print("Shake cycle ");
          Serial.print(shakeCounter);
          Serial.println(" completed, continue forward");
        }
      }
      break;

    case SHAKE_STOP:
      // 在 STOP 状态期间发送一次 MQTT 消息(例如进入停止后 0.5 秒发送)
      if (!mqttStopSent && (now - stateStartTime >= 500)) {
        sendMotorStatus("MOTOR_STOPPED");   // 可改为其他内容,如 "TAKE_PHOTO"
        mqttStopSent = true;
      }
      if (now - stateStartTime >= STOP_DURATION) {
        shakeCounter = 0;
        shakeState = SHAKE_FORWARD;
        stateStartTime = now;
        target_torque = FORWARD_VOLTAGE;
        Serial.println("Stop done -> Start new shake sequence");
      }
      break;
  }
  // ========== 状态机结束 ==========
}


“行空板K10 代码”:使用 Mind+ 软件编写,完整代码。

screenshot-新建项目-1775483217606.png


       “二哈2 模型文件”:训练好的模型文件(.kmodel)可共享,但建议读者自行拍摄骰子图像进行训练,以获得更好的泛化能力。

 

 

 

【总结】

       这个项目将生硬的数学公式变成了看得见、玩得转的互动装置。学生既学习了编程、电路、机器视觉,又在玩的过程中深刻理解了概率的本质。作为创客教育的一个案例,它完美诠释了 “做中学,学中做”的理念。

 

创作许可协议

本项目采用 None(不开放任何权利,保留所有权利) 进行许可。

评论(0)
- 没有更多了 -