1
@@ -2714,8 +2714,8 @@
if (false) {
connected = SerialBT.connect(dname);
} else {
- uint8_t address[6] = {0x24, 0xA1, 0x60, 0x47, 0x84, 0x4E}; // MACアドレスが 24:A1:60:47:84:4E のとき
- connected = SerialBT.connect(address);
+ uint8_t address[6] = {0x24, 0xA1, 0x60, 0x47, 0x84, 0x4E}; // MACアドレスが 24:A1:60:47:84:4E のとき
+ connected = SerialBT.connect(address);
}
if (connected) {
Serial.println("Connected Succesfully!");
@@ -2794,15 +2794,138 @@
-
- Bluetooth Low Energy
- アドバータイズ
+
+ その他のBluetooth利用例
+ BLEHIDDeviceを用いると、Human Interface Device(HID) Profileを導入して、マウスやキーボードの代用品が作成できます。(詳細は省略します。)
- 電力制御
+ 電源制御・電力制御
+
+ESP.restart() で、再起動します。
+M5.Axp.Write1Byte(0x32, M5.Axp.Read8bit(0x32) | 0x80); で、電源OFFします。
+setCpuFrequencyMhz(240) で、CPUのクロック数を変更できます。設定できる値は、240、160、80、40、20、10ですが、無線通信するなら80以上を設定する必要があります。
+M5.Axp.ScreenBreath(8) で、画面の明るさを調整できます。8〜15が標準的ですが、7でも微かに読めます。
+
+ リスト 26 に、Preferenceと電源制御のサンプルを示します。Aボタンでcountを増やし、Aボタン長押しでcountをリセットします。Bボタンで再起動、電源ボタンで電源OFFします。
+
+ リスト 26 src/pref01.ino
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55 | #include <M5StickCPlus.h>
+#include <Preferences.h>
+
+Preferences pref;
+
+int count = 0;
+void setup() {
+ M5.begin();
+ M5.Lcd.setRotation(0); //縦型
+ M5.Lcd.fillScreen(CYAN);
+ M5.Lcd.setTextSize(2);
+ loadCount(&count);
+ M5.Beep.tone(2000, 500);
+ M5.Lcd.printf("count = %d\n", count);
+}
+
+void loop() {
+ M5.Beep.update();
+ M5.update();
+ if (M5.BtnA.wasReleasefor(1000) ) { // Aボタン長押し
+ count = 0;
+ saveCount(&count);
+ M5.Beep.tone(1200, 300);
+ M5.Lcd.printf("count = %d\n", count);
+ } else if (M5.BtnA.wasReleased()) { // Aボタン押し
+ count++ ;
+ saveCount(&count);
+ M5.Beep.tone(2000, 300);
+ M5.Lcd.printf("count = %d\n", count);
+ } else if (M5.BtnB.wasReleased()) { // Bボタン押し
+ reboot();
+ } else if (M5.Axp.GetBtnPress() == 2) { // 電源ボタン押し
+ poweroff();
+ }
+ delay(50);
+}
+
+void loadCount(int *c) {
+ pref.begin("mydata", false);
+ *c = pref.getInt("count");
+ pref.end();
+}
+void saveCount(int *c) {
+ pref.begin("mydata", false);
+ pref.putInt("count", *c);
+ pref.end();
+}
+
+void poweroff() {
+ M5.Axp.Write1Byte(0x32, M5.Axp.Read8bit(0x32) | 0x80);//PowerOff
+}
+
+void reboot() {
+ ESP.restart();
+}
+
+ |
+
その他
diff --git a/_build/html/week2.html b/_build/html/week2.html
index 8720a6b..f60d926 100644
--- a/_build/html/week2.html
+++ b/_build/html/week2.html
@@ -97,6 +97,8 @@
1週目
2週目
@@ -180,6 +182,276 @@
Git を利用すると、複数人で作業したファイルを統合しやすいです。
+
+ タスク
+ 複数の機能を1つの loop() にまとめようとすると、プログラムが複雑になります。タスクを用いると、 loop() に相当する関数を複数定義し、並列に動作させることができます。
+ リスト 27 に、タスクを利用する例を示します。3つの異なるタスクを作成し、それぞれの関数内部で setup() と loop() に相当する処理を記述しています。
+引数の詳細については、非公式日本語リファレンス を参照してください。
+ここの例では、1つのファイルに記述していますが、タスクごとに別のファイルにすることもできます。 例では使用していませんが、 TaskHandle_t は、タスクの一時停止(サスペンド)や、再開(レジューム)、削除のときに利用します。
+
+ リスト 27 src/task01.ino
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70 | #include <M5StickCPlus.h>
+
+int interval_msec[] = { 333, 1000, 2000 }; // Led, Lcd, Beep
+TaskHandle_t tH[3];
+
+void ledTask(void *pvParam) {
+ /** setup をここに書く **/
+ portTickType lastTime;
+ int PIN = 10;
+ pinMode(PIN, OUTPUT); // PINのモード設定
+ int highOrLow = 0;
+ for (;;) {
+ /** loop をここに書く **/
+ lastTime = xTaskGetTickCount();
+ vTaskDelayUntil(&lastTime, interval_msec[0] ); // 第2引数に、実行間隔ミリ秒を指定
+ digitalWrite(PIN, highOrLow); // HIGH = 1, LOW = 0
+ highOrLow = 1 - highOrLow; // HIGH <=> LOW を切り替える
+ }
+}
+
+void lcdTask(void *pvParam) {
+ /** setup をここに書く **/
+ portTickType lastTime;
+ M5.Lcd.setRotation(0);
+ M5.Lcd.fillScreen(GREEN);
+ M5.Lcd.setTextColor(WHITE, OLIVE);
+ M5.Lcd.setTextSize(2);
+ M5.Lcd.setCursor(0, 0);
+ int count = 0;
+ for (;;) {
+ /** loop をここに書く **/
+ M5.Lcd.printf("count=%d\n", count);
+ count++;
+ lastTime = xTaskGetTickCount();
+ vTaskDelayUntil(&lastTime, interval_msec[1] ); // 第2引数に、実行間隔ミリ秒を指定
+ if (count % 10 == 0) {
+ M5.Lcd.fillScreen(GREEN);
+ M5.Lcd.setCursor(0, 0);
+ }
+ }
+}
+
+void beepTask(void *pvParam) {
+ /** setup をここに書く **/
+ portTickType lastTime;
+ int f[8] = { 262, 294, 330, 349, 392, 440, 494, 524 };
+ int note = 0;
+ for (;;) {
+ /** loop をここに書く **/
+ lastTime = xTaskGetTickCount(); // ここでの時刻を変数に保存
+ M5.Beep.tone( f[note] );
+ delay(500); //0.5秒鳴らす
+ M5.Beep.mute();
+ note = (note+1)%8;
+ vTaskDelayUntil(&lastTime, interval_msec[2] );
+ // 途中の処理やdelayは含まず、「保存」時刻の2秒後まで待つ。
+ }
+}
+
+void setup() {
+ M5.begin();
+
+ xTaskCreatePinnedToCore(ledTask , "LedT", 4096, NULL, 1, &tH[0], 1/*<= CoreNo.*/ );
+ xTaskCreatePinnedToCore(lcdTask , "LcdT", 4096, NULL, 1, &tH[1], 1/*<= CoreNo.*/ );
+ xTaskCreatePinnedToCore(beepTask,"BeepT", 4096, NULL, 1, &tH[2], 0/*<= CoreNo.*/ );
+}
+
+void loop() {
+ delay(10);
+}
+
+ |
+
+
+ タスク生成時に、引数を渡すこともできます。リスト 28 に、引数を渡す例を示します。(次で述べるミューテックスを使って、排他制御もしています。)
+
+ リスト 28 src/task02.ino
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45 | xSemaphoreHandle mutex; //ミューテックス(排他制御用)
+
+void withArgTask(void *pvParam) {
+ int num = *(int*) pvParam; // 引数は、グローバル変数のアドレスをポインタで渡す
+ BaseType_t mStatus;
+ char* tsknm = pcTaskGetTaskName(NULL); //自タスク名を取得するならNULL、他タスク名を取得するならタスクハンドルを引数に指定する。
+ while (1) {
+ mStatus = xSemaphoreTake(mutex, 500); // ミューテックスを取得
+ if (mStatus == pdPASS) {
+ Serial.println("----");
+ Serial.printf("[%s] ", tsknm );
+ delay(500);
+ for (int i = 1; i < 6; i++) {
+ Serial.printf("%d ", i * num);
+ delay(300);
+ }
+ delay(300);
+ Serial.printf("done \n");
+ delay(300);
+ xSemaphoreGive(mutex); // ミューテックスを解放
+ vTaskDelete(NULL); // 自タスクを削除する
+ } else {
+ delay(random(10,100)); //ミューテックスがとれなかったらランダムに待つ
+ }
+ }
+}
+
+int arg[] = {2, 3, 5, 7, 11} ;
+char tskname[5];
+
+void setup() {
+ Serial.begin(115200);
+ mutex = xSemaphoreCreateMutex(); // ミューテックス作成
+}
+
+void loop() {
+ Serial.println("-------");
+ if (mutex != NULL) { // ミューテックスの作成に成功していたら
+ for (int i = 0; i < 5; i++) {
+ sprintf(tskname, "x%d", arg[i]);
+ xTaskCreatePinnedToCore(withArgTask, tskname, 4096, &arg[i], 1, NULL , 1);
+ }
+ }
+ delay(20 * 1000); // 次のタスクの仕込みまで、20秒待つ
+}
+
+ |
+
+
+ 警告
+ 引数をアドレスで渡すとき、関数内で宣言したローカル変数は使えません。グローバルな変数を使用する必要があります。
+
+
+
+ ミューテックスとセマフォ
+ 複数のタスクを並列動作させると、リソースに同時アクセスすることで意図しない動作を引き起こすことがあります。
+ミューテックスを用いると、リソースに同時にアクセスできるタスクを限定することができます。これを「排他制御」と呼びます。
+ バイナリセマフォは、タスク間またはタスクと割り込み間の同期に適しています。
+バイナリセマフォとミューテックスは似ていますが、いくつか本質的な違いがあります。ミューテックスは優先度を継承する機構が備わっていますが、バイナリセマフォには備わっていません。
+
+ ただし、バイナリセマフォを用いるより、RTOS Task Notifications
+を用いたほうが、高速かつメモリ使用量を削減できるようです。
+
+
diff --git a/src/pref01.ino b/src/pref01.ino
new file mode 100644
index 0000000..a86d7cc
--- /dev/null
+++ b/src/pref01.ino
@@ -0,0 +1,55 @@
+#include
+#include
+
+Preferences pref;
+
+int count = 0;
+void setup() {
+ M5.begin();
+ M5.Lcd.setRotation(0); //縦型
+ M5.Lcd.fillScreen(CYAN);
+ M5.Lcd.setTextSize(2);
+ loadCount(&count);
+ M5.Beep.tone(2000, 500);
+ M5.Lcd.printf("count = %d\n", count);
+}
+
+void loop() {
+ M5.Beep.update();
+ M5.update();
+ if (M5.BtnA.wasReleasefor(1000) ) { // Aボタン長押し
+ count = 0;
+ saveCount(&count);
+ M5.Beep.tone(1200, 300);
+ M5.Lcd.printf("count = %d\n", count);
+ } else if (M5.BtnA.wasReleased()) { // Aボタン押し
+ count++ ;
+ saveCount(&count);
+ M5.Beep.tone(2000, 300);
+ M5.Lcd.printf("count = %d\n", count);
+ } else if (M5.BtnB.wasReleased()) { // Bボタン押し
+ reboot();
+ } else if (M5.Axp.GetBtnPress() == 2) { // 電源ボタン押し
+ poweroff();
+ }
+ delay(50);
+}
+
+void loadCount(int *c) {
+ pref.begin("mydata", false);
+ *c = pref.getInt("count");
+ pref.end();
+}
+void saveCount(int *c) {
+ pref.begin("mydata", false);
+ pref.putInt("count", *c);
+ pref.end();
+}
+
+void poweroff() {
+ M5.Axp.Write1Byte(0x32, M5.Axp.Read8bit(0x32) | 0x80);//PowerOff
+}
+
+void reboot() {
+ ESP.restart();
+}
\ No newline at end of file
diff --git a/src/task01.ino b/src/task01.ino
new file mode 100644
index 0000000..0a4aec8
--- /dev/null
+++ b/src/task01.ino
@@ -0,0 +1,70 @@
+#include
+
+int interval_msec[] = { 333, 1000, 2000 }; // Led, Lcd, Beep
+TaskHandle_t tH[3];
+
+void ledTask(void *pvParam) {
+ /** setup をここに書く **/
+ portTickType lastTime;
+ int PIN = 10;
+ pinMode(PIN, OUTPUT); // PINのモード設定
+ int highOrLow = 0;
+ for (;;) {
+ /** loop をここに書く **/
+ lastTime = xTaskGetTickCount();
+ vTaskDelayUntil(&lastTime, interval_msec[0] ); // 第2引数に、実行間隔ミリ秒を指定
+ digitalWrite(PIN, highOrLow); // HIGH = 1, LOW = 0
+ highOrLow = 1 - highOrLow; // HIGH <=> LOW を切り替える
+ }
+}
+
+void lcdTask(void *pvParam) {
+ /** setup をここに書く **/
+ portTickType lastTime;
+ M5.Lcd.setRotation(0);
+ M5.Lcd.fillScreen(GREEN);
+ M5.Lcd.setTextColor(WHITE, OLIVE);
+ M5.Lcd.setTextSize(2);
+ M5.Lcd.setCursor(0, 0);
+ int count = 0;
+ for (;;) {
+ /** loop をここに書く **/
+ M5.Lcd.printf("count=%d\n", count);
+ count++;
+ lastTime = xTaskGetTickCount();
+ vTaskDelayUntil(&lastTime, interval_msec[1] ); // 第2引数に、実行間隔ミリ秒を指定
+ if (count % 10 == 0) {
+ M5.Lcd.fillScreen(GREEN);
+ M5.Lcd.setCursor(0, 0);
+ }
+ }
+}
+
+void beepTask(void *pvParam) {
+ /** setup をここに書く **/
+ portTickType lastTime;
+ int f[8] = { 262, 294, 330, 349, 392, 440, 494, 524 };
+ int note = 0;
+ for (;;) {
+ /** loop をここに書く **/
+ lastTime = xTaskGetTickCount(); // ここでの時刻を変数に保存
+ M5.Beep.tone( f[note] );
+ delay(500); //0.5秒鳴らす
+ M5.Beep.mute();
+ note = (note+1)%8;
+ vTaskDelayUntil(&lastTime, interval_msec[2] );
+ // 途中の処理やdelayは含まず、「保存」時刻の2秒後まで待つ。
+ }
+}
+
+void setup() {
+ M5.begin();
+
+ xTaskCreatePinnedToCore(ledTask , "LedT", 4096, NULL, 1, &tH[0], 1/*<= CoreNo.*/ );
+ xTaskCreatePinnedToCore(lcdTask , "LcdT", 4096, NULL, 1, &tH[1], 1/*<= CoreNo.*/ );
+ xTaskCreatePinnedToCore(beepTask,"BeepT", 4096, NULL, 1, &tH[2], 0/*<= CoreNo.*/ );
+}
+
+void loop() {
+ delay(10);
+}
\ No newline at end of file
diff --git a/src/task02.ino b/src/task02.ino
new file mode 100644
index 0000000..5e42e95
--- /dev/null
+++ b/src/task02.ino
@@ -0,0 +1,45 @@
+xSemaphoreHandle mutex; //ミューテックス(排他制御用)
+
+void withArgTask(void *pvParam) {
+ int num = *(int*) pvParam; // 引数は、グローバル変数のアドレスをポインタで渡す
+ BaseType_t mStatus;
+ char* tsknm = pcTaskGetTaskName(NULL); //自タスク名を取得するならNULL、他タスク名を取得するならタスクハンドルを引数に指定する。
+ while (1) {
+ mStatus = xSemaphoreTake(mutex, 500); // ミューテックスを取得
+ if (mStatus == pdPASS) {
+ Serial.println("----");
+ Serial.printf("[%s] ", tsknm );
+ delay(500);
+ for (int i = 1; i < 6; i++) {
+ Serial.printf("%d ", i * num);
+ delay(300);
+ }
+ delay(300);
+ Serial.printf("done \n");
+ delay(300);
+ xSemaphoreGive(mutex); // ミューテックスを解放
+ vTaskDelete(NULL); // 自タスクを削除する
+ } else {
+ delay(random(10,100)); //ミューテックスがとれなかったらランダムに待つ
+ }
+ }
+}
+
+int arg[] = {2, 3, 5, 7, 11} ;
+char tskname[5];
+
+void setup() {
+ Serial.begin(115200);
+ mutex = xSemaphoreCreateMutex(); // ミューテックス作成
+}
+
+void loop() {
+ Serial.println("-------");
+ if (mutex != NULL) { // ミューテックスの作成に成功していたら
+ for (int i = 0; i < 5; i++) {
+ sprintf(tskname, "x%d", arg[i]);
+ xTaskCreatePinnedToCore(withArgTask, tskname, 4096, &arg[i], 1, NULL , 1);
+ }
+ }
+ delay(20 * 1000); // 次のタスクの仕込みまで、20秒待つ
+}
\ No newline at end of file
diff --git a/week1.rst b/week1.rst
index 135f20f..fc2d047 100755
--- a/week1.rst
+++ b/week1.rst
@@ -480,7 +480,7 @@
ここでは、Bluetoothを用いて、上記と同様、2台のデバイス間で文字列を送受信する例を示します。Bluetoth では、機器同士は同一の「プロファイル」という通信方式に対応している必要があります。Serial Protocol Profile は、Bluetooth無線通信で仮想シリアル接続を可能にするプロファイルです。
-.. note:: 基本的に `ESP32同士をBluetoothシリアルでつないでみる `_ と同じです。
+.. note:: 基本的に `ESP32同士をBluetoothシリアルでつないでみる `_ と同じです。その他、一般的なBluetoothとBLEについての解説は、`ESP32による近距離無線通信の実験② BLE通信 `_ が参考になります。
(1) スレーブ用デバイスのMACアドレスを調べる
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -496,28 +496,45 @@
(2) マスタ用デバイスにサンプルを書き込む
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-:numref:`bts01_master` を、2台目のM5StickCPlusで実行します。このとき、(1)で調べたMACアドレスを指定します。
+:numref:`bts01_master` を、2台目のM5StickCPlusで実行します。このとき、ハイライトした行のところに、(1)で調べたMACアドレスを指定します。少し時間がかかりますが、デバイス名で接続先を指定することもできます。起動するときは、スレーブ側を先に起動しておき、あとでマスタ側を起動してください。
.. literalinclude:: src/bts01_master.ino
:caption:
:name: bts01_master
:language: arduino
:linenos:
+ :emphasize-lines: 30
-Bluetooth Low Energy
+その他のBluetooth利用例
--------------------------------------
-アドバータイズ
+BLEHIDDeviceを用いると、Human Interface Device(HID) Profileを導入して、マウスやキーボードの代用品が作成できます。(詳細は省略します。)
Preference
-------------------------------------------
+再起動をすると、プログラムがリセットされて、通常の変数や配列データは消えてしまいます。Preferenceを用いると、不揮発性のフラッシュ領域を使ってデータを保存・復元することができます。
-電力制御
+https://github.com/espressif/arduino-esp32/blob/master/libraries/Preferences/examples/StartCounter/StartCounter.ino
+
+
+電源制御・電力制御
-----------------------------------
+- ``ESP.restart()`` で、再起動します。
+- ``M5.Axp.Write1Byte(0x32, M5.Axp.Read8bit(0x32) | 0x80);`` で、電源OFFします。
+- ``setCpuFrequencyMhz(240)`` で、CPUのクロック数を変更できます。設定できる値は、240、160、80、40、20、10ですが、無線通信するなら80以上を設定する必要があります。
+- ``M5.Axp.ScreenBreath(8)`` で、画面の明るさを調整できます。8〜15が標準的ですが、7でも微かに読めます。
+
+:numref:`pref01` に、Preferenceと電源制御のサンプルを示します。Aボタンでcountを増やし、Aボタン長押しでcountをリセットします。Bボタンで再起動、電源ボタンで電源OFFします。
+
+.. literalinclude:: src/pref01.ino
+ :caption:
+ :name: pref01
+ :language: arduino
+ :linenos:
その他
diff --git a/week2.rst b/week2.rst
index b454a9d..0e09e28 100755
--- a/week2.rst
+++ b/week2.rst
@@ -14,4 +14,49 @@
- 複数のファイルを置いたときの挙動について: ``***.ino`` ファイルの内容は、単純にメインのタブ(フォルダ名と同じinoファイル)にマージされます。``***.cpp`` や ``***.c`` という拡張子でファイルを作成した場合は、``***.h`` を作成する必要があります。`参考:Properly using separate tabs with Arduino IDE `_
- Git を利用すると、複数人で作業したファイルを統合しやすいです。
+タスク
+--------------------------------------------------
+複数の機能を1つの ``loop()`` にまとめようとすると、プログラムが複雑になります。タスクを用いると、 ``loop()`` に相当する関数を複数定義し、並列に動作させることができます。
+
+:numref:`task01` に、タスクを利用する例を示します。3つの異なるタスクを作成し、それぞれの関数内部で ``setup()`` と ``loop()`` に相当する処理を記述しています。
+引数の詳細については、`非公式日本語リファレンス `_ を参照してください。
+ここの例では、1つのファイルに記述していますが、タスクごとに別のファイルにすることもできます。 例では使用していませんが、 ``TaskHandle_t`` は、タスクの一時停止(サスペンド)や、再開(レジューム)、削除のときに利用します。
+
+.. literalinclude:: src/task01.ino
+ :caption:
+ :name: task01
+ :language: arduino
+ :linenos:
+.. :emphasize-lines: 6-7, 15,19
+
+.. note:: 参考: `FreeRTOSでマルチタスク (on ESP32) `_
+
+タスク生成時に、引数を渡すこともできます。:numref:`task02` に、引数を渡す例を示します。(次で述べるミューテックスを使って、排他制御もしています。)
+
+.. literalinclude:: src/task02.ino
+ :caption:
+ :name: task02
+ :language: arduino
+ :linenos:
+ :emphasize-lines: 1,6,8,20,21,33,41
+
+.. warning:: 引数をアドレスで渡すとき、関数内で宣言したローカル変数は使えません。グローバルな変数を使用する必要があります。
+
+ミューテックスとセマフォ
+----------------------------------------------------------
+
+複数のタスクを並列動作させると、リソースに同時アクセスすることで意図しない動作を引き起こすことがあります。
+ミューテックスを用いると、リソースに同時にアクセスできるタスクを限定することができます。これを「排他制御」と呼びます。
+
+バイナリセマフォは、タスク間またはタスクと割り込み間の同期に適しています。
+バイナリセマフォとミューテックスは似ていますが、いくつか本質的な違いがあります。ミューテックスは優先度を継承する機構が備わっていますが、バイナリセマフォには備わっていません。
+
+
+
+.. note:: 参考: `RTOS binary semaphore API `_
+
+ただし、バイナリセマフォを用いるより、`RTOS Task Notifications `_
+を用いたほうが、高速かつメモリ使用量を削減できるようです。
+
+- MCU RTOS習得(2020年版) http://happytech.jp/bRTOS.html
|