The ThinkPad APS Accelerometer Interface
In late 2003, IBM announced a feature called the Active Protection
System (APS) in some of its line of ThinkPads. This feature is used
to "park" the hard disk head when sudden motion or
acceleration (such as being dropped) is detected. A parked head has
much less chance of damaging data than if the head is over sectors
containing data when impact occurs. The system proved very valuable
and other vendors followed suit. In early 2005, Apple announced their
Sudden Motion
Sensor system as an addition to their PowerBook line.
All of these systems use accelerometers placed on the motherboard
to detect motion. A kernel software driver interfaces with the
accelerometer and exposes its acceleration readings to interested
user-space programs. The Mac OS X system is described in detail by
Amit Singh in the previously referenced article. The Windows system
on ThinkPads has not been described in detail, and there was no port
for Linux.
I was interested in interfacing with the accelerometer from Linux,
mostly for the sake of curiosity, but I also wanted to show Amit that
his TT can't hold corners as well as my Corolla (and if I could prove
it to him using Linux, so much the better). Any such port was
certainly to begin with a thorough understanding of the Windows APS.
Fortunately, I know another Windows kernel hacker here at Almaden,
Anurag Sharma. We started a kernel
debugging session over serial cable using Windbg. We discovered that
APS is comprised of two drivers, shockprf.sys and shockmgr.sys.
shockprf.sys interfaces with the accelerometer hardware, while
shockmgr.sys subscribes to the data from shockprf.sys and must be the
component used to initiate parking of the drive head. Since we were
primarily interested in the protocol used to get information out of
the accelerometer, our debugging focused on shockprf.sys.
We discovered that shockprf.sys was performing port I/O in the
range 0x1600 to 0x162f. We looked at the values that shockmgr.sys was
reading out of shockprf.sys and watched for those values to come in
through shockprf.sys' port reads. We discovered a pattern of port
writes and reads which repeatedly produced the values that
shockmgr.sys was interested in. Armed with the read/write sequence
that the accelerometer responded to from Windows, the task was now to
produce a Linux driver which did the same thing.
Since the amount of data that the accelerometer produces is very
small, I decided the best way to expose it in Linux was through the
/proc filesystem. I wrote a quick kernel module. Behold! Acceleration
values! I performed a hard boot to make sure that everything would
still work, and unfortunately, it did not. There was a bit of
initialization port I/O that we missed. More protocol analysis from
the Windows side produced our initialization sequence. After adding
that code to the Linux driver, all was well.
I could finally prove to Amit that his TT was slow. We strapped
the ThinkPad into the passenger seats of both of our cars and drove
up our "closed
course." He destroyed me. Although I refuse to give Amit the pleasure of
my posting our comparative graphs, I will post the noteworthy performance
of Anu's new 350Z.
As far as I could tell, the accelerometer in the ThinkPad has a 5
microsecond refresh rate. This is fast enough for most real-world
applications. Although the lateral G values are probably slightly
exaggerated due to body roll, it is interesting to note the relative
detail in the graph. At time 520, there's a stop sign. One can
observe Anu's acceleration become negative (breaking) approaching the
stop sign, and then become sharply positive (forward acceleration).
One can also see him shift at times 580 and 620. One can also deduce
rough gearbox ratios between 1st, 2nd, and 3rd gears based on his
acceleration values in each gear. Whether Anu accelerates through
corners is also evident. Further, if one was interested in Anu's
speed, he would only need to integrate the green curve (assuming a
flat course, uphill courses will give slightly higher forward
acceleration values than actually achieved, assuming calibration
on flat ground). Unfortunately, the
ThinkPad accelerometers seem only to expose "X" and "Y"
acceleration values. Unlike the PowerBook, "Z" values (or
"up" and "down" acceleration) are not available.
Kernel Module
I am not permitted to release my source code yet, but I am working
to make it available. However, I am permitted to describe how it
works. Below is a detailed description of how my Linux kernel module
communicates with the ThinkPad accelerometer.
My driver is written as four files:
One containing acceleration common
functions
One containing proc functionality
One containing thread functions
for asynchronous reading
One containing kernel module initialization and entry points
They rely on a packed accelerometer_data structure which contains:
an unsigned char representing the
accelerometer state
an unsigned short representing the
X acceleration value
an unsigned short representing the
Y acceleration value
an unsigned char representing the
temperature of the accelerometer in Celsius.
an unsigned short representing
some variation (maybe a weighted average of the previous n
readings?) of the X acceleration value
an unsigned short representing
some variation (maybe a weighted average of the previous n
readings?) of the Y acceleration value
an unsigned char representing the
temperature of the accelerometer in Celsius (again?).
an unsigned char of unknown use.
an unsigned char indicating whether the mouse and keyboard
are in use.
Understanding what I mean by a latch is also important for
understanding this logic. In this context, a latch is simply an I/O
port and a value. To “wait for a latch” is to wait for an
inb from that port to produce a specified value by repeatedly
performing inb's and checking the value. To check a latch is simply
to do a single inb from the port and to report whether the value read
from that port is the same as a specified value. The accelerometer
uses these “latches” for synchronization: reporting data
ready, and handshaking.
My acceleration common functions file contains functions which
perform the following:
A static function which checks
a port latch for a certain value.
This function simply takes
an unsigned short port value and an unsigned char compare value and
returns whether the inb from the specified port (bitwise anded with
0xff) yields the specified value. No waiting is done here.
A static function which waits
for a latch to have a certain value
This function also takes
an unsigned short port value and an unsigned char compare value. It
loops over check_latch 10 times, udelaying 5 microseconds each time,
waiting for the check to become true. (A latch seems like it never
takes more than 50 microseconds to become set). I return whether the
latch value becomes the specified value within the allotted time.
A static function which checks
the refresh state of the accelerometer data
This simple
issues a latch check of port 0x1604 for value 0x50
A static function which issues
a refresh request to the accelerometer.
This function takes
a single argument which tells it whether to refresh synchronously or
not. This function first issues a refresh state check, if it is
already refreshed, it returns success. If not, it does an outb at
port 0x1610 of value 0x11, and then an outb at port 0x161f of value
0x1. If the synchronous flag is set, it does a latch wait on port
0x1604 for value 0x50. The function then returns whether the latch
wait was successful.
A non-static function which reads
the accelerometer data.
This function takes an
accelerometer_data structure and returns whether the read was
successful. It issues a synchronous accelerometer data refresh
request (which is likely to return immediately since the refresh
latch is likely already set). If this refresh fails, this function
returns the failure. Otherwise, it reads ports 0x1611 through
0x161E, assigning the 13 bytes worth of values to the 13 bytes large
accelerometer_data structure. It then tells the accelerometer that
it is done reading the data. It then issues an asynchronous refresh
request and exits successfully.
A static function which tells the
accelerometer that it is done reading the data
This function
does two port reads. One art port 0x161f, and the next at port
0x1604, discarding the values it gets.
A non-static function which initializes the accelerometer.
This lengthy function takes a timeout value (in seconds) as the
maximum time that it should take to try to initialize itself. I
initialize a "seconds waited so far" variable to zero. I
issue an outb at port 0x1610 of value 0x13, followed by an outb at
port 0x161f of value 0x01. I then wait for latch 0x161f for value
0x0, and then wait for latch 0x1611 for value 0x3. Three more outbs
at ports 0x1610, 0x1611, and 0x161f of values 0x17, 0x81, and 0x01,
respectively follow. Four more waits for latches 0x161f, 0x1611,
0x1612, and 0x1613 for values 0x0, 0x0, 0x60, and 0x0 respectively
follow. Then three more outbs at ports 0x1610, 0x1611, and 0x161f of
values 0x14, 0x01, and 0x01 respectively follow. Then I wait for
latch 0x161f for value 0x0. Then five outbs at ports 0x1610, 0x1611,
0x1612, 0x1613, and 0x161f of values 0x10, 0xc8, 0x00, 0x02, and
0x01 respectively follow. I then wait for latch 0x161f for value 0x0
again. I then issues a synchronous refresh of the accelerometer
data, and wait for latch 0x1611 to become 0x0. The next part is a
little bit tricky because it can take a long time for the
accelerometer to complete. I loop forever until latch 0x1611 becomes
0x02. Inside this loop, I check the timeout value against the "time
waited so far" variable. If the function has taken too long, we
return failure. Otherwise, I call the function which reads the
accelerometer data and I throw away the (probably garbage) data
(this read somehow seems to kick the accelerometer into being
initialized). I set_current_state to TASK_INTERRUPTIBLE, and
schedule_timeout for HZ. I then increment our "seconds waited
so far" variable and continue the loop. If the loop ever exits
successfully, the function returns success. A good value to pass for
the initialize timeout value is 10 seconds.
My acceleration proc functions file only implements open, release,
and read.
open simply starts the read thread
(if it is not already started) in the acceleration thread functions
file.
release stops the aforementioned
read thread.
read simply spits out the last acceleration_data structure
that was read by the read thread. (Of course, this structure is
protected by a semaphore).
My acceleration thread functions file implements a function which
is constantly reading.
The read thread first daemonizes, then loops over a volatile
variable which is set the the proc release function which tells this
thread to stop. It then issues a read of the accelerometer data (of
course, protected by a semaphore). It then set_current_state's to
TASK_INTERRUPTABLE, and schedule_timeout's to a value passed in as a
module parameter (I made the refresh rate settable at insmod time),
or an appropriate default value.
My acceleration kernel module initialization and entry points file
contains the regular kernel module init stuff.
The init function requests region
for the specified port range, inits the accelerometer, does an
initial read, initializes semaphores, create_proc_entry's, and sets
the entry's proc_fops to a file_operations structure with .open,
.read and .release initialized.
The exit function remove_proc_entry's, makes sure the read
thread is stopped, and releases region.
Simple DirectMedia Layer Modification
Simple modifications to the SDL can enable
any game which uses SDL to use the accelerometer as input for games.
A full discussion of this functionality can be found here.
Updates
July 2005 I'm pleased to see that this has been helpful to the Linux community. A sourceforge project based on this document has produced a working driver.
October 2005 In the linux kernel! As of kernel version 2.6.14, drivers/hwmon/hdaps.c will be compiled when SENSORS_HDAPS is on.