feat: adds navigator trackball driver

This commit is contained in:
Florian Didron
2025-08-25 07:43:31 +07:00
parent 838a8d411c
commit 3bfe7c9c2d
12 changed files with 427 additions and 2 deletions

View File

@@ -123,7 +123,7 @@ ifeq ($(strip $(MOUSEKEY_ENABLE)), yes)
MOUSE_ENABLE := yes
endif
VALID_POINTING_DEVICE_DRIVER_TYPES := adns5050 adns9800 analog_joystick azoteq_iqs5xx cirque_pinnacle_i2c cirque_pinnacle_spi paw3204 pmw3320 pmw3360 pmw3389 pimoroni_trackball custom
VALID_POINTING_DEVICE_DRIVER_TYPES := adns5050 adns9800 analog_joystick azoteq_iqs5xx cirque_pinnacle_i2c cirque_pinnacle_spi paw3204 pmw3320 pmw3360 pmw3389 pimoroni_trackball navigator_trackball custom
ifeq ($(strip $(POINTING_DEVICE_ENABLE)), yes)
ifeq ($(filter $(POINTING_DEVICE_DRIVER),$(VALID_POINTING_DEVICE_DRIVER_TYPES)),)
$(call CATASTROPHIC_ERROR,Invalid POINTING_DEVICE_DRIVER,POINTING_DEVICE_DRIVER="$(POINTING_DEVICE_DRIVER)" is not a valid pointing device type)
@@ -160,6 +160,9 @@ ifeq ($(strip $(POINTING_DEVICE_ENABLE)), yes)
else ifneq ($(filter $(strip $(POINTING_DEVICE_DRIVER)),pmw3360 pmw3389),)
SPI_DRIVER_REQUIRED = yes
SRC += drivers/sensors/pmw33xx_common.c
else ifeq ($(strip $(POINTING_DEVICE_DRIVER)), navigator_trackball)
I2C_DRIVER_REQUIRED = yes
SRC += drivers/sensors/navigator.c
endif
endif
endif

View File

@@ -0,0 +1,44 @@
// Copyright 2025 ZSA Technology Labs, Inc <contact@zsa.io>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "quantum.h"
#include "navigator.h"
float scroll_accumulated_h = 0;
float scroll_accumulated_v = 0;
bool set_scrolling = false;
bool navigator_turbo = false;
bool navigator_aim = false;
report_mouse_t pointing_device_task_kb(report_mouse_t mouse_report) {
// Turbo mode is used to increase the speed of the mouse cursor
// by multiplying the x and y values by a factor.
if (navigator_turbo) {
mouse_report.x *= NAVIGATOR_TURBO_MULTIPLIER;
mouse_report.y *= NAVIGATOR_TURBO_MULTIPLIER;
}
// Aim mode is used to slow down the mouse cursor
// by dividing the x and y values by a factor.
if (navigator_aim) {
mouse_report.x /= NAVIGATOR_AIM_DIVIDER;
mouse_report.y /= NAVIGATOR_AIM_DIVIDER;
}
if (set_scrolling) {
scroll_accumulated_h += (float)mouse_report.x / NAVIGATOR_SCROLL_DIVIDER;
scroll_accumulated_v += (float)mouse_report.y / NAVIGATOR_SCROLL_DIVIDER;
mouse_report.h = (int8_t)scroll_accumulated_h;
#ifdef NAVIGATOR_SCROLL_INVERT
mouse_report.v = (int8_t)-scroll_accumulated_v;
#else
mouse_report.v = (int8_t)scroll_accumulated_v;
#endif
scroll_accumulated_h -= (int8_t)scroll_accumulated_h;
scroll_accumulated_v -= (int8_t)scroll_accumulated_v;
mouse_report.x = 0;
mouse_report.y = 0;
}
return mouse_report;
}

View File

@@ -0,0 +1,17 @@
// Copyright 2025 ZSA Technology Labs, Inc <contact@zsa.io>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#ifndef NAVIGATOR_SCROLL_DIVIDER
#define NAVIGATOR_SCROLL_DIVIDER 10
#endif
#ifdef POINTING_DEVICE_DRIVER_navigator_trackball
#define NAVIGATOR_TURBO_MULTIPLIER 3
#define NAVIGATOR_AIM_DIVIDER 3
#endif
#ifdef POINTING_DEVICE_DRIVER_navigator_trackpad
#define NAVIGATOR_TURBO_MULTIPLIER 2
#define NAVIGATOR_AIM_DIVIDER 2
#endif

View File

@@ -0,0 +1,281 @@
// Copyright 2025 ZSA Technology Labs, Inc <contact@zsa.io>
// SPDX-License-Identifier: GPL-2.0-or-later
// This is the QMK driver for the Navigator Trackball. It is comprised of two ICs:
// 1. The sci18is606 is a i2c to spi bridge that converts the i2c protocol to the spi protocol. It allows the trackball to
// be plugged using the TRRS jack used by ZSA keyboards or any other split keyboard.
// 2. The paw3805ek is a high-speed motion detection sensor. It is used to detect the motion of the trackball.
#include "i2c_master.h"
#include "navigator_trackball.h"
#include <stdint.h>
#include <stdio.h>
#include "quantum.h"
const pointing_device_driver_t navigator_trackball_pointing_device_driver = {
.init = navigator_trackball_device_init,
.get_report = navigator_trackball_get_report,
.get_cpi = navigator_trackball_get_cpi,
.set_cpi = navigator_trackball_set_cpi
};
uint8_t current_cpi = DEFAULT_CPI_TICK;
uint8_t has_motion = 0;
uint8_t trackball_init = 0;
deferred_token callback_token = 0;
// The sequence of commands to configure and boot the paw3805ek sensor.
paw3805ek_reg_seq_t paw3805ek_configure_seq[] = {
{0x06, 0x80}, // Software reset
{0x00, 0x00}, // Request the sensor ID
{0x09 | WRITE_REG_BIT, 0x5A}, // Disable the write protection
#ifdef MOUSE_EXTENDED_REPORT
{0x19 | WRITE_REG_BIT, 0x30}, // Set the sensor orientation, set motion data length to 16 bits
#else
{0x19 | WRITE_REG_BIT, 0x34}, // Set the sensor orientation, set motion data length to 8 bits
#endif
//{0x26 | WRITE_REG_BIT, 0x10}, // Enable burst mode
{0x09 | WRITE_REG_BIT, 0x00}, // Enable the write protection
};
// A wrapper function for i2c_transmit that adds the address of the bridge chip to the data.
i2c_status_t sci18is606_write(uint8_t *data, uint8_t length) {
return i2c_transmit(NAVIGATOR_TRACKBALL_ADDRESS, data, length, NAVIGATOR_TRACKBALL_TIMEOUT);
}
// A wrapper function for i2c_receive that adds the address of the bridge chip to the data.
i2c_status_t sci18is606_read(uint8_t *data, uint8_t length) {
return i2c_receive(NAVIGATOR_TRACKBALL_ADDRESS, data, length, NAVIGATOR_TRACKBALL_TIMEOUT);
}
// A wrapper function that allows to write and optionally read from the bridge chip.
i2c_status_t sci18is606_spi_tx(uint8_t *data, uint8_t length, bool read) {
i2c_status_t status = sci18is606_write(data, length);
wait_us(length * 15);
// Read the SPI response if the command expects it
if (read) {
status = sci18is606_read(data, length);
}
if (status != I2C_STATUS_SUCCESS) {
trackball_init = 0;
}
return status;
}
// Configure the bridge chip to enable SPI mode.
i2c_status_t sci18is606_configure(void) {
uint8_t spi_conf[2] = {SCI18IS606_CONF_SPI, SCI18IS606_CONF};
i2c_status_t status = sci18is606_write(spi_conf, 2);
wait_ms(10);
if (status != I2C_STATUS_SUCCESS) {
trackball_init = 0;
}
return status;
}
bool paw3805ek_set_cpi(void) {
uint8_t next_cpi_x = 0;
uint8_t next_cpi_y = 0;
// traverse the sequence by compairing the cpi_x value with the current cpi_x value
// set the cpi to the next value in the sequence
switch (current_cpi) {
case 1: {
next_cpi_x = CPI_X_800;
next_cpi_y = CPI_Y_800;
break;
}
case 2: {
next_cpi_x = CPI_X_1000;
next_cpi_y = CPI_Y_1000;
break;
}
case 3: {
next_cpi_x = CPI_X_1200;
next_cpi_y = CPI_Y_1200;
break;
}
case 4: {
next_cpi_x = CPI_X_1600;
next_cpi_y = CPI_Y_1600;
break;
}
case 5: {
next_cpi_x = CPI_X_2000;
next_cpi_y = CPI_Y_2000;
break;
}
case 6: {
next_cpi_x = CPI_X_2400;
next_cpi_y = CPI_Y_2400;
break;
}
case 7: {
next_cpi_x = CPI_X_3000;
next_cpi_y = CPI_Y_3000;
break;
}
default: {
current_cpi = DEFAULT_CPI_TICK;
next_cpi_x = CPI_X_800;
next_cpi_y = CPI_Y_800;
break;
}
}
paw3805ek_reg_seq_t cpi_reg_seq[] = {
{0x09 | WRITE_REG_BIT, 0x5A}, // Disable write protection
{0x0D | WRITE_REG_BIT, next_cpi_x},
{0x0E | WRITE_REG_BIT, next_cpi_y},
{0x09 | WRITE_REG_BIT, 0x00}, // Enable the write protection
};
// Run the spi sequence to configure the cpi.
for (uint8_t i = 0; i < sizeof(cpi_reg_seq) / sizeof(paw3805ek_reg_seq_t); i++) {
uint8_t buf[3];
buf[0] = NCS_PIN;
buf[1] = cpi_reg_seq[i].reg;
buf[2] = cpi_reg_seq[i].data;
if (sci18is606_spi_tx(buf, 3, true) != I2C_STATUS_SUCCESS) {
return false;
}
}
return true;
}
// Run the paw3805ek configuration sequence.
bool paw3805ek_configure(void) {
for (uint8_t i = 0; i < sizeof(paw3805ek_configure_seq) / sizeof(paw3805ek_reg_seq_t); i++) {
uint8_t buf[3];
buf[0] = NCS_PIN;
buf[1] = paw3805ek_configure_seq[i].reg;
buf[2] = paw3805ek_configure_seq[i].data;
if (sci18is606_spi_tx(buf, 3, true) != I2C_STATUS_SUCCESS) {
return false;
}
// Wait for the sensor to restart after the software reset cmd
wait_ms(1);
// Check the sensor ID to validate the spi link after the reset
if (i == 1 && buf[1] != PAW3805EK_ID) {
return false;
}
}
return true;
}
// Assert the CS pin to read the motion register.
bool paw3805ek_has_motion(void) {
uint8_t motion[3] = {0x01, 0x02, 0x00};
if (sci18is606_spi_tx(motion, 3, true) != I2C_STATUS_SUCCESS) {
return false;
}
return motion[1] & 0x80;
}
// Read the motion data from the paw3805ek sensor.
void paw3804ek_read_motion(report_mouse_t *mouse_report) {
#ifdef MOUSE_EXTENDED_REPORT
uint8_t delta_x_l[2] = {0x01, 0x03};
if (sci18is606_spi_tx(delta_x_l, 3, true) != I2C_STATUS_SUCCESS) {
return;
}
uint8_t delta_y_l[2] = {0x01, 0x04};
if (sci18is606_spi_tx(delta_y_l, 3, true) != I2C_STATUS_SUCCESS) {
return;
}
uint8_t delta_x_h[2] = {0x01, 0x11};
if (sci18is606_spi_tx(delta_x_h, 3, true) != I2C_STATUS_SUCCESS) {
return;
}
uint8_t delta_y_h[2] = {0x01, 0x12};
if (sci18is606_spi_tx(delta_y_h, 3, true) != I2C_STATUS_SUCCESS) {
return;
}
mouse_report->x = (int16_t)((delta_x_h[1] << 8) | delta_x_l[1]);
mouse_report->y = (int16_t)((delta_y_h[1] << 8) | delta_y_l[1]);
#else
uint8_t delta_x[2] = {0x01, 0x03};
if (sci18is606_spi_tx(delta_x, 3, true) != I2C_STATUS_SUCCESS) {
return;
}
uint8_t delta_y[2] = {0x01, 0x04};
if (sci18is606_spi_tx(delta_y, 3, true) != I2C_STATUS_SUCCESS) {
return;
}
mouse_report->x = delta_x[1];
mouse_report->y = delta_y[1];
#endif
}
// Deffered execution callback that periodically checks for motion.
uint32_t sci18is606_read_callback(uint32_t trigger_time, void *cb_arg) {
if (!trackball_init) {
navigator_trackball_device_init();
return NAVIGATOR_TRACKBALL_PROBE;
}
if (paw3805ek_has_motion()) {
has_motion = 1;
}
return NAVIGATOR_TRACKBALL_READ;
}
void navigator_trackball_device_init(void) {
i2c_init();
if (sci18is606_configure() == I2C_STATUS_SUCCESS) {
paw3805ek_configure();
} else {
return;
}
trackball_init = 1;
if (!callback_token) {
// Register the callback to read the trackball motion
callback_token = defer_exec(NAVIGATOR_TRACKBALL_READ, sci18is606_read_callback, NULL);
}
}
report_mouse_t navigator_trackball_get_report(report_mouse_t mouse_report) {
if (!trackball_init) {
return mouse_report;
}
if (has_motion) {
has_motion = 0;
paw3804ek_read_motion(&mouse_report);
}
return mouse_report;
}
uint16_t navigator_trackball_get_cpi(void) {
return current_cpi;
}
void restore_cpi(uint8_t cpi) {
current_cpi = cpi;
paw3805ek_set_cpi();
}
void navigator_trackball_set_cpi(uint16_t cpi) {
if (cpi == 0) { // Decrease one tick
if (current_cpi > 1) {
current_cpi--;
paw3805ek_set_cpi();
}
} else {
if (current_cpi < CPI_TICKS) {
current_cpi++;
paw3805ek_set_cpi();
}
}
};

View File

@@ -0,0 +1,70 @@
// Copyright 2025 ZSA Technology Labs, Inc <contact@zsa.io>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include "pointing_device.h"
#ifndef NAVIGATOR_TRACKBALL_ADDRESS
# define NAVIGATOR_TRACKBALL_ADDRESS 0x50
#endif
#ifndef NAVIGATOR_TRACKBALL_TIMEOUT
# define NAVIGATOR_TRACKBALL_TIMEOUT 100
#endif
#define NAVIGATOR_TRACKBALL_READ 7
#define NAVIGATOR_TRACKBALL_PROBE 1000
#define NCS_PIN 0x01
#define PAW3805EK_ID 0x31
#define SCI18IS606_CONF 0xDC //00001110b // MSB first, Mode 3, 155kHz
#define SCI18IS606_RW_SPI 0x00
#define SCI18IS606_CONF_SPI 0xF0
#define SCI18IS606_CLR_INT 0xF1
#define SCI18IS606_GET_ID 0xFE
#define WRITE_REG_BIT 0x80
/*
The PAW3805EK datasheet suggests the following CPI values for the X and Y axes:
CPI X-axis Y-axis
800 0x1F 0x22
1000 0x26 0x2A
1200 0x2E 0x32
1600 0x3C 0x43
2000 0x4C 0x54
2400 0x5B 0x64
3000 0x70 0x7B
*/
#define CPI_TICKS 7
#define DEFAULT_CPI_TICK 1
#define CPI_X_800 0x1F
#define CPI_Y_800 0x22
#define CPI_X_1000 0x26
#define CPI_Y_1000 0x2A
#define CPI_X_1200 0x2E
#define CPI_Y_1200 0x32
#define CPI_X_1600 0x3C
#define CPI_Y_1600 0x43
#define CPI_X_2000 0x4C
#define CPI_Y_2000 0x54
#define CPI_X_2400 0x5B
#define CPI_Y_2400 0x64
#define CPI_X_3000 0x70
#define CPI_Y_3000 0x7B
typedef struct {
uint8_t reg;
uint8_t data;
} paw3805ek_reg_seq_t;
const pointing_device_driver_t navigator_trackball_pointing_device_driver;
void navigator_trackball_device_init(void);
report_mouse_t navigator_trackball_get_report(report_mouse_t mouse_report);
uint16_t navigator_trackball_get_cpi(void);
void navigator_trackball_set_cpi(uint16_t cpi);
void restore_cpi(uint8_t cpi);

View File

@@ -304,6 +304,7 @@ bool process_record_kb(uint16_t keycode, keyrecord_t *record) {
void eeconfig_init_kb(void) { // EEPROM is getting reset!
keyboard_config.raw = 0;
keyboard_config.led_level = 4;
keyboard_config.navigator_cpi = 3;
eeconfig_update_kb(keyboard_config.raw);
eeconfig_init_user();
}

View File

@@ -68,6 +68,7 @@ typedef union {
struct {
uint8_t led_level : 3;
bool disable_layer_led : 1;
uint8_t navigator_cpi : 3;
bool placeholder : 1;
};
} keyboard_config_t;

View File

@@ -466,6 +466,7 @@ void eeconfig_init_kb(void) { // EEPROM is getting reset!
keyboard_config.rgb_matrix_enable = true;
keyboard_config.led_level = true;
keyboard_config.led_level_res = 0b11;
keyboard_config.navigator_cpi = 3;
eeconfig_update_kb(keyboard_config.raw);
eeconfig_init_user();
}

View File

@@ -45,6 +45,8 @@ typedef union {
bool rgb_matrix_enable :1;
bool led_level :1;
uint8_t led_level_res :2; // DO NOT REMOVE
uint8_t navigator_cpi :3;
};
} keyboard_config_t;

View File

@@ -301,6 +301,7 @@ void eeconfig_init_kb(void) { // EEPROM is getting reset!
keyboard_config.raw = 0;
keyboard_config.led_level = true;
keyboard_config.led_level_res = 0b11;
keyboard_config.navigator_cpi = 3;
eeconfig_update_kb(keyboard_config.raw);
eeconfig_init_user();
}

View File

@@ -4,6 +4,7 @@
#pragma once
#include <stdint.h>
#include "quantum.h"
extern bool mcp23018_leds[];
@@ -19,7 +20,7 @@ typedef union {
uint32_t raw;
struct {
bool disable_layer_led : 1;
bool placeholder : 1;
uint8_t navigator_cpi : 3;
bool led_level : 1;
uint8_t led_level_res : 2; // DO NOT REMOVE
};

View File

@@ -74,6 +74,9 @@ typedef struct {
# include "spi_master.h"
# include "drivers/sensors/pmw33xx_common.h"
# define POINTING_DEVICE_MOTION_PIN_ACTIVE_LOW
elif defined(POINTING_DEVICE_DRIVER_navigator_trackball)
# include "i2c_master.h"
# include "drivers/sensors/navigator_trackball.h"
#else
void pointing_device_driver_init(void);
report_mouse_t pointing_device_driver_get_report(report_mouse_t mouse_report);