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.
Starting the driver
- -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.
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.
-
