Android Things has a unique ability to easily connect to external electronics components with the Peripheral API and built-in device support. In this article you will learn about the different types of peripherals you can connect to in order to customize your IoT devices with Android Things.
Working With Different Interfaces
Android Things allows most devices to be connected to your prototyping board through the use of the Peripheral API, which supports GPIO, PWM, I2C, SPI and UART interfaces, each of which are industry standard interfaces for communicating with peripherals. In this section you will learn what these interfaces are, and how to communicate with devices that you have hooked up to your Android Things prototyping board using these connections.
GPIO
General Purpose Input/Output (GPIO) pins are used for digital (binary) communication with components, such as reading whether a button is pressed or not, or turning an LED on or off. Of the I/O methods that you will see in this tutorial, GPIO is by the simplest to use, only requiring one pin and using boolean values for high or low states.
Before you can connect to a GPIO pin, you will need to know the unique name of that pin. You can get the names of all available pins by retrieving the PeripheralManagerService
and calling getGpioList()
. On a Raspberry Pi, this will return the following list:
[BCM12, BCM13, BCM16, BCM17, BCM18, BCM19, BCM20, BCM21, BCM22, BCM23, BCM24, BCM25, BCM26, BCM27, BCM4, BCM5, BCM6]
To figure out which pins each of these represent, you can refer to the Raspberry Pi I/O diagram.
Once you have the name of the pin that you will be reading from or writing to, you can get a Gpio
object reference to that pin by calling openGpio(String pin_name)
from your PeripheralManagerService
.
try { mGpio = service.openGpio(PIN_NAME); } catch (IOException e){ }
GPIO pins can be used for input or output. If you will use the pin to read in information, you will need to configure the pin direction as DIRECTION_IN
, and set the trigger type for that pin so that it knows when to let your apps know that something has occurred.
mGpio.setDirection(Gpio.DIRECTION_IN); mGpio.setEdgeTriggerType(Gpio.EDGE_BOTH);
The trigger types consist of EDGE_NONE
, EDGE_RISING
, EDGE_FALLING
, and EDGE_BOTH
. If a button is pressed, then a rising event occurs as the button circuit is completed and a high-voltage signal appears on the physical pin. A falling event happens when the button is released. Your code will be notified of changes based on the type of trigger you set.
Now that your GPIO is listening for edge triggers, you will need to create a GpioCallback
to register the value of the GPIO component within your app.
private GpioCallback mCallback = new GpioCallback() { @Override public boolean onGpioEdge(Gpio gpio) { try { Log.d("Tuts+", "GPIO value: " + gpio.getValue()); } catch( IOException e ) { } return super.onGpioEdge(gpio); } @Override public void onGpioError(Gpio gpio, int error) { super.onGpioError(gpio, error); } };
Once your callback is created, register it with your Gpio
object.
mGpio.registerGpioCallback(mCallback);
If your GPIO pin is writing information, then you will need to set the direction as DIRECTION_OUT_INITIALLY_LOW
or DIRECTION_OUT_INITIALLY_HIGH
, depending on whether you want the component to start as on or off. No trigger types are required for an output pin.
mGpio.setDirection(Gpio.DIRECTION_OUT_INITIALLY_LOW);
In order to write to the device, you can call setValue(boolean)
on the Gpio
object to set the state of the component.
mGpio.setValue(true);
Once your app is done running, you will need to unregister your input callback, if it was created and registered, and close access to the peripheral using the close()
method in onDestroy()
.
@Override protected void onDestroy() { super.onDestroy(); if( mGpio != null ) { try { mGpio.unregisterGpioCallback(mCallback); mGpio.close(); mGpio = null; } catch( IOException e ) { } } }
PWM
Pulse Width Modulation (PWM) devices use the alternation of digital states, known as a proportional control signal, to function. There are three major parts of a proportional control signal to be aware of:
- Frequency: This describes how often the output pulse repeats. The unit of measurement for frequency is hertz (Hz).
- Duty Cycle: This represents the width of a pulse within a set frequency. Duty cycle is expressed as a percentage of high signals within a frequency, so a cycle that is up half of the time would have a 50% duty cycle.
- Period: This represents the time it takes for each up/down cycle to occur. Period is inversely related to frequency.
By adjusting the duty cycle of a signal, you can control the average 'on' time of a wave. Below you can see an example of a 50% period signal.
Some devices that can use a PWM signal include servo motors, which use the frequency to determine their position, or an LED matrix, that can use the PWM signal to adjust brightness.
Even with just motors and GPIO devices, you can create a large number of IoT devices, such as a "smart" cat tree with motorized laser.
Similarly to GPIO, you can retrieve a list of available PWM ports by creating a PeripheralManagerService
and calling getPwmList()
. On a Raspberry Pi, this list will look like the following:
[PWM0, PWM1]
Once you know the name of the PWM pin that you want to use, you can open a connection to that peripheral using the openPwm(String)
method. This will need to be wrapped in a try/catch
block to handle the possibility of a thrown IOException
.
try { mPwm = service.openPwm(PIN_NAME); } catch( IOException e ) { }
Once you have opened a connection to your PWM pin, you can control settings on it, such as the frequency and duty cycle.
mPwm.setPwmFrequencyHz(120); mPwm.setPwmDutyCycle(25); mPwm.setEnabled(true);
Before you finish with your app and destroy the Activity
, you will need to close the connection and release the reference to your PWM device.
@Override protected void onDestroy() { super.onDestroy(); if( mPwm != null ) { try { mPwm.close(); mPwm = null; } catch( IOException e ) { } } }
I2C
The Inter-Integrated Circuit (I2C) bus allows your project to communicate with multiple devices over one physical connection, and it allows you to send complex data with only a few pins of your Pi or other embedded device. I2C uses synchronous communication between devices, and relies on a clock signal to ensure that devices are responding at the appropriate time.
The device that issues the clock signal, which will often be your Android Things device, is known as the master, and all connected peripherals that receive that signal are known as slaves.
Unlike PWM and GPIO, which only require a single pin, I2C devices require three connections:
- Shared clock signal (abbreviated SCL), which issues the clock signal from the master device to the slave devices.
- Shared Data Line (abbreviated SDA), which is the connection used for actual data transfer. Because I2C is a synchronous (half-duplex) communication standard, meaning data can only move in one direction at a time, only one device may use this connection at any particular point in time.
- An electrical ground connection.
It's worth noting that every I2C slave device is programmed with a set address, and will only respond when the master device makes a data request for that particular address.
Some peripherals that can use this connection method include segmented LED matrix displays, and various advanced sensors.
Each Android Things device will have a set of pins that are used for I2C, which you can find by looking at the documentation for your specific prototyping board. For the Raspberry Pi, you can reference the pinout image at the top of this article, which states that the SDA and SDL are pins 3 and 5. To find the name of your I2C, you can run the following code:
PeripheralManagerService manager = new PeripheralManagerService(); List<String> deviceList = manager.getI2cBusList(); if( !deviceList.isEmpty() ) { Log.d( "Tuts+", deviceList.toString() ); }
The above code snippet will output the following on a Raspberry Pi:
[I2C1]
Once you know the name of your I2C bus, you can connect to devices on that bus using their software address. You can find software address and other low-level connection details in the peripheral component's "datasheet"—required reading if you want to use a peripheral for an embedded project!
private I2cDevice mDevice; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); try { PeripheralManagerService manager = new PeripheralManagerService(); mDevice = manager.openI2cDevice(I2C_DEVICE_NAME, I2C_ADDRESS); } catch (IOException e) {} }
When communicating with a device over I2C, you can use the System Management Bus (SMBus) protocol to place data inside of registers or to retrieve data that has been saved in registers on each peripheral device. This is done using the device address and register address, and Android Things allows you to read or write individual bytes from a register or groups of bytes from multiple registers with the following methods:
-
readRegByte()
andwriteRegByte()
: Read or write a single byte from a specific register. -
readRegWord()
andwriteRegWord()
: Read or write bytes from two sequential registers on a peripheral in little-endian format. -
readRegBuffer()
andwriteRegBuffer()
: Read or write bytes from up to 32 consecutive registers. Values are written or retrieved as abyte array
.
public void singleByte(I2cDevice device, int address) throws IOException { // Read one register from slave byte value = device.readRegByte(address); // Write the value back to slave device.writeRegByte(address, value); } public byte[] multipleBytes(I2cDevice device, int startAddress) throws IOException { // Read three consecutive register values byte[] data = new byte[3]; device.readRegBuffer(startAddress, data, data.length); return data; }
When your app is ready to close on your Android Things device, be sure to unregister your I2C bus.
@Override protected void onDestroy() { super.onDestroy(); if (mDevice != null) { try { mDevice.close(); mDevice = null; } catch (IOException e) {} } }
SPI
Serial Peripheral interface (SPI) connections work similarly to I2C connections, except they support full-duplex communication. This means that data can be read and written to peripherals simultaneously, rather than requiring the master device to request information from the slave peripherals. A clock signal is still required to control data flow from multiple devices on the SPI signal bus. SPI requires a minimum of four connections to function:
- Master Out Slave In (MOSI)
- Master In Slave Out (MISO)
- Clock signal (CLK)
- Shared ground (GND)
In addition, a fifth connection is required if multiple SPI devices are attached to an SPI bus. This is the Chip Select (CS), which is used to signal the hardware address of peripherals, so that your master device can communicate with a specific slave.
Some examples of peripherals that use SPI include LED dot matrix boards and SD card readers, and it's worth noting that many peripherals that support I2C will also support SPI.
Just like the previous examples, you will need to know the name of the SPI connection on your Android Things board. You can find this by creating a PeripheralManagerService
and calling getSpiBusList()
on that object. On a Raspberry Pi you will have the following available:
[SPI0.0, SPI0.1]
Once you know the name of the SPI bus that you will use, you can open a connection to it.
private SpiDevice mDevice; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); try { PeripheralManagerService manager = new PeripheralManagerService(); mDevice = manager.openSpiDevice(SPI_DEVICE_NAME); } catch (IOException e) { } }
In order for devices to communicate over the SPI bus, they all must "speak the same language", which is to say they must be configured for the same clock rate and data format. While some peripherals can adjust their data format and clock rate, others cannot. As all devices on an SPI bus must operate with the same configuration, it is important to know the capabilities of your peripherals before attempting to include them on your SPI bus. There are four settings/parameters that will need to be set for your SPI bus:
- SPI mode: The SPI mode has two major components: whether the clock signal is high or low while no data is being transferred, and which edge of a pulse is used for transferring data.
- Frequency: This represents the shared clock signal in Hz, and will most likely be a value determined by the capabilities of your slave devices.
- Bit justification: Used to set the endianness for your data. By default, Android Things will use big-endian, putting the most significant big (MSB) first.
- Bits per word (BPW): Controls how many bits will be sent to a slave device before toggling the chip select signal. By default, eight bits per word will be sent.
In your Android Things java code, you can set these four values on your SPI bus like so:
// Low clock, leading edge transfer device.setMode(SpiDevice.MODE0); device.setFrequency(16000000); device.setBitsPerWord(8); device.setBitJustification(false);
How you interact with your devices on the SPI bus depends on whether you are operating in full or half-duplex mode. If your devices are configured for half-duplex, then you will want to exchange data between the slaves and master device with the read()
and write()
methods on the SpiDevice
object. If you are operating in full-duplex mode, then you will want to use the transfer()
method with two byte array buffers: one with the data to be sent, and another empty buffer for storing response data.
//Half-duplex mode public void sendCommand(SpiDevice device, byte[] buffer) throws IOException { // Shift data out to slave device.write(buffer, buffer.length); // Read the response byte[] response = new byte[32]; device.read(response, response.length); } // Full-duplex mode public void sendCommand(SpiDevice device, byte[] buffer) throws IOException { byte[] response = new byte[buffer.length]; device.transfer(buffer, response, buffer.length); }
When your app is ready to close on your Android Things device, be sure to unregister your SPI device.
@Override protected void onDestroy() { super.onDestroy(); if (mDevice != null) { try { mDevice.close(); mDevice = null; } catch (IOException e) {} } }
UART
More complex peripherals, such as LCD displays, SD card readers, and GPS modules, will often use serial communication through UART ports when communicating with a master device. UART allows devices to asynchronously send raw data to a buffer on another device. This buffer is then read in a first-in-first-out order. It also allows both data format and transfer speed to be configured on both connected devices. Both devices must agree on a transfer speed, as UART does not support a clock signal, but transfer speeds tend to be faster than I2C and data can be transferred in full-duplex mode between devices. Unlike I2C and SPI, only one attached peripheral is allowed per UART connection.
UART peripherals have three core wires:
- RX, which is used to receive data.
- TX, which is used to transmit data.
- GND, electrical ground.
On peripheral components, these pins will generally be labeled like so:
In addition, some UART devices may include two more connections for controlling data flow:
- Request to Send (RTS)
- Clear to Send (CLS)
Similar to all of the previous examples, you will need to know the name of your Android Things device's UART connection. You can find that with the following code:
PeripheralManagerService manager = new PeripheralManagerService(); List<String> deviceList = manager.getUartDeviceList(); if (!deviceList.isEmpty()) { Log.d("Tuts+", deviceList.toString()); }
On a Raspberry Pi, the log output will look like this:
[UART0]
Once you know the name of your UART connection, you can create an Android Things UART device in the same way you have opened other device connections.
PeripheralManagerService manager = new PeripheralManagerService(); mDevice = manager.openUartDevice(UART_DEVICE_NAME);
Once you have your device, you will need to configure the format for the data frames that will be sent between devices. There are four properties to be aware of:
- Number of data bits: UART can send between five and nine bits to represent a character of data. Sending less bits allows for a faster data transfer rate, but limits the range of characters that can be sent.
- Parity bit: Used to check if the content of a transmission's data sums to an even or odd number. If parity is set to none, then the parity bit is removed from the transmission. This is used for error checking on transmissions.
- Stop bits: This can be set to either 1 or 2 bits. The transmission will idle for a duration equal to the number of stop bits to indicate that a data frame has ended.
- Baud rate: Baud rate is the transmission speed for your data frame. Since there are no clock signals in UART, both devices must be configured with a baud rate before transmission begins.
Each of these settings can be configured on a UART device like so:
uart.setBaudrate(9600); uart.setDataSize(6); uart.setParity(UartDevice.PARITY_NONE); uart.setStopBits(2);
Earlier you learned that there are two wiring settings for UART devices, those with additional control signals, and those without. You can configure your UART device in code by using the setHardwareFlowControl()
method.
uartDevice.setHardwareFlowControl(UartDevice.HW_FLOW_CONTROL_AUTO_RTSCTS); //or uartDevice.setHardwareFlowControl(UartDevice.HW_FLOW_CONTROL_NONE);
Once you've finished configuring your devices, it's time to read and write data from your Android Things board and your UART peripheral. Writing is handled by calling the write()
method with a byte array buffer and the length of that array.
uartDevice.write(buffer, buffer.length);
While you can continuously poll for data in your device's read buffer, your better bet is to create a UartDeviceCallback
object, which provides a method named onUartDeviceDataAvailable()
that will be triggered when data is available. When it is, you can read data from your device's data buffer. You will be able to associate this callback with your UART device with the registerUartDeviceCallback
, though you will need to remember to call unregisterUartDeviceCallback
when you are done using the callback. When data is available, you can retrieve it by using the UART read()
method.
private UartDeviceCallback mUartCallback = new UartDeviceCallback() { @Override public boolean onUartDeviceDataAvailable(UartDevice uart) { byte[] buffer = new byte[MAX_BUFFER_SIZE]; try { uartDevice.read(buffer, buffer.length) //do something with the data in the buffer byte array } catch (IOException e) {} //Returning true keeps the callback active. If you return false, //the callback will automatically unregister. return true; } @Override public void onUartDeviceError(UartDevice uart, int error) {} }; @Override protected void onStart() { super.onStart(); mDevice.registerUartDeviceCallback(mUartCallback); } @Override protected void onStop() { super.onStop(); mDevice.unregisterUartDeviceCallback(mUartCallback); }
When you are finished with your Activity
, be sure to close and nullify the reference to your UART device.
@Override protected void onDestroy() { super.onDestroy(); if (uartDevice != null) { try { uartDevice.close(); uartDevice = null; } catch (IOException e) {} } }
Advanced I/O and Future Updates
While the Peripheral API allows you to communicate with almost any device that can be wired to your Android Things board, Google has provided some additional support to make building your IoT devices even easier. This section will take a look at some of the additional peripheral support available through Android Things.
Native I/O
While Android Things provides a simple way for Android developers to get into IoT development with the Android SDK and Java, many IoT apps already exist in C and C++, and other developers would prefer to work in those languages for various reasons. To support this, Google has supplemented the Peripheral API with support for native applications and the NDK. I won't go into depth on this topic, but the connections are established and used in very similar ways to those discussed above.
WiFi and Bluetooth
Without a way to connect to the Internet, IoT devices would simply be things. With built-in wireless support, your Android Things devices can connect to the Internet and use various online resources, such as Firebase and other Google services, or any other back-end service. In addition, using tools such as Google Play Services and the Nearby Connections API can allow users to communicate directly with your device over their WLAN network.
Although not currently enabled in the developer preview versions of Android Things, Bluetooth connectivity is also an important IoT capability. Using Bluetooth, you can create multiple IoT devices or accessories and have them communicate across short distances. This allows each device to have additional contextual information about its environment, such as a user's home or workspace, in order to best serve the user.
USB
Some devices do work with prototype board USB ports, although it's not entirely enabled in the current iteration of the Android Things developer preview. In the second developer preview, Google has added USB audio out with text to speech support, and developers have confirmed that other devices work, such as USB microphones. As USB APIs are tested and approved in later versions of Android Things, there will be a wide array of devices that can be simply plugged into your IoT device as an accessory.
Cameras and Displays
Some devices, such as the Raspberry Pi, come with additional I/O connections built in, such as HDMI, Display, and Camera. Luckily, all of these are enabled with Android Things, allowing you to attach a camera with a 15cm ribbon cable...
... or a display through a ribbon cable or HDMI connection:
Conclusion
Congratulations! You've covered a lot of ground, but have learned incredibly valuable information about connecting and controlling peripheral devices with an Android Things board. Using this knowledge, you should be able to connect and communicate with various hardware components.
In the next post of this series, we will use our knowledge of the Peripherals API to create a software driver for a GPIO motion sensor component, which can then be used in a larger Android Things project.
In the meantime, check out some of our other tutorials and articles about Android development!
Comments