1. Blog>
  2. Building a Rust Driver for PineTime’s Touch Controller

Building a Rust Driver for PineTime’s Touch Controller

by: Jan 12,2021 4376 Views 0 Comments Posted in Activities

Internet of Things Rust Gadgets programming Nrf52

Pretend you’re my IoT student. I give you a PineTime Smart Watch and challenge you to “Make it work… Especially the touch screen!” The Touch Screen appears to be lacking some documentation. What would you do?

PineTime Touch Screen = Touch Panel + Display Panel

We carefully pry open the PineTime Smart Watch with tweezers (or forceps) and examine the Touch Screen. It looks like a single chunk of plastic… But it’s actually two delicate pieces fused together: Touch Panel and Display Panel

PineTime Touch Panel and Display Panel. From http://files.pine64.org/doc/datasheet/pinetime/PineTime%20LCD%20Panel.jpg and http://files.pine64.org/doc/datasheet/pinetime/PineTime%20Touch%20Panel.jpg

The Touch Panel and Display Panel are connected to the I2C and SPI connectors on the PineTime board…

Connectors for ST7789 Display Controller and Hynitron CST816S Capacitive Touch Controller

Last week we have programmed the ST7789 Display Controller (via SPI) to render colour text and graphics on the Display Panel. This week we’ll look at the Hynitron CST816S Capacitive Touch Controller for PineTime’s Touch Panel…

Probing PineTime’s I2C Bus for Touch Controller

The PineTime Touch Controller is connected to the nRF52 Microcontroller via an I2C Bus. That stands for Inter-Integrated Circuit and is pronounced “I-squared-C”. (I used to call it “I-two-C” until my IoT students corrected me)

News Flash! I2C works more like a Light Rail transit line than a Bus. It runs on two rails (wires), shuttles tiny packets of passengers (data) between a Main Station (Master Microcontroller) and Sub Stations (Slave Devices)…

Light Rail Transit Line

In Light Rail, the train is sent from the Main Station to each Sub Station to drop off passengers. And we expect the train to pick up passengers from each Sub Station. A Sub Station with drop-offs but no pick-ups will be so spooky… like a Ghost Station!

I2C (Inter-Integrated Circuit) Bus on PineTime

Thankfully there are no Ghost Stations in I2C… When the Microcontroller sends a packet to a Slave Device at a particular address, the Slave Device must respond with another packet. This called I2C Probing and the Rust code on PineTime looks like this…

/// Probe the I2C bus to discover I2C devices
pub fn probe() -> MynewtResult<()> {
  // For each I2C address 0 to 127...
  for addr in 0..128 {
    // Probe the I2C address at I2C Port 1. Time out after 1,000 milliseconds (1 second).
    let rc = unsafe { hal::hal_i2c_master_probe(1, addr, 1000) };
    // If we received an acknowledgement...
    if rc != hal::HAL_I2C_ERR_ADDR_NACK as i32 {
      // I2C device found
      console::print("0x"); console::printhex(addr); console::print("\n"); console::flush();
    }
  }
  console::print("Done\n"); console::flush();
  Ok(())
}
/* I2C devices found:
  0x18: Accelerometer: https://ae-bst.resource.bosch.com/media/_tech/media/datasheets/BST-BMA423-DS000.pdf
  0x44: Heart Rate Sensor: http://files.pine64.org/doc/datasheet/pinetime/HRS3300%20Heart%20Rate%20Sensor.pdf */

Probing the I2C Bus on PineTime to discover I2C devices. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/touch_sensor.rs

There are only 128 possible I2C Addresses (0 to 127) so it’s feasible to identify all I2C Slave Devices by probing each address. (Here’s a list of I2C Addresses for popular devices)

Running the above code on PineTime we find two devices on our I2C Bus, at I2C Addresses 0x18 and 0x44. That’s the Accelerometer and Heart Rate Sensor. (I verified against the docs)

But where’s the Touch Controller? Do Ghost Stations really exist on I2C?

PineTime Touch Controller responds to Special Events

Back to our Light Rail analogy… Suppose we have a Sub Station that serves an Amusement Park. The Sub Station operates on special occasions like weekends and holidays. Most of the time it would appear as a Ghost Station… Coming to life only on special occasions!

What is this “Special Event" that brings our Touch Controller to life?

Maybe it’s low on power? This power problem happens often with wireless gadgets like NB-IoT. We connect the 5 Volt power from the ST-Link USB dongle and… Nothing changes! (Though the PineTime Battery gets properly charged now… Nice!)

Connecting the 5V Charging Pad on PineTime to the 5V Pin of ST-Link with a folded loop of Single Core Wire (22 AWG)

(Don’t forget the “Only One Power Source” OOPS Rule… Our computer’s USB port supplies both 3.3 Volt and 5 Volt power sources… Never use two different power sources… OOPS!)

Touch Controller State Transition Diagram from http://files.pine64.org/doc/datasheet/pinetime/CST816S%E6%95%B0%E6%8D%AE%E6%89%8B%E5%86%8CV1.1.pdf.

We stare hard at this State Transition Diagram from the Touch Controller doc. (The English Translation looked alien so we’re using the original Chinese version with my translation) What is it trying to tell us?

It says that the Touch Controller can be woken from the Standby Mode… By Touch!

We tap the screen… Sure enough our Ghost Station comes to life! Our Touch Controller needs a little tap (the “Special Event”) to wake it up and respond to I2C packets. Mystery solved!

With screen tapping and I2C probing, we finally locate PineTime’s Touch Controller… At I2C Address 0x15

I2C Streaming with PineTime Touch Controller

To discover which parts of the screen are touched (up to 10 points), we need to read 63 bytes from the PineTime Touch Controller… That’s 63 consecutive I2C Registers, 1 byte per register!

There’s an efficient way to read 63 consecutive I2C Registers from the PineTime Touch Controller. Some people call it Bulk Reading but let’s call it I2C Streaming.

Ever used a Rubber Hose to siphon petrol / gasoline / fish tank water? We suck a bit of air out of the Rubber Hose, maybe we’ll taste a bit of the petrol / gasoline / fish tank water (whoa that’s nasty), and the liquid comes streaming out. (Yes you can remove the hose from your mouth now!)

The liquid keeps streaming until you stop it. I2C Streaming works just like that… We instruct the Touch Controller (via its I2C Address 0x15) to send us the value of I2C Register #0. The Touch Controller will stream a list of consecutive I2C Register values (Register #0, #1, #2, …) Until we receive 63 values and we tell the Touch Controller “Stop!!!”

I2C Streaming runs in five steps like this…

Steps to stream I2C Registers #0 to #62 from the I2C Touch Controller at I2C Address 0x15

Steps 1 through 5 are explained in the Rust code below that reads a range of I2C Registers (Registers #0 to #62, total 63 Registers) from the Touch Controller (at I2C Address 0x15)

/// Read a range of I2C registers from the I2C address `addr` (7-bit address), starting at `start_register` for count `num_registers`. Save into `buffer`.
fn read_register_range(addr: u8, start_register: u8, num_registers: u8, buffer: &mut[u8]) -> MynewtResult<()> {
  assert!(buffer.len() >= num_registers as usize, "i2c buf"); // Buffer too small
  assert!(start_register + num_registers < 128, "i2c addr");  // Not 7-bit address
  // Step 1: Prepare to read I2C Device Registers:
  //  System sends Clock Signal on SCL to sync Microcontroller with I2C Device
  // Step 2: Transmit the I2C Address and the starting Register Number:
  //  Send the Start Condition (High to Low SDA Transition)...
  //  Followed by I2C Address (7 bits)...
  //  Followed by Write Mode (1 bit, value 0)...
  //  Followed by starting Register Number (8 bits)
  unsafe { 
    I2C_BUFFER[0] = start_register; // I2C Packet buffer contains starting Register Number (1 byte)
    I2C_DATA.address = addr;     // I2C Packet address (7 bits)
    I2C_DATA.len = I2C_BUFFER.len() as u16;   // I2C Packet data size is 1 byte
    I2C_DATA.buffer = I2C_BUFFER.as_mut_ptr(); // I2C Packet data points to packet buffer
  };
  let _rc1 = unsafe { hal::hal_i2c_master_write(1, &mut I2C_DATA, 1000, 0) }; // No stop yet, must continue even if we hit an error
  // Step 3: Prepare to receive the stream of I2C Device Register values...
  //  Send the Start Condition (High to Low SDA Transition)...
  //  Followed by I2C Address (7 bits)...
  //  Followed by Read Mode (1 bit, value 1)
  unsafe { 
    I2C_BUFFER[0] = 0x00;   // I2C Packet buffer should be empty (provided by caller)
    I2C_DATA.address = addr; // I2C Packet address (7 bits)
    I2C_DATA.len = num_registers as u16;  // I2C Packet data size is number of Registers to read
    I2C_DATA.buffer = buffer.as_mut_ptr(); // I2C Packet data points to packet buffer
  };
  // Step 4: Receive the requested number of Register values from I2C Device (1 byte per register)
  // Step 5: Send the Stop Condition (Low to High SDA Transition)
  let rc2 = unsafe { hal::hal_i2c_master_read(1, &mut I2C_DATA, 1000, 1) };
  if rc2 == hal::HAL_I2C_ERR_ADDR_NACK as i32 {
    assert!(false, "i2c fail"); // I2C read failed
    return Ok(());        // TODO: Return an error
  }
  Ok(())
}

Rust code to read a range of I2C Registers from an I2C Device on PineTime. The steps in the comments refer to the steps in the diagram above. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/touch_sensor.rs

Yep the code looks alarming with the unsafe blocks (because we are calling C APIs from Rust). Eventually we should implement the I2C Interface for Rust Embedded HAL… And the unsafe blocks will be gone forever.

Convert PineTime Touch Data

The PineTime Touch Controller generates 63 bytes of raw touch data on every touch. (According to the reference code here) When converted, these 63 bytes produce 10 points of touch information. Each point of touch information contains…

X (Horizontal) and Y (Vertical) Coordinates of the touched point. (0, 0) represents top left, (239, 239) represents bottom right.

Action: Whether the point was just touched, just untouched or kept in contact

Finger: A number that identifies the finger that touched the point

Pressure: How hard the point was touched

Area: Size of the touched area

/// Touch Event Info for multiple touches. Based on https://github.com/lupyuen/hynitron_i2c_cst0xxse/blob/master/cst0xx_core.h#L104-L115
struct TouchEventInfo {
  /// Array of 10 touch points
  touches:  [TouchInfo; HYN_MAX_POINTS],
  /// How many touch points
  count:   u8,
  point_num: u8,
}

/// Touch Info for a single touch. Based on https://github.com/lupyuen/hynitron_i2c_cst0xxse/blob/master/cst0xx_core.h#L104-L115
struct TouchInfo {
  /// X coordinate
  x:     u16,
  /// Y coordinate
  y:     u16,
  /// Action: 0 = down, 1 = up, 2 = contact
  action:   u8,
  /// Which finger touched
  finger:   u8,     
  /// Pressure of touch
  pressure:  u8,
  /// Area touched
  area:    u8,
}

/// Max touch channels for the touch controller. Based on https://github.com/lupyuen/hynitron_i2c_cst0xxse/blob/master/cst0xx_core.h
const HYN_MAX_POINTS: usize = 10;

63 bytes of raw touch data converted to 10 points of touch information. This is the Rust representation of the 10 touch points. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/touch_sensor.rs

That’s a lot of information for each point touched… Why do we need so much information? Why the finger-pointing?

Think of the classic 2010 iPad game Fruit Ninja… The game tracks the swiping motion of each finger to figure out which fruits (and bombs) you’re slashing. So the tracking of each finger is important for swiping-based apps. (Like Tinder)

The Touch Controller doesn’t really know whether you touched the screen with your left forefinger or your right thumb (or your nose)… It just assigns a number to the finger (or appendage) that’s swiping the screen. The Touch Action (touched / untouched / in contact) is necessary to determine whether this is the start, middle or end of a swipe.

Based on the reference code, we derived the following Rust function for reading and converting the 63 bytes of raw touch data into touch points. Note that only the first 5 touch points out of 10 are converted. (Probably because it’s not meaningful to track 10 fingers on a tiny watch screen)

/// Read touch controller data. This only works when the screen has been tapped and the touch controller wakes up.
/// Ported from https://github.com/lupyuen/hynitron_i2c_cst0xxse/blob/master/cst0xx_core.c#L407-L466
fn read_touchdata(data: &mut TouchEventInfo) -> MynewtResult<()> {
  read_register_range(      // Read the range of I2C registers...
    TOUCH_CONTROLLER_ADDRESS, // From the touch controller
    0,             // Starting from register 0
    POINT_READ_BUF as u8,   // Number of registers to read (63)
    unsafe { &mut buf }    // Save the read data into `buf`
  ).expect("read touchdata fail");
  *data = fill_zero!(TouchEventInfo);
  data.point_num = unsafe { buf[FT_TOUCH_POINT_NUM] & 0x0F };
  data.count   = 0;

  // Populate the first 5 touch points
  for i in 0..CFG_MAX_TOUCH_POINTS {
    let pointid = unsafe { buf[HYN_TOUCH_ID_POS + HYN_TOUCH_STEP * i] } >> 4;
    if pointid >= HYN_MAX_ID { break; }

    // Compute X and Y coordinates
    data.count += 1;
    let x_high = unsafe { buf[HYN_TOUCH_X_H_POS + HYN_TOUCH_STEP * i] & 0x0F } as u16;
    let x_low = unsafe { buf[HYN_TOUCH_X_L_POS + HYN_TOUCH_STEP * i] } as u16;
    data.touches[i].x = (x_high << 8) | x_low;

    let y_high = unsafe { buf[HYN_TOUCH_Y_H_POS + HYN_TOUCH_STEP * i] & 0x0F } as u16;
    let y_low = unsafe { buf[HYN_TOUCH_Y_L_POS + HYN_TOUCH_STEP * i] } as u16;
    data.touches[i].y = (y_high << 8) | y_low;

    // Compute touch action (0 = down, 1 = up, 2 = contact) and finger ID
    data.touches[i].action =
      unsafe { buf[HYN_TOUCH_EVENT_POS + HYN_TOUCH_STEP * i] } >> 6;
    data.touches[i].finger =
      unsafe { buf[HYN_TOUCH_ID_POS  + HYN_TOUCH_STEP * i] } >> 4;

    // Compute touch pressure and area
    data.touches[i].pressure =
      unsafe { buf[HYN_TOUCH_XY_POS + HYN_TOUCH_STEP * i] }; // Can't be constant value
    data.touches[i].area =
      unsafe { buf[HYN_TOUCH_MISC  + HYN_TOUCH_STEP * i] } >> 4;

    // If no more touch points, stop
    if (data.touches[i].action == 0 || data.touches[i].action == 2// If touch is down or contact
      && (data.point_num == 0) {
      break;
    }
  }
  Ok(())
}

/// Buffer for raw touch data (63 bytes)
static mut buf: [u8; POINT_READ_BUF] = [0; POINT_READ_BUF];

/// Touch Controller I2C Address: https://github.com/lupyuen/hynitron_i2c_cst0xxse
const TOUCH_CONTROLLER_ADDRESS: u8 = 0x15;

Rust code to convert raw touch data to touch point information. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/touch_sensor.rs

Display PineTime Touch Data

Now that we have the converted touch data, let’s display the data on the screen to verify that we have converted the data correctly. Here’s the Rust code that takes coordinates (X, Y) and displays them as X = …, Y = … on the PineTime screen…

/// Display the touched (X, Y) coordinates
pub fn show_touch(x: u16, y: u16) -> MynewtResult<()> {
  // Format coordinates as text into a fixed-size buffer
  let mut buf_x = ArrayString::<[u8; 20]>::new();
  let mut buf_y = ArrayString::<[u8; 20]>::new();
  write!(&mut buf_x, " X = {} ", x)
    .expect("show touch fail");
  write!(&mut buf_y, " Y = {} ", y)
    .expect("show touch fail");
     
  // Prepare the text for rendering
  let text_x = fonts::Font12x16::<Rgb565>
    ::render_str(&buf_x)
    .stroke(Some(Rgb565::from((0xff, 0xff, 0xff)))) // White
    .fill(Some(Rgb565::from((0x00, 0x00, 0x00))))  // Black
    .translate(Coord::new(40, 100));
  let text_y = fonts::Font12x16::<Rgb565>
    ::render_str(&buf_y)
    .stroke(Some(Rgb565::from((0xff, 0xff, 0xff)))) // White
    .fill(Some(Rgb565::from((0x00, 0x00, 0x00))))  // Black
    .translate(Coord::new(40, 130));
     
  // Render text to display
  unsafe {
    DISPLAY.draw(text_x);   
    DISPLAY.draw(text_y);   
  }
  Ok(())
}

Rust code to display the touched coordinates on the screen. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/display.rs

Notice that we passed a special arrayvec::ArrayString type to the write!() macro for storing the formatted text. That’s because the write!() macro normally accepts a String type for storing formatted text… But String relies on Dynamic Heap Memory which we don’t support in our Rust Application.

Why no Heap in our application? For Embedded Applications it’s a good practice to budget our memory requirements in advance and allocate our variables statically. Yes that means we have to do more planning while designing our programs… But we prevent problems like running out of heap memory (and heap fragmentation).

In the above code we have budgeted 20 bytes in our fixed-size arrayvec::ArrayString buffer for storing each formatted text string.

Displaying touched coordinates on PineTime screen

Mr Diego Barrios Romero has an excellent suggestion for handling Rust strings without heaps…

For the strings you can also use the String type from the heapless crate like this

let mut buffer:

heapless::String< heapless::consts::U64 >

= heapless::String::new();

Transform PineTime Touch Interrupt to Mynewt Event Callback

We have our PineTime Touch Controller all figured out… Reading the I2C Registers, converting the touch data and displaying the touch data. But something important is still missing…

Reading, converting and displaying touch data

Remember the Ghost Station? We can’t read the I2C Registers on the Touch Controller unless the screen has been tapped… So how will we know when to read the registers?

Need to handle the Touch Interrupt and get an Event Callback first

Fortunately the Touch Controller generates a Touch Interrupt every time the screen is tapped. So let’s hook on to the Touch Interrupt and do the touch processing there!

/// Reset Pin for touch controller. Note: NFC antenna pins must be reassigned as GPIO pins for this to work.
const TOUCH_RESET_PIN: i32 = 10// P0.10/NFC2: TP_RESET

/// Interrupt Pin for touch controller. We listen for the touch controller interrupt and trigger an event.
const TOUCH_INTERRUPT_PIN: i32 = 28// P0.28/AIN4: TP_INT

/// Reset GPIO Pin
static mut TOUCH_RESET: MynewtGPIO = fill_zero!(MynewtGPIO);
static mut TOUCH_DELAY: MynewtDelay = fill_zero!(MynewtDelay);

/// Initialise the touch controller. NFC antenna pins must already be reassigned as GPIO pins:
/// Set `NFC_PINS_AS_GPIO: 1` in hw/bsp/nrf52/syscfg.yml. To check whether whether NFC antenna 
/// pins have been correctly reassigned as GPIO pins, use the `nrf52` crate and check that the output is `fe`:
/// ```rust
/// let peripherals = nrf52::Peripherals::take().unwrap();
/// let nfcpins = peripherals.UICR.nfcpins.read().bits();
/// console::print("nfcpins = "); console::printhex(nfcpins as u8); console::print("\n");
/// ```
pub fn start_touch_sensor() -> MynewtResult<()> {
  console::print("Rust touch sensor\n");

  // Init GPIO for the Reset Pin
  unsafe { TOUCH_RESET.init(TOUCH_RESET_PIN) ? };

  // Reset the touch controller by switching the Reset Pin low then high with pauses. Based on https://github.com/lupyuen/hynitron_i2c_cst0xxse/blob/master/cst0xx_core.c#L1017-L1167
  unsafe {
    TOUCH_RESET.set_low() ? ;
    TOUCH_DELAY.delay_ms(20);
    TOUCH_RESET.set_high() ? ;
    TOUCH_DELAY.delay_ms(200); TOUCH_DELAY.delay_ms(200);   
  };

  // Initialise the touch event with the callback function
  unsafe { TOUCH_EVENT.ev_cb = Some( touch_event_callback ) };

  // Configure the touch controller interrupt (active when low) to trigger a touch event
  let rc = unsafe { hal::hal_gpio_irq_init(
    TOUCH_INTERRUPT_PIN,       // GPIO pin to be configured
    Some( touch_interrupt_handler ), // Call `touch_interrupt_handler()` upon detecting interrupt
    core::ptr::null_mut(),      // No arguments for `touch_interrupt_handler()`
    hal::hal_gpio_irq_trigger_HAL_GPIO_TRIG_FALLING, // Trigger when interrupt goes from high to low
    hal::hal_gpio_pull_HAL_GPIO_PULL_UP        // Pull up the GPIO pin
  ) };
  assert_eq!(rc, 0, "IRQ init fail");

  // Start monitoring for touch controller interrupts
  unsafe { hal::hal_gpio_irq_enable(TOUCH_INTERRUPT_PIN) };
  Ok(())
}

Assigning an Interrupt Handler for Touch Interrupt. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/touch_sensor.rs

The Touch Interrupt is raised via a normal GPIO Pin (P0.28). In the Rust code above, we ask Mynewt OS to watch out for the Touch Interrupt on the GPIO Pin, and call touch_interrupt_handler() when the GPIO Pin switches from High to Low. (That’s when the screen is tapped)

/// Interrupt handler for the touch controller, triggered when a touch is detected
extern "C" fn touch_interrupt_handler(arg: *mut core::ffi::c_void) {
  // We forward a touch event to the Default Event Queue for deferred processing. Don't do any processing here.
  unsafe { TOUCH_EVENT.ev_arg = arg };
  // Fetch the Default Event Queue. TODO: Use dedicated Event Queue for higher priority processing.
  let queue = os::eventq_dflt_get()
    .expect("GET fail");
	unsafe { os::os_eventq_put(queue, &mut TOUCH_EVENT) }; // Trigger the callback function `touch_event_callback()`
}

/// Event that will be forwarded to the Event Queue when a touch interrupt is triggered
static mut TOUCH_EVENT: os_event = fill_zero!(os_event); // Init all fields to 0 or NULL

Touch interrupt handler that’s called upon touch. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/touch_sensor.rs

touch_interrupt_handler() is an Interrupt Handler Function. Note that this Rust function is declared extern “C” because it will be called by Mynewt OS whenever the Touch Interrupt occurs. So it appears to Mynewt OS like a regular C function.

touch_interrupt_handler() looks… underwhelming. Where’s all the touch data processing we covered earlier?

That’s because Interrupt Handlers are not meant for doing any kind of data processing! Mynewt OS has gone out of its usual routine, just to call our Interrupt Handler because somebody touched the screen. Mynewt OS could have been doing something very important when the interrupt occurred!

Would be so rude of us to snatch Mynewt OS away from some important task… Just to swipe some fruits / bombs / dating profiles!

So let’s be polite to Mynewt OS and take up as little time as possible. We’ll defer the swiping of the fruit / bomb / dating profile… By adding a Mynewt Event to Mynewt’s Event Queue.

// Initialise the touch event with the callback function
unsafe { TOUCH_EVENT.ev_cb = Some( touch_event_callback ) };
...
/// Callback for the touch event that is triggered when a touch is detected
extern "C" fn touch_event_callback(_event: *mut os_event) {
  unsafe { 
    // Fetch the touch data from the touch controller
    read_touchdata(&mut TOUCH_DATA)
      .expect("touchdata fail");
    // Display the touch data
    display::show_touch(
      TOUCH_DATA.touches[0].x,
      TOUCH_DATA.touches[0].y
    ).expect("show touch fail");
  }
}

/// Touch data will be populated here
static mut TOUCH_DATA: TouchEventInfo = fill_zero!(TouchEventInfo);

Event callback for handling touch events. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/touch_sensor.rs

Here’s the Rust code that will eventually be triggered via the Mynewt Event, after Mynewt OS has completed the important tasks. We read the touch data (via I2C Streaming) and display the first detected touch point.

The complete flow from Touch Interrupt to Touch Data Display looks like this. The Rust Driver for PineTime Touch Controller is incomplete because we haven’t implemented Hit Testing to detect the graphic or text on the screen that’s tapped… So Hit Testing is coming next! (And if you wish to help out, drop me a note!)

PineTime Touch Controller fully functioning with Touch Interrupts

References

Please refer to the “References” section at the end of the previous article “Sneak Peek of PineTime Smart Watch… And why it’s perfect for teaching IoT

To run the code on a PineTime Smart Watch, follow the instructions in the section “Programming the PineTime Smart Watch” of the above article.

Note: The content and the pictures in this article are contributed by the author. The opinions expressed by contributors are their own and not those of PCBWay. If there is any infringement of content or pictures, please contact our editor (steven@pcbway.com) for deleting.

Written by

Join us
Wanna be a dedicated PCBWay writer? We definately look forward to having you with us.
  • Comments(0)
You can only upload 1 files in total. Each file cannot exceed 2MB. Supports JPG, JPEG, GIF, PNG, BMP
0 / 10000
    Back to top