Accessing the Bluetooth low energy Accelerometer on the TI SensorTag
Accessing the Bluetooth low energy Accelerometer on the TI SensorTag
Accessing the Bluetooth low energy Accelerometer on the TI SensorTag
Bluetooth low energy offers a great way for iPhone and iPad apps to connect to external devices. Bluetooth low energy is free of the shackles imposed by Apple's MFi program, so anyone can write apps to connect to Bluetooth low energy devices. This blog explores how to connect to the accelerometer on the new TI SensorTag, a Bluetooth low energy peripheral designed to be a hardware and software reference program for the CC2541 chip.
The SensorTag supports a total of six sensors, including a thermometer, barometer, humidity meter, magnetometer and gyroscope. Those sensors are covered in companion blogs. And, just like with Lord of the Rings, there's one blog to control them all. See the blog Controlling the TI SensorTag with techBASIC for an overview, as well as a complete GUI program that shows the readings on all six sensors. It covers the basics of connecting to a Bluetooth low energy device.
While this blog is completely independent of the others, it does assume that you already have some idea how to connect to Bluetooth low energy devices. The basic connection code is presented without further comment. The complete program is a short, simple text-based program that makes it very easy to experiment with the accelerometer. It's presented at the end of the blog. Controlling the TI SensorTag with techBASIC has a more complete program that accesses all six sensors and presents the results in a GUI environment.
What's an Accelerometer?
An accelerometer measures acceleration by detecting the force exerted on a mass, then uses the equation
to find the acceleration. The exact way the force and mass are measured varies a lot from one brand of sensor to another, from measuring the capacitance as the plates making up the capacitor are deformed to looking at the voltage generated by a crystal as it is deformed. The SensorTag uses the KXTJ9 accelerometer from Kionox. It's a three-axis accelerometer with a selectable range of ±2G, ±4G or ±8G. The SensorTag's firmware uses the ±2G setting, exposing 8 bits of the 14-bit resolution of the chip, giving a precision of 0.015625G. If you saw the blog showing the iPhone on a rocket, you know TI has also prepared a special version of the firmware that supports the ±8G range, for a precision of 0.0625G.
Accessing the Accelerometer
Looking at the Generic Attribute Profile (GATT) for the accelerometer, we find three attributes in the accelerometer service.
Like all sensors, the accelerometer draws power. Keeping it off when not in use helps prolong the life of the battery. The 0xAA12 attribute is used to turn the accelerometer on and off. Our simple program just turns it on.
CASE "F000AA12" & servicesHeader
DIM value(1) as INTEGER
value(1) = 1
peripheral.writeCharacteristic(characteristics(i), value, 1)
Pass a 0 instead of a 1 to turn the accelerometer back off.
0xAA11 is used to read the accelerometer. There are two ways to read the acceleration, either reading it when the program wants an acceleration value or asking the accelerometer to notify the program whenever a new sensor reading is available. The first step is to tell the accelerometer which way we want to read values. To receive notifications when a value is available, write a 1 and 0 to the characteristic, then ask it to start sending notifications.
SELECT CASE characteristics(i).uuid
CASE "F000AA11" & servicesHeader
DIM value(2) as INTEGER
value = [0, 1]
peripheral.writeCharacteristic(characteristics(i), value, 0)
peripheral.setNotify(characteristics(i), 1)
Use readCharacteristic to read a single value.
peripheral.readCharacteristic(characteristics(i), 1)
The accelerometer sends samples about once a second unless you tell it otherwise. Use 0xAA13 to set the sample rate. The value is expressed in tens of milliseconds, so passing 100 gives the default sample rate of once per second. Keep in mind that the hardware uses the sample rate as a suggestion, not a firm value! If the time between samples is really important, record the time with the sample value.
Whether you read the data once using readCharacteristic, or ask for notifications with setNotify, the values are always returned using a call to BLECharacteristicInfo.
SUB BLECharacteristicInfo (time AS DOUBLE, peripheral AS BLEPeripheral, characteristic AS BLECharacteristic, kind AS INTEGER, message AS STRING, err AS LONG)
IF kind = 2 THEN
DIM value(1) AS INTEGER
value = characteristic.value
SELECT CASE characteristic.uuid
CASE "F000AA11" & servicesHeader
! Update the accelerometer.
c = 64.0
p% = value(1)
IF p% BITAND $0080 THEN p% = p% BITOR $FF00
x = p%/c
p% = value(2)
IF p% BITAND $0080 THEN p% = p% BITOR $FF00
y = p%/c
p% = value(3)
IF p% BITAND $0080 THEN p% = p% BITOR $FF00
z = p%/c
PRINT x, y, z
CASE ELSE
PRINT "Read from "; characteristic.uuid
END SELECT
ELSE IF kind = 3 AND err <> 0 THEN
PRINT "Error writing "; characteristic.uuid; ": ("; err; ") "; message
END IF
END SUB
The value itself is returned as three bytes, where each byte is a signed value containing the acceleration along one axis. Naturally enough, the order is x, y and z. Dividing by 64 converts from the range of the byte value, with a range of -128 to 127, to G force, with a range of about -2G to 2G.
Using the Accelerometer
The first step in using any sensor is to calibrate it. There are differences in individual sensors, and converting from a byte value to ±2G by dividing by 64 isn't perfect. After all, we get 128 values to the left of zero, and 127 on the positive side—it can't be exact.
Fortunately, we live on a pretty good calibration device. The surface of the Earth is, by definition, 1G. Lay the accelerometer on the table and run the program. The device may not be perfectly flat, but you can get the overall acceleration this way:
or, in BASIC,
a = SQR(x*x + y*y + z*z)
If you like, collect a few hundred measurements and average the results. They should be 1G. Are they? If not, adjust all of your measurements by an appropriate factor so they are.
If you're really up on your physics, you may realize that the Earth's gravity is not the same strength everywhere. It varies because the Earth is not perfectly round, because it spins, and because of mineral deposits. This variation is smaller than 1%, though, and the precision of the accelerometer is about 1.5% of a gravity, so it's an error we can safely ignore.
Many people are familiar with one of the basic formulas of physics that allows us to calculate the distance something has traveled based on the time and acceleration. Here it is.
To do anything useful, we also need to remember that
The accelerometer gives us acceleration. To keep things simple, let's assume we get back one sample every 0.1 second from sliding the SensorTag across a table, and the samples are 0.1G, 0.2G and 0.3G along the direction we slide the SensorTag. Using this equation, and starting at rest, after one tenth of a second
The factor of 9.81 converts from gravities to meters per second squared.
Continuing on for the next two measurements,
After 0.3 seconds, the SensorTag has traveled about 2.5cm (almost an inch) across the table, and is traveling at a little over a half meter per second.
So now you're equipped to tape a SensorTag to any moving object and track where it is and how fast it's going, right? Ah, if life were only that simple. It works fine in theory, but in practice, an accelerometer is not a good way to track movement. The problem is the errors that crop in. There are two kinds. One is random error, where the measurements are off by a bit, but vary around the correct reading. The other is systematic error, where the values are off in a specific direction. Real measurements have both, and they add up fast. The longer you take measurements, the more error will accumulate. While I personally find the whole subject of error analysis fascinating, that's not what this blog is about, and you may not share my fascination. If you'd like to know more about these errors, start with web searches on systematic error, random error and error analysis. The short version, though, is that calculating velocity and distance using an accelerometer is going to work a lot better for short, rapid movements like a rocket flight than for long, slow ones like tracking the progress of a robot. Error is still an issue for short, rapid movements; too, it's just not as big an issue.
The Source
Here's the source for the accelerometer program. It's pretty simple, writing the values to the console. This is a great program to experiment a bit with the accelerometer. For another, see the source code for the main article, Controlling the TI SensorTag with techBASIC. That article displays all six SensorTag sensors in a nice GUI environment.
! Simple program to access the accelerometer on the TI SensorTag.
! Set up variables to hold the peripheral and the characteristics
! for the battery and buzzer.
DIM sensorTag AS BLEPeripheral
! We will look for these services.
DIM servicesHeader AS STRING, services(1) AS STRING
servicesHeader = "-0451-4000-B000-000000000000"
services(1) = "F000AA10" & servicesHeader : ! Accelerometer
accel% = 1
! Start the BLE service and begin scanning for devices.
debug = 0
BLE.startBLE
DIM uuid(0) AS STRING
BLE.startScan(uuid)
! Called when a peripheral is found. If it is a Sensor Tag, we
! initiate a connection to it and stop scanning for peripherals.
!
! Parameters:
! time - The time when the peripheral was discovered.
! peripheral - The peripheral that was discovered.
! services - List of services offered by the device.
! advertisements - Advertisements (information provided by the
! device without the need to read a service/characteristic)
! rssi - Received Signal Strength Indicator
!
SUB BLEDiscoveredPeripheral (time AS DOUBLE, peripheral AS BLEPeripheral, services() AS STRING, advertisements(,) AS STRING, rssi)
IF peripheral.bleName = "TI BLE Sensor Tag" THEN
sensorTag = peripheral
BLE.connect(sensorTag)
BLE.stopScan
END IF
END SUB
! Called to report information about the connection status of the
! peripheral or to report that services have been discovered.
!
! Parameters:
! time - The time when the information was received.
! peripheral - The peripheral.
! kind - The kind of call. One of
! 1 - Connection completed
! 2 - Connection failed
! 3 - Connection lost
! 4 - Services discovered
! message - For errors, a human-readable error message.
! err - If there was an error, the Apple error number. If there
! was no error, this value is 0.
!
SUB BLEPeripheralInfo (time AS DOUBLE, peripheral AS BLEPeripheral, kind AS INTEGER, message AS STRING, err AS LONG)
IF kind = 1 THEN
! The connection was established. Look for available services.
IF debug THEN PRINT "Connection made."
peripheral.discoverServices(uuid)
ELSE IF kind = 2 OR kind = 3 THEN
IF debug THEN PRINT "Connection lost: "; kind
BLE.connect(sensorTag)
ELSE IF kind = 4 THEN
! Services were found. If it is one of the ones we are interested
! in, begin discovery of its characteristics.
DIM availableServices(1) AS BLEService
availableServices = peripheral.services
FOR s = 1 to UBOUND(services, 1)
FOR a = 1 TO UBOUND(availableServices, 1)
IF services(s) = availableServices(a).uuid THEN
IF debug THEN PRINT "Discovering characteristics for "; services(s)
peripheral.discoverCharacteristics(uuid, availableServices(a))
END IF
NEXT
NEXT
END IF
END SUB
! Called to report information about a characteristic or included
! services for a service. If it is one we are interested in, start
! handling it.
!
! Parameters:
! time - The time when the information was received.
! peripheral - The peripheral.
! service - The service whose characteristic or included
! service was found.
! kind - The kind of call. One of
! 1 - Characteristics found
! 2 - Included services found
! message - For errors, a human-readable error message.
! err - If there was an error, the Apple error number. If there
! was no error, this value is 0.
!
SUB BLEServiceInfo (time AS DOUBLE, peripheral AS BLEPeripheral, service AS BLEService, kind AS INTEGER, message AS STRING, err AS LONG)
IF kind = 1 THEN
! Get the characteristics.
DIM characteristics(1) AS BLECharacteristic
characteristics = service.characteristics
FOR i = 1 TO UBOUND(characteristics, 1)
IF service.uuid = services(accel%) THEN
! Found the accelerometer.
SELECT CASE characteristics(i).uuid
CASE "F000AA11" & servicesHeader
! Tell the accelerometer to begin sending data.
IF debug THEN PRINT "Start accelerometer."
DIM value(2) as INTEGER
value = [0, 1]
peripheral.writeCharacteristic(characteristics(i), value, 0)
peripheral.setNotify(characteristics(i), 1)
CASE "F000AA12" & servicesHeader
! Turn the accelerometer sensor on.
IF debug THEN PRINT "Accelerometer on."
DIM value(1) as INTEGER
value(1) = 1
peripheral.writeCharacteristic(characteristics(i), value, 1)
CASE "F000AA13" & servicesHeader
! Set the sample rate to 500ms.
DIM value(1) as INTEGER
value(1) = 50
IF debug THEN PRINT "Setting accelerometer sample rate to "; value(1)
peripheral.writeCharacteristic(characteristics(i), value, 1)
END SELECT
END IF
NEXT
END IF
END SUB
! Called to return information from a characteristic.
!
! Parameters:
! time - The time when the information was received.
! peripheral - The peripheral.
! characteristic - The characteristic whose information
! changed.
! kind - The kind of call. One of
! 1 - Called after a discoverDescriptors call.
! 2 - Called after a readCharacteristics call.
! 3 - Called to report status after a writeCharacteristics
! call.
! message - For errors, a human-readable error message.
! err - If there was an error, the Apple error number. If there
! was no error, this value is 0.
!
SUB BLECharacteristicInfo (time AS DOUBLE, peripheral AS BLEPeripheral, characteristic AS BLECharacteristic, kind AS INTEGER, message AS STRING, err AS LONG)
IF kind = 2 THEN
DIM value(1) AS INTEGER
value = characteristic.value
SELECT CASE characteristic.uuid
CASE "F000AA11" & servicesHeader
! Update the accelerometer.
c = 64.0
p% = value(1)
IF p% BITAND $0080 THEN p% = p% BITOR $FF00
x = p%/c
p% = value(2)
IF p% BITAND $0080 THEN p% = p% BITOR $FF00
y = p%/c
p% = value(3)
IF p% BITAND $0080 THEN p% = p% BITOR $FF00
z = p%/c
PRINT x, y, z
CASE ELSE
PRINT "Read from "; characteristic.uuid
END SELECT
ELSE IF kind = 3 AND err <> 0 THEN
PRINT "Error writing "; characteristic.uuid; ": ("; err; ") "; message
END IF
END SUB
Wednesday, October 31, 2012
Programs in this blog use some features from techBASIC 2.4. We had expected Apple to release it by now. Once they do, it will be a free upgrade to the current version, techBASIC 2.3. techBASIC 2.3 is available now in the App Store.