The iPhone 4, latest generation iPod touch, and the iPad have a vibrational gyroscope in addition to an accelerometer and a magnetometer. The MicroElectroMechanical (MEMs) gyroscope inside the iPhone 4 and the 4th generation iPod touch is the AGD8 2032, nearly identical to an off-the-shelf STMicroelectronics L3G4200D device The iPad 2 uses an AGD8 2103 sensor, also from STMicroelectronics. These models operate by making use of a plate called a “proof mass” that oscillates when a drive signal is applied to capacitor plates. When the user rotates the phone, the proof mass is displaced in the X, Y and Z directions and an ASIC processor measures the capacitance change of the plates. The capacitance variation is used to detect the angular rate applied to the package.
An accelerometer provides measurement of forces in the X, Y and Z-axes but it cannot measure rotation. On the other hand, since a gyroscope is a rate of change device, you are able to measure the change in rotations around an axis. By using both sensors in combination you can measure the movement of the device in a six degrees-of-freedom inertial system, allowing you to use dead reckoning to find the physical location (and orientation of the device) relative to an initial starting position.
All inertial systems have an inherent drift, so dead reckoning should not be regarded as being stable over the long term.
The arrival of iOS 4 brought with it the new Core Motion framework; this new framework allows your application to receive motion data from both the accelerometer and (on the latest generation of devices) the gyroscope.
There is no support for Core Motion in the iOS Simulator, therefore all testing of your Core Motion related code must be done on the device. The code in this chapter will only work on devices that have a gyroscope, see Chapter 1 for more information.
With the CMMotionManager
class
you can start receiving accelerometer, gyroscope, and combined device
motion events at a regular interval, or you can poll them
periodically:
CMMotionManager *motionManager = [[CMMotionManager alloc] init]; if (!motionManager.isDeviceMotionAvailable) { NSLog(@"Device supports motion capture."); }
Remember to release the manager after you’re done with it:
[motionManager release];
The CMMotionManager
class offers
both the raw accelerometer and gyroscope data separately as well a
combined CMDeviceMotion
object that
encapsulates the processed device motion data from both the accelerometer
and the gyroscope. With this combined motion measurement Core Motion
provides highly accurate measurements of device attitude, the (unbiased)
rotation rate of a device, the direction of gravity on a device, and the
acceleration that the user is giving to a device.
The rotation rate reported by the CMDeviceMotion
object is different than that
reported directly by the gyroscope. Even if the device is sitting flat
on the table the gyro will not read zero. It will read some non-zero
value that differs from device to device and over time due to changes in
things like device temperature. Core Motion actively tracks and removes
this bias from the gyro data.
The CMMotionManager
class
offers two approaches to obtaining motion data. The simplest way is to
pull the motion data. Your application will start an instance of the
manager class and periodically ask for measurements of the combined
device motion:
[motionManager startDeviceMotionUpdates]; CMDeviceMotion *motion = motionManager.deviceMotion;
Although if you are only interested in the raw gyroscope data (or accelerometer data) you can also ask for those directly:
CMGyroData *gyro = motionManager.gyroData; CMAccelerometerData *accel = motionManager.accelerometerData;
This is the most efficient method of obtaining motion data. However, if there isn’t a natural timer in your application—such as a periodic update of your main view—then you may need an additional timer to trigger your update requests. Remember to stop the updates and release the motion manager after you’re done with them:
[motionManager stopDeviceMotionUpdates]; [motionManager release];
Your application should create only a single instance of the
CMMotionManager
class. Multiple
instances of this class can affect the rate at which an application
receives data from the accelerometer and gyroscope.
Instead of using this simple pull methodology, you can specify an
update interval and implement a block of code for handling the motion
data. The manager class can then be asked to deliver updates using the
NSOperationsQueue
, which allows the
handler to push the measurements to the application. For example:
motionManager.deviceMotionUpdateInterval = 1.0/60.0; [motionManager startDeviceMotionUpdatesToQueue: queue withHandler: handler];
or similarly for the individual accelerometer and gyroscope data:
[motionManager startAccelerometerUpdatesToQueue:queue withHandler: handler]; [motionManager startGyroUpdatesToQueue:queue withHandler:handler];
With this second methodology you’ll get a continuous stream of motion data, but there is a large increased overhead associated with implementing it (see Table 6-1). Your application may not be able to keep up with the associated data rate especially if the device is in rapid motion.
At 100Hz | At 20Hz | |||
Total | Application | Total | Application | |
DeviceMotion | 65% | 20% | 65% | 10% |
Accelerometer | 50% | 15% | 46% | 5% |
Accel + Gyro | 51% | 10% | 50% | 5% |
[a] Figures for an application running on an iPhone 4 running iOS 4.0 (Reproduced with permission. Credit: Jeffrey Powers, Occipital) |
Using Core Motion’s combined CMDeviceMotion
object, as opposed to accessing
the raw CMAccelerometer
or CMGyroData
objects, consumes roughly 15% more
total CPU regardless of the update rate. The good news is that is not
because of the gyroscope itself; reading both the accelerometer and
gyroscope directly is not noticeably slower than reading the
accelerometer on its own.
Because of this associated CPU overheads push is really only recommended for data collection applications where the point of the application is to obtain the motion data itself. However if your application needs to be rapidly updated as to device motion you can do this easily:
CMMotionManager *motionManager = [[CMMotionManager alloc] init];
motionManager.deviceMotionUpdateInterval = 1.0/60.0;
if (motionManager.deviceMotionAvailable ) {
queue = [[NSOperationQueue currentQueue] retain];
[motionManager startDeviceMotionUpdatesToQueue:queue
withHandler:^ (CMDeviceMotion *motionData, NSError *error) {
CMAttitude *attitude = motionData.attitude;
CMAcceleration gravity = motionData.gravity;
CMAcceleration userAcceleration = motionData.userAcceleration;
CMRotationRate rotate = motionData.rotationRate;
// handle data here......
}];
} else {
[motionManager release];
}
If we were interested solely in the raw gyroscope data we could do the following:
CMMotionManager *motionManager = [[CMMotionManager alloc] init];
motionManager.gyroUpdateInterval = 1.0/60.0;
if (motionManager.gyroAvailable) {
queue = [[NSOperationQueue currentQueue] retain];
[motionManager startGyroUpdatesToQueue:queue
withHandler: ^ (CMGyroData *gyroData, NSError *error) {
CMRotationRate rotate = gyroData.rotationRate;
NSLog(@"rotate x = %f, y = %f, z = %f", rotate.x, rotate.y, rotate.z);
// handle rotation-rate data here......
}];
} else {
[motionManager release];
}
If we want both the raw and gyroscope and accelerometer readings
outside of the CMDeviceMotion
object,
we could modify the above code as highlighted:
CMMotionManager *motionManager = [[CMMotionManager alloc] init]; motionManager.gyroUpdateInterval = 1.0/60.0; motionManager.accelerometerUpdateInterval = 1.0/60.0; if (motionManager.gyroAvailable && motionManager.accelerometerAvailable) { queue = [[NSOperationQueue currentQueue] retain]; [motionManager startGyroUpdatesToQueue:queue withHandler: ^ (CMGyroData *gyroData, NSError *error) { CMRotationRate rotate = gyroData.rotationRate; NSLog(@"rotate x = %f, y = %f, z = %f", rotate.x, rotate.y, rotate.z);// handle rotation-rate data here......
}]; [motionManager startAccelerometerUpdatesToQueue:queue withHandler: ^ (CMAccelerometerData *accelData, NSError *error) { CMAcceleration accel = accelData.acceleration; NSLog( @"accel x = %f, y = %f, z = %f", accel.x, accel.y, accel.z);// handle acceleration data here......
}]; } else { [motionManager release]; }
Let’s go ahead and implement a simple view-based application to
illustrate how to use the gyroscope on its own before looking again at
Core Motion and CMDeviceMotion
. Open
Xcode and start a new View-based Application iPhone project and name it
“Gyroscope” when prompted for a filename.
Since we’ll be making use of the Core Motion framework, the first thing we need to do is add it to our new project. Click on the project file at the top of the Project navigator window on the right in Xcode, select the Target and click on the Build Phases tab, click on the Link with Libraries drop down and click on the + button to open the file pop-up window. Select CoreMotion.framework from the list of available frameworks and click the Add button.
Now go ahead and click on the GyroscopeViewController.xib file to open it in
Interface Builder. As you did for the accelerometer back in Chapter 4, you’re going to build a simple
interface to report the raw gyroscope readings. Go ahead and drag and drop
three UIProgressView
from the Object
Library into the View window, then add two UILabel
elements for each progress bar: one to
hold the X, Y, or Z label and the other to hold the rotation measurements.
After you do that, the view should look something a lot like Figure 6-1.
Go ahead and close the Utilities panel and click to open the
Assistant Editor. Then Control-Click and drag from the three UIProgressView
elements, and the three UILabel
elements that will hold the measured
values, to the GyroscopeViewController.h header file which
should be displayed in the Assistant Editor on the right-hand side of the
interface (see Figure 6-2).
This will automatically create and declare three UILabel
and three UIProgressView
variables as an IBOutlet
. Since they aren’t going to be used
outside the class, there isn’t much point in declaring them as class
properties, which you’d do with a Control-click and drag from the element
to outside the curly brace. After doing this, the code should look like
this:
#import <UIKit/UIKit.h> @interface GyroscopeViewController : UIViewController { IBOutlet UIProgressView *xBar; IBOutlet UIProgressView *yBar; IBOutlet UIProgressView *zBar; IBOutlet UILabel *xLabel; IBOutlet UILabel *yLabel; IBOutlet UILabel *zLabel; } @end
Close the Assistant Editor, return to the Standard Editor and click
on the GyroscopeViewController.h
interface file. Go ahead and import the Core Motion header file, and
declare a CMMotionManager
and NSOperationQueue
instance variables. Here’s how
the should look when you are done:
#import <UIKit/UIKit.h> #import <CoreMotion/CoreMotion.h> @interface GyroscopeViewController : UIViewController { IBOutlet UIProgressView *xBar; IBOutlet UIProgressView *yBar; IBOutlet UIProgressView *zBar; IBOutlet UILabel *xLabel; IBOutlet UILabel *yLabel; IBOutlet UILabel *zLabel; CMMotionManager *motionManager; NSOperationQueue *queue; } @end
Make sure you’ve saved your changes and click on the corresponding GyroscopeViewController.m implementation file to open it in the Xcode editor. You don’t actually have to do very much here, as Interface Builder handled most of the heavy lifting with respect to the UI, you just need to go ahead an implement the guts of the application to monitor the gyroscope updates:
@implementation GyroscopeViewController - (void)dealloc { [xBar release]; [yBar release]; [zBar release]; [xLabel release]; [yLabel release]; [zLabel release]; [queue release]; [super dealloc]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } #pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; motionManager = [[CMMotionManager alloc] init]; motionManager.gyroUpdateInterval = 1.0/2.0; // Update every 1/2 second. if (motionManager.gyroAvailable) { NSLog(@"Gyroscope avaliable"); queue = [[NSOperationQueue currentQueue] retain]; [motionManager startGyroUpdatesToQueue:queue withHandler: ^ (CMGyroData *gyroData, NSError *error) { CMRotationRate rotate = gyroData.rotationRate; xLabel.text = [NSString stringWithFormat:@"%f", rotate.x]; xBar.progress = ABS(rotate.x); yLabel.text = [NSString stringWithFormat:@"%f", rotate.y]; yBar.progress = ABS(rotate.y); zLabel.text = [NSString stringWithFormat:@"%f", rotate.z]; zBar.progress = ABS(rotate.z); }]; } else { NSLog(@"Gyroscope not available"); [motionManager release]; } } - (void)viewDidUnload { [motionManager stopGyroUpdates]; [motionManager release]; [xBar release]; xBar = nil; [yBar release]; yBar = nil; [zBar release]; zBar = nil; [xLabel release]; xLabel = nil; [yLabel release]; yLabel = nil; [zLabel release]; zLabel = nil; [super viewDidUnload]; } - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return (interfaceOrientation == UIInterfaceOrientationPortrait); } @end
The CMRotationRate data structure provides the rate of rotations around X-, Y-, and Z-axes in units of radians per second. When inspecting the structure remember the right-hand rule to determine the direction of positive rotation. With your thumb in the positive direction on an axis, your fingers curl will give the positive rotation direction around that axis (see Figure 6-3). A negative rotation goes away from the tips of those fingers.
Let’s test that: click on the Run button in the Xcode toolbar to build and deploy the application to your device (remember you can’t test this code in the iOS Simulator). If all goes well you should see something much like Figure 6-4 as you roll the device around the Y-axis.
As mentioned before the measurement of rotation rate encapsulated
by a CMGyroData
object is biased by
various factors. You can obtain a much more accurate (unbiased)
measurement by accessing the rotationRate
property of CMDeviceMotion
if that is needed by your application.
Let’s go ahead and build a similar application to the one above, but
this time reporting the data exposed by the CMDeviceMotion
object:
CMAttitude *attitude = motionData.attitude; CMAcceleration gravity = motionData.gravity; CMAcceleration userAcceleration = motionData.userAcceleration; CMRotationRate rotate = motionData.rotationRate;
Open Xcode and start a new iPhone project, select a View-based
Application template, and name the project “Motion” when prompted for a
filename. As before, import the Core Motion framework into the project and
then click on the MotionViewController.xib file to open it in
Interface Builder, and then proceed to drag-and-drop UIProgressBar
and UILabel
elements into your View in a similar
manner as you did for the Gyroscope application earlier in the chapter.
You’ll need labels for the yaw, pitch and roll values, along with progress
bars and labels for the user acceleration, gravity and rotation
values.
Once you’ve done this, go ahead and connect the various bars and
labels as IBOutlet
using the Assistant
Editor into the MotionViewController.h interface file as in
Figure 6-5.
Once you’ve done this, save your changes and open up the MotionViewController.h interface file in the Standard Editor. Go ahead and import the Core Motion framework:
#import <CoreMotion/CoreMotion.h>
For this example you’re going to pull the device motion updates
rather than push them using the NSOperationQueue
and a handler block. Add the
following instance variables to the view controller class:
CMMotionManager *motionManager; NSTimer *timer;
Then in the corresponding MotionViewController.m implementation file,
modify the viewDidLoad
method as
follows:
- (void)viewDidLoad { [super viewDidLoad]; motionManager = [[CMMotionManager alloc] init]; motionManager.deviceMotionUpdateInterval = 1.0 / 10.0; [motionManager startDeviceMotionUpdates]; if (motionManager.deviceMotionAvailable ) { timer = [NSTimer scheduledTimerWithTimeInterval:0.2f target:self selector:@selector(updateView:) userInfo:nil repeats:YES]; } else { [motionManager stopDeviceMotionUpdates]; [motionManager release]; } }
This will start the motion manager and begin polling for device
motion updates. Add the following lines to the viewDidUnload
method to corresponding stop the
timer and updates:
[timer invalidate]; [motionManager stopDeviceMotionUpdates]; [motionManager release];
Once you have done this you should go ahead and implement the
updateView:
method that will be called
by the NSTimer
object:
-(void) updateView:(NSTimer *)timer { CMDeviceMotion *motionData = motionManager.deviceMotion; CMAttitude *attitude = motionData.attitude; CMAcceleration gravity = motionData.gravity; CMAcceleration userAcceleration = motionData.userAcceleration; CMRotationRate rotate = motionData.rotationRate; yawLabel.text = [NSString stringWithFormat:@"%2.2f", attitude.yaw]; pitchLabel.text = [NSString stringWithFormat:@"%2.2f", attitude.pitch]; rollLabel.text = [NSString stringWithFormat:@"%2.2f", attitude.roll]; accelIndicatorX.progress = ABS(userAcceleration.x); accelIndicatorY.progress = ABS(userAcceleration.y); accelIndicatorZ.progress = ABS(userAcceleration.z); accelLabelX.text = [NSString stringWithFormat:@"%2.2f",userAcceleration.x]; accelLabelY.text = [NSString stringWithFormat:@"%2.2f",userAcceleration.y]; accelLabelZ.text = [NSString stringWithFormat:@"%2.2f",userAcceleration.z]; gravityIndicatorX.progress = ABS(gravity.x); gravityIndicatorY.progress = ABS(gravity.y); gravityIndicatorZ.progress = ABS(gravity.z); gravityLabelX.text = [NSString stringWithFormat:@"%2.2f",gravity.x]; gravityLabelY.text = [NSString stringWithFormat:@"%2.2f",gravity.y]; gravityLabelZ.text = [NSString stringWithFormat:@"%2.2f",gravity.z]; rotIndicatorX.progress = ABS(rotate.x); rotIndicatorY.progress = ABS(rotate.y); rotIndicatorZ.progress = ABS(rotate.z); rotLabelX.text = [NSString stringWithFormat:@"%2.2f",rotate.x]; rotLabelY.text = [NSString stringWithFormat:@"%2.2f",rotate.y]; rotLabelZ.text = [NSString stringWithFormat:@"%2.2f",rotate.z]; }
Save your changes and hit the Run button in the Xcode toolbar to build and deploy the application to your device. If all goes well you should see something much like Figure 6-6.
At this stage we can illustrate the difference between gravity and
user-contributed acceleration values reported by Core Motion to the raw
acceleration values reported by the UIAccelerometer
, discussed back in Chapter 4.
Re-open the MotionViewController.xib file in Interface
Builder and add another section to the UI, which will report the raw
readings from the UIAccelerometer
object. Go ahead and connect these three bars to IBOutlet instance
variables in the MotionViewController.h interface file using
the Assistant Editor as before, see Figure 6-7.
As you can see from Figure 6-7, I’ve changed the
UIProgressView
style from “Default”
to “Bar” using the Attributes inspector in the Utility pane. This will
help differentiate this section—data reported from the UIAccelerometer
—from the other sections whose
values are reported by the CMMotionManager
.
Once that is done, close the Assistant Editor and open the
MotionViewController.h interface
file using the Standard Editor. Go ahead and declare the view controller
as a UIAccelerometerDelegate
and add
a UIAccelerometer
instance variable
as shown here:
@interface MotionViewController : UIViewController <UIAccelerometerDelegate> {
...
UIAccelerometer *accelerometer;
}
@end
before opening the corresponding implementation file. In the
viewDidLoad
method, add the following
code to initialize the UIAccelerometer
object. You should see Chapter 4 for more details on the UIAccelerometer
class and associated
methods:
- (void)viewDidLoad { [super viewDidLoad]; motionManager = [[CMMotionManager alloc] init]; motionManager.deviceMotionUpdateInterval = 1.0 / 10.0; [motionManager startDeviceMotionUpdates]; if (motionManager.deviceMotionAvailable ) { timer = [NSTimer scheduledTimerWithTimeInterval:0.2f target:self selector:@selector(updateView:) userInfo:nil repeats:YES]; } else { [motionManager stopDeviceMotionUpdates]; [motionManager release]; } accelerometer = [UIAccelerometer sharedAccelerometer]; accelerometer.updateInterval = 0.2f; accelerometer.delegate = self; }
After doing this all you need to do is add the accelerometer:didAccelerate:
delegate
method:
- (void)accelerometer:(UIAccelerometer *)meter didAccelerate:(UIAcceleration *)acceleration { rawAccelLabelX.text = [NSString stringWithFormat:@"%2.2f", acceleration.x]; rawAccelIndicatorX.progress = ABS(acceleration.x); rawAccelLabelY.text = [NSString stringWithFormat:@"%2.2f", acceleration.y]; rawAccelIndicatorY.progress = ABS(acceleration.y); rawAccelLabelZ.text = [NSString stringWithFormat:@"%2.2f", acceleration.z]; rawAccelIndicatorZ.progress = ABS(acceleration.z); }
Click on the Run button in the Xcode toolbar to build and deploy the application to the device as before. If all goes well you should see something much like Figure 6-8.
Move the device around and you can see how the raw accelerometer values and the derived gravity and user acceleration values correspond to each other.