联系了 RA 的工作人员,给我补发了一块拓展板,不过快递还要几天才能到,就先把 tinyML 这部分做了。整体思路是用一个 IMU 作为数据源,采集三种运动状态——静止、拿起、晃动,每种各 50 组左右,用这批数据来训练一个轻量分类模型。IMU 选用 MPU6050,所以第一步要先把 MPU6050 的输入采集通道配置好。
为了方便接入 MPU6050,这里在 Arduino 接口上接了一块拓展面包板,把传感器固定在上面走线。

arduino接口的引脚定义如下
| UNO RESET | P813 | 复位,带上拉 |
| UNO D19 | PA15 | SCL9_C |
| UNO D18 | PA14 | SDA9_C |
| UNO AREF | VREFH0 | 模拟参考 |
| UNO D13 | P412 | RSPCKB_B |
| UNO D12 | P410 | MISOB_B |
| UNO D11 | P411 | MOSIB_B |
| UNO D10 | P413 | SSLB0_B |
| UNO D9~ | PA07 | GTIOC7A - PWM |
| UNO D8 | P504 | 数字 I/O 口 |
| UNO D7 | P505 | 数字 I/O 口 |
| UNO D6~ | PA06 | GTIOC7B - PWM |
| UNO D5~ | PA11 | GTIOC6A - PWM |
| UNO D4 | P506 | 数字 I/O 口 |
| UNO D3~ | P907 | GTIOC13A - PWM |
| UNO D2 | P509 | 数字 I/O 口 |
| UNO D1 | P409 | TXD3_A |
| UNO D0 | P408 | RXD3_A |
| UNO A0 | P004 | AN000 |
| UNO A1 | P003 | AN104 |
| UNO A2 | P007 | AN004 |
| UNO A3 | P011 | AN106 |
| UNO A4 | P014 | AN007/DA0 |
| UNO A5 | P015 | AN105/DA1 |
按标准 Arduino 定义,IIC 用的两个引脚应该是 PA14(SDA)和 PA15(SCL)。但实际配置时发现这两个引脚都已经被 SDRAM 占用了,在 Pin 配置里根本挪不出来。

于是去查了 RA8D1 的硬件复用表,结果发现 PA14/PA15 压根就没有 IIC 复用功能——它俩只能做 GPIO、SDRAM 数据线这类用途。也就是说,板子把标准 Arduino 的 SDA/SCL 位接到了没有 IIC 能力的引脚上,所以无论在 Pin 里怎么找,都配不出 IIC。
RA8D1 一共有两个硬件 IIC 通道,逐个梳理可用引脚:
IIC0(SCL0/SDA0)可选 P204/P205、P400/P401、P408/P409。其中 P400/P401 被摄像头和 I3C 复用,P408/P409 被 SCI3 串口(即 Arduino D0/D1)占用,只剩 P204/P205 空闲。
IIC1(SCL1/SDA1)可选 P100/P101、P205/P206、P511/P512。其中 P100/P101 被 OSPI Flash 占用,P206 被 MIPI-DSI 占用,P511/P512 走 I3C 排针那一路,工程里已经配好并启用。
看着引脚不少,真正能自由支配的其实没几个。而且实际在 Pin 里找的时候发现,连 P204/P205 这两个引脚都没有暴露出来。

所以能走的只剩 I3C 排针上的 P511/P512,这一路引脚工程已经自动配好了。虽然是从 I3C 排针引出的,但配置本身走的是 I2C 模式(硬件上也不支持 I3C),所以直接当普通 I2C 用就行。

这里需要把引脚改成开漏输出,I2C 总线必须开漏才能正常做线与和上拉。P511 改完之后,P512 同理。

stack 也已经自动配置好了,只需要把从机地址改一下即可(MPU6050 的 AD0 接地时地址为 0x68)。

最终接线如上图。传感器依旧固定在 Arduino 接口上,IIC 用杜邦线连到 P511/P512,固定住主要是为了后面采集数据时姿态稳定、不受线材晃动干扰。

接下来是数据采集的代码。逻辑是按键触发一次采集,采集期间点亮 LED 作为提示,按一个采样周期读一次 MPU6050,边采边通过串口打印成 CSV,方便直接落盘。原始值在片上就转成工程单位,并且统一乘以固定倍数用整数表示,避免引入浮点。
#include "imu_logger.h"
#include <stdio.h>
volatile imu_log_state_t g_imu_state = IMU_LOG_STATE_INIT;
volatile uint32_t g_imu_capture_seq = 0;
volatile uint8_t g_imu_who_am_i = 0;
/* ---------- 按键和LED ---------- */
static bool key_down(void)
{
bsp_io_level_t level;
R_IOPORT_PinRead(&g_ioport_ctrl, IMU_LOG_KEY_PIN, &level);
return (level == IMU_LOG_KEY_PRESSED);
}
// 按下并且消抖后还按着才算
static bool key_pressed(void)
{
if (!key_down()) return false;
R_BSP_SoftwareDelay(20, BSP_DELAY_UNITS_MILLISECONDS);
return key_down();
}
// 等到松手
static void key_wait_up(void)
{
while (key_down());
R_BSP_SoftwareDelay(20, BSP_DELAY_UNITS_MILLISECONDS);
}
static void led_on(void) { R_IOPORT_PinWrite(&g_ioport_ctrl, IMU_LOG_LED_PIN, IMU_LOG_LED_ON); }
static void led_off(void) { R_IOPORT_PinWrite(&g_ioport_ctrl, IMU_LOG_LED_PIN, IMU_LOG_LED_OFF); }
/* ---------- 原始值转工程单位,乘1000用整数,免得用float ---------- */
// 默认2g量程,LSB=16384
static int32_t accel_mg(int16_t raw)
{
return ((int32_t)raw * 1000) / 16384;
}
// 默认250dps量程,灵敏度131,先乘10000再除
static int32_t gyro_mdps(int16_t raw)
{
return ((int32_t)raw * 10000) / 1310;
}
// 手册: 温度 = raw/340 + 36.53
static int32_t temp_mc(int16_t raw)
{
return ((int32_t)raw * 1000) / 340 + 36530;
}
/* ---------- 采集,边采边打 ---------- */
static void do_capture(void)
{
// 表头
printf("# mpu6050 capture, seq=%lurn", (unsigned long)(g_imu_capture_seq + 1));
printf("# who_am_i=%urn", g_imu_who_am_i);
printf("t_us,ax_mg,ay_mg,az_mg,gx_mdps,gy_mdps,gz_mdps,temp_mcrn");
for (uint32_t i = 0; i < IMU_LOG_MAX_SAMPLES; i++) {
// 隔一个采样周期读一次
if (i != 0)
R_BSP_SoftwareDelay(IMU_LOG_PERIOD_US, BSP_DELAY_UNITS_MICROSECONDS);
mpu6050_raw_t raw;
if (mpu6050_read_raw(&raw) != FSP_SUCCESS)
break; // 读失败就停
printf("%lu,%ld,%ld,%ld,%ld,%ld,%ld,%ldrn",
(unsigned long)(i * IMU_LOG_PERIOD_US),
(long)accel_mg(raw.accel_x),
(long)accel_mg(raw.accel_y),
(long)accel_mg(raw.accel_z),
(long)gyro_mdps(raw.gyro_x),
(long)gyro_mdps(raw.gyro_y),
(long)gyro_mdps(raw.gyro_z),
(long)temp_mc(raw.temp));
}
}
/* ---------- 初始化 ---------- */
fsp_err_t imu_logger_init(void)
{
led_off();
if (mpu6050_open(IMU_LOG_ACCEL_FS, IMU_LOG_GYRO_FS) != FSP_SUCCESS) {
g_imu_state = IMU_LOG_STATE_INIT_FAILED;
return FSP_ERR_ASSERTION;
}
uint8_t id;
mpu6050_who_am_i(&id);
g_imu_who_am_i = id;
g_imu_state = IMU_LOG_STATE_IDLE;
return FSP_SUCCESS;
}
void imu_logger_run(void)
{
// 初始化失败就一直闪灯
if (g_imu_state == IMU_LOG_STATE_INIT_FAILED) {
while (1) {
led_on();
R_BSP_SoftwareDelay(150, BSP_DELAY_UNITS_MILLISECONDS);
led_off();
R_BSP_SoftwareDelay(150, BSP_DELAY_UNITS_MILLISECONDS);
}
}
while (1) {
g_imu_state = IMU_LOG_STATE_IDLE;
if (!key_pressed()) {
R_BSP_SoftwareDelay(5, BSP_DELAY_UNITS_MILLISECONDS);
continue;
}
led_on(); // 灯亮的3秒别动板子
g_imu_state = IMU_LOG_STATE_CAPTURING;
do_capture();
g_imu_capture_seq++;
g_imu_state = IMU_LOG_STATE_READY;
led_off();
key_wait_up();
}
}三个动作各触发一轮采集,每组拿到 50 组左右的数据,分别存到 still、shake、pickup 三个目录下。
然后是模型训练。网络用了 2 个一维卷积层(conv1、conv2)+ 2 个 BatchNorm 层 + 1 个全连接层(fc),结构很轻,契合 tinyML 部署到 MCU 的目标。输入是 6 轴 × 时间点的序列,经过两层卷积和一次时间轴下采样后,用全局平均池化压成每通道一个特征,再送进分类头输出三类的 logits。
代码如下
import torch
from torch import nn
import pandas as pd
from torch.utils.data import Dataset
import os
from torch.utils.data import DataLoader, random_split
class IMUDataset(Dataset):
LABEL_MAP = {"still": 0, "shake": 1, "pickup": 2}
def __init__(self, root = "imu_data"):
self.samples = []
for name,label in self.LABEL_MAP.items():
folder = os.path.join(root,name)
for fname in os.listdir(folder):
path = os.path.join(folder,fname)
self.samples.append((path,label))
def __len__(self):
return len(self.samples)
def __getitem__(self, i):
path, label = self.samples[i]
df = pd.read_csv(path)
data = df.values.astype("float32")
data = data[:, 1:]
x = torch.from_numpy(data)
x = x.T
y = label
return x,y
class IMUNet(nn.Module):
"""三状态分类 CNN
输入: x [batch, 6, 300] 6轴, 300个时间点
输出: [batch, 3] 三类的原始分数(logits)
"""
def __init__(self, n_classes=3):
super().__init__() # 必须: 启动父类 nn.Module 的登记机制
# 下面每个 self.xxx = 一个层, 赋值时自动被"注册"进参数名单
self.conv1 = nn.Conv1d(6, 16, kernel_size=5, padding=2) # 6->16通道
self.bn1 = nn.BatchNorm1d(16) # 归一化, 稳定训练
self.pool = nn.MaxPool1d(2) # 时间轴砍半
self.conv2 = nn.Conv1d(16, 32, kernel_size=3, padding=1) # 16->32通道
self.bn2 = nn.BatchNorm1d(32)
self.gap = nn.AdaptiveAvgPool1d(1) # GlobalAvgPool -> 每通道压成1个数
self.fc = nn.Linear(32, n_classes) # 分类头: 32特征 -> 3类
self.relu = nn.ReLU()
def forward(self, x):
# x 进来: [batch, 6, 300]
x = self.relu(self.bn1(self.conv1(x))) # [b, 16, 300]
x = self.pool(x) # [b, 16, 150]
x = self.relu(self.bn2(self.conv2(x))) # [b, 32, 150]
x = self.gap(x) # [b, 32, 1]
x = x.flatten(1) # [b, 32] 去掉最后的1维
x = self.fc(x) # [b, 3]
return x
full_ds = IMUDataset('imu_data')
n_test = int(len(full_ds) * 0.2)
n_train = len(full_ds) - n_test
train_ds, test_ds = random_split(full_ds, [n_train, n_test])
batch_size = 16
train_iter = DataLoader(train_ds, batch_size=batch_size, shuffle=True)
test_iter = DataLoader(test_ds, batch_size=batch_size, shuffle=False)
print(f'训练样本 {n_train}, 测试样本 {n_test}')
# ---------- 模型 / 损失 / 优化器 ----------
net = IMUNet(n_classes=3)
loss = nn.CrossEntropyLoss()
updater = torch.optim.Adam(net.parameters(), lr=1e-3) # net.parameters() 自动拿全部参数
# ---------- 训练 ----------
num_epochs = 30
for epoch in range(num_epochs):
net.train() # 切训练模式(BatchNorm 需要)
net_loss, n = 0.0, 0
for X, y in train_iter:
y_hat = net(X)
l = loss(y_hat, y)
updater.zero_grad() # 清空上轮梯度
l.backward() # 反向传播
updater.step() # 更新参数
net_loss += l.item() * y.numel()
n += y.numel()
# 测试集准确率
net.eval() # 切评估模式
correct, total = 0, 0
with torch.no_grad():
for X, y in test_iter:
y_hat = net(X)
correct += (y_hat.argmax(dim=1) == y).sum().item()
total += y.numel()
print(f'epoch {epoch+1:2d}, loss {net_loss/n:.4f}, test acc {correct/total:.4f}')
# ---------- 训练完保存权重 ----------
torch.save(net.state_dict(), 'imu_net.pth')
print('模型已保存到 imu_net.pth')训练结果如下。
从日志能看出整个训练过程很顺:loss 从 1.20 稳步降到 0.25 左右,测试集准确率在前几个 epoch 就冲到了 0.86,大约第 11 个 epoch 稳定在 0.93 以上,第 24 个 epoch 起多次触及 1.0。最终 29 个 epoch 收敛下来,测试集准确率稳定在 0.96~1.0 之间。
考虑到三个动作(静止、拿起、晃动)在信号特征上本身区分度就比较高——静止时各轴基本不变,拿起是一段带方向的低频加速度,晃动则是高频往复——加上样本量虽小但比较干净,能达到这个准确率是符合预期的。测试集只有 29 个样本,准确率在几个点之间跳动主要是样本基数太小导致的,单个样本分类对错就会带来约 3.4% 的波动,所以不必太在意 1.0 和 0.9655 之间的来回,整体已经收敛得相当稳。
我要赚赏金
