MustafaMulla29/wireless-mouse-pcb

A tscircuit package created by MustafaMulla29.

Version
1.0.16
License
unset
Stars
0

firmware/src/pmw3610.c

/* SPDX-License-Identifier: Apache-2.0 */

#include "pmw3610.h"

#include <errno.h>
#include <stdbool.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/util.h>

#define PMW_PIN_RESET  10
#define PMW_PIN_SCLK   12
#define PMW_PIN_SDIO   14
#define PMW_PIN_NCS    16
#define PMW_PIN_MOTION 17

#define PMW_REG_PRODUCT_ID   0x00
#define PMW_REG_MOTION       0x02
#define PMW_REG_PERFORMANCE  0x11
#define PMW_REG_BURST_READ   0x12
#define PMW_REG_RUN_DOWNSHIFT 0x1B
#define PMW_REG_REST1_RATE   0x1C
#define PMW_REG_REST1_DOWNSHIFT 0x1D
#define PMW_REG_OBSERVATION1 0x2D
#define PMW_REG_SPI_CLOCK    0x41
#define PMW_REG_PAGE         0x7F
#define PMW_REG_RES_STEP     0x85

#define PMW_PRODUCT_ID 0x3E
#define PMW_SPI_CLOCK_ON  0xBA
#define PMW_SPI_CLOCK_OFF 0xB5

static const struct device *const gpio0 = DEVICE_DT_GET(DT_NODELABEL(gpio0));

static inline void spi_delay(void)
{
	/* 1 us comfortably exceeds every 120-250 ns bit-level minimum. */
	k_busy_wait(1);
}

static void sdio_output(void)
{
	(void)gpio_pin_configure(gpio0, PMW_PIN_SDIO, GPIO_OUTPUT_HIGH);
}

static void sdio_input(void)
{
	(void)gpio_pin_configure(gpio0, PMW_PIN_SDIO, GPIO_INPUT);
}

static void shift_out(uint8_t value)
{
	sdio_output();
	for (int bit = 7; bit >= 0; bit--) {
		(void)gpio_pin_set(gpio0, PMW_PIN_SDIO, (value >> bit) & 1U);
		spi_delay();
		(void)gpio_pin_set(gpio0, PMW_PIN_SCLK, 1);
		spi_delay();
		(void)gpio_pin_set(gpio0, PMW_PIN_SCLK, 0);
		spi_delay();
	}
}

/* The PMW3610 holds SCLK high for tSRAD after the final address bit, then
 * presents D7 on the following falling edge. This cannot use shift_out()
 * because that helper immediately emits the final falling edge.
 */
static void shift_out_read_address(uint8_t reg)
{
	sdio_output();
	for (int bit = 7; bit >= 1; bit--) {
		(void)gpio_pin_set(gpio0, PMW_PIN_SDIO, (reg >> bit) & 1U);
		spi_delay();
		(void)gpio_pin_set(gpio0, PMW_PIN_SCLK, 1);
		spi_delay();
		(void)gpio_pin_set(gpio0, PMW_PIN_SCLK, 0);
		spi_delay();
	}

	(void)gpio_pin_set(gpio0, PMW_PIN_SDIO, reg & 1U);
	spi_delay();
	(void)gpio_pin_set(gpio0, PMW_PIN_SCLK, 1);
	spi_delay();
	sdio_input();
	/* PMW3610 tSRAD is 4 us from the final address rising edge. */
	k_busy_wait(4);
	(void)gpio_pin_set(gpio0, PMW_PIN_SCLK, 0);
	spi_delay();
}

static uint8_t shift_in(bool more_bytes)
{
	uint8_t value = 0;

	for (int bit = 7; bit >= 0; bit--) {
		(void)gpio_pin_set(gpio0, PMW_PIN_SCLK, 1);
		spi_delay();
		value |= (uint8_t)(gpio_pin_get(gpio0, PMW_PIN_SDIO) > 0) << bit;
		if (bit > 0 || more_bytes) {
			(void)gpio_pin_set(gpio0, PMW_PIN_SCLK, 0);
			spi_delay();
		}
	}

	return value;
}

static void write_raw(uint8_t reg, uint8_t value)
{
	(void)gpio_pin_set(gpio0, PMW_PIN_NCS, 0);
	spi_delay();
	shift_out(reg | BIT(7));
	shift_out(value);
	/* PMW3610 tSCLK-NCS(write) is 10 us. */
	k_busy_wait(10);
	(void)gpio_pin_set(gpio0, PMW_PIN_NCS, 1);
	/* PMW3610 tSWW is 30 us. */
	k_busy_wait(30);
}

static uint8_t read_reg(uint8_t reg)
{
	uint8_t value;

	(void)gpio_pin_set(gpio0, PMW_PIN_NCS, 0);
	spi_delay();
	shift_out_read_address(reg & 0x7F);
	value = shift_in(false);
	spi_delay();
	(void)gpio_pin_set(gpio0, PMW_PIN_NCS, 1);
	(void)gpio_pin_set(gpio0, PMW_PIN_SCLK, 0);
	k_busy_wait(1);
	return value;
}

static void write_sequence(const uint8_t (*pairs)[2], size_t count)
{
	write_raw(PMW_REG_SPI_CLOCK, PMW_SPI_CLOCK_ON);
	k_busy_wait(300);
	for (size_t i = 0; i < count; i++) {
		write_raw(pairs[i][0], pairs[i][1]);
	}
	write_raw(PMW_REG_SPI_CLOCK, PMW_SPI_CLOCK_OFF);
}

static void read_burst4(uint8_t data[4])
{
	(void)gpio_pin_set(gpio0, PMW_PIN_NCS, 0);
	spi_delay();
	shift_out_read_address(PMW_REG_BURST_READ);
	for (size_t i = 0; i < 4; i++) {
		data[i] = shift_in(i + 1 < 4);
	}
	spi_delay();
	(void)gpio_pin_set(gpio0, PMW_PIN_NCS, 1);
	(void)gpio_pin_set(gpio0, PMW_PIN_SCLK, 0);
	/* PMW3610 tBEXIT is 250 ns. */
	k_busy_wait(1);
}

static int16_t sign_extend_12(uint16_t value)
{
	if ((value & BIT(11)) != 0U) {
		value |= 0xF000;
	}
	return (int16_t)value;
}

int pmw3610_set_cpi(uint16_t cpi)
{
	uint8_t step;
	uint8_t orientation = 0;

	if (cpi < 200 || cpi > 3200 || (cpi % 200) != 0) {
		return -EINVAL;
	}

	step = (uint8_t)(cpi / 200);
	if (IS_ENABLED(CONFIG_PMW3610_SWAP_XY)) {
		orientation |= BIT(7);
	}
	if (IS_ENABLED(CONFIG_PMW3610_INVERT_X)) {
		orientation |= BIT(6);
	}
	if (IS_ENABLED(CONFIG_PMW3610_INVERT_Y)) {
		orientation |= BIT(5);
	}

	const uint8_t sequence[][2] = {
		{PMW_REG_PAGE, 0xFF},
		{PMW_REG_RES_STEP, (uint8_t)(orientation | step)},
		{PMW_REG_PAGE, 0x00},
	};
	write_sequence(sequence, ARRAY_SIZE(sequence));
	return 0;
}

int pmw3610_init(void)
{
	uint8_t observation;
	const uint8_t clear_observation[][2] = {
		{PMW_REG_OBSERVATION1, 0x00},
	};
	const uint8_t required_settings[][2] = {
		{PMW_REG_PERFORMANCE, 0x0D},
		{PMW_REG_RUN_DOWNSHIFT, 0x04},
		{PMW_REG_REST1_RATE, 0x04},
		{PMW_REG_REST1_DOWNSHIFT, 0x0F},
	};

	if (!device_is_ready(gpio0)) {
		return -ENODEV;
	}

	(void)gpio_pin_configure(gpio0, PMW_PIN_NCS, GPIO_OUTPUT_HIGH);
	(void)gpio_pin_configure(gpio0, PMW_PIN_SCLK, GPIO_OUTPUT_LOW);
	(void)gpio_pin_configure(gpio0, PMW_PIN_SDIO, GPIO_OUTPUT_HIGH);
	(void)gpio_pin_configure(gpio0, PMW_PIN_MOTION, GPIO_INPUT | GPIO_PULL_UP);
	(void)gpio_pin_configure(gpio0, PMW_PIN_RESET, GPIO_OUTPUT_LOW);
	k_msleep(1);
	(void)gpio_pin_set(gpio0, PMW_PIN_RESET, 1);
	k_msleep(5);

	/* Reset the serial port and wait more than one 150 us frame. */
	(void)gpio_pin_set(gpio0, PMW_PIN_NCS, 0);
	spi_delay();
	(void)gpio_pin_set(gpio0, PMW_PIN_NCS, 1);
	k_busy_wait(200);

	if (read_reg(PMW_REG_PRODUCT_ID) != PMW_PRODUCT_ID) {
		return -ENODEV;
	}

	write_sequence(clear_observation, ARRAY_SIZE(clear_observation));
	k_msleep(10);
	observation = read_reg(PMW_REG_OBSERVATION1);
	if ((observation & 0x0F) != 0x0F) {
		return -EIO;
	}

	/* Required dummy reads after power-up. */
	(void)read_reg(0x02);
	(void)read_reg(0x03);
	(void)read_reg(0x04);
	(void)read_reg(0x05);

	write_sequence(required_settings, ARRAY_SIZE(required_settings));
	return pmw3610_set_cpi(1200);
}

int pmw3610_read_motion(struct pmw3610_motion *motion)
{
	uint8_t data[4];
	uint16_t x;
	uint16_t y;

	if (motion == NULL) {
		return -EINVAL;
	}

	motion->valid = false;
	motion->overflow = false;
	motion->x = 0;
	motion->y = 0;

	if (gpio_pin_get(gpio0, PMW_PIN_MOTION) > 0) {
		return 0;
	}

	read_burst4(data);
	/* PixArt requires a Motion-register write after burst reads to clear
	 * residual motion before the next sample.
	 */
	write_raw(PMW_REG_MOTION, 0x00);
	motion->valid = (data[0] & BIT(7)) != 0U;
	motion->overflow = (data[0] & BIT(4)) != 0U;
	x = (uint16_t)data[1] | ((uint16_t)(data[3] & 0xF0) << 4);
	y = (uint16_t)data[2] | ((uint16_t)(data[3] & 0x0F) << 8);
	motion->x = sign_extend_12(x);
	motion->y = sign_extend_12(y);
	return 0;
}