minibook-dual-accelerometer
minibook-dual-accelerometer copied to clipboard
Dual accelerometer driver and tablet-mode detection service for Chuwi MiniBook X
Dual-accelerometer tablet-mode switching for the Chuwi MiniBook X
Introduction
A number of 2-in-1 convertible laptops today lack a hardware hinge-position
sensor, and instead use a pair of accelerometers to determine the relative
positions of the base and display. Some such laptops do this transparently in
firmware, but others (such as my Chuwi MiniBook X) require software to
interpret the accelerometer outputs and notify the firmware appropriately. In
the Windows 11 installation that the MiniBook X ships with, this appears to
all be handled inside the accelerometer driver (mxc6655angle.dll)
This repo is an attempt to implement a similar 'software angle sensor' for Linux. Since floating-point math is frowned-upon inside the Linux kernel, we cannot do this entirely inside a driver as in Windows. Instead, we have separate driver and userspace components: the kernel exposes accelerometer data to userspace, and provides a mechanism to notify the firmware of tablet-mode changes. Meanwhile, on the userspace side, a service interprets the accelerometer data, and notifies the driver (which in turn notifies the firmware) of any mode changes.
When the driver triggers a switch into or out tablet mode, the firmware disables or enables the keyboard and touchpad, and issues an HID tablet-mode-switch event, just as would happen if there was a physical switch. Most desktop environments recognise these events, and enable or disable screen rotation, and change UI elements as appropriate.
Why reinvent the wheel?
There already exist a couple of userspace solutions for tablet mode on the MiniBook X, both manual and accelerometer-based. However, the manual solution is Gnome-specific and does not make use of the accelerometers, and the accelerometer-based solution is rather complex (three separate daemons, and virtual input devices that intercept the built-in keyboard and touchpad), did not work reliably for me, and essentially duplicates functionality that already exists to support devices with 'real' tablet switches.
By leveraging the MiniBook's own firmware to do the tablet-mode switching, we eliminate this complexity and duplication of effort - userspace sees the same tablet-mode-switch input events that it would if it had a 'real' hinge-position sensor, and the firmware handles disabling the keyboard and trackpad when in tablet mode without needing awkward interception of inputs.
Status
(see also: TODO.md)
While the driver and angle-sensor service are both currently functional, I would consider them to be at the proof-of-concept stage. Do not expect them to be reliable or stable (or safe) at present. I am notably bad at both signal-processing and trigonometry, which is basically everything that the angle-sensor service has to do! I would welcome any suggestions as to how to do a better job of it.
Once the driver and angle-sensor service have seen a bit more testing and been proven to work reliably, I would like to try to get the platform driver into the mainline Linux kernel, and package up the angle sensor service for easy installation.
This repo contains two different drivers - platform-driver contains my
attempt at a proper driver for eventual inclusion in the mainline kernel.
This driver allows both accelerometers to be detected automatically, and
registers a platform device to which it adds a sysfs property for triggering
tablet-mode switching. However, to do this it requires some supporting
patches elsewhere in the kernel, so while it can be built out-of-tree, it
isn't any use without a rebuilt kernel anyway.
hack-driver is a quick and dirty hack that simply exposes the tablet-mode
switching method to userspace, without the accelerometer detection feature.
This driver can be built out-of-tree on any kernel, so while it isn't a
complete solution, it's more immediately useful, and its lack of
accelerometer detection can be worked around in user space.
Supported hardware and software
I run this on my MiniBook X N100 under OpenSUSE Tumbleweed, and don't have the time or extra hardware to do much testing beyond that. I would very much appreciate anyone willing to test on other distros, or other similar hardware (e.g. the earlier N5100 MiniBook X, the 8" MiniBook, other Chuwi convertible laptops).
How to install and use
If you've got this far and haven't been scared off yet, the following steps should get you set up using the 'hack driver' on your existing kernel:
-
Install DKMS, Python 3, and the
numpyandpyudevPython packages. -
As root, run
make install, or manually:-
Install
hack-driver/60-sensor-chuwi.rulesinto/etc/udev/rules.d. -
Install
hack-driver/chuwi-ltsm-hack.rulesinto/etc/udev/rules.d. -
Install
angle-sensor-service/angle-sensor.pyto/usr/local/sbin/angle-sensorwith execute permissions. -
Install
angle-sensor-service/chuwi-tablet-control.shto/usr/local/sbin/chuwi-tablet-controlwith execute permissions. -
Install
angle-sensor-service/angle-sensor.sysconfigto/etc/sysconfig/angle-sensor. -
Install
angle-sensor-service/angle-sensor.serviceto/etc/systemd/system/angle-sensor.service. -
Build and install the
chuwi-ltsm-hackkernel module by runningdkms install hack-driverin this directory.
-
Running make uninstall will uninstall everything and (hopefully) bring your
system back to how it was before.
Software info
Platform Driver
Kernel patch
While the dual-accelerometer driver is buildable out-of-tree, supporting changes are required in the kernel:
drivers/acpi/scan.c: Add MDA6655 to list of device IDs ignored during device
enumeration. This is necessary to allow our driver to instantiate both
accelerometers specified in its hardware resources, instead of one. Note that
this gets built into the kernel itself, rather than a module (otherwise we could
just build and load modified versions of of the mxc4005 and intel-hid
modules rather than rebuilding the whole kernel).
drivers/iio/accel/mxc4005.c: Remove MDA6655 from ACPI ID list. The change
above renders it redundant.
drivers/platform/x86/intel/hid.c: Add MiniBook X product and vendor IDs to
VGBS method allowlist. This allows the MiniBook X's firmware to send
tablet-state HID events.
chuwi-dual-accel kernel module
The chuwi-dual-accel module is a platform driver that matches the MDA6655
device ID. It instantiates two mxc4005 IIO accelerometer devices, named
MDA6655-mxc4005.display and MDA6655-mxc4005.base, and creates a write-only
sysfs property named chuwi_dual_accel_tablet_mode triggers the LTSM ACPI
method - write a single character '1' to this property to enter tablet mode,
'0' to enter laptop mode (any following characters, and characters other that
'0' and '1', are ignored).
As a safety measure (since keyboard inputs are disabled in tablet mode), the driver forces a transition to laptop mode when it is loaded and unloaded.
Hack driver
The chuwi-ltsm-hack module simply adds a chuwi_ltsm_hack sysfs file to
/sys/bus/acpi/MDA6655:00, that triggers the LTSM ACPI method in the same
way as the chuwi-dual-accel driver.
The included udev rule takes care of adding the second accelerometer and
loading the hack driver when the first accelerometer is detected, and a
modprobe configuration file is used to allow the intel-hid module to
recognise tablet-mode switch events.
angle-sensor
angle-sensor is a Python script that polls the accelerometers (and lid
switch), and calls a configurable command (e.g. chuwi-tablet-control) to take
action when it determines that a tablet-mode change has occurred, based on
accelerometer and lid-switch inputs.
For convenience, the angle sensor service uses the terms 'hinge axis' and 'tilt axis' when talking about orientation. When looking at the laptop in normal 'in your lap' orientation, 'hinge axis' rotation is back and forth (in the axis of the hinge), and 'tilt axis' is left to right, perpendicular to the hinge.
Its basic logic for determining state is:
-
If the lid switch is closed, ignore accelerometers and report
CLOSEDstate. -
If the change in acceleration since the last poll exceeds more than a certain threshold (configurable by
--jerk-threshold), do nothing, since erratic motion will render hinge angle calculations unreliable. -
If the base acceleration vector is off-horizontal on the tilt axis by more than a certain amount (configurable by
--tilt-threshold), do nothing, since hinge angle calculation becomes less reliable as the Z component diminshes (like how rotation sensing becomes unreliable the further the device is from vertical). -
Otherwise, report
LAPTOPorTABLETstate based on the hinge-axis angle between the display and base acceleration vectors. The criteria for state changes can be tuned with--thresholdand--hysteresis.
An additional TENT state is defined, but not detected or reported at present.
This state is intended to represent when the hinge is open further than 180
degrees but not fully folded back into 'tablet' position, allowing the laptop to
stand vertically in a portrait orientation.
chuwi-tablet-control
chuwi-tablet-control is a simple shell script, designed to be called by
angle-sensor, to set the tablet state. It takes a single argument, which can
be one of CLOSED, LAPTOP, TENT, or TABLET, and writes 0 or 1 to
/sys/devices/platform/MDA6655:00/chuwi_dual_accel_tablet_mode as appropriate.
States are translated to chuwi_dual_accel_tablet_mode writes as follows:
| State | Value written | Comment |
|---|---|---|
CLOSED |
0 |
Safety measure - always enter laptop mode when lid is closed |
LAPTOP |
0 |
|
TENT |
1 |
Not implemented in angle-sensor yet |
TABLET |
1 |
Hardware info
Both accelerometers are MEMSIC MXC6655 devices, using a thermal sensing element.
The accelerometer inside the display is at address 0x15 on I2C bus
1, and the base accelerometer is at the same address on I2C bus 0.
The accelerometers are oriented so that in tablet mode, their vectors should be roughly pointing in the same direction. Since I can't draw worth a damn, this table is an attempt to explain the orientation of the accelerometers while in tablet mode.
| + direction | |
|---|---|
| X | In plane of screen, towards camera |
| Y | In plane of screen, towards fan |
| Z | Normal to keyboard (i.e. away from viewer) |
These orientations do NOT match the suggested orientation given in the kernel IIO documentation, which suggests for a handheld device with the camera at the top of the screen:
| + direction | |
|---|---|
| X | In plane of screen, towards right hand edge of screen |
| Y | In plane of screen, towards front-facing camera |
| Z | Normal to screen, (i.e. towards viewer) |
udev/60-sensor-chuwi.rules defines
ACCEL_MOUNT_MATRIX properties that transform their vectors into the
IIO-recommended orientation while also compensating for the inherent 90
degree rotation of the MiniBook X's physical display. No suggested
orientation exists for a sensor in the keyboard, but using the same
ACCEL_MOUNT_MATRIX as the display is convenient, since this maintains the
property of the vectors being equal when in tablet mode. This information
should really be in the hwdb, but hwdb entries are keyed off of modalias
values, which in our case is the same for both sensors, so dedicated rules
based on device path and name are necessary here.
Interestingly both accelerometers on my MiniBook X have a significant, but stable offset (approximately -6 kg/m2) in the Z axis. This impacts the accuracy of the hinge angle measurement, though not unusably so. This has also been observed on another unit, so it is likely a design quirk rather than a fault.
DSDT
The relevant bits from a disassembly of my MiniBook X N100's DSDT are below.
Note the two I2cSerialBusV2 resources for the two accelerometers, and the
LTSM method that updates the HID switch state and generates 'standard'
tablet-mode and laptop-mode events.
The GMTR method looks interesting too - it returns 24 bytes of data (always
PARB on my machine). At a guess, it looks to be two 3x3 matrices of signed
8-bit values (almost certainly orientations of the two sensors?), followed by
6 bytes - either a 3-element vector of 16-bit values applying to both
sensors, or two 3-element vectors of 8-bit values, one for each sensor. The
orientation matrices don't look immediately useful to us, and the purpose of
the following data isn't clear - it doesn't make sense as zero-offset
calibration values.
Device (_SB)
{
/* ... */
Device (ACMK)
{
Name (_ADR, Zero) // _ADR: Address
Name (_HID, "MDA6655") // _HID: Hardware ID
Name (_CID, "MDA6655") // _CID: Compatible ID
Name (_DDN, "Accelerometer with Angle Calculation") // _DDN: DOS Device Name
Name (_UID, One) // _UID: Unique ID
Name (_DEP, Package (0x02) // _DEP: Dependencies
{
^PC00.I2C0,
^PC00.I2C1
})
Method (_CRS, 0, NotSerialized) // _CRS: Current Resource Settings
{
Name (RBUF, ResourceTemplate ()
{
I2cSerialBusV2 (0x0015, ControllerInitiated, 0x00061A80,
AddressingMode7Bit, "\\_SB.PC00.I2C1",
0x00, ResourceConsumer, , Exclusive,
)
I2cSerialBusV2 (0x0015, ControllerInitiated, 0x00061A80,
AddressingMode7Bit, "\\_SB.PC00.I2C0",
0x00, ResourceConsumer, , Exclusive,
)
})
Return (RBUF) /* \_SB_.ACMK._CRS.RBUF */
}
Method (GMTR, 0, Serialized)
{
Name (PARA, Buffer (0x18)
{
/* 0000 */ 0x00, 0xFF, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // ........
/* 0008 */ 0x01, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, // ........
/* 0010 */ 0x00, 0xFF, 0xB9, 0xAF, 0x1E, 0x05, 0x14, 0x10 // ........
})
Name (PARB, Buffer (0x18)
{
/* 0000 */ 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, // ........
/* 0008 */ 0x01, 0x01, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, // ........
/* 0010 */ 0x00, 0xFF, 0xB9, 0xAF, 0x1E, 0x05, 0x14, 0x13 // ........
})
Name (PARC, Buffer (0x18)
{
/* 0000 */ 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, // ........
/* 0008 */ 0xFF, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, // ........
/* 0010 */ 0x00, 0xFF, 0xB9, 0xAF, 0x1E, 0x05, 0x14, 0x10 // ........
})
Name (PARD, Buffer (0x18)
{
/* 0000 */ 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, // ........
/* 0008 */ 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, // ........
/* 0010 */ 0x00, 0xFF, 0xB9, 0xAF, 0x1E, 0x05, 0x14, 0x10 // ........
})
If (Ones)
{
Return (PARB) /* \_SB_.ACMK.GMTR.PARB */
}
Else
{
Local0 = 0x0D
Switch (ToInteger (Local0))
{
Case (0x10)
{
Return (PARC) /* \_SB_.ACMK.GMTR.PARC */
}
Case (0x05)
{
Return (PARD) /* \_SB_.ACMK.GMTR.PARD */
}
Default
{
Return (PARA) /* \_SB_.ACMK.GMTR.PARA */
}
}
}
}
Method (_STA, 0, NotSerialized) // _STA: Status
{
If (Ones)
{
If ((GGIV (0x09080011) == Zero))
{
Return (0x0F)
}
Return (Zero)
}
If (Ones)
{
If (Ones)
{
Return (0x0F)
}
Return (Zero)
}
Return (Zero)
}
Method (PRIM, 0, NotSerialized)
{
Name (RBUF, Buffer (One)
{
0x01 // .
})
Return (RBUF) /* \_SB_.ACMK.PRIM.RBUF */
}
Method (LTSM, 1, NotSerialized)
{
If ((Arg0 == Zero))
{
^^PC00.LPCB.H_EC.KBCD = Zero
PB1E |= 0x08
ADBG ("UPBT(LTSM) Laptop Start")
^^PC00.LPCB.H_EC.UPBT (0x06, One)
Notify (HIDD, 0xCD) // Hardware-Specific
}
Else
{
^^PC00.LPCB.H_EC.KBCD = 0x03
PB1E &= 0xF7
ADBG ("UPBT(LTSM) Slate Start")
^^PC00.LPCB.H_EC.UPBT (0x06, Zero)
Notify (HIDD, 0xCC) // Hardware-Specific
}
}
}
}