TakSan0のアイコン画像

可変色懐中電灯

TakSan0 2021年02月25日に作成  (2021年02月28日に更新)

はじめに

本体を回転させることで好きな色が出せる懐中電灯です。
好きな色の光を手軽に出せる懐中電灯が欲しくて作りました。

まずは、完成形紹介動画をご覧ください。

ここに動画が表示されます

仕組み

中に組み込んでいる3軸加速度センサで、重力加速度が掛かっている(下向き)方向が測定できます。懐中電灯を回転することで、センサが何度回転しているかを 0~359°の範囲で測定します。

その回転角度に応じた色相を求めそれをRGBカラーLEDで出力する仕組みです。

使用部品

品名 型番 or SPEC 個数 参考価格 備考
マイコン基板 Adafruit Trinket M0 1 1,600
3軸加速度センサ Adafruit KP-ADXL343 1 1,540
フルカラーRGB LED Adafruit NeoPixel WS2812B 5φ砲丸タイプ 3 204
バッテリ リチウムイオンポリマー電池 110mAh 1 700
チップ抵抗 10KΩ 3 0.3 (1000個入り特価品)
M3ビス M3x6 2 120
ナット M2 2 60
ヘッダピン 2pin オス+メス 1 10
M2ビス M2x6 2 120
ナット M2 2 10
プッシュスイッチ MS-402 1 90
スライドスイッチ SS-12SDP2 1 75
コネクタセット PH2pin コネクタ+ポスト 1 30
コネクタセット EH3pin コネクタ+ポスト 1 25
懐中電灯 3LEDライト 1 110 DAISOで購入
基板固定具 不明 2 20
下敷き PET製※ 1 5 (A4サイズをカット) 基板固定用

参考価格で複数個入りの物は個数で割って必要数を乗算した価格です。

ハードウェア

100 円ショップで手に入る 3LED 懐中電灯を改造しました。
スイッチが外付けで追加している以外は、外見はほぼそのままです。

改造前の懐中電灯

構成部品

以下の部品で構成されます。
(1) 懐中電灯本体
(2) RGB LEDモジュール
(3) 基板固定用プラ板
(4) 加速度センサ
(5) マイコン基板
(6) バッテリ
(7) プッシュスイッチ

部品ごとに制作ポイントを書いていきます。

(1) 懐中電灯本体

元々の筐体は、下の写真の通りに分解し、使えるものは使って必要なところを交換・改造しました。下記①~⑩の通りです。

本体構成
① 本体ケース ⇒ 底面にスイッチ用の取り付け穴を開けて他はそのまま使用
② 頭部(LED実装部) ⇒ さらに分解して使ったので、次の写真で説明
③ 電池ボックス ⇒ 電池は使用しないので未使用
④ スイッチ一体型電池ボックスマイナス端子 ⇒ マイナス部分に伸びる金具を途中で切断し、スイッチ機構だけ流用
⑤ スイッチ裏押さえ ⇒ ④の先端部を絶縁するため、PET板を張り付け、そのまま使用
⑥ 頭部パッキン → そのまま使用

②の構成
⑦ レンズ付き頭部カバ ⇒ そのまま使用
⑧ LED実装基板 ⇒ もともとついていた白色 LED を外して他はそのまま使用
⑨ LED ガイドカバー ⇒ NeoPixelの方が少し大きくて入らなかったため、穴の径を少し拡大
⑩ プラス側電池接点: 電池が不要でしかも突起が邪魔になるので、スペーサーを取り去り、短いタッピングビスに交換

(2) RGB LEDモジュール

シリアル制御タイプの NeoPixel フルカラーRGB LEDです。
今回は元々ついていた白色 LED と交換するために、近い形である 5mmφの砲丸型を使用
しました。基板を調べると、白色LED のアノード同士、カソード同士が並列に接続されるようにパターンが引かれていました。
元の白色LED撤去

内側 アノードのパターンを NeoPixel の 電源+ 側、外側の カソードのパターンを電源 -側にそのまま流用することにしました。
アノードのパターンと真ん中の電池ボックスプラス側が当たる突起部分のパターンの間に電流制限用チップ抵抗が入っていましたが(写真の3時の位置位の黒っぽくなった部分)、使わないので外してしまいました。

NeoPixelは内側2本が丁度電源になっていますので、それを基板のホールに差し込み、
電源端子はホールに差し込む
データ線2本は表側(部品側)の根元で直角に折り曲げ、IN と OUT どおしを写真の様に数珠つなぎに繋ぎます。1つ目のIN端子 が届く場所のパターンが無いところに、線1本が通る程の穴を開けておきます。3つ目の OUT 端子はそのまま未接続(写真では足が折れ曲がっている方)にします。
データラインを数珠繋ぎに

電線を先ほどあけた穴から基板の表側から遠し(白線)、裏側のアノードパターンに + (赤線)を カソードパターンに - (黒線)を付けてコネクター (EH3pin ポスト) に接続します。 空中配線になるので、接続部分はしっかり熱収縮チューブで保護しています。
コネクタ取り付け

後は、頭部を元の形に組み立てたら完成です。突起部はなくなり、代わりにタッピングビスで固定しています。 後から思うとEHでは嵩張りすぎるので、もっと小型のコネクタにした方がよかったかと思います。
頭部完成

(3) 基板固定用プラ板

狭いところに押し込めるために薄く小さい板に固定することでスペースを稼ぎます。
プラ板(下敷き)を切って(25mmX50mm位) ボードやスペーサーの位置に取り付け穴を開けました。
ボディーの内側に丁度挟まって固定されるくらいのサイズにしました。

透明プラ板なので見辛いですが

(4) 加速度センサ

i2c 制御タイプの 3軸加速度センサーを使用します。
i2c バスなので、プルアップしておく必要がありますが、スペースもあまりないので、センサー側に直付けしました。
加速度センサにプルアップ抵抗実装

(5) マイコン基板

超小型で且つ、LiPo で使用しやすかったという理由と余っていたからという理由で、 Trinket M0 基板を使いました。
結構マイナーなものかと思いますが、Arduino IDE 対応で小型で3.3Vでセンサとi2cで接続できるものなら何でもいいかと思います。
今度 Seeed の XIAO あたりを試してみたいと思っています。

CPU側に取り付けるのは基本的に配線のみにしていますが、唯一ボード側にスイッチ用プルアップ抵抗を接続します。(写真のセンサー出ている赤色の 3V電源線から緑色のスイッチ端子への配線の間の)表面実装チップ抵抗とポリウレタン線を使用しました。

配線も直接つけるものは、電源用のスイッチと加速度センサーだけにして、プッシュスイッチ、LED頭部、LiPoバッテリーは、コネクタを介して接続することで切り離しが出来るようにしています。そうしないとプラ板を引っ張りだせませんし、出し入れしていると、よくもげます。(この辺はかなり試行錯誤しました。)
CPU基板側の実装

(6) バッテリ

Trinket は LiPo を使用するための端子と、レギュレータが用意されいる点、本来 5V が必要な NeoPixel を十分駆動できる電圧なので、LiPoバッテリを使うことにしました。
あまり容量の大きいものは入らないので、 手持ちで一番小さい 110mAh の物を使いました。
電池なくなるのが早いので、もう一ランク大きい方がよさそうですが、今度他の容量のものも試してみたいと思っています。

(7) プッシュスイッチ

5mmφの穴を懐中電灯の底面に開け、取り付けました。
キャプションを入力できます
2pin ヘッダピンのオスメスセットを中継させ(QIコネクタのオスメスだと長く長すぎるので邪魔になるため)、コネクタで取り外しができるようにしました。空中配線になるのでしっかり収縮チューブ被せました。

キャプションを入力できます

接続

名称 バッテリ TrinketM0 加速度センサ NeoPixel スイッチ 備考
BAT+ + Bat(VBAT) X 2(VDD) X
GND - Gnd(GND) 4(GND) 3(GND) X
VCC X 3V(3V3) 1(+V) X X
LED DI X 1~(IO3) X 1(Din) SW Gnd側
SW_IN X 3(IO8) X X SW 信号側 10KΩでVCCにプルアップ
SDA X 0(SDA) 2(SDA_SDI) X X
SCL X 12(SCL) 3(SCL_SCLK) X X

組み立て

マイコン基板周りから、土台となるプラ板に取り付けていきます。
マイコン周りの接続

裏側にはLiPoバッテリーを両面テープで張り付けました。配線が長く色々と引っかかるので、テープで仮止めしています。
裏側にバッテリ

頭部と接続するとこうなります。ソフトの開発はこの状態で行っていました。電池のマイナス端子用の金属版はまだ切っていない状態の写真です。

PH2pin コネクタとスライドSWが付いているのは、充電用です。
充電のたびに外すのが面倒だったために取り付けました。
嵩張って入らなくなるので最後に外すつもりでしたが、狭い筐体にも関わらず収まってしまったのでそのままです。
頭部との接続

短く切った電池のマイナス端子用の金属版の一部はマイコンのグランドと接続していますが、切っても残った部分が何かの端子に接触する可能性があるので、スイッチの裏抑え部品に、両面テープでプラ板を貼ってガードします。
これも透明なプラ板なのでみにくいですがガード

最後に組み立てたものを、線がもげないように慎重に押し込みます。

慎重に押し込む

頭部との接続コネクターが引っかからない位置に押し込んで、慎重にふたを閉めて完成です。

接続図

接続図

ソフトウェア

以下、ざっくりと解説しますが、詳しくはソースを参照してください。

開発環境

Arduino IDE
ボードマネージャーで "Adafruit SAMD Boards - Trinket M0" を選択
ライブラリマネージャーで以下をインストール

  • Adafruit_Sensor
  • Adafruit_ADXL343
  • Adafruit_NeoPixel

処理の流れ

マルチタスクやリアルタイム性などを求めない為、loop() 関数内で(1) ~ (5) を順番に実行して繰り返しているだけです。

(1) センサ値取得
(2) 角度計算
(3) 色演算
(4) LED制御
(5) キー入力判定

(1) センサ値取得

加速度の精度はあまり追求していません。わずかな傾きの差で色が変わっても人間の目にはあまり違いが認識できないからです。
ですので、ADXL343 のライブラリには様々な機能が盛り込まれていますが、単純な使い方しかしていません。

3軸分の加速度レジスタを読み出して、重力加速度 G に変換する係数をかけているだけです。

センサー値としては、静止している状態で、重力加速度 G = 9.8 が、各 X 軸,Y軸 , Z軸 の3方向成分に分解された値が入ることになります。

(2) 角度計算

本体を横向きにしたときに回転角度を求めるためには、今回の取り付け方向では、x軸と z軸の加速度値を元に atan2 (アークタンジェント2) 使用して計算しました。
三角関数について解説すると長くなりすぎるので割愛します。

y軸は上向きまたは下向きを検出するために使用しています。この計算も三角関数系の解説が必要になるので割愛します。

(3) 色演算

色相とは、光の3原色のうち赤を 0°の位置、緑を120°の位置、青を240°の位置になるように配置したものです。この3原色に角度が近づくほどその色の成分が高くなり離れる程低くなるように配色されています。
ここで色相を詳しく説明してもあまり意味はないので割愛します(こればっか)。

ただし、R G B いずれか2つの角度の遠近で値を決めることになるので、 3色全てを使う灰色や白系の色は出ません。
そのため、上向きに45度以上傾くと白になるように、下向きに45以上傾くと消灯になるようにしました。上向きまたは下向きにすると、 Z軸の値が大きくなるというのを利用しました。

厳密にフルカラーを謳うには3軸ともちゃんと回転演算して、輝度成分を入れて YUV の様にした方がよさそうです。
ソフトでどうにかなるレベルではありますが大変そうなので今回は断念しました。

(4) LED制御

NeoPixel ライブラリを使うと簡単に R,G,B の値を255 諧調で個別に設定できます。
余り大きい値を指定すると電池の消耗も激しく明るすぎるので、明るさを制限しています。
明るくしたい場合は MAX_BRT の定義値を大きくすることで調整できるようにしています。(これ以上明るくは試していません。)

(5) キー入力判定

組み込み業界ではよくあるスイッチのチャタリング防止処理をしています。
あと、エッジ検出処理 (OFF-> ON に変わったときのみ反応するように) もしています。
OFF-> ON した時(ボタン押下時) 色固定の ON/OFF を切り替えています。

その他、デバッグ情報をシリアル出力しているので、Arduino IDE の "シリアルプロッタ” 機能でグラフを表示してデバッグします。

キャプションを入力できます

ソース

AnyColorToach.ino

// [[[ インクルード ]]] #include <Arduino.h> #include <Adafruit_NeoPixel.h> #include <Adafruit_ADXL343.h> // [[[ マクロ定義 ]]] #define ADXL_343_DEVICE_SENSOR_ID 1234 #define ON true #define OFF false #define KEY_INPUT_PIN 3 // ボタン制御ピン #define ONBOARD_LED_PIN LED_BUILTIN // オンボードLED制御ピン #define NEOPIXEL_PIN 1 // NeoPixel LED制御ピン #define NUM_PIXELES 3 // NeoPixel 使用個数 // < ポジション設定 > #define PIXEL_NUM_INIT 0 // 起動ステップ // < 色設定マクロ > #define MAX_BRT 0x40 // 輝度最大値 #define MIN_BRT 0x00 // 輝度最小値 #define KEY_CHATTER_MASK 0x1Fu // [[[ 型定義 ]]] // [[[ 変数 ]]] uint16_t key_data = 0; uint8_t key_now = ON; uint8_t key_before = OFF; bool color_locked = false; Adafruit_ADXL343 accelSensor(ADXL_343_DEVICE_SENSOR_ID); Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUM_PIXELES, NEOPIXEL_PIN, NEO_RGB + NEO_KHZ800); // [[[ 巻子プロトタイプ ]]] void setup(); void loop(); // <<< 初期化関数(Arduino準拠関数) >>> void setup() { // put your setup code here, to run once: Serial.begin(115200); // センサーの初期化 accelSensor.begin(); // 端子設定 pinMode(KEY_INPUT_PIN, INPUT); // 色固定、解除状態にする color_locked = false; pinMode(ONBOARD_LED_PIN, OUTPUT); digitalWrite(ONBOARD_LED_PIN, LOW); // レンジ設定 (4G) accelSensor.setRange( ADXL343_RANGE_4_G ); // 起動時は消灯 -> R -> G -> B の順に1LEDずつ点灯させる。 pixels.begin(); pixels.setPixelColor(0, pixels.Color(MIN_BRT, MIN_BRT, MIN_BRT)); pixels.setPixelColor(1, pixels.Color(MIN_BRT, MIN_BRT, MIN_BRT)); pixels.setPixelColor(2, pixels.Color(MIN_BRT, MIN_BRT, MIN_BRT)); pixels.show(); delay(500); pixels.setPixelColor(0, pixels.Color(MAX_BRT, MIN_BRT, MIN_BRT)); pixels.setPixelColor(1, pixels.Color(MIN_BRT, MIN_BRT, MIN_BRT)); pixels.setPixelColor(2, pixels.Color(MIN_BRT, MIN_BRT, MIN_BRT)); pixels.show(); delay(500); pixels.setPixelColor(0, pixels.Color(MIN_BRT, MIN_BRT, MIN_BRT)); pixels.setPixelColor(1, pixels.Color(MIN_BRT, MAX_BRT, MIN_BRT)); pixels.setPixelColor(2, pixels.Color(MIN_BRT, MIN_BRT, MIN_BRT)); pixels.show(); delay(500); pixels.setPixelColor(0, pixels.Color(MIN_BRT, MIN_BRT, MIN_BRT)); pixels.setPixelColor(1, pixels.Color(MIN_BRT, MIN_BRT, MIN_BRT)); pixels.setPixelColor(2, pixels.Color(MIN_BRT, MIN_BRT, MAX_BRT)); pixels.show(); delay(500); } // <<< メイン処理関数(Arduino準拠関数) >>> void loop() { static uint8_t key_before = 0xff; static uint8_t r=0, g=0, b=0; uint8_t key_now; int i; float cur_ang; float accel_data[3]; // ■(1) センサ値取得 accel_data[0] = (float)accelSensor.getX() * ADXL343_MG2G_MULTIPLIER * SENSORS_GRAVITY_STANDARD; accel_data[1] = (float)accelSensor.getY() * ADXL343_MG2G_MULTIPLIER * SENSORS_GRAVITY_STANDARD; accel_data[2] = (float)accelSensor.getZ() * ADXL343_MG2G_MULTIPLIER * SENSORS_GRAVITY_STANDARD; // センサ値をデバッグシリアルに出力(Arduinoシリアルプロット機能で波形表示する為) for (i = 0; i<3; i++) { Serial.print(accel_data[i]*10.0f); // X10 倍して出力(角度などに比べると、レンジが小さくてがグラフが見にくくなるので) Serial.print(" "); } // ■(2) 角度計算 // 傾きを求める (0-359°) cur_ang = atan2( -1.0*accel_data[0], accel_data[2])*180.0/3.1415926; // <傾斜核を 0~360範囲に丸め込む> if (cur_ang < 0) cur_ang+=360.0; if (cur_ang >= 360.0) cur_ang-=360.0; // 角度値をデバッグシリアルに出力(Arduinoシリアルプロット機能で波形表示する為) Serial.print(cur_ang); Serial.print(" "); // ■(3) 色演算 if ( false == color_locked) { // 上向けたとき白 (Y軸が45°以上) if (accel_data[1] > (SENSORS_GRAVITY_EARTH / 1.414f)) { r = MAX_BRT; g = MAX_BRT; b = MAX_BRT; } // 下向けたとき黒 (Y軸が45°以下) else if (accel_data[1] < (-1.0 * SENSORS_GRAVITY_EARTH / 1.414f)) { r = MIN_BRT; g = MIN_BRT; b = MIN_BRT; } else { // 傾きを色相として、RGBを求める if (cur_ang < 60.0f) { r = MAX_BRT; g = (uint8_t) ( ( (cur_ang - 0.0f ) / 60.0f ) * (float)( MAX_BRT - MIN_BRT ) + (float)MIN_BRT ); b = MIN_BRT; } else if (cur_ang < 120.0f) { r = (uint8_t) ( ( (120.0f - cur_ang ) / 60.0f ) * (float)( MAX_BRT - MIN_BRT ) + (float)MIN_BRT ); g = MAX_BRT; b = MIN_BRT; } else if (cur_ang < 180.0f) { r = MIN_BRT; g = MAX_BRT; b = (uint8_t) ( ( (cur_ang - 120.0f ) / 60.0f ) * (float)( MAX_BRT - MIN_BRT ) + (float)MIN_BRT ); } else if (cur_ang < 240.0f) { r = MIN_BRT; g = (uint8_t) ( ( (240.0f - cur_ang ) / 60.0f ) * (float)( MAX_BRT - MIN_BRT ) + (float)MIN_BRT ); b = MAX_BRT; } else if (cur_ang < 300.0f) { r = (uint8_t) ( ( (cur_ang - 240.0f ) / 60.0f ) * (float)( MAX_BRT - MIN_BRT ) + (float)MIN_BRT ); g = MIN_BRT; b = MAX_BRT; } else { r = MAX_BRT; g = MIN_BRT; b = (uint8_t) ( ( (360.0f - cur_ang ) / 60.0f ) * (float)( MAX_BRT - MIN_BRT ) + (float)MIN_BRT );; } } } // ■(4) LED制御 // R G B 値を3つの lED に反映 for (i = 0; i<NUM_PIXELES; i++) pixels.setPixelColor(i, pixels.Color(r, g, b)); pixels.show(); // RGB値をデバッグシリアルに出力(Arduinoシリアルプロット機能で波形表示する為) Serial.print(r); Serial.print(" "); Serial.print(g); Serial.print(" "); Serial.print(b); Serial.print(" "); // ■(5) キー入力判定 // キーチャタリング防止処理(毎回取り込み値をシフトしていき数ビット分一致したら決定させる) key_data <<= 1; if (digitalRead(KEY_INPUT_PIN) == 0) key_data |= 1; if ( KEY_CHATTER_MASK == (key_data & KEY_CHATTER_MASK ) ) key_now = ON; else if ( 0 == (key_data & KEY_CHATTER_MASK ) ) key_now = OFF; else ; // 何もしない // キー変化エッジ検出(変化があったときのみ処理する on->off 又は off->on ) if ( key_now != key_before) { // ON エッジの場合( off-> on の場合) if (key_now == ON) { // 色固定状態を反転する color_locked = (color_locked)? false: true; digitalWrite(ONBOARD_LED_PIN, ((color_locked)? HIGH: LOW)); } } // 前回値として覚えておく key_before = key_now; // 色固定状態をデバッグシリアルに出力(Arduinoシリアルプロット機能で波形表示する為) Serial.print(color_locked * 100); //(角度などに比べると、1/0 ではレンジが小さくてがグラフが見にくくなるので) Serial.println(" "); // 気持ち待つ (10msec) delay(10); }

用途

  • フィギュアやジオラマ、プラモデルのライトアップ
  • カラーセンサーのデバッグに
  • RGB三原色の説明をするのに

    制作者自体が何のために作ったのか余りわかっていない←これが一番問題 (゚Д゚;)

最後に

100円ショップの物って、何かもう一つ足りなくて、何かを組み入れたくなっちゃうんですよね…というノリで作ってみました。
こんな簡単なものなのに、作ってみると試行錯誤で結構苦労しました。

落ち

投稿原稿が完成した直後に壊れました orz
写真を撮るために分解を繰り返すうちに、もげた線が当たって電源が破損したようです。
USBケーブルでは電源がはいるので、USB VBAT と LiPo を切り分けるためのレギュレータ手前のショットキーバリアが壊れたみたいというところまでわかったのですが...
ショットキーバリアを交換するか、XIAO 等で作り直すか、落ち着いたら悩もうと思います。

TakSan0のアイコン画像
大阪在住の本業組込み系SEです。ハードに近いところのソフト屋さんです。 電子工作は趣味でやっています。 よろしくお願いします。 ▼こちらにも別の作品を投稿しています。参考まで。 https://protopedia.net/prototyper/taksan
  • TakSan0 さんが 2021/02/25 に 編集 をしました。 (メッセージ: 初版)
  • TakSan0 さんが 2021/02/28 に 編集 をしました。 (メッセージ: 接続図を追加)
ログインしてコメントを投稿する