シリーズ『1ボタンだけのワイヤレスキーボード』、次はファームウェアについて書いていきたいと思います。
1ボタンだけのワイヤレスキーボード
(1) 準備編
(2) プリント基板の製作編
(3) ファームウェアの作成編 ←いまここ
(4) ケースとキーキャップの製作編
マイコンの選定
1ボタンだけのワイヤレスキーボードを作るにあたって、マイコンを何にするかは重要です。私がよく使っているM5Stack製品はESP32シリーズを使用していますが、ESP32は消費電力が大きく、コイン電池で長期間作動させるには不向きです。そこでRaspberry Pi Pico Wで作れないかと考えました。RP2040が乗ったデバイスを調べてみると、Seeed StudioのXIAOシリーズが目に留まりました。
切手大のサイズで、これならコンパクトに作れそうです。さっそく注文して届いた商品を見たところ、RP2040ではなくnRF52840の方を間違えて買ってしまったことに気づきました。nRF52840、初めて聞くマイコンだな。どんなものかと気になって調べると、非常に省電力でBLEに対応したマイコンだということがわかりました。
当初の予定とは違いましたが今回の目的には最適です。今回はnRF52840で開発を進めることにしました。
クセが強い
普段M5Stackばかり使っていたので、ちょっと戸惑うこともありました。書き込むときはUSBコネクタの横にある極小のボタンをダブルクリックしないといけないのですが、そうするとArduino IDE側で認識されるUSBポートが変わるので、USBポートを変更してから書き込まなくてはいけません。またシリアル出力を見たい場合はAdafruit_TinyUSB.hをincludeする必要があるようです。
動作の流れ
- (1) 電源投入すると30秒間アドバタイズする(高速10秒+低速20秒)
- (2) ペアリングおよび接続が行われるとHIDキーボードとして動作する。接続されないまま30秒過ぎると電源オフ(deep sleep)
- (3) アイドル状態が30分続くと切断して電源オフ(deep sleep)
- (4) deep sleep中にキーを押すと復帰
プログラムでは4つのキーが定義されていますが、実際にはD0に繋がったキーのみ使用しています。deep sleepからの復帰もこのキーです。
ダウンロード
今回作成したプログラムは、以下のGitHubのページからダウンロードできます。
https://github.com/kaz-mac/xiao_1key_blekey
プログラムについて
詳細はGitHubのソースを見ていただくとして、ここでは細かい部分について書いていきたいと思います。
シリアルポートのハマりポイント
if (DEBUG_USB) {
while (!Serial) { // これをやるとバッテリー駆動時に進まなくなるのでUSB接続必須
delay(100);
if (millis() > 5000) break;
}
}
USBポートに繋いでデバッグしているとき、起動直後のSerial.print()の内容が出てこないことに気づきました。while (!Serial)でシリアルが有効になるまで待機すれば解決します。ところがUSBポートに接続しない状態でバッテリーで起動した場合、ここから進まなくなってしまうので注意が必要です。
LEDのHigh/Lowが逆な点に注意
digitalWrite(LED_RED, LOW); // 点灯
digitalWrite(LED_RED, HIGH); // 消灯
基板上に付いているLEDは他のArduinoボードとは違って、LOWで点灯し、HIGHで消灯します。普通とは逆なので注意が必要です。
ボタンCLASS
#include "AnyButton.h"
AnyButton btn1, btn2, btn3, btn4;
pinMode(GPIO_BTN1, INPUT_PULLDOWN);
btn1.configButton(AnyButton::TypePush, AnyButton::ModeDirect, AnyButton::SpanEver);
ボタンの状態取得にはオリジナルのボタンクラスを利用しました。これは以前ETS2用のHシフターを作ったときのライブラリをそのまま流用しました。このボタンクラスはプッシュボタンをトグルスイッチのような振る舞いに見せたり、ボタンの状態変化を捉えることができます。
省電力化のテクニック
// 外部QSPI Flash Memory(省電力化のために使用)
#include <Adafruit_SPIFlash.h>
Adafruit_FlashTransport_QSPI flashTransport;
Adafruit_SPIFlash flash(&flashTransport);
// オンボードQSPI Flash MemoryをDeep Power-downモードにして省電力化する
flashTransport.begin();
flashTransport.runCommand(0xB9);
delayMicroseconds(5);
flashTransport.end();
これは以前『XIAO BLE nRF52840で何をするとどう消費電流が変わるのか調べてみた』でやった省電力対策で、オンボードQSPI Flash MemoryをDeep Sleepモードにするというものです。使用していないなら止めても問題ないですね。これでdeep sleep時は2~4uAくらいまで落とすことができます。恐るべしnRF52840の省電力性!
バッテリー電圧の測定
// バッテリー電圧測定の準備
analogReference(AR_INTERNAL_2_4); // VREF = 2.4V
analogReadResolution(10); // 10bit A/D
pinMode(VBAT_ENABLE, OUTPUT);
digitalWrite(VBAT_ENABLE, LOW); // VBAT_ENABLEをLOWにすると測定できる
uint16_t vbatRaw = analogRead(PIN_VBAT);
mv = vbatRaw * 2400 / 1023; // VREF = 2.4V, 10bit A/D
mv = mv * 1510 / 510; // 1M + 510k / 510k
BAT+ BAT- に接続したコイン電池の電圧を測定しています。ここで測定した電圧から残量を推測します。
バッテリー残量の推測
// バッテリー残量%を推定する
#ifdef BATTERY_CR2032
// CR2032放電特性テーブル
mv = constrain(mv, 1700, 3000);
uint16_t tableMv[] = { 3000, 2700, 2600, 2500, 2400, 2300, 2250, 2200 };
uint16_t tablePer[] = { 100, 98, 42, 22, 12, 4, 2, 0 };
size_t tableNum = sizeof(tableMv) / sizeof(tableMv[0]);
for (int i=0; i<tableNum-1; i++) {
if (mv <= tableMv[i] && mv > tableMv[i+1]) {
per = map(mv, tableMv[i+1], tableMv[i], tablePer[i+1], tablePer[i]);
break;
}
}
#endif
BLEではキーボードのバッテリーの情報も通知することができます。0~100の%で与えないといけないため、残り何%かを求めなくてはなりません。バッテリーの放電特性はカーブを描いているので、まずは放電特性を調べました。このときに使用したプログラムは『XIAO BLE nRF52840でBLEアドバタイズの多段中継器を作ってみた』で作成したプログラムを使っています。
青が実際に測定した結果、オレンジが近似曲線です。所々こころがぴょんぴょんしているところがありますが、これは室温が原因だと推測されます。上がっているタイミングは朝エアコンを付けた時間です。こんな如実に表れるんですね。このデータを元に近似曲線を作り、電圧から残量(%)を計算するようにしました。
なお、このデータはちょっと納得できていなくて、このブログを書いている現在、再測定中です。定電流1mAについては、正確には3Vのとき1mAという意味です。3KΩの抵抗を並列に入れて電圧の変化を調べています。何が納得できていないかというと、計算すると容量が108mAhしかなかったのです。CR2032の公証容量は220mAhです。いくらダイソーのだからといって少なすぎますし、他の人が測定した結果とも合いません。何か測定方法を間違えているのでしょうか…。
LEDの点滅
現在電源が入っているのか、ペアリング待ちなのか、を確認できるようにするため、LEDで状態を表示するようにしました。しかしLEDは結構消費電力が大きいので(参考→XIAO nRF52840での調査結果)、コイン電池で運用する際はなるべく使いたくありません。そこで今回は瞬間的に光らせることで、消費電力を抑えるようにしました。
// タイマー処理 LEDオン
void timerLedOn(TimerHandle_t xTimer) {
TimerData* data = (TimerData*)pvTimerGetTimerID(xTimer);
ledOn(data->color);
if (timer2 != NULL) xTimerStart(timer2, 0);
}
// タイマー処理 LEDオフ
void timerLedOff(TimerHandle_t xTimer) {
TimerData* data = (TimerData*)pvTimerGetTimerID(xTimer);
ledOff(data->color);
}
// LEDを点滅する
void blinkLED(uint8_t color, uint16_t timeOn, uint16_t timeCycle) {
timerData.color = color;
stopBlink(); // 既存のタイマーを解除
timer1 = xTimerCreate( // timer1 LEDを点灯するタイマー
"timerLedOn",
pdMS_TO_TICKS(timeCycle),
pdTRUE, // 繰り返す
(void*)&timerData,
timerLedOn
);
timer2 = xTimerCreate( // timer2 LEDを消灯するタイマー
"timerLedOff",
pdMS_TO_TICKS(timeOn),
pdFALSE, // 一度きり
(void*)&timerData,
timerLedOff
);
if (timer1 != NULL && timer2 != NULL) {
timerLedOn(timer1);
xTimerStart(timer1, 0);
}
}
LEDが点灯した後に「オフにするため」のタイマーと、消した後に再び「オンにする」タイマーの、2つのタイマーを使用しています。ESP32とかだとTickerを使って簡単にタイマーが作れちゃうんですが、nRF52840は無いようで(?)、ちょっと難しいやり方になりました。
修飾キーが押された処理
// ボタン1が押された場合の処理 CTRLキー
hid_keyboard_report_t report;
state = (btn1.getStateChanged());
if (state == 2) { // 押された
report = { KEYBOARD_MODIFIER_LEFTCTRL, 0, {0} }; // CTRL押しっぱなし
blehid.keyboardReport(&report);
active = true;
} else if (state == 1) { // 離した
blehid.keyRelease();
active = true;
}
今作ってるのはCTRLキーのみのキーボードです。押してる間だけ押してる状態にして、離したら離した状態にする必要があります。このreportという構造体には複数の押したキーを指定できるのですが、ここではCTRLキーのみを指定しています。これで blehid.keyboardReport(&report); で1回送信すれば、押しっぱなしの状態にできます。ボタンを離したことを検知したらblehid.keyRelease(); でキーの状態を戻します。
マクロ操作
// キー入力 複数
void keyPushModifer(uint8_t key, uint8_t modifier, uint32_t wait=20) {
hid_keyboard_report_t report = {0};
report.modifier = modifier;
report.keycode[0] = key;
blehid.keyboardReport(&report);
delay(wait);
blehid.keyRelease();
delay(wait);
}
// ボタン2が押された場合の処理 CTRL+C
if ((btn2.getStateChanged()) == 2) {
keyPushModifer(HID_KEY_C, KEYBOARD_MODIFIER_LEFTCTRL);
}
今回は1ボタンしかキーは実装されていませんが、実はプログラム上では他のキーが存在します。実験したときのがそのまま残っているだけですが…。これは2番目のキーを押すと、CTRL+Cを送信しています。先ほどとは違い、1回押したら実際には押して→離すの一連の動作をするようになっています。ちなみに3番目のキーはCTRL+Vです。この2つのキーでコピペができます。
if ((btn4.getStateChanged()) == 2) {
String str = "Battery "+String(batt.mv)+"mV "+String(batt.per)+"%\n";
blehid.keySequence(str.c_str(), 10);
}
↑これは4番目のキーを押したときに、バッテリーの情報を文字列にしたものを、キー入力として与えるものです。Bluefruitライブラリってこんなこともできるんですね。
keyPushModifer(HID_KEY_GRAVE, KEYBOARD_MODIFIER_LEFTALT);
blehid.keySequence("qawsedrftgyhujikolp\n", 10);
keyPushModifer(HID_KEY_GRAVE, KEYBOARD_MODIFIER_LEFTALT);
それでは↑これはどうなるでしょう?ぜひ試してみてください。
Watch Dog Timer
// WDTの設定
NRF_WDT->CONFIG = 0x01; // Configure WDT to run when CPU is asleep
NRF_WDT->CRV = 1+32768*120; // CRV = timeout * 32768 + 1
NRF_WDT->RREN = 0x01; // Enable the RR[0] reload register
NRF_WDT->TASKS_START = 1; // Start WDT
// WDT Update
NRF_WDT->RR[0] = WDT_RR_RR_Reload;
今回は必要性が無かったのでコメントにしていますが、こんな感じでWDTの設定ができます。これは120秒の例です。
Deep Sleep
// deep sleepモードに入る(復帰はリスタートになる)
void enterDeepSleep() {
nrf_gpio_cfg_sense_set(NRF_GPIO_PIN_MAP(0,2), NRF_GPIO_PIN_SENSE_HIGH); // P0.02 = D0ピンでdeep sleepから復帰
sd_power_system_off();
// NRF_POWER->SYSTEMOFF = 1;
}
これはnRF52840をDeep Sleepモードに移行する例です。Deep Sleepに入ると電流値が2~4uAほどまで下がるので、コイン電池で運用している時に便利です。NRF_GPIO_PIN_MAP(0,2)というのは復帰するためのピンの設定で、P0.02ピンというのはXIAO nRF52840ではD0ピンに相当します。このPで始まるピンは、XIAO nRF52840のピンアサイン表に書いてあります。他のピンでもいくつか試してみましたが、正常に復帰することができました。復帰は再起動となり、プログラムの先頭から開始されます。
NRF_GPIO_PIN_SENSE_HIGHはLOW→HIGHになったら復帰をする設定です。キーは押すとVCCに繋がる配線になっているので、押されたら電源オンです。
まとめ
ということで今回はじめてnRF52840というマイコンを触りましたが、なかなか面白いデバイスだと思いました。何より低消費電力なところが素晴らしいですね。Bluefruitライブラリも充実していて、簡単にBLEデバイスを作ることができます。XIAO nRF52840は外部に出てるピン数も多いので、自作キーボードを作るにはもってこいですね。
さて、次回はシリーズの最後、ケースとキーキャップを作成します。
1ボタンだけのワイヤレスキーボード
(1) 準備編
(2) プリント基板の製作編
(3) ファームウェアの作成編 ←いまここ
(4) ケースとキーキャップの製作編
.