まずはこれを見てください
通常、Arduinoのデジタルピンは HIGH (5V) か LOW (0V) のどちらかしか出力できません。つまり、LEDは「点く」か「消える」かの2択です。
しかし、この動画を見てください。LEDがまるで呼吸するように、滑らかに明るさを変えています。 しかも、PWM対応ピン(~マーク)だけでなく、すべてのピンが同時に滑らかに光っています。
今回は、Arduino UNO用LEDシールド と Arduino UNO R4 を使い、ソフトウェアの力で「光の波」を作り出しました。
使用したハードウェア
前回のセル・オートマトンに続き、今回も 「Arduino UNO用LEDシールド」 を使用しています。
使用機材
- Arduino UNO R4 Minima
- Arduino UNO用LEDシールド (全20ピンLED搭載)
Arduino R4の高速処理が鍵
Arduino UNO R4は 48MHzの高速クロック を搭載しています。これにより、ソフトウェアで全ピンを高速制御しても処理落ちせず、カメラで撮影してもフリッカー(ちらつき)が出ない ほど滑らかな描画が可能になりました。
技術のキモ:Software PWMとは?
PWMの基本原理
Arduinoには、アナログ出力(analogWrite)ができるピンが用意されています。これらは ハードウェアPWM(Pulse Width Modulation) というタイマー回路を内蔵したピンです。
PWMとは、「ものすごい速さでONとOFFを繰り返す」制御方法です。
PWMの原理:ONの時間(デューティ比)で明るさを調整
ONの時間(パルス幅)を長くすれば明るく見え、短くすれば暗く見えます。人間の目にはその点滅が速すぎて(通常490Hz〜980Hz)見えないため、明るさが連続的に変わったように錯覚するのです。
デューティ比 25%: ■□□□■□□□■□□□ → 暗い
デューティ比 50%: ■■□□■■□□■■□□ → 中間
デューティ比 75%: ■■■□■■■□■■■□ → 明るい
なぜ「Software」なのか?
通常のArduino UNO(R3やR4)では、ハードウェアPWMが使えるピンは6本程度しかありません。 しかし、このLEDシールドには 20個のLED(D0-D13の14本 + A0-A5の6本)が搭載されています。
全ピンで明るさを制御したい場合、ハードウェアPWMピンだけでは足りません。
そこで、「プログラム(ソフトウェア)で強引に高速点滅させる」 ことで、すべてのピンをPWM制御する手法が Software PWM です。
Software PWMの実装原理
// 基本的なアイデア(簡略化)
void softwarePWM(int brightness) { // brightness: 0〜255
for (int cycle = 0; cycle < 256; cycle++) {
if (brightness > cycle) {
digitalWrite(LED_PIN, HIGH); // ON
} else {
digitalWrite(LED_PIN, LOW); // OFF
}
delayMicroseconds(10); // 短時間待機
}
}
0〜255のカウンターを回しながら、設定した明るさ値よりカウンターが小さい間はON、大きくなったらOFFにします。 これを すべてのピンに対して並列に実行することで、全ピンの明るさを同時に制御できます。
Software PWMのタイミングチャート
Arduino UNO R4の高速処理能力
Arduino UNO R4(RA4M1、48MHz)の性能により、以下が実現できています:
- 高速処理: 20ピン全てを同時に高速制御しながら、波の計算も並行実行
- 高精度タイマー:
micros()による細かい時間制御で、滑らかなPWM波形を生成 - フリッカーフリー: 数kHzの高周波PWMにより、目にもカメラにも優しい描画
これにより、全ピンのSoftware PWMと複雑な波の計算を並行実行しても、滑らかな動作が実現できました。
滑らかに光るLED(フリッカーなし)
3つの「光の波」パターン
今回は 約10秒ごとに自動で切り替わる 3つのアルゴリズムを実装しました。
1. Deep Ocean(深海)
特徴
- 動き: 大きな波がゆっくりと左から右へ流れます。
- 周期: 約3秒で1周期(ゆったりとした呼吸のリズム)
- 技術: ガンマ補正(γ=2.0) を適用
Deep Oceanの波形:ゆったりとした大きな波が左から右へ流れる
ガンマ補正とは?
人間の目は 明るさの変化を対数的に感じる 特性があります。つまり、暗い部分の微妙な変化には敏感ですが、明るい部分では変化が鈍感になります。
単純な sin() 関数だけでは、暗い部分が真っ黒に潰れてしまい、階調が失われます。
そこで、計算した明るさ値を 2乗(γ=2.0) することで、暗い部分の階調を豊かにし、人間の目に自然な明暗変化を作り出しています。
// ガンマ補正の計算式
float linearBrightness = (sin(t) + 1.0) * 127.5; // 0〜255
float correctedBrightness = pow(linearBrightness / 255.0, 2.0) * 255.0;
ガンマ補正の効果(線形 vs γ=2.0)
見どころ
見ていて落ち着く、瞑想的なモードです。暗い部分の階調が非常に豊かで、まるで深海の光のゆらぎを見ているような感覚になります。
2. Cyber Pulse(電脳パルス)
特徴
- 動き: 細かい波が高速で 逆方向(右から左) に流れます。
- 周期: 約1秒で1周期(激しくパルスする感じ)
- 波長: Deep Oceanの3倍細かい波(
waveLen = 0.6)
Cyber Pulseの波形:細かい波が高速で右から左へ流れる
技術ポイント
波の進行方向を逆にするには、時間項の符号を反転させるだけです。
// Deep Ocean(左→右)
float val = sin(t * speed + i * waveLen);
// Cyber Pulse(右→左)
float val = sin(t * speed - i * waveLen); // 符号を反転
見どころ
データ通信を行っているような、サイバーパンク的な演出です。R4の処理速度の高さを視覚的に体感できます。SF映画のコンピューター画面のような雰囲気を醸し出します。
3. Sonar Ripple(波紋)
特徴
- 動き: ボードの中心から外側に向かって、波紋が広がります。
- 周期: 約2秒で1周期
- 空間的な広がり: 一次元のLED配置でありながら、2次元的な波紋を表現
アルゴリズムの工夫
中心(D6とD7の間 = インデックス6.5)からの距離を計算し、その距離を位相に反映させています。
for (int i = 0; i < 14; i++) {
float dist = abs(i - 6.5); // 中心からの距離
float val = sin(t * speed - dist * waveLen);
// ...
}
これにより、中心で波が発生し、外側に向かって伝播していく波紋効果が生まれます。
Sonar Rippleの位相図:中心から外側へ波紋が広がる
見どころ
ソナー(音波探知機) や 水面の波紋 のような、物理的な広がりを感じさせます。一次元のLED配置にも関わらず、空間的な奥行きを演出できています。
おまけ:連動するレベルメーター
下段の A0〜A5ピン(6本)は、ただ光っているだけではありません。
上段のD0〜D13の波の明るさの平均値を計算し、それを6ビットのバーグラフとして表示しています。
// 上段の波のエネルギーを計算
int avgBrightness = 0;
for (int i = 0; i < 14; i++) {
avgBrightness += waveBrightness[i];
}
avgBrightness /= 14; // 平均値
// 6ビットのバーグラフに変換(0〜6本の点灯)
int barLevel = map(avgBrightness, 0, 255, 0, 6);
for (int i = 0; i < 6; i++) {
levelMeterState[i] = (i < barLevel) ? 1 : 0;
}
これにより、「波のエネルギーを計測している」 ような演出が加わり、ガジェットとしての精密感・SF感が高まります。
完全なソースコード
以下が、実際に動作しているArduinoスケッチの完全版です。
/*
Demo: 3-Pattern PWM Wave Animation
Board: Arduino Uno R4 Minima / WiFi
D0-D13: Wave Display (14 LEDs)
A0-A5 : Level Meter (6 LEDs)
*/
// ピン定義
const int wavePins[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 };
const int numWavePins = sizeof(wavePins) / sizeof(wavePins[0]);
const int meterPins[] = { A0, A1, A2, A3, A4, A5 };
const int numMeterPins = sizeof(meterPins) / sizeof(meterPins[0]);
// 波のパターン定義
struct WavePattern {
float speed; // 速度(波の周期)
float waveLen; // 波長(隣接LED間の位相差)
bool reverse; // 逆方向フラグ
bool ripple; // 波紋モードフラグ
bool useGamma; // ガンマ補正フラグ
};
WavePattern patterns[] = {
{ 0.002, 0.2, false, false, true }, // Deep Ocean(ガンマ補正あり)
{ 0.006, 0.6, true, false, false }, // Cyber Pulse(高速逆方向)
{ 0.003, 0.4, false, true, false } // Sonar Ripple(波紋)
};
int currentPattern = 0;
unsigned long patternStartTime = 0;
const unsigned long patternDuration = 10000; // 10秒ごとに切り替え
// 明るさ配列
byte waveBrightness[14];
byte levelMeterState[6];
void setup() {
// ピン初期化
for (int i = 0; i < numWavePins; i++) {
pinMode(wavePins[i], OUTPUT);
}
for (int i = 0; i < numMeterPins; i++) {
pinMode(meterPins[i], OUTPUT);
}
patternStartTime = millis();
}
void loop() {
unsigned long currentMillis = millis();
// パターン切り替え判定
if (currentMillis - patternStartTime > patternDuration) {
currentPattern = (currentPattern + 1) % 3;
patternStartTime = currentMillis;
}
WavePattern &p = patterns[currentPattern];
// --- 1. 計算フェーズ ---
// sin関数を使って、各LEDの目標の明るさ(0-255)を計算
for (int i = 0; i < numWavePins; i++) {
float phase;
if (p.ripple) {
// 波紋モード:中心からの距離で位相を決定
float dist = abs(i - 6.5); // 中心(6.5)からの距離
phase = currentMillis * p.speed - dist * p.waveLen;
} else {
// 通常モード:左から右(または逆)に進む波
phase = currentMillis * p.speed + (p.reverse ? -1 : 1) * i * p.waveLen;
}
float val = sin(phase);
float brightness = (val + 1.0) * 127.5; // -1〜1 を 0〜255 に変換
// ガンマ補正(Deep Oceanのみ)
if (p.useGamma) {
brightness = pow(brightness / 255.0, 2.0) * 255.0;
}
waveBrightness[i] = (byte)brightness;
}
// レベルメーターの計算(波の平均明るさ)
int avgBrightness = 0;
for (int i = 0; i < numWavePins; i++) {
avgBrightness += waveBrightness[i];
}
avgBrightness /= numWavePins;
int barLevel = map(avgBrightness, 0, 255, 0, numMeterPins);
for (int i = 0; i < numMeterPins; i++) {
levelMeterState[i] = (i < barLevel) ? 1 : 0;
}
// --- 2. 描画フェーズ (Software PWM) ---
// ここを高速で回すことで、擬似的にアナログ出力を再現
unsigned long pwmStart = micros();
while (micros() - pwmStart < 5000) { // 5ms間描画(= 200Hz PWM)
int pwmCycle = micros() & 0xFF; // 0-255のカウンター
// 波のPWM
for (int i = 0; i < numWavePins; i++) {
digitalWrite(wavePins[i], (waveBrightness[i] > pwmCycle) ? HIGH : LOW);
}
// レベルメーターのPWM
for (int i = 0; i < numMeterPins; i++) {
digitalWrite(meterPins[i], levelMeterState[i]);
}
}
}
コードのポイント解説
1. パターン構造体で管理
struct WavePattern {
float speed; // 速度
float waveLen; // 波長
bool reverse; // 逆方向
bool ripple; // 波紋モード
bool useGamma; // ガンマ補正
};
3つのパターンのパラメータを配列で管理し、currentPattern で切り替えることで、コードの重複を防いでいます。
2. 計算と描画の分離
// フェーズ1: 計算(数ms)
for (int i = 0; i < 14; i++) {
waveBrightness[i] = calculate(...);
}
// フェーズ2: 描画(5ms間高速ループ)
while (micros() - pwmStart < 5000) {
// 全ピンを高速ON/OFF
}
重い計算(sin関数など) と 軽い描画(digitalWrite) を分離することで、描画フェーズを高速に保っています。
3. マイクロ秒単位のPWM制御
int pwmCycle = micros() & 0xFF; // 0〜255のカウンター
digitalWrite(pin, (brightness > pwmCycle) ? HIGH : LOW);
micros() の下位8ビット(0〜255)をカウンターとして使用し、設定した明るさと比較することで、Software PWMを実現しています。
このループが 約256μs(0.256ms)で1周するため、PWM周波数は 約3.9kHz になります。これは人間の目では完全に見えず、カメラでもフリッカーが出にくい周波数です。
R4の 48MHz高速処理 により、高周波PWMを実現でき、人間の目にもカメラにも自然な明暗変化 を作り出せています。
まとめ:デバッグツールがアートガジェットに
このLEDシールドは、本来は 「Lチカ」や「ピンの状態確認」 のために作ったデバッグツールです。
しかし、こうしてプログラムを工夫するだけで:
- 波のシミュレーション装置
- 1/fゆらぎの癒やしガジェット
- インテリアライト
- プログラミング教育ツール
として使えることがわかりました。
得られた知見
- 「機能が単一(LEDがついているだけ)」 であることは、逆に言えば 「ソフトウェア次第で何にでもなれる」
- Arduino R4の性能向上 は、Software PWMのような「力技」を実用レベルに引き上げた
- ガンマ補正 など、人間の知覚特性を考慮した調整が、見た目の美しさに直結する
応用アイデア
今回のコードをベースに、以下のような発展も可能です:
- 音楽に同期: マイクで音を拾い、波の速度や振幅を変える
- 温度センサー連動: 室温に応じて波の色(LEDの色温度)を変える
- Bluetooth制御: スマホアプリからパターンやパラメータを変更
- 複数台の同期: 複数のシールドを並べて「波の伝播」を表現
ぜひ、あなたのアイデアで新しい光り方を作ってみてください。
購入・技術資料
BOOTHで販売中
価格: ¥2,000(税込・送料別)
オープンソース資料
回路図と、今回のSoftware PWMのArduinoスケッチは、GitHubで公開しています:
回路図・基板データ・サンプルコードを自由に利用できます
単純なON/OFFだけのデジタルピンが、プログラムの力で「呼吸」し始める。
それは、ハードウェアとソフトウェアの境界が曖昧になる瞬間です。
ぜひ、あなたもこのLEDシールドで、「光のアルゴリズム」 を体験してみてください。