シャポログ

18650 2セル駆動の LEDランタンを作った

眩しすぎるX看板のLEDランタンを作った」で買った白色LED が 50個以上余ったのでちゃんとした(?) LEDランタンを作ってみました。

動画

仕様

回路

電流制限抵抗を介して直接 LED を駆動することもできますが、電源の状態に関わらず明るさを一定に保つことと、電流制限抵抗の数を抑えたかったので、NJM2360AD を使って DC/DC を構成して昇圧し、直列接続した LED を 20mA で駆動します。

調光はボリュームの値を ATtiny85 の ADC で読み取り、PWM で LED の明るさを制御します。電圧は RESETピンの ADC で読み取るので、ボリュームを下げてもリセットがかからないよう電圧範囲が 2.5~5V の間になるようにしています。人間の目の明るさの感じ方は光量に対してリニアではなく対数的なので、ボリュームには Aカーブのものを使用します。

バッテリーの電圧を ADC で読み取り、残量を計算して 4個の LED にレベル表示します。ダイナミック点灯により 3つのポートで 4つの LED を制御します。

バッテリーは直列で使用するので、AliExpress で買った 2セル用の保護モジュール で保護します。

USB-PD のトリガにはスイッチサイエンスで買った べーたさんUSB-PD_Adapter PDA-02S を使用しました。

プログラム

Arduino ATtiny Core を使用しました。

調光

デフォルトでは PWM周波数が低すぎてカメラで撮るとカメラのフレームレートと干渉してしまうため、TCCR1レジスタを変更して Timer1 のプリスケーラを無効化しています。

volume_to_pwm関数でボリュームの状態を読み取り、PWM に反映します。光量に対する明るさの感じ方は Aカーブのボリュームを使用することで考慮済みなので、ADC値をリニアに PWM の Duty比に反映します。バッテリー状態を考慮し、電池切れが近い場合は PWM値を 1/4に、電池切れ状態の場合は 0 にします。

バッテリー監視・レベル表示

battery_to_level関数でバッテリー状態を読み取ります。電流によって入力ダイオード 11EQS04 の電圧降下が変わってしまうため、これを何となく考慮して生のバッテリー電圧を推定します。

読み取った電圧からバッテリー状態を 5段階のレベル値にします。ノイズ対策のため ±100mV のヒステリシスを持たせています。

電池切れが近くなるとレベル表示を 1秒周期で点滅させます。電池切れ状態になると 0.5秒周期で点滅させます。

drive_level_meter関数でダイナミック点灯により電圧レベルを LED に表示します。

USB接続時は VBUS の電圧が ADC に入ってくるのでレベル表示はフルになります。

#include <stdint.h>

static const int ADC_VOLUME = 0;
static const int ADC_BATTERY = 3;
static const int PIN_LED_COM = 1;
static const int PIN_LED_P0 = 0;
static const int PIN_LED_P1 = 2;
static const int PIN_LED_LAMP = 4;

uint16_t volume_adc_offset = 512;
uint16_t pwm_value = 0;

uint8_t battery_level = 4;

uint8_t meter_drive_index = 0;
uint8_t meter_state = 0;

void volume_to_pwm();
void battery_to_level();
void drive_level_meter();

void setup() {
    // デフォルトではPWM周波数が低すぎてカメラで撮ると点滅するため
    // PWM (Timer1) のプリスケーラを無しにする
    // 8MHz / 256 = 31.25kHz
    uint8_t tmp = TCCR1;
    tmp &= 0xf0;
    tmp |= 0x01; // CS[3:0]=0b0001
    TCCR1 = tmp;

    // RESET端子をボリューム入力として使用するため、
    // ボリューム入力は VCC/2 でオフセットしている
    // スイッチ付きボリュームのスイッチが ON になったときの ADC値を
    // ボリューム入力の最小値として使用するため保持しておく
    volume_adc_offset = analogRead(ADC_VOLUME);
}

void loop() {
    // ボリュームの状態を明るさに反映
    volume_to_pwm();

    // バッテリー状態の読み取り
    battery_to_level();

    // バッテリーレベルメータのダイナミック点灯制御
    drive_level_meter();
    delay(1);
}

// ボリュームの状態を明るさに反映
void volume_to_pwm() {
    if (battery_level == 0) {
        // 電池切れ --> 消灯
        pwm_value = 0;
    }
    else {
        // ボリュームの値読み取り
        int16_t adc = analogRead(ADC_VOLUME);

        // 電源投入時の値を最小とする
        if (adc < volume_adc_offset) adc = volume_adc_offset;

        // PWM値に変換
        // 最小値 (電源投入時の値) を 1、電源電圧を 255 として換算
        pwm_value = 1 + ((int32_t)adc - volume_adc_offset) * 254 / (1023 - volume_adc_offset);

        if (battery_level == 1) {
            // 間もなく電池切れ --> 明るさ制限
            pwm_value /= 4;
        }

        // 明るさゼロにならないようにする
        if (pwm_value < 1) pwm_value = 1;
    }

    // PWM に反映
    analogWrite(PIN_LED_LAMP, pwm_value);
}

// バッテリー状態の読み取り
void battery_to_level() {
    int16_t adc = analogRead(ADC_BATTERY);

    // 抵抗分圧前の電圧を算出
    int16_t battery_mv = (int32_t)adc * 10000 / 1023;

    // 入力ダイオード(11EQS04)の順電圧降下を加味する (300-450mV)
    int16_t diode_vf_mv = 300 + (int32_t)pwm_value * 150 / 255;
    battery_mv += diode_vf_mv;

    // レベル閾値
    static const int16_t thresh[] = {
        0, 6000, 6200, 6800, 7400, 8000, 9999
    };
    int16_t lower_thresh = thresh[battery_level];
    int16_t upper_thresh = thresh[battery_level + 1];

    if (battery_level == 0) {
        // 電池切れ状態はそのまま保持
        // (一度電源を切るまで復帰しない)
    }
    else {
        // レベル遷移 (100mV のヒステリシス付き)
        if (battery_mv < lower_thresh - 100) {
            // レベルダウン
            if (battery_level > 0) battery_level--;
        }
        else if (battery_mv > upper_thresh + 100) {
            // レベルアップ
            if (battery_level < 5) battery_level++;
        }
    }

    // レベルメーターの点灯状態に反映
    switch(battery_level) {
    case 0:
        // 電池切れ --> 速い点滅
        meter_state = (millis() % 500 < 250) ? 0x1 : 0x0;
        break;
    case 1:
        // 間もなく電池切れ --> 遅い点滅
        meter_state = (millis() % 1000 < 500) ? 0x1 : 0x0;
        break;
    case 2:
        meter_state = 0x1;
        break;
    case 3:
        meter_state = 0x3;
        break;
    case 4:
        meter_state = 0x7;
        break;
    default:
        meter_state = 0xf;
        break;
    }
}

// バッテリーレベルメータのダイナミック点灯制御
void drive_level_meter() {
    pinMode(PIN_LED_COM, INPUT);
    pinMode(PIN_LED_P0, INPUT);
    pinMode(PIN_LED_P1, INPUT);
    switch (meter_drive_index) {
    case 0:
        digitalWrite(PIN_LED_COM, 1);
        digitalWrite(PIN_LED_P0, (meter_state & 1) ^ 1);
        digitalWrite(PIN_LED_P1, 1);
        break;
    case 1:
        digitalWrite(PIN_LED_COM, 0);
        digitalWrite(PIN_LED_P0, (meter_state >> 1) & 1);
        digitalWrite(PIN_LED_P1, 0);
        break;
    case 2:
        digitalWrite(PIN_LED_COM, 1);
        digitalWrite(PIN_LED_P0, 1);
        digitalWrite(PIN_LED_P1, ((meter_state >> 2) & 1) ^ 1);
        break;
    case 3:
        digitalWrite(PIN_LED_COM, 0);
        digitalWrite(PIN_LED_P0, 0);
        digitalWrite(PIN_LED_P1, (meter_state >> 3) & 1);
        break;
    }
    pinMode(PIN_LED_COM, OUTPUT);
    pinMode(PIN_LED_P0, OUTPUT);
    pinMode(PIN_LED_P1, OUTPUT);
    meter_drive_index = (meter_drive_index + 1) & 3;
}

筐体

Fusion 360 で設計し、FlashForge Adventurer3 でプリントしました。

簡単に電池を交換できるよう、リモコンなどの電池の蓋を参考にして開けられるようにしました。

フロントパネルはスナップフィットになっており、簡単に開けてメンテナンスできるようにしました。

組み上げ・完成

動作時間

3500mAh のバッテリー 2本で、明るさ最大時で 5時間程度の動作時間と推定しました。

実際に試したところ、バッテリ切れ判定で消灯するまで 5.5時間ほど点灯し続けたので、概ねスペック通りになりました。

本記事執筆時点でボリューム 50%時の動作時間を確認中ですが、20時間くらいは点灯し続けそうです。

関連リンク