Accessing the Bluetooth low energy Barometer on the TI SensorTag
Accessing the Bluetooth low energy Barometer on the TI SensorTag
Accessing the Bluetooth low energy Barometer on the TI SensorTag
This blog explores how to read atmospheric pressure using new TI SensorTag, a Bluetooth low energy peripheral designed to be a hardware and software reference program for the CC2541 chip. It shows a simple, text-based program that's great for experimenting with the sensor. The complete listing appears at the end of the blog. A companion blog, Controlling the TI SensorTag with techBASIC, gives background information about Bluetooth low energy devices, the TI SensorTag, and accessing Bluetooth devices using an iPhone or iPad. It also gives a complete program that accesses all six sensors on the TI SensorTag, showing the values using a GUI interface.
What's a Barometer?
A barometer measures the pressure exerted by a gas or liquid. The T5400 Digital Barometric Pressure Sensor used on the SensorTag is designed for use in air at pressures from 0.3 atmospheres to 1.1 atmospheres. There are a lot of competing units for pressure, including pascals or hectopascals (hundreds of pascals), pounds per square inch, Torr, millimeters of mercury and bar. One bar is roughly one standard atmosphere, and since our barometer is designed for atmospheric pressure, that's the unit we'll use in this blog.
There are a lot of reasons for the pressure in the atmosphere to vary. One is altitude. Barometers are a common way to measure the altitude of anything that flies. We're a mile high here in Albuquerque, where the barometric pressure is 0.85 Bar today. Another common reason for air pressure to vary is weather. A pressure change is common as a weather front moves through. The pressure drops as a wet weather system approaches, then climbs again after it passes by.
Accessing the Barometer
Here's the Generic Attribute Profile (GATT) for the barometer service.
Like the other sensors on the SensorTag, the second attribute, 0xAA42, turns the sensor on or off. Unlike the other sensors, it has another use. Several values are needed to calculate the barometric pressure from the raw data returned by the sensor. Writing 2 to attribute 0xAA42 tells the barometer to calculate these calibration values. Here's the code to turn the sensor on and ask it to find the calibration values.
CASE "F000AA42" & servicesHeader
DIM value(1) as INTEGER
value(1) = 1
peripheral.writeCharacteristic(characteristics(i), value, 1)
value(1) = 2
peripheral.writeCharacteristic(characteristics(i), value, 1)
You can turn the barometer off by writing a zero, but this simple program doesn't turn the sensor off.
The calibration data is needed before any of the sensor data can be used. The 0xAA43 attribute is used to read the calibration data. Here's the code to ask the device to send back the calibration data.
CASE "F000AA43" & servicesHeader
peripheral.readCharacteristic(characteristics(i))
The calibration data returned is an array of sixteen bytes making up eight values. All values are two-byte integers stored least significant byte first, but the first four are unsigned values, and the last four are signed. Here's the code from BLECharacteristicInfo that reads the calibration data and stores it for later use.
CASE "F000AA43" & servicesHeader
FOR i = 1 TO 4
j = 1 + (i - 1)*2
m_barCalib(i) = (value(j) BITOR (value(j + 1) << 8)) BITAND $00FFFF
NEXT
FOR i = 5 TO 8
j = 1 + (i - 1)*2
m_barCalib(i) = value(j) BITOR (value(j + 1) << 8)
NEXT
There are two ways to read sensors on the SensorTag, either reading it when the program wants the pressure or asking the barometer to notify the program whenever a new sensor reading is available. The next step is to tell the barometer which way we want to read values. Write a 1 and 0 to the characteristic to receive notifications when a value is available, then ask it to start sending notifications.
CASE "F000AA41" & 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 barometer can take several seconds to return the first value, so be patient. The simple demonstration program also doesn't check to make sure the calibration data has been read before processing the barometer readings, so the first value reported may be zero. The values will become reasonable once the calibration data has been read.
Whether you read the data once using readCharacteristic or ask for notifications with setNotify, the values are always returned using a call to BLECharacteristicInfo. The values returned consist of a temperature and the raw pressure reading, both stored in two-byte integers with the least significant byte first. The temperature is signed, while the pressure is an unsigned value. Here's the complete set of equations to convert the raw temperature and pressure to an atmospheric pressure using the eight calibration values.
P is the pressure in pascals; divide by 100,000 to convert from pascals to bars.
You might have noticed that there are eight calibration constants, but we only used six. The first two are used to convert the raw temperature to degrees Celsius using the formula
Here's what we get after converting the equations to BASIC.
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 "F000AA41" & servicesHeader
! Update the pressure indicator.
Tr = value(1) BITOR (value(2) << 8)
S = m_barCalib(3) + Tr*(m_barCalib(4)/2^17 + Tr*m_barCalib(5)/2^34)
O = m_barCalib(6)*2^14 + Tr*(m_barCalib(7)/8.0 + Tr*m_barCalib(8)/2^19)
Pr = (value(3) BITOR (value(4) << 8)) BITAND $00FFFF
Pa = (S*Pr + O)/2^14
! Convert from Pascal to Bar.
Pa = Pa/100000.0
PRINT "Pressure: "; Pa; " Bar";
! Add the temperature.
T = m_barCalib(2)/2^10 + Tr*m_barCalib(1)/2^24
PRINT ", temperature: "; T
CASE "F000AA43" & servicesHeader
! Get the pressure callibration data.
IF debug THEN PRINT "Calibration data read."
FOR i = 1 TO 4
j = 1 + (i - 1)*2
m_barCalib(i) = (value(j) BITOR (value(j + 1) << 8)) BITAND $00FFFF
NEXT
FOR i = 5 TO 8
j = 1 + (i - 1)*2
m_barCalib(i) = value(j) BITOR (value(j + 1) << 8)
NEXT
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 Source
Here's the complete source for the barometer program. See the source code from Controlling the TI SensorTag with techBASIC for a program that displays the barometric pressure using a nice GUI.
! Simple program to access the barometer 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) = "F000AA40" & servicesHeader : ! Pressure
press% = 1
! Start the BLE service and begin scanning for devices.
debug = 0
BLE.startBLE
DIM uuid(0) AS STRING
BLE.startScan(uuid)
! Create a place for the barometer calibration values.
DIM m_barCalib(8)
! 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(press%) THEN
! Found the pressure sensor.
SELECT CASE characteristics(i).uuid
CASE "F000AA41" & servicesHeader
! Tell the pressure sensor to begin sending data.
IF debug THEN PRINT "Start pressure sensor."
DIM value(2) as INTEGER
value = [0, 1]
peripheral.writeCharacteristic(characteristics(i), value, 0)
peripheral.setNotify(characteristics(i), 1)
CASE "F000AA42" & servicesHeader
! Trun the pressure sensor on.
IF debug THEN PRINT "Pressure on."
DIM value(1) as INTEGER
value(1) = 1
peripheral.writeCharacteristic(characteristics(i), value, 1)
value(1) = 2
peripheral.writeCharacteristic(characteristics(i), value, 1)
CASE "F000AA43" & servicesHeader
! Get the calibration data.
peripheral.readCharacteristic(characteristics(i))
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 "F000AA41" & servicesHeader
! Update the pressure indicator.
Tr = value(1) BITOR (value(2) << 8)
S = m_barCalib(3) + Tr*(m_barCalib(4)/2^17 + Tr*m_barCalib(5)/2^34)
O = m_barCalib(6)*2^14 + Tr*(m_barCalib(7)/8.0 + Tr*m_barCalib(8)/2^19)
Pr = (value(3) BITOR (value(4) << 8)) BITAND $00FFFF
Pa = (S*Pr + O)/2^14
! Convert from Pascal to Bar.
Pa = Pa/100000.0
PRINT "Pressure: "; Pa; " Bar";
! Add the temperature.
T = m_barCalib(2)/2^10 + Tr*m_barCalib(1)/2^24
PRINT ", temperature: "; T
CASE "F000AA43" & servicesHeader
! Get the pressure callibration data.
IF debug THEN PRINT "Calibration data read."
FOR i = 1 TO 4
j = 1 + (i - 1)*2
m_barCalib(i) = (value(j) BITOR (value(j + 1) << 8)) BITAND $00FFFF
NEXT
FOR i = 5 TO 8
j = 1 + (i - 1)*2
m_barCalib(i) = value(j) BITOR (value(j + 1) << 8)
NEXT
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.