Table of Contents

Programming Device Drivers in Minix

Introduction

This tutorial helps you to get started with programming device drivers on Minix in C. A device driver is a computer program which interacts with real hardware components. For example, the computer which you use now to read this web page has a device driver for the display monitor. Another example is the disk driver, which reads and writes raw data from disk(s) in your computer. Clearly, computer systems would be useless without good device drivers.

Requirements

Developing a device driver requires programming skills and knowledge of the target device(s). On Minix, device drivers are programmed in the C language. In this tutorial we assume that you know the basics of the C programming language, and that you have a working Minix system, > version 3.2.1.

Getting Started

The following sections contain step-by-step instructions to try out programming simple device drivers.

Please note that while this page may be slightly outdated, an up-to-date version of the hello driver is part of the MINIX3 source tree. You can find it in /usr/src/minix/drivers/examples/hello. The rest of this document will assume that the driver is not yet there, and will take you through the steps to create the driver yourself. Example 1 shows how to set up the infrastructure and create a very basic working driver process. Example 2 extends it into a simple character driver. The MINIX3 source tree's hello.c corresponds to the second example.

Example 1: Hello, World

Let's try the most simple driver possible first – the Hello World device driver. It has only one task: print a hello message on startup, and then terminate. :-)

First create a directory in the Minix source tree to place the source code of our driver. As root, execute:

# cd /usr/src/minix/drivers/examples
# mkdir hello
# cd hello

To compile the device driver, we need a Makefile. You can use the following code as your Makefile to compile the driver:

Makefile:

# Makefile for the hello driver.
PROG=   hello
SRCS=   hello.c
 
FILES=${PROG}.conf
FILESNAME=${PROG}
FILESDIR= /etc/system.conf.d
 
DPADD+= ${LIBCHARDRIVER} ${LIBSYS}
LDADD+= -lchardriver -lsys
 
MAN=
 
.include <minix.service.mk>

Then, create a C program file with the hello world program:

hello.c:

#include <stdio.h>
#include <stdlib.h>
#include <minix/syslib.h>
 
int main(int argc, char **argv)
{
    sef_startup();
 
    printf("Hello, World!\n");
    return EXIT_SUCCESS;
}

Ignore the sef_startup() call for now. We'll explain its purpose later.

In addition, we must create a hello.conf file. It contains the permissions for the service. Each device driver typically only needs to access one real hardware device, and uses a few functions provided by Minix. To give our simple hello try-out driver enough permissions to experiment with, create hello.conf with:

hello.conf:

service hello
{
        system
                UMAP            # 14
                IRQCTL          # 19
                DEVIO           # 21
        ;
        ipc
                SYSTEM PM RS LOG TTY DS VM VFS
                pci inet amddev
                ;
    uid 0;
};

Now try to see if everything compiles correctly:

# make clean
# make
# make install

If you did not receive any errors, it is time to try and run the device driver. Starting, stopping and restarting device drivers must be done using the minix-service(8) command. To start the hello program, enter the following command:

#minix-service up /service/hello
Hello, World!
RS: restarting /service/hello, restarts 0

And what a surprise, it displays the Hello, World! message :-) in the kernel log. Check with writing the contents of /var/log/messages. Stop the driver with:

#minix-service down hello

We received another message as well from the so-called Reincarnation Server (RS). In Minix as a microkernel, device drivers are separate programs which send and receive message to communicate with the other operating system components. Device drivers, like any other program, may contain bugs and could crash at any point in time. The Reincarnation server will attempt to restart device drivers when it notices they are abruptly killed by the kernel due to a crash, or in our case when they exit(2) unexpectedly. You can see the Reincarnation Server in the process list as rs, if you use the ps(1) command. The Reincarnation Server sends keep-a-live messages to each running device driver on the system periodically, to ensure they are still responsible and not i.e. stuck in an infinite loop.

So how do we deal with this? We let our hello driver reply to the keep-a-live messages, so RS will correctly detect it is still running. Fortunately, as we will see in the next example, there is a library libchardriver on Minix. This library takes care of various tasks common to all character drivers. For example, it performs the communication with the Reincarnation Server and the operating system components transparently.

In contrast, one functionality that is directly exposed to the device driver developer is the initialization protocol. When a new device driver (or any other system service) is started, RS will send the driver an initialization message with information on how to initialize properly. The driver is expected to reply back with OK (initialization completed successfully) or with an error (initialization failed). In the former case, RS will assume the new device driver has been started correctly. In the latter case, RS will immediately shut down the driver without attempting to restart it. The user will be informed with a message on the console. Fortunately, the initialization protocol is completely hidden in the System Event Framework (SEF) that exposes library calls to let developers handle initialization in an easy and effective way.

At the very beginning of the code in main() the developer has to register one or more initialization callbacks and then let SEF do the rest by calling sef_startup(). This is the reason why we inserted a call to sef_startup() in the code above. Each callback is nothing but a function that receives initialization data as input and returns a status code to determine the result of the initialization process. As of now, developers can register callbacks for three types of initializations: fresh (when a service is started the first time), live update (when a service is dynamically updated to a new version), restart (when a service is restarted after a crash or a controlled restart).

Example 2: /dev/hello

In this example we will extend the hello driver and re-implement it using libchardriver. Instead of just printing a hello on startup, we now want to use a device file /dev/hello to read the Hello World message. Each character and block driver is associated with a major device number. Thus, we need to pick a free major device number for the device–one that is not already in use for another device driver. We will use the major ID 17 in this example, which is assigned to the hello driver as it is (see minix/dmap.h). We start by making the /dev/hello file:

# mknod /dev/hello c 17 0

This command creates a character device with major number 17 and minor number 0 (the minor number is not important for the hello driver), named /dev/hello.

You can reuse the Makefile from Example 1. Now create the hello.h header file:

hello.h:

#ifndef __HELLO_H
#define __HELLO_H
 
/** The Hello, World! message. */
#define HELLO_MESSAGE "Hello, World!\n"
 
#endif /* __HELLO_H */

As you can see, it contains a pre-processor macro for the message. Now place this source code in the hello.c file:

hello.c:

#include <minix/drivers.h>
#include <minix/chardriver.h>
#include <stdio.h>
#include <stdlib.h>
#include <minix/ds.h>
#include "hello.h"
 
/*
 * Function prototypes for the hello driver.
 */
static int hello_open(devminor_t minor, int access, endpoint_t user_endpt);
static int hello_close(devminor_t minor);
static ssize_t hello_read(devminor_t minor, u64_t position, endpoint_t endpt,
    cp_grant_id_t grant, size_t size, int flags, cdev_id_t id);
 
/* SEF functions and variables. */
static void sef_local_startup(void);
static int sef_cb_init(int type, sef_init_info_t *info);
static int sef_cb_lu_state_save(int, int);
static int lu_state_restore(void);
 
/* Entry points to the hello driver. */
static struct chardriver hello_tab =
{
    .cdr_open	= hello_open,
    .cdr_close	= hello_close,
    .cdr_read	= hello_read,
};
 
/** State variable to count the number of times the device has been opened.
 * Note that this is not the regular type of open counter: it never decreases.
 */
static int open_counter;
 
static int hello_open(devminor_t UNUSED(minor), int UNUSED(access),
    endpoint_t UNUSED(user_endpt))
{
    printf("hello_open(). Called %d time(s).\n", ++open_counter);
    return OK;
}
 
static int hello_close(devminor_t UNUSED(minor))
{
    printf("hello_close()\n");
    return OK;
}
 
static ssize_t hello_read(devminor_t UNUSED(minor), u64_t position,
    endpoint_t endpt, cp_grant_id_t grant, size_t size, int UNUSED(flags),
    cdev_id_t UNUSED(id))
{
    u64_t dev_size;
    char *ptr;
    int ret;
    char *buf = HELLO_MESSAGE;
 
    printf("hello_read()\n");
 
    /* This is the total size of our device. */
    dev_size = (u64_t) strlen(buf);
 
    /* Check for EOF, and possibly limit the read size. */
    if (position >= dev_size) return 0;		/* EOF */
    if (position + size > dev_size)
        size = (size_t)(dev_size - position);	/* limit size */
 
    /* Copy the requested part to the caller. */
    ptr = buf + (size_t)position;
    if ((ret = sys_safecopyto(endpt, grant, 0, (vir_bytes) ptr, size)) != OK)
        return ret;
 
    /* Return the number of bytes read. */
    return size;
}
 
static int sef_cb_lu_state_save(int UNUSED(state), int UNUSED(flags)) {
/* Save the state. */
    ds_publish_u32("open_counter", open_counter, DSF_OVERWRITE);
 
    return OK;
}
 
static int lu_state_restore() {
/* Restore the state. */
    u32_t value;
 
    ds_retrieve_u32("open_counter", &value);
    ds_delete_u32("open_counter");
    open_counter = (int) value;
 
    return OK;
}
 
static void sef_local_startup()
{
    /*
     * Register init callbacks. Use the same function for all event types
     */
    sef_setcb_init_fresh(sef_cb_init);
    sef_setcb_init_lu(sef_cb_init);
    sef_setcb_init_restart(sef_cb_init);
 
    /*
     * Register live update callbacks.
     */
    sef_setcb_lu_state_save(sef_cb_lu_state_save);
 
    /* Let SEF perform startup. */
    sef_startup();
}
 
static int sef_cb_init(int type, sef_init_info_t *UNUSED(info))
{
/* Initialize the hello driver. */
    int do_announce_driver = TRUE;
 
    open_counter = 0;
    switch(type) {
        case SEF_INIT_FRESH:
            printf("%s", HELLO_MESSAGE);
        break;
 
        case SEF_INIT_LU:
            /* Restore the state. */
            lu_state_restore();
            do_announce_driver = FALSE;
 
            printf("%sHey, I'm a new version!\n", HELLO_MESSAGE);
        break;
 
        case SEF_INIT_RESTART:
            printf("%sHey, I've just been restarted!\n", HELLO_MESSAGE);
        break;
    }
 
    /* Announce we are up when necessary. */
    if (do_announce_driver) {
        chardriver_announce();
    }
 
    /* Initialization completed successfully. */
    return OK;
}
 
int main(void)
{
    /*
     * Perform initialization.
     */
    sef_local_startup();
 
    /*
     * Run the main loop.
     */
    chardriver_task(&hello_tab);
    return OK;
}

Let's try to understand what the above code does. First, it has several #include lines for the required prototypes and functions used in the program. Then, it declares a struct chardriver. This structure is filled with callback functions which will be invoked by libchardriver at runtime. It has callback functions for performing open, read, write and close operations on the device driver. Most of the action happens in the hello_read function. It used the sys_safecopyto function to copy the HELLO_MESSAGE string from the device driver program, to the user program reading from /dev/hello. The operating system will then on behalf of the device driver take care of properly copying the bytes between the two programs. An Input/Output vector iov is used by the sys_safecopyto function to describe the memory address for storing bytes and number of requested bytes by the user application.

In the main() function there are only two simple calls. sef_local_startup() is called at the very beginning to register SEF callbacks and then complete initialization. In sef_local_startup() the same function sef_cb_init_fresh() is registered as a callback for all the supported initialization types. This means that the initialization code executed when the driver starts will be always the same regardless of the device driver starting fresh, after a live update, or after a restart.

In sef_local_startup() we have also registered callbacks for live update events. A live update event is triggered by RS when a new version of a system service is available. At that point, RS sends a message to the old version of the service asking to prepare for a particular state as required by the update. The service will decide whether to accept the update state requested or reject the update otherwise. In the former case, the service commits itself to prepare for the update and reply back to RS when the target state has been reached. Only at that point, RS installs the update and the new version can be initialized from the state where the old version left off. Fortunately, the live update protocol is hidden in the System Event Framework and the driver developer does not have to be concerned with all the details. To support live update for a system service, the developer should only register callbacks to let SEF know what to do when a live update request comes along. The most important callback types for live update are state_isvalid and prepare. A state_isvalid callback must be registered to tell SEF what update states should be accepted. A prepare callback is, in contrast, called every time a system service is given a chance to prepare to update (after an update request has been accepted). The callback must take the target state as input and return OK only if the service is ready for the requested update.

The hello driver is completely stateless and we can support live update starting from any state and let RS perform the update at the first possible chance. To accomplish this, we register two predefined callback implementations for the state_isvalid and prepare callback types. These are the library functions implemented within the SEF framework sef_cb_lu_state_isvalid_standard and sef_cb_lu_prepare_always_ready, respectively. The callback sef_cb_lu_state_isvalid_standard accepts any standard live update state and the callback sef_cb_lu_prepare_always_ready always reports that the service is ready to be updated. When the device driver is not stateless, the developer has to provide appropriate implementations for these two callback types to support standard states and possibly some other custom states defined for the driver. The currently defined standard states are SEF_LU_STATE_WORK_FREE (the service is not doing any work), SEF_LU_STATE_REQUEST_FREE (the service has no pending request), SEF_LU_STATE_PROTOCOL_FREE (the service is not involved in a protocol).

Finally, the main() function executes chardriver_task() to let libchardriver start processing user requests and invoke our driver callback functions.

Now compile the program again using the same commands as in example 1, and start the driver with the minix-service(8) command. We supply -dev /dev/hello to indicate that the newly started driver is responsible for the major device identified by /dev/hello - namely, major 17, which we picked earlier.

# minix-service up /service/hello -dev /dev/hello
Hello, World!

Bingo! No more restarts from the Reincarnation Server. Now try to see if we can read the Hello World message using a device file.

We can simply use cat(1) to read from the hello device:

# cat /dev/hello
hello_open()
hello_read()
hello_read()
hello_close()
Hello, World!
#

If you get the message above, the hello driver works! :D

Now let's try to restart the driver with the minix-service(8) command to simulate a failure:

# minix-service refresh hello
Hello, World!
Hey, I've just been restarted!
# cat /dev/hello
hello_open()
hello_read()
hello_read()
hello_close()
Hello, World!
#

Finally, let's try it with a live update. Make the following change to the hello.h header file:

hello.h:

#ifndef __HELLO_H
#define __HELLO_H
 
/** The Hello, World! message. */
#define HELLO_MESSAGE "Hello, New World!\n"
 
#endif /* __HELLO_H */

Now recompile it and update the driver with the minix-service(8) command:

# make clean 
# make
# make install
# minix-service update /service/hello -state 1    # request an update state where no work is in progress (i.e. SEF_LU_STATE_WORK_FREE=1)
Hello, New World!
Hey, I'm a new version!
# cat /dev/hello
hello_open()
hello_read()
hello_read()
hello_close()
Hello, New World!
#

As expected, the driver is updated live and the new version immediately takes over.

Hello, New World! B)

Device Drivers in the Real World

Example 3: CMOS System Clock

Now let's try to write a device driver for a real hardware component in your computer, like the RTC clock. This device keeps track of the system time, and has a very simple I/O interface. Remember from the Introduction that writing device drivers is not only about programming, but also about understanding the hardware itself? Then before you continue, read these documents about the RTC:

Let's try to modify the hello driver we wrote in example 2, so we can use /dev/time to read the current system time from the RTC. You can reuse the Makefile from previous examples if you want. First create a header file with the needed definitions:

time.h:

#ifndef __TIME_H
#define __TIME_H
 
/* Major ID of /dev/time. */
#define TIME_MAJOR  17
 
/* Base I/O port of the CMOS. */
#define CMOS_PORT   0x70
 
/* Data field offsets of the RTC in CMOS. */
#define RTC_SECONDS      0
#define RTC_MINUTES      2
#define RTC_HOURS        4
#define RTC_DAY_OF_WEEK  6
#define RTC_DAY_OF_MONTH 7
#define RTC_MONTH        8
#define RTC_YEAR         9
 
/* RTC Registers and Flags. */
#define RTC_STATUS_A     10
#define RTC_STATUS_B     11
#define RTC_UIP          0x80
#define RTC_BCD          0x04
 
#endif /* __TIME_H */

Now place this C program in /usr/src/minix/drivers/examples/time:

time.c:

#include <stdio.h>
#include <stdlib.h>
#include <minix/ds.h>
#include <minix/chardriver.h>
 
#include "time.h"
 
/* Function prototypes for the time driver. */
int time_open(devminor_t minor, int access, endpoint_t user_endpt);
int time_close(devminor_t minor);
int time_read  (devminor_t minor, u64_t position, endpoint_t endpt,
        cp_grant_id_t grant, size_t size, int flags, cdev_id_t id);
 
int cmos_read_byte (int offset);
unsigned bcd_to_bin (unsigned value);
void time_from_cmos (char *buffer, int size);
 
/* SEF functions and variables. */
void sef_local_startup (void);
int sef_cb_init_fresh (int type, sef_init_info_t *info);
 
/* Entry points to the time driver. */
struct chardriver time_tab =
{
        .cdr_open = time_open,
        .cdr_close = time_close,
        .cdr_read = time_read,
};
 
int time_open(devminor_t minor, int access, endpoint_t user_endpt)
{
    printf("time_open()\n");
    return OK;
}
 
int time_close(devminor_t minor)
{
    printf("time_close()\n");
    return OK;
}
 
int cmos_read_byte(int offset)
{
    uint32_t value = 0;
    int r;
 
    if ((r = sys_outb(CMOS_PORT, offset)) != OK)
    {
        panic("sys_outb failed: %d", r);
    }
    if ((r = sys_inb(CMOS_PORT + 1, &value)) != OK)
    {
        panic("sys_inb failed: %d", r);
    }
    return value;
}
 
unsigned bcd_to_bin(unsigned value)
{
    return (value & 0x0f) + ((value >> 4) * 10);
}
 
void time_from_cmos(char *buffer, int size)
{
    int sec, min, hour, day, mon, year;
 
    /*
          * Wait until the Update In Progress (UIP) flag is clear, meaning
          * that the RTC registers are in a stable state.
          */
    while (!(cmos_read_byte(RTC_STATUS_A) & RTC_UIP))
    {
        ;
    }
    /* Read out the time from RTC fields in the CMOS. */
    sec  = cmos_read_byte(RTC_SECONDS);
    min  = cmos_read_byte(RTC_MINUTES);
    hour = cmos_read_byte(RTC_HOURS);
    day  = cmos_read_byte(RTC_DAY_OF_MONTH);
    mon  = cmos_read_byte(RTC_MONTH);
    year = cmos_read_byte(RTC_YEAR);
 
    /* Convert from Binary Coded Decimal (BCD), if needed. */
    if (cmos_read_byte(RTC_STATUS_B) & RTC_BCD)
    {
        sec  = bcd_to_bin(sec);
        min  = bcd_to_bin(min);
        hour = bcd_to_bin(hour);
        day  = bcd_to_bin(day);
        mon  = bcd_to_bin(mon);
        year = bcd_to_bin(year);
    }
    /* Convert to a string. */
    snprintf(buffer, size, "%04d-%02x-%02x %02x:%02x:%02x\n",
             year + 2000, mon, day, hour, min, sec);
}
 
ssize_t time_read(devminor_t minor, u64_t position, endpoint_t endpt,
        cp_grant_id_t grant, size_t size, int flags, cdev_id_t id)
{
    int bytes, ret;
    char buffer[1024];
 
    printf("time_read()\n");
 
    /* Retrieve system time from CMOS. */
    time_from_cmos(buffer, sizeof(buffer));
 
    bytes = MIN(strlen(buffer) - (int) position, size);
 
    if (bytes <= 0)
    {
        return OK;
    }
 
    ret = sys_safecopyto(endpt, grant, 0, (vir_bytes) buffer + position, bytes);
 
    if(ret != OK) return 0;
 
    return bytes;
}
 
void sef_local_startup()
{
    /* Register init callbacks. */
    sef_setcb_init_fresh(sef_cb_init_fresh);
    sef_setcb_init_lu(sef_cb_init_fresh);      /* treat live updates as fresh inits */
    sef_setcb_init_restart(sef_cb_init_fresh); /* treat restarts as fresh inits */
 
    /* Register live update callbacks. */
    sef_setcb_lu_prepare(sef_cb_lu_prepare_always_ready);         /* agree to update immediately when a LU request is received in a supported state */
    sef_setcb_lu_state_isvalid(sef_cb_lu_state_isvalid_standard); /* support live update starting from any standard state */
 
    /* Let SEF perform startup. */
    sef_startup();
}
 
int sef_cb_init_fresh(int type, sef_init_info_t *info)
{
    /* Initialize the time driver. */
    return(OK);
}
 
int main(int argc, char **argv)
{
    /* Perform initialization. */
    sef_local_startup();
 
    /* Run the main loop. */
    chardriver_task(&time_tab);
    return OK;
}

Let's write a Makefile to describe our executable and conf file:

# Makefile for the time driver.
PROG=   time
SRCS=   time.c

FILES=${PROG}.conf
FILESNAME=${PROG}
FILESDIR=/etc/system.conf.d

DPADD+= ${LIBCHARDRIVER} ${LIBSYS}
LDADD+= -lchardriver -lsys

.include <minix.service.mk>

Now we need to give the new time device driver access to the CMOS ports 0x70 and 0x71 using the time.conf file mentioned in the Makefile. Putting it in /etc/system.d/ lets service read it. This is the contents:

service time
{
    io
        0x70:2;
        system
                UMAP            # 14
                DEVIO           # 21
        ;
        ipc
                SYSTEM PM RS LOG TTY DS VM VFS
                pci inet amddev
                ;
    uid 0;
};

To read a byte from the CMOS, a program needs to first write the offset which is requests to read to the address I/O port, 0x70. Then it can read the data byte from I/O port 0x71. The time driver uses this mechanism to read the appropriate fields from the CMOS to retrieve the system time, and then outputs it as a string in the same way as we did in example 2.

Let's give it a try:

# make
    compile  time/time.o
       link  time/time
# make install
    install  /service/time
    install  /etc/system.conf.d/time
# mknod /dev/time c 18 0
# minix-service up /service/time -dev /dev/time
# cat /dev/time 
2014-09-10 15:48:21
# date
Wed Sep 10 15:48:32 GMT 2014

Congratulations, you just wrote your first real device driver on Minix 3! :-) You can verify that the driver correctly reads the time by using the 'date(1)' command.

Example 4: RS232 Serial Port

Feedback

Finished the tutorial? :-) Tell us what you think.