A simple device control I/O function handler example

Updated: April 19, 2023

The client's devctl() call is formally defined as:

#include <sys/types.h>
#include <unistd.h>
#include <devctl.h>

int
devctl (int fd,
        int dcmd,
        void *dev_data_ptr,
        size_t nbytes,
        int *dev_info_ptr);

We should first understand this function before we look at the resource manager side of things. The devctl() function is used for “out of band” or “control” operations. For example, you may be writing data to a sound card (the actual digital audio samples that the sound card should convert to analog audio), and you may decide that you need to change the number of channels from 1 (mono) to 2 (stereo), or the sampling rate from the CD-standard (44.1 kHz) to the DAT-standard (48 kHz). You may also find that you need to change the sample rate for recording sound from 22kHz to 44.1kHz depending on the application. The devctl() function is the appropriate way to do this. When you write a resource manager, you may find that you don't need any devctl() support at all and that you can perform all the functionality needed simply through the standard read() and write() functions. You may, on the other hand, find that you need to mix devctl() calls with the read() and write() calls, or indeed that your device uses only devctl() functions and does not use read() or write().

The devctl() function takes these arguments:

fd
The file descriptor of the resource manager that you're sending the devctl() to.
dcmd
The command itself—a combination of two bits worth of direction, and 30 bits worth of command (see discussion below).
dev_data_ptr
A pointer to a data area that can be sent to, received from, or both.
nbytes
The size of the dev_data_ptr data area.
dev_info_ptr
An extra information variable that can be set by the resource manager.

The top two bits in the dcmd encode the direction of data transfer, if any. For details, see the description in the I/O reference section (under Device control I/O function handler).

When the _IO_DEVCTL message is received by the resource manager, it's handled by your device control I/O function handler. Here is a very simple example, which we'll assume is used to set the number of channels and the sampling rate for the audio device we discussed above:

/*
 * io_devctl1.c
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/neutrino.h>
#include <sys/iofunc.h>
#include <devctl.h>

#define AUDIO_DAC 1
#define AUDIO_ADC 2

extern void audio_set_nchannels(unsigned num);
extern uint32_t audio_get_samplerate(unsigned port);
extern void audio_set_samplerate(unsigned port, unsigned rate);

#define DCMD_AUDIO_SET_CHANNEL_MONO         __DION(_DCMD_MIXER, 1)
#define DCMD_AUDIO_SET_CHANNEL_STEREO       __DION(_DCMD_MIXER, 2)
#define DCMD_AUDIO_GET_SAMPLE_RATE_DAC      __DIOF(_DCMD_MIXER, 4, uint32_t)
#define DCMD_AUDIO_SET_SAMPLE_RATE_DAC      __DIOT(_DCMD_MIXER, 5, uint32_t)
#define DCMD_AUDIO_GET_SAMPLE_RATE_ADC      __DIOF(_DCMD_MIXER, 6, uint32_t)
#define DCMD_AUDIO_SET_SAMPLE_RATE_ADC      __DIOT(_DCMD_MIXER, 7, uint32_t)

int
io_devctl (resmgr_context_t *ctp, io_devctl_t *msg,
           iofunc_ocb_t *ocb)
{
    int     sts;

    /* 1) Create the reply buffer at the start of ctp->msg so we make
     * sure we have enough space */
    struct _io_devctl_reply *reply = (struct _io_devctl_reply *)ctp->msg;

    /* Create a pointer to the rate variable for the _GET_ devctls. */
    unsigned nbytes = 0;
    uint32_t *rate = _DEVCTL_DATA(*reply);

    /* 2) Verify we have the entire devctl header in the buffer */
    if( ctp->size < sizeof(msg->i) ) {
        return EBADMSG;
    }

    /* 3) See if it's a standard devctl() */
    if ((sts = iofunc_devctl_default (ctp, msg, ocb)) !=
        _RESMGR_DEFAULT)
    {
        return (sts);
    }

    /* How many bytes did the client send in addition to the devctl header? */
    size_t payload_size = ctp->size - sizeof(msg->i);

    /* 4) See which command it was, and act on it */
    switch (msg -> i.dcmd) {
    case DCMD_AUDIO_SET_CHANNEL_MONO:
        /* Read or write access are sufficient */
        if( !(ocb->ioflag & (_IO_FLAG_WR | _IO_FLAG_RD)) ) {
            return EPERM;
        }
        audio_set_nchannels (1);
        break;

    case DCMD_AUDIO_SET_CHANNEL_STEREO:
        /* Read or write access are sufficient */
        if( !(ocb->ioflag & (_IO_FLAG_WR | _IO_FLAG_RD)) ) {
            return EPERM;
        }
        audio_set_nchannels (2);
        break;

    case DCMD_AUDIO_GET_SAMPLE_RATE_DAC:
        /* Write access is required for accessing the DAC */
        if( !(ocb->ioflag & _IO_FLAG_WR) ) {
            return EPERM;
        }

        /* Verify that the client wants enough bytes */
        if( ctp->info.dstmsglen < (sizeof(*reply) + sizeof(uint32_t)) ) {
            return EINVAL;
        }

        /* Set up the reply to the client */
        nbytes = sizeof(uint32_t);
        *rate = audio_get_samplerate (AUDIO_DAC);
        break;

    case DCMD_AUDIO_SET_SAMPLE_RATE_DAC:
        /* Write access is required for accessing the DAC */
        if( !(ocb->ioflag & _IO_FLAG_WR) ) {
            return EPERM;
        }

        /* Verify that we got all the bytes. */
        if( payload_size < sizeof(uint32_t) ) {
            return EBADMSG;
        }

        rate = _DEVCTL_DATA(msg->i);
        audio_set_samplerate (AUDIO_DAC, *rate);
        break;

    case DCMD_AUDIO_GET_SAMPLE_RATE_ADC:
        /* Read access is required for accessing the ADC */
        if( !(ocb->ioflag & _IO_FLAG_RD) ) {
            return EPERM;
        }

        /* Verify that the client wants enough bytes */
        if( ctp->info.dstmsglen < (sizeof(*reply) + sizeof(uint32_t)) ) {
            return EINVAL;
        }
        
        /* Set up the reply to the client */
        nbytes = sizeof(uint32_t);
        *rate = audio_get_samplerate (AUDIO_ADC);
        break;

    case DCMD_AUDIO_SET_SAMPLE_RATE_ADC:
        /* Read access is required for accessing the ADC */
        if( !(ocb->ioflag & _IO_FLAG_RD) ) {
            return EPERM;
        }

        /* Verify that we got all the bytes. */
        if( payload_size < sizeof(uint32_t) ) {
            return EBADMSG;
        }

        rate = _DEVCTL_DATA(msg->i);
        audio_set_samplerate (AUDIO_ADC, *rate);
        break;

    /* 5) In case it's a command that we don't recognize, fail it */
    default:
        return (ENOSYS);
    }

    /* 6) Tell the client that it worked */
    memset(reply, 0, sizeof(*reply));
    SETIOV (ctp->iov, reply, sizeof(*reply) + nbytes);
    return (_RESMGR_NPARTS (1));
}
Step 1
We set up where the reply is going to be written to. The client is expecting the response in an _io_devctl_reply structure, with (depending on the value of dcmd) the sample rate immediately following the structure in memory. This response is going to take sizeof(msg->o) + sizeof(uint32_t) bytes. The buffer that we have and can use for sending replies is ctp->msg and it is ctp->msg_max_size bytes long. Because it's the same buffer that we used for receiving the commands from the client, we need to be careful to finish processing the input before we start writing the output to the buffer. The devctl() message from the client is sitting at offset ctp->offset into the buffer ctp->msg. If we have enough space between ctp->offset and ctp->msg_max_size for the entire reply, we could use that. However, it's much safer to just write our reply at the beginning of the buffer, which gives us the full ctp->msg_max_size bytes of data to write our response.
Step 2
We see again the use of a helper function, this time iofunc_devctl_default(), which is used to perform all default processing for devctl(). The iofunc_devctl_default() function is called when you don't supply your own device control I/O function handler and let iofunc_func_init() initialize the I/O and connect functions tables for you.

We include iofunc_devctl_default() in our device control I/O function handler function because we want it to handle all the regular devctl() cases for us. We examine the return value; if it's not _RESMGR_DEFAULT, then this means that the iofunc_devctl_default() function “handled” the request, so we just pass along its return value as our return value.

If the constant _RESMGR_DEFAULT is the return value, then we know that the helper function didn't handle the request and that we should check to see if it's one of ours.

Step 3
The checking for our custom devctl() commands is done here. We simply check the dcmd values that the client code sent to see if there's a match. Note that we call the fictitious functions audio_set_nchannels(), audio_get_samplerate() and audio_set_samplerate() to accomplish the actual “work” for the client.

As part of our processing of the various audio devctl() commands, we need to check permissions. If the client is getting or setting the sample rate of the digital to analog converter (DAC), which is output, then they need to have opened the resource manager handle for writing. If the client is setting the sample rate of the analog to digital converter (ADC), which is input, then they need to have opened the resource manager handle for reading. Note here that the permissions being checked are related to whether the DAC or ADC is being accessed, not based on whether devctl() is sending or receiving data from the client (i.e., the use of __DIOF and __DIOT does not correlate with read and write permissions).

In devctl() calls that set sample rate, we also need to make sure that the client sent enough data. The length checks at the beginning of the devctl() function are not sufficient because each devctl() might be expecting the client to send a different amount of data. We do the full check for data now that we know exactly how much data to expect. Once we've done this check, we can safely access this data and set the sample rate.

In devctl() calls that get sample rate, we make sure the client has enough space to receive the response from the resource manager.

Step 4
This step is simply good defensive programming. We return an error code of ENOSYS to tell the client that we didn't understand their request.
Step 5
If we get to this line, we successfully handled the dcmd passed to devctl(). We need to respond to the client. We return a value to the resource manager library encoded by the macro _RESMGR_NPARTS(), telling it that we're returning a one-part IOV. This value is then returned to the client.

Instead of using the macro _RESMGR_NPARTS(), we could have used the _RESMGR_PTR() macro:

    /* 6) tell the client it worked */
    memset(reply, 0, sizeof(*reply));
    return (_RESMGR_PTR (ctp, reply, sizeof(*reply) + nbytes));
  

The devctl() command requires us to send a response to the client for devctl() to be successful. It is insufficient to just return EOK without any response message.