From 1a4af3adf90bd7c20e2faa4eed6715845e7f7fd6 Mon Sep 17 00:00:00 2001
From: ploopyco <54917504+ploopyco@users.noreply.github.com>
Date: Tue, 11 Nov 2025 17:02:52 -0500
Subject: [PATCH] Add PixArt PAW-3222 mouse sensor driver (#25763)
---
builddefs/common_features.mk | 4 +-
docs/features/pointing_device.md | 17 +++
drivers/sensors/paw3222.c | 174 ++++++++++++++++++++++
drivers/sensors/paw3222.h | 47 ++++++
quantum/pointing_device/pointing_device.h | 4 +
5 files changed, 245 insertions(+), 1 deletion(-)
create mode 100644 drivers/sensors/paw3222.c
create mode 100644 drivers/sensors/paw3222.h
diff --git a/builddefs/common_features.mk b/builddefs/common_features.mk
index 1da13997b5..cd4e67a4bd 100644
--- a/builddefs/common_features.mk
+++ b/builddefs/common_features.mk
@@ -125,7 +125,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 paw3222 pmw3320 pmw3360 pmw3389 pimoroni_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)
@@ -157,6 +157,8 @@ ifeq ($(strip $(POINTING_DEVICE_ENABLE)), yes)
SRC += drivers/sensors/cirque_pinnacle.c
SRC += drivers/sensors/cirque_pinnacle_gestures.c
SRC += $(QUANTUM_DIR)/pointing_device/pointing_device_gestures.c
+ else ifeq ($(strip $(POINTING_DEVICE_DRIVER)), paw3222)
+ SPI_DRIVER_REQUIRED = yes
else ifeq ($(strip $(POINTING_DEVICE_DRIVER)), pimoroni_trackball)
I2C_DRIVER_REQUIRED = yes
else ifneq ($(filter $(strip $(POINTING_DEVICE_DRIVER)),pmw3360 pmw3389),)
diff --git a/docs/features/pointing_device.md b/docs/features/pointing_device.md
index d6dcddcdf0..eedbf59b95 100644
--- a/docs/features/pointing_device.md
+++ b/docs/features/pointing_device.md
@@ -267,6 +267,23 @@ The paw 3204 sensor uses a serial type protocol for communication, and requires
The CPI range is 400-1600, with supported values of (400, 500, 600, 800, 1000, 1200 and 1600). Defaults to 1000 CPI.
+### PAW-3222 Sensor
+
+To use the PAW-3222 sensor, add this to your `rules.mk`:
+
+```make
+POINTING_DEVICE_DRIVER = paw3222
+```
+
+The following pins must be defined in `config.h`:
+
+| Setting (`config.h`) | Description | Default |
+| --------------------- | ------------------------------------------------------------------ | ---------------------------- |
+| `PAW3222_CS_PIN` | (Required) The pin connected to the chip select pin of the sensor. | `POINTING_DEVICE_CS_PIN` |
+| `PAW3222_SPI_DIVISOR` | (Required) The SPI clock divisor. This is dependent on your MCU. | _not defined_ |
+
+The CPI range is up to 4,000. Defaults to 1,000 CPI.
+
### Pimoroni Trackball
To use the Pimoroni Trackball module, add this to your `rules.mk`:
diff --git a/drivers/sensors/paw3222.c b/drivers/sensors/paw3222.c
new file mode 100644
index 0000000000..57f89a12d6
--- /dev/null
+++ b/drivers/sensors/paw3222.c
@@ -0,0 +1,174 @@
+/* Copyright 2024 Colin Lam (Ploopy Corporation)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "paw3222.h"
+#include "wait.h"
+#include "gpio.h"
+#include "spi_master.h"
+#include "pointing_device_internal.h"
+
+#define MSB1 0x80
+#define MSB0 0x7F
+
+const pointing_device_driver_t paw3222_pointing_device_driver = {
+ .init = paw3222_init,
+ .get_report = paw3222_get_report,
+ .set_cpi = paw3222_set_cpi,
+ .get_cpi = paw3222_get_cpi,
+};
+
+// Convert a 12-bit twos complement binary-represented number into a
+// signed 12-bit integer.
+static int16_t convert_twoscomp_12(uint16_t data) {
+ if ((data & 0x800) == 0x800)
+ return -2048 + (data & 0x7FF);
+ else
+ return data;
+}
+
+void paw3222_write(uint8_t reg_addr, uint8_t data) {
+ spi_start(PAW3222_CS_PIN, false, 3, PAW3222_SPI_DIVISOR);
+ wait_us(1); // Tncs_lead
+ spi_write(reg_addr | MSB1);
+ spi_write(data);
+ wait_us(1); // Tncs_lag
+ spi_stop();
+}
+
+uint8_t paw3222_read(uint8_t reg_addr) {
+ spi_start(PAW3222_CS_PIN, false, 3, PAW3222_SPI_DIVISOR);
+ wait_us(1); // Tncs_lead
+ spi_write(reg_addr & MSB0);
+ wait_us(10); // Tprep_rd
+ uint8_t data = spi_read();
+ wait_us(1); // Tncs_lag
+ spi_stop();
+
+ return data;
+}
+
+bool paw3222_init(void) {
+ gpio_set_pin_output(PAW3222_CS_PIN);
+
+ // CS must be kept low at power-up stage for at least 1ms
+ gpio_write_pin_low(PAW3222_CS_PIN);
+ wait_ms(10);
+ gpio_write_pin_high(PAW3222_CS_PIN);
+
+ spi_init();
+
+ // reboot
+ paw3222_write(0x06, 0x80);
+ wait_ms(50);
+
+ if (!paw3222_check_signature()) {
+ return false;
+ }
+
+ // initialize
+ paw3222_write(0x09, 0x5A);
+ paw3222_write(0x0D, 0x23);
+ paw3222_write(0x0E, 0x24);
+ paw3222_write(0x19, 0x1C);
+ paw3222_write(0x05, 0xA1);
+ paw3222_write(0x2B, 0x6D);
+ paw3222_write(0x30, 0x2E);
+ paw3222_write(0x5C, 0xDF);
+ paw3222_write(0x7F, 0x01);
+ paw3222_write(0x06, 0x14);
+ paw3222_write(0x31, 0x25);
+ paw3222_write(0x34, 0xC4);
+ paw3222_write(0x36, 0xCC);
+ paw3222_write(0x37, 0x42);
+ paw3222_write(0x38, 0x01);
+ paw3222_write(0x3A, 0x76);
+ paw3222_write(0x3B, 0x34);
+ paw3222_write(0x42, 0x39);
+ paw3222_write(0x43, 0xF2);
+ paw3222_write(0x44, 0x39);
+ paw3222_write(0x45, 0xF0);
+ paw3222_write(0x46, 0x12);
+ paw3222_write(0x47, 0x39);
+ paw3222_write(0x48, 0xE3);
+ paw3222_write(0x49, 0x48);
+ paw3222_write(0x4A, 0xD3);
+ paw3222_write(0x4B, 0x98);
+ paw3222_write(0x64, 0x46);
+ paw3222_write(0x71, 0x28);
+ paw3222_write(0x72, 0x28);
+ paw3222_write(0x7F, 0x00);
+ paw3222_write(0x09, 0x00);
+
+ // read a burst, then discard
+ paw3222_read_burst();
+
+ return true;
+}
+
+report_paw3222_t paw3222_read_burst(void) {
+ report_paw3222_t report = {0};
+
+ uint8_t motion = paw3222_read(0x02);
+ if ((motion & MSB1) == MSB1) {
+ // Motion detected
+ uint16_t dx = (uint16_t)paw3222_read(0x03);
+ uint16_t dy = (uint16_t)paw3222_read(0x04);
+ uint16_t dxy_hi = (uint16_t)paw3222_read(0x12);
+
+ dx = dx | ((dxy_hi & 0xF0) << 4);
+ dy = dy | ((dxy_hi & 0x0F) << 8);
+
+ report.dx = convert_twoscomp_12(dx);
+ report.dy = convert_twoscomp_12(dy);
+ }
+
+ return report;
+}
+
+void paw3222_set_cpi(uint16_t cpi) {
+ uint16_t cpival = (cpi) < (480) ? (480) : ((cpi) > (4020) ? (4020) : (cpi));
+ uint8_t cpival_x = (cpival + (30 / 2)) / 30;
+ uint8_t cpival_y = (cpival + (29 / 2)) / 29;
+
+ paw3222_write(0x09, 0x5A);
+ paw3222_write(0x0D, cpival_x);
+ paw3222_write(0x0E, cpival_y);
+ paw3222_write(0x09, 0x00);
+}
+
+uint16_t paw3222_get_cpi(void) {
+ uint8_t cpival = paw3222_read(0x0D);
+ return (uint16_t)(cpival * 30);
+}
+
+report_mouse_t paw3222_get_report(report_mouse_t mouse_report) {
+ report_paw3222_t data = paw3222_read_burst();
+
+ if (data.dx != 0 || data.dy != 0) {
+ pd_dprintf("Raw ] X: %d, Y: %d\n", data.dx, data.dy);
+ mouse_report.x = CONSTRAIN_HID_XY(data.dx);
+ mouse_report.y = CONSTRAIN_HID_XY(data.dy);
+ }
+
+ return mouse_report;
+}
+
+bool paw3222_check_signature(void) {
+ uint8_t checkval_1 = paw3222_read(0x00);
+ uint8_t checkval_2 = paw3222_read(0x01);
+
+ return (checkval_1 == 0x30 && checkval_2 == 0x02);
+}
diff --git a/drivers/sensors/paw3222.h b/drivers/sensors/paw3222.h
new file mode 100644
index 0000000000..8dbf6a38bb
--- /dev/null
+++ b/drivers/sensors/paw3222.h
@@ -0,0 +1,47 @@
+/* Copyright 2024 Colin Lam (Ploopy Corporation)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+#include "pointing_device.h"
+
+#ifndef PAW3222_CS_PIN
+# ifdef POINTING_DEVICE_CS_PIN
+# define PAW3222_CS_PIN POINTING_DEVICE_CS_PIN
+# else
+# error "No chip select pin defined -- missing POINTING_DEVICE_CS_PIN or PAW3222_CS_PIN define"
+# endif
+#endif
+
+#ifndef PAW3222_SPI_DIVISOR
+# error "No PAW3222 SPI divisor defined -- missing PAW3222_SPI_DIVISOR"
+#endif
+
+typedef struct {
+ int16_t dx;
+ int16_t dy;
+} report_paw3222_t;
+
+extern const pointing_device_driver_t paw3222_pointing_device_driver;
+
+bool paw3222_init(void);
+report_paw3222_t paw3222_read_burst(void);
+void paw3222_set_cpi(uint16_t cpi);
+uint16_t paw3222_get_cpi(void);
+report_mouse_t paw3222_get_report(report_mouse_t mouse_report);
+bool paw3222_check_signature(void);
diff --git a/quantum/pointing_device/pointing_device.h b/quantum/pointing_device/pointing_device.h
index 5dfb9ce196..bb0e43af3c 100644
--- a/quantum/pointing_device/pointing_device.h
+++ b/quantum/pointing_device/pointing_device.h
@@ -39,6 +39,10 @@ typedef struct {
#elif defined(POINTING_DEVICE_DRIVER_pmw3320)
# include "drivers/sensors/pmw3320.h"
# define POINTING_DEVICE_MOTION_PIN_ACTIVE_LOW
+#elif defined(POINTING_DEVICE_DRIVER_paw3222)
+# include "spi_master.h"
+# include "drivers/sensors/paw3222.h"
+# define POINTING_DEVICE_MOTION_PIN_ACTIVE_LOW
#elif defined(POINTING_DEVICE_DRIVER_adns9800)
# include "spi_master.h"
# include "drivers/sensors/adns9800.h"