Learn how to add your own custom commands to your QNX network driver using lessons learned from a real project described below.

QNX defines a set of IOCTL commands that a driver must implement in order to support PTP. These are defined in usr/include/netdrvr/ptp.h

#define PTP_GET_RX_TIMESTAMP    0x100  /* get RX timestamp */ 
#define PTP_GET_TX_TIMESTAMP    0x101  /* get TX timestamp */ 
#define PTP_GET_TIME            0x102  /* get time */ 
#define PTP_SET_TIME            0x103  /* set time */ 
#define PTP_SET_COMPENSATION    0x104  /* set compensation */ 
#define PTP_GET_COMPENSATION    0x105  /* get compensation */

Along with the IOCTL codes above there are data structures that define the format of the data transferred by each command. For example, the get/set time commands use the following:

/* get/set time */ 
typedef struct { 
    int32_t sec;    /* ptp clock seconds */ 
    int32_t nsec;   /* ptp clock nanoseconds */ 
ptp_time_t;

These commands are used by the PTP daemon to implement the PTP protocol.

We wanted to add the following commands to extend the PTP capability:

  • Enable 1PPS (1 Hz pulse-per-second) output
  • Disable 1PPS
  • Set Tx and Rx latency adjustment

This was done by defining our own IOCTL codes and data structures:

#define PTP_ENABLE_PPS    0x200  /* PPS output enable            */ 
#define PTP_DISABLE_PPS   0x201  /* PPS output disable           */ 
#define PTP_SET_LATENCY   0x202  /* Set Tx/Rx latency adjustment */ 

typedef struct { 
    uint32_t tx;    /* Tx path latency (in nanoseconds) */ 
    uint32_t rx;    /* Rx path latency (in nanoseconds) */ 
ptp_latency_t; 

The PTP IOCTL commands are sent by the application to the driver using the driver-specific command SIOCSDRVSPEC, which takes a struct ifdrv object as an argument. This is defined iusr/include/net/if.h

struct  ifdrv { 
    char          ifd_name[IFNAMSIZ]; /* if name, e.g. "en0"    */ 
    unsigned long ifd_cmd;            /* command code           */   
    size_t        ifd_len;            /* length of data buffer  */   
    void         *ifd_data;           /* pointer to data buffer */ 
};

We created a user-space utility to issue the commands. A network device does not have an associated device node in the file system so an IOCTL is sent via a socket created as follows:

int s = socket(AF_INET, SOCK_DGRAM, 0);

The PPS enable/disable commands do not require any data so these were straight-forward to implement. All that was required was a struct ifdrv object.

static int enable_pps(int schar *if_nameint enable) 
{ 
    struct ifdrv ifd; 

    strncpy(ifd.ifd_name, if_name, sizeof(ifd.ifd_name)); 

    ifd.ifd_cmd  = (enable) ? PTP_ENABLE_PPS : PTP_DISABLE_PPS; 
    ifd.ifd_len  = 0; 
    ifd.ifd_data = NULL; 

    if (ioctl(s, SIOCGDRVSPEC, &ifd) == -1) { 
            warn("SIOCGDRVSPEC %s"ifd.ifd_name); 
            return (-1); 
    } 

    return 0; 
}

At the destination the driver’s IOCTL handler extracts the pointer to the IOCTL data. The OS takes care of ensuring that the pointer makes sense in the receiving address space.

int lan743x_ioctl(struct ifnet *ifpunsigned long cmdcaddr_t data) 
{ 
    lan743x_dev_t    *lan743x; 
    struct ifdrv     *ifd; 
    int               result = EOK; 

    if (ifp == NULL) { 
        return EINVAL; 
    } 

    lan743x = ifp->if_softc; 

    switch (cmd) { 
    case SIOCSDRVSPEC:          /* Driver-specific set command */ 
    case SIOCGDRVSPEC:          /* Driver-specific get command */ 

        ifd = (struct ifdrv *)data; 
        result = lan743x_drvspec_ioctl(lan743x, ifd); 
        break; 
...

The situation becomes more complicated when the IOCTL command includes a data buffer. The pointer to this buffer is in the context of the calling address space (the user-space utility) but will not make sense at the driver, which operates in the context of the io-pkt network stack process.

The QNX ioctl() reference page has the following statement about commands with data buffers:

In QNX Neutrino 6.5.0 and later, ioctl() handles embedded pointers, so you don’t have to use ioctl_socket() instead.

This lead me to believe that the buffer address would be automatically translated. This turned out to be an incorrect assumption but more about that later.

The latency adjustment command in the user-space application was set up as follows:

static int ptp_latency_set(int      s, 
                           char    *if_name, 
                           unsigned tx_latency 
                           unsigned rx_latency) 
{ 
    struct { 
        struct ifdrv  ifd; 
        ptp_latency_t latency; 
    } cmd; 

    struct ifdrv  *ifd = &cmd.ifd; 
    ptp_latency_t *adj = &cmd.latency; 

    strncpy(ifd->ifd_name, if_name, sizeof(ifd->ifd_name)); 

    ifd->ifd_cmd  = PTP_SET_LATENCY; 
    ifd->ifd_data = adj; 
    ifd->ifd_len  = sizeof(*adj); 

    adj->tx = tx_latency; 
    adj->rx = rx_latency; 
 
    if (ioctl(s, SIOCSDRVSPEC, &cmd) == -1) { 
            warn("SIOCSDRVSPEC %s"ifd->ifd_name); 
            return (-1); 
    } 

    return 0; 
}

At the driver the latency values were extracted as follows:

#define IFD_DATA(ifd)           (((uint8_t *)ifd) + sizeof(*ifd)) 
 
static int lan743x_ptp_copyin(void *dstconst void *srcsize_t length) 
{ 
    int result = EOK; 

    if (ISSTACK) { 
        result = copyin(src, dst, length); 
    } else { 
        memcpy(dst, src, length); 
    } 

    return result; 
} 

static int lan743x_ptp_latency_update(lan743x_dev_t *lan743xstruct ifdrv *ifd) 
{ 
    ptp_latency_t latency; 

    if (ifd->ifd_len != sizeof(latency)) { 
        return EINVAL; 
    } 

    uint32_t *data = (void *)IFD_DATA(ifd); 

    /* 
     * Get the adjustment value. 
     */ 
    int result = lan743x_ptp_copyin(&latency, IFD_DATA(ifd), sizeof(latency)); 

    if (result == EOK) { 
        uint32_t tx_latency = latency.tx; 
        uint32_t rx_latency = latency.rx; 
... 
    } 

    return EOK; 
}

When this code was run no errors were reported but the PTP latency were garbage. Investigating further we discovered the latency values were not being copied into the buffer in the driver address space.

Going back to the ioctl() documentation we looked more closely at the following:

Some ioctl() commands map to calls to fcntl()tcgetattr()tcsetattr(), and tcsetsid(). Other commands are transformed into devctl() commands, and the rest are simply passed to devctl(). Here’s a summary (for details, see the Devctl and Ioctl Commands reference):

The SIOCSDRVSPEC is one of the commands passed to devctl(). This command is defined as follows:

int devctl( int     filedes, 
            int     dcmd, 
            void   *dev_data_ptr, 
            size_t  n_bytes, 
            int    *dev_info_ptr );

The first three parameters are those passed to ioctl(). The n_bytes parameter is defined as 

The size of the data to be sent to the driver, or the maximum size of the data to be received from the driver. 

Changing the ioctl() command above to the following devctl() command fixed the problem and passed the latency data values to the driver:

    if (devctl(s, SIOCSDRVSPEC, &cmd, sizeof(cmd), NULL) == -1) { 
            warn("SIOCSDRVSPEC %s"ifd->ifd_name); 
            return (-1); 
    }

To understand what was going we contacted QNX technical support who provided the following useful explanation of what was happening “under the hood”:

API call order:
ioctl -> (special handling for spcific cmds or ioctl_handler) ->devctl -> MsgSend (for one part iov)/MsgSendv (for multi part iov)

This tells us that the devctl() constructs a message to send to the network stack. We can do the same in our application. The following code replaces the devctl() call shown above.

    io_devctl_t msg; 
    iov_t       iov[2]; 
  
    msg.i.type        = _IO_DEVCTL; 
    msg.i.combine_len = sizeof(msg.i); 
    msg.i.dcmd        = SIOCSDRVSPEC; 
    msg.i.nbytes      = sizeof(cmd); 
    msg.i.zero        = 0; 

    // Setup data to the device. 
    SETIOV(&iov[0], &msg.i, sizeof(msg.i)); 
    SETIOV(&iov[1], &cmd,   sizeof(cmd)); 

    if (MsgSendv(s, iov, 2, NULL, 0) == -1) { 
            warn("SIOCSDRVSPEC %s", ifd->ifd_name); 
            return -1; 
    }

This clearly reveals the message passing architecture of QNX operating system.