本記事はトリリオンノード研究会のセンサに手を近づけてビープ音を鳴らすのスケッチをより詳細に解説した内容になっています。
使用したもの
- Leafony Basic Kit A1.0
- Leafony Extension Kit A1.0のAI02A SP&PIR
- USBケーブル(A-MicroB)
スケッチの書き込みにはArduino IDE 1.8.11を用いています。
Leafonyとは
Leafonyは一円硬貨と比べられるくらい小さい、超小型のオープンソースハードウェアです。
リーフと呼ばれる電子基板を接続して組み合わせることで、非常にコンパクトな電子機器を作成できます。
工場出荷時には、Leafonyの4-Sensorsリーフに搭載されている温度・湿度・照度・傾きセンサの値を、Web Bluetoothを使ってスマートフォンなどの端末に送信するスケッチがAVR MCUリーフに書き込まれています。
2020年2月1日から新たにESP-WROOM-32が搭載されているリーフが同梱されたESP32 Wi-Fi Kit A1.0も発売開始されました
プロセッサのリーフを変更することでATmega328Pに限らずどのような環境でも使用できるようになっています。
個人でLeafonyバス準拠の基板を作成して販売するような活動も推奨されています。
GitHubでLeafonyのデータシートやKiCadのテンプレートが公開されているので、自作する際は参考になりそうです。
leafony.comやトリリオンノード研究会のサイトに詳しい資料が掲載されています。
リーフの組み立て
LeafonyのリーフはLeafony busによって相互接続されます。
リーフを上から見て次の順になるように積み重ねて接続します。
- SP & PIRリーフ(Extension KitのCMT-1203と書かれたスピーカーが載っている基板)
- USBリーフ(Basic Kit)
- AVR MCUリーフ(Basic Kitの側面にプッシュボタンがある基板)
- CR2032リーフ(Basic Kitの底面にコイン電池を入れられる基板)
コネクタ同士がハマるように作られているので、指で押さえてパチッと音が鳴ってくっつけば接続されている状態になります。
リーフ同士を分離させるときには、基板側を持って上側に剥くようにするとリーフを取り外せます。
リーフが接続できたら、付属しているM2ねじを締めます。
このとき、ねじは必ず交互に締めます。
組み立て後のリーフ同士がどのように接続されているのか考えてみます。
SP&PIRのリーフの回路図を見ると、人感センサーはI2C通信のスレーブデバイスにするためにSDAとSCL、割り込み処理用にD2と繋がり、圧電スピーカーはD5と繋がっていることが分かります。
同じリポジトリからAVR MCUリーフの回路図、USBリーフの回路図も見てみると、D0(RXD)とD1(TXD)もシリアル通信するために接続されていることが分かります。
これによってスケッチを書き込めるようになっています。
ピンアウトについてはExcelファイルが公開されています。2020年2月14日現在は次の図のようになっています。
使いたいモジュールが載ったリーフを積み重ねるだけで回路を組めるので、回路周りのことをほとんど意識せずに使えるのは凄いですね!
LeafonyでLチカする
組み立てが完了したら動作確認をしてみましょう。
スケッチを書き込む前に環境構築が必要になります。
クイックスタートに開発環境の構築手順が載っているので参考にしました。
AVR MCUへの書き込みはドライバーが必要になります。FTDI VCP Driverサイトから各種環境のドライバーがインストールできます。
Windowsの場合はCommentsのsetup executableからインストーラーがダウンロードできるのでこれを利用すると簡単です。
次に、Arduino IDEの[ツール]では以下のように設定します。
- ボード: Arduino Pro or Pro Mini
- プロセッサ: ATmega328P (3.3V, 8 MHz)
シリアルポートについては事前に調べておくか、LeafonyをUSB接続して出現したものを使用します。
スケッチを書き込める状態になったはずなのでLチカのスケッチを書き込んで動作確認します。
AVR MCUリーフのプッシュボタンの隣にLEDが実装されているので、Arduino Unoなどと同様にしてこのLEDをチカチカできます
[ファイル]→[スケッチ例]→[01.Basics]→[Blink]を開くと次のスケッチが表示されるので、そのまま書き込みましょう。
Blink
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH);
delay(1000);
digitalWrite(LED_BUILTIN, LOW);
delay(1000);
}
スケッチ書き込み完了後、AVR MCUリーフに載っているLEDが点滅しているのが確認できるはずです。
人感センサーを扱う
SP & PIRリーフは圧電スピーカーと人感センサー(PIR)を搭載しています。
人感センサーは人間がいるかどうかを判別するセンサーです。よくトイレの照明に使われているヘドバンしないと反応しないアレです。
搭載されているAK9754AEは赤外線によって人間を感知し、割り込みピンから信号を出力します。
人感センサーの設定
まず、setup
で人感センサーの設定をします。
設定について詳しいことを知りたいときは、AK9754AEのデータシートの31~35ページにどのアドレスにデータを書き込めばいいのかが記載されています。
人感センサーの設定
#include <Arduino.h>
#include <Wire.h>
constexpr int I2C_PIR_ADDRESS = 0x65;
volatile boolean catchHuman = false;
void setup() {
attachInterrupt(0, []() {
catchHuman = true;
}, FALLING);
Wire.begin();
Serial.begin(115200);
delay(100);
writeI2C(0x20, 0xFF); // CNTL1 RESET
writeI2C(0x2A, 0xF2); // CNTL11 人感アルゴリズム有効 / 割り込み出力有効
writeI2C(0x25, 0x0F); // CNTL6 センサゲイン205%(最大)
writeI2C(0x2B, 0xFF); // CNTL12 連続測定モード
delay(1000);
}
void loop() {}
void writeI2C(int address, byte value) {
Wire.beginTransmission(I2C_PIR_ADDRESS);
Wire.write(address);
Wire.write(value);
Wire.endTransmission();
}
attachInterrupt(interrupt, function, mode)
関数は外部割り込みが発生したときに、第二引数で指定した関数を実行できます。
第一引数には割り込み番号を指定します。0を指定すればD2が指定されます。
第三引数で割り込みを発生させるトリガを指定しますが、今回はピンの状態がHIGH
からLOW
に変わったときに発生させるのでFALLING
を指定します。
詳しくはArduino Referenceを読むとよいでしょう。
上のスケッチでは割り込みが発生したときにcatchHuman
をtrue
にしています。
割り込み処理の中で変更する変数については、不正確な値にならないようにvolatile
修飾子をつけてRAMから読むようにします。
ArduinoでI2Cデバイスと通信する際にはWireライブラリを使います。Wire.h
をincludeすることで利用できます。
スレーブデバイスのアドレスが必要ですが、回路図にAK9754AEのI2Cアドレスは0x65
と書かれているのでこれを使います。
マスターとしてI2Cバスに加わるには、setup
内でWire.begin
を引数なしで呼びます。
I2C通信でデータを書き込む際には次のようにします。
Wire.beginTransmission(address)
に渡したスレーブアドレスに対して送信を開始Wire.write(value)
で書き込み先となるレジスタアドレスをキューに入れるWire.write(value)
で書き込むデータをキューに入れるWire.endTransmission(true)
でキューに入れたバイトを送信して、STOPメッセージを送信
Wire.endTransmission(stop)
は引数がtrue
のとき、もしくは引数を省略すると停止メッセージを送信してI2Cバスを開放します。
引数にfalse
を渡すと再起動メッセージを送信するため、連続して次の送信を行うことが可能になります(読み込みで使います)。
この状態でLeafonyにスケッチを書き込んでも割り込み処理は一度しか発生しないので注意してください。
人感センサーのST2レジスタを読み込むまでは測定データが更新されないためです。
測定データを取得する
先のI2C通信と同じ要領で指定したアドレスから数バイト読み込みます。
大雑把ですが、各レジスタアドレスとデータは次のような関係になっています。
アドレス | 内容 |
---|---|
0x04 | 接近検知フラグ、データの準備ができたかどうか |
0x05 | 人感センサーの測定データ下位 8bit |
0x06 | 人感センサーの測定データ上位 8bit |
0x07 | 内部温度センサーの測定データ下位 8bit |
0x08 | 内部温度センサーの測定データ上位 8bit |
0x09 | 測定データ読み出し後に読む |
人感センサーと内部温度センサーの測定データは2の補数表現になっていることがあるようです。それを考慮した上で定数を掛ける必要があります。
そのため、calculate
関数を次のように定義します。
センサーの測定データを変換する関数
double calculate(byte upper, byte lower, double coefficient) {
unsigned short data = (unsigned short) ((upper << 8) | lower);
return (data & 0x8000) == 0x8000
? (double) ((~data + 1) * coefficient) * -1
: (double) (data * coefficient);
}
センサーのデータ読み込み部について考えます。
旭化成のデータシートの28、29ページから数式を引用すると、
人感センサーの出力電流(pA):
内部温度センサーの表示値(℃):
とあるのでこれを利用します。
また、接近検知フラグは0x04
の5桁目に入っています。
これらのことを踏まえて、読み込み処理を次のように実装しました。
人感センサーの測定データを格納する構造体
struct SensorData {
// ヒトが接近したかどうか
boolean detection;
// IRセンサーの測定データ[pA]
double ir;
// 内部温度センサーの測定データ[℃]
double temperature;
};
I2C通信で人感センサーのデータを読み込む
SensorData readI2C() {
SensorData data;
byte bytes[I2C_BUFFER_LENGTH];
Wire.beginTransmission(I2C_PIR_ADDRESS);
Wire.write(I2C_READ_START_ADDRESS);
Wire.endTransmission(false);
Wire.requestFrom(I2C_PIR_ADDRESS, I2C_BUFFER_LENGTH);
for (int i = 0; i < I2C_BUFFER_LENGTH; i++) {
bytes[i] = Wire.read();
}
data.detection = (boolean) ((bytes[0] & 0x10) >> 4);
data.ir = calculate(bytes[2], bytes[1], 0.4578);
data.temperature = calculate(bytes[4], bytes[3], 0.0019837) + 25;
return data;
}
I2C通信でデータを読み込む際には次のようにします。
Wire.beginTransmission(address)
に渡したスレーブアドレスに対して送信を開始Wire.write(value)
で読み込み始めるレジスタアドレスをキューに入れるWire.endTransmission(false)
でキューに入れたバイトを送信して、RESTARTメッセージを送信Wire.requestFrom(address, length, true)
で指定したスレーブアドレスから指定した長さのバイトを読み込んだ後、STOPメッセージを送信Wire.available()
でread
で取得できる残りバイト長を取得できるのでwhile
などでループするWire.read()
でスレーブアドレスからマスターに送信されたバイトを取得する
これで人感センサーから測定データを取得できるようになりました。
loop
内でreadI2C
を呼んで動作確認してみましょう。
シリアルポートに読み込んだ値を出力するようにしておきます。
動作確認用コード
void loop() {
SensorData data = readI2C();
print(data);
delay(1000);
}
void print(SensorData data) {
if (data.detection) {
Serial.println("---接近---");
}
Serial.print(data.ir);
Serial.println("\tpA");
Serial.print(data.temperature);
Serial.println("\t\t\tdeg");
Serial.println();
}
ボーレートを115200
にしたシリアルモニタで測定データを確認できれば成功です。
圧電スピーカーで音楽を再生する
人感センサーで人間の接近を検知したら音楽を鳴らすようにしましょう
入った途端に特徴的なメロディが鳴るといったらファミマの入店音ですね。
正式には稲田康さんが作曲された メロディーチャイムNO.1 ニ長調 作品17「大盛況」 という曲です。元々パナソニックのドアホンから流すために作曲されたメロディだそうです。
製品に組み込む際には権利関係の確認が必要ですが、個人的に鳴らす分には問題なさそうなのでこの曲を再生することにします。
Arduinoで和音を鳴らすためにはMozziやPWMDAC_Synthがありますが、SP&PIRリーフの圧電スピーカーはD5を使用しているので残念ながら対応していません。
そのため、今回は単音で再生することを考えます。
ArduinoのチュートリアルのtoneMelodyに音符同士を判別しやすくする定数1.30
と音階の周波数が載っているのでこれを使用します。
音階の周波数はpitches.h
に定義してincludeしておきます(完成したスケッチの項にも載せています)。
BPMや、圧電スピーカーを鳴らすのに使用するピン(D5)、楽譜などを定義します。
定数・音符の構造体・楽譜の定義
#include "pitches.h"
constexpr int BPM = 100;
constexpr float NOTE_DISTINCTION = 1.30;
constexpr float BASE_TONE_DURATION = 60000 * 4 / BPM / NOTE_DISTINCTION;
constexpr int SPEAKER_PIN = 5;
struct Note {
int pitch;
float duration;
};
Note notes[] = {
{ NOTE_FS5, 8 },
{ NOTE_D5 , 8 },
{ NOTE_A4 , 8 },
{ NOTE_D5 , 8 },
{ NOTE_E5 , 8 },
{ NOTE_A5 , 3 },
{ NOTE_E5 , 8 },
{ NOTE_FS5, 8 },
{ NOTE_E5 , 8 },
{ NOTE_A4 , 8 },
{ NOTE_D5 , 3 }
};
上で定義した楽譜を再生する関数を定義します。
楽譜を再生する関数
#include <Arduino.h>
void playSound() {
for (auto note : notes) {
int duration = BASE_TONE_DURATION / note.duration;
tone(SPEAKER_PIN, note.pitch, duration);
delay(duration * NOTE_DISTINCTION);
noTone(SPEAKER_PIN);
}
}
Arduinoでは拡張for文が使えるので、利用すると楽譜が後から差し替えられるので便利です。
tone(pin, frequency, duration)
は指定したピンに、指定した周波数の矩形波を生成する関数です。
ミリ秒部分を指定しないとnoTone
を呼び出すまで矩形波を生成します。
音楽を再生できるようになったのでloop
内で人間を検知したときにplaySound
関数を呼ぶようにしましょう
人間を検知したときに圧電スピーカーから音を鳴らす
void loop() {
SensorData data = readI2C();
print(data);
if (catchHuman) {
playSound();
catchHuman = false;
}
delay(1000);
}
人が通りそうな場所にLeafonyを仕掛ける
Leafonyで一番楽しいと感じたところは、超小型でかつコイン電池で動作してケースにマグネットがついているので色々なところに貼り付けられるところです。
どこにでも設置できるので、センサーから測定データを得てBluetoothで送信したいときにも便利ですね。
オフィスの入口の机の下に貼り付けて、しばらく入室音を鳴らして遊んでみました。
人感センサーが反応しやすくなるように天面は外してあります。
完成したスケッチ
スケッチの全体は次のようになります。
コメントはDoxygenの仕様に則って記入しています。
Leafonyで入室音を鳴らすスケッチ
#include <Arduino.h>
#include <Wire.h>
#include "pitches.h"
//! @brief PIRのアドレス
constexpr int I2C_PIR_ADDRESS = 0x65;
//! @brief 読み出し開始位置のアドレス
constexpr int I2C_READ_START_ADDRESS = 0x04;
//! @brief 読みだすバイト数
constexpr int I2C_BUFFER_LENGTH = 6;
//! @brief 1分間に4分音符の数。拍数
constexpr int BPM = 100;
//! @brief 音符を判別しやすくために用いる
constexpr float NOTE_DISTINCTION = 1.30;
//! @brief 全音符を鳴らす長さ
constexpr float BASE_TONE_DURATION = 60000 * 4 / BPM / NOTE_DISTINCTION;
//! @brief 圧電スピーカーを接続したピン
constexpr int SPEAKER_PIN = 5;
//! @brief ヒトを接近検知しているかどうか(割込処理で使用する)
volatile boolean catchHuman = false;
/**
* @brief 人感センサーリーフが取得したデータ
*/
struct SensorData {
//! @brief ヒトが接近したかどうか
boolean detection;
//! @brief IRセンサーの測定データ[pA]
double ir;
//! @brief 内部温度センサーの測定データ[℃]
double temperature;
};
struct Note {
//! @brief 音の周波数
int pitch;
//! @brief n分音符
float duration;
};
//! @brief 楽譜
Note notes[] = {
{ NOTE_FS5, 8 },
{ NOTE_D5 , 8 },
{ NOTE_A4 , 8 },
{ NOTE_D5 , 8 },
{ NOTE_E5 , 8 },
{ NOTE_A5 , 3 },
{ NOTE_E5 , 8 },
{ NOTE_FS5, 8 },
{ NOTE_E5 , 8 },
{ NOTE_A4 , 8 },
{ NOTE_D5 , 3 }
};
void setup() {
attachInterrupt(0, []() {
catchHuman = true;
}, FALLING);
Wire.begin();
Serial.begin(115200);
delay(100);
writeI2C(0x20, 0xFF); // CNTL1 RESET
writeI2C(0x2A, 0xF2); // CNTL11 人感アルゴリズム有効 / 割り込み出力有効
writeI2C(0x25, 0x0F); // CNTL6 センサゲイン205%(最大)
writeI2C(0x2B, 0xFF); // CNTL12 連続測定モード
delay(1000);
}
void loop() {
SensorData data = readI2C();
print(data);
if (catchHuman) {
playSound();
catchHuman = false;
}
delay(1000);
}
/**
* @brief スレーブデバイスに1byteのデータを書き込む
* @param address 書き込み先のアドレス
* @param value 書き込むデータ
*/
void writeI2C(int address, byte value) {
Wire.beginTransmission(I2C_PIR_ADDRESS);
Wire.write(address);
Wire.write(value);
Wire.endTransmission();
}
/**
* @brief 人感センサーの測定値を読み込む
* @return 接近の有無、IRセンサーの出力電流[pA]、内部温度
*/
SensorData readI2C() {
SensorData data;
byte bytes[I2C_BUFFER_LENGTH];
Wire.beginTransmission(I2C_PIR_ADDRESS);
Wire.write(I2C_READ_START_ADDRESS);
Wire.endTransmission(false);
Wire.requestFrom(I2C_PIR_ADDRESS, I2C_BUFFER_LENGTH);
for (int i = 0; i < I2C_BUFFER_LENGTH; i++) {
bytes[i] = Wire.read();
}
// 接近検知フラグ: 接近を検知すると5桁目が1になり、測定データバッファの読み出し完了時に0になる
data.detection = (boolean) ((bytes[0] & 0x10) >> 4);
data.ir = calculate(bytes[2], bytes[1], 0.4578);
data.temperature = calculate(bytes[4], bytes[3], 0.0019837) + 25;
return data;
}
/**
* @brief センサーの測定データを変換する
* @param upper 上位8bit
* @param lower 下位8bit
* @param coefficient 変換に用いる係数
* @return センサーから得られた測定値
*/
double calculate(byte upper, byte lower, double coefficient) {
unsigned short data = (unsigned short) ((upper << 8) | lower);
return (data & 0x8000) == 0x8000
? (double) ((~data + 1) * coefficient) * -1
: (double) (data * coefficient);
}
/**
* @brief 人感センサーから得られたデータをシリアルポートに出力する
* @param data 人感センサーから得られた測定値
*/
void print(SensorData data) {
if (data.detection) {
Serial.println("---接近---");
}
Serial.print(data.ir);
Serial.println("\tpA");
Serial.print(data.temperature);
Serial.println("\t\t\tdeg");
Serial.println();
}
/**
* @brief 圧電スピーカーを使って音楽を再生する
*/
void playSound() {
for (auto note : notes) {
int duration = BASE_TONE_DURATION / note.duration;
tone(SPEAKER_PIN, note.pitch, duration);
delay(duration * NOTE_DISTINCTION);
noTone(SPEAKER_PIN);
}
}
全ての音階の周波数をそのまま載せると長いのでpitches.h
の内容は使用する定数のみに省略してあります。
pitches.h
#define NOTE_A4 440
#define NOTE_AS4 466
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_CS5 554
#define NOTE_D5 587
#define NOTE_DS5 622
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_FS5 740
#define NOTE_G5 784
おまけ
マクドナルドのポテトが揚がったときの音にしたいときはnotes
を次のものに差し替えます
ポテトが揚がったときの音
constexpr int BPM = 130;
Note notes[] = {
{ NOTE_G5, 8 },
{ NOTE_F5, 8 },
{ NOTE_G5, 8 },
{ 0, 8 },
{ NOTE_G5, 8 },
{ NOTE_F5, 8 },
{ NOTE_G5, 8 },
{ 0, 8 },
{ NOTE_G5, 8 },
{ NOTE_F5, 8 },
{ NOTE_G5, 8 },
{ 0, 8 },
{ NOTE_G5, 8 },
{ NOTE_F5, 8 },
{ NOTE_G5, 8 },
{ 0, 8 },
{ NOTE_G5, 8 },
{ NOTE_F5, 8 },
{ NOTE_G5, 8 }
};
参考資料
投稿者の人気記事
-
isakon
さんが
2020/02/14
に
編集
をしました。
(メッセージ: 初版)
-
isakon
さんが
2020/02/14
に
編集
をしました。
(メッセージ: ピンアウトの画像を追加)
-
isakon
さんが
2020/02/19
に
編集
をしました。
-
isakon
さんが
2020/11/20
に
編集
をしました。
ログインしてコメントを投稿する