#1. はじめに
LED を円形に並べた置時計が数多くあります。MakerFaire Tokyo 2012 において、Silrium さんの作品1でその存在を知りました。AliExpress, Banggood, eBay には安価な電子工作キット234があります。RGB LED 使用の製品5もあります。Flyandace さんの作品6には独特の雰囲気があります。反省会駆動さんの作品7には薄型のこだわりがあります。tadashi101 さんの作品8は、360 個の LED のはんだ付けが必要で話題になりました。LED を 3 重の円に並べた作品910もあります。私もオリジナルを作ってみたいと思いました。
#2. ハードウェア
プリント基板 (PCB) を作成しました。回路図等を GitHub に置きました11。中国の PCB 製造業者の多くが格安サービスを設定していて、その仕様が FR-4(ガラスエポキシ)、2層、10cm × 10cm 以内、5~10 枚です。このサイズに収めます。
###RGB LED
10cm × 10cm のプリント基板上に LED を円形に 60 個並べます。5050 サイズ (5mm × 5mm) では収まらず、2020 サイズ (2mm × 2.2mm) が必要です。WorldSemi 社の WS2812C-202012を使います。WS2812B-202013は、置時計用には電流値(明るさ)が大きすぎ、発熱も心配です。PWM で抑えると明暗の余地が減ります。WS2812C-2020 は、バイパスコンデンサが必要ですが、発熱対策や表現力で有利です。
WS2812B-2020 | WS2812C-2020 | |
---|---|---|
バイパスコンデンサ | 不要 | 要 |
チャネルあたりの電流 | 16mA | 5mA |
1個あたりの消費電力 | 0.24W | 0.075W |
###NeoPixel
NeoPixel14は Adafruit 社の LED 製品のブランドです。「デジタル RGB LED」または「スマート LED」と呼ばれる WorldSemi 社15の WS2811, WS2812 や Shenzhen LED Color 社16の SK6812 が使われています。"1/4 60 Ring"17は掛時計の製作にぴったりと思われます。
###M5Atom
M5Stack 社の M5Atom18は、小型で安価な IoT 開発用デバイスです。素材は ESP32 です。単体で WiFi 接続が可能で、時刻合わせに NTP を使えます。Arduino-IDE などの開発環境があります。M5Atom Lite19は NeoPixel 仕様の LED を 1 個、M5Atom Matrix20は 25 個搭載しています。M5Atom.h が FastLED.h をインクルードしているため、Arduino IDE のライブラリ・マネージャで予め FastLED をインストールしておく必要があります。
###プリント基板
LED を円形に 60 列× 3 行、中央に 16 列× 6 行、計 276 個を並べることができました。おもて面に SMT 部品を配置しリフローを片面で済ませます。うら面に M5Atom やコネクタなどの背の高い部品を配置し、スモーク板などの取り付けを邪魔しない様にします。
PCB CAD には KiCad21を使用しました。フットプリントの円形配置には、Kicad Action Plugins - Place footprints22を利用しました。四隅をマウスバイト23で折り取れる様にはしましたが、部品が接近しているので慎重な作業が必要です。
おもて面にシルクのスペースがなく、苦肉の策でうら面に移動しました。部品の特定の際は、おもて面に残せたシルクを手がかりにうら面のシルクで確認します。
M5Atom の周辺にあるグラウンドのベタパターンが影響し、置き方によっては WiFi が繋がり難いかもしれません。プリント基板を外した状態では繋がり易くなる場合、プリント基板の影響と特定できます。
###電源
WS2812C-2020 の電源は 5V です。DC ジャックから供給します。M5Atom のピンソケットへも逆流防止のショットキーバリア・ダイオード (SBD) を経由して供給します。M5Atom は、USB-C か、またはピンソケットからの 5V を受けて動作します。
WS2812C-2020 は、1 素子あたり (R, G, B 各々) 5mA ですので、仕様上の最大消費電流・電力は以下です。
5mA \times 3/LED \times 276 LED/PCB = 4.14A\\
5V \times 4.14A = 20.7W
FR-4 基板で放熱も考慮しない状況で 20W(4A) は危険です。最大でも 5W(1A)程度になる様に WS2812C-2020 の明るさを 1/4 に絞ります。テスタで測定したところ、1/4 の明るさで R, G, B を同時に光らせると、5V 電源の電流は約 1A を指します。このとき、プリント基板がかなり暖かくなります。1/2 の明るさにすると 2A(10W)を指し、プリント基板が触れないほど熱くなります。
プログラミングを誤るなどして予想外に発熱した場合には、躊躇なく DC ジャックからプラグを抜いて電源を遮断します。修正したプログラムを書き込んだ後で DC ジャックにプラグを差し込みます。
実験中の安全のため、5V 電源(AC アダプタ)側で最大でも 2A 程度24に抑えるのが得策です。
置時計のデモプログラムの実行中の消費電流(電力)は、平均 0.2A 前後(1W前後)、パーリンノイズのデモを実行中は、平均 0.35A程度(2W弱)です。
#3. FastLED
FastLED25は、RGB LED を扱うためのライブラリです。特に NeoPixel への対応が充実しています。Arduino IDE で利用できます。Adafruit が提供している NeoPixel ライブラリ26とは別のものです。
###I2S
ESP32 の I2S (Inter IC Sound) 機能を利用して FastLED の処理を効率化できます27。FastLED.h (M5Atom.h) をインクルードする前に I2S を有効化する指定をします。
NeoPixel 仕様では、LED を直列に接続します。LED へのデータをシリアルにして 800kHz の規定のタイミングで途切れることなく送り出す必要があります。276 個 1 列 の LED へのデータ送出には 8.3ms かかります。
\frac {8bit \times 3color \times 276LED}{800kHz} = 8.28ms
I2S を指定しない場合、FastLED は ESP32 の RMT (Remote Control) 機能を使用します。RMT ではデータ転送をプログラムで行うのに対し、I2S では DMA (Direct Memory Access) を利用します。ソフトウェアの負荷が軽減され、WiFi など他の処理との共存に有利です。I2S を使用する他のソフトウェアには制限が生じます。
###ポート
今回の外付け円形 LED では、M5Atom の GPIO を 1 ポート使用します。
FastLED は、2 チャネルある I2S のうち 1 チャネルを使用します。I2S の LCD モードを使用して 24 ポート並列にデータ送信ができます。各ポートに接続する LED は同一仕様(タイミング・数)で、全 LED を一括で制御する必要があります。
クラス CFastLED のインスタンスとして FastLED が予め宣言されています。これとは別のインスタンスを FastLED2 などと宣言しても同様に動作します。しかし、I2S が FastLED と FastLED2 とで干渉するため同時には使用できません。
M5Atom の内蔵 LED は、今回の外付け円形と統合的に扱うことは可能と考えられます。しかしながら M5Atom ライブラリは、内蔵 LED 用に FastLED を使います。M5Atom ライブラリによる内蔵 LED (display) との共存はあきらめ、M5.begin( ) で無効にしています。
ちなみに、RMT は 8 チャネルあります。FastLED では順番待ち処理により 16 ポートまで拡張できます。
###GPIO
LED への信号は M5Atom のピンソケットから取り出します。WS2812C-2020 のデータ入力は $V_{IL}<0.7V$、$V_{IH}>2.7V$ です。プリント基板内で配線も短く、3.3V レベルの M5ATom の GPIO を直結できます。
GPIO の選択にあたり、M5Atom 本体と拡張ユニット (Atom Base) のいくつかについて、ピンソケットの GPIO の使用状況を調べました。
pin socket | M5Atom Lite/Matrix | M5Atom Echo | QR-CODE | GPS | TF-CARD | RS232 RS485 | Switch/-D |
---|---|---|---|---|---|---|---|
GPIO19 | BCLK | MOSI | MOSI | TX | Relay2 | ||
GPIO21 | SCL (Matrix) | SCL (3v3 pull-up) | |||||
GPIO22 | DataOut | TX | RX | Relay1 | |||
GPIO23 | GPIO34 (ADC1_CH6) | DataIn | TRIG | CLK | CLK | TX | |
GPIO25 | SDA (Matrix) | SDA (3v3 pull-up) | |||||
GPIO33 | ADC1-CH5 | LRCK | DLED | MISO | MISO | RX |
以下は候補から外し、将来の拡張用に Grove コネクタに取り出します。
- GPIO21, GPIO25: I2C (SCL/SDA) に使用。将来の拡張は、RTC, 環境センサなど
- GPIO23(GPIO34), GPIO33: アナログ入力機能あり。将来の拡張は、環境センサ、十字ボタン、アラームなど
GPIO22 は、様々な用途に使用されています。LED への信号出力は GPIO22 に決めました。M5Atom Echo の GPIO22 は I2S (DataOut) に使われています。M5Atom Echo は使用できません。GPIO19 は GPIO23, GPIO33 と共に SPI (MOSI, CLK, MISO) に使用されています。GPIO19 は未接続のまま残します。
###初期設定
ハードウェア構成を反映した初期設定のコーディング例です。LED データは CRGB 型の配列 leds[ ] に保持します。FastLED.addLeds( ) で LED データや GPIO を指定します。WS2812C-2020 の RGB の順序 GRB も併せて指定します。FastLED.setBrightness( ) で、消費電力抑制のため全体的な明るさを 1/4 の 64 に設定します。
#define FASTLED_ESP32_I2S true
#include <M5Atom.h>
// for external LEDs
const int pin_to_leds = 22; // connect to GPIO22
const int num_of_leds = 276;
CRGB leds[num_of_leds];
const int brightness = 64; // 64 of 255
void setup()
{
// M5Atom
const bool serial_enable = true;
const bool i2c_enable = true;
const bool display_enable = true;
M5.begin(serial_enable, i2c_enable, !display_enable);
FastLED.addLeds<WS2812, pin_to_leds, GRB>(leds, num_of_leds);
FastLED.setBrightness(brightness);
}
#4. 座標
LED を 1 列に接続します。円形部分では 1 周したら次の列に接続します。長方形部分では、まず左上から下に繋ぎ、次の列は下から上に繋ぎます。これを 8 回繰り返します。この蛇行接続 (serpentine, zigzag) は、市販製品28でも採用されています。
LED データを保持する 1 次元配列 leds[ ] に対し、円形部分 leds1, 矩形部分 leds2 を仮想的に構成し、各々について (x, y) 座標に色を書き込む関数を用意します。色として foreground_color を用意して予め設定しておきます。
// circle: leds1[0..60][0..2] --> leds[0..179]
const int leds1_num_of_x = 60;
const int leds1_num_of_y = 3;
// rectangle: leds2[0..15][0..5] --> leds[180..275]
const int leds2_num_of_x = 16;
const int leds2_num_of_y = 6;
const int head_of_leds2 = leds1_num_of_x * leds1_num_of_y;
// for fastLED
const int num_of_leds = leds1_num_of_x * leds1_num_of_y + leds2_num_of_x * leds2_num_of_y;
CRGB leds[num_of_leds];
CRGB foreground_color = CRGB::SlateGray;
// put a dot on leds1[x][y]
void PutDotLeds1(int x, int y)
{
if (x >= 0 && x < leds1_num_of_x && y >= 0 && y < leds1_num_of_y)
leds[x + leds1_num_of_x * y] = foreground_color;
}
// put a dot on leds2[x][y]
void PutDotLeds2(int x, int y)
{
if (x >= 0 && x < leds2_num_of_x && y >= 0 && y < leds2_num_of_y)
if (x % 2 == 0)
leds[head_of_leds2 + leds2_num_of_y * x + y] = foreground_color;
else
leds[head_of_leds2 + leds2_num_of_y * x + leds2_num_of_y - 1 - y] = foreground_color;
}
#5.色
色の指定方法は、FastLED の Wiki29 に詳しいです。全体像を分かる範囲で図にしました。中心にあるのが CRGB です。CRGB データは直接指定できます。いくつかの方法で表現された色を CRGB に変換する処理30があります。また、CRGB データを LED に送り出す際に補正する処理があります。
###CRGB
RGB データを詰め込んだ構造体です。Red, Green, Blue を各々 8 bit 符号なし整数 0 ~ 255 で表します。数値表現の他に予め定義された名前(約 150 色)で指定できます。
int red = 255; // 0..255
int green = 255; // 0..255
int blue = 255; // 0..255
CRGB color1 = CRGB(red, green, blue);
CRGB color2 = CRGB::DarkOliveGreen; // pre-defined color
###CHSV
色を色相 (Hue)、彩度 (Saturation/Chroma)、明度 (Value/Brightness) で指定できます。本来の色相 0 ~ 360、彩度 0 ~ 100、明度 0 ~ 100 に対し、すべて 8 bit 符号なし整数 0 ~ 255 で指定します。色相には予め定義された名前(8 色)があります。色相から RGB への変換には、FastLED 独自の Rainbow Hue Chart30が使用されます。本来の Spectrum Hue Chart よりも黄色の幅が広くなっています。
int hue = 0; // 0..255 for 0..<360
int satulation = 255; // 0..255 for 0..100
int value = 255; // 0..255 for 0..100
CRGB color1 = CHSV(hue, saturation, value);
CRGB color2 = CHSV(HUE_RED, saturation, value); // pre-defined hue
###カラーパレット
色調を一括で切り替えるカラーパレット機能があります。カラーパレットに 0-255 の値 (color index) を入れると対応する色が得られます。予め 8 種のカラーパレット31が用意されています。色を例えば 16 色並べて独自のカラーパレットを定義できます。256 の色の生成には、補間なし (NOBLEND)、補間あり (LINEARBLEND) を指定できます。カラーパレットに RainbowColors_p を選択すると、color index は色相 hue に相当します。
Palette | 色調 | 備考 |
---|---|---|
CloudColors_p | 雲 | |
LavaColors_p | 溶岩 | |
OceanColors_p | 海 | |
ForestColors_p | 森 | |
RainbowColors_p | 虹 | |
RainbowStripeColors_p | 虹 | 色の境界に黒帯、RainbowStripesColors_p も同じ |
PartyColors_p | パーティ | 陽気な雰囲気にする |
HeatColors_p | 黒体放射 | 0~240 |
CRGBPalette16 palette_color = RainbowColors_p;
// hue ring
for (int i = 0; i < 60; ++i) {
int hue = 256.0 * i / 60.0 + 0.5;
// by CHSV
leds[i] = CHSV(hue, 255, 255);
// through the color palette of RainbowColors_p
leds[i] = ColorFromPalette(palette_color, hue, 255);
}
###色温度
色温度 (Temperature)32の補正ができます。黒体放射(9 種)、ガス封入管のスペクトル(10 種)、および「補正なし」が用意されています。
FastLED.setTemperature(Tungsten40W);
###色補正
LED の R, G, B 間の差を合わせ込む色補正 (Color Correction)33 ができます。4種(実質 2 種)の補正値と「補正なし」が用意されています。
名称 | 補正値 | 備考 |
---|---|---|
TypicalSMD5050 | 0xFFB0F0 | 5mm ピクセル |
TypicalLEDStrip | 0xFFB0F0 | 一般的な LED テープ |
Typical8mmPixel | 0xFFE08C | 8mm ピクセル |
TypicalPixelString | 0xFFE08C | スルーホールパッケージ |
UncorrectedColor | 0xFFFFFF | 補正なし |
WS2812C-2020 用はありません。実際に色相環を表示すると、青が非常に弱く、緑が強すぎます。赤、緑、青が同じ幅になる様な補正値を独自に定義しました。明るさ補正 FastLED.setBrightness( ) の影響もあるかもしれません。
const int brightness_bf025 = 64; // original for BF-025(WS2812C-2020)
const CRGB correction_bf025 = 0xB080FF; // original for BF-025(WS2812C-2020)
FastLED.setBrightness(brightness_bf025);
FastLED.setCorrection(correction_bf025);
###ディザリング
明るさ補正 FastLED.setBrightness( ) により、各色の明るさを均等に圧縮します。このときビット落ちにより細かな違いが消えてしまいます。ディザリング (dithering)34は、明るさの増減を短時間に繰り返して中間的な明るさを実現します。通常オンで、指定でオフにできます。
ディザリングを機能させるためには、LED へのデータ書き込み FastLED.show( ) を頻繁に呼び出す必要があります。待機時間中ディザリングを機能させるために FastLED.delay( ) が用意されています。FastLED.delay( ) は、FastLED.show( ) を定期的に呼び出します。
今回の置時計では FastLED.show( ) を 20ms 周期 (50fps) で呼び出しています。delay( ) の代わりに FastLED.delay( ) とすると激しくちらつきます。FastLED.show( ) をやめて FastLED.delay( ) に任せると細かなちらつきになります。FastLED.show( ) と delay( ) の場合でも 10ms 周期 (100fps) とすると細かなちらつきが出ます。ディザリングをオフにするとちらつきが見えなくなります。今回は、通常の delay( ) としています。ちらつきが気になる場合ディザリングをオフにします。
ディザリング | 処理 | 20ms (50fps) | 10ms (100fps) |
---|---|---|---|
あり | FastLED.show( ); FastLED.delay( ); | 激しいちらつき | |
あり | FastLED.delay( ); | 細かなちらつき | |
あり | FastLED.show( ); delay( ); | ちらつき見えず | 細かなちらつき |
なし | FastLED.show( ); delay( ); | ちらつき見えず |
// FastLED.setDither(DISABLE_DITHER); // uncomment to prevent flicker
###パーリンノイズ
FastLED には、スケーリング、三角関数、乱数などを整数で高速に行う数学ライブラリがあります35。自然なテクスチャを擬似的に生成するパーリンノイズ (Perlin Noise)36も用意されています。
8bit 版である inoise8( ) の引数 (16bit) は、上位 8bit が整数部、下位 8bit が小数部です。上位が周期単位の、下位が周期内の変化を決める模様です。3 次元の inoise8(x, y, z) において、x, y を平面座標に、z を時間経過に使用すると、テクスチャが変化する様子を見ることができます。以下のデモでは、平面の短辺 y のサイズを周期スケールとし、時間経過の 1s を周期としてパーリンノイズを色相 Hue としています。ノイズの特性上、0, 255 付近の色(赤)はほとんど現れませんでした。
const int loop_ms = 20; // 20ms
int last_ms = 0;
void DemoNoise()
{
while (true) {
for(int y = 0; y < leds2_num_of_y; ++y)
for(int x = 0; x < leds2_num_of_x; ++x) {
foreground_color = CHSV(inoise8(ScaleXY(x), ScaleXY(y), ScaleZ()));
PutDotLeds2(x, y);
}
FastLED.show();
delay(loop_ms - (millis() - last_ms));
last_ms = millis();
}
}
int ScaleXY(int xy)
{
return xy * 256 / leds2_num_of_y; // scale(integer part) = num_of_y
}
int ScaleZ()
{
return last_ms * 256 / 1000; // period(integer part) = 1s
}
#6. 例
FastLED に添付のプログラミング例 (examples) のいくつかを、今回用に修正しました37。動作の様子が YouTube3839404142にあります。修正点は主に以下です。
- I2S 利用を宣言する
- ピン番号を 22 にする
- LED 数を 276 にする
- 色補正を 0xB080FF にする
#define FASTLED_ESP32_I2S true
#define LED_PIN 22
#define NUM_LEDS 276
const CRGB correction_default = 0xB080FF;
FastLED.setCorrection(correction_default);
#7. 置時計
置時計のデモを作成しました43。動作の様子が YouTube44にあります。以下の機能があります。
- 時・分・秒・曜日・年・月・日の表示(スクロール)
- 時・分・秒のアナログ風表示(彗星の尾)
- 文字盤背景色の変化
- デモ: 時刻表示中のデモ (DemoClock)
- デモ: カウントダウンタイマー (DemoTimeShock)
- デモ: ランプテスト表示 (DemoCyron)
- デモ: 色相環表示 (DemoHueRing)
- デモ: パーリンノイズ (DemoNoise)
時刻表示中のデモには以下があります。
- 文字色の切替(シアン・赤・緑・青)
- フォント表示(記号・数字、英大文字、英小文字)
- テキスト表示(英大文字、英小文字混在)
- エイリアン
動作中 M5Atom のボタンで以下を操作します。切替の状況はシリアルモニタで確認できます。
長押し時間 | 表示 | 効果 |
---|---|---|
- | Pale | カラーパレットを切替 |
>1 秒 | Next | デモを切替、【クロックデモ】デモ内容を切替(文字色、フォント) |
>2 秒 | Exit | デモを切替 |
>3 秒 | Temp | 色温度を切替 |
>4 秒 | Corr | 色補正を切替 |
>5 秒 | Scal | 【パーリンノイズデモ】スケール (x, y) を切替 |
>6 秒 | Peri | 【パーリンノイズデモ】周期 (z) を切替 |
>7 秒 | Null | ボタン操作を取消 |
カウントダウンタイマー動作中、M5Atom のボタンで以下を操作します。切替の状況はシリアルモニタで確認できます。
長押し時間 | 表示 | 状態 | 効果 |
---|---|---|---|
- | - | カウントダウン停止中 | カウントダウン開始 |
- | - | カウントダウン中 | クイズモードに移行、賞金額(千円)を表示 |
>1 秒 | Stop | - | カウントダウンを中止、初期値に戻す |
>2 秒 | Time | - | カウントダウンを中止、初期値を変更 |
>3 秒 | Pale | - | カラーパレットを変更 |
>4 秒 | Exit | - | 終了、デモを切替 |
>5 秒 | Null | - | ボタン操作を取消 |
###デモデータ
デモのデータを Excel で作成45しました。フォントやパターンは、Excel から Arduino IDE のコードに
コピー・ペーストしました。
- フォント作成 (Font Editor)
- パターン作成 (Pattern Editor)
- 彗星の尾のパラメータ決定 (tail of comet)
- 色の選択 (CRGB color)
RTC の追加
停電時の時刻保持 (RTC、バッテリバックアップ) をプリント基板4647の追加で実現できます。一時的に NTP による時刻取得ができない場合などに有効です。デモプログラムは、RTC がなくても動作します。RTC に関する記事を書きました4849。NTP が使えない環境では他の設定手段がなく、まだ十分ではありません。
課題
実用的には、以下の様な制限があります。
- 時刻合わせに WiFi接続、インターネット接続 (NTP) が必須
- サウンド機能がない
- アラーム機能(アラーム時刻設定)がない
また、以下のような機能強化や追加が考えられます。
- 12 時間表示、ゼロサプレス
- ストップウォッチ、カウント機能
- 振り子、星の瞬きなどのギミック
- 毎正時のからくり(演奏・演出)
- 温度・湿度・外気温・天気・電力など、センサー情報・外部情報の取得と表示
- 外部からのメッセージの指定と表示
8. おわりに
スイッチサイエンスさんで頒布50しています。予め丸型にした基板をBOOTH で頒布51しています。実用には、まだ完成度が低いですが、数多くのことを学ぶことができました。RGB と HSV の関係、カラーパレットの考え方、パーリンノイズなどの基本的な内容も初めて知りました。さらに FastLED ライブラリの使い方や可能性、M5Atom ライブラリのボタンの使い方まで、今後に生かしていきたいと思います。
-
DS1302 Rotating LED Display Alarm Electronic Clock Module DIY Kit Light Control LED Temperature Display For Arduino DS1302 ↩
-
Geekcreit® Fourth Generation DIY EC1838B DS1302 Light Control Rotation LED Electronic Clock Kit Music Alarm Clock With Housing - Blue Digital Tube + Blue & White LED ↩
-
DIY Multifunction Digital LED Electronic Temperature Clock Kits Alarm Time Suite ↩
-
Flyandace xo "Circular 80 LED clock version 3.1 DS1302 74hc595 atmega8 Juno AVR HD" ↩
-
Adafruit NeoPixel 1/4 60 Ring - 5050 RGBW LED w/ Integrated Drivers - Natural White - ~4500K(色温度違いあり) ↩
-
[examples]https://github.com/botanicfields/PCB-RGB-LED-276-M5Atom/tree/main/examples) ↩