はじめに
モータやファンなどの回転機械では、振動の周波数成分を調べることで、設備の状態を把握する手掛かりが得られます。
以前、M5Stack Core2と外付け加速度センサADXL345を使った簡易振動測定装置を試作しました。今回は、さらに簡単な構成を目指し、M5StickS3に内蔵されているBMI270だけで振動を測定し、FFT解析結果を本体画面に表示する装置を作りました。
外付けセンサや接続ケーブルは不要です。測定対象に設置した後は、ボタンを操作せず自動的に測定を繰り返します。
本記事の装置は、周波数成分を簡易的に確認するための試作品です。校正された振動計の代わりになるものではありません。
1. 使用するもの
- M5StickS3(BMI270内蔵)
- USBケーブル(プログラム書き込み用)
- Arduino IDEを使用できるパソコン
使用するArduinoライブラリは M5Unified (ver.0.2.20)、arduinoFFT(ver.2.0.4) です。Wire はArduino環境に含まれるI²C通信用ライブラリです。M5Stack by M5Stack(ver.3.3.9) を導入し、Arduino IDEで使用する ボード(M5StickS3) と ポート を選択します。
2. 装置の仕様
| 項目 | 内容 |
|---|---|
| マイコン | M5StickS3 |
| 加速度センサ | BMI270(3軸加速度・3軸角速度センサ、本体内蔵) |
| 測定方向 | Z軸(画面に垂直な方向) |
| 加速度測定範囲 | ±8 g(プログラム設定値) |
| サンプリング周波数 | 約809.6 Hz(実測による推定値) |
| FFTデータ数 | 1024点 |
| 周波数表示範囲 | 0~200 Hz |
| FFT周波数間隔 | 約0.791 Hz |
| 表示 | 加速度RMS、上位3ピーク周波数と相対振幅、FFTスペクトル |
| シリアル出力 | 上位5ピーク周波数など |
| 動作 | 自動繰り返し測定 |
BMI270は3軸加速度を取得できますが、今回のプログラムはZ軸だけを解析します。M5StickS3を画面が上になる向きで置くと、Z軸は画面に垂直な方向です。
3. データ取得とFFT解析の仕組み
BMI270のFIFOを利用する
加速度を高い周波数で読み取る際、マイコンから一定間隔でI²C通信を行う方法では、画面描画などの処理の影響を受ける場合があります。
そこで、BMI270内部にデータを一時保存する FIFO(First In, First Out) 機能を利用しました。センサに蓄積された加速度データをまとめて読み出し、1024点のデータとしてFFT解析します。
サンプリング周波数について
今回のプログラムでは、BMI270のサンプリング周波数を800 Hzに設定しています。設定は次の部分で行っています。
writeReg(0x40, 0xAB); // 加速度データ出力レート:800 Hz
サンプリング周波数は800 Hzに設定しましたが、一定時間内にセンサのFIFOに蓄積されたデータ数を測定し、その個数を経過時間で割ることで実際のサンプリング周波数を推定しました。その結果、約809.6 Hzであったため、FFT解析にはこの値を使用しました。
constexpr double FS = 809.6;
このFSはFFT解析で周波数を計算するための値であり、変更してもBMI270の実際のサンプリング周波数は変わりません。
今回のFFTのデータ数は1024点であるため、周波数間隔は次のようになります。
$$
\Delta f = \frac{809.6}{1024} \simeq 0.791\ \mathrm{Hz}
$$
なお、809.6 HzはFIFOに蓄積されたデータ数から求めた推定値であり、校正によって保証された値ではありません。使用する個体や測定条件が異なる場合は、実際のサンプリング周波数を確認することが望まれます。
本記事の補足で、809.6 Hzを求める具体的な方法を紹介します。
RMSとピーク周波数
取得したZ軸加速度から平均値を引き、振動成分のRMSを計算します。その後、ハニング窓を適用してFFT解析し、0~200 Hzのスペクトルを求めます。
ピーク周波数は、FFTの極大値と左右の点の対数振幅による放物線補間で推定します。このため、FFTの周波数間隔より細かい数値が表示されますが、その表示桁数が測定精度を表すわけではありません。
4. Arduinoプログラム
スケッチはこちらを参照ください
#include <M5Unified.h>
#include <Wire.h>
#include <arduinoFFT.h>
#include <math.h>
// M5StickS3 Vibration FFT Ver. 0.4d
// Z-axis vibration measurement, auto-measurement + top five FFT peaks
constexpr uint8_t BMI = 0x68;
constexpr int N = 1024;
constexpr double FS = 809.6;
constexpr double ACC_SCALE = 8.0 * 9.80665 / 32768.0;
constexpr double MAX_DISPLAY_HZ = 200.0;
constexpr int TOP_PEAKS = 5;
constexpr double MIN_PEAK_SEPARATION_HZ = 4.0;
static double vReal[N];
static double vImag[N];
ArduinoFFT<double> FFT(vReal, vImag, N, FS);
double acRms = 0.0;
double peakHz = 0.0;
double peakAmplitude = 0.0;
int fifoError = 0;
int collectedCount = 0;
struct PeakInfo {
double hz;
double magnitude;
int bin;
};
PeakInfo topPeaks[TOP_PEAKS];
int peakCount = 0;
bool writeReg(uint8_t reg, uint8_t value) {
Wire.beginTransmission(BMI);
Wire.write(reg);
Wire.write(value);
uint8_t err = Wire.endTransmission();
if (err != 0) {
Serial.printf("I2C WRITE ERROR: reg=0x%02X value=0x%02X err=%d\n",
reg, value, err);
}
return err == 0;
}
bool readReg(uint8_t reg, uint8_t &value) {
Wire.beginTransmission(BMI);
Wire.write(reg);
if (Wire.endTransmission(false) != 0) return false;
if (Wire.requestFrom(BMI, (uint8_t)1) != 1) return false;
value = Wire.read();
return true;
}
bool readFifoLength(uint16_t &length) {
uint8_t lo, hi;
if (!readReg(0x24, lo)) return false;
if (!readReg(0x25, hi)) return false;
length = ((uint16_t)(hi & 0x3F) << 8) | lo;
return true;
}
bool configureBMI270() {
if (!writeReg(0x40, 0xAB)) return false; // Nominal 800 Hz
if (!writeReg(0x41, 0x02)) return false; // +/- 8 g
if (!writeReg(0x48, 0x00)) return false; // Headerless FIFO
if (!writeReg(0x49, 0x40)) return false; // Accelerometer only
delay(30);
uint8_t check = 0;
if (!readReg(0x49, check)) return false;
return check == 0x40;
}
bool collectSamples() {
fifoError = 0;
collectedCount = 0;
uint8_t chipId = 0;
if (readReg(0x00, chipId)) {
Serial.printf("Before FIFO flush: CHIP_ID=0x%02X\n", chipId);
} else {
Serial.println("Before FIFO flush: CHIP_ID READ ERROR");
}
if (!writeReg(0x7E, 0xB0)) {
fifoError = 1;
return false;
}
delay(5);
uint32_t startMs = millis();
while (collectedCount < N) {
if (millis() - startMs > 4000) {
fifoError = 2;
Serial.printf("Timeout: count=%d\n", collectedCount);
return false;
}
uint16_t fifoBytes = 0;
if (!readFifoLength(fifoBytes)) {
fifoError = 3;
return false;
}
int framesAvailable = fifoBytes / 6;
if (framesAvailable <= 0) {
delay(1);
continue;
}
int framesToRead = framesAvailable;
if (framesToRead > 4) framesToRead = 4;
if (framesToRead > N - collectedCount) framesToRead = N - collectedCount;
int bytesToRead = framesToRead * 6;
Wire.beginTransmission(BMI);
Wire.write(0x26);
if (Wire.endTransmission(false) != 0) {
fifoError = 4;
return false;
}
int received = Wire.requestFrom(BMI, (uint8_t)bytesToRead);
if (received != bytesToRead) {
fifoError = 5;
Serial.printf("I2C read error: requested=%d received=%d\n",
bytesToRead, received);
return false;
}
for (int i = 0; i < framesToRead; i++) {
Wire.read(); Wire.read(); // X-axis
Wire.read(); Wire.read(); // Y-axis
uint8_t zl = Wire.read();
uint8_t zh = Wire.read();
int16_t rawZ = (int16_t)((uint16_t)zl | ((uint16_t)zh << 8));
vReal[collectedCount++] = rawZ * ACC_SCALE;
}
}
Serial.printf("FIFO OK: %d samples\n", collectedCount);
return true;
}
// Frequency interpolation using the original log-parabolic method.
double interpolatedHz(int bin, int maxBin) {
double delta = 0.0;
if (bin > 1 && bin < maxBin) {
double a = log(vReal[bin - 1] + 1e-12);
double b = log(vReal[bin] + 1e-12);
double c = log(vReal[bin + 1] + 1e-12);
double denom = a - 2.0 * b + c;
if (fabs(denom) > 1e-12) delta = 0.5 * (a - c) / denom;
if (delta > 0.5) delta = 0.5;
if (delta < -0.5) delta = -0.5;
}
return (bin + delta) * FS / N;
}
// Find distinct local maxima, sorted by FFT magnitude (largest first).
void extractTopPeaks(int maxBin) {
peakCount = 0;
for (int k = 2; k < maxBin; k++) {
if (!(vReal[k] > vReal[k - 1] && vReal[k] >= vReal[k + 1])) continue;
double hz = interpolatedHz(k, maxBin);
double magnitude = vReal[k];
// Merge nearby maxima, keeping the stronger one.
int nearby = -1;
for (int i = 0; i < peakCount; i++) {
if (fabs(hz - topPeaks[i].hz) < MIN_PEAK_SEPARATION_HZ) {
nearby = i;
break;
}
}
if (nearby >= 0) {
if (magnitude <= topPeaks[nearby].magnitude) continue;
for (int j = nearby; j < peakCount - 1; j++) topPeaks[j] = topPeaks[j + 1];
peakCount--;
}
int insertAt = 0;
while (insertAt < peakCount && topPeaks[insertAt].magnitude >= magnitude) {
insertAt++;
}
if (insertAt >= TOP_PEAKS) continue;
if (peakCount < TOP_PEAKS) peakCount++;
for (int j = peakCount - 1; j > insertAt; j--) topPeaks[j] = topPeaks[j - 1];
topPeaks[insertAt] = {hz, magnitude, k};
}
}
void analyzeFFT() {
double mean = 0.0;
for (int i = 0; i < N; i++) mean += vReal[i];
mean /= N;
double sumSq = 0.0;
for (int i = 0; i < N; i++) {
vReal[i] -= mean;
sumSq += vReal[i] * vReal[i];
vImag[i] = 0.0;
}
acRms = sqrt(sumSq / N);
FFT.windowing(FFTWindow::Hann, FFTDirection::Forward);
FFT.compute(FFTDirection::Forward);
FFT.complexToMagnitude();
int maxBin = (int)(MAX_DISPLAY_HZ * N / FS);
int peakBin = 1;
double maxValue = 0.0;
for (int k = 1; k <= maxBin; k++) {
if (vReal[k] > maxValue) {
maxValue = vReal[k];
peakBin = k;
}
}
peakHz = interpolatedHz(peakBin, maxBin);
peakAmplitude = 2.0 * maxValue / (N * 0.5);
extractTopPeaks(maxBin);
}
void printTopPeaks() {
Serial.println("FFT Peaks (strongest first):");
if (peakCount == 0) {
Serial.println(" No local maxima found");
return;
}
double strongest = topPeaks[0].magnitude;
for (int i = 0; i < peakCount; i++) {
double relative = strongest > 0.0 ? 100.0 * topPeaks[i].magnitude / strongest : 0.0;
Serial.printf(" %d: %.3f Hz (relative magnitude %.1f%%)\n",
i + 1, topPeaks[i].hz, relative);
}
Serial.println("Note: weak peaks may be noise; relative magnitude is not calibrated acceleration.");
}
// M5StickS3 Vibration FFT Ver. 0.4d
// Improved spectrum display using per-pixel maximum
void drawSpectrum() {
auto &d = M5.Display;
d.fillScreen(BLACK);
d.setTextColor(WHITE, BLACK);
d.setTextSize(1);
const int w = d.width();
const int h = d.height();
d.setCursor(3, 3);
d.print("Vibration FFT v0.4d");
d.setCursor(3, 17);
d.printf("RMS: %.3f m/s2", acRms);
// Display the three strongest FFT peaks.
const uint16_t peakColors[3] = {
GREEN, YELLOW, MAGENTA
};
for (int i = 0; i < 3; i++) {
const int y = 33 + i * 15;
d.setCursor(3, y);
d.setTextColor(peakColors[i], BLACK);
if (i < peakCount) {
const double rel =
topPeaks[0].magnitude > 0.0
? 100.0 * topPeaks[i].magnitude
/ topPeaks[0].magnitude
: 0.0;
d.printf("%d: %5.1fHz %3.0f%%",
i + 1, topPeaks[i].hz, rel);
} else {
d.printf("%d: --", i + 1);
}
}
d.setTextColor(WHITE, BLACK);
// Spectrum display area.
const int x0 = 12;
const int y0 = 84;
const int plotW = w - 22;
const int plotH = h - y0 - 18;
if (plotW < 10 || plotH < 10) return;
const int bottom = y0 + plotH;
d.drawLine(x0, bottom,
x0 + plotW, bottom, WHITE);
d.drawLine(x0, y0,
x0, bottom, WHITE);
// Maximum FFT magnitude in the display range.
const int maxBin =
(int)(MAX_DISPLAY_HZ * N / FS);
double maxMag = 0.0;
for (int k = 1; k <= maxBin; k++) {
if (vReal[k] > maxMag) {
maxMag = vReal[k];
}
}
if (maxMag <= 0.0) maxMag = 1.0;
// Improved spectrum drawing:
// Map every FFT bin to its nearest display pixel,
// and retain the maximum magnitude in that pixel.
//
// This prevents narrow peaks from being skipped.
for (int x = 0; x < plotW; x++) {
double pixelMax = 0.0;
// FFT bins assigned to this display pixel.
for (int k = 1; k <= maxBin; k++) {
const double freq = (double)k * FS / N;
const int px = (int)round(
freq / MAX_DISPLAY_HZ * (plotW - 1));
if (px == x && vReal[k] > pixelMax) {
pixelMax = vReal[k];
}
}
const double mag = pixelMax / maxMag;
int barH = (int)round(mag * plotH);
if (barH > plotH) barH = plotH;
if (barH < 0) barH = 0;
if (barH > 0) {
d.drawLine(x0 + x, bottom,
x0 + x, bottom - barH, CYAN);
}
}
// Frequency axis labels.
d.setCursor(x0, bottom + 3);
d.print("0");
d.setCursor(x0 + plotW / 2 - 10, bottom + 3);
d.print("100");
d.setCursor(x0 + plotW - 32, bottom + 3);
d.print("200Hz");
}
void showReady() {
M5.Display.fillScreen(BLACK);
M5.Display.setTextColor(WHITE);
M5.Display.setTextSize(1);
M5.Display.setCursor(8, 12);
M5.Display.println("Vibration FFT v0.4c");
M5.Display.setCursor(8, 34);
M5.Display.println("Auto measure / 5 sec");
M5.Display.setCursor(8, 55);
M5.Display.println("Range: 0-200 Hz");
}
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
Serial.begin(115200);
delay(1000);
Wire.begin(M5.In_I2C.getSDA(), M5.In_I2C.getSCL());
Wire.setClock(400000);
uint8_t chipId = 0;
if (!readReg(0x00, chipId) || chipId != 0x24) {
M5.Display.fillScreen(BLACK);
M5.Display.println("BMI270 ERROR");
while (true) delay(100);
}
if (!configureBMI270()) {
M5.Display.fillScreen(BLACK);
M5.Display.println("FIFO CONFIG ERROR");
while (true) delay(100);
}
Serial.println();
Serial.println("M5StickS3 Vibration FFT v0.4c");
Serial.println("Automatic measurement every 5 seconds");
showReady();
}
void loop() {
Serial.println();
Serial.println("Sampling start");
M5.Display.fillScreen(BLACK);
M5.Display.setTextColor(WHITE);
M5.Display.setCursor(8, 20);
M5.Display.println("Measuring...");
if (collectSamples()) {
analyzeFFT();
drawSpectrum();
Serial.printf("RMS : %.5f m/s2\n", acRms);
Serial.printf("Peak : %.3f Hz\n", peakHz);
printTopPeaks();
Serial.println("FFT complete");
} else {
M5.Display.fillScreen(BLACK);
M5.Display.setTextColor(RED);
M5.Display.setCursor(8, 20);
M5.Display.printf("FIFO ERROR: %d", fifoError);
Serial.printf("FIFO ERROR: %d\n", fifoError);
}
delay(5000);
}
プログラムのポイント
-
configureBMI270():加速度ODR、測定レンジ、FIFOを設定します。 -
collectSamples():FIFOから1024点のZ軸加速度を取得します。 -
analyzeFFT():平均値の除去、RMS計算、ハニング窓、FFTを実行します。 -
interpolatedHz():ピークと左右の3点から周波数を補間します。 -
extractTopPeaks():近接ピークを整理し、振幅の大きい順に抽出します。 -
drawSpectrum():画面にRMS、上位3ピーク、0~200 Hzのスペクトルを表示します。
電源投入後は自動的に測定を繰り返します。シリアルモニタを115200 bpsで開くと、ピーク周波数なども確認できます。
5. 動作確認:3つの周波数を同時に検出する
試作装置の動作を確認するため、スマートフォンから110 Hz、150 Hz、195 Hzの音を同時に出力しました。スマートフォンの背面を上にし、その上にM5StickS3を置いてZ軸方向の振動を測定しました。
5回測定した結果を示します。
| 測定回数 | 110 Hz成分 | 150 Hz成分 | 195 Hz成分 |
|---|---|---|---|
| 1 | 110.03 | 150.01 | 195.03 |
| 2 | 110.02 | 150.01 | 195.03 |
| 3 | 110.04 | 150.02 | 195.03 |
| 4 | 110.03 | 150.01 | 195.03 |
| 5 | 110.02 | 150.01 | 195.02 |
| 平均 | 110.03 | 150.02 | 195.03 |
単位:Hz。表中の数値はFFTのピーク補間による推定値であり、小数点以下の桁数は測定精度を示しません。
5回とも設定した3つの周波数に対応するピークを検出できました。液晶画面には、上位3ピークの周波数と相対振幅が表示されます。
今回の確認はスマートフォンの音を利用した簡易試験です。校正された振動発生装置による精度検証ではありません。
6. 実際に使う際の注意点
M5StickS3の背面にはマグネットがあり、磁石が吸着する対象への固定に利用できます。ただし、振動が本体へ十分に伝わるかどうかは取付状態に左右されます。柔らかい材料を挟んだ場合や固定が不十分な場合には、測定値が変化する可能性があります。
また、今回のプログラムはZ軸のみを解析するため、測定対象に対する本体の向きにも注意が必要です。加速度RMSの絶対値を精度よく測定できることは確認していないため、まずは振動の周波数成分を簡易的に調べる用途を想定しています。
7. おわりに
M5StickS3の内蔵加速度センサBMI270を利用し、外付けセンサなしでFFT解析を行う簡易振動計を試作しました。110 Hz、150 Hz、195 Hzの3つの周波数成分を同時に検出でき、装置本体の小さな画面でも周波数スペクトルを確認できました。
以前のM5Stack Core2+ADXL345による装置は、外付けセンサを使用する代わりにmicroSDへのデータ保存が可能でした。今回の装置はデータ保存機能を省き、構成の簡単さと小型化を優先しています。
なお、M5Stack Core2にも加速度センサが内蔵されているため、プログラムを変更すれば、外付けセンサを使用せずに同様の装置を構成できます。
振動計測の入門や、マイコンによるセンサデータ取得・FFT解析の学習にも使える題材だと思います。
補足 実際のサンプリング周波数の確認方法
BMI270の加速度データ出力レートは800 Hzに設定していますが、FFT解析で周波数を求める際には、実際のサンプリング周波数を把握しておくことが重要です。
そこで、BMI270のFIFO機能を利用し、一定時間内に蓄積された加速度データ数からサンプリング周波数を推定しました。
具体的には、FIFOをクリアした後、約300 msの間に蓄積されたデータのフレーム数を読み取り、経過時間で割って周波数を求めます。この測定を20回繰り返し、平均値を計算します。
今回の試作では、この方法によってサンプリング周波数を約809.6 Hzと推定しました。
以下に、確認用プログラムを示します。
スケッチはこちらを参照ください
#include <M5Unified.h>
#include <Wire.h>
// BMI270 FIFO-based output data rate estimate
// M5StickS3 / accelerometer ODR setting: nominal 800 Hz
constexpr uint8_t BMI_ADDR = 0x68;
constexpr int TRIALS = 20;
constexpr uint32_t WINDOW_US = 300000; // 300 ms
constexpr int BYTES_PER_FRAME = 6; // X, Y, Z: int16_t each
bool writeReg(uint8_t reg, uint8_t value) {
Wire.beginTransmission(BMI_ADDR);
Wire.write(reg);
Wire.write(value);
return Wire.endTransmission() == 0;
}
bool readReg(uint8_t reg, uint8_t &value) {
Wire.beginTransmission(BMI_ADDR);
Wire.write(reg);
if (Wire.endTransmission(false) != 0) return false;
if (Wire.requestFrom(BMI_ADDR, (uint8_t)1) != 1) return false;
value = Wire.read();
return true;
}
bool readFifoLength(uint16_t &length) {
uint8_t lo, hi;
if (!readReg(0x24, lo) || !readReg(0x25, hi)) return false;
length = ((uint16_t)(hi & 0x3F) << 8) | lo;
return true;
}
void setup() {
M5.begin(M5.config());
Serial.begin(115200);
delay(1000);
Wire.begin(M5.In_I2C.getSDA(), M5.In_I2C.getSCL());
Wire.setClock(400000);
uint8_t chipId = 0;
if (!readReg(0x00, chipId) || chipId != 0x24) {
Serial.println("BMI270 not found (expected CHIP_ID=0x24)");
while (true) delay(1000);
}
if (!writeReg(0x40, 0xAB) || // Accel ODR nominal 800 Hz
!writeReg(0x41, 0x02) || // Accel range +/-8 g
!writeReg(0x48, 0x00) || // FIFO headerless
!writeReg(0x49, 0x40)) { // FIFO accelerometer only
Serial.println("BMI270 configuration failed");
while (true) delay(1000);
}
delay(100);
Serial.println("trial,elapsed_us,fifo_bytes,frames,estimated_hz");
}
void loop() {
double sumHz = 0;
int valid = 0;
for (int i = 1; i <= TRIALS; ++i) {
// Clear the FIFO before each timed accumulation interval.
if (!writeReg(0x7E, 0xB0)) {
Serial.println("FIFO flush failed");
break;
}
uint32_t start = micros();
while ((uint32_t)(micros() - start) < WINDOW_US) {
delayMicroseconds(100);
}
uint16_t bytes = 0;
if (!readFifoLength(bytes)) {
Serial.println("FIFO length read failed");
break;
}
uint32_t elapsed = micros() - start;
uint16_t frames = bytes / BYTES_PER_FRAME;
// 300 ms at 800 Hz is ~240 frames (1440 bytes), below FIFO capacity.
double hz = frames * 1000000.0 / elapsed;
Serial.printf("%d,%lu,%u,%u,%.3f\n", i,
(unsigned long)elapsed, bytes, frames, hz);
sumHz += hz;
++valid;
delay(100);
}
if (valid > 0) {
Serial.printf("Mean of %d trials: %.3f Hz\n", valid, sumHz / valid);
}
Serial.println("Restart device to measure again.");
while (true) delay(1000);
}
なお、この方法では、FIFOのクリアやデータ数の読み取りに伴う時間の誤差、フレーム数の整数単位での変動などが生じます。そのため、得られる値はサンプリング周波数の推定値であり、精密な校正値ではありません。
参考資料
- M5StickS3製品資料 https://docs.m5stack.com/ja/core/StickS3
- M5Stackで振動をFFT解析してみる― 周波数スペクトル、回転周波数との比較 ― https://qiita.com/_Moony/items/5b0ea5059bbcc3fe53a5


