【创意智造组】当创客遇上概率论:用FOC电机和二哈2打造智能色子概率统计装置
将机械运动、机器视觉与数学概率融合,用数据验证大数定律
【项目起源】
概率统计是数学中一个既抽象又贴近生活的分支。课堂上我们学习“掷骰子”每个面出现的概率约为1/6,但真正动手掷上几千次并记录结果,对学生来说既枯燥又耗时。如果能做一个“自动掷骰子、自动识别点数、自动统计”的装置,就能在课堂上实时展示概率分布的形成过程,让数学“活”起来。
基于这个想法,我设计了这套系统:用高精度FOC电机驱动盒子“摇骰子”,利用二哈2 HuskyLens视觉传感器识别点数,通过MQTT实现电机控制与视觉识别的联动,最终在行空板K10屏幕上动态显示统计结果。整个项目融合了“电机控制、物联网通信、嵌入式AI、数据处理”等多个创客技能点。

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

【制作过程】
1. 机械结构:让盒子“摇”起来
为了保证每次停止后骰子位置随机,盒子的运动必须足够剧烈。我采用了一个透明亚克力盒子,将电机轴与盒子底部固定。电机通过正反转、急停等方式使骰子在盒内无规律碰撞。盒子内壁放入纸筒,让盒内部底面实现圆形,有利于骰子滚动,同时避免骰子撞击声过大且保护骰子。
“关键点”:电机与盒子连接要绝对紧固,否则高速转动下会产生松动。

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

(可参考与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张)。

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



3. 在线训练模型并导出。
4. 将模型通过二哈2的U盘模式烧录到传感器中。
5. 在代码中调用 `huskylens.switchAlgorithm(129)` 切换到自定义模型。
识别结果通过 `huskylens.getCachedCenterResult()` 获取,统计每个点数出现的次数,并实时刷新屏幕显示。


5. MQTT通信测试
启动 SIoT 服务后,在行空板K10的串口监视器观察订阅成功信息。用 MQTT 客户端发送 `"MOTOR_STOPPED"` 到 `siot/motor` 主题,验证能否触发拍照。确认无误后,将 ESP32 与行空板同时上电,系统即自动运行。
【数学概率融合:从实验中验证大数定律】
我们让装置连续运行75次,记录每个点数出现的频率。下面是一次实验的部分数据:

可以看到,随着实验次数增加,频率越来越接近理论概率 1/6 ≈ 0.1667。这直观地展示了“大数定律”:当试验次数足够多时,事件发生的频率趋近于其概率。
课堂上可以让学生分别运行100次、200次、500次,观察柱状图的变化,从而理解“概率是频率的极限”这一核心思想。

【代码与模型获取】
“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+ 软件编写,完整代码。

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



