メインページに戻る

このページのゴール

「NeoPixelとM5StickC Plus2の機能を組み合わせたデモを完成させること」

今日のねらい

  • GPIOの基本を理解し、WS2812B(NeoPixel)を Adafruit_NeoPixel で制御できる。
    • GPIO(General Purpose Input/Output)は、マイコンのピンをプログラムから汎用のデジタル信号(HIGH/LOW)として扱える仕組み。
  • IMU(加速度)の値を使い、振る・向きに応じてLEDの色/明るさを変えられる。
    • IMUは加速度・ジャイロの複合センサ。今回は加速度(ax, ay, az)から傾きや揺れを数値化してLED制御に結びつける。
  • ブロッキング/ノンブロッキングの違いを理解し、millis() 駆動の非ブロッキング制御を書ける。
    • delay() はその間ループ処理が止まる(ブロッキング)ため、ボタンやIMU入力を取りこぼしやすい。
    • millis() の経過時間で更新タイミングを判定しつつループを止めない設計により、アニメーションと入力処理を両立する。
  • 2進数・16進数・10進数の関係を理解し、24bit色の表現を読めて書ける。
    • 1色あたり8bit=0〜255(10進)=0x00〜0xFF(16進)=0b00000000〜11111111(2進)。
    • 24bit色は R/G/B 各8bit(例:赤=(255,0,0)=0xFF0000)。コード中の 0xRRGGBB やビット演算の意味をつかむ。
  • HSV色空間の基礎を理解し、直感的に「色相・彩度・明るさ」を操作できる。
    • HSVは H(色相), S(彩度), V(明るさ)で色を表す。strip.ColorHSV(h, s, v) でHSV→RGBに変換でき、Hを回せば虹色表現、S/Vで鮮やかさや明滅が直感的に作れる。仕上げに strip.gamma32() を通すと見栄えが良い。
  • ガンマ補正の目的を理解し、自然な見え方のグラデーションを作れる。
    • LEDの出力は線形だが、人間の明るさ知覚は非線形。strip.gamma32() などでガンマ補正をかけると暗部が沈みにくく階調が滑らかになる。
    • 全体の明るさは strip.setBrightness()、色の滑らかさはガンマ処理、と役割を区別して使う。

準備物

  • M5StickC Plus2
  • USB-C ケーブル
  • WS2812B(NeoPixel)ストリップ
  • ジャンパー線
    • M5StickC Plus2では、ブレッドボードのようにジャンパー線を差し込んで回路を構築できますが、差し込む場所を間違えた際のリスクを考慮して、コネクタ版を用意しました。
    • ジャンパー線でも実施可能であることは覚えておいてください。


1. 事前準備

  • Arduino IDEはM5StickCPlus2 ボード(※前回指定のバージョンのまま)
  • ライブラリ:「Adafruit NeoPixel」をライブラリマネージャからインストールしておく
    • Adafruit NeoPixelは、NeoPixel(WS2812B)を簡単に制御するための、業界標準のライブラリです。複雑な信号タイミングの制御をこのライブラリが担ってくれるため、ユーザーはstrip.setPixelColor()のような簡単な関数だけでLEDの色を変えられます。

    • (参考)1年生の文化情報工学基礎演習では WS2822S 搭載LEDを使用しました。信号の中継仕様や故障時の挙動が WS2812B と異なる場合があります(今回の授業は WS2812B を前提)。詳しくはこちらで説明されています。要は途中のLEDが壊れた場合などでLEDの挙動が変わります。

2. 配線

コネクタを繋げるだけでOKです。

どのように繋がっているか?

WS2812B 端子5V / DIN / GND

M5StickC Plus2(Grove/PORTA)※ どちらの側面も使えます

  • 赤:5V → LED の 5V
  • 黒:GND → LED の GND
  • 黄:GPIO32→ LED の DIN
  • (白:GPIO33は未使用)

注意:LEDの矢印(信号方向)が M5Stick → DIN になる向きで配線すること。


3. まずは光らせてみる

はじめは低輝度(数値 40 前後)でテストしてみましょう。

コード(03_fixed_colors)
#include <M5StickCPlus2.h>  // M5StickC Plus2 専用のライブラリを読み込む
#include <M5Unified.h>  // M5Stackシリーズを統合的に扱えるライブラリ
#include <Adafruit_NeoPixel.h>  // NeoPixel を扱うためのライブラリを読み込む

// --- 定数の定義 ---
#define PIXEL_PIN   32                // LEDストリップを接続したGPIO番号(M5StickC Plus2のGrove黄 = GPIO32)
#define PIXEL_NUM   8                 // LEDの数(ここでは8個分)
#define PIXEL_TYPE  NEO_GRB + NEO_KHZ800 // WS2812Bに合わせた設定(色順: GRB, 通信速度: 800kHz)

// NeoPixel制御用のオブジェクトを生成(stripという名前で扱えるようにする)
Adafruit_NeoPixel strip(PIXEL_NUM, PIXEL_PIN, PIXEL_TYPE);

void setup() {
  strip.begin();          // NeoPixelライブラリを初期化(これを呼ばないと動作しない)

  // --- 各LEDの色を個別に設定する ---
  strip.setPixelColor(0, strip.Color(40, 0, 0));    // LED0: 赤(明るさ40/255)
  strip.setPixelColor(1, strip.Color(0, 40, 0));    // LED1: 緑
  strip.setPixelColor(2, strip.Color(0, 0, 40));    // LED2: 青
  strip.setPixelColor(3, strip.Color(40, 40, 0));   // LED3: 黄(赤+緑)
  strip.setPixelColor(4, strip.Color(0, 40, 40));   // LED4: シアン(緑+青)
  strip.setPixelColor(5, strip.Color(40, 0, 40));   // LED5: マゼンタ(赤+青)
  strip.setPixelColor(6, strip.Color(40, 40, 40));  // LED6: 白(赤+緑+青)
  strip.setPixelColor(7, strip.Color(10, 10, 10));  // LED7: 暗い灰色(弱めの白)

  strip.show();  // 上で設定した色をまとめてLEDに反映する(これを呼ぶまで光らない)
}

void loop() {
  // このサンプルでは何も処理を書かないので、
  // setup()で点灯させた色がずっと固定で光り続ける
}

コード解説

NeoPixelライブラリ

  • #include <Adafruit_NeoPixel.h> は Adafruit 提供の公式ライブラリ
  • 複数LEDを1本のデータ線GPIO32LED StripのDin)で制御するためのタイミング制御を肩代わり。

定数の意味

  • PIXEL_PIN 32 … データ線は GPIO32
  • PIXEL_NUM 8 … LED の個数に合わせて変更。
  • PIXEL_TYPE色順(GRB/RGB)と通信速度(800kHz)。色順が合わないと色がズレる。(色順が合わない場合は NEO_GRBNEO_RGB を切り替えて確認。)

setup() の流れ

  • strip.begin() → 初期化
  • setPixelColor(i, Color(R,G,B))RGB 数値を直接指定(明るさもここで決まる)
  • strip.show() → まとめて反映(呼ぶまでLEDの光は変化しない)

loop() が空の理由

  • 固定点灯の確認用。動きを付ける時は loop() に処理を追加します。

色指定の考え方

  • strip.Color(R, G, B) の引数は 0〜255 の整数値
    • (255, 0, 0) → 真っ赤
    • (0, 255, 0) → 緑
    • (0, 0, 255) → 青
    • (255, 255, 255) → 白
  • 今回は (40, 0, 0) のように 控えめな値で色を指定
  • 複数色を足すと混色になります(赤+緑=黄、緑+青=シアン、赤+青=マゼンタ)

4. 光が流れるような演出を作る(関数で制御する:colorWipe)

次はもう少し複雑な光らせ方にチャレンジしてみましょう。
8個のLEDを「赤 → 緑 → 青」の順に流れるように点灯させるサンプルです。
関数化して再利用できる形にしています。

コード(04_colorwipe)
#include <M5StickCPlus2.h>  // M5StickC Plus2 専用のライブラリを読み込む
#include <M5Unified.h>  // M5Stackシリーズを統合的に扱えるライブラリ
#include <Adafruit_NeoPixel.h>  // Adafruit NeoPixel ライブラリを読み込む

// --- 定数定義 ---
#define PIXEL_PIN   32
#define PIXEL_NUM   8
#define PIXEL_TYPE  NEO_GRB + NEO_KHZ800

Adafruit_NeoPixel strip(PIXEL_NUM, PIXEL_PIN, PIXEL_TYPE);

void setup() {
  strip.begin();
  strip.show();   // 初期は全消灯
}

void loop() {
  // 順に赤 → 緑 → 青 で流す(RGBの数値は控えめ=40)
  colorWipe(strip.Color(40, 0, 0), 40);   // 赤
  colorWipe(strip.Color(0, 40, 0), 40);   // 緑
  colorWipe(strip.Color(0, 0, 40), 40);   // 青
}

// 指定色を「先頭から順に」塗っていく関数
void colorWipe(uint32_t c, uint8_t wait) {
  for (int i = 0; i < strip.numPixels(); i++) {
    strip.setPixelColor(i, c);  // i番目だけ更新
    strip.show();               // 反映
    delay(wait);                // 待つ=流れるように見える
  }
}

コード解説

setup()

  • strip.begin() → 初期化
  • strip.show() → 初期状態反映(全消灯)

loop()

  • colorWipe(color, wait) を色違いで3回呼び出し。
  • wait を短くすれば速く、長くすればゆったり流れる。

colorWipe()

void colorWipe(uint32_t c, uint8_t wait) {
  for (int i = 0; i < strip.numPixels(); i++) {
    strip.setPixelColor(i, c);  // i番目の色を設定
    strip.show();               // その場で反映
    delay(wait);                // 少し待って「流れ」を演出
  }
}
  • 先頭から順番にLEDを指定色cに塗り替えます。
  • 1個点ける→表示→少し待つ…を繰り返すので、色が流れていくように見えます。
  • strip.show()は「これまでの設定をまとめて物理LEDに送る」命令です。

引数の意味

  • cstrip.Color(R, G, B) で作る24bit色(0〜255、各色8bitずつ)。今回は安全のため 40 程度の控えめな値を使用しています。
  • wait… 各LEDを点けたあとに待つミリ秒(動きの速さ)。
    • 例:10ms → 速い、100ms → ゆっくり。
    • 長い値を使うなら、型を uint16_t にしておくと安心です(255msを超えるとき)。

・ブロッキング挙動

  • delay(wait) 中は他の処理が止まる(ブロッキング)。
  • そのため、このままだとボタン入力やIMU読み取りを同時に扱いにくいという欠点はあります。

・よくあるアレンジ

一気に塗る(アニメなし)

for (int i = 0; i < strip.numPixels(); i++) strip.setPixelColor(i, c);
strip.show();  // ← showをループ外に出すと「瞬間で全塗り」

グラデーションで流す(LEDごとに色を少し変える)

for (int i = 0; i < strip.numPixels(); i++) {
  uint32_t ci = strip.Color(40, 0, i*5); // 青を少しずつ増やす等
  strip.setPixelColor(i, ci);
  strip.show();
  delay(wait);
}

HSVでレインボー(色相を回す)

for (int i = 0; i < strip.numPixels(); i++) {
  uint16_t hue = (i * 65535UL) / strip.numPixels(); // 0..65535
  uint32_t c = strip.gamma32(strip.ColorHSV(hue, 255, 40)); // V=40
  strip.setPixelColor(i, c);
  strip.show();
  delay(wait);
}

※ 余裕のある方は是非上のアレンジを試してみましょう


5. ボタンで光り方を変えたい(ブロッキングの壁)

5.1 ブロッキング版

下のコードは、BtnA を押すと色を変えるつもりなのに、反応が悪い/効かないことを体験するための例です。わざと反応が悪くなるコードにしています。
原因はなんでしょうか?

コード(05_blocking)
#include <M5StickCPlus2.h>  // M5StickC Plus2 専用のライブラリを読み込む
#include <M5Unified.h>  // M5Stackシリーズを統合的に扱えるライブラリ
#include <Adafruit_NeoPixel.h>

#define PIXEL_PIN  32
#define PIXEL_NUM  8
#define PIXEL_TYPE NEO_GRB + NEO_KHZ800

Adafruit_NeoPixel strip(PIXEL_NUM, PIXEL_PIN, PIXEL_TYPE);

uint8_t mode = 0; // 0=赤, 1=緑, 2=青

void setup() {
  auto cfg = M5.config();     // M5の設定オブジェクト
  M5.begin(cfg);              // M5初期化(ボタンや画面を使えるようにする)

  strip.begin();              // NeoPixel 初期化
  strip.show();               // 全消灯で開始

  // 画面にメッセージを表示
  M5.Display.setRotation(1);
  M5.Display.setFont(&lgfxJapanGothic_24);
  M5.Display.println("Blocking demo: BtnAで即色変更…のはず");
}

void loop() {
  M5.update(); 
  if (M5.BtnA.wasPressed()) {
    mode = (mode + 1) % 3;
  }

  // --- モードに応じて色を決定(if-elseで分かりやすく) ---
  uint32_t c;
  if (mode == 0) {
    c = strip.Color(40, 0, 0);   // 赤
  } else if (mode == 1) {
    c = strip.Color(0, 40, 0);   // 緑
  } else {
    c = strip.Color(0, 0, 40);   // 青
  }

  colorWipe(c, 200);
}

// 指定色を「順番に」LEDへ塗っていく(ブロッキング版)
void colorWipe(uint32_t c, uint16_t wait_ms) {
  for (int i = 0; i < strip.numPixels(); ++i) {
    strip.setPixelColor(i, c);
    strip.show();
    delay(wait_ms);
  }
}

なぜ反応しないのか(少し考えてから開いてみましょう

コード内の★マークがポイントです。


#include <M5StickCPlus2.h>  // M5StickC Plus2 専用のライブラリを読み込む
#include <M5Unified.h>  // M5Stackシリーズを統合的に扱えるライブラリ
#include <Adafruit_NeoPixel.h>

#define PIXEL_PIN  32
#define PIXEL_NUM  8
#define PIXEL_TYPE NEO_GRB + NEO_KHZ800

Adafruit_NeoPixel strip(PIXEL_NUM, PIXEL_PIN, PIXEL_TYPE);

uint8_t mode = 0; // 0=赤, 1=緑, 2=青

void setup() {
  auto cfg = M5.config();     // M5の設定オブジェクト
  M5.begin(cfg);              // M5初期化(ボタンや画面を使えるようにする)

  strip.begin();              // NeoPixel 初期化
  strip.show();               // 全消灯で開始

  // 画面にメッセージを表示
  M5.Display.setRotation(1);
  M5.Display.setFont(&lgfxJapanGothic_24);
  M5.Display.println("Blocking demo: BtnAで色変更…のはず");
}

void loop() {
  M5.update(); // ★毎ループで入力状態を更新(これがないとボタンイベント取れない)

  // ★BtnAを押したらモード切替する…つもりだが反応しづらい
  if (M5.BtnA.wasPressed()) {
    mode = (mode + 1) % 3;
  }

  // --- モードに応じて色を決定(if-elseで分かりやすく) ---
  uint32_t c;
  if (mode == 0) {
    c = strip.Color(40, 0, 0);   // 赤
  } else if (mode == 1) {
    c = strip.Color(0, 40, 0);   // 緑
  } else {
    c = strip.Color(0, 0, 40);   // 青
  }

  // ★ここが問題:delayで長時間止まるので、その間ボタンを読めない
  colorWipe(c, 200);
}

// 指定色を「順番に」LEDへ塗っていく(ブロッキング版)
void colorWipe(uint32_t c, uint16_t wait_ms) {
  for (int i = 0; i < strip.numPixels(); ++i) {
    strip.setPixelColor(i, c);
    strip.show();
    delay(wait_ms); // ★この間CPUが占有され、M5.update()も呼べない
  }
}
  • colorWipe() の中にある delay(wait_ms) が数百ms×LED数だけ処理を止める
    → その間に BtnA を押しても M5.update() が呼ばれず無視される
  • 結果、「ほぼ反応しない」状態になる

ポイント

  • delay() を多用すると「ブロッキング」になり、並行処理(ボタンの読み取りなど)ができない
  • これを解決するには、ノンブロッキング(millisベースの状態管理) が必要
  • イメージ例
    • ブロッキング: 「友達と話している最中(delay())に電話がかかってきても、話が終わるまで電話に出られない状態」
    • ノンブロッキング: 「友達と話しながら、LINEの通知(M5.update())も確認できる状態」

5.2 ノンブロッキング版(BtnAで色切替)

delay() をやめて、「次に進める時刻」を millis() で管理します。
LEDは1ステップずつ
進めるので、loop() は止まらずに回り続け、ボタンの入力も取り逃しません

  • BtnA:色モード切り替え(赤→緑→青)
  • 色の切替え時に流れを先頭からリセットしてわかりやすく
コード(05b_nonblocking)
#include <M5StickCPlus2.h>  // M5StickC Plus2 専用のライブラリを読み込む
#include <M5Unified.h>  // M5Stackシリーズを統合的に扱えるライブラリ
#include <Adafruit_NeoPixel.h>  // WS2812B(NeoPixel)制御ライブラリ

// --- NeoPixel の設定 ---
#define PIXEL_PIN  32                 // LEDストリップを接続するピン
#define PIXEL_NUM  8                  // LEDの数
#define PIXEL_TYPE NEO_GRB + NEO_KHZ800 // LEDのデータフォーマット(機種ごとに決まっている)

// NeoPixelオブジェクト生成
Adafruit_NeoPixel strip(PIXEL_NUM, PIXEL_PIN, PIXEL_TYPE);

// --- グローバル変数 ---
uint8_t mode = 0;         // 表示モード: 0=赤, 1=緑, 2=青
int idx = 0;              // 現在点灯させているLEDのインデックス
uint32_t lastUpdate = 0;  // 前回更新した時刻(millis基準)
uint16_t intervalMs = 200; // LEDを進める間隔(ミリ秒)

void setup() {
  // --- M5本体の初期化 ---
  auto cfg = M5.config();
  M5.begin(cfg);

  // --- NeoPixelの初期化 ---
  strip.begin();
  strip.show();  // 全部消灯状態からスタート

  // --- ディスプレイ設定 ---
  M5.Display.setRotation(1);                // 横向きに表示
  M5.Display.setFont(&lgfxJapanGothic_24);  // 日本語フォント設定
  M5.Display.println("Non-blocking demo: BtnAで色変更OK");
}

void loop() {
  M5.update();  // ボタン入力などM5の状態を更新

  // --- ボタン操作 ---
  if (M5.BtnA.wasPressed()) {   // BtnAが押されたら
    mode = (mode + 1) % 3;      // 0→1→2→0 と循環
    idx = 0;                    // LED点灯位置をリセット
    strip.clear();              // すべて消灯
    strip.show();
  }

  // --- 現在のモードに応じて色を選択 ---
  uint32_t c;
  if (mode == 0) {
    c = strip.Color(40, 0, 0);   // 赤
  } else if (mode == 1) {
    c = strip.Color(0, 40, 0);   // 緑
  } else {
    c = strip.Color(0, 0, 40);   // 青
  }

  // --- ノンブロッキング処理(delayを使わずに時間管理) ---
  if (millis() - lastUpdate >= intervalMs) { // 一定時間経過したら
    strip.setPixelColor(idx, c); // 現在のLEDを点灯
    strip.show();

    idx++;                       // 次のLEDに進む
    if (idx >= strip.numPixels()) {
      idx = 0;                   // 全部終わったら最初に戻る
      strip.clear();             // リセットして次の周回へ
    }
    lastUpdate = millis();       // 更新時刻を記録
  }
}

補足説明

  • delay() が消えた
    • millis() の差分で時間を測り、一定間隔ごとに1つ進める。
    • その間も loop() は高速で回るので M5.update() が呼ばれ、ボタン入力を逃さない。
      • loop()の中のif (millis() - lastUpdate >= intervalMs)が「〇〇秒経ったかな?」と毎回チェックしている。
  • ボタン操作が効くように
    • BtnA を押すと mode が切り替わり、次の色の流れを先頭から開始する。
    • 「流れている途中で色を変えたい」がちゃんと反応する。
  • 挙動
    • LEDが順に赤→緑→青で流れる。
    • ボタンを押せばその場で次の色に切り替わる。

6. IMUで「動き」に応じて明るさ調整(速く動かすほど明るくなる)

ここからはIMUの値でLEDの明るさを変える応用です。
M5StickC Plus2を振ると明るく、静止すると暗くなります。
考え方はシンプルで、合成加速度(x, y, z の平方和の平方根)と1G(静止時)との差を「動きの指標」として使います。
ノイズをならすために平滑化をかけ、0〜0.6Gあたりの範囲を明るさのスケールにマップします。
色は薄紫(R=80, G=0, B=120)で固定、RGBを直接スケーリングして明るさを表現します。

コード(06_imu_brightness)
#include <M5StickCPlus2.h>  // M5StickC Plus2 専用のライブラリを読み込む
#include <M5Unified.h>                   // M5デバイス(ボタン/画面/IMUなど)を使うライブラリ
#include <Adafruit_NeoPixel.h>           // NeoPixel(WS2812B)制御用ライブラリ
#include <math.h>                        // 数学関数(sqrtf, fabsfなど)を使う標準ライブラリ

#define PIXEL_PIN   32                   // NeoPixelデータ線をつなぐGPIO番号(M5StickC Plus2のGrove黄 = GPIO32)
#define PIXEL_NUM   8                    // 接続しているNeoPixelのLED個数
#define PIXEL_TYPE  NEO_GRB + NEO_KHZ800 // 色順と通信周波数(WS2812B標準設定)

Adafruit_NeoPixel strip(PIXEL_NUM, PIXEL_PIN, PIXEL_TYPE); // NeoPixel制御オブジェクトを生成

float activityLP = 0.0f;                 // 「動きの強さ」を平滑化した値(ローパスフィルタ後)
const float alpha = 0.08f;               // 平滑化係数(大きいと反応速い、小さいと滑らか)

void setup() {
  auto cfg = M5.config();                // M5設定オブジェクトを取得
  M5.begin(cfg);                         // M5デバイス初期化(画面/IMU/ボタン等が使えるようになる)

  M5.Display.setRotation(1);             // 画面を横向き表示に設定
  M5.Display.setTextSize(3);             // デバッグ表示用の文字サイズ
  M5.Imu.begin();                        // IMU初期化(加速度・ジャイロが使えるようになる)

  strip.begin();                         // NeoPixelを初期化
  strip.show();                          // 初期状態(全消灯)を反映
}

void loop() {
  M5.update();                           // ボタンやセンサの状態を更新

  // --- IMUから加速度を取得 ---
  float ax, ay, az;                      // 加速度を格納する変数
  M5.Imu.getAccel(&ax, &ay, &az);        // 加速度 [G] を3軸分取得
  float g = sqrtf(ax*ax + ay*ay + az*az);// 合成加速度の大きさ(ベクトル長)を計算
  float activity = fabsf(g - 1.0f);      // 静止時1Gとの差を「動きの指標」とする(fabsfは絶対値)

  // --- 平滑化処理(指数移動平均) ---
  // 過去の値と新しい値を (1-α):(α) の比率で混ぜることで、ノイズを減らす
  activityLP = (1.0f - alpha) * activityLP + alpha * activity;

  // --- 動きの大きさを明るさにマッピング ---
  // 0.0〜0.6G の範囲を 5〜120 の明るさに割り当てる
  float a = constrain(activityLP, 0.0f, 0.6f) / 0.6f;  // constrainで範囲外を0.0〜0.6に収めて正規化
  uint8_t bright = (uint8_t)(5 + a * 115);             // 0〜1の範囲を5〜120に変換

  // --- LEDを点灯(色は固定の薄紫で、明るさだけ変える) ---
  strip.setBrightness(bright);                        // LED全体の明るさを設定
  for (int i = 0; i < strip.numPixels(); ++i) {       // すべてのLEDについて
    strip.setPixelColor(i, strip.Color(80, 0, 120));  // 薄紫(R=80, G=0, B=120)を設定
  }
  strip.show();                                       // LEDに反映

  // --- デバッグ表示(画面に数値を出す) ---
  M5.Display.setCursor(0, 0);                        // 表示位置を左上に設定
  M5.Display.printf("|a|-1G: %.2f  Bright: %3d\n", activityLP, bright);
                                                    // 平滑化後の値と明るさを表示

  delay(10);                                         // 軽く間隔を空ける(10ms)
}

何をしているか

  • 静止中:加速度センサは常に重力 1Gを感じています(値 ≒ 1.0)。
  • 振る/動かす:重力に運動による加速度が上乗せされ、合計の大きさが 1G からズレます。
  • この「1Gからのズレ量」を動きの強さとして使い、強いほど明るくします。

数式で見る

  1. 合成加速度の大きさ(5StickCが全体としてどれくらいの力を受けているかを示す)
    \[ g = \sqrt{a_x^2 + a_y^2 + a_z^2} \]
  2. 動きの指標(振れ)
    \[ \text{activity} = \left| g – 1.0 \right| \]
    • 静止なら ≒ 0、振るほど大きくなる。

ゆらぎをならす(平滑化)

  • センサは微小ノイズや手ぶれで値がピクピク動いてしまいます。そのような見た目のチラつきを抑えるため、指数移動平均(EMA)で平滑化しています。(参考
    \[ \text{activityLP} = (1-\alpha)\,\text{activityLP} + \alpha \,\text{activity} \]
    • コードの 0.92/0.08
      (1−α)=0.92 ⇒ α=0.08 の意味。
    • αを小さくすると滑らか(遅め)、大きくするとキビキビ(速め)。
      • 平滑化は、ノイズや細かい揺れを無視して、大きな動きだけを捉えるための工夫です。

明るさへのマッピング(スケール設計)

  • 実運用では 0〜0.6G 程度がよく出るレンジかなと(個人差あり)。
  • そこで 0.0〜0.60〜1正規化し、5〜120 の明るさにマップしています。この辺りはケースバイケースです。
float a = constrain(activityLP, 0.0f, 0.6f) / 0.6f; // 0..1
uint8_t bright = (uint8_t)(5 + a * 115);           // 5..120
  • 下限5:光っているのがわかる。
  • 上限120:眩しすぎない、安全側の上限
  • constrain() とは?
    • Arduino に用意されている便利な関数で、値を 指定した範囲に制限(クリップ) するものです。
    • constrain(x, a, b)
      x … 制限したい値
      a … 下限(最小値)
      b … 上限(最大値)

LEDの色を固定して明るさを変える

色は薄紫 (R=80, G=0, B=120) に固定し、strip.setBrightness(bright) で全体の明るさをまとめて調整しています。
この方法だと、色味は一定のまま、センサの値に応じて 暗い → 明るい がなめらかに変化します。
(※「色味も変えたい」場合は、HSV色空間を使い H(色相)を動かす応用が可能)

更新タイミング(処理の粒度)

  • delay(10) について
    • 数値を小さくすると反応が速くなるがCPU負荷・表示負荷が増える。
    • 大きくすると省電力&安定だが反応が鈍く感じる可能性が高い。
    • ブロッキング注意
      • ボタン等同時制御を加えるときはノンブロッキングの骨格millis()管理)に寄せていくと良い。

7. 姿勢で色相変更+動きに応じて明るさ変更

姿勢で色相(H)が変わり、動きで明るさ(V)が変わる」発展版です。

HSVってなに?

HSV は色を

  • Hue(ヒュー:色相=色み/0〜360°相当、NeoPixelでは 0〜65535)
  • Saturation(サチュレーション:彩度=鮮やかさ/0〜255)
  • Value(バリュー:明るさ/0〜255)
    の3要素で表す方法です。

画像参照元

画像参照元

RGBとの違い・メリット

  • RGB は「赤・緑・青の混ぜ具合」。色相や明るさだけを変えたい時に調整が直感的じゃないことがある。
  • HSV は「色み(H)」「鮮やかさ(S)」「明るさ(V)」が独立してるので、
    • Hだけ回して虹色にしたり、
    • Vだけ上げ下げして明滅させたり、
    • Sを下げてパステル/白っぽくしたり、
      が簡単。
  • 変換ツールを触ってみるとイメージしやすいかなと思います。

NeoPixel(Adafruit_NeoPixel)での扱い

  • strip.ColorHSV(h, s, v) で HSV → RGB に変換してくれます。
    • h0〜65535(0 と 65535 が赤、途中が虹のグラデ)
    • s0〜255(0で白っぽい、255でビビッド)
    • v0〜255(明るさ)
  • 仕上げに strip.gamma32(color) を通すと、人間の目に自然な見え方になります。
コード(07_hsv_orientation)
#include <M5StickCPlus2.h>  // M5StickC Plus2 専用のライブラリを読み込む
#include <M5Unified.h>                   // M5の画面/ボタン/IMUを扱う
#include <Adafruit_NeoPixel.h>           // NeoPixel(WS2812B)制御
#include <math.h>                        // atan2f, sqrtf, fabsf など数値計算

// --- NeoPixel設定 ---
#define PIXEL_PIN   32                   // データ線のGPIO(M5StickC Plus2のGrove黄=GPIO32)
#define PIXEL_NUM   8                    // LEDの個数(手元の本数に合わせて)
#define PIXEL_TYPE  NEO_GRB + NEO_KHZ800 // WS2812B標準:色順GRB/800kHz

Adafruit_NeoPixel strip(PIXEL_NUM, PIXEL_PIN, PIXEL_TYPE); // NeoPixel制御オブジェクト

// --- 動き(=明るさ)用ローパス ---
float activityLP = 0.0f;                 // 動き指標の平滑化値
const float alpha = 0.08f;               // 平滑化係数α(大→反応速い/小→滑らか)

// ------------------------------------------------------------------
// 姿勢(加速度ベクトル)から色相Hを求め、彩度Sと明るさVを指定して色を返す関数
// ax, ay, az : 加速度(G)。vは明るさ(0-255)
// ここでは H: 水平面での角度(atan2) → 色相、S: 端末の傾き(az)で少し減衰、V: 呼び出し側で指定
// ------------------------------------------------------------------
uint32_t colorFromOrientation(float ax, float ay, float az, uint8_t v) {
  float ang = atan2f(ay, ax);            // 水平面での方位角(-π..π):x軸基準でy方向の角度
  // -π..π → 0..65535 に線形マップ(NeoPixelのHSVは色相を0..65535で表現)
  uint16_t hue = (uint16_t)((ang + (float)M_PI) * 65535.0f / (2.0f * (float)M_PI));

  // 彩度S:端末が真上/真下(az≒±1)のときは少し淡く(見た目の変化を出す演出)
  // |az|を0..1にクリップ→0.0..0.6倍して 1.0-その値 → 0.4..1.0 の範囲でSを決定
  uint8_t sat = (uint8_t)(255.0f * (1.0f - constrain(fabsf(az), 0.0f, 1.0f) * 0.6f));

  // HSV→RGB変換(Vは引数vを使用)
  uint32_t c = strip.ColorHSV(hue, sat, v);

  // ガンマ補正で人間の見え方に合わせる(暗部が沈みすぎない)
  return strip.gamma32(c);
}

void setup() {
  auto cfg = M5.config();                // M5設定オブジェクト
  M5.begin(cfg);                         // 画面/ボタン/IMUを初期化
  M5.Display.setRotation(1);             // 画面横向き
  M5.Display.setTextSize(2);             // デバッグ表示の文字サイズ
  M5.Imu.begin();                        // IMU初期化(加速度/ジャイロ)

  strip.begin();                         // NeoPixel初期化
  strip.setBrightness(60);               // ベースの明るさ(上限の目安、後でVでも調整)
  strip.show();                          // 反映(初期は全消灯)
}

void loop() {
  M5.update();                           // 入力更新(今回は主にIMU)

  // --- IMUから加速度を取得(単位:G。静止で概ね |(ax,ay,az)| ≒ 1)---
  float ax, ay, az;
  M5.Imu.getAccel(&ax, &ay, &az);

  // --- 明るさV:動きの大きさで決める(|合成加速度| と 1G の差分)---
  float g = sqrtf(ax*ax + ay*ay + az*az);        // 合成加速度の大きさ
  float activity = fabsf(g - 1.0f);              // 静止(1G)からのズレ=動きの強さ(>=0)

  // 指数移動平均で平滑化:activityLP ← (1-α)*old + α*new
  activityLP = (1.0f - alpha) * activityLP + alpha * activity;

  // 0.0..0.6G を 10..200 にマップして、V(輝度:0..255)として使う
  // constrainで外乱をクリップ→0..1正規化→スケーリング→最低輝度10を足す
  float norm = constrain(activityLP, 0.0f, 0.6f) / 0.6f;       // 0..1
  uint8_t val = (uint8_t)(10 + norm * 190.0f);                 // 10..200(必要に応じて調整)

  // --- 色相H/彩度S:姿勢(向き)から決める ---
  // H: 水平面の向き(atan2f(ay,ax)) → 赤〜緑〜青…に連続変化
  // S: az(上下向き)で少し減衰(真上/真下で淡く)※演出
  uint32_t col = colorFromOrientation(ax, ay, az, 255);        // ここではV=最大255で色を作る

  // --- 表示:色味はcol、明るさはsetBrightnessでvalに(色相と明るさを分離制御)---
  strip.setBrightness(val);                                    // 明るさ(V相当)を一括設定
  for (int i = 0; i < strip.numPixels(); ++i) {
    strip.setPixelColor(i, col);                               // 全LEDを同じ色に
  }
  strip.show();                                                // LEDに反映

  // --- デバッグ表示:チューニングのため数値を出す ---
  M5.Display.setCursor(0, 0);
  M5.Display.printf("ax=% .2f ay=% .2f az=% .2f\n", ax, ay, az);
  M5.Display.printf("activityLP=%.2f  V=%3d\n", activityLP, val);

  delay(10);                                                   // 軽く間引き(更新間隔10ms)
}

使い方と調整ポイント

  • ガンマ補正strip.gamma32() で見た目を自然に(暗部が沈みにくい)。
    • LEDの出力はだいたい「数値に比例して直線的」に明るくなります(リニア)。
    • 人間の目は明るさを「対数っぽく」感じます(非リニア)。
    • そのギャップで、低い値が暗黒・中間が急に明るすぎに見えがちです。
      → 小さな明るさ変化が見えづらく、ヌメッとしたグラデーションにならない。
      • そこでリニアなRGB値を、人間の感覚に近いカーブに変換します。
      • 詳しくはこちらなど
  • 色相(H):本体を左右に回すと色がぐるっと変化(atan2f(ay, ax))。
  • 彩度(S)az を使って「真上/真下では少し淡く」なる演出。要らなければ sat=255; 固定でOK。
  • 明るさ(V):振ると明るく、止めると暗く(activityLP0.0..0.6G → 10..200 にマップ)。
    • 反応の速さは alpha(平滑化係数)で調整。大きいと即応、小さいと滑らか。
    • 明るさレンジは 10..200 を環境に合わせてチューニング(暗室なら下げ、明るい場所なら上げ)。

8. 課題

課題の評価軸についての質問があったので、ここで明確にしておきます。

  • 基礎課題
    • 基本的には条件を満たしていて、期限まで提出されていれば5点
      • ※ 第3回の基礎課題は厳しめに採点します
    • 提出が遅れると減点
  • 発展課題
    • コンセプトと実装クオリティで厳しく評価します。(毎回軸を設定しようと思いますが)
    • 各回5点満点中、大体平均2~3点になるように採点
  • 最終発表
    • コンセプトと実装クオリティで厳しく評価します。
    • 15点満点中、大体平均7~8点になるように採点)

※ ChatGPT、Gemini、Claudeなど使ってOKです。ただし、個人的には指示は具体的にして、1指令ずつコードを積み上げていったほうが学びは深いのかなと思います。例えば、「M5StickCPlus2の角度に応じてLEDの光り方を変化させて」 など抽象的な指示ではなくて、「M5StickCPlus2の姿勢を取得してください」「得られた姿勢の値に応じてLEDの光り方を変化させてください」と言ったように、ステップを分解して指示すると一つ一つの機能が理解しやすいのかなと思います。また、毎回コードすべてにコメントをつけてもらうようにして、自分で1行1行確認すること。わからない行は都度ChatGPTなどに聞くこと。

基礎課題

姿勢で色相変更+動きに応じてLEDの明るさを変更させるデモを完成させる。

提出内容(2つ)

  • ソースコードをMoodleに提出してください。
  • 動作中の動画をMoodleに提出してください。

発展課題

NeoPixel × 2つ以上のM5stickC PLus2の機能」を組み合わせ、インタラクティブに反応する小作品を作ってください。作品詳細も記載してください。
作品概要(コンセプト)と実装クオリティで評価します。

提出内容(2つ)

  • ソースコード(~.ino)をMoodleに提出してください。
    • また、readme.txtを作成し、以下のフォーマットで作品の詳細を記載してソースコードと一緒にMoodleに提出してください(以下は例)。
/*
作品名:姿勢矯正
作品概要(コンセプト):M5stickCplus2を首の裏側に装着する。スマホを見ている時の首の角度に応じて、スマホに装着したLEDが光る。長時間続く場合はブザーが鳴る。
M5の機能:IMU, ブザー
*/
  • 動作中の動画をMoodleに提出してください。

※ 次回の第3回まで2週間も空いてしまうため、予習がてら進めておくと良いかもです。また、3日後に第4回の講義がある点についても注意です。
10/20(月) #3 M5StickC Plus2基礎:PCとの連携に向けて 〜p5.js基礎〜
10/23(木) #4 M5StickC Plus2 で学ぶネットワーク活用:HTTP


オプション:GitHubについて

すべてのコードをGitHubにアップするようにしました。

使ったことある方はご活用ください。
使ったことがない方もこちらを参考にして、この機会に使ってみてください。