ArduinoのPinChange割り込みライブラリとタイマーライブラリを使う
公開日 更新日 2020/09/23
前回、素早くマイクミュートやPCスリープが行えるボタンを自作することで、とても晴れやかな気持ちになれました。
今回は、その後日談としてArduinoプログラムをもう少し手直しした改良版を作ったので、その変更点をまとめようと思います。
Contents
改良版 Arduinoソースコード
何はともあれ変更後のソースコードです。変更点については後半にまとめます。
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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
#include <Keyboard.h> #include <PinChangeInterrupt.h> #include <arduino-timer.h> #define BUTTON1_PIN 8 #define BUTTON2_PIN 7 #define LED_PIN 3 #define BUTTON_CHECK_INTERVAL_MILLIS 8 #define BUTTON_CHECK_THRESHOLD 3 #define KEYBOARD_OP_INTERVAL_MILLIS 12 enum buttonOperation {pressKey, releaseKey}; struct macroDefinition { buttonOperation op; char key; }; struct tickArg { macroDefinition *def; int numOfDefs; }; /** Alt + a */ const macroDefinition macroToggleMicrophone[] = { {pressKey, KEY_LEFT_ALT}, {pressKey, 'a'}, {releaseKey} }; /** Win + x -> u -> s */ const macroDefinition macroEnterSleep[] = { {pressKey, KEY_LEFT_GUI}, {pressKey, 'x'}, {releaseKey}, {pressKey, 'u'}, {releaseKey}, {pressKey, 's'}, {releaseKey} }; Timer<1, millis> countTimer; Timer<2, millis, tickArg> macroTimer; Timer<>::Task countTimerTask; Timer<>::Task lastOperationTask; int button_counter = 0; bool button_pressed = false; void setup() { // Deactivate built-in LEDs pinMode(LED_BUILTIN_TX, INPUT); pinMode(LED_BUILTIN_RX, INPUT); pinMode(BUTTON1_PIN, INPUT_PULLUP); pinMode(BUTTON2_PIN, INPUT_PULLUP); pinMode(LED_PIN, OUTPUT); attachPCINT(digitalPinToPCINT(BUTTON1_PIN), pinChangeFunc, CHANGE); Keyboard.begin(); } void loop() { countTimer.tick(); macroTimer.tick(); } void pinChangeFunc() { uint8_t trigger = getPinChangeInterruptTrigger(digitalPinToPCINT(BUTTON1_PIN)); if (trigger != (button_pressed ? FALLING : RISING)) { countTimer.cancel(countTimerTask); button_counter = 0; button_pressed = !button_pressed; countTimerTask = countTimer.every(BUTTON_CHECK_INTERVAL_MILLIS, tickButtonCheck); } } bool tickButtonCheck() { button_counter++; if (button_counter > BUTTON_CHECK_THRESHOLD) { if (button_pressed) { digitalWrite(LED_PIN, HIGH); scheduleMacroOperations(); } else { digitalWrite(LED_PIN, LOW); } return false; } return true; } void scheduleMacroOperations() { if (lastOperationTask) { return; } macroDefinition *macroDefs; int numOfMacroDefs; if (digitalRead(BUTTON2_PIN) == LOW) { macroDefs = macroToggleMicrophone; numOfMacroDefs = sizeof(macroToggleMicrophone) / sizeof(macroDefinition); } else { macroDefs = macroEnterSleep; numOfMacroDefs = sizeof(macroEnterSleep) / sizeof(macroDefinition); } tickMacroOperation({macroDefs, numOfMacroDefs}); } bool tickMacroOperation(tickArg argDef) { macroDefinition *macroDef = argDef.def; if (macroDef->op == pressKey) { Keyboard.press(macroDef->key); } else if (macroDef->op == releaseKey) { Keyboard.releaseAll(); } if (argDef.numOfDefs > 1) { argDef.def++; argDef.numOfDefs--; lastOperationTask = macroTimer.in(KEYBOARD_OP_INTERVAL_MILLIS, tickMacroOperation, argDef); } else { lastOperationTask = NULL; } return false; } |
変更点
変更点1 Pin Change Interrupt割り込み
Arduinoはマイクロコントローラーの各種割り込み機能を扱うことができます。
今回はそれらの割り込み機能のうち、特定のGPIOへの通電状況の変化をきっかけにした割り込み処理を実行させるPin Change Interruptという機能を、ボタンを押したときの処理の実装に使うようにしました。
前回のコード(loop()関数内で一定間隔でボタン用GPIOの状態を監視する処理)に比べ、この変更による改善点は以下3点です。
- ボタンの状態変化が処理の起点になるので、監視処理にあるようなタイミングによる遅延が起きにくい
- loop()内でdigitalRead()を頻繁に実行してよいのか気になってたので、気持ちを落ち着かせることができる
- loop()からボタンの監視処理を分離できるので、ソースコードが少し見やすくなる
1点目にある遅延は監視間隔を短くすると気にならなくなるものの、それだけ2点目にあげたdigitalRead()実行回数が増えてしまうため、たとえ問題なく動作しようともそのような実装が最善とは思えません。
ここがPin Change Interrupt割り込みを使った処理にしたことによって得られる一番の改善点です。
Arduino標準機能で割り込みを扱うことは可能なのですが、マイクロコントローラーのマニュアルをにらめっこして実装する必要があり1段階敷居が高くなってしまいます。
これは実装は可能なのですが将来問題が起きた時の保守性を考えると、ソースコードが複雑になればそれだけ保守性の低下を招くため、極力楽に読み取れるソースコードを維持します。
これには、Pin Change Interruptを扱うライブラリを導入することで解決しました。
今回使用したライブラリは、NicoHoodさんがGitHubで公開しているPinChangeInterrupt(v1.2.7)です。
ほかにもライブラリは存在しますが、それほど使い勝手が変わらないと思ったのでGoogleで検索して最初にヒットしたものにしました。
インストールは、Arduino IDEのメニューから[Sketch] – [Include Library] – [Manage Libraries]をたどったLibrary Manager画面から実施です。
ソースコードの該当箇所は、64行目でのattachPCINT()によるPin Change検出時に実行するハンドラの登録、および74行目から始まるハンドラ定義の本体pinChangeFunc()。
変更点2 タイマー割り込み
前回のコードでいう、loop()関数内にて実装してた「ボタンを一定時間押したかどうかの判定処理」と、「特定間隔ごとのUSBキーボード処理」の2つをタイマー割り込みを提供するライブラリを使い実装しました。
この変更による改善点は以下2点です。
- 非同期実行で実現してるため、loop()関数からUSBキーボードの一連の操作処理を分離させ、loop()関数を短時間で完了させることができる
- ボタンを押したかどうかの判定に使うようなフラグを排除してソースコードが少し見やすくなる
今回扱っている自作ボタンのPCスリープは7個のキー操作なので、実行時間は1秒未満であり前回のコードでもストレスに感じることはなく、わざわざこの変更により非同期実行を導入する必要はありません。
しかしながら、せっかくPinChange割り込みを活用した変更を加えることにしたので、ついでにタイマー割り込みも導入することでソースコードの見通しを一気に向上させてみました。
ここで使用したライブラリは、contremさんがGitHubで公開しているarduino-timer(v2.1.0)です。
インストールは先ほど同様に、Arduino IDEのメニューから[Sketch] – [Include Library] – [Manage Libraries]をたどったLibrary Manager画面から実施です。
ソースコードの該当箇所は主に、「ボタンを一定時間押したかどうかの判定処理」については80行目のタイマーへのハンドラ登録と84行目から始まるハンドラ定義の本体tickButtonCheck()、「特定間隔ごとのUSBキーボード処理」については126行目のタイマーへのハンドラ登録と115行目から再帰的に呼びだすハンドラ定義の本体tickMacroOperation()。
変更点3 キーボード操作のデータ構造
前回のコードでは、USBキーボードの操作は、キー入力関数Keyboard.press()等を必要数だけハードコーディングされてました。
さすがにこれは手を抜いた作りだと思いましたので、キーボードの操作を定義したデータとして持たせ、キー入力実行時にこの定義データを読み取りながら操作するという処理に変更します。
例えば、将来もっとキー入力を長くしたり判定を加えるなど複雑にしようと思うならば、このようなデータ構造でもたせる必要があるので、このタイミングで変更しておきました。
ソースコードの該当箇所は、25行目~45行目の定義データ、117行目~121行目でのデータ解析によるキー操作処理。
変更点4 GPIOポート
1度作った後に組み立てにくいと感じ、よく見るとワイヤーの配線したGPIOの位置がよくないためだったとわかりました。
そこでワイヤーの位置を3,14,15から3,7,8に変更しました。
なお、今回使ったPinChange割り込みは使えるポートを意識する必要があるそうで、その範囲内でよい場所を決めました。
この変更に伴いソースコードの5,6行目の定義も変更。
最後に
自作ボタンV1の完成後、欲が出てV2へと改良しました。
これは問題を直したということではなく、ソースコードを見ていたら気になったために調査&実装に至った改善をまとめたものになります。
Arduinoはライブラリが豊富にあるので、やりたいことに対してそれほど苦労することなく実現できるのは素晴らしいと思います。
レスポンシブ広告
関連記事
-
AndroidとiPhoneのUSB充電器の規格の違いに関する疑問を調査
先月タブレットを購入して以来、スマートフォンやタブレットをUSB接続で充電するた …
-
ATmega328へArduinoを書き込む
Arduino UNO等のArduinoボードを購入すると、USB端子に直接接続 …
-
USBケーブルの違いから来るもろもろの比較
USBケーブルって端子の形が同じなのだから、どれを使ってもいい感じに使えるんじゃ …
-
Arduinoでロータリーエンコーダーの動作確認
ロータリーエンコーダーは、よくボリューム調整のつまみに使われているようなモジュー …
-
Attiny13Aの工場出荷時の書き込みエラーを対策
Attiny13Aは工場出荷時に低速の動作周波数になってる模様です。 この時Ar …
-
SlackのIncoming Webhooksが失敗した時の対処
我が家のIoTとしてSlackのIncoming Webhooksを活用したサー …
-
ATmega328の書き込み装置をArduino UNO用シールドとして作成
前回、AVRマイクロコントローラーのATmega328PへのArduinoブート …
-
素早くマイクミュートとPCスリープが出来るDIYボタンを作る
2020年はCOVID-19の影響で長期にわたる外出自粛が続きました。9月時点で …
-
ドア開閉イベントをSlackへお知らせするセンサーを作る
最近Raspberry Piを手に入れてから個人的にマイコンが流行っていて、WE …
-
「子ども見守りサービス」をDIYで自作する
最近「子ども見守りサービス」が注目されているようです。 どうも私は課金の発生する …