【ルネサス技術コラム】Arduino Nano R4で商品落下を検知する簡易ソリューションを作ってみた

売り場や倉庫、作業現場では、

  • 商品が棚や展示台から落ちたことにすぐ気づけない

  • 無人時間帯に落下が起きても発見が遅れる

  • 破損や再陳列の対応が後手になる

  • 「いつ・どのタイミングで落ちたのか」が分かりにくい

といった困りごとがあります。

こうした課題に対して、まず試しやすいのが、加速度センサで「落下らしい動き」や「大きな衝撃」「大きな傾き」を検知し、ブザーでその場に知らせる方法です。今回も、Renesas RA4M1搭載のArduino Nano R4 と MPU6050加速度センサ、ブザーを使って、商品落下を検知して警告音で知らせる簡易システムを作ってみました。

今回の考え方は、商品そのものにセンサを付けるというより、商品を載せる小型トレー・展示台・治具などにセンサを固定し、その台が大きく傾いたり衝撃を受けたりしたら「落下イベント」とみなすというものです。

なお、今回の検知は厳密な事故判定システムではなく、落下らしきイベントを簡易に見つけるためのPoCです。まずは「見逃しにくくする」「その場で気づけるようにする」ことを主眼にしています。

目次

はじめに: どんな困りごとに対するソリューションなのか

商品落下の問題は、単に「物が落ちる」だけではありません。現場では次のような二次的な課題につながることがあります。

  • 落下後の破損や汚損

  • 売り場の見た目悪化

  • 補充や再陳列の手間

  • 落下した商品による安全面のリスク

  • 原因分析の難しさ

特に、常時目視できない場所や、担当者がその棚をずっと見ていない環境では、落下が起きた瞬間に気づけるかどうかが大きな差になります。

そこで今回は、

「商品や展示台が大きく動いた瞬間を検知して知らせたい」
「少なくとも“落下らしきイベントが起きた”ことを見逃しにくくしたい」 という困りごとに対して、シンプルなセンサ構成で取り組みます。

という困りごとに対して、シンプルなセンサ構成で取り組みます。

今回のソリューション概要

今回の仕組みでは、MPU6050加速度センサを使って、商品を載せた台の状態を読み取ります。

検知の考え方は次の通りです。

  • 急な衝撃が入った

  • 大きく傾いた

  • 落下に近い異常な加速度変化が起きた

といった条件のいずれかを満たした時に、落下イベントとして扱います。

そして、イベントを検知したら次のように動作します。

  • ブザー: 警告音を鳴らす

  • シリアルモニタ: 加速度、傾き、検知ログを出力

これにより、目の前にいなくても、少なくとも近くにいる担当者が音で異常に気づけるようになります。

ローカル検知の基本構成

今回の構成は、加速度センサで動きと傾きを見るというシンプルなものです。

基本の流れは次の通りです。

  1. センサから加速度を取得する

  2. 現在の姿勢角を計算する

  3. 通常時の姿勢と比べて大きく傾いていないか確認する

  4. 大きな衝撃や異常な加速度がないか確認する

  5. 条件を超えたら落下イベントとして扱い、ブザーで警告する

この方式にすると、カメラを使わなくても、比較的少ない部品でその場で気づける落下検知を試せます。

Arduino Nano R4を使う理由

今回は Arduino Nano R4 を使っています。

公式ドキュメントでは、Arduino Nano R4 は Renesas RA4M1を搭載した32-bit Arm Cortex-M4/48MHz/5V動作のボードとして案内されています。小型で扱いやすく、センサPoCをブレッドボード上でまとめやすい構成です。

また、PlatformIO上でもRA4M1/48MHz/256KB Flash / 32KB RAM のボードとして扱われ、board = nano_r4 で利用できます。今回のような加速度センサの読み取りと警告処理には十分な構成です。

小型の治具や展示台の近くに組み込みやすい点でも、Nano系フォームファクタは扱いやすい選択肢です。

使用した主な部品

・Arduino Nano R4
・MPU6050 加速度/ジャイロセンサ
・圧電ブザー
・ブレッドボード
・ジャンパーピン
・USB-C ケーブル

開発環境と実装のポイント

開発環境には Visual Studio Code と PlatformIO IDE を使用しました。PlatformIOでは、platform = renesas-ra, board = nano_r4, framework = arduino の設定で利用できます。

なぜ加速度だけでなく傾きも見るのか

落下イベントは、必ずしも「大きな衝撃」だけで表れるわけではありません。たとえば、

・商品を載せた台が大きく傾いて倒れた
・ゆっくり滑って落ちた
・衝撃よりも姿勢変化が大きい

といったケースもあります。

そのため今回は、

  • 加速度の大きさ

  • 通常姿勢からの傾き差

の両方を見ることで、単純な衝撃検知よりも扱いやすくしています。

閾値は現場で調整が必要

落下の仕方や台の材質、設置場所によって、適切な閾値は変わります。今回の値はPoC用の目安なので、誤検知と見逃しのバランスを見ながら現場に合わせて調整するのが前提です。

クールダウンを入れる理由

落下直後はセンサ値が大きく乱れるため、何度も連続検知してしまうことがあります。そこで、一定時間は再検知しないようにして、1回の出来事を1イベントとして扱いやすくしています。

配線情報

今回は、MPU6050をI2C接続し、さらにブザーを1本追加します。

配線表

モジュール

信号

接続先

補足

MPU6050

VCC

5V

モジュール仕様に応じて給電

MPU6050

GND

GND

共通GND

MPU6050

SDA

SDA

I2C通信用

MPU6050

SCL

SCL

I2C通信用

ブザー

+

D8

警告音出力

ブザー

-

GND

共通GND

配線イメージを言葉で整理すると

・MPU6050 は SDA / SCL で Nano R4 に接続
・ブザーは D8 と GND に接続
・センサとブザーは共通GNDにする

実際のプログラム

今回のローカル検知版で使用したプログラムは以下の通りです。 商品を載せた台の衝撃と傾きを見て落下イベントを検知し、ブザーを鳴らします。
※本プログラムは、生成AIを用いて作成しております。

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <math.h>

Adafruit_MPU6050 mpu;
const int buzzerPin = 8;

// 通常姿勢の基準
float basePitch = 0.0f;
float baseRoll = 0.0f;

// 検知しきい値(厳しめ)
const float freeFallThresholdG = 0.20f;
const float impactThresholdG = 2.80f;
const float tiltThresholdDeg = 65.0f;

// free-fall後、この時間内に衝撃 or 大傾きが来たら落下とみなす
const unsigned long fallConfirmWindowMs = 800;

// 再検知防止
unsigned long lastDetectTime = 0;
const unsigned long detectCooldownMs = 4000;

// free-fall候補状態
bool fallCandidate = false;
unsigned long fallCandidateTime = 0;

float radToDeg(float rad)
{
    return rad * 180.0f / PI;
}

void playAlert10Sec()
{
    for (int i = 0; i < 20; i++)
    {
        tone(buzzerPin, 2200);
        delay(250);
        noTone(buzzerPin);
        delay(250);
    }
}

void calibrateBaseline()
{
    const int samples = 30;
    float pitchSum = 0.0f;
    float rollSum = 0.0f;

    for (int i = 0; i < samples; i++)
    {
        sensors_event_t a, g, temp;
        mpu.getEvent(&a, &g, &temp);

        float pitch = radToDeg(atan2(a.acceleration.x, sqrt(a.acceleration.y * a.acceleration.y + a.acceleration.z * a.acceleration.z)));
        float roll = radToDeg(atan2(a.acceleration.y, sqrt(a.acceleration.x * a.acceleration.x + a.acceleration.z * a.acceleration.z)));

        pitchSum += pitch;
        rollSum += roll;
        delay(20);
    }

    basePitch = pitchSum / samples;
    baseRoll = rollSum / samples;
}

void setup()
{
    Serial.begin(115200);
    delay(1000);

    pinMode(buzzerPin, OUTPUT);
    Wire.begin();

    if (!mpu.begin())
    {
        Serial.println("Failed to find MPU6050.");
        while (true)
        {
            delay(1000);
        }
    }

    mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
    mpu.setGyroRange(MPU6050_RANGE_500_DEG);
    mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);

    calibrateBaseline();

    Serial.println("Product drop detector started.");
    Serial.print("Base pitch: ");
    Serial.println(basePitch);
    Serial.print("Base roll: ");
    Serial.println(baseRoll);
}

void loop()
{
    unsigned long now = millis();

    sensors_event_t a, g, temp;
    mpu.getEvent(&a, &g, &temp);

    float ax = a.acceleration.x;
    float ay = a.acceleration.y;
    float az = a.acceleration.z;

    float totalAccel = sqrt(ax * ax + ay * ay + az * az);
    float totalG = totalAccel / 9.80665f;

    float pitch = radToDeg(atan2(ax, sqrt(ay * ay + az * az)));
    float roll = radToDeg(atan2(ay, sqrt(ax * ax + az * az)));

    float pitchDiff = fabs(pitch - basePitch);
    float rollDiff = fabs(roll - baseRoll);
    float tiltDiff = max(pitchDiff, rollDiff);

    bool freeFallLike = totalG < freeFallThresholdG;
    bool impactLike = totalG > impactThresholdG;
    bool tippedOver = tiltDiff > tiltThresholdDeg;

    // 一定時間で free-fall 候補を失効
    if (fallCandidate && (now - fallCandidateTime > fallConfirmWindowMs))
    {
        fallCandidate = false;
    }

    // まず free-fall 候補を記録
    if (!fallCandidate && freeFallLike && (now - lastDetectTime > detectCooldownMs))
    {
        fallCandidate = true;
        fallCandidateTime = now;
        Serial.println("[Candidate] free-fall like movement detected");
    }

    bool dropDetected = false;

    // free-fallの後に強い衝撃 or 大きな傾きが来たら本検知
    if (fallCandidate && (impactLike || tippedOver) && (now - lastDetectTime > detectCooldownMs))
    {
        dropDetected = true;
        fallCandidate = false;
    }

    // かなり強烈な衝撃 + 大きな傾きは単独でも検知
    if (!dropDetected && impactLike && tippedOver && (now - lastDetectTime > detectCooldownMs))
    {
        dropDetected = true;
        fallCandidate = false;
    }

    if (dropDetected)
    {
        lastDetectTime = now;
        Serial.println("=== DROP EVENT DETECTED ===");
        Serial.print("G: ");
        Serial.println(totalG, 3);
        Serial.print("Pitch: ");
        Serial.println(pitch, 1);
        Serial.print("Roll: ");
        Serial.println(roll, 1);
        Serial.print("TiltDiff: ");
        Serial.println(tiltDiff, 1);

        playAlert10Sec();
    }

    Serial.print("G: ");
    Serial.print(totalG, 2);
    Serial.print(" | TiltDiff: ");
    Serial.print(tiltDiff, 1);
    Serial.print(" | freeFall: ");
    Serial.print(freeFallLike ? "Y" : "N");
    Serial.print(" | impact: ");
    Serial.print(impactLike ? "Y" : "N");
    Serial.print(" | candidate: ");
    Serial.println(fallCandidate ? "Y" : "N");

    delay(150);
}

PlatformIO設定例

[env:nano_r4]
platform = renesas-ra
board = nano_r4
framework = arduino
monitor_speed = 115200
lib_deps =
    adafruit/Adafruit MPU6050
    adafruit/Adafruit Unified Sensor

プログラムのポイント

  • MPU6050 から加速度を取得しています。

  • 加速度の合成値から総加速度(G)を求め、落下っぽい低加速度や大きな衝撃を見ています。

  • さらに、初期姿勢を基準にして傾き差を計算し、通常状態から大きく倒れていないかも確認しています。

  • 自由落下らしい状態を検出した後に衝撃または大きな傾きが発生した場合、あるいは強い衝撃と大きな傾きが同時に発生した場合に落下イベントとして扱っています。

  • ブザーを鳴らすことで、視覚ではなく音で異常に気づけるようにしています。

  • シリアルモニタにはG値と傾き差を出力しているため、どの程度の変化で反応したかを確認しやすくしています。

  • 一定時間のクールダウンを入れることで、1回の落下で何度も連続検知しにくいようにしています。

動作確認

プログラムを書き込んだら、センサを固定した台や治具を実際に動かして、ブザーとシリアルモニタの変化を確認します。

たとえば、

・台を軽く傾ける
・大きめに傾ける
・机に軽くぶつける
・商品台を持ち上げて置き直す
・わざと「倒れた状態」に近い姿勢を作る

といった操作をすると、通常時はシリアルモニタに G と TiltDiff が出力され、条件を超えた時には DROP EVENT DETECTED が表示されてブザーが鳴ります。

また、シリアルモニタを見ることで、

・どのくらいのG値だったか
・どのくらい傾いたか
・どの条件で反応したか

も確認できます。

この動作確認を通して、実際の設置環境でどの閾値が妥当かを調整しやすくなります。

激しく動かすとブザーが鳴ります。

シリアルの出力画面
段ボールの中に入れておくことで、落下したときにブザー音で知らせることができます
激しい動きを検知してブザーが鳴ります ※ジャンパーピンで接続しているだけなので壊れない程度に

発展編:通知やログ保存への拡張

ローカル警告だけでもPoCとしては十分ですが、実際に使うなら「その場で気づく」から「履歴を残す」「遠隔通知する」へ広げたくなります。

たとえば、次のような拡張が考えられます。

・発生時刻を保存する 
・落下回数を日別で集計する
・売り場ごとの検知頻度を比較する
・複数の棚や展示台に展開する
・別系統の通信モジュールやゲートウェイと連携して通知する

今後の展望・改善案

今回の試作は「落下らしきイベントを簡易に検知する」ことを主目的にしていますが、実運用を考えるとさらに改善の余地があります。

誤検知を減らす

清掃や陳列作業、人の接触などによる不要な検知を減らすため、次のような工夫が考えられます。

・定時間以上の傾き継続を検知条件とする
・衝撃と傾きの両方を満たした場合のみ検知する
・設置場所ごとに閾値を変更する
・フィルタリング処理によって一時的な振動を除去する

他センサとの組み合わせ

加速度センサだけでなく、

・距離センサ
・荷重センサ
・フォトセンサ
・振動センサ

などを組み合わせることで、「本当に商品が落下したか」をより高い精度で判定できるようになります。

例えば、

・棚に商品が存在するか
・落下後に商品がなくなっているか
・商品の重量変化があったか

といった情報を組み合わせることで、誤検知を抑えながら検知精度を向上させることができます。

ケース化・固定方法の改

実運用を考えると、センサの固定方法やケース形状で挙動がかなり変わります。PoCの次の段階では、センサの取り付け位置と固定の再現性も重要になります。

ログ保存と遠隔通知

今回のPoCではブザーによるローカル通知のみですが、実運用では発生履歴を残すことも重要です。

例えば、

・発生日時の記録
・落下回数の集計
・棚ごとの発生頻度分析
・保守・改善活動への活用

などが考えられます。

さらに、

・MQTT
・Web API
・クラウドサービス

などと連携することで、スマートフォンやPCへの遠隔通知も実現できます。

クラウド連携・可視化

落下イベントをクラウドへ蓄積することで、

・ダッシュボード表示
・発生傾向分析
・店舗間比較
・異常発生ランキング

などの可視化も可能になります。

単発の落下検知だけでなく、

「どこで落下が多いのか」「どの時間帯に発生しやすいのか」といった改善活動につながる情報として活用できます。

Renesas製品への展開

今回はArduino Nano R4に搭載されているRenesas RA4M1を用いたローカル検知PoCですが、将来的には無線通信機能を備えた製品との組み合わせも考えられます。

例えば、

・Renesas RA6シリーズ
・Wi-Fi/Bluetooth対応モジュール
・各種IoTゲートウェイ

などを活用することで、

・複数棚の一括監視
・クラウドへのイベント集約
・多拠点管理
・常時接続型の監視システム

へ発展させることが可能です。

また、AIを活用した異常傾向分析や予兆検知と組み合わせることで、単なる落下通知から現場改善支援ツールへ発展させることも期待できます。

まとめ

今回は、Arduino Nano R4とMPU6050加速度センサ、ブザーを使って、商品落下を簡易に検知してその場で警告する仕組みを作ってみました。

今回のテーマは、単にセンサ値を読むことではなく、

・落下にすぐ気づきたい
・目の届かない時間帯の異常を見逃しにくくしたい
・いつ起きたか分からない問題を減らしたい
・小さく試せるPoCから始めたい

という困りごとに対して、比較的少ない部品で落下イベント検知を試すことにあります。

構成自体はシンプルですが、

・加速度を見る
・傾きを見る
・ブザーで知らせる
・シリアルで変化を確認する

という流れを通して、落下を“その場で気づけるイベント”に変える題材になりました。

いきなり大規模な監視システムへ進む前に、まずは商品台や展示台の落下検知をArduinoで試してみるところから始めたい時に、ちょうどよいテーマだと思います。

参考リンク