ジョイスティックマウスのカーソルを滑らかに動かしたい

ジョイスティックマウス

滑らかにしたい。したいので調べたことを書いてく。

ジョイスティックの傾き

左から静止状態、X軸が0に変わるギリギリの位置、止まるまで目一杯動かした時の位置。意識的には右の位置で0に変わってるように思うけど、実際は真ん中の位置で0になる。22°ぐらいで0、でも39°ぐらいまでは動く。この差が理想と現実の差を生んでるように思う。ストッパーをつけるとか、値を伸張するとか?ストッパーは3Dプリンタとか使わないとだし、値は伸長したところで22°ー39°間は値が動かなくなるので意味無い気もする。

ジョイスティックの可変抵抗の特性

https://www.marutsu.co.jp/contents/shop/marutsu/mame/78.html

可変抵抗には特性があるそうで、ABCカーブの違いがあるみたい。じゃあこのスティックに使われている可変抵抗の特性はなんじゃらほいと思ったけど、調べるとしたら分解して抵抗を調べるか、角度ごとに値のサンプルを取ってカーブの近似を求めるとかしないといけないと思うのですが、正直できる気がしないし気が引けるのでやりたくない。ここはBカーブであるとの前提のもとに動いてはどうか。どうなんでしょうか。

C++のint-long型の端数処理

map()内部で除算があり、analogRead()はint型で、map()はlong型なのでどちらも整数型だから端数が出たときの丸め方で数値がずれるんじゃないかと思った。実験してみる。

void setup() {
  Serial.begin(9600);
  delay(1000);
  for (int i = 0 ; i <= 1023; i++) {
    Serial.print(map(i, 0, 1023, -3, 3));
    Serial.println();
  }
}

void loop() {
}

こんなコードを書きましてー、出てきた数字をエクセルさんで計算した結果と比較してみる。長いので変化した部分だけ。

-3≦out≦3の場合

raw map
(excel)
map
(arduino)
roundup
(excel)
rounddown
(excel)
0 -3 -3 -3 -3
1 -2.994134897 -3 -3 -2
2 -2.988269795 -3 -3 -2
169 -2.008797654 -3 -3 -2
170 -2.002932551 -3 -3 -2
171 -1.997067449 -2 -2 -1
172 -1.991202346 -2 -2 -1
339 -1.011730205 -2 -2 -1
340 -1.005865103 -2 -2 -1
341 -1 -1 -1 -1
342 -0.994134897 -1 -1 0
343 -0.988269795 -1 -1 0
510 -0.008797654 -1 -1 0
511 -0.002932551 -1 -1 0
512 0.002932551 0 1 0
513 0.008797654 0 1 0
680 0.988269795 0 1 0
681 0.994134897 0 1 0
682 1 1 1 1
683 1.005865103 1 2 1
684 1.011730205 1 2 1
851 1.991202346 1 2 1
852 1.997067449 1 2 1
853 2.002932551 2 3 2
854 2.008797654 2 3 2
1021 2.988269795 2 3 2
1022 2.994134897 2 3 2
1023 3 3 3 3

-12≦out≦12の場合

グラフの引用は上と同じとこだけ。

raw map
(excel)
map
(arduino)
roundup
(excel)
rounddown
(excel)
0 -12 -12 -12 -12
1 -11.97653959 -12 -12 -11
2 -11.95307918 -12 -12 -11
169 -8.035190616 -9 -9 -8
170 -8.011730205 -9 -9 -8
171 -7.988269795 -8 -8 -7
172 -7.964809384 -8 -8 -7
339 -4.046920821 -5 -5 -4
340 -4.023460411 -5 -5 -4
341 -4 -4 -4 -4
342 -3.976539589 -4 -4 -3
343 -3.953079179 -4 -4 -3
510 -0.035190616 -1 -1 0
511 -0.011730205 -1 -1 0
512 0.011730205 0 1 0
513 0.035190616 0 1 0
680 3.953079179 3 4 3
681 3.976539589 3 4 3
682 4 4 4 4
683 4.023460411 4 5 4
684 4.046920821 4 5 4
851 7.964809384 7 8 7
852 7.988269795 7 8 7
853 8.011730205 8 9 8
854 8.035190616 8 9 8
1021 11.95307918 11 12 11
1022 11.97653959 11 12 11
1023 12 12 12 12

うーん。エクセルも癖があって、はっきりとは言えないけど。負の数の場合、エクセルでいうROUNDUPで正の数だとROUNDDOWNと一致する。ただ、Arduinoの方がきれいに階段状になっていて、これはエクセルがアレなのかな?でもまぁ、510-513あたりを見るとほんの少しだけど、Arduinoのmap()は正側に値が振れているような気がする。感覚的には、ROUNDDOWNが近いかなぁと思った。map()のリファレンスには

The map() function uses integer math so will not generate fractions, when the math might indicate that it should do so. Fractional remainders are truncated, and are not rounded or averaged.

Notes & Warnings

As previously mentioned, the map() function uses integer math. So fractions might get suppressed due to this. For example, fractions like 3/2, 4/3, 5/4 will all be returned as 1 from the map() function, despite their different actual values. So if your project requires precise calculations (e.g. voltage accurate to 3 decimal places), please consider avoiding map() and implementing the calculations manually in your code yourself.

ていうことなので、自分でやれってことかぁ。うーんうーん。

直線のグラフを曲線に変換

ハードウェアの電圧からAnalogRead()で数値を取得するところの特性は分からないものの、取得した数値は0-1023で変化する直線のグラフになる。で、等速直線運動っていうのは人間の感覚と合ってないらしい。

https://www.autoexe.co.jp/kijima/column8.html

最初は徐々に速度が上がっていって、時間が経つと速度の上がり方が急になる、みたいな。そういうのが人間の感覚に合ってるみたい。だから、0-1023の値をそういう感じに変化させることができれば滑らか感を得られるのではと思った。で、そういう変換、直線のグラフにカーブをかけるような変換はどういうのがあるんだろうと調べてみた。イメージとしては

こういう感じ。考え方合ってんのか不安になってきた。どうなんだろ。

y=x^3

https://www.wolframalpha.com/input/?i=y%3Dx%5E3&lang=ja

x^3のグラフがそれっぽい。とりあえずエクセルで0-1023に合うように試してみる。

  1. 0-1023を、-1から1に変換する。
    • =((A2-0)/(1023-0))*(1-(-1))+(-1)
  2. 1の値を3乗する。
    • =POWER(B2,3)
  3. 2の値を0-1023に変換する。
    • =((C2-(-1))/(1-(-1)))*(1023-0)

するとこんな感じのグラフになった。

よさげな気がする。3のとこでMouse.moveで使う範囲に変換してもいいかな。

シグモイド関数

探してたらシグモイド関数というのを見つけた。

https://ja.wikipedia.org/wiki/%E3%82%B7%E3%82%B0%E3%83%A2%E3%82%A4%E3%83%89%E9%96%A2%E6%95%B0

ここの標準シグモイド関数っていうのがそれっぽい。けど0.5付近で急になるのじゃなくてそこは潰れてほしい。てことは2つ重ねりゃいいんじゃね?

CC BY-SA 3.0, Link

こんな感じに。やってみよー。

ゲインが1の場合(a=1)

グラフ見るとxが-6から6の区間で、yが0-1でいい感じに変化してるみたいなのでそんな感じにする。

  1. 0-511・512-1023の2区間に分けて、それぞれ(-6)-6に変換
    • =((A2-0)/(511-0))*(6-(-6))+(-6)
    • =((A514-512)/(1023-512))*(6-(-6))+(-6)
  2. 1-1をシグモイド関数で変換して、(-1)-0の区間にするため-1する。1-2は変換するだけ。
    • =((TANH(B2/2)+1)/2)-1
    • =(TANH(B515/2)+1)/2
  3. 目的の区間に変換。ここでは0-1。
    • =(C2-(-1))/(1-(-1))

でけた。

ゲインが5の場合(a=5)

ゲインを5にすると、xが(-1)-1の時に、yが0-1になるっぽく、きれいなのでこっちでもやってみる。上記の流れをちょっと変えるだけ。

  1. (-1)-1に変換
    • =((A2-0)/(511-0))*(1-(-1))+(-1)
    • =((A514-512)/(1023-512))*(1-(-1))+(-1)
  2. シグモイド関数で変換
    • =((TANH((B2*5)/2)+1)/2)-1
    • =(TANH((B514*5)/2)+1)/2
  3. 0-1の区間に変換
    • =(C514-(-1))/(1-(-1))

できた。

違い

gif動画

a=1とa=5の場合だと曲がりがちょっとだけ違う。どうがいいんだろな。

これさ

数式を1行にまとめると

=(((((TANH(((((A2-0)/(511-0))*(1-(-1))+(-1))*5)/2)+1)/2)-1)-(-1))/(1-(-1)))*(1-0)

こんな長くなる。どうやってコード書けばいいんだ。

コードにしてみる

と言っててもどうにもならんので、愚直にコードに起こしてみる。最終的にMouse.moveで使う値になるように。Arduinoのリファレンスに即して-12から12の範囲になるようにしたい。

y=x^3

double tmp = 0;
signed char result = 0;

double mapf(float in , float in_min, float in_max, float out_min, float out_max) {
  return ((in - in_min) / (in_max - in_min)) * (out_max - out_min) + out_min;
}

void setup() {
  Serial.begin(9600);
  delay(1000);

  for (int i = 0 ; i <= 1023; i++) {

    tmp = mapf(i, 0, 1023, -1, 1);
    tmp = pow(tmp, 3);
    tmp = mapf(tmp, -1, 1, -12, 12);
    result = (signed char) tmp;

    Serial.println(result);
  }
}

void loop() {
}

こんなコードを書きまして、結果が

おおう、だいぶ中央が潰れてしまった。これはどうじゃろ…、ちょっとtmp = pow(tmp, 3)のとこをtmp = pow(tmp, 3) + tmpにしてみる。

こんな感じかなぁ…。

シグモイド関数

#include <math.h>

double tmp = 0;
signed char result = 0;

double mapf(float in , float in_min, float in_max, float out_min, float out_max) {
  return ((in - in_min) / (in_max - in_min)) * (out_max - out_min) + out_min;
}

void setup() {
  Mouse.begin();
  Serial.begin(9600);
  delay(1000);

  for (int i = 0 ; i <= 1023; i++) {
    if ( i <= 511) {
      tmp = mapf(i, 0, 511, -6, 6);
      tmp = ((tanh(tmp / 2) + 1) / 2) - 1;
      
    } else if ( i >= 512) {
      tmp = mapf(i, 512, 1023, -6, 6);
      tmp = (tanh(tmp / 2) + 1) / 2;
      
    }

    tmp = mapf(tmp, -1, 1, -12, 12);
    result = (signed char) tmp;

    Serial.println(result);
  }
}

void loop() {
}

ハイパボリックタンジェントという舌噛みそうな関数を使うために#include <math.h>する。あとは条件分岐して書いてみた。結果は

うーん、これも潰れてる気がするし、変化する部分は直線的になってるような気がする。とりあえずゲイン5でもやってみた結果が

若干変わった…ゲイン10でもやってみよ。0-1023からの変換は(-0.4)-4にしてみた。

なんか何がいいのか分かんねーな…。実際使ってみて、触り心地を確かめてみないと机上の空論だなぁ。

速度を計ってみる

いっぱい計算してるので、速度がちょっと気になる。計算が遅かったらジョイスティックを動かした後に、遅れてカーソルが動くとかになりそうだし。ただ基準が分からんよな。0-1023を100回繰り返した。

  • y=x^3+xの場合
    • 結果 millis()では0。micros()で300µs
  • シグモイド関数の場合
    • 結果 28,813ms

全然ちゃうなぁ。y=x^3+xの方がいいかもなぁ…。

カテゴリー: したい タグ: , , パーマリンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です