Close

Synaptics ClickPad as a USB mouse

A project log for Reuse Laptop Touchpads

Turn a touchpad from an old laptop into a standalone mouse.

deling-renDeling Ren 08/05/2024 at 18:080 Comments

Context

I harvested a touchpad from an old HP Envy Sleekbook 6 and wanted to use it as a standalone mouse. It's a clickpad. I.e. it has only one physical button and right click is simulated in software.

Goals:

Status:

Most laptop touchpads are made by Synaptics. They usually use a PS/2 interface. By default (i.e. without any special drivers), they can usually simulate a regular PS/2 mouse. I.e. they can report finger movements and button clicks. But that's about it. They don't support more sophisticated gestures we normally find on laptops. To achieve those gestures, we need to implement proprietary but published (with some caveats) Synaptics protocols, documented in [Synaptics PS/2 TouchPad Interfacing Guide](touchpad_RevB.pdf).

Touchpad info

More info obtained from programatic queries, based on section 4.4. Information queries.

Implementing PS/2 on an MCU

So, I'm using an atmel mega32u4 to interface with the touchpad. Any Leonardo clone should work. The reason I picked this MCU is its native USB HID support. Another alternative is to use tinyusb library to bit bang USB protocol on supported MCUs. It's probably pretty straight-forward too.

Basically, I implemented syncrhonous writing, synchronous reading, and asynchronous reading. Synchronous writing because async writing is difficult to use. However, given the async nature of PS/2 protocol, it makes sense to have async, interrupt based reading. I.e. bits are transferred via interrupts and stored in a buffer. I have also implemented synchronous reading as a means to read responses after each write.
I'm using [external interrupts](https://developerhelp.microchip.com/xwiki/bin/view/products/mcu-mpu/8-bit-avr/structure/extint/) to interact with the clock pin. So the clock pin needs to be one of these: 0, 1, 2, 3, 7 (D2, D3, D1, D0, E6, respectively). It's also possible to use pin change interrupts with slight changes to the code:

*(digitalPinToPCICR(clock_pin)) |= 1 << digitalPinToPCICRbit(clock_pin);
*(digitalPinToPCMSK(clock_pin)) |= 1 << digitalPinToPCMSKbit(clock_pin);
...
ISR(PCINT0_vect) { ...}

With the PS/2 implemented, it's really straight-forward to interact the touchpad as a standard PS/w2 mouse. The code is [here](https://github.com/delingren/ps2_mouse).

The rest of this doc focuses on the proprietary Synaptics expansion of the PS/2 protocol.

Data packets

The touchpad sends 6-byte packets to the host. These packets contain information such as finger positions, pressure, width. This particular touchpad can detect 3 fingers. But it only reports the positions of two. When more than one finger is pressed, it reports the states of two fingers in alternating packets.

Since the communication is inevitably noisy, packets could be lost or altered. And it's not very critical to catch each and every frame. Some packets have distinctive features (e.g. fixed bits at certain places) and can be used as synchronization and recovery packets. So if an unexpected packet is received, I keep discarding packets until I'm in sync again.

State machine logic

I am simulating the behaviour of a MacBook since that's what I'm used to. Most PC laptops behave the same too, with "tap to click" feature turned off. In the following text, whenever I say "two fingers", I mean two or more fingers.

For now, we only do tracking, scrolling, left and right buttons, where right button click is simulated with a two finger click, just like MacBook. Windows optionally supports this too, as long as the touchpad is multi-touch.

The logic is quite complicated. I might need to give it a little more thought and simplify a little. For now, we have three main valid states, identified by two state variables, finger count and button state. Finger count is the number of fingers on the pad. Button state is a bitmap representing the states of left and right buttons.

Idle

This is where no fingers are on the touchpad. Conditions:
* finger count == 0
Note that even when there's no fingers are pressed, the button could still be pressed, by a non captive object such as a pen.

Tracking

This is the state where we report the coordinate delta to the OS who will move the cursor accordingly. There are a couple of substates. conditions:
* finger count == 1 || finger count == 2 && button state != 0

When one finger is pressed, we're tracking regardless if the button is pressed. When two fingers are pressed and the button is *not* pressed, we always scroll. But if two fingers are pressed and the button is also pressed, we also track. This is MacBook's behaviour, which is different from Windows, at least Windows 11 on an HP Envy x360.

Scrolling

This is the state where two fingers are on the pad and their vertical movements are treated as scrolling. Conditions:
* finger count == 2 || button state == 0

Optimizations

Smoothing

I found that if we faithfully report the finger positions in each frame, the cursor wobbles a lot, due to inherent noise and instability of human fingers. To mitigate, a few mechanisms are implemented:

* Threshold. If the delta between two frames is below a threshold, we assume it's not intentional. The number is emperical and I fine tuned it a few iterations to a place where I'm happy with false positives and false negatives.
* Averaging. Instead of reporting the position of each frame, I keep track of the average of last 5 frames, to remove sudden movements.

Precision Scrolling

Scrolling seems to have much less granularity. The HID report uses an integer. I find it quite jerky to even report an amount of 1 in each frame. The reason is the frame rate is too high.

So, I decided to have two scrolling intentions: precision scrolling and fast scrolling. When the finger movements are slow and small, I only generate a report every few frames, and the movement is only 1. Once the speed has passed a certain threshold, I assume the user's intention is to quickly scroll over a big area. In this case, I report each frame and the amount is proportional to the actual movement.

Source code:

https://github.com/delingren/touchpad

TODOs

Discussions