====== 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.0.
===== 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/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. 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/drivers
# mkdir hello
# cd hello
To compile the device driver, we need a [[http://en.wikipedia.org/wiki/Make_(software)|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
Then, create a C program file with the hello world program:
//hello.c//:
#include
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 [[http://en.wikipedia.org/wiki/Microkernel|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 [[.:sef|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|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. We start by making the /dev/hello file:
# mknod /dev/hello c 17 0
This command creates a **c**haracter 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
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 [[http://en.wikipedia.org/wiki/Callback_(computer_science)|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|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 [[.:sef|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|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|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|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
# make
# make install
# service update /usr/sbin/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_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)
===== Device Drivers in the Real World =====
#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
#include
#include
#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);
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** [[http://en.wikipedia.org/wiki/Port-mapped_I/O|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.
==== Example 4: RS232 Serial Port ====
===== Feedback =====
Finished the tutorial?** :-) **Tell us** [[http://groups.google.com/group/minix3/browse_thread/thread/aa17a4c6f891a585|what you think]]. **