This is a reimplementation of the trackball firmware, which hopefully makes the mouse a bit more comfortable to use. It features:
- A simple interrupt counter coupled with a low-pass filter to smooth out the
(x,y)
position; - An exponential “scaling” function to make slow movements be more precise and keep fast movements of the cursor;
- Less code:
debouncer.ino
,glider.ino
andratemeter.ino
were removed, since they aren’t used anymore (because I couldn’t figure out how they work); - The mouse wheel was reimplemented and decoupled from the cursor.
It took me some time to figure out how to compile it, so I also added a small tutorial on how to build the firmware from source with Arduino CLI. Please check it out and build your own (I’ll provide some binaries after I’m fairly certain I won’t brick anyone’s keyboards).
This version of the firmware is forked from ClockworkPi’s repository, so it doesn’t have the features @yatli added, but it should be straightforward to merge the modifications.
About the trackball and the keyboard
DISCLAIMER: most of the next links are to PDF files.
The trackball is a Panasonic EVQWJN mounted with the 4 suggested AN48841B Hall Sensors and a switch (it may also feature a RGB LED, but does not). This seems to be a high quality trackball, and was even cited as an example in Apple’s Mighty Mouse patent, although it was discontinued by Panasonic in 2015.
The keyboard’s microcontroller is the STM32F103R8 (ARM Cortex M3 32-bit) or one of its variants (please visually check your version). This microcontroller specifically features an application note regarding Hall sensors (see AN4013, page 35).
The ideal implementation
The application note explains how to use a Hall-effect sensor to estimate the position of a rotating motor (which could be adapted to the trackball’s case) and the suggested implementation is via the builtin timers (TIMx
, in which x = 1,2,3,4
). There’s even a code example on the Arduino core the keyboard uses.
When I first found this, I was going to drop my implementation and start from scratch using the timers, but there’s a problem:
// trackball (in keys_io_map.h)
#define HO1 PC8
#define HO2 PC9
#define HO3 PC10
#define HO4 PC11
#define KEY0 PC12
The pins the trackball is connected to (see CON8B
in the keyboard and the trackball schematics) can’t be remapped to the hardware timers. At least not all of them:
-
PC8
(orLEFT_PIN
) isTIM1_CH1
-
PC9
(orUP_PIN
) isTIM3_CH4
-
PC10
(orRIGHT_PIN
) isUSART3_TX
-
PC11
(orDOWN_PIN
) isUSART3_RX
(orADC1_EXTI
/ADC2_EXTI
)
For an easier visualization, you can use the STM32CubeMX tool to check the pins.
The current implementation
The way the trackball encoder is implemented right now is via the external interrupt (EXTI
) feature of the STM32 (since all pins can be used for EXTI
):
- An interrupt is attached to each pin of the trackball (
HO1
,HO2
,HO3
andHO4
), which correspond to a specific direction:
// Run interrupt function when corresponding PIN changes its value
attachInterrupt(LEFT_PIN, &interrupt<PIN_LEFT>, ExtIntTriggerMode::CHANGE);
attachInterrupt(RIGHT_PIN, &interrupt<PIN_RIGHT>, ExtIntTriggerMode::CHANGE);
attachInterrupt(UP_PIN, &interrupt<PIN_UP>, ExtIntTriggerMode::CHANGE);
attachInterrupt(DOWN_PIN, &interrupt<PIN_DOWN>, ExtIntTriggerMode::CHANGE);
- When the ball rolls to a certain direction, it moves a corresponding wheel, which magnetically activates the Hall-effect sensor. This generates an alternating pulse (11 of these per full rotation) which triggers the
interrupt
function every time the pulse changes its value:
template <TrackballPin PIN>
static void interrupt()
{
// Count the number of times the trackball rolls towards a certain
// direction (when the corresponding PIN changes its value).
// This part of the code should be minimal, so that the next
// interrupts are not blocked from happening.
direction_counter[PIN] += 1;
}
- After some unspecified amount of time, interrupts are disabled temporarily, the movement vector (
x
,y
) is calculated and the counters are reset:
// Stop interrupts from happening. Don't forget to re-enable them!
noInterrupts();
// Calculate x and y positions
float x = direction_counter[PIN_RIGHT] - direction_counter[PIN_LEFT];
float y = direction_counter[PIN_DOWN] - direction_counter[PIN_UP];
// Clear counters
std::fill(std::begin(direction_counter), std::end(direction_counter), 0);
// Re-enable interrupts (Mouse.move needs interrupts)
interrupts();
- Both positions are scaled and filtered, then the mouse is moved
// Non-linear scaling
// SENSITIVITY * sign(x) * exp(abs(x) / sqrt(SENSITIVITY))
x = position_scale(x);
y = position_scale(y);
// Filter x and y
x = iir_filt(&iir_x, x);
y = iir_filt(&iir_y, y);
// Move mouse
while ((int)x != 0 || (int)y != 0)
{
// Only 8bit values are allowed,
// so clamp and execute move() multiple times
int8_t x_byte = clamp<int8_t>(x);
int8_t y_byte = clamp<int8_t>(y);
// Move mouse with values in the range [-128, 127]
dv->Mouse->move(x_byte, y_byte, 0);
// Decrement the original value, stop if done
x -= x_byte;
y -= y_byte;
}
This implementation is slower than the timer solution (but I can’t be sure how much slower) and causes the measurements of (x
,y
) to be imprecise, resulting in unusual movements. For example, when trying to scroll diagonally, the mouse will most often move like a staircase. For this reason, a low-pass filter was added as an attempt to smooth out the movement, even though it adds some delay.
Suggestion
For the next iteration of the keyboard’s PCB, please try to change the pins of the trackball so that the timers of the STM32 may be used.