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.
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.0.
The following sections contain step-by-step instructions to try out programming simple device drivers.
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.
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/drivers/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.
First create a directory in the Minix source tree to place the source code of our driver. As root, execute:
# cd /usr/src/drivers # 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 DPADD+= ${LIBCHARDRIVER} ${LIBSYS} LDADD+= -lchardriver -lsys MAN= BINDIR?= /usr/sbin .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. 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. Before we do that, we must modify the /etc/system.conf file. It contains a list of all system services (servers and drivers) and their permissions. 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, append the following to /etc/system.conf:
/etc/system.conf:
service hello { system UMAP # 14 IRQCTL # 19 DEVIO # 21 ; ipc SYSTEM PM RS LOG TTY DS VM VFS pci inet amddev ; uid 0; };
Starting, stopping and restarting device drivers must be done using the service(8) command. To start the hello program, enter the following command:
# service up /usr/sbin/hello Hello, World! RS: restarting /usr/sbin/hello, restarts 0
And what a surprise, it displays the Hello, World! message Stop the driver with:
# 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).
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. 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(message *m); static int hello_close(message *m); static struct device * hello_prepare(dev_t device); static int hello_transfer(endpoint_t endpt, int opcode, u64_t position, iovec_t *iov, unsigned int nr_req, endpoint_t user_endpt, unsigned int flags); /* 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); static int lu_state_restore(void); /* Entry points to the hello driver. */ static struct chardriver hello_tab = { hello_open, hello_close, nop_ioctl, hello_prepare, hello_transfer, nop_cleanup, nop_alarm, nop_cancel, nop_select, NULL }; /** Represents the /dev/hello device. */ static struct device hello_device; /** State variable to count the number of times the device has been opened. */ static int open_counter; static int hello_open(message *UNUSED(m)) { printf("hello_open(). Called %d time(s).\n", ++open_counter); return OK; } static int hello_close(message *UNUSED(m)) { printf("hello_close()\n"); return OK; } static struct device * hello_prepare(dev_t UNUSED(dev)) { hello_device.dv_base = make64(0, 0); hello_device.dv_size = make64(strlen(HELLO_MESSAGE), 0); return &hello_device; } static int hello_transfer(endpoint_t endpt, int opcode, u64_t position, iovec_t *iov, unsigned nr_req, endpoint_t UNUSED(user_endpt), unsigned int UNUSED(flags)) { int bytes, ret; printf("hello_transfer()\n"); if (nr_req != 1) { /* This should never trigger for character drivers at the moment. */ printf("HELLO: vectored transfer request, using first element only\n"); } bytes = strlen(HELLO_MESSAGE) - ex64lo(position) < iov->iov_size ? strlen(HELLO_MESSAGE) - ex64lo(position) : iov->iov_size; if (bytes <= 0) { return OK; } switch (opcode) { case DEV_GATHER_S: ret = sys_safecopyto(endpt, (cp_grant_id_t) iov->iov_addr, 0, (vir_bytes) (HELLO_MESSAGE + ex64lo(position)), bytes, D); iov->iov_size -= bytes; break; default: return EINVAL; } return ret; } static int sef_cb_lu_state_save(int UNUSED(state)) { /* 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. */ /* - Agree to update immediately when LU is requested in a valid state. */ sef_setcb_lu_prepare(sef_cb_lu_prepare_always_ready); /* - Support live update starting from any standard state. */ sef_setcb_lu_state_isvalid(sef_cb_lu_state_isvalid_standard); /* - Register a custom routine to save the state. */ 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, CHARDRIVER_SYNC); 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 driver. 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_transfer 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 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.
# service up /usr/sbin/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_transfer() hello_transfer() 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 service(8) command to simulate a failure:
# service refresh hello Hello, World! Hey, I've just been restarted! # cat /dev/hello hello_open() hello_transfer() hello_transfer() 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 service(8) command:
# make clean install # service update hello 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_transfer() hello_transfer() hello_close() Hello, New World! #
As expected, the driver is updated live and the new version immediately takes over.
Hello, New World! B)
Stale page
The rest of this page must be revised to reflect the current state of MINIX3.
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/drivers/time:
time.c:
#include "../drivers.h" #include "../libdriver/driver.h" #include <stdio.h> #include <stdlib.h> #include <minix/ds.h> #include "time.h" /* * Function prototypes for the time driver. */ _PROTOTYPE( PRIVATE char * time_name, (void) ); _PROTOTYPE( PRIVATE int time_open, (struct driver *d, message *m) ); _PROTOTYPE( PRIVATE int time_close, (struct driver *d, message *m) ); _PROTOTYPE( PRIVATE struct device * time_prepare, (int device) ); _PROTOTYPE( PRIVATE void time_from_cmos, (char *buffer, int size) ); _PROTOTYPE( PRIVATE int cmos_read_byte, (int offset) ); _PROTOTYPE( PRIVATE unsigned bcd_to_bin, (unsigned value) ); _PROTOTYPE( PRIVATE int time_transfer, (int procnr, int opcode, u64_t position, iovec_t *iov, unsigned nr_req) ); _PROTOTYPE( PRIVATE void time_geometry, (struct partition *entry) ); /* SEF functions and variables. */ _PROTOTYPE( void sef_local_startup, (void) ); _PROTOTYPE( int sef_cb_init_fresh, (int type, sef_init_info_t *info) ); /* Entry points to the time driver. */ PRIVATE struct driver time_tab = { time_name, time_open, time_close, nop_ioctl, time_prepare, time_transfer, nop_cleanup, time_geometry, nop_signal, nop_alarm, nop_cancel, nop_select, nop_ioctl, do_nop, }; /** Represents the /dev/time device. */ PRIVATE struct device time_device; PRIVATE char * time_name(void) { printf("time_name()\n"); return "time"; } PRIVATE int time_open(d, m) struct driver *d; message *m; { printf("time_open()\n"); return OK; } PRIVATE int time_close(d, m) struct driver *d; message *m; { printf("time_close()\n"); return OK; } PRIVATE struct device * time_prepare(dev) int dev; { time_device.dv_base.lo = 0; time_device.dv_base.hi = 0; time_device.dv_size.lo = 0; time_device.dv_size.hi = 0; return &time_device; } PRIVATE int cmos_read_byte(offset) int offset; { unsigned long value = 0; int r; if ((r = sys_outb(CMOS_PORT, offset)) != OK) { panic("time", "sys_outb failed", r); } if ((r = sys_inb(CMOS_PORT + 1, &value)) != OK) { panic("time", "sys_inb failed", r); } return value; } PRIVATE unsigned bcd_to_bin(value) unsigned value; { return (value & 0x0f) + ((value >> 4) * 10); } PRIVATE void time_from_cmos(buffer,size) 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); } PRIVATE int time_transfer(proc_nr, opcode, position, iov, nr_req) int proc_nr; int opcode; u64_t position; iovec_t *iov; unsigned nr_req; { int bytes, ret; char buffer[1024]; printf("time_transfer()\n"); /* Retrieve system time from CMOS. */ time_from_cmos(buffer, sizeof(buffer)); bytes = strlen(buffer) - position.lo < iov->iov_size ? strlen(buffer) - position.lo : iov->iov_size; if (bytes <= 0) { return OK; } switch (opcode) { case DEV_GATHER_S: ret = sys_safecopyto(proc_nr, iov->iov_addr, 0, (vir_bytes) (buffer + position.lo), bytes, D); iov->iov_size -= bytes; break; default: return EINVAL; } return ret; } PRIVATE void time_geometry(entry) struct partition *entry; { printf("time_geometry()\n"); entry->cylinders = 0; entry->heads = 0; entry->sectors = 0; } PRIVATE 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(); } PRIVATE int sef_cb_init_fresh(int type, sef_init_info_t *info) { /* Initialize the time driver. */ u32_t this_proc; /* Lookup our task number. */ if (ds_retrieve_label_num("time", &this_proc) != OK) { printf("time: ds_retrieve_label_num() failed: %s\n", strerror(errno)); return EXIT_FAILURE; } /* Map major number to our process. */ if (mapdriver(this_proc, TIME_MAJOR, STYLE_DEV, TRUE) != OK) { printf("time: mapdriver() failed: %s\n", strerror(errno)); return EXIT_FAILURE; } /* Initialization completed successfully. */ return(OK); } PUBLIC int main(int argc, char **argv) { /* * Perform initialization. */ sef_local_startup(); /* * Run the main loop. */ driver_task(&time_tab, DRIVER_STD); return OK; }
Now we need to give the new time device driver access to the CMOS ports 0x70 and 0x71. Append this entry to in /etc/system.conf:
service time { io 0x70:2; system UMAP # 14 IRQCTL # 19 DEVIO # 21 #SDEVIO # 22 SETALARM # 24 TIMES # 25 GETINFO # 26 SAFECOPYFROM # 31 SAFECOPYTO # 32 SETGRANT # 34 PROFBUF # 38 SYSCTL ; 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:
# service up /usr/sbin/time -dev /dev/time # cat /dev/time time_open() time_transfer() time_transfer() time_close() 2009-12-02 19:32:46
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.
Finished the tutorial? Tell us what you think.