【注意・免責事項】
・リチウムイオン電池は、誤った使用や取り扱いにより発熱・発火・破裂などの危険が生じる可能性があります。
・本記事の内容は参考情報であり、正確性・安全性・適合性を保証するものではありません。
・本記事により発生した損害・事故・火災などについて一切の責任を負いません。
・全ての分解・制作・実験はご自身の判断と責任において実施してください。
はじめに
使い捨てタバコ(通称:シーシャ、ベイプ、など)には、繰り返し充電可能なリチウムイオン電池が入っています。そのまま捨ててしまうのはもったいないと思いUSBから充電できるようにしてみました。リチウムイオン電池は、一般的に300~500回くらい繰り返し充電可能で、3.7V(実際は4.2〜3.0V程度)なので3.3V系のマイコンなどの電子工作の電源としてちょうどよさそうです。また、実際どのくらい使えるのか確認する為、放電時の電流を計測しバッテリーの劣化度合も調べてみました。
まずは分解
3種類の電子タバコを分解してリチウムイオン電池を取り出してみました。内部の基本的な作りはだいたい同じで主に下記の部品で構成されています。
- リチウムイオン電池
- 吸気センサ(正式な名称は??)
- 電熱線
- リキッド(香料・グリセリン等)を染み込ませた綿
吸うと吸気センサがONになり、電熱線が熱せられリキッドが蒸発して煙になる仕組みのようです。リチウムイオン電池の容量は、さまざまで吸気回数が多いタイプの方が容量が大きいリチウムイオン電池が入っていました。今回使った電子タバコは、公称値で 280mAh, 550mAh, 900mAh でした。電圧はどれも3.7Vの記載がありました。吸気センサは口で制御するデバイスなどにも利用できるかも。
タイプ①:280mAh
タイプ②:550mAh
タイプ③:900mAh
USB充電バッテリーの作成
TP4056モジュールの改修
リチウムイオン電池はそのまま充電できません。電圧・電流・温度などの管理が必須です。充電制御用によく使われるICがTP4056ですが、モジュール基板として販売されているモノは主に18650電池用(1000mAh~)が多いようです。このモジュール基板をそのまま電子タバコ用のリチウムイオン電池に接続してしまうと大電流が供給されてしまい非常に危険です。TP4056のデータシートを見ると抵抗:によって、供給する電流を制限できるようになっています。この抵抗を、充電したいリチウムイオン電池の容量に合わせて変更する必要があります。
ちなみに、TP4056モジュールは数100円以下で購入可能です。この記事で使用した基板はAliExpressで10個パック700円で購入しました。
TP4056モジュールにはバリエーションがいろいろあるようで抵抗:の位置が上記の写真とは異なる場合がありますので注意してください。 TP4056のPin2(PROG)とPin3(GND)に繋がっている抵抗を探してください。また、過放電・過電流保護IC付きのものと、単なる充電ICのみのものがあります。安全のため保護回路付きの使用を推奨します。
の抵抗値の決め方
充放電制御にはCレートという重要な考え方があります。Cレートとは充放電の速さの事です。一定の電流で充放電した場合、電池の容量を1時間で完全充電または放電させる電流の速さを1Cと定義しています。例えば、1000mAhの電池を1Cの速さでフル充電させる場合、1000mAで1時間かかります。0.5Cだと500mAで2時間、0.1Cだと100mAで10時間といった感じです。リチウムイオン電池は速く充放電する(つまりCレートが大きい)と早く劣化が進みます。但し、電流を制限するので充電時間は長くなってしまいます。様々な条件にもよりますが、一般的に0.3~0.5Cくらいで充電するとリチウムイオン電池の劣化を抑えられるようです。
今回は約0.5Cくらいになるようにしたいと思います。①: 280mAh の場合だと、140mA(280mAh × 0.5) 程度に電流を制限すればよさそうです。TP4056のデータシートの設定テーブルを参照すると約9kΩくらいのようですが、そんな抵抗は持っていないので10kΩにしました。10kΩとすると 電流は130mAに制限されるので、Cレートは、130mA ÷ 280mAh = 0.46C になります。 ②③も同様に下表の様にを決めました。③は手持ちで適当な抵抗がなかったのでCレートはちょっと大きめ。
| Battery | 抵抗値( | 最大電流() | Cレート | 想定充電時間 |
|---|---|---|---|---|
| ①: 280mAh | 10 kΩ | 130 mA | 0.46 | 2.15 h |
| ②: 550mAh | 5.1 kΩ | 約240 mA | 0.44 | 2.29 h |
| ③: 900mAh | 2.0 kΩ | 580 mA | 0.64 | 1.5 h |
チップ抵抗を取り外して普通の1/6Wカーボン抵抗を無理やり取り付けた様子。同じサイズのチップ抵抗にした方がよいかも。
TP4056とリチウムイオン電池の接続
下記のように接続します。±だけは間違えないように!!
USBコネクタは充電のみに使い、電流の取り出しはout端子から行います。コネクタはUSB-CですがPD充電は対応していませんので5Vで充電されます。
TP4056モジュールのLEDは充電中は赤、充電完了すると青に点灯します。
ケースの制作
裸のままだと扱いにくいので3Dプリンタでケースを作成しました。ケースに収めると見た目もいい感じです。リチウムイオン電池の形状が3種3様で、ぴったりはまるサイズにするのが結構大変でした。
実際に使ってみた
CH32V003マイコンにつなげてLチカをしてみました。③900mAhのリチウムイオン電池で試しましたが4日間くらいはLEDがチカチカし続けました。
電流測定器の作成
とりあえずLチカはできる事は確認できましたが、後どのくらい使えるのか、どのくらい劣化が進んでいるのか気になったので実際の充電容量・劣化度合を簡易的に測定してみました。
測定方法
フル充電した状態から負荷抵抗に一定の電流を流し続け、電圧が低下するまでどれだけ電流が流れたかを計測します。単位時間に流れた電流量を加算してトータルの放電容量を求めます。そして、 実際のトータル放電容量 ÷ 電池容量の公称値 × 100で バッテリー健康度 (SoH:State of Health) を求めたいと思います。
比較しやすいように、電池容量によって負荷抵抗を変え、どのバッテリーとも約0.3C になるようにしました。もし、電池の劣化が全く無く公称値通りの容量があれば約3時間くらいは電流を供給し続けるはずです。
| Battery | 負荷抵抗値 | 電流(@3.7V) | 放電Cレート | 理想放電時間 |
|---|---|---|---|---|
| ①: 280mAh | 44 Ω | 84.1 mA | 0.30 | 3.33 h |
| ②: 550mAh | 12 Ω | 168.2 mA | 0.31 | 3.27 h |
| ③: 900mAh | 12 Ω | 308.3 mA | 0.30 | 2.92 h |
電流計測はINA231モジュールで行い放電容量の計算や測定データの記録はESP32で処理し、WEBサーバー経由で電圧・電流・放電容量のグラフを見れるようにしました。
放電している間、ずっとデスクに置いておくのは怖いので、爆発してもいい場所(そんな所あるか?)に設置できるようにESP32を使って遠隔で測定できるようにしました。WiFiが届く場所ならどこでも測定できます。
回路図
一定の電流が流れるように負荷抵抗としてセメント抵抗を使います。INA231の電圧測定端子は負荷抵抗と並列に接続し、シャント抵抗(電流測定端子)を負荷抵抗と直列に接続するようにします。シャント抵抗の位置は電流が流れる回路上であればどこでも測定はできますが、GNDが浮かないようにハイサイド(電源と負荷の間にシャント抵抗を挟む)にするのが一般的なようです。INA231とESP32(DEVKIT VI DOIT)はI2Cで接続し通信します。測定側の電圧・電流に影響しないようにINA231とESP32の電源は、ESP32のUSB端子から供給するようにしています。
ソフトウェア
開発環境はArduino IDE 2.3.9を使いました。
INA231の制御はI2CでRead/Writeするだけなので簡単ですが電流測定値の係数の考え方がちょっと面倒です。シャント抵抗値と計測単位・測定レンジに合わせて設定する必要があります。互換があるINA226ライブラリはあったので利用した方が簡単かもしれません。
電圧・電流は1秒毎に測定し、1分単位で平均値を計算してグラフにしています。測定データは240分保持できるようにしてますが、RAMを節約する為、16bit固定小数点にして配列に格納するようにしてます。
index_html.h はSPIFFSに書き込みたかったのですがArduino IDE 2.x 系では対応してないみたいですね。やり方はありそうでしたが、面倒なので普通にFlashROMに置くようにしています。
battery_capacity_meter.ino
// Arduino IDE setting bord: "DOIT ESP32 DEVKIT V1"
#include <WiFi.h>
#include <WebServer.h>
#include "index_html.h"
#include "ina231.h"
#include "led_status.h"
#define MAX_DATA_NUM (60 * 4)
uint16_t dat_time[MAX_DATA_NUM] = {0}; // u16.0 [min]
uint16_t dat_volt[MAX_DATA_NUM] = {0}; // u12.4 [mv]
uint16_t dat_curt[MAX_DATA_NUM] = {0}; // u12.4 [mA]
uint16_t dat_capa[MAX_DATA_NUM] = {0}; // u12.4 [mAh]
uint32_t dat_index = 0;
uint32_t ave_num = 0;
float ave_volt = 0.0;
float ave_curt = 0.0;
float total_capa = 0.0;
uint32_t pre_measur_time = 0;
WebServer server(80);
void server_init() {
WiFi.begin(WIFI_SSID, WIFI_PASSWD); // ★接続したい環境に合わせてSSID,PASSWDを設定する
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(WiFi.localIP());
server.on("/", []() {
server.send_P(200, "text/html", INDEX_HTML);
});
server.on("/data", []() {
server.send(200, "application/json",
set_json_data(dat_index + 1, dat_time, dat_volt, dat_curt, dat_capa));
});
server.begin();
}
float calc_ave(float val_ave, uint32_t n, float val_new) {
if (n < 1) {
return val_new;
}
return (val_ave * n + val_new) / (n + 1);
}
void measurement(void) {
uint32_t measur_time = millis(); // 測定時刻取得
float now_volt = ina231_get_voltage(); // 電圧取得
float now_curt = ina231_get_current(); // 電流取得
float dt = measur_time - pre_measur_time; // 前回測定からの間隔
total_capa += now_curt * dt / (60 * 60 * 1000); // 単位時間に流れた電流
pre_measur_time = measur_time;
Serial.printf("[%3d:%02d]: %.2f[mv] %.2f[mA] %.1f[mAh]\n", dat_index, ave_num, now_volt, now_curt, total_capa);
led_change_status((3000 > now_volt) ? LED_STATUS_VOLT_DROP : LED_STATUS_MEASURING); // 電圧が低下したら通知
// 平均値計算
ave_volt = calc_ave(ave_volt, ave_num, now_volt);
ave_curt = calc_ave(ave_curt, ave_num, now_curt);
// 最新データは毎回更新
dat_time[dat_index] = (uint16_t)(measur_time / (1000 * 60)); // だいだい分
dat_volt[dat_index] = (uint16_t)(ave_volt * 16); // u12.4
dat_curt[dat_index] = (uint16_t)(ave_curt * 16); // u12.4
dat_capa[dat_index] = (uint16_t)(total_capa * 16); // u12.4
// 1分毎のグラフデータ更新
if (++ave_num >= 60) {
ave_num = 0;
if (++dat_index >= MAX_DATA_NUM) { // バッファがいっぱい
dat_index = MAX_DATA_NUM - 1;
led_change_status(LED_STATUS_BUFF_OVER);
}
}
// 1秒未満の場合1秒間隔にする為の待ち
uint32_t t0 = millis();
uint32_t t1 = measur_time + 1000;
if (t1 > t0) {
delay(t1 - t0);
}
}
void setup() {
Serial.begin(115200);
server_init();
ina231_init();
led_init();
}
void loop() {
server.handleClient();
measurement();
}
index_html.h
const char INDEX_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Current logger</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1"></script>
<style>
body {
background: #121212;
color: #e8e8e8;
font-family: sans-serif;
margin: 20px;
}
canvas {
background: #232323;
border-radius: 12px;
height: 200px !important;
width: 100% !important;
margin-bottom: 6px;
}
h2 {
margin: 0 0 8px;
color: #ffffff;
}
.elapsed-time {
font-size: 32px;
color: #b0d6ff;
margin: 0 0 18px;
}
.chart-section {
margin-bottom: 12px;
padding: 12px;
background: #1f1f1f;
border: 1px solid #303030;
border-radius: 12px;
}
</style>
</head>
<body>
<div class="elapsed-time" id="elapsedTime">測定時間: -- 分</div>
<div class="chart-section">
<h2 id="voltageHeading">電圧: -- [mV]</h2>
<canvas id="chartVoltage"></canvas>
</div>
<div class="chart-section">
<h2 id="currentHeading">電流: -- [mA]</h2>
<canvas id="chartCurrent"></canvas>
</div>
<div class="chart-section">
<h2 id="capacityHeading">放電容量: -- [mAh]</h2>
<canvas id="chartCapacity"></canvas>
</div>
<script>
let chartV, chartI, chartC;
async function fetchData() {
const res = await fetch('/data');
return await res.json();
}
async function drawCharts() {
const json = await fetchData();
const labelsRaw = Array.isArray(json.labels) ? json.labels : [];
const voltageRaw = Array.isArray(json.voltage) ? json.voltage : [];
const currentRaw = Array.isArray(json.current) ? json.current : [];
const capacityRaw = Array.isArray(json.capacity) ? json.capacity : [];
const minLen = Math.min(labelsRaw.length, voltageRaw.length, currentRaw.length, capacityRaw.length);
const labels = labelsRaw.slice(0, minLen).map(m => { return String(m);});
const voltage = voltageRaw.slice(0, minLen);
const current = currentRaw.slice(0, minLen);
const capacity = capacityRaw.slice(0, minLen);
const latestIndex = minLen - 1;
const elapsedMinutes = Number(labelsRaw[latestIndex]) || latestIndex;
const maxX = Math.max(300, labels.length);
document.getElementById('elapsedTime').textContent = `測定時間: ${elapsedMinutes} 分`;
document.getElementById('voltageHeading').textContent = `電圧: ${voltage[latestIndex]} [mV]`;
document.getElementById('currentHeading').textContent = `電流: ${current[latestIndex]} [mA]`;
document.getElementById('capacityHeading').textContent = `放電容量: ${capacity[latestIndex]} [mAh]`;
const commonOptions = {
animation: false,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(20, 20, 20, 0.95)',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: '#444',
borderWidth: 1
}
},
scales: {
x: {
type: 'linear',
min: 0,
max: maxX,
ticks: {
color: '#d6d6d6',
font: { size: 24 },
stepSize: 60,
autoSkip: false,
minRotation: 0,
maxRotation: 0
},
grid: {
color: 'rgba(255,255,255,0.16)',
borderColor: 'rgba(255,255,255,0.35)',
lineWidth: 1
},
title: { display: true, text: '時間 (分)', color: '#d6d6d6' }
},
y: {
ticks: {
color: '#d6d6d6',
font: { size: 24 },
autoSkip: true,
maxTicksLimit: 5,
callback: function(value) {
return value;
}
},
grid: {
color: 'rgba(255,255,255,0.16)',
borderColor: 'rgba(255,255,255,0.35)',
lineWidth: 1
}
}
}
};
// 電圧
if (chartV) chartV.destroy();
chartV = new Chart(document.getElementById('chartVoltage').getContext('2d'), {
type: 'line',
data: {
labels: labels,
datasets: [{
data: voltage,
borderColor: 'red',
backgroundColor: 'rgba(255, 0, 0, 0.2)',
fill: true,
borderWidth: 4,
pointRadius: 0
}]
},
options: commonOptions
});
// 電流
if (chartI) chartI.destroy();
chartI = new Chart(document.getElementById('chartCurrent').getContext('2d'), {
type: 'line',
data: {
labels: labels,
datasets: [{
data: current,
borderColor: 'blue',
backgroundColor: 'rgba(0, 0, 255, 0.2)',
fill: true,
borderWidth: 4,
pointRadius: 0
}]
},
options: commonOptions
});
// 放電容量
if (chartC) chartC.destroy();
chartC = new Chart(document.getElementById('chartCapacity').getContext('2d'), {
type: 'line',
data: {
labels: labels,
datasets: [{
data: capacity,
borderColor: 'green',
backgroundColor: 'rgba(0, 128, 0, 0.2)',
fill: true,
borderWidth: 4,
pointRadius: 0
}]
},
options: commonOptions
});
}
drawCharts();
setInterval(drawCharts, 60000);
</script>
</body>
</html>
)rawliteral";
String cnv_json(String label, uint16_t dat[], uint32_t num, float dec_point, int disp_point) {
String json = "\"" + label + "\": [";
if (num > 0) {
for (int i=0; i<num - 1; i++) {
json += String(float(dat[i]) / dec_point, disp_point) + ',';
}
json += String(float(dat[num - 1]) / dec_point, disp_point);
}
return json + "]";
}
String set_json_data(uint32_t n, uint16_t time[], uint16_t volt[], uint16_t curt[], uint16_t capa[]) {
String json = "{\n";
json += cnv_json("labels", time, n, 1, 0) + ",\n";
json += cnv_json("voltage", volt, n, 16, 2) + ",\n";
json += cnv_json("current", curt, n, 16, 2) + ",\n";
json += cnv_json("capacity", capa, n, 16, 1);
json += "\n}";
return json;
}
ina231.h
#include <stdint.h>
#include <Wire.h>
#define INA231_SLV_ADR (0b1000000)
#define INA231_REG_CONFIG (0u)
#define INA231_REG_SHUNT_VOL (1u)
#define INA231_REG_BUS_VOL (2u)
#define INA231_REG_POWER (3u)
#define INA231_REG_CURRENT (4u)
#define INA231_REG_CALIB (5u)
#define INA231_REG_MASK (6u)
#define INA231_REG_ALERT (7u)
void ina231_write(uint8_t reg, uint16_t val) {
Wire.beginTransmission(INA231_SLV_ADR); // set reg pointer
Wire.write(reg);
Wire.write(val >> 8);
Wire.write(val & 0xff);
if (Wire.endTransmission(true)) {
Serial.println("Error Write End-Transmission.");
}
}
uint16_t ina231_read(uint8_t reg) {
Wire.beginTransmission(INA231_SLV_ADR); // set reg pointer
Wire.write(reg);
if (Wire.endTransmission(true)) {
Serial.println("Error Read End-Transmission.");
return 0;
}
// read reg
uint8_t buf[2] = {0u, 0u};
uint8_t bytes = Wire.requestFrom(INA231_SLV_ADR, sizeof(buf));
if (0 == bytes) {
Serial.println("Error Recive request.");
return 0;
}
Wire.readBytes(buf, bytes);
return ((uint16_t)(buf[0]) << 8) | buf[1];
}
float ina231_get_voltage() {
uint16_t vol = ina231_read(INA231_REG_BUS_VOL);
return (float)vol * 1.25; // [mv]
}
float ina231_get_current() {
uint16_t cur = ina231_read(INA231_REG_CURRENT);
return (float)cur * 0.1; // [mA]
}
void ina231_init() {
Wire.begin();
Wire.setClock(400000);
// 初期化
ina231_write(INA231_REG_CONFIG, 0x8000); // reset
ina231_write(INA231_REG_CONFIG, 0x4127); // mode
ina231_write(INA231_REG_CALIB, 0x0200); // 0.1[mA/bit] : シャント抵抗=R100(0.1Ω)
delay(100);
Serial.printf("CONFIG : 0x%04x\n", ina231_read(INA231_REG_CONFIG));
Serial.printf("SHUNT_VOL : 0x%04x\n", ina231_read(INA231_REG_SHUNT_VOL));
Serial.printf("BUS_VOL : 0x%04x\n", ina231_read(INA231_REG_BUS_VOL));
Serial.printf("POWER : 0x%04x\n", ina231_read(INA231_REG_POWER));
Serial.printf("CURRENT : 0x%04x\n", ina231_read(INA231_REG_CURRENT));
Serial.printf("CALIB : 0x%04x\n", ina231_read(INA231_REG_CALIB));
Serial.printf("MASK : 0x%04x\n", ina231_read(INA231_REG_MASK));
Serial.printf("ALERT : 0x%04x\n", ina231_read(INA231_REG_ALERT));
}
led_status.h
#include <Ticker.h>
#define LED_STATUS_MEASURING (1000)
#define LED_STATUS_VOLT_DROP (100)
#define LED_STATUS_BUFF_OVER (30)
#define LED_STATUS_NONE (0)
Ticker led_status;
void led_isr() {
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}
void led_init() {
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
}
void led_change_status(uint32_t status) {
static uint32_t now_status = LED_STATUS_NONE;
if ((now_status != status) && (LED_STATUS_BUFF_OVER != now_status)) {
now_status = status;
led_status.detach();
led_status.attach_ms(status, led_isr);
}
}
起動時にシリアル出力でIPアドレスが表示されるので、そこにブラウザでアクセスすると、↓こんな感じで現在の測定値とグラフを見る事ができます。
測定している様子
測定中はESP32の青LEDが1秒毎に点滅します。電圧が3.0V以下になると青LEDの点滅が速くなりますので、これが測定終了の合図です。
バッテリー健康度の測定
測定結果は下記の様になりました。どのグラフもリチウムイオン電池の放電特性どおりになっていてるので正しく測定できているように見えます。電圧・電流が徐々に下がっていき、前述の表の理想放電時間(2.92~3.33 h)あたりで急に0になっています。これはTP4056モジュール基板についている過放電保護ICが機能したからだと考えられます。また、今回の測定では一定負荷なので放電容量が単調増加していき公称値の電池容量の付近で止まっていいるのも想定通りです。ちなみに、放電しきった後に何回か電圧・電流の小さな山が見えるのはなんでしょうかね??
①:280mAh
②:550mAh
- 実際の充電容量:
593.6 mAh - バッテリー健康度:
100.8% - コメント: 調べてみたところ公称値の±10%くらいは誤差や個体差があるようです。また、今回は0.3Cでゆっくり放電したので限界まで放電できたのだと思われます。
③:900mAh
- 実際の充電容量:
802.5 mAh - バッテリー健康度:
89.2% - コメント: 最近購入したモノでしたが、電流計測器プログラムのデバッグや、Lチカ実験などで何回も充放電を繰り返したので最も劣化が進んだのかもしれません。充電Cレートも他よりも大きめだったし。
まとめ
使い捨てタバコに入っているリチウムイオン電池は思ったより使える事がわかりました。どうせ使い捨て用なので、あまり品質の良くないリチウムイオン電池が入っているのかなぁと予想していましたが、測定結果を見る限りでは普通に使えそうな感じです。捨てる電池だったので、これなら気楽に電子工作などに使えますね。
リチウムイオン電池を捨てる場合、発火リスクが高いため可燃ごみ等に混ぜないでください。自治体の分別ルールに従い廃棄してください。
投稿者の人気記事





-
momo
さんが
前の土曜日の21:41
に
編集
をしました。
(メッセージ: 初版)
Opening
hakatamax
前の日曜日の17:36
ログインしてコメントを投稿する非常に勉強になりました!
アリエクで買ったTP4056を何も考えずに小容量のリチウムイオンバッテリーに繋ぎ充電していました。Cレートに基づいた抵抗をつけた上での充電に変更します!