Native PIO

The native Peripheral I/O (PIO) APIs let you write C/C++ code to control GPIO, PWM, I2C, SPI and UART peripherals that access the same underlying peripheral service as the standard PIO APIs. These APIs utilize the Android Native Development Kit.

Adding the required permission

Add the required permission for this API to your app's manifest file:

<uses-permission android:name="com.google.android.things.permission.USE_PERIPHERAL_IO" />

Creating a client

The APeripheralManagerClient class is defined in the Android Things native library. Use this class to create a client to communicate with the manager for each type of interface (for example, GPIO). The client sets up the connection to the interface:

APeripheralManagerClient* client = APeripheralManagerClient_new();
AGpio* gpio;
int openResult = APeripheralManagerClient_openGpio(client, BUTTON_GPIO, &gpio);
// Check openResult since native commands return non-zero values on error (see Error handling section below)

When the app is destroyed, be sure to delete the connection and the client:

AGpio_delete(gpio);
APeripheralManagerClient_delete(client);

Reading from an input

To read a GPIO port as an input:

  1. Configure it as an input using AGpio_setDirection() with the mode AGPIO_DIRECTION_IN.
  2. Optional: Configure the pin voltage represented by an input value of true (active) with AGpio_setActiveType():

    • AGPIO_ACTIVE_HIGH: High voltage (near IOREF) is active. This is the default value.
    • AGPIO_ACTIVE_LOW: Low voltage (near zero) is active.
  3. Access the current state with the AGpio_getValue() function.

int setDirectionResult = AGpio_setDirection(gpio, AGPIO_DIRECTION_IN);
//AGPIO_ACTIVE_HIGH is the default so no need to use AGpio_setActiveType()
int gpioValue;
if (AGpio_getValue(gpio, &gpioValue) != 0) {
    LOGE("failed to get value for GPIO: %s", BUTTON_GPIO);
}

Interrupt

You could set up a loop to poll (as above) the GPIO interface continuously, but that might be a waste of system resources. You might also miss some events, depending on the poll interval. Consider the case of a button wired as an input to a GPIO interface. Most of the time, the button will not be pressed but you still need to constantly check it.

A better way to check the interface would be to set up an interrupt. The interrupt fires when an external event occurs, like a button press.

Declare the state changes that trigger an interrupt event using the AGpio_setEdgeTriggerType() function. The edge trigger supports the following four types:

  • AGPIO_EDGE_NONE: No interrupt events. This is the default value.
  • AGPIO_EDGE_RISING: Interrupt on a value transition from false to true
  • AGPIO_EDGE_FALLING: Interrupt on a value transition from true to false
  • AGPIO_EDGE_BOTH: Interrupt on all transitions

This example shows how to set up an interrupt that will fire on the falling edge of the input signal (assuming the button was connected to a pull-up resistor):

int setEdgeTriggerResult = AGpio_setEdgeTriggerType(gpio, AGPIO_EDGE_FALLING);

Create a file descriptor for this interrupt:

int fd;
int getPollingFdResult = AGpio_getPollingFd(gpio, &fd);

Next, return the looper associated with the calling thread and add the file descriptor to it. A looper efficiently polls a file descriptor to see if it has data available on it.

ALooper* looper = ALooper_forThread();
int addFdResult = ALooper_addFd(looper, fd, LOOPER_ID_USER, ALOOPER_EVENT_INPUT, NULL, NULL);

Now poll the looper with an indefinite timeout. Acknowledge any interrupt events (in this case, print out the pin/signal name connected to the button). Note that execution will block until there is an event on the file descriptor or the app is exited.

while (!app->destroyRequested) {
    android_poll_source* source;
    // wait indefinitely for an interrupt or a lifecycle event.
    int pollResult = ALooper_pollOnce(-1, NULL, NULL, (void**)&source);
    if (pollResult >= 0) {
        if (source != NULL) {
            // forward event to native-app-glue to handle lifecycle and input event
            // and update `app` state.
            source->process(app, source);
        }
        if (pollResult == LOOPER_ID_USER) {
            int ackInterruptResult = AGpio_ackInterruptEvent(fd);
            LOGI("GPIO \"%s\" changed: button pressed", BUTTON_GPIO);
        }
    }
}

Error handling

Many of the native GPIO-related commands return non-zero integer values on error. You can use these return values in conditionals:

int gpioValue;
if (AGpio_getValue(gpio, &gpioValue) != 0) {
    LOGE("failed to get value for GPIO: %s", BUTTON_GPIO); // Retry later
}

ASSERT statements are useful for catching fatal errors. If the condition (specified in the first parameter of the function) does not evaluate to true, then the program logs the debug statement and terminates:

ASSERT(client, "failed to open peripheral manager client");

Since the following command returns an integer value for errors, you can use it as the condition in an ASSERT statement:

int openResult = APeripheralManagerClient_openGpio(client, BUTTON_GPIO, &gpio);
ASSERT(openResult == 0, "failed to open GPIO: %s", BUTTON_GPIO);