Sample Acts
アクトの動作を理解するため、いくつかのサンプルを用意しています。
最初にBRD_APPTWELITEとParent_MONOSTICKの解説に目を通すようにしてください。
アクトのサンプル中で以下の項目は共通の設定項目になり、以下で解説します。
const uint32_t APP_ID = 0x1234abcd;
const uint8_t CHANNEL = 13;
const char APP_FOURCHAR[] = "BAT1";
act0 から始まるアクト(Act)は、actを始める - Opening actで紹介されたものを収録しています。LEDやボタンの動作のみの単純なものですが、最初にお試しいただくことをお勧めします。
名前
内容
act0
処理の記述がないテンプレート
act1
Lチカ(LEDの点滅)
act2
タイマーを用いたLチカ
act3
2つのタイマーを用いたLチカ
act4
ボタン(スイッチ)を用いたLED点灯
IO通信(標準アプリケーションApp_Tweliteの基本機能)
App_TweLite で必要な配線と同じ配線想定したボードサポート <BRD_APPTWELITE>
を用いたサンプルです。
このサンプルは App_TweLite と通信できません。
M1を読み取り、親機か子機を決める。
DI1-DI4 の値を読み取ります。Buttons クラスにより、チャタリングの影響を小さくするため、連続で同じ値になったときにはじめて変化が通知されます。変化があったときには通信を行います。
AI1-AI4 の値を読み取ります。
DIの変化または1秒おきに、DI1-4, AI1-4, VCC の値を、自身が親機の場合は子機へ、子機の場合は親機宛に送信します。
受信したパケットの値に応じで DO1-4, PWM1-4 に設定する。
役割
例
親機
最低限 M1=GND, DI1:ボタン, DO1:LEDの配線をしておく。
子機
最低限 M1=オープン, DI1:ボタン, DO1:LEDの配線をしておく。
// use twelite mwx c++ template library
#include <TWELITE>
#include <NWK_SIMPLE>
#include <BRD_APPTWELITE>
全てのアクトで<TWELITE>
をインクルードします。ここでは、シンプルネットワーク <NWK_SIMPLE>
とボードサポート <BRD_APPTWELITE>
をインクルードしておきます。
/*** Config part */
// application ID
const uint32_t APP_ID = 0x1234abcd;
// channel
const uint8_t CHANNEL = 13;
/*** function prototype */
MWX_APIRET transmit();
void receive();
/*** application defs */
const char APP_FOURCHAR[] = "BAT1";
uint8_t u8devid = 0;
uint16_t au16AI[5];
uint8_t u8DI_BM;
サンプルアクト共通宣言
長めの処理を関数化しているため、そのプロトタイプ宣言(送信と受信)
アプリケーション中のデータ保持するための変数
void setup() {
// 変数の初期化
for(auto&& x : au16AI) x = 0xFFFF;
u8DI_BM = 0xFF;
/*** SETUP section */
// App_Twelite仕様のボード定義をシステムに登録します。
auto&& brd = the_twelite.board.use<BRD_APPTWELITE>();
// check DIP sw settings
u8devid = (brd.get_M1()) ? 0x00 : 0xFE;
// setup analogue
Analogue.setup(true, ANALOGUE::KICK_BY_TIMER0); // setup analogue read (check every 16ms)
// setup buttons
Buttons.setup(5); // init button manager with 5 history table.
// the twelite main class
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL) // set channel (pysical channel)
<< TWENET::rx_when_idle(); // open receive circuit (if not set, it can't listen packts from others)
// Register Network
auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
nwksmpl << NWK_SIMPLE::logical_id(u8devid); // set Logical ID. (0x00 means parent device)
/*** BEGIN section */
// start ADC capture
Analogue.begin(pack_bits(
BRD_APPTWELITE::PIN_AI1,
BRD_APPTWELITE::PIN_AI2,
BRD_APPTWELITE::PIN_AI3,
BRD_APPTWELITE::PIN_AI4,
PIN_ANALOGUE::VCC)); // _start continuous adc capture.
// Timer setup
Timer0.begin(32, true); // 32hz timer
// start button check
Buttons.begin(pack_bits(
BRD_APPTWELITE::PIN_DI1,
BRD_APPTWELITE::PIN_DI2,
BRD_APPTWELITE::PIN_DI3,
BRD_APPTWELITE::PIN_DI4),
5, // history count
4); // tick delta (change is detected by 5*4=20ms consequtive same values)
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- BRD_APPTWELITE(" << int(u8devid) << ") ---" << mwx::crlf;
}
大まかな流れは、各部の初期設定、各部の開始となっています。
このオブジェクトはTWENETの中核としてふるまいます。
auto&& brd = the_twelite.board.use<BRD_APPTWELITE>();
ボードの登録(このアクトでは<BRD_APPTWELITE>
を登録しています)。以下のように use の後に <>
で登録したいボードの名前を指定します。
ユニバーサル参照(auto&&
)にて得られた戻り値として、参照型でのボードオブジェクトが得られます。このオブジェクトにはボード特有の操作や定義が含まれます。
u8devid = (brd.get_M1()) ? 0x00 : 0xFE;
ここではボードオブジェクトを用いbrd.get_M1()
を呼び出すことでM1ピンの設定を読み出します。1
がスイッチをセットしGND側になっていることを示します。M1 がセットされた場合にu8devid
を0x00
に、そうでなければ0xFE
を指定しています。これはネットワーク上での役割が親機(0x00
)であるか子機(0xFE
)あるかを決定します。この値は <NWK_SIMPLE>
の設定に使用します。
// the twelite main class
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL) // set channel (pysical channel)
<< TWENET::rx_when_idle(); // open receive circuit (if not set, it can't listen packts from others)
the_twelite
に設定を反映するには <<
を用います。
TWENET::appid(APP_ID)
アプリケーションIDの指定
TWENET::channel(CHANNEL)
チャネルの指定
TWENET::rx_when_idle()
受信回路をオープンにする指定
次にネットワークを登録します。
auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
nwksmpl << NWK_SIMPLE::logical_id(u8devid);
1行目は、ボードの登録と同じ書き方で <>
には <NWK_SIMPLE>
を指定します。
2行目は、<NWK_SIMPLE>
の設定です。先ほどボードから得たM1ピンの状態によって親機アドレス(0x00
)か子機アドレス(0xFE
)を格納したu8devid
を指定します。
the_twelite.begin(); // start twelite!
setup()
関数の末尾で the_twelite.begin()
を実行しています。
ADC(アナログディジタルコンバータ)を取り扱うクラスオブジェクトです。
Analogue.setup(true, ANALOGUE::KICK_BY_TIMER0);
初期化Analogue.setup()
で行います。パラメータのtrue
はADC回路の安定までその場で待つ指定です。2番目のパラメータは、ADCの開始をTimer0に同期して行う指定です。
Analogue.begin(pack_bits(
BRD_APPTWELITE::PIN_AI1,
BRD_APPTWELITE::PIN_AI2,
BRD_APPTWELITE::PIN_AI3,
BRD_APPTWELITE::PIN_AI4,
PIN_ANALOGUE::VCC));
ADCを開始するにはAnalogue.begin()
を呼びます。パラメータはADC対象のピンに対応するビットマップです。
ビットマップを指定するのにpack_bits()
関数を用います。可変数引数の関数で、各引数には1を設定するビット位置を指定します。例えばpack_bits(1,3,5)
なら2進数で 101010
の値が戻ります。この関数はconstexpr
指定があるため、パラメータが定数のみであれば定数に展開されます。
パラメータと指定されるBRD_APPTWELITE::
にはPIN_AI1..4
が定義されています。App_Tweliteで用いるAI1..AI4に対応します。AI1=ADC1, AI2=DIO0, AI3=ADC2, AI4=DIO2 と割り当てられています。PIN_ANALOGUE::
にはADCで利用できるピンの一覧が定義されています。
DIO (ディジタル入力) の値の変化を検出します。Buttonsでは、メカ式のボタンのチャタリング(摺動)の影響を軽減するため、一定回数同じ値が検出されてから、値の変化とします。
Buttons.setup(5);
初期化は Buttons.setup()
で行います。パラメータの 5 は、値の確定に必要な検出回数ですが、設定可能な最大値を指定します。内部的にはこの数値をもとに内部メモリの確保を行っています。
Buttons.begin(pack_bits(
BRD_APPTWELITE::PIN_DI1,
BRD_APPTWELITE::PIN_DI2,
BRD_APPTWELITE::PIN_DI3,
BRD_APPTWELITE::PIN_DI4),
5, // history count
4); // tick delta
開始は Buttons.begin()
で行います。1番目のパラメータは検出対象のDIOです。BRD_APPTWELITE::
に定義されるPIN_DI1-4
(DI1-DI4) を指定しています。2番めのパラメータは状態を確定するのに必要な検出回数です。3番めのパラメータは検出間隔です。4
を指定しているので4msごとに5回連続で同じ値が検出できた時点で、HIGH, LOWの状態が確定します。
Timer0.begin(32, true); // 32hz timer
App_Twelite ではアプリケーションの制御をタイマー起点で行っているため、このアクトでも同じようにタイマー割り込み・イベントを動作させます。もちろん1msごとに動作しているシステムのTickTimerを用いても構いません。
上記の例の1番目のパラメータはタイマーの周波数で32Hzを指定しています。2番目のパラメータをtrue
にするとソフトウェア割り込みが有効になります。
Timer0.begin()
を呼び出したあと、タイマーが稼働します。
Serial オブジェクトは、初期化や開始手続きなく利用できます。
Serial << "--- BRD_APPTWELITE("
<< int(u8devid)
<< ") ---" << mwx::crlf;
上記の例では "--- BRD_APPTWELITE(254) ---\r\n"
といった文字列を表示します。ここでは10進数の数値文字列として表示するためにint(u8devid)
としています。int
型でない場合は出力の意味合いが変わりバイトの書き出しになるので注意してください。
ループ関数は TWENET ライブラリのメインループからコールバック関数として呼び出されます。ここでは、利用するオブジェクトが available になるのを待って、その処理を行うのが基本的な記述です。ここではアクトで使用されているいくつかのオブジェクトの利用について解説します。
TWENET ライブラリのメインループは、事前にFIFOキューに格納された受信パケットや割り込み情報などをイベントとして処理し、そののちloop()
が呼び出されます。loop()
を抜けた後は CPU が DOZE モードに入り、低消費電流で新たな割り込みが発生するまでは待機します。
したがってCPUが常に稼働していることを前提としたコードはうまく動作しません。
/*** loop procedure (called every event) */
void loop() {
if (Buttons.available()) {
uint32_t bp, bc;
Buttons.read(bp, bc);
u8DI_BM = uint8_t(collect_bits(bp,
BRD_APPTWELITE::PIN_DI4, // bit3
BRD_APPTWELITE::PIN_DI3, // bit2
BRD_APPTWELITE::PIN_DI2, // bit1
BRD_APPTWELITE::PIN_DI1)); // bit0
transmit();
}
if (Analogue.available()) {
au16AI[0] = Analogue.read(PIN_ANALOGUE::VCC);
au16AI[1] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI1);
au16AI[2] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI2);
au16AI[3] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI3);
au16AI[4] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI4);
}
if (Timer0.available()) {
static uint8_t u16ct;
u16ct++;
if (u8DI_BM != 0xFF && au16AI[0] != 0xFFFF) { // finished the first capture
if ((u16ct % 32) == 0) { // every 32ticks of Timer0
transmit();
}
}
}
// receive RF packet.
if (the_twelite.receiver.available()) {
receive();
}
}
DIO(ディジタルIO)の入力変化を検出したタイミングで available になり、Buttons.read()
により読み出します。
if (Buttons.available()) {
uint32_t bp, bc;
Buttons.read(bp, bc);
1番目のパラメータは、現在のDIOのHIGH/LOWのビットマップで、bit0から順番にDIO0,1,2,.. と並びます。例えば DIO12 であれば bp & (1UL << 12)
を評価すれば HIGH / LOW が判定できます。ビットが1になっているものがHIGHになります。
次にビットマップから値を取り出してu8DI_BM
に格納しています。ここではMWXライブラリで用意したcollect_bits()
関数を用いています。
u8DI_BM = uint8_t(collect_bits(bp,
BRD_APPTWELITE::PIN_DI4, // bit3
BRD_APPTWELITE::PIN_DI3, // bit2
BRD_APPTWELITE::PIN_DI2, // bit1
BRD_APPTWELITE::PIN_DI1)); // bit0
/* collect_bits は以下の処理を行います。
u8DI_BM = 0;
if (bp & (1UL << BRD_APPTWELITE::PIN_DI1)) u8DI_BM |= 1;
if (bp & (1UL << BRD_APPTWELITE::PIN_DI2)) u8DI_BM |= 2;
if (bp & (1UL << BRD_APPTWELITE::PIN_DI3)) u8DI_BM |= 4;
if (bp & (1UL << BRD_APPTWELITE::PIN_DI4)) u8DI_BM |= 8;
*/
collect_bits()
は、上述のpack_bits()
と同様のビット位置の整数値を引数とします。可変数引数の関数で、必要な数だけパラメータを並べます。上記の処理では bit0 は DI1、bit1 は DI2、bit2 は DI3、bit3 は DI4の値としてu8DI_BMに格納しています。
App_Twelite では、DI1から4に変化があった場合に無線送信しますので、Buttons.available()
を起点に送信処理を行います。transmit()
処理の内容は後述します。
transmit();
ADCのアナログディジタル変換が終了した直後のloop()
で available になります。次の ADC が開始するまでは、データは直前に取得されたものとして読み出すことが出来ます。
if (Analogue.available()) {
au16AI[0] = Analogue.read(PIN_ANALOGUE::VCC);
au16AI[1] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI1);
au16AI[2] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI2);
au16AI[3] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI3);
au16AI[4] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI4);
}
ADC値を読むには Analogue.read()
または Analogue.read_raw()
メソッドを用います。read()
はmVに変換した値、read_raw()
は 0..1023 のADC値となります。パラメータにはADCのピン番号を指定します。ADCのピン番号はPIN_ANALOGUE::
やBRD_APPTWELITE::
に定義されているので、こちらを利用しています。
周期的に実行されるADCの値は、タイミングによってはavailable通知前のより新しい値が読み出されることがあります。
このアクトでは32Hzと比較的ゆっくりの周期で処理しているため、available判定直後に処理すれば問題にはなりませんが、変換周期が短い場合、loop()
中で比較的長い時間のかかる処理をしている場合は注意が必要です。
Analogue
には、変換終了後に割り込みハンドラ内から呼び出されるコールバック関数を指定することが出来ます。例えば、このコールバック関数からFIFOキューに値を格納する処理を行い、アプリケーションループ内ではキューの値を逐次読み出すといった非同期処理を行います。
Timer0
は32Hzで動作しています。タイマー割り込みが発生直後の loop()
で available になります。つまり、秒32回の処理をします。ここでは、ちょうど1秒になったところで送信処理をしています。
if (Timer0.available()) {
static uint8_t u16ct;
u16ct++;
if (u8DI_BM != 0xFF && au16AI[0] != 0xFFFF) { // finished the first capture
if ((u16ct % 32) == 0) { // every 32ticks of Timer0
transmit();
}
}
}
AppTweliteでは約1秒おきに定期送信を行っています。Timer0
がavailableになったときにu16ct
をインクリメントします。このカウンタ値をもとに、32回カウントが終わればtransmit()
を呼び出し無線パケットを送信しています。
u8DI_BM
とau16AI[]
の値判定は、初期化直後かどうかの判定です。まだDI1..DI4やAI1..AI4の値が格納されていない場合は何もしません。
受信パケットがある場合の処理です。
if (the_twelite.receiver.available()) {
receive();
}
無線パケットを受信後のloop()
ではthe_twelite.receiver.available()
がtrue
を返します。受信パケットの取り扱いについては receive()
関数の解説で行います。
TWENETがパケットを受信してから時間をたってからの受信パケットの参照は安全ではありません。
内部のデータが新しい受信パケットの内容に上書きされ、この新しい内容を参照することになります。availableになってから速やかに処理するようにloop()
を記述してください。内部的には、1パケット分余分に保持できる余裕はあります。
無線パケットの送信要求をTWENETに行う関数です。本関数が終了した時点では、まだ無線パケットの処理は行われません。実際に送信が完了するのは、送信パラメータ次第ですが、数ms後以降になります。ここでは代表的な送信要求方法について解説します。
MWX_APIRET transmit() {
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
Serial << "..DI=" << format("%04b ", u8DI_BM);
Serial << format("ADC=%04d/%04d/%04d/%04d ", au16AI[1], au16AI[2], au16AI[3], au16AI[4]);
Serial << "Vcc=" << format("%04d ", au16AI[0]);
Serial << " --> transmit" << mwx::crlf;
// set tx packet behavior
pkt << tx_addr(u8devid == 0 ? 0xFE : 0x00) // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
<< tx_retry(0x1) // set retry (0x1 send two times in total)
<< tx_packet_delay(0,50,10); // send packet w/ delay (send first packet with randomized delay from 100 to 200ms, repeat every 20ms)
// prepare packet payload
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(APP_FOURCHAR, 4) // string should be paired with length explicitly.
, uint8_t(u8DI_BM)
);
for (auto&& x : au16AI) {
pack_bytes(pkt.get_payload(), uint16_t(x)); // adc values
}
// do transmit
return pkt.transmit();
}
return MWX_APIRET(false, 0);
}
MWX_APIRET transmit()
MWX_APIRET
はuint32_t
型のデータメンバを持つ戻り値を取り扱うクラスです。MSB(bit31)が成功失敗、それ以外が戻り値として利用するものです。
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
ネットワークオブジェクトをthe_twelite.network.use<NWK_SIMPLE>()
で取得します。そのオブジェクトを用いて.prepare_tx_packet()
によりpkt
オブジェクトを取得します。
ここではif文の条件判定式の中で宣言しています。宣言したpkt
オブジェクトはif節の終わりまで有効です。pktオブジェクトはbool型の応答をし、ここではTWENETの送信要求キューに空きがあって送信要求を受け付ける場合にtrue
、空きがない場合にfalse
となります。
pkt << tx_addr(u8devid == 0 ? 0xFE : 0x00) // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
<< tx_retry(0x1) // set retry (0x3 send four times in total)
<< tx_packet_delay(0,50,10); // send packet w/ delay (send first packet with randomized delay from 100 to 200ms, repeat every 20ms)
パケットの設定はthe_twelite
の初期化設定のように<<
演算子を用いて行います。
tx_addr()
パラメータに送信先アドレスを指定します。0x00
なら自分が子機で親機宛に、0xFE
なら自分が親機で任意の子機宛のブロードキャストという意味です。
tx_retry()
パラメータに再送回数を指定します。例の1
は再送回数が1回、つまり合計2回パケットを送ります。無線パケット1回のみの送信では条件が良くても数%程度の失敗はあります。
tx_packet_delay()
送信遅延を設定します。一つ目のパラメータは、送信開始までの最低待ち時間、2番目が最長の待ち時間です。この場合は送信要求を発行後におよそ0msから50msの間で送信を開始します。3番目が再送間隔です。最初のパケットが送信されてから10ms置きに再送を行うという意味です。
ペイロードは積載物という意味ですが、無線パケットでは「送りたいデータ本体」という意味でよく使われます。無線パケットのデータにはデータ本体以外にもアドレス情報などいくつかの補助情報が含まれます。
送受信を正しく行うために、データペイロードのデータ並び順を意識するようにしてください。ここでは以下のようなデータ順とします。このデータ順に合わせてデータペイロードを構築します。
# 先頭バイトのインデックス: データ型 : バイト数 : 内容
00: uint8_t[4] : 4 : 4文字識別子
04: uint8_t : 1 : DI1..4のビットマップ
06: uint16_t : 2 : Vccの電圧値
08: uint16_t : 2 : AI1のADC値 (0..1023)
10: uint16_t : 2 : AI2のADC値 (0..1023)
12: uint16_t : 2 : AI3のADC値 (0..1023)
14: uint16_t : 2 : AI4のADC値 (0..1023)
上記のデータペイロードのデータ構造を実際に構築してみます。データペイロードは pkt.get_payload()
により simplbuf<uint8_t>
型のコンテナとして参照できます。このコンテナに上記の仕様に基づいてデータを構築します。
auto&& payl = pkt.get_payload();
payl.reserve(16); // 16バイトにリサイズ
payl[00] = APP_FOURCHAR[0];
payl[01] = APP_FOURCHAR[1];
...
payl[08] = (au16AI[0] & 0xFF00) >> 8; //Vcc
payl[09] = (au16AI[0] & 0xFF);
...
payl[14] = (au16AI[4] & 0xFF00) >> 8; // AI4
payl[15] = (au16AI[4] & 0xFF);
上記のように記述できますがMWXライブラリでは、データペイロード構築のための補助関数pack_bytes()
を用意しています。
// prepare packet payload
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(APP_FOURCHAR, 4) // string should be paired with length explicitly.
, uint8_t(u8DI_BM)
);
for (auto&& x : au16AI) {
pack_bytes(pkt.get_payload(), uint16_t(x)); // adc values
}
pack_bytes
の最初のパラメータはコンテナを指定します。この場合はpkt.get_payload()
です。
そのあとのパラメータは可変数引数でpack_bytes
で対応する型の値を必要な数だけ指定します。pack_bytes
は内部で.push_back()
メソッドを呼び出して末尾に指定した値を追記していきます。
3行目のmake_pair()
は標準ライブラリの関数でstd::pair
を生成します。文字列型の混乱(具体的にはペイロードの格納時にヌル文字を含めるか含めないか)を避けるための指定です。make_pair()
の1番目のパラメータに文字列型(char*
やuint8_t*
型、uint8_t[]
など)を指定します。2番目のパラメータはペイロードへの格納バイト数です。
4行目はuint8_t
型でDI1..DI4のビットマップを書き込みます。
7-9行目ではau16AI
配列の値を順に書き込んでいます。この値はuint16_t
型で2バイトですが、ビッグエンディアンの並びで書き込みます。
これでパケットの準備は終わりです。あとは、送信要求を行います。
return pkt.transmit();
パケットを送信するにはpkt
オブジェクトのpkt.transmit()
メソッドを用います。戻り値としてMWX_APIRET
型を返していますが、このアクトでは使っていません。
パケットの受信が確認できた(つまりthe_twelite.receiver.available()
がtrue
になった)ときの処理です。ここでは、相手方から伝えられたDI1..DI4の値とAI1..AI4の値を、自身のDO1..DO4とPWM1..PWM4に設定します。
void receive() {
auto&& rx = the_twelite.receiver.read();
// expand the packet payload
char fourchars[5]{};
auto&& np = expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
, make_pair((uint8_t*)fourchars, 4) // 4bytes of msg
);
// check header
if (strncmp(APP_FOURCHAR, fourchars, 4)) { return; }
// read rest of payload
uint8_t u8DI_BM_remote = 0xff;
uint16_t au16AI_remote[5];
expand_bytes(np, rx.get_payload().end()
, u8DI_BM_remote
, au16AI_remote[0]
, au16AI_remote[1]
, au16AI_remote[2]
, au16AI_remote[3]
, au16AI_remote[4]
);
Serial << format("DI:%04b", u8DI_BM_remote & 0x0F);
for (auto&& x : au16AI_remote) {
Serial << format("/%04d", x);
}
Serial << mwx::crlf;
// set local DO
digitalWrite(BRD_APPTWELITE::PIN_DO1, (u8DI_BM_remote & 1) ? HIGH : LOW);
digitalWrite(BRD_APPTWELITE::PIN_DO2, (u8DI_BM_remote & 2) ? HIGH : LOW);
digitalWrite(BRD_APPTWELITE::PIN_DO3, (u8DI_BM_remote & 4) ? HIGH : LOW);
digitalWrite(BRD_APPTWELITE::PIN_DO4, (u8DI_BM_remote & 8) ? HIGH : LOW);
// set local PWM : duty is set 0..1024, so 1023 is set 1024.
Timer1.change_duty(au16AI_remote[1] == 1023 ? 1024 : au16AI_remote[1]);
Timer2.change_duty(au16AI_remote[2] == 1023 ? 1024 : au16AI_remote[2]);
Timer3.change_duty(au16AI_remote[3] == 1023 ? 1024 : au16AI_remote[3]);
Timer4.change_duty(au16AI_remote[4] == 1023 ? 1024 : au16AI_remote[4]);
}
まず受信パケットのデータrx
を取得します。rx
からアドレス情報やデータペイロードにアクセスします。
auto&& rx = the_twelite.receiver.read();
次の行では、受信パケットデータには、送信元のアドレス(32bitのロングアドレスと8bitの論理アドレス)などの情報を参照しています。
Serial << format("..receive(%08x/%d) : ",
rx.get_addr_src_long(), rx.get_addr_src_lid());
MWXライブラリにはtransmit()
の時に使ったpack_bytes()
の対になる関数expand_bytes()
が用意されています。
char fourchars[5]{};
auto&& np = expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
, make_pair((uint8_t*)fourchars, 4) // 4bytes of msg
);
1行目ではデータ格納のためのchar
型の配列を宣言しています。サイズが5バイトなのは末尾にヌル文字を含め、文字出力などでの利便性を挙げるためです。末尾の{}
は初期化の指定で、5バイト目を0にすれば良いのですが、ここでは配列全体をデフォルトの方法で初期化、つまり0
にしています。
2行目でexpand_bytes()
により4バイト文字列を取り出しています。パラメータにコンテナ型を指定しない理由は、この続きを読み出すための読み出し位置を把握する必要があるためです。1番目のパラメータでコンテナの先頭イテレータ(uint8_t*
ポインタ)を指定します。.begin()
メソッドにより取得できます。2番目のパラメータはコンテナの末尾の次を指すイテレータで.end()
メソッドで取得できます。2番目はコンテナの末尾を超えた読み出しを行わないようにするためです。
3番目に読み出す変数を指定しますが、ここでもmake_pair
によって文字列配列とサイズのペアを指定します。
読み出した4バイト文字列の識別子が、このアクトで指定した識別子と異なる場合は、このパケットを処理しません。
if (strncmp(APP_FOURCHAR, fourchars, 4)) { return; }
続いて、データ部分の取得です。DI1..DI4の値とAI1..AI4の値を別の変数に格納します。
// read rest of payload
uint8_t u8DI_BM_remote = 0xff;
uint16_t au16AI_remote[5];
expand_bytes(np, rx.get_payload().end()
, u8DI_BM_remote
, au16AI_remote[0]
, au16AI_remote[1]
, au16AI_remote[2]
, au16AI_remote[3]
, au16AI_remote[4]
);
先ほどのexpand_bytes()
の戻り値np
を1番目のパラメータにしています。先に読み取った4バイト文字列識別子の次から読み出す指定です。2番目のパラメータは同様です。
3番目以降のパラメータはデータペイロードの並びに一致した型の変数を、送り側のデータ構造と同じ順番で並べています。この処理が終われば、指定した変数にペイロードから読み出した値が格納されます。
確認のためシリアルポートへ出力します。
Serial << format("DI:%04b", u8DI_BM_remote & 0x0F);
for (auto&& x : au16AI_remote) {
Serial << format("/%04d", x);
}
Serial << mwx::crlf;
数値のフォーマット出力が必要になるのでformat()
を用いています。>>
演算子向けにprintf()
と同じ構文を利用できるようにしたヘルパークラスですが、引数の数が4つまでに制限されています。(Serial.printfmt()
には引数の数の制限がありません。)
1行目の "DI:%04b"
は"DI:0010"
のようにDI1..DI4のビットマップを4桁で表示します。3行目の"/%04d"
は"/3280/0010/0512/1023/1023"
のように Vcc/AI1..AI4の値を順に整数で出力します。5行目のmwx::crlf
は改行文字列を出力します。
これで必要なデータの展開が終わったので、あとはボード上のDO1..DO4とPWM1..PWM4の値を変更します。
// set local DO
digitalWrite(BRD_APPTWELITE::PIN_DO1, (u8DI_BM_remote & 1) ? HIGH : LOW);
digitalWrite(BRD_APPTWELITE::PIN_DO2, (u8DI_BM_remote & 2) ? HIGH : LOW);
digitalWrite(BRD_APPTWELITE::PIN_DO3, (u8DI_BM_remote & 4) ? HIGH : LOW);
digitalWrite(BRD_APPTWELITE::PIN_DO4, (u8DI_BM_remote & 8) ? HIGH : LOW);
// set local PWM : duty is set 0..1024, so 1023 is set 1024.
Timer1.change_duty(au16AI_remote[1] == 1023 ? 1024 : au16AI_remote[1]);
Timer2.change_duty(au16AI_remote[2] == 1023 ? 1024 : au16AI_remote[2]);
Timer3.change_duty(au16AI_remote[3] == 1023 ? 1024 : au16AI_remote[3]);
Timer4.change_duty(au16AI_remote[4] == 1023 ? 1024 : au16AI_remote[4]);
digitalWrite()
はディジタル出力の値を変更します。1番目のパラメータはピン番号で、2番目はHIGH
(Vccレベル)かLOW
(GNDレベル)を指定します。
Timer?.change_duty()
はPWM出力のデューティ比を変更します。パラメータにデューティ比 0..1024 を指定します。最大値が1023でないことに注意してください(ライブラリ内で実行される割り算のコストが大きいため2のべき乗である1024を最大値としています)。0
にするとGNDレベル、1024
にするとVccレベル相当の出力になります。
2台のシリアル接続しているTWELITEの片方からPING(ピン)の無線パケットを送信すると、他方からPONG(ポン)の無線パケットが返ってきます。
いずれかを2台。
TWELITE R でUART接続されているTWELITE DIPなど
// use twelite mwx c++ template library
#include <TWELITE>
#include <NWK_SIMPLE>
全てのアクトで<TWELITE>
をインクルードします。ここでは、シンプルネットワーク <NWK_SIMPLE>
をインクルードしておきます。
// application ID
const uint32_t APP_ID = 0x1234abcd;
// channel
const uint8_t CHANNEL = 13;
// DIO pins
const uint8_t PIN_BTN = 12;
/*** function prototype */
void vTransmit(const char* msg, uint32_t addr);
/*** application defs */
// packet message
const int MSG_LEN = 4;
const char MSG_PING[] = "PING";
const char MSG_PONG[] = "PONG";
サンプルアクト共通宣言
長めの処理を関数化しているため、そのプロトタイプ宣言(送信と受信)
アプリケーション中のデータ保持するための変数
void setup() {
/*** SETUP section */
Buttons.setup(5); // init button manager with 5 history table.
Analogue.setup(true, 50); // setup analogue read (check every 50ms)
// the twelite main class
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL) // set channel (pysical channel)
<< TWENET::rx_when_idle(); // open receive circuit (if not set, it can't listen packts from others)
// Register Network
auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
nwksmpl << NWK_SIMPLE::logical_id(0xFE) // set Logical ID. (0xFE means a child device with no ID)
<< NWK_SIMPLE::repeat_max(3); // can repeat a packet up to three times. (being kind of a router)
/*** BEGIN section */
Buttons.begin(pack_bits(PIN_BTN), 5, 10); // check every 10ms, a change is reported by 5 consequent values.
Analogue.begin(pack_bits(PIN_ANALOGUE::A1, PIN_ANALOGUE::VCC)); // _start continuous adc capture.
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- PingPong sample (press 't' to transmit) ---" << mwx::crlf;
}
大まかな流れは、各部の初期設定、各部の開始となっています。
このオブジェクトはTWENETを操作するための中核クラスオブジェクトです。
// the twelite main class
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL) // set channel (pysical channel)
<< TWENET::rx_when_idle(); // open receive circuit (if not set, it can't listen packts from others)
the_twelite
に設定を反映するには <<
を用います。
TWENET::appid(APP_ID)
アプリケーションIDの指定
TWENET::channel(CHANNEL)
チャネルの指定
TWENET::rx_when_idle()
受信回路をオープンにする指定
次にネットワークを登録します。
auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
nwksmpl << NWK_SIMPLE::logical_id(0xFE);
<< NWK_SIMPLE::repeat_max(3);
1行目は、ボードの登録と同じ書き方で <>
には <NWK_SIMPLE>
を指定します。
2行目は、<NWK_SIMPLE>
の設定で、0xFE
(ID未設定の子機)という指定を行います。
3行目は、中継回数の最大値を指定しています。この解説では中継には触れませんが、複数台で動作させたときにパケットの中継が行われます。
the_twelite.begin(); // start twelite!
setup()
関数の末尾で the_twelite.begin()
を実行しています。
ADC(アナログディジタルコンバータ)を取り扱うクラスオブジェクトです。
Analogue.setup(true);
初期化Analogue.setup()
で行います。パラメータのtrue
はADC回路の安定までその場で待つ指定です。
Analogue.begin(pack_bits(PIN_ANALOGUE::A1, PIN_ANALOGUE::VCC), 50);
ADCを開始するにはAnalogue.begin()
を呼びます。パラメータはADC対象のピンに対応するビットマップです。
ビットマップを指定するのにpack_bits()
関数を用います。可変数引数の関数で、各引数には1を設定するビット位置を指定します。例えばpack_bits(1,3,5)
なら2進数で 101010
の値が戻ります。この関数はconstexpr
指定があるため、パラメータが定数のみであれば定数に展開されます。
パラメータにはPIN_ANALOGUE::A1
(ADC0)とPIN_ANALOGUE::VCC
(モジュール電源電圧)が指定されています。
2番目のパラメータには50
が指定されています。ADCの動作はデフォルトではTickTimerで開始されていて、
DIO (ディジタル入力) の値の変化を検出します。Buttonsでは、メカ式のボタンのチャタリング(摺動)の影響を軽減するため、一定回数同じ値が検出されてから、値の変化とします。
Buttons.setup(5);
初期化は Buttons.setup()
で行います。パラメータの 5 は、値の確定に必要な検出回数ですが、設定可能な最大値を指定します。内部的にはこの数値をもとに内部メモリの確保を行っています。
Buttons.begin(pack_bits(PIN_BTN),
5, // history count
10); // tick delta
開始は Buttons.begin()
で行います。1番目のパラメータは検出対象のDIOです。BRD_APPTWELITE::
に定義されるPIN_BTN
(12) を指定しています。2番めのパラメータは状態を確定するのに必要な検出回数です。3番めのパラメータは検出間隔です。10
を指定しているので10msごとに5回連続で同じ値が検出できた時点で、HIGH, LOWの状態が確定します。
Serial オブジェクトは、初期化や開始手続きなく利用できます。
Serial << "--- PingPong sample (press 't' to transmit) ---" << mwx::crlf;
シリアルポートへの文字列出力を行います。mwx::crlf
は改行文字です。
ループ関数は TWENET ライブラリのメインループからコールバック関数として呼び出されます。ここでは、利用するオブジェクトが available になるのを待って、その処理を行うのが基本的な記述です。ここではアクトで使用されているいくつかのオブジェクトの利用について解説します。
TWENET ライブラリのメインループは、事前にFIFOキューに格納された受信パケットや割り込み情報などをイベントとして処理し、そののちloop()
が呼び出されます。loop()
を抜けた後は CPU が DOZE モードに入り、低消費電流で新たな割り込みが発生するまでは待機します。
したがってCPUが常に稼働していることを前提としたコードはうまく動作しません。
void loop() {
// read from serial
while(Serial.available()) {
int c = Serial.read();
Serial << mwx::crlf << char(c) << ':';
switch(c) {
case 't':
vTransmit(MSG_PING, 0xFF);
break;
default:
break;
}
}
// Button press
if (Buttons.available()) {
uint32_t btn_state, change_mask;
Buttons.read(btn_state, change_mask);
// Serial << fmt("<BTN %b:%b>", btn_state, change_mask);
if (!(change_mask & 0x80000000) && (btn_state && (1UL << PIN_BTN))) {
// PIN_BTN pressed
vTransmit(MSG_PING, 0xFF);
}
}
// receive RF packet.
while (the_twelite.receiver.available()) {
auto&& rx = the_twelite.receiver.read();
// rx >> Serial; // debugging (display longer packet information)
uint8_t msg[MSG_LEN];
uint16_t adcval, volt;
uint32_t timestamp;
// expand packet payload (shall match with sent packet data structure, see pack_bytes())
expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
, msg // 4bytes of msg
// also can be -> std::make_pair(&msg[0], MSG_LEN)
, adcval // 2bytes, A1 value [0..1023]
, volt // 2bytes, Module VCC[mV]
, timestamp // 4bytes of timestamp
);
// if PING packet, respond pong!
if (!strncmp((const char*)msg, "PING", MSG_LEN)) {
// transmit a PONG packet with specifying the address.
vTransmit(MSG_PONG, rx.get_psRxDataApp()->u32SrcAddr);
}
// display the packet
Serial << format("<RX ad=%x/lq=%d/ln=%d/sq=%d:" // note: up to 4 args!
, rx.get_psRxDataApp()->u32SrcAddr
, rx.get_lqi()
, rx.get_length()
, rx.get_psRxDataApp()->u8Seq
)
<< format(" %s AD=%d V=%d TS=%dms>" // note: up to 4 args!
, msg
, adcval
, volt
, timestamp
)
<< mwx::crlf
<< mwx::flush;
}
}
while(Serial.available()) {
int c = Serial.read();
Serial << mwx::crlf << char(c) << ':';
switch(c) {
case 't':
vTransmit(MSG_PING, 0xFF);
break;
default:
break;
}
}
Serial.available()
がtrue
の間はシリアルポートからの入力があります。内部のFIFOキューに格納されるためある程度の余裕はありますが、速やかに読み出すようにします。データの読み出しはSerial.read()
を呼びます。
ここでは't'
キーの入力に対応してvTransmit()
関数を呼び出しPINGパケットを送信します。
DIO(ディジタルIO)の入力変化を検出したタイミングで available になり、Buttons.read()
により読み出します。
if (Buttons.available()) {
uint32_t btn_state, change_mask;
Buttons.read(btn_state, change_mask);
1番目のパラメータは、現在のDIOのHIGH/LOWのビットマップで、bit0から順番にDIO0,1,2,.. と並びます。例えば DIO12 であれば btn_state & (1UL << 12)
を評価すれば HIGH / LOW が判定できます。ビットが1になっているものがHIGHになります。
// Serial << fmt("<BTN %b:%b>", btn_state, change_mask);
if (!(change_mask & 0x80000000) && (btn_state && (1UL << PIN_BTN))) {
// PIN_BTN pressed
vTransmit(MSG_PING, 0xFF);
初回確定以外の場合かつPIN_BTNのボタンが離されたタイミングでvTransmit()
を呼び出しています。押したタイミングにするには(!(btn_state && (1UL << PIN_BTN)))
のように条件を論理反転します。
Timer0
は32Hzで動作しています。タイマー割り込みが発生直後の loop()
で available になります。つまり、秒32回の処理をします。ここでは、ちょうど1秒になったところで送信処理をしています。
if (Timer0.available()) {
static uint8_t u16ct;
u16ct++;
if (u8DI_BM != 0xFF && au16AI[0] != 0xFFFF) { // finished the first capture
if ((u16ct % 32) == 0) { // every 32ticks of Timer0
transmit();
}
}
}
AppTweliteでは約1秒おきに定期送信を行っています。Timer0
がavailableになったときにu16ct
をインクリメントします。このカウンタ値をもとに、32回カウントが終わればtransmit()
を呼び出し無線パケットを送信しています。
u8DI_BM
とau16AI[]
の値判定は、初期化直後かどうかの判定です。まだDI1..DI4やAI1..AI4の値が格納されていない場合は何もしません。
受信パケットがある場合の処理です。
while (the_twelite.receiver.available()) {
auto&& rx = the_twelite.receiver.read();
uint8_t msg[MSG_LEN];
uint16_t adcval, volt;
uint32_t timestamp;
// expand packet payload (shall match with sent packet data structure, see pack_bytes())
expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
, msg // 4bytes of msg
// also can be -> std::make_pair(&msg[0], MSG_LEN)
, adcval // 2bytes, A1 value [0..1023]
, volt // 2bytes, Module VCC[mV]
, timestamp // 4bytes of timestamp
);
// if PING packet, respond pong!
if (!strncmp((const char*)msg, "PING", MSG_LEN)) {
// transmit a PONG packet with specifying the address.
vTransmit(MSG_PONG, rx.get_psRxDataApp()->u32SrcAddr);
}
// display the packet
Serial << format("<RX ad=%x/lq=%d/ln=%d/sq=%d:" // note: up to 4 args!
, rx.get_psRxDataApp()->u32SrcAddr
, rx.get_lqi()
, rx.get_length()
, rx.get_psRxDataApp()->u8Seq
)
<< format(" %s AD=%d V=%d TS=%dms>" // note: up to 4 args!
, msg
, adcval
, volt
, timestamp
)
<< mwx::crlf
<< mwx::flush;
}
無線パケットを受信後のloop()
ではthe_twelite.receiver.available()
がtrue
を返します。受信パケットの取り扱いについては receive()
関数の解説で行います。
TWENETがパケットを受信してから時間をたってからの受信パケットの参照は安全ではありません。
内部のデータが新しい受信パケットの内容に上書きされ、この新しい内容を参照することになります。availableになってから速やかに処理するようにloop()
を記述してください。内部的には、1パケット分余分に保持できる余裕はあります。
まず受信パケットのデータrx
を取得します。rx
からアドレス情報やデータペイロードにアクセスします。
while (the_twelite.receiver.available()) {
auto&& rx = the_twelite.receiver.read();
次の行では、受信パケットデータには、送信元のアドレス(32bitのロングアドレスと8bitの論理アドレス)などの情報を参照しています。
Serial << format("..receive(%08x/%d) : ",
rx.get_addr_src_long(), rx.get_addr_src_lid());
MWXライブラリにはtransmit()
の時に使ったpack_bytes()
の対になる関数expand_bytes()
が用意されています。
uint8_t msg[MSG_LEN];
uint16_t adcval, volt;
uint32_t timestamp;
// expand packet payload (shall match with sent packet data structure, see pack_bytes())
expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
, msg // 4bytes of msg
// also can be -> std::make_pair(&msg[0], MSG_LEN)
, adcval // 2bytes, A1 value [0..1023]
, volt // 2bytes, Module VCC[mV]
, timestamp // 4bytes of timestamp
);
1行目から3行目までは、データを格納する変数を指定しています。
6行目でexpand_bytes()
によりパケットのペイロードのデータを変数に格納します。1番目のパラメータでコンテナの先頭イテレータ(uint8_t*
ポインタ)を指定します。.begin()
メソッドにより取得できます。2番目のパラメータはコンテナの末尾の次を指すイテレータで.end()
メソッドで取得できます。2番目はコンテナの末尾を超えた読み出しを行わないようにするためです。
3番目以降のパラメータに変数を列挙します。列挙した順番にペイロードの読み出しとデータ格納が行われます。
msg
に読み出した4バイト文字列の識別子が"PING"
の場合はPONGメッセージを送信する処理です。
if (!strncmp((const char*)msg, "PING", MSG_LEN)) {
vTransmit(MSG_PONG, rx.get_psRxDataApp()->u32SrcAddr);
}
続いて到着したパケット情報を表示します。
Serial << format("<RX ad=%x/lq=%d/ln=%d/sq=%d:" // note: up to 4 args!
, rx.get_psRxDataApp()->u32SrcAddr
, rx.get_lqi()
, rx.get_length()
, rx.get_psRxDataApp()->u8Seq
)
<< format(" %s AD=%d V=%d TS=%dms>" // note: up to 4 args!
, msg
, adcval
, volt
, timestamp
)
<< mwx::crlf
<< mwx::flush;
数値のフォーマット出力が必要になるのでformat()
を用いています。>>
演算子向けにprintf()と同じ構文を利用できるようにしたヘルパークラスですが、引数の数が4つまでに制限されています。(Serial.printfmt()
には引数の数の制限がありません。)
mwx::crlfは改行文字(CR LF)を、mwx::flushは出力完了待ちを指定します。
無線パケットの送信要求をTWENETに行う関数です。本関数が終了した時点では、まだ無線パケットの処理は行われません。実際に送信が完了するのは、送信パラメータ次第ですが、数ms後以降になります。ここでは代表的な送信要求方法について解説します。
void vTransmit(const char* msg, uint32_t addr) {
Serial << "vTransmit()" << mwx::crlf;
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
// set tx packet behavior
pkt << tx_addr(addr) // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
<< tx_retry(0x3) // set retry (0x3 send four times in total)
<< tx_packet_delay(100,200,20); // send packet w/ delay (send first packet with randomized delay from 100 to 200ms, repeat every 20ms)
// prepare packet payload
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(msg, MSG_LEN) // string should be paired with length explicitly.
, uint16_t(analogRead(PIN_ANALOGUE::A1)) // possible numerical values types are uint8_t, uint16_t, uint32_t. (do not put other types)
, uint16_t(analogRead_mv(PIN_ANALOGUE::VCC)) // A1 and VCC values (note: alalog read is valid after the first (Analogue.available() == true).)
, uint32_t(millis()) // put timestamp here.
);
// do transmit
pkt.transmit();
}
}
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
ネットワークオブジェクトをthe_twelite.network.use<NWK_SIMPLE>()
で取得します。そのオブジェクトを用いて.prepare_tx_packet()
によりpkt
オブジェクトを取得します。
ここではif文の条件判定式の中で宣言しています。宣言したpkt
オブジェクトはif節の終わりまで有効です。pktオブジェクトはbool型の応答をし、ここではTWENETの送信要求キューに空きがあって送信要求を受け付ける場合にtrue
、空きがない場合にfalse
となります。
pkt << tx_addr(addr) // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
<< tx_retry(0x3) // set retry (0x3 send four times in total)
<< tx_packet_delay(100,200,20); // send packet w/ delay (send first packet with randomized delay from 100 to 200ms, repeat every 20ms)
パケットの設定はthe_twelite
の初期化設定のように<<
演算子を用いて行います。
tx_addr()
パラメータに送信先アドレスを指定します。0x00
なら自分が子機で親機宛に、0xFE
なら自分が親機で任意の子機宛のブロードキャストという意味です。
tx_retry()
パラメータに再送回数を指定します。例の3
は再送回数が3回、つまり合計4回パケットを送ります。無線パケット1回のみの送信では条件が良くても数%程度の失敗はあります。
tx_packet_delay()
送信遅延を設定します。一つ目のパラメータは、送信開始までの最低待ち時間、2番目が最長の待ち時間です。この場合は送信要求を発行後におよそ100msから200msの間で送信を開始します。3番目が再送間隔です。最初のパケットが送信されてから20ms置きに再送を行うという意味です。
ペイロードは積載物という意味ですが、無線パケットでは「送りたいデータ本体」という意味でよく使われます。無線パケットのデータにはデータ本体以外にもアドレス情報などいくつかの補助情報が含まれます。
送受信を正しく行うために、データペイロードのデータ並び順を意識するようにしてください。ここでは以下のようなデータ順とします。このデータ順に合わせてデータペイロードを構築します。
# 先頭バイトのインデックス: データ型 : バイト数 : 内容
00: uint8_t[4] : 4 : 4文字識別子
08: uint16_t : 2 : AI1のADC値 (0..1023)
06: uint16_t : 2 : Vccの電圧値 (2000..3600)
10: uint32_t : 4 : millis()システム時間
上記のデータペイロードのデータ構造を実際に構築してみます。データペイロードは pkt.get_payload()
により simplbuf<uint8_t>
型のコンテナとして参照できます。このコンテナに上記の仕様に基づいてデータを構築します。
上記のように記述できますがMWXライブラリでは、データペイロード構築のための補助関数pack_bytes()
を用意しています。
// prepare packet payload
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(msg, MSG_LEN) // string should be paired with length explicitly.
, uint16_t(analogRead(PIN_ANALOGUE::A1)) // possible numerical values types are uint8_t, uint16_t, uint32_t. (do not put other types)
, uint16_t(analogRead_mv(PIN_ANALOGUE::VCC)) // A1 and VCC values (note: alalog read is valid after the first (Analogue.available() == true).)
, uint32_t(millis()) // put timestamp here.
);
pack_bytes
の最初のパラメータはコンテナを指定します。この場合はpkt.get_payload()
です。
そのあとのパラメータは可変数引数でpack_bytes
で対応する型の値を必要な数だけ指定します。pack_bytes
は内部で.push_back()
メソッドを呼び出して末尾に指定した値を追記していきます。
3行目のmake_pair()
は標準ライブラリの関数でstd::pair
を生成します。文字列型の混乱(具体的にはペイロードの格納時にヌル文字を含めるか含めないか)を避けるための指定です。make_pair()
の1番目のパラメータに文字列型(char*
やuint8_t*
型、uint8_t[]
など)を指定します。2番目のパラメータはペイロードへの格納バイト数です。
4,5,6行目は、数値型の値 (uint8_t
, uint16_t
, uint32_t
)を格納します。符号付などの数値型、char
型など同じ数値型であっても左記の3つの型にキャストして投入します。
analogRead()
とanalogRead_mv()
は、ADCの結果を取得するものです。前者はADC値(0..1023)、後者は電圧[mv](0..2470)となります。モジュールの電源電圧は内部的に分圧抵抗の値を読んでいるためその変換を行うadalogRead_mv()
を利用しています。
これでパケットの準備は終わりです。あとは、送信要求を行います。
pkt.transmit();
パケットを送信するにはpkt
オブジェクトのpkt.transmit()
メソッドを用います。
親機アプリケーション(MONOSTICK用)
MONOSTICKを親機として使用するアクトです。子機からのパケットのデータペイロードをシリアルポートに出力します。
このアクトの解説の前にBRD_APPTWELITEの解説をご覧ください。
サンプルアクトの子機からのパケットを受信して、アスキー形式で出力する。
役割
例
子機
サンプルアクトの子機設定 (例: BLUE PAL または RED PAL +AMBIENT SENSE PALにPAL_AMBアクトを書き込んだもの)
// use twelite mwx c++ template library
#include <TWELITE>
#include <NWK_SIMPLE>
#include <MONOSTICK>
MONOSTICK用のボードビヘイビア<MONOSTICK>
をインクルードしています。このボードサポートには、LEDの制御、ウォッチドッグ対応が含まれます。
void setup() {
/*** SETUP section */
auto&& brd = the_twelite.board.use<MONOSTICK>();
brd.set_led_red(LED_TIMER::ON_RX, 200); // RED (on receiving)
brd.set_led_yellow(LED_TIMER::BLINK, 500); // YELLOW (blinking)
// the twelite main class
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL) // set channel (pysical channel)
<< TWENET::rx_when_idle(); // open receive circuit (if not set, it can't listen packts from others)
// Register Network
auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
nwksmpl << NWK_SIMPLE::logical_id(0x00); // set Logical ID. (0x00 means parent device)
/*** BEGIN section */
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- MONOSTICK_Parent act ---" << mwx::crlf;
}
ボードサポートの使用とLEDの動作を設定しています。
auto&& brd = the_twelite.board.use<MONOSTICK>();
brd.set_led_red(LED_TIMER::ON_RX, 200); // RED (on receiving)
brd.set_led_yellow(LED_TIMER::BLINK, 500); // YELLOW (blinking)
2行目では赤色のLEDを無線パケットを受信したら200ms点灯する設定をしています。最初のパラメータはLED_TIMER::ON_RX
が無線パケット受信時を意味します。2番目は点灯時間をmsで指定します。
3行目はLEDの点滅指定です。1番目のパラメータはLED_TIMER::BLINK
が点滅の指定で、2番目のパラメータは点滅のON/OFF切り替え時間です。500msごとにLEDが点灯、消灯(つまり1秒周期の点滅)を繰り返します。
auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
nwksmpl << NWK_SIMPLE::logical_id(0x00);
ここではNWK_SIMPLE::logical_id(0x00)
を設定し親機(論理ID=0x00)であることを指定します。
パケットを受信したとき、その内容を表示します。ここでは2種類の表示を行っています。
void loop() {
// receive RF packet.
while (the_twelite.receiver.available()) {
auto&& rx = the_twelite.receiver.read();
Serial << ".. coming packet (" << int(millis()&0xffff) << ')' << mwx::crlf;
// output type1 (raw packet)
// uint8_t : 0x01
// uint8_t : src addr (LID)
// uint32_t : src addr (long)
// uint32_t : dst addr (LID/long)
// uint8_t : repeat count
// total 11 bytes of header.
//
// N : payload
{
serparser_attach pout;
pout.begin(PARSER::ASCII, rx.get_psRxDataApp()->auData,
rx.get_psRxDataApp()->u8Len, rx.get_psRxDataApp()->u8Len);
Serial << "RAW PACKET -> ";
pout >> Serial;
Serial << mwx::flush;
}
// output type2
{
smplbuf_u8<128> buf;
mwx::pack_bytes(buf
, uint8_t(rx.get_addr_src_lid()) // src addr (LID)
, uint8_t(0xCC) // cmd id (0xCC, fixed)
, uint8_t(rx.get_psRxDataApp()->u8Seq) // seqence number
, uint32_t(rx.get_addr_src_long()) // src addr (long)
, uint32_t(rx.get_addr_dst()) // dst addr
, uint8_t(rx.get_lqi()) // LQI
, uint16_t(rx.get_length()) // payload length
, rx.get_payload() // payload
);
serparser_attach pout;
pout.begin(PARSER::ASCII, buf.begin(), buf.size(), buf.size());
Serial << "FMT PACKET -> ";
pout >> Serial;
Serial << mwx::flush;
}
}
}
最初の出力は<NWK_SIMPLE>
の制御データを含めたデータをすべて表示します。制御データは11バイトあります。通常は制御情報を直接参照することはありませんが、あくまでも参考です。
serparser_attach pout;
pout.begin(PARSER::ASCII, rx.get_psRxDataApp()->auData,
rx.get_psRxDataApp()->u8Len, rx.get_psRxDataApp()->u8Len);
Serial << "RAW PACKET -> ";
pout >> Serial;
Serial << mwx::flush;
// 参考:制御部のパケット構造
// uint8_t : 0x01 固定
// uint8_t : 送信元のLID
// uint32_t : 送信元のロングアドレス(シリアル番号)
// uint32_t : 宛先アドレス
// uint8_t : 中継回数
1行目は出力用のシリアルパーサをローカルオブジェクトとして宣言しています。内部にバッファを持たず、外部のバッファを流用し、パーサーの出力機能を用いて、バッファ内のバイト列を書式出力します。
2行目はシリアルパーサーのバッファを設定します。すでにあるデータ配列、つまり受信パケットのペイロード部を指定します。serparser_attach pout
は、既にあるバッファを用いたシリアルパーサーの宣言です。pout.begin()
の1番目のパラメータは、パーサーの対応書式をPARSER::ASCII
つまりアスキー形式として指定しています。2番目はバッファの先頭アドレス。3番目はバッファ中の有効なデータ長、4番目はバッファの最大長を指定します。出力用で書式解釈に使わないため4番目のパラメータは3番目と同じ値を入れています。
6行目でシリアルポートへ>>
演算子を用いて出力しています。
7行目のSerial << mwx::flush
は、ここで出力が終わっていないデータの出力が終わるまで待ち処理を行う指定です。(Serial.flush()
も同じ処理です)
2番目の出力はより実践的です。ユーザが定義した並び順で書式を構成します。
smplbuf_u8<128> buf;
mwx::pack_bytes(buf
, uint8_t(rx.get_addr_src_lid()) // 送信元の論理ID
, uint8_t(0xCC) // 0xCC
, uint8_t(rx.get_psRxDataApp()->u8Seq) // パケットのシーケンス番号
, uint32_t(rx.get_addr_src_long()) // 送信元のシリアル番号
, uint32_t(rx.get_addr_dst()) // 宛先アドレス
, uint8_t(rx.get_lqi()) // LQI:受信品質
, uint16_t(rx.get_length()) // 以降のバイト数
, rx.get_payload() // データペイロード
);
serparser_attach pout;
pout.begin(PARSER::ASCII, buf.begin(), buf.size(), buf.size());
Serial << "FMT PACKET -> ";
pout >> Serial;
Serial << mwx::flush;
1行目はアスキー書式に変換する前のデータ列を格納するバッファをローカルオブジェクトとして宣言しています。
2行目はpack_bytes()
を用いてデータ列を先ほどのbuf
に格納します。データ構造はソースコードのコメントを参照ください。pack_bytes()
のパラメータにはsmplbuf_u8 (smplbuf<uint8_t, ???>)
形式のコンテナを指定することもできます。
13,14,17行目は、シリアルパーサーの宣言と設定、出力です。
環境センサーパル AMBIENT SENSE PAL を用い、センサー値の取得を行います。
このアクトの解説の前にBRD_APPTWELITEの解説をご覧ください。
受信の確認のためParent_MONOSTICKの解説をご覧ください。
環境センサーパル AMPIENT SENSE PAL を用い、センサー値の取得を行います。
コイン電池で動作させるための、スリープ機能を利用します。
#include <TWELITE>
#include <NWK_SIMPLE>
#include <PAL_AMB> // include the board support of PAL_AMB
環境センサーパル <PAL_AMB>
のボードビヘイビアをインクルードします。
void setup() {
/*** SETUP section */
// use PAL_AMB board support.
auto&& brd = the_twelite.board.use<PAL_AMB>();
// now it can read DIP sw status.
u8ID = (brd.get_DIPSW_BM() & 0x07) + 1;
if (u8ID == 0) u8ID = 0xFE; // 0 is to 0xFE
// LED setup (use periph_led_timer, which will re-start on wakeup() automatically)
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
// the twelite main object.
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL); // set channel (pysical channel)
// Register Network
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(u8ID); // set Logical ID. (0xFE means a child device with no ID)
/*** BEGIN section */
Wire.begin(); // start two wire serial bus.
Analogue.begin(pack_bits(PIN_ANALOGUE::A1, PIN_ANALOGUE::VCC)); // _start continuous adc capture.
the_twelite.begin(); // start twelite!
startSensorCapture(); // start sensor capture!
/*** INIT message */
Serial << "--- PAL_AMB:" << FOURCHARS << " ---" << mwx::crlf;
}
最初にボードサポート <PAL_AMB>
を登録します。ボードサポートの初期化時にセンサーやDIOの初期化が行われます。最初に行うのは、ボードのDIP SWなどの状態を確認してから、ネットワークの設定などを行うといった処理が一般的だからです。
auto&& brd = the_twelite.board.use<PAL_AMB>();
u8ID = (brd.get_DIPSW_BM() & 0x07) + 1;
if (u8ID == 0) u8ID = 0xFE; // 0 is to 0xFE
ここでは、ボード上の4ビットDIP SWのうち3ビットを読み出して子機のIDとして設定しています。0の場合は、ID無しの子機(0xFE
)とします。
LEDの設定を行います。ここでは 10ms おきに ON/OFF の点滅の設定をします(スリープを行い起床時間が短いアプリケーションでは、起床中は点灯するという設定とほぼ同じ意味合いになります)。
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
このアクトではもっぱら無線パケットを送信しますので、TWENET の設定では動作中に受信回路をオープンにする指定(TWENET::rx_when_idle()
)は含めません。
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL); // set channel (pysical channel)
ボード上のセンサーはI2Cバスを用いますので、バスを利用開始しておきます。
Wire.begin(); // start two wire serial bus.
ボード上のセンサーの取得を開始します。startSensorCapture()
の解説を参照ください。
startSensorCapture();
void loop() {
auto&& brd = the_twelite.board.use<PAL_AMB>();
// mostly process every ms.
if (TickTimer.available()) {
// wait until sensor capture finish
if (!brd.sns_LTR308ALS.available()) {
// this will take around 50ms.
// note: to save battery life, perform sleeping to wait finish of sensor capture.
brd.sns_LTR308ALS.process_ev(E_EVENT_TICK_TIMER);
}
if (!brd.sns_SHTC3.available()) {
brd.sns_SHTC3.process_ev(E_EVENT_TICK_TIMER);
}
// now sensor data is ready.
if (brd.sns_LTR308ALS.available() && brd.sns_SHTC3.available() && !b_transmit) {
Serial << "..finish sensor capture." << mwx::crlf
<< " LTR308ALS: lumi=" << int(brd.sns_LTR308ALS.get_luminance()) << mwx::crlf
<< " SHTC3 : temp=" << brd.sns_SHTC3.get_temp() << 'C' << mwx::crlf
<< " humd=" << brd.sns_SHTC3.get_humid() << '%' << mwx::crlf
<< mwx::flush;
// get new packet instance.
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
// set tx packet behavior
pkt << tx_addr(0x00) // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
<< tx_retry(0x1) // set retry (0x1 send two times in total)
<< tx_packet_delay(0, 0, 2); // send packet w/ delay
// prepare packet payload
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(FOURCHARS, 4) // just to see packet identification, you can design in any.
, uint32_t(brd.sns_LTR308ALS.get_luminance()) // luminance
, uint16_t(brd.sns_SHTC3.get_temp())
, uint16_t(brd.sns_SHTC3.get_humid())
);
// do transmit
MWX_APIRET ret = pkt.transmit();
Serial << "..transmit request by id = " << int(ret.get_value()) << '.' << mwx::crlf << mwx::flush;
if (ret) {
u8txid = ret.get_value() & 0xFF;
b_transmit = true;
}
else {
// fail to request
sleepNow();
}
}
}
}
// wait to complete transmission.
if (b_transmit) {
if (the_twelite.tx_status.is_complete(u8txid)) {
Serial << "..transmit complete." << mwx::crlf << mwx::flush;
// now sleeping
sleepNow();
}
}
}
このアクトでは、1ms周期で呼び出させる(アプリケーションループの呼び出しタイミングは不定期な遅延が発生します)TickTimerのイベントを起点としてセンサーの読み出しや送信要求を行います。TickTimer.available()
がtrue
になったときに処理を行います。これによりおよそ1msごとにif節内の処理を行います。
if (TickTimer.available()) {
ボード上のセンサーは .sns_LTR308ALS
または .sns_SHTC3
という名前でアクセスでき、このオブジェクトに操作を行います。センサーの完了待ちを行います。まだセンサーの取得が終わっていない場合(.available()
がfalse
)はセンサーに対して時間経過のイベント(.process_ev(E_EVENT_TICK_TIMER)
)を送付します。
if (!brd.sns_LTR308ALS.available()) {
brd.sns_LTR308ALS.process_ev(E_EVENT_TICK_TIMER);
}
if (!brd.sns_SHTC3.available()) {
brd.sns_SHTC3.process_ev(E_EVENT_TICK_TIMER);
}
上記2つのセンサーがavailableになった時点で、無線送信などの処理を行います。
if (brd.sns_LTR308ALS.available()
&& brd.sns_SHTC3.available() && !b_transmit) {
送信手続きについては他のアクトのサンプルと同様です。ここでは、再送1回、再送遅延を最小にする設定になっています。
pkt << tx_addr(0x00) // 親機0x00宛
<< tx_retry(0x1) // リトライ1回
<< tx_packet_delay(0, 0, 2); // 遅延は最小限
パケットにセンサーデータを書き込み際に、センサーデータを取得しています。センサーの値を取得するメソッドはセンサーごとに違います。
// prepare packet payload
pack_bytes(pkt.get_payload()
, make_pair(FOURCHARS, 4)
, uint32_t(brd.sns_LTR308ALS.get_luminance()) // luminance
, uint16_t(brd.sns_SHTC3.get_temp_cent())
, uint16_t(brd.sns_SHTC3.get_humid_per_dmil())
);
照度センサーは.get_luminance() : uint32_t
で得られます。
温湿度センサーは以下のように取得できます。
.get_temp_cent()
: int16_t
: 1℃を100とした温度 (25.6 ℃なら 2560)
.get_temp()
: float
: float値 (25.6 ℃なら 25.6)
.get_humid_dmil()
: int16_t
: 1%を100とした湿度 (56.8%なら 5680)
.get_temp()
: float
: float値 (56.8%なら 56.8)
得られた値のうち温度値は int16_t
ですが、送信パケットのデータ構造は符号なしで格納するため、uint16_t
にキャストしています。
送信が成功すれば b_tansmit
を true
にし、u8txid
に送信パケットの識別IDを格納し、完了待ちします。送信が失敗すれば sleepNow()
によりスリープします。
// do transmit
MWX_APIRET ret = pkt.transmit();
if (ret) {
u8txid = ret.get_value() & 0xFF;
b_transmit = true;
}
else {
// fail to request
sleepNow();
}
loop()
中 b_transmit
が true
になっている場合は、送信完了チェックを行い、完了すれば sleepNow()
によりスリープします。
// wait to complete transmission.
if (b_transmit) {
if (the_twelite.tx_status.is_complete(u8txid)) {
Serial << "..transmit complete." << mwx::crlf << mwx::flush;
// now sleeping
sleepNow();
}
}
送信完了に確認は the_twelite.tx_status.is_complete(u8txid)
で行っています。u8txid
は送信時に戻り値として戻されたID値です。
ボード上のセンサーの取得を開始します。多くのセンサーは、取得を開始してから完了するまで数msから数十msの時間を必要とします。
void startSensorCapture() {
auto&& brd = the_twelite.board.use<PAL_AMB>();
// start sensor capture
brd.sns_SHTC3.begin();
brd.sns_LTR308ALS.begin();
b_transmit = false;
}
5行目で、温湿度センサー SHTC3 のデータ取得を開始します。
6行目で、照度センサー LTR308ALS のデータ取得を開始します。
7行目は、送信完了フラグをクリアします。
スリープに入る手続きをまとめています。
void sleepNow() {
uint32_t u32ct = 1750 + random(0,500);
Serial << "..sleeping " << int(u32ct) << "ms." << mwx::crlf << mwx::flush;
the_twelite.sleep(u32ct);
}
ここでは、起床までの時間を乱数により 1750ms から 2250ms の間に設定しています。これにより他の同じような周期で送信するデバイスのパケットとの連続的な衝突を避けます。
3行目、この例ではシリアルポートからの出力を待ってスリープに入ります。通常は消費エネルギーを最小化したいため、スリープ前のシリアルポートの出力は最小限(または無し)にします。
スリープ前にflushを行うと、出力が不安定になる場合があります。
スリープに入るには the_twelite.sleep()
を呼びます。この呼び出しの中で、ボード上のハードウェアのスリープ前の手続きなどが行われます。たとえばLEDは消灯します。
パラメータとしてスリープ時間をmsで指定しています。
TWELITE PAL では、必ず60秒以内に一度起床し、ウォッチドッグタイマーをリセットしなければなりません。スリープ時間は60000
を超えないように指定してください。
スリープから復帰し起床すると wakeup()
が呼び出されます。そのあとloop()
が都度呼び出されます。wakeup()
の前に、UARTなどの各ペリフェラルやボード上のデバイスのウェイクアップ処理が行われます。例えばLEDの点灯制御を再始動します。
void wakeup() {
Serial << mwx::crlf
<< "--- PAL_AMB:" << FOURCHARS << " wake up ---"
<< mwx::crlf
<< "..start sensor capture again."
<< mwx::crlf;
startSensorCapture();
}
ここではセンサーのデータ取得を開始しています。
より安全な実装として、このアクトでは以下の例外処理を強化します。
センサー取得部分でavailableにならなかった場合の例外処理が記述されてません。
送信完了待ちで送信完了が通知されない場合の例外処理が記述されていません。
上記いずれもタイムアウト処理を行います。現在の時間は millis()
により取得できます。
uint32_t t_start;
// 時間待ちの処理を開始した時点でタイムスタンプを保存
t_start = millis();
...
// loop() でタイムアウトのチェック
if (millis() - t_start > 100) {
sleepNow();
}
アクト PAL_AMB-UseNap は、センサーのデータ取得待ちをスリープで行い、より低消費エネルギーで動作できます。
PAL_AMB のサンプルを少し改良して、センサーデータ取得中の待ち時間(約50ms)を、スリープで待つようにします。
このアクトの解説の前にPAL_AMBのアクトの解説をご覧ください。
begin()
関数はsetup()
関数を終了し(そのあとTWENETの初期化が行われる)一番最初のloop()
の直前で呼ばれます。
void begin() {
sleepNow(); // the first time is just sleeping.
}
setup()
終了後に初回スリープを実行します。setup()
中にセンサーデータ取得を開始していますが、この結果は評価せず、センサーを事前に一度は動かしておくという意味あいで、必ずしも必要な手続きではありません。
起床後の手続きです。以下の処理を行います。
まだセンサーデータの取得開始をしていない場合、センサーデータ取得を行い、短いスリープに入る。
直前にセンサーデータ取得開始を行ったので、データを確認して無線送信する。
void wakeup() {
if (!b_senser_started) {
// delete/make shorter this message if power requirement is harder.
Serial << mwx::crlf
<< "--- PAL_AMB:" << FOURCHARS << " wake up ---"
<< mwx::crlf
<< "..start sensor capture again."
<< mwx::crlf;
startSensorCapture();
b_senser_started = true;
napNow(); // short period sleep.
} else {
Serial << "..wake up from short nap.." << mwx::crlf;
auto&& brd = the_twelite.board.use<PAL_AMB>();
b_senser_started = false;
// tell sensors waking up.
brd.sns_LTR308ALS.process_ev(E_EVENT_START_UP);
brd.sns_SHTC3.process_ev(E_EVENT_START_UP);
}
}
上記の分岐をグローバル変数のb_sensor_started
により制御しています。!b_sensor_started
の場合はセンサー取得開始(startSensorCapture()
)を行い、napNow()
により短いスリープに入ります。時間は100msです。
napNow()
によるスリープ復帰後、b_sensor_started==true
の節が実行されます。ここでは、2つのセンサーに対してE_EVENT_START_UP
イベントを通知しています。このイベントは、センサーの取得が終了するのに十分な時間が経過したことを意味します。この通知をもとにsns_LTR308ALS
とsns_SHTC3
はavailableになります。この後loop()
に移行し、無線パケットが送信されます。
センサーに通知するイベントは必要な時間待ちが終わったかどうかを判定するために使われます。実際時間が経過しているかどうかはnapNow()
で正しい時間を設定したかどうかで決まります。短い時間で起床した場合は、必要とされる時間経過に足りないため、続く処理でセンサーデータが得られないなどのエラーが出ることが想定されます。
ごく短いスリープを実行する。
void napNow() {
uint32_t u32ct = 100;
Serial << "..nap " << int(u32ct) << "ms." << mwx::crlf;
the_twelite.sleep(u32ct, false, false, TWENET::SLEEP_WAKETIMER_SECONDARY);
}
sleepのパラメータの2番目をtrueにすると前回のスリープ復帰時刻をもとに次の復帰時間を調整します。常に5秒おきに起床したいような場合設定します。
3番目をtrueにするとメモリーを保持しないスリープになります。復帰後はwakup()は呼び出されじ、電源再投入と同じ処理になります。
4番目はウェイクアップタイマーの2番目を使う指定です。ここでは1番目は通常のスリープに使用して、2番目を短いスリープに用いています。このアクトでは2番目を使う強い理由はありませんが、例えば上述の5秒おきに起床したいような場合、短いスリープに1番目のタイマーを用いてしまうとカウンター値がリセットされてしまい、経過時間の補正計算が煩雑になるため2番目のタイマーを使用します。
開閉センサーパル OPEN-CLOSE SENSE PAL を用い、センサー値の取得を行います。
このアクトの解説の前にBRD_APPTWELITEの解説をご覧ください。
受信の確認のためParent_MONOSTICKの解説をご覧ください。
開閉センサーパル OPEN-CLOSE SENSE PAL を用い、磁気センサーの検出時に割り込み起床し、無線送信します。
コイン電池で動作させるための、スリープ機能を利用します。
#include <TWELITE>
#include <NWK_SIMPLE>
#include <PAL_>
開閉センサーパルのボード ビヘイビア<PAL_MAG>
をインクルードします。
void setup() {
/*** SETUP section */
// use PAL_AMB board support.
auto&& brd = the_twelite.board.use<PAL_MAG>();
// now it can read DIP sw status.
u8ID = (brd.get_DIPSW_BM() & 0x07) + 1;
if (u8ID == 0) u8ID = 0xFE; // 0 is to 0xFE
// LED setup (use periph_led_timer, which will re-start on wakeup() automatically)
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
// the twelite main object.
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL); // set channel (pysical channel)
// Register Network
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(u8ID); // set Logical ID. (0xFE means a child device with no ID)
/*** BEGIN section */
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- PAL_MAG:" << FOURCHARS << " ---" << mwx::crlf;
}
最初にボードビヘイビア<PAL_MAG>
を登録します。ボードビヘイビアの初期化時にセンサーやDIOの初期化が行われます。最初に行うのは、ボードのDIP SWなどの状態を確認してから、ネットワークの設定などを行うといった処理が一般的だからです。
auto&& brd = the_twelite.board.use<PAL_MAG>();
u8ID = (brd.get_DIPSW_BM() & 0x07) + 1;
if (u8ID == 0) u8ID = 0xFE; // 0 is to 0xFE
ここでは、ボード上の4ビットDIP SWのうち3ビットを読み出して子機のIDとして設定しています。0の場合は、ID無しの子機(0xFE
)とします。
LEDの設定を行います。ここでは 10ms おきに ON/OFF の点滅の設定をします(スリープを行い起床時間が短いアプリケーションでは、起床中は点灯するという設定とほぼ同じ意味合いになります)。
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
begin()
関数はsetup()
関数を終了し(そのあとTWENETの初期化が行われる)一番最初のloop()
の直前で呼ばれます。
void begin() {
sleepNow(); // the first time is just sleeping.
}
setup()
終了後にsleepNow()
を呼び出し初回スリープを実行します。
void sleepNow() {
uint32_t u32ct = 60000;
pinMode(PAL_MAG::PIN_SNS_OUT1, PIN_MODE::WAKE_FALLING);
pinMode(PAL_MAG::PIN_SNS_OUT2, PIN_MODE::WAKE_FALLING);
the_twelite.sleep(u32ct);
}
スリープに入るまえに磁気センサーのDIOピンの割り込み設定をします。pinMode()
を用います。2番めのパラメータはPIN_MODE::WAKE_FALLING
を指定しています。これはHIGHからLOWへピンの状態が変化したときに起床する設定です。
7行目でthe_twelite.sleep()
でスリープを実行します。パラメータの60000は、TWELITE PAL ボードのウォッチドッグをリセットするために必要な起床設定です。リセットしないと60秒経過後にハードリセットがかかります。
スリープから復帰し起床すると wakeup()
が呼び出されます。そのあとloop()
が都度呼び出されます。wakeup()
の前に、UARTなどの各ペリフェラルやボード上のデバイスのウェイクアップ処理(ウォッチドッグタイマーのリセットなど)が行われます。例えばLEDの点灯制御を再始動します。
void wakeup() {
if (the_twelite.is_wokeup_by_wktimer()) {
sleepNow();
}
}
ここではウェイクアップタイマーからの起床の場合(the_twelite.is_wokeup_by_wktimer()
)は再びスリープを実行します。これは上述のウォッチドッグタイマーのリセットを行う目的のみの起床です。
磁気センサーの検出時の起床の場合は、このままloop()処理に移行します。
ここでは、検出された磁気センサーのDIOの確認を行い、パケットの送信を行い、パケット送信完了後に再びスリープを実行します。
void loop() {
if (!b_transmit) {
if (auto&& pkt =
the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet())
uint8_t b_north =
the_twelite.is_wokeup_by_dio(PAL_MAG::PIN_SNS_NORTH);
uint8_t b_south =
the_twelite.is_wokeup_by_dio(PAL_MAG::PIN_SNS_SOUTH);
Serial << "..sensor north=" << int(b_north)
<< " south=" << int(b_south) << mwx::crlf;
// set tx packet behavior
pkt << tx_addr(0x00)
<< tx_retry(0x1)
<< tx_packet_delay(0, 0, 2);
// prepare packet payload
pack_bytes(pkt.get_payload()
, make_pair(FOURCHARS, 4)
, b_north
, b_south
);
// do transmit
MWX_APIRET ret = pkt.transmit();
if (ret) {
u8txid = ret.get_value() & 0xFF;
b_transmit = true;
}
else {
// fail to request
sleepNow();
}
} else {
sleepNow();
}
} else {
if (the_twelite.tx_status.is_complete(u8txid)) {
b_transmit = 0;
sleepNow();
}
}
}
b_transmit
変数によってloop()
内の振る舞いを制御しています。送信要求が成功した後、この値を1にセットしパケット送信完了待ちを行います。
if (!b_transmit) {
磁気センサーの検出DIOピンの確認を行います。検出ピンは二種類あります。N極検知とS極検知です。単に磁石が近づいたことだけを知りたいならいずれかのピンの検出されたことが条件となります。
uint8_t b_north =
the_twelite.is_wokeup_by_dio(PAL_MAG::PIN_SNS_NORTH);
uint8_t b_south =
the_twelite.is_wokeup_by_dio(PAL_MAG::PIN_SNS_SOUTH);
起床要因のピンを確認するにはthe_twelite.is_wokeup_by_dio()
を用います。パラメータはピン番号です。戻り値をuint8_tに格納しているのはパケットのペイロードに格納するためです。
通信条件の設定やペイロードにデータを格納後、送信を行います。
// do transmit
MWX_APIRET ret = pkt.transmit();
その後、loop()
中 b_transmit
が true
になっている場合は、完了チェックを行い、完了すれば sleepNow()
によりスリープします。
if (the_twelite.tx_status.is_complete(u8txid)) {
b_transmit = 0;
sleepNow();
}
送信完了に確認は the_twelite.tx_status.is_complete(u8txid)
で行っています。u8txid
は送信時に戻り値として戻されたID値です。
動作センサーパル MOTION SENSE PAL を用い、センサー値の取得を行います。
このアクトの解説の前にBRD_APPTWELITEの解説をご覧ください。
受信の確認のためParent_MONOSTICKの解説をご覧ください。
動作センサーパル MOTION SENSE PAL を用い、加速度センサーの加速度を連続的に計測し、無線送信します。
コイン電池で動作させるための、スリープ機能を利用します。
#include <TWELITE>
#include <NWK_SIMPLE>
#include <PAL_>
動作センサーパルのボードビヘイビア<PAL_MOT>
をインクルードします。
void setup() {
/*** SETUP section */
// board
auto&& brd = the_twelite.board.use<PAL_MOT>();
brd.set_led(LED_TIMER::BLINK, 100);
// the twelite main class
the_twelite
<< TWENET::appid(APP_ID)
<< TWENET::channel(CHANNEL);
// Register Network
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(0xFE);
/*** BEGIN section */
the_twelite.begin(); // start twelite!
brd.sns_MC3630.begin(SnsMC3630::Settings(
SnsMC3630::MODE_LP_14HZ, SnsMC3630::RANGE_PLUS_MINUS_4G));
/*** INIT message */
Serial << "--- PAL_MOT(Cont):" << FOURCHARS
<< " ---" << mwx::crlf;
}
最初にボードビヘイビア<PAL_MOT>
を登録します。ボードビヘイビアの初期化時にセンサーやDIOの初期化が行われます。最初に行うのは、ボードのDIP SWなどの状態を確認してから、ネットワークの設定などを行うといった処理が一般的だからです。
auto&& brd = the_twelite.board.use<PAL_MOT>();
u8ID = (brd.get_DIPSW_BM() & 0x07) + 1;
if (u8ID == 0) u8ID = 0xFE; // 0 is to 0xFE
ここでは、ボード上の4ビットDIP SWのうち3ビットを読み出して子機のIDとして設定しています。0の場合は、ID無しの子機(0xFE
)とします。
LEDの設定を行います。ここでは 10ms おきに ON/OFF の点滅の設定をします(スリープを行い起床時間が短いアプリケーションでは、起床中は点灯するという設定とほぼ同じ意味合いになります)。
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
brd.sns_MC3630.begin(SnsMC3630::Settings(
SnsMC3630::MODE_LP_14HZ, SnsMC3630::RANGE_PLUS_MINUS_4G));
加速度センサーの計測を開始します。加速度センサーの設定(SnsMC3630::Settings
)には計測周波数と測定レンジを指定します。ここでは14HZの計測(SnsMC3630::MODE_LP_14HZ
)で、±4Gのレンジ(SnsMC3630::RANGE_PLUS_MINUS_4G
)で計測します。
開始後は加速度センサーの計測が秒14回行われ、その値はセンサー内部のFIFOキューに保存されます。センサーに28回分の計測が終わった時点で通知されます。
begin()
関数はsetup()
関数を終了し(そのあとTWENETの初期化が行われる)一番最初のloop()
の直前で呼ばれます。
void begin() {
sleepNow(); // the first time is just sleeping.
}
setup()
終了後にsleepNow()
を呼び出し初回スリープを実行します。
void sleepNow() {
pinMode(PAL_MOT::PIN_SNS_INT, WAKE_FALLING);
the_twelite.sleep(60000, false);
}
スリープに入るまえに加速度センサーのDIOピンの割り込み設定をします。FIFOキューが一定数まで到達したときに発生する割り込みです。pinMode()
を用います。2番めのパラメータはPIN_MODE::WAKE_FALLING
を指定しています。これはHIGHからLOWへピンの状態が変化したときに起床する設定です。
3行目でthe_twelite.sleep()
でスリープを実行します。パラメータの60000は、TWELITE PAL ボードのウォッチドッグをリセットするために必要な起床設定です。リセットしないと60秒経過後にハードリセットがかかります。
加速度センサーのFIFO割り込みにより、スリープから復帰し起床すると wakeup()
が呼び出されます。そのあとloop()
が都度呼び出されます。wakeup()
の前に、UARTなどの各ペリフェラルやボード上のデバイスのウェイクアップ処理(ウォッチドッグタイマーのリセットなど)が行われます。例えばLEDの点灯制御を再始動します。
void wakeup() {
Serial << "--- PAL_MOT(Cont):" << FOURCHARS
<< " wake up ---" << mwx::crlf;
b_transmit = false;
txid[0] = 0xFFFF;
txid[1] = 0xFFFF;
}
ここではloop()
で使用する変数の初期化を行っています。
ここでは、加速度センサー内のFIFOキューに格納された加速度情報を取り出し、これをもとにパケット送信を行います。パケット送信完了後に再びスリープを実行します。
void loop() {
auto&& brd = the_twelite.board.use<PAL_MOT>();
if (!b_transmit) {
if (!brd.sns_MC3630.available()) {
Serial << "..sensor is not available."
<< mwx::crlf << mwx::flush;
sleepNow();
}
// send a packet
Serial << "..finish sensor capture." << mwx::crlf
<< " seq=" << int(brd.sns_MC3630.get_que().back().t)
<< "/ct=" << int(brd.sns_MC3630.get_que().size());
// calc average in the queue.
{
int32_t x = 0, y = 0, z = 0;
for (auto&& v: brd.sns_MC3630.get_que()) {
x += v.x;
y += v.y;
z += v.z;
}
x /= brd.sns_MC3630.get_que().size();
y /= brd.sns_MC3630.get_que().size();
z /= brd.sns_MC3630.get_que().size();
Serial << format("/ave=%d,%d,%d", x, y, z) << mwx::crlf;
}
for (int ip = 0; ip < 2; ip++) {
if(auto&& pkt =
the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet())
// set tx packet behavior
pkt << tx_addr(0x00)
<< tx_retry(0x1)
<< tx_packet_delay(0, 0, 2);
// prepare packet (first)
uint8_t siz = (brd.sns_MC3630.get_que().size() >= MAX_SAMP_IN_PKT)
? MAX_SAMP_IN_PKT : brd.sns_MC3630.get_que().size();
uint16_t seq = brd.sns_MC3630.get_que().front().t;
pack_bytes(pkt.get_payload()
, make_pair(FOURCHARS, 4)
, seq
, siz
);
// store sensor data (36bits into 5byts, alas 4bits are not used...)
for (int i = 0; i < siz; i++) {
auto&& v = brd.sns_MC3630.get_que().front();
uint32_t v1;
v1 = ((uint16_t(v.x/2) & 4095) << 20) // X:12bits
| ((uint16_t(v.y/2) & 4095) << 8) // Y:12bits
| ((uint16_t(v.z/2) & 4095) >> 4); // Z:8bits from MSB
uint8_t v2 = (uint16_t(v.z/2) & 255); // Z:4bits from LSB
pack_bytes(pkt.get_payload(), v1, v2); // add into pacekt entry.
brd.sns_MC3630.get_que().pop(); // pop an entry from queue.
}
// perform transmit
MWX_APIRET ret = pkt.transmit();
if (ret) {
Serial << "..txreq(" << int(ret.get_value()) << ')';
txid[ip] = ret.get_value() & 0xFF;
} else {
sleepNow();
}
}
}
// finished tx request
b_transmit = true;
} else {
if( the_twelite.tx_status.is_complete(txid[0])
&& the_twelite.tx_status.is_complete(txid[1]) ) {
sleepNow();
}
}
}
b_transmit
変数によってloop()
内の振る舞いを制御しています。送信要求が成功した後、この値を1にセットしパケット送信完了待ちを行います。
if (!b_transmit) {
最初にセンサーがavailableかどうかを確認します。割り込み起床後であるため、availableでないのは通常ではなく、そのままスリープします。
if (!brd.sns_MC3630.available()) {
Serial << "..sensor is not available."
<< mwx::crlf << mwx::flush;
sleepNow();
}
無線送信パケットでは使用しないのですが、取り出した加速度の情報を確認してみます。
Serial << "..finish sensor capture." << mwx::crlf
<< " seq=" << int(brd.sns_MC3630.get_que().front().t)
<< "/ct=" << int(brd.sns_MC3630.get_que().size());
// calc average in the queue.
{
int32_t x = 0, y = 0, z = 0;
for (auto&& v: brd.sns_MC3630.get_que()) {
x += v.x;
y += v.y;
z += v.z;
}
x /= brd.sns_MC3630.get_que().size();
y /= brd.sns_MC3630.get_que().size();
z /= brd.sns_MC3630.get_que().size();
Serial << format("/ave=%d,%d,%d", x, y, z) << mwx::crlf;
}
加速度センサーの計測結果はbrd.sns_MC3630.get_que()
で得られるFIFOキューに格納されます。
加速度センサーの計測結果を格納している構造体 axis_xyzt
は x, y, z の三軸の情報に加え、続き番号 t が格納されています。
格納されているサンプル数はキューのサイズ(brd.sns_MC3630.get_que().size()
)を読み出すことで確認できます。通常は28サンプルですが処理の遅延等によりもう少し進む場合もあります。最初のサンプルはfront()
で取得することができます。その続き番号はfront().t
になります。
ここでは、サンプルをキューから取り出す前にサンプルの平均をとってみます。キューの各要素にはfor文(for (auto&& v: brd.sns_MC3630.get_que()) { ... }
) でアクセスできます。for文内の v.x, v.y, v.z
が各要素になります。ここでは各要素の合計を計算しています。for文終了後は要素数で割ることで平均を計算しています。
次にパケットを生成して送信要求を行いますが、データ量が大きいため2回に分けて送信します。そのため送信処理がfor文で2回行われます。
for (int ip = 0; ip < 2; ip++) {
送信するパケットに含めるサンプル数とサンプル最初の続き番号をパケットのペイロードの先頭部分に格納します。
// prepare packet (first)
uint8_t siz = (brd.sns_MC3630.get_que().size() >= MAX_SAMP_IN_PKT)
? MAX_SAMP_IN_PKT : brd.sns_MC3630.get_que().size();
uint16_t seq = brd.sns_MC3630.get_que().front().t;
pack_bytes(pkt.get_payload()
, make_pair(FOURCHARS, 4)
, seq
, siz
);
最後に加速度データを格納します。先程は平均値の計算のためにキューの各要素を参照のみしましたが、ここではキューから1サンプルずつ読み出してパケットのペイロードに格納します。
for (int i = 0; i < siz; i++) {
auto&& v = brd.sns_MC3630.get_que().front();
uint32_t v1;
v1 = ((uint16_t(v.x/2) & 4095) << 20) // X:12bits
| ((uint16_t(v.y/2) & 4095) << 8) // Y:12bits
| ((uint16_t(v.z/2) & 4095) >> 4); // Z:8bits from MSB
uint8_t v2 = (uint16_t(v.z/2) & 255); // Z:4bits from LSB
pack_bytes(pkt.get_payload(), v1, v2); // add into pacekt entry.
brd.sns_MC3630.get_que().pop(); // pop an entry from queue.
}
加速度センサーからのデータキューの先頭を読み出すのは.front()
を用います。読みだした後.pop()
を用いて先頭キューを開放します。
加速度センサーから取得されるデータは1Gを1000としたミリGの単位です。レンジを±4Gとしているため、12bitの範囲に入るように2で割って格納します。データ数を節約するため最初の4バイトにX,Y軸とZ軸の上位8bitを格納し、次の1バイトにZ軸の下位4bitの合計5バイトを生成します。
2回分の送信待ちを行うため送信IDはtxid[]
配列に格納します。
MWX_APIRET ret = pkt.transmit();
if (ret) {
Serial << "..txreq(" << int(ret.get_value()) << ')';
txid[ip] = ret.get_value() & 0xFF;
} else {
sleepNow();
}
その後、loop()
中 b_transmit
が true
になっている場合は、完了チェックを行い、完了すれば sleepNow()
によりスリープします。
} else {
if( the_twelite.tx_status.is_complete(txid[0])
&& the_twelite.tx_status.is_complete(txid[1]) ) {
sleepNow();
}
}
送信完了に確認は the_twelite.tx_status.is_complete()
で行っています。txid[]
は送信時に戻り値として戻されたID値です。
PAL_MOTアクトでは連続的に加速度データを取得して都度無線送信していました。このアクトではスリープ復帰後に数サンプル加速度データを取得しそのデータを送ります。
このアクトの解説の前にPAL_MOTのアクトの解説をご覧ください。
本サンプルは、収録バージョンによって差が大きいため本ページでは2つの解説を記載します。
※ 最新のコードは「サンプルアクト>最新版の入手」を参照ください。
起床→加速度センサーの取得開始→加速度センサーのFIFO割り込み待ち→加速度センサーのデータの取り出し→無線送信→スリープという
起床→加速度センサーの取得開始→加速度センサーのFIFO割り込み待ち→加速度センサーのデータの取り出し→無線送信→スリープという流れになります。
enum class E_STATE {
INIT = 0,
START_CAPTURE,
WAIT_CAPTURE,
REQUEST_TX,
WAIT_TX,
EXIT_NORMAL,
EXIT_FATAL
} eState;
列挙体として eState
変数を宣言しています。
void begin() {
// sleep immediately, waiting for the first capture.
sleepNow();
}
setup()
を終了した後に呼ばれます。ここでは初回スリープを実行しています。
void wakeup() {
Serial << crlf << "--- PAL_MOT(OneShot):"
<< FOURCHARS << " wake up ---" << crlf;
eState = E_STATE::INIT;
}
起床後は状態変数eState
を初期状態INITにセットしています。この後loop()
が実行されます。
void loop() {
auto&& brd = the_twelite.board.use<PAL_MOT>();
bool loop_more;
do {
loop_more = false;
switch(eState) {
...
}
} while(loop_more);
loop()
の基本構造は状態変数eState
によるswitch ... case節です。eStateの初期状態はINITです。loop_more
は状態変数を書き換えた直後、loop()
を抜ける前にもう一度実行したいときにtrue
にセットします。
以下では各case節を解説します。eState
の初期状態はINITです。
case E_STATE::INIT:
brd.sns_MC3630.get_que().clear(); // clear queue in advance (just in case).
loop_more = true;
eState = E_STATE::START_CAPTURE;
break;
状態INITでは、初期化(結果格納用のキューのクリア)を行います。
case E_STATE::START_CAPTURE:
u32tick_capture = millis();
brd.sns_MC3630.begin(
// 400Hz, +/-4G range, get four samples (can be one sample)
SnsMC3630::Settings(
SnsMC3630::MODE_LP_400HZ, SnsMC3630::RANGE_PLUS_MINUS_4G, 4));
eState = E_STATE::WAIT_CAPTURE;
break;
状態START_CAPTUREでは、MC3630センサーのFIFO取得を開始します。ここでは400Hzで4サンプル取得できた時点でFIFO割り込みが発生する設定にしています。タイムアウトのチェックのため、開始時点のシステム時刻をu32tick_capture
に格納します。
case E_STATE::WAIT_CAPTURE:
if (brd.sns_MC3630.available()) {
brd.sns_MC3630.end(); // stop now!
eState = E_STATE::REQUEST_TX; loop_more = true;
} else if ((millis() - u32tick_capture) > 100) {
Serial << crlf << "!!!FATAL: SENSOR CAPTURE TIMEOUT.";
eState = E_STATE::EXIT_FATAL;
}
break;
状態WAIT_CAPTUREでは、FIFO割り込みを待ちます。割り込みが発生し結果格納用のキューにデータが格納されるとsns_MC3630.available()
がtrue
になります。
タイムアウトした場合は状態EXIT_FATALに遷移します。
case E_STATE::REQUEST_TX:
u32tick_tx = millis();
txid = TxReq();
if (txid) {
eState = E_STATE::WAIT_TX;
} else {
Serial << crlf << "!!!FATAL: TX REQUEST FAILS.";
eState = E_STATE::EXIT_FATAL;
}
break;
状態REQUEST_TXではローカル定義関数TxReq()を呼び出し、得られたセンサーデータの処理と送信パケットの生成、そうし尿級を行います。タイムアウトのチェックのため、開始時点のシステム時刻をu32tick_tx
に格納します。
case E_STATE::WAIT_TX:
if(the_twelite.tx_status.is_complete(txid.get_value())) {
eState = E_STATE::EXIT_NORMAL; loop_more = true;
} else if (millis() - u32tick_tx > 100) {
Serial << crlf << "!!!FATAL: TX TIMEOUT.";
eState = E_STATE::EXIT_FATAL;
}
break;
状態WAIT_TXでは、無線パケットの送信完了を待ちます。
タイムアウト時には状態EXIT_FATALに遷移します。
case E_STATE::EXIT_NORMAL:
sleepNow();
break;
case E_STATE::EXIT_FATAL:
Serial << flush;
the_twelite.reset_system();
break;
一連の動作が完了したときは状態EXIT_NORMALに遷移しローカル定義の関数sleepNow()
を呼び出しスリープを実行します。またエラーを検出した場合は状態EXIT_FATALに遷移し、システムリセットを行います。
この関数では、センサーより得られたサンプル値の取得と、複数サンプルの平均値の計算、
int32_t x = 0, y = 0, z = 0;
for (auto&& v: brd.sns_MC3630.get_que()) {
x += v.x;
y += v.y;
z += v.z;
}
x /= brd.sns_MC3630.get_que().size();
y /= brd.sns_MC3630.get_que().size();
z /= brd.sns_MC3630.get_que().size();
取得サンプルの平均値を計算します。
auto&& x_minmax = std::minmax_element(
get_axis_x_iter(brd.sns_MC3630.get_que().begin()),
get_axis_x_iter(brd.sns_MC3630.get_que().end()));
brd.sns_MC3630.get_que().clear(); // clean up the queue
X軸の最大値と最小値を計算します。
ここではイテレータとstd::minmax_element()
アルゴリズムを用いて計算します。get_axis_x_iter
はキューのイテレータをパラメータとして、axis_xyzt
構造体の.x
にアクセスするものです。
ここでキューをクリア.sns_MC3630.get_que().clear()
しています。
// prepare tx packet
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
// set tx packet behavior
pkt << tx_addr(0x00) // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
<< tx_retry(0x1) // set retry (0x1 send two times in total)
<< tx_packet_delay(0, 0, 2); // send packet w/ delay
// prepare packet (first)
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(FOURCHARS, 4) // just to see packet identification, you can design in any.
, uint16_t(x)
, uint16_t(y)
, uint16_t(z)
, uint16_t(*x_minmax.first) // minimum of captured x
, uint16_t(*x_minmax.second) // maximum of captured x
);
// perform transmit
ret = pkt.transmit();
if (ret) {
Serial << "..txreq(" << int(ret.get_value()) << ')';
}
最期にパケットの生成と送信を要求を行います。パケットには X, Y, Z 軸の加速度、X軸の最小値,Y軸の最小値を含めています。
MWSDK2020_05 版の SDK 添付のサンプルコードです。
※ 最新のコードは「サンプルアクト>最新版の入手」を参照ください。
起床→加速度センサーの取得開始→加速度センサーのFIFO割り込み待ち→加速度センサーのデータの取り出し→無線送信→スリープという流れになります。
起床後加速度センサーを稼働させます。
void wakeup() {
Serial << mwx::crlf << "--- PAL_MOT(OneShot):" << FOURCHARS << " wake up ---" << mwx::crlf;
auto&& brd = the_twelite.board.use<PAL_MOT>();
brd.sns_MC3630.get_que().clear(); // clear queue in advance (just in case).
brd.sns_MC3630.begin(SnsMC3630::Settings(
SnsMC3630::MODE_LP_400HZ, SnsMC3630::RANGE_PLUS_MINUS_4G, 4));
// 400Hz, +/-4G range, get four samples (can be one sample)
b_transmit = false;
txid = 0xFFFF;
}
加速度センサーの結果を保存するキューの内容を抹消(.sns_MC3630.get_que().clear()
)しておきます。加速度センサーのサンプルの取得忘れがあったり、また停止させるまでに次のサンプルが取得したような場合を想定します。
ここで加速度センサーを都度開始します。設定は400Hz,±4G,FIFO割り込みは4サンプルとしています。4サンプルも必要ない場合は1サンプルの設定でも構いません。
void loop() {
auto&& brd = the_twelite.board.use<PAL_MOT>();
if (!b_transmit) {
if (brd.sns_MC3630.available()) {
brd.sns_MC3630.end(); // stop now!
Serial << "..finish sensor capture." << mwx::crlf
<< " ct=" << int(brd.sns_MC3630.get_que().size());
// get all samples and average them.
int32_t x = 0, y = 0, z = 0;
for (auto&& v: brd.sns_MC3630.get_que()) {
x += v.x;
y += v.y;
z += v.z;
}
x /= brd.sns_MC3630.get_que().size();
y /= brd.sns_MC3630.get_que().size();
z /= brd.sns_MC3630.get_que().size();
Serial << format("/ave=%d,%d,%d", x, y, z) << mwx::crlf;
// just see X axis, min and max
//auto&& x_axis = get_axis_x(brd.sns_MC3630.get_que());
//auto&& x_minmax = std::minmax_element(x_axis.begin(), x_axis.end());
auto&& x_minmax = std::minmax_element(
get_axis_x_iter(brd.sns_MC3630.get_que().begin()),
get_axis_x_iter(brd.sns_MC3630.get_que().end()));
brd.sns_MC3630.get_que().clear(); // clean up the queue
// prepare tx packet
if (auto&& pkt = nwk.the_twelite.network.use<NWK_SIMPLE>()) {
auto&& pkt = nwk.prepare_tx_packet(); // get new packet instance.
// set tx packet behavior
pkt << tx_addr(0x00) // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
<< tx_retry(0x1) // set retry (0x1 send two times in total)
<< tx_packet_delay(0, 0, 2); // send packet w/ delay
// prepare packet (first)
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(FOURCHARS, 4) // just to see packet identification, you can design in any.
, uint16_t(x)
, uint16_t(y)
, uint16_t(z)
, uint16_t(*x_minmax.first) // minimum of captured x
, uint16_t(*x_minmax.second) // maximum of captured x
);
// perform transmit
MWX_APIRET ret = pkt.transmit();
if (ret) {
Serial << "..txreq(" << int(ret.get_value()) << ')';
txid = ret.get_value() & 0xFF;
} else {
sleepNow();
}
// finished tx request
b_transmit = true;
}
}
} else {
// wait until transmit completion.
if(the_twelite.tx_status.is_complete(txid)) {
sleepNow();
}
}
}
このアクトでは、サンプル取得後、すぐに加速度センサーの動作を停止します。
if (brd.sns_MC3630.available()) {
brd.sns_MC3630.end(); // stop now!
取得サンプルの平均値を計算します。
int32_t x = 0, y = 0, z = 0;
for (auto&& v: brd.sns_MC3630.get_que()) {
x += v.x;
y += v.y;
z += v.z;
}
x /= brd.sns_MC3630.get_que().size();
y /= brd.sns_MC3630.get_que().size();
z /= brd.sns_MC3630.get_que().size();
X軸の最大値と最小値を計算します。
auto&& x_minmax = std::minmax_element(
get_axis_x_iter(brd.sns_MC3630.get_que().begin()),
get_axis_x_iter(brd.sns_MC3630.get_que().end()));
brd.sns_MC3630.get_que().clear(); // clean up the queue
ここではイテレータとstd::minmax_element()
アルゴリズムを用いて計算します。get_axis_x_iter
はキューのイテレータをパラメータとして、axis_xyzt
構造体の.x
にアクセスするものです。
最後にキューをクリア.sns_MC3630.get_que().clear()
しています。
パルスカウンターを用いたアクト例です。
パルスカウンターは、マイコンを介在せず信号の立ち上がりまたは立ち下りの回数を計数するものです。不定期のパルスを計数し一定回数までカウントが進んだ時点で無線パケットで回数を送信するといった使用方法が考えられます。
このアクトの解説の前にBRD_APPTWELITEの解説をご覧ください。
受信の確認のためParent_MONOSTICKの解説をご覧ください。
子機側のDIO8に接続したパルスを計数し、一定時間経過後または一定数のカウントを検出した時点で無線送信する。
子機側はスリープしながら動作する。
// Pulse Counter setup
PulseCounter.setup();
パルスカウンターの初期化を行います。
void begin() {
// start the pulse counter capturing
PulseCounter.begin(
100 // 100 count to wakeup
, PIN_INT_MODE::FALLING // falling edge
);
sleepNow();
}
パルスカウンターの動作を開始し、初回スリープを実行します。PulseCounter.begin()
の最初のパラメータは、起床割り込みを発生させるためのカウント数100
で、2番目は立ち下がり検出PIN_INT_MODE::FALLING
を指定しています。
void wakeup() {
Serial << mwx::crlf
<< "--- Pulse Counter:" << FOURCHARS << " wake up ---"
<< mwx::crlf;
if (!PulseCounter.available()) {
Serial << "..pulse counter does not reach the reference value." << mwx::crlf;
sleepNow();
}
}
起床時にPulseCounter.available()
を確認しています。availableつまりtrue
になっていると、指定したカウント数以上のカウントになっていることを示します。ここではfalse
の場合再スリープしています。
カウント数が指定以上の場合はloop()
で送信処理と送信完了待ちを行います。
uint16_t u16ct = PulseCounter.read();
パルスカウント値の読み出しを行います。読み出した後カウンタはリセットされます。
Slp_Wk_and_Tx は、定期起床後、何か実行(センサーデータの取得など)を行って、その結果を無線パケットとして送信するようなアプリケーションを想定した、テンプレートソースコードです。
setup(), loop()
の形式では、どうしても loop()
中が判読しづらい条件分岐が発生しがちです。本Actでは、loop()
中を switch構文による単純な状態遷移を用いることで、コードの見通しを良くしています。
このアクトの解説の前にBRD_APPTWELITEの解説をご覧いただくことを推奨します。
起動後、速やかにスリープする
setup()
: 初期化する
begin()
: スリープに遷移する
スリープ起床後、状態変数を初期化し、以下の順に動作を行う
wakeup()
: スリープからの起床、各初期化を行う
loop()
状態INIT->WORK_JOBに遷移: 何らかの処理を行う(このActでは 1ms ごとの TickCount ごとにカウンタを更新し 100 カウント後にTX状態に進む)
loop()
状態TX: 送信要求を行う
loop()
状態WAIT_TX: 送信完了待ちを行う
loop()
状態EXIT_NORMAL: スリープする (1. に戻る)
loop()
状態EXIT_FATAL: エラーが発生した場合は、モジュールリセットする
#include <TWELITE>
#include <NWK_SIMPLE>
#include "Common.h"
パケット送信を行うため <NWK_SIMPLE>
をインクルードしています。また、アプリケーションIDなど基本的な定義は "Common.h"
に記述しています。
Common.h
には基本的な定義に加え、以下の列挙体が定義されています。こちらを状態変数として利用します。
enum class E_STATE {
INIT = 0, // INIT STATE
WORK_JOB, // do some job (e.g sensor capture)
TX, // reuest transmit
WAIT_TX, // wait its completion
EXIT_NORMAL, // normal exiting.
EXIT_FATAL // has a fatal error (will do system reset)
};
void setup() {
/*** SETUP section */
txreq_stat = MWX_APIRET(false, 0);
// the twelite main class
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL) // set channel (pysical channel)
<< TWENET::rx_when_idle(false); // open receive circuit (if not set, it can't listen packts from others)
// Register Network
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(DEVICE_ID); // set Logical ID.
/*** BEGIN section */
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- Sleep an Tx Act ---" << crlf;
}
the_twelite
クラスオブジェクトの初期化とネットワーク <NWK_SIMPLE>
の登録を行います。
void begin() {
Serial << "..begin (run once at boot)" << crlf;
SleepNow();
}
setup()
の直後に一度だけ呼び出されます。SleepNow()
関数夜を呼び出して初回のスリープ手続きを行います。
void wakeup() {
Serial << crlf << int(millis()) << ":wake up!" << crlf;
eState = E_STATE::INIT;
}
起床直後に呼び出されます。ここでは状態変数eState
を初期状態E_STATE::INIT
にセットします。この後loop()
が呼び出されます。
void loop() {
bool loop_more; // if set, one more loop on state machine.
do {
loop_more = false; // set no-loop at the initial.
switch(eState) {
case E_STATE::INIT:
eState = E_STATE::WORK_JOB; loop_more = true;
break;
case E_STATE::WORK_JOB:
if (TickTimer.available())
if(--dummy_work_count == 0)
eState = E_STATE::TX: loop_more = true;
break;
case E_STATE::TX:
txreq_stat = vTransmit();
if(txreq_stat) {
u32millis_tx = millis();
eState = E_STATE::WAIT_TX;
} else {
eState = E_STATE::EXIT_FATAL; loop_more = true;
}
break;
case E_STATE::WAIT_TX:
if (the_twelite.tx_status.is_complete(txreq_stat.get_value())) {
eState = E_STATE::EXIT_NORMAL; loop_more = true;
} else if (millis() - u32millis_tx > 100) {
eState = E_STATE::EXIT_FATAL; loop_more = true;
}
break;
case E_STATE::EXIT_NORMAL:
SleepNow();
break;
case E_STATE::EXIT_FATAL:
the_twelite.reset_system();
break;
}
} while (loop_more);
上記のコードは、実際のコードを簡略化したものです。
このコードではloop_more
を条件としてdo..while()
構文のループになっています。これはloop()
を脱出せずに次の状態のコード(case節)を実行する目的です。
wakeup()
でINIT状態にセットされていますので初回のloop()
ではINITが評価されます。ここは変数の初期化しているだけです。続く状態の処理を行うのにloop()
を脱出する必要がないためloop_more
をtrue
にしています。
WORK_JOB状態ではTichTimer.available()
になるたびにカウンタを減算し0になったら次の状態TXに遷移します。
TX状態ではvTransmit()
関数を呼び出しパケット送信要求を行います。この時点ではまだパケットが送信されていないため、この時点でスリープをしてはいけません。パケット送信要求が成功したらWAIT_TX状態に遷移します。タイムアウトの管理のため、この時点でのシステム時間u32millis_tx
を保存します。失敗したらEXIT_FATALに遷移します(モジュールのリセット実行)。
WAIT_TX状態では送信IDに対応する送信が完了したかを判定し、完了したらEXIT_NORMAL、100ms以上経過したらEXIT_FATAL状態に遷移します。
EXIT_NORMAL状態はSleepNow()
を呼び出し、スリープします。
EXIT_FATAL状態ではシステムリセットを行います。
void SleepNow() {
uint16_t u16dur = SLEEP_DUR;
u16dur = random(SLEEP_DUR - SLEEP_DUR_TERMOR, SLEEP_DUR + SLEEP_DUR_TERMOR);
Serial << int(millis()) << ":sleeping for " << int(u16dur) << "ms" << crlf << mwx::flush;
the_twelite.sleep(u16dur, false);
}
周期スリープを行います。スリープ時間はrandom()
関数を用いて、一定の時間ブレを作っています。これは複数のデバイスの送信周期が同期した場合、著しく失敗率が上がる場合があるためです。
MWX_APIRET vTransmit() {
Serial << int(millis()) << ":vTransmit()" << crlf;
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
// set tx packet behavior
pkt << tx_addr(0x00) // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
<< tx_retry(0x1) // set retry (0x3 send four times in total)
<< tx_packet_delay(0,0,2); // send packet w/ delay (send first packet with randomized delay from 0 to 0ms, repeat every 2ms)
// prepare packet payload
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(FOURCC, 4) // string should be paired with length explicitly.
, uint32_t(millis()) // put timestamp here.
);
// do transmit
//return nwksmpl.transmit(pkt);
return pkt.transmit();
}
return MWX_APIRET(false, 0);
}
ID=0x00
の親機宛に無線パケットの送信要求を行います。格納されるデータはActサンプルで共通に使われている4文字識別子(FOURCC
)に加え、システム時間[ms]が含まれます。
MWX_APIRET
はuint32_t
型をラップしたクラスで、MSBを失敗成功のフラグとし、以下31ビットをデータとして用いています。pkt.transmit()
の戻り型になっており、送信要求の成功と失敗ならびに送信IDをデータ部に格納します。
WirelessUARTはシリアル通信を行います。
このアクトの解説の前にBRD_APPTWELITEの解説をご覧ください。
2台のUART接続のTWELITE同士をアスキー書式で通信する。
いずれかを2台。
TWELITE R でUART接続されているTWELITE DIPなど
void setup() {
/*** SETUP section */
// the twelite main class
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL) // set channel (pysical channel)
<< TWENET::rx_when_idle(); // open receive circuit (if not set, it can't listen packts from others)
// Register Network
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
uid = random(1, 5); // set uid by random() (1..4)
nwk << NWK_SIMPLE::logical_id(uid); // set Logical ID. (0xFE means a child device with no ID)
/*** BEGIN section */
SerialParser.begin(PARSER::ASCII, 128); // Initialize the serial parser
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- WirelessUart (id=" << int(uid) << ") ---" << mwx::crlf;
}
論理IDはrandom(1,5)
により1,2,3,4のいずれかの値に割り当てています。通常は、論理IDを設定保存したデータ、またはDIP SWといったハードウェアの情報から生成します。
SerialParser.begin(PARSER::ASCII, 128);
シリアルパーサーを初期化します。
while(Serial.available()) {
if (SerialParser.parse(Serial.read())) {
Serial << ".." << SerialParser;
const uint8_t* b = SerialParser.get_buf().begin();
uint8_t addr = *b; ++b; // the first byte is destination address.
transmit(addr, b, SerialParser.get_buf().end());
}
}
シリアルからのデータ入力があった時点で、シリアルパーサーに1バイト入力します。アスキー形式が最後まで受け付けられた時点でSerialParser.parse()
はtrue
を戻します。
SerialParser
は内部バッファに対してsmplbuf
でアクセスできます。上の例ではバッファの1バイト目を送信先のアドレスとして取り出し、2バイト目から末尾までをtransmit()
関数に渡します。
パケットを受信したときには、送信元を先頭バイトにし続くペイロードを格納したバッファsmplbuf_u8<128> buf
を生成し、出力用のシリアルパーサーserparser_attach pout
からシリアルに出力しています。
if (the_twelite.receiver.available()) {
auto&& rx = the_twelite.receiver.read();
// check the packet header.
const uint8_t* p = rx.get_payload().begin();
if (rx.get_length() > 4 && !strncmp((const char*)p, (const char*)FOURCHARS, 4)) {
Serial << format("..rx from %08x/%d", rx.get_addr_src_long(), rx.get_addr_src_lid()) << mwx::crlf;
smplbuf_u8<128> buf;
mwx::pack_bytes(buf
, uint8_t(rx.get_addr_src_lid()) // src addr (LID)
, make_pair(p+4, rx.get_payload().end()) ); // data body
serparser_attach pout;
pout.begin(PARSER::ASCII, buf.begin(), buf.size(), buf.size());
Serial << pout;
}
}
テストデータは必ずペースト機能を用いてターミナルに入力してください。入力にはタイムアウトがあるためです。
参考: TWE ProgrammerやTeraTermでのペーストはAlt+Vを用います。
:FE00112233X
:FE001122339C
任意の子機宛に00112233
を送付します。
:03AABBCC00112233X
:03AABBCC0011223366
子機3番に対してAABBCC00112233
を送付します。
テンプレートコードです。
act0は中身が空でしたがScratchには以下のコードが含まれます。
void setup() {
/*** SETUP section */
txreq_stat = MWX_APIRET(false, 0);
// the twelite main class
the_twelite
<< TWENET::appid(APP_ID) // アプリケーションID
<< TWENET::channel(CHANNEL) // チャネル
<< TWENET::rx_when_idle(); // 受信有
// Register Network
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(0xFE); // 子機、特定アドレス指定なし(0xFE)
/*** BEGIN section */
Buttons.begin(pack_bits(PIN_BTN), 5, 10); // ボタン処理初期化
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- Scratch act ---" << mwx::crlf;
}
アプリケーションID APP_ID
, 無線チャネルCHANNEL
、受信有、子機アドレス0xFE
として始動します。
void begin() {
Serial << "..begin (run once at boot)" << mwx::crlf;
}
始動時setup()
の後に1回だけ呼び出されます。メッセージの表示のみ。
if (Buttons.available()) {
uint32_t bm, cm;
Buttons.read(bm, cm);
if (cm & 0x80000000) {
// the first capture.
}
Serial << int(millis()) << ":BTN" << format("%b") << mwx::crlf;
}
Buttonsによる連続参照により状態を確定します。ボタン状態が変化したらシリアルに出力します。
while(Serial.available()) {
int c = Serial.read();
Serial << '[' << char(c) << ']';
switch(c) {
case 'p': ... // millis() を表示
case 't': ... // 無線パケットを送信 (vTransmit)
case 's': ... // スリープする
}
}
シリアルから1文字読み込んで、入力文字に応じた処理をします。
if (the_twelite.receiver.available()) {
auto&& rx = the_twelite.receiver.read();
// just dump a packet.
Serial << format("rx from %08x/%d",
rx.get_addr_src_long(), rx.get_addr_src_lid()) << crlf;
}
パケットを受信したら、送信元のアドレス情報を表示します。
void wakeup() {
Serial << int(millis()) << ":wake up!" << mwx::crlf;
}
スリープ起床時に最初に呼び出されます。メッセージの表示のみ。
MWX_APIRET vTransmit() {
Serial << int(millis()) << ":vTransmit()" << mwx::crlf;
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
// set tx packet behavior
pkt << tx_addr(0xFF) // 同報通信=ブロードキャスト
<< tx_retry(0x1) // 再送1回
<< tx_packet_delay(100,200,20); // 送信時遅延100-200msの間に送信、再送間隔20ms
// 送信データの指定(アプリケーションごとに決める)
pack_bytes(pkt.get_payload()
, make_pair("SCRT", 4) // 4文字識別子
, uint32_t(millis()) // タイムスタンプ
);
// 送信要求を行う
return pkt.transmit();
}
// .prepare_tx_packet() 時点で失敗している
return MWX_APIRET(false, 0);
}
送信要求を行う最小限の手続きです。
この関数を抜けた時点では、まだ要求は実行されていません。しばらく待つ必要があります。この例では100-200msの送信開始の遅延があるため、送信が開始されるのは早くて100ms後です。
Unit_で始まるアクト(Act)は、ごく単機能の記述や動作を確認するためのものです。
名前
内容
Unit_ADC
ADCを動作させるサンプルです。100msごとにADCを連続実行しつつ約1秒おきに読み出し表示します。[s]
キーでスリープします。
Unit_I2Cprobe
I2Cバスをスキャンして、応答のあるデバイス番号を表示します(この手順で応答しないデバイスもあります)。
Unit_delayMicoroseconds
delayMicroseconds()
の動作を確認します。16MhzのTickTimerのカウントとの比較をします。
Unit_brd_CUE
TWELITE CUEの加速度センサー,磁気センサー,LEDの動作確認を行います。ターミナルから[a]
,[s]
,[l]
キーを入力します。
Unit_brd_PAL_NOTICE
通知パル(NOTICE PAL)のLED点灯を試します。起動時に全灯フラッシュ、その後はシリアル入力で操作します。
r
,g
,b
,w
: 各色の点灯モードをトグルする
R
,G
,B
,W
: 各色の明るさを変更する(消灯・全灯時は無効)
c
: 周期を変化させる(点滅時)
C
: 点滅時のデューティを変化させる(点滅時)
Unit_div100
10,100,1000の割り算と商を求めるdiv10()
,div100()
,div1000()
の動作確認を行います。-99999~99999まで計算を行い通常の/
,%
による除算との経過時間の比較をします。
Unit_div_format
div10()
,div100()
,div1000()
の結果を文字列出力します。
Unit_Pkt_Parser
パケット情報のパーサーpktparserの使用サンプルです。App_Wingsの出力を解釈することが出来ます。 ※ TWELITE無線モジュール同士をシリアル接続して、一方をApp_Wingsとして他方でその出力を解釈したいような場合です。他方をTWELITE無線モジュール以外に接続したい場合は「他のプラットフォーム」を参照ください。
Uint_EEPROM
EEPROMの読み書きテストコードです。