智算多多



在AIoT(人工智能+物联网)全面爆发的今天,无数零基础开发者想入门这个赛道,却都陷入了致命的认知误区:
其实随着TinyML(微型机器学习)技术的成熟,哪怕是只有2KB RAM、32KB Flash的Arduino Uno,也能运行轻量的AI分类模型,实现完全端侧的实时推理、智能决策,不需要云端、不需要高端硬件、不需要深厚的算法功底,总成本不到50元,零基础也能快速落地。
本文就以六轴传感器手势识别+智能灯光决策系统为实战案例,带大家完整走通「传感器数据采集→数据集标注→轻量分类模型训练→模型量化转换→Arduino端侧部署→智能决策执行」的全流程,真正实现物联+AI的端侧落地。
| 硬件名称 | 型号/规格 | 单价 | 核心作用 |
|---|---|---|---|
| 主控板 | Arduino Uno R3 | 30元左右 | 物联网终端核心,负责传感器数据采集、TinyML模型推理、智能决策执行、灯光控制 |
| 六轴传感器 | MPU6050模块 | 12元左右 | 采集三轴加速度+三轴角速度数据,是手势识别的核心数据来源,内置DMP运动处理单元,数据精度高 |
| 灯光执行单元 | RGB共阴极LED灯 | 3元左右 | 智能决策的执行终端,不同手势对应不同灯光颜色/亮灭状态 |
| 辅助配件 | 220Ω电阻3个、杜邦线、面包板 | 5元左右 | 电路连接、保护LED灯、方便调试 |
Arduino、MPU6050、RGB灯的接线极其简单,全程无需焊接,用杜邦线即可完成,我们用一张清晰的拓扑图展示:
关键接线注意事项:
- MPU6050的VCC建议接3.3V(Arduino的I2C引脚为3.3V电平),避免接5V烧坏传感器;
- MPU6050的AD0引脚接地时,I2C地址为
0x68;接3.3V时地址为0x69,方便多传感器级联;- RGB LED的每个颜色引脚必须串联220Ω限流电阻,否则会因电流过大烧坏LED灯。
我们的项目分为数据采集标注、模型训练转换、Arduino端侧部署三大环节,环境搭建全程零基础友好,跟着步骤操作即可:
Adafruit MPU6050:MPU6050传感器驱动库TensorFlow Lite Micro:端侧AI推理核心库Adafruit Unified Sensor:传感器通用驱动依赖库# 串口通信、数据处理、可视化
pip install pyserial pandas numpy matplotlib
# 模型训练与转换
pip install tensorflow==2.15.0 tensorflow-model-optimization scikit-learn
注意:TensorFlow 2.15.0是与TensorFlow Lite Micro兼容性最好的版本,避免使用最新版导致模型转换失败。
我们的系统采用完全端侧的物联+AI架构,所有数据采集、AI推理、决策执行全部在Arduino单片机上完成,无需云端服务器、无需网络连接,真正实现低延迟、高隐私、高可靠的智能决策。
端侧物联+AI完整闭环
| 手势类别 | 动作描述 | 智能决策执行结果 |
|---|---|---|
| 静止 | 传感器保持水平静止不动 | 关闭所有LED灯 |
| 上挥 | 传感器沿Y轴向上快速挥动 | 红色LED灯常亮 |
| 下挥 | 传感器沿Y轴向下快速挥动 | 绿色LED灯常亮 |
| 左挥 | 传感器沿X轴向左快速挥动 | 蓝色LED灯常亮 |
| 右挥 | 传感器沿X轴向右快速挥动 | 红/绿/蓝LED全亮(白色) |
AI模型的上限由数据质量决定,没有高质量的标注数据,再好的模型也无法得到好的效果。我们的手势识别系统,需要采集至少500个标注样本(每个手势100个),就能训练出准确率95%以上的模型。
重复采集
在Arduino IDE中新建项目,命名为Gesture_Data_Collection,复制以下代码,烧录到Arduino Uno R3中:
#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
// ==================== 配置参数 ====================
const int BAUD_RATE = 115200; // 串口波特率
const int SAMPLE_RATE = 100; // 采样率:100Hz(10ms采集一次)
const int SAMPLE_SIZE = 100; // 单样本数据点数:100个(1秒)
const int MPU6050_ADDR = 0x68; // MPU6050 I2C地址(AD0接地)
// ==================== 全局变量 ====================
Adafruit_MPU6050 mpu;
// 样本数据存储数组
float accelX[SAMPLE_SIZE], accelY[SAMPLE_SIZE], accelZ[SAMPLE_SIZE];
float gyroX[SAMPLE_SIZE], gyroY[SAMPLE_SIZE], gyroZ[SAMPLE_SIZE];
int sampleIndex = 0;
bool isCollecting = false;
// ==================== 核心函数 ====================
// 初始化MPU6050传感器
void initMPU6050() {
if (!mpu.begin(MPU6050_ADDR)) {
Serial.println("【错误】MPU6050初始化失败,请检查接线!");
while (1) delay(10); // 卡死等待修复
}
// 配置传感器量程:±2g加速度,±250°/s角速度,适合手势识别
mpu.setAccelerometerRange(MPU6050_RANGE_2_G);
mpu.setGyroRange(MPU6050_RANGE_250_DEG);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
delay(100);
Serial.println("【成功】MPU6050初始化完成!");
Serial.println("请输入's'开始采集手势,输入'q'停止采集...");
}
// 采集单帧传感器数据
void collectSingleData() {
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
// 存储三轴加速度、角速度数据
accelX[sampleIndex] = a.acceleration.x;
accelY[sampleIndex] = a.acceleration.y;
accelZ[sampleIndex] = a.acceleration.z;
gyroX[sampleIndex] = g.gyro.x;
gyroY[sampleIndex] = g.gyro.y;
gyroZ[sampleIndex] = g.gyro.z;
sampleIndex++;
}
// 串口发送完整手势样本
void sendSampleToPC() {
Serial.println("GESTURE_START"); // 样本开始标记
for (int i = 0; i < SAMPLE_SIZE; i++) {
Serial.print(accelX[i], 4);
Serial.print(",");
Serial.print(accelY[i], 4);
Serial.print(",");
Serial.print(accelZ[i], 4);
Serial.print(",");
Serial.print(gyroX[i], 4);
Serial.print(",");
Serial.print(gyroY[i], 4);
Serial.print(",");
Serial.println(gyroZ[i], 4);
}
Serial.println("GESTURE_END"); // 样本结束标记
}
// ==================== 主程序 ====================
void setup() {
Serial.begin(BAUD_RATE);
while (!Serial) delay(10); // 等待串口连接
initMPU6050();
}
void loop() {
// 监听串口指令
if (Serial.available() > 0) {
char cmd = Serial.read();
if (cmd == 's') {
isCollecting = true;
sampleIndex = 0;
Serial.println("【提示】开始采集,请在1秒内完成手势动作!");
} else if (cmd == 'q') {
isCollecting = false;
Serial.println("【提示】采集已停止!");
}
}
// 循环采集数据
if (isCollecting && sampleIndex < SAMPLE_SIZE) {
collectSingleData();
delay(1000 / SAMPLE_RATE); // 严格控制采样间隔
}
// 采集完成,发送数据到PC
if (isCollecting && sampleIndex >= SAMPLE_SIZE) {
sendSampleToPC();
isCollecting = false;
sampleIndex = 0;
Serial.println("【成功】手势样本采集完成!输入's'继续采集下一个样本...");
}
}
在PC上新建Python文件data_collection_labeling.py,复制以下代码,修改串口端口号(Windows为COM3/COM4,Linux/Mac为/dev/ttyUSB0等):
import serial
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
# ==================== 配置参数 ====================
SERIAL_PORT = "COM3" # 替换为你的Arduino串口端口
BAUD_RATE = 115200
SAMPLE_SIZE = 100 # 单样本数据点数
SAMPLE_DURATION = 1.0 # 单样本时长1秒
GESTURE_CLASSES = ["静止", "上挥", "下挥", "左挥", "右挥"]
CSV_SAVE_PATH = "gesture_dataset.csv"
# ==================== 全局变量 ====================
ser = None
current_sample = None
data_list = []
# ==================== 核心函数 ====================
# 初始化串口连接
def init_serial():
global ser
try:
ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=1)
print(f"【成功】串口连接成功:{SERIAL_PORT}")
return True
except Exception as e:
print(f"【错误】串口连接失败:{e}")
return False
# 读取Arduino传输的手势样本
def read_gesture_sample():
global current_sample
sample_data = []
in_sample = False
while True:
line = ser.readline().decode("utf-8").strip()
if not line:
continue
# 样本开始标记
if line == "GESTURE_START":
in_sample = True
sample_data = []
# 样本结束标记
elif line == "GESTURE_END":
in_sample = False
if len(sample_data) == SAMPLE_SIZE:
current_sample = np.array(sample_data)
return True
else:
print(f"【错误】样本长度异常:{len(sample_data)},期望:{SAMPLE_SIZE}")
return False
# 解析单帧数据
elif in_sample:
try:
frame_data = list(map(float, line.split(",")))
if len(frame_data) == 6:
sample_data.append(frame_data)
except Exception as e:
print(f"【警告】数据解析失败:{e}")
continue
# 可视化手势样本曲线
def plot_sample_curve():
global current_sample
if current_sample is None:
return
# 创建2行3列的子图,分别展示三轴加速度、角速度
fig, axs = plt.subplots(2, 3, figsize=(16, 8))
time_axis = np.linspace(0, SAMPLE_DURATION, SAMPLE_SIZE)
# 三轴加速度曲线
axs[0, 0].plot(time_axis, current_sample[:, 0], label="accelX", color="#1890ff")
axs[0, 0].set_title("X轴加速度", fontsize=12)
axs[0, 0].legend()
axs[0, 0].grid(True, alpha=0.3)
axs[0, 1].plot(time_axis, current_sample[:, 1], label="accelY", color="#52c41a")
axs[0, 1].set_title("Y轴加速度", fontsize=12)
axs[0, 1].legend()
axs[0, 1].grid(True, alpha=0.3)
axs[0, 2].plot(time_axis, current_sample[:, 2], label="accelZ", color="#fa8c16")
axs[0, 2].set_title("Z轴加速度", fontsize=12)
axs[0, 2].legend()
axs[0, 2].grid(True, alpha=0.3)
# 三轴角速度曲线
axs[1, 0].plot(time_axis, current_sample[:, 3], label="gyroX", color="#1890ff")
axs[1, 0].set_title("X轴角速度", fontsize=12)
axs[1, 0].legend()
axs[1, 0].grid(True, alpha=0.3)
axs[1, 1].plot(time_axis, current_sample[:, 4], label="gyroY", color="#52c41a")
axs[1, 1].set_title("Y轴角速度", fontsize=12)
axs[1, 1].legend()
axs[1, 1].grid(True, alpha=0.3)
axs[1, 2].plot(time_axis, current_sample[:, 5], label="gyroZ", color="#fa8c16")
axs[1, 2].set_title("Z轴角速度", fontsize=12)
axs[1, 2].legend()
axs[1, 2].grid(True, alpha=0.3)
# 添加标注按钮
plt.subplots_adjust(bottom=0.2)
button_list = []
for idx, gesture in enumerate(GESTURE_CLASSES):
ax = plt.axes([0.1 + idx * 0.15, 0.05, 0.12, 0.075])
btn = Button(ax, gesture)
btn.on_clicked(lambda event, g=gesture: save_labeled_data(g))
button_list.append(btn)
plt.suptitle("手势样本数据可视化", fontsize=16)
plt.show()
# 保存标注后的数据到CSV文件
def save_labeled_data(gesture_label):
global current_sample, data_list
if current_sample is None:
print("【警告】无有效样本数据!")
return
# 按帧拆分数据,添加样本ID和标签
sample_id = len(data_list) // SAMPLE_SIZE
for i in range(SAMPLE_SIZE):
data_list.append({
"sample_id": sample_id,
"time": i * (SAMPLE_DURATION / SAMPLE_SIZE),
"accelX": current_sample[i, 0],
"accelY": current_sample[i, 1],
"accelZ": current_sample[i, 2],
"gyroX": current_sample[i, 3],
"gyroY": current_sample[i, 4],
"gyroZ": current_sample[i, 5],
"label": gesture_label
})
# 保存到CSV文件
df = pd.DataFrame(data_list)
df.to_csv(CSV_SAVE_PATH, index=False, encoding="utf-8-sig")
print(f"【成功】标注完成:{gesture_label} | 总样本数:{sample_id+1} | 已保存到{CSV_SAVE_PATH}")
plt.close()
# ==================== 主程序 ====================
if __name__ == "__main__":
if not init_serial():
exit(1)
print("="*50)
print("手势数据采集与标注工具已启动")
print("请在Arduino串口监视器中输入's'开始采集手势样本")
print("="*50)
while True:
if read_gesture_sample():
print("【成功】样本读取完成,正在打开可视化界面...")
plot_sample_curve()
s,立即在1秒内完成对应的手势动作;gesture_dataset.csv数据集文件,用于后续模型训练。针对Arduino的低算力、小内存环境,我们选择1D CNN(一维卷积神经网络)作为分类模型,它非常适合处理时序传感器数据,同时参数少、体积小、推理速度快,完美适配单片机的端侧部署。
精度达标
在PC上新建Python文件model_train_convert.py,复制以下代码,确保数据集文件gesture_dataset.csv在同一目录下:
import pandas as pd
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten, Dense, Dropout
from tensorflow.keras.utils import to_categorical
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import tensorflow_model_optimization as tfmot
# ==================== 配置参数 ====================
DATASET_PATH = "gesture_dataset.csv"
SAMPLE_SIZE = 100 # 单样本数据点数
NUM_FEATURES = 6 # 6轴传感器数据
NUM_CLASSES = 5 # 5种手势类别
MODEL_SAVE_PATH = "gesture_model.h5"
TFLITE_MODEL_PATH = "gesture_model.tflite"
TINYML_MODEL_CC = "gesture_model.cc"
TINYML_MODEL_H = "gesture_model.h"
# ==================== 核心函数 ====================
# 加载与预处理数据集
def load_and_preprocess_data():
# 加载CSV数据集
df = pd.read_csv(DATASET_PATH)
# 按样本ID分组,重构为[样本数, 时间步, 特征数]的格式
samples = []
labels = []
for sample_id in df["sample_id"].unique():
sample_df = df[df["sample_id"] == sample_id]
# 提取6轴特征数据
features = sample_df[["accelX", "accelY", "accelZ", "gyroX", "gyroY", "gyroZ"]].values
samples.append(features)
# 提取标签
label = sample_df["label"].iloc[0]
labels.append(label)
# 转换为numpy数组
X = np.array(samples)
y = np.array(labels)
# 标签编码:字符串→整数→独热编码
label_to_int = {label: idx for idx, label in enumerate(["静止", "上挥", "下挥", "左挥", "右挥")]}
y = np.array([label_to_int[label] for label in y])
y = to_categorical(y, NUM_CLASSES)
# 数据标准化:消除量纲影响
scaler = StandardScaler()
X = scaler.fit_transform(X.reshape(-1, NUM_FEATURES)).reshape(-1, SAMPLE_SIZE, NUM_FEATURES)
# 划分训练集与测试集(8:2)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
print(f"数据集加载完成:训练集{X_train.shape[0]}个样本,测试集{X_test.shape[0]}个样本")
return X_train, X_test, y_train, y_test, scaler, label_to_int
# 构建轻量1D CNN分类模型
def build_lightweight_model():
model = Sequential([
# 第一层卷积:提取低级时序特征
Conv1D(filters=16, kernel_size=3, activation="relu", input_shape=(SAMPLE_SIZE, NUM_FEATURES)),
MaxPooling1D(pool_size=2),
Dropout(0.2),
# 第二层卷积:提取高级时序特征
Conv1D(filters=32, kernel_size=3, activation="relu"),
MaxPooling1D(pool_size=2),
Dropout(0.2),
# 展平与全连接层
Flatten(),
Dense(32, activation="relu"),
Dropout(0.2),
# 输出层:5分类softmax
Dense(NUM_CLASSES, activation="softmax")
])
# 编译模型
model.compile(
optimizer="adam",
loss="categorical_crossentropy",
metrics=["accuracy"]
)
model.summary()
return model
# 模型训练与验证
def train_model(model, X_train, X_test, y_train, y_test):
# 训练模型
history = model.fit(
X_train, y_train,
epochs=50,
batch_size=16,
validation_data=(X_test, y_test),
verbose=1
)
# 评估模型精度
test_loss, test_acc = model.evaluate(X_test, y_test, verbose=0)
print(f"模型训练完成!测试集准确率:{test_acc:.4f}")
# 保存模型
model.save(MODEL_SAVE_PATH)
print(f"模型已保存:{MODEL_SAVE_PATH}")
return model, history
# INT8量化与TFLite模型转换
def quantize_and_convert_tflite(model, X_train):
# 代表性数据集生成函数(用于INT8量化校准)
def representative_data_gen():
for i in range(X_train.shape[0]):
yield [X_train[i:i+1].astype(np.float32)]
# 配置INT8量化转换器
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_data_gen
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
# 转换模型
tflite_model = converter.convert()
# 保存TFLite模型
with open(TFLITE_MODEL_PATH, "wb") as f:
f.write(tflite_model)
model_size = len(tflite_model) / 1024
print(f"INT8量化完成!TFLite模型大小:{model_size:.2f} KB")
print(f"模型已保存:{TFLITE_MODEL_PATH}")
return tflite_model
# 转换为TinyML支持的Arduino头文件
def convert_to_tinyml_header(tflite_model):
import subprocess
# 用xxd工具转换为C语言字节数组
with open(TINYML_MODEL_CC, "wb") as f:
subprocess.run(["xxd", "-i", TFLITE_MODEL_PATH], stdout=f, check=True)
# 修改变量名,生成头文件
with open(TINYML_MODEL_CC, "r", encoding="utf-8") as f:
cc_content = f.read()
# 替换变量名
cc_content = cc_content.replace("unsigned char gesture_model_tflite[]", "const unsigned char g_gesture_model[]")
cc_content = cc_content.replace("unsigned int gesture_model_tflite_len", "const unsigned int g_gesture_model_len")
# 写入修改后的.cc文件
with open(TINYML_MODEL_CC, "w", encoding="utf-8") as f:
f.write(cc_content)
# 生成.h头文件
h_content = """#ifndef GESTURE_MODEL_H
#define GESTURE_MODEL_H
extern const unsigned char g_gesture_model[];
extern const unsigned int g_gesture_model_len;
#endif
"""
with open(TINYML_MODEL_H, "w", encoding="utf-8") as f:
f.write(h_content)
print(f"TinyML模型文件生成完成:{TINYML_MODEL_CC}、{TINYML_MODEL_H}")
print("请将这两个文件复制到Arduino项目目录中")
# ==================== 主程序 ====================
if __name__ == "__main__":
# 1. 加载与预处理数据
X_train, X_test, y_train, y_test, scaler, label_to_int = load_and_preprocess_data()
# 2. 构建模型
model = build_lightweight_model()
# 3. 训练模型
model, history = train_model(model, X_train, X_test, y_train, y_test)
# 4. 量化与转换为TFLite模型
tflite_model = quantize_and_convert_tflite(model, X_train)
# 5. 转换为Arduino可用的头文件
convert_to_tinyml_header(tflite_model)
gesture_model.cc和gesture_model.h两个文件,用于后续Arduino端部署。注意:Windows系统需要安装Git Bash或WSL才能使用xxd命令,也可以手动在线转换TFLite模型为C语言字节数组。
现在,我们将训练好的TinyML模型部署到Arduino Uno R3,实现端侧实时AI推理与智能灯光决策。
系统初始化
在Arduino IDE中新建项目,命名为Gesture_AI_Control,将之前生成的gesture_model.cc和gesture_model.h文件复制到项目目录中,然后复制以下代码:
#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
// TinyML核心库
#include <TensorFlowLite.h>
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/schema/schema_generated.h"
// 我们训练好的模型头文件
#include "gesture_model.h"
// ==================== 配置参数 ====================
const int BAUD_RATE = 115200;
const int SAMPLE_RATE = 100;
const int SAMPLE_SIZE = 100;
const int MPU6050_ADDR = 0x68;
// LED控制引脚
const int PIN_LED_RED = 9;
const int PIN_LED_GREEN = 10;
const int PIN_LED_BLUE = 11;
// 模型配置
const int NUM_FEATURES = 6;
const int NUM_CLASSES = 5;
const int TENSOR_ARENA_SIZE = 64 * 1024; // 64KB张量内存池
// 手势类别映射
const char* GESTURE_NAMES[] = {"静止", "上挥", "下挥", "左挥", "右挥"};
// ==================== 全局变量 ====================
Adafruit_MPU6050 mpu;
// 传感器数据存储
float accelX[SAMPLE_SIZE], accelY[SAMPLE_SIZE], accelZ[SAMPLE_SIZE];
float gyroX[SAMPLE_SIZE], gyroY[SAMPLE_SIZE], gyroZ[SAMPLE_SIZE];
int sampleIndex = 0;
bool isCollecting = true;
// TinyML全局变量
const tflite::Model* model = nullptr;
tflite::MicroInterpreter* interpreter = nullptr;
TfLiteTensor* input_tensor = nullptr;
TfLiteTensor* output_tensor = nullptr;
uint8_t tensor_arena[TENSOR_ARENA_SIZE]; // 张量内存池
// ==================== 核心函数 ====================
// 初始化MPU6050传感器
void initMPU6050() {
if (!mpu.begin(MPU6050_ADDR)) {
Serial.println("【错误】MPU6050初始化失败!");
while (1) delay(10);
}
mpu.setAccelerometerRange(MPU6050_RANGE_2_G);
mpu.setGyroRange(MPU6050_RANGE_250_DEG);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
Serial.println("【成功】MPU6050初始化完成");
}
// 初始化TFLite Micro推理引擎
void initTFLiteMicro() {
// 加载模型
model = tflite::GetModel(g_gesture_model);
if (model->version() != TFLITE_SCHEMA_VERSION) {
Serial.println("【错误】模型版本不匹配!");
while (1) delay(10);
}
// 定义模型用到的算子
static tflite::MicroMutableOpResolver<5> resolver;
resolver.AddConv1D();
resolver.AddMaxPool1D();
resolver.AddFullyConnected();
resolver.AddSoftmax();
resolver.AddReshape();
// 初始化解释器
static tflite::MicroInterpreter static_interpreter(
model, resolver, tensor_arena, TENSOR_ARENA_SIZE
);
interpreter = &static_interpreter;
// 分配张量内存
TfLiteStatus allocate_status = interpreter->AllocateTensors();
if (allocate_status != kTfLiteOk) {
Serial.println("【错误】张量内存分配失败!");
while (1) delay(10);
}
// 获取输入输出张量
input_tensor = interpreter->input(0);
output_tensor = interpreter->output(0);
Serial.println("【成功】TFLite Micro推理引擎初始化完成");
}
// 初始化LED引脚
void initLED() {
pinMode(PIN_LED_RED, OUTPUT);
pinMode(PIN_LED_GREEN, OUTPUT);
pinMode(PIN_LED_BLUE, OUTPUT);
// 初始关闭所有LED
digitalWrite(PIN_LED_RED, LOW);
digitalWrite(PIN_LED_GREEN, LOW);
digitalWrite(PIN_LED_BLUE, LOW);
Serial.println("【成功】LED执行单元初始化完成");
}
// 采集单帧传感器数据
void collectSingleData() {
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
accelX[sampleIndex] = a.acceleration.x;
accelY[sampleIndex] = a.acceleration.y;
accelZ[sampleIndex] = a.acceleration.z;
gyroX[sampleIndex] = g.gyro.x;
gyroY[sampleIndex] = g.gyro.y;
gyroZ[sampleIndex] = g.gyro.z;
sampleIndex++;
}
// 数据预处理:转换为模型输入的INT8格式
void preprocessData() {
for (int i = 0; i < SAMPLE_SIZE; i++) {
int base_idx = i * NUM_FEATURES;
// 量化缩放:与训练时的标准化对应,可根据实际scaler参数调整
input_tensor->data.int8[base_idx + 0] = (int8_t)(accelX[i] * 30);
input_tensor->data.int8[base_idx + 1] = (int8_t)(accelY[i] * 30);
input_tensor->data.int8[base_idx + 2] = (int8_t)(accelZ[i] * 30);
input_tensor->data.int8[base_idx + 3] = (int8_t)(gyroX[i] * 10);
input_tensor->data.int8[base_idx + 4] = (int8_t)(gyroY[i] * 10);
input_tensor->data.int8[base_idx + 5] = (int8_t)(gyroZ[i] * 10);
}
}
// 执行AI推理,返回手势分类结果
int predictGesture() {
// 执行推理
TfLiteStatus invoke_status = interpreter->Invoke();
if (invoke_status != kTfLiteOk) {
Serial.println("【错误】AI推理失败!");
return 0;
}
// 找到概率最高的手势类别
int max_idx = 0;
int8_t max_value = output_tensor->data.int8[0];
for (int i = 1; i < NUM_CLASSES; i++) {
if (output_tensor->data.int8[i] > max_value) {
max_value = output_tensor->data.int8[i];
max_idx = i;
}
}
return max_idx;
}
// 根据手势结果控制LED灯
void controlLED(int gesture_idx) {
// 先关闭所有LED
digitalWrite(PIN_LED_RED, LOW);
digitalWrite(PIN_LED_GREEN, LOW);
digitalWrite(PIN_LED_BLUE, LOW);
// 根据手势执行对应逻辑
switch (gesture_idx) {
case 1: // 上挥 → 红灯亮
digitalWrite(PIN_LED_RED, HIGH);
break;
case 2: // 下挥 → 绿灯亮
digitalWrite(PIN_LED_GREEN, HIGH);
break;
case 3: // 左挥 → 蓝灯亮
digitalWrite(PIN_LED_BLUE, HIGH);
break;
case 4: // 右挥 → 全亮(白色)
digitalWrite(PIN_LED_RED, HIGH);
digitalWrite(PIN_LED_GREEN, HIGH);
digitalWrite(PIN_LED_BLUE, HIGH);
break;
default: // 静止 → 全灭
break;
}
}
// ==================== 主程序 ====================
void setup() {
Serial.begin(BAUD_RATE);
while (!Serial) delay(10);
Serial.println("="*50);
Serial.println("Arduino端侧AI手势识别系统启动");
Serial.println("="*50);
// 初始化各模块
initLED();
initMPU6050();
initTFLiteMicro();
Serial.println("【成功】系统初始化完成!开始实时手势识别...");
Serial.println("="*50);
}
void loop() {
// 循环采集传感器数据
if (isCollecting && sampleIndex < SAMPLE_SIZE) {
collectSingleData();
delay(1000 / SAMPLE_RATE);
}
// 采集完成,执行AI推理与决策
if (isCollecting && sampleIndex >= SAMPLE_SIZE) {
// 数据预处理
preprocessData();
// AI推理
int gesture_idx = predictGesture();
// 串口输出结果
Serial.print("【推理结果】手势:");
Serial.print(GESTURE_NAMES[gesture_idx]);
Serial.print(" | 置信度:");
Serial.println(output_tensor->data.int8[gesture_idx]);
// 执行LED控制
controlLED(gesture_idx);
// 重置采集索引,进入下一轮采集
sampleIndex = 0;
}
}
gesture_model.cc、gesture_model.h和主程序放在同一Arduino项目目录下;恭喜你!你已经成功在50元的Arduino单片机上,实现了一套完整的端侧物联+AI智能决策系统!
TENSOR_ARENA_SIZE的大小,Arduino Uno R3最大可设置为64KB,避免内存分配失败;物联+AI的核心,从来不是高端的硬件和复杂的算法,而是用最低的成本、最简单的方案,解决真实的业务问题。本文用不到50元的硬件,带大家完整走通了「数据采集→模型训练→端侧部署→智能决策」的全流程,真正实现了零基础也能落地的端侧AI系统。
随着TinyML技术的发展,未来会有越来越多的低成本单片机具备端侧AI能力,这也是物联网行业的核心发展趋势——让每一个传感器都具备智能决策能力,无需云端、无需网络、低延迟、高隐私。
希望这篇文章能帮你推开物联+AI的大门,从纸上谈兵走向真正的落地实战。