RTEMS GPIO API

This lengthy post proposes several possible changes to the current RPI GPIO API and some additions which can make it suitable to be used by any BSP, and is intended as a base for further discussion within the RTEMS community. The included code snippets are explanatory stubs, and all naming is likely subject to change.

This API should satisfy at least the following requirements:

  • Ability to manage digital input and output pins, providing an unified interface to any user application to perform this type of I/O independently of the BSP used (the specific pins used must, however, be managed by the application for each of the BSPs/platforms where the application is to be used);
  • Ability to add and manage additional types of I/O by a specific BSP, without affecting any other BSP or user application. The methods to deal with these new types are to be provided by the BSP to the API, during initialization;
  • The API must be able to manage interrupts, and allow gpio pins to be used as shared IRQ lines;
  • The API should provide function prototypes which are to be implemented by the BSPs to access their specific hardware;
  • The API should ensure atomicity where needed;
  • The API must allow for dynamic pin re-purposing;
  • The API should have at least a method for software switch debounce.

BSP API Initialization

Each BSP must provide its specific GPIO layout and I/O capabilities during the API initialization. At this point I am assuming that the only I/O that is universal to all platforms using a GPIO interface is digital I/O, meaning that any other I/O must be defined and managed by each requiring BSP as to avoid bloating the API. This may be done by filling a few structs such as the following.

GPIO layout/partitioning:

typedef struct
{
        /* Number of GPIO banks. */
        unsigned int gpio_bank_count;
        unsigned int pins_per_bank;
} gpio_layout;

Any BSP specific I/O types, in addition to plain digital I/O, which require the use of GPIO pins:

This may include interfaces such as analog I/O, data buses such as SPI or I2C, JTAG, and so on. Each I/O type would be defined through a struct, which at the same time could define an array of pins where this I/O could be performed. The purpose of this feature would be to allow the API itself to reflect the hardware capabilities, and warn the user if it tries to setup a pin with an I/O function it cannot provide. Since this implies an additional overhead (as the API would have to check the array every time a pin is requested to the API), maybe a BSP_OPT can be used to set this array as NULL to bypass this check (so the user would be responsible to ensure that each pin he uses can provide the intended I/O function).

typedef struct
{
        /* I/O function code, specified by the BSP. */
        unsigned int io_type;

        /* Matrix of bank/pins where this I/O can be performed. */
        unsigned int** pins;

        /* Pointer to the function which setups this I/O type. */
        gpio_error (*config_io) (gpio_pin conf);
} gpio_io_type;

The possible values for the io_type field would be defined by each BSP, together with the function (config_io) which the API will call whenever it receives a request to setup that I/O type on a pin.

The API creates and manages a linked list of gpio_io_type structs, so the specific types can be easily accessed.

API initialization

The API uses the following data structures:

typedef enum
{
        DIGITAL_IN,
        DIGITAL_OUT,
        BSP_SPECIFIC,
        NONE
} gpio_pin_function;

Every time a pin is requested to the API, it should state which function it should have. DIGITAL_IN and DIGITAL_OUT are the I/O functions that the API provides and manages on a generic and unified fashion to any application. If the function is set to BSP_SPECIFIC, then the API will know that it should call the BSP defined function to setup that pin. A function of NONE states that the pin is not being used, and may be requested to the API.

typedef struct
{
        /* The bsp defined function code. */
        unsigned int bsp_function;

        void* pin_data;
} gpio_specific_data;

If a pin is requested with the function BSP_SPECIFIC, this struct must be filled with the BSP defined function code, as well as any data it requires through the pin_data void pointer.

typedef struct
{
        /* Number of clock ticks that must pass between interrupts
        unsigned int clock_tick_interval;
} gpio_debounce_conf;

The gpio_debounce_conf should be used to set the debounce settings for a given pin. This is to be used by an helper function which performs software debouncing, avoiding a flood of interrupts on a switch release.

typedef struct
{
        /* Main ISR handling task for this pin. */
        rtems_id task_id;

        gpio_interrupt enabled_interrupt;

        gpio_handler_flag handler_flag;
  
        /* Linked list of interrupt handlers. */
        gpio_handler_list *handler_list;

        /* Software switch debounce settings. Should be NULL if not used. */
        gpio_debounce_conf *debounce;
} gpio_interrupt_conf;


If the pin will be listening to interrupts, the gpio_interrupt_conf must be used to do the setup. More details about the interrupt handling on the API is described in the section “Interrupt processing”.

The gpio_handler_list is a linked list of user/application defined IRS handlers and corresponding arguments, defined as follows:

typedef struct _gpio_handler_list
{
  struct _gpio_handler_list *next_isr;

  gpio_irq_state (*handler) (void *arg);

  void *arg;
} gpio_handler_list;

Next is shown the main data struct which defines the complete configuration of a gpio pin.

typedef struct
{
        unsigned int bank;
        unsigned int pin;

        gpio_pin_function function;

        /* True for active-low, false for active-high. */
        bool gpio_polarity;

        /* Pull resistor setting. */
        gpio_input_mode input_mode;

        /* Pin interrupt configuration. Should be NULL if not used. */
        gpio_interrupt_conf* interrupt;
        
        /*
         * TODO: other relevant generic fields?
         */

        /* Struct with bsp specific data.
         * If function == BSP_SPECIFIC this should have a pointer to
         * a gpio_specific_data struct.
         *
         * If not this field may be NULL. This is passed to the bsp function so any bsp specific data
         * can be passed to it through this pointer. */
        void* bsp_specific;
} gpio_pin;

For each bsp specific I/O function, the BSP should define a struct with the desired fields so that the API can pass it to the BSP defined function which will setup the pin. The bsp_specific pointer can also be used to pass bsp specific data for digital input or output processing functions within the BSP. If the specified function is set to BSP_SPECIFIC then a gpio_specific_data should be passed with this pointer.

The gpio_input_mode is defined as follows:

typedef enum
{
  PULL_UP,
  PULL_DOWN,
  NO_PULL_RESISTOR
} gpio_input_mode;

An user application must fill a gpio_pin struct for every pin it needs, and then give it to the following function which will configure the pin depending on the specified pin function.

gpio_error gpio_request(gpio_pin conf)
{
        switch ( conf.function ) {
               case DIGITAL_IN :
                    return gpio_setup_input(conf);
                case DIGITAL_OUT :      
                    return gpio_setup_output(conf);
                case BSP_SPECIFIC :
                    return bsp_setup_io(conf);
                case NONE:
                     return <some GPIO_error, as this is an unneeded call>
        }
}

Taking the gpio_setup_input as an example, it could be defined as:

gpio_error gpio_setup_input(gpio_pin conf)
{
        /* Parameter checking. */

        /* Check if pin is not being used. */

        return bsp_setup_input(conf)
}

The bsp_setup_function must be implemented by any bsp supporting the API, hence it will be different for every bsp/platform.

While the gpio_setup_output would be similar to the above, the bsp_setup_io function could be defined as:

gpio_error bsp_setup_io(gpio_pin conf)
{
        gpio_specific_data *sd = (gpio_specific_data) conf.bsp_specific;

        if ( sd == null ) {
           return <some gpio_error, as this function requires bsp specific data>
        }

        /* Check the linked list of gpio_io_type structs for the config_io function pointer which configures this type of pin. */

        /* Calls the found config_io function pointer, and sends along the conf struct. */
}

With a digital input or output pin configured and requested, an application may call the set, clear or get functions.

Taking the gpio_set function as an example:

gpio_error gpio_set(gpio_pin conf)
{
        /* Parameter checking. */

        return bsp_gpio_set(conf);
}

The bsp_gpio_set, which is to be implemented by the bsp, must contain the specific hardware code to set the given pin to the logical high.

When a pin is not needed anymore, it may be released through a function such as:

gpio_error gpio_release(gpio_pin conf)
{
        /* Parameter checking. */

        /* Remove any attached ISR. */

        assert(bsp_release(conf));

        /* Update API pin bookkeeping. */

        /* return */
}

User Application View Point

At this point we can evaluate how an application may look like while using the API.

Suppose we want a simple application that blinks some LED, and that it should work on BSP A and BSP B. Since they are two different platforms, the actual GPIO pin where the LED is connected in each platform will be different.

Before using a GPIO pin, it should be requested to the API. This is done by filling a gpio_pin struct with the pin and bank numbers, as well as the desired pin function and other relevant configurations.

gpio_pin led_pin = {
         #ifdef BSP_A
                .bank = 3,
                .pin = 1,
         #else
                .bank = 1,
                .pin = 27,
         #endif

         .function = DIGITAL_OUT,

         (...)
};

With the ifdef conditions in place the pin definition can be easily customized on a bsp/platform basis.

Having the pin definition, the application may now request it from the API:

assert(gpio_request(led_pin) == GPIO_SUCCESS);

And use it as needed:

while ( 1 ) {

      gpio_clear(led_pin);

      sleep(1);

      gpio_set(led_pin);

      sleep(1);
}

By continuing to use the led_pin struct to refer to the pin even after the request it is ensured that the user does not mistakenly requests pin x and tries to access pin y (as it may currently happens with the RPI API, where the actual pin number is used with the API functions). In the event that the led_pin structure is changed after the request, only the bank and pin number fields are considered outside the gpio_request call, and because the API has its own bookkeeping of which pins are being used and how, the user can be safe in the knowledge that any change in the led_pin struct done after the request is not taken into account, unless the pin is released and requested again with the new configuration.

API management data and functions

My view for the rtems-wide GPIO API is that is can be more than just an unified list of function calls, so that gpio applications can be used independently of the BSP. It should also:

  • monitor which pins are available, and which pins are being used (along with their current configuration);
  • handle the rtems side of interrupt handling, by creating/disabling ISR routines;
  • providing locking mechanisms where they are needed, such as in the functions creating/disabling interrupts;
  • provide helper functions, such as software switch debounce capabilities.

The current RPI GPIO API already provides much of the above, and it can be easily be made generic by carving out the RPI specific code.

Pin bookkeeping

The API maintains an array of gpio_pin structs, where the current execution status of GPIO is reflected. This is updated every time a pin is requested, and checked before using any pin.

Interrupt processing

The API has the following enums related to interrupt handling:

typedef enum
{
  FALLING_EDGE = 0,
  RISING_EDGE,
  LOW_LEVEL,
  HIGH_LEVEL,
  BOTH_EDGES,
  BOTH_LEVELS,
  NONE
} gpio_interrupt;

The gpio_interrupt enum defines the different types of interrupt enabled on a given pin. Not sure at this point if the BOTH_EDGES and BOTH_LEVELS are a common feature on most platforms.

typedef enum
{
  IRQ_HANDLED,
  IRQ_NONE
} gpio_irq_state;

The gpio_irq_state enum defines the return values of an ISR. It should state if the handler has handled the interrupt or not, and is useful when more than one ISR is defined on a single pin (gpio pin as a shared IRQ line for multiple devices).

typedef enum
{
  SHARED_HANDLER,
  UNIQUE_HANDLER
} gpio_handler_flag;

The gpio_handler_flag enum defines the two types of ISR: shared or unique. If a pin is requested to listen to a certain interrupt with the SHARED_HANDLER flag then multiple handlers (one per device connected to the pin) can be attached to that pin. Otherwise only one will be attached.

The ISR processing is currently all done through threaded IRQ’s, meaning that the ISR itself is not executed in ISR context, but in a separate task/thread. Each interrupt vector have an actual ISR (generic) which only task is to wake the correct ISR tasks, reducing the time spent in ISR context to a minimum.

void generic_isr(void* arg)
{
        /* Disables interrupt vector. */

        /* Calls memory barrier. */

        /* Calls function to probe the hardware to assess which interrupt(s) fired. */

        /* Wakes the ISR handler task on any pin that has a pending interrupt. */

        /* Clears all active interrupts on the hardware. */

        /* Calls memory barrier, before enabling the interrupt vector. */

        /* Enables interrupt vector. */
}

All hardware related functions are defined by the BSP’s. The API only defines the function prototypes so it can use them.

Each pin has a generic_handler_task, which is responsible for calling all the attached ISR handlers on that pin and check if any have acknowledged the interrupt and processed it. If no ISR has acknowledged the interrupt, treats it as a spurious interrupt.

Each ISR is responsible for probing their corresponding device (if multiple devices are connected to a single pin), and if their device has indeed generated an interrupt it then should be acknowledged and processed. The handled state is then returned to the generic_handler_task.

rtems_task generic_handler_task(rtems_task_argument arg)
{
        /* If the pin has a debouncing function active, call it. */

        while ( 1 ) {
              /* sleep until the generic_isr call's */

              /* retrieve the pins handler list. */

              /* Call all handlers in sequence, and count how many have handled the interrupt. */
        }

        /* If none has handled the interrupt, treat it as a spurious interrupt */
}


Enabling an interrupt

The API has an interrupt counter (initially zero), which is incremented every time an API pin is requested stating that it will be listening to some interrupt. A brief overview of the function handling this is shown below:

gpio_enable_interrupt(int dev_pin, gpio_interrupt interrupt, gpio_handler_flag flag, gpio_irq_state (*handler) (void *arg), void *arg)
{
        /* Parameter checking. */

        /* Acquire the API's interrupt lock. */

        /* If the pin has no interrupt, create and start generic_handler_task with the application-defined ISR routine. */

        /* If the interrupt_counter is zero, installs a generic ISR with rtems_interrupt_handler_install().
         * During this step the interrupt vector is disabled.
         */

        /* Enable the interrupt on hardware. */

        /* Update API bookkeeping. Increases the interrupt_counter. */

        /* Release the API's interrupt lock. */
}

Switch debouncing

The API keeps a record of the last clock tick where an interrupt was detected on a given pin. If the current clock tick is too close to that then ignores this interrupt.

int debounce_switch(int dev_pin)
{
        /* Get rtems_clock_get_ticks_since_boot() */


        if ( last_isr_tick < current_isr_tick ) {

                return -1;

        }


        last_isr_tick = current_isr_tick


        return 0;
}