Last year I have added support for the Raspberry Pi GPIO I/O, but during the initial stages of that work it was identified that it would be useful to have a RTEMS-wide GPIO API. This lead me to generalize the RPI GPIO API so it could be used by other platforms. This post is a summary of the current features and capabilities of the API, and follows another post.
Initial note
Any reference to hardware registers are only meant to help the picturing process. The API does not deal with registers directly, instead it uses some abstractions which are explained next.
Nomenclature
GPIO bank – In the context of this API a GPIO bank refers to a GPIO register bank, where each bit controls a corresponding pin. Each BSP is expected to provide a value for the BSP_GPIO_PINS_PER_BANK constant, which should contain the amount of GPIO pins each bank can control (the register size). The API supports at most 32 pins per bank, and its the API that manages the banks, calculating the exact bank-pin pair from the received processor GPIO pin number. Because every bank has the same size, the API calculates how many banks there are by taking into account the total number of GPIO pins (through the BSP-defined BSP_GPIO_COUNT constant) and the number of pins per bank. Since the total count of GPIO pins might not be a multiple of the amount of pins per bank, it is possible that some platforms will have a shorter last bank (where some bits at the end of the register are reserved). The API accounts for this in the internal pin tracking data structure, so the API will never allow an access to a pin that does not exist.
Select bank – Each pin function is selected in hardware through registers, but since a pin may have more than the two basic functions (digital input and output), the selection registers usually require more than one bit per pin, meaning that each select register will control less pins that the other GPIO registers. For function selection directives the API uses a select bank, and each BSP can provide the amount of GPIOs a select bank can control through the BSP_GPIO_PINS_PER_SELECT_BANK constant.
Constants
Each BSP must provide at least the following constants:
BSP_GPIO_PIN_COUNT – Number of GPIO pins available to the API.
BSP_GPIO_PINS_PER_BANK – Number of GPIO pins per GPIO bank (usually the register size of the platform).
The next constant, although desirable, may be omitted:
BSP_GPIO_PINS_PER_SELECT_BANK – If the platform GPIO pin function selection registers control fewer pins than the usual GPIO bank (i.e.: the platform has more than 2 functions per pin, hence each pin will require more bits in the selection register, hence each selection bank will be shorter in the amount of pins), this constant can be provided. Also note that by providing this constant the API will call the rtems_gpio_bsp_multi_select function, so its implementation will have to be complete. This is more detailed in the following sections.
GPIO pin numbering
When referring to a specific GPIO pin the API expects from the callers the GPIO pin number in the processor. This eases the pin identification and avoids storing the bank-pin pair in memory which would lead to greater memory requirements, as every pin in a GPIO bank will have the same bank number (potentially each bank would end with 32 x 32-bits of redundant data, to store the same bank number). The API converts the processor numbering to a bank-pin pair (about two division operations), and that is what is sent to the BSP code.
A BSP can provide human-readable references to pins through constants or pre-defined configurations.
For instance a GPIO pin reference may look like:
#define GPIO_40 40
to refer to gpio pin 40 (processor numbering), but in the case of an on-board LED (or other pin that might have a pre-defined behavior), the BSP might provide a configuration such as:
const rtems_gpio_pin_conf act_led = { { /* Activity LED */ .pin_number = 16, .function = DIGITAL_OUTPUT, .pull_mode = NO_PULL_UP; .interrupt = NULL, .output_enabled = FALSE, .logic_invert = FALSE, .bsp_specific = NULL };
Then an application can use the pin just by requesting the act_led configuration.
GPIO configurations
As mentioned previously, it is possible to define pin configuration tables to request any given pin configuration. It is also possible to define several versions for a given configuration, and update it as needed during run-time.
The data structures to be used are as follows:
typedef struct { /* Processor pin number. */ uint32_t pin_number; rtems_gpio_function function; /* Pull resistor setting. */ rtems_gpio_pull_mode pull_mode; /* If digital out pin, set to TRUE to set the pin to logical high, * or FALSE for logical low. If not a digital out then this * is ignored. */ bool output_enabled; /* If true inverts digital in/out applicational logic. */ bool logic_invert; /* Pin interrupt configuration. Should be NULL if not used. */ rtems_gpio_interrupt_configuration *interrupt; /* Structure with BSP specific data, to use during the pin request. * If function == BSP_SPECIFIC this should have a pointer to * a rtems_gpio_specific_data structure. * * 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; } rtems_gpio_pin_conf;
If the pin should listen to some interrupt, then that configuration should go in the following structure:
typedef struct { rtems_gpio_interrupt active_interrupt; rtems_gpio_handler_flag handler_flag; /* Interrupt handler function. */ rtems_gpio_irq_state (*handler) (void *arg); /* Interrupt handler function arguments. */ void *arg; /* Software switch debounce settings. It should contain the amount of clock * ticks that must pass between interrupts to ensure that the interrupt * was not caused by a switch bounce. * If set to 0 this feature is disabled . */ uint32_t debounce_clock_tick_interval; } rtems_gpio_interrupt_configuration;
Each pin may only listen to one type of interrupt, although it is possible to listen to both edges or both levels at the same time. The handler_flag defines if the pin will have a single handler, or potentially more. At configuration time is only possible to define one handler, so any remaining handler should be installed with rtems_gpio_interrupt_handler_install.
Pin tracking
One role of the API is to keep track of the GPIO state and to synchronize the access to the GPIO banks. It uses an uni-dimensional array of the following structure:
typedef struct { rtems_gpio_function pin_function; /* GPIO pull resistor configuration. */ rtems_gpio_pull_mode resistor_mode; /* If true inverts digital in/out applicational logic. */ bool logic_invert; /* True if the pin is on a group. */ bool on_group; /* Interrupt data for a pin. This field is NULL if no interrupt is enabled * on the pin. */ gpio_pin_interrupt_state *interrupt_state; } gpio_pin;
This structure requires 8 bytes of memory per GPIO pin, which may increase if it has enabled interrupts. If it does it will be allocated in the interrupt_state pointer, and its definition is as follows:
typedef struct { /* GPIO pin's bank. */ uint32_t bank_number; /* Currently active interrupt. */ rtems_gpio_interrupt active_interrupt; /* Id of the task that will be calling the user-defined ISR handlers * for this pin. */ rtems_id handler_task_id; /* ISR shared flag. */ rtems_gpio_handler_flag handler_flag; /* Linked list of interrupt handlers. */ rtems_chain_control handler_chain; /* Switch-deboucing information. */ uint32_t debouncing_tick_count; rtems_interval last_isr_tick; } gpio_pin_interrupt_state;
The bank_number is here so that the threaded handler has a reference to the pin’s bank number, so it can acquire the lock while the interrupt is being handled.
Having an array allows for faster retrieval of the pin state, since they can be identified directly with the pin number. This reasoning assumes that no platform will have enough reserved pins (i.e.: pins that are not physically available), such that the memory wasted tracking these ghost pins is worse than fetching the pin state in a more custom data structure such as a tree.
Pin group
GPIO pins can be grouped together and used as a single entity. This can be useful to simplify GPIO operations (possibility to address multiple pins at once) or to bit-bang data buses and other GPIO-based interfaces.
To accomplish this each group has sets of pins, which can then be addressed as if they were one. The API allows to write and read data to a group, through the use of bitmasks, and it can do this because each group has separate sets for inputs and outputs. It also allows a group to have/use BSP specific functions, through a third set of pins.
A group can be defined using the following data structure:
typedef struct { const rtems_gpio_pin_conf *digital_inputs; uint32_t input_count; const rtems_gpio_pin_conf *digital_outputs; uint32_t output_count; const rtems_gpio_pin_conf *bsp_specifics; uint32_t bsp_specific_pin_count; } rtems_gpio_group_definition;
A group must have at least one set of pins filled with at least one pin, and while every pin in a given set must belong to the same GPIO bank, different sets can belong to different banks. The group operations actuate on the pins depending on the order they have in the group definition. For instance if the pins 1, 3, 7 and 13 are set as digital outputs in a given group and the value 0x5h (0101b) is written to the group, then pins 1 and 7 will be set with logical low, while 3 and 13 will be set with logical high. To ensure the fewer register accesses possible every operation from pin function selection to pin accesses is done through the API’s multi-pin functions. This means that reading the value of a group (which means reading the group’s digital inputs) will translate in a single register call (which is also a reason to have all pins in a set belong in the same bank). The group write function however translates into two register calls, as set and clear registers are usually separate (and that is how the API assumes it, by calling the BSP functions that do multi pin set and multi pin clear).
If the group includes pins with BSP-specific functions then the API only performs synchronization operations, and delegates the operation to the BSP through the following call:
rtems_status_code rtems_gpio_bsp_specific_group_operation( uint32_t bank, uint32_t *pins, uint32_t pin_count, void *arg );
Any needed data to the operation, or any data to be retrieved through it should pass through the arg void pointer.
A group definition is sent to the API through the following call:
rtems_status_code rtems_gpio_define_pin_group( const rtems_gpio_group_definition *group_definition, rtems_gpio_group *group );
The function parses the group definition to validate it and fills the following internal data structure:
struct rtems_gpio_group { rtems_chain_node node; uint32_t *digital_inputs; uint32_t digital_input_bank; uint32_t input_count; uint32_t *digital_outputs; uint32_t digital_output_bank; uint32_t output_count; uint32_t *bsp_speficifc_pins; uint32_t bsp_specific_bank; uint32_t bsp_specific_pin_count; rtems_id group_lock; };
This structure is also the definition for the opaque type rtems_gpio_group, which the function rtems_gpio_create_pin_group instantiates and returns to the caller so it can refer to the group.
This structure keeps the pin sets for each function type, along with the corresponding bank numbers. The pin numbering stored here is relative to the corresponding bank.
Each group also has their own lock, so all group operations are synchronized.
Internally the groups are stored on a rtems chain, so each group is simply a node of that chain.
Error checking
The API returns rtems_status_code errors when needed, and the exact errors that each function might give are documented in the corresponding doxygen documentation. Additional information can be printed in some locations if the DEBUG constant is defined. The API also relies in some assertions in situations where the error should not happen (such as a timeout error while waiting for a semaphore with the NO_WAIT flag).
Parameter validation
Each function validates the data received as parameters, but most validations can be superfluous if a certain configuration was tested and is known to work. An application may use a certain GPIO configuration, and the API verifies if the configuration is valid every time it is used. However, if the configuration is to remain untouched, the API will waste time during execution to verify the same configuration over and over again. An approach to this could be to define a constant that would made this checkings optional, so they could be used during the development of the application, but as soon as the configurations stabilize this checks could be removed to avoid wasting resources during run-time.
Locking
Locking was added to the API by having a mutex per bank. Since each bank of pins corresponds to a register in hardware, it must be ensured that multiple threads/cores using the API do not stumble in a race condition while operating on pins within the same bank. At the same time the API tracks the GPIO pins status on an internal data structure, so it must also be ensured that the access to a pin status is atomic (having a mutex per bank ensures that only other banks pin states can be updated). Locks are always acquired before checking the pin state to avoid a race condition with another thread that might be requesting or changing the state.
Locks are obtained right before a critical section in every function that has them, and relinquishes them before any other API function call (there is no lock sharing) except if calling a BSP code function, even if that function will have to acquire it again. This eases the complexity of the code, and allows for GPIO context switches, as other pins in that bank may be processed in the meantime.
Interrupts
Interrupt handling in the API
At this time the API relies on threaded interrupt handlers. Each bank of pins (or interrupt vector) has an rtems interrupt handler, installed through rtems_interrupt_handler_install(), and each pin has a task (thread) that is created and started when interrupts are enabled on that pin through the API. After the initial setup each task enters on an infinite loop, which starts by putting the task to sleep.
Each bank has a rtems handler, while each pin has a threaded handler. Threaded handlers allow for minimal time spent on ISR context, but also means that the interrupt may not be handled right away. A possible solution may be to allow the API user to define if it wants threaded handling or not. Using threaded handlers also means that on a SMP setup multiple cores can process multiple interrupts at the same time. On a single core system, however, it would be better to have a task per bank, to minimize context switching.
Every time an interrupt is sensed on a GPIO bank, all interrupts on that bank are disabled and the hardware interrupt line on that vector is read. Every pin that shows as having a pending interrupt have their threaded handler called (since all tasks are sleeping it just sends a notification to wake up, avoiding the overhead of starting the task), and so the interrupt line can be cleared and interrupts enabled again on that bank.
Each pin’s task begins by applying the software switch debouncing function (if activated), and then all interrupt handlers associated with that pin are called in sequence. When everything is done the task’s infinite loop starts another iteration and puts the task to sleep until the next interrupt.
Interrupt management calls
The API relies on two functions to control interrupt generation on hardware, and provides other two for GPIO interrupt handler management.
For interrupt control it relies on the bsp_interrupt_vector_enable() and bsp_interrupt_vector_disable() function from the irq-generic API to enable and disable interrupt generation on a specified vector (GPIO bank). These are used to temporarily disable interrupts while they are being processed in the ISR handler, and are
For handler management (associate/disassociate interrupt handlers to a particular GPIO pin) the API provides its own functions, which operate on the pin’s rtems chain of handlers.
Possible improvements
Currently all interrupt driven actions are performed in threads, meaning that between the interrupt and the actual action some time have passed by. This behavior can be made optional by making the threaded handling an option, such that non threaded handlers would be called from ISR context. Having the two options allow for faster processing of an interrupt if the action it fires is small, or a delayed processing if the handler needs to block or can potentially take some time (both not compatible with ISR-time execution, where the system is only focused in the interrupt handling).
Another point is the amount of tasks/threads required in threaded handling, as pointed before.
API functions
API initializing
rtems_status_code rtems_gpio_initialize(void);
This function initializes the pin state structure and creates the bank locks. It uses an atomic flag to ensure that it is initialized only once.
GPIO group functions
rtems_gpio_group *rtems_gpio_create_pin_group(void); rtems_status_code rtems_gpio_define_pin_group( const rtems_gpio_group_definition *group_definition, rtems_gpio_group *group ); uint32_t rtems_gpio_read_group(rtems_gpio_group *group); rtems_status_code rtems_gpio_write_group( uint32_t data, rtems_gpio_group *group ); rtems_status_code rtems_gpio_group_bsp_specific_operation( rtems_gpio_group *group, void *arg );
API functions for configuration tables:
rtems_status_code rtems_gpio_request_configuration( const rtems_gpio_pin_conf *conf ); rtems_status_code rtems_gpio_multi_select( const rtems_gpio_pin_conf *pins, uint8_t pin_count ); rtems_status_code rtems_gpio_update_configuration( const rtems_gpio_pin_conf *conf );
rtems_gpio_request_configuration and rtems_gpio_update_configuration only parse the given configurations and perform the required API calls. Because the API knows the current state of every pin, it can compare the current state with the new one received through rtems_gpio_update_configuration and update it accordingly (if valid, of course).
rtems_gpio_multi_select allow several pins to be configured at the same time, or in the worst case, in a single API call. This worst case scenario may happen if the platform does not provide the needed information for multiple pin selection, in which case the API will cycle through the configurations, one by one.
Apart from multiple pin selection other configurations such as pull resistors or interrupt handling details is done a pin at a time (only the pin function selection is done in parallel). The advantage is that while a pin function selection takes usually two register accesses by a platform (read the selection register and change the required bits while maintaining the other pins data untouched), this function can use those two accesses to select a whole select bank. Without this function each pin selection would (or will if a platform happens to not support it) have access the registers two times per pin.
Also note that since each write to a bank implies a write to the corresponding register, multiple pin selection can only help if the pins being selected belong to the same selection bank, as access to multiple selection banks will always result in separate calls per selection bank.
GPIO digital I/O functions:
rtems_status_code rtems_gpio_multi_set( uint32_t *pin_numbers, uint32_t pin_count ); rtems_status_code rtems_gpio_multi_clear( uint32_t *pin_numbers, uint32_t pin_count ); uint32_t rtems_gpio_multi_read( uint32_t *pin_numbers, uint32_t pin_count ); rtems_status_code rtems_gpio_set(uint32_t pin_number); rtems_status_code rtems_gpio_clear(uint32_t pin_number); uint8_t rtems_gpio_get_value(uint32_t pin_number);
API direct operations:
rtems_status_code rtems_gpio_request_pin( uint32_t pin_number, rtems_gpio_function function, bool output_enable, bool logic_invert, void *bsp_specific ); rtems_status_code rtems_gpio_resistor_mode( uint32_t pin_number, rtems_gpio_pull_mode mode ); rtems_status_code rtems_gpio_interrupt_handler_install( uint32_t pin_number, rtems_gpio_irq_state (*handler) (void *arg), void *arg ); rtems_status_code rtems_gpio_enable_interrupt( uint32_t pin_number, rtems_gpio_interrupt interrupt, rtems_gpio_handler_flag flag, rtems_gpio_irq_state (*handler) (void *arg), void *arg ); rtems_status_code rtems_gpio_interrupt_handler_remove( uint32_t pin_number, rtems_gpio_irq_state (*handler) (void *arg), void *arg );
rtems_status_code rtems_gpio_disable_interrupt(uint32_t pin_number);
These functions allow for manual configuration of pins within the API, and stand as an alternative for the configuration based functions which parse a configuration and call these functions as needed.
API pin release functions
rtems_status_code rtems_gpio_release_pin(uint32_t pin_number); rtems_status_code rtems_gpio_release_configuration( const rtems_gpio_pin_conf *conf ); rtems_status_code rtems_gpio_release_multiple_pins( const rtems_gpio_pin_conf *pins, uint32_t pin_count ); rtems_status_code rtems_gpio_release_pin_group( rtems_gpio_group group );
These functions allow pin repurposing, by declaring to the API that a given pin or pins are now available for another function. In hardware terms the pin state will remain unchanged, with the only exception being if the pin has interrupts enabled, in which case interrupts are disabled, the interrupt_state pointer in the GPIO internal tracking structure is freed. Pull-up resistor state and the current pin function stay the same in hardware.
Software debouncing function:
rtems_status_code rtems_gpio_debounce_switch( uint32_t pin_number, int ticks );
The switch debouncing function only applies to digital input pins with interrupts enabled. It works by recording the clock tick of the previously handled interrupt, and by requiring a certain number of clock ticks to pass before allowing the next interrupt to be handled.
Tristate pins
Tristate refers to the ability of a pin to be both a digital input or output pin, by switching the directions as needed. Since this may be BSP specific, a BSP may have this capability by using the BSP_SPECIFIC function.
BSP functions
Although the API is set to be generic it should not disregard platform-specific features. The API provides a series of function headers which a BSP will need to implement in order for the API to work with it, but at the same time it also allows platform specific data to reach BSP code and be processed by the BSP without bloating the API with features that most platforms do not support.
BSP/platform specific functionalities
To keep the API generic it only operates the two defining functions of a GPIO pin: digital input and output. However, most platforms rely on GPIO pins to perform functions ranging from ADCs to data buses such as I2C or SPI. To account for these additional functions (which may or may not exist on a given platform), the API provides hooks so applications/drivers can use them. For these the API role is limited to pin tracking, synchronization and interrupt management. The setup and control code for the BSP specific function is left to the BSP to implement.
The API also allows BSP specific data to be used with digital input and output pins, but the processing of that data is left to the BSP implementation.
This data is sent through void pointers, meaning that a BSP can define a data structure that can hold any information that it may require from an application, and then document it so GPIO users on that platform know what to send, and what to expect. Any BSP specific detail has to be documented by the BSP itself.
The list of functions that a BSP must implement is as follows:
Multiple pin operation functions:
rtems_status_code rtems_gpio_bsp_multi_set( uint32_t bank, uint32_t bitmask ); rtems_status_code rtems_gpio_bsp_multi_clear( uint32_t bank, uint32_t bitmask ); uint32_t rtems_gpio_bsp_multi_read(uint32_t bank, uint32_t bitmask);
These functions receive a bitmask with the pins that are to be operated in the given bank. Most implementations of these should be a simple write of the given bitmask to the given bank’s operation register. If not the bitmask may also be iterated.
Although the function receives a 32-bit mask, if the BSP defined a GPIO bank to have less than 32 pins then that will be the size of the bitmask.
Single pin operation function:
rtems_status_code rtems_gpio_bsp_set(uint32_t bank, uint32_t pin); rtems_status_code rtems_gpio_bsp_clear(uint32_t bank, uint32_t pin); uint8_t rtems_gpio_bsp_get_value(uint32_t bank, uint32_t pin);
Same as the previous functions, but only applying to a single pin.
GPIO pin function selection:
rtems_status_code rtems_gpio_bsp_select_input( uint32_t bank, uint32_t pin, void *bsp_specific ); rtems_status_code rtems_gpio_bsp_select_output( uint32_t bank, uint32_t pin, void *bsp_specific ); rtems_status_code rtems_bsp_select_specific_io( uint32_t bank, uint32_t pin, uint32_t function, void *pin_data );
The select input and output functions should assign the given pin with the corresponding GPIO function in the platform’s hardware. The bsp_specific void pointer behavior is defined by the BSP being used. In the case of an platform specific GPIO function the BSP should define an integer identifier to refer to each function within the BSP, and that is the code used by an application to refer to it. This code is received as the “function” parameter in the rtems_bsp_select_specific_io function, and the pin_data void pointer is to be used to send additional data (same as the bsp_specific pointer in the other functions).
GPIO pull resistor setup:
rtems_status_code rtems_gpio_bsp_set_resistor_mode( uint32_t bank, uint32_t pin, rtems_gpio_pull_mode mode );
Pull resistors can only be configured on a single pin basis.
Interrupt operations:
The following functions should retrieve the current interrupt status on a given bank or interrupt vector (the API retrieves the vector number through the rtems_gpio_bsp_get_vector function), or clear any pending interrupts on a given vector.
uint32_t rtems_gpio_bsp_interrupt_line(rtems_vector_number vector); void rtems_gpio_bsp_clear_interrupt_line( rtems_vector_number vector, uint32_t event_status ); rtems_vector_number rtems_gpio_bsp_get_vector(uint32_t bank);
Functions to enable/disable interrupts on a given GPIO pin should also be implemented.
rtems_status_code rtems_bsp_enable_interrupt( uint32_t bank, uint32_t pin, rtems_gpio_interrupt interrupt ); rtems_status_code rtems_bsp_disable_interrupt( uint32_t bank, uint32_t pin, rtems_gpio_interrupt interrupt );
The following two functions may be implemented by just returning RTEMS_NOT_DEFINED, meaning that although they are not required for the API to work, it will have some limitations.
rtems_status_code rtems_gpio_bsp_multi_select( rtems_gpio_multiple_pin_select *pins, uint32_t pin_count, uint32_t select_bank );
If the BSP does not support multiple pin function selection the API will always do the selection one pin at a time. Supposing that an application tries to create a group with 10 output pins (for instance), having this function could do it in 2 register access. Otherwise it would result in 20 accesses.
rtems_status_code rtems_gpio_bsp_specific_group_operation( uint32_t bank, uint32_t *pins, uint32_t pin_count, void *arg );
If a BSP does not provide support any additional GPIO function then the implementation of this function is not necessary, and any attempt to create a GPIO group with bsp specific functions will result on an error.