Dalius's blog

Friday, September 1, 2023

Joystick algorithms for QMK

My current keyboard is 34 keys split Architeutis Dux, but I still need to move my right hand away from keyboard to mouse from time to time. I am aware about options that are possible (cirque trackpad, various trackballs and etc.), but I wanted to investigate what can be done with joystick (as it is quite cheap option).

I have used following reddit discussions as starting point:

Hardware

So I bought thumb joystick similar to this one (https://www.sparkfun.com/products/9032) for 1.2€.

My experience was quite similar to this one (source):

“The SparkFun Thumb joystick is probably not what you want. It’s stiff and is inclined to stay in one of the two axes (ie moving ‘diagonally’ is awkward). I had planned to use this in a build but it’s just too shit to use.”

It is really too stiff for daily use. In addition I have found that it reports no movement even if you push lever a little bit, and reports lever fully pushed even if it is pushed to the middle only.

Otherwise connecting it to Elite-C was quite easy (as this is what I have currently on my keyboard and there are 5 free unused pins at the bottom). So I have checked documentation which ADC pins I can use in QMK docs on ADC driver which said that I can use F0 and F1. So I have soldered wires to joystick contacts and wired (without soldering) wires to Elite-C. It looked like this:

top.jpg

Another reason why I will not use it is that it is too high (compared to my keyboard): top.jpg

Lastly you can push this joystick to produce mouse click but IMHO it is impractical as it is really hard to push it.

Algorithms

You can find my experiments in this git branch: https://github.com/daliusd/qmk_firmware/tree/analog-joystick-barrett

Each experiment has its own commit in case you want to try it out (read till the end why you shouldn’t do it).

IBM Patent

People on reddit in several discussions mentioned this patent: https://patents.google.com/patent/US5570111A

The main idea of this patent is that user tries to predict where pointer will end up and releases joystick accordingly. Algorithm itself is quite simple and have some smart bits:


// Nice square root approximation IMHO
int16_t z = ax + ay - ((2 * (ax < ay  ? ax : ay)) / 3);

if (z > 4) {
    // If user is releasing joystick then compensate and move cursor a little
    // bit back
    int16_t zi = (z - zPrev) * 6 + z;

    int16_t xCalc = zi == 0 ? 0 : x * z * maxCursorSpeed / zi / speedRegulator;
    int16_t yCalc = zi == 0 ? 0 : y * z * maxCursorSpeed / zi / speedRegulator;

    report.x   = xCalc;
    report.y   = yCalc;

} else {
    report.x = 0;
    report.y = 0;
}

// NOTE: I have not committed this line of code but it is necessary
zPrev = z;

GitHub code link

Result: it doesn’t work at all. When you start releasing cursor it jumps back more than you would like (e.g. 200 points). That might be related stiffness of joystick I have, but overall this algorithm was disappointment.

Linear

Current QMK (2023-09-01) implementation uses x^2 weighting for lever. That means that initially cursor is really slow when you push lever down but after pushing it halfway it starts speeding up. Therefore I have decided to look what happens if I would simply use linear approach:

int8_t xCalc = x * maxCursorSpeed / speedRegulator;
int8_t yCalc = y * maxCursorSpeed / speedRegulator;

GitHub code link

Result: There was not significant different between QMK and this algorithm. Both felt quite similar.

Accelerated

This was quite desperate attempt. At this point I have found out that my joystick reports it is fully pushed when it is only halfway pushed. Here I have decided to simply emulate QMK mouse keys.

Here is how it looks like for X axis:

int16_t x = axisCoordinate(ANALOG_JOYSTICK_X_AXIS_PIN, xOrigin);
if (abs(x) < 5) {
    xC = 0;
} else {
    float maxX = (float)x / 100 * abs(x) * maxCursorSpeed / speedRegulator;
    if (abs(xC) < abs(maxX)) {
        xC += maxX / stepsToMax;
    } else {
        xC = maxX;
    }
}

GitHub code link

Result: This has not produced anything interesting. Then I have tried to use lever position both for acceleration and maximum speed (as you see in the code). That has not produced anything different or better than previous attempts.

Two zones attempt

Now try to do following experiment: choose point in other side of the screen and try to move cursor exactly to it (you can use mouse, trackpad or trackball, up to you). What you should notice that you most probably will not hit the position in the first try and will correct position slowly. That gave me idea that I can make joystick to have two states: if you push it slightly it moves 5 times slower than pushed fully. I have used following formula to calculate weights for it:

top.jpg

You can use following Javascript (in Node or Browser console) to generate weights array:

JSON.stringify(
  Array.from(Array(101).keys()).map(
    x => Math.ceil((((x/100-0.4)**3+0.064)/0.282*100))
  )
)

GitHub code link

Result: This produced better result than x^2 for me. It is quite easier to move cursor slowly in this mode if you need to. I have even found out that it is OK to change ANALOG_JOYSTICK_SPEED_REGULATOR from default 20 to 10 in case you use big screen (I work on 13 inch screens).

Two zones (simplified)

Since my joystick is reporting 100 when pushed halfway why not report 1/5th speed always (except for 100)?

int16_t ax = abs(x);
int16_t ay = abs(y);

int16_t xC = (ax == 100 ? 100 : ax == 0 ? 0 : 30) * (x < 0 ? -1 : 1) *
             maxCursorSpeed / speedRegulator;
int16_t yC = (ay == 100 ? 100 : ay == 0 ? 0 : 30) * (y < 0 ? -1 : 1) *
             maxCursorSpeed / speedRegulator;

GitHub code link

Result: This does not feel smooth enough however. It looks like naturally you want to feel that cursor is speeding up or slowing down and not starts moving fast instantly.

Summary

As result I have created this PR so other could try out two zones or other options (maybe 3 zones): https://github.com/qmk/qmk_firmware/pull/21883

It would be interesting to hear if others would try it out and let me know how it feels. Meanwhile I think I will try to get PSP 1000 joystick or something similar (which is way smaller and hopefully not that stiff) and maybe create keyboard with it.