在下最近打算製作一個可以調整音量的工具
雖然一鍵一功能的設計不需要太多學習要求,製作又直觀,但佔據較多空間,因此想使用一個零件完成這個操作
例如以旋鈕順時針轉動提升音量、逆時針轉動降低音量,旋鈕按下時切換靜音
外觀
旋轉編碼器(Rotary Encoder) 與 電位器(Potentiometer) 很相似
但 旋轉編碼器 內部並 沒有電阻,而 軸心 能夠無限制地轉動
引腳
一般 旋轉編碼器 通常有 3支引腳 ,分別是: 1號訊號引腳 、 2號訊號引腳 及 公共引腳
普遍 旋轉編碼器 的 軸心 還具備 按壓按鈕 功能,因此共有 5支引腳
通常為了方便區分,2支引腳為按壓按鈕功能,沒有區分功能,其中一支接地
3支引腳中,左引腳及右引腳分別為 2支訊號引腳,中央的引腳為 公共引腳(Common) ,通常連接到 接地
還有一些 旋轉編碼器 焊接到 印刷電路板 上,並標記引腳的功能
一款常見的模組化 旋轉編碼器 ,稱為 KY-040 ,通常會在一些 STEM 學習套裝中一同發售
印刷電路板 背面的線路焊接 電阻器 作為 上拉電阻
還有一些 旋轉編碼器 會焊接電容器 ,令訊號比較穩定
運作原理
實際上 2支訊號引腳 與 按壓按鈕 的功能完全相同,只是型態與操作方式不同
軸心 連接到到 公共引腳,而 2支訊號引腳 分別連接到 2個固定的終端
轉動軸心時,公共引腳 會依特定次序連接到 2個終端
由於轉動軸心的時間短而且快,因此會快速地切換 2個終端的狀態
從而產生 2組相同但有時間偏差的訊號,便可以得知轉動的方向
格雷碼
根據轉動軸心而令 2個端終 產生一種有次序的訊號,稱為 格雷碼(Gray Code)
引腳A | 引腳B | 狀態(順時針) |
---|---|---|
0 | 0 | 0 (靜止狀態) |
1 | 0 | 1 |
1 | 1 | 2 |
0 | 1 | 3 |
0 | 0 | 0 (完成) |
引腳A | 引腳B | 狀態(逆時針) |
---|---|---|
0 | 0 | 0 (靜止狀態) |
0 | 1 | 3 |
1 | 1 | 2 |
1 | 0 | 1 |
0 | 0 | 0 (完成) |
從狀態表中會發現當順時針轉動時,狀態次序為 01230 ;而當逆時針轉動時,狀態次序為 03210
然而在實際的電路中,由於電子訊號太快,不可能偵測到完整的 01230 或 03210 的訊號
因此順時針轉動時,只需要判斷 01, 12, 23, 30 其中一個狀態
而逆時針轉動時,同樣只需要判斷 03, 32, 21, 10 其中一個狀態
線路原型
在下使用 Raspberry Pi Pico 來測試
實際線路
由於原裝的 Raspberry Pi Pico 使用 Micro USB-Type B 插口
碰巧在下測試時找不到 Micro USB-Type B 的連接線
因此使用 Raspberry Pi Pico 的兼容開發板測試
編寫程式
Arduino
#define r1 14 #define r2 15 byte oldState = 0; byte newState = 0; int position = 0; void setup() { Serial.begin(115200); pinMode(r1, INPUT_PULLUP); pinMode(r2, INPUT_PULLUP); } void loop() { bool v1 = !digitalRead(r1); bool v2 = !digitalRead(r2); if (!v1 && !v2) { newState = 0; } else if (v1 && !v2) { newState = 1; } else if (v1 && v2) { newState = 2; } else if (!v1 && v2) { newState = 3; } if (oldState == 3 && newState == 0) { position++; Serial.println(position); } else if (oldState == 1 && newState == 0) { position--; Serial.println(position); } oldState = newState; }
使用 Arduino IDE 編寫的 Sketch
MicroPython
from machine import Pin r1 = Pin(14, Pin.IN, Pin.PULL_UP) r2 = Pin(15, Pin.IN, Pin.PULL_UP) old_state = 0 new_state = 0 position = 0 while True: v1 = not r1.value() v2 = not r2.value() if not v1 and not v2: new_state = 0 elif v1 and not v2: new_state = 1 elif v1 and v2: new_state = 2 elif not v1 and v2: new_state = 3 if old_state == 3 and new_state == 0: position += 1 print(position) elif old_state == 1 and new_state == 0: position -= 1 print(position) old_state = new_state
在下亦使用 MicroPython 達到相同效果
CircuitPython
在下亦編寫 CircuitPython 測試
import board from digitalio import DigitalInOut, Direction, Pull r1 = DigitalInOut(board.GP14) r1.direction = Direction.INPUT r1.pull = Pull.UP r2 = DigitalInOut(board.GP15) r2.direction = Direction.INPUT r2.pull = Pull.UP old_state = 0 new_state = 0 position = 0 while True: v1 = not r1.value v2 = not r2.value if not v1 and not v2: new_state = 0 elif v1 and not v2: new_state = 1 elif v1 and v2: new_state = 2 elif not v1 and v2: new_state = 3 if old_state == 3 and new_state == 0: position += 1 print(position) elif old_state == 1 and new_state == 0: position -= 1 print(position) old_state = new_state
CircuitPython 與 MicroPython 的程式碼接近相同
測試效果
閣下如果仔細觀察的話,會發現轉動軸心時,出現一個錯誤數值
原因是 旋轉編碼器 有 制動設計,有機會在轉動時產生 回彈
物件導向
在下以 物件導向(Object Oriented) 方式編寫 旋轉編碼器類別 ,將 旋轉編碼器 與 主程式 分離
方便管理 主程式 與 其他裝置 互動
Arduino
// RotaryEncoder.h class RotaryEncoder { private: byte r1; byte r2; byte oldState = 0; byte newState = 0; int position = 0; public: RotaryEncoder(byte r1, byte r2) { this->r1 = r1; this->r2 = r2; pinMode(this->r1, INPUT_PULLUP); pinMode(this->r2, INPUT_PULLUP); } void setPosition(int position) { this->position = position; } int getPosition() { bool v1 = !digitalRead(this->r1); bool v2 = !digitalRead(this->r2); if (!v1 && !v2) { newState = 0; } else if (v1 && !v2) { newState = 1; } else if (v1 && v2) { newState = 2; } else if (!v1 && v2) { newState = 3; } if (this->oldState == 3 && this->newState == 0) { position++; } else if (this->oldState == 1 && this->newState == 0) { position--; } this->oldState = this->newState; return this->position; } }; // .ino #include "RotaryEncoder.h" RotaryEncoder rotaryEncoder(14, 15); int oldPosition = 0; void setup() { Serial.begin(115200); } void loop() { int position = rotaryEncoder.getPosition(); if (oldPosition != position) { oldPosition = position; Serial.println(position); } }
MicroPython
# rotary_encoder.py from machine import Pin class RotaryEncoder: def __init__(self, r1Pin, r2Pin): self.__r1 = Pin(r1Pin, Pin.IN, Pin.PULL_UP) self.__r2 = Pin(r2Pin, Pin.IN, Pin.PULL_UP) self.__old_state = 0 self.__new_state = 0 self.__position = 0 def set_position(self, position): self.__position = position def get_position(self): v1 = not self.__r1.value() v2 = not self.__r2.value() if not v1 and not v2: self.__new_state = 0 elif v1 and not v2: self.__new_state = 1 elif v1 and v2: self.__new_state = 2 elif not v1 and v2: self.__new_state = 3 if self.__old_state == 3 and self.__new_state == 0: self.__position += 1 elif self.__old_state == 1 and self.__new_state == 0: self.__position -= 1 self.__old_state = self.__new_state return self.__position # main.py from rotary_encoder import RotaryEncoder rotary_encoder = RotaryEncoder(14, 15) old_position = 0 while True: position = rotary_encoder.get_position() if old_position != position: old_position = position print(position)
CircuitPython
# rotary_encoder.py from digitalio import DigitalInOut, Direction, Pull class RotaryEncoder: def __init__(self, r1Pin, r2Pin): self.__r1 = DigitalInOut(r1Pin) self.__r1.direction = Direction.INPUT self.__r1.pull = Pull.UP self.__r2 = DigitalInOut(r2Pin) self.__r2.direction = Direction.INPUT self.__r2.pull = Pull.UP self.__old_state = 0 self.__new_state = 0 self.__position = 0 def set_position(self, position): self.__position = position def get_position(self): v1 = not self.__r1.value v2 = not self.__r2.value if not v1 and not v2: self.__new_state = 0 elif v1 and not v2: self.__new_state = 1 elif v1 and v2: self.__new_state = 2 elif not v1 and v2: self.__new_state = 3 if self.__old_state == 3 and self.__new_state == 0: self.__position += 1 elif self.__old_state == 1 and self.__new_state == 0: self.__position -= 1 self.__old_state = self.__new_state return self.__position # code.py import board from rotary_encoder import RotaryEncoder rotary_encoder = RotaryEncoder(board.GP14, board.GP15) old_position = 0 while True: position = rotary_encoder.get_position() if old_position != position: old_position = position print(position)
基本上 Arduino 及 MicroPython 及 CircuitPython 大致上相同
CircuitPython 提供 原生函式庫 的 旋轉編碼器類別 來應用 旋轉編碼器
因此實際不需要自行編寫 旋轉編碼器類別
import board from rotaryio import IncrementalEncoder encoder = IncrementalEncoder(board.GP14, board.GP15) old_position = 0 while True: position = encoder.position if old_position != position: old_position = position print(position)
中斷請求
不論使用 Arduino 或 MicroPython 或 CircuitPython ,都需要不斷檢查引腳的訊號
再因應訊號來執行對應操作,因此通常會將這些檢查編寫在迴圈中
這種方法稱為 輪詢(Polling)
由於 輪詢 需要不斷重覆檢查訊號,即使沒有訊號變化亦會檢查,導致資源浪費
如果需要檢查的裝置很多,編寫的內容亦會非常混亂,運作的速度亦會變較慢
在下發現很多 旋轉編碼器 的例子都會使用 中斷(Interrupt) 來達到效果
而且使用 中斷 的情況下,都不需要在 Arduino Sketch 的 loop 中編寫內容亦能達到檢查效果
Arduino
Arduino 可以使用 attachInterrupt(interruptNumber, callbackFunction, mode) 來達到效果
- interruptNumber 不是 引腳編號,而是 中斷編號
需要配合 digitalPinToInterrupt 將 引腳編號 轉換為 中斷編號 - callbackFunction 是 回呼功能
- mode 設定在甚麼狀態觸發 中斷
- CHANGE 當訊號 改變時觸發
- FALLING 當訊號 由高至低變化時觸發
- RISING 當訊號 由低至高變化時觸發
- LOW 當訊號 在低電壓時觸發
- HIGH 當訊號 在高電壓時觸發(不是所有微控制器都支援)
了解用法後,可以之前的程式改寫成
#define r1 14 #define r2 15 #define sw 16 byte oldState = 0; byte newState = 0; int position = 0; void rotateHandler() { bool v1 = !digitalRead(r1); bool v2 = !digitalRead(r2); if (!v1 && !v2) { newState = 0; } else if (v1 && !v2) { newState = 1; } else if (v1 && v2) { newState = 2; } else if (!v1 && v2) { newState = 3; } if (oldState == 3 && newState == 0) { position++; Serial.println(position); } else if (oldState == 1 && newState == 0) { position--; Serial.println(position); } oldState = newState; } void pressHandler() { if (!digitalRead(sw)) { position = 0; Serial.println(position); } } void setup() { Serial.begin(115200); pinMode(r1, INPUT_PULLUP); pinMode(r2, INPUT_PULLUP); pinMode(sw, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(r1), rotateHandler, CHANGE); attachInterrupt(digitalPinToInterrupt(r2), rotateHandler, CHANGE); attachInterrupt(digitalPinToInterrupt(sw), pressHandler, CHANGE); } void loop() { }
同樣能夠做到相同的效果
MicroPython
而 MicroPython 則使用 Pin.irq(callbackFunction, mode) 來達成
- callbackFunction 是 回呼功能(與 Arduino 相似)
- mode 設定在甚麼狀態觸發 中斷
- Pin.IRQ_FALLING 當訊號由高至低變化時觸發
- Pin.IRQ_RISING 當訊號由低至高變化時觸發
MicroPython 只有 2種觸發狀態 ,如果將 2種觸發狀態 同時觸發 (即是 Pin.IRQ_FALLING | Pin.IRQ_RISING)
都只有 3種觸發狀態 ,並沒有 低電壓 或 高電壓 的 觸發狀態
from machine import Pin r1 = Pin(14, Pin.IN, Pin.PULL_UP) r2 = Pin(15, Pin.IN, Pin.PULL_UP) sw = Pin(16, Pin.IN, Pin.PULL_UP) old_state = 0 new_state = 0 position = 0 def rotate_handler(pin): global r1, r2, old_state, new_state, position v1 = not r1.value() v2 = not r2.value() if not v1 and not v2: new_state = 0 elif v1 and not v2: new_state = 1 elif v1 and v2: new_state = 2 elif not v1 and v2: new_state = 3 if old_state == 3 and new_state == 0: position += 1 print(position) elif old_state == 1 and new_state == 0: position -= 1 print(position) old_state = new_state def press_handler(pin): global sw, position if not sw.value(): position = 0 print(position) r1.irq(rotate_handler, Pin.IRQ_RISING | Pin.IRQ_FALLING) r2.irq(rotate_handler, Pin.IRQ_RISING | Pin.IRQ_FALLING) sw.irq(press_handler, Pin.IRQ_RISING | Pin.IRQ_FALLING) while True: pass
效果與 Arduino 相同
CircuitPython
CiruitPython 截至發佈文章前暫時沒有 中斷 ,但如果只是針對 旋轉編碼器 的實作
雖然 Adafruit 有提及使用 asyncio 及 countio 來達到效果,但實際並不是 中斷
而且 countio 並不支援 RP2040 ,即是 Raspberry Pi Pico 及其他使用 RP2040 的 微控制器都無法使用 中斷
語法限制
雖然 Arduino 及 MicroPython 都使用相似的方式 ,但 callbackFunction 存在限制
- 不能提交擁有參數 回呼功能 (MicroPython 中斷 的 回呼功能 必須只有 1個參數)
- 如果 回呼功能 在 類別 中使用,只可使用 靜態方法(Static Method) 或 全局方法(Global Method)
- 雖然支援 Lamdba 方法,但 Lamdba 只能存取 全局變數(Global Variable)
MicroPython
這些情況 Arduino 無法解決,但 MicroPython 卻不限制 回呼功能 只能使用 靜態方法
因此可以使用 物件方法(Object Method) 來替代,便可以同時使用 物件導向 及 中斷
# rotary_encoder.py from machine import Pin class RotaryEncoder: def __init__(self, r1Pin, r2Pin): self.__r1 = Pin(r1Pin, Pin.IN, Pin.PULL_UP) self.__r2 = Pin(r2Pin, Pin.IN, Pin.PULL_UP) self.__r1.irq(self.__rotate, Pin.IRQ_RISING | Pin.IRQ_FALLING) self.__r2.irq(self.__rotate, Pin.IRQ_RISING | Pin.IRQ_FALLING) self.__old_state = 0 self.__new_state = 0 self.position = 0 def __rotate(self, pin): v1 = not self.__r1.value() v2 = not self.__r2.value() if not v1 and not v2: self.__new_state = 0 elif v1 and not v2: self.__new_state = 1 elif v1 and v2: self.__new_state = 2 elif not v1 and v2: self.__new_state = 3 if self.__old_state == 3 and self.__new_state == 0: self.position += 1 elif self.__old_state == 1 and self.__new_state == 0: self.position -= 1 self.__old_state = self.__new_state # main.py import rotary_encoder from RotaryEncoder rotary_encoder = RotaryEncoder(14, 15) old_position = 0 while True: position = rotary_encoder.position if old_position != position: old_position = position print(position)
MicroPython 暫時唯一能夠同時使用 物件導向 及 中斷,而且還可以省卻 setter 及 getter
補充資料
改裝
由於在下想用 原始的旋轉編碼器 ,不希望有太多電子零件影響實際效果,但原本的引腳又不方便連接到麵包板
因此萬用板改裝自 旋轉編碼器(連引腳)
使用 旋轉編碼器 製作的 音量控制器
速度過快
由於 Arduino 的執行速度比 MicroPython 或 CircuitPython 快
令 微控制器 有足夠時間捕捉到多次旋轉軸心時的回彈
因此需要加入 delay 來去除回彈訊號,但使用 中斷 則不能使用 delay 或 delayMicroseconds
(在 Arduino中斷 回呼函數 中的 micros 或 millis 必定傳回 0 ,因此無法計算延遲;但可以使用其他方法延遲)
二進制及格雷碼轉換
二進制 與 格雷碼 非常相似,可以使用
byte numberToGray(byte number) { byte gray = number >> 1; gray ^= number; return gray; } bool v1 = !digitalRead(r1); bool v2 = !digitalRead(r2); byte gray = numberToGray(v1 | v2 << 1);
二進制 與 格雷碼 非常相似,可以使用這個轉換程式簡化大量判斷
總結
旋轉編碼器 是其中一種基本電子零件
在下最初以為 旋轉編碼器 與 電位器 都似相同功能的電子零件,但實際兩者應用上完全不同
之前在下改裝的遊戲控制器,因為需要在 loop 的情況中不斷檢查輸入狀態
導致當需檢查大量按鈕時,變得很慢
因此在下將所有按鈕的使用中斷檢查,運作速度立即變得正常
沒有留言 :
張貼留言