gt9x 0.1.0

A no_std driver for the GT9x series of capacitive touch screen controllers, supporting both async and blocking interfaces.
Documentation
use core::{convert::Infallible, marker::PhantomData};
use embedded_hal_async::{digital, i2c};
use heapless::Vec;

use crate::{chip::*, reg};

/// Driver errors
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Debug)]
pub enum Error<I2cE, IntE> {
    /// I2C error
    I2C(I2cE),
    /// Interrupt pin error / wait error
    Int(IntE),
    /// Invalid Data
    InvalidData,
    /// Invalid Chip
    InvalidChip,
    /// Invalid Product ID
    InvalidProductId,
}

#[doc(hidden)]
pub trait PinMarker {}
/// Marker for Gt9x with interrupt pin
pub struct IntPin;
impl PinMarker for IntPin {}
/// Marker for Gt9x without interrupt pin
pub struct NoIntPin;
impl PinMarker for NoIntPin {}

/// Asynchronous GT9x driver
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct Gt9x<'a, C, I2C, PIN, PM, CM = ()>
where
    C: Chip,
    I2C: i2c::I2c,
    PM: PinMarker,
    CM: CacheMaintenance,
{
    chip: PhantomData<C>,
    pub i2c: I2C,
    pub i2c_addr: u8,
    pub int: Option<PIN>,
    pm: PhantomData<PM>,
    pub buf: &'a mut [u8],
    cm: CM,
}

impl<'a, C, I2C> Gt9x<'a, C, I2C, (), NoIntPin, ()>
where
    C: Chip,
    I2C: i2c::I2c,
{
    /// Create a new GT9x driver without an interrupt pin.
    ///
    /// The buffer `buf` must be large enough to hold the touch data.
    /// The required size is `(MAX_POINTS * 8).max(4)`.
    pub fn new(i2c: I2C, buf: &'a mut [u8]) -> Self {
        Self::new_internal(i2c, buf, None, ())
    }
}

impl<'a, C, I2C, PIN> Gt9x<'a, C, I2C, PIN, IntPin, ()>
where
    C: Chip,
    I2C: i2c::I2c,
    PIN: digital::Wait,
{
    /// Create a new GT9x driver with an interrupt pin.
    ///
    /// The buffer `buf` must be large enough to hold the touch data.
    /// The required size is `(MAX_POINTS * 8).max(4)`.
    pub fn new_int(i2c: I2C, buf: &'a mut [u8], int: PIN) -> Self {
        Self::new_internal(i2c, buf, Some(int), ())
    }
}

impl<'a, C, I2C, PIN, PM, CM> Gt9x<'a, C, I2C, PIN, PM, CM>
where
    C: Chip,
    I2C: i2c::I2c,
    PM: PinMarker,
    CM: CacheMaintenance,
{
    /// The default i2c address is 0x5D. If the chip requires a different address,
    /// it can be set using the `set_i2c_addr` method.
    fn new_internal(i2c: I2C, buf: &'a mut [u8], int: Option<PIN>, cm: CM) -> Self {
        let required_len = (C::MAX_POINTS as usize * 8).max(4);
        assert!(buf.len() >= required_len, "Buffer too small");

        if let Some(alignment) = cm.required_alignment() {
            assert!(
                alignment > 0 && alignment.is_power_of_two(),
                "Cache alignment must be a power of two."
            );
            assert!(
                buf.as_ptr() as usize % alignment == 0,
                "Buffer start address is not aligned as required by the cache manager."
            );
            assert!(
                buf.len() % alignment == 0,
                "Buffer length is not a multiple of the alignment required by the cache manager."
            );
        }

        Gt9x {
            chip: PhantomData,
            i2c,
            i2c_addr: 0x5D,
            int,
            pm: PhantomData,
            buf,
            cm,
        }
    }

    /// Set the I2C address of the touch screen controller.
    /// The default address is 0x5D.
    pub fn set_i2c_addr(&mut self, addr: u8) {
        self.i2c_addr = addr;
    }

    /// Write data to the touch screen controller.
    pub async fn write<E>(&mut self, write: &[u8]) -> Result<(), Error<I2C::Error, E>> {
        self.i2c
            .write(self.i2c_addr, write)
            .await
            .map_err(Error::I2C)
    }

    /// Write data to and read data from the touch screen controller.
    pub async fn write_read<E>(
        &mut self,
        write: &[u8],
        len: usize,
    ) -> Result<(), Error<I2C::Error, E>> {
        self.i2c
            .write_read(self.i2c_addr, write, &mut self.buf[..len])
            .await
            .map_err(Error::I2C)?;
        self.cm.invalidate_dcache_by_slice(&mut self.buf);
        Ok(())
    }

    /// Initialize the touch screen controller.
    /// This method will check the product ID of the chip.
    pub async fn init(&mut self) -> Result<(), Error<I2C::Error, Infallible>> {
        self.write(reg::CMD::ModeRead.as_bytes()).await?;
        self.write_read(reg::ID_ADDR, 4).await?;
        match core::str::from_utf8(&self.buf[..4]) {
            Ok(product_id) => {
                #[cfg(feature = "defmt")]
                defmt::trace!("Product ID: {}", product_id);
                if product_id == C::ID {
                    self.write(reg::CLEAR_STATUS).await?;
                    Ok(())
                } else {
                    #[cfg(feature = "defmt")]
                    defmt::error!("Expected: {}, found: {}", C::ID, product_id);
                    Err(Error::InvalidProductId)
                }
            }
            Err(_) => {
                #[cfg(feature = "defmt")]
                defmt::error!("InvalidData: {}", self.buf[..4]);
                Err(Error::InvalidData)
            }
        }
    }

    async fn _get_touches<E>(
        &mut self,
    ) -> Result<Vec<Point, { C::MAX_POINTS as usize }>, Error<I2C::Error, E>>
    where
        [(); C::MAX_POINTS as usize]:,
    {
        self.write_read(reg::STATUS_ADDR, 1).await?;
        let status = reg::Status::from_bits(self.buf[0]);

        if status.num_points() == 0 || !status.buffer_status() {
            self.write(reg::CLEAR_STATUS).await?;
            return Ok(Vec::new());
        }

        self.write_read(&reg::POINT_BASE, status.num_points() as usize * 8)
            .await?;

        let mut points = Vec::new();
        for i in 0..status.num_points().min(C::MAX_POINTS) {
            let base = (i as usize) * 8;
            let point = Point {
                id: self.buf[base] & 0x0F,
                x: u16::from_le_bytes([self.buf[base + 1], self.buf[base + 2]]),
                y: u16::from_le_bytes([self.buf[base + 3], self.buf[base + 4]]),
                area: u16::from_le_bytes([self.buf[base + 5], self.buf[base + 6]]),
            };
            points.push(point).map_err(|_| Error::InvalidData)?;
        }

        self.write(reg::CLEAR_STATUS).await?;
        Ok(points)
    }
}

impl<'a, C, I2C, PIN, CM> Gt9x<'a, C, I2C, PIN, IntPin, CM>
where
    C: Chip,
    I2C: i2c::I2c,
    PIN: digital::Wait,
    CM: CacheMaintenance,
{
    /// Create a new GT9x driver with an interrupt pin and cache maintenance.
    pub fn new_int_cache(i2c: I2C, buf: &'a mut [u8], int: PIN, cm: CM) -> Self {
        Self::new_internal(i2c, buf, Some(int), cm)
    }

    /// Get the current touch points.
    /// This method will wait for the interrupt pin to go high before reading the touch data.
    pub async fn get_touches(
        &mut self,
    ) -> Result<Vec<Point, { C::MAX_POINTS as usize }>, Error<I2C::Error, PIN::Error>>
    where
        [(); C::MAX_POINTS as usize]:,
    {
        self.int
            .as_mut()
            .unwrap()
            .wait_for_rising_edge()
            .await
            .map_err(Error::Int)?;
        self._get_touches().await
    }
}

impl<'a, C, I2C, CM> Gt9x<'a, C, I2C, (), NoIntPin, CM>
where
    C: Chip,
    I2C: i2c::I2c,
    CM: CacheMaintenance,
{
    /// Create a new GT9x driver with cache maintenance.
    pub fn new_cache(i2c: I2C, buf: &'a mut [u8], cm: CM) -> Self {
        Self::new_internal(i2c, buf, None, cm)
    }

    /// Get the current touch points.
    /// This method will poll the status register to check for new touch data.
    pub async fn get_touches(
        &mut self,
    ) -> Result<Vec<Point, { C::MAX_POINTS as usize }>, Error<I2C::Error, Infallible>>
    where
        [(); C::MAX_POINTS as usize]:,
    {
        self._get_touches().await
    }
}

/// A trait for cache maintenance operations.
///
/// This is useful when the touch data buffer is located in a memory region
/// that is managed by a data cache (e.g., D-Cache in ARM Cortex-M7).
pub trait CacheMaintenance {
    /// Returns the required alignment in bytes if cache maintenance is active.
    ///
    /// If this returns `Some(N)`, the driver will assert that the provided buffer's
    /// start address and length are multiples of `N`.
    /// If it returns `None`, no checks are performed.
    /// `N` must be a power of two.
    fn required_alignment(&self) -> Option<usize>;

    /// Invalidates the D-Cache for the given slice.
    fn invalidate_dcache_by_slice(&self, slice: &mut [u8]);
}

impl CacheMaintenance for () {
    #[inline(always)]
    fn required_alignment(&self) -> Option<usize> {
        None
    }

    #[inline(always)]
    fn invalidate_dcache_by_slice(&self, _slice: &mut [u8]) {}
}