diff --git a/builddefs/common_features.mk b/builddefs/common_features.mk index 56eed80ae0..a8342f5ccc 100644 --- a/builddefs/common_features.mk +++ b/builddefs/common_features.mk @@ -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 diff --git a/drivers/sensors/navigator.c b/drivers/sensors/navigator.c new file mode 100644 index 0000000000..90ca16709a --- /dev/null +++ b/drivers/sensors/navigator.c @@ -0,0 +1,44 @@ +// Copyright 2025 ZSA Technology Labs, Inc +// 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; +} diff --git a/drivers/sensors/navigator.h b/drivers/sensors/navigator.h new file mode 100644 index 0000000000..73e809715b --- /dev/null +++ b/drivers/sensors/navigator.h @@ -0,0 +1,17 @@ +// Copyright 2025 ZSA Technology Labs, Inc +// 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 diff --git a/drivers/sensors/navigator_trackball.c b/drivers/sensors/navigator_trackball.c new file mode 100644 index 0000000000..b6114625e5 --- /dev/null +++ b/drivers/sensors/navigator_trackball.c @@ -0,0 +1,281 @@ +// Copyright 2025 ZSA Technology Labs, Inc +// 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 +#include +#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(); + } + } +}; diff --git a/drivers/sensors/navigator_trackball.h b/drivers/sensors/navigator_trackball.h new file mode 100644 index 0000000000..f364c583aa --- /dev/null +++ b/drivers/sensors/navigator_trackball.h @@ -0,0 +1,70 @@ +// Copyright 2025 ZSA Technology Labs, Inc +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once +#include +#include +#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); diff --git a/keyboards/zsa/ergodox_ez/ergodox_ez.c b/keyboards/zsa/ergodox_ez/ergodox_ez.c index dd71656af1..d6f19ffcd2 100644 --- a/keyboards/zsa/ergodox_ez/ergodox_ez.c +++ b/keyboards/zsa/ergodox_ez/ergodox_ez.c @@ -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(); } diff --git a/keyboards/zsa/ergodox_ez/ergodox_ez.h b/keyboards/zsa/ergodox_ez/ergodox_ez.h index 22e63d6dbe..cf29bdf646 100644 --- a/keyboards/zsa/ergodox_ez/ergodox_ez.h +++ b/keyboards/zsa/ergodox_ez/ergodox_ez.h @@ -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; diff --git a/keyboards/zsa/moonlander/moonlander.c b/keyboards/zsa/moonlander/moonlander.c index df800eb799..7d61a2face 100644 --- a/keyboards/zsa/moonlander/moonlander.c +++ b/keyboards/zsa/moonlander/moonlander.c @@ -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(); } diff --git a/keyboards/zsa/moonlander/moonlander.h b/keyboards/zsa/moonlander/moonlander.h index 74abc404a4..44e8bc83f7 100644 --- a/keyboards/zsa/moonlander/moonlander.h +++ b/keyboards/zsa/moonlander/moonlander.h @@ -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; diff --git a/keyboards/zsa/voyager/voyager.c b/keyboards/zsa/voyager/voyager.c index 5a1698ffec..f31738ea88 100644 --- a/keyboards/zsa/voyager/voyager.c +++ b/keyboards/zsa/voyager/voyager.c @@ -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(); } diff --git a/keyboards/zsa/voyager/voyager.h b/keyboards/zsa/voyager/voyager.h index 1d028dd3c9..05fc1f338c 100644 --- a/keyboards/zsa/voyager/voyager.h +++ b/keyboards/zsa/voyager/voyager.h @@ -4,6 +4,7 @@ #pragma once +#include #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 }; diff --git a/quantum/pointing_device/pointing_device.h b/quantum/pointing_device/pointing_device.h index 1559645619..9233db25e2 100644 --- a/quantum/pointing_device/pointing_device.h +++ b/quantum/pointing_device/pointing_device.h @@ -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);