2019/08/15

Logicoolゲーミングマウスのスクロールホイールの解析と実装

はじめに

Logicool(Logitech)のゲーミングマウスは大抵の場合光学式のスクロールホイールを有しています。一般的な物理ホイールや光学式は2本の信号線を使ってホイールの回転数や回転方向を読み取りますが、Logicoolのホイールエンコーダーは1本の信号線でマイコンとやりとりします。それについての解析や実装をした文献があまり無いので記事にしました。

この記事の対象者

本記事は詳細かつ汎用性の薄い内容です。以下のような人を対象としています。
・Logicoolのマウス筐体に自作の制御基板(マイコン)を組み込もうとしている人
本記事では、Logicoolのマウスが持つ光学式スクロールホイールをスポイルせずに活用するための、マイコンの自作ファームウェアへの実装の支援を目的としています。

なお、Logicoolのマウスに別のメーカの制御基板(MCU)を組み込もうとしている場合は、信号線の本数や制御方法が根本的に違うため、間に互いの信号をエミュレートして仲介する機能が追加で必要になると思われます。

特許について

Logitech(Logicool)は、光学式エンコーダーの特許を取得しています。

Transmission of differential optical detector signal over a single line
https://patents.google.com/patent/US6552716

1本の線だけでホイールがどちらに回ったかを読み取れるという特殊なホイールエンコーダーです。通常の方法では、2つの信号線を2つのフォトインタラプタなどに接続し、フォトインタラプタの出力タイミングの違いから回転したタイミングと回転方向を取得するのですが、Logicoolのものはエンコーダー内に回路があり、そちらで信号処理した上で1つの信号線で回転したタイミングと回転方向ををやりとりします。

1. 各部品の結線

G300rの例をとって解説します。Logicoolのホイールエンコーダーは赤外線LED(IRLED)とエンコーダー本体の2つの電子部品から構成されています。
G300rの基板上部

1.1 IR LED

左側の透明な部品は赤外線LEDです。光る側にドーム状のレンズがついています。
以下に各ピンの結線先を示します。

アノード

基板に+Aと書いてあります。
USBのVcc(5V)に直結されています。

カソード

マイコンと実測148Ωの抵抗を介して接続されています。ピン番号は割愛(目が限界感)
論理的な動作は後述します。



1.2 エンコーダー

右側の黒い部品はエンコーダーです。メジャーな部品に例えると、トランジスタに見た目は似ています。型番やデータシートは数時間探したのですが見つかりませんでした。多分Logicoolにしか卸していないと思います。C国の通販サイトでは少し探すとバラ売りされていますが、流通経路は不明です。
以下に各ピンの結線先を示します。

電源

USBのVcc(5V)に直結されています。

信号線

MCUに直結されています。MCUのピンは内蔵プルアップかもしれません。

GND

USBのGND(0V)に直結されています。


2. 各信号線の動作

2.1 IR LEDのPWM動作

IR LEDのカソード側はPWM動作しています。普段は電圧はVcc(=電位差がないのでLEDは光らない状態)で、約11.5%の間だけ、電圧が約1.25V下がり、LEDを光らせる動作を一秒間に5000回しています。つまり5KHz デューティー比88.5%のPWMです(5VとMCUのピン電圧の差を利用してLEDを駆動していうため、通常のLEDの制御とはデューティー比が逆転することに注意してください)。


2.2 エンコーダーの信号線の動作

エンコーダーの信号線も5KHzで1連の動作をしています。

何もないとき

何も無いときはマイコン側からの信号のみが発出されています。
下の図でいうと右側の部分です。
マイコン側から2usのパルスが発出され、8usほど待って再度2usのパルスが発出されます。
右側に注目(青が信号線、黄色がLED)

ホイールが下に動いた時

ホイールが下に動いた時は、1回目のマイコン側のパルスの直後に、エンコーダー側から出力があります。下図の左右で見比べればわかりますが、左の方はエンコーダー側がホイールが下に動いたことを検知したため、1回目のパルスの直後にエンコーダーからの出力があります。結果として、マイコンの2回目のパルスが長くなっているように見えます。
左側に注目(青が信号線、黄色がLED)


ホイールが上に動いた時

ホイールが下に動いた時は、2回目のマイコンのパルスの直後にエンコーダー側から出力があります。下図の中央がその状態を示しており、2回目のパルス後に一瞬出力が下がりますが、その後約188usに渡ってエンコーダーの信号の出力があることが分かります。
中央に注目(青が信号線、黄色がLED)


つまり、、、どういうことだってばよ?

MCU向けのプログラム的に書き下すとこのようになります。
①ピンを出力ピンにし、2usのパルスを出力
②ピンを入力ピンにし、8usの間にエンコーダからの出力を読んで、出力があればホイールは下に動いている
③ピンを出力ピンにし、2usのパルスを出力
④ピンを入力ピンにし、188usの間にエンコーダからの出力を読んで、出力があればホイールは上に動いてる

※ただし、④の間にピンを読み続けていたら、それ以外のこと(例えば、USBでの通信やマウスセンサからの信号の受信)が全くできないので、数us後の値を読めば良いと思います。

ホイールの1ノッチ分での信号の出力回数

ホイールの1ノッチ、つまり1回だけホイールが動いた場合に上の信号は6回~9回程度出力されます。この回数はある程度ばらつきがあるため、ソフトウェアで補正する必要があります。



3. 実装

実装例としてTeensy LC+ Arduino Studioでのコードの一部を載せます。

3.1 LEDのPWM制御

LEDが5KHz周期で点滅している部分は、PWM制御で行います。大抵の場合セットアップシーケンスでPWM出力を設定します。
analogWriteFrequency(17, 5000); //17ピンに5kHz周期でPWM出力する
analogWrite(17, 228); //17ピンに228のデューティー比でPWM出力する
90%のデューティー比としたいので、228÷255=0.89となる228を指定しています。

3.2 ホイールの読み出し

5KHzで一連の処理を読み出す必要があります。
TeensyのIntervalTimerを使用するよう、グローバルで宣言します。
IntervalTimer WheelTimer; //WheelTimerをIntervalTimerとして利用する

セットアップシーケンスでタイマーをスタートさせます。
WheelTimer.begin(readWheel, 200); //200us毎にreadWheel関数を呼び出す
毎200usで呼び出す関数は下記のような感じです。
void readWheel() {
  int scroll_val = 0;
  int a, b;
  pinMode (WHL, OUTPUT); //出力ピンにする
  digitalWrite(WHL, HIGH); //ピンをHIGHへ
  delayMicroseconds(1); //本当は2us
  digitalWrite(WHL, LOW); //ピンをLOWへ
  pinMode (WHL, INPUT); //入力ピンにする
  delayMicroseconds(2); //エンコーダーの出力の立ち上がりを待つ
  a = digitalRead(WHL); //エンコーダーの値を読む(HIGHならホイールダウン)
  delayMicroseconds(2); //ちょっと待つ
  pinMode (WHL, OUTPUT); //出力ピンにする
  digitalWrite(WHL, HIGH); //ピンをHIGHへ
  delayMicroseconds(1); //本当は2us
  digitalWrite(WHL, LOW); //ピンをLOWへ
  pinMode (WHL, INPUT); //入力ピンにする
  delayMicroseconds(2); //エンコーダーの出力の立ち上がりを待つ
  b = digitalRead(WHL);; //エンコーダーの値を読む(HIGHならホイールアップ)
  wheel_ticks += b - a; 
}
基本的には、ピンのI/Oを切り替えながら、適宜ディレイを入れてあるべき姿にします。
前半がホイールダウンを読む部分、後半がホイールアップを読む部分です。

随所のdelayMicroseconds()がオシロスコープの測定値より短いのは、マイコン側のオーバーヘッドも考慮して、実測値と一番近い長さになるように測定しながら調整したためです。これはマイコンによって調整の必要があるでしょう。

wheel_ticksはグローバル変数で、これをホイールの回転数として出力します。ただし、ホイールの1ノッチあたり6~9程度は変化するので、USBに出力する際は後処理が必要です。


謝辞

本記事の情報、実装についてはqsxcv氏の投稿及びソースコードを参考にさせていただきました。