2022年6月19日日曜日

Seeed XIAO BLEの充電回路を使いこなす

最近、国内の各電子部品店でSeeed XIAO BLEという製品が取り扱われるようになりました。親指サイズのデバイスで、nRF52840というARM Cortex-M4 CPUを持ったマイコンモジュールで、Bluetooth 5.0に対応しています。もちろん技適も通っています。

末尾に"Sense"が付いたモデルと付いていないモデルの2種類があります。Sense付きは700円程度高くなりますが、6軸IMUとPDMマイクを搭載しているようです。それ以外は同じです。

さて、特記すべきことに、このデバイスにはBQ25101というTexas Instrument製のリチウムイオン電池充電コントローラーが付いています。そのため、適当なリチウムポリマー電池などを直結して、充電式のBluetoothデバイスなんかを簡単に開発することができます。なんというかすごい時代ですね。本当にIoTが個人レベルで作りやすくなったと感じます。

ハードウェア

回路全体

さて、一応Seeed wikiにこのマイコンボードの回路図は載っているのですが、非常に見にくくてわかりずらいです。そのうえ、現物との対応もあまりとれません。ということでいろいろひも解いてみました。

結論から言うと、回路は以下のようになっています(電源にかかわらない回路は省略しています)。

点線で書いた部分のみがこのボードの外の配線となります。ですので、バッテリーを基板の裏面にある「BAT+」「BAT-」端子に接続するだけです。裏面のSMD端子なのが少し不便ですが、まあそれはよいでしょう。

Seeed wikiより引用)

ただし、回路図から見てわかるように、Seeed XIAO BLEに搭載されているのは充電コントローラーだけで、過放電保護回路は付いていません。電子工作で手に入るリチウムポリマー電池は保護回路付のものも多いですが、18650サイズのリチウムイオン電池など、保護回路が付いていないものはDW01/FS8205などの過充放電保護ICなどを組み込むとよいでしょう。詳しくはこちらの記事を参照してください。

ちなみに、Q1はなぜダイオードではなくFETを付けているのかと思う人もいるかもしれませんが、これはダイオードでの約0.6Vの損失をなくすためです。FETだとダイオードに比べてほとんど電圧降下しませんので、3.7Vのリチウムイオン電池で駆動する際も、3.3V以上の電圧をレギュレーターに供給できるようになります。

充電コントロール回路

さて、メインディッシュは充電コントローラーのBQ25101となります。 とはいっても、BQ25101はよくあるタイプの充電コントローラーで、電圧がある一定(約4.2V)になるまでは定電流充電を行い、そこから先は定電圧充電を行うタイプのものです。電池の温度制御(温度が高くなりすぎないように電流を抑える機能)もあるようですが、今回は殺されている(TS端子に温度センサではなくただの抵抗が取り付けられている)ので機能しません。

充電プロファイル(データシートより引用)

ISET端子は充電電流を決める端子で、その抵抗値により決まります。範囲は13.5kΩ(10mA)から0.54kΩ(250mA)です。このボードではR18に2.7kΩが付いているので、デフォルトで50mA流れます。また、マイコンからP0.13をLOW出力してあげればさらに2.7kΩが並列につながるため、充電電流が50mA増えて合計100mAを流すことができます。

~CHG端子は充電状態か否かを示す端子です。オープンドレイン端子となっていて、LOW(=FET ON)で充電状態、Open(=FET OFF)で充電完了状態を意味します。充電LEDがこの回路に付いているのでマイコンの制御なしにそのままLEDが機能しますが、マイコン側からP0.17の値を読み込むことで充電中か否かを判断することもできます。

さらに、R16とR17で分圧されたバッテリー電圧をアナログピンで読み込む回路も一緒に入っています。これによってバッテリー電圧をマイコンで知ることができます。もちろんこの回路を有効にするにはP0.14をLOW出力する必要があります。普段はこのポートを入力(ハイインピーダンス)にしておくことで、R16-R17の計1.5MΩを流れる非常に微量の電流(約2uA)ではありますが節電することができます。

なお、このマイコンボードではバッテリー電圧を測ることしかできないため、例えば負荷が大きく上下するような環境でこの充電回路を使用した場合、負荷が上がれば(=電流が大きくなれば)バッテリーの内部抵抗での電圧降下が大きくなるためバッテリー電圧は下がり、その後負荷が下がったときにバッテリーの電圧は回復します。そのような場合は、バッテリー電圧しか見ていないと正しく電池の消耗状態を把握することができません。もっと厳密に電池残量を測るには、クーロンカウンタと呼ばれる電荷の移動量を監視するICが必要になってきますが、このボードにはそのようなものは無いため、外付けしない限りはあまり厳密な電池残量の把握はできません。電圧はあくまでも参考程度というものになります。

ソフトウェア

さて、充電回路がわかったところでソフトを作っていきましょう。今回はサンプルでBluetoothキーボードとして使えるデバイスを作っていきます。

Bluetooth 4.0から規格が制定され、もちろんこのSeeed XIAO BLEでも対応しているBLE(Bluetooth Low Energy)にはBattery Serviceというものがあり、ホスト側にバッテリー残量を知らせることができます。例えばWindows11だと、Battery Serviceに対応しているデバイスでは以下のように電池残量を示すアイコンが出てきて、極端にバッテリー残量が低下しているなどの場合は通知で警告してくるなどの機能があります。

このSeeed XIAO BLEはせっかくここまでの充電回路を持っているデバイスなので、このBattery Serviceを使いこなしていきましょう。

環境構築

さて、今回はArduinoで開発していきます。

Seeed Wikiにも書いてある通り、Arduinoの「追加のボードマネージャのURL」に "https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json" を追加して「Seeed nRF52 Boards」をインストールするのですが、ここで注意点があります。バージョン1.0.0をインストールしてください。デフォルトだと最新?の2.6.1?が出てきますが、ここで今回使うライブラリは1.0.0しか対応していないようなので、1.0.0を入れる必要があります。

あとは通常通りでOKです。ボード選択などを適宜して使える状態にすれば準備完了です。もしわからなくても、ググればそれなりに情報は出てくるはずです。

setupの実装

早速実装していきましょう。 Adafruitのライブラリを使用していきますが、特段ライブラリをインストールせずとも使えるはずです。

#include <bluefruit.h>

#define	PIN_HICHG	22
#define	PIN_INVCHG	23

BLEDis bledis;
BLEHidAdafruit blehid;
BLEBas blebas;

ヘッダーはこんな感じです。なぜかHICHG(100mA充電)端子のピンと~CHGのピンがヘッダファイルで定義されていないのでここで定義しておきます。

サービスは3つ使い、DIS (Device Information Service)、HID (Human Interface Service)、BAS (Battery Service)です。DISはデバイス情報を伝えるサービス、HIDはキーボード動作そのもののサービスです。

void setup() 
{
	setup_battery();

	Serial.begin(115200);

	setup_ble();
}

void setup_battery()
{
	// High speed charging (100mA)
	pinMode(PIN_HICHG, OUTPUT);
	digitalWrite(PIN_HICHG, LOW);

	pinMode(PIN_INVCHG, INPUT);
}

void setup_ble(void) 
{
	Bluefruit.autoConnLed(false);
	Bluefruit.begin();
	Bluefruit.setTxPower(4);  // Check bluefruit.h for supported values
	Bluefruit.setName("nRF52840 Keyboard");

	// Configure and Start Device Information Service
	bledis.setManufacturer("EH500_Kintarou");
	bledis.setModel("nRF52840");
	bledis.begin();

	blehid.begin();
	blebas.begin();

	// Advertising packet
	Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
	Bluefruit.Advertising.addTxPower();
	Bluefruit.Advertising.addAppearance(BLE_APPEARANCE_HID_KEYBOARD);

	// Include BLE HID service
	Bluefruit.Advertising.addService(blehid);

	// Include BLE battery service
	Bluefruit.Advertising.addService(blebas);

	// There is enough room for the dev name in the advertising packet
	Bluefruit.Advertising.addName();

	Bluefruit.Advertising.restartOnDisconnect(true);
	Bluefruit.Advertising.setInterval(32, 244);  // in unit of 0.625 ms
	Bluefruit.Advertising.setFastTimeout(30);    // number of seconds in fast mode
	Bluefruit.Advertising.start(0);  // 0 = Don't stop advertising after n seconds
}

Setupルーチンはこんな感じでよいでしょう。まずは100mA充電を有効化し、その後にBLEのセットアップをします。各Serviceを起動して、最後はアドバタイジングの設定をすればOKです。

バッテリー残量

さて、先ほど出てきた問題です。loopルーチンを実装するにあたって、バッテリー残量をバッテリー電圧から推定しなければなりません。ということで、マイコンからバッテリー電圧を見たらどのように見えるのかを確認してみました。使用したバッテリーはaitendoで売っている300mAhのリチウムポリマー電池です。なお購入してから数年間経っているため、性能が落ちているかもしれませんがそこはご容赦ください。

まずは充電です。電池を過放電防止保護がかかるまですっからかんにしてからSeeed XIAO BLEの100mA設定で充電を開始しました。

1時間45分くらいかけて4.3Vくらいまで電圧が上がって、その先は定電圧モードになっています。4時間くらいのところで充電が終了してその拍子に少し電圧が下がっているのが見て取れます。ちなみに、これはこのマイコンボードのA/D変換で測った電圧で、4.3Vくらいまで上がっていますが、手元のテスターで測るとピッタリ4.2Vでした。抵抗の誤差とかなのかなという感じです。

続いて放電です。充電が終わってから過放電保護がかかるまで電子負荷装置で100mAで放電をしました。

放電はこんな感じです。4.1Vくらいからほとんど一定に電圧が下がっていき、3.6Vくらいからガクンと落ちています。

loopの実装

さて、こんな感じでどうでしょう。

#define BAT_AVERAGE_COUNT	16
#define BAT_AVERAGE_MASK	0x0F

void loop()
{
	loop_led();
	loop_battery();
}

void loop_led()
{
	digitalWrite(LED_RED, HIGH);
	digitalWrite(LED_GREEN, HIGH);

	uint32_t ms = millis();

	if(Bluefruit.connected()) {
		uint32_t interval = ms % 3000;
		digitalWrite(LED_BLUE, (interval < 100) ? LOW : HIGH);
	} else {
		uint32_t interval = ms % 2000;
		digitalWrite(LED_BLUE, (interval < 100 || (interval >= 200 && interval < 300)) ? LOW : HIGH);
	}
}

void loop_battery()
{
	static uint32_t lastMeasure = 0;
	static bool lastIsCharging = false;
	uint32_t ms = millis();
	bool isCharging = battery_isCharging();
	static uint16_t rawvalues[BAT_AVERAGE_COUNT];
	static uint8_t index = 0;
	static uint8_t count = 0;
	
	if(ms - lastMeasure > 3000) {
		if(lastIsCharging != isCharging) {	// 充電状態が変わったらリセット
			index = 0;
			count = 0;
		}

		pinMode(VBAT_ENABLE, OUTPUT);
		digitalWrite(VBAT_ENABLE, LOW);
		rawvalues[index] = (uint16_t)analogRead(PIN_VBAT);
		pinMode(VBAT_ENABLE, INPUT);

		index = (index + 1) & BAT_AVERAGE_MASK;
		count = min(count + 1, BAT_AVERAGE_COUNT);
		uint16_t rawtotal = 0;
		for(int i = 0; i < count; i++)
			rawtotal += rawvalues[i];
		
		double volt = (double)rawtotal / count / 1024 * 3.6 / 510 * 1510;	// 10bit, Vref=3.6V, 分圧比1000:510

		if(isCharging) {
			if(volt <= 3.78)
				blebas.notify(1);
			else if(volt <= 4.02)
				blebas.notify(3);
			else if(volt <= 4.25)
				blebas.notify((uint8_t)((volt - 4) * 140 / 5 + 0.5) * 5);	// 4.25Vで35%になるよう5%単位
			else
				blebas.notify(70);	// ここからは定電圧領域になるので電圧じゃほとんどわからない
		} else {
			if(volt <= 3.5)
				blebas.notify(1);
			else if(volt <= 3.62)
				blebas.notify(3);
			else if(volt <= 3.8)
				blebas.notify((uint8_t)(((volt - 3.62) * 277.78 + 5) / 5 + 0.5) * 5);	// 3.8Vで55%、3.62Vで5%
			else if(volt <= 4.1)
				blebas.notify((uint8_t)(((volt - 3.8) * 150 + 55) / 5 + 0.5) * 5);	// 4.1Vで100%、3.8Vで55%
			else
				blebas.notify(100);
		}

		lastMeasure = ms;
		lastIsCharging = isCharging;
	}
}

bool battery_isCharging()
{
	return digitalRead(PIN_INVCHG) == LOW;
}

バッテリーは上の測定結果をもとにパーセンテージを表示するようにしています。しかし、先述の通り電圧によるバッテリー残量測定は目安程度にしかなりませんし、とくに充電なんかは定電圧領域に入ると電圧で判別することはほとんどできなくなるので70%固定とかいう雑な実装をしています。もう少し頑張りたかったら経過時間なども考慮に入れてみるのもいいかもしれません。

また、充放電以外にも接続前は青色LEDが2回点滅、接続後は1回点滅にしたかったのでそういう処理も入っています。

これでだいたいSeeed XIAO BLEをリチウムポリマー電池で使う準備が整いました。やりたいことの実装へ移っていきましょう。