这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 【瑞萨RA8D1测评】IMU动作识别的数据采集与模型训练

共1条 1/1 1 跳转至

【瑞萨RA8D1测评】IMU动作识别的数据采集与模型训练

菜鸟
2026-09-18 19:28:21     打赏

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

为了方便接入 MPU6050,这里在 Arduino 接口上接了一块拓展面包板,把传感器固定在上面走线。

image.png

arduino接口的引脚定义如下

Arduino 管脚MCU 端口特别功能
UNO RESETP813复位,带上拉
UNO D19PA15SCL9_C
UNO D18PA14SDA9_C
UNO AREFVREFH0模拟参考
UNO D13P412RSPCKB_B
UNO D12P410MISOB_B
UNO D11P411MOSIB_B
UNO D10P413SSLB0_B
UNO D9~PA07GTIOC7A - PWM
UNO D8P504数字 I/O 口
UNO D7P505数字 I/O 口
UNO D6~PA06GTIOC7B - PWM
UNO D5~PA11GTIOC6A - PWM
UNO D4P506数字 I/O 口
UNO D3~P907GTIOC13A - PWM
UNO D2P509数字 I/O 口
UNO D1P409TXD3_A
UNO D0P408RXD3_A
UNO A0P004AN000
UNO A1P003AN104
UNO A2P007AN004
UNO A3P011AN106
UNO A4P014AN007/DA0
UNO A5P015AN105/DA1

按标准 Arduino 定义,IIC 用的两个引脚应该是 PA14(SDA)和 PA15(SCL)。但实际配置时发现这两个引脚都已经被 SDRAM 占用了,在 Pin 配置里根本挪不出来。

image.png

于是去查了 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 这两个引脚都没有暴露出来。

image.png

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

image.png

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

屏幕截图 2026-09-18 122955.png

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

屏幕截图 2026-09-17 224734.png

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

ec3cda96809deb9a6c0f8c7084379458_720.jpg

接下来是数据采集的代码。逻辑是按键触发一次采集,采集期间点亮 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')

训练结果如下。image.png

从日志能看出整个训练过程很顺: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 之间的来回,整体已经收敛得相当稳。













共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]