QNX 4 Device Drivers Introduction When you need to provide an interface to a traditional form of character device (modem, terminal or printer) which will be accessed by programs which expect to see a 100% POSIX 1003.1 interface, then you probably need to write a Dev Driver process. The POSIX interface includes services such as modem control, line discipline, baud rates, parity, echo, line editing, timed reads, interbyte timing, etc. All of these services are provided for and managed by the QNX4 Dev process. QNX4 Dev Drivers simply provide the hardware interface between Dev and the hardware using a private, highly optimized interface. The only responsibility of a QNX4 Dev Driver is to provide access to the hardware. The real work is done by Dev itself. Before we start, it should be pointed out that Dev Drivers are not the only method of interfacing to hardware devices in QNX4. In fact, unless your device looks very much like a typical serial device, you probably don't want to write a Dev Driver. Many types of hardware are better handled in QNX4 by a server process which responds to custom messages directly from client processes. Remember that any process can access the hardware if it has sufficient privity, so such server processes are remarkably easy to develop, and are often much easier to develop than a Dev Driver (see the Client/Server technical note for more information). If the best way to access a piece of hardware is deemed to be through standard I/O calls (open, close, read and write), then a message-driven client/server approach may not be appropriate. Nonetheless, a Dev Driver may still not be the best approach. A special form of server process can be written which adopts a portion of the QNX4 namespace (eg: /dev/tape) and which responds to a well defined set of I/O messages (IO_OPEN, IO_CLOSE, etc.) This type of server will be an I/O Manager and can provide highly optimized I/O directly to the hardware. Such servers allow for specialized non-POSIX functionality and this approach avoids the possibly unnecessary overhead of communicating through another process; Dev. (see the I/O Manager technical note for more information). This technical note addresses the case where POSIX style character I/O is required. This includes a vast number of devices such as multiport, intelligent serial adapters, windows-based terminal emulators, printer drivers and many, many more. Overview QNX is a modular operating system. As such, various system services are provided by independent server processes. In the case of POSIX 1003.1 compatible devices (character special files), I/O services are provided by a common Device Server process called Dev. Dev owns the part of the namespace beginning with the prefix '/dev' and receives messages from processes which wish to do I/O operations on pathnames beginning with this prefix. All POSIX features and side-effects are managed exclusively by Dev. including blocking and non-blocking reads and writes, timed read operations, flow control, echo, line editing, etc. The interface to the hardware devices themselves is provided by a series of Dev Driver processes. These drivers communicate directly with the hardware and interface with Dev via a set of three data queues as shown in Figure 1 below: Dev output buffer Driver ÚÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄ¿ ³///³ÄÄÄÄÄÄÄÄÄÄÄ>³||||||||||||||||||||||||³ÄÄÄ>³\\\\³ ³///³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³\\\\³ ³///³ input buffer ³\\\\³ ³///³ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³\\\\³ ³///³<ÄÄÄÄÄÄÂÄÄÄij|||||||||||||||||||||||³<ÄÄÄij\\\\³ ³///³ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³\\\\³ ³///³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³\\\\³ ³///³ canonical (edit) buffer ³ ³\\\\³ ³///³ ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ³\\\\³ ³///³<ÄÄÄÄÄÄÄÄÄÄij||||||||||||³<ÄÄÄÄÄÄÄÄÄÙ ³\\\\³ ³///³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÙ ³\\\\³ ÀÄÄÄÙ ÀÄÄÄÄÙ Figure 1: Communication via queues between Dev and a Device Driver The function of a QNX4 Dev Driver (unlike that of a more classic UNIX device driver, or even a QNX2 device driver) is simply to transmit data bytes to the hardware device and to receive data bytes from the hardware and insert them into the raw input queue. Aside from a few (optional) control functions, there is little else required of a QNX4 Dev Driver. QNX4 Dev Drivers are normal QNX processes compiled to run at privity 1 in order to gain access to the hardware I/O registers. These drivers can be started and stopped dynamically. When a driver starts, it attempts to register itself and allocate resources from Dev by calling the dev_drvr_register () function. At this time the driver indicates how many devices it will be handling, what prefix the devices will be given and some housekeeping information. Provided that there is sufficient resources available, Dev will reply with a driver control structure which contains generic information which will be used by the driver in subsequent calls. Included in this structure is the address of a function to call (called tti) which must be used to add data into the input data queue. Included in the parameters passed to the dev_drvr_register () function is the address of three routines in the driver's space which can be called by Dev to indicate significant events. The Kick () function will be called by Dev whenever something happens with the output buffer (ie: data available). The Stty () function is called by Dev whenever something happens to the termios (device control) structure which requires the attention of the driver (such as change in baud rate or parity). The Ioctl () function is called to perform general control operations (sendbreak, dropline, etc.) The driver must then call dev_drvr_mount () for each of the devices. In this call, the driver must supply the individual sizes for each of the input, output and canonical buffers. Each call to dev_drvr_mount () can include one or more devices. If more than one device is specified, then the library routine will attempt to pack the I/O buffers into as few shared memory segments as possible which is a good way to conserve system resources. Provided that the resources can all be acquired, Dev will reply with a device control structure for each device. This structure will contain pointers to the three data queues as well as a unique tty# (handle) for each device. This tty# must be used when calling the tti () function to identify which input queue is to receive the data. Finally, once all devices are registered and mounted, it is necessary to enable (arm) each of the output channels by calling the dev_drvr_arm () function for each device. This will complete the registration process and will allow Dev to start placing data in the output queues. The driver is now ready for its mainline mode of operation. A final function is available for releasing all of the resources assigned to a Dev Driver before it terminates. The dev_drvr_shutdown () call is used just before a driver terminates. Dev Drivers should catch the SIGTERM signal and perform an orderly shutdown which allows the slay utility to be used to cleanly terminate a driver. Processing Input Data Most device drivers in QNX4 will be interrupt driven when it comes to input data. For this reason, the facilities provided by Dev are geared towards allowing efficient and convenient data input directly from interrupt handlers. QNX4 interrupt handlers are capable of triggering a proxy when they return. The tti () function accepts as parameters a tty# (indicating the device), a 16-bit input value comprised of 8 data bits and 8 control bits, and a value which can be used to access Dev's data segment from within the context of the interrupt handler. The function tti () will either return a zero or it will return the process-id of a proxy which should be triggered to wake up Dev. An interrupt handler simply has to return (in ax) the return value of tti () to have this effect. If the tti () function is called at process time, then it will be necessary to Trigger () the proxy if a non-zero value is returned. The tti () function has been designed to be as efficient as possible and simply places the data into the raw input queue. It returns either a zero or a proxy if a pre-conditioned event has been triggered. Dev set up these pre-conditions based on pending I/O requests. Some of these conditions depend on the relationship between received data bytes and timers and may in fact cause further conditions to be armed, so it is necessary to call tti () for every byte received. The bottom byte of the data word passed to tti () is the character received. The top byte contains flags indicating special conditions: Good Data 00000000 data H/W Break 10000000 00000000 Bad Data 01000DFP data H/W Event 00100000 0000chnf D-Data Overrun c-carrier received F-Framing Error h-hangup (loss of carrier) P-Parity Error n-hardware handshaking ON f-hardware handshaking OFF Processing Output Data Output starts when the Kick () function is called. It is the responsibility of the driver to examine the output buffer on every Kick () call and attempt to output any data which is available. A pointer to the output buffer which has changed is passed as the only argument Kick (). Control Operations Dev will call the Stty () function when it requires the driver to change some aspect of the hardware (such as baud rate, data bits, stop bits or parity). The new settings can be found using the pointer to a termios structure which is passed as the only argument to Stty (). Dev will call the Ioctl () function whenever it requires some special action of the driver (such as assert or remove a break condition, or start/stop a dropline condition). It will also call Ioctl () to provide the driver with useful information (such as first open or last close). Special Modes and Options A flag word is passed as one of the arguments to dev_drvr_register () which can contain bits indicating special modes of operation. These bits are described in the manual page for the dev_drvr_register () function. Compiling the Driver The interface between Dev and a Dev Driver is designed to consume as few resources as possible while still maintaining a high speed, secure environment. Both Dev and the Dev Driver far-call into the other's code space. While operating in this code space, the interface makes available one data segment for accessing global variables. The dev_driver_register () process ensures that LDT aliases exist for the appropriate code and data segments. It should be noted that the following restrictions apply within the interface functions Kick (), Stty () and Ioctl (): 1. Only global variables in the same data segment as the variable drv_ctrl (which was passed to dev_drvr_register) can be accessed. 2. Only functions in the same code segment as the interface routine (Kick, Stty or Ioctl) can be called. 3. In general, no system functions or library routines can be used other than Trigger (). 4. When an interface routine is called by Dev, it will be using the stack of Dev. Thus stack usage should be kept to a minimum and one should not assume that SS=DS. 5. Stack checking must be disabled. The above rules are easiest to enforce if the Dev Driver is compiled in 16-bit (or 32-bit) small model with stack checking disabled. As a rule of thumb, the interface routines should do nothing more than modify global variables, optionally trigger a proxy, then return. If more work must be done, it is recommended that a proxy be triggered which will wake up the Dev Driver process which can then perform the bulk of the work at process time. This avoids having to deal with the above restrictions. Dev Drivers must be compiled with privity 1 (-T1). Library Routine Reference dev_drvr_register ----------------------------------------------------------- Synopsis #include #include int dev_drvr_register( char *name, char *prefix, int nttys, int (far *Kick) ( struct output_buffer far *), int (far *Stty) (struct termios far *) int (far *Ctrl) (union ioctl_entry far *) struct driver_ctrl *dry_ctrl, unsigned flags ); Description The dev_drvr_register function is used to create new character special devices. 'name' is a symbolic name to be associated with this class of device. This will be the string found in the driver_type member of a _dev_info_entry which is returned by dev_info and is displayed as the device Type by the stty utility. Name cannot exceed 16 characters. 'prefix' is the string to be used as the first part of a device name. All devices created as a result of this call will have names in the /dev directory which start with this prefix and end in a number (eg: registering 3 devices with the prefix 'tst' will create 3 devices called '/dev/tst1', '/dev/tst2' and '/dev/tst3'. prefix cannot exceed 8 characters. 'nttys' is the number of devices (or units) to be created with this prefix. 'drv_ctrl' is a pointer to a driver_ctrl structure which will be filled if the function is successful. This structure contains information which is common to all the newly created devices. The structure is defined in and includes the following members: short int base_tty The 'internal' tty number assigned to the first device. The remaining devices are assigned sequential numbers. short int nttys The number of devices belonging to this handle. pid_t (far *tti) A far pointer to a function (inside (short, short, unsigned) Dev's address space) which must be used to enter data into the input queue. The first argument is the internal tty number of the device (base + unit - 1). The second argument is the received data character. The third argument is a data selector to use when calling tti (). The function returns either a zero (do nothing) or a proxy which should be Triggered to 'kick' Dev. short unsigned tti_ds A data selector to be used as the third argument to tti(). pid_t hw_pid A proxy to trigger Dev when a significant event occurs. short unsigned flags A bitfield of flag bits associated with this new device class. The bits are defined in . At the time of this printing the following bits are defined: DRVR_GETS_PIDS IOCTL_OPEN passes down pid, fd, etc. (this feature did not exist until QNX 4.10). 'flags' is used to control optional modes of driver operation. The flag bits are defined in . At the time of this printing, the following driver flags are defined: DRVR_SCAN open () with no suffix causes Dev to scan for an unused unit. (eg: /dev/con free console). DRVR_NOTTY Not a terminal device (eg: /dev/null). If set, this flag causes the togetattr () function to fail, which cause isatty () to fail. DRVR_LOCK_HFLOW IHFLOW/OHFLOW in c_cflag cannot be changed, even by stty. DRVR_SINGLE_OPEN Open () will fail if the device is already opened. DRVR_HEX_SUFFIX Suffixes are 00 based hex rather than 1 based decimal (eg: Dev.pty). DRVR_STTY Driver gets called on every tcsetattr () call rather than just when baud/stop-bits/ parity are changed. DRVR_NO_SUFFIX Suppress the numeric suffix if only 1 device is registered (eg: /dev/null will be displayed instead of /dev/null1). 'Kick' is the address of a function to be called by Dev every time something new happens with the output queue (new data or flow control change). Kick will be passed a single parameter which is a far pointer to the output buffer which has changed. 'Stty' is the address of a function to be called by Dev every time something significant happens to the termios control structure associated with this device. Unless the DRVR_STTY mode is enabled, significant events are deemed to be only those which require some attention by the hardware (baud rate, parity, etc.). Stty will be passed a single parameter which is a far pointer to the termios structure which has changed. 'Ctrl' is the address of a function to be called by Dev to perform control (or housekeeping) operations. Ctrl will be passed a single parameter which is a far pointer to an ioctl_entry which is a multi-purpose structure containing a control operation. In general, it is permissible to simply ignore these commands, but more sophisticated drivers may be able to use this information to provide extra services or features. The ioctl_entry is defined in . The first short int defines the type of the operation. The second short int is usually a unit number (1 ... N) which indicates which of the devices owned by this driver is being operated on. On return, it is expected that the driver will replace the first word with a status (0 usually meaning success). At the time of this printing, the following types are defined: IOCTL_OPEN Called on first open(). IOCTL_CLOSE Called on last close(). IOCTL_CTL Called by qnx_ioctl(). IOCTL_STARTBREAK Assert break condition (if possible). IOCTL_STOPBREAK Remove the break condition. IOCTL_STARTDROP Lower modem control (ie: drop communication line). IOCTL_STOPDROP Raise modem control (return to normal). Returns The dev_drvr_register function returns a (positive integer) handle to be used with subsequent calls to dev_drvr_mount and dev_drvr_shutdown. Errors When an error has occurred, the function returns a negative value indicating the type of error that has been detected. (-1) Unable to communicate with Dev. (-1) Insufficient resources. See Also dev_drvr_mount, dev_drvr_arm, dev_drvr_shutdown Example Classification QNX dev_drvr_mount ------------------------------------------------------------ Synopsis #include int dev_drvr_mount (int handle, int tty, int nttys, struct device_ctrl *ctrl ); Description The dev_drvr_mount function is used to create nttys instances of new character special devices. The value in tty is the starting internal tty number for the first device in the group being mounted (based on the base_tty value returned in the drv_ctrl structure). An array of initialized device_ctrl entries is passed to this function (one for each of the nttys). Each entry on input should define the following structures: short unsigned isize Size of input buffer to allocate. short unsigned osize Size of output buffer to allocate. short unsigned csize Size of canonical (edit) buffer to allocate. This call will attempt to allocate all the resources necessary for this entire group of devices. Where possible, buffers will be packed into as few memory selectors as possible, to conserve resources. The buffers and control structures will all be made addressable by both this driver and Dev. If successfully mounted, the function will return a zero and the remaining device control entries will be filled for each device. At the time of this printing, the following members are defined: short int tty Internal tty number of this unit. struct input_buffer far *ibuf Pointer to raw input buffer. struct output_buffer far *obuf Pointer to raw output buffer. struct canon_buffer far *cbuf Pointer to edit buffer. struct termios far *termios Pointer to device control structure. struct stat far *stat Pointer to device's directory entry. The value of tty in this device_ctrl structure is the value to use as the first argument to tti () when entering data for this unit. obuf is a pointer to an output buffer control structure which contains information required for outputting data. ibuf, cbuf, termios and stat are all provided for informational purposes, but need not actually be used directly by the driver. Returns The dev_drvr_mount function returns a zero if unsuccessful. dev_drvr_arm -------------------------------------------------------------- Synopsis #include int dev_drvr_arm( int tty ); Description The dev_drvr_arm function is used to enable a newly created character special device. No data will be placed in the (already established) device output buffer by Dev until this call is made. 'tty' is the internal tty number (base+unit-1) of the device being enabled. Returns The dev_drvr_arm function returns a zero if successful Errors When an error has occurred, the function returns a negative value indicating the type of error that has been detected. (-1) No prior call to dev_drvr_register. (-1) Error communicating with Dev. (-1) Bad tty. See Also dev_drvr_register, dev_drvr_mount, dev_drvr_shutdown Example Classification QNX When an error has occurred, the function returns a negative value indicating the type of error that has been detected. (-1) No prior call to dev_drvr_register. (-2) Unable to allocate the memory for the requested device buffers. (-4) Bad handle. (-4) Unable to install the new devices. See Also dev_drvr_register, dev_drvr_arm, dev_drvr_shutdown Example Classification QNX dev_drvr_shutdown -------------------------------------------------------------- Synopsis #include int dev_drvr_shutdown (unsigned handle); Description The dev_drvr_shutdown function is used to release all resources associated with the character special devices associated with handle. This function must be called before a device driver terminates. Returns The dev_drvr_shutdown function returns zero if unsuccessful. Errors When an error has occurred, the function returns a negative value indicating the type of error that has been detected. (-1) No prior call to dev_drvr_register. (-1) Bad handle. See Also dev_drvr_register, dev_drvr_mount, dev_drvr_arm Example Classification QNX