Stub io-char driver

The following example demonstrates a fully functional io-char driver, which generates data by repeatedly returning the next character from the a-z sequence. Once started, io-char creates a new controller thread in the background. The main thread then exits, returning control to the calling process (e.g., shell), and the controller thread remains running until stopped by a signal.

The driver uses io-char to handle all client communication, using a resource manager that creates a new character device under /dev/demo1, as shown in the following diagram:
Figure 1io-char client communication

Starting the driver

To start the driver, call devc-*, devc-ser*. You can use the following options that aren't listed in the documentation:
  • -V - increments verbosity level. You can specify this option multiple times:
    • 0 times — default; no output is generated.
    • 1 time (-V) — transmitted data is printed to standard output.
    • 2 times (-VV or -V -V) — baud, line control, and line status call messages are printed to standard output.
  • -p - process priority of the resource manager.
In the following code example, a client reads data from /dev/demo1, and the driver repeatedly returns the data (abcdefghijklmnopqrstuvwxyz) one character at a time (all commands must be executed as root user):
# ./devc-demo -V
# pidin Ar
...
854450196 ./devc-demo -V
...
# cat /dev/demo1
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnop...
[interrupted by CTRL+C]
# echo "Hello, World!" > /dev/demo1
H# ello, World!
[output mangled because driver is printing to standard output at the same time a shell]

Driver implementation

This driver is implemented using three functions:

  • main() - the main entry point of the driver executable, which initializes devices and driver, starts the driver running, and exits.
  • tto() - the required implementation that handles data to be sent.
  • pulse_handler() - a function that is called by a timer pulses that simulates receiving incoming data.

The following code provides a simple implementation that writes data to /dev/demo1 and simulates input by generating a new character four times per second:

/*
 * Copyright (c) 2025, BlackBerry Limited. All rights reserved.
 *
 * BlackBerry Limited and its licensors retain all intellectual property and
 * proprietary rights in and to this software and related documentation. Any
 * use, reproduction, disclosure or distribution of this software and related
 * documentation without an express license agreement from BlackBerry Limited
 * is strictly prohibited.
 */


/* This is a completely "demo" devc-driver, written to illustrate the use of
 * the io-char library, rather than to do any work.  By stripping out
 * all hardware work, or reducing it to the bare minimum, it should more 
 * clearly illustrate the use of the io-char framework.
 * 
 * It will be driven off a timer "txing"/"rxing" one byte per firing of the
 * 1ms timer.  Outgoing data will be sent nowhere, but it will be "drained"
 * from the output queue according to the io-char library, as expected.
 * Similarly, input data will be "made-up" from nowhere.
 * 
 * With the default ticksize of 1ms, this will give a "nominal" baud rate
 * of 8 kbaud.
 */

#include <stdio.h>
#include <errno.h>
#include <signal.h>
#include <malloc.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/neutrino.h>
#include <sys/dispatch.h>
#include <termios.h>
#include <devctl.h>
#include <sys/dcmd_chr.h>
#include <sys/iomsg.h>
#include <atomic.h>
#include <sys/io-char.h>
#include <sys/iofunc.h>
#include <sched.h>

#define DEFAULT_DRIVER_PRIO 24
int priority = DEFAULT_DRIVER_PRIO;

int verbose;

TTYDEV demodev;
/* ttyctrl MUST be this name cause the dev_lock()/dev_unlock() macros have
 * this name hard-coded into them.
 */
TTYCTRL ttyctrl;
TTYINIT demoinit;

/* Wake up periodically and generate stub input data. */
int pulse_handler (message_context_t *ctp, int code, unsigned flags, void *handle);

int main( int argc, char ** argv )
{
    int ret;
    int opt;
    int prio;
    unsigned unit;
    int self_coid;
    struct sigevent ev;
    struct itimerspec itime;
    timer_t timer_id;
    int timer_pulse_code;
   
    /* Since many serial drivers create multiple devices, a common initializer
     * structure is created and filled with the defaults for this driver, then
     * potentially modified by common or "standard" devc-* options in the 
     * options() parsing code.  This structure will be passed to ttc when each
     * device structure is initialized and used to set the parameters for it.
     * 
     * init_ttyinit() fills the TTYINIT structure in with our values.
     */    
    TTYINIT demo_defaults = {
        0,            /*  port */
        0,            /*  port_shift */
        0,            /*  intr */
        8000,         /*  baud */
        2048,         /*  isize, size of input buffer */
        2048,         /*  osize, size of output buffer */
        256,          /*  csize, size of canonical input line buffer for edit
		               *  mode
					   */
        0,            /*  c_cflag */
        0,            /*  c_iflag */
        0,            /*  c_lflag */
        0,            /*  c_oflag */
        0,            /*  fifo */
        1000,         /*  clk  (is this meaningful for non-serial?) */
        1,            /*  div  (is this meaningful for non-serial?) */
        "/dev/demo",  /*  name */
    };

    demoinit = demo_defaults;

	/*  Initialize the device init to raw mode. */
    ttc(TTC_INIT_RAW, &demoinit, 0);
   
    /* IO_CHAR_SERIAL_OPTIONS specifies the standard oi-char options, and
     * ttc( TTC_SET_OPTION, ...) handles those options, updating the ttyinit
     * structure to reflect any changes.
     */
    while((opt = getopt(argc, argv, IO_CHAR_SERIAL_OPTIONS "Vp:")) != -1) {
        switch(ttc(TTC_SET_OPTION, &demoinit, opt)) {
            case 'V':
                verbose++;
                break;
            case 'p':
                prio = atoi( optarg );
                if (prio < 1 || prio > 253)
                {
                    printf("invalid prio %d requested, out of range\n", prio );
                } else
                {
                    priority = prio;
                }
                break;          
        }
    }
  
    /* Among other things, TTC_INIT_PROC does a dispatch_create(), so the dpp
     * in the ttyctrl structure is valid after this call.  This is useful
     * for a pulse_attach() to run code driven by an interrupt or timer.
     */
    ttyctrl.max_devs = 1;
    ret = ttc(TTC_INIT_PROC, &ttyctrl, priority);
    if( ret == -1 )
    {
        perror( "ttc:TTC_INIT_PROC");
    }
                 
    /* Setup the device description structure based on the initializer
	 * structure and allocate needed buffers and do other per-device
	 * initialization.
     */
    demodev.ibuf.head = demodev.ibuf.tail = demodev.ibuf.buff = malloc(demodev.ibuf.size = demoinit.isize);
    if( ! demodev.ibuf.buff )
    {
        perror( "malloc");
        exit( EXIT_FAILURE );
    }
    demodev.obuf.head = demodev.obuf.tail = demodev.obuf.buff = malloc(demodev.obuf.size = demoinit.osize);
    if( ! demodev.obuf.buff )
    {
        perror( "malloc");
        exit( EXIT_FAILURE );
    }
    demodev.cbuf.head = demodev.cbuf.tail = demodev.cbuf.buff = malloc(demodev.cbuf.size = demoinit.csize);
    if( ! demodev.cbuf.buff )
    {
        perror( "malloc");
        exit( EXIT_FAILURE );
    }
    demodev.highwater = demodev.ibuf.size - (demodev.ibuf.size < 128 ? demodev.ibuf.size/4 : 32);
    strcpy(demodev.name, demoinit.name);
    demodev.baud = demoinit.baud;
    demodev.fifo = demoinit.fifo;
    demodev.flags = EDIT_INSERT | LOSES_TX_INTR;
    demodev.c_cflag = demoinit.c_cflag;
    demodev.c_iflag = demoinit.c_iflag;
    demodev.c_lflag = demoinit.c_lflag;
    demodev.c_oflag = demoinit.c_oflag;

    /* Initialize termios cc codes to an ANSI terminal. */
    ret = ttc(TTC_INIT_CC, &demodev, 0);
    if( ret == -1 )
    {
        perror( "ttc:TTC_INIT_CC");
    }

    /* Initialize the device's name. Assume that the basename is set in device
	 * name.  This will attach to the path assigned by the name and unit
	 * combined.
	 */
    unit = SET_NAME_NUMBER(1) | NUMBER_DEV_FROM_USER;
    ret = ttc(TTC_INIT_TTYNAME, &demodev, unit);
    if( ret == -1 )
    {
        perror( "ttc:TTC_INIT_TTYNAME");
    }

    /* Setup our "hardware" here. */
    
    /* Register a handler for pulses. */
    timer_pulse_code = pulse_attach (ttyctrl.dpp, MSG_FLAG_ALLOC_PULSE, 0, pulse_handler, &demodev);
    
    /* Create a connection to the channel that our resource manager is
	 * receiving on. */
    self_coid = message_connect (ttyctrl.dpp, MSG_FLAG_SIDE_CHANNEL);
     
    /* This macro fills in the event structure. */
    SIGEV_PULSE_INIT(&ev, self_coid, priority, timer_pulse_code, 0);

    /* Create and arm a 1 ms timer to deliver the pulse (to be handled by the
	 * pulse_handler() function). */    
    ret = timer_create(CLOCK_REALTIME, &ev, &timer_id);
    if( ret == -1 )
    {
        perror( "timer_create");
        exit( EXIT_FAILURE );
    }
    itime.it_value.tv_sec = 1; /* one second startup delay */
    itime.it_value.tv_nsec = 0;
    itime.it_interval.tv_sec = 0;
    itime.it_interval.tv_nsec = 250*1000*1000; /*  250 ms */
    ret = timer_settime(timer_id, 0, &itime, NULL);
    if( ret == -1 )
    {
        perror( "timer_settime");
        exit( EXIT_FAILURE );
    }

    /* If the rest of our setup has succeeded, register our pathname... */
    ret = ttc(TTC_INIT_ATTACH, &demodev, 0);
    if( ret == -1 )
    {
        perror( "ttc:TTC_INIT_ATTACH");
    }
    /* ...and start the driver main loop. */
    ttc(TTC_INIT_START, &ttyctrl, 0);
    
    return 0;
}

/* tto is called by io-char when a client call needs processing, usually
 * write() or devctl()
 */
int tto(TTYDEV *dev, int action, int arg1) {
    int status = 0;
    TTYBUF *bup = &dev->obuf;
    char c;
    
    switch(action) {
    case TTO_STTY:
        /* Implement any stty changes.  All of the new state has already been
		 * updated in the dev structure, so we would just have to implement
		 * the new state.
		 */
        if (verbose > 1)
        {
            printf("got a stty request, baud rate is now: %d\n", dev->baud );
        }
        break;
    case TTO_CTRL:
        /* Implement line control, if appropriate. */
        if (verbose > 1)
        {
            printf("we got a line control request, could change DTR/RTS/etc\n");
        }
        break;
    case TTO_LINESTATUS:
        /* Implement line status info, if appropriate. */
        if (verbose > 1)
        {
            printf("we got a line status request, should check & return the line status\n");
        }
        break;
    case TTO_DATA:
        /* This is the new data to tx, from the IO_WRITE handler. */ 
        if (bup->cnt > 0)
        {
            if(!((dev->flags & (OHW_PAGED|OSW_PAGED)) && !(dev->xflags & OSW_PAGED_OVERRIDE)))
			{
				/* Get the next byte to transmit. */
				dev_lock(dev);
				c = tto_getchar( dev );
				dev_unlock(dev);
		
				/* Print the character.  Here you would do something with c,
				 * the character hw_out_somehow(c).  Currently, only output to
				 * stdout if verbose, otherwise just drop ignore.
				 */
				if(verbose) {
					write(1, &c, 1);
				}

				/* Clear the OSW_PAGED_OVERRIDE flag as we only want
				 * one character to be transmitted in this case.
				 */
				if (dev->xflags & OSW_PAGED_OVERRIDE) {
					atomic_clr(&dev->xflags, OSW_PAGED_OVERRIDE);
				}
			}
        }

        /* Check if any clients need to be notified, and return appropriate
		 * bits if they do. */
        status = tto_checkclients( dev );
        break;
    default:
	    /* Ignore everything else. */
        break;
    }

    return status; 
}

/* This function is called whenever a pulse is received. */
int pulse_handler (message_context_t *ctp, int code, unsigned flags, void *handle)
{
    TTYDEV *dev = handle;
    int status = 0;
    TTYBUF *bup = &dev->obuf;
    char c;
    static int count = -1;
    count = (count+1) % 26;
    
    /* Handle rx. */
    if(! (dev->flags & (IHW_PAGED|ISW_PAGED)))
    {
        /* If not input flow controlled, generate data. */
        status |= tti( dev, ('a'+count) );
        /* Of course if really flow controlled, wouldn't be getting data. */
    }

    if( status ) {
      /* Wakeup io-char to process something, if needed. */
      iochar_send_event( dev );
    }
    
    return 0;   
}

This driver:

  • Can be compiled using qcc -V gcc_ntoaarch64le main.c -lio-char -lsmmu -lsecpol.

  • Has a main() function that:

    • initializes the TTYINIT struct.

    • calls ttc() to set device to raw mode.

    • uses TTC_SET_OPTION to parse default devc-* and devc-ser* options.

    • parses custom options.

    • uses TTC_INIT_PROC to initialize the io-char library and set its priority.

    • initializes the TTYDEV struct.

    • uses TTC_INIT_TTYNAME to setup device name.

    • initializes stub hardware communication.

    • uses TTC_INIT_ATTACH to register driver device path.

    • uses TTC_INIT_START to run the driver.

  • Provides implementation of the tto() function, which:

    • sets the baud rate.

    • sets line control.

    • checks the line status.

    • uses tto_getchar() to get data from output buffer.

    • sends data to stub hardware.

    • uses tto_checkclients() to check if clients need to be notified.

  • Provides implementation that receives data from stub hardware on a timer pulse that:

    • generates a byte.

    • uses tti() to send data to input buffer.

    • notifies clients.

Page updated: