こんにちは、あろっちです。
M5Stack スクロールユニットはマウスのスクロールホイール風ロータリーエンコーダ拡張ユニットです。

Arduinoライブラリが用意されており、I2C経由でロータリーエンコーダの状態を取得できるので、手軽に実装できて様々な活用方法がありそうなイケてるデバイスと感じました。
M5Stack ジョイスティック2ユニットはI2C経由でジョイスティックの状態が取得できるように作られており、こちらも使い勝手がよさそうです。

ただし、いずれの製品もライブラリがM5Stack製品 (ESP32) での使用が前提で作られていて他のプラットフォームではそのまま使えない点が惜しいところ。
ライブラリのソースコードを確認したところ、Wireの初期化部分を適切に変更することでRaspberry Pi Pico/RP2040ボード(Arduino-Pico)でも動作可能になることが分かりました。そこで、今回はその修正方法について紹介します。
また、スクロールユニット用のMicroPython向けライブラリを作成したので、こちらも紹介します。
ライブラリの修正内容
Arduinoライブラリ(M5UnitScroll、M5UnitJoystick2)はインストール済みの前提とします。
インストールしたArduinoライブラリの格納場所は以下の記事を参照してください。
M5Stack スクロールユニット Arduinoライブラリ (M5UnitScroll)
修正前
/*! @brief Initialize the Scroll. */
bool M5UnitScroll::begin(TwoWire *wire, uint8_t addr, uint8_t sda, uint8_t scl, uint32_t speed) {
_wire = wire;
_addr = addr;
_sda = sda;
_scl = scl;
_speed = speed;
_wire->begin(_sda, _scl);
_wire->setClock(_speed);
delay(10);
_wire->beginTransmission(_addr);
uint8_t error = _wire->endTransmission();
if (error == 0) {
return true;
} else {
return false;
}
}
_wire->begin(_sda, _scl);
の箇所を以下のように修正します。
/*! @brief Initialize the Scroll. */
bool M5UnitScroll::begin(TwoWire *wire, uint8_t addr, uint8_t sda, uint8_t scl, uint32_t speed) {
_wire = wire;
_addr = addr;
_sda = sda;
_scl = scl;
_speed = speed;
#if defined(ARDUINO_ARCH_RP2040) && !defined(ARDUINO_ARCH_MBED)
_wire->setSDA(_sda);
_wire->setSCL(_scl);
_wire->begin();
#else
_wire->begin(_sda, _scl);
#endif
_wire->setClock(_speed);
delay(10);
_wire->beginTransmission(_addr);
uint8_t error = _wire->endTransmission();
if (error == 0) {
return true;
} else {
return false;
}
}
M5Stack ジョイスティック2ユニット Arduinoライブラリ (M5UnitJoystick2)
修正前
bool M5UnitJoystick2::begin(TwoWire *wire, uint8_t addr, uint8_t sda, uint8_t scl, uint32_t speed)
{
_wire = wire;
_addr = addr;
_sda = sda;
_scl = scl;
_speed = speed;
_wire->begin(_sda, _scl);
_wire->setClock(speed);
delay(10);
_wire->beginTransmission(_addr);
uint8_t error = _wire->endTransmission();
if (error == 0) {
return true;
} else {
return false;
}
}
_wire->begin(_sda, _scl);
の箇所を以下のように修正します。
bool M5UnitJoystick2::begin(TwoWire *wire, uint8_t addr, uint8_t sda, uint8_t scl, uint32_t speed)
{
_wire = wire;
_addr = addr;
_sda = sda;
_scl = scl;
_speed = speed;
#if defined(ARDUINO_ARCH_RP2040) && !defined(ARDUINO_ARCH_MBED)
_wire->setSDA(_sda);
_wire->setSCL(_scl);
_wire->begin();
#else
_wire->begin(_sda, _scl);
#endif
_wire->setClock(speed);
delay(10);
_wire->beginTransmission(_addr);
uint8_t error = _wire->endTransmission();
if (error == 0) {
return true;
} else {
return false;
}
}
いずれのライブラリも#ifなどのプリプロセッサを使用することで既存コードへの影響をなくし、Raspberry Pi Pico/RP2040ボード(Arduino-Pico)に対応させています。
Raspberry Pi Pico/RP2040ボード(Arduino-Pico)のプリプロセッサを使った判定については以下の記事を参照してください。
MicroPython向けライブラリ
M5Stack スクロールユニット
ライブラリのコードです。マイコンに保存します。
from machine import I2C, Pin
import time
# 定数の定義
SCROLL_ADDR = 0x40
ENCODER_REG = 0x10
BUTTON_REG = 0x20
RGB_LED_REG = 0x30
INC_ENCODER_REG = 0x50
BOOTLOADER_VERSION_REG = 0xFC
JUMP_TO_BOOTLOADER_REG = 0xFD
FIRMWARE_VERSION_REG = 0xFE
I2C_ADDRESS_REG = 0xFF
class M5UnitScroll:
def __init__(self, i2c, addr=SCROLL_ADDR):
self.i2c = i2c
self.addr = addr
self.initialized = False
time.sleep(0.01)
try:
self.i2c.writeto(self.addr, b"")
self.initialized = True
except OSError:
self.initialized = False
def _write_bytes(self, addr, reg, buffer):
data = bytearray([reg]) + buffer
self.i2c.writeto(addr, data)
def _read_bytes(self, addr, reg, length):
self.i2c.writeto(addr, bytearray([reg]), False)
return self.i2c.readfrom(addr, length)
def get_encoder_value(self):
data = self._read_bytes(self.addr, ENCODER_REG, 2)
value = int.from_bytes(data, "little")
if value >= 0x8000:
value -= 0x10000
return value
def get_inc_encoder_value(self):
data = self._read_bytes(self.addr, INC_ENCODER_REG, 2)
value = int.from_bytes(data, "little")
if value >= 0x8000:
value -= 0x10000
return value
def get_button_status(self):
data = self._read_bytes(self.addr, BUTTON_REG, 1)
return data[0] == 0x00
def set_led_color(self, color, index=0):
data = bytearray(
[index, (color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF]
)
self._write_bytes(self.addr, RGB_LED_REG, data)
def get_led_color(self):
data = self._read_bytes(self.addr, RGB_LED_REG, 4)
return (data[2] << 16) | (data[1] << 8) | data[0]
def set_encoder_value(self, encoder):
data = encoder.to_bytes(2, "little")
self._write_bytes(self.addr, ENCODER_REG, data)
def reset_encoder(self):
self._write_bytes(self.addr, 0x40, bytearray([1]))
def get_dev_status(self):
try:
self.i2c.writeto(self.addr, b"")
return True
except OSError:
return False
def get_bootloader_version(self):
self.i2c.writeto(self.addr, bytearray([BOOTLOADER_VERSION_REG]), False)
return self.i2c.readfrom(self.addr, 1)[0]
def get_firmware_version(self):
self.i2c.writeto(self.addr, bytearray([FIRMWARE_VERSION_REG]), False)
return self.i2c.readfrom(self.addr, 1)[0]
以下はテスト用のコードです。I2CのGPIOは実際に接続されているGPIOのピン番号に変更してください。デフォルトはSCL=1、SDA=0
from machine import I2C, Pin
import time
from M5UnitScroll import M5UnitScroll
def main():
# I2Cの初期化
i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000)
scroll = M5UnitScroll(i2c)
# デバイスの初期化確認
if not scroll.initialized:
print("Failed to initialize M5UnitScroll")
return
# LEDの色を緑に設定
scroll.set_led_color(0x00FF00)
# エンコーダの値をリセット
scroll.reset_encoder()
boot_version = scroll.get_bootloader_version()
firmware_version = scroll.get_firmware_version()
device_status = scroll.get_dev_status()
print(
"Bootloader Version:",
boot_version,
"Firmware Version:",
firmware_version,
"Device Status:",
device_status,
)
# エンコーダがクリックされるまで待機
while not scroll.get_button_status():
time.sleep_ms(100)
# エンコーダの値をリセット
scroll.set_encoder_value(0)
# エンコーダの値を20msごとに表示
while True:
encoder_value = scroll.get_encoder_value()
inc_encoder_value = scroll.get_inc_encoder_value()
btn_status = scroll.get_button_status()
print(
"Encoder Value:",
encoder_value,
"Inc Encoder Value:",
inc_encoder_value,
"Button Status:",
btn_status,
)
time.sleep_ms(20)
# プログラムのエントリーポイント
if __name__ == "__main__":
main()
M5Stack ジョイスティック2ユニット
ライブラリのコードです。マイコンに保存します。
from machine import I2C, Pin
import time
# 定数の定義
JOYSTICK2_ADDR = 0x63
JOYSTICK2_ADC_VALUE_12BITS_REG = 0x00
JOYSTICK2_ADC_VALUE_8BITS_REG = 0x10
JOYSTICK2_BUTTON_REG = 0x20
JOYSTICK2_RGB_REG = 0x30
JOYSTICK2_ADC_VALUE_CAL_REG = 0x40
JOYSTICK2_OFFSET_ADC_VALUE_12BITS_REG = 0x50
JOYSTICK2_OFFSET_ADC_VALUE_8BITS_REG = 0x60
JOYSTICK2_FIRMWARE_VERSION_REG = 0xFE
JOYSTICK2_BOOTLOADER_VERSION_REG = 0xFC
ADC_8BIT_RESULT = 0
ADC_16BIT_RESULT = 1
class M5UnitJoystick2:
def __init__(self, i2c, addr=JOYSTICK2_ADDR):
self.i2c = i2c
self.addr = addr
self.initialized = False
time.sleep(0.01)
try:
self.i2c.writeto(self.addr, b"")
self.initialized = True
except OSError:
self.initialized = False
def _write_bytes(self, addr, reg, buffer):
data = bytearray([reg]) + buffer
self.i2c.writeto(addr, data)
def _read_bytes(self, addr, reg, length):
self.i2c.writeto(addr, bytearray([reg]), False)
return self.i2c.readfrom(addr, length)
def get_joy_adc_value_x(self, adc_bits):
if adc_bits == ADC_16BIT_RESULT:
reg = JOYSTICK2_ADC_VALUE_12BITS_REG
data = self._read_bytes(self.addr, reg, 2)
value = int.from_bytes(data, "little")
elif adc_bits == ADC_8BIT_RESULT:
reg = JOYSTICK2_ADC_VALUE_8BITS_REG
data = self._read_bytes(self.addr, reg, 1)
value = data[0]
return value
def get_joy_adc_value_y(self, adc_bits):
if adc_bits == ADC_16BIT_RESULT:
reg = JOYSTICK2_ADC_VALUE_12BITS_REG + 2
data = self._read_bytes(self.addr, reg, 2)
value = int.from_bytes(data, "little")
elif adc_bits == ADC_8BIT_RESULT:
reg = JOYSTICK2_ADC_VALUE_8BITS_REG + 1
data = self._read_bytes(self.addr, reg, 1)
value = data[0]
return value
def get_button_value(self):
reg = JOYSTICK2_BUTTON_REG
data = self._read_bytes(self.addr, reg, 1)
return data[0]
def set_rgb_color(self, color):
self._write_bytes(self.addr, JOYSTICK2_RGB_REG, color.to_bytes(4, "little"))
def get_rgb_color(self):
data = self._read_bytes(self.addr, JOYSTICK2_RGB_REG, 4)
return int.from_bytes(data, "little")
def get_bootloader_version(self):
reg = JOYSTICK2_BOOTLOADER_VERSION_REG
data = self._read_bytes(self.addr, reg, 1)
return data[0]
def get_firmware_version(self):
reg = JOYSTICK2_FIRMWARE_VERSION_REG
data = self._read_bytes(self.addr, reg, 1)
return data[0]
def get_joy_adc_16bits_value_xy(self, adc_x, adc_y):
reg = JOYSTICK2_ADC_VALUE_12BITS_REG
data = self._read_bytes(self.addr, reg, 4)
adc_x[0] = int.from_bytes(data[0:2], "little")
adc_y[0] = int.from_bytes(data[2:4], "little")
def get_joy_adc_8bits_value_xy(self, adc_x, adc_y):
reg = JOYSTICK2_ADC_VALUE_8BITS_REG
data = self._read_bytes(self.addr, reg, 2)
adc_x[0] = data[0]
adc_y[0] = data[1]
def get_joy_adc_12bits_offset_value_x(self):
reg = JOYSTICK2_OFFSET_ADC_VALUE_12BITS_REG
data = self._read_bytes(self.addr, reg, 2)
value = int.from_bytes(data, "little")
if value >= 0x8000:
value -= 0x10000
return value
def get_joy_adc_12bits_offset_value_y(self):
reg = JOYSTICK2_OFFSET_ADC_VALUE_12BITS_REG + 2
data = self._read_bytes(self.addr, reg, 2)
value = int.from_bytes(data, "little")
if value >= 0x8000:
value -= 0x10000
return value
def get_joy_adc_8bits_offset_value_x(self):
reg = JOYSTICK2_OFFSET_ADC_VALUE_8BITS_REG
data = self._read_bytes(self.addr, reg, 1)
value = data[0]
if value >= 0x80:
value -= 0x100
return value
def get_joy_adc_8bits_offset_value_y(self):
reg = JOYSTICK2_OFFSET_ADC_VALUE_8BITS_REG + 1
data = self._read_bytes(self.addr, reg, 1)
value = data[0]
if value >= 0x80:
value -= 0x100
return value
def set_joy_adc_value_cal(
self,
x_neg_min,
x_neg_max,
x_pos_min,
x_pos_max,
y_neg_min,
y_neg_max,
y_pos_min,
y_pos_max,
):
data = bytearray(16)
data[0:2] = x_neg_min.to_bytes(2, "little")
data[2:4] = x_neg_max.to_bytes(2, "little")
data[4:6] = x_pos_min.to_bytes(2, "little")
data[6:8] = x_pos_max.to_bytes(2, "little")
data[8:10] = y_neg_min.to_bytes(2, "little")
data[10:12] = y_neg_max.to_bytes(2, "little")
data[12:14] = y_pos_min.to_bytes(2, "little")
data[14:16] = y_pos_max.to_bytes(2, "little")
self._write_bytes(self.addr, JOYSTICK2_ADC_VALUE_CAL_REG, data)
def get_joy_adc_value_cal(
self,
x_neg_min,
x_neg_max,
x_pos_min,
x_pos_max,
y_neg_min,
y_neg_max,
y_pos_min,
y_pos_max,
):
data = self._read_bytes(self.addr, JOYSTICK2_ADC_VALUE_CAL_REG, 16)
x_neg_min[0] = int.from_bytes(data[0:2], "little")
x_neg_max[0] = int.from_bytes(data[2:4], "little")
x_pos_min[0] = int.from_bytes(data[4:6], "little")
x_pos_max[0] = int.from_bytes(data[6:8], "little")
y_neg_min[0] = int.from_bytes(data[8:10], "little")
y_neg_max[0] = int.from_bytes(data[10:12], "little")
y_pos_min[0] = int.from_bytes(data[12:14], "little")
y_pos_max[0] = int.from_bytes(data[14:16], "little")
メインコードからは次のように初期化します。
from machine import I2C, Pin
from M5UnitJoystick2 import M5UnitJoystick2
i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000)
joystick = M5UnitJoystick2(i2c)
if not joystick.initialized:
print("Failed to initialize M5UnitJoystick2")
関連記事
当ブログのマイコン記事です。ぜひご覧ください。
コメント