4. Arduino Nano 33 BLE Sense上的运动识别
任务目标
在Arduino Nano 33 BLE Sense上实现基于IMU(惯性测量单元)数据的实时运动识别,能够区分静止、走路、跑步三种状态。工作流程为:采集IMU数据 → 在PC端训练LSTM模型 → 将模型转换为TensorFlow Lite格式 → 部署到开发板进行实时推理。
环境与依赖
- 硬件:Arduino Nano 33 BLE Sense(板载LSM9DS1 IMU)
- PC端:Python 3.8+,安装
tensorflow==2.13.*、numpy、pandas、scikit-learn、tensorflow-lite(用于模型转换) - Arduino端:Arduino IDE 1.8.19+,安装
Arduino_LSM9DS1库、Arduino_TensorFlowLite库(版本≥2.4.0)
实现方案
整体分为三个环节:数据采集(Arduino端)、模型训练与转换(PC端)、部署推理(Arduino端)。本案例提供完整可运行的代码,并限定在以下范围:使用6轴IMU数据(加速度计3轴+陀螺仪3轴),每帧采集30个采样点(窗口大小30),采样频率约50Hz,即每个窗口代表约0.6秒的运动数据。模型输入为30×6的时序特征,输出为3类动作概率。
第一步:Arduino数据采集固件
将以下代码烧录到开发板,通过串口以CSV格式输出IMU原始数据。每行包含6个浮点数(ax, ay, az, gx, gy, gz),波特率115200。采集时分别保持静止、走路、跑步各3分钟,保存为三个CSV文件用于训练。
#include <Arduino_LSM9DS1.h>
const int WINDOW_SIZE = 30;
float ax, ay, az, gx, gy, gz;
void setup() {
Serial.begin(115200);
while (!Serial);
if (!IMU.begin()) {
Serial.println("IMU init failed!");
while (1);
}
Serial.println("ax,ay,az,gx,gy,gz");
}
void loop() {
for (int i = 0; i < WINDOW_SIZE; i++) {
if (IMU.accelerationAvailable() && IMU.gyroscopeAvailable()) {
IMU.readAcceleration(ax, ay, az);
IMU.readGyroscope(gx, gy, gz);
Serial.print(ax); Serial.print(",");
Serial.print(ay); Serial.print(",");
Serial.print(az); Serial.print(",");
Serial.print(gx); Serial.print(",");
Serial.print(gy); Serial.print(",");
Serial.println(gz);
delay(20); // 约50Hz采样
}
}
// 每个窗口后发送空行作为分隔
Serial.println();
}
第二步:PC端LSTM训练与模型转换
将采集的三个CSV文件(static.csv, walk.csv, run.csv)放在与脚本相同目录下。运行以下Python脚本完成数据预处理、LSTM训练、模型转换为TensorFlow Lite格式,并输出C语言头文件供Arduino使用。
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout
from tensorflow.keras.utils import to_categorical
import tensorflow as tf
WINDOW = 30
STEP = 15 # 窗口滑动步长
def load_data(filename, label):
df = pd.read_csv(filename)
data = df.values.astype(np.float32)
windows = []
for i in range(0, len(data) - WINDOW, STEP):
windows.append(data[i:i+WINDOW])
return np.array(windows), np.full(len(windows), label)
X_static, y_static = load_data('static.csv', 0)
X_walk, y_walk = load_data('walk.csv', 1)
X_run, y_run = load_data('run.csv', 2)
X = np.concatenate([X_static, X_walk, X_run], axis=0)
y = np.concatenate([y_static, y_walk, y_run], axis=0)
# 归一化(使用全局统计量)
mean = X.mean(axis=(0,1), keepdims=True)
std = X.std(axis=(0,1), keepdims=True) + 1e-6
X = (X - mean) / std
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
y_train = to_categorical(y_train, num_classes=3)
y_test_cat = to_categorical(y_test, num_classes=3)
# LSTM模型
model = Sequential([
LSTM(32, input_shape=(WINDOW, 6), return_sequences=False),
Dropout(0.3),
Dense(16, activation='relu'),
Dense(3, activation='softmax')
])
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
model.fit(X_train, y_train, epochs=20, batch_size=32, validation_split=0.1, verbose=1)
# 评估
loss, acc = model.evaluate(X_test, y_test_cat, verbose=0)
print(f"测试准确率: {acc:.3f}")
# 转换为TFLite并生成C头文件
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
# 保存为头文件(使用xxd格式)
with open('model.tflite', 'wb') as f:
f.write(tflite_model)
# 生成C数组头文件
import subprocess
subprocess.run(['xxd', '-i', 'model.tflite', 'model_data.h'], check=True)
print("模型已保存为 model_data.h")
运行后得到 model_data.h 文件,其中包含 model_tflite 数组和长度变量。记录终端输出的测试准确率作为模型性能参考。
第三步:Arduino部署与实时推理
将生成的 model_data.h 放入Arduino项目文件夹。以下代码实现:每采集30个采样点(约0.6秒)执行一次推理,通过串口输出识别结果(0=静止,1=走路,2=跑步)。
#include <Arduino_LSM9DS1.h>
#include <TensorFlowLite.h>
#include "tensorflow/lite/micro/all_ops_resolver.h"
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "model_data.h"
const int WINDOW = 30;
const int NUM_FEATURES = 6;
const int NUM_CLASSES = 3;
float imu_data[WINDOW][NUM_FEATURES];
int data_index = 0;
// 归一化参数(需与训练时一致)
const float MEAN[NUM_FEATURES] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
const float STD[NUM_FEATURES] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f};
namespace {
const tflite::Model* model = nullptr;
tflite::MicroInterpreter* interpreter = nullptr;
TfLiteTensor* input_tensor = nullptr;
TfLiteTensor* output_tensor = nullptr;
constexpr int kTensorArenaSize = 8 * 1024;
uint8_t tensor_arena[kTensorArenaSize];
}
void setup() {
Serial.begin(115200);
while (!Serial);
if (!IMU.begin()) {
Serial.println("IMU init failed!");
while (1);
}
// 加载模型
model = tflite::GetModel(model_tflite);
static tflite::AllOpsResolver resolver;
static tflite::MicroInterpreter static_interpreter(model, resolver, tensor_arena, kTensorArenaSize);
interpreter = &static_interpreter;
interpreter->AllocateTensors();
input_tensor = interpreter->input(0);
output_tensor = interpreter->output(0);
Serial.println("Model loaded.");
}
void loop() {
float ax, ay, az, gx, gy, gz;
if (IMU.accelerationAvailable() && IMU.gyroscopeAvailable()) {
IMU.readAcceleration(ax, ay, az);
IMU.readGyroscope(gx, gy, gz);
imu_data[data_index][0] = ax;
imu_data[data_index][1] = ay;
imu_data[data_index][2] = az;
imu_data[data_index][3] = gx;
imu_data[data_index][4] = gy;
imu_data[data_index][5] = gz;
data_index++;
delay(20);
if (data_index >= WINDOW) {
// 填充输入张量并归一化
for (int i = 0; i < WINDOW; i++) {
for (int j = 0; j < NUM_FEATURES; j++) {
input_tensor->data.f[i * NUM_FEATURES + j] =
(imu_data[i][j] - MEAN[j]) / STD[j];
}
}
// 推理
interpreter->Invoke();
int pred = 0;
float max_prob = output_tensor->data.f[0];
for (int c = 1; c < NUM_CLASSES; c++) {
if (output_tensor->data.f[c] > max_prob) {
max_prob = output_tensor->data.f[c];
pred = c;
}
}
const char* labels[] = {"静止", "走路", "跑步"};
Serial.print("识别结果: ");
Serial.println(labels[pred]);
data_index = 0;
}
}
}
重要:Arduino代码中的
MEAN 和 STD 数组必须替换为Python训练脚本中实际计算出的全局均值与标准差(在脚本中打印 mean.flatten() 和 std.flatten() 获取)。否则推理结果将严重失真。验证方法与通过标准
- 数据采集验证:烧录第一步代码后,打开串口监视器,应看到6列浮点数持续输出。静止时加速度计数值接近(0, 0, ±1g),走路/跑步时波形有明显波动。
- 训练验证:Python脚本运行结束后,终端输出测试集准确率。当三个类别数据量各≥500个窗口时,准确率应≥0.90。若低于此值,增加采集数据量或训练轮次。
- 部署验证:烧录部署代码后,打开串口监视器(波特率115200)。依次进行以下动作,每个动作持续5秒以上,观察输出:
- 保持静止 → 应输出“静止”
- 原地匀速走路 → 应输出“走路”
- 原地跑步 → 应输出“跑步”
常见问题
- 编译错误(内存不足):若提示
region 'RAM' overflowed,将kTensorArenaSize从8192减小到6144,或使用tflite::MicroMutableOpResolver<10>替代AllOpsResolver以减少内存占用。 - 推理结果始终为同一类:检查归一化参数是否正确填入,以及IMU数据是否正常更新(可在填充前打印原始值调试)。
- 串口输出乱码:确认Arduino IDE串口监视器波特率设置为115200,且开发板供电稳定(建议USB直连电脑)。