How to Develop a GPIO Resource Manager?

icon

ABOUT THE AUTHOR

Picture of Bhavin Sharma
Bhavin Sharma
Bhavin Sharma is a Linux kernel and BSP engineer specializing in embedded systems development for platforms like Rockchip and i.MX8. He possesses deep expertise in device drivers, board bring-up, and boot time optimisation. He is an active Linux kernel contributor.

Recent market research shows that the global market for embedded systems will grow to more than USD 169 billion by 2030.(MVR) This growth will be fueled by new developments in automation, the Internet of Things (IoT), and real-time computing.

As embedded platforms get more complicated, it becomes very important to manage General-Purpose Input/Output (GPIO) well to keep the system stable, fast, and safe.

GPIO ports are the main way that hardware and software talk to each other. They control sensors, indicators, and other devices that are connected to the computer. But if you don’t manage GPIO usage, it can lead to resource conflicts, inefficiency, and even system crashes.

To fix this, engineers use a GPIO Resource Manager, which is a structured solution that makes it easier to assign, monitor, and control pins. Not only does it make sure that resources are used in a planned way, but it also makes operations safer, especially in embedded environments based on QNX.

This blog shows a seven-step process for making a GPIO Resource Manager. It shows how careful design and process management can help Product Engineering Services work better.

What is a GPIO Resource Manager?

A GPIO Resource Manager is a program that lets client apps set GPIO pins as inputs or outputs and read or change their states. It hides the complicated hardware details and gives higher-level applications a stable API to safely use GPIO resources.

Seven-Step Process for Creating a GPIO Resource Manager

To provide a brief overview, the purpose of this resource manager is to offer an interface for clients, specifically applications, to configure GPIOs as input or output and to read or write their current state.

Let’s start by examining the headers utilized within this resource manager.

				
					#include <sys/dispatch.h>
#include <sys/iofunc.h>
#include <sys/mman.h>
#include <sys/neutrino.h>
#include <devctl.h>
#include <sys/iomsg.h
				
			

Here are the key variables required for resource manager:

				
					static resmgr_connect_funcs_t    connect_funcs;
static resmgr_io_funcs_t         io_funcs;
static iofunc_attr_t             attr;

/* declare variables we'll be using */
resmgr_attr_t        resmgr_attr;
dispatch_t           *dpp;
dispatch_context_t   *ctp;
int                  id;
				
			

Let’s proceed by initializing the necessary structures and registering our resource manager. First, request I/O privileges by passing the flag _NTO_TCTL_IO to the ThreadCtl() function as shown in the below snap.

				
					ThreadCtl( _NTO_TCTL_IO, 0 );
				
			

Now, let’s proceed with the steps outlined earlier to create our resource manager.

  1. Initialize the dispatch interface:

    To establish a mechanism for clients to send messages to the resource manager, we need to initialize the dispatch interface, as demonstrated in the snap below.

				
					 /* initialize dispatch interface */
 dpp = dispatch_create_channel(-1, DISPATCH_FLAG_NOLOCK );
 if (dpp == NULL) {
     fprintf(stderr, "Unable to allocate dispatch handle.\n");
     return EXIT_FAILURE;
 }
				
			

DISPATCH_FLAG_NOLOCK: Used to omit locking considering that the mapping of message types to corresponding handlers is static upon initialization.

-1: ID of existing channel can be passed instead; however, in this case, we are creating a new channel.

  1. Initialize the resource manager attributes:

    These attributes are to be passed while registering the pathname. The snap below demonstrates how to initialize these attributes.

				
					/* initialize resource manager attributes */
memset(&resmgr_attr, 0, sizeof resmgr_attr);
resmgr_attr.nparts_max = 1;
resmgr_attr.msg_max_size = 2048;
				
			

.nparts_max: Number of buffers an application code (or client) should be replied with.

.msg_max_size: The size of message buffer required for any communication with resource manager.

  1. Initialize functions to be used for handling messages:

    Upon calling iofunc_func_init(), both the provided structures— connect_funcs and connect_funcs —are populated with default handlers for connection and I/O messages, respectively. To utilize our custom message handler function, we need to override the default handler. As demonstrated in the code snap below, a custom function pointer is passed to handle functionalities of device control commands (devctl).

				
					/* initialize functions for handling messages */
iofunc_func_init(_RESMGR_CONNECT_NFUNCS, &connect_funcs, _RESMGR_IO_NFUNCS, &io_funcs);
io_funcs.devctl = io_devctl;
				
			

Simplify GPIO control on QNX with our custom Resource Manager solutions.

_RESMGR_CONNECT_NFUNCS: This represents the number of entries to be populated in the connect table. Typically, the value of _RESMGR_CONNECT_NFUNCS

&connect_funcs: This is a pointer to the resmgr_connect_funcs_t structure, which is used to obtain the default handler functions.

_RESMGR_IO_NFUNCS: This indicates the number of entries to be filled in the I/O table. Generally, the value of _RESMGR_IO_NFUNCS is passed here.

&io_funcs: This is a pointer to the resmgr_io_funcs_t structure, which retrieves the default handler functions for I/O operations.

  1. Initialize the attribute structure used by the device:

    The attribute structure holds essential information about the specific device linked to the pathname. This structure includes details such as permissions, the device type, as well as the owner and group IDs. Below is a code snap that demonstrates how to initialize the attribute structure for the device associated with /dev/gpio.

				
					/* initialize attribute structure used by the device */
iofunc_attr_init(&attr, 0664, 0, 0);
				
			

&attr: A pointer to the iofunc_attr_t structure that will be initialized.

0644: This value specifies the type and access permissions to be assigned to the resource.

0: This can be NULL, or a pointer to an iofunc_attr_t structure to be used to initialize the structure pointed to by attr.

0: This can also be NULL, or a pointer to a _client_info structure that contains the information about a client connection.

  1. Put a name into the namespace:

    Before a resource manager can receive messages from other programs, it must notify the process manager that it is responsible for a specific pathname prefix. This is accomplished through pathname registration. Once the name is registered, other processes can locate and connect to the resource manager using the registered name. Below is a code snap to perform pathname registration.

				
					/* attach our device name */
id = resmgr_attach(
        dpp,            /* dispatch handle        */
        &resmgr_attr,   /* resource manager attrs */
        "/dev/gpio",  /* device name            */
        _FTYPE_ANY,     /* open type              */
        0,              /* flags                  */
        &connect_funcs, /* connect routines       */
        &io_funcs,      /* I/O routines           */
        &attr);         /* handle                 */

if(id == -1) {
    fprintf(stderr, "Unable to attach name.\n");
    return EXIT_FAILURE;
}


				
			

dpp: Pointer to initialized dispatch interface handle.

&resmgr_attr: Pointer to resource manager attributes.

/dev/gpio: Pathname to be attached with the resource manager.

_FTYPE_ANY: File type for open request to be handled.

/dev/gpio: Pathname to be attached with the resource manager.

0: Flags controlling the pathname resolution.

&connect_funcs: Pointer to initialized connect function structure.

&io_funcs: Pointer to initialized io function structure.

&attr: Pointer to the structure used to associate with the pathname.

  1. Allocate the context structure:

    The context structure contains a buffer that is used for receiving messages. The size of this buffer is determined by the value specified during the initialization of the resource manager attribute structure. Below is the code snap demonstrating the allocation of the context structure.

				
					/* allocate a context structure */
ctp = dispatch_context_alloc(dpp);
				
			

dpp: Pointer to initialized dispatch interface handle.

  1. Start the resource manager message loop:

    The loop monitors for any incoming messages; therefore, we detach it from the controlling terminal and run it in the background as a system daemon, as shown in the code snippet below.

				
					procmgr_daemon(0, PROCMGR_DAEMON_NOCLOSE | PROCMGR_DAEMON_NODEVNULL);
				
			

0: Status to be returned to the parent process.

PROCMGR_DAEMON_NOCLOSE: If this flag isn’t set, the API will close all file descriptors except for standard input, output, and error.

PROCMGR_DAEMON_NODEVNULL: If this flag isn’t set, the API redirects standard input, output and error to /dev/null.

Once the resource manager establishes its name, it begins receiving messages whenever a client program attempts to perform an operation. The resource manager receives these messages within the dispatch_block() function. Then we call dispatch_handler(), which decodes the incoming message and invokes the appropriate handler function based on the connect and I/O function tables that were previously passed in. Below is the snap of the message loop.

				
					while(1) {
        if((ctp = dispatch_block(ctp)) == NULL) {
        fprintf(stderr, "block error\n");
        return EXIT_FAILURE;
        }
        dispatch_handler(ctp);
}
				
			

The above steps focused on how a resource manager is created. Now, let’s take a look at how the previously assigned custom io_devctl handler function is implemented.

Handling io_devctl messages

This section provides valuable insights into the implementation of devctl messages for a GPIO resource manager, though it doesn’t delve deeply into the details of handling these messages.

First, some custom commands need to be defined. These commands will be used by the client when requesting operations to be performed via the registered path. Below is the code snap for the same.

				
					#define DCMD_GPIO_SET_MODE                __DIOT(_DCMD_MISC, 0, gpio_info_t)
#define DCMD_GPIO_READ                    __DIOTF(_DCMD_MISC, 1, gpio_info_t)
#define DCMD_GPIO_WRITE                   __DIOTF(_DCMD_MISC, 2, gpio_info_t)
				
			

gpio_info_t: A structure used to store information about any GPIO during communication.

_DCMD_MISC: Macro for miscellaneous devctl command.

0,1,2: Values that the io_devctl handler will receive upon processing the defined messages.

__DIOT: Macro used to define commands that pass information to the device.

__DIOTF: Macro for defining commands that facilitate both passing information to and retrieving it from the device.

When the resource manager receives a devctl message, it invokes the io_devctl handler. Let’s take a closer look at its implementation.

Within the handler, it is essential to verify that the received message does not contain any of the default commands. If it does, there is no need for further processing as shown in the below snap. However, if the message contains any of our defined commands, those commands must be handled by us. Below is the snap of the function handler implemented.

				
					int io_devctl(resmgr_context_t *ctp, io_devctl_t *msg, RESMGR_OCB_T *ocb) {
        int status, nbytes;
        gpio_info_t *io_msg;

        if ((status = iofunc_devctl_default(ctp, msg, ocb)) != _RESMGR_DEFAULT) {
                return status;
        }

        status = 0; nbytes = 0;

        io_msg = _IO_INPUT_PAYLOAD(msg);

        switch (msg->i.dcmd) {

        case DCMD_GPIO_SET_MODE:
                status = set_io_mode(io_msg);
        break;

        case DCMD_GPIO_READ:
                status = read_io(io_msg);
                if (status < 0)
                        break;
                msg->o.ret_val = 0;
                return _RESMGR_PTR(ctp, &msg->o,sizeof(msg->o) + sizeof(gpio_info_t));

        case DCMD_GPIO_WRITE:
                status = write_io(io_msg);
                if (status < 0)
                        break;
                msg->o.ret_val = 0;
                return _RESMGR_PTR(ctp, &msg->o,sizeof(msg->o) + sizeof(gpio_info_t));

        default:
                        return ENOSYS;
        }

        if (status < 0) {
                return EINVAL;
        }

        _RESMGR_STATUS(ctp, 0);
        return _RESMGR_NPARTS(0);
}


				
			

Below is the breakdown of the entire code:

Ø First, we called iofunc_devctl_default() to handle any default messages and checked if a command was received that this function does not handle.

Ø If a non-default command is received, we obtain the pointer to the payload address and verify if the command matches any of those defined earlier.

Ø If it does, we invoke the appropriate functions to process the command and take the necessary actions.

Ø If the command is DCMD_GPIO_SET_MODE, we set the GPIO mode as specified in the payload of the message.

Ø If the command is DCMD_GPIO_READ, we read the status of the GPIO indicated in the payload.

Ø If the command is DCMD_GPIO_WRITE, we configure the GPIO to output mode and write the specified output as per the received payload.

To test this resource manager, a client application is needed that can access the pathname and send io_devctl messages to the resource manager. In this implementation, we have not provided handlers for read or write operations; thus, those messages will be managed by the default handlers.

This approach demonstrates one way to implement a resource manager and provide an interface for clients to access devices. However, developers can tailor a resource manager to their specific requirements by creating custom message handlers and determining how to process the received messages.

Enhance hardware performance through optimized GPIO handling on QNX

Building Custom Resource Managers in QNX for Stability and Flexibility

Thus, by retrieving the outlined steps, developers are capable of intercepting new, efficient resource managers (device drivers) that run directly, not calling the QNX kernel. This modular structure enables resource managers to operate at the level of user space processes, thus improving QNX operating system extensibility and dependability for product engineering services.

An important strength of QNX is this microkernel approach: adding or replacing resource managers does not affect the integral system. Drivers are created and tested as any conventional application; therefore, their creation is easy and hardly ever causes a system collapse. Moreover, each resource manager runs in a separate protected address space, which means that no bug or failure can proliferate throughout the OS.

This isolation is however especially useful where system reliability is paramount as in mission-critical applications. Resource management in QNX is equally efficient whether it is devoted to hardware devices or custom filesystems, allowing for completely safe resource control consequently making them essential for QNX RTOS, safety-critical, and embedded systems.

Conclusion

Ready to Build Reliable QNX-Based Systems?

Silicon Signals is an expert in providing end to end Product Engineering Services, from customizing BSPs and developing drivers to fully integrating QNX-based systems. Our engineers have worked with real-time and safety-critical applications to build custom resource managers, improve embedded performance, and make sure that everything stays stable.

Partner with Silicon Signals if you want to make your product more reliable, scalable, and quick to market. Let’s turn your embedded vision into a safe, ready-to-use solution for production.

Get in touch with us today to talk about your next QNX or embedded development project.

Request a Free Consultation

Interested in collaborating with Silicon Signals on your next big idea? Please contact us and let us know how we can help you.